halkyon/docs/adr/0033-halkyon-todo.md

400 lines
31 KiB
Markdown
Raw Normal View History

# ADR 0033: Halkyon Todo, what it does and how it looks
- **Status:** decided 1 Nov 2026, by the developer (D1–D10 below, the agent's
recommendations taken as a whole). Built the same day: steps 1 to 3, Today and
Upcoming as lists, recurrence, reminders and the week strip (step 4), the task
pane on wide windows and search (D2).
- **Numbering:** 0025–0029 are held (ADR 0030's note), 0032 is Jellyfin's; this is the
next free number.
- **Builds on:** ADR 0030, D7, D12, D14, D15 and D60 (Train's sealed rows, its shell, its
frame and settings, which Todo takes); ADR 0017 (the account's keys); ADR 0031 (Todo's
icon); Train's D61 (What's new).
## Context
On 1 Nov 2026 the developer asked for work on Todo to start from 68 screenshots of
Todoist's Android app (31 Oct 2026, `~/Downloads/todoist-screenshots-mobile/`), "following
the same foundational reasoning of Halkyon Learn and Halkyon Train": when the bar and when
the rail, a wash of strawberry, settings grouped as theirs. What the screenshots show:
- **Five tabs** (Inbox, Today, Search, Upcoming, Browse), a floating add button above the
bar, the bar's items and the button's side set by the person; Browse holds the
projects, Reporting, templates and the cog to Settings.
- **A task** has a title, a description, a **date** (optional time and **duration**,
"06:30–07:00") and, apart from it, a **deadline** (date only, its own icon, neutral
colour; a task may have a deadline and no date), a priority (P1–P4), labels, reminders
(relative to the time, automatic 30 minutes before a timed task; by location in the paid
plans), a recurrence (completing it moves it on; its sub-tasks reset), sub-tasks
(counted "0/5" on its row), comments with attachments. Rows colour the date by meaning
(overdue red, tomorrow orange, this week purple).
- **Projects** have sections (name, description, count), seen as a list, as a **board** (a
column a section, paged) or as a **calendar** (a week strip opening into the month, an
agenda below). **Today** puts Overdue first, with Reschedule for all of them;
**Upcoming** is an agenda from today on, or a **3-day** grid of hours where a long press
picks 30 minutes. Every view has its own **Display** sheet: layout, completed tasks,
grouping, sorting, filters by date, deadline, priority and label.
- **The task sheet** shows a row for each field set and a chip for each one not, the
sub-tasks, and a comment field at its foot; **quick add** is a card above the keyboard
with chips (date, deadline, priority, reminders, project), dates recognised in what is
typed.
- **Productivity**: a daily and weekly goal, streaks, karma, days off and a vacation mode;
**Reporting**, the completed tasks by day.
- **Settings**: Account, General (home view, smart dates, reset sub-tasks, the week's
first day, how "next week" and "weekend" are read, swipe actions), Theme, Navigation
bar, Quick Add, Productivity, Reminders, Notifications (a morning overview and an evening
review), About, What's new.
## Decisions (1 Nov 2026, the developer's)
- **D1 Todo's frame is Train's (ADR 0030, D60)**: go_router with a `StatefulShellRoute`,
every tab keeping its own stack; halkyon_ui's `NavigationFrame`, a bar floating above the
foot below medium windows and a rail with Todo's icon on top from medium on, a tick on
every choice; Todo's account sheet goes. Over a frame of Todo's own, which would make the
three apps behave differently on the same window.
- **D2 Four tabs: Today, Upcoming, Inbox, Browse** (recommended, over Todoist's five and over
a bar the person arranges). Today opens. Browse holds the projects, the labels and,
later, saved filters, with the cog to Settings atop it as Train's Home and Profile carry
theirs. **Search** is an icon in every tab's app bar, not a tab, as neither Learn nor
Train gives search a tab. **Adding a task** is a floating button above the bar
(Train's `TabScreen` places it clear of the bar), on the rail's screen from medium
windows on. From expanded windows on, a task opens in a pane beside its list, as Train's
Maps opens a route's map beside the list (Material's list–detail layout), and in a
bottom sheet below. Arranging the bar may come later; Search may become a tab if it
proves to be used as one. As built (1 Nov): the pane is the frame's, beside whichever tab
is shown (`TaskPane` around the shell's tabs), 420 wide, the list keeping the rest; a
task tapped there opens in it, another replaces it, what was typed into the last
kept. Narrowed below expanded, the pane hides and tasks open in sheets again. Search is
a screen pushed onto the tab's own stack (back returns to the tab, a task found opens in
the pane where there is one): as typed, the projects whose name holds it and the tasks
every word is found in, in their title, description or labels' names, whatever the
case and the accents, open ones first by their date, done ones last; on the device
alone, which the screen says, as the instance reads no task (D5).
- **D3 Every screen washes down from the top in strawberry** (the developer's):
`TopGradient` under the app's `ScreenColor`, `PaletteColor.strawberry`, as Learn's in
cobalt and Train's in carrot; buttons and links stay cobalt.
- **D4 Settings as Learn's and Train's** (the developer's): a screen over the tabs,
grouped in `SectionCard`s. Sync's card first (as Learn's), the operator's card for
whoever operates the instance, then Todo's own groups (*Tasks*: the week's first day
from the language unless set, resetting sub-tasks; *Reminders*: the automatic reminder
before a timed task, snoozing), the account near the foot (halkyon_app's
`AccountTiles`, `SimpleLoginSection`, `AccountSecuritySection`), What's new and About
Halkyon Todo last, the version small below them. Groups come as their settings are
built; none is shown empty. **Tasks and Reminders** (as built, 1 Nov, the developer's
"as much as possible without my input"), kept in the person's sealed settings row
(`todo_settings`, D17), so on every device: *Start the week on* (the language's
unless set; the week strip, the date picker's next week, the filters' this week and
the repeat picker follow it, `firstWeekdayOf`); *Reset sub-tasks* (on at first, as
Todoist's; off, a recurring task done leaves its sub-tasks done); *Automatic reminder*
(none at first, so that nothing reminds a person who didn't ask; at the time, or 5 to
60 minutes before): a task gaining a time, added so or given one, without reminders of
its own gets it, written into the task where the person sees and may remove it, and a
task that already had a time keeps what the person left it (`TaskRepository`, every
way of giving a time at once); *When snoozed* (15 minutes at first; 5 to 60): each
reminder's notification has a Snooze action, the reminder coming back that much later
under an ID of its own, scheduled from what the notification carries (`snooze.dart`),
so on Android with the app closed too (`snoozeInBackground`, the plugin's
`ActionBroadcastReceiver` declared), and on Linux by the running app, as its reminders;
the plan of the person's reminders leaves the snoozed ones alone. On iOS, macOS and
Windows since 1 Nov 2026 (the developer asked for it): Apple's systems take a
notification's actions from categories registered as the app starts, so there is one
for each length a snooze can be (`snoozeCategory`, `snoozeCategories`), its action
in the language the app starts in (the device's, as `resolveAppLocale` picks it; a
language changed meanwhile shows from the next start) and not to the foreground, so
that on iOS the plugin's isolate snoozes with the app closed, as on Android
(`AppDelegate` registers the plugins for it). Windows hands back only the button's
arguments, so the button carries the notification's ID and payload
(`windowsSnoozeArguments`, read by `snoozeOf` as the others' action). macOS and
Windows launch the app for an action while it is closed: the gateway snoozes what
launched it as it starts. Built and tested on Linux alone: none of the three was
tried on a device yet.
- **D5 Todo's rows are sealed, always, as Train's (ADR 0030, D7, D15)**, synced with
`serverpod_offline_sync`: every payload ADR 0017's sealed content under the person's
data key (`content_key`, halkyon_app's `PayloadEnvelope`), nothing readable in a column,
not a title nor a date. Recommended over readable rows (ADR 0002's default), which would
let the server send reminders by email and a morning overview, and would make shared
projects simpler: what a person plans to do is theirs, the server needs to read none of
it to sync it, and Todo's rows can't be unsealed later without the person's devices. So
**reminders are scheduled on the device**, as local notifications on each device that
holds the account's keys; there is no email reminder and no overview the server writes.
A project shared later is sealed under a key its members share, which is that step's to
decide.
As built (1 Nov, the developer's choice of `flutter_local_notifications` over
`awesome_notifications` and platform code of Halkyon's own): a task's reminders are
minutes before the time on its date, in its sealed payload, so every device of the
person's reminds; a task with a day alone has none until the automatic reminder (D4)
is a setting. Each device plans its reminders whenever the tasks change, comes to the
front, or every six hours: the next 48 within 30 days (iOS keeps 64 pending), a
recurring task's next time alone, each under an ID hashed from its task and offset so
that planning again replaces it. They are scheduled with the system on Android, iOS,
macOS and Windows, so they come while the app is closed, at their instant: a device
moved to another zone has them moved once the app is opened. Linux schedules none, so
there the running app shows them. Android brings them at the minute only where the
person allows exact alarms (`SCHEDULE_EXACT_ALARM`, off by default since Android 14),
else within a few minutes; the app doesn't send the person to that setting. The
permission to notify is asked when a task first gets a reminder; a notification's
text, the task's title, is hidden on a locked Android screen. Signing out, or
switching person, cancels the person's reminders on the device. Tapping one opens the
app, not yet its task.
- **D6 Four tables: `project`, `section`, `task`, `label`** (recommended, as Train keeps a
workout and its series apart, D14): one row each, changed when edited, the last writer
winning, every payload JSON with a `schemaVersion`, UUID v7 ids. Inbox is a project the
app makes once per person, under a row ID keyed by the account (Train's D57,
`keyedRowId`), not marked in its payload (as built, 1 Nov): every device makes the
same row, so two making it offline write one, and the instance can tell neither
which row is the Inbox nor whose it is; a payload's mark would let a moved row
claim to be it. A **sub-task** is a task naming its
parent. A task's payload: title, description, its project and section, its parent, its
place among its siblings, a **date** (a day, or a day and a time with an optional
duration, the time as the person's wall clock with its time zone), a **deadline** (a
day), a priority (1–4), its labels' ids, a recurrence (D7), its reminders, when it was
completed. Completing a recurring task moves its date on and writes the occurrence done
into its payload's history, so Reporting can count it. Comments and attachments are a
later step (attachments need sealed files, which no Halkyon app has yet).
- **D7 A recurrence is a structured rule, set through a picker** (recommended, over
parsing what is typed): every n days, weeks (on which weekdays), months (on which day,
or the nth weekday), years, from the date or from completion, until a day or for n
times. Dates and tokens typed into quick add ("tomorrow 6am", "every Monday", `#project`,
`p1`), in English and German, are a later step, built on the same rule.
As built (1 Nov): the rule is part of the task's date (`Repeat` in `TaskDate`, its
payload's `date.repeat`), so a task without a date doesn't recur and clearing the date
ends it; schema 1 holds it, as no task was written before. Done, a task counted from its
date moves to the rule's first day after both its date and today (an overdue one isn't
moved onto a day already past), counted from completion it moves the rule's units on
from today, on no particular weekday; its deadline moves as far, its time is kept, its
done sub-tasks open again, and when it was done is added to its `completions`. Undo
writes back every task that changed. The picker offers the rules that fit its day
(every day, every weekday, its weekday, its day or nth weekday of the month, its day of
the year) and a rule of the person's own; weeks start on Monday for every n weeks, until
the week's first day is a setting (D4).
- **D8 The first version** (recommended): Inbox and projects with sections, shown as a
list; sub-tasks; Today (Overdue first, rescheduling them together, then today's); Upcoming
as an agenda with the week strip opening into the month; the task sheet (rows for the
fields set, chips for the others, the sub-tasks); quick add; labels; completed tasks shown
or hidden per view; reminders on the device. **Later**, each put to the developer when
its turn comes: the board and calendar layouts, Upcoming's 3-day grid, each view's
Display sheet (grouping, sorting, filters), saved filters, Reporting and goals (karma
not before the developer asks for it), location reminders, comments and attachments,
typed dates, sharing.
- **D9 Colours that mean something come from halkyon_ui's palette** (recommended): a
project's colour is picked from `PaletteColor`, with its localised name, as Learn's
colours are; priorities and dates take palette colours chosen not to blur against the
strawberry wash, so overdue and P1 are **cerise**, not strawberry, P2 carrot, P3
cobalt, P4 none; tomorrow and this week as Todoist's meanings go, in palette colours.
Each is checked against the wash in light and dark before it is kept.
- **D10 The icon stays as ADR 0031 drew it** (cobalt through pacific into fern), though
the wash is strawberry: Learn's icon ends in strawberry and washes in cobalt, so an
app's wash and its icon already differ, and Todo's and Meditate's icons are kept
related (ADR 0031, D1). The agent made no recommendation; this is the default the
decision of 1 Nov left in place, to be asked again if the screens look wrong beside it.
## Decisions after trying it (1 Nov 2026, the developer's)
The developer used the first build beside Todoist's and asked for these; where an
alternative was put to them, the choice is theirs.
- **D11 Sheets are dialogs on the desktop, at every width** (over the window's size
alone, and over Todo alone): halkyon_ui's `showAdaptiveSheet` shows a centred dialog
(Material's term) on Linux, macOS and Windows however narrow the window, and on phones
and tablets a bottom sheet below expanded windows as before, so Learn and Train follow.
A bottom sheet is a touch surface; rising from the foot of a desktop window it reads
as a phone's screen. In a narrow window the dialog keeps a smaller margin. A sheet
that can't go through it asks `sheetShowsAsDialog` and follows the same rule: Learn's
conversation is a dialog as tall as the window there, nothing to drag. Train's route
import still opens its bottom sheet everywhere, until Train 0.1.1 is out.
- **D12 The tabs are Inbox, Today, Upcoming, Browse; Today still opens** (amends D2's
order, not what opens).
- **D13 A task's sheet** names where the task is on top (its project, an icon once tasks
are above it, then each task above it; the project goes to its list, a task opens in
the sheet's place), as Learn's path through a studyset, and has a ⋮ menu in place of
its close button (`SheetScaffold`'s `heading` and `closeButton`): when it was added
(from its UUID v7, so the payload needn't say it), Duplicate (with its sub-tasks, right
after it), hiding or showing its done sub-tasks (`hideDoneSubTasks` in its payload,
so every device hides them, schema 1 as nothing is released), Close in the pane, and
Delete after asking. Copy link, View activity, Email to the task, Print and Open in a
new window wait for what they need (deep links, an activity log; mailing in is ruled
out by D5). The description's icon stays by its first line.
- **D14 Rows and menus**: a row shows its project and section ("Inbox / Errands" with the
project's icon) at the end of the line of its date, not beside the whole row; every
menu entry has its icon, a destructive one in the error colour (`MenuEntry`). A
section's heading stays atop the list while its tasks scroll beneath it, until the
next pushes it away (`PinnedHeaderSliver`). Upcoming's month unfolds in place beneath
its strip, from its name or its handle dragged down, the arrows then going by month,
the handle dragged up folding it; a button beside the month goes back to today once
another week shows. Tasks are chosen to act on together, as Todoist's: a row swiped
right or held, or *Select tasks* in Today's, Upcoming's and a project's ⋮, a tap then
choosing or letting go (a row held is dragged since D16); the app bar counts them, with an arrow (or back) ending it,
and a bar takes the navigation bar's place: date, move, labels (those all have
ticked; a tick added to each, one unticked taken off each), priority, and ⋮ with
complete (undone together), duplicate, deadline and delete (asked first). *Assign*
waits for shared projects and *View activity* for a history, neither of which Todo
has. A row swiped left asks for its date. Neither swipe removes the row; a sheet's
sub-tasks and search's results aren't chosen (`TaskSelection`, one for the frame).
- **D15 Typed tokens in quick add** (amends D7's later step; the developer's, 1 Nov, after
asking whether the keyboard's language could choose): `#project`, `@label`,
`{deadline}`, `!!1` and `p1` to `p4`, `!30m`, dates and times (`9am`, `Mon 18:00`,
`ev Tue 7pm`) and recurrences (`every day`, `every 2 days`, `every second day`), each
highlighted as it is recognised and undone with a tap. The words are data, a table per
language (weekdays, months, *tomorrow*, *every*, *next*, times), read in the app's
language and English, so a new app language adds a table, not a parser. Not the
keyboard's language: Flutter tells nothing of it, Android doesn't reliably, and it
changes as the person switches keyboards while typing. Symbols hold in every language.
As built (`parseQuickAdd`, `QuickAddWords.en` and `.de`): a `#` naming no project
stays text, an `@` naming no label makes one as the task is added; only the first of
each kind counts, the rest stays in the title; a time alone is its next to come
(tomorrow once it has passed today), a weekday alone the coming one (today too),
`next` one after today; a month needs its day (`may` alone is a word), a number alone
is none; a recurrence starts on its first day from today (`every weekday` on a Sunday:
Monday). `!30m` makes a task without a time due 30 minutes from now, reminded then,
and one with a time reminded 30 minutes before it; with a day but no time it is
text. German's weekdays only by their whole names, its short forms (`so`, `do`,
`die`, `mit`) being everyday words. Quick add shows its title alone at first, its
chips once typing starts, a chip picked taking its token out of the title, and the
description and the deadline behind its + (the description also from the down
arrow). As `#` or `@` is typed, up to five projects or labels whose name, or a word
of it, starts with what follows are offered below the title, one tapped completing
it; none for a symbol inside a word (`C#`), nor once a name is typed whole.
- **D16 Dragging** (the developer's, 1 Nov, as Todoist's): a row of a project's list
(the Inbox's too), held, is dragged into another place, a line showing where: the
upper half of a row puts it before that row, the lower half after it, and the lower
half reached 48 px to the right of where the drag began makes it that row's last
sub-task; a section's heading takes it first in the section, or last in the group
before from its upper half. It goes with its sub-tasks, and *Order changed* offers
to undo it (`TaskRepository.place`, never under itself or its own sub-task). The
list scrolls at its edges. Holding a row therefore drags it everywhere it can be
dragged and no longer selects it (D14): a swipe to the right does. The add button
is dragged straight away, without a hold, its place meanwhile a larger × where
dropping it gives up: dropped between a project's rows, quick add adds there (each
next task below the one before); on a day in Upcoming, for that day; on Today, for
today. Upcoming's rows, overdue ones too, held and dropped on another day move there,
their time, duration and recurrence kept, *Moved to …* offering to undo it; their
order within a day stays their times' and priorities', and Today's rows aren't
dragged.
- **D17 Each project's Display, synced** (the developer's, 1 Nov, as Todoist's; D8's
later step): a project's screen, the Inbox's too, has a Display button beside its ⋮, a
dot on it while anything differs from how a project shows at first. Its sheet sets
completed tasks (moved out of the ⋮), grouping (none, which keeps the sections; date,
deadline, added date, priority, label), sorting (manual, name, date, deadline, added
date, priority; a tie, or a task without what is sorted by, last and by hand), and
filters by date and deadline (all, today, this week from the language's first
weekday, next 7 days, this month, next 30 days, none) and by priority and label (any
ticked). Each change is written at once; Reset all puts the project back. Grouped,
the groups stand in the sections' place under headings that stay atop the list
(overdue first, then each day, none last; P1 first; a task under each of its labels);
rows are dragged (D16) only while sorted by hand and grouped by sections. The choices
are kept **in the project's sealed row**, so every device of the person's shows the
project alike (`ProjectView`, the payload's `view`, written only once set; values a
version doesn't know read as at first). The schema stays 1: an older version reads
such a project, and renaming it there drops its Display, which beats a newer schema
it couldn't read at all. Upcoming's Display (Today has none, as in Todoist) is in
a sealed person-settings row (`todo_settings`, below), which is also to hold D4's
settings. **The Board** (as built, 1 Nov), the Display's Layout (List,
Board): a column for the tasks in no section, *(No section)*, always first, then one
a section with its menu, each heading counting its open tasks, each task a card,
each column ending with Add task, a last column adding a section; grouped otherwise,
a column a group, nothing dragged. Below a medium width (the board's own, not the
window's, so the pane beside the tabs counts) one column at a time, paged, the next
peeking in, settling on the nearest when let go, dots below saying where; from it on,
columns 320 wide side by side, scrolled across. Cards are dragged as rows (D16),
within and between columns, every scrollable they are in scrolling at its edges; a
column takes a card or the add button dropped below its cards, last in it. **The
Calendar** (as built, 1 Nov, the developer's choices): Upcoming's week strip and agenda,
shared (`DayAgenda`), over the project's tasks its Display keeps, sub-tasks too, a
day's tasks by time or, sorted otherwise, in that order; *No date* beside the month
opens those without one. Grouping is left out of its Display, and *Future
occurrences* (off at first, kept with the rest) shows each repeating task not done on
the days it will come back, the first as if done today (`Repeat.next`), until its
recurrence ends, 400 at most: muted rows with the recurrence's mark in place of the
check, opening the task, neither ticked off nor dragged (`calendarOf`). **The month**
(built 1 Nov 2026, the developer asked for it): from 720 pixels of the Calendar's own
width on (seven days of a little over a hundred each, a desktop's window beside its
rail; below it the agenda, as on a phone), the month's weeks from the week's first
day (`monthWeeks`), the days of the months beside it muted, each day with as many of
its tasks as fit, a line each (its time, its title, its mark in its priority's
colour), and the rest counted (*3 more*), opening all of the day's in a sheet. Past
days show their tasks too, overdue or done (`monthOf`; the agenda counts those as
overdue, a month has no such place), later times of repeating ones from today on.
The month's name, back to today, *No date*, the month before and after above it. A
task held and dropped on another day moves there as on Upcoming's (`DayDropArea`,
`moveTask`), a day's free space tapped opens quick add on it in the project (where
the agenda has the add button, which can still be dropped on a day), a line tapped
opens its task. The 3-day grid stays Upcoming's. **Upcoming's Display** (as built, 1 Nov, the developer's
"as much as possible without my input"): the same sheet over another target
(`DisplayTarget`: `projectDisplay`, `upcomingDisplay`), its layout Agenda or 3-day,
done tasks, future occurrences, sorting (Smart, by time then priority, as Todoist
calls it; name, deadline, added date, priority) and the filters by deadline, priority
and label; no grouping nor date filter, Upcoming being days already. In the 3-day
grid, Colouring: by priority, by the first label with a colour, or by project. It is
kept in **`todo_settings`**, a synced table of the same shape as `label` (one row a
person, under an ID keyed by the account as the Inbox's, `todo/settings`, sealed for
its table and row; `SettingsRepository`), the migration `settings` creating the table,
its index on `spaceId` and its foreign key to the space, cascading on delete, so a
person's deletion takes it with their space. None kept, or one that doesn't open, reads
as at first. **The 3-day grid** (`ThreeDayGrid`): today and the two days after (never
before today), stepped or swiped three days at a time; tasks without a time *All day*
above, timed ones as blocks as long as they last (half an hour at least, to midnight
at most), overlapping ones side by side in lanes (`layOutDay`); a long press on a free
half hour marks it and opens quick add there for half an hour; a block opens its task;
*Overdue* opens a sheet of the overdue tasks with Reschedule. A block held and
dragged (built 1 Nov 2026) moves its task to another of the three days and another
time, its top where it is dropped snapping to a quarter hour (the long press's half
hour is too coarse to move a block a little), the time it would take shown under the
pointer, its duration, recurrence and reminders kept; dropped on a day's *All day*,
it loses its time and duration. Either can be undone for a moment, as a row dropped
on Upcoming's day (`moveTask`, shared with `DayDropArea`). A later time of a
repeating task doesn't move: only the task itself, at its next time, does. Dragging
past the three days shown waits until someone misses it.
## Steps
1. **The frame**: go_router and `NavigationFrame` with the four tabs (empty screens that
say what comes), the strawberry wash, the cog, Settings as D4 with what exists today
(the account, What's new, About, the version); the account sheet removed.
2. **The first synced model**: `content_key` and `project` (Inbox made once), its gateway
and repository on Learn's and Train's pattern, the server's access rules each with a
test asserting denial; walked through with the developer as the first of Todo's.
3. **Tasks**: `section`, `task`, `label`; Inbox and a project's list; the task sheet; quick
add.
4. **Today and Upcoming**, recurrence (D7), reminders on the device.
5. Todo's first release (0.1.0), its *What's new* drafted by `tool/release.dart`.
Decided 2 Nov 2026 (the developer's, the first two the agent's recommendations):
- **Deployed beside Learn's server, as Train is** (ADR 0030, D46):
`deploy/compose.todo.yaml` at todo.halkyon.eu, a member of Learn's register, its
database `halkyon_todo` under a role of its own, taken in by `redeploy.sh` and
`backup.sh` where `.env` switches it on, both now going over every app server so
switched on (`train`, `todo`). Each compose file names its server to the register
in a variable of its own, `INSTANCE_MEMBER_TRAIN`, `INSTANCE_MEMBER_TODO`, which
halkyon_base adds to `INSTANCE_MEMBERS`, each address once (agent's choice): a
second compose file's default of `INSTANCE_MEMBERS` would have replaced Train's,
and Learn's register would no longer have told Train of an account gone.
*Alternatives:* the release config alone, Todo's instance later.
- **A key of its own** (as Train's D58): a PKCS #12 keystore at
`~/.local/share/halkyon-todo/android-release.p12`, alias `halkyon-todo`, in Proton
Pass as *Halkyon Todo – Android release keystore*, pinned in
`tool/release/todo-android-certificate.sha256` once the developer has made it;
until then the release tool releases nothing. *Alternative:* Learn's key, which
would let one app's build pass for the other's update.
- **The APK, in the F-Droid repository, and a snap** (the developer's, over the APK
and F-Droid alone): the listing `app.halkyon.todo.yml` (category *Time*,
`NonFreeNet` for the server's SSPL, as Learn's says), the snap `halkyon-todo` to
the Store's edge channel, as Learn's, from `apps/todo/snap/`. snapcraft builds one
recipe a folder, `snap/snapcraft.yaml`, and the root's 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` so that its build machine and Flutter
part stay from one release to the next, Todo's folder in place of the clone's
`snap/` but for `snap/local/`, which the recipes share (agent's choice; over a
worktree, whose `.git` file points to a folder LXD doesn't see, and over moving
Learn's recipe, which its releases and docs name). The other platforms come
later.
- Android's backup and transfer to another phone are off, as Train's D53 (agent's
choice): the reminders scheduled keep their tasks' titles unsealed on the phone.
## Consequences
- No email reminder, morning overview or evening review: the server can't read what is
due. A device without the account's keys neither reads nor writes Todo's rows, as in
Train.
- Todo's server holds no logic over tasks; what Today, Upcoming and Reporting show is
worked out on the device, over every task the person has. That is fine for thousands of
tasks; a person with far more would need the device to keep an index of its own.
- Sharing a project is harder than with readable rows (a key per shared project, its
members' keys wrapping it); D5 accepts that for now.