halkyon/apps/learn/halkyon_learn_server/migrations/migration_registry.txt

57 lines
1.9 KiB
Text
Raw Normal View History

### AUTOMATICALLY GENERATED DO NOT MODIFY
###
### This file is generated by Serverpod when creating a migration, do not modify it
### manually. If a collision is detected in this file when doing a code merge, resolve
### the conflict by removing and recreating the conflicting migration.
20260916165410927
20260930094729362-studysets
20260930122235111-cards
20260930145136285-reviews
20261001064926911-tree-tags-assets
20261002035419149-file-allowance
feat: notes in studysets, synced and merged as cards are, with tags Server: three synced tables, only additions (migration 20261002191122122-notes): `note` (its studyset, a fractional position), `note_revision` (append-only, as a card's: parents, author, report, the payload {schemaVersion, title, body}) and `note_tag` (one row per assignment, never updated, as `card_tag`). Each indexes `spaceId`, and everything goes with its space, studyset, note or tag. A migration of synced tables locks app builds from before it out of syncing (ADR 0002, rule 3): cheap now, before the first release. App, `feature/note`: - `NoteContent` is a title and rich text, merged apart (ADR 0003): a title changed on one device and the body on another both survive, a body changed on two is a conflict. - `NoteRepository` creates a note at the end of its studyset, saves an edit on the revisions it started from (`saveEdit`, as the card composer now does), says when a note was last edited (its newest head's UUID v7), counts notes per studyset, and deletes a note's revisions blanked first (ADR 0002, rule 2). - Notes share the person's tags: `setTagsOfNote`, and folding tags of one name or undoing a tag's deletion covers notes too. - The delayed deletion with its Undo moves to `core/async` (`DelayedDeletions`): `StudysetDeletions` builds on it and now takes a studyset's notes too; `NoteDeletions` deletes a single note. Tests: content, merge, payload, repository, deletions, tags, the device migration guard, and sync tests against the server (four). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-02 21:53:56 +02:00
20261002191122122-notes
feat: learning paths in studysets, synced, with progress and tags Server: five synced tables, only additions (migration 20261002211346701-learning-paths): - `learning_path`: its studyset, a fractional position, and its own content in the payload {title, secondaryTitle, description}, the last writer winning, as a studyset's; - `learning_path_step`: its path, a position, and its kind ('info', 'singleChoice', 'multipleChoice', 'dragAndDrop', 'fillInTheBlank'), fixed once created; - `learning_path_step_revision`: a step's content as append-only revisions, as a card's: {title, body, exercise, rightFeedback, wrongFeedback, sources}; - `learning_path_answer`: an append-only log (right, wrong, continued, reset), as reviews are, so answers from two devices never conflict; - `learning_path_tag`: one row per assignment, as `card_tag`. App, `feature/learning_path`: the five exercises and their JSON, grading that marks only the learner's own wrong picks, progress worked out from the log (a step is done once answered right since its last fresh start), `LearningPathRepository` (steps saved on what they started from, merged per field) and `PathProgressRepository` (answers, resets with Undo, the times steps were done). Paths share the person's tags; deleting a path or a step can be undone for a moment, and a studyset's deletion takes its paths. Tests: models, codecs, repositories, tags, the device migration guard, and sync tests against the server (four). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-02 23:31:20 +02:00
20261002211346701-learning-paths
feat: PDF sources: read, take notes and cards from pages, deadlines PDFs live in a studyset's new Files tab: uploaded from Add material (a file the studyset holds already asks first), each row with its pages, cards, notes and size, how far it is read and its reading deadline; renamed, given a deadline (a page or a chapter by a day), marked as read, started afresh, deleted with a moment to undo. The reader (/pdfs/:id) opens where reading left off: on wide windows a rail of tools (search, fit, zoom, fullscreen), a page counter with the chapter, and a sidebar; on phones a top bar, a page scrubber and a sheet. The sidebar holds the PDF's flashcards, by page or chapter, made from selected text or written there, each tied to its page and tagged with its chapter; and the page's note, an ordinary note tied to the page. Screenshots of a region become images that know their page. Cards and screenshots lead back to their page; reading on past a chapter with cards suggests studying it. Home shows the deadlines running and to come, and a timeline of them. Server: pdf_source rows (content in the payload, last writer wins) and an append-only pdf_reading log, the furthest page derived from it so devices never conflict. Files up to 50 MB for PDFs (20 MB for others; a larger file that isn't a PDF is deleted once stored). A deleted PDF takes its file's asset row along unless another PDF has the same file. Card payload 4 and note payload 2 keep the page. The reader draws with pdfrx (PDFium) behind PdfEngine in Dependencies; widget tests use a fake, and integration_test/pdf_engine_test.dart checks the real engine on a device or the desktop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-03 14:40:08 +02:00
20261003110855698-pdf-sources
20261007205440868-card-buried-at
20261008165051407-pdf-position
20261008223835206-dictionaries
20261009123850448-dictionary-kind
feat: a dictionary's corrections reach the cards made from it, by offer Every dictionary entry now has a version: 16 hex digits of the SHA-256 of what cards take of it (headword, reading, entry), made by the loader and carried on every hit. A card keeps the version it took (card schema 8). DictionaryEndpoint.changed returns, of the entries named with the versions cards keep (1,000 at most a call; the app asks 500 at a time), those whose version is another now. The card browser says how many cards are behind their dictionary and takes the corrections at a tap (decided 2026-10-09: by offer, never by themselves). Taking one writes the entry as it is now over the revision in which the card last took it, beside whatever came after, so the revisions merge per side as two devices' edits do (ADR 0003): a side only the dictionary changed takes the correction, a side the person changed too is a conflict settled in the composer, whose words now name that case. Cards made before versions are never offered one. The server rereads its dictionary folder hourly now, not daily: the dictionaries service fills its volume after an instance's first start, and an hour with nothing changed costs a hash of each file. Migration 20261009145434785-dictionary-entry-version: adds dictionary_entry.version, text NOT NULL DEFAULT '' (rows loaded before have none and are never reported as corrected), and by hand empties dictionary_source."fileHash", so every source loads once more and its entries get their versions; the schema stays as definition.sql says. The old server goes on working until the switch. 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>
2026-10-09 17:32:00 +02:00
20261009145434785-dictionary-entry-version
feat: public profiles, a handle on the account domain with name, bio, links and picture Stage 1 of the public tier (ADR 0005). A person with an identity has a public profile: a handle by Mastodon's rules (1 to 30 letters, digits or underscores, unique whatever its case, a few kept for the instance), changeable until they first publish, on the instance's account domain (ACCOUNT_DOMAIN, else the server's host; the instance info names it), so `raisondetredev@halkyon.eu`; a display name, a bio, up to four links and an avatar. The table is server only: the app gets a Profile without the person's id. ProfileEndpoint (identified only) reads, saves and removes the caller's own; ProfilesEndpoint finds anyone's by handle. An avatar is a PNG of at most 1 MB that the app draws anew from the picture chosen, its middle square at 400 pixels, so a photo's metadata never leaves the device. The server keeps it in the public storage named by its content, so its URL tells nothing about whose it is, and deletes it once no profile shows it. Deleting a person deletes their profile and its picture. In the app, the Profile tab holds the profile as a form, laid out as the old app's; anonymous people are told GitHub sign-in gives them one. Migration 20261009184924904-public-profiles creates public_profile with a unique index on the person and one on the handle in lower case; it touches no other table, and the old server never sees it. 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>
2026-10-09 20:55:16 +02:00
20261009184924904-public-profiles
20261009195137836-published-studysets
20261009202105673-explore
20261009215552429-learner-count
20261009220324372-based-on
feat(feedback): reports and suggestions on published studysets, gathered by the instance Stage 4 of the public tier (ADR 0005, rule 6), its server and the app's data. A learner writes a report or a suggestion on a published studyset, a card, note or step of it, in their own synced space, so it works offline; replies and the author's decisions (closed, hidden, accepted, declined) likewise, in the writer's or the author's space. The instance gathers them: public to everyone signed in, but for what the author hides; for the author's inbox and the writer's. What counts is decided as rows are read, since a modified app could write anything: threads from people with an identity and a public profile the author hasn't blocked, replies from the writer and the author alone, decisions from the author alone, no suggestion under "no derivatives". The author gets the files a suggestion names from its writer's, and the next release credits the writers of the suggestions accepted since the last (Co-authored-by). Deleting a person deletes their blocks and what they read; what they wrote goes with their space. In the app, the repository writes reports and suggestions (a card, note or step as the learner has it, without what is theirs alone, with the files it names) and takes an accepted suggestion into the author's own card, note or step. 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>
2026-10-10 01:04:17 +02:00
20261009224606274-feedback
feat(assistant): Claude reads studysets through a connector on the instance A Claude plan can't drive a chat inside another app, but Claude's apps (every plan, the phone apps too) can add a remote MCP server as a custom connector. The instance now is one (ADR 0006): Serverpod's web server serves the MCP endpoint (/mcp, Streamable HTTP, JSON answers, no sessions) with five read-only tools over the person's own synced rows (their studysets, a studyset's cards and notes a page at a time, a search across cards, a note, their progress), and the OAuth the specification asks for: protected resource and authorization server metadata, dynamic client registration, authorization code with PKCE (S256 only), tokens bound to the resource, iss in the response, refresh tokens that rotate. Caddy forwards these paths alone to the web server, which production now runs on 8082. People connect with a code from the app (Settings › Assistant › Connect Claude): eight Crockford characters, valid for ten minutes and once, typed on the authorization page, which names where it returns to, loads nothing from elsewhere and may not be framed. Anonymous people can connect too. Codes and tokens are kept only as SHA-256 hashes; an access token lives an hour, a refresh token 30 days. The app lists what is connected and disconnects it, which ends its tokens at once; deleting the account deletes it; a daily job forgets what expired and clients nobody connected within a week. InstanceInfo says where the connector is (additive); the migration only adds server-only tables, so devices and older apps are untouched. The handlers take plain requests, so the integration tests drive the whole flow (registration, the code, tokens, refresh, the tools, another person's content, disconnecting, expiry, pruning, account deletion) without a web server. 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>
2026-10-10 12:13:00 +02:00
20261010095547719-assistant-connector
20261012162411835-studyset-deleted-at
feat(jellyfin): watch from one's own Jellyfin server and learn the words of its subtitles Animelon's way of learning from video, from the person's own Jellyfin server: connected per device (Settings, or a studyset's Add material › Watch from Jellyfin) with a name and password or a Quick Connect code, over HTTPS only (plain HTTP only to a server on the same machine); the token stays in the system's keyring. Shows, films and videos tied to a studyset follow the person in their settings as the server's and the item's ids alone (settings schema 9), named by the server when shown. The Watch screen plays the video (video_player on Android, media_kit's libmpv on Linux and Windows) with the line in the studyset's front language cut into the dictionary's words with furigana, and the back language's line beneath; a word tapped pauses the video and becomes a card with the line and its translation as its first hint, tagged with the show. The video's words are listed, most frequent first. The sound is the track in the studyset's language, whichever the file marks default, and the menu switches it; the subtitles read are a whole track, never a forced one (which only covers what the sound doesn't say), and captions for the hard of hearing only without plain ones. The line and its translation take the video's width in a box of a fixed height, so the video keeps its size; in the dark theme the video's colours glow around it, as YouTube's ambient mode, and the words' panel takes the colour of the episode's picture under the pointer. A line is cut into words with one request: DictionaryEndpoint.withKeys looks up every stretch of it that may be a word, as it stands and as every word it may be a conjugation of, in the new dictionary_key table (one row per key, indexed; migration dictionary-keys adds the table and its index, nothing else), which the loader fills beside the entries. The instance gets the text of each line looked up and keeps none of it, as the Watch screen says. Linux builds need libmpv-dev, and the app links libmpv: the snap stages Ubuntu's libmpv2, makes the update-alternatives links for BLAS and LAPACK that ffmpeg's speech recognition needs (staging never runs install scripts), and removes what the GNOME runtime and core24 already hold, as older copies of cairo, pango and librsvg hid the runtime's from its GTK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 11:31:37 +02:00
20261013035552510-dictionary-keys
20261014153932647-release-colour
feat(auth): sign in with an email address, keeping only a keyed hash of it People without GitHub can now keep an anonymous account, and sign in on another device, with an email address and a code mailed to it each time; no password. The instance keeps only HMAC-SHA256 of the address, keyed with its emailSignInPepper (ADR 0010): from what someone types it finds the person and tells whether an address is free, it mails only an address someone has just typed, and it can't write to anybody otherwise or show anybody their address. The device remembers the address it signed in with, in the person's folder, to show it. It works as GitHub does. A code for a new address makes it the signed-in person's, who becomes identified and keeps everything. A code for another person's address gives a switch token, which signs in as them without a session; a device holding studysets asks first. Asking for a code answers alike for every address. Codes are eight Crockford characters, valid 15 minutes and three tries (Serverpod's code challenges); three codes an hour per address, ten per network. In development the code goes to the server's log; elsewhere there is no email sign-in until a mail service is set up (instance.info().emailSignIn). Someone identified may add the other way in: the Profile tab lists how they sign in and adds GitHub or an address. They are never switched to another account: an address or GitHub account that is another's is said to be so. Switching now deletes the account left behind only when it was anonymous; it deleted any, which only anonymous people could reach so far. The migration adds email_identity and email_code_request with their indexes and foreign keys to Serverpod's challenges; nothing else changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-15 19:42:21 +02:00
20261015171326465-email-sign-in
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
20261015220121195-verified-links
feat(discussions): questions on published studysets, answered by learners A learner asks about a published studyset, a topic in it, or one card, note or step of a release, and others answer, as on Stack Exchange (ADR 0013): questions and answers in the app's editor without images, sounds or videos, comments of up to 600 characters under the question and each answer. There are no votes: the asker accepts an answer, which comes first, then the oldest; the author's posts are marked. The instance reads from git's file list which topics a question's target sits in, so a topic lists what is asked within it and below it. Posts live in two server-only tables, written through endpoints, so the synced tables stay as they are and v0.17.0 keeps syncing. Everyone signed in reads; writing needs an identity and a public profile; the studyset's author hides posts and blocks people with feedback's blocks; a withdrawn studyset takes no new questions; anyone writes at most 30 posts an hour. A post with others' replies under it is emptied when deleted, else deleted; deletePerson deletes what a person wrote the same way, and a published studyset's deletion takes its discussions with it. The migration adds Serverpod's gen_random_uuid_v7(), discussion_post and discussion_seen with their indexes and cascading foreign keys, and changes nothing else. In the app, discussions open from a copy's studyset or topic menu, a studied card's "Ask about this", Explore's summary and the Publish sheet; the Profile tab's inbox lists those the person follows, and its count includes what is new in them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-15 23:52:40 +02:00
20261015220626303-discussions
20261017110130653
20261019121406578-halkyon-base
20261019201023295-profiles-in-halkyon-base
20261019204112282-deletion-notices
20261020111943300-passwords
20261020125117584-sessions-ended
20261020185750538-recovery-proof-key
fix(auth): codes wait after ten wrong in a row, and the owner is told Whoever knew the password could guess codes at 10 an hour forever, unnoticed, each guess checked against every authenticator (the second reading's M4). Every code an account's second factor refuses after a right password (sign-in, a phrase's reset, a change) or from a signed-in device (removing an authenticator, new recovery codes, the code from one confirmed before) now counts, per account, from every network; after every tenth in a row no code is taken (codesWait, saying until when), an hour, then twice as long each time, a day at the most (longestCodeWait, second_factor_waits.dart): at a day, ten guesses a day still have about a 1 % chance a year against one authenticator, so the first wait mails the owner that someone knows their password. The instance keeps no address, so sign-in and the phrase's reset now send the address the device typed (and its language), used only for that mail after its hash is checked against the account's. A code accepted clears it all. Migration (module, Learn's and Todo's servers, Learn's device schema): CREATE TABLE "second_factor_failures" ("id" bigserial PRIMARY KEY, "authUserId" uuid NOT NULL, "failures" bigint NOT NULL, "waits" bigint NOT NULL, "waitUntil" timestamp, "updatedAt" timestamp NOT NULL) with a unique index on "authUserId": a new table, nothing touched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 21:47:38 +02:00
20261020194228286-second-factor-waits
fix(auth): a session's wrong codes end the session, not the account's codes Wrong codes for removing an authenticator, new recovery codes or confirming one counted toward the account's waits (the re-review's third finding): a stolen session alone, without the password, could keep the owner's codes waiting for up to a day at a time, renewed without end, holding them out of signing in and of changing the password, the way to end that session, and nobody was mailed. Those codes now count per session, where the session is (the register's own, or a member's, which forwards the code): each try counted before its code is checked (one statement, so tries sent at once count each), a code accepted clearing the count, and the tenth wrong in a row ending the session (sessionEnded; the device says it was signed out). A try that checked no code (a refusal of something else) is taken back. The account's waits now count only codes after a right password, and the per-account ten tries an hour, which a session could fill as well, go. Migration (module, Learn's and Todo's servers, Learn's device schema): CREATE TABLE "session_code_failures" ("id" bigserial PRIMARY KEY, "sessionId" uuid NOT NULL, "authUserId" uuid NOT NULL, "failures" bigint NOT NULL, "updatedAt" timestamp NOT NULL) with a unique index on "sessionId": a new table, nothing touched. Its rows go with a code accepted, with the person, and hourly once their session is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 22:53:20 +02:00
20261020204619104-session-code-tries
20261020211224692-email-code-purpose
20261021100422048-session-ended-warning
20261021110159867-second-factor-paths
20261021134803749-reset-hold-outs
20261021184601197-private-studysets
feat(assistant)!: no hand-off and no connector; the field searches the web (ADR 0022) The developer decided on 23 Oct to drop the hand-off to an AI assistant outside the app, and Claude's connector with it, in v0.19.0, the app and the server together: build 22 raises minimumAppBuild anyway, so no app calling the connector's endpoints uses the instance after it. An instance is to have one way to an assistant, its operator's (ADR 0021, proposed). The app: the field at the foot of Home, and the button in the Library, on a studyset and while studying, search the web alone, through the engine chosen in Settings or Android's own search, with the parts of a card the person picks; a studyset's name and description are never offered. Gone: the assistants a question opened in (AiAssistant, AssistantHandOff), the switch between them and the web, Settings' choice of assistant, Connect Claude with its route, cubit, repository and gateway, and their texts. A settings file from before keeps the old keys until the next change of a setting writes it anew; nothing reads them. The server: lib/src/assistant/ (MCP, its OAuth, the five tools, connect codes and tokens), AssistantEndpoint, the daily forgetting of the connector's secrets (its scheduled call cancelled at start), its lines in the deletion hook, instance.info().assistantConnectorUrl (nullable, so an older app reads it as none), and Caddy's forwarding of /mcp, /oauth/* and /.well-known/oauth-*. The migration drops the connector's five tables, children first: DROP TABLE "assistant_token" CASCADE; -- tokens of a connection DROP TABLE "assistant_grant" CASCADE; -- OAuth codes of a connection DROP TABLE "assistant_connection" CASCADE; -- a person's connected client DROP TABLE "assistant_connect_code" CASCADE; DROP TABLE "assistant_client" CASCADE; -- OAuth clients registered No other table references them, so CASCADE drops only their own foreign keys. For the minute between the maintenance run and the switch, the old server answers the connector with errors and fails to delete an account (its hook still deletes from these tables); ADR 0022 says so. BREAKING CHANGE: a connected Claude gets no answer from the deploy on, and the app no longer opens a question in an AI assistant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 15:33:53 +02:00
20261023131636437
20261023181810112-oidc-module
feat(base): SimpleLogin accounts' keys without SRP, signing up in one call (ADR 0020, step 3) An account that signs in with SimpleLogin keeps its keys under a password that signs nobody in (D19): `account_password` keeps the password's own proof key (Ed25519, `passwordProofKey`) in place of SRP's verifier, which becomes nullable. The migration adds the column and drops the NOT NULL, nothing else. - `simpleLogin.signUp(attempt, handle, setup)`: someone new whose identity waits under the attempt gets, in one transaction, an account (identified), the link, a profile with the handle and the keys; then a session (D18). Refused, the identity still waiting, for a setup out of range and a handle refused as saving a profile refuses it. - `simpleLogin.checkHandle(attempt, handle)`: whether someone new could take a handle, at most 30 an hour per attempt and 60 per network. - `simpleLogin.complete` says, for an account, whether it has its keys and its handle, and keeps the sign-in ten minutes under its attempt, the account as its caller: D20's confirmation for the changes to come. - `keys.begin`, `keys.unlock`: the keys for a signed-in device once the password is proven with its proof key, for a nonce, bound to the base URL the device addressed; wrong proofs counted per session as ADR 0017's. - An authenticator is enrolled only for a password that signs in. - Members ask the register for all of it; `RemoteRegister` and the instance API carry the four new operations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 21:49:44 +02:00
20261023192913178-simplelogin-keys
feat(base)!: sign-ups as the operator says, waiting accounts, operators (ADR 0021, step 1) SIGN_UP takes open (unset), approval or closed; anything else stops the server as it starts. Closed refuses a SimpleLogin sign-up before its handle is counted, an address sign-up only once the code has proved the address, so nothing tells whether an address has an account. Approval lets a new account wait, at most ten at once: no session is issued for a waiting or suspended account, only a pass (32 random bytes, kept as its SHA-256, 30 days, five per account) and four BIP-39 words the operator reads out to match it. The pass looks, forgets, and deletes an account that was never approved; nothing else. Operators hold the scope operator, given first by SQL (D32), then named in the app: InstanceOperatorEndpoint lists, approves and refuses waiting accounts, finds accounts, suspends them (ending their sessions), and names and takes back operators, never the last. account.operating() counts the waiting for the badge. Members ask the register (five new operations, a refusal kind signUp), and say closed while it is out of reach. Never-approved accounts go after 14 days in the daily pruning. The migration only adds the tables waiting_account and waiting_pass, so the server before it keeps working on the new schema. BREAKING CHANGE: a register and its members must run this version together: a member asks the register operations an older one doesn't know. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 15:04:21 +01:00
20261025135744595-waiting-accounts
feat(base,learn,todo): the operator's assistants, and the instance's privacy page (ADR 0021, step 2) The server half of the assistants (points 4 to 8, D27 to D46): the operator sets language models in deploy/assistants.env, grants them to people with limits, and the module forwards their questions with the operator's key, counting what each used, never what was asked. - assistantsFrom: ASSISTANTS, ASSISTANT_ID_KEY, ASSISTANT_<ID>_NAME, _PROTOCOL (anthropic, openai, openai-responses), _BASE_URL, _MODEL, _KEY, _IMAGES, _PRICE_INPUT and _PRICE_OUTPUT (per million tokens), ASSISTANT_CURRENCY, ASSISTANT_OWN_KEYS; anything unusable stops the server as it starts, naming the variable. - The assistant endpoint (identified): offered, agree (D40), ask as a Serverpod stream (D35): the text as the model writes it, then the whole turn, sent once the request is counted; figures, the caller's own. Checked in order: at most 2 questions on their way per person (tooManyAtOnce), the grant, the agreement, limits in the pricing's unit now (limitOutdated), the month's and the day's limits (with when they reset, UTC), the question (malformed, imagesNotRead, tooLarge past 400,000 characters, an image over 3,932,160 bytes or more than 20 images); output capped at 16,384 tokens. Each request carries the person's opaque ID (HMAC-SHA256 under ASSISTANT_ID_KEY, D30). Once sent, the provider's reply is read to its end even if the app cancels, and counted on a session of its own; a provider's failure is logged by kind and status, told as providerKey, providerBusy (with its wait), providerRefused or providerFailed. - The assistantOperator endpoint (identified and operator, on the register): setUp, grants, grant (assistantNotSet, invalidLimit, notFound), revoke, figures of a UTC month. - Tables assistant_grant and assistant_figures (per person, assistant and UTC day), in the module's, Learn's and Todo's migrations. The daily pruning keeps this month and the two before (forgetOldAssistantFigures, D41); deletePerson deletes both. A member offers no assistant (D43). - instance.basics() gains assistantOwnKeys. - /privacy (D39, addPrivacyPageRoute, on the register's web server, before the profile pages' route): made from the instance's settings, English or German, no JavaScript: who may sign up, where the data and backups are, what the module keeps (the account, sessions, IP addresses, the server's log), the host's privacyPagePoints (Learn's: studysets synced, private ones encrypted on the devices with what stays readable, what is published, dictionary lookups, the learner count), each assistant with its model and host, what goes and what is counted, and whether personal keys are allowed. Never a key. Tests: 70 on the assistants (grants, limits, refusals in order, streaming, cancelling, counting, concurrency, the operator's denials, deletion, pruning), 19 on the settings, 8 on the privacy page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 18:07:01 +01:00
20261025165102861-assistants
20261025184004860-assistant-conversations
20261026140329810-assistant-budgets
refactor(base,learn): people's files kept by halkyon_base ADR 0043, D2, D4, D6 and D7, the first step towards Todo's attachments: Learn's file store moves into the module, for every server that keeps files, and Learn becomes its first host. halkyon_base gets the store as Learn had it: uploads into the caller's own space by SHA-256, downloads by a row the caller may read, each person's room (FILE_LIMIT_IDENTIFIED_MB, FILE_LIMIT_INSTANCE_MB, the operator's file_allowance, which moves into the module unchanged) and the pruning of files no live row names. It stays free of the sync engine: a host hands it a FileStore saying which of its rows name a file and what it takes (HalkyonBase.files); without one the files endpoint refuses. Its tests, with refusals, run on a stand-in host. Learn hands it learnFiles: its asset rows, read by their space's members, a readable file over 20 MB only as a PDF, up to 100 MB. Its app calls the module; its own asset endpoint stays for released apps alone, forwarding to the module in Learn's own models, whose names are on the wire. Its file tests still pass through it, end to end. Migrations: the module's creates file_allowance. Learn's server migration holds version rows alone, as tables are matched by name (ADR 0014); its device migration creates the module's empty table, as every module's. Todo's, Train's and Meditate's create the empty table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 00:11:36 +01:00
20261110225358224-file-store-in-halkyon-base
feat(base,learn): spaces people share, kept by halkyon_base ADR 0044, step 2 (D1, D2, D11, D13, and D15 to D17, decided today). halkyon_base keeps the spaces several people share, for every app: shared_space, its members with their roles (Owner, Editor, Viewer), and invitations by handle waiting for someone with no account on the service yet. The sharedSpace endpoint creates a space, lists its members, invites by handle, changes roles, removes members, hands the space on, leaves and deletes it. Who may do what: - Only people with an identity reach the endpoint. - A space the caller isn't in can't be told from one that doesn't exist. - Members are named by their membership, never by their account's ID. - Only the Owner manages the members, unless the space lets members invite. - The Owner's role passes only by handing the space on, and the Owner leaves only naming who takes it on. - An invitation counts as an answer about a handle, as it tells whether one exists. Every rule has a test asserting the refusal. The module stays free of the sync engine: a host hands it a SpaceGrants, and every call runs in the module's transaction, so the engine's grants follow the roles or nothing changes. The Owner and an Editor write; a Viewer reads. Learn hands over SyncSpaceGrants on OfflineSyncSpaces. Todo hands over none yet, so its server refuses until its projects are shared. Deleting an account hands each space the person owns to the Editor who joined first, else the Viewer who joined first. A space with no other member goes. This runs before the host's own deletions. A person's first sign-in on a service makes their waiting invitations memberships, in the same transaction that makes them a person there. Migrations: the module's and every host's (Learn, Todo, Train, Meditate) create the three tables, their indexes and the two cascading foreign keys to shared_space, and nothing else. The old server keeps working. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 10:33:12 +01:00
20261111091720823-shared-spaces
20261111100124819-shared-space-keys