Right-click an entry and agents whose triggers match appear under their own Run Agent
submenu — deliberately not folded in with tasks, because an agent run is a chat session and
not a job, and one menu promising both would lie about what a click does. The modal shows the
absolute target path, autofills entry_path with it (tilde expansion stays the server's job),
and on Run links to /chat?cwd=<runs dir> instead of a queue entry: there is no job row to view.
Trigger matching and category grouping are now shared with tasks rather than duplicated, and
the task input form is reused as-is.
Rescan counts agents and invalidates their caches, so a new AGENT.md shows up on the button
rather than after the 60s staleTime.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
officer-photos owns the whole Immich contract: the instance URL and the API
key live there and nowhere else, and the platform side is an auth-gated
forwarder holding no credentials. The route surface is an allow-list keyed on
the first path segment, so admin, auth, api-keys, sessions, jobs, system-config
and libraries are unreachable by construction rather than by enumeration.
The UI mirrors Immich's own sidebar — timeline, explore, map, search, albums,
people, favorites, sharing, archive, trash — because the point of a sidecar
screen is to reproduce what the upstream already ships, then extend it. The
timeline reads Immich's columnar time-bucket format directly; selection lives
in the URL per docs/navigation-audit.md.
Two things worth knowing for anyone touching this later:
- `duration` is an integer count of milliseconds in Immich 3.0. It was an
HH:MM:SS.mmm string before, and every stale example still shows that form.
- the map container is sized with h-full/w-full, never `absolute inset-0`.
maplibre's stylesheet sets `position: relative; overflow: hidden` on the
element it is given, and an unlayered vendor rule beats Tailwind 4's layered
`.absolute` regardless of source order — so the div collapses to height 0 and
clips its own canvas away. Nothing errors: the GL context is healthy, tiles
download and pixels are drawn into a buffer nobody ever composites.
maplibre-gl is pinned to 5.x deliberately; 6.0 resolves a separate worker file
from import.meta.url, which Officer's index.html fallback answers with HTML.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
deletes the last of the projects/apps cluster: the published-app store
(/api/apps + /api/app-serve), the project dev-server and its websocket
proxy (/api/dev-server + /api/dev-server-proxy), the shared html-rewrite
they were the only consumers of, and their frontend — the Preview panel,
the UserApp panel/header, useUserApps and the /settings/apps screen.
also drops getUserProjectsDir and getUserAppsDir, the ProjectType and
ProjectDefinition types, and the 'dev-server' websocket provider from
server.tsx. nothing on disk is touched.
1440 deletions, 31 insertions. tsgo clean.
the owner's work, committed as one unit rather than split: the registration
files (App.tsx, Dock, AppRegistry, hono.ts, the schema and db barrels,
ecosystem.config.cjs) all reference modules under src/servers/{api,sidecar}/wallet
and src/workspaces/officerdev/src/apps/Wallet, so committing the shared plumbing
on its own would leave a commit that does not build.
officer-wallet is a new pm2 peer holding seed material sealed under an owner
passphrase on top of VAULT_STORE_KEY, with an unlock ttl after which the root key
is wiped from memory. five backends: on-chain via esplora, and lnd, clnrest,
lndhub and nwc for lightning. bolt11 encode/decode is implemented in-tree.
no secrets in the diff — the key-shaped literals under sidecar/wallet are the
bolt11 spec vectors and the bip39 "abandon … about" vector. .env.example gains
placeholders only. bun test src/servers/sidecar/wallet: 38 pass, 0 fail.
not reviewed line by line; assembled and verified to build, not audited.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a /invoices workspace screen replicating the parts of the invoiceshelf web ui
that are actually used: dashboard, invoices, estimates, recurring invoices,
payments, expenses, customers, items and reports.
one document editor serves invoices, estimates and recurring invoices — they
share the line-item table, the discounts, the taxes and the totals, and differ
only in a header strip. the arithmetic is a port of upstream's
use-document-calculations, rounding points included, because the server
re-validates the totals it is sent. money is integer minor units throughout and
is converted to major units at the edges only.
selection and editor state live in the url (`/invoices/:section`, `?selected=`,
`?edit=`), so the nav highlight is derived rather than held, back closes an
editor instead of leaving the screen, and a half-written form survives a reload.
sending a document is behind an explicit confirmation — it emails the customer.
built against the running 2.4.2 instance rather than the 3.0 checkout in
_references; the two differ materially. typechecked, not yet exercised in a
browser.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
officer-transmission is a new pm2 peer that owns the transmission rpc
connection and exposes a curated /_officer/* contract instead of proxying
raw rpc. it absorbs the three quirks callers otherwise have to know about:
the 409 x-transmission-session-id handshake, failures returned as
{"result": "..."} inside http 200, and basic auth where an empty username
must send no header at all.
/transmission is the ui, on the workspace/panel framework: a filter nav and
three sections (torrents, stats, settings). the torrent list is virtualised
with 30 available columns, multi-select, and a right-click menu; the detail
pane covers general, files as a real tree, peers and trackers. filters and
the open torrent live in the url, so a filtered view is a link.
phase 1 goal was parity with _references/transmission-web. follow-up work is
recorded in TODO.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
officer-headscale owns the whole Headscale contract: the registered servers and
their admin api keys, the >=0.29 version floor, and every multi-call composition
the ui needs. the platform side is auth+forward only and holds no headscale
credentials, so the existing /api/vpn/enroll route and its HEADSCALE_* env vars
are untouched and unrelated.
officer manages many servers rather than one. the owner registers each with a url
and a key generated on that server and switches between them; exactly one is
active, enforced by a partial unique index rather than by convention. keys are
encrypted at rest and never leave the sidecar — the list projection cannot return
one. registration validates before it saves: an unauthenticated GET /version to
prove something headscale-shaped is there and meets the floor, then an
authenticated call to prove the key works. an edit that moves either half
re-validates.
there is deliberately no transparent /api/v1/* passthrough. headscale serialises
every uint64 as a json string and its rest shape moved repeatedly below 0.29;
proxying raw would push all of that into the browser, which is the mistake the
soulseek panels made with 37 raw upstream calls.
the /headscale workspace is nav + view over the panel system. only the servers
section is implemented — nodes, users and pre-auth keys say so plainly rather
than rendering an empty table that reads as a failed fetch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Being retired, so it is deleted rather than fixed — it had both defects dashboards just
had (edit dropping ?selected=, an abandoned edit following you onto the next item) and
repairing them was work for something on its way out.
Gone: the two screens and the panel apps, the three routes, the dock item and its entry
in the default dock paths (client and the three server-side copies), the page-title rule,
the app-registry entries, the barrel exports, the `projects` table with its queries and
row types, and the proj-meta/proj-layout/proj-terminals/proj-host-terminals branches in
the dashboards endpoint. The chat context and terminal state-key special cases for
`proj-layout-` went with them.
Deliberately kept: `getUserProjectsDir` in data-path.ts — the apps and dev-server routers
resolve user apps under the same on-disk Projects/ directory and are unrelated to this
feature. Nothing on disk is touched.
Needs `bun db:push` to drop the table; the schema change is the only thing standing
between the code and the database. One row was in it.
tsgo clean, 56/56 tests, no references left in src. Not yet exercised at runtime — the
restart is what will prove it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clicking any link shifted the whole app up by ~70px: the top of the content slid behind
the fixed header, and the nav dock — which should stay hidden until you reach for it —
appeared at the bottom. It survived navigation and only a full reload cleared it.
`overflow-hidden` still makes an element a scroll container; it only hides the scrollbar.
The nav Dock is absolute inside this section and hides itself with
translateY(calc(100% + 24px)), and a transformed descendant contributes its transformed
box to the scrollable overflow area — so the section had ~70px of scrollable range it was
never meant to have. Clicking a link moves focus, the browser scrolls the nearest scroll
container to reveal the focused element, and everything inside (all of it absolute
inset-0) went up with it, the parked dock included.
overflow-clip clips identically but does not create a scroll container, so there is
nothing for focus-into-view to scroll.
Diagnosed from the running app rather than by reading: the section reported
scrollTop=70 at the moment the layout was wrong, matching the 70px the content and the
music bar had moved, while innerHeight/visualViewport stayed at 556 (ruling out the
dvh/URL-bar theory) and the outer div's scrollHeight equalled its clientHeight (ruling
out the shell). Confirmed fixed in the browser.
The outer div at :21 is also overflow-hidden and is left alone — it has no scrollable
overflow, so it cannot exhibit this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two panels (search + transfers) via WorkspaceView, coordinating over the
soulseek:refresh channel. Adds the page-title rule and documents the
route conventions in CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The music dock was position:fixed, overlaying the bottom of the content, so the
nav dock needed a pile of hacks to dodge it: a hand-measured MUSIC_DOCK_HEIGHT
(72) constant, a presence flag, a translateY lift, and a matching hover-threshold
lift (via a ref) so it wouldn't slide away under the cursor. Fragile the moment
the music dock's height changed.
Now the dock is an in-flow bottom bar that reserves its own height:
- DashboardLayout is a flex column: the content region (flex-1, min-h-0) shrinks
when the music dock takes its space; MusicPlayerHost renders an in-flow bar
(shrink-0) instead of a fixed overlay.
- The nav Dock is absolute within the content region and measures its reveal/hide
boundary from that region's bottom edge (a boundaryRef) — so it always sits just
above whatever's at the bottom, music dock or not, with zero knowledge of it.
- Deleted MUSIC_DOCK_HEIGHT, musicDockPresent, the lift, and the transform hack.
Bonus: content at the very bottom is no longer hidden under the fixed dock.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
invalidateQueries only refetches mounted queries, so a rescan pressed anywhere
but the Tasks/Skills/… page just marked those lists stale — the new item wasn't
picked up until you opened that page. Pass refetchType: 'all' so every item
cache refreshes immediately, wherever the button is pressed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The nav dock rides MUSIC_DOCK_HEIGHT higher while the music dock is up, but the
hide threshold was still measured from the bottom — so hovering the raised dock
read as past HIDE_THRESHOLD and it slid away under the cursor. Lift hideAt (and
the magnify gate) by MUSIC_DOCK_HEIGHT; the reveal trigger stays at the edge.
Also read musicDockPresent via a ref so the once-registered mousemove handler
reacts to music starting/stopping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"Activity" (placeholder name — jobs/tasks were taken) = watch background tasks
scroll in parallel with chat. Built DB-free; NOT restarted — deploy + test in
the morning.
- NDJSON progress contract (activity/progress.ts): capabilities append
{job,cap,phase,status,pct,detail,ts,...} lines; tolerant parser treats any
JSON object with phase/status as structured progress, else a raw log line.
- Backend (activity/router.ts, owner-only, path-guarded):
- GET /api/activity/tasks — registry by scanning /tmp/claude-*/<cwd>/tasks/
*.output (harness run_in_background) + announced detached jobs.
- POST /api/activity/announce {name,path} — register a detached (setsid) job's
log so it's followable too (the setsid case is on the critical path, since
the warm worker now makes plain run_in_background the default for heavy jobs).
- GET /api/activity/stream?task=<id>|path=<abs> — SSE tail (poll + offset),
emitting {kind:'line'|'progress'} with NDJSON parsed.
- Frontend /activity screen + Radio nav item: task list (active dot) → live tail
with a phase/pct progress header + raw log, following the /system-monitor pattern.
- Retention: startChatEventRetention() prunes chat_session_events >7d every 6h
(wired in bootstrap) so the durable queue stays bounded.
Verified headlessly (no restart): parseTailLine classification, and the scan
finds 53 real task output files. Endpoints + UI untested until deploy.
Deferred (see handoff): cross-device sync + OpenCode parity (both touch the
now-stable chat path — won't ship un-restart-tested); task:progress into chat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Backend GET /api/system-monitor/stats: one snapshot — CPU overall + per-core
(two /proc/stat samples), memory + swap (/proc/meminfo), disks (df), load,
uptime, top-20 processes (ps). Owner-only via the account gate.
- Frontend /system-monitor screen: polls every 2s; CPU/Memory/Disks cards with
bars + per-core mini-bars, top-processes table. Nav dock "Monitor" item.
- Register /music and /system-monitor in usePageTitle RULES so the browser tab
and editable header title update on those routes (/music was missing too).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A full-page music browser reusing the app-wide player + dock. Sidebar lists
libraries (1st level of ~/Music); main is a card-grid folder browse with rich
pages: albums show a header + tracklist, and artist folders render their album
cards grouped into discography sections (Studio Albums / Live / Compilation / …)
using /music/discography. Cards have hover-play; everything feeds useMusicPlayer.
Adds a green Music dock item (/music) to the default dock.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The widget browses the /api/music/* library (search albums → tracklist) and hands
a queue to an app-wide player. The player (useMusicPlayer, useGlobal-backed) and
its site-wide bottom dock (MusicPlayerHost) live in the persistent DashboardLayout,
so playback survives route changes. Dock has cover/title/artist, drag-scrubbing
(SeekBar), volume (persisted), and prev/play/next/close. The nav Dock slides up by
MUSIC_DOCK_HEIGHT while the music dock is present.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Officer is single-user: the server owner is the only account, created once by
/auth/bootstrap. Everything that existed to serve additional users was
unreachable, so it is gone rather than left looking like it does something.
Accounts: drop the invite / resend-invite / delete / list-users routes and the
Users settings screen, the inert /auth/signup handler, and the account
verification chain it fed (verify, resend-verification, VerifyScreen, the
UserInvite + VerifyAdmin + VerifyRegistration templates). /auth/verify-token
survives for password resets only, and now requires a reset-password token
rather than accepting any signed JWT.
Roles: drop the users.role column and the four-value USER_ROLES enum. The
permissions table granted every role identical methods, and every
role === 'Super Admin' check was permanently true. The JWT no longer carries a
role claim.
Sandbox: remove sidecar/sandbox.ts and its five call sites. bwrap was selected
only for non-Super-Admin users, so it never ran. It was also not a usable agent
jail as written — --share-net, the project root (with .env) bound read-only,
and runuser dropping to the server's own uid. Rebuilding it for agent
containment would be a different construction, and git history keeps this one.
getHomeDir keeps its DATA_PATH meaning; the new getOwnerHomeDir resolves the
owner's real login home, which is what terminals, chats and task runs use.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deletes the Automation and NewAutomation screens, their /automation and
/new-automation routes, the Dashboard barrel exports, the dock item (+ default
dock path), and the page-title rule. TaskRunnerModal is kept — the file browser
still uses it to run tasks on files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each dashboard route now sets its own document.title (route→title map in
usePageTitleSync, mounted in DashboardLayout). The title also renders centered in
the top header; clicking it edits inline and updates the browser tab live —
per-tab only, reset on navigation/refresh (not persisted).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New POST /api/rescan re-runs the boot item setup (ensureItemDirs +
ensureToolLoader) and returns live item counts; the header button calls it
and invalidates the item query caches so the UI refetches from disk.
Also fix ensure-tool-loader to write into OFFICER_ITEMS_DIR/extensions (the
runtime read path) instead of the now-unread DATA_PATH/extensions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Pipeline jobs now persist to DB with progress tracking and cost accumulation
- Jobs survive WebSocket disconnects with in-memory event buffer replay
- New /jobs list page with search, status badges, and cost display
- New /jobs/:id detail page with live WebSocket attachment and REST fallback
- Two-column layout using WorkspaceLayout for resizable steps/output panels
- Streaming messages tagged with stepIndex/iterationLabel for per-step output grouping
- TaskRunnerModal links to job detail page once job is created
- Dock entry added for Jobs page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two-tier model policy: system policy (admin, allowedModels) controls
member access, per-user hiddenModels controls personal visibility.
All model selectors now use useUserVisibleModels. Moved providers
and model visibility out of system settings, AI models out of profile.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>