Commit graph halkyon/packages
Author SHA1 Message Date
2535da15de
fix(app): a recovery phrase's and codes' files name the instance
Saved from several apps or instances, they differed by " (1)" alone. They
name the instance now, without the app's word, as an operator's apps share
one account: halkyon.eu-recovery-phrase.txt, localhost-8280-recovery-codes.txt.

Whats-New: A saved recovery phrase or recovery codes file now names its instance.
Whats-New-De: Gespeicherte Wiederherstellungsphrasen und -codes nennen jetzt ihre Instanz im Dateinamen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-05 22:55:28 +01:00
23d3bcb4ca
feat(app,learn): the device's appearance and language, halkyon_app's, as Learn's Settings show them
The developer asked for Todo, Train and Meditate to offer Learn's
Appearance and Language (ADR 0014's amendment of 4 Nov 2026). Their
model, store, cubit and card move into halkyon_app: Appearance and Look
(a BCP 47 tag, null for the device's language), FileLookStore keeping it
in the app's support folder as JSON written by rename (per device, not
synced, read before the first frame), LookCubit for the apps to provide
above InstanceGate, and LookSettingsCard / DeviceLookCard with the texts
moved from Learn's arbs (look*, the same words). Each app passes the
languages it speaks, named in their own language.

Learn keeps its stored choice where it was, among its DeviceSettings,
and feeds the shared card from them: no migration, so nobody's language
or appearance moves, and nothing changes on its screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-05 08:58:40 +01:00
bc4953aaf8
fix(learn,ui): the open assistant field holds still: no flicker, no sliding away, no resizing under the keyboard (ADR 0021, D69)
Inside a studyset the field flickered as it opened and closed: the
floating location switched between a raised centre and the centre,
which Scaffold animates by scaling the button out and in. It keeps one
location now, raised above the licence line by padding. While open it
no longer slides away as the studyset or the Library scrolls
(ScrollHidingFab.pinned, new), and the keyboard covers Home, the
Library, a studyset and a session rather than shrinking them; the field
lifts itself by as much of the keyboard as reaches above it.
NavigationFrame no longer resizes for the keyboard itself, leaving it
to each tab's own scaffold, which still does by default (Todo's,
Train's and Meditate's tests pass unchanged).

