Six recurring, non-obvious learnings: Gradle config-cache corruption fix, Room NOT NULL-column migrations needing @ColumnInfo(defaultValue), the scrypt wf=15 rationale (do not bump to 18), Compose dialogs not being adb/uiautomator-tappable (verify via instrumented tests), backticks breaking git commit -m, and wireless adb port rotation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
115 lines
7.8 KiB
Markdown
115 lines
7.8 KiB
Markdown
# «Med det samme» — Claude Code project memory
|
|
|
|
Local-first Android medication reminder. My personal MyTherapy replacement: no ads,
|
|
no account, no analytics, no cloud service of yours. Single user, single device.
|
|
Reminders are the entire point — reliability beats polish wherever they trade off.
|
|
The name says the brief: nag me to take the dose *now*.
|
|
|
|
- Package / applicationId: `no.naiv.meddetsamme`
|
|
- Display name: `Med det samme` (in `strings.xml`, referenced from the manifest)
|
|
- minSdk 26, targetSdk 35, JVM 17, Kotlin + Compose (Material3)
|
|
- Stack: Room (+KSP), WorkManager, OkHttp, kotlinx-serialization, kage (age crypto).
|
|
Gradle Kotlin DSL + version catalog. No AWS SDK, no DI framework.
|
|
|
|
<!-- Personal prefs (24h, concise, explain-why, Linux-assumed) may already be in my
|
|
global ~/.claude/CLAUDE.md. If so, trim the Working agreement here to avoid drift. -->
|
|
|
|
## Working agreement
|
|
- Senior dev, Linux host. Be concise; explain *why* for non-obvious choices in a line
|
|
or two (XAI, not essays). 24h clocks everywhere.
|
|
- Never claim it builds without running the build. Verify; don't assert.
|
|
- Verify current stable library versions yourself before bumping — don't trust memory.
|
|
- Smallest change that works. No speculative abstraction.
|
|
- Ask before anything irreversible (schema migrations, data deletion).
|
|
- This is a medication app: wrong schedule/dose math is a real harm. Keep schedule
|
|
logic pure and unit-tested before anything depends on it.
|
|
|
|
## Decisions — don't relitigate (flag and wait if you think one's wrong)
|
|
1. Native, not PWA — PWA notification reliability is unacceptable for meds.
|
|
2. Exact alarms (`USE_EXACT_ALARM`), always gated on `canScheduleExactAlarms()` with an
|
|
inexact fallback; `setExactAndAllowWhileIdle` so Doze doesn't eat doses.
|
|
3. Re-arm every alarm on `BOOT_COMPLETED` + `MY_PACKAGE_REPLACED` (state is wiped then).
|
|
4. Surface a one-time battery-optimisation exemption prompt — biggest cause of dropped
|
|
reminders on aggressive OEMs.
|
|
5. Google Auto Backup carries ONLY the app's own age-encrypted blob (`files/gbackup/`,
|
|
include-rules in dataExtractionRules/fullBackupContent exclude everything else).
|
|
Blob exists only while a passphrase is set; restore on a new device requires typing
|
|
it (Keystore never migrates). Amended 2026-06 from a full opt-out, on user request.
|
|
6. Own backup: versioned JSON → encrypted → S3-compatible PUT to self-hosted **Garage**,
|
|
path-style, hand-rolled SigV4. One serializer for export, import, and auto-backup.
|
|
7. Crypto target is **age** (passphrase/scrypt) so backups are `age -d`-decryptable and
|
|
never lock me into this app. JCE AES-GCM is the baseline behind an interface; verify
|
|
age against the age test vectors before trusting it.
|
|
8. **FEST, not Felleskatalogen**, and **not** as a live API: its open Rekvirent extract
|
|
is a SOAP/WCF M30 XML dump. `tools/fest_flatten.py` (in-repo, amended 2026-06) flattens
|
|
it into a slim JSON bundled as an APK asset; release builds auto-refresh it when >30
|
|
days old. Autocomplete is offline; a downloaded file via Settings can override the bundle.
|
|
The ban covers Felleskatalogen as a *data source* — browser links to felleskatalogen.no
|
|
for human reading are fine (added 2026-06).
|
|
9. Refill is **derived** from inventory + consumption. Prescription renewal
|
|
(`rxExpiryEpochDay`, `refillsRemaining`) is tracked **separately** from stock.
|
|
|
|
## Toolchain (verified 2026-06, re-verify before bumping)
|
|
- AGP 9 has built-in Kotlin: applying `org.jetbrains.kotlin.android` is an ERROR. The
|
|
compose/serialization plugin versions must match AGP's *bundled* KGP (2.3.10 for
|
|
AGP 9.2), not the newest Kotlin.
|
|
- Kotlin stays 2.3.x until KSP supports 2.4 (KSP versions standalone since 2.3.0: plain "2.3.9").
|
|
- compileSdk 37 (AndroidX hard-requires), targetSdk 35 — separate knobs, keep both.
|
|
- Room 3 (`androidx.room3`) is alpha — stay on Room 2.x. `room-ktx` is merged into `room-runtime`.
|
|
- androidx security-crypto is fully deprecated — SettingsStore is our own Keystore wrapper instead.
|
|
- No appcompat: `?attr/colorControlNormal` doesn't exist; Compose tints icons via LocalContentColor.
|
|
- Gradle config-cache can corrupt → `BuildOperationInvocationException (no error message)`;
|
|
fix: `rm -rf .gradle/configuration-cache` (it's not a code error).
|
|
- A migration that ADDs a NOT NULL column needs `@ColumnInfo(defaultValue = "0")` on the entity,
|
|
else Room's migration validation fails (exported schema has no default, the ALTER does).
|
|
|
|
## Load-bearing — change with care
|
|
- The reminder subsystem is the reliability core. After any change, re-verify: alarms
|
|
re-arm on boot/update, escalation cancels on Taken, the next occurrence is always
|
|
armed, Taken decrements inventory.
|
|
- Escalation contract: due dose → PENDING log + notify, re-nag every 10 min (cap ~6 /
|
|
~1 h) until Taken or Snooze; Snooze pushes the next nag 15 min and stays PENDING.
|
|
- The S3 SigV4 signer: if a PUT fails it's almost always clock skew or a non-path-style
|
|
endpoint, not the maths. Don't "fix" the signer first.
|
|
- The schedule engine is pure — add tests here before touching the math.
|
|
- DoseActions is the ONLY implementation of Taken/Snooze/Skip/Undo, shared by the
|
|
notification receiver and UI. Never add a second path — they'd disagree about inventory.
|
|
- Backup DTO field names AND enum constant names (MedForm/DoseStatus) are part of the
|
|
backup-file contract; renaming breaks restore of old backups. Bump BackupFile.VERSION instead.
|
|
- kage is trusted only because CctvScryptVectorTest passes — keep the vendored CCTV
|
|
vectors green when bumping kage.
|
|
- Doctor PDF formatting is locale-pinned to nb; the default locale prints English month names.
|
|
- Backup scrypt work factor is 15, not age's default 18 — 18 needs ~256 MiB and OOM-crashes
|
|
on-device; `largeHeap` only covers DECRYPTING foreign wf-18 files. Don't bump it back.
|
|
|
|
## Out of scope — don't add without asking
|
|
Caregiver/"Team" alerts, multi-profile, Health Connect, streaks/gamification,
|
|
injection-site tracking, symptom/mood diary. (Modelling "trackable" generically is fine
|
|
if cheap; build no diary UI.)
|
|
|
|
## Commands
|
|
All gradlew calls need `JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64` — system java is a JDK 25 EA.
|
|
- Build debug: `./gradlew assembleDebug`
|
|
- Unit tests: `./gradlew test`
|
|
- Lint: `./gradlew lint`
|
|
- Release: `./gradlew assembleRelease` — signs via gitignored `keystore.properties`
|
|
(keystore in `~/.keystores/`; file absent → unsigned build, don't "fix" that)
|
|
- adb/emulator are not on PATH: `~/android-sdk/platform-tools/adb`, `~/android-sdk/emulator/emulator`
|
|
- Emulator verify: create a throwaway AVD from `system-images;android-35;google_apis;x86_64`,
|
|
delete after. NEVER use the personal `tilfluktsrom` AVD.
|
|
- Inspect device DB: `adb shell run-as no.naiv.meddetsamme sqlite3 databases/med-det-samme.db "'SQL'"`
|
|
- Verify alarms: `adb shell dumpsys alarm | grep meddetsamme` — `window=0` means exact
|
|
- The phone (Pixel 7 Pro) has a RELEASE-signed install: `adb install` of debug builds is
|
|
rejected on signature; use release builds for the phone, emulator for debug.
|
|
- **INCIDENT RULE (2026-06-10, data was lost): never run a device-touching command
|
|
unscoped.** Every `adb` call gets `-s <serial>`; connected tests get
|
|
`ANDROID_SERIAL=emulator-<n>`. The build fails connected* tasks without an emulator
|
|
serial — that guard stays. The phone is never a test target.
|
|
- Can't drive Compose dialogs via adb/uiautomator (esp. with the soft keyboard up — it
|
|
reports phantom button nodes and taps don't land). Verify dialog flows with instrumented
|
|
tests (see the backup round-trip / migration tests), not screen tapping.
|
|
- `git commit -m`: no backticks — bash command-substitutes `` `word` `` inside double quotes
|
|
and eats it. Use single-quoted or plain messages.
|
|
- Wireless adb port rotates between sessions; rediscover with `adb devices` / `adb connect <ip:port>`.
|
|
|
|
See @README.md for architecture detail.
|