chore(server): generated code as the generator writes it

dart format had gone over the generated files too (a blank line between
import groups, other line breaks), which CI's check against a fresh
`serverpod generate` would have refused. AGENTS.md now says to format
only the files one wrote.

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>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-09 00:53:32 +02:00
commit 7008fcac52
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
27 changed files with 93 additions and 101 deletions

View file

@ -119,7 +119,7 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
## CI and deploys ## CI and deploys
- **CI** (`.forgejo/workflows/ci.yaml`) runs `tool/check.sh` on every push, on the Forgejo runner on sahaquiel (`deploy/runner/`), except for a commit that passed before: the job marks passed commits in the runner's home (`checked/`, kept 90 days), so a branch's commit fast-forwarded into `main` goes straight to its deploy. Jobs run directly on the host as the unprivileged user `runner`, never in containers: Docker access would be root on the production server. The runner's systemd unit keeps jobs away from production (no `/home`, no Docker socket, Docker's container networks unreachable, memory and CPU capped); keep it that way when changing it. `runner` has user ID 61000 and must never share one with a container's processes (PostgreSQL and the server run as 999, not remapped): the same user ID can read their environment and files. The job's first step (*Sandbox*) fails if any of this leaks. Flutter is pinned in the workflow (`FLUTTER_VERSION`): move it together with the local one. The server tests there get throwaway passwords from the environment (`SERVERPOD_…`, new for every job), since `passwords.yaml` is never committed; without them they exit with code 1 and no message (Serverpod's own template does the same). CI also runs `serverpod generate` and fails if the committed generated code differs from its output. CI runs on pushes only: a pull request from a fork would run its code on the production server. - **CI** (`.forgejo/workflows/ci.yaml`) runs `tool/check.sh` on every push, on the Forgejo runner on sahaquiel (`deploy/runner/`), except for a commit that passed before: the job marks passed commits in the runner's home (`checked/`, kept 90 days), so a branch's commit fast-forwarded into `main` goes straight to its deploy. Jobs run directly on the host as the unprivileged user `runner`, never in containers: Docker access would be root on the production server. The runner's systemd unit keeps jobs away from production (no `/home`, no Docker socket, Docker's container networks unreachable, memory and CPU capped); keep it that way when changing it. `runner` has user ID 61000 and must never share one with a container's processes (PostgreSQL and the server run as 999, not remapped): the same user ID can read their environment and files. The job's first step (*Sandbox*) fails if any of this leaks. Flutter is pinned in the workflow (`FLUTTER_VERSION`): move it together with the local one. The server tests there get throwaway passwords from the environment (`SERVERPOD_…`, new for every job), since `passwords.yaml` is never committed; without them they exit with code 1 and no message (Serverpod's own template does the same). CI also runs `serverpod generate` and fails if the committed generated code differs from its output. So never run `dart format` over the server's `lib/` or `test/` as a whole: it reformats the generated code (`lib/src/generated/`, `serverpod_test_tools.dart`); format the files you wrote. CI runs on pushes only: a pull request from a fork would run its code on the production server.
- **A green `main` deploys learn.halkyon.eu** through `deploy/redeploy.sh`, the only command the runner's key may run as `deploy` (pinned in `authorized_keys`, from sahaquiel itself only). It deploys the commit CI checked while it is still the tip, as a fast-forward, only if every new commit is signed by a key in `deploy/allowed-signers` (the deployed copy decides), after dumping the database (the last 3 dumps stay in `~deploy/backups`: the way back from a deploy, not backups), applies the migrations in a run of their own before switching (`--role=maintenance --apply-migrations`: a failing migration stops the deploy while the old server keeps serving, so migrations must keep the old server working until the switch), and fails unless the new server answers. A failed deploy leaves the checkout at the deployed commit; run the workflow again to retry. By hand: `ssh sahaquiel-deploy halkyon-learn/deploy/redeploy.sh <commit>`. Never `docker compose down -v` there. - **A green `main` deploys learn.halkyon.eu** through `deploy/redeploy.sh`, the only command the runner's key may run as `deploy` (pinned in `authorized_keys`, from sahaquiel itself only). It deploys the commit CI checked while it is still the tip, as a fast-forward, only if every new commit is signed by a key in `deploy/allowed-signers` (the deployed copy decides), after dumping the database (the last 3 dumps stay in `~deploy/backups`: the way back from a deploy, not backups), applies the migrations in a run of their own before switching (`--role=maintenance --apply-migrations`: a failing migration stops the deploy while the old server keeps serving, so migrations must keep the old server working until the switch), and fails unless the new server answers. A failed deploy leaves the checkout at the deployed commit; run the workflow again to retry. By hand: `ssh sahaquiel-deploy halkyon-learn/deploy/redeploy.sh <commit>`. Never `docker compose down -v` there.
- **Backups** (`deploy/backups.md`): every hour, `deploy/backup.sh` (a systemd timer, as `deploy`) dumps the database, which holds people's files too, and restic stores it in a Hetzner Storage Box (24 hourly, 7 daily, 4 weekly, 3 monthly); the box's own snapshots protect it from a taken-over server. A restore takes the instance back up to an hour, and people who started since lose what their device held: their app no longer finds its session, starts a new anonymous account and deletes the old folder. So a change that makes devices lose their session, or keeps deleted people's data longer, changes what a restore costs; say so in `deploy/backups.md`. - **Backups** (`deploy/backups.md`): every hour, `deploy/backup.sh` (a systemd timer, as `deploy`) dumps the database, which holds people's files too, and restic stores it in a Hetzner Storage Box (24 hourly, 7 daily, 4 weekly, 3 monthly); the box's own snapshots protect it from a taken-over server. A restore takes the instance back up to an hour, and people who started since lose what their device held: their app no longer finds its session, starts a new anonymous account and deletes the old folder. So a change that makes devices lose their session, or keeps deleted people's data longer, changes what a restore costs; say so in `deploy/backups.md`.
- **Releases** are the project's, not an instance's: the release, the Snap Store listing and the website point to learn.halkyon.app; the builds connect to learn.halkyon.eu, the project's instance, until the app lets people choose one. They come from a developer's laptop, never from CI: `tool/release.sh` on `main` as pushed and green in CI. Versions follow semantic versioning, worked out from the commits since the last release (`feat` raises the minor number, `fix`/`perf` the patch number, breaking ones the major from 1.0 on; other types release nothing), so **commit messages keep their type prefix**. It writes the version and a raised build number into the app's pubspec in a signed release commit (`chore(release): vX.Y.Z`) with a signed tag (a key in `deploy/allowed-signers`), builds one universal APK from a checkout of the tag, signed with the app's key, and the snap (`snap/snapcraft.yaml`, the committed HEAD, built in LXD), both with `SERVER_URL=https://learn.halkyon.eu/`; signs `SHA256SUMS` with GPG; pushes only once both builds passed their checks; and publishes on code.halkyon.app through Forgejo's API and in the F-Droid repository. The Snap Store upload is off until `snap_store_channel` in the script names a channel. A run that stopped after its release commit and tag carries on with them. The key's password and the Forgejo token are asked for at each run and never stored. The app's key is a PKCS #12 keystore outside the repository (also in the maintainer's password manager); there is no Google Play, so it is the app's identity: lost, installed apps can't be updated. Its certificate's SHA-256 is in `tool/release/android-certificate.sha256`, and the script releases no APK signed otherwise; other release builds fall back to the debug key (`android/app/build.gradle.kts`), so `flutter run --release` works anywhere. The APK also goes into the project's own F-Droid repository, https://halkyon.app/fdroid/repo, through `tool/fdroid_repo.sh` (fdroidserver v2.4.5): its settings and the app's listing are in `tool/release/fdroid/` (the anti-features TetheredNet and NonFreeNet declared with their reasons and F-Droid's own icons); its index is signed with a key of its own (a PKCS #12 keystore beside the app's, password asked for, certificate pinned in `tool/release/fdroid-index-certificate.sha256`, since a new key makes the F-Droid app refuse the repository); and it is uploaded to ramiel as the user `fdroid`, whose key can only write into the folder Caddy serves (rrsync), the APKs first and the indexes last. - **Releases** are the project's, not an instance's: the release, the Snap Store listing and the website point to learn.halkyon.app; the builds connect to learn.halkyon.eu, the project's instance, until the app lets people choose one. They come from a developer's laptop, never from CI: `tool/release.sh` on `main` as pushed and green in CI. Versions follow semantic versioning, worked out from the commits since the last release (`feat` raises the minor number, `fix`/`perf` the patch number, breaking ones the major from 1.0 on; other types release nothing), so **commit messages keep their type prefix**. It writes the version and a raised build number into the app's pubspec in a signed release commit (`chore(release): vX.Y.Z`) with a signed tag (a key in `deploy/allowed-signers`), builds one universal APK from a checkout of the tag, signed with the app's key, and the snap (`snap/snapcraft.yaml`, the committed HEAD, built in LXD), both with `SERVER_URL=https://learn.halkyon.eu/`; signs `SHA256SUMS` with GPG; pushes only once both builds passed their checks; and publishes on code.halkyon.app through Forgejo's API and in the F-Droid repository. The Snap Store upload is off until `snap_store_channel` in the script names a channel. A run that stopped after its release commit and tag carries on with them. The key's password and the Forgejo token are asked for at each run and never stored. The app's key is a PKCS #12 keystore outside the repository (also in the maintainer's password manager); there is no Google Play, so it is the app's identity: lost, installed apps can't be updated. Its certificate's SHA-256 is in `tool/release/android-certificate.sha256`, and the script releases no APK signed otherwise; other release builds fall back to the debug key (`android/app/build.gradle.kts`), so `flutter run --release` works anywhere. The APK also goes into the project's own F-Droid repository, https://halkyon.app/fdroid/repo, through `tool/fdroid_repo.sh` (fdroidserver v2.4.5): its settings and the app's listing are in `tool/release/fdroid/` (the anti-features TetheredNet and NonFreeNet declared with their reasons and F-Droid's own icons); its index is signed with a key of its own (a PKCS #12 keystore beside the app's, password asked for, certificate pinned in `tool/release/fdroid-index-certificate.sha256`, since a new key makes the F-Droid app refuse the repository); and it is uploaded to ramiel as the user `fdroid`, whose key can only write into the folder Caddy serves (rrsync), the APKs first and the indexes last.