Whats-New: Inside a studyset, the question field opens without flickering, stays while you scroll, and the screen no longer jumps when the keyboard comes.
Whats-New-De: In einem Lernset öffnet sich das Fragefeld ohne Flackern, bleibt beim Scrollen stehen, und der Bildschirm springt nicht mehr, wenn die Tastatur kommt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-05 08:58:38 +01:00
b4b27a1d0c
feat(learn,app): the field's row as one, its model by name in its provider's colour, the microphone dictating (ADR 0021, D69)
The developer's review of 4 Nov 2026. Below the question, × and + stand
at the start, who answers is centred and the conversations and the
microphone stand at the end, every button the same rounded square, the
microphone listening and Send too. The row above the text ("Only what I
choose · 2 parts") goes, as +'s sheet offers the same choice; a web
search gets + with that row alone. Who answers shows its model's name
alone ('Haiku 4.5', AssistantProvider.modelName: Anthropic's without
"Claude", others as they are), outlined in its provider's colour (D60).
The microphone is halkyon_app's Dictation: what is said goes where the
cursor stands, and it listens again after each pause until stopped. It
gives way to Send once anything could be sent, as Claude's field does.

Whats-New: The question's buttons are in one row: close and + on the left, who answers in the middle, Conversations and the microphone on the right.
Whats-New: What goes along is chosen under +, for the web search too.
Whats-New: Who answers shows the model's name, in its provider's colour.
Whats-New: The microphone keeps listening after a pause, writing where the cursor is, until you stop it.
Whats-New-De: Die Knöpfe der Frage stehen in einer Reihe: Schließen und + links, wer antwortet in der Mitte, Unterhaltungen und Mikrofon rechts.
Whats-New-De: Was mitgeht, wählst du unter +, auch für die Websuche.
Whats-New-De: Wer antwortet, zeigt den Namen des Modells, in der Farbe seines Anbieters.
Whats-New-De: Das Mikrofon hört nach einer Pause weiter zu und schreibt, wo der Cursor steht, bis du es stoppst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-05 08:58:37 +01:00
2d27451cb8
fix(app,learn,meditate): a device without speech recognition says so, and how to get one
On GrapheneOS, which installs no recogniser, the microphone did nothing
and stayed listening: speech_to_text's initialize throws a
PlatformException (recognizerNotAvailable) there rather than answering
false, and DeviceSpeechInput let it through. A listen now answers how it
went (SpeechStart: listening, noRecognizer, unavailable) and never
throws; Learn's field and Meditate's dream say that no speech
recognition is installed and name two to install (FUTO Voice Input,
Google's speech services), in halkyon_app's texts. It also passes
androidNoBluetooth, as the apps declare no Bluetooth permission the
plugin would otherwise ask for.

Whats-New: Where the device has no speech recognition, the microphone says so and how to get one.
Whats-New-De: Wo das Gerät keine Spracherkennung hat, sagt das Mikrofon das und wie du eine bekommst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-05 08:58:37 +01:00
bcbefe38a3
feat(ui,train): a pull far enough to refresh ticks, lightly, as the navigation does
AppRefreshIndicator ticks (selectionClick, as a choice on the floating bar
or the rail) once the pull commits: as it snaps back to refresh, or as it
refreshes at once, whichever comes first, and not for a pull let go too
early. Train's list of days pulled to read the band used Flutter's own
RefreshIndicator, the older spinner; it takes the app's now, and the tick
with it.

Whats-New: Pulling a list down to refresh ticks lightly.
Whats-New-De: Eine Liste zum Aktualisieren herunterziehen gibt ein leichtes Tippen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-05 08:58:31 +01:00
3eb757c921
style(app): the profile page's link a row alone, without a Column around it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:32:12 +01:00
918080ce46
Merge branch 'learn-fb' into integration 2026-11-04 09:32:06 +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
10744bcd56
Merge branch 'train-maps' into integration 2026-11-04 09:32:03 +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
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
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
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
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
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
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
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
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
4898d0a04a
feat(learn): the assistant's conversation a dialog on the desktop and in expanded windows (Todo's ADR 0033, D11)
- halkyon_ui's sheetShowsAsDialog says whether showAdaptiveSheet shows
  a dialog here, for a sheet of its own that can't go through it.
- Learn's conversation, a half-height sheet dragged up, asks it: where
  sheets are dialogs it is a dialog up to 720 wide and as tall as the
  window, with no handle to drag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-01 18:20:58 +01:00
b908019969
feat(todo): dialogs on the desktop, the tabs reordered, a task's path and menu (ADR 0033, D11 to D14)
- halkyon_ui: showAdaptiveSheet shows a dialog on Linux, macOS and Windows
  at every width (bottom sheets stay on phones and tablets below expanded
  windows), so Learn and Train follow; SheetScaffold takes a heading in
  the title's place and may leave out its close button.
- Todo: the tabs are Inbox, Today, Upcoming, Browse, Today still opening;
  `serverpod start` launches the app on Linux, as it has no web build.
- A task's sheet shows where the task is (its project, an icon once tasks
  are above it, and the tasks above it, each opening what it names) and a
  ⋮ menu in place of its close button: when it was added (its UUID v7),
  Duplicate with its sub-tasks, hiding its done sub-tasks (synced in its
  payload), Close in the pane, Delete after asking. The description's icon
  stays by its first line.
- A row names its project and section at the end of its date's line; every
  menu entry has its icon.
- Learn: a studyset's path starts at the Library tab's icon, named by its
  tooltip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-01 16:53:38 +01:00
7727317c68
feat(app): what's new after an update, drafted by the release tool (ADR 0030, D61)
halkyon_app's WhatsNew reads what each release changed from the app's
assets/whats_new/ (<version>.<language>.txt, English and German, a change
a line): after an update a sheet lists every release since the one the
device showed last, newest first, once; Settings shows the running
version's again, above About. Nothing on a fresh install.

