Extends DoseTime with two orthogonal filters, as the brief foresaw
('cyclic/taper extend from the same model'):
- Cyclic (cycleActiveDays/cycleLengthDays from anchor): N-on/M-off, e.g.
21/28 contraception. occursOn gains a cycle-window check; nextOccurrence
is now a uniform day-by-day scan over occursOn (replacing the interval
fast-path) so weekly/interval/cyclic/windowed all fall out of one
correct path.
- Validity window (startEpochDay/endEpochDay, inclusive): a taper is
several daily rows with descending amount and adjacent windows.
dailyConsumption now takes : cyclic scales by active/length, and
rows outside their window contribute 0 so a finished taper step stops
inflating days-of-supply.
UI: dose-time dialog gains a Syklisk section (på/av days) and is now
scrollable; a 'Lag nedtrapping' generator creates the windowed rows from
start dose / step-down / days-per-step / step-count. ScheduleText renders
'· syklus 21/28' and '· 11. juni–13. juni'.
Migration 2→3 is additive ADD COLUMNs with defaults matching the entity
(@ColumnInfo defaultValue), proven by an instrumented MigrationTestHelper
test. Backup DTO extended with defaults so v2 files still parse. 9 new
engine unit tests; versionCode 3 / 0.3.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Icon: a capsule in a hurry — stadium pill at 45° with motion lines
trailing, «med det samme» as imagery. versionCode 2 / versionName 0.2.0
with a bump-on-release note. Lint: KTX conversions (prefs.edit{},
toUri, createBitmap), mipmap-anydpi-v26 merged into mipmap-anydpi
(minSdk 26 makes the qualifier dead weight), compose-bom 2026.05.01,
Gradle wrapper 9.5.1, and the three deliberate-decision warnings
(BatteryLife/OldTargetApi/NewerVersionAvailable) disabled with
documented reasons. Lint now runs clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Post-incident: an unscoped connectedDebugAndroidTest fanned out to every
adb device including the personal phone, where the debug test APK's
signature mismatch was resolved by UNINSTALLING the release app — wiping
its data. The v1->v2 Room migration was initially blamed but is innocent
(three emulator replays preserved data; dumpsys firstInstallTime ==
lastUpdateTime proved the phone got a fresh install, not an upgrade).
connected* tasks now fail fast unless ANDROID_SERIAL names an emulator.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Amends decision #8: the producer now lives in tools/fest_flatten.py
(stdlib Python, streaming iterparse over the 115 MB M30 XML) instead of
an out-of-repo server job. It pulls fest251.zip from dmp.no, flattens
8934 active human-use brand entries (name/strength/unit/form/ATC +
modal package size from pakningsinfo), and writes the asset checked in
at app/src/main/assets/fest/slim.json — so autocomplete works offline
out of the box. preReleaseBuild depends on refreshFestData: fast-exits
under 30 days, keeps the old file on download failure (offline release
builds never break). FestRepository: downloaded cache wins over the
bundled asset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stock, package size and prescription state move to inventory_item;
medication is now a regimen (itemId FK RESTRICT — deleting a stock item
can never silently take adherence history with it). Supply warnings
aggregate consumption across all regimens sharing an item. New Lager
screen with one-tap '+1 pakning' restock; med editor picks an existing
item or creates one inline via the shared ItemFormState (one form, no
drift). Backup format v2 mirrors the split and still restores v1 files
by performing the same conversion the DB migration does.
MIGRATION_1_2 statements are copied from the exported 2.json and proven
by an instrumented MigrationTestHelper test on the emulator (validates
against the schema AND asserts data survival) before it ever touches
the phone's medical record. Heads-up: connectedAndroidTest must target
the emulator (ANDROID_SERIAL) — the phone's release signature rejects
debug test APKs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keystore at ~/.keystores/meddetsamme-release.jks (4096-bit RSA, 30 yr),
credentials in keystore.properties — both outside version control. A
clone without the file still builds (unsigned release) instead of
failing, so nothing breaks for CI or a fresh checkout.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Milestone 6. kage 0.4.0 over Jagged: Android-first (explicitly supports
API 26, matching minSdk), Kotlin, ships its own primitives through
BouncyCastle instead of relying on JCA ChaCha20-Poly1305 which Android
only provides from API 28. AgeBackupCrypto sits behind the same
BackupCrypto interface as the JCE baseline; age is now first in all()
so new encrypted backups are written as binary age v1 files,
decryptable anywhere with 'age -d' (decision #7). Old MDS1/JCE backups
keep decrypting via format auto-detection. The C2SP/CCTV scrypt vector
subset (19 files) is vendored into test resources: both success
vectors decrypt with matching payload SHA-256, all 17 failure vectors
are rejected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Milestone 5. One BackupManager entry point so export, import and
auto-backup share the same serializer and crypto by construction.
Explicit DTOs decouple the backup contract from the Room schema.
JCE baseline (PBKDF2-HMAC-SHA256 600k → AES-256-GCM) behind the
BackupCrypto interface with format auto-detection on decrypt; age
lands behind the same interface in milestone 6. SigV4 signer verified
against AWS's published documentation example signature. Settings in
SharedPreferences encrypted under a non-exportable AndroidKeyStore key
— replaces the now fully deprecated androidx security-crypto with the
same construction (~70 lines, zero deps); threat-model comments state
plainly that the stored auto-backup passphrase only protects against
bucket compromise. WorkManager daily periodic job, inexact by design.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Milestone 4. AlarmScheduler is the only AlarmManager writer; one stable
PendingIntent requestCode per dose-time means re-arming replaces and can
never stack (the no-drift invariant is OS-enforced). DoseAlarmReceiver
handles both occurrence and escalation alarms idempotently and always
re-arms the next occurrence before anything else so the chain can't
break. nagCount lives in dose_log so the ~6-nag cap survives reboot.
SCHEDULE_EXACT_ALARM added maxSdk 32 (USE_EXACT_ALARM is 33+ only).
Emulator-verified (API 35): exact alarm armed (window=0,
policy_permission), fired on time, HIGH notification with Tatt/Utsett,
escalation armed +10 min, next day re-armed, Tatt → TAKEN + inventory
10→9 + escalation cancelled, reboot → BootReceiver re-armed everything.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Milestone 2. Medication / DoseTime / DoseLog per the brief's model.
DoseLog.doseTimeId is SET_NULL (schedule edits must not erase adherence
history) while medId CASCADEs; DoseLog.amount is snapshotted at logging
time so later dose changes don't rewrite the record. Inventory decrement
is a single SQL UPDATE clamped at 0 to stay race-free under escalating
notifications. Enums stored as TEXT; schema exported to app/schemas as
the migration baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Milestone 1 of the build brief. Version catalog pins verified against
Maven Central / Google Maven on 2026-06-10; Kotlin held at 2.3.10 to
match AGP 9.2's bundled KGP (built-in Kotlin) because KSP has no
Kotlin 2.4 release yet. compileSdk 37 (forced by core-ktx 1.19),
targetSdk 35 per brief. allowBackup=false + full dataExtractionRules
opt-out; own encrypted backup comes in milestone 5.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>