View file

@ -11,7 +11,6 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt; import 'dart:typed_data' as _idt;
import 'package:serverpod/serverpod.dart' as _is; import 'package:serverpod/serverpod.dart' as _is;
/// A file in a person's content, an image or a sound (ADR 0004). This row is synced; /// A file in a person's content, an image or a sound (ADR 0004). This row is synced;

View file

@ -11,7 +11,6 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:serverpod/serverpod.dart' as _is; import 'package:serverpod/serverpod.dart' as _is;
import '../auth/github_sign_in_refusal.dart' as _irgbwy1y; import '../auth/github_sign_in_refusal.dart' as _irgbwy1y;
/// Signing in with GitHub, or linking a GitHub account, was refused; nothing /// Signing in with GitHub, or linking a GitHub account, was refused; nothing

View file

@ -13,7 +13,6 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5; import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5;
import 'package:serverpod/serverpod.dart' as _is; import 'package:serverpod/serverpod.dart' as _is;
import '../studyset/studyset.dart' as _ie5k1jc2; import '../studyset/studyset.dart' as _ie5k1jc2;
/// A flashcard in a studyset. Its content lives in its revisions /// A flashcard in a studyset. Its content lives in its revisions

View file

@ -12,10 +12,8 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt; import 'dart:typed_data' as _idt;
import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5; import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5;
import 'package:serverpod/serverpod.dart' as _is; import 'package:serverpod/serverpod.dart' as _is;
import '../card/card.dart' as _i4926i81; import '../card/card.dart' as _i4926i81;
/// One version of a card's content, never changed once written (ADR 0001, /// One version of a card's content, never changed once written (ADR 0001,

