diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index d10cea7..898ba6e 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -88,14 +88,55 @@ list screens. - Choose test timestamps mid-window (36h, 180h, 300h) so now-relative arithmetic cannot flip a bucket during the run. +## Where to start (a map for a fresh session) + +The repo has ~145 Kotlin files. Most tasks touch one seam. Find it here before +reading code. + +| To change... | Start at | Then | +|---|---|---| +| What a list shows (filter, sort, sections, hiding) | `ui/screens//*ListViewModel.kt`, the `*UiState` class at the top | Its test in `app/src/test/.../ui/screens//` | +| A card's look | `ui/components/Card.kt`; badges/chips in `ui/components/core/` | Tones in `ui/theme/StatusTone.kt` | +| An edit sheet | `ui/components/EditSheet.kt` / `AddReminderSheet.kt`; shared rows in `ui/components/sheet/SheetParts.kt` | Drafts in `data/model/SheetDraft.kt` | +| When a memo or tag-alarm rings, arms, advances | `data/model/Reminder.kt` (memo lifecycle) and `data/model/TagAlarm.kt` (tag-alarm rules); `ReminderSchedule.kt` for the words | `ReminderModelTest`, `TagAlarmModelTest`, `ReminderScheduleTest` | +| Arming / turning off a tag-alarm from anywhere | `tagalarm/TagAlarmService.kt` (the only entry point) | Callers: `MainActivity`, `RemindersListViewModel`, `ReminderViewViewModel` | +| What happens on an NFC tap | `MainActivity.handleNfcIntent` → `routeScannedTag` (tag-alarm first, then chore paths) | `nfc/NfcHandler.kt` for reading, writing, erasing | +| Scheduling, ringing, boot, snooze | `alarm/AlarmScheduler.kt` (`syncReminder` after every mutation), `AlarmReceiver`, `AlarmActionReceiver`, `BootWorker`, `AlarmActivity` + `AlarmRinger` | `notification/NotificationHelper.kt` for channels and the full-screen intent | +| A Settings page | `ui/screens/settings/SettingsScreen.kt` (the `SettingsSubScreen` enum and dispatch) + one `SubScreen.kt`; controls in `CozyControls.kt` | Its own `ViewModel.kt` if it has state worth testing (`TagsViewModel` is the pattern) | +| Supabase reads/writes | `data/repository/ChoreRepository.kt`, `TaskRepository.kt` | `supabase/schema.sql` for tables, RLS, grants | +| Widgets | `widget/` (Glance); destinations in `WidgetNav.kt` | `WidgetUpdater.updateAll` after data changes | +| Theme, palettes, contrast | `ui/theme/Theme.kt`, `Color.kt`, `DashTokens.kt`, `Contrast.kt` | | + +Facts that save a detour: + +- A **chore is a row in the `tags` table**; its `tagId` is the primary key and the + NFC id. Chore ids and tag-alarm tag ids share one id space; a tag has one job. +- **Memos are on-device** (`ReminderRepository`, DataStore). They never reach + Supabase. So are settings, category styles, snoozes and the sticker record. +- **State that crosses tabs lives on `MainActivity`** as `mutableStateOf` and is + handed through `DashNavGraph` as parameters plus "consumed" callbacks: pending + NFC tag, NFC write request, NFC capture, notification deep link, tag-alarm + conflict. Follow that pattern rather than a new bus. +- **Every alarm mutation ends with `alarmScheduler.syncReminder(record)`.** It + cancels and re-arms from the record, so the receiver, boot and snooze paths + need no per-feature alarm code (LESSONS #57). +- **Pure state, tested.** List logic lives in `*UiState` data classes and + `data/model`, never in composables. Tests are named for behaviours. +- **Errors shown to users go through `userFacingMessage()`** (`data/supabase/`), + which keeps the reason and drops the request dump. +- **No build here.** The web/remote container has no Android SDK; CI is the check. + `python3 a11y_check.py` and `python3 check_changelog_fragment.py` do run. +- `LESSONS.md` is long: `grep -n "^## [0-9]" LESSONS.md` lists the headings; + read only the ones your task touches. + ## Architecture Notes - **UI layer:** Jetpack Compose + Material 3, MVVM with ViewModels; navigation via Compose Navigation (single Activity, `DashNavGraph.kt`) - **DI:** Hilt (`di/AppModule.kt`, `di/SupabaseModule.kt`) - **Data layer:** `ChoreRepository` and `TaskRepository` read/write a shared Supabase project (Postgrest) for `chores`, `chore_logs`, `todos`, `owners` — no local database for chore/task data. `SettingsRepository` (DataStore) persists Supabase credentials and user preferences locally. A small Room database (`data/database/AppDatabase.kt`, `dash.db`) stores saved custom colour themes only — schema changes need an explicit migration, never `fallbackToDestructiveMigration`. - **Theme:** Five built-in Material 3 palettes (Cream default, implementing the "Cozy Cream" design system; Mist, Sage, Coral, Teal in `ui/theme/Color.kt`) plus a custom theme with per-role colour pickers and background overrides; light/dark/system brightness and a WCAG high-contrast toggle (`DashTheme` in `ui/theme/Theme.kt`). Headers use Lora (serif), body/UI text uses Nunito (`ui/theme/Type.kt`); shared shape/spacing tokens live in `ui/theme/Shape.kt` and `ui/theme/Dimens.kt`, and the fixed status tones (rose/amber/sage) in `ui/theme/Color.kt` + `ui/theme/StatusTone.kt`. -- **Background work:** WorkManager (`BootWorker`, `DailyStaleChoreWorker`) + AlarmManager (`AlarmScheduler`, `AlarmReceiver`) for task reminders, scheduled via Hilt-injected workers. -- **NFC:** `MainActivity` handles NFC foreground dispatch; `NfcHandler` extracts tag IDs to match against chores. +- **Background work:** WorkManager (`BootWorker`, `DailyStaleChoreWorker`) + AlarmManager (`AlarmScheduler`, `AlarmReceiver`, `AlarmActivity` for the full-screen ring) for task reminders and memos, scheduled via Hilt-injected workers. A tag-alarm's morning (first ring plus follow-ups) advances through the memo's `remindAt` under one alarm identity. +- **NFC:** `MainActivity` handles NFC foreground dispatch and routes a scanned id: a tag-alarm's tag arms it (`TagAlarmService`), anything else goes to the chore paths. `NfcHandler` reads ids (text record, `chordash://tag?tag=` or `chordash://memo?memo=` URI, hardware UID), writes them, and erases stickers. Settings › NFC tags (`TagsSubScreen`) is the maintenance page. - **Permissions:** `NFC`, `SCHEDULE_EXACT_ALARM`, `USE_EXACT_ALARM`, `POST_NOTIFICATIONS`, `RECEIVE_BOOT_COMPLETED`, `VIBRATE`, `INTERNET` (required for Supabase), `ACCESS_NOTIFICATION_POLICY` (lets the app appear in Settings > Do Not Disturb access and lets reminder alarms bypass Do Not Disturb). Do not add new permissions without discussion, and document the reason for each one in the manifest. ## Key Rules diff --git a/README.md b/README.md index 933201b..ba4de81 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,24 @@ # choreDash + taskDash — Android -Native Android app combining two household tools: - -- **choreDash** — chore tracker with NFC tag support. Tap a tag on the fridge, washing machine, etc. to log that chore as done. -- **taskDash** — shared to-do list with categories, priority, due dates, and per-task reminder notifications. - -Both tools read from / write to the same Supabase project used by the web app. -Credentials are entered once in the **Settings** tab and persisted in DataStore. +A native Android app for a household: three tools in one, sharing one Supabase +project with the taskDash web app. + +- **Chores**: recurring jobs tracked by NFC stickers. Tap the tag on the washing + machine and the chore is logged; the list shows what is overdue, due soon, or + fresh, with owners, categories, snoozes and smart visibility. +- **Tasks**: a shared to-do list with categories, priority, due dates or periods, + and per-task reminders. +- **Memos**: on-device reminders in three shapes. A once-only memo rings once. A + repeating memo rings on chosen weekdays. A **tag-alarm** is a dormant morning + alarm that a tap on its NFC tag arms for the next morning only, for people whose + mornings follow no fixed pattern. + +Around them: home-screen widgets (Next up, a pinned item, quick-add), five colour +palettes plus a custom theme builder with a WCAG high-contrast mode, and a +Settings tab that also covers notification style, categories, and NFC tag +maintenance. + +Current status: beta (`0.x.y`). See [`CHANGELOG.md`](CHANGELOG.md). --- @@ -20,103 +32,153 @@ Credentials are entered once in the **Settings** tab and persisted in DataStore. | Compile SDK | 35 | | Min SDK | 26 (Android 8) | +The phone needs NFC for chores and tag-alarms; everything else works without it. + --- -## Build +## Build and check ```bash -# Debug APK -./gradlew assembleDebug - -# Run unit tests -./gradlew test - -# Lint -./gradlew lintDebug +./gradlew assembleDebug # debug APK +./gradlew testDebugUnitTest # JVM unit tests (app/src/test) +./gradlew lintDebug # Android lint +python3 a11y_check.py # every clickable carries a semantics role +python3 check_changelog_fragment.py # the PR has a valid changelog fragment ``` +Unit tests are the behaviour spec for the list screens and the memo rules: +each test name states one thing the app guarantees, in plain words. Read +`OwnerFilterTest`, `ChoreUiStateTest`, `TaskUiStateTest`, `ReminderUiStateTest` +and `TagAlarmModelTest` before changing what a list shows or when an alarm rings. + --- ## Project structure ``` app/src/main/java/com/mapgie/dash/ - DashApplication.kt # HiltAndroidApp + WorkManager Configuration.Provider - MainActivity.kt # NFC foreground dispatch, Compose entry point - alarm/ - AlarmReceiver.kt # BroadcastReceiver for task reminders - AlarmScheduler.kt # AlarmManager wrapper - BootReceiver.kt # Re-schedules alarms after reboot - BootWorker.kt # HiltWorker: queries pending reminders on boot - DailyStaleChoreWorker.kt # HiltWorker: daily stale-chore notification + DashApplication.kt # HiltAndroidApp + WorkManager configuration + MainActivity.kt # Single activity: NFC dispatch and routing, notification deep links, theme + alarm/ # AlarmScheduler (AlarmManager), AlarmReceiver, AlarmActivity + AlarmRinger + # (the full-screen ring), BootWorker, DailyStaleChoreWorker + tagalarm/ # TagAlarmService: the one place a tag-alarm is armed or turned off + nfc/ # NfcHandler: read a tag's id, write or erase a tag; NfcWriteRequest + notification/ # Channels, delivery styles (Alarm / Notification / Silent), permissions + permission/ # Settings deep links for exact alarms, notifications, full-screen, DND data/ - model/ # Chore.kt, Task.kt, Owner.kt + enums/extension fns - preferences/ # SettingsRepository (DataStore) - repository/ # ChoreRepository, TaskRepository (Supabase) - supabase/ # SupabaseClientProvider - database/ # Room DB (AppDatabase + dao/ + entities/) for saved custom themes - di/ - AppModule.kt # Hilt module (app-wide dependencies) - SupabaseModule.kt # Hilt module (Supabase client wiring) - notification/ - NotificationHelper.kt # Channel creation + show helpers - permission/ - PermissionHelper.kt # Settings deep links for exact alarms + notifications - nfc/ - NfcHandler.kt # NDEF/URI/raw-hex tag-ID extraction - util/ # DateFormat, CalendarShareUtils (.ics export) - widget/ # Glance home-screen widgets + update workers + model/ # Chore, Task, Owner, Reminder (memo) + the pure rules: + # TagAlarm.kt, ReminderSchedule.kt, OwnerFilter, sort keys, drafts + repository/ # ChoreRepository, TaskRepository (Supabase); ReminderRepository (on-device) + preferences/ # DataStore: SettingsRepository, CategoryStyleStore, ChoreSnoozeStore, + # TagStickerStore (which ids this phone has met on a sticker) + supabase/ # SupabaseClientProvider, user-facing error trimming + database/ # Room (dash.db): saved custom colour themes only + di/ # Hilt modules ui/ - theme/ # Color.kt, Theme.kt, Type.kt, colour picker + saved-theme UI - navigation/ # DashNavGraph.kt + navigation/ # DashNavGraph: tabs, the speed dial, app-level dialogs screens/ - chores/ # ChoreListViewModel + ChoreListScreen - tasks/ # TaskListViewModel + TaskListScreen - reminders/ # RemindersListViewModel + RemindersListScreen - settings/ # SettingsViewModel + SettingsScreen - licenses/ # LicensesScreen - components/ # ChoreCard, LogBottomSheet, EditChoreSheet, - # TaskCard, EditTaskSheet, AddMenuFab + chores/ tasks/ reminders/ # One *ListScreen + *ListViewModel each; *UiState is pure and tested + reminder/ # The full-screen ring / nudge view a notification opens + settings/ # SettingsScreen + one *SubScreen.kt per page (Categories, NFC tags, ...) + licenses/ + components/ # Cards, edit sheets, dialogs; core/ (header, badges, chips), sheet/ (sheet parts) + theme/ # Palettes, tokens, typography, colour picker, saved themes + widget/ # Glance widgets + refresh workers + util/ # Date formatting, .ics export +app/src/test/ # JUnit tests on the JVM; no Android +supabase/schema.sql # The shared schema: tables, RLS policies, grants; idempotent +changelog/unreleased/ # One JSON fragment per PR; the release workflow consolidates them +LESSONS.md # Numbered lessons from bugs already fixed; check before fixing a new one +.claude/CLAUDE.md # Working rules and a map of where things live ``` --- -## NFC setup +## Where data lives + +| Data | Where | Shared with other phones | +|---|---|---| +| Chores (`tags`), scans, tasks (`todos`), owners | Supabase | Yes | +| Memos, including tag-alarms and their tag ids | DataStore on the phone | No | +| Settings, credentials, category styles, snoozes, sticker record | DataStore on the phone | No | +| Saved custom themes | Room on the phone | No | -1. Write NDEF Text records to your NFC tags (any NFC writer app). -2. The tag ID is used to identify a chore — see `NfcHandler.extractTagId()`. -3. When the app receives an NFC intent, `LogBottomSheet` opens pre-filled with the matching chore (or shows "Unknown tag" if no chore matches). +A chore **is** a row in the `tags` table, and its `tag_id` is both its primary key +and the id its NFC sticker carries. Memos never touch Supabase. + +--- + +## NFC + +Every tag the app reads resolves to one id: an NDEF text record, the `tag` or +`memo` query parameter of a `chordash://` URI, or the sticker's hardware UID when +it carries nothing. One id space serves chores and tag-alarms, and **a tag has +one job**: an id a chore owns cannot be a tag-alarm's, and the other way round. + +- **Chore tags.** Tap a sticker: with the app open, the log sheet opens for that + chore; from the home screen, the chore is logged and a toast confirms it. A + sticker no chore knows opens a new chore with the id filled in. Write a chore's + id to a blank sticker from its edit sheet (`chordash://tag?tag=`). +- **Tag-alarm tags.** Name the tag from the memo's Tag row ("Office A" becomes + `office-a`), write it to a blank sticker (`chordash://memo?memo=`), or scan + a card that already carries an id (an office pass). A tap sets the alarm for the + next time its first ring comes round, today or tomorrow, weekday ignored, and + the alarm is dormant again after that morning. Tapping again never turns it off. +- **Settings › NFC tags.** Identify any tag (what it belongs to), list every + chore's and tag-alarm's tag, write an id to a sticker, erase a sticker, unlink a + tag-alarm, and filter chores by whether this phone has met them on a sticker. + +NFC needs the screen on and unlocked. An unformatted sticker works too; the app +formats it on the first write. --- ## Supabase setup -This app has no backend of its own. It reads/writes a Supabase project directly -(optionally the same project as the taskDash web app). Each install needs its own -project and credentials: - -1. Create a free project at [supabase.com](https://supabase.com). -2. Open **SQL Editor → New query**, paste the entire contents of - [`supabase/schema.sql`](supabase/schema.sql), and run it. This creates the - `owners`, `tags`, `scans`, and `todos` tables with the columns and row-level - security policies the app expects. -3. Add at least one row to `owners` (e.g. `INSERT INTO owners (handle) VALUES ('alex');`) - so the "I am" picker in Settings has something to show. -4. In Supabase, go to **Settings → API** and copy the **Project URL** and the - **anon / public key**. -5. In the app, open **Settings → Supabase connection** and enter the Project URL, - anon key, and your owner handle, then tap **Save**. - -If you're pairing this app with the taskDash web app, point both at the same -Supabase project; `schema.sql` covers both apps' tables (including `todos` and -`owners`), so you only need to run it once. +The app has no backend of its own. Each install points at a Supabase project, +optionally the same one as the taskDash web app. + +1. Create a project at [supabase.com](https://supabase.com). +2. Open **SQL Editor → New query**, paste all of + [`supabase/schema.sql`](supabase/schema.sql), and run it. It creates `owners`, + `tags`, `scans` and `todos` with their row-level security policies and the + table grants the Data API needs. Every statement is idempotent, so re-running + it later is safe and never touches rows. +3. Add at least one row to `owners`, for example + `INSERT INTO owners (handle) VALUES ('alex');`. +4. In Supabase, **Settings → API**: copy the **Project URL** and the publishable + (anon) key. +5. In the app, **Settings → Supabase connection**: enter both and your owner + handle, then Save. + +If a request ever fails with "permission denied for table ...", the table is +missing its grant: re-run the grants block at the bottom of `schema.sql`. See +[`supabase/README.md`](supabase/README.md) for the optional workflow that applies +the schema automatically on merge. + +--- + +## Contributing + +- **Changelog.** Every PR that touches app code adds one fragment at + `changelog/unreleased/.json` with a `bump` (`patch` / `minor` / `major`) + and the user-facing lines. Never edit `CHANGELOG.md` or the version in + `app/build.gradle.kts` by hand; the **Release** workflow consolidates fragments, + bumps the version, and publishes the APK. +- **CI on every PR:** build and unit tests, lint, CodeQL, the accessibility role + check, the changelog fragment check, and a licence screen sync check. +- **Rules of the house** are in [`.claude/CLAUDE.md`](.claude/CLAUDE.md): + versioning, accessibility (every clickable has a role, colour is never the only + signal, 44dp targets), no dashes in user-facing text, credentials never logged. + [`LESSONS.md`](LESSONS.md) holds the numbered lessons from bugs already fixed. --- ## Binary files -`app/debug.keystore` and `gradle/wrapper/gradle-wrapper.jar` are binary files. -After cloning, copy them from a local choreDash checkout or generate a new debug keystore with: +`app/debug.keystore` and `gradle/wrapper/gradle-wrapper.jar` are binary files. +After cloning, copy them from a local choreDash checkout or generate a new debug +keystore with: ```bash keytool -genkey -v -keystore app/debug.keystore -alias androiddebugkey \ @@ -126,6 +188,6 @@ keytool -genkey -v -keystore app/debug.keystore -alias androiddebugkey \ --- -## Open-source licenses +## Open-source licences -See **Settings → Open-source licenses** inside the app. +See **Settings → About → Open-source licences** inside the app.