# Halkyon Todo What holds for every app and package of this repository is in the [root AGENTS.md](../../AGENTS.md); it applies here in full, and Learn's patterns (`apps/learn/AGENTS.md`) wherever Todo needs the same thing. - **A shell on the shared packages** (decided 2026-10-19; what Todo is to do and how it looks: ADR 0033, decided 2026-11-01): signing in, the instances, the account and the look work; since 1 Nov 2026 a person's projects are kept and synced with `serverpod_offline_sync`, every row sealed (ADR 0033, D5, D6): `content_key` and `project` on the server (`deleteTodoData` deletes a person's space with them), and on the device `TodoKeys` (Train's `TrainKeys` copied with Todo's own key IDs, `todo1.…`; to be shared in halkyon_app once Train's 0.1.1 is out), `SealedRows` (`lib/src/core/sealed/`: a table's rows sealed for their table and row, under which `ProjectRepository`, the Inbox under a row ID keyed by the account, made once as the person's data opens, never deleted, `SectionRepository`, `TaskRepository`, a task deleted with its sub-tasks, and `LabelRepository`), one sync gateway per table, and `TodoPerson` opening the person's sync database, its `SyncService` and all of them, as Train's; a project or a section is deleted with its tasks (`TodoPerson.deleteProject`, `deleteSection`). A task's date is a day with an optional wall-clock time and duration, no time zone (`TaskDate`), and its recurrence (`Repeat`, ADR 0033, D7: `TaskRepository.complete` moves a recurring task on, reopens its sub-tasks and returns what it changed for `restore` to undo; picked through `pickRepeat`) and its reminders, minutes before its time and at times of their own (D5, D18: `reminders` and `reminderTimes`, `ReminderTime`; a task's payload keeps the fields a newer version wrote, `TaskContent.unknown`, so its schema stays 1; `planReminders` plans them, `ReminderScheduler` under the person's app hands them to `ReminderGateway`, `SystemReminderGateway` on `flutter_local_notifications`, the system scheduling them but on Linux, where the running app shows them; `TodoPerson.dispose` cancels them; the permission to notify asked wherever a reminder is planned, once an app start, and Settings' Reminders saying while notifications are off, with Turn on, `NotificationAccess`, D31; Android's manifest declares the boot receiver and exact alarms, `USE_EXACT_ALARM` and `SCHEDULE_EXACT_ALARM` up to API 32, `test/android/exact_alarms_test.dart`, the build desugars). A task's locations (D32): `TaskLocation` in its sealed payload as `locations`, several being alternatives, each reminding on arriving, on leaving or not; `lib/src/feature/location/`: `showLocationPicker` (a map with a pin fixed in its middle, of fixed size above a sheet pulled up over it, a `SidePane` beside it in a wider window, what is around it named, a compass, and a search by city or district and name on a screen of its own, `PlaceSearchScreen`, over `PlaceSearchGateway`, `TodoDependencies.places`, the instance's seam, komoot's public Photon in a debug build with `--dart-define=TODO_PUBLIC_PHOTON=true` alone; results as a list and pins on Train's OpenFreeMap map, `mapTiles`, none in tests; a pin placed by hand), `+` in quick add, Where in the sheet; reminders on Android alone, `planLocationWatches`, `ReminderScheduler`, `LocationReminderGateway` over `LocationReminders.kt` (a foreground service's fixes, no Google services; a two-minute dwell, `LocationDwell`, its JUnit tests run with `./gradlew :app:testDebugUnitTest`, not in CI; `adb logcat -s HalkyonLocation` in a debug build), `LocationReminderAccess` as `NotificationAccess`. A debug build is `app.halkyon.todo.debug`, *Todo Debug*, beside the release. Packages: `halkyon_todo_server`, `halkyon_todo_client` (generated, never edited), `halkyon_todo_flutter`; app ID `app.halkyon.todo` on every platform; no web build. Licences as Learn's (server SSPL, client and app AGPL; `LICENSE.md`). - **The server** includes `halkyon_base` (ADR 0014) and configures it in `server.dart` (`todoBase()`, `lib/src/todo_base.dart`): its basics say it serves `todo` (never change it once apps use it), sign-in mails name Halkyon Todo, the source is this repository, `minimumAppBuild` 1, a person's space deleted with them (`deletePersonData: [deleteTodoData]`, `lib/src/todo_data.dart`). Authentication through the module's builders: a password, SimpleLogin on the register, email codes proving an address, server-side sessions; no anonymous accounts (ADR 0020, D25), nor GitHub (D36). **Accounts** (ADR 0016): Todo holds its own register when it runs alone, and is a member of the instance's register (Learn's server on learn.halkyon.eu) where the operator sets `ACCOUNT_REGISTER_URL` (`todoBase(accounts: AccountsMode.fromEnvironment())`) with the instance's shared `INSTANCE_SECRET` (`instanceSecret`); then sign-in, handles and profiles are the register's, the email settings are the register's alone, and an identified person's deletion goes to the register first. `addInstanceRoutes` serves the calls between the services on its web server, and the server doesn't start without the secret where it needs it. `schedulePruning` (`lib/src/pruning.dart`) keeps the module's pruning scheduled, one entry each however often the server starts (and cancels the one under the pruning's name before ADR 0020, D25): the anonymous accounts left from before daily, sign-up attempts and codes hourly, as the register the deletions its members haven't confirmed hourly, as a member the people the register no longer knows daily (each a no-op in the other mode). Session logs are never persisted, in any run mode. Migrations (2026-10-19): the initial one (Serverpod's tables, its auth modules' and the module's `email_identity`, `email_code_request`), `profiles` (`public_profile`), `deletion-notices` (`account_deletion_notice`), and since 1 Nov 2026 `projects` (`content_key`, `project` and the sync engine's tables) and `tasks` (`section`, `task`, `label`), `settings` (`todo_settings`), `filters` (`saved_filter`, D35), and `comments` (`task_comment`, D36). **Places** (D32): `SearchEndpoint` (`lib/src/search/`, copied from Train's: `offered`, `places`, `Place`, `PlacePoint`, `SearchRefused`) finds them, and what is around a point (`nearby`), through the instance's Photon (`PHOTON_URL`, `deploy/compose.todo-search.yaml`; none: not offered), for those signed in, nothing kept or logged, a shop's car park and charger left out. Tests (`test/integration/todo_server_test.dart`) call the module's endpoints through the server's own dispatch (`halkyon_base.`), since the generated test tools expose the project's endpoints alone. - **The app** is halkyon_app's `InstanceGate` and `SessionGate` with Todo's frame (`todoFrame`: "Todo" under "Halkyon", its icon, `todo.example.org`, its own sentence) and `ServerpodInstanceChecker(app: 'todo')`; `TodoDependencies` (`AppDependencies`) on one instance, `TodoPerson` (empty) per person; a person's app in Todo's frame (ADR 0033, D1 to D4, since 1 Nov 2026), as Train's: go_router (`buildTodoRouter`, `TodoPaths`) with a `StatefulShellRoute`, halkyon_ui's `NavigationFrame` (`TodoShell`) with the tabs Inbox, Today (where it opens), Upcoming and Browse (D12; Browse's first row opens every Halkyon app's Profile over the tabs, halkyon_app's `HalkyonProfile` at `TodoPaths.profile`, decided 2026-11-02, as Todoist keeps the account there), (`lib/src/tabs/`; ADR 0033, D8, since 1 Nov 2026): Today (overdue first with Reschedule, then today's; or, as its Display sets, `todayDisplay`, `TodayView` in the person's sealed settings, D30, today's hours, `HourGrid` with one day, beside the Plan, `PlanPanel`: Overdue, To plan and Time-blocked, filtered by project for the session, its rows dragged onto an hour and the day's blocks back onto it through `moveTask`; a panel from a medium width of Today's own on, shown or hidden as remembered, on a phone a sheet over the day's foot, part of the screen, lowering while a row is held), Upcoming (overdue, then the next 28 days, empty ones muted), Inbox and a project's screen (`ProjectScreen`, from Browse: its tasks above and in its sections, shown as its Display sets (D17: `ProjectView` in the project's sealed payload, `DisplayButton` and `showDisplaySheet` in `display_sheet.dart`; its layout, completed tasks, grouping, sorting, filters, applied by `ProjectTasks` with `groupTasks`; laid out as a board, `ProjectBoard`, `project_board.dart`: a column a section, paged below a medium width, side by side from it; as a table, `ProjectTable`, `project_table.dart`, D36: from 720 pixels of its width on, a list below, offered from expanded windows on; its columns in `ProjectView.columns`, in order, Task moving among them, `showTableColumns` turning them and the task cell's `TaskDetail`s on and off, a heading's menu filtering, sorting, moving and hiding its column, sideways scrolling where they don't fit; as a calendar, `ProjectCalendarView`: Upcoming's `DayAgenda` over `calendarOf`, with *No date* and future occurrences of repeating tasks, and from 720 pixels of its width on a month, `MonthGrid` over `monthOf`), sections added, renamed and deleted, the project edited (`editProject`, `project_editor.dart`, as Todoist's Add project: name, description, colour, parent, favourite, layout, D28; its default Display, `defaultView`, what Display's Reset returns to) and deleted, with its sub-projects), a calendar's feed linked to a project (D39, `lib/src/feature/calendar/`: the address in the project's sealed payload, `calendarUrl`, written in its editor; `CalendarCubit` following the projects, fetching each feed by the device, `HttpCalendarFeeds`, as the app opens, hourly and on a pull, the last copy in the person's folder, `FileCalendarStore`; `readIcal` reading a feed's events, recurrence by `rrule`; `FakeCalendarFeeds` in `test/support/`), its events read-only beside the tasks (`EventTile`, `showEvent`, `eventsOn`, `eventsFrom`) in Today's list, the agenda, the hour grids (`layOutItems`, `_EventBlock`), the month (`_EventLine`) and saved filters of days (`filterTakesEvent`), Browse (the favourite projects, then the projects with their open counts, sub-projects indented and folding under their parents, `projectTree`, D28, the row to *Filters & labels*, what doesn't open counted), *Filters & labels* (D35, `lib/src/feature/filter/`: `FiltersScreen`, a filter's or a label's tasks, `FilterScreen`, `LabelScreen`, `editFilter`; `FilterQuery`, Todoist's query language read on the device, each word in German too, `tasksOfFilter`; `FilterRepository` on the sealed `saved_filter`), comments in a task's sheet (D36, `lib/src/feature/comment/`: `TaskComments`, `CommentRepository` on the sealed `task_comment`, `TaskRepository`'s `onDeleted` taking them along with their task, `linkPieces`, http and https alone), Reporting (D36, `lib/src/feature/report/`, Browse's row: `ReportingScreen`, the daily and weekly goal with their streaks and charts and the history, by project; `productivity.dart` works them out on the device from `completedAt` and `completions`; `GoalSettings` in the person's sealed settings, days off and vacations kept; `GoalReachedCard` on Today, once a day); a task's row (`TaskTile`: its check in its priority's colour, its date in the colour of how near it is, deadline, sub-tasks done, labels; in its rounded box, a list's rows a group, `taskGroup`, D20), lists pulled down to sync (`TaskRefresh`, D21), quick add (`showQuickAdd`, a bar docked above the keyboard, D23, the task it adds brought into view just above it, `TaskReveal` in `task_reveal.dart`: the row claiming it in `TaskTile`, `TabScreen`'s `TaskRevealRoom` leaving room for the bar at the lists' end and scrolling on to a row not built yet; the developer's review, 10 Nov 2026: title, date, priority, project, labels, description, deadline as boxed buttons, the description and deadline behind its +, projects and labels suggested as `#` and `@` are typed; what is typed read by `parseQuickAdd`, `lib/src/feature/task/model/quick_add_parser.dart`, in `QuickAddWords`' tables of the app's language and English, a table per language, D15; since D18 `on` before a day, `for 2h` durations with a time, every `!30m`, and `!1pm` or `!tomorrow 9am` reminders at a time of their own; a highlighted word kept as text by a tap or a Backspace right after it, that occurrence alone, its range following the edits, `shiftKept`, D29; its last button the microphone while the title is empty, halkyon_app's `Dictation` on `TodoDependencies.speech`, the phone's voice input app first where one is installed, `VoiceInput.kt`, Meditate's ADR 0038, D18, `FakeSpeechInput` in tests, the send button once there is text) and its sheet (`openTaskSheet`; from expanded windows on the pane beside the tabs instead, `TaskPane`, D2; a dialog on the desktop, D11), its title wrapping and read as quick add's, its words highlighted and set as it is saved (`TitleController`, `parseTitle`, `title_tokens.dart`, shared with quick add), where the task is on top (`_TaskPath`, its steps opening in place, as its sub-tasks do, `TaskTile.onOpen`, its sub-tasks held and dragged into another order among themselves, `_placedSubTask`, back returning to the task before, D26) and its ⋮ menu in place of a close button (when it was added, `addedAt` from its UUID v7; Duplicate, `TaskRepository.duplicate`; its done sub-tasks hidden, `hideDoneSubTasks`; Delete; D13), menus' entries with their icons (`MenuEntry`, D14), sections' headings pinned while their tasks scroll (`ProjectTasks`, slivers), dragging (D16, `task_drag.dart`: a project's rows `DraggableTask` in `TaskDropTarget`s giving each half its `DropSpot`, `TaskRepository.place`; `AddTaskButton` dragged onto those, onto Today's `NewTaskDropArea`, or onto a day of Upcoming, `DayDropArea`, opening quick add there, but not over a day's hours, Today's Calendar and Upcoming's 3-day, `AddTaskButton(draggable: false)`; Upcoming's rows dragged onto a day move to it), tasks chosen together (`TaskSelection` held by `TodoShell` in `TaskSelectionScope`; a row swiped right, or *Select tasks* in a ⋮; `TabScreen` then counts them and shows `TaskSelectionBar` in the navigation bar's place; a row swiped left asks for its date; `TaskTile(selectable: false)` in a sheet's sub-tasks and search), Upcoming's agenda (`DayAgenda`, `lib/src/tabs/day_agenda.dart`, a project's Calendar's too) or 3-day grid (`HourGrid`, `lib/src/tabs/hour_grid.dart`, Today's Calendar too with `days: 1`, `layOutDay`, a line at the time now, a strip on each day's right left free of blocks, `HourGrid.freeStrip`; a tap or a click adding half an hour, a mouse dragged or a finger held and dragged a stretch of quarter hours; its blocks dragged to another day and time, or onto *All day*, through `moveTask`), as its Display sets (`upcomingDisplay`, kept in the person's sealed settings row, `SettingsRepository` on `todo_settings`, D17) and week strip (`WeekStrip`, a day picked scrolling the agenda to it, its month unfolding in place, a way back to today), search from every tab's app bar (`SearchButton` in `TabScreen`, `SearchScreen` on the tab's stack, `searchTasks` matching words folded to lower case without accents), all over `TodoCubit`, which reads everything again on any change; colours from halkyon_ui's palette (`task_texts.dart`, D9), every screen washing down in strawberry (`ScreenColor`, `TabScreen`); Settings (`lib/src/settings/`) over the tabs from Browse's cog (its dot while sync fails), grouped in `SectionCard`s: Sync's card, the operator's card, the device's appearance and language (halkyon_app's `DeviceLookCard` on `LookCubit`, kept in `/look.json`, `todoLanguages`; ADR 0014's amendment of 4 Nov 2026), Tasks and Reminders (`todo_settings_cards.dart`, D4: the week's first day, `firstWeekdayOf`; resetting sub-tasks; the automatic reminder, added by `TaskRepository` to a task gaining a time; how long Snooze lasts, the notification's action, `snooze.dart`, `snoozeInBackground`: given with the notification on Android and Linux, Apple's from a category for each length, registered at start, Windows' a button carrying the notification's ID and payload; iOS' `AppDelegate` registers the plugins for the background isolate), the account (halkyon_app's `AccountTiles`, whose account on which instance, Switch account, Sign out, Delete account, which asks first, with SimpleLogin and the account's security) in Todo's words, What's new (halkyon_app's `WhatsNewTile`, Train's ADR 0030, D61, the texts in `assets/whats_new/`, shown once after an update by `WhatsNewOnUpdate` around the tabs, `WhatsNewScope` in `main.dart`) and halkyon_app's About (`showAppAbout`: the source of what the instance runs, from `TodoDependencies.sourceCodeUrl`, and the licence list), the version below (`TodoDependencies.appVersion`). Nothing of the app's goes to Android's backup or to another phone (as Train's D53: `allowBackup` off and `res/xml/data_extraction_rules.xml` excluding every folder, `test/android/backup_rules_test.dart`), as the reminders scheduled keep their tasks' titles unsealed. Texts through gen-l10n in British English (the template), German (informal du), an empty `en_US` and `en_GB` (`test/l10n/arb_files_test.dart` keeps them in step). The look follows the system's theme mode and language until Todo has settings. Countries in where an instance keeps its data are named by halkyon_app's CLDR names (`AssetCountryNames`), its language picked by halkyon_app's `resolveAppLocale`. Widget tests start Todo's whole tree with halkyon_app's fakes (`test/app/todo_app_test.dart`). - **The icon** is Lucide's `list-todo` fading from cobalt through violet into strawberry, its backdrop (ADR 0031, D5), drawn with the other apps' by the repository's `tool/app_icons.py` (`python3 -I tool/app_icons.py todo` from the root): `assets/icon/halkyon_todo_icon.png`, Lucide's ISC licence beside it, and every platform's launcher icon through flutter_launcher_icons. `registerLucideLicense` puts the notice in the licence list. - **`serverpod start`** runs in `apps/todo/halkyon_todo_server` (it launches the app from `lib/driver.dart` on Linux, `device: linux`, as Todo has no web build); `.mcp.json`'s `--server-dir` names it when a session works on Todo. Its tests need throwaway passwords in its git-ignored `config/passwords.yaml` (`database`, `serviceSecret`, `serverSideSessionKeyHashPepper`, `emailSignInPepper`) or from the environment, as CI gives them. Learn's and Todo's development servers use the same ports: run one at a time. - **Deployed** (ADR 0033, step 5) beside learn.halkyon.eu's server once `deploy/.env` names `deploy/compose.todo.yaml`: at todo.halkyon.eu (`config/production.yaml`, the image `Dockerfile` as Train's), a member of Learn's register (named to it by `INSTANCE_MEMBER_TODO`), its database `halkyon_todo` in the instance's PostgreSQL; `redeploy.sh` and `backup.sh` take Todo's server and database in only where it is switched on, and the operator's steps are `deploy/sidecars/README.md`, *Todo*. - **Releases** (ADR 0033, step 5) come from `dart run tool/release.dart todo` on the developer's laptop, as Learn's and Train's (root AGENTS.md, Learn's *Releases*): `apps/todo/release.dart`, the APK and the snap for todo.halkyon.eu, on code.halkyon.app, the APK in the project's F-Droid repository (`tool/release/fdroid/metadata/app.halkyon.todo.yml`, `NonFreeNet`), the snap to the Store's edge channel; signed with Todo's own key (`~/.local/share/halkyon-todo/android-release.p12`, alias `halkyon-todo`, pinned in `tool/release/todo-android-certificate.sha256`; `android/app/build.gradle.kts` signs with it when the release tool hands it over, else with the debug key). The snap's recipe is `apps/todo/snap/` (its icon from `tool/app_icons.py`): snapcraft builds one recipe a folder and the root's `snap/` is Learn's, so the release tool builds Todo's in a clone of the repository at the tag kept in `~/.cache/halkyon-release/snap/halkyon-todo`, Todo's folder in place of the clone's `snap/` but for the shared `snap/local/` (`SnapRelease.projectPath`, `_snapProject`).