feat(instance): data locations as precise as a country or a state

halkyon_shared's 96f2d14: an operator's DATA_LOCATION and
BACKUP_LOCATION may name a subdivision (ISO 3166-2, US-CA, DE-BY)
besides the EU or a country, and the app adds "(EU)" to a member
country and the country to a subdivision ("Germany (EU)", "California,
United States") under the most specific flag there is. deploy/.env and
the backups' guide say so; a Storage Box is named by its country now.
The package registers its notices (the flags, CLDR's subdivision
names); Learn's own CLDR notice for the world map stays, its
registration renamed to say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-19 19:04:39 +02:00
commit 23e4645d55
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
10 changed files with 31 additions and 26 deletions

View file

@ -161,7 +161,7 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
- **Every synced table indexes `spaceId`** (`indexes: <table>_space_idx: fields: spaceId`, e.g. `studyset.spy.yaml`): deleting a space cascades into every synced table, and Postgres doesn't index foreign keys by itself, so account deletion and pruning would otherwise read whole tables. - **Every synced table indexes `spaceId`** (`indexes: <table>_space_idx: fields: spaceId`, e.g. `studyset.spy.yaml`): deleting a space cascades into every synced table, and Postgres doesn't index foreign keys by itself, so account deletion and pruning would otherwise read whole tables.
- **The client's local migration** (in `halkyon_learn_client/lib/migrations/`, written by the same `create-migration`) creates the device's SQLite schema from the whole definition, so it also contains the server's own tables. Only the synced tables and the bookkeeping ever hold data on a device. - **The client's local migration** (in `halkyon_learn_client/lib/migrations/`, written by the same `create-migration`) creates the device's SQLite schema from the whole definition, so it also contains the server's own tables. Only the synced tables and the bookkeeping ever hold data on a device.
- **Older apps** can't be made to update once published. Before the first sync of each launch the app asks whether the server still works with its build (`minimumAppBuild` in `instance.info()`, from `lib/src/instance/app_compatibility.dart` in the server, which Learn's configuration of halkyon_base carries; the build is the number after `+` in the app's version), and the sync engine refuses an app whose synced tables differ from the server's. Both end in `AppServerMismatchException` (halkyon_app), and the app says so: a banner on Home and the Sync card in Settings. Raise `minimumAppBuild` in the commit that breaks older apps (an endpoint they call changes or goes), to the build of the first app that works again; a change to the synced tables needs no raise. Every release raises the build number. It is 21 since sign-in and the account's endpoints moved into halkyon_base (ADR 0014): builds up to 20 (v0.17.0) call endpoints that are gone and can't read the module's refusals, so **the server with the module deploys only together with the release of build 21**; Learn's own `instance.info()` stays, with every field, as released apps read `minimumAppBuild` there. - **Older apps** can't be made to update once published. Before the first sync of each launch the app asks whether the server still works with its build (`minimumAppBuild` in `instance.info()`, from `lib/src/instance/app_compatibility.dart` in the server, which Learn's configuration of halkyon_base carries; the build is the number after `+` in the app's version), and the sync engine refuses an app whose synced tables differ from the server's. Both end in `AppServerMismatchException` (halkyon_app), and the app says so: a banner on Home and the Sync card in Settings. Raise `minimumAppBuild` in the commit that breaks older apps (an endpoint they call changes or goes), to the build of the first app that works again; a change to the synced tables needs no raise. Every release raises the build number. It is 21 since sign-in and the account's endpoints moved into halkyon_base (ADR 0014): builds up to 20 (v0.17.0) call endpoints that are gone and can't read the module's refusals, so **the server with the module deploys only together with the release of build 21**; Learn's own `instance.info()` stays, with every field, as released apps read `minimumAppBuild` there.
- **Instances** (decided 2026-10-10, as Moshidon does): the device keeps an account on each instance it was used with, one in use at a time (halkyon_app's instances, ADR 0014; Learn gives them `learnFrame`, its words and look, and `AssetCountryNames`). `InstanceChoice` holds them (`instances.json` in the app's support folder); a new install chooses at first start (`FirstStartScreen`, under the wordmark on a cobalt wash: two tiles as Proton's account page shows its services, decided 2026-10-18, the build's own in one tap, with where it keeps people's data (below), and another instance, which unfolds the address to type, checked with halkyon_base's `instance.basics()`, which must name the app `learn` (`ServerpodInstanceChecker(app: 'learn')`: another Halkyon app's instance is refused); `InstanceTile`: a ring from the primary colour to the tertiary, a glow and a tagline come in under the pointer, on the keyboard's focus, while pressed or open, and without a mouse the taglines always show), and one from before there was a choice goes on with the build's (`SERVER_URL`), as it has a `people/` folder. `InstanceGate` builds `Dependencies.create(instance, device)` (`main.dart`) for the one in use and builds it anew on a switch, the previous closed first; `DeviceParts` (version, sounds, Pomodoro platform, device settings) outlive switches. Each instance's people's folders and session are its own (`InstanceStorage`): the build's own instance keeps `people/` and Serverpod's own session key, so existing installs keep everything; others `instances/<name>/people/` and a key of their own. Settings › Switch account lists them (`showAccountSwitcher`): another one switches, Add account chooses another instance, and removing one not in use deletes its folder and stored session (the session stays valid on the instance until it expires). Signing out still ends only the account in use. **Signed out, the app asks how to start** (decided 2026-10-16, `StartScreen`): on the instance in use, with GitHub, by email or without an account, or on another instance; it makes an anonymous account by itself only at the very first start. A session the server ended leads there too. That the device is signed out is kept in the people's folder (`SignedOutMark`, `signed_out`), so a restart asks again. Settings names the kind of account and its instance ("Anonymous on learn.halkyon.eu"). **Where an instance keeps people's data** (decided 2026-10-18): its operator says so in `deploy/.env` (`DATA_LOCATION`, `BACKUP_LOCATION`: a country's code or `EU`, the backups also `none`; where the disks are, never which law governs, as a court reaches the operator wherever the server stands), halkyon_base's `instance.basics()` hands it on (Learn's `instance.info()` too), and the first start's tile and Switch account show it once the instance answers (`InstanceLocationBuilder`): the flag of where the data is (`InstanceFlag`, from the `flag` package, which draws lipis' flag-icons, whose MIT notice halkyon_app registers, `registerFlagIconsLicense`) and the words, as labels rather than sentences ("Data: European Union", "Backups: Switzerland"), since "in" would need the article some countries take; countries are named by the world map's CLDR names (Learn's `AssetCountryNames`, halkyon_app's `CountryNames`). - **Instances** (decided 2026-10-10, as Moshidon does): the device keeps an account on each instance it was used with, one in use at a time (halkyon_app's instances, ADR 0014; Learn gives them `learnFrame`, its words and look, and `AssetCountryNames`). `InstanceChoice` holds them (`instances.json` in the app's support folder); a new install chooses at first start (`FirstStartScreen`, under the wordmark on a cobalt wash: two tiles as Proton's account page shows its services, decided 2026-10-18, the build's own in one tap, with where it keeps people's data (below), and another instance, which unfolds the address to type, checked with halkyon_base's `instance.basics()`, which must name the app `learn` (`ServerpodInstanceChecker(app: 'learn')`: another Halkyon app's instance is refused); `InstanceTile`: a ring from the primary colour to the tertiary, a glow and a tagline come in under the pointer, on the keyboard's focus, while pressed or open, and without a mouse the taglines always show), and one from before there was a choice goes on with the build's (`SERVER_URL`), as it has a `people/` folder. `InstanceGate` builds `Dependencies.create(instance, device)` (`main.dart`) for the one in use and builds it anew on a switch, the previous closed first; `DeviceParts` (version, sounds, Pomodoro platform, device settings) outlive switches. Each instance's people's folders and session are its own (`InstanceStorage`): the build's own instance keeps `people/` and Serverpod's own session key, so existing installs keep everything; others `instances/<name>/people/` and a key of their own. Settings › Switch account lists them (`showAccountSwitcher`): another one switches, Add account chooses another instance, and removing one not in use deletes its folder and stored session (the session stays valid on the instance until it expires). Signing out still ends only the account in use. **Signed out, the app asks how to start** (decided 2026-10-16, `StartScreen`): on the instance in use, with GitHub, by email or without an account, or on another instance; it makes an anonymous account by itself only at the very first start. A session the server ended leads there too. That the device is signed out is kept in the people's folder (`SignedOutMark`, `signed_out`), so a restart asks again. Settings names the kind of account and its instance ("Anonymous on learn.halkyon.eu"). **Where an instance keeps people's data** (decided 2026-10-18): its operator says so in `deploy/.env` (`DATA_LOCATION`, `BACKUP_LOCATION`: `EU`, a country's code or a subdivision's, ISO 3166-2 such as `US-CA`, the backups also `none`, decided 2026-10-19; where the disks are, never which law governs, as a court reaches the operator wherever the server stands), halkyon_base's `instance.basics()` hands it on (Learn's `instance.info()` too), and the first start's tile and Switch account show it once the instance answers (`InstanceLocationBuilder`): the most specific flag of where the data is (`InstanceFlag`, from the `flag` package, which draws lipis' flag-icons: a subdivision's own where it has one, Scotland's, else its country's) and the words, as labels rather than sentences ("Data: European Union", "Data: Germany (EU)", "Backups: California, United States"), since "in" would need the article some countries take; a member country of the EU gets "(EU)" and a subdivision its country; countries are named by the world map's CLDR names (Learn's `AssetCountryNames`, halkyon_app's `CountryNames`), subdivisions by Unicode CLDR 48.2's, bundled in halkyon_app; `registerHalkyonAppLicenses` registers the flags' notice and that CLDR's, beside Learn's own for the map.
- **Asking an AI assistant or searching the web** (decided 2026-10-10; `feature/assistant/`): a device setting, off unless the person turns it on (Settings › Assistant), shows a field at the foot of Home (`AssistantBar`) and a button that unfolds into one (`AssistantButton`): at the lower left of the Library, across from Create studyset, at the left above a studyset's bottom bar (floating alone at the lower left from expanded windows on), and at the lower left while studying, above the buttons that answer the card (decided 2026-10-16, as Vaia places it). A switch at the field's start sends to the AI assistant chosen in Settings, named in the field, or to a web search and is remembered (`assistantTarget`). Above the field, chips offer the parts of what it is about (`AssistantTopic.parts`: a card's front, back, each hint, its explanation, the studyset's name and description), none chosen, so only what the person picks goes along, each cut at 600 characters; a web search takes the question and the chosen text, never the studyset's name or description. Nothing is answered here: `AssistantHandOff` opens the chosen assistant's new conversation with the question and a compact line per chosen part written in, for the person to send (`AiAssistant`, decided 2026-10-14: Proton's Lumo by default, by its guest chat `https://lumo.proton.me/guest#prefill=…`, the question in the fragment, which reaches no server, as Lumo's page reads it, undocumented; its address for one's own account loses the question when nobody is signed in; then ChatGPT, Claude, Mistral, Perplexity, which answers at once, Duck.ai, Grok, which asks first, Kagi Assistant, HuggingChat, or the person's own address with `%s`; only assistants whose address fills a question in, so not Gemini, which has none, nor Copilot, whose stopped working in November 2025; each cut at what it takes, 8,000 to 12,000 characters), and `WebSearch` opens the chosen engine's results (`SearchEngine`: Brave Search by default on desktops, Ecosia, DuckDuckGo, Startpage, Qwant, Google, Bing, or the person's own address with `%s`), or on Android hands the question to the device's own web search (`ACTION_WEB_SEARCH` in `MainActivity.kt`, Brave Search where nothing answers). Halkyon apps use no LLM API keys: their pricing is opaque and open to abuse, and the person decides each time what an LLM sees. A Claude plan can't drive a chat inside another app (Anthropic allows no third party to sign in with it), so Claude reads studysets through the instance's connector instead (ADR 0006): `halkyon_learn_server/lib/src/assistant/` serves MCP (`/mcp`, Streamable HTTP, JSON answers, no sessions; five read-only tools in `connector_tools.dart`) and its OAuth (`/.well-known/oauth-*`, `/oauth/register`, `/oauth/authorize`, `/oauth/token`) on Serverpod's web server, which production runs on 8082 for these paths alone (Caddy forwards them; `connectorOf` takes the public address from `webServer`). People connect with a code from Settings › Assistant › Connect Claude (`AssistantEndpoint.connectCode`, eight Crockford characters, ten minutes, once), typed on the authorization page; codes and tokens are kept as SHA-256 hashes. The handlers take plain requests (`ConnectorRequest`), so `test/integration/assistant/` drives the whole flow without a web server; the Relic routes only translate. - **Asking an AI assistant or searching the web** (decided 2026-10-10; `feature/assistant/`): a device setting, off unless the person turns it on (Settings › Assistant), shows a field at the foot of Home (`AssistantBar`) and a button that unfolds into one (`AssistantButton`): at the lower left of the Library, across from Create studyset, at the left above a studyset's bottom bar (floating alone at the lower left from expanded windows on), and at the lower left while studying, above the buttons that answer the card (decided 2026-10-16, as Vaia places it). A switch at the field's start sends to the AI assistant chosen in Settings, named in the field, or to a web search and is remembered (`assistantTarget`). Above the field, chips offer the parts of what it is about (`AssistantTopic.parts`: a card's front, back, each hint, its explanation, the studyset's name and description), none chosen, so only what the person picks goes along, each cut at 600 characters; a web search takes the question and the chosen text, never the studyset's name or description. Nothing is answered here: `AssistantHandOff` opens the chosen assistant's new conversation with the question and a compact line per chosen part written in, for the person to send (`AiAssistant`, decided 2026-10-14: Proton's Lumo by default, by its guest chat `https://lumo.proton.me/guest#prefill=…`, the question in the fragment, which reaches no server, as Lumo's page reads it, undocumented; its address for one's own account loses the question when nobody is signed in; then ChatGPT, Claude, Mistral, Perplexity, which answers at once, Duck.ai, Grok, which asks first, Kagi Assistant, HuggingChat, or the person's own address with `%s`; only assistants whose address fills a question in, so not Gemini, which has none, nor Copilot, whose stopped working in November 2025; each cut at what it takes, 8,000 to 12,000 characters), and `WebSearch` opens the chosen engine's results (`SearchEngine`: Brave Search by default on desktops, Ecosia, DuckDuckGo, Startpage, Qwant, Google, Bing, or the person's own address with `%s`), or on Android hands the question to the device's own web search (`ACTION_WEB_SEARCH` in `MainActivity.kt`, Brave Search where nothing answers). Halkyon apps use no LLM API keys: their pricing is opaque and open to abuse, and the person decides each time what an LLM sees. A Claude plan can't drive a chat inside another app (Anthropic allows no third party to sign in with it), so Claude reads studysets through the instance's connector instead (ADR 0006): `halkyon_learn_server/lib/src/assistant/` serves MCP (`/mcp`, Streamable HTTP, JSON answers, no sessions; five read-only tools in `connector_tools.dart`) and its OAuth (`/.well-known/oauth-*`, `/oauth/register`, `/oauth/authorize`, `/oauth/token`) on Serverpod's web server, which production runs on 8082 for these paths alone (Caddy forwards them; `connectorOf` takes the public address from `webServer`). People connect with a code from Settings › Assistant › Connect Claude (`AssistantEndpoint.connectCode`, eight Crockford characters, ten minutes, once), typed on the authorization page; codes and tokens are kept as SHA-256 hashes. The handlers take plain requests (`ConnectorRequest`), so `test/integration/assistant/` drives the whole flow without a web server; the Relic routes only translate.
- **Where sync stands** is `SyncService.status` (syncing, the last sync that went through, the last failure; since the app started), which `SyncStatusBloc` (halkyon_app, provided by `PersonApp`) hands to the trouble dot on the Settings cogs (on Home and Profile; no tab carries it), the Sync card in Settings and the banner on Home. Only the last outcome counts: one sync that goes through clears a failure. It also counts the changes not on the server yet (`pending`): every save asks for a sync (`requestSync`), and a sync that goes through takes those saved before it started; the engine can't say, since the server only tells it at the start of a sync what it has. The count is kept in `sync_pending.json` in the person's folder (`PendingChangesStore`) and shown on the Sync card, beside whether a sync ever went through on this device (`syncedHere`, read as the person's data opens): until one has, an empty library says it is on its way from the instance rather than that there are no studysets (decided 2026-10-17: a first sync brings everything at once, at its end). The Library's Refresh and pulling it down sync now (`syncAndWait`, which a pull's spinner waits on). - **Where sync stands** is `SyncService.status` (syncing, the last sync that went through, the last failure; since the app started), which `SyncStatusBloc` (halkyon_app, provided by `PersonApp`) hands to the trouble dot on the Settings cogs (on Home and Profile; no tab carries it), the Sync card in Settings and the banner on Home. Only the last outcome counts: one sync that goes through clears a failure. It also counts the changes not on the server yet (`pending`): every save asks for a sync (`requestSync`), and a sync that goes through takes those saved before it started; the engine can't say, since the server only tells it at the start of a sync what it has. The count is kept in `sync_pending.json` in the person's folder (`PendingChangesStore`) and shown on the Sync card, beside whether a sync ever went through on this device (`syncedHere`, read as the person's data opens): until one has, an empty library says it is on its way from the instance rather than that there are no studysets (decided 2026-10-17: a first sync brings everything at once, at its end). The Library's Refresh and pulling it down sync now (`syncAndWait`, which a pull's spinner waits on).
- **Serverpod's version moves together with `serverpod_offline_sync`**, which allows only a narrow range of Serverpod (v0.0.10: `>=4.0.4 <4.1.0`; v0.0.8 pinned v4.0.0 exactly, which the root `pubspec.yaml` overrode until 2026-10-09). Every package pins Serverpod's packages to one version, halkyon_base's too: upgrade them all together with the sync packages, halkyon_base and the CLI version CI generates code with (`SERVERPOD_VERSION` in `.forgejo/workflows/ci.yaml`), after reading the sync package's changelog. Never override a version the sync package doesn't allow without checking what changed in `serverpod_database`, which it builds on. Its shared part, `serverpod_offline_sync`, comes from our fork (the root `pubspec.yaml`'s `dependency_overrides`, `project-halkyon/serverpod-offline-sync` on code.halkyon.app, pinned to a signed commit of branch `halkyon/batch-inserts`, which stacks six branches): `halkyon/byte-payloads` (decided 2026-10-12: v0.0.10 compares byte payloads byte by byte through a UUID parse that throws for each byte, so one sync touching a few thousand cards blocked the server for minutes); `halkyon/inbound-merges` (2026-10-17: `onInboundMerge`, called only when changes from the remote peer were merged, which the app's sync uses; `onMergeSuccess` fires for a space only pushed to, so every repository read everything again after each save's sync); `halkyon/fast-deletes` (2026-10-17: a local delete skips the foreign key projection pass when it can't change it, `canSkipProjectionAfterLocalDelete`; the pass loaded nearly every synced row, 8 s for one delete in a library of 8,000 cards, 30 s for a card; not reported upstream yet, the developer's call); `halkyon/fast-merges` (2026-10-17: a merge only inserting new rows proven unable to change the projection skips its passes, `canLeaveProjectionUnchangedByMergedInserts`, 9.5 s to 60 ms for a rating another device made in such a library; and projections are rebuilt once after the synced tables changed, not on every session and merge, which fixes #150 below); `halkyon/fast-merge-deletes` (2026-10-17: a merge only deleting rows skips the pass when every visible row below them goes with them, `canLeaveProjectionUnchangedByMergedDeletes`: a card deleted on one device took the server 5.4 s to merge and each other device 4 s, now 35 and 99 ms); `halkyon/batch-inserts` (2026-10-17: such a merge writes its new rows up to 1,000 to a statement, and the insert proof takes a parent of the row's own table inserted before it, as nested studysets are: a first push of 8,000 cards took the server 35 to 41 s, now about 5; a device receiving it 60 s, now 44, as Serverpod's SQLite adapter still inserts row by row). Move the fork along when upgrading, and drop each change once a release has it. - **Serverpod's version moves together with `serverpod_offline_sync`**, which allows only a narrow range of Serverpod (v0.0.10: `>=4.0.4 <4.1.0`; v0.0.8 pinned v4.0.0 exactly, which the root `pubspec.yaml` overrode until 2026-10-09). Every package pins Serverpod's packages to one version, halkyon_base's too: upgrade them all together with the sync packages, halkyon_base and the CLI version CI generates code with (`SERVERPOD_VERSION` in `.forgejo/workflows/ci.yaml`), after reading the sync package's changelog. Never override a version the sync package doesn't allow without checking what changed in `serverpod_database`, which it builds on. Its shared part, `serverpod_offline_sync`, comes from our fork (the root `pubspec.yaml`'s `dependency_overrides`, `project-halkyon/serverpod-offline-sync` on code.halkyon.app, pinned to a signed commit of branch `halkyon/batch-inserts`, which stacks six branches): `halkyon/byte-payloads` (decided 2026-10-12: v0.0.10 compares byte payloads byte by byte through a UUID parse that throws for each byte, so one sync touching a few thousand cards blocked the server for minutes); `halkyon/inbound-merges` (2026-10-17: `onInboundMerge`, called only when changes from the remote peer were merged, which the app's sync uses; `onMergeSuccess` fires for a space only pushed to, so every repository read everything again after each save's sync); `halkyon/fast-deletes` (2026-10-17: a local delete skips the foreign key projection pass when it can't change it, `canSkipProjectionAfterLocalDelete`; the pass loaded nearly every synced row, 8 s for one delete in a library of 8,000 cards, 30 s for a card; not reported upstream yet, the developer's call); `halkyon/fast-merges` (2026-10-17: a merge only inserting new rows proven unable to change the projection skips its passes, `canLeaveProjectionUnchangedByMergedInserts`, 9.5 s to 60 ms for a rating another device made in such a library; and projections are rebuilt once after the synced tables changed, not on every session and merge, which fixes #150 below); `halkyon/fast-merge-deletes` (2026-10-17: a merge only deleting rows skips the pass when every visible row below them goes with them, `canLeaveProjectionUnchangedByMergedDeletes`: a card deleted on one device took the server 5.4 s to merge and each other device 4 s, now 35 and 99 ms); `halkyon/batch-inserts` (2026-10-17: such a merge writes its new rows up to 1,000 to a statement, and the insert proof takes a parent of the row's own table inserted before it, as nested studysets are: a first push of 8,000 cards took the server 35 to 41 s, now about 5; a device receiving it 60 s, now 44, as Serverpod's SQLite adapter still inserts row by row). Move the fork along when upgrading, and drop each change once a release has it.

View file

@ -82,9 +82,12 @@ SOURCE_URL=
JELLYFIN_URL= JELLYFIN_URL=
# Where this instance keeps people's data, and its backups, which the app shows as a flag # Where this instance keeps people's data, and its backups, which the app shows as a flag
# and in words when someone chooses an instance: a country's code (ISO 3166-1 alpha-2, # and in words when someone chooses an instance: `EU`, a country's code (ISO 3166-1
# `CH`) or `EU`; for the backups also `none`. Where the disks are, not which law governs # alpha-2, `DE`, `CH`) or a subdivision's (ISO 3166-2, `DE-BY`, `US-CA`); for the backups
# you as the operator: claim only that. Empty: the app says nothing. # 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.
DATA_LOCATION= DATA_LOCATION=
BACKUP_LOCATION= BACKUP_LOCATION=

View file

@ -49,9 +49,9 @@ Once, in this order. The commands name who runs them, and where.
### 1. The Storage Box (you, in Hetzner Console) ### 1. The Storage Box (you, in Hetzner Console)
1. Create a Storage Box (BX11, 1 TB, is plenty) in any location (Germany and Finland are 1. Create a Storage Box (BX11, 1 TB, is plenty) in any location (Germany or Finland, both
in the EU; the app shows where, from `BACKUP_LOCATION` in `deploy/.env`, so set it to in the EU; the app shows where, from `BACKUP_LOCATION` in `deploy/.env`, so set it to
the box's: `EU` there), with a password, which the box's country: `DE` or `FI`, shown as "Germany (EU)"), with a password, which
goes into your password manager. Turn on **SSH Support** (port 23, which restic uses) goes into your password manager. Turn on **SSH Support** (port 23, which restic uses)
and **External Reachability**: without it, the box only answers inside Hetzner's and **External Reachability**: without it, the box only answers inside Hetzner's
network. Samba and WebDAV stay off; FTP and SFTP on port 22 can't be turned off. network. Samba and WebDAV stay off; FTP and SFTP on port 22 can't be turned off.

View file

@ -82,8 +82,8 @@ services:
# The Jellyfin server suggested to people; empty: jellyfin. on the # The Jellyfin server suggested to people; empty: jellyfin. on the
# instance's own domain, if one answers there; `none`: none. # instance's own domain, if one answers there; `none`: none.
JELLYFIN_URL: ${JELLYFIN_URL:-} JELLYFIN_URL: ${JELLYFIN_URL:-}
# Where people's data and the backups are, which the app shows; empty: # Where people's data and the backups are, which the app shows: `EU`, a
# it says nothing. # country (`DE`) or a subdivision (`US-CA`); empty: it says nothing.
DATA_LOCATION: ${DATA_LOCATION:-} DATA_LOCATION: ${DATA_LOCATION:-}
BACKUP_LOCATION: ${BACKUP_LOCATION:-} BACKUP_LOCATION: ${BACKUP_LOCATION:-}
# How much room people's files may take, in MiB; empty: the defaults. # How much room people's files may take, in MiB; empty: the defaults.

View file

@ -12,7 +12,7 @@ dependencies:
git: git:
url: https://code.halkyon.app/project-halkyon/halkyon_shared.git url: https://code.halkyon.app/project-halkyon/halkyon_shared.git
path: packages/halkyon_base_client path: packages/halkyon_base_client
ref: e23cc77f7c30a2a07015237ef0f054d1d1ccad2b ref: 96f2d1461803599bdf5b14783f4b3d3a2810742f
serverpod_auth_idp_client: 4.0.4 serverpod_auth_idp_client: 4.0.4
serverpod_client: 4.0.4 serverpod_client: 4.0.4
serverpod_offline_sync_client: 0.0.10 serverpod_offline_sync_client: 0.0.10

View file

@ -23,8 +23,10 @@ Future<void> main() async {
registerFontLicenses(); registerFontLicenses();
registerCjkFontLicenses(); registerCjkFontLicenses();
registerKanjiVgLicense(); registerKanjiVgLicense();
registerFlagIconsLicense(); // The flags' notice and Unicode CLDR's for the subdivisions' names.
registerCldrLicense(); registerHalkyonAppLicenses();
// Unicode CLDR's for the world map's country names, as that copy gives it.
registerWorldMapCldrLicense();
registerMecabLicenses(); registerMecabLicenses();
// The instance the build names (`--dart-define=SERVER_URL=…`, or // The instance the build names (`--dart-define=SERVER_URL=…`, or
// `assets/config.json`): the first offered, and the one installs from // `assets/config.json`): the first offered, and the one installs from

View file

@ -4,7 +4,7 @@ import 'package:flutter/services.dart';
/// Adds the Unicode CLDR's notice to the app's licence list, beside the /// Adds the Unicode CLDR's notice to the app's licence list, beside the
/// packages' own: the world map's country names come from it, and its /// packages' own: the world map's country names come from it, and its
/// licence asks that its notice go with them. /// licence asks that its notice go with them.
void registerCldrLicense() => LicenseRegistry.addLicense(() async* { void registerWorldMapCldrLicense() => LicenseRegistry.addLicense(() async* {
yield LicenseEntryWithLineBreaks(const [ yield LicenseEntryWithLineBreaks(const [
'Unicode CLDR', 'Unicode CLDR',
], await rootBundle.loadString('assets/maps/LICENSE-CLDR.txt')); ], await rootBundle.loadString('assets/maps/LICENSE-CLDR.txt'));

View file

@ -74,14 +74,14 @@ dependencies:
git: git:
url: https://code.halkyon.app/project-halkyon/halkyon_shared.git url: https://code.halkyon.app/project-halkyon/halkyon_shared.git
path: packages/halkyon_app path: packages/halkyon_app
ref: e23cc77f7c30a2a07015237ef0f054d1d1ccad2b ref: 96f2d1461803599bdf5b14783f4b3d3a2810742f
# Halkyon's low-level parts (halkyon_shared): the queues the repositories # Halkyon's low-level parts (halkyon_shared): the queues the repositories
# run their work in (SerialQueue, CoalescingRunner). # run their work in (SerialQueue, CoalescingRunner).
halkyon_core: halkyon_core:
git: git:
url: https://code.halkyon.app/project-halkyon/halkyon_shared.git url: https://code.halkyon.app/project-halkyon/halkyon_shared.git
path: packages/halkyon_core path: packages/halkyon_core
ref: e23cc77f7c30a2a07015237ef0f054d1d1ccad2b ref: 96f2d1461803599bdf5b14783f4b3d3a2810742f
halkyon_learn_client: halkyon_learn_client:
path: ../halkyon_learn_client path: ../halkyon_learn_client
# Halkyon's look, shared by its apps (halkyon_shared): the theme, the bundled Noto # Halkyon's look, shared by its apps (halkyon_shared): the theme, the bundled Noto
@ -92,7 +92,7 @@ dependencies:
git: git:
url: https://code.halkyon.app/project-halkyon/halkyon_shared.git url: https://code.halkyon.app/project-halkyon/halkyon_shared.git
path: packages/halkyon_ui path: packages/halkyon_ui
ref: e23cc77f7c30a2a07015237ef0f054d1d1ccad2b ref: 96f2d1461803599bdf5b14783f4b3d3a2810742f
# Anki's fields are HTML; the editor brings this parser along already. # Anki's fields are HTML; the editor brings this parser along already.
html: ^0.15.6 html: ^0.15.6
# Talks to Jellyfin servers; Serverpod's client brings it along already. # Talks to Jellyfin servers; Serverpod's client brings it along already.

View file

@ -20,7 +20,7 @@ dependencies:
git: git:
url: https://code.halkyon.app/project-halkyon/halkyon_shared.git url: https://code.halkyon.app/project-halkyon/halkyon_shared.git
path: packages/halkyon_base_server path: packages/halkyon_base_server
ref: e23cc77f7c30a2a07015237ef0f054d1d1ccad2b ref: 96f2d1461803599bdf5b14783f4b3d3a2810742f
# Reads the pages profile links point to, for their rel="me" back-links. # Reads the pages profile links point to, for their rel="me" back-links.
html: ^0.15.6 html: ^0.15.6
# Mails email sign-in's codes over SMTP (ADR 0010). # Mails email sign-in's codes over SMTP (ADR 0010).

View file

@ -820,8 +820,8 @@ packages:
dependency: transitive dependency: transitive
description: description:
path: "packages/halkyon_app" path: "packages/halkyon_app"
ref: e23cc77f7c30a2a07015237ef0f054d1d1ccad2b ref: "96f2d1461803599bdf5b14783f4b3d3a2810742f"
resolved-ref: e23cc77f7c30a2a07015237ef0f054d1d1ccad2b resolved-ref: "96f2d1461803599bdf5b14783f4b3d3a2810742f"
url: "https://code.halkyon.app/project-halkyon/halkyon_shared.git" url: "https://code.halkyon.app/project-halkyon/halkyon_shared.git"
source: git source: git
version: "0.1.0" version: "0.1.0"
@ -829,8 +829,8 @@ packages:
dependency: transitive dependency: transitive
description: description:
path: "packages/halkyon_base_client" path: "packages/halkyon_base_client"
ref: e23cc77f7c30a2a07015237ef0f054d1d1ccad2b ref: "96f2d1461803599bdf5b14783f4b3d3a2810742f"
resolved-ref: e23cc77f7c30a2a07015237ef0f054d1d1ccad2b resolved-ref: "96f2d1461803599bdf5b14783f4b3d3a2810742f"
url: "https://code.halkyon.app/project-halkyon/halkyon_shared.git" url: "https://code.halkyon.app/project-halkyon/halkyon_shared.git"
source: git source: git
version: "0.1.0" version: "0.1.0"
@ -838,8 +838,8 @@ packages:
dependency: transitive dependency: transitive
description: description:
path: "packages/halkyon_base_server" path: "packages/halkyon_base_server"
ref: e23cc77f7c30a2a07015237ef0f054d1d1ccad2b ref: "96f2d1461803599bdf5b14783f4b3d3a2810742f"
resolved-ref: e23cc77f7c30a2a07015237ef0f054d1d1ccad2b resolved-ref: "96f2d1461803599bdf5b14783f4b3d3a2810742f"
url: "https://code.halkyon.app/project-halkyon/halkyon_shared.git" url: "https://code.halkyon.app/project-halkyon/halkyon_shared.git"
source: git source: git
version: "0.1.0" version: "0.1.0"
@ -847,8 +847,8 @@ packages:
dependency: transitive dependency: transitive
description: description:
path: "packages/halkyon_core" path: "packages/halkyon_core"
ref: e23cc77f7c30a2a07015237ef0f054d1d1ccad2b ref: "96f2d1461803599bdf5b14783f4b3d3a2810742f"
resolved-ref: e23cc77f7c30a2a07015237ef0f054d1d1ccad2b resolved-ref: "96f2d1461803599bdf5b14783f4b3d3a2810742f"
url: "https://code.halkyon.app/project-halkyon/halkyon_shared.git" url: "https://code.halkyon.app/project-halkyon/halkyon_shared.git"
source: git source: git
version: "0.0.1" version: "0.0.1"
@ -856,8 +856,8 @@ packages:
dependency: transitive dependency: transitive
description: description:
path: "packages/halkyon_ui" path: "packages/halkyon_ui"
ref: e23cc77f7c30a2a07015237ef0f054d1d1ccad2b ref: "96f2d1461803599bdf5b14783f4b3d3a2810742f"
resolved-ref: e23cc77f7c30a2a07015237ef0f054d1d1ccad2b resolved-ref: "96f2d1461803599bdf5b14783f4b3d3a2810742f"
url: "https://code.halkyon.app/project-halkyon/halkyon_shared.git" url: "https://code.halkyon.app/project-halkyon/halkyon_shared.git"
source: git source: git
version: "0.1.0" version: "0.1.0"