Aptabase hashes its user_id from IP + user agent with a salt that rotates
daily, so events can never be linked across days and no retention or MAU
figure can come out of the export. In 19 days of data not one of 82 user_ids
appears on two dates, which is the artefact, not the behaviour.
Instead of an identity, every event now carries this install's own counters,
kept in UserDefaults/DataStore: tenure_days, launch_no, active_days and
dormant_days. Only derived day counts leave the device, so the privacy
position is unchanged.
They make the curve countable in the export: launch_no == 1 marks exactly one
launch per install, dormant_days >= 1 exactly one launch per calendar day, so
D_k is the share of installs seen again with tenure_days == k. Event date
minus tenure_days is the install date, which gives full cohort tables.
Installs predating the counters have no install date and report
tenure_days == -1 forever, so they can be excluded instead of inflating the
new-install cohort.
The Kotlin counter arithmetic sits in a pure advance() so it can be tested
without a Context; this adds the app module's first JVM test source set.
Play blocks updates from August 31, 2026 unless the app targets API 36.
Bump compileSdk/targetSdk to 36 in :app and :baselineprofile, and AGP to
8.10.1 -- 8.7.3 cannot build against API 36. Gradle stays at 8.11.1, which
is what AGP 8.10 requires.
Nothing in the app relies on the behaviours Android 16 changed: edge-to-edge
is already opted in via enableEdgeToEdge(), the manifest pins no orientation
or resizability, and the two bundled native libraries (graphics-path,
datastore_shared_counter) are already 16 KB page aligned.
Align the version with iOS (1.8.1, code 88) so the API 36 build can ship.
The old gate needed two exports, three days of install age, one prompt per
version and 120 days between prompts. Aptabase shows only 9% of users ever
export and 5% export twice, so `Review Prompt Requested` never fired once in
19 days and the store has a single rating.
Replace the export counter with three milestones -- a fully configured system,
an opened bill of materials, a completed export -- each counted at most once
per install, and ask once two different ones are reached. Recording and asking
are now separate: milestones are booked mid-task, where StoreKit and Play drop
the request, so the ask happens from calm screens only (system overview,
export preview dismissal, share sheet dismissal).
Two fixes that made the old prompt lose its slot for good: the throttle keys
were written before checking for a foreground scene, and the export trigger
fired while the Quick Look sheet was still animating away. Both platforms now
mark the throttle only once the store API really has a review flow to show.
Add a Settings entry that links straight to the store review page. It is never
throttled, and 45% of users open Settings versus 9% who export. New analytics
event `Review Milestone Reached` makes the funnel measurable.
Existing installs keep their legacy export credit and still need a second
milestone before being asked.
A .strings value may span several physical lines, so deleting such an
entry line by line leaves orphaned lines that the parser folds into the
preceding value. Every key behind that point then disappears at runtime
while plutil -lint still reports OK. Tab titles are looked up without a
defaultValue:, so the tab bar rendered raw keys like "tab.overview"
while the rest of the UI fell back to its English defaults.
LocalizationIntegrityTests parses each table out of the app bundle and
fails on multi-line values, on missing keys that have no fallback, and
on leftover cable.pro.* entries. LocalizedTabBarUITests launches the app
in German and asserts the tab bar is translated.
recommendedCrossSection now satisfies two criteria and takes whichever
demands more copper: the voltage-drop budget, and an ampacity at least
as high as the fuse protecting the circuit (ABYC E-11: the OCP rating
must not exceed the conductor's ampacity). Ampacity comes from ABYC
E-11 Table 6A for AWG and ISO 13297 Table A1 for mm², both 105 °C.
Before this, a 40 A load over 0.5 m was sized 1.5 mm² / AWG 16 next to
a 50 A fuse: a cable the fuse can never protect. Long runs stay
voltage-drop driven and unchanged.
The drop budget is now stored per system (3 / 5 / 10 %) and editable in
the system editor, with a global default in Settings that the
standalone calculator uses and new systems inherit.
Cross-sections are derived, never entered by the user, so LoadCableSync
re-derives them when a system opens and when its budget changes.
Library loads are computed instead of being stored as a 1.0 mm²
placeholder. LoadConfigurationStatus therefore reports only what the
user can actually fix: missing length or current.
Also removes the dead cable.pro.* strings and folds three copies of the
mm²-to-AWG conversion into ElectricalCalculations.nearestAWG.
Title, short description, localised full descriptions and the 1.8.0
release notes for en-US, de-DE, es-ES, fr-FR and nl-NL, ready for
`fastlane supply`. The service account key stays out of the repo.
Cold starts ran unoptimised: no profileinstaller dependency and no
profile in the bundle, so ART interpreted Compose on first launch
(class verification measured at 128 bytecodes/s in an emulator ANR).
Add the androidx.baselineprofile producer module with an AOSP managed
device, a generator covering startup plus the four detail tabs, and
commit the generated baseline and startup profiles.
android/gradle/wrapper only held the properties file, so the project
could not be built from the command line at all. Generate the wrapper
for the pinned 8.11.1 distribution and ignore the .kotlin scratch dir.
QLPreviewController only renders its share, print and open-in actions
when it is inside a navigation controller, so diagram and report
previews were dead ends. Wrap it in one, add an explicit Done button
and assert both in a UI test.
The Overview share menu offered a wiring diagram and a full report even
for an empty system. Gate it on loads, batteries or chargers being
present (iOS + Android) and add UI tests for both states.
ComponentLibraryItem gained componentCategory, which left the test
target uncompilable. Add the argument to all call sites and a
regression test for displayVoltage skipping a zero voltageIn.
.quickLookPreview() is a macOS-only SwiftUI modifier, so the app target
did not compile for iOS at all. Present QLPreviewController through a
UIViewControllerRepresentable in a sheet instead; clearing previewURL
still triggers cleanup and the review prompt.
PocketBase stores battery nominal voltage in voltage_out while
voltage_in is 0.0 (not null). The nil-coalescing operator only
skips null, so 0.0 was returned instead of the real voltage.
Skip zero values when resolving displayVoltage on both platforms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
iOS: route exports through QLPreviewController so users can inspect
before sharing. Android: add a zoomable full-screen diagram preview
with a share button; PDF still goes straight to the share sheet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
iOS: moved registerSuccessfulExport() into onDismiss so it fires
after the share sheet closes, not while it is still open.
Android: hold launchReview() until the activity is RESUMED so the
dialog cannot overlap the share chooser.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Overview PDF: usable energy summary, cable length, power loss,
usable energy rows (matching iOS tables)
- Analytics: align event names and properties with iOS
- Localize default component names and editor dialog labels
- Unit system default via device measurement system (API 28+)
- Preserve BOM/affiliate fields when editing batteries/chargers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Request an App Store / Play Store review after a successful export
(Overview PDF, BOM PDF, or wiring diagram). A shared gate keeps prompts
rare: >=2 successful exports, >=3 days since install, >=120 days since the
last prompt, and at most once per app version. A one-time migration
backdates existing users so the prompt can fire on their first export
after updating. Logs a "Review Prompt Requested" analytics event.
iOS uses StoreKit's AppStore.requestReview(in:) with UserDefaults state;
Android uses the Play In-App Review API with DataStore state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cross-platform refinements to appearance/battery/charger editors, tabs
and navigation, plus persistence, screenshot previews and CLAUDE.md docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fetch the wiring diagram from the VoltPlan API and share it as a
standalone PNG from the Overview share menu on both platforms, with a
localized error when the diagram cannot be generated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tweaks to the PocketBase-backed component library: item parsing,
repository/view-model fetching, and the library screen UI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bundle the rotating onboarding illustrations (light/dark) and wire them
into the Android onboarding info component, matching the iOS carousel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add density-specific launcher PNGs with a new foreground, a monochrome
layer for themed icons, and a light background; drop the old vector
foreground.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Read upload-keystore credentials from a gitignored keystore.properties
(falling back to unsigned release when absent), bundle full native debug
symbols for Play crash symbolication, and ignore keystore secrets.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace preview-based screenshot rendering with simulator XCUITests
driven by shooter.sh (per-language locale + status bar overrides,
xcparse extraction). Add batteries-list accessibility identifier and
update CLAUDE.md screenshot docs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace client-side Amazon affiliate resolution with server-side
redirects via voltplan.app/{componentId} and voltplan.app/search?q=.
Remove AmazonAffiliate, affiliate link fetching, and per-model
affiliate fields in favor of a single componentID.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full feature-parity port of the iOS app under ./android (package
app.voltplan.cable): Systems, bottom-nav system detail (Overview,
Components, Batteries, Chargers), calculator/loads, battery & charger
editors, overview with runtime/charge goals, Bill of Materials with PDF
export, VoltPlan PocketBase component library, and Aptabase analytics.
Room persistence, 5-language localization. Verified to build (assembleDebug).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Fix AWG 0/00/000/0000 bug (all resolved to 0 in Swift) using negative
int convention (-1 through -4) with formatAWG() for 1/0–4/0 display
- Add 7.5A fuse size and change fuse type from Int to Double
- Add alternator power source type with distinct bolt.car.fill icon
- Migrate all NSLocalizedString calls to String(localized:defaultValue:)
- Update translations for runtime subtitle (ES/FR/NL: current→maximum),
usable capacity footer text, and NL override wording
- Store length always in meters, convert at display time in CalculatorView
- Add preview-friendly inits for ComponentLibraryView and LoadsView
- Expand test coverage for calculations, fuses, AWG, and edge cases
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add first launch event, share tracking for overview PDF and diagram
exports. Add SystemOverviewPDFExporter for A4 PDF generation. Update
charger model with new configuration fields and localization for all
5 languages. Refresh app icon assets and CLAUDE.md build instructions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Show BOM button for unsaved loads (no longer requires save first)
- Set US fallback affiliate tag for unknown countries
- Localize Amazon search queries in all 5 languages (EN/DE/ES/FR/NL)
- Add affiliate URL/country fields to SavedBattery model
- Auto-detect unit system (imperial for US locale, metric otherwise)
- Set charger input voltage based on locale (120V US, 230V EU)
- Remove StoreKitManager and CableProPaywallView
- Add CLAUDE.md project instructions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add dutyCyclePercent and defaultUtilizationFactorPercent to ComponentLibraryItem
with normalization logic and backend field fetching
- Change default dailyUsageHours from 1h to 24h
- Replace goal editor stepper with day/hour/minute wheel pickers
- Update app icon colors and remove duplicate icon assets
- Move SavedBattery.swift into Batteries/ directory, remove Pods group
- Add iPad-only flag and start frame support to screenshot framing scripts
- Rework localized App Store screenshot titles across all languages
- Add runtime goals and BOM completed items to sample data
- Bump version to 1.5.1 (build 41)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>