Meds due at the same wall-clock minute now share a single notification
instead of one card each — they resolve to an identical scheduledAtMillis,
which becomes the occurrence (and notification) key. Per the feature
request: "Tatt alle" / "Utsett alle" handle the whole morning batch in
one tap; a single med just renders as a normal card with "Tatt".
- Notification id is per-occurrence (scheduledAtMillis), not per-log, so
co-timed doses collapse onto one card and re-nags replace it.
- DoseActionReceiver buttons carry the occurrence and call group ops
(takeAll/snoozeAll/muteAll); DoseActions keeps a single per-dose state
mutation (takeState/snoozeState/muteState) reused by both the group ops
and the app's per-dose actions, so the two surfaces can't diverge.
- The card is rebuilt from getActiveAtOccurrence: still-unresolved doses
with nagCount <= cap. "Ikke forstyrr" now sets nagCount above the cap so
a muted dose drops off the card, while a naturally-capped dose (== cap)
stays visible and actionable.
- refreshOccurrence re-renders silently after a single in-app action and
never creates a card for a not-yet-due occurrence.
Adds DoseActionsGroupTest (instrumented) covering takeAll, muteAll, and
the muted-vs-capped distinction at the data layer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two ways to stop the every-10-min nag, per feature request:
- A global "Gjenta påminnelser" switch in Settings. Off → a due dose
alerts once and goes quiet; only the automatic re-nag chain is gated,
so an explicit "Utsett 15 min" still gives one more reminder. The gate
sits at the three automatic arm points (fireOccurrence, escalate
re-arm, armAll resume-after-reboot), never on Snooze.
- An "Ikke forstyrr" action that appears from the second reminder on
(nagCount >= 1). It stops nagging this one dose but leaves it PENDING
so it can still be taken later — distinct from Skip. Implemented by
jumping to the existing terminal state (nagCount = NAG_CAP) rather than
a new column, so it needs no migration and survives reboot for free
(armAll re-arms only nagCount < cap).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a recurring month-day season window to DoseTime (seasonStartMmdd/
seasonEndMmdd): a med rests outside its window and re-arms automatically
every year. The pure ScheduleEngine gains a season filter plus one-year
overrides -- startEpochDay/endEpochDay act as early-start/extend brackets
for seasonal rows while staying hard bounds for tapers.
- Daily SupplyCheck nudges before a season opens (Start naa) and before it
closes (Forleng), handled by the new SeasonActionReceiver.
- Med list lists resting seasonal meds in a "Sesong (hviler)" section.
- Dose-time editor gains a season block and a "scroll for more" hint so
cyclic/season below the time picker are discoverable.
- Room migration v3->v4 (additive), backup DTO extended, engine/text tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each history row now starts with a green check (taken) or red cross (not
taken), so the day scans at a glance. The split mirrors adherencePercent —
only TAKEN counts as adhered — so the icons agree with the percentages on top.
Also fixes the nonsense label: a past PENDING dose showed "Venter" as if it
were still due. A history-local historyLabel maps PENDING -> "Ikke tatt";
the shared DoseStatus.label is untouched, so Today still shows "Venter".
The fixed takenGreen/missedRed are deliberately not derived from the palette,
or dynamic Material You colours could tint a "taken" tick non-green. Colour is
paired with shape (check vs cross) and text, so meaning never rests on colour
alone (WCAG 1.4.1). The trailing tonal DoseStatusBadge gave way to a muted
text label, since the leading icon now carries the colour signal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Release under MIT. Note that the bundled FEST extract stays under DMPs
own open-data terms, not MIT, since it is third-party data shipped in
the APK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When two or more doses are due now (overdue and unresolved), show one
button that takes them all in a single tap — the common case of a
morning handful. It loops over DoseActions.ensureLog + take, the same
path each cards "Ta nå" uses, so inventory decrement and escalation
cancel stay consistent; take() is idempotent so a stale row cannot
double-decrement. Scoped to due-now only, so a later dose cannot be
taken early by accident.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
Documents how each backup path has actually been exercised — Google
Auto Backup verified on emulator (status card, blob round-trip via local
transport, restore card by screenshot; decrypt+import by instrumented
test since ADB can't tap the Compose dialog reliably), file import/
export verified earlier, S3 still pending a live bucket. Captures the
scrypt wf=15 rationale and the safe real-hardware test protocol.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Split the overloaded isBackupConfigured into isS3Configured (gates the
S3 upload path — unchanged behaviour) and isAnyBackupConfigured (S3 OR
a passphrase, which drives the always-on Google blob). The settings
status card and the Today nudge now use the latter, so a passphrase-only
setup reads as protected instead of 'ikke satt opp'. Status line
distinguishes the three states: Google-only, Google+S3, and nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user's report: with only a backup passphrase set (no S3), 'Test
backup nå' errored 'ikke konfigurert' even though Google Auto Backup
works on the passphrase alone. New runConfiguredBackups() writes the
Google blob whenever a passphrase exists and uploads to S3 only when
those credentials are present, throwing only when NOTHING is set up.
The toast now names what actually ran ('Kryptert Google-kopi
oppdatert' and/or the bucket key). Two instrumented tests pin both
ends: passphrase-only succeeds, nothing-configured throws.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per request: re-enable allowBackup but scope it to ONE file via
include-rules — files/gbackup/latest.json.age, an age-encrypted export
written only while a backup passphrase is set (never plaintext to
Google; DB/prefs stay excluded). On a new device the OS restores the
blob, the Today screen detects an empty DB + present blob and offers
'Gjenopprett' — which demands the passphrase typed, since the Keystore
key never migrates. Clearing the passphrase deletes the blob.
scrypt work factor dropped 18→15: age's desktop default needs a 256 MiB
working set and OOM-crashed on-device (found the hard way); 15 = 32 MiB,
still real brute-force cost, and largeHeap covers decrypting foreign
files made at 18. Verified by instrumented round-trip tests (write →
empty DB → restore; wrong passphrase keeps DB empty; cleared passphrase
removes blob) — UI automation couldn't drive the multi-field dialog
reliably, so the proof lives in the test instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Seven fields and three buttons drowned everything else in Settings.
S3/Garage config, test, restore-from-bucket and file export/import now
live on a dedicated Sikkerhetskopi page; main settings shows a status
card (red 'ikke satt opp' until configured) plus Varsler, FEST and
Legeoversikt. The Today-screen backup nudge deep-links straight to the
new page. PassphraseDialog moved to shared Components.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'Ta nå' + 'Angre' on a not-yet-due dose leaves a PENDING log with a
future scheduledAt; armAll() (every app start/update) then resumed
'escalation' for it as if it were overdue — nagging hours early. Three
layers: armAll only resumes escalation for past-due logs, the
escalation receiver drops anything scheduled in the future (defense
in depth), and undo resets nagCount + cancels any escalation so stale
counts can't eat the real quota when the dose actually falls due.
Emulator-verified with both cases side by side: past-due PENDING nags
within the minute, future PENDING stays silent.
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>
Settings gains 'Gjenopprett siste backup fra bøtta': lists the bucket
(classic query-less ListObjects — an empty canonical query sidesteps a
whole class of SigV4 encoding mismatches; keys filtered client-side),
picks the lexicographically newest timestamped key, downloads, then
runs the existing import path (format auto-detect, passphrase prompt,
replace-all, alarm re-arm). Closes the circle: after a device loss,
recovery is in-app instead of laptop + age -d. parseListKeys is pure
and unit-tested; first ListObjects page only (1000 keys), which the
bucket's lifecycle expiration keeps us far below.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The brief promised 'dose logging → adherence history'; the data was
always there, now it has a screen. Read-only by design — corrections
happen on the Today screen while fresh, history is the record. Days
grouped newest-first, discontinued meds still named (getAllWithItems
includes inactive), and today's not-yet-due doses are excluded from
the percentages so they don't drag the score down. DoseStatusBadge
moved to shared Components.kt, reused by both screens.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A typo in a write-only passphrase field encrypts every backup with a
phrase the user cannot reproduce — unrecoverable by design. Both the
settings field and the manual-export dialog now demand a matching
repeat before accepting. Import is unchanged (wrong passphrase there
just fails loudly).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Caught by the first-ever lint run. Split into values/ (Material.Light)
and values-night/ (Material) instead — same pre-Compose window flash
behavior on every supported API level. Remaining lint warnings are
deliberate: BatteryLife is decision #4 (sideloaded, Play policy n/a),
OldTargetApi keeps targetSdk 35 per the brief, and the Kotlin 2.4
'newer version' nags stay blocked by KSP compatibility.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New SummaryPreviewScreen renders the generated PDF page by page with
the platform PdfRenderer (2x scale, no new dependencies) and puts share
in the top bar — see before you send. The settings button now opens the
preview instead of jumping straight to the share sheet; the share path
reuses the already-rendered file via DoctorSummaryPdf.shareIntentFor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Manrope (variable wght, instanced at 600/700/800) for display/headline/
title roles; Inter static Regular/Medium/SemiBold for body/label. M3
baseline metrics retained — only family and weight change, so component
layouts and accessibility text scaling are untouched. Both fonts are
OFL (THIRD_PARTY_LICENSES.md added) with full æøå/µ coverage, verified
by emulator screenshot. ScheduleText.daysCount fixes 'lager for 1
dager' spotted in that screenshot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Seven hand-drawn 24dp vectors (tablet/capsule/liquid/injection/drops/
spray/other) so what-you-take reads at a glance. Icons are decorative;
the rows' merged contentDescription now includes the form label so
TalkBack gains the same information the icon conveys visually.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MedDetSammeTheme: dynamic color on Android 12+ (follows the wallpaper),
green brand palette matching the launcher icon as fallback, dark mode
follows the system (the app was light-only until now). Today screen:
date subtitle, tonal time chips, status badges (Tatt/Venter/Utsatt/
Hoppet over) using scheme roles so contrast holds in both modes and
under any dynamic palette, overdue doses get error-container cards,
resolved ones fade back, FilledTonalButton for the primary action.
Apotek-ærend banner in secondaryContainer; centred empty state; letter
avatars and low-stock colour cues in the med/lager lists. Platform
window theme switched to DayNight so the pre-Compose flash matches.
Verified via emulator screenshots in light and dark mode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The daily digest splits in two: low stock stays an auto-cancel
notification re-posted each morning while below the per-item limit;
prescription renewal becomes a separate ONGOING notification starting
7 days before rx expiry — survives 'Clear all', swipe-dismissable on
Android 14+, returns each morning until the rx date is updated, and
cancels itself once renewed (post-or-cancel in SupplyCheckReceiver).
Check time is now configurable (Innstillinger → Varsler, M3 TimePicker,
default 10:00); same PendingIntent so re-arming replaces. 'Slå opp i
Felleskatalogen' buttons on the item editor and the med editor's
summary card open the browser — decision #8 clarified: the ban covers
Felleskatalogen as a data source, not human-facing links.
Emulator-verified: both notifications post with correct flags
(ONGOING_EVENT vs AUTO_CANCEL), auto-clear after renewal/restock,
alarm follows the configured time (10:00 → 07:30), FK button opens
Chrome.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Post-incident follow-up: the gap between 'app supports encrypted
backups' and 'backups actually configured' cost real data on
2026-06-10. A quiet card on the Today screen now points at settings
until S3 is configured; 'Ikke nå' dismisses it permanently.
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>
Written for the repo's future reader (mostly me): why each reliability
mechanism exists, how to decrypt a backup off-device with age -d, the
FEST pipeline, and the keystore.properties signing convention. CLAUDE.md
now points at it for architecture detail, per its own placeholder.
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>
The 7 day chips overflowed the dose-time dialog (no scroll, week cut
off) — FlowRow wraps them, with full day names for TalkBack. Enhet
auto-fills from Form while it still holds the previous form's default
(tablett/kapsel/ml/dråper/doser), killing the apparent Enhet/Form
overlap; manual overrides like IU stick. Styrke stays Styrke — it
holds '500 mg', which is neither unit nor form.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Captures what cost time this session: AGP 9 built-in Kotlin (KGP-bundled
version pinning), KSP's standalone versioning blocking Kotlin 2.4,
JAVA_HOME requirement, release signing flow, emulator verification
recipes, and the new code contracts (DoseActions single-path, backup
DTO/enum names as file-format contract, CCTV gate on kage bumps).
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 9. Hand-rolled back-stack navigation (4 screens don't justify
navigation-compose). DoseActions is now the single implementation of
Taken/Snooze/Skip/Undo shared by notification receiver and UI — two
code paths for 'taken' would eventually disagree about inventory.
Screens read DAO Flows directly; every schedule mutation re-arms
alarms via armAll() (idempotent, OS-deduplicated). One-time battery
prompt + POST_NOTIFICATIONS request on the today screen. Settings:
S3/Garage config, typed-passphrase export/import (replace-all import
re-arms alarms), FEST refresh, PDF share. TalkBack: merged semantics
with full descriptions on dose cards, content descriptions on icon
buttons and day chips. Doctor PDF locale pinned to Bokmål — verified
on emulator after catching English month names on an en-US device.
Emulator-verified end-to-end: battery prompt → system dialog, med
created via editor (validation catches bad input), dose time added →
exact alarm armed immediately, Ta nå → TAKEN + inventory 20→19 +
reactive UI update, Angre → PENDING + inventory restored, settings
renders, PDF generated and share sheet opened.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Milestone 8. docs/fest-dataset.md is the interface to the out-of-repo
server job that flattens the M30 Rekvirent extract (FEST, not
Felleskatalogen — decision #8) into slim JSON. The repository
downloads on demand, parses BEFORE overwriting the cache (a failed
refresh can never clobber a working dataset), and serves all searches
from memory. Ranking (prefix > word-prefix > substring) is a pure
object with tests. Every failure mode degrades to manual entry —
autocomplete is a convenience, never a gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Milestone 7. Human-readable A4 summary (meds, schedule, supply days,
rx expiry/reit, 30-day adherence, notes) — deliberately distinct from
the machine-readable JSON backup, zero PDF dependencies. The Norwegian
schedule phrasing (ScheduleText) and adherence math are pure and
unit-tested; a wrong schedule line on a doctor's desk is a clinical
communication error, so it gets engine-grade testing. FileProvider
exposes only cacheDir/summaries.
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 3. Single authority for DoseTime semantics: weekly mask
(bit 0 = Sunday) vs every-N-days from anchor (mask ignored when
intervalDays > 1). nextOccurrence is strictly-after by contract so the
alarm layer can pass the occurrence it just fired and never re-arm the
same minute. Wall-clock LocalDateTime throughout — instant conversion
happens at the alarm edge, so an 08:00 dose stays 08:00 across DST.
Consumption rate is prospective (from schedule) because days-of-supply
predicts forward; historical consumption stays in the DAO.
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>