Commit graph halkyon/apps
Author SHA1 Message Date
adbb2c4f2f
feat(train): where sync stands in Settings, with Sync now, and the cog's dot
As Learn's and Todo's: halkyon_app's Sync card first in Settings, and the
dot on the cog while sync fails; the person's SyncService is now part of
TrainPerson, which was the only reason Train had neither (the developer's
review of 3 Nov 2026).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:09 +01:00
82436ad030
Merge branch 'todo-fb' into integration 2026-11-04 09:32:08 +01:00
e54de4002e
docs(todo): ADR 0033, D19 to D25, the developer's review of 3 Nov 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:08 +01:00
ef80453fa0
feat(todo,meditate): what a setting does behind its question mark
The developer's review of 3 Nov 2026 (ADR 0033, D25): an explanation stands
one tap away, behind halkyon_ui's HelpButton beside the setting's name,
rather than always below it. Todo: resetting sub-tasks, the automatic
reminder (its value stays below), counting a recurrence from completion.
Meditate: keeping the screen on, the streak, the morning's dream prompt.
What is the person's own (a value, a count) stays in view; an empty list's
message and quick add's hint about highlighted words, shown only while
there are some, stay too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:08 +01:00
140a283d0e
feat(todo): a task's sheet partway up and its parts in rounded boxes, as Todoist's
The developer's review of 3 Nov 2026 (ADR 0033, D24): on a phone the sheet
opens as high as its content, at most 65 % of the window, the list above it
in view, and is dragged up to the whole window or down to close; its title,
description, date and reminders each in a rounded box, its priority, labels,
deadline and project as a row of chips (named "Priority", "Labels",
"Deadline" while unset), and its sub-tasks in a box ending in Add sub-task.
Where sheets are dialogs, and in the pane beside the list, the same parts.
Todo keeps no comments, so the sheet has no comment field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:07 +01:00
8438caa489
feat(todo): quick add a small bar docked above the keyboard, as Todoist's
The developer's review of 3 Nov 2026 (ADR 0033, D23): in place of the tall
sheet with its title and Add button, a rounded bar just above the keyboard,
the list still in view behind a lighter barrier: the title, then a row of
boxed buttons (+ for the description and the deadline, the date, the
priority, the labels, the project), each its icon alone until something is
set, and a send button. Its buttons show from the start rather than once
typing begins (D15 had them come with the first letter). Where sheets are
dialogs, the same bar in a dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:07 +01:00
918080ce46
Merge branch 'learn-fb' into integration 2026-11-04 09:32:06 +01:00
c7ad78f88d
feat(todo): Display's layouts as pictures with radio buttons, its rows grouped in rounded boxes
As Todoist's (the developer's review of 3 Nov 2026, ADR 0033, D22): each
layout a phone's outline with its picture, its name below on one line (no
"Calenda/r" split across two), a radio button marking the one in use; the
switches, the sorting and the filters each a group in a rounded box under a
plain heading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:06 +01:00
753527aed3
feat(todo): rows in rounded boxes, Today as Todoist's, lists pulled to sync, a project's own colour
From the developer's review of 3 Nov 2026 (ADR 0033, D19 to D21):
- Today's tab shows today's day in its calendar, Browse the menu's lines,
  Display's button Lucide's square-menu.
- A task's row stands in a rounded box, the rows of a list a group rounded
  at its ends; the box is the row's own surface, so a chosen row's colour no
  longer shows under Upcoming's pinned week strip as the list scrolls. Its
  project and section end the line of its date at the row's right edge.
- Today has at most two groups: what is overdue, folding under its heading,
  and today's, headed "Tuesday, 3 Nov" ("Dienstag, 3. Nov."), without a
  "· Today" after it; Upcoming's days take the same form.
- Upcoming's way back to today shows whenever another day is picked.
- Today, Upcoming, the Inbox and every project pulled down sync now, an
  empty list too, as Learn's.
