halkyon/apps/todo/AGENTS.md

11 lines
23 KiB
Markdown
Raw Normal View History

# 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.
2026-11-06 00:12:01 +01:00
- **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`).
feat(todo,app): files attached to tasks and comments Halkyon's ADR 0043, D1, D3, D5 and D8: any file up to 100 MB, attached to a task's description or to a comment, sealed as everything of Todo's. Server: the attachment table, synced and sealed as the others, with the sealed file's hash readable for the store; todoFiles hands halkyon_base its rows, readable by their space's members, and takes sealed files alone; the files no attachment names go daily. Its tests hold downloads to the row's space, refuse a readable file and prune what no live row names. Migration attachments: CREATE TABLE "attachment" with its indexes and the foreign key to its space, cascading as every synced table's. App: AttachmentRepository seals each file for its row, keeps it sealed and opened in the person's folder, uploads it after each sync and fetches it as it shows, both held back by Without files on mobile data but for a file tapped. Under the description and a comment's text, a row of them: pictures as thumbnails opening full-size, other files as chips opening in the system's app, else saved; the paperclip beside the description and in the comment's composer adds them, the composer holding them until it is sent. A task deleted takes its files and its comments' along, a comment its own. halkyon_app opens a file in the system's app (FileOpener): on Android through its plugin and a FileProvider of its own class, sharing the cache's halkyon_opened/ folder alone; on the desktop as a file: URI. Whats-New: Attach files to a task's description or a comment with the paperclip: pictures show as thumbnails, other files open in their app. Whats-New-De: Hänge mit der Büroklammer Dateien an die Beschreibung einer Aufgabe oder an einen Kommentar: Bilder erscheinen als Vorschau, andere Dateien öffnen sich in ihrer App. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 01:17:22 +01:00
- **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`), people's files kept in the module's store (`files: todoFiles`, `lib/src/attachment/todo_files.dart`: the `attachment` rows, sealed files alone, Halkyon's ADR 0043). 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), and the files no attachment names any more daily (`FilePruningFutureCall`). 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), `comments` (`task_comment`, D36), `file-store` (halkyon_base's `file_allowance`) and `attachments` (`attachment`, Halkyon's ADR 0043). **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. **Forecasts** (D40): `WeatherEndpoint` (`lib/src/weather/`, Train's ADR 0036 copied: `offered`, `forecast`, `ForecastHour`, `WeatherRefused`, `Forecasts`) answers a place's hours for the next 16 days from Open-Meteo, the place rounded to about a kilometre, for those signed in, nothing kept or logged, `WEATHER=off` and `OPEN_METEO_API_KEY` as Train's. Tests (`test/integration/todo_server_test.dart`) call the module's endpoints through the server's own dispatch (`halkyon_base.<endpoint>`), 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<TodoPerson>`) 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`), the weather (D40, `lib/src/feature/weather/`: `ForecastCubit` following the places `forecastPlaces` names, the person's own from Settings, `TodoSettings.weatherPlace`, else each open dated task's first place where it needs weather, through `WeatherGateway`, `InstanceWeather` on the server's `weather`, `TodoDependencies.weather`, `FakeWeather` in `test/support/`, asked as the app opens, hourly and on a pull, held in memory; a task's `WeatherNeeds` in its sealed payload as `weather`, `missingOn` judging them over its hours, `weatherSpan`; its row's mark, `TaskTile.weatherMarkKey`, its sheet's Weather row, `pickWeatherNeed
- **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`).