tool/release.dart drafts the missing texts from the commits that count
after "Go on?" (German marked TRANSLATE:), opens them in the editor until
they fit (no mark left, F-Droid's 500 characters) and commits them with
the version. The English is the Forgejo notes' What's new, each language
F-Droid's changelog of the build, written at publishing and copied in by
tool/fdroid_repo.sh. Learn and Train bundle their texts, Learn's 0.22.0
and Train's 0.1.0 written to start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-31 19:36:20 +01:00
581cf1f1cd
feat(ui): tabs folding into the title, in Learn's and Train's Profile (ADR 0030, D60)
halkyon_ui's FoldingTabs: the tab row under the app bar's title at the
top, scrolling away with the content, the chosen tab's name then small
under the title, which, tapped, scrolls back up to the row, as Strava's.
Learn's Profile (Profile, Statistics) is built on it, and Train's
Profile in tabs: Progress (your body, the last 12 weeks, this year),
Days, and Plans where the device doesn't record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-31 19:12:11 +01:00
7473366f66
feat(train): a recording under way above the bar on every tab, and a saved one opens its page (ADR 0030, D60)
halkyon_ui's NavigationFrame takes a status, standing above the floating
bar below medium windows and at the foot of the screen's panel from there
on. Train's RecordingBar fills it while a recording runs or waits to be
saved: its activity, moving time and distance, paused or stopped, a tap
back to it. Saving now opens the workout's page in Home's tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-31 12:12:55 +01:00
b45db6ea7c
feat(ui): every app's frame around its tabs, Learn's moved into halkyon_ui
NavigationFrame is Learn's AppShell without its router and its tabs: a
navigation rail from medium windows on with the app's icon on top,
leading to the first tab, a bar floating above the foot below, the
background of the screen in view around it, and a tick on every choice.
TabsStack is Learn's container of the tabs' navigators, every tab kept
alive and told whether it shows. Learn's shell is built on both, its
tests unchanged; Train's is next.

The rail's icon is drawn from 120 px at most in memory, as Learn's was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 23:52:39 +01:00
839d49c4cc
fix(train): no row's ID tells the instance a date: the days' IDs are keyed by the account (ADR 0030, D57)
A day's, night's and heart chunk's ID was a UUID of version 5 of the
person and the date, which the instance can hash for every date itself:
each ID told it which days the band was worn. halkyon_keys' keyedRowId
hashes the name under the account's X25519 secret key (BLAKE2b, a UUID of
version 8), the same on every device of the account and unreadable
without its keys; ContentCipher.rowId and TrainKeys.rowId hand it on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 16:12:01 +01:00
acb291afcb
feat(app): the recovery phrase's words typed back are suggested from BIP-39's list (ADR 0017)
Each of the three fields suggests the words of BIP-39's whole English
list beginning with what is typed, never the phrase's own, and refuses a
word not on it as its field is left, so that a typo is caught before the
words are compared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:53:10 +01:00
18f456eed9
feat(base,train): Train signs in with SimpleLogin, on ways back of its own (ADR 0020)
The forwarding page's table gains train-loopback (port 43825) and
train-scheme (app.halkyon.train://). Train's app had kept Todo's
loopback port 43824 from its shell; it now has its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:53:09 +01:00
39b43217f6
refactor(app): delayed deletions move into halkyon_app (ADR 0030, D24)
Learn's DelayedDeletions, unchanged, with its test, so that Train's
workouts can be deleted with an undo as Learn's items are. Learn's
deletions import it from halkyon_app now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:52:50 +01:00
3486932cbe
refactor(app): the payload envelope moves into halkyon_app (ADR 0030, D15)
Learn's PayloadEnvelope moves unchanged into halkyon_app, so that Train
seals its rows with the same envelope; Learn imports it from there. It
gets tests of its own: a payload sealed for its table, row, column and
item opens there alone, a changed one or one under a key not open never
does, and a readable one is trusted only signed by the account where it
was signed for.

Learn's app tests haven't run with the move (its build needs clang,
missing on this machine); analysis passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:52:46 +01:00
a0f2ea8de5
fix(base): the test of resets shown to the owner's devices asks the sign-in ways on its own clock
All checks were successful
CI / check (push) Successful in 36m43s
CI / deploy (push) Successful in 2m13s
Its clock is fixed at 20 Oct 2026 and the reset it asks lapses a week
after taking effect, so asked on the real clock it was gone from 30 Oct.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:52:36 +01:00
fe2b4389b7
fix(base): the test of a reset taken effect asks the sign-in ways on its own clock
All checks were successful
CI / check (push) Successful in 35m57s
CI / deploy (push) Successful in 2m21s
It read the real clock, on which the reset had lapsed a week after taking effect,
so it failed from 27 Oct 2026.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-29 15:51:21 +01:00
cc87120bc8
feat(app): the operator's overview of screens, grants to several people at once with one budget each, assistants by provider
Halkyon Learn's ADR 0021, D57 to D63, on halkyon_base's budgets and
effort:

- The operator's screen is an overview (Waiting, Operators, Accounts,
  Assistants, each with its count) whose rows open screens of their own,
  read 50 at a time as they scroll (OperatorListCubit, OperatorPage), so
  hundreds of accounts push nothing else down (D62).
