Files
platform/TODO.md
T
pastilhasandClaude Opus 5 2f92f9b15c note the headscale gaps in the todo
Found while reading the app, not while fixing anything. The policy editor is the
only one that costs an ssh session today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 13:48:38 +00:00

10 KiB

TODO

Deferred work. Context: Officer is collapsing from multi-tenant / open-source-ready to a single-user platform. Treat multi-tenant indirection as accidental complexity, not a requirement.

Single-user cleanup

  • Remove dead username plumbing. Mostly resolved by deleting the chat channels — the handlers and send-and-await.ts that threaded toShellUsername(...) through to nothing are gone. What remains: send-claude-code.ts still declares username without using it, and toShellUsername has one real caller left (provision.tsgenerateClaudeSettings), so it may be inlinable.

  • Delete or gut scripts/provision-existing-users.sh. Done — deleted the script (it ran sudo useradd …, the source of the vestigial andrepadez/john-wick/fedra/miguelbenoliel Unix accounts). Also dropped the dead per-user VNC desktop provisioning (provisionVncEnv, the startxfce4 xstartup) from provision.ts — the mirror self-provisions its passwd in vnc-manager.ts — and the Pi .pi/agent/sessions seed.

  • Collapse the rest of the multi-tenant machinery. Candidates, in rough order of payoff: roles (Super Admin/Member), the sandboxed-vs-unsandboxed path split, per-email home dirs under dev-data/{email}/home, per-user server state (dock, user apps, AppRegistry keyed by email), and auth (passkeys-per-origin, JWT signin, unmounted PasskeyGate).

