Skip to content

Theseus ops inbox (safecall-ops) ​

Approved: 17 September 2026 (#153)

Amended: 11 October 2026: the inbox is a tool on the start screen and lives while Theseus is unlocked. The connection heals itself, and a new event shows as a banner on whatever screen is open. See Lifetime and Banner.

Wantok-unlocked Theseus shows a foreground inbox for the vendor ops topic. This is not a Keryx staff category, not a site QR, and not a field token.

Layer 1 (Wantok unlock) stays in front. The unlock screen has no inbox. Pairing (site QR) is optional; the inbox works without it.

The inbox is the Ops inbox tool on the start screen. Its card shows the stream state, the events not seen yet (3 new) or else the number of events, and the newest title.

Purpose and non-goals ​

Purpose

  • Subscribe to {binary_name}-ops (default safecall-ops) while Theseus is unlocked and in the foreground.
  • Show ntfy events as a list + detail. Parse SafeCall alert data v1 when present; keep legacy bodies readable.
  • Name where the production subscribe credential lives.

Non-goals

  • FCM / APNs / killed-state delivery / public-store GTM.
  • Staff topics (TST000-sos, Keryx QR categories).
  • Ack / assign / resolve. Archive / delete workflow.
  • Changing ntfy_publish_ops_health publishers.
  • A shared Dart package with Keryx. A customer-box wantok role.

Approved decisions ​

ChoiceValue
Topic{binary_name}-ops, default safecall-ops. Not a Keryx category; omitted from GET /features and Keryx QR mint
HostCompany notify URL (server_ntfy_ops_server_url). Not the customer site ntfy URL from a Keryx QR
AuthAuthorization: Bearer tk_… subscribe-only. Never th_, never server_mobile_api_key, never a Keryx QR subscribe_auth
Release--dart-define=THESEUS_OPS_SERVER_URL=… and --dart-define=THESEUS_OPS_SUBSCRIBE_AUTH=…. Optional --dart-define=THESEUS_OPS_TOPIC=safecall-ops (defaults to that name). Empty URL or auth fails closed (no anonymous subscribe)
Debug / testInjectable OpsConfig. No production host and no real tk_ in git. Unset defines show Ops not configured
TransportForeground HTTP NDJSON GET {url}/{topic}/json?since=all. Opened at unlock, closed while the app is in the background, cancelled on lock / app close. Reconnects by itself. No package:ntfy
ParserThin copy of Keryx SafeCall alert data v1 parse. List hides the transport block
PersistenceIn memory while Theseus is unlocked, at most 500 events (oldest dropped). Gone on lock / app close. ntfy since=all fills the recent cache

Owner: Thomas Minitsios. Store: the approved Keryx / sourcectl secret store. Do not invent a second store.

Publish path stays on the SafeCall box (server_ntfy_ops_* / ntfy_publish_ops_health). Ops titles are already prefixed with {server_id}:.

Lifetime ​

One app-level TheseusOpsInboxController (theseus/lib/ops/ops_inbox_controller.dart) owns the subscription and the in-memory list. The inbox screen and the start screen card only render from it.

MomentWhat happens
Unlock (or cold start already unlocked)The app creates the controller. It subscribes once. No config: stays Not configured, no request (fail closed; a config with an empty URL, topic or token counts as none)
Start screen, or inside another toolEvents keep arriving. The Ops inbox card shows Connecting… / Live / Reconnecting…, the events not seen yet or the number of events, and the newest title
Inbox screen opened / closedNothing starts or stops. The list is the controller's list. While the inbox is on screen every event counts as read
Connection drops, or the connect failsStatus becomes Reconnecting…; events so far are kept. Theseus connects again by itself after 2 s, then 5, 15, 30 and every 60 s for as long as it is unlocked and in the foreground. The backoff starts over once it is live. Retry now on the inbox screen connects at once
Connect gets no answerGiven up on after 15 s; the retry loop takes over
Connection goes silentntfy sends a keepalive line about every 45 s. No line at all for 100 s means the connection is dead (a half-open socket after a Wi-Fi roam): it is dropped and the retry loop takes over
Subscribe refused (HTTP 401 / 403)Refused: the notify host does not accept the subscribe token of this build. Not retried automatically; Retry on the inbox screen still works
App goes to the background (paused)The connection is closed and nothing retries: the stream is foreground only
Back from the backgroundTheseus subscribes again at once. A system dialog (inactive only) does not reconnect
Reconnectntfy replays its recent cache (since=all); a replayed event replaces its copy, no duplicates. What the inbox did not have yet was missed during the outage: it counts as new
Lock Theseus, or app closeThe controller is disposed: stream cancelled, list dropped, no retry pending

Still foreground only: no FCM / APNs, nothing arrives while the app is suspended or closed. Still in memory only: nothing is written to disk.

What counts as new ​

  • The cache ntfy replays when Theseus first connects after an unlock is history: it is listed, not counted as new. The card shows the plain number of events in a quiet badge.
  • Everything that arrives later, and everything a reconnect brings that the inbox did not have, is new: the card badge says 3 new.
  • Opening the inbox reads everything.
  • ntfy does not mark where its replay ends, so the first 5 s of a connection count as replay. An event published in exactly those 5 s after a first connect is treated as history.

A notification has to reach the user wherever they are in the app, also inside a tool. TheseusOpsToastHost (theseus/lib/widgets/ops_toast.dart) sits above the navigator and shows what the controller announces:

AnnouncementBanner
A new eventIts title, Ops inbox · tap to open. An alert that is not resolved is drawn in the error colours with a strong haptic; anything else neutral with a light one
Events missed during an outageOne banner when the reconnect has replayed them: the newest title and 3 new ops events · tap to open
The stream has been down for a while (three failed reconnects, about 20 s)Ops inbox disconnected, once per outage
The stream is back after thatOps inbox live again
  • The banner sits under the app bar, never over the bottom of the screen: the bottom belongs to a tool's controls (the recorder's Moving button), the app bar to back and the tool's actions.
  • It leaves by itself after 6 s, on its close button, or on a tap, which opens the inbox above the current screen.
  • Events that arrive while a banner is up are counted on it.
  • Nothing is shown while the inbox itself is open, and nothing on the unlock screen.

