halkyon/apps/todo/AGENTS.md
Tizian 「ティツィアーン」 574b379f11
feat(todo): Today laid out as a calendar, planned from its Plan by dragging, as Todoist's desktop Today (ADR 0033, D30)
The developer asked for Todoist's desktop Today planning: a day of hours
and a Plan beside it, tasks dragged from it onto an hour. Today gets a
Display, its layout alone (List or Calendar), kept in the person's sealed
settings row as Upcoming's (`TodayView`, the payload's `today`, schema
still 1). Its Calendar is one day of `HourGrid`; *Plan: n* in its app bar
shows or hides the Plan (`PlanPanel`): Overdue with Reschedule, To plan
(today's without a time) and Time-blocked (today's with one), each
folding with its count, narrowed for the session by *All projects*'
Filter by project (a search and a box a project, a sub-project with its
parents' path). A row dropped on an hour is moved to today at that time,
its duration kept; a block dropped on the Plan loses its time; both undone
from the snack bar, through `moveTask`. From a medium width of Today's own
on the Plan stands beside the day, shown or hidden as remembered; on a
phone it is a sheet over the day's foot (the developer's choice, over the
Plan on wide windows alone and over a strip above the day), part of the
screen rather than a modal route, so that the day behind it takes the
drop, and it lowers to its heading while one of its rows is held.

Whats-New: Today's Display offers a Calendar layout: today's hours, with a Plan of what is overdue, to plan and time-blocked, whose tasks you drag onto an hour.
Whats-New-De: Die Anzeige von Heute bietet das Layout Kalender: die Stunden des Tages, mit einem Plan aus Überfälligem, Zu planen und Eingeplant, dessen Aufgaben du auf eine Stunde ziehst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-05 08:58:43 +01:00

15 KiB

Halkyon Todo

What holds for every app and package of this repository is in the root 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; Android's manifest declares the boot receiver and exact alarms, the build desugars). 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), and settings (todo_settings). 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 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), Browse (the favourite projects, then the projects with their open counts, sub-projects indented and folding under their parents, projectTree, D28, labels, what doesn't open counted); 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: 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, 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), where the task is on top (_TaskPath, its steps opening in place, as its sub-tasks do, TaskTile.onOpen, 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 TaskDropTargets 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; 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; 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 SectionCards: Sync's card, the operator's card, the device's appearance and language (halkyon_app's DeviceLookCard on LookCubit, kept in <app support>/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).