- A project's screen washes in its own colour, as a studyset's in Learn.
- "Add section" in place of "New section".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:06 +01:00
df713431dd
feat(learn): the lock beside a studyset no longer changes its privacy, which Edit does
The developer's review of 3 Nov 2026: a studyset is made private, or
non-private, in Edit alone. The lock's sheet still says what its privacy
means, offers publishing one that isn't private, and finishes one only
partly private.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:05 +01:00
830aea10fe
feat(learn,app): explanations wait behind a question mark beside their title
What explains a setting or a section, rather than what is the person's
own, moves behind halkyon_ui's HelpButton, beside its title through the
new ExplainedTitle (the developer's review of 3 Nov 2026): in Learn's
settings (citations, the Pomodoro timer, grammar, the web search,
Jellyfin, the device's dictionaries, counting one's learning), the study
settings, export's progress, the Jellyfin tags and the study button's
back first; in halkyon_app, setting up a password, adding SimpleLogin,
own keys not allowed, the profile page's link back and the operator's
grant sheet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:05 +01:00
c984678c5b
feat(learn): voice mode, a conversation spoken as Claude's "Let's talk" (ADR 0021, D68)
Beside the field's microphone, a button opens voice mode over the whole
screen: a glow rising from below, a big microphone, and +, who answers,
the speaker and End in one row. It listens with the device's recogniser,
asks what was heard in one kept conversation, reads the answer aloud
with the device's speech (flutter_tts, its Markdown left out) and
listens again. On Linux there is none, as there is no microphone; where
nothing reads aloud, answers are shown as text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:04 +01:00
7be31dcbfc
feat(learn): + opens a sheet as Claude's, Camera, Photos and Files, and what goes along in rounded rows
Following the developer's review of 3 Nov 2026 (ADR 0021, D67):

- + below the assistant's field, and below a conversation's follow-up,
  opens "Add to your question" as Claude's "Add to session": three big
  tiles side by side, Camera (where the device takes photos) and Photos
  for an assistant reading images, and Files; below them, in the field, a
  row leading to what goes along, showing how far the assistant looks and
  how many parts are chosen.
- Camera and Photos through image_picker (flutter.dev's own): the camera's
  own app and the system's photo picker, no permission asked on Android, a
  file dialog on the desktop, which has no Camera; photos made at most
  2048 pixels a side, read on the device like any file attached.
- What goes along is chosen in rounded rows, as Claude's Permission, in
  place of radio buttons and chips: how far it looks with a tick, private
  studysets with a switch, the parts each with a tick, a card's image with
  its thumbnail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:04 +01:00
a7904bac05
feat(learn): a studyset is made private in Edit, no longer from its options
The developer's review of 3 Nov 2026: Make private and Make non-private
leave a studyset's options (its card's menu, its screen's); a top-level
studyset's Edit has a Visibility section instead, its switch making it
private, or non-private, at once once the person agreed in the dialog as
before (ADR 0019), not on Save, and what that means behind the question
mark. A topic's Edit offers none, its privacy being its tree's. The lock on
a studyset's card still explains its privacy and keeps its own button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:04 +01:00
10744bcd56
Merge branch 'train-maps' into integration 2026-11-04 09:32:03 +01:00
96b1a0fc64
feat(learn): the assistant's field spans the screen, its buttons boxed and still as who answers changes
Following the developer's review of 3 Nov 2026 (ADR 0021, D67):

- Unfolded in the Library, the field spans the screen between its margins,
  as on Home and a studyset: it fell short on the right, by the padding
  around the two folded buttons, which now stands only around those.
- Who answers stands right before the buttons at the right, which no longer
  shift as its name changes (Claude Haiku, Claude Opus): it fills the space
  left, aligned to its end, rather than a share of it.
- Every button of the field and of a conversation's follow-up is boxed, a
  tile a shade above the field with rounded corners, as Claude's apps have
  them; who answers too.
- On a studyset with a licence, the unfolded field takes its usual place
  over the licence's line rather than squeezing in above it; folded, its
  row still keeps clear of the line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:03 +01:00
3e90753b08
feat(train): a workout's page as Strava's, maps readable and in the app's language, routes opening near the person (ADR 0030, D63)
- A workout with a track: its map to the foot of the screen under a sheet
  with its name; once the sheet settles, the map fits the track anew, eased,
  into what is left free; pulled up, the round buttons over the map fold
  into an app bar whose chevron lowers the sheet again. From expanded
  windows on, the map beside the rest. The page stays in Home but opens
  over the shell, without the floating bar.
- What the map's source sees moves behind a question mark.
- The map's credit on the app's theme: flutter_map's, built on
  flutter/material, never saw it (material_ui's Theme is a class of its
  own) and drew the theme's text on white; map_tiles read that Theme too,
  and so never chose the instance's dark map (D50).
- Places named in the app's language (name:<language>) where OpenFreeMap's
  styles ask for the English name.
- The route planner's hint behind a question mark; a new route's map opens
  at zoom 14 where the phone was last known to be, else where the newest
  workout with a track started; its details beside the map from expanded
  windows on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:02 +01:00
8c70ad98a1
feat(app): an authenticator's key copied, marked sensitive, and its code six digits
A phone without a camera whose authenticator doesn't open otpauth: had no
way but typing the key. It is copied again, the key alone (ADR 0017
amended, the developer's decision of 3 Nov 2026): on Android through each
app's SensitiveClipboard.kt, marked EXTRA_IS_SENSITIVE, so Gboard and the
system show no preview and keep it out of their histories; elsewhere through
Flutter's clipboard. Emptied 60 seconds later if the clipboard still holds
it. The recovery phrase and codes stay never copied.

The code's field counted "/7", taking an optional space for a seventh
character: it now takes six digits alone, a pasted space dropped, and counts
nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:01 +01:00
abac248b76
refactor(meditate): the player's buttons read their cubit as they are pressed
leancode_lint's avoid_context_read_in_build, which only the root's analysis
runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:01 +01:00
c44cf55fd4
docs(meditate): ADR 0038, D16, the operator's library, as the developer asked
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:00 +01:00
811182b376
feat(meditate): the instance's library in the app: Explore, its player, courses, keeping offline
Explore shows the instance's library below Meditate's practices, where it
has one: its courses and items by category (Meditate's eleven, named in
English and German, and the operator's own) and by the same search; an
instance without one shows nothing more. An item's page: its recordings
to choose, Play, a favourite (kept in the sealed settings under
library/<id>), keeping it on the device and removing it, and for a sound,
a silent sitting with it looped; its credit and licence. The player
plays, pauses and seeks; played to its end it is kept as a sitting, and
as its course's lesson done (lesson_done, sealed and synced). Settings
lists the library's credits beside About; Today shows its favourites.

The last catalogue is kept in the person's folder for offline; files are
fetched with the catalogue's access, asked for again shortly before it
ends. ADR 0038, D16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:00 +01:00
0b6f27d278
feat(meditate): the library's accesses name nobody, and the privacy page says so
An access to the library's files now holds from the hour begun, so
everyone given one within the same hour gets the same and a fetch can't
be told apart by account. The privacy page says what the library means
for people: signed in alone, fetched naming no account, what they
listened to sealed. ADR 0038, D16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:31:59 +01:00
929974443d
feat(meditate): the operator's library on the server, and a course's lessons done synced
An operator's folder (MEDITATE_LIBRARY_DIR) of recordings, pictures and a
YAML manifest, library.yaml (commented sample: library.example.yaml), read
as the server starts and every ten minutes, held in memory. Items without
a licence or credit, files outside the folder, unknown categories and
courses pointing at items not served are refused and logged; a manifest
that doesn't read keeps the library served before. bin/check_library.dart
checks a folder before deploying.

The catalogue goes to signed-in people alone (LibraryEndpoint), with a
12-hour access the server signs; the web server's /library/<served name>
serves the files to that access only, by ranges (relic's StaticHandler).
No request path reaches the file system.

lesson_done: a lesson done, sealed and synced, a row each; the migration
creates the table with its spaceId index and the cascading foreign key to
the sync engine's spaces, as dream's did. ADR 0038, D16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:31:59 +01:00
433e544d44
feat(meditate): dreams spoken: a microphone in the dream's editor, and Today's Speak it
The editor dictates where the cursor stands with halkyon_app's Dictation,
showing it listens and the words heard, listening again after each pause
until stopped or three silent listens, in the app's language. No
microphone where the device has no recogniser (Linux). On Android it asks
for recognition on the device; the privacy page says what each platform
does. Android's, iOS's and macOS's microphone declarations as Learn's.
ADR 0038, D15.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:31:58 +01:00
603218ef96
refactor(app): the device's speech recognition moves from Learn into halkyon_app, with dictation into a text
Learn's `SpeechInput` and `DeviceSpeechInput` (ADR 0021, D67) are
halkyon_app's now, and its `FakeSpeechInput` is in `testing.dart`, so
Meditate's dream journal (ADR 0038, D15) listens through the same code.
Learn imports them from there and behaves as before: its composer and
its tests are unchanged but for the imports, and `DeviceSpeechInput()`
asks for what it asked before.

`DeviceSpeechInput` takes three options, off by default:
- `preferOnDevice`: on Android, recognition on the device where it can
  (Android 12 on), else offline; should that fail before a word is
  heard (not a pause), it listens as Android otherwise does from then
  on.
- `pauseFor`: how long a pause may last before the recogniser stops.
- `listenFor`: how long one listen may last at most.

`Dictation` writes what is heard where the cursor stands
(`DictationInsertion`: in place of a selection, set apart by a space),
shows the words so far, and listens again after each pause while it is
on, stopping by itself after three listens running heard nothing.

speech_to_text moves from Learn's pubspec to halkyon_app's, the lock
unchanged. Todo, Train and Meditate link the plugin now; their
generated plugin registrants (macOS, Windows) say so. An app declares
the microphone itself, and Todo and Train declare none, so nothing
there can listen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:31:58 +01:00
86c9f4522a
feat(meditate): the dream journal, a fourth tab, Today's morning card
ADR 0038, D14. A dream (`Dream`) holds the night it belongs to (the
morning's date, today's by default), an optional title, its text, the
marks lucid, nightmare and recurring, a clarity of 1 to 5, the mood on
waking and free labels, and when it was written; a row each in `dream`,
sealed for its row with the person's key as the sittings
(`DreamRepository` on `SealedRows`, `SyncDreamGateway`), followed by
`JournalCubit` for every screen.

- The Journal, between Explore and Profile: dreams by month, the
  newest first, searched over title, text and labels as Explore
  searches, filtered by marks, `+` above the bar; empty, it says that
  entries are sealed on the device before they sync.
- A dream's screen over the tabs: a new one with its text first and
  focused, the rest folded below; a kept one to read, with Edit and
  Delete (asked first); leaving one written but not saved asks first.
- Today, from 4 to 12 o'clock while last night has no dream, a gentle
  card leading to a new one; Settings' Journal card switches it off
  (`dreamPrompt` in the sealed settings, schema 1 kept).
- Profile's Practice: this month's dreams and the lucid ones, once the
  journal holds one.
- English and German texts; What's new 0.1.0 names the journal; the
  account's deletion says it takes the dreams too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:31:58 +01:00
769ae3b20a
feat(meditate): dreams on the server, sealed and synced like the sittings
The dream journal's table (ADR 0038, D14): `dream`, in the sync engine's
database, a row a dream, its content sealed on the device (key ID,
payload, nonce), so the instance sees only that a dream exists, its size
and when it changed. The same rules as the sittings: the sync engine's
connector refuses a call without a session, each row lives in its
person's space, and deleting the person deletes their dreams with it
(the deletion test now writes and counts a dream too, and leaves
another person's).

The migration `20261103130213716-dreams`:
- `CREATE OR REPLACE FUNCTION gen_random_uuid_v7()`: the generator
  repeats the UUID v7 function every migration; replacing it with the
  same body changes nothing.
- `CREATE TABLE "dream"`: `id` a UUID v7 by default, `spaceId` the
  person's sync space, `keyId` the key it is sealed with, `payload` the
  sealed bytes (not null), `nonce` the sealing's nonce.
- `CREATE INDEX "dream_space_idx"` on `spaceId`, so deleting a space
  finds its dreams without scanning the table.
- The foreign key from `spaceId` to `offline_sync_spaces("id")` with
  `ON DELETE CASCADE`: deleting the space deletes its dreams.
- The version rows of every module in `serverpod_migrations`.
A new, empty table: nothing existing changes and no data moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:31:57 +01:00
cb026dce8d
feat(meditate): the app: Today, Explore, Profile, the sitting and its bells
halkyon_meditate_flutter on halkyon_app and halkyon_ui (ADR 0038): the
first start, sign-in and account as every Halkyon app's, a bar below
medium windows and a rail with the icon from medium on, fern washes,
Settings in cards (Sync, the operator's, Sitting, Practice, the
account, What's new, About, the version), English and German.

- Today: a greeting by the hour, Sit now at 5, 10, 20 minutes and the
  timer's own length, the practice of the day, the week with the days
  in a row, favourites, recently sat (D9).
- Explore: six practices of Halkyon's own, written for the app, found
  by a search without case or accents, by goal and by kind; each page
  with how it goes, its lengths, its heart and Start (D4, D10).
- The sitting, over the tabs: its time from the clock, a ring counting
  down, the cue of the moment, Pause and End, the bells at the start,
  between and at the end, the screen kept on; ended early, kept from a
  minute on where the person says so (D5, D6).
- Profile: every Halkyon app's, then the person's practice: minutes,
  sittings, days, the week and every sitting kept, to delete (D8).
- Settings and sittings sealed with the person's key and synced (D7).

The bells are synthesised by apps/meditate/tool/bells.py, the icon is
Lucide's sprout from cobalt through pacific into fern
(tool/app_icons.py). Releases and the deploy are a later step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:31:56 +01:00
235d9cf75b
feat(meditate): Meditate's server, its sealed tables, and SimpleLogin's way back to it
halkyon_meditate_server and its generated client, copied from Todo's
and stripped to the module, the register and the sync engine (ADR 0038,
D1, D7): content_key, meditate_settings and sitting, synced and sealed,
each indexing spaceId and deleted with the person's space; two
migrations, the initial one and sittings (purely additive, the ADR
explains the SQL); ports 8280 to 8282 and PostgreSQL 8290 (D13).
Tests: the module's basics naming `meditate`, the sync engine refusing
calls without a session, the register's modes, and a person's deletion
taking their rows alone.

halkyon_base's forwarding page knows Meditate's ways back
(meditate-loopback on 43826, meditate-scheme app.halkyon.meditate),
which halkyon_app's repository names among the apps' ports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:31:56 +01:00
f5b657dfad
feat(todo): reminders at a time of their own planned and set in a task's sheet, durations in its date sheet
planReminders plans each reminder at a time of its own of an open task,
whatever its date, under an ID of its own; the notification says
"Reminder" for a task without a time. The task's sheet always has its
reminders' row: those before its time where it has one, those at a time
of their own each with its remove button and "Remind at a time" (a day,
then a time), the permission asked as a first one is chosen. The date
sheet sets a duration once there is a time (none, 15 minutes to 4
hours, as Todoist's), a time cleared taking it with it. Recorded as
ADR 0033, D18 (the developer's, 3 Nov 2026).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:31:55 +01:00
d40eca6e50
feat(todo): quick add reads on before a day, durations, every !30m and !1pm
`on` (German `am`) before a day is taken with it, as `at` before a time;
`for 2h`, `for 1h30m`, `for 90 min`, `für 1,5 Std.` set the date's
duration where there is a time, and stay text without one; every `!30m`
counts, each once, sorted, a task without a date due at the latest; and
`!1pm`, `!13:00`, `!tomorrow 9am`, `!Monday 8:30` are reminders at a time
of their own, a time without a day on the task's day, else the next to
come. Todoist's newsletter's four examples parse whole (ADR 0033, D18).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:31:55 +01:00
3c68a22a5f
feat(todo): a task holds reminders at a time of their own, and keeps the fields it doesn't know
ReminderTime is a day and minutes after midnight, the wall clock without
a zone as TaskDate's; a task's reminderTimes are written as
[{"day", "time"}] and left out where there are none, so older payloads
read with none. The payload's schema stays 1 (as D17's view): what a
newer version wrote at its top that this one doesn't read is kept in
TaskContent.unknown and written back beneath the fields it knows, so a
task changed here keeps it (ADR 0033, D18).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:31:54 +01:00
996e584338
chore(release): learn-v0.23.0
All checks were successful
CI / check (push) Successful in 40m57s
CI / deploy (push) Successful in 1m1s
Build 26.

Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-03 17:47:36 +01:00
478171dd87
chore(release): train-v0.2.0
All checks were successful
CI / check (push) Successful in 42m35s
CI / deploy (push) Successful in 1m5s
Build 2.

Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-03 17:33:08 +01:00
551a29445f
build(train): Train on the desktop as a snap, to the Store's edge channel with each release (ADR 0030, D62)
Built as Todo's in a clone of its own, from apps/train/snap/, for train.halkyon.eu; its
listing says the desktop shows, plans and keeps the statistics, and records nothing.
The snap tests now hold every snap built in a clone of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-03 12:16:05 +01:00
ac387b5250
feat(icons): every icon fades from cobalt into its app's backdrop; Learn shows its own inside the app; Train's dumbbell (ADR 0031, D5 to D7)
All checks were successful
CI / check (push) Successful in 41m0s
CI / deploy (push) Successful in 4m37s
Each icon now ends in the colour its app's screens wash down from: Learn cobalt into
purple, Todo through violet into strawberry (Learn's former gradient), Train unchanged
into carrot, Meditate's to come through pacific into fern (Todo's former). Train's glyph
is Lucide's dumbbell, Meditate's will be the sprout.

Learn shows its graduation cap atop the rail, on the launch screen and on halkyon_app's
screens, as Todo and Train show theirs; the kingfisher leaves the app and stays the
F-Droid repository's icon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 23:12:20 +01:00
3ac1880ac8
build(deploy)!: the instance's compose file is compose.learn.yaml, beside compose.train.yaml and compose.todo.yaml
No file has Compose's default name any more, so .env's COMPOSE_FILE is always set,
naming compose.learn.yaml first (.env.example sets it). compose.yaml stays as a link to
compose.learn.yaml for the one deploy that switches learn.halkyon.eu's .env over: the
deploy's merge runs with the COMPOSE_FILE from before. A later commit removes it.

BREAKING CHANGE: an instance's deploy/.env must set COMPOSE_FILE, starting with
compose.learn.yaml, before the link goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 23:12:15 +01:00
5469ce791f
build(deploy): the server's checkout is ~deploy/halkyon, the repository's name
All checks were successful
CI / check (push) Successful in 40m10s
CI / deploy (push) Successful in 58s
The folder kept its name from before the monorepo (ADR 0015), though
Train and Todo deploy from it too. redeploy.sh, the backup's units and
the operator's steps name ~deploy/halkyon; the Compose project keeps the
name halkyon-learn, as Docker names its volumes after it.

The server moves after this is deployed: the running redeploy.sh is read
whole before it runs, so the deploy of this commit still works in the
old folder; then the folder is moved, the pinned commands in deploy's
authorized_keys and the backup's units changed, and the containers
recreated on the new paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 18:56:39 +01:00
e61d3abb4f
build(todo): Todo deployable beside Learn's server, and released as an APK, on F-Droid and as a snap (ADR 0033, step 5)
deploy/compose.todo.yaml adds Todo's server at todo.halkyon.eu, a member
of Learn's register, its database halkyon_todo in the instance's
PostgreSQL, its image built as Train's. redeploy.sh and backup.sh go over
every app server .env switches 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, as a second compose file's default
would have replaced Train's.

apps/todo/release.dart configures the release tool for Todo's APK and
snap: a key of its own (pinned once made; until then nothing is
released), the F-Droid listing (Time, NonFreeNet), and the snap from
apps/todo/snap/, which the tool builds in a clone of the repository at the
tag with that folder in place of snap/, as snapcraft builds one recipe a
folder and the root's is Learn's. What's new for 0.1.0 in English and
German, now shown. Android's backup and transfer are off, as Train's
(D53): the reminders scheduled keep their tasks' titles unsealed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 16:04:00 +01:00
e673aa33aa
feat(train): the Loop's own detection of training, switched on its screen (ADR 0037)
The band's screen shows whether the Loop detects training by itself and
switches it, with the shortest session it keeps (10, 15, 20 or 30
minutes), so a session nobody recorded still comes in afterwards as a
workout (ADR 0030, D39). Read on each connection and after the band's
setup through the SDK's getUserDeviceSettings, set with
setAutomaticTrainingDetectionSettings; its sensitivity stays as the band
has it, as the SDK documents no range for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 14:56:17 +01:00
e4585b478d
feat(train): reminders to move and workouts suggested from the steps (ADR 0037)
Settings' Moving more, on Android: a reminder to get up after an idle
time, and a suggested walk or run, each off until turned on (then every
30 minutes, weekdays 9:00 to 18:00, not 12:00 to 13:00, each value the
person's), turning on asking for the activity and notification
permissions.

A WorkManager job every 15 minutes reads the phone's step counter and
keeps two hours of readings. It reminds where fewer than 50 steps came
over the idle time, all of it within the hours and outside the break,
on the person's days, never while Train records or the phone is in a
car (Android's car mode, or a car's Bluetooth audio or hands-free). It
suggests a walk or a run where the last span averaged a walking or
running cadence, once an hour; tapped, the Record sheet for it opens.
The decisions are pure Kotlin (Movement.kt), tested on the JVM; the
notifications' words are the app's, written into the job's
configuration as the person's app opens.

Polar's SDK sets neither the band's inactivity alert nor tells of one
without an all-day connection, hence the phone's steps (ADR 0037).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 14:56:12 +01:00
3f7b409e80
feat(train): workouts' weather from Open-Meteo, through the instance (ADR 0036)
A workout with a track gets the weather over it as its page opens, once
while it has none: the hours from the one it started in to the one it
ended in, kept in its sealed payload (temperature, feels-like, humidity,
precipitation, the WMO code, the wind, its gusts and direction) and shown
under its totals with the link to Open-Meteo its licence asks for.

Train's server asks Open-Meteo in the person's place (the `weather`
endpoint), so Open-Meteo never sees their address: the place rounded to
two decimals (also on the phone), a span of at most 48 hours, the
forecast API for recent spans and the archive for older ones, for people
signed in, at most 30 requests a person a minute, nothing kept or
logged. On by default; `WEATHER=off` switches it off, and
`OPEN_METEO_API_KEY` moves it to the paid hosts for an instance used
commercially.

Health Connect's fingerprint leaves the weather out, so keeping it
writes nothing again. No table and no migration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 14:01:34 +01:00
508994be3a
feat(train): strength plans and sessions (ADR 0035)
Strength plans of exercises from a catalogue or the person's own, in
blocks of one exercise or several done in turn (supersets, circuits),
each with its sets, reps or seconds, weights and rests; each a sealed
`strength_plan` row. Plans are marked with what the place chosen lacks,
and a session offers swaps moving the same way that the place allows.

A session runs on every device, set by set with the rest counted down,
each weight made up from the place's plates, bars and fixed weights with
the plate changes between sets, kept in strength_session.json after each
change so one cut off is carried on, and saved as a workout of the new
activity Strength with its sets, listed on its page; strength training
in Health Connect and FIT, no GPX.

Migration 20261102113104496-strength-plans is purely additive: it
creates the table strength_plan (id uuid v7, spaceId, keyId, the sealed
payload and its nonce), its index on spaceId, and a foreign key to
offline_sync_spaces with ON DELETE CASCADE, so deleting a person's sync
spaces deletes their plans; then the modules' version rows. The
gen_random_uuid_v7 function is created or replaced as in every
migration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 13:31:51 +01:00
064b911aa2
feat(train): equipment by place and a plate calculator (ADR 0034)
The places a person trains at (home, a gym), each a sealed, synced
`training_place` row with its equipment: bars and handles with their own
weight, plates by weight and count, dumbbells and kettlebells, the rest
by count, the person's own by name. A new place starts from what a home
or a gym usually has. The place last chosen is remembered on the device,
never its location.

The plate calculator loads each weight both sides alike, the heaviest
load not above it with the fewest plates, heavier inside, and over a
series of sets picks the loads making the fewest plate moves, listing
what comes off and goes on. Equipment is in Settings; strength plans
are to build on it.

The migration only creates the table, its space index and its cascade
to the sync engine's spaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 13:31:46 +01:00
b7b0b937fe
feat(app): one Profile for every app, the person's own tab first, then each app's (ADR 0014)
The public profile was already one per person for every service of an
instance (the register, ADR 0016); its app side was Learn's alone. It
moves into halkyon_app: the model, the repository on the module's
endpoints, the form, the avatar drawn on the device and the sheet with
someone's profile. They take the Public prefix (PublicProfileRepository,
BasePublicProfileGateway), as Train's own ProfileRepository holds the
body's figures. AppDependencies gains profiles and files, the picker the
avatar is chosen with, which moves out of Learn too.

HalkyonProfile is every app's Profile screen: the person's own tab first,
the same in every app (the account and the ways they sign in, the app's
own cards, then the public profile), then the app's own tabs, folding into
the title. Learn adds its inbox and Statistics. Train adds Progress, Days
and, where nothing records, Plans, and so opens on the person's tab, no
longer on Progress. Todo, which has no Profile tab, opens it from a row at
the top of Browse, where Todoist keeps the account.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 11:11:11 +01:00
a0bb75f1ad
feat(learn): pick up learning on Home, the studysets studied latest first, and Progress closer to Vaia's
Home gets a widget of its own, Pick up learning, below the goal: the
studysets at the top of the library that were studied, the one rated last
first, so one studied long ago sinks. Three show, the others behind Show
more. Each shows its name, which opens it, how much is learnt, when it was
last studied, and today's task (cards to review, new cards to learn), which
starts studying it, outlined while reviews wait. The progress is the
library's, worked out once for every screen; when each was last studied
comes from the reviews Home reads anyway. With nothing studied it takes no
room.

A dashboard arranged before it gets the widget below the goal, wherever
the goal is: a widget a layout misses now goes after the one before it in
a new dashboard, no longer at the end.

On a studyset's Progress, the tiles are filled without an outline, To
review has a history icon (amber while cards are due), the learnt bar is
thicker, and the exam follows the figures, as Vaia asks for its date
there: an invitation to choose it, opening the study plan's sheet, or,
once chosen, how ready the person is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 09:17:07 +01:00
447e00c52a
feat(learn): the assistant's field laid out as Claude's, with files attached and questions spoken, and no Check (ADR 0021, D67)
The question's text stands above its buttons: + attaching files, who
answers as a chip (Effort now a submenu of it), the conversations kept,
and the microphone while nothing is written, which gives way to the
button sending. What goes along is one row above the text, opening a
sheet with how far the assistant looks and the parts to send. A
conversation's follow-up has the same + and microphone.

Files are read on the device: an image goes as itself to an assistant
that reads images, a PDF as its pages' text, a text file as its text,
each cut at 20,000 characters and labelled by its name after the
question.

Voice is the device's own speech recognition (speech_to_text: Android,
Apple, Windows; none on Linux), in the app's language, the microphone
asked for once tapped. The privacy page says that Android's recogniser
may send the sound to Google or the phone's maker.

The field's Check goes, with its LanguageTool sheet (amending ADR 0024);
the editors' underlining and Why? stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 08:46:37 +01:00
5641285d31
feat(learn): Explore by what a licence allows, a skeleton while the library loads, and an assistant's agreement per provider (ADR 0005, rule 12; ADR 0021, D66)
- Explore filters by what a studyset's licence lets a learner do: adapt
  it (no NoDerivatives) and use it commercially (no NonCommercial),
  which the instance reads from the release's licence
  (ExploreQuery.adaptable, commercial) and the app the same way
  (LicenseUse). An instance from before ignores both.
- While the studysets are read, the Library shows shimmering skeletons
  of their cards rather than a spinner: halkyon_ui's new Shimmer and
  SkeletonBox, still where the system asks for no animations.
- The scope's "Include private studysets" is offered only while the
  person has a private studyset.
- An agreement to what goes where is per provider (protocol and host):
  agreeing to one assistant agrees to every assistant of that provider,
  those granted later too, and the agreement sheet says so. No
  migration: it is written on every grant of the provider, so taking
  one back keeps it for the others, and read per provider from any of
  them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 05:19:18 +01:00
138c4dd46e
feat(learn): the assistant's button folded on Home too, the conversation at full height folding into it, and smaller fixes from the developer's list (ADR 0021, D64, D65)
- Home's assistant starts folded into its button at the lower left, as
  in the Library, where a field stood at its foot.
- The button has Material 3's floating action button shape (a rounded
  square) rather than a circle, its gradient ring kept.
- On a studyset below expanded windows it stands in one row with Add
  material and Study rather than above them.
- While studying, the ratings below the card make way while its field
  is open rather than standing under it.
- The conversation opens at the window's full height, its header above
  the list, so a drag on the header moves the sheet and a drag on the
  list scrolls it. Close ends the conversation; any other way out folds
  it into the button (ConversationDockCubit, above the router), its
  answer still coming, until any button opens it again or another
  conversation starts.
- An answer's Copy icon lines up with the answer's text.
- A sense's grammar notes run on after its glosses as one flowing text,
  so a short note such as "n" no longer takes a line of its own.
- A card's hints show their count in a grey badge beside the title, so
  it no longer reads as the hint's number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 05:19:18 +01:00
1aa679fe3e
fix(train): a recording's screen opens as it starts, and pages run on beneath the floating bar (ADR 0030, D21)
An indoor row started showed its notification but not its screen: the
Record tab's card pushed the screen only after the service had started,
and gave up silently where the card was gone by then. Train's frame now
opens it as the recording's phase turns to recording; one found running
as the app opens leaves the tabs as they were. The notification tapped
opens the recording's screen, not merely the app (RecordingOpenings.kt).

A workout's page kept a SafeArea at its foot, whose inset under the
floating bar is the bar itself: its content stopped at the bar's top, a
plain band behind it. It runs on beneath the bar as the tabs do, and the
tabs' lists and a day's page end with the body's bottom padding rather
than a fixed 88 px, which left their last item under the bar.

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