Commit graph

984 commits

Author SHA1 Message Date
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
83f586a2aa
docs(train): ADR 0030, D63, the maps after the developer's review
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
30dd8c96eb
feat(ui): the question mark's explanation offers a link beside OK
HelpButton takes an action, a label and what it does, shown beside OK where
there is more to read elsewhere, as what a map's source keeps (Train's ADR
0030, D63).

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
55fc245859
feat(ui): a question mark that keeps a section's explanation one tap away
HelpButton, beside a section's or a setting's title: the explanation opens
in a small dialog, under its title where given, rather than standing in the
everyday view (the developer's review of 3 Nov 2026). FieldInfoButton's
tooltip stays for a field's short hint.

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
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
76846244ca
docs(meditate): ADR 0038, D15, dreams spoken, 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: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
83edaeabe0
docs(meditate): ADR 0038, D14, the dream journal the developer asked for
The developer's request of 3 Nov 2026: a dream journal, sealed and
synced as the sittings, with Today's morning card and its switch in
Settings. What an entry holds beyond the request, its own `dream`
table, the fourth tab between Explore and Profile and why, the
Journal's list, search and filters, the editor and Profile's two
figures are the agent's recommendations, to be confirmed. The `dreams`
migration's SQL is explained under Migrations; it is step 5, the
release moving to step 6.

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
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
504aca3e68
docs(meditate): ADR 0038, what Halkyon Meditate does and how it looks
Proposed by the agent on 3 Nov 2026, the developer having asked to go on
without their interjection: three tabs (Today, Explore, Profile) on
Todo's and Train's frame, fern washes, Halkyon's own practices as text
cues with a silent timer, bells synthesised for the app, a sitting over
the tabs, sealed and synced settings and sittings, the figures worked
out on the device, development ports of its own. Every decision is the
agent's recommendation, to be confirmed.

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
8565db3d89
fix(release): every snap packed without --output, into its project, then moved
snapcraft builds in LXD with the project mounted at /root/project and refuses
any --output naming a folder of this machine: the root's own build/release/
for Learn's snap (found releasing Learn 0.23.0) as the clone itself for Todo's
and Train's (Todo 0.1.0, both 3 Nov 2026). Every snap now packs into its
project and is moved into the release's folder from there, checked with
snapcraft 9.2.1 at the root. A failure after none of several apps was done
now says "none" rather than nothing.

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
learn-v0.23.0
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
train-v0.2.0
Build 2.

Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-03 17:33:08 +01:00
f06fc23023
build(release): several apps released in one run (train:minor learn), their version commits pushed without the checks
All checks were successful
CI / check (push) Successful in 41m7s
CI / deploy (push) Successful in 1m2s
The release tool and tool/with_signing_keys.sh take several apps, each with its version
after a colon or none, and release them one after another, each as alone, stopping at
the first that fails; the key script fetches every named app's key.

The pre-push hook skips tool/check.dart when a push brings release commits alone: the
release tool's chore(release) subject, an app's pubspec changed in its version line and
its texts of what's new, nothing else (tool/release_push.dart). Signatures are checked as
before, and CI still runs the checks. A release no longer costs a run of the checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-03 12:16:10 +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
c8d94d0ceb
fix(release): a snap built in a clone of its own is packed without --output, into the clone
All checks were successful
CI / check (push) Successful in 40m26s
CI / deploy (push) Successful in 1m4s
todo-v0.1.0
snapcraft builds in LXD with the project mounted at /root/project and passes --output
on unchanged: naming the clone itself, it took the clone for a file in the folder above,
outside the project, and refused it again. Without --output it packs into the clone
(checked with snapcraft 9.2.1 in Todo's clone), from where the release tool moves it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-03 10:35:49 +01:00
34001b1aff
fix(release): a snap built in a clone of its own is packed inside it, then moved into the release's folder
All checks were successful
CI / check (push) Successful in 39m47s
CI / deploy (push) Successful in 59s
snapcraft packs nowhere outside the project it builds, so Todo 0.1.0's release stopped
after its snap was built ("Cannot create packages outside of the project tree").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-03 09:21:34 +01:00
f51e3ab2b1
build(ci): the deploy step is named for the instance, which it deploys whole, not learn.halkyon.eu alone
All checks were successful
CI / check (push) Successful in 38m47s
CI / deploy (push) Successful in 58s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-03 00:28:38 +01:00
738eee898e
build(deploy)!: compose.yaml's link goes, now learn.halkyon.eu's .env names compose.learn.yaml
The link carried the deploy that switched the server's COMPOSE_FILE over (2 Nov 2026).

BREAKING CHANGE: an instance whose deploy/.env still names compose.yaml, or sets no
COMPOSE_FILE, must name compose.learn.yaml first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-03 00:28:33 +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
5727a15353
build(deploy)!: Learn's database password is LEARN_POSTGRES_PASSWORD, as Train's and Todo's are prefixed
Still the password of the instance's PostgreSQL superuser, halkyon_learn, which Learn's
server signs in as; only its name in deploy/.env changes. PostgreSQL's image still takes
it as POSTGRES_PASSWORD, its own variable.

BREAKING CHANGE: an instance's deploy/.env names it LEARN_POSTGRES_PASSWORD.

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
384fc38919
build(deploy)!: Learn's session pepper is LEARN_SESSION_KEY_HASH_PEPPER, as Train's and Todo's are prefixed
SESSION_KEY_HASH_PEPPER went to Learn's server alone, beside
TRAIN_SESSION_KEY_HASH_PEPPER and TODO_SESSION_KEY_HASH_PEPPER. The
pepper's value stays: changed, everyone would be signed out of Learn.
POSTGRES_PASSWORD keeps its name, as it is the instance's PostgreSQL
superuser's, not Learn's alone.

BREAKING CHANGE: an instance's deploy/.env must set
LEARN_SESSION_KEY_HASH_PEPPER (SESSION_KEY_HASH_PEPPER's value) before
this is deployed; without it Compose stops at once, and the deploy fails
before anything is switched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 18:56:38 +01:00
3d53610a87
build(deploy)!: Learn's domain is LEARN_DOMAIN, as Train's and Todo's are TRAIN_DOMAIN and TODO_DOMAIN
DOMAIN named the domain of Learn's site alone (Caddy's), beside the
prefixed domains of the other apps' compose files.

BREAKING CHANGE: an instance's deploy/.env must set LEARN_DOMAIN (DOMAIN's
value) before this is deployed; without it Compose stops at once, and the
deploy fails before anything is switched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 18:49:23 +01:00
3219742dbd
build(todo): Todo's Android key pinned (ADR 0033, step 5)
All checks were successful
CI / check (push) Successful in 40m36s
CI / deploy (push) Successful in 3m37s
The certificate of the key Todo's releases are signed with (alias
halkyon-todo), made in memory and kept in Proton Pass alone; the release
tool releases no APK signed otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 16:37:27 +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