feat(app,learn,todo): the operator's "This instance" card, screen and badge (ADR 0021, step 1)

Whoever operates the instance approves, refuses and suspends accounts
and names other operators in the app (D21 to D24, D34):

- halkyon_app: InstanceOperatorRepository on
  ServerpodInstanceOperatorGateway (the module's instanceOperator
  endpoint; refusals as OperatorFailed: notFound, isOperator,
  lastOperator, notAllowed for 401 and 403, failed), in AppDependencies.
  OperatingCubit counts the accounts that wait: asked as the person's
  app opens, after every change, and every 5 minutes only while they
  operate the instance, so nobody else's app keeps asking.
  InstanceOperatorCard (or its tiles) and WaitingAccountsBadge show
  nothing at all for anyone else. InstanceOperatorScreen, laid out as
  Settings: the accounts that wait, each showing its four words to read
  out before Approve (D33), Refuse for new ones after asking; who
  operates, Take back after asking, the last always kept; Name an
  operator by handle; Find an account by the start of its handle, with
  Suspend after asking and Name as operator. English and German texts
  ("Admin" as the role word in German).
- Learn: the card right after the Sync card in Settings, which the
  cogs' count leads to as their dot leads to Sync; the count on the
  Settings cogs of Home and Profile.
- Todo: the tiles in the account sheet and the count on its button.
  Todo's server is a member, whose `operating()` is always null, so
  nothing shows there until it holds the register.

Tests: the gateway's mapping, the repository, both cubits with their
denials, the card, the badge and the screen; Learn's Settings shows the
card and the count to an operator and neither to anyone else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-25 15:55:28 +01:00
commit 31b9c17850
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -40,13 +40,33 @@ Written as the apps are: Dart 3.13's primary constructors, Material from
a switch, the previous ones closed first; `SessionGate` shows the app of
whoever is signed in and asks how to start while nobody is
(`StartScreen`: SimpleLogin, a password, another instance; an instance out of reach is said so, with Try again,
and one offering no way to create an account too), at a first start as
after signing out; an anonymous account's session, left from before
and one offering no way to create an account too; where new accounts wait
for the operator's approval, or none are made, it says so first), at a
first start as after signing out; while an account signed in or up to
here waits for the operator (Halkyon Learn's ADR 0021, D31), `WaitScreen`
shows its handle and the four words the operator reads out for the person
to match, asks every 30 seconds and as the app comes back, deletes a new
account after asking, and once approved leads to signing in once more
(the wait kept in the keyring per instance, `SecureAccountWaitStore`, so a
restart shows it again); an anonymous account's session, left from before
there were none, is deleted as the app starts (ADR 0020, D25), falling
back to signing out on the device; `PersonScope` opens each person's own
dependencies and closes them when the person goes. `resolveAppLocale`
picks the app's language as every Halkyon app does: the first preferred
language it speaks, never by country alone, British English otherwise.
- **The instance's operators** (`lib/src/operator/`, Halkyon Learn's ADR
0021, D21 to D24 and D34): `InstanceOperatorRepository` on
`ServerpodInstanceOperatorGateway` (the module's `instanceOperator`
endpoint, refusals as `OperatorFailed`), `OperatingCubit` (how many
accounts wait, null for anyone who doesn't operate the instance; asked as
the person's app opens, after each change, and every 5 minutes only while
they operate it), `InstanceOperatorCard` or `InstanceOperatorTiles` for an
app's settings and `WaitingAccountsBadge` for its Settings cog, both
nothing at all for anyone else, and `InstanceOperatorScreen`: the accounts
that wait, approved once their person confirmed the words read out, or
refused; who operates, named by handle and taken back (never the last);
accounts found by the start of their handle, suspended or named. Only the
register's service answers: on a member, nothing shows.
- **About** (`showAppAbout`): the app's name (its frame's), its version,
icon and legalese where it gives them, where the source of what the
instance runs is (`InstanceSourceCode`, from `AppDependencies.sourceCodeUrl`;
@ -174,8 +194,9 @@ Written as the apps are: Dart 3.13's primary constructors, Material from
- **A client of `halkyon_base` alone** (`BaseClient`): to ask an instance
about itself, and to sign in as another person than the app's client holds.
- **Testing** (`package:halkyon_app/testing.dart`): fakes of every boundary
(`FakeAuthGateway`, `FakePasswordGateway`, `FakeAccountCrypto`,
`FakeInstanceChecker`, the stores in memory), a frame
(`FakeAuthGateway`, with waits, `fakeWaitOf`; `FakePasswordGateway`,
`FakeAccountCrypto`, `FakeInstanceChecker`, `FakeInstanceOperatorGateway`,
the stores in memory), a frame
(`testFrame`), `testApp`, and `TestAppDependencies`.
## Using it in an app
@ -202,14 +223,19 @@ Written as the apps are: Dart 3.13's primary constructors, Material from
(copy Learn's or Todo's `ScreenGuard.kt` and call `ScreenGuard.attach` in
`configureFlutterEngine`), or secrets show in screenshots and recents.
6. **The dependencies**: the app's composition root implements
`AppDependencies<P>` (`serverName`, `auth`, `signedOut`, `passwords`,
`fileSaver`, `sourceCodeUrl`, `openPerson`, `dispose`), its person's
dependencies `Disposable`. Its `passwords` is a `PasswordRepository` on
`AppDependencies<P>` (`serverName`, `auth`, `passwords`, `simpleLogin`,
`instanceOperators`, `fileSaver`, `sourceCodeUrl`, `openPerson`,
`dispose`), its person's dependencies `Disposable`. Its `passwords` is a `PasswordRepository` on
`ServerpodPasswordGateway(client, base: client.modules.base)`,
`SodiumAccountCrypto()`, `SecureAccountKeyStore(storage.keysOf(instance))`,
the auth gateway and the app's `SignInAddresses`. Its `auth` is an
`AuthRepository` on `ServerpodAuthGateway(client, base:
client.modules.base)`.
client.modules.base, waits:
SecureAccountWaitStore(storage.waitOf(instance)))`, whose `initialize()`
it awaits before anything asks who is signed in. Its `instanceOperators`
is an `InstanceOperatorRepository` on
`ServerpodInstanceOperatorGateway(client.modules.base)`; the person's app
provides an `OperatingCubit` on it, for the card and the badge.
7. **The tree**:
```dart