Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
709 lines
55 KiB
Markdown
709 lines
55 KiB
Markdown
# 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 (both amended by
|
||
D31: exact alarms through `USE_EXACT_ALARM`, the permission asked wherever a reminder
|
||
is planned); 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 (come with D32), 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.
|
||
Asked again on 2 Nov: changed by ADR 0031, D5, the icon now fading from cobalt
|
||
through violet into strawberry, its backdrop.
|
||
|
||
## 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 (but for labels and, since D18,
|
||
reminders); 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 (its Plan's are since D30).
|
||
- **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 had none until D30) 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.
|
||
- **D18 Quick add as Todoist's newsletter writes it** (the developer's, 3 Nov 2026, the
|
||
four recommended, after comparing a newsletter's examples): *Check work emails every
|
||
weekday at 9am !30m*, *Prepare document on Monday at 10am for 2h*, *Stop watching that
|
||
viral cat video !1pm !2pm !3pm !4pm*, *File my tax return every year on 1st March*.
|
||
- **`on` before a day** is taken with it (German `am`, `on` and `am` being words of the
|
||
tables, `QuickAddWords.on`), as `at` (`um`) before a time is; before anything that is
|
||
no day it stays text (*Work on report*, *Treffen am Bahnhof*). `am` after a number is
|
||
still the morning (*9 am*), read first as a time.
|
||
- **Durations**: `for` (`für`) and an amount with its unit, `for 2h`, `for 30m`, `for
|
||
1h30m`, `for 90 min`, `for 2 hours`, `für 2 Std`, `für 30 Min`, `für 1,5 Std.`, a
|
||
decimal point or comma either way, a day at most, into `TaskDate.duration`. The
|
||
units are the tables' (`durationUnits`), read by a pattern over the text, as a word
|
||
splits at a comma. **Only with a time**: a duration lasts from one, so without one
|
||
(*Read for 2h*, *Read tomorrow for 2h*) it stays text, as `!30m` on a day without a
|
||
time does (recommended, over giving the task a time to hold it, which would invent
|
||
one). The date sheet sets it once there is a time (*Duration*: none, 15, 30, 45
|
||
minutes, 1, 1½, 2, 3, 4 hours, as Todoist's time sheet offers), a time cleared taking
|
||
it with it; a row and the sheet show the span (*Tomorrow 14:00–15:30*) and the 3-day
|
||
grid a block as long, a block dragged keeping it (D17).
|
||
- **Every `!30m` counts**, each once, sorted (`[30, 60]`), as do the `@labels`; the
|
||
other kinds still count once. With a time, each is so long before it; without a date,
|
||
the task is due at the latest and each reminds that long from now (*Leave !30m !1h*:
|
||
due in an hour, reminded in 30 minutes and then); on a day without a time they stay
|
||
text. The title's highlighting marks each token, whatever its kind.
|
||
- **Reminders at a time of their own**: `!1pm`, `!13:00`, `!tomorrow 9am`, `!Monday
|
||
8:30`, `!morgen um 9 Uhr`, as many as typed, into a task's `reminderTimes`
|
||
(`ReminderTime`: a day and minutes after midnight, the wall clock without a zone,
|
||
as `TaskDate`'s, so one comes at that hour wherever the device is). **A time without
|
||
a day** is on the task's day where it has one (a recurrence's first), else today, or
|
||
tomorrow where that time has passed, as a time alone is (D15). Each is a moment of
|
||
its own: it neither needs a date nor follows one, a recurring task's coming once,
|
||
not every time. A `!` before no time (*!tomorrow*, *!9*) stays text. Planned with the
|
||
rest (`planReminders`, an ID of their own each), the notification saying *Due at
|
||
14:00* for a task with a time, *Reminder* for one without; the task's sheet now
|
||
always has its reminders' row, the reminders before its time only where it has
|
||
one, and those at a time of their own each with its ✕ and *Remind at a time* (a day,
|
||
then a time), the permission asked as a first one is chosen.
|
||
- **The payload's schema stays 1** (as D17's view): `reminderTimes`, as `[{"day":
|
||
"2026-11-03", "time": 780}]`, left out where there are none, so older payloads read
|
||
with none. And from now on **a task's payload keeps what this version doesn't know**
|
||
(`TaskContent.unknown`, the fields at its top that it doesn't read, written back
|
||
beneath the ones it does, which win): Todo isn't released yet, so every version out
|
||
keeps a newer one's fields as it changes a task, where Learn's and Train's payloads
|
||
drop them. Recommended over bumping the schema for each field, which would show a
|
||
task locked on every device not yet updated. A field a newer version adds within the
|
||
date, or one whose meaning hangs on fields an older one changes, isn't kept so; such
|
||
a change still bumps the schema. No server model changes: the payload is sealed.
|
||
|
||
## Decisions after trying 0.1.0 on a phone (3 Nov 2026, the developer's)
|
||
|
||
The developer's review of 3 Nov 2026, with Todoist's screens beside Todo's; the project
|
||
label at a row's end the developer's answer (right-aligned, as Todoist's, over left after
|
||
the date), the rest the developer's asks, carried out as described.
|
||
|
||
- **D19 Today and its tabs' icons**: Today's tab shows today's day of the month inside a
|
||
blank calendar, as Lucide's `calendar-1` holds a 1, drawn again at midnight
|
||
(`TodayIcon`); Browse is Lucide's `menu`, Display's button `square-menu`. **Today has at
|
||
most two groups**: what is overdue, folding under its heading (its chevron, beside
|
||
Reschedule; the fold not kept), and today's, headed by the day alone. **A day's heading**
|
||
reads as Todoist's, "Tuesday, 3 Nov" ("Dienstag, 3. Nov.", the pattern a text of each
|
||
language, `dayHeadingPattern`); Upcoming's days keep "· Tomorrow" where it helps, Today's
|
||
has no "· Today". Upcoming's way back to today shows whenever another day than today is
|
||
picked, not only off this week.
|
||
- **D20 Rows in rounded boxes, a list's rows a group** (as Todoist's): each row its own
|
||
surface (`TaskTile`'s box, `TaskRowPosition`, `taskGroup`), rounded at the group's ends
|
||
and slightly at the joins, so a chosen row's colour stays inside it as the list scrolls
|
||
under a pinned heading (it showed under Upcoming's week strip, as the row's tile was
|
||
painted on the screen's surface below the list). Its project and section end the line
|
||
of its date at the row's right edge.
|
||
- **D21 Lists pulled down sync now**, as Learn's: Today, Upcoming's agenda, the Inbox and
|
||
every project (`TaskRefresh`, halkyon_ui's `AppRefreshIndicator` over halkyon_app's
|
||
`syncAndWait`), an empty list too (`PullRoom`). A board's columns and the 3-day grid
|
||
don't, as they scroll sideways or both ways. **A project's screens wash in its own
|
||
colour**, as a studyset's in Learn; one without keeps Todo's strawberry.
|
||
- **D22 Display as Todoist's**: each layout a phone's outline with its picture, its name
|
||
below on one line, a radio button marking the one in use; the rest in rounded groups
|
||
under plain headings. "Add section" in place of "New section".
|
||
- **D23 Quick add a small bar docked above the keyboard** (amends D8, D15): the title,
|
||
then one row of boxed buttons (+ for the description and deadline, the date, the
|
||
priority, the labels, the project), each its icon alone until something is set, and a
|
||
send button, the list in view behind a lighter barrier. Its buttons show from the
|
||
start (D15 had its chips come with the first letter), as Todoist's bar has them. Where
|
||
sheets are dialogs, the same bar in a dialog.
|
||
- **D24 A task's sheet partway up, its parts in rounded boxes** (amends D13): on a phone
|
||
it opens as high as its content, at most 65 % of the window, and is dragged up to the
|
||
whole window or down to close (`DraggableScrollableSheet`); the title, description,
|
||
date and reminders each a box, the priority, labels, deadline and project a row of
|
||
chips (named while unset), the sub-tasks a box ending in Add sub-task. No principle of
|
||
the project's argues against Todoist's layout: grouped rounded boxes are what the
|
||
review asks for across the apps. Todo keeps no comments, so there is no comment field.
|
||
- **D25 Explanations one tap away**: what a setting does stands behind halkyon_ui's
|
||
`HelpButton` beside its name (`TitleWithHelp`), not always below it, in Todo and
|
||
Meditate; what is the person's own (a value) stays in view, as do an empty list's
|
||
message and quick add's hint while words are highlighted.
|
||
|
||
## Decisions after trying 0.2.0 (4 Nov 2026, the developer's)
|
||
|
||
The developer's review of 4 Nov 2026, again beside Todoist's screens.
|
||
|
||
- **D26 What the review fixed and added**: a time or duration picked in the date sheet
|
||
counts however the sheet closes (it had counted only with a day tapped), on the task's
|
||
day, or today where it had none, as Todoist's; editing a project keeps its Display.
|
||
**Today counts its tasks** under its title, overdue and today's together ("14 tasks",
|
||
as Todoist's), not while tasks are chosen. **A row's check stands by its title's first
|
||
line**, not in the middle of the row: a `ListTile` centres its leading or tops it at
|
||
its padding, never at the title's line, so the check and the texts share its title,
|
||
the texts lowered by half of what the 48 dp tap target is taller than a line.
|
||
**A sub-task opens in its task's sheet**, as the sheet's path did, not in a sheet over
|
||
it; the sheet keeps what it showed, the system's back returning to the last (agent's
|
||
choice: a `BackButtonListener`, over a `PopScope`, which would also have held back a
|
||
tap beside the sheet, a swipe and Escape, which close it whole; a picker over the
|
||
sheet is closed by back first).
|
||
- **D27 Listed in F-Droid as *Task* and *Calendar & Agenda*** (the developer's, both,
|
||
over *Time*, which F-Droid's categories give to clocks and timers): no other app of
|
||
the repository used *Time*, so the repository no longer lists it.
|
||
- **D28 A project's editor as Todoist's Add project, with sub-projects and
|
||
favourites** (the developer's, over the dialog of a name and a colour): a screen of its
|
||
own on a phone (*Add project*, *Edit project*, ← and ✓ in its app bar), the same in a
|
||
dialog where sheets are dialogs; the name counted to 120, a description, then rounded
|
||
rows: the colour (its palette name, opening the picker), the parent project (*No
|
||
parent* or a project, from a sheet listing them indented; never the project itself
|
||
nor any project below it, `parentChoices`, so none becomes its own ancestor), a
|
||
Favourite switch; then the Layout, Display's pictures (`LayoutPicker`). All of it is
|
||
**in the project's sealed payload** (`description`, `parentId`, `favorite`,
|
||
`defaultView`), no server model changed, no migration, the schema still 1; the payload
|
||
now keeps the fields it doesn't know, as a task's (D18), so a later version's survive
|
||
an edit here (a 0.2.0 device editing a project still drops these). **The default
|
||
Display**: a new project starts with the layout picked, and Display's *Reset all*
|
||
returns to it (`defaultView`, the dot on Display marking a Display that differs from
|
||
it); the editor sets its layout, and a layout picked anew there shows at once.
|
||
Projects made before keep List as their default. **Browse** lists each project's
|
||
sub-projects indented under it, folding under its chevron (the fold not kept, agent's
|
||
choice: a choice of the device, and quick to make again), and the favourite projects
|
||
in a *Favourites* group above the projects. A project whose parent is gone or doesn't
|
||
open here, or projects whose parents run in a circle (two devices each moving one
|
||
under the other), stand as projects of their own, none lost (`projectTree`). **Deleting
|
||
a project deletes its sub-projects**, with their sections and tasks, as Todoist does,
|
||
the confirmation naming them first (`TodoPerson.deleteProject`). *Alternatives:* the
|
||
sub-projects moved up a level (kept, but surprising after a confirmation that named
|
||
only the parent); a depth limit as Todoist's (four levels), left until deep trees turn
|
||
out to be a problem.
|
||
- **D29 Quick add keeps a word as text where it stands, and from Backspace too**
|
||
(amends D15; the developer's, 4 Nov 2026). A word tapped had been kept as text by its
|
||
text, so in "Watch The Daily Show Daily" tapping the first *Daily* also stopped the
|
||
second one making the task repeat. A word kept is now **that occurrence**, a range of
|
||
the title (`QuickAddRange`, `parseQuickAdd(ignored:)`; a token overlapping one stays
|
||
text), which follows the edits: the edit found from the common start and end of the
|
||
text before and after, ranges after it moved, one it touches dropped as its word has
|
||
changed (`shiftKept`); typing right before or after a kept word leaves it kept.
|
||
**Backspace right after a highlighted word keeps it as text**, as Todoist's: the
|
||
character isn't deleted, the next Backspace deletes it (a quick add input formatter,
|
||
so the keyboard's own text is put back too); the hint under the title says so. And
|
||
**a word is highlighted while the keyboard still composes it**: Android's keyboards
|
||
(Gboard with suggestions, Japanese ones) compose a word until a space, and the title
|
||
had shown the plain composing text until then; the highlight is drawn with the
|
||
composing underline over it. *Alternatives:* keeping by text but only the word
|
||
tapped and those before it (still surprising, and lost on reordering); a long press
|
||
instead of Backspace (undiscoverable, and the tap already does it).
|
||
**The last button dictates while the title is empty**, as Todoist's and as Learn's
|
||
assistant field (ADR 0021, D69): the microphone, filled as the send button is,
|
||
dictating into the title where its cursor stands and listening again after each pause
|
||
(halkyon_app's `Dictation` on the device's recogniser, `TodoDependencies.speech`), what
|
||
is said read as if typed; a stop button in the container colour while it listens;
|
||
the send button once there is text, and alone where the device recognises no speech
|
||
(Linux). A phone without a recogniser (GrapheneOS without Google's services) is told
|
||
how to get one (halkyon_app's `speechNoRecognizer`). Android declares the microphone
|
||
and the recognition service it queries, iOS and macOS the usage texts and macOS the
|
||
audio input entitlement, as Learn's. *Alternatives:* a microphone beside the send
|
||
button always (one more button in a bar already full, and Todoist's swaps them).
|
||
- **D30 Today planned on a calendar, as Todoist's desktop Today** (the developer's, 4
|
||
Nov 2026, from their screenshots of Todoist's desktop app): Today gets a Display
|
||
(amends D17's "Today has none"), its **Layout** alone, *List* or *Calendar*, with
|
||
Display's pictures (`LayoutPicker`; no Board, a day having no sections), kept in the
|
||
person's sealed settings row as Upcoming's (`TodayView` in `todo_settings`, the
|
||
payload's `today`, schema still 1: an older version reads it, and writing its settings
|
||
there drops it, as D17 accepted for a project's view). *Calendar* is **one day of
|
||
hours**, Upcoming's 3-day grid generalised to any number of days (`HourGrid`, D17's
|
||
`ThreeDayGrid` renamed, `days: 1`): today's tasks without a time *All day* on top,
|
||
timed ones as blocks as long as they last, overlapping ones side by side, a **line at
|
||
the time it is now** (its time in cerise in the hours' column, drawn again each
|
||
minute; Upcoming's grid has it too), a block dragged to another hour moving there
|
||
(snapping to a quarter hour), onto *All day* losing its time, a long press adding a
|
||
task, as D17. With one day there are no day names, arrows nor *Overdue* button: what
|
||
is overdue waits in the Plan. The day scrolls to the hour before now at first.
|
||
**The Plan** (`PlanPanel`): *Plan: 3* in Today's app bar, in the Calendar layout
|
||
alone, shows or hides it, counting what it holds. Its groups, each folding under its
|
||
heading with its count (the fold not kept): *Overdue* with Reschedule (Today's, in
|
||
cerise as Todoist's), *To plan* (today's tasks without a time; the developer's
|
||
working name, Todoist naming no such group), *Time-blocked* (today's with a time,
|
||
Todoist's word; German *Zu planen* and *Eingeplant*); done tasks and later times of
|
||
repeating ones are never in it, nor in the day, so neither is planned. *All
|
||
projects* opens *Filter by project*, a field searching the projects' names and a box
|
||
to tick for each (the Inbox first, a sub-project with its parents' path, "Life /
|
||
Home", D28), several at once, applied as ticked; it narrows the Plan alone, not the
|
||
day, and is kept while the app runs, not synced (agent's choice: a filter for one
|
||
planning session). A row held and dropped on an hour is moved to **today at that
|
||
time** (its top where the pointer is, snapped as a block's), its duration kept or,
|
||
without one, half an hour drawn; a block dropped on the Plan loses its time and stays
|
||
today; both through `moveTask`, *Moved to …* offering to undo it; a Plan's row dropped
|
||
back on the Plan changes nothing. **Where it stands**: from a medium width of
|
||
Today's own on (the window-size classes over the body's width, as the board's D17,
|
||
so the task pane beside the tabs counts), a panel 340 wide beside the day, shown or
|
||
hidden as the person last left it, remembered with the settings (`TodayView.plan`,
|
||
open at first); **on a phone, a sheet over the day's foot** (the developer's choice,
|
||
over the Plan on wide windows alone and over a strip of tasks above the day), half
|
||
the body high above the floating bar, closed until asked and while the app runs;
|
||
a row held there **lowers the sheet to its heading**, the day free behind it, and it
|
||
rises again once the row is dropped. The sheet is part of Today's screen, not a
|
||
modal route (agent's choice): a modal sheet's barrier would take the drop meant for
|
||
the day behind it, and closing the route under a drag ends the row's draggable.
|
||
The add button hides while the phone's sheet is open. *Alternatives:* the Plan on
|
||
wide windows alone (a phone couldn't plan by dragging), a strip above the day
|
||
(too little room for three groups), the project filter synced (a planning
|
||
session's narrowing would follow the person to their other devices).
|
||
- **D31 Reminders at the minute, and the permission to notify asked wherever one is
|
||
planned** (the developer's, 4 Nov 2026, after Todo's reminders never showed on a
|
||
phone, GrapheneOS on Android 14, installed afresh from F-Droid). Two faults: the
|
||
permission to notify (Android 13's `POST_NOTIFICATIONS`, iOS' and macOS' alert) was
|
||
asked only where a task's sheet chose its first reminder, so reminders typed in quick
|
||
add (`!30m`, `!1pm`), added by the automatic reminder (D4) or brought by sync were
|
||
planned and scheduled, and the system dropped every one without a word; and exact
|
||
alarms, `SCHEDULE_EXACT_ALARM`, are off from Android 14 on until the person allows
|
||
them in *Alarms & reminders*, so they came inexact, minutes late.
|
||
- **Asked wherever a reminder is planned**: whenever the plan of the device's
|
||
reminders holds one (`ReminderScheduler`), `NotificationAccess` reads whether the
|
||
device may notify (`ReminderGateway.notificationsAllowed`: Android's
|
||
`areNotificationsEnabled`, Apple's `checkPermissions`; Linux and Windows ask
|
||
nothing) and where it may not, asks, **once an app start**, the device's and not the
|
||
person's: refused, nobody is asked again and again (Android itself stops showing
|
||
its dialog after two refusals, iOS after one). The task sheet's ask goes through the
|
||
same place, so the plan's doesn't follow it.
|
||
- **Settings › Reminders says *Notifications are off*** while reminders are planned
|
||
and the device may not show them, with *Turn on*: asks again, and where that doesn't
|
||
allow it (refused again, or the system asks no more), opens the system's
|
||
notification settings of the app (the plugin's `openAppNotificationSettings`, no
|
||
package or platform code of Halkyon's added). Read again whenever the app comes
|
||
back, so notifications allowed there count at once. (Agent's choice: Turn on asks
|
||
first and opens the settings after a refusal, over telling the two apart with
|
||
Android's `shouldShowRequestPermissionRationale`, which would have needed a method
|
||
channel of the app's own for a case that ends in the settings either way.)
|
||
- **Exact alarms through `USE_EXACT_ALARM`** (the developer's choice), granted with the
|
||
install from Android 13 on to an app whose purpose is reminding of what a person
|
||
planned (calendars, alarm clocks, reminders), with `SCHEDULE_EXACT_ALARM` still
|
||
declared for Android 12 and 12L, which don't know the other, up to API 32 alone
|
||
(`test/android/exact_alarms_test.dart`). `AlarmManager.canScheduleExactAlarms` is
|
||
true with either, so the plugin's `canScheduleExactNotifications` is, and reminders
|
||
are `exactAllowWhileIdle`, a snoozed reminder too (the developer's, as it had been
|
||
inexact while snoozes were minutes long; an idle phone may bring an inexact one later
|
||
than the snooze lasts). *Alternatives:* leading the person to *Alarms &
|
||
reminders* (one more permission to ask and explain, which most would never find
|
||
again once refused), staying inexact (reminders minutes late, which a reminder at a
|
||
time can't be). Google Play reviews `USE_EXACT_ALARM` against that purpose; Todo is
|
||
one, and F-Droid asks nothing of it.
|
||
|
||
- **D32 A task's locations, with reminders on arriving or leaving** (the developer's, 5
|
||
Nov 2026): a location says where a task happens; a task may have several, which count
|
||
as alternatives, each with its own reminder: none, on arriving, or on leaving.
|
||
`TaskLocation` (name, address text, position, optional OSM type and ID, trigger) goes
|
||
in the task's sealed payload as `locations`, schema still 1 (D18's unknown fields stay);
|
||
recent places are derived from the tasks (`recentLocations`), not kept.
|
||
- *Finding a place.* People don't know addresses, so a search starts from a city or
|
||
district and a name (`Lindau EDEKA`, `Aeschach EDEKA`), and the map is part of
|
||
picking, not an extra (the developer's): results list name, then street, district
|
||
and city, so that two EDEKAs differ, and show as pins; a pin or a row chooses, and a
|
||
tap on the map places a pin by hand. The phone's last known position centres the map
|
||
and biases the search, where it may be read. `PlaceSearchGateway` is the seam:
|
||
`TodoDependencies.places` is the instance's search once wired, and until then
|
||
`PublicPhotonGateway` to komoot's public Photon, in a debug build asked for it alone
|
||
(`--dart-define=TODO_PUBLIC_PHOTON=true`), with a generic User-Agent and no personal
|
||
data; without either the picker says search isn't available. Car parks and charging
|
||
stations named after a place are left out (seen on `Lindau EDEKA`). The map is Train's
|
||
(`flutter_map`, `vector_map_tiles`, OpenFreeMap's Liberty and Dark), a small copy of
|
||
`vectorTiles`, no tile layer in widget tests. Alternatives named: a street-address
|
||
form (refused: nobody knows them), the picker without a map (refused by the developer).
|
||
- *Quick add.* `+` is a keyword as `#` and `@`: recent places suggested as it is typed,
|
||
below them the way into the search with what was typed; a place named before becomes
|
||
a token (`parseQuickAdd`'s `locations`), kept as text by a tap or Backspace as any
|
||
other (D29); an unknown `+word`, or a `+` a space follows, stays text. A + menu entry
|
||
and a pin button with the name or a count do the same by hand. The task sheet has a
|
||
Where box, each place with its ⋮; a task's row shows the name and how many more.
|
||
- *Reminders* are on the device, Android alone (`LocationReminderGateway`, a no-op
|
||
elsewhere, which offers none), as the instance reads no task: the system's
|
||
`LocationManager.addProximityAlert`, 150 m, no Google services, which GrapheneOS
|
||
lacks. Planned from the open tasks as `ReminderScheduler` plans reminders
|
||
(`planLocationWatches`), cancelled in `TodoPerson.dispose`, kept in the app's private
|
||
preferences (titles unsealed there, as reminders' are; `allowBackup` is off),
|
||
registered anew after a restart or an update and on the process's first plan, as a
|
||
force stop drops them unseen. Fused-provider geofencing was refused: it is Google's.
|
||
- *Dwell*, so that driving past an EDEKA on the way to the next doesn't remind (the
|
||
developer's question): crossing the circle schedules a check two minutes later, a
|
||
crossing back cancels it, and the check notifies only if the phone is still on that
|
||
side (`LocationDwell`, pure, with JUnit tests CI doesn't run yet), by a fix at most
|
||
90 s old where there is one, else by the system's last event. A red light of over
|
||
two minutes beside the shop still reminds; that is accepted for now.
|
||
- *Once*: a task reminds once until it changes (title, day, places: `stamp`), and an
|
||
alternative that reminded stops the task's others. The notification goes on the
|
||
reminders' channel and opens the app.
|
||
- *Permission*: the position first, then, told why, *Allow all the time* in the
|
||
system's settings, asked as a person chooses such a reminder and never by planning
|
||
(a task may sync in); Settings' Reminders says while it is off, with Turn on, as D31.
|
||
- *A debug build installs beside the release* (`.debug`, *Todo Debug*), the keys
|
||
differing; SimpleLogin's way back keeps the scheme the instance allows, so with both
|
||
installed Android asks which app answers it. Debug builds log to
|
||
`adb logcat -s HalkyonLocation`.
|
||
|
||
## 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` (made 2 Nov 2026, in memory alone,
|
||
`$XDG_RUNTIME_DIR`, and kept nowhere but Proton Pass). *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` (categories *Task* and
|
||
*Calendar & Agenda* since D27, `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.
|