apps/train with halkyon_train_server, _client and _flutter, as Todo's:
halkyon_base configured by trainBase(), its own register alone and a
member of the instance's where the operator names one, the app on
halkyon_app's gates with Train's frame, account sheet and About, texts
in British English and German that speak of workouts, app.halkyon.train
on Android, iOS, Linux, macOS and Windows.
Where it differs from Todo: development ports of its own (8180 to 8190),
so it runs beside Learn's server; one initial migration with the schema
Todo's migrations end at (the same tables, compared line by line), not
Todo's seventeen; and Todo's icon as a placeholder until the icon tool
of ADR 0031 draws Train's bike. The workspace, the licences, the READMEs
and AGENTS.md name it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The deploy of 46be9983 failed while building the server's image: the
Dockerfile copies halkyon_core since ADR 0021's step 1 (a060fbc2), but
.dockerignore still left it out of the build's context, and halkyon_llm,
which the module forwards the operator's assistants with since step 2,
was never copied at all. Nothing was switched: the instance kept running
v0.20.0's server.
CI never builds the images (its runner has no Docker, by design), so
test/deploy/images_test.dart now holds each Dockerfile to its package's
path dependencies, dev dependencies included as pub resolves them: every
one copied, kept in the narrowed workspace, and not left out by
.dockerignore. It failed on both before this fix. Built locally since:
the server's image builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
ADR 0017's step 1, on libsodium (sodium 4.1.0+1, whose build hook
compiles the libsodium 1.0.22 it ships: a C compiler and make wherever
it builds):
- Argon2id (64 MiB, 3 passes, kept with the account and never below
that) makes a master key, which libsodium's KDF splits into SRP's
secret (x) and the key-encryption key.
- The account's X25519 and Ed25519 key pairs, their private halves
wrapped with XChaCha20-Poly1305 under the KEK and under the recovery
key, bound to their public keys.
- A 24-word BIP-39 recovery phrase (the English list, MIT) and the
recovery key derived from it.
- Data keys sealed to the account with crypto_box_seal, bound to their
IDs; content in ADR 0002's envelope, encrypted with a data key and
signed with Ed25519 (detached), bound to a context the caller names.
- Secrets in libsodium's guarded memory where its API takes them.
- Tests: libsodium's Argon2id known answers, BIP-39's English vectors,
round trips and every refusal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
ADR 0017's step 1. Generic over group and hash (SrpGroup); production
takes RFC 5054's 3072-bit group with SHA-256. k = H(N | PAD(g)),
u = H(PAD(A) | PAD(B)), K = H(PAD(S)),
M1 = H(H(N) xor H(g) | H(I) | s | PAD(A) | PAD(B) | K),
M2 = H(PAD(A) | M1 | K). A and B must lie in 1..N-1 and u must not be
0; a and b are 256 bits from Random.secure(); proofs are compared in
constant time. x comes from the caller (halkyon_keys makes it); RFC
5054's own x is there for the vectors only.
- The register's half in halkyon_base_server (base.dart: srpChallenge,
srpVerify), the device's written by hand in halkyon_base_client
(srp.dart: SrpClient, srpVerifier). Serverpod's generator cleans and
writes only a client's lib/src/protocol/ (serverpod_cli 4.0.4's
_getDirectoriesRequiringCleaning; a generate leaves srp/ untouched).
- Tests: RFC 5054 Appendix B on both halves; the client against the
register, succeeding and every refusal.
- The module's tests depend on its client, and pub resolves every
member's dev dependencies, so Learn's server image takes the module's
client into its workspace too.
- The format check covers the hand-written half.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
- Melos 8.9 as the root's dev dependency, with scripts across the
workspace (analyze, test:dart, test:flutter, generate, check) and its
filters (--diff, --include-dependents); cli_util overridden to 0.5.2
for it, since Learn's flutter_launcher_icons asks for 0.4 and uses only
its logger, unchanged in 0.5.
- tool/check.dart in place of tool/check.sh: packages, formatting of our
own files, analysis, the tools' tests, then every package's tests
through Melos (--changed-since=<commit> for only what changed and what
depends on it). tool/pre-push is the hook: the signature check, then
the checks. CI generates every server's code and runs the same check.
- The images build from the root with a workspace narrowed to what they
need (tool/workspace_only.sh); compose, the deploy's migration test,
the snap, the release script, the editor's launch files and Learn's
asset tools (run from apps/learn) follow the new layout.
- The project's source is named as project-halkyon/halkyon (source link,
snap, F-Droid listing, release uploads); the AGPL packages carry the
licence's own text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A person with an identity and a public profile publishes a studyset with
everything inside it as its next release (ADR 0005, rule 3): the server
reads their synced rows and writes them through the git command as a
commit on main and the annotated tag vN, in a bare repository of the
studyset's own, named by a random public id made at the first publish.
A release is a directory per studyset with a file per card, note, path,
step and PDF (its id, position, tag names and payload as synced, keys
sorted and indented) and the files the content names, without what is
only the author's (a card's report, a PDF's deadline). Publishing is
refused without a licence allowing passing it on in every studyset ("no
derivatives" allowed), with revisions to settle, with files not uploaded
yet, when nothing changed, or for a name that won't do; the name is
fixed from the first release, as is the person's handle.
In the app, Share in a studyset's action row opens the Publish sheet;
publishing syncs first, and settles by itself the revisions that merge
cleanly, listing only real conflicts. The server's image gets git, the
repositories a volume of their own, and the hourly backup a git bundle of
each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
EDRDG's licence asks of a server showing its dictionaries that it update
them at least monthly. The instance now does it alone: a `dictionaries`
service in compose, built from the curator (halkyon_learn_curator/
Dockerfile, its own unprivileged user 10001, 1 GB at most), runs `curate
update --every 7`, downloading EDRDG's files of the day once its files are
a week old and replacing them in a volume the server mounts read-only.
The curator never enters the server's image or process (decided
2026-10-09: a container of its own, weekly). The server rereads the
folder daily at 04:07 (DictionaryReloadFutureCall), loading what changed,
and loads now run one at a time. redeploy.sh builds both images.
An operator who'd rather supply the files removes the service and mounts
deploy/dictionaries/ into the server, as compose.yaml says.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The curator:
- A word JMdict says is usually written in kana gets a kana headword,
even where the JLPT lists give kanji (その, not 其の), and a rare or
search-only spelling is never the headword while a usual reading
exists (そして, not 而して); 9,611 headwords change, 96 of them N5.
- Keys hold full-width letters as typed (Tシャツ as tシャツ), デュ/テュ/
フュ/イェ romanised, long a, i and e typed once as loose keys (kado for
カード), and a blank JLPT line is skipped.
The server:
- A search is one query ordered by tier, then rank: a loose or prefix
match no longer pushes an exact one out of the list (75-150 ms over
all of JMdict, before 150-450).
- Queries find long vowels with macrons spelt おお or ー and typed once
(ōkii, kōhī), ん typed n' or nn, and full-width letters.
- Pages of a label are ordered by rank and id, so entries that share a
rank are neither repeated nor skipped.
- The loader never throws (an unreadable folder ended the server from
the background), refuses two files of one source rather than loading
both at every start, reads no rows back when replacing a source, and
takes its time from clock.
- Dictionary files stay out of the Docker build context.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Caddy, the Serverpod server and PostgreSQL 16 in deploy/. Only Caddy publishes ports; the server and Postgres are reachable only inside the Compose network (Docker bypasses ufw, and the server listens on every interface). Secrets come from deploy/.env as environment variables; there is no production passwords.yaml, and .dockerignore keeps local secrets out of the build context.
The server image builds on Serverpod's template with an Ubuntu 24.04 runtime: complete glibc incl. libgcc_s for the SQLite libraries Serverpod 4 bundles, CA certificates, a non-root user, and exec so SIGTERM reaches the server. The scaffold's dev-only docker-compose.yaml is removed.
Verified locally: image builds, all native libraries resolve, no passwords.yaml in the image, migrations apply at start, Caddy serves 200, port 8080 unreachable from the host, Postgres in UTC.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>