Email

  • Gmail-style search operators (done 2026-07-24, 0e5b91e). parseEmailQuery + searchEmails in sidecar/email/store.ts parse: from:/to:/subject:/body: → FTS5 column filters; has:attachment, is:unread/is:read, label:X, before:/after:YYYY-MM-DD → SQL WHERE on emails; quoted values → exact phrases; free text → prefix-AND full-text; unknown op:val falls back to free text. Structured-filters-only queries skip the FTS join. Validated on the real 18k-mail DB. Search box placeholder hints at operators.

    • OR (done 2026-07-24, 85d38fb). Query splits on top-level uppercase OR into branches; each branch is a self-contained condition (id IN (FTS subquery) + its SQL filters) and branches are OR'd — so OR works across full-text and structured filters. Verified on the 18k DB (from:github OR from:deepgram = 90+7 = 97 exactly).
    • Remaining: parenthesised grouping (nesting (a OR b) c), in:sent/inbox/spam/trash (folder scope), and relative dates (newer_than:7d). Grouping needs a recursive parser.
    • Group search results into threads too. Folder views group by conversation (ee11c94), but /email/search still returns one row per message. Apply the same thread_id collapse to the FTS result set so search matches Gmail's grouped results.
  • Conversation threading (done 2026-07-24, ee11c94). thread_id column on emails: header-based for new mail (id = sha1(Message-Id), so References[0] hashes to the root's id — computeThreadId in sidecar/email/store.ts), subject+counterpart backfill for already-synced mail. /messages collapses to one row per thread (window fn) with count/unread; /thread/:id + /thread/:id/read; reader renders a collapsible stack. Verified on synthetic + 18k real DB.

    • Upgrade old mail to exact threading. One-time full re-fetch to capture References for already-synced mail — replaces the subject+counterpart fallback (which can over-merge recurring same-subject mail from one sender). Heavy/network-bound; opt-in.
    • Store the RFC Message-Id header on ingest so replies can set a real In-Reply-To (currently reply threading leans on subject/participants; id is a local hash, not the header).
  • Compose/reply/send (done 2026-07-24). Gmail SMTP send with contacts autocomplete, rich contenteditable body (inline images at the caret via paste/drag-drop, Bcc), single close button.

  • busy_timeout on the email db (done 2026-07-24, 9527e0b). API + email sidecar share emails.db; wait out a concurrent writer instead of 500-ing with "database is locked".

  • Multiple views in the Email screen (tabs). The screen grows beyond the current mailbox into several switchable views:

    • Sender/domain management view. Left panel: controls to group mail by sender or by domain (room for more grouping axes later). From a group, run bulk actions on that sender/domain: unsubscribe + mark irrelevant, delete all mail from it, block/ignore future incoming, etc. Think "inbox cleanup / triage" — operate on a whole sender at once. - Needs: aggregate query (count/size per sender + per domain), a block/ignore list the sync/IDLE path honors on incoming, an unsubscribe action (List-Unsubscribe header / link), and bulk delete over a sender/domain.
    • Chat view. The existing AI email-assistant flow — likely stays roughly as-is, just lives as one of the tabs (main view).
    • Open question: tabs vs. some other view switcher; which view is default.

Transmission

Phase 1 (parity with _references/transmission-web) shipped 2026-07-30: the officer-transmission sidecar plus /transmission (torrents / stats / settings). Everything below is phase 2 — none of it exists in the reference app, so none of it was in scope for parity.

Probably needed regardless

  • Resizable detail pane. TorrentsView pins it at DETAIL_HEIGHT = '45%', which is a guess. Either a drag handle or a persisted height in useLocalStorageState, alongside the column set.
  • Revisit DEFAULT_VISIBLE_COLUMNS. Eleven of thirty, chosen before ever seeing the table rendered against the real 43-torrent library. Likely wrong in both directions.
  • Files tab on huge torrents. detail/FilesTab.tsx builds the whole tree and renders every node — no virtualisation. Fine at 14 files; unknown at a few thousand. If it stalls, the fix is useVirtualizer over a flattened visible-node list, the same shape as TorrentTable.

Ideas, unprioritised

  • Container→host path mapping. The daemon reports its own namespace (/downloads/complete), which is not a path on this host. A mapping table (/downloads~/hdds/08_TB_01/Torrents, /downloadsTV14_TB_02/Torrents) would make locations clickable through to /files and make "Set location" offer real destinations. Deliberately dropped from phase 1: the reference app has no such feature, so it would have been config nothing read.
  • Push instead of poll. Transmission RPC has no push channel, so useTransmissionData polls every 5s. The sidecar could poll once and fan out over a WebSocket, which is both cheaper and what every other live surface in the platform already does.
  • Cross-surface links. Soulseek and download-media both land files in the same library; Transmission is the third door to it. Worth a think about whether they should know about each other at all.

Headscale

Gaps against headscale's own API, found while reading the app on 2026-08-05. None is urgent; the first is the only one you would otherwise SSH in to do.

  • ACL policy is not wired at all. /api/v1/policy (GET/PUT) has no sidecar route and no screen. Editing the policy means SSHing to the host, which is the one thing this app exists to avoid. Wants a text editor with the server's own validation error surfaced on save, not a form — the policy is HuJSON and headscale is the authority on whether it parses.

  • A node cannot be moved between users. /api/v1/node/{id}/user is missing, so a node can be renamed, tagged, route-approved and expired, but not re-owned.

  • Users are create/delete/list only. No rename (/api/v1/user/{id}/rename/{newName}).

  • Nothing refreshes. No query in useHeadscaleData.ts polls, so a node going offline (or coming back) only appears on a manual reload. A modest refetchInterval on the nodes list is probably the whole fix.

Known bugs

  • bootstrap.ts runs npm install -g for Pi on every boot. findPiPackageDir checks stale paths and an outdated package name, so installPi always fires and floods the logs with EEXIST noise. Should detect @earendil-works/pi-coding-agent at the real npm prefix.

  • VNC mirror can orphan/duplicate x11vnc across sidecar restarts. FIXED 2026-08-01 in the Xvnc rewrite. The diagnosis was right and survived the move off mirroring: the running desktop is tracked in module-level state, so a sidecar restart forgot it while the server kept running orphaned, and waitForPort treated ANY listener on 5900 as success — so the next start reported success while the browser talked to the stale process. Now reclaimPort frees 5900 (TERM, then KILL after 2s) before spawning, and waitForPort also fails if the process we spawned has exited, so a listener that is not ours can no longer be mistaken for a healthy start.

Infra (alpha)

  • Delete /etc/systemd/system/officer-vnc.service. Hand-installed unit (not in this repo) that ran vncserver :1 -geometry 1920x1080 -localhost yes -fg with Restart=on-failure, enabled at boot — it kept a whole parallel XFCE session alive on :1 (283 MB, 166 tasks) independently of the platform, and silently respawned it whenever the display was killed. Obsolete now that the Desktop panel mirrors :0 via x11vnc. Disabled 2026-07-15 (systemctl disable --now), but the unit file is still on disk. Nothing in the codebase recreates it and nothing documents it, so delete the file rather than leave a mystery service one systemctl enable away from returning.

  • ufw blocks port 9010 on the LAN. Default deny incoming; only 22/tcp, 80/tcp, 443/tcp, and everything on tailscale0 are allowed — so http://192.168.47.196:9010 is unreachable from the LAN while localhost and Tailscale work. Fix: sudo ufw allow from 192.168.47.0/24 to any port 9010 proto tcp

Backburner

  • Music tagging feature (/music, Mp3tag-inspired). v1 was scoped as a two-panel workspace — file browser left, table right, single "Load" context-menu item flattening audio files into the table. Discarded before completion; would need GET /file-browser/flatten-audio, a useFilesAPI.flattenAudio method, and the Music app panel rebuilt from scratch.