feat(deploy): Docker Compose stack for the instance
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>
2026-09-27 17:18:31 +02:00
|
|
|
# Copy to deploy/.env, restrict it (chmod 600), and fill in. deploy/.env is git-ignored
|
2026-10-13 23:11:40 +02:00
|
|
|
# and never goes into an image. How it reaches the server is decided when the server is
|
feat(deploy): Docker Compose stack for the instance
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>
2026-09-27 17:18:31 +02:00
|
|
|
# set up (e.g. rendered from Proton Pass on the deploying machine and copied over SSH,
|
|
|
|
|
# so the server itself never holds Proton credentials).
|
|
|
|
|
|
2026-10-13 23:11:40 +02:00
|
|
|
# `http://localhost` for the local rehearsal; `learn.halkyon.eu` on its server.
|
feat(deploy): Docker Compose stack for the instance
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>
2026-09-27 17:18:31 +02:00
|
|
|
DOMAIN=http://localhost
|
|
|
|
|
|
feat(profile): profile pages on the account domain, and links verified by rel="me"
A profile now has a page on the web, at the root of the account domain
(https://halkyon.eu/@raisondetredev where the operator set ACCOUNT_DOMAIN,
else on the instance's web server), so the address belongs to the
identity rather than to Learn and stays when other services share it
(ADR 0011). The page shows what the app's profile sheet shows, in English
or German, without JavaScript or cookies, asks search engines not to list
it, marks every link rel="me" so other sites can verify links to it, and
sends a visitor who asked on another host to its own address. Caddy
forwards /@… to the web server, and serves nothing else on the account
domain; compose now passes ACCOUNT_DOMAIN on, which it never did.
A link is verified as Mastodon verifies one: its page links back to the
profile's with rel="me". profile.checkLinks fetches each link's page, at
most ten times an hour, over HTTPS on port 443 and to public addresses
only, connecting to the address it checked and verifying the certificate
for the host, with three redirects, 1 MiB and ten seconds at most, and
keeps what links back on the profile (verifiedLinks, a column of a table
no device syncs). A changed link loses its check, a new handle all of
them. The app shows the page's address to copy, checks a link saved by
itself and on Check links, and marks verified links in the form and in
others' profiles; a check answered after a newer save only adds its
checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-15 20:29:30 +02:00
|
|
|
# The domain people's handles are on (`raisondetredev@halkyon.eu`) and their profile
|
|
|
|
|
# pages (`https://halkyon.eu/@raisondetredev`, ADR 0011), when it isn't DOMAIN's host:
|
|
|
|
|
# point its DNS (A and AAAA) at this server, and Caddy serves the profile pages there,
|
|
|
|
|
# nothing else. Unset: DOMAIN's host. Decide before anyone follows a handle: it can
|
|
|
|
|
# never change once the fediverse knows it.
|
|
|
|
|
#ACCOUNT_DOMAIN=halkyon.eu
|
2026-10-19 23:10:08 +02:00
|
|
|
|
|
|
|
|
# The instance's other services (optional, ADR 0016), such as Halkyon Todo: Halkyon
|
|
|
|
|
# Learn holds the instance's register of accounts, so an address, a GitHub account and a
|
|
|
|
|
# handle are one person in every service, and deleting the account deletes it in all.
|
|
|
|
|
# The register learns when someone signs in to which service (the privacy page says so).
|
|
|
|
|
# INSTANCE_SECRET is the secret the services share, carried by every call between them:
|
|
|
|
|
# the same value in each service's environment, never given to a device. Empty: no
|
|
|
|
|
# service calls another. Generate: openssl rand -hex 32
|
|
|
|
|
#INSTANCE_SECRET=
|
|
|
|
|
# The web servers of the other services as this server reaches them on the instance's
|
|
|
|
|
# own network (never through the internet), separated by commas or spaces
|
|
|
|
|
# (`http://todo-server:8082`): told when an account goes, so each deletes that person's
|
|
|
|
|
# data too, and told again hourly until each answers. Each of them sets, in its own
|
|
|
|
|
# environment, the same INSTANCE_SECRET and ACCOUNT_REGISTER_URL, this server's web
|
|
|
|
|
# server as it reaches it (`http://server:8082`). Learn never takes ACCOUNT_REGISTER_URL:
|
|
|
|
|
# it doesn't start with one, as it holds the register.
|
|
|
|
|
#INSTANCE_MEMBERS=
|
feat(profile): profile pages on the account domain, and links verified by rel="me"
A profile now has a page on the web, at the root of the account domain
(https://halkyon.eu/@raisondetredev where the operator set ACCOUNT_DOMAIN,
else on the instance's web server), so the address belongs to the
identity rather than to Learn and stays when other services share it
(ADR 0011). The page shows what the app's profile sheet shows, in English
or German, without JavaScript or cookies, asks search engines not to list
it, marks every link rel="me" so other sites can verify links to it, and
sends a visitor who asked on another host to its own address. Caddy
forwards /@… to the web server, and serves nothing else on the account
domain; compose now passes ACCOUNT_DOMAIN on, which it never did.
A link is verified as Mastodon verifies one: its page links back to the
profile's with rel="me". profile.checkLinks fetches each link's page, at
most ten times an hour, over HTTPS on port 443 and to public addresses
only, connecting to the address it checked and verifying the certificate
for the host, with three redirects, 1 MiB and ten seconds at most, and
keeps what links back on the profile (verifiedLinks, a column of a table
no device syncs). A changed link loses its check, a new handle all of
them. The app shows the page's address to copy, checks a link saved by
itself and on Check links, and marks verified links in the form and in
others' profiles; a check answered after a newer save only adds its
checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-15 20:29:30 +02:00
|
|
|
|
feat(deploy): sidecars beside the instance, Jellyfin the first, behind an edge network
An instance may now run services beside itself, each a compose file of
its own switched on in deploy/.env (COMPOSE_FILE=compose.yaml:compose.
jellyfin.yaml), so the deploy runs them with the instance. The first is
Jellyfin, which people watch and listen to learn from: the instance then
suggests it (JELLYFIN_URL from JELLYFIN_DOMAIN), and Caddy serves its
site, refusing every change to a user from outside, so a passwordless
demo account stays everyone's; its admin signs in through an SSH tunnel.
The instance's compose gets a second network, `edge`, for Caddy (at a
fixed address, which sidecars trust as their proxy) and the server;
Postgres stays on the internal one. Caddy loses its needless way to
Postgres, and a sidecar never gains one. A sidecar publishes no port,
as the CI runner may reach 127.0.0.1, and runs as a user ID of its own.
Caddy imports the sidecars' sites from sidecars/, mounted by their
compose files (none without), and redeploy.sh recreates it when one
changes. sidecars/README.md has the steps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 00:16:53 +02:00
|
|
|
# Sidecars switched on beside the instance (sidecars/README.md), each by its compose file
|
|
|
|
|
# after the instance's own: compose.jellyfin.yaml, a Jellyfin people watch and listen to
|
|
|
|
|
# learn from. Unset: the instance alone.
|
|
|
|
|
#COMPOSE_FILE=compose.yaml:compose.jellyfin.yaml
|
|
|
|
|
|
|
|
|
|
# With the Jellyfin sidecar: the domain it answers at, which the instance then suggests
|
|
|
|
|
# to its people (the app's Jellyfin sign-in fills it in).
|
|
|
|
|
#JELLYFIN_DOMAIN=jellyfin.example.org
|
|
|
|
|
|
feat(deploy): Docker Compose stack for the instance
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>
2026-09-27 17:18:31 +02:00
|
|
|
# Postgres password, shared by the postgres and server services.
|
|
|
|
|
# Generate: openssl rand -base64 32
|
|
|
|
|
POSTGRES_PASSWORD=
|
|
|
|
|
|
|
|
|
|
# Pepper for hashing session keys (server-side sessions). NEVER change it once
|
|
|
|
|
# sessions exist: everyone would be signed out, and anonymous accounts can't sign
|
|
|
|
|
# back in. Rotation only via fallbackSessionKeyHashPeppers.
|
|
|
|
|
# Generate: openssl rand -base64 32
|
|
|
|
|
SESSION_KEY_HASH_PEPPER=
|
feat(server): GitHub sign-in that keeps only the GitHub ID
As decided, a GitHub sign-in keeps nothing but the numeric GitHub ID, so the login is our own, on Serverpod's GitHub helpers (lib/src/auth/github.dart): Serverpod's stock login also stores the name and email in a profile and downloads the avatar, and its endpoint is never exposed. The server exchanges the authorization code with the client secret, reads the ID, and drops the access token.
GitHubUpgradeEndpoint links the GitHub ID to the signed-in anonymous person, grants identified (to their existing sessions too) and removes the anonymous identity, in one transaction: the person, their spaces and their studysets stay the same. GitHubSignInEndpoint gives a session for a person who has the GitHub account already; it's called without a session, since Serverpod issues no session for another person inside a signed-in call, and it never creates people. InstanceEndpoint.info() tells the app the instance's GitHub client ID. Refusals are GitHubSignInRefusedException with a reason: GitHub sign-in off here, the account taken by another person, another GitHub account linked already, an unknown account.
GitHub sign-in is on only where the passwords hold githubClientId and githubClientSecret (empty counts as missing, as Compose passes unset variables on as empty strings); production takes them from GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET in deploy/.env. grantIdentified can now join a caller's transaction. No migration: the GitHub accounts table comes with the auth module.
Tests, refusals first: upgrading when not signed in (checked by switching the guard off: the test fails), both ways without a GitHub app, an account taken by someone else with nothing changed, a second GitHub account, an unknown account with nobody created; then linking keeps the person and reaches their sessions and stores no email, linking twice changes nothing, signing in reaches the right person.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 13:35:51 +02:00
|
|
|
|
|
|
|
|
# GitHub sign-in (optional): the client ID and secret of this instance's own GitHub
|
|
|
|
|
# OAuth app (GitHub → Settings → Developer settings → OAuth Apps), registered with the
|
|
|
|
|
# callback URLs app.halkyon.learn://github-callback and http://127.0.0.1/github-callback.
|
|
|
|
|
# Empty: GitHub sign-in is off, and the app doesn't offer it.
|
|
|
|
|
GITHUB_CLIENT_ID=
|
|
|
|
|
GITHUB_CLIENT_SECRET=
|
2026-09-30 16:19:46 +02:00
|
|
|
|
feat(auth): codes go by mail, through the SMTP service an instance sets up
Email sign-in's codes are mailed over SMTP (pub.dev's mailer), through
whichever service the passwords name: emailSmtpHost, emailSmtpPort,
emailSmtpUser, emailSmtpPassword and emailFrom (EMAIL_SMTP_* and
EMAIL_FROM in deploy/.env, with EMAIL_SIGN_IN_PEPPER). TLS only:
STARTTLS, or TLS directly on 465. learn.halkyon.eu sends through Proton's
SMTP submission. Without a service, codes still go to the log in
development, and elsewhere there is no email sign-in.
The mail is plain text, in English or German: the code first and in the
subject, so it shows in the notification and the system can offer it,
the instance it signs in to, its 15 minutes, and that it can be ignored;
no links, no tracking, no HTML. A mail that couldn't be sent is a refusal
of its own (mailFailed): nothing of the request is kept, and the log
names the error's type only, as the service's answer may name the
address. A fake SMTP server on loopback drives the real mailer in the
tests: it delivers, and nothing goes to a server without TLS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-15 20:00:52 +02:00
|
|
|
# Email sign-in (optional, ADR 0010): people sign in with a code mailed to their address,
|
|
|
|
|
# of which the instance keeps only a keyed hash. Off unless the pepper and all of the SMTP
|
|
|
|
|
# service's settings are set; the app then doesn't offer it.
|
|
|
|
|
# The pepper: NEVER change it once anybody signs in with an address, as every address's
|
|
|
|
|
# hash would be lost, and with it those people's way in.
|
|
|
|
|
# Generate: openssl rand -hex 32
|
|
|
|
|
EMAIL_SIGN_IN_PEPPER=
|
|
|
|
|
# The SMTP service codes go through, over TLS only (STARTTLS, or TLS directly on 465).
|
|
|
|
|
# Proton (a paid plan with an address on your own domain): smtp.protonmail.ch, 587, the
|
|
|
|
|
# address as the user, an SMTP token made for it in Proton Mail's settings (IMAP/SMTP) as
|
|
|
|
|
# the password, and the same address as EMAIL_FROM. Some hosts block outgoing 25 and 465;
|
|
|
|
|
# 587 gets through.
|
|
|
|
|
EMAIL_SMTP_HOST=
|
|
|
|
|
EMAIL_SMTP_PORT=587
|
|
|
|
|
EMAIL_SMTP_USER=
|
|
|
|
|
EMAIL_SMTP_PASSWORD=
|
|
|
|
|
# The address codes come from (shown as "Halkyon Learn"); its domain needs SPF, DKIM and
|
|
|
|
|
# DMARC records for the service, or mail lands in spam.
|
|
|
|
|
EMAIL_FROM=
|
|
|
|
|
|
2026-10-20 13:32:21 +02:00
|
|
|
# Password sign-in (ADR 0017): an address, a password that never leaves the device, and
|
|
|
|
|
# a code from an authenticator app. Off unless all three keys and email sign-in are set
|
|
|
|
|
# up (resets go by mail); each key at least 32 characters, or the server doesn't start.
|
|
|
|
|
# They stay outside the database, so a copy of it alone gives none of this away:
|
|
|
|
|
# TOTP_KEY seals the authenticators' secrets: changing it makes every authenticator
|
|
|
|
|
# useless (people then reset theirs, 72 hours, by mail).
|
|
|
|
|
# RECOVERY_CODE_KEY keys the recovery codes' hashes: changing it voids every recovery
|
|
|
|
|
# code (people make new ones once signed in).
|
|
|
|
|
# SIGN_IN_DECOY_KEY makes the answers for addresses nobody has look like an account's:
|
|
|
|
|
# changing it lets someone who watched those answers tell which addresses are unused.
|
|
|
|
|
# Generate each: openssl rand -hex 32
|
|
|
|
|
TOTP_KEY=
|
|
|
|
|
RECOVERY_CODE_KEY=
|
|
|
|
|
SIGN_IN_DECOY_KEY=
|
feat(base): signing in with SimpleLogin through the register (ADR 0020, step 2)
The `simpleLogin` endpoint on halkyon_oidc's SimpleLogin preset: `begin`
for the app and platform (the state's marker for the forwarding page), at
most 30 an hour per network; `complete` with SimpleLogin's code, signed out
a session for the account with that SimpleLogin ID, signed in SimpleLogin
added to the caller's account, who becomes identified and anonymous no
longer. Someone new gets no account yet: their identity waits 30 minutes
under the attempt's ID (D16), with a handle suggested from their name,
`anna_mueller` (D17), for the sign-up of step 3.
The register alone holds the instance's SimpleLogin app
(`simpleLoginClientId`, `simpleLoginClientSecret`) and exchanges codes; a
member asks it for both steps and issues its own session. `SIGN_IN_WAYS`
says which ways the register offers, the instance's basics and the ways
in carry `simpleLogin`, GitHub links nobody more where SimpleLogin is
offered, and Learn's and Todo's pruning forgets the unfinished sign-ins
hourly. No migration: the tables are halkyon_oidc's, already in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 21:03:15 +02:00
|
|
|
# SimpleLogin (ADR 0020): people sign in with their SimpleLogin or Proton account, of
|
|
|
|
|
# which the instance keeps only SimpleLogin's ID for them (no address, no name). Off
|
|
|
|
|
# unless both are set: the client ID and secret of this instance's own SimpleLogin app
|
|
|
|
|
# (SimpleLogin → Settings → Developer → New website), registered with the callback
|
|
|
|
|
# /simplelogin-callback on the server's web address (on learn.halkyon.eu's server
|
|
|
|
|
# https://learn.halkyon.eu/simplelogin-callback), its forwarding page back to the apps.
|
|
|
|
|
SIMPLELOGIN_CLIENT_ID=
|
|
|
|
|
SIMPLELOGIN_CLIENT_SECRET=
|
|
|
|
|
# Another callback than this server's own page, as the app registered it (https only).
|
|
|
|
|
#SIMPLELOGIN_CALLBACK_URL=https://learn.example.org/simplelogin-callback
|
|
|
|
|
# The ways in the app offers, of those set up above: simplelogin, password, or both
|
|
|
|
|
# (separated by spaces or commas). Empty: every way set up. An unknown name stops the
|
|
|
|
|
# server as it starts.
|
|
|
|
|
#SIGN_IN_WAYS=simplelogin
|
|
|
|
|
|
2026-10-20 13:32:21 +02:00
|
|
|
# The day GitHub stops signing anyone in, three months after the release that brings
|
|
|
|
|
# passwords (ADR 0017, D2): from then on its people sign in with their address and
|
|
|
|
|
# password only, and the daily pruning forgets every GitHub ID. Empty: GitHub goes on.
|
|
|
|
|
#GITHUB_SIGN_IN_ENDS=2027-01-22
|
|
|
|
|
|
2026-09-30 16:19:46 +02:00
|
|
|
# Where the source of what this instance runs is, shown in the app's About dialog: both
|
|
|
|
|
# licences (AGPL for the app, SSPL for the server) require offering it to the people who
|
|
|
|
|
# use the instance. Empty: the project's repository, which is right for unmodified code.
|
|
|
|
|
# Running modified code, set it to where your modified source is published.
|
|
|
|
|
SOURCE_URL=
|
Limit how much room people's files take
An anonymous person may keep 100 MB of images and sounds, an identified one
500 MB, and one person more by a file_allowance row the operator adds; all
of them together 15 GB, since every file is in the database and in each dump
the server keeps (decided 2026-10-02). Each comes from the environment
(FILE_LIMIT_*_MB), with these defaults, documented in deploy/.env.example.
The server checks when an upload starts, counting unfinished uploads so they
can't fill the disk unnoticed, and again when it finishes, counting what is
kept, so uploads started side by side can't overshoot; a refusal is a
FileLimitExceededException, and an upload refused as it finishes is deleted.
deletePerson deletes the allowance with the person. Migration: the new
table file_allowance, only added.
The app keeps a refused file waiting on the device and stops that round of
uploads; the Anki import warns when a deck's files may not fit in the room
left (asset.usage()).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-02 05:57:57 +02:00
|
|
|
|
feat(dictionary): German, Greek, English, Chinese and Korean words
A studyset in German, Greek, English, Chinese or Korean finds its
dictionary as one in Japanese does (ADR 0008), and its subtitles are cut
into words by its language (`WordCutting`): Chinese by its longest
words as they stand, the longer words over each cut offered as for
Japanese; Korean at its spaces, a word the dictionary lacks as it stands
taken without the particle it ends with (집에서 is 집, 아이 stays a
child); every other language at its spaces, a word written exactly so
first (the noun Essen before the verb essen). A studyset finds the
dictionary of its language tag, else of its language alone (zh for
zh-Hans).
A Wiktionary word shows its genders, principal forms, IPA, romanisation
and Hanja; a German or Greek noun of one gender goes onto its card with
its article (das Haus, το σπίτι); its tag is its word class, named in
full (Noun, Adjective, Preposition), as Wiktionary's adj and adv would
read as JMdict's codes. Articles, pronouns, prepositions and
conjunctions are left out of a video's words. Chinese words show their
pinyin over each character on the Watch screen, carry their HSK level
as a tag (HSK-1 … HSK-7-9), and the dictionary adds a level whole; the
Watch screen filters by HSK levels for Chinese, JLPT's for Japanese, and
by none for a language without. A reading is typed in a session only
when it is kana, so pinyin isn't. Words are looked up on the web on
Jisho in Japanese, on Wiktionary at the language's section otherwise,
and a search's hint speaks of kana and romaji only in Japanese.
`DICTIONARY_LANGUAGES` in deploy/.env names the languages an instance
keeps (unset: de,el,en,ko,zh; empty: none).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:45:15 +02:00
|
|
|
# The languages besides Japanese whose dictionaries this instance keeps (the dictionaries
|
|
|
|
|
# service): Chinese from CC-CEDICT, weekly (some 4 MB), in simplified and traditional
|
docs: ADR 0009, feature parity for the other languages
What applies of Japanese's features to Chinese, Korean, Greek, German and
English, with the developer's choices and each decision's reason: Unihan's
characters, KRDict's words and levels, Make Me a Hanzi's and the curator's
own strokes, typed pinyin, radicals and readings by language; what doesn't
apply or has no open data. AGENTS.md, the server's README and the
instance's example environment follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 12:46:34 +02:00
|
|
|
# characters; Korean from KRDict, about monthly (some 84 MB); their characters from
|
|
|
|
|
# Unihan, weekly (some 8 MB); the rest from Wiktionary (German and Greek with English
|
|
|
|
|
# meanings, English with German ones; for Korean the forms of its words and the
|
|
|
|
|
# readings of its Hanja), whose whole dump (some 3 GB) it downloads whenever kaikki.org
|
|
|
|
|
# makes a new one, about monthly, and converts in ten minutes. Unset: de,el,en,ko,zh.
|
|
|
|
|
# Empty: none. Files of a language taken out are removed, and the server unloads them.
|
feat(dictionary): German, Greek, English, Chinese and Korean words
A studyset in German, Greek, English, Chinese or Korean finds its
dictionary as one in Japanese does (ADR 0008), and its subtitles are cut
into words by its language (`WordCutting`): Chinese by its longest
words as they stand, the longer words over each cut offered as for
Japanese; Korean at its spaces, a word the dictionary lacks as it stands
taken without the particle it ends with (집에서 is 집, 아이 stays a
child); every other language at its spaces, a word written exactly so
first (the noun Essen before the verb essen). A studyset finds the
dictionary of its language tag, else of its language alone (zh for
zh-Hans).
A Wiktionary word shows its genders, principal forms, IPA, romanisation
and Hanja; a German or Greek noun of one gender goes onto its card with
its article (das Haus, το σπίτι); its tag is its word class, named in
full (Noun, Adjective, Preposition), as Wiktionary's adj and adv would
read as JMdict's codes. Articles, pronouns, prepositions and
conjunctions are left out of a video's words. Chinese words show their
pinyin over each character on the Watch screen, carry their HSK level
as a tag (HSK-1 … HSK-7-9), and the dictionary adds a level whole; the
Watch screen filters by HSK levels for Chinese, JLPT's for Japanese, and
by none for a language without. A reading is typed in a session only
when it is kana, so pinyin isn't. Words are looked up on the web on
Jisho in Japanese, on Wiktionary at the language's section otherwise,
and a search's hint speaks of kana and romaji only in Japanese.
`DICTIONARY_LANGUAGES` in deploy/.env names the languages an instance
keeps (unset: de,el,en,ko,zh; empty: none).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:45:15 +02:00
|
|
|
#DICTIONARY_LANGUAGES=de,el,en,ko,zh
|
|
|
|
|
|
feat(jellyfin): the instance suggests a Jellyfin server, signed in to with a tap
An instance now names a Jellyfin server for its people to learn from
(instance.info().jellyfinUrl): its operator's JELLYFIN_URL (`none`: none),
else https://jellyfin. on the domain registered for its public host, as
learn.halkyon.eu suggests jellyfin.halkyon.eu, once that answers as a
Jellyfin server. The registered domain comes from Mozilla's Public Suffix
List, kept in the server's data folder, so an instance at example.co.uk
never names someone else's jellyfin.co.uk. The instance looks as it
starts and again hourly when asked; the app never reaches for a guessed
address.
In the app, Add material offers From Jellyfin as soon as the instance
suggests a server, before anyone signed in to one. The sign-in fills the
server in and finds it by itself, and lists the people the server shows
on its login screen: one without a password, as a demo's, signs in with
a tap; one with a password is named in the form, which asks for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 23:25:33 +02:00
|
|
|
# The Jellyfin server the app suggests to this instance's people, who watch and listen
|
|
|
|
|
# to learn from it (Halkyon Learn's Watch screen). Empty: `https://jellyfin.` on the
|
|
|
|
|
# domain registered for this instance (learn.example.org → jellyfin.example.org), if a
|
|
|
|
|
# Jellyfin server answers there; `none`: no suggestion.
|
|
|
|
|
JELLYFIN_URL=
|
|
|
|
|
|
2026-10-18 20:35:21 +02:00
|
|
|
# Where this instance keeps people's data, and its backups, which the app shows as a flag
|
2026-10-19 19:04:39 +02:00
|
|
|
# and in words when someone chooses an instance: `EU`, a country's code (ISO 3166-1
|
|
|
|
|
# alpha-2, `DE`, `CH`) or a subdivision's (ISO 3166-2, `DE-BY`, `US-CA`); for the backups
|
|
|
|
|
# also `none`. As precise as you know: the app adds "(EU)" to a member country and the
|
|
|
|
|
# country to a subdivision ("Germany (EU)", "California, United States"). Where the disks
|
|
|
|
|
# are, not which law governs you as the operator: claim only that. Empty: the app says
|
|
|
|
|
# nothing.
|
2026-10-18 20:35:21 +02:00
|
|
|
DATA_LOCATION=
|
|
|
|
|
BACKUP_LOCATION=
|
|
|
|
|
|
Limit how much room people's files take
An anonymous person may keep 100 MB of images and sounds, an identified one
500 MB, and one person more by a file_allowance row the operator adds; all
of them together 15 GB, since every file is in the database and in each dump
the server keeps (decided 2026-10-02). Each comes from the environment
(FILE_LIMIT_*_MB), with these defaults, documented in deploy/.env.example.
The server checks when an upload starts, counting unfinished uploads so they
can't fill the disk unnoticed, and again when it finishes, counting what is
kept, so uploads started side by side can't overshoot; a refusal is a
FileLimitExceededException, and an upload refused as it finishes is deleted.
deletePerson deletes the allowance with the person. Migration: the new
table file_allowance, only added.
The app keeps a refused file waiting on the device and stops that round of
uploads; the Anki import warns when a deck's files may not fit in the room
left (asset.usage()).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-02 05:57:57 +02:00
|
|
|
# How much room people's images and sounds may take on this instance, in MiB. Empty: the
|
|
|
|
|
# defaults, 100 for an anonymous person (anonymous accounts are meant to become identified
|
|
|
|
|
# ones), 500 for one signed in with GitHub, and 15000 for everyone together (0: no limit
|
|
|
|
|
# for everyone together). Every file is in the database, so also in each of the 3 dumps
|
|
|
|
|
# this server keeps (and a fourth while a deploy writes its own): files take five times
|
|
|
|
|
# their size on the disk. One person may get more than the default; as the operator,
|
|
|
|
|
# in deploy/ (their id is their account's, "authUserId"):
|
|
|
|
|
# docker compose exec postgres psql -U halkyon_learn -d halkyon_learn -c "INSERT INTO
|
|
|
|
|
# file_allowance (\"authUserId\", \"maxBytes\", note) VALUES ('<id>', 2147483648, 'asked by mail')"
|
|
|
|
|
FILE_LIMIT_ANONYMOUS_MB=
|
|
|
|
|
FILE_LIMIT_IDENTIFIED_MB=
|
|
|
|
|
FILE_LIMIT_INSTANCE_MB=
|