the framework has no tests, no error boundaries and a handful of known defects that keep resurfacing mid-feature. write them down once, ranked, so they can be picked off in the context of whatever is being built. notable: the dashboards PATCH dispatcher silently drops any key family it has no branch for, and three in use today (tmux, nvim, claude-code, all from Terminal's statePrefix) match nothing — so that state never persists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
185 lines
13 KiB
Markdown
185 lines
13 KiB
Markdown
# 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.ts` → `generateClaudeSettings`), so it may
|
|
be inlinable.
|
|
|
|
- [x] **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
|
|
|
|
- [x] **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.
|
|
- [x] **`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.
|
|
|
|
- [x] **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).
|
|
|
|
- [x] **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.
|
|
- [x] **`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`,
|
|
`/downloadsTV` → `14_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 — all closed the same
|
|
day. Kept here for what the policy work turned up, which is not obvious from the code.
|
|
|
|
- [x] **ACL policy.** `/api/v1/policy` GET/PUT behind `/_officer/policy`, with a plain HuJSON textarea
|
|
that sends the text byte for byte and shows headscale's verdict verbatim (line and column
|
|
included). Officer does not pre-validate: headscale owns the only parser that resolves groups,
|
|
tags and hosts, and a second weaker one would disagree with the thing that actually enforces.
|
|
**The mode cannot be read.** A file-backed policy is still served over GET; only a PUT refuses,
|
|
with "update is disabled for modes other than 'database'". So the first save is what discovers
|
|
writability, and a refusal becomes a persistent read-only banner. Verified live, not inferred.
|
|
|
|
- [x] **A node can be moved between users.** `/api/v1/node/{id}/user`, in the expanded node card next
|
|
to the tag editor — both being the things that decide which policy rules apply to a node.
|
|
|
|
- [x] **User rename** — was already shipped end to end (`/users/:id/rename`, `UsersView`); this entry
|
|
was stale when it was written.
|
|
|
|
- [x] **Polling** — `useHeadscaleNodes` has had `refetchInterval: 20_000` all along; also stale.
|
|
|
|
- [x] **Device invites, platform side** (`COMMS/OFFSCALE_INVITE_ENROLLMENT.md` §5, built 2026-08-05).
|
|
Authorize-new-device form, the one-time link with copy/share/QR, and the invite list with revoke —
|
|
`/_officer/enroll/invites` in `invites.ts`, `InvitesView`. **The records are not Officer's.** They
|
|
proxy to the server's Officer Companion, because the joining phone has to claim without an Officer
|
|
account and this sidecar is loopback-only behind our auth; the spec's own "an invite must work when
|
|
the platform is down" argument says the same. Officer stores no invite and no claim token.
|
|
**Live on all four servers since 2026-08-05.** The 401 that blocked it was a companion-side prefix
|
|
parse (a Headscale key prefix is a fixed 12 chars and may contain `-`; they split on the first one);
|
|
fixed upstream. Verified end to end against pastilhas-eu: create returns the
|
|
`officer-offscale://join#…` link, list shows the record, revoke flips it to `revoked`. The list
|
|
envelope is not in the spec, so `pickInvites` takes the body's array whatever it is keyed under.
|
|
Four gaps were sent back to the spec author: `keys.ts` has no
|
|
sub-day key TTL for the 5-minute claim key, "the sidecar must refuse plaintext" is unenforceable
|
|
behind nginx, `tailnet` is not a headscale concept and has to be recorded on the invite, and
|
|
`/api/v1/enroll/*` collides with headscale's own namespace.
|
|
|
|
## Workspaces & Panels
|
|
|
|
Its own list — **`docs/workspace-panel-todo.md`** — because it is long and actively worked. Full
|
|
analysis behind it: `COMMS/workspace-panel-framework-analysis-2026-08-07.md`.
|
|
|
|
Headlines: the framework has **no panel lifecycle** (a panel can be destroyed but never told, which is
|
|
the real cause of the terminal orphan leak); three state-key families are **silently dropped on every
|
|
write**, orphaning a pty per reload; Running Shells has **404'd since 2026-07-31**; there are **zero
|
|
error boundaries** anywhere in the repo; and `dashboards.id` is a **global** primary key fed by
|
|
`slugify(name)`, so two members naming a dashboard the same thing collide.
|
|
|
|
## 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.
|
|
|
|
- [x] **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.
|