docs(meditate): ADR 0038, D16, the operator's library, as the developer asked

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-03 16:48:45 +01:00
commit c44cf55fd4
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -204,6 +204,89 @@ names and pictures are its own). Everything below is Halkyon's own.
query on Android, the microphone's and speech recognition's usage texts on iOS and
macOS, and macOS's `audio-input` entitlement.
## The operator's library (3 Nov 2026, the developer's request)
- **D16 Guided content the operator provides**, asked for by the developer on 3 Nov
2026, structured as 7Mind's catalogue is (items with a title, a category, a
description, a speaker and recordings; categories; courses of ordered lessons). Only
the structure is taken: nothing of 7Mind's (texts, recordings, names, ids) is in the
repository, its tests or its fixtures, whose entries are made up. The agent's
recommendations, to be confirmed:
- **A folder and a manifest.** The operator mounts a folder into Meditate's server and
names it in `MEDITATE_LIBRARY_DIR`: recordings and pictures, and `library.yaml`, YAML
rather than JSON because operators write it by hand and want comments (a commented,
made-up example: `halkyon_meditate_server/library.example.yaml`). An item: `id` (the
operator's slug, a-z, 0-9 and `-`, which sittings, favourites and lessons done refer
to, so never changed), `title`, `category`, `description`, `speaker`, `language`
(BCP 47), `image`, one or more recordings under `audio` (each a `file`, a `label`
telling them apart by length or voice, and a `duration`), and **`licence` and
`credit`, required**, with an optional `licence-url`. A course: `id`, `title`,
`description`, `category`, `language`, `image` and `lessons` in order, each an
`item` with an optional `title` of its own. The duration is the manifest's, in
seconds or `m:ss`, not read from the file: reading MP3's, AAC's and Opus's lengths
without a decoder is unreliable, and a wrong length would count wrong sittings; a
check that reads it can come with the deploy's image.
- **Categories**: Meditate's eleven keys (`meditation`, `sleep-story`, `sound`,
`movement`, `autogenic-training`, `progressive-muscle-relaxation`, `routine`,
`dream-journey`, `knowledge`, `morning-story`, `other`), named by the app in English
and German; and **the operator's own** under `categories:` with a key and an English
and a German name, never one of Meditate's keys: an instance's library is its own,
and "other" alone would hide what it offers.
- **Checked as read, refusing what fails, with the reason**: an item without a licence
or credit, a recording or picture outside the folder (a `..`, an absolute path or a
link leading out, each resolved), missing, or of another type (recordings: MP3, M4A,
AAC, Ogg, Opus, FLAC; pictures: JPEG, PNG, WebP), a wrong id, category or language,
no duration, an id taken twice, a course pointing at an item not served; the rest is
served. A manifest that doesn't read at all leaves the library as it was served (an
edit half done), one removed ends it. `bin/check_library.dart <folder>` reads a folder
as the server would, before it is deployed, and exits 1 on any problem.
- **In memory, not in the database**: the manifest is the library's whole truth and
small, read as the server starts and again every ten minutes by a future call
(`LibraryReloadFutureCall`, as Learn's dictionaries are reread), an unchanged folder
costing a hash of the manifest and a stat of each file. No table, no migration, and
nothing to keep in step.
- **For signed-in people alone** (`LibraryEndpoint.catalogue`, `requireLogin`): the
operator's licences may not allow offering it to the public.
- **The files on Serverpod's web server**, `/library/<served name>`, by ranges (relic's
`StaticHandler`: `Range`, `If-Range`, `ETag`), as the platforms' players fetch a
recording. A file is asked for by a served name the reading gave it (a hash of its
place, size and time with its extension); no request's path reaches the file system.
Since players can't all send the session's header, the catalogue brings an
**access**: an hour's start plus 12 hours, signed with a key the server makes as it
starts (a restart ends every access; the app asks again), in the URL's query.
Without one: 401; one not signed here or ended: 403; any other name: 404. Starting
from the hour, the access is the same for everyone given it within that hour, so a
fetch names no account (the privacy page says so). Rejected: an endpoint streaming
the bytes (Serverpod's endpoints answer whole, no ranges), Caddy serving the folder
(it can't check a session or an access without a plugin).
- **In the app**: Explore shows the library below Meditate's six practices, with chips
of the categories it uses and the same search, its courses (lessons done counted)
then its items; an item's page has its recordings, Play, the heart, keeping it on the
device, for a sound a silent sitting with it looped (the timer's length), and its
credit and licence; a course's page lists its lessons, ticked once done. The player
(`GuidePlayer` on audioplayers, beside the bells') plays, pauses and seeks; played to
its end, an item is kept as a sitting of its whole length under `library/<id>`, so
the figures count it, and as its course's lesson done; stopped before, nothing.
**Playing on with the screen off** (Android's foreground service and media
notification, iOS's `audio` background mode, as `audio_service` does) is for later:
not cheap to do well.
- **The person's, sealed and synced**: favourites of the library in the settings' list
beside the practices', as `library/<id>`; lessons done in a new table, `lesson_done`,
a row each under an ID keyed by the account, the course and the item, so two devices
doing it make one row; both name the operator's ids, so a lesson whose course went
counts again should it return.
- **Offline**: the last catalogue is kept in the person's folder (`library.json`), and a
recording is kept on the device only on the person's asking, removed again on it (and
with their folder); a kept one plays from there.
- **No library, nothing**: an instance without one shows no section in Explore and no
credits in Settings.
- **Credits**: Settings lists every item's credit and licence beside About (*Library
credits*), and each item's page shows its own.
- **The deploy, later** (step 6): the folder mounted read-only into Meditate's
container (`/srv/meditate-library:/library:ro`, `MEDITATE_LIBRARY_DIR=/library`),
Caddy forwarding `/library/*` to the web server with the rest, `check_library`
compiled into the image so an operator checks a folder on the host.
## Steps
1. **The shell** (3 Nov): Todo's copied and renamed, its features left out, the three tabs
@ -216,7 +299,10 @@ names and pictures are its own). Everything below is Halkyon's own.
5. **The dream journal** (3 Nov): D14, the `dream` table and its migration, the Journal
tab, a dream's screen, Today's card, Settings' *Journal*, Profile's figures; and
dreams spoken (D15).
6. **Released and deployed** (later, as Todo's step 5): `apps/meditate/release.dart`, a key
6. **The operator's library** (3 Nov): D16, the server's reading, endpoint, files and
check, the `lesson_done` table and its migration, Explore's section, an item's and a
course's pages, the player, keeping on the device, the credits.
7. **Released and deployed** (later, as Todo's step 5): `apps/meditate/release.dart`, a key
of its own, the F-Droid listing, a snap, `deploy/compose.meditate.yaml` at
meditate.halkyon.eu (its `.env` values the developer has set already, 2 Nov), its
database and role, `redeploy.sh` and `backup.sh` taking it in.
@ -242,13 +328,20 @@ names and pictures are its own). Everything below is Halkyon's own.
`dream_space_idx` on `spaceId`, its foreign key to `offline_sync_spaces("id")` with
`ON DELETE CASCADE`, so a person deleted takes their dreams with their space; then the
version rows. A new, empty table: nothing existing changes.
- `library` (D16): the table `lesson_done` as `dream` is (`CREATE TABLE "lesson_done"`
with the same columns, `id` a UUID v7 by default, the function again `CREATE OR
REPLACE`), the index `lesson_done_space_idx` on `spaceId` (btree), its foreign key to
`offline_sync_spaces("id")` `ON DELETE CASCADE`, so a person deleted takes their
lessons done with their space; then the version rows. The library itself has no
table. A new, empty table: nothing existing changes.
## Later
- Recorded guidance (D4), the cues read out by text-to-speech, bells in the background
(D6), a daily reminder (D12), sittings written to Health Connect as mindfulness
sessions (Android 14's `MindfulnessSessionRecord`), courses of several practices in
order (7Mind's courses), ambient sounds.
sessions (Android 14's `MindfulnessSessionRecord`), courses of Meditate's own
practices in order; for the library (D16), playing on with the screen off, durations
read from the files, the pictures shown.
## Consequences
@ -256,4 +349,7 @@ names and pictures are its own). Everything below is Halkyon's own.
`practice.dart` and its words in the two ARBs; a practice once used keeps its ID, as
sittings name it.
- Without recorded voices, Meditate is a timer with guidance more than a library;
recorded guidance (D4) is what would make it more.
recorded guidance (D4) is what would make it more, and an instance's library (D16)
brings it where its operator offers one.
- An operator's library is theirs to license: Meditate refuses an item without a licence
and credit, but can't check that they hold.