The URL model is done; rows are still opaque-click divs, which was the original finding and is now easier to fix than it was. Recorded honestly rather than leaving an entry that reads as untouched work. Also recorded the pattern behind the four bugs this produced: the list of overlay params has an owner, the acts that clear it do not, and every site deciding for itself is what let the breadcrumb ship wrong an hour after the rule was written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
361 lines
26 KiB
Markdown
361 lines
26 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
|
|
|
|
- [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.
|
|
|
|
## 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.
|
|
|
|
## 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.
|