- A handle is picked among the accounts there are (AccountSuggestionField):
  appointing an operator and granting take no other (D61).
- AssistantGrantsScreen replaces AssistantGrantsCard: the assistants set
  up by provider, with their efforts and prices; the people granted, found
  by their handle's start; one sheet granting several assistants, a
  provider's at once, to several people, each with one budget in money and
  tokens a day and a month, or changing one person's (D58, D59).
- Assistants are grouped by provider wherever they are listed, each under
  its name with a dot in its colour (ProviderLabel, D60).
- AssistantsCard shows what is left of the person's budget for all their
  assistants together; the months of the figures run oldest first (D57).
- AssistantChoice says the efforts a question may ask at and the ceiling
  on output each asks for (D63); a person's own key gets none and keeps
  4,096 tokens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 16:21:53 +01:00
24ed63585e
feat(base)!: one budget per person for all their assistants, the effort each takes, the operator's lists by page
A grant now says only which assistant a person may use, and keeps their
agreement; the limits are the person's (assistant_budget): one per UTC
day and month in money and in tokens, each optional, money counting the
assistants with prices, tokens every one (Halkyon Learn's ADR 0021, D58,
D59). assistantOperator.grant gives several people several assistants
with one budget, change sets one person's exactly, revoke takes back
everything; grantees and granteesOf list people with their assistants,
budget and use, 50 at a time. The migration carries each person's grant
limits into a budget: per unit, their sum, a grant without a limit making
it none.

assistants.env gives each assistant a default and a highest effort
(_EFFORT, _MAX_EFFORT; refused at start where the provider takes none);
ask takes one within that range, refusing another (invalidEffort), and
caps the output at its ceiling (D63).

instanceOperator.waiting and accounts come 50 at a time from an offset,
and counts gives the rows of the operator's screen (D62).

