putSecret/getSecret, `bun run secret:set`, and notify taking its credentials from the store with env as the fallback. this was described two messages ago as a later thing; it is about fifteen lines and it was blocking the honest answer to "where does the .p8 go", so it is done. getKey was the wrong door for anything supplied from outside — it MINTS a random 32-byte value when the purpose is absent, which for a foreign credential means silently authenticating to apple with a fake. getSecret returns null instead, which is what lets the env fallback work. putSecret retires the previous row rather than deleting it: a mistyped paste over a working key should be recoverable, and retiredKeys already exists for that shape. one transaction, because the partial unique index means the retire has to land before the insert. the CLI reads stdin and has deliberately no --value flag: a secret in argv is in shell history, in ps output, and in any audit log that records command lines. offering the flag would make the convenient path the leaky one. it prints a length and the last four characters, never the value. also `bun run notify:test`, which fires officer/core:test at a registered device. the sidecar's port is ephemeral, so it now writes its address to data/notify-server.url on boot and removes it on clean shutdown; the CLI treats an unreachable address as "not running" rather than trusting the file, because a crash leaves one behind. the script says what actually happened, because the day-waster here is that a send with nothing configured returns success and delivers nothing — it refuses up front when channels is empty, and calls out sent:0/failed:0 as "no device registered" rather than letting it read as success. verified end to end with a p-256 key generated locally and then deleted from the store: zero APNS vars in .env, sidecar reported channels: apns, and the send reached the channel. store is back to jwt and offscale only.
714 lines
51 KiB
Markdown
714 lines
51 KiB
Markdown
# TODO
|
|
|
|
Deferred work.
|
|
|
|
**Context, corrected 2026-08-07.** This file used to open by saying Officer was "collapsing from
|
|
multi-tenant / open-source-ready to a **single-user platform**", and told you to treat multi-tenant
|
|
indirection as accidental complexity. **That direction was reversed.** The permission permission model
|
|
shipped on 2026-08-07 to serve a real goal — deploy to the company server, onboard people, give each
|
|
one their own Gitea account through the platform. Per-user scoping is now a requirement, and the items
|
|
below that proposed deleting it have been removed rather than left to mislead the next reader.
|
|
|
|
What did NOT reverse: `execution` permissions (terminal, chat, tasks, files, desktop, browser) run as
|
|
the owner's OS user and can never be granted. Indirection there really is accidental complexity.
|
|
|
|
## Multi-user
|
|
|
|
- [ ] **`deprovisionOsAccount` is written but has never run against a real account.** Landed 2026-08-12 in
|
|
`os-user-deprovision.ts` and wired into `deleteUserHandler`, which now refuses to delete the row when
|
|
the Linux teardown fails — so a failure is retryable instead of forgotten. Only the pure guards
|
|
(`guardDeletable`, `guardMemberTree`, `parseSubUidEntry`) have tests; the reap loop, the `chown -R`
|
|
sever and `userdel` have been exercised by nobody. `docs/deprovision-os-account.md` → "What is still
|
|
unproven" has the five-step validation, and it has to happen on the production host with a throwaway
|
|
account that has **a shell left open** and **a container writing as a non-root user** — those are the
|
|
two cases the quiet path passes vacuously.
|
|
|
|
- [ ] **The terminal replays terminal QUERIES, which get typed into the shell.** `sidecar/pty/sessions.mjs`
|
|
replays the whole scrollback on attach; query sequences in the buffer get re-asked, xterm.js answers,
|
|
and the answers arrive as keystrokes. Visible to a member daily. Fix is to strip query sequences in
|
|
`appendBuffer`, so a replay reproduces output and never re-issues requests.
|
|
|
|
- [ ] **The web terminal renders a long URL as unreadable fragments.** Claude Code's first-run login prints
|
|
a ~400-character OAuth URL; the web terminal shows scattered characters with large gaps, nothing
|
|
selectable. Half worked around by `2a8f004` (OSC 52, so "press c to copy" reaches the clipboard) — the
|
|
rendering itself is undiagnosed. This is every new member's first five minutes.
|
|
`docs/open-threads-after-per-user-claude.md` §1 has what is known and where to start.
|
|
|
|
- [ ] **Agent sessions are not durable, and it is one property behind three symptoms.** A sidecar restart
|
|
loses session identity, which is why `endTurnIfAgentIsGone` must skip sessions with no recorded
|
|
`userId`, why a stuck "generating" spinner survives until a reconnect, and why any crash in that process
|
|
is destructive rather than merely inconvenient. Fixing the three separately would miss that they are one
|
|
missing property. `docs/open-threads-after-per-user-claude.md` §2.
|
|
|
|
- [ ] **`ProcessTransport is not ready for writing` — survivable since `8c4f150`, still unexplained.** A
|
|
floating rejection inside the SDK's own input pump, with no frames from our code, so no `await` of ours
|
|
can catch it. It crashed `officer-agent` four times on 2026-08-11, once truncating a turn mid-sentence;
|
|
the `unhandledRejection` backstop has caught it once since. Best hypothesis is the `claude` CLI exiting
|
|
while `streamInput` is still pumping. It needs looking at after the next occurrence, not catching in the
|
|
act — markers to grep in `docs/open-threads-after-per-user-claude.md` §3.
|
|
|
|
- [x] **No way to create a second account.** Fixed 2026-08-11 on `sidecar-app-store`: `POST /api/users`
|
|
(`api/users/create-user.ts`, owner-gated) plus an Add-account form in
|
|
Settings → User management. Created accounts are `status: 'Active'` — the column defaults to
|
|
`'Unverified'` and `signin.ts` refuses anything else with a bare UNAUTHORIZED, which is the trap
|
|
the hand-INSERT route fell into. The owner sets the password and reads it out; `passwordChangedAt`
|
|
stays null. Directories come from the shared `provisionUserDirs`/`USER_DIRS` in `data-path.ts`,
|
|
which `scripts/provision-user-dirs.ts` now imports rather than restating.
|
|
|
|
- [ ] **Still no invite flow, and no password reset for a member.** The owner types the password and
|
|
tells the person, which means the owner knows it and the member cannot change it back if they
|
|
forget theirs — recovery today is delete-and-recreate. An invite (token, expiry, member sets
|
|
their own) needs a mail path. This is the next piece, not a nice-to-have.
|
|
|
|
- [x] **A second Super Admin was storable, and made the owner nondeterministic.** Fixed 2026-08-11.
|
|
`ck_users_owner_is_super_admin` pins user 1's role but a row-level CHECK cannot see other rows, and
|
|
`updateUserRoleHandler` happily promoted anyone — while `getOwnerUser()` was
|
|
`WHERE role='Super Admin' LIMIT 1` with no ORDER BY. Two holders would have made "who owns this
|
|
server" a question the query plan answered, and that answer feeds the agent sidecar's identity,
|
|
vault access and origin scoping. Both write paths now refuse the role, the list endpoint offers
|
|
`assignableRoles` without it, and `getOwnerUser()` orders by id.
|
|
|
|
- [ ] **`dashboards.id` is a global primary key, and ids are `slugify(name)`.** Two accounts cannot
|
|
both have a dashboard named "Home". Reachable today: six accounts exist. The recommendation on
|
|
the table is a composite PK `(user_id, id)` — it matches the `uq_dashboards_user_id` index
|
|
already there and keeps every stored `ws-layout-<id>` address valid, which uuid ids would not.
|
|
Note the drizzle composite-PK re-diff quirk in `databases/CLAUDE.md`. Full analysis in
|
|
`docs/workspace-panel-todo.md` §3.
|
|
|
|
- [ ] **`permissions/authorize.ts` has no automated tests.** `registry.test.ts` covers the pure
|
|
registry functions and the totality check; the resolver that does the owner bypass, the grant
|
|
lookup, the role cache and the fail-closed catches is exercised only by hand. It is the file
|
|
standing between a Member and a shell.
|
|
|
|
- [ ] **`assertPermissionTotality` checks the wrong list, and `registry.test.ts` has been red since
|
|
2026-08-13.** It is fed `Object.keys(handlers)` from `server.tsx`, but Bun serves the _route table_.
|
|
Those diverged when the cliamp/desktop/vault plugins were switched off: `/api/cliamp/ws` and
|
|
`/api/cliamp/audio/ws` are still live routes with their handlers and registry claims commented out.
|
|
Not exploitable — `isWsProviderAllowed` finds no permission and 403s a member; the owner upgrades onto
|
|
a dead socket. But the boot check that exists to stop exactly this cannot see it. Two fixes: point
|
|
totality at the route table, and either delete the dead routes or restore their claims. The 8 failing
|
|
tests in `registry.test.ts` are the same drift — `REAL_WS` still lists all nine providers as served,
|
|
which is why nobody noticed. Found 2026-08-14.
|
|
|
|
- [ ] **No empty state for a denied screen.** A member who reaches a route their role lacks gets a
|
|
broken panel or an endless spinner rather than a clean refusal.
|
|
|
|
- [ ] **`getOwnerHomeDir(email)` ignores its argument** whenever `HOME_DIR` is set, which it is here —
|
|
every caller resolves to the owner's real login home. Safe only because all seven callers sit
|
|
behind `execution` permissions. If per-user home confinement is ever attempted, this is the
|
|
function to start from.
|
|
|
|
- [ ] **`pty`, `vault` and `opencode` receive no identity at all.** Every other sidecar validates
|
|
`X-Officer-User`. The pty sidecar keys purely on a `sessionId` from the query string and its
|
|
`/_officer/sessions` endpoints list and kill _every_ session on the box; vault and opencode take
|
|
no user argument. All three are covered today only because `terminal`, `vault` and the agent are
|
|
owner-only permissions — that is a correct outcome resting on the wrong layer, and it is the
|
|
thing to fix first if any of them is ever granted.
|
|
|
|
- [ ] **Radicale is configured `type = owner_only`** (`sidecar/caldav/radicale.ts:54`) while the caldav
|
|
sidecar itself is fully per-user and confines every JSON read to `/dav/<userId>/`. The platform
|
|
side is ready for members; the CalDAV server underneath is not.
|
|
|
|
- [ ] **The music library is one global index.** `sidecar/music/indexer.ts` reads `HOME_DIR` and serves
|
|
every account from it. Favourites, playlists and now-playing _are_ per-user. Deliberate for now
|
|
(one household, one library) but worth stating rather than discovering.
|
|
|
|
- [ ] **`markInterruptedJobs()` and `getOldestPendingJob()` are platform-wide.** The pipeline queue is a
|
|
single global lane; ownership is enforced one layer up, in `pipeline-jobs-routes.ts`, by an
|
|
explicit `job.userId !== user.id → 404` on every by-id route. Correct today, but the queue itself
|
|
has no notion of whose work it is running.
|
|
|
|
- [x] **Cross-user writes in the notify sidecar** (fixed 2026-08-07, this session).
|
|
`DELETE /_officer/devices/:token` deleted by token with no user predicate, so any account with the
|
|
`notify` permission could deregister another's device; and `POST /_officer/notify` let a request
|
|
body's `userId` override the proxy-injected `X-Officer-User`, so the same account could push to
|
|
another's devices. `deletePushDevice` now takes an optional `userId` (the route passes it, the
|
|
APNs/FCM dead-token paths deliberately do not) and the header now wins over the body.
|
|
|
|
- [ ] **Remove dead `username` plumbing.** `send-claude-code.ts` declares `username` in two types
|
|
without using it. (`toShellUsername` is NOT dead — `server.tsx:190` and
|
|
`pipeline-job-manager.ts:267` both call it. The `provision.ts` caller this item used to name no
|
|
longer exists.)
|
|
|
|
## Email
|
|
|
|
Email became a plugin on 2026-08-18 (`plugins/email/`, `gitea.officer.dev/plugins/email`). Its work queue
|
|
went with it — the open items pointed at `sidecar/email/store.ts`, a path in another repository, which is
|
|
exactly the way a leftover list sends someone to edit files that are not there.
|
|
|
|
Anything still owed to the mail client belongs in that repository's own TODO.
|
|
|
|
## 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.
|
|
|
|
- [ ] **Renaming a panel key is a silent data migration, and the repair is destructive.** A saved layout
|
|
holds `appType` strings. `normalizeLayout` replaces any appType not in the allow-list with the
|
|
caller's `fallback`, and `WorkspaceView.tsx:97` writes that back — so a renamed key turns every
|
|
saved layout into N copies of whichever panel is `allowed[0]`, permanently, on one page load.
|
|
Renaming `headscale-*` → `offscale-*` did exactly this: `/offscale` rendered three server pickers,
|
|
`ServersView` never mounted, and it survived every restart because the code was correct and the
|
|
data was not (2026-08-17, `ed3806f9`).
|
|
|
|
Plugin screens are now cleared at install, which covers plugins and nothing else. The general fix
|
|
is a rule about wholesale mismatch: **a layout in which NO panel has a recognised appType is
|
|
residue from older code — discard it and use the screen's default**, rather than collapsing it.
|
|
`fallback: allowed[0]` is arbitrary (it is whatever `panels.ts` happens to list first) and should
|
|
probably not be a per-panel substitution at all.
|
|
|
|
## Code editor
|
|
|
|
- [ ] **The owner wants to go deeper here — ideas pending (noted 2026-08-15).** Raised right after the file
|
|
browser gained its first links into `/code-editor`. Do this after the file-browser test pass.
|
|
|
|
What is worth knowing before starting, all established on 2026-08-15:
|
|
|
|
**It has been effectively invisible for six months.** The app landed 2026-02-21 (`9e9acd96`), the
|
|
screen 2026-02-17, and `1fa3a659` on 2026-07-25 is titled "**restore** the code editor screen" — so
|
|
it was dropped and brought back at least once. It IS in `CORE_DOCK_ITEMS` as "Editor", but
|
|
`DEFAULT_DOCK_PATHS` is `['/', '/files', '/terminal', '/dashboards', '/chat']` and the owner's own
|
|
`dock_configs` row does not list it either. Until today the only way in was typing the URL.
|
|
|
|
**So it has no users, and that is the risk.** A surface nobody reached for six months is where things
|
|
rot quietly. Nothing about the editor itself has been verified — whether saving works, how the file
|
|
tree behaves, what closing the last tab does, what an unsaved-changes navigation does. Only the
|
|
plumbing was checked: `?file=` is read (`EDITOR_FILE_PARAM`), `/code-editor` renders with `urlState`,
|
|
and `CodeEditor.tsx:60-76` fetches a path that arrives in the URL rather than only matching already
|
|
open tabs.
|
|
|
|
**One bug of exactly this kind was already found and fixed** in the FileViewer's editor on the same
|
|
day: the Edit toggle was gated on `content` rather than `content !== null`, so an empty file — the
|
|
one "New file" produces — could not be edited at all. Assume siblings.
|
|
|
|
**Open question, deliberately not decided:** `Open in editor` in the file browser's row menu still
|
|
navigates to `/code-editor`, which is the last action that leaves the browser. Editing now happens
|
|
in place via double-click → the viewer's pane. Either `/code-editor` earns its keep as a genuinely
|
|
different tool (tree, tabs, multi-file) or that menu item should go.
|
|
|
|
## Files app — routing
|
|
|
|
**The URL model is done (2026-08-15).** The folder is the pathname —
|
|
`/files/archive/Tests/platform/docs` — and `?view=` is a NAME within it, not a second copy of the path.
|
|
Deep links and refresh work, which they never had. Landed across `559560de`, `98ba604a`, `7e31564c`,
|
|
`0dc88b51`, `7359af2f`, with `files-route.ts` + 23 tests as the one owner of encoding and of which params
|
|
belong to the overlay.
|
|
|
|
Four bugs came out of that work and are fixed: closing a pane sent you home; `?view=` was stripped on
|
|
every page load by a mount effect that had made deep links impossible since 2026-02-23; navigating kept a
|
|
pane belonging to the folder you left; and the breadcrumb opted out of that last rule because it is a
|
|
`<Link>` and never called the function enforcing it.
|
|
|
|
- [ ] **Rows are still not links.** This is the half that did not get done, and it is the original
|
|
finding. Both list and grid render `<div onDoubleClick>` (`FileItem.tsx:701,748`), and search
|
|
results render `<div onClick>` (`FileViewContainer.tsx:187`) — the "opaque click" that
|
|
`docs/navigation-audit.md` names. No cmd-click into a new tab, no middle-click, not
|
|
link-focusable, and the target lives in a closure rather than the DOM.
|
|
|
|
It is now much easier than it was: `folderHref(path)` gives a folder's URL and `hrefForPath` gives
|
|
a full `To` with the overlay already stripped, so a row becomes
|
|
`<Link to={hrefForPath(childPath)}>` and a file row becomes a link that sets `?view=<name>`. The
|
|
breadcrumb has done exactly this since before today and is the model.
|
|
|
|
Watch for: a row is also a click target for SELECTION (single click selects, double opens,
|
|
shift/cmd extend). An anchor changes what those gestures mean by default, so the selection
|
|
handlers have to keep working and `preventDefault` where they win.
|
|
|
|
- [ ] **The acts that clear the overlay have no owner, only the list does.** `VIEWER_PARAMS` and
|
|
`withoutViewerParams` live in `files-route.ts` and are shared. But every site still decides FOR
|
|
ITSELF whether to call them, which is precisely how the breadcrumb shipped wrong an hour after the
|
|
rule was written. Three bugs in a row came from this. A single `navigateToFolder()` that every
|
|
caller must go through — rather than a helper they may remember — is the fix if a fourth appears.
|
|
|
|
- [ ] **A panel's folder is still not addressable.** `urlPath` is true only on the `/files` screen;
|
|
panels keep the path in local state, so they always open at their base and cannot be linked or
|
|
restored. Deliberate as written — a dashboard can hold two browsers and one URL cannot serve both —
|
|
but worth re-deciding rather than inheriting.
|
|
|
|
## Release-binary dependencies (2026-08-19)
|
|
|
|
Built: a plugin can declare `release: { url, identify }` for a dependency no package manager carries.
|
|
Gitea's `tea` is the motivating case and the first user.
|
|
|
|
Two things it does NOT do, both deliberate and both worth knowing:
|
|
|
|
- **No checksum.** The download is verified by RUNNING it through the same `identify` rule the probe
|
|
uses, which catches an HTML error page, a truncated file and a wrong-architecture build — none of them
|
|
print what the plugin said to look for. It does not catch a compromised upstream. A checksum per
|
|
os/arch is a table that goes stale every release and would stop installs the day upstream re-cuts a
|
|
tag; HTTPS and a URL the owner reads in the install dialog is what stands behind it today.
|
|
- **The version is pinned in the manifest.** `tea` is fixed at 0.15.1, so a new release needs a plugin
|
|
update. That is the right default — an install that silently fetches whatever is latest is not
|
|
reproducible — but there is no "check for a newer one" anywhere.
|
|
|
|
## Inputs outside forms (2026-08-19) — swept, fixed, closed
|
|
|
|
The "19 files" count in the previous version of this entry came from one grep
|
|
(`components/ui/input`) and was a starting point, not the answer. A wider sweep — bare `<input>` and
|
|
`<textarea>` too, across the platform and every installed plugin — found **87 files with a matching
|
|
element and no enclosing `<form>`**. Three were false positives (the `Input`/`Textarea` primitives
|
|
themselves, and a `DataTable` test file). Of the remaining 84, **41 were fixed** (real forms, single
|
|
fields with an obvious action, or multi-field editors) and **43 were left alone**, almost all because
|
|
they're a search/filter box with nothing to submit.
|
|
|
|
**Fixed — platform (24 files, one commit):** `CapabilityPage`, `ApifyConfig`, `ApiKeys`,
|
|
`DavAppPasswords`, `GoogleOAuthConfig`, `AIHarnessesSection` (local-provider connect, its auth step,
|
|
and both API-key edit rows), `OCRSection`, `SMTPSection` (split into two independent forms — save vs.
|
|
send-test-email, since one form would have made Enter in the test-email field trigger Save),
|
|
`STTSection`, `TTSSection`, `AppStoreDetail`, `ChatHeader`, `BackgroundJobDialog`, `QuestionActivity`,
|
|
`WebpageDialog`, `DirPickerModal`, `SessionRow`, `CommandHqCategories`, `FileItem`, `NewEntryDialog`,
|
|
`TaskRunnerModal` (the Agentic and Pipeline ready phases), `PreviewHeader`, `widgets/DailyGoals`,
|
|
`widgets/Pomodoro`.
|
|
|
|
**Fixed — plugins (17 files, one commit per plugin, each pushed to its own remote):**
|
|
`bitcoin-wallet` (`ChainSourceCard`, `RescanCard`, `WalletSettingsView`), `email` (`Compose`,
|
|
`EmailAccounts`), `invoices` (`DocumentEditor`, `RecordEditors`, `SendDocumentDialog`), `offscale`
|
|
(`NodesView` — rename and tags fields), `soulseek` (`SoulseekConnection`), `transmission`
|
|
(`ConnectionView`, `SettingsView`, `AddTorrentDialog`, `MoveTorrentDialog`, `RenameDialog`,
|
|
`TorrentOptionsDialog`), `vault` (`VaultPanel`). `jellyfin` and `music` needed no changes.
|
|
|
|
**Left alone — search/filter boxes with no submit action** (the large majority of the 43): `JobsPage`,
|
|
`SettingsPanel`, `SearchInput`, `sounds/Catalog`, `widgets/QuickNotes`, `Dashboards/DashboardListApp`,
|
|
`SystemMonitor/DockerView`, `FileBrowser*` toolbar/header/view-container files, `ChatHistory` list
|
|
filters, `invoices`' six list views (`CustomersListView`, `EstimatesListView`, `ExpensesListView`,
|
|
`InvoicesListView`, `ItemsListView`, `ReportsView`, `components.tsx`), `jellyfin` (`PlayerControls` —
|
|
range sliders, not text fields — and `SearchSection`), `music` (`MusicBrowser`, `MusicPlayerHost`),
|
|
`offscale` (`Cards`, `PolicyAssistant` — already handles Enter/Shift+Enter correctly, `PolicyView` — a
|
|
textarea, never auto-submits), `soulseek` (`SearchResults`, `SharesBrowser`), `transmission`
|
|
(`TorrentToolbar`), `FileViewer` renderers (`AudioRenderer`/`VideoRenderer` — volume/seek sliders), and
|
|
the main chat composers (`InputArea`, `ChatLauncher`, `SoulseekChat`) — all textareas with their own
|
|
Enter-sends/Shift+Enter-newlines handling, where a `<form>` would add nothing and risks regressing that
|
|
handling.
|
|
|
|
**Left alone — genuinely ambiguous, flagged rather than guessed:**
|
|
|
|
- `TaskRunnerModal`'s `ScriptRunner` ready phase offers three simultaneously-visible, non-equivalent
|
|
primary actions ("Run here" / "Queue" / "Run as job"); there's no single canonical action for Enter to
|
|
trigger.
|
|
- `Dashboards/DashboardPreview`'s `NamePanel` (name + description) has its submit button in a
|
|
_different_, independently-mounted panel (`CreatePanel`) elsewhere in the workspace layout tree — a
|
|
`<form>` can't span two panels without restructuring the wizard.
|
|
- `ColorPicker` and `MarkdownEditor` have zero call sites anywhere in the repo right now; both are
|
|
shaped to be embedded in a future caller's own form (uncontrolled inputs with `name` attributes, or a
|
|
plain controlled textarea with no submit of its own), so there's nothing to wrap yet.
|
|
- `widgets/Weather`'s `LocationSetup` is a live autocomplete with several result rows and no single
|
|
obvious Enter target (which of N matches?).
|
|
|
|
**Noticed, out of scope, not fixed:**
|
|
|
|
- `ModelSelector`'s provider-picker button has no `type` attribute — harmless today only because it
|
|
currently sits outside every `<form>`; a landmine if that layout changes.
|
|
- `MusicBrowser`'s filter input uses `(e) =>` instead of the house `ev` convention — pre-existing, not
|
|
touched since the file needed no fix.
|
|
- `EmailList.tsx`, and a few plugin files touched by this sweep (`VaultPanel`, `ConnectionView`,
|
|
`SoulseekConnection`, `ChainSourceCard`), have pre-existing `prettier --check` drift outside the lines
|
|
this pass touched — left as found, not reformatted, per "don't reformat what you're not fixing."
|
|
Relatedly: **`prettier --write` on a `plugins/<name>/…` path silently no-ops when run from the
|
|
platform root**, because Prettier v3 respects the root `.gitignore` and `/plugins/*/` is in it — it
|
|
exits 0 with no diff and no error. Formatting a plugin file requires running prettier from inside
|
|
that plugin's own directory (still resolves the platform's `.prettierrc` by walking up).
|
|
- A few "Save"/"I've saved it"-style buttons (`ApiKeys`, `DavAppPasswords`, `DirPickerModal`) have no
|
|
`type` attribute but currently sit outside any `<form>`, so it's harmless today — worth remembering if
|
|
a future edit wraps that region.
|
|
|
|
## Affordances (2026-08-19)
|
|
|
|
Agreed and written down in `plugins/CLAUDE.md`; **nothing is built**. An affordance is what the surface
|
|
on the other end offers the agent — declared by the code that implements it, read at runtime. The first
|
|
two are image rendering (core) and gitea (`git` for git, `tea` for everything else).
|
|
|
|
Built 2026-08-19: `servers/affordances/load.ts` reads the tree and compiles one block, and
|
|
`claude-manager.ts` appends it to every Claude session via
|
|
`systemPrompt: { type: 'preset', preset: 'claude_code', append }`. Read per session, so a plugin
|
|
installed an hour ago is live on the next message.
|
|
|
|
Still to build:
|
|
|
|
- `capabilities/affordances/` created by the setup script, with `officer/core/` populated from an
|
|
officer-capabilities repository. The directory exists on this machine by hand; a fresh install gets
|
|
nothing, so the first affordance is currently a local artefact rather than a shipped one.
|
|
- **Opencode delivery.** It has its own driver and gets nothing, so image rendering is currently a claim
|
|
that holds on one harness — exactly the drift affordances exist to remove. Deliberate: opencode work is
|
|
paused pending its v2 migration and this would be thrown away. Revisit when v2 lands.
|
|
- A bare `claude` run in a terminal on the host gets nothing either, since it never passes through
|
|
`claude-manager`. Would need a generated file, which is the thing `user-instance.ts` refuses to write
|
|
for the owner.
|
|
- ~~Plugin contribution~~ — built 2026-08-19 (`affordances/publish.ts`). An `affordances/` directory in
|
|
the plugin repository is copied to `capabilities/affordances/<publisher>/<plugin>/` on install and
|
|
removed on uninstall. Gitea is the first to ship one.
|
|
- **The other categories.** A plugin can contribute affordances and nothing else. Skills, tools, tasks
|
|
and processes are the obvious next ones and were deliberately left out — one mechanism, proven, before
|
|
five. When they land, `publishAffordances` generalises rather than gaining siblings.
|
|
- **`officer/core` and `officerdev/<plugin>` are both correct — do not normalise them.** The path uses
|
|
`publisher` verbatim, so a plugin published by officerdev lands under `officerdev/`. `officer/core` is
|
|
not a plugin at all: it is the PLATFORM's own, shipped by the setup script from the capabilities
|
|
repository. Two spellings marking two different things, arrived at by accident and kept on purpose.
|
|
Anyone tidying this into one name is removing the distinction, not the inconsistency.
|
|
|
|
Open, and deliberately deferred:
|
|
|
|
- **Scoping.** Everything is loaded into every conversation. Fine at two files, not at twenty. A
|
|
`surface` field was drafted and dropped because it was doing two jobs at once: for image rendering it
|
|
means _what the client renders_ (a phone, a web bubble, a cron run with nobody watching), for gitea
|
|
_what tooling exists on this machine_. Those are different axes and one field cannot be both. Decide
|
|
the axis before adding the field — and note this codebase has already been bitten by a field that
|
|
validated itself and was never consumed (`unauthenticatedPaths`, see `plugins/public-paths.ts`).
|
|
- **Drift.** The rule is "declared where implemented", but a hand-authored markdown file in a repository
|
|
is not that — `ChatImage` could stop rendering images and no file would notice. Accepted for now
|
|
because prose that reaches every harness beats a registry that reaches one; revisit if it bites.
|
|
|
|
## Plugin ecosystem — before it opens to third parties (2026-08-18)
|
|
|
|
Not now. Months away. Recorded so it is not re-derived.
|
|
|
|
- [ ] **A plugin install is not pinned, so a review would bind a repository rather than code.**
|
|
`fetch.ts:70` is `git clone --depth 1 <source>` — no ref — so it takes whatever the default branch
|
|
points at when the install runs. A plugin reviewed at commit X installs at whatever was pushed
|
|
since, and `--depth 1` leaves no history to compare against.
|
|
|
|
This is the shape of the npm attack already documented in `CLAUDE.md` (2026-08-04), and the same
|
|
shape as the Chrome Web Store's real failures — not bad extensions passing review, but good ones
|
|
sold or compromised and then pushing an update to users who had already accepted them.
|
|
|
|
Fix: a `reviewedCommit` on the catalogue entry, checkout detached at that SHA, and **verify
|
|
`rev-parse HEAD` matches or refuse**. The verification is the part that matters. An update then
|
|
becomes "a new reviewed SHA exists" — a catalogue change rather than something the author triggers.
|
|
|
|
- [ ] **Third-party plugin sidecars are unconfined.** A plugin sidecar runs `bun run <script>` with
|
|
`cwd: PLATFORM_DIR`, so Bun auto-loads `.env` and it gets `POSTGRES_URL`; it runs as the service
|
|
user, which owns `secrets/officer-keys.db` and can therefore mint an owner JWT. Fine for
|
|
first-party plugins, total compromise for anyone else's.
|
|
|
|
The mechanism to fix it already exists and is pointed elsewhere: `spawn-as-member.ts` drops
|
|
privileges with `setpriv --reset-env` and a six-name allowlist for exactly this reason.
|
|
|
|
- [ ] **No install-from-git-URL.** The catalogue is the only route for a remote plugin, so the
|
|
"build your own, give it to a friend" path has no affordance. Small, and needed before the
|
|
unofficial channel is real.
|
|
|
|
## Plugin system — gaps the email extraction found (2026-08-18)
|
|
|
|
- [ ] **A plugin cannot declare its own npm dependencies, so the PLATFORM is carrying them.** `imapflow`,
|
|
`mailparser` and `@types/mailparser` are in the platform's `package.json` and are imported by nothing
|
|
in `src/` — they exist solely for `plugins/email/sidecar/`. Plugins have no `package.json`, run with
|
|
`cwd: PLATFORM_DIR`, and resolve from the platform's `node_modules`, so deleting those three lines
|
|
breaks the installed plugin on its first sync.
|
|
|
|
This does not scale and it is the wrong party holding the risk: the platform cannot be answerable for
|
|
what an arbitrary plugin decides to install, and `bunfig.toml` sets `frozenLockfile = true` precisely
|
|
so a dependency change is a deliberate, reviewed act. Publishing will be reviewed rather than a
|
|
free-for-all, which narrows the threat but does not move the responsibility.
|
|
|
|
What it probably needs: a `dependencies` field in the manifest (the manifest already holds
|
|
`osDependencies` for host binaries, which is the same shape of problem solved once), installed into
|
|
the plugin's own directory at install time rather than merged into the platform's tree. Note the
|
|
install path already learned not to trust `$PATH` — `pushPluginSchema` resolves a binary rather than
|
|
spawning `bunx` — so whatever runs the install has to do the same.
|
|
|
|
Until then: `imapflow` and `mailparser` STAY. Removing them is not tidying, it is breaking email.
|
|
|
|
- [ ] **`email_accounts/` is created by the PLATFORM for every account, and should be the plugin's job.**
|
|
`USER_DIRS` in `data-path.ts` pre-creates it at 0700 for every user, whether or not the email plugin
|
|
is installed — so a core install provisions a directory named for a plugin it does not have.
|
|
|
|
Ownership should invert: the plugin provisions its own storage, in all three moments —
|
|
|
|
1. **on plugin install**, for accounts that already exist;
|
|
2. **on user provisioning**, for accounts created afterwards;
|
|
3. **lazily**, the first time a provisioned user opens the plugin, so a machine that missed either of
|
|
the above still heals itself.
|
|
|
|
The third is the one that makes the other two non-critical, and it belongs in the email sidecar
|
|
rather than in the platform's account handler.
|
|
|
|
This is entangled with the `email_db` leak below — core reads that directory today, so the plugin
|
|
cannot become its sole owner until that is resolved too. Do them together.
|
|
|
|
- [ ] **The platform still knows email's on-disk vocabulary, deliberately, and it is untested by anything.**
|
|
`findEmailAccountName` in `data-path.ts` reads `DATA_PATH/<user>/email_accounts/*/emails.db` and picks
|
|
the newest by mtime. Two core callers use it: `user-instance.ts` (`OFFICER_EMAIL_DB`, for the agent's
|
|
`email_db` MCP tool) and `chat/websocket.ts` (`resolveEmailCwd`, for a chat opened against a mailbox).
|
|
`ChatPanelWrapper.tsx` hardcodes `workspace.id === 'email'` to trigger the second.
|
|
|
|
This traded a CODE dependency for a CONVENTION, which was the right trade at the time — the code
|
|
dependency is what put `officer-agent` into a restart loop on 2026-08-13 (`fbb6d6c7`). But the
|
|
convention has no enforcement on either side: if the plugin ever renames `emails.db`, moves to one
|
|
database per folder, or nests differently, core silently points the agent at a path that does not
|
|
exist. No import breaks, no type fails, no test covers it. The tool just goes quiet.
|
|
|
|
The clean version needs two things the platform does not have: a way for a plugin to ship an MCP tool,
|
|
and a way for a plugin to declare a chat context. Until both exist this stays — but it should be a
|
|
decision someone re-takes, not one that is inherited.
|
|
|
|
## Plugin repos — what writing their READMEs turned up
|
|
|
|
Three READMEs were written on 2026-08-16 (`music`, `offscale`, `example`) and pushed to their own repos.
|
|
Writing them meant reading each plugin against its own documentation, which is the cheapest audit there
|
|
is. Everything below was found that way and none of it is fixed.
|
|
|
|
**Offscale is still branded headscale in shipped, user-visible places.**
|
|
|
|
- [ ] `web/panels.ts` registers three panels named "Headscale servers", "Headscale", "Headscale" — these
|
|
are what a user reads in the panel picker.
|
|
- [ ] `web/OffscaleServerPicker.tsx:26` renders the literal heading `Headscale`.
|
|
- [ ] `web/ConsoleView.tsx:22` mints terminal session ids as `headscale-console-<id>`. That is persisted
|
|
state rather than a comment, so renaming it orphans live console sessions — cheap now, less so later.
|
|
- [ ] ~12 stale `/headscale` route paths in comments across `web/`, plus `App.tsx:78` in the platform.
|
|
- [ ] `PLUGIN.md` has ~11 stale self-references (`headscale_servers`, `listHeadscaleServers`, permission
|
|
key `headscale`, `/api/headscale`, panel keys, `useHeadscaleSection()`).
|
|
|
|
**Documentation that actively lies.**
|
|
|
|
- [ ] `plugins/offscale/PLUGIN.md:445-449` says the ACL assistant is deliberately unwired and must not be
|
|
wired up. It IS wired — `sidecar/assist.ts`, `claude-proxy.ts`, `web/PolicyAssistant.tsx` and the
|
|
manifest's `readOnlyWrites` all ship.
|
|
- [ ] `plugins/offscale/OFFSCALE_API.md` claims to publish the contract "in full" and omits six live
|
|
surfaces, including all of `/_officer/companion/*` and `/_officer/enroll/invites`.
|
|
- [ ] `plugins/music/MUSIC_API.md` says favourites and now-playing are served by the platform from
|
|
Postgres; the sidecar serves them itself, scoped by `X-Officer-User`. It also documents the playlist
|
|
item key as `<albumRel>/<file>` while the schema and favourites use the home path — the same file
|
|
contradicts itself.
|
|
- [ ] `plugins/music/MUSIC_API.md` documents now-playing as one snapshot per user with no mention of
|
|
`?device=`, which the schema makes part of the primary key. A mobile client written to that doc is
|
|
correct only by accident.
|
|
- [ ] `plugins/example/manifest.ts:12` lists `web/Router.tsx` as the frontend convention; discovery
|
|
requires `web/panels.ts` AND `web/layout.ts`. The same file documents an `ownerOnly` field that does
|
|
not exist and never did.
|
|
- [ ] The Companion contract docs the offscale code cites (`COMMS/OFFSCALE_COMPANION_API.md`,
|
|
`COMMS/OFFSCALE_INVITE_ENROLLMENT.md`) exist nowhere — `COMMS/` was deleted. Two of the plugin's
|
|
eight sections depend on a service with no published spec.
|
|
|
|
**Conventions a plugin author has to guess.** All verified in `discover.ts`/`mount.ts`, none documented
|
|
outside the example's new README:
|
|
|
|
- [ ] Panel `key`s and layout `appType`s must agree, and a mismatch fails SILENTLY — `PluginScreen.tsx`
|
|
pins `appTypes.allowed` with `fallback: allowed[0]`, so a typo renders the first panel.
|
|
- [ ] The `<appName>_` table prefix is unenforced. The barrel is `export *` across every plugin into one
|
|
`public` schema, so two plugins choosing the same unprefixed name collide with no error until push.
|
|
- [ ] A manifest is module-cached for the process lifetime: editing it does nothing until
|
|
`pm2 restart officer`, and `outdated` cannot see a version bump either.
|
|
- [ ] Permission-key prefixing is convention, not validation. Only a collision with a CORE key is refused,
|
|
silently, at mount, with a console error.
|
|
|
|
**Genuinely missing.**
|
|
|
|
- [ ] `plugins/offscale` spawns the `ssh` binary (`sidecar/ssh.ts`) but declares no `osDependencies`, so
|
|
install never checks for it and the Console fails at runtime instead of refusing at install.
|
|
`PLUGIN.md:421` already lists it as a host dependency — the manifest is the file that is wrong.
|
|
- [ ] `scripts/reindex-music.ts` resolves its port file from `process.env.DATA_PATH ?? cwd/data`, and
|
|
`DATA_PATH` stopped being an env var on 2026-08-13. Run from the repo it looks in
|
|
`platform/data/music/.server` and never finds it. One line.
|
|
- [ ] `plugins/music/web/MusicPlayerHost.tsx` header says it is mounted once in the persistent
|
|
DashboardLayout; it moved into `MusicDetail`. That is exactly the comment that would mislead
|
|
someone about why playback stops off-route.
|
|
- [ ] A plugin's `assets/icon.png` is only published while installed, so its real icon is invisible in the
|
|
store before install. The catalogue's `iconUrl` works around it, and an author has to know to add one.
|
|
|
|
## Waiting on the owner to run it
|
|
|
|
- [ ] **The AI usage panel works for one account; the multi-account half is unproven.** Landed and verified
|
|
live 2026-08-18 — click the harness pill in the chat composer and it shows Claude Code's own usage
|
|
display, limits with countdowns, tokens by day and by model.
|
|
|
|
What is NOT proven is the reason it was built: a second Anthropic login, the account tabs, and a
|
|
stored row surviving a switch. **Group C of `docs/ai-usage-panel.md`** is that, and test 10 is the one
|
|
most likely to be wrong — a refresh must not overwrite an account it could not measure, and that
|
|
failure is silent.
|
|
|
|
Deliberately deferred rather than forgotten: the owner switches accounts when the work needs it, and
|
|
the worst case is one wrong tab. Run Group C the first time a second login lands on this machine.
|
|
|
|
Also unexercised: the model dropdown no longer renders the literal string `claude-code` for a chat
|
|
that named no tier. Needs `officer-claude-code` restarted, not just `officer` — the parser in the
|
|
sidecar is what emits it.
|
|
|
|
- [ ] **Plan mode is live and has never been exercised.** Landed `1b811dfa` on 2026-08-17; all three
|
|
processes were restarted the same evening, so the code IS running. Not one part of it has been used:
|
|
not the toggle, not the plan card, not the round trip from the sidecar's pending promise to a browser
|
|
and back, not the stored column, not `settleAllPlans`.
|
|
|
|
**`docs/chat-plan-mode-test-plan.md` is the checklist** — eleven ordered tests, what each should show,
|
|
and what its failure looks like. Test 4 (approve) is the one most likely to be wrong: it is the case
|
|
that changed the design, because approving `ExitPlanMode` drops the harness into `default` rather than
|
|
back into `bypassPermissions`.
|
|
|
|
Safe to leave untested. Plan mode is opt-in and off by default, so a conversation that never touches
|
|
the toggle behaves exactly as it did before. The one failure worth knowing in advance is a turn that
|
|
never ends after a plan is answered — that would be a leaked `canUseTool` promise, and
|
|
`pm2 restart officer-claude-code` clears it.
|
|
|
|
Design and the probe results behind it: `docs/chat-plan-mode.md`.
|
|
|
|
## Known bugs
|
|
|
|
- [ ] **Background job logs appeared INSIDE an open file in the code editor.** Reported 2026-08-16 by the
|
|
owner, on the production host, running as a member with a `bun dev` background job streaming. The
|
|
editor had a file open; the runner's log output was rendered into that file's buffer. **It was never
|
|
saved** — the file on disk was intact, and the editor was not marked dirty as far as the report goes.
|
|
So this is a DISPLAY-layer mixing, not a write path: the bytes reached a buffer that only exists in
|
|
the browser.
|
|
|
|
Not reproduced, not diagnosed, and deliberately not guessed at further than this. What is worth
|
|
recording is where the two streams could meet, because both were added in the same week and they are
|
|
the only two things writing text into a panel:
|
|
|
|
- `useTaskDetail` polls `GET /chat/tasks/:id` every 2s while a job runs and returns `{kind:'log', text}`.
|
|
`CodeSurface` renders it — and `CodeSurface` is shared with the editor's own surfaces.
|
|
- The editor keys open files by PATH (`useEditorState`), while the task panel keys by TASK ID, and
|
|
several panel-scoped stores in between are keyed by `panelId` through `useGlobal` /
|
|
`usePanelMemory`. A key collision between those namespaces would produce exactly this: one panel's
|
|
text arriving in another's buffer, with no server involvement and nothing persisted.
|
|
|
|
**Screenshot 2026-08-16 answers the first question and kills two theories.** The tab carries a DIRTY
|
|
dot, and the log lines have Monaco line numbers continuing the file's own sequence (…180, then eight
|
|
lines of dotenv/POSTGRES_URL output, then 189 `</Link>`). So the MODEL was mutated — this is the
|
|
editor's content state, not its render, and the earlier "the file was okay" means it was simply never
|
|
saved. Note the splice is mid-file, not a replacement: JSX text and log text share line 180
|
|
(`Forgot?--env-file=.env --watch src/server.tsx`).
|
|
|
|
Ruled out by that: `CodeSurface` renders with shiki, not Monaco, so the two cannot share an editor
|
|
instance. And the app's only other Monaco mount (`FileViewer/renderers/EditorRenderer.tsx`) passes no
|
|
`path`, so its model URI is `inmemory://model/N` and cannot collide with the editor's `file:///…`.
|
|
|
|
So the pollution arrives as TEXT through the editor's own content path — `openFile`, `setContent`, or
|
|
the end-of-turn refresh that re-reads every clean file. That last one is the prime suspect: it is the
|
|
only thing that writes a buffer without a keystroke, it runs exactly when a background job is
|
|
producing output, and it lands its result by path.
|
|
|
|
**Narrowed 2026-08-16, and a trap is now set.** `reloadFile` sets `originalContent` and clears
|
|
`isDirty`, so the end-of-turn refresh CANNOT produce the dirty dot in the screenshot — the refresh
|
|
path is out. Nothing in the app injects text into the page either: no `execCommand('insertText')`,
|
|
no synthetic keyboard events, no clipboard writes near the editor. That leaves Monaco's own change
|
|
event, which means it looks like a paste or a programmatic `executeEdits`.
|
|
|
|
**`docs/editor-buffer-corruption.md` is the procedure — read that when someone reports this.** A
|
|
trap is set: `reportSuspiciousEdit` in CodeEditor.tsx records any insertion over 200 characters
|
|
arriving while the editor is NOT focused, with the stack at that moment, into
|
|
`localStorage['officer:editor-buffer-trap']` (last five). It persists rather than only logging
|
|
because this arrives as a report hours later, when the console is long gone — the question becomes
|
|
"paste this key" instead of "what did you see?". Delete all of it once the cause is known.
|
|
|
|
One thing to rule out first, because it is mine and it is recent: `2b70a570` gave `<Editor>` a real
|
|
`path`, so models are now keyed by file URI globally rather than being per-mount `inmemory://`. Two
|
|
panels open on the same file now share one model. That should be an improvement, but it is a change
|
|
in exactly the layer that turned out to be involved.
|
|
|
|
Worth noting because it is the reason to look at all: an editor that can show content the file does
|
|
not contain is a correctness problem even when it never writes — the next person's instinct will be to
|
|
save the file to "fix" it.
|
|
|
|
- [ ] **`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`
|
|
|
|
- [ ] **Move the remaining env credentials into the secret store.** `putSecret`/`getSecret` and
|
|
`bun run secret:set` landed 2026-08-19, and notify now reads `apns-p8`, `apns-key-id`,
|
|
`apns-team-id`, `fcm-service-account` and `discord-webhook` from the store with env as a
|
|
fallback. Every other plugin credential is still env-only — same argument applies: bun
|
|
auto-loads `.env` into every PM2 process, so each one is readable from `/proc/<pid>/environ`
|
|
of twenty processes that have no business with it.
|
|
|
|
## 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.
|