View file

@ -12,7 +12,6 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5; import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5;
import 'package:serverpod/serverpod.dart' as _is; import 'package:serverpod/serverpod.dart' as _is;
import '../dictionary/dictionary_credit.dart' as _it7o1p0g; import '../dictionary/dictionary_credit.dart' as _it7o1p0g;
/// A dictionary the instance holds: one source in one meaning language, as /// A dictionary the instance holds: one source in one meaning language, as

View file

@ -19,7 +19,6 @@ import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
as _iais; as _iais;
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart' import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart'
as _izehhkf5; as _izehhkf5;
import '../asset/asset_endpoint.dart' as _ibwtj1i8; import '../asset/asset_endpoint.dart' as _ibwtj1i8;
import '../auth/account_endpoint.dart' as _ii6ycf6v; import '../auth/account_endpoint.dart' as _ii6ycf6v;
import '../auth/anonymous_idp_endpoint.dart' as _in0zita6; import '../auth/anonymous_idp_endpoint.dart' as _in0zita6;

View file

@ -12,18 +12,14 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _ida; import 'dart:async' as _ida;
import 'package:clock/clock.dart' as _io0w16m8; import 'package:clock/clock.dart' as _io0w16m8;
import 'package:serverpod/serverpod.dart' as _is; import 'package:serverpod/serverpod.dart' as _is;
import '../asset/file_pruning_future_call.dart' as _ie5h5r65; import '../asset/file_pruning_future_call.dart' as _ie5h5r65;
import '../auth/pruning_future_call.dart' as _igevg04y; import '../auth/pruning_future_call.dart' as _igevg04y;
/// Invokes a future call. /// Invokes a future call.
typedef _InvokeFutureCall = Future<void> Function( typedef _InvokeFutureCall =
String name, Future<void> Function(String name, _is.SerializableModel? object);
_is.SerializableModel? object,
);
extension ServerpodFutureCallsGetter on _is.Serverpod { extension ServerpodFutureCallsGetter on _is.Serverpod {
/// Generated future calls. /// Generated future calls.

View file

@ -12,10 +12,8 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt; import 'dart:typed_data' as _idt;
import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5; import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5;
import 'package:serverpod/serverpod.dart' as _is; import 'package:serverpod/serverpod.dart' as _is;
import '../studyset/studyset.dart' as _ie5k1jc2; import '../studyset/studyset.dart' as _ie5k1jc2;
/// A learning path in a studyset: one lesson, an ordered sequence of steps the /// A learning path in a studyset: one lesson, an ordered sequence of steps the

View file

@ -13,7 +13,6 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5; import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5;
import 'package:serverpod/serverpod.dart' as _is; import 'package:serverpod/serverpod.dart' as _is;
import '../learning_path/learning_path_step.dart' as _i0qbf905; import '../learning_path/learning_path_step.dart' as _i0qbf905;
/// One answer to a step of a learning path, never changed once written: where a /// One answer to a step of a learning path, never changed once written: where a

View file

@ -13,7 +13,6 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5; import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5;
import 'package:serverpod/serverpod.dart' as _is; import 'package:serverpod/serverpod.dart' as _is;
import '../learning_path/learning_path.dart' as _ixd0fbzl; import '../learning_path/learning_path.dart' as _ixd0fbzl;
/// A step of a learning path: something to read, or an exercise to answer. Its /// A step of a learning path: something to read, or an exercise to answer. Its

View file

@ -12,10 +12,8 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt; import 'dart:typed_data' as _idt;
import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5; import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5;
import 'package:serverpod/serverpod.dart' as _is; import 'package:serverpod/serverpod.dart' as _is;
import '../learning_path/learning_path_step.dart' as _i0qbf905; import '../learning_path/learning_path_step.dart' as _i0qbf905;
/// One version of a step's content, never changed once written (ADR 0001, /// One version of a step's content, never changed once written (ADR 0001,

View file

@ -13,7 +13,6 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5; import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5;
import 'package:serverpod/serverpod.dart' as _is; import 'package:serverpod/serverpod.dart' as _is;
import '../studyset/studyset.dart' as _ie5k1jc2; import '../studyset/studyset.dart' as _ie5k1jc2;
/// A note in a studyset: rich text the person writes and reads, never studied. /// A note in a studyset: rich text the person writes and reads, never studied.

View file

@ -12,10 +12,8 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt; import 'dart:typed_data' as _idt;
import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5; import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5;
import 'package:serverpod/serverpod.dart' as _is; import 'package:serverpod/serverpod.dart' as _is;
import '../note/note.dart' as _iu6jc5sx; import '../note/note.dart' as _iu6jc5sx;
/// One version of a note's content, never changed once written (ADR 0001, /// One version of a note's content, never changed once written (ADR 0001,

View file

@ -13,7 +13,6 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5; import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5;
import 'package:serverpod/serverpod.dart' as _is; import 'package:serverpod/serverpod.dart' as _is;
import '../pdf_source/pdf_source.dart' as _ijdmmu1w; import '../pdf_source/pdf_source.dart' as _ijdmmu1w;
/// How far a PDF has been read, as an entry that is never changed once written: /// How far a PDF has been read, as an entry that is never changed once written:

View file

@ -12,10 +12,8 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt; import 'dart:typed_data' as _idt;
import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5; import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5;
import 'package:serverpod/serverpod.dart' as _is; import 'package:serverpod/serverpod.dart' as _is;
import '../studyset/studyset.dart' as _ie5k1jc2; import '../studyset/studyset.dart' as _ie5k1jc2;
/// A PDF in a studyset, to read and learn from: its cards and notes can be tied /// A PDF in a studyset, to read and learn from: its cards and notes can be tied

View file

@ -25,7 +25,6 @@ import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
as _iais; as _iais;
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart' import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart'
as _izehhkf5; as _izehhkf5;
import 'asset/asset.dart' as _ipb7vkxr; import 'asset/asset.dart' as _ipb7vkxr;
import 'asset/file_allowance.dart' as _i9uv0f1n; import 'asset/file_allowance.dart' as _i9uv0f1n;
import 'asset/file_limit_exceeded_exception.dart' as _it5d94hq; import 'asset/file_limit_exceeded_exception.dart' as _it5d94hq;

View file

@ -13,7 +13,6 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5; import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5;
import 'package:serverpod/serverpod.dart' as _is; import 'package:serverpod/serverpod.dart' as _is;
import '../card/card.dart' as _i4926i81; import '../card/card.dart' as _i4926i81;
/// One rating of a card, never changed once written: a card's schedule isn't /// One rating of a card, never changed once written: a card's schedule isn't

View file

@ -11,17 +11,13 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:io' as _idi; import 'dart:io' as _idi;
import 'package:serverpod/serverpod.dart' as _is; import 'package:serverpod/serverpod.dart' as _is;
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart' import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart'
as _izehhkf5; as _izehhkf5;
import 'endpoints.dart' as _iavctuc6; import 'endpoints.dart' as _iavctuc6;
import 'protocol.dart' as _il2as5qe; import 'protocol.dart' as _il2as5qe;
import 'sync_tables.dart' as _ii3f3u05; import 'sync_tables.dart' as _ii3f3u05;
export 'package:serverpod/serverpod.dart' hide Serverpod; export 'package:serverpod/serverpod.dart' hide Serverpod;
export 'future_calls.dart' show ServerpodFutureCallsGetter; export 'future_calls.dart' show ServerpodFutureCallsGetter;
/// The Serverpod server for this project. /// The Serverpod server for this project.

View file

@ -11,7 +11,6 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt; import 'dart:typed_data' as _idt;
import 'package:serverpod/serverpod.dart' as _is; import 'package:serverpod/serverpod.dart' as _is;
/// A person's settings that follow them to every device (ADR 0004): study goals /// A person's settings that follow them to every device (ADR 0004): study goals

Some files were not shown because too many files have changed in this diff Show more