Dev vs release ​

Debug / widget tests inject TheseusOpsConfig (fake host + fake Bearer tk_theseus_ops_dev). They never call the production notify host.

Release builds (kReleaseMode) require both dart-defines. Empty or missing URL or auth: the inbox stays on Ops not configured and opens no HTTP request.

Internal/release flutter build must pass the URL and subscribe header from the NAS notes + Apple Passwords. Do not commit tk_….

Operator row: OA-AT-ops (done 17 September 2026: user safecall-ops-subscriber, token label safecall-ops; token in Apple Passwords; notes on NAS secrets/theseus/ops/token_label_notes.txt).

How to mint the subscribe token ​

On hetest2, ntfy needs sudo (/etc/ntfy/server.yml is not world-readable). Create a dedicated user with read-only on safecall-ops only. Do not reuse safecall-token-manager (* read-only). Do not put that topic on a Keryx QR.

bash
sudo ntfy user add safecall-ops-subscriber
sudo ntfy access safecall-ops-subscriber "safecall-ops" read-only
sudo ntfy token add --label="safecall-ops" safecall-ops-subscriber

Copy host / username / token-label notes (not the token) to /srv/sourcectl/secrets/theseus/ops/ (file token_label_notes.txt, 17 September 2026). Put the tk_… value in Apple Passwords (item NTFY; username safecall-ops-subscriber; Notes field. The password field holds the ntfy login password).

Threat notes ​

IssueTreatment
Stolen APK / backup contains dart-define authToken is a subscribe secret. Rotate on the notify host; rebuild Theseus.
Anonymous subscribe to production notifyFail closed: no request without auth. Dual-run anonymous read is not a Theseus fallback.
Site QR / field token used as ntfy authRejected by construction. Ops config is Wantok-global dart-define, not T3 prefs.
Staff categories in the inboxStream requests only the ops topic.
Lost unlocked phoneForeground inbox until Theseus is locked or app data is cleared. No FCM in this window.

Current gap ​

T5 adds field APIs. Do not subscribe to Keryx staff topics from Theseus.