Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 43 additions & 2 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<tab>/*ListViewModel.kt`, the `*UiState` class at the top | Its test in `app/src/test/.../ui/screens/<tab>/` |
| A card's look | `ui/components/<Thing>Card.kt`; badges/chips in `ui/components/core/` | Tones in `ui/theme/StatusTone.kt` |
| An edit sheet | `ui/components/Edit<Thing>Sheet.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 `<Name>SubScreen.kt`; controls in `CozyControls.kt` | Its own `<Name>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
Expand Down
214 changes: 138 additions & 76 deletions README.md
Original file line number Diff line number Diff line change
@@ -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).

---

Expand All @@ -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=<id>`).
- **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=<id>`), 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/<slug>.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 \
Expand All @@ -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.
Loading