BREAKING CHANGE: the offer, the grants and the operator's lists changed
shape; Learn's server now asks for build 25.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 16:21:53 +01:00
0590a4b43d
feat(llm): a request carries how much the model thinks, in each provider's field
LlmRequest.effort (LlmEffort: low, medium, high, xhigh, max, Anthropic's
names) goes as Anthropic's output_config.effort, as reasoning.effort to
the Responses API and OpenRouter, and as reasoning_effort to OpenAI's chat
completions, Gemini and any other host. endpoint.efforts says which
levels a host takes (Gemini low to high, Mistral none); send refuses
another before anything goes, the fake client too. LlmEffort.outputCeiling
gives the room for output each level wants, as thinking counts towards
the ceiling (Halkyon Learn's ADR 0021, D63).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 16:21:53 +01:00
46be998329
fix(learn): an editor with nobody signed in above it checks nothing
Some checks failed
CI / check (push) Successful in 10s
CI / deploy (push) Failing after 7s
The full window editor is also shown without a person's scope, as a
privacy test pumps it, where GrammarHost asserted for want of one.
halkyon_app's PersonScope gains maybeOf beside of.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:11 +01:00
6e63312bfa
docs: the assistant answering in Learn, as step 3 built it
Learn's AGENTS.md describes the field with the assistants answering in it
(ADR 0021, step 3): who answers, the scope, the conversation sheet and its
lookups, images, private studysets' warnings, kept conversations and the
tasks; and the testing trap of awaiting a finished subscription's cancel.
The shared packages' READMEs name providerTermsOf and onThisMachine.
Learn's README no longer says there is no end-to-end encryption, which
private studysets changed (ADR 0019).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:09 +01:00
bf46f8c460
feat(learn): a private studyset goes to an assistant only once agreed
ADR 0021, D9 and D17. Before a question sends a private studyset's
content to a remote model, through the parts chosen or a wider reach
with private studysets switched on, a warning names the studysets, the
provider's host and its terms (where checked: Anthropic, OpenAI,
Google, OpenRouter), and is asked once per studyset and assistant: the
agreement is kept in the person's sealed settings (schema 13,
privateAgreements, folded only where both rows hold it), shown in the
studyset's privacy dialog with Take back, and marked on the folded
button while it holds. Follow-ups and Try again ask again once taken
back, as the scope keeps where the parts came from (partsFrom). No
warning for an own assistant on this machine (loopback or localhost,
halkyon_llm's LlmEndpoint.onThisMachine); the local network may be
shared.

Texts that step 3 made untrue: the consent sheet now says what an
assistant looks up goes too and that conversations are kept encrypted
for the person's devices; the privacy dialogs no longer mention
showing a studyset to Claude; the privacy page says what is looked up
and when a private studyset goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:09 +01:00
36899cdc3b
feat(app,learn,todo): the assistants in Settings and on the operator's screen
ADR 0021, step 2's app half. halkyon_app asks the module's assistant
endpoints (ServerpodAssistantGateway, ServerpodAssistantOperatorGateway)
and a person's own provider directly (halkyon_llm), behind
AssistantRepository: an AssistantChoice is a GrantedAssistant, asked
through the instance, which counts it, or a PersonalAssistant with the
person's own key, which the instance never sees. assistant_messages.dart
turns a conversation into the endpoint's models and back.

AssistantsCard, for an app's settings: what is granted and left of its
limits, agreed to once on a sheet saying what goes where, the person's
use by UTC month, the instance's privacy page, and their own assistants
where the operator allows own keys (kept ones stay listed, unasked,
where it doesn't). AssistantGrantsCard on InstanceOperatorScreen: grants
by handle with their limits in the pricing's unit, changed and taken
back after asking, everyone's use by month; a found account's menu
grants too. Nothing shows where nothing is offered or can be read.
package:halkyon_app/assistant_models.dart holds the models in pure Dart.

Learn keeps own assistants in the person's settings (ownAssistants,
schema 12, SettingsOwnAssistants); SettingsRepository.update refuses
(OwnAssistantRefused) to write a key where the settings would not be
sealed (SettingsGateway.seals). Learn depends on halkyon_llm directly.
Todo wires the gateways; a member offers no assistant. No feature asks
an assistant yet (step 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:07 +01:00
1e5d7040fd
docs(llm): the library doc names the Responses API
It still named two protocols after D46 added the third.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:07 +01:00
2da6555082
fix(base): the German privacy page names no "Betreiber"
Halkyon's German texts say "Admin" for the role and "wer die Instanz
betreibt" for whoever runs it, as the app's do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:07 +01:00
dbae802915
feat(base,learn,todo): the operator's assistants, and the instance's privacy page (ADR 0021, step 2)
The server half of the assistants (points 4 to 8, D27 to D46): the
operator sets language models in deploy/assistants.env, grants them to
people with limits, and the module forwards their questions with the
operator's key, counting what each used, never what was asked.

- assistantsFrom: ASSISTANTS, ASSISTANT_ID_KEY, ASSISTANT_<ID>_NAME,
  _PROTOCOL (anthropic, openai, openai-responses), _BASE_URL, _MODEL,
  _KEY, _IMAGES, _PRICE_INPUT and _PRICE_OUTPUT (per million tokens),
  ASSISTANT_CURRENCY, ASSISTANT_OWN_KEYS; anything unusable stops the
  server as it starts, naming the variable.
- The assistant endpoint (identified): offered, agree (D40), ask as a
  Serverpod stream (D35): the text as the model writes it, then the
  whole turn, sent once the request is counted; figures, the caller's
  own. Checked in order: at most 2 questions on their way per person
  (tooManyAtOnce), the grant, the agreement, limits in the pricing's
  unit now (limitOutdated), the month's and the day's limits (with when
  they reset, UTC), the question (malformed, imagesNotRead, tooLarge
  past 400,000 characters, an image over 3,932,160 bytes or more than
  20 images); output capped at 16,384 tokens. Each request carries the
  person's opaque ID (HMAC-SHA256 under ASSISTANT_ID_KEY, D30). Once
  sent, the provider's reply is read to its end even if the app
  cancels, and counted on a session of its own; a provider's failure is
  logged by kind and status, told as providerKey, providerBusy (with
  its wait), providerRefused or providerFailed.
- The assistantOperator endpoint (identified and operator, on the
  register): setUp, grants, grant (assistantNotSet, invalidLimit,
  notFound), revoke, figures of a UTC month.
- Tables assistant_grant and assistant_figures (per person, assistant
  and UTC day), in the module's, Learn's and Todo's migrations. The
  daily pruning keeps this month and the two before
  (forgetOldAssistantFigures, D41); deletePerson deletes both.
  A member offers no assistant (D43).
- instance.basics() gains assistantOwnKeys.
- /privacy (D39, addPrivacyPageRoute, on the register's web server,
  before the profile pages' route): made from the instance's settings,
  English or German, no JavaScript: who may sign up, where the data and
  backups are, what the module keeps (the account, sessions, IP
  addresses, the server's log), the host's privacyPagePoints (Learn's:
  studysets synced, private ones encrypted on the devices with what
  stays readable, what is published, dictionary lookups, the learner
  count), each assistant with its model and host, what goes and what is
  counted, and whether personal keys are allowed. Never a key.

Tests: 70 on the assistants (grants, limits, refusals in order,
streaming, cancelling, counting, concurrency, the operator's denials,
deletion, pruning), 19 on the settings, 8 on the privacy page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:06 +01:00
038aca5439
feat(llm): halkyon_llm, language models' APIs streamed over plain HTTP (ADR 0021, step 2)
A pure Dart package, no SDKs, one interface (LlmClient) over Anthropic's
Messages API, OpenAI's chat completions, in each known host's dialect and
the local servers' (D45), and OpenAI's Responses API, stateless (D46). A
model's turn keeps what its provider wants back with the tools' results:
Anthropic's thinking blocks, the Responses API's reasoning items and
message phase, Gemini's thought signatures. Failures come by kind, the
provider's words cut and without keys.

Tested on replies written from the providers' documentation, replayed in
pieces of 1, 7 and 4,096 bytes; tool/record_llm.dart records real ones
with a key of one's own, which the tests then run on too (D36).
FakeLlmClient for the code that uses it. BSD-3-Clause, as halkyon_oidc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:06 +01:00
31b9c17850
feat(app,learn,todo): the operator's "This instance" card, screen and badge (ADR 0021, step 1)
Whoever operates the instance approves, refuses and suspends accounts
and names other operators in the app (D21 to D24, D34):

- halkyon_app: InstanceOperatorRepository on
  ServerpodInstanceOperatorGateway (the module's instanceOperator
  endpoint; refusals as OperatorFailed: notFound, isOperator,
  lastOperator, notAllowed for 401 and 403, failed), in AppDependencies.
  OperatingCubit counts the accounts that wait: asked as the person's
  app opens, after every change, and every 5 minutes only while they
  operate the instance, so nobody else's app keeps asking.
  InstanceOperatorCard (or its tiles) and WaitingAccountsBadge show
  nothing at all for anyone else. InstanceOperatorScreen, laid out as
  Settings: the accounts that wait, each showing its four words to read
  out before Approve (D33), Refuse for new ones after asking; who
  operates, Take back after asking, the last always kept; Name an
  operator by handle; Find an account by the start of its handle, with
  Suspend after asking and Name as operator. English and German texts
  ("Admin" as the role word in German).
- Learn: the card right after the Sync card in Settings, which the
  cogs' count leads to as their dot leads to Sync; the count on the
  Settings cogs of Home and Profile.
- Todo: the tiles in the account sheet and the count on its button.
  Todo's server is a member, whose `operating()` is always null, so
  nothing shows there until it holds the register.

Tests: the gateway's mapping, the repository, both cubits with their
denials, the card, the badge and the screen; Learn's Settings shows the
card and the count to an operator and neither to anyone else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:05 +01:00
7ac715cd97
feat(app): a wait screen for accounts that wait for the operator (ADR 0021, step 1)
An account signed in or up to on an instance whose operator approves
new accounts, or one suspended, gets no session but a wait: the device
keeps its pass, words and handle in the keyring per instance
(SecureAccountWaitStore, InstanceStorage.waitOf), the auth status is
Waiting, and SessionGate shows WaitScreen in its own app, so the start
screen's sheets go with it. Every sign-in flow hands over to it: the
password's sign-in, sign-up, reset and account reset, SimpleLogin's
sign-in, sign-up and mending; their cubits show no problem then.

The wait screen shows the handle, the four words the operator reads out
for the person to match, and that Halkyon never asks anyone to read them
out (D33); it asks where the wait stands as it opens, every 30 seconds,
as the app comes back and when asked. A new account can be deleted with
its pass, a suspended one can't (ADR 0017). Approved, it leads to the
usual sign-in, once more as before (D31); a pass the instance knows no
more ends the wait. The start screen says where new accounts wait for
approval, and offers no Create an account where the operator takes none;
both refusals (closed, too many waiting) have their words, en and de.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:05 +01:00
a060fbc21d
feat(base)!: sign-ups as the operator says, waiting accounts, operators (ADR 0021, step 1)
SIGN_UP takes open (unset), approval or closed; anything else stops the
server as it starts. Closed refuses a SimpleLogin sign-up before its
handle is counted, an address sign-up only once the code has proved the
address, so nothing tells whether an address has an account. Approval
lets a new account wait, at most ten at once: no session is issued for a
waiting or suspended account, only a pass (32 random bytes, kept as its
SHA-256, 30 days, five per account) and four BIP-39 words the operator
reads out to match it. The pass looks, forgets, and deletes an account
that was never approved; nothing else.

Operators hold the scope operator, given first by SQL (D32), then named
in the app: InstanceOperatorEndpoint lists, approves and refuses waiting
accounts, finds accounts, suspends them (ending their sessions), and
names and takes back operators, never the last. account.operating()
counts the waiting for the badge. Members ask the register (five new
operations, a refusal kind signUp), and say closed while it is out of
reach. Never-approved accounts go after 14 days in the daily pruning.

The migration only adds the tables waiting_account and waiting_pass, so
the server before it keeps working on the new schema.

BREAKING CHANGE: a register and its members must run this version
together: a member asks the register operations an older one doesn't
know.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:05 +01:00
b2aa7667c4
refactor(core,keys): BIP-39's English list moves into halkyon_core
The list a recovery phrase's words are of moves from halkyon_keys into
halkyon_core, exported as bip39English, so that the server module can
take a waiting account's matching code from it (Halkyon Learn's ADR
0021, D33) without depending on halkyon_keys and libsodium. Its test
(2048 words, sorted, the published file's SHA-256, unique by their first
four letters) moves along; its MIT licence too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:05 +01:00
4e33133733
feat(base,app)!: GitHub signs nobody in any more (ADR 0020, D36)
Every account signs in with SimpleLogin or a password now, so GitHub
goes from the module, the shared app package, Learn and Todo, ahead of
ADR 0017's GITHUB_SIGN_IN_ENDS.

- halkyon_base: no `gitHubSignIn` endpoint, GitHub identity provider or
  GitHub refusals; no `githubClientId`/`githubClientSecret`, no
  `GITHUB_SIGN_IN_ENDS` and no `forgetGitHubAccounts`; the register
  loses `account-with-github`. `SignInWays.gitHub`, the basics'
  `gitHubClientId` and `gitHubSignInEnds`, and `PasswordRefusal`'s
  `signInAgain` and `noAddress` go. A first password takes a code
  mailed to the person's own address; an account without one sets none
  (`wrongCredentials`).
- Learn's server: `instance.info()` without `gitHubClientId`, the
  pruning without GitHub, and `minimumAppBuild` 24, as builds up to 23
  (v0.20.0) require `SignInWays.gitHub`: the server deploys with the
  release of build 24.
- halkyon_app: no GitHub button or problems on the start screen, no
  password offered after a GitHub sign-in, and `ServerpodAuthGateway`
  without the browser half only GitHub used (`(client, base:)`). The
  account section offers a first password only to an account with an
  address. `LoopbackWebAuth` and the apps' schemes stay, SimpleLogin's
  ways back.
- Learn's Profile lists no GitHub; the texts go (English, German).
- Serverpod's `serverpod_auth_idp_github_account` stays, as its
  migrations make it; a row goes with its account, and ADR 0020's D36
  gives operators the SQL to delete those left.
- deploy/: compose.yaml and .env.example without GitHub. The READMEs,
  AGENTS.md files and ADR 0017 amended.

BREAKING CHANGE: builds up to 23 can't read the instance's answers
about an account's ways in; minimumAppBuild is 24.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:16:39 +01:00
1b79214764
feat(app): a reset of the keys without the phrase, its banner, and the keys' proof key for a password that signs in (ADR 0020, D31–D35)
All checks were successful
CI / check (push) Successful in 31m14s
CI / deploy (push) Has been skipped
SimpleLogin's sheet, its password and its recovery phrase both lost:
under the lost-password step's phrase field, Lost your recovery phrase
too? says what stays and what is lost and asks for new keys
(`keys.startReset`) with the sign-in of a moment ago, which the device
then drops. The next sign-in says when while it waits, and once due
offers new keys, the sign-up's own password and phrase steps
(`keys.completeReset`), or the password, which calls it off (D31).

Every app's home shows `KeysResetBanner` where "too old" shows, and the
account section's SimpleLogin row the same words (`KeysResetNotice`),
each with Call it off, the password's proof alone
(`keys.callOffResets`, D32), and, once due on a device that holds the
keys, Finish here, which wraps the same keys anew (D33, D35). Both show
only a reset of an account that signs in with SimpleLogin alone; ADR
0017's resets show in its own section, which now shares `AlertBox` with
them.

An account whose password signs in has no Forgot your password? there,
and is asked its password and a code once while its keys lack the proof
key, at a sign-in or on adding SimpleLogin (`password.setProofKey`,
D34). `PasswordUnlock` carries the proof key, which its forgetting
leaves, and the keys' password proves for a purpose given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 23:24:44 +02:00
fe2ef02c92
feat(base): a reset of the keys without the phrase, and the keys' proof key for a password that signs in (ADR 0020, D31–D34)
An account that signs in with SimpleLogin alone, its password and its
recovery phrase both lost, asks for new keys with a SimpleLogin sign-in
(`keys.startReset`); another sign-in carries it out 72 hours later
(`keys.completeReset`, `notDue` with when before), with the same keys
rewrapped where the device still holds them (D33). No mail: every
signed-in device shows it, and any right proof of the password's or the
phrase's key, or a mend, calls it off; `keys.callOffResets` takes the
password's proof alone (D32). The row is ADR 0017's account reset of kind
`account`, so no migration; ADR 0017's reset by mail now takes only an
account whose password signs in.

An account with ADR 0017's password that added SimpleLogin had keys but
no proof key, so no new device signed in with SimpleLogin could open
them: `password.setProofKey` sets the one that same password makes, with
the owner's proof binding it (D34), and ADR 0017's new password takes it
away. Only an account that signs in with SimpleLogin alone changes
through `keys`. `simpleLogin.complete` says whether the password signs
in, whether the keys have the proof key, and when a reset takes effect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 23:24:44 +02:00