The title runs over as many lines as it needs instead of trailing off, and what it says is highlighted as in quick add and set as it is saved (the developer's review, 10 Nov 2026): a day and its recurrence, the priority, the deadline, labels, reminders and places added, the project the task moves to; a word tapped, or a Backspace right after it, keeps it as text. Enter still saves. Quick add's controller and reading of its title are shared for it (title_tokens.dart). Whats-New: In a task's details, a long title now wraps, and words like "every Monday" in it are recognised as in quick add. Whats-New-De: In den Details einer Aufgabe bricht ein langer Titel jetzt um, und Wörter wie „jeden Montag“ darin werden wie beim schnellen Hinzufügen erkannt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
19 KiB
19 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_keyandprojecton the server (deleteTodoDatadeletes a person's space with them), and on the deviceTodoKeys(Train'sTrainKeyscopied 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 whichProjectRepository, 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, andLabelRepository), one sync gateway per table, andTodoPersonopening the person's sync database, itsSyncServiceand 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.completemoves a recurring task on, reopens its sub-tasks and returns what it changed forrestoreto undo; picked throughpickRepeat) and its reminders, minutes before its time and at times of their own (D5, D18:remindersandreminderTimes,ReminderTime; a task's payload keeps the fields a newer version wrote,TaskContent.unknown, so its schema stays 1;planRemindersplans them,ReminderSchedulerunder the person's app hands them toReminderGateway,SystemReminderGatewayonflutter_local_notifications, the system scheduling them but on Linux, where the running app shows them;TodoPerson.disposecancels 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_ALARMandSCHEDULE_EXACT_ALARMup to API 32,test/android/exact_alarms_test.dart, the build desugars). A task's locations (D32):TaskLocationin its sealed payload aslocations, 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, aSidePanebeside 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, overPlaceSearchGateway,TodoDependencies.places, the instance's seam, komoot's public Photon in a debug build with--dart-define=TODO_PUBLIC_PHOTON=truealone; 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,LocationReminderGatewayoverLocationReminders.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 HalkyonLocationin a debug build),LocationReminderAccessasNotificationAccess. A debug build isapp.halkyon.todo.debug, Todo Debug, beside the release. Packages:halkyon_todo_server,halkyon_todo_client(generated, never edited),halkyon_todo_flutter; app IDapp.halkyon.todoon 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 inserver.dart(todoBase(),lib/src/todo_base.dart): its basics say it servestodo(never change it once apps use it), sign-in mails name Halkyon Todo, the source is this repository,minimumAppBuild1, 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 setsACCOUNT_REGISTER_URL(todoBase(accounts: AccountsMode.fromEnvironment())) with the instance's sharedINSTANCE_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.addInstanceRoutesserves 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'semail_identity,email_code_request),profiles(public_profile),deletion-notices(account_deletion_notice), and since 1 Nov 2026projects(content_key,projectand the sync engine's tables) andtasks(section,task,label),settings(todo_settings),filters(saved_filter, D35), andcomments(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.<endpoint>), since the generated test tools expose the project's endpoints alone. - The app is halkyon_app's
InstanceGateandSessionGatewith Todo's frame (todoFrame: "Todo" under "Halkyon", its icon,todo.example.org, its own sentence) andServerpodInstanceChecker(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 aStatefulShellRoute, halkyon_ui'sNavigationFrame(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'sHalkyonProfileatTodoPaths.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,TodayViewin the person's sealed settings, D30, today's hours,HourGridwith 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 throughmoveTask; 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:ProjectViewin the project's sealed payload,DisplayButtonandshowDisplaySheetindisplay_sheet.dart; its layout, completed tasks, grouping, sorting, filters, applied byProjectTaskswithgroupTasks; 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 inProjectView.columns, in order, Task moving among them,showTableColumnsturning them and the task cell'sTaskDetails 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'sDayAgendaovercalendarOf, with No date and future occurrences of repeating tasks, and from 720 pixels of its width on a month,MonthGridovermonthOf), 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, 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;FilterRepositoryon the sealedsaved_filter), comments in a task's sheet (D36,lib/src/feature/comment/:TaskComments,CommentRepositoryon the sealedtask_comment,TaskRepository'sonDeletedtaking 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.dartworks them out on the device fromcompletedAtandcompletions;GoalSettingsin the person's sealed settings, days off and vacations kept;GoalReachedCardon 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: 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 byparseQuickAdd,lib/src/feature/task/model/quick_add_parser.dart, inQuickAddWords' tables of the app's language and English, a table per language, D15; since D18onbefore a day,for 2hdurations with a time, every!30m, and!1pmor!tomorrow 9amreminders 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'sDictationonTodoDependencies.speech, the phone's voice input app first where one is installed,VoiceInput.kt, Meditate's ADR 0038, D18,FakeSpeechInputin 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, back returning to the task before, D26) and its ⋮ menu in place of a close button (when it was added,addedAtfrom 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 rowsDraggableTaskinTaskDropTargets giving each half itsDropSpot,TaskRepository.place;AddTaskButtondragged onto those, onto Today'sNewTaskDropArea, 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 (TaskSelectionheld byTodoShellinTaskSelectionScope; a row swiped right, or Select tasks in a ⋮;TabScreenthen counts them and showsTaskSelectionBarin 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 withdays: 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, throughmoveTask), as its Display sets (upcomingDisplay, kept in the person's sealed settings row,SettingsRepositoryontodo_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 (SearchButtoninTabScreen,SearchScreenon the tab's stack,searchTasksmatching words folded to lower case without accents), all overTodoCubit, 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 inSectionCards: Sync's card, the operator's card, the device's appearance and language (halkyon_app'sDeviceLookCardonLookCubit, 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 byTaskRepositoryto 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'AppDelegateregisters the plugins for the background isolate), the account (halkyon_app'sAccountTiles, 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'sWhatsNewTile, Train's ADR 0030, D61, the texts inassets/whats_new/, shown once after an update byWhatsNewOnUpdatearound the tabs,WhatsNewScopeinmain.dart) and halkyon_app's About (showAppAbout: the source of what the instance runs, fromTodoDependencies.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:allowBackupoff andres/xml/data_extraction_rules.xmlexcluding 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 emptyen_USanden_GB(test/l10n/arb_files_test.dartkeeps 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'sresolveAppLocale. Widget tests start Todo's whole tree with halkyon_app's fakes (test/app/todo_app_test.dart). - The icon is Lucide's
list-todofading from cobalt through violet into strawberry, its backdrop (ADR 0031, D5), drawn with the other apps' by the repository'stool/app_icons.py(python3 -I tool/app_icons.py todofrom the root):assets/icon/halkyon_todo_icon.png, Lucide's ISC licence beside it, and every platform's launcher icon through flutter_launcher_icons.registerLucideLicenseputs the notice in the licence list. serverpod startruns inapps/todo/halkyon_todo_server(it launches the app fromlib/driver.darton Linux,device: linux, as Todo has no web build);.mcp.json's--server-dirnames it when a session works on Todo. Its tests need throwaway passwords in its git-ignoredconfig/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/.envnamesdeploy/compose.todo.yaml: at todo.halkyon.eu (config/production.yaml, the imageDockerfileas Train's), a member of Learn's register (named to it byINSTANCE_MEMBER_TODO), its databasehalkyon_todoin the instance's PostgreSQL;redeploy.shandbackup.shtake Todo's server and database in only where it is switched on, and the operator's steps aredeploy/sidecars/README.md, Todo. - Releases (ADR 0033, step 5) come from
dart run tool/release.dart todoon 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, aliashalkyon-todo, pinned intool/release/todo-android-certificate.sha256;android/app/build.gradle.ktssigns with it when the release tool hands it over, else with the debug key). The snap's recipe isapps/todo/snap/(its icon fromtool/app_icons.py): snapcraft builds one recipe a folder and the root'ssnap/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'ssnap/but for the sharedsnap/local/(SnapRelease.projectPath,_snapProject).