Decided 2026-10-01. Deleting a studyset from its own screen closes that
screen, which would have dropped a deletion kept there, and the studyset
would have quietly stayed. StudysetDeletions, opened with the person's data,
now keeps every deletion that can still be undone: a deleted studyset leaves
every list at once, and once the time to undo is over the whole subtree is
deleted, what is inside first, each studyset's cards blanked before it. It
closes first with the person's data: what still waits is dropped and stays,
what is under way finishes while the repositories are open. A failed
deletion shows what is left again.
The library's bloc hands its deletions to it and leaves out what it reports
as pending, instead of keeping timers of its own. Tests that follow a real
timer wait for the outcome (eventually) rather than for a fixed time a busy
machine might not keep.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The model learns the tree (ADR 0004): a studyset's children in order (by
position, ties by id), its subtree in tree order (the studyset, then each
child with its own subtree), and its ancestors from the top level down,
for the breadcrumb. Studysets moved into each other on two devices make a
cycle; subtrees stop where it comes back, and climbing stops at the
studysets that show at the top level.
The repository creates a studyset inside a parent, after its siblings;
moves one into another studyset or to the top level, after the studysets
already there, writing only its parent, position and updatedAt; refuses a
move into the studyset itself or anything inside it; and reorders a
studyset among its siblings with a fractional index, as cards do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Decided 2026-10-01. Deleting still asks first; once confirmed, the studyset
leaves the library at once and "Studyset deleted" offers Undo for three
seconds. Only when the time to undo is over (four seconds: the message's
three and a moment for a late tap) are its cards and then the studyset
blanked and deleted, since blanking can't be undone (ADR 0002, rule 2). A
deletion still waiting when the library closes (the app quits, the person
changes) is dropped, and the studyset stays.
The library's bloc keeps the waiting deletions; the event that ends the
time to undo is private, so no screen can skip it. AGENTS.md now describes
the library's shared widgets, the sheets (and why a sheet that asks before
discarding can't be swiped away), the order of studysets, and this Undo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Decided 2026-10-01: after the starred ones, the library lists the most
recently changed studysets first. That needs a change date, so studyset
gains updatedAt (ADR 0004): when the studyset itself last changed, its
content, place, star or archive date, written by the device making the
change in the same update as the change; the last writer wins. Studysets
from before have none and come last, the newest of them first.
The branch's migration is still unreleased (not merged, not deployed), so it
is recreated rather than followed by a second one:
20261001064926911-tree-tags-assets replaces 20260930213716558 and differs
only by one line on the server, ALTER TABLE "studyset" ADD COLUMN
"updatedAt" timestamp without time zone (nullable, so existing rows keep
null and no data changes), the same column in the table devices rebuild,
and its version. The model's comment on the payload no longer lists schema
1's fields alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Decided 2026-10-01, in place of "closing the account dialog cancels it",
which had no dialog left once the account moved into the Profile tab:
choosing another tab cancels a sign-in waiting for GitHub in the browser,
since the person has left it behind. Settings, opened over the profile, is
not leaving it.
Screens learn whether their tab shows from TabVisibility, which the tabs'
container now provides: go_router's indexed stack as before (every tab
alive, hidden ones offstage with their animations paused), each tab wrapped
in it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The library tab becomes a grid of studyset cards (up to 400 dp wide, all
equally tall), starred ones first, and the archive as a section of its own
below, closed until opened and counting what rests there. It lists the top
level: studysets without a parent, those whose parent is gone, and those in
a cycle (ADR 0004). Each card's progress follows the studysets and every
change of a card, here or synced, and is worked out again after a study
session; a slow, older working out never overwrites a newer one.
- Search: a field below the app bar filters by title and secondary title,
opens the archive while it finds something there, and says when nothing
matches; closing it shows everything again.
- Creating and editing: a sheet with the title, the secondary title (typed
in capitals, as every surface shows it) and the colour, picked from the
palette's twenty, whose names are localised; a new studyset is cobalt,
and an existing one keeps its colour, even one only a newer app version
knows, until another is picked. Create waits for a title. Closing the
sheet with changes asks first. It can't be swiped away: a bottom sheet
swiped away closes without asking its PopScope (Flutter issue 152120,
still open), so such sheets have no drag handle and close by their close
button, the back gesture or a tap beside them, which all ask.
- Archiving doesn't ask, since nothing is lost; it offers to undo, as does
unarchiving. Deleting asks, then deletes the studyset with its cards, each
blanked first.
- The create button floats in compact windows and slides away while
reading onwards; from medium windows on it sits in the app bar.
The old list and its title-only dialog are gone; the studyset screen's
tests create studysets through the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A studyset in the library: a bar in its colour, its title with the secondary
title as a kicker, what it holds, and beside the title the action it
invites inside a ring its mature cards fill, with a badge counting the due
cards (at most "99+", growing inwards so it never passes the card's edge).
The action is a review when reviews are due, studying when only new cards
are, and adding content otherwise, which opens the studyset: the card only
invites a session that has something to study. A long press stars it, as
does the star, which a hovering pointer reveals and which stays while
starred; the options edit, archive or unarchive, and delete it. An archived
studyset rests: dimmed, neutral, paused. Until its progress is known, a
muted bar stands in for "No content yet", which it might not be.
A starred star is amber, dark enough in the light theme to stand out from
the cards as a graphic must (3:1), a colour the theme extras now hold. It is
Material's filled star: Lucide has no filled one, and the fill is what says
the star is on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
What a library card shows about its studyset's cards: how many there are,
how many a session started now would study (the study queue's length, so the
card only invites a session that has something to study), how many of those
are reviews rather than new cards, and how many are mature.
A card is mature once FSRS's stability, the days it is remembered with a 90 %
chance, reaches 21: the three weeks after which spaced-repetition apps
traditionally call a card mature, measured by memory rather than by the
interval. A schedule now carries its stability.
Replaying every card's schedule from its reviews moves from the study
session's bloc into the review repository (schedulesOf), and the start of the
learner's local day into a function beside the study queue, so the session
and the library compute alike.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A studyset is now its content (StudysetContent, everything the synced payload
holds) and the columns that change on their own: where it sits (parent,
position), and when it was archived or starred. Like a card's content, the
payload's part is edited as one value; the columns are written one by one,
so starring or archiving on one device and editing on another all survive a
sync (ADR 0004).
The payload's schema 2 adds the secondary title, the colour (a palette name),
the licence (SPDX identifier or name, holder, year) and the languages of the
cards' two sides (BCP 47), each left out when not given. Schema 1 reads as
schema 2 without them, and a field of the wrong kind makes the payload
unreadable rather than half read. Older app versions skip schema 2 studysets,
which costs nothing before the first release; fields foreseeable for the
first release's features are in, so none of them needs a schema 3 later.
The repository stars, unstars, archives and unarchives (at clock time, in
UTC), writing only that column. A new sync test (run against a server)
checks that a star and an archive on one device and a rename on another all
arrive on both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The pieces the library's cards and its create/edit sheet are built from, each
with its tests:
- TitleWithKicker: a title with an optional kicker above it (a studyset's
secondary title, uppercased in the primary colour), a trailing action lined
up with the title, and a reserved kicker line so cards side by side line up.
- ContentCountsLine: "3 studysets • 52 flashcards", leaving out what is
unknown or zero, with a placeholder when nothing is left; labels are plural
messages, so every language words its own counts.
- showAdaptiveSheet and SheetScaffold: a bottom sheet below expanded windows
and a dialog from there on (a bottom sheet is a touch surface; expanded
windows are mostly desktops). The title with its close button and the
footer stay in view, only the body scrolls; the close button asks the
route to close, so a sheet can ask before discarding changes.
- showUndoSnackBar: what just happened, with Undo for three seconds.
- ScrollHidingFab: a floating action button that slides away while reading
onwards and returns on scrolling up, and stays where nothing scrolls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The app gets its look and navigation before more features land on them.
- Theme (core/theme/): hand-written light and dark Material 3 colour schemes around
cobalt and copper; ThemeExtras for the colours Material has no role for (states, the
four FSRS ratings); the 20 colours a person can give content. Tests hold text to WCAG
AA contrast on each palette colour and on the dark theme's primary colour and surface.
- Layout and shared widgets: Material 3's window-size classes (core/layout/);
SectionCard, AppLoadingIndicator, confirmDestructiveAction and FoundById
(core/widget/). Lucide icons.
- Navigation with go_router: four tabs (Home, Library, Explore, Profile), each keeping
its own stack of screens, as a navigation rail with the app's icon from 600 dp and as
a navigation bar below; choosing the open tab again returns to its first screen. A
studyset's screen sits in the library; studying, cards and settings cover the tabs.
- Routes carry ids: FoundById finds the item in the repository's live list, so a
studyset deleted on another device, or an old link, says it no longer exists. A
screen that deletes its own item keeps showing it while it closes, rather than
flashing that message.
- Every person gets an app of their own (PersonApp, lib/src/app/): a
MaterialApp.router with its own GoRouter and PersonScope around the navigator, built
by SessionGate inside the person's keyed subtree. Screens read the person's
repositories wherever they are built, so the static route() helpers and the listener
that closed every pushed screen on a change of person are gone: a change of person
now disposes the whole router and starts again at home.
- The account dialog becomes the Profile tab, with Settings (sign out, delete the
account, About) behind a gear. A waiting GitHub sign-in now keeps waiting while
another tab shows; a change of person still cancels it.
- The new screens' strings come from Flutter's gen-l10n (English for now). The
generated files are committed, left out of analysis and formatting, and written
anew by `flutter pub get`, so CI's check of generated code also catches stale ones.
Widget tests now start the whole app (test/support/pump_app.dart) at the screen's path
and move through it as a person would, by tabs and taps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The schema of the features planned for the first release, as one migration: every
migration of a synced table locks older app versions out of syncing (ADR 0002, rule 3),
which costs nothing before that release. New card types need no migration; they are the
next version of the revision payload.
- studyset: `parent` (the tree; cycles and orphans show at the top level) and `position`
among siblings (fractional index; existing studysets at 'a0').
- tag (its name in the payload) and card_tag, one row per assignment, so concurrent
adds and removes merge.
- review_log.mode: review, reverse (a schedule of its own) or cram (never moves a
schedule); existing reviews are 'review'.
- person_settings: study goals now, preferences later; random ids, since an id derived
from the person could be claimed by someone else and break the owner's sync.
- asset: a file's hash, type, size and name; the file goes into Serverpod's private
cloud storage, served through an owner-checked endpoint (to come).
Migration 20260930211154224-tree-tags-assets only adds: four tables with their space
indexes and foreign keys, three columns with defaults. On devices SQLite rebuilds
studyset and review_log to add foreign keys, which is safe only with foreign keys off,
as Serverpod's runner does; a new test replays every client migration on a database
with a row in each of our synced tables and checks that none is lost (with foreign keys
on, it fails as it should).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The app took the editor from our fork of AppFlowy Editor in a folder next to the
repository, which nobody else had, and which CI can't reach either. The fork now lives at
https://code.halkyon.app/project-halkyon/appflowy-editor (public; dual-licensed AGPL-3.0
or MPL-2.0 like upstream), on its branch halkyon/win32-v6, and the app depends on it by
git URL, pinned to the signed commit 0dac8a0. That commit adds `publish_to: none` to the
fork: it is a git dependency only, and the package name belongs to AppFlowy on pub.dev.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Signing in with GitHub on Linux ended in "Unable to connect" at 127.0.0.1:43823 after authorizing. flutter_web_auth_2 (v5.1.0, unchanged on its master) ends every pending sign-in when the app is resumed. That suits mobile, where coming back without finishing means giving up, but on the desktop the window regaining focus resumes the app, and the package's loopback server closed before GitHub sent the browser back. FluffyChat's issue #3341 and the package's #171 report the same.
The app now puts its own Linux and Windows implementation behind the package's platform interface (LoopbackWebAuth): the system browser and a loopback address, as RFC 8252 recommends, waiting until GitHub answers, the person cancels, or five minutes pass. It listens on 127.0.0.1 only and answers only the callback's path. Serverpod's PKCE and state check stay in use. There, the account dialog shows "Continue in your browser." with a Cancel button, and closing the dialog cancels the sign-in.
Serverpod (v4.0.3) reports a cancelled browser round trip as an unknown error, so the gateway knows a cancel by having asked for it. A cancel on GitHub's own page, or leaving the browser on Android, still reads as a failure; AGENTS.md says so.
The regression test drives the package's front through a change of focus. With the package's behaviour it fails with the same refused connection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Run against serverpod start, two assumptions of the sync tests turned out wrong, and all eight tests pass with the fixes.
The SQL checks connected to the development database over TCP, but the embedded PostgreSQL that serverpod start runs listens on a Unix socket on Linux and macOS and trusts local connections. queryServer now connects through the socket, by a short link in /tmp because socket paths are limited to about 100 bytes, and keeps TCP with the configured password for other platforms.
The forgery test expected the server to refuse Alice's write into Bob's space and log it. In fact an honest device only syncs the spaces the server announces, so the forged row never leaves Alice's device: no error, no log entry, nothing on the server. The test now claims what it proves, that the row never reaches Bob and is not on the server. The server's own check against a modified client (_assertCanMergeInboundSpace) stays untested; reaching it would mean speaking the sync protocol by hand. AGENTS.md says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The account dialog offers "Delete account", which asks first and then starts anew with an anonymous account. Signing out of an anonymous account deletes it, and so does switching away from one to GitHub, as decided: nobody could ever sign into it again, so nothing should wait on the server. The confirmations say so.
When a person's data opens, PersonFolders wipes every other person's folder: one person uses a device at a time, so their sessions ended here, whether they signed out, switched, were revoked or deleted. Opening waits for the previous person's database to close, so the files can go even on Windows; if closing hangs, opening goes on after ten seconds and the folder goes next time.
Tests: the repository's sign-out and deletion, the session bloc starting anew after a deletion, the dialog (cancelling keeps the account), and PersonFolders on a real temporary folder. AGENTS.md records deletion, pruning and the folders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A studyset's card list gains "Study". The study screen shows the front, then the answer and the four ratings, each labelled with when the card would come back; every rating is recorded with how long answering took, and the queue is worked out anew after each.
Schedules are never stored: scheduleOf replays a card's reviews in time order with FSRS-6 (fsrs v2.0.1, pinned exactly so every device on one app version computes the same). The package's fuzz can only be seeded through an API meant for tests, and due dates don't feed back into a card's state, so the replay runs without fuzz and fuzzedDays spreads the final due date over FSRS's range, picked by a stable FNV-1a fraction of the card's id and review count. studyQueue takes due cards earliest first, then new cards in studyset order, 20 per studyset and local day, and brings a learning card due within 20 minutes early when nothing else is due. Deleting a card deletes its reviews.
Tests: the replay (learning, growing intervals, the same schedule whatever order reviews arrive in, previews matching what a rating yields), the fuzz range, the queue (order, the daily limit counted per local day, learn-ahead), the repository, the session bloc under a controlled clock, the screen, and a sync test for ratings on two devices. AGENTS.md records the review log and scheduling, and how plugin lint rules are ignored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The account dialog opens Flutter's about dialog: the licences (AGPL-3.0-or-later for the app, SSPL-1.0 for the server), the address of the source the instance names, as selectable text, and "View licenses" with every package's notice, which the BSD and MIT packages require in a distributed app. The address comes through a small about feature (gateway, repository, bloc) that says so when the instance can't be reached. Tests cover the bloc both ways and the way from the account dialog; AGENTS.md records the obligation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The screen that says "Couldn't open this device's data." had no button, so the person was stuck until they restarted the app. It now offers "Try again", which opens the person's data anew; a test covers a first attempt that fails and a second that works.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A studyset's screen lists its cards (reorderable; a marker on cards changed on two devices) and opens a card's screen with an editor for each side. Saving appends a revision whose parents are all current heads. mergeRevisions merges several heads per side against their newest common ancestor, as decided: different sides merge automatically, the same side changed differently is a conflict. The card screen then shows every version of that side with "Keep this", and saving is possible once each conflicted side is settled, which resolves the conflict for every device. Nothing is merged silently inside a side.
CardRepository watches a studyset's cards, creates at the end and moves by fractional index (fractional_indexing_dart, checked against the reference algorithm's test vectors), and deletes by blanking every revision first (ADR 0002, rule 2). Deleting a studyset now deletes its cards the same way before the studyset goes. Every person gets a CardRepository whose revisions name them as author. RichContent gains plainText for the list's previews.
Tests: the merge (chains, different and same sides, conflicts, resolving, three heads, missing parents, arrival order), the payload, the repository against a fake gateway, both blocs, and the widgets, including that cancelling a delete keeps the card. The sync tests, which need the running server, cover merging and conflicts across two devices and blank revisions on the server; their device helper is now shared in test/sync/support. AGENTS.md describes the revision pattern, the fractional order, and two tooling traps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Cards are the second feature that stores content as a versioned UTF-8 JSON payload, so the shared part moves to core/content/payload.dart: encoding with a schemaVersion, decoding that refuses garbage and newer schemas, and the blanked redactedPayload (formerly redactedStudysetPayload). The studyset codec now builds on it and behaves as before; its tests pass unchanged apart from the rename.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The account dialog offers "Sign in with GitHub" to anonymous people where the instance has GitHub sign-in. It links the GitHub account to the person, who keeps all their studysets. If the account already belongs to another person (typically their account from another device), the app switches to that person right away when this device holds nothing, and otherwise asks first: switch and leave this device's studysets behind, or stay anonymous.
ServerpodAuthGateway uses Serverpod's GitHubSignInService for the browser part (PKCE), with the system browser and a loopback address on Linux and Windows (http://127.0.0.1:43823/github-callback) rather than an embedded webview, and app.halkyon.learn://github-callback elsewhere; Android gets the callback activity. After linking, the device's copy of the session gets the identified scope. A switch signs in through a client without a session, swaps sessions directly (never through "signed out", which would start another anonymous account), then ends the anonymous session left behind.
GitHubSignInBloc holds the flow; tests cover every branch (GitHub sign-in off, linked, cancelled on GitHub, switching right away, asking with staying anonymous as the refusal, switching once confirmed, a failure), and the widget tests the dialog. StudysetRepository gains all(), a one-off read: Stream.first on its stream never completes under the widget tests' fake clock, because the broadcast subscription's cancel future belongs to the root zone; AGENTS.md records it along with the GitHub pattern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The app and the client package are under AGPL-3.0-or-later, the server under SSPL-1.0, as decided for before the first publish: the server builds on Serverpod's core, which is SSPL, while the app links only BSD-3 packages and the client is generated from our own models. "Or later" keeps the code compatible with future AGPL versions without asking every contributor. Each package carries the official text in its LICENSE (from gnu.org and the SPDX licence data) and a notice in its README; LICENSE.md explains the split and covers the files outside the packages.
CONTRIBUTING.md: contributions come under the licence of the package they change, with no copyright assignment; every commit carries a Signed-off-by certifying the Developer Certificate of Origin, quoted verbatim; tool/check.sh runs before pushes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Two guarantees of step 7 had no test. An open studyset closes when the person changes (here: the server revokes the session, and the app starts anew with another anonymous person), and the next person sees none of it. PersonScope shows the person's app once their data is open, says so when it can't be opened and then leaves no unhandled error behind (checked by removing the handler: the test fails), and closes the person's data when it goes away.
testDependencies takes an optional openPerson for this, and the fake auth gateway can revoke the session.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The first tests that need serverpod start running (ADR 0001, rule 9: the client's and the server's sync engines can't share one process), tagged requires-server: tool/check.sh skips them, flutter test -t requires-server runs them. Every device is its own client and local database with the app's real gateway and repository, and syncs only when a test says so.
They state: two devices of one person end up with the same studysets; a deleted studyset leaves the other devices, and its payload is blank on the server, checked with SQL against the development database because the engine keeps deleted rows; a studyset written into another person's space never reaches them; claiming another person's space shows none of their studysets, since the server sends only the spaces the signed-in person has access to. As written here, the forgery test also expects the server to refuse and log the write, and the SQL checks connect over TCP; the first run against the server corrected both (see the commit "the sync tests pass against the running server").
They live in the app package: the server's Docker build resolves its dependencies with only the server package present, so a dev dependency on the client package would break it. They never initialize a Flutter test binding, which would replace real HTTP with a mock. Without a server they fail at sign-in with "connection refused".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Moves every Serverpod package, and the four overrides for serverpod_offline_sync, from v4.0.2 to v4.0.3 before the first synced table's migration exists, so that migration is created on the version that stays.
v4.0.3 contains fixes only. Checked before moving: serverpod_database gained only backwards-compatible changes, none of which the sync package calls into. Server-side session keys are hashed as before (the cheaper argon2 applies to JWT refresh tokens, which we don't use), so no session ends. The auth module's profile image relation is now deferred, which fixes restoring a Postgres dump; step 7's migration carries that change.
argon2 now runs in serverpod_argon2 (BSD-3), a new dependency of the server only: a prebuilt, statically linked library that needs no glibc, bundled by dart build cli like the SQLite libraries, so the Docker build is unchanged. It doesn't reach the app.
The code generator still fails on Dart v3.13's primary constructors in endpoint files (tried again), so the server keeps the four syntax lints off. The regenerated test tools gain withServerpod's ephemeralDatabase option.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The first synced feature. Every person gets their own local database (<app support>/people/<authUserId>/sync.sqlite): PersonScope opens it inside SessionGate's per-person subtree through Dependencies.openPerson, together with its sync and the person's repositories (PersonDependencies), and closes it when the person changes. SessionGate then also closes pushed screens, which live above the person's subtree; a screen's route() hands its bloc the repository it needs.
SyncStudysetGateway reads and writes the studyset table through the sync session, names the payload column in every update and asks for a sync after every write. StudysetRepository encodes and decodes the payload (studyset_payload.dart: UTF-8 JSON with a schema version; a newer schema or garbage is skipped with a log), re-reads after its own writes and after merges, and blanks the payload before deleting, since the engine keeps deleted rows until the space is deleted (ADR 0002, rule 2). A failed read reaches the list as an error, and the next change reads again. SyncService syncs on triggers: at start, 2 s after the last local write, on resume and every 5 minutes, one sync at a time, and lets a running sync finish before the database closes.
Screens: the list, now the home screen in place of the editor playground, creates studysets through a dialog; a studyset's screen edits its title and description, saves on request and asks before deleting.
Tests: the payload codec, the repository against a fake gateway (including a failed read and rows from a newer app), the sync triggers under fakeAsync, both blocs' sequences, and the screens, including that cancelling the delete keeps the studyset. The session tests now settle until the person's data is open. AGENTS.md describes the per-person graph, synced data and the envelope as it now is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Adopts serverpod_offline_sync v0.0.8 (ADR 0001): its module (nickname offline_sync) and the experimental database: sync in generator.yaml, its server and client packages, and overrides that keep the four Serverpod core packages on v4.0.2, since the package pins v4.0.0 exactly and the spike showed it works on v4.0.2. The app's new dependencies come along because the workspace shares one lock file.
Studyset is the first synced model. Its space owns it, so it has no owner column, and only the envelope is a column: keyId and nonce stay null (no end-to-end encryption; ADR 0002, rule 6), and payload holds the content as versioned UTF-8 JSON, so a new content field changes the payload's schema version instead of this table, whose every migration locks older app versions out of syncing (rule 3). The audience column of the original design is gone: it served key selection, and the engine's spaces decide who reads a row.
The generated server and client code is included. The migration isn't: it is created together with the move to Serverpod v4.0.3 and reviewed first, like the client's local migration (the client's lib/migrations is still empty).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
leancode_lint's bloc_subclasses_naming wants every subclass of a bloc's event and state classes to start with the base class's name (SessionEventStarted, SessionStateActive). The rule only applies when the bloc shares a library with them, which is why it stayed quiet while SessionBloc's events and states lived in files of their own. They now share the bloc's file, so the rule guards them, and all blocs follow one convention.
The auth gateway now imports only Client from the generated client: the generated package defines a <Model>Repository class for every table model, whose names clash with the app's own repositories.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The app's first patterns. Dependencies (lib/src/core/dependencies) is the composition root, built at startup and handed down by DependenciesScope; it replaces the scaffold's global client and restores a stored session before the first frame, so a restart never creates a second anonymous account. AuthGateway with ServerpodAuthGateway is the boundary to the server's authentication, AuthRepository signs in anonymously when nobody is signed in, and SessionBloc (flutter_bloc) keeps a person signed in, also after signing out or a revoked session. SessionGate shows a spinner while signing in, offers to try again when that fails, and builds the app anew for every person. AccountButton explains that progress is saved on this device and asks before signing out of an anonymous account, which can't be restored. AppBlocObserver logs all blocs.
Tests: the repository, the bloc's sequences (a stored session is kept, a failed sign-in is retried, signing out starts anew), and the widgets, including that cancelling the sign-out keeps the account. test/support holds the fake gateway and testDependencies(). AGENTS.md describes the composition root and the test factory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Flutter regenerated the macOS plugin registrant for the two native plugins the editor brings, device_info_plus and file_picker_darwin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
RichContent (lib/src/core/content) keeps the editor's document JSON as a deeply unmodifiable copy plus a formatVersion; it is pure Dart and compared by value (equatable), so models and later the server can use it. fromJson refuses formats newer than the app knows. The name avoids a clash with Flutter's RichText widget.
lib/src/core/editor converts it to and from the editor's Document (a mutable deep copy on the way in, since the editor keeps and changes parts of the JSON it gets) and holds RichContentEditor, which reports every document change as new RichContent and opens empty content with a paragraph to type into, and RichContentView, the one read-only renderer, sized to its content with IntrinsicHeight as the editor's own example does for lists.
A placeholder screen edits a document and shows it again after storing it as JSON and reading it back. It keeps its state with setState because it is temporary; blocs come with the first real feature. AGENTS.md describes RichContent and equatable.
Tests: equality, immutability, the JSON round trip with Japanese text and marks, format checks, the round trip through the editor document including an edit, and the view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The app uses appflowy_editor from our fork of its main branch (01eccc6), moved to file_picker v13 and device_info_plus v13, so that everything in the pub workspace resolves to win32 v6, which Serverpod needs; the unchanged editor needs win32 v5. It is a local path until the fork is hosted, then a git URL pinned to a commit. equatable v3 provides value equality for models and states.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Flutter 3.47 moved Material and Cupertino out of the SDK into material_ui and cupertino_ui, which ship weekly. The SDK imports are deprecated from the November 2026 stable release and removed in 2027, and the strict pre-push check would block every push then. The app's one Material import now comes from material_ui ^1.5.0, so everything built from here on uses the new import.
Packages that still import package:flutter/material.dart (Serverpod's Flutter packages, later the rich-text editor) see the app's theme and localizations only through MaterialUiCompatibilityBridge, set in MaterialApp.builder. The bridge is deprecated on purpose, as a reminder to remove it, so its use carries a documented ignore until those packages have moved.
google_fonts is removed: it was unused, and by default it downloads fonts from Google at runtime, which sends every user's IP address to Google.
AGENTS.md: Material comes from material_ui.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The workspace root's analysis_options.yaml includes leancode_lint 27 and enables its analyzer plugin (rules for blocs, Equatable and Flutter layout); the server and the app include that file, while the generated client keeps its own options. Plugins can only be enabled at the root of a pub workspace and only reach packages whose options include the root file, which was verified before settling on this layout. Most rules are warnings, casts, inference and raw types are strict, and the check treats infos as fatal too. lints and flutter_lints are no longer needed.
tool/check.sh checks formatting, runs dart analyze --fatal-infos and the server tests; .git/hooks/pre-push links to it, so every push has to pass it. AGENTS.md describes the checklist and how to install the hook in a fresh clone.
Fixes for the new rules: package imports, run() returns Future<void>, the server setup as one cascade, a primary constructor for the app widget, a const and a redundant argument. The workspace root keeps its conventional name `_` with a documented ignore.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Deviations from a stock `serverpod create`:
- Dropped Flutter web support, for two reasons:
- Appflowy editor had issues in the past, potentially related to the
Flutter-Web IME bridge. Upstream has also open bugs in relation to
that:
- AppFlowy-IO/appflowy-editor#1029 [Bug][Web] Unwanted space on click
and overall space bugs (open)
- AppFlowy-IO/appflowy-editor#782 [Bug] Editor auto-scrolling when
pressing space too quickly if placed inside a scrollable (open)
- Offline-first is a required feature. serverpod_offline_sync's SQLite
support in browsers, maybe via sqlite3.wasm + OPFS, is not
documented.
- SDK constraints raised to ^3.13.0, instead of ^3.12.2, so enjoy the
newest language features.
- Pinned to Flutter 3.47.4 via FVM.
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>