- POST /reindex now runs the build to completion before responding (via a
coalescing reindexNow), and GET /manifest ensures a fresh (debounced 3s)
index first — so on-disk changes show up on a plain app refresh, not only
via the explicit reindex sheet.
- Log each resync in the officer-music sidecar (start + one-line summary,
or a failure line).
- Fix albums whose cover file isn't a decodable image (junk .jpg): the
manifest cover flag and the skip check now reflect whether a cover was
actually cached, so they settle to cover:false instead of rebuilding every
run (and the app no longer 404s fetching a cover that was never there).
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>
Adds a Super Admin ("owner") identity — SUPER_ADMIN_EMAIL, else the
bootstrap/first user (super-admin.ts) — and closes the hole where a music
account could sign into the full platform:
- Rename EXPO_PUBLIC_CLIENT_ORIGIN -> OFFICER_APP_ORIGIN.
- PUBLIC_URL + OFFICER_APP_ORIGIN are owner-only origins; MUSIC_APP_ORIGIN
stays path-scoped to /api/auth + /api/music.
- Account backstop (origin-independent): a valid non-owner token may reach
only /api/auth + /api/music regardless of Origin — airtight even if the
header is omitted/forged.
- signin rejects a non-owner logging in from an owner-only origin.
Owner keeps full access (verified); music users are confined to the music app.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add MUSIC_APP_ORIGIN to the origin allowlist and a global
originScopeMiddleware that restricts scoped app origins (the standalone
officer-music client) to their permitted path prefixes — /api/auth and
/api/music — and 403s everything else. The main web origin is unaffected,
and the gate no-ops while MUSIC_APP_ORIGIN is unset.
Lets extra sign-in-only users authenticate through the music app and reach
only music + auth, without reintroducing any per-user permission scheme.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Whisper feeds its own decoded output forward as the prompt for the next 30s
window, which is what makes it loop once it slips. max_context is the budget for
that carried text; setting it to 0 stops the loops but also disables the initial
prompt, since whisper.cpp gates both on n_max_text_ctx > 0. carry_initial_prompt
puts the prompt in a static slot that is filled first and in full, so making the
prompt longer than the budget leaves nothing over for decoded text — the prompt
reaches every window and none of the model's own output does.
The prompt is a style exemplar, not an instruction: whisper imitates what it is
primed with. A first version described the format ("Commas separate clauses")
and a character surname came back as "Commas", so both prompts are now ordinary
conversational prose with no meta-language, one per language.
Also switch to verbose_json for segment timestamps and rejoin the segments into
paragraphs on pause length, rather than emitting one line per utterance with a
leading space; decode with beam search instead of greedy; widen the VAD segments
so they break at real pauses instead of at breaths; and stop verbose_json from
re-running a full language auto-detect that step 1 already answered.
Measured on a 43 minute episode against the previous output: redundant repeated
sentences 18 -> 9 (the long distinctive loops are gone, what remains is a
catchphrase), paragraphs 1 -> 93, lone-punctuation lines 3 -> 0, lines with a
leading space 1021 -> 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MusicBrowser now lists a folder's children as single-column list items and drills
via the shared channel (libraries → artists → albums). When the current path is an
album leaf it lists the album's siblings and highlights it, so you can switch
albums from the left while the right shows the tracklist. Handles the non-uniform
library layouts via the manifest's tracks count.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Split the screen into two registered panel apps that coordinate via a
'music:cwd' panel channel, like /chat:
- music-browser (left): library selector, publishes the path.
- music-detail (right): renders the path — album tracklist, artist discography
sections, or a folder grid — and drives the app-wide player.
MusicScreen is now a WorkspaceView over a horizontal 2-panel layout (persisted as
screens/music), so the panels are resizable. Registered in AppRegistry.
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>
Top-level dirs of ~/Music are libraries (tabs); within one you drill through
folders via /file-browser/ls with a breadcrumb until a folder has tracks, then
its songs (titled from the indexed /music/meta, filenames as fallback) with a
cover + play-all. Selecting a track feeds the app-wide player. Handles the
non-uniform library layouts (Albums/<Artist>/<Album> vs DJ Sets/<Artist>).
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>
A reload button (next to +) so newly-deployed widgets show up without a manual
hard-refresh after a rebuild+restart.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each Albums/<Artist>/_discography.md (author-maintained source of truth, never
modified) is compiled into a per-artist discography.json in the cache = album
folder → normalized release type (Studio/Live/Compilation/Single/EP/…), so the
player can split an artist's album list into sections.
- indexer.ts: parse the md table, normalize the Type (EP?→EP, Compilation (VA)→
Compilation, …), write discography.json. The artist folder's `v` now includes
_discography.md so regenerating it re-syncs just that small JSON (isolated from
the albums' meta/cover). Manifest gains `disco: true` on such entries. Also
fixed the skip check to require all expected outputs to exist, so artist/
cover-only folders no longer rebuild every run. New `discographies` counter.
- sidecar: GET /discography?path=<artist rel> (ETag/304), documented in the
contract header.
- MUSIC_API.md: §2.4 + manifest disco flag + resync algorithm updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
x11vnc mirrors :0, but with no monitor attached the connector has no EDID and no
CRTC, so GNOME renders nothing and the remote desktop is black. Reproduced the
hard way on this box: the desktop only worked because the session had started
while a screen was plugged in, and survived exactly until the next restart.
The step captures a connected monitor's EDID, installs it as
drm.edid_firmware with video=<connector>:1920x1080e so the connector reports
permanently attached, and adds a login-time hook to raise the resolution — the
replayed EDID's *preferred* mode is the captured panel's native one, which can be
tiny, and GNOME picks preferred. monitors.xml is the documented override but its
monitor matching did not take.
The EDID can only be captured from a screen that is plugged in, so a headless run
skips with instructions rather than pretending to succeed. Re-running is safe:
GRUB is left alone once the argument is present, and the mode setter no-ops when
the mode is already right.
officer-set-display.sh discovers the output and the largest mode within a cap
rather than hardcoding either, since setup runs before X exists and cannot know
them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-scale 0.5 was hardcoded on the assumption that :0 is 4K. With no monitor
plugged in X falls back to something tiny — 800x480 on this box — and halving
that served an unreadable 400x240.
Read the framebuffer width from xrandr and scale only above 2560px. When the
width cannot be read, serve 1:1: too many pixels beats a thumbnail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
setup.sh installed pm2 but never ran anything with it, so a fresh install
finished with every dependency in place and nothing listening. That is not
cosmetic: /desktop returns 503 until officer-vnc is connected, and chat needs
officer-claude.
Adds a step that runs `pm2 startOrRestart ecosystem.config.cjs`, saves the
process list, and enables the boot unit when it is not already there. Using
startOrRestart rather than start means apps added to the ecosystem since the last
run get picked up — officer-music is in the ecosystem on this box but was never
running, for exactly that reason.
The verification block now reports which services are up, with the names read
from ecosystem.config.cjs so the list cannot drift as sidecars are added.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The desktop page has never worked on a fresh install. Two faults, both fatal.
The password could never be created. DesktopView fetches /desktop/vnc-password
before opening the WebSocket, but ensureVncPassword ran only from startSession,
which only the WebSocket triggers — so the endpoint answered "not configured",
the UI stopped, and the socket that would have provisioned it was never opened.
A new vnc:ensure-password sidecar command provisions it directly; the endpoint
asks for it instead of returning 500.
The rfbauth file could never be written either. ensureVncPassword shelled out to
tigervnc's `vncpasswd -f`, which is not installed — and, contrary to the comment
in setup-desktop.sh, is not in tigervnc-common, which ships only tigervncconfig.
The failure was swallowed because only a zero exit wrote the file, so x11vnc got
-rfbauth pointing at nothing. x11vnc writes that format itself with -storepasswd,
so the dependency is gone and a failure now throws.
Verified on the box: the endpoint returns a password, .vnc/{passwd,password} are
written 0600, and the sidecar reports mirroring :0 on 5900 with x11vnc using the
generated rfbauth file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bun's fetch aborts at 300s by default. whisper.cpp runs at roughly 8x realtime,
so a 43-minute recording needs about 5.4 minutes and died with "The operation
timed out" some 20 seconds short of the answer — long enough to look like a slow
machine rather than a ceiling.
AbortSignal.timeout() does not raise that ceiling; only `timeout: false` does,
which the DOM RequestInit type does not declare, hence the local BunRequestInit.
Verified end to end: a 43-minute episode through the task now completes in 547s
with a 27k-character transcript, where it previously failed at exactly 300s.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Standalone reference for the mobile team: auth, streaming (/stream +
X-Audio-Duration), the synced library index (manifest/meta/cover + per-album v
diffing, ETag/304), building/refreshing (reindex + SSE progress), the
recommended resync algorithm, and the data shapes (IndexMeta/Manifest/
IndexStatus/IndexReport).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
It described a product that no longer exists: a multi-user intranet for small
businesses, a user-invitation API, bun dev serving a separate dashboard on port
5000, and a closing rule to "always consider user isolation and role-based
access" — the opposite of how this codebase now works. An agent opening this repo
read that before anything accurate.
Now mirrors the deployment root: AGENTS.md points at CLAUDE.md so the two cannot
drift, and lists which of the remaining root documents are current.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The proxy is an opaque catch-all, so the endpoint surface wasn't perceivable from
the platform side. Add a contract header (all routes + params + SSE/response
shapes) atop the sidecar fetch handler where the routes are defined, and point
the proxy router at it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The orientation layer above platform/CLAUDE.md and capabilities/CLAUDE.md: which
of the three directories a change belongs in, the two-repo git rules, how to run
and verify without disturbing the running server, and the task system's
conventions — including the ones capabilities/CLAUDE.md omits, like INPUT_INCLUDE
and inline: ask.
Also records failure modes found by running things rather than reading them: the
vision model inventing text for images that have none, Whisper's translate being
English-only, and the Host header that protected routes require.
Lives here rather than at the deployment root so it is versioned and reaches
every install; the root CLAUDE.md points at it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a live progress channel for the library index:
- indexer.ts: progress subscribers (onIndexProgress) + throttled emit during the
walk, and buildReport() for a final summary.
- sidecar: GET /reindex/stream (SSE) — triggers a build if idle (?trigger=0 to
watch only), streams `progress` events, ends with a `done` event carrying the
report; auto-proxied at /api/music/reindex/stream for the app. Sidecar also
writes DATA_PATH/music/.server (its port) for local tooling.
- scripts/reindex-music.ts: CLI that reads the port file, follows the SSE, prints
live progress + a final report. Run: bun scripts/reindex-music.ts
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OCR now exists as a task, with recursion, multi-select scoping and a choice of
inline or job. The file viewer's OCR button is untouched, and /file-browser/ocr
stays — it is what both that button and the task call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The officer-music sidecar now builds a cache tree mirroring the library (server
counterpart of the app's music-index.ts), and exposes an rsync-clean diff surface.
Indexer (indexer.ts): walks HOME_DIR/Music; per album computes a version `v` =
hash of the source signature (track name+size+mtime, cover size+mtime); ffprobe
→ meta.json (phone IndexMeta schema: file/title/artist/albumArtist/album/track/
year/durationSec); ffmpeg compresses the cover to <=600px q5 cover.jpg. Writes
DATA_PATH/music/cache/<rel>/. Incremental (skip albums whose `v` is unchanged),
prunes cache dirs for albums removed from the library, maintains manifest.json.
Endpoints (sidecar, auto-proxied by /api/music/*):
POST /reindex async build; GET /reindex/status polls progress
GET /manifest { version, albums: { "<rel>": { v, cover, tracks } } }
GET /meta?path=<rel> album meta.json (ETag: v, 304 on If-None-Match)
GET /cover?path=<rel> compressed cover (ETag: v, 304 on If-None-Match)
Phone resync: GET /manifest, diff `v` against last-stored → fetch only changed
albums' meta+cover; drop rels missing from the manifest. No re-download of
unchanged albums.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract Audio now exists as a task, with recursion, multi-select scoping, a
format choice and multi-track handling — none of which the one-shot menu entry
had, since it always produced a single mp3.
/extract-audio stays because the file viewer's button plays its output rather
than saving it beside the video, but it no longer returns a cached file: like
transcription and OCR, it always redoes the work. The path is now named outRel,
since it is an output location rather than a cache.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds an officer-music sidecar that owns an audio-streaming HTTP server, and a
thin authenticating proxy on the platform. All processing (path resolution,
byte-range streaming, ffprobe duration) is in the sidecar; the platform only
authenticates and forwards.
App-facing contract (handoff):
GET /api/music/stream?path=<home-relative path>&token=<jwt>
- auth via userMiddleware (Bearer or ?token= for media elements)
- 200 full / 206 on Range, with Accept-Ranges, Content-Length,
Content-Range, Content-Type, and X-Audio-Duration (seconds, ffprobe)
- path resolved within HOME_DIR, traversal-guarded (400); 404 if missing
Purpose: stream + seek without pre-downloading the whole file — the app can
read X-Audio-Duration instead of scanning for VBR duration.
Pieces:
- sidecar/music/{index.ts,stream-audio.ts}: Bun.serve on a random port, /stream
+ /health, duration cached by path+mtime; reports its port via a new
music:server sidecar event on connect.
- api/music/{sidecar-server.ts,router.ts}: capture the port; reverse-proxy
/api/music/* → sidecar, streaming status + headers through.
- protocol.ts music:server event; hono.ts mounts /api/music; ecosystem adds
officer-music.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both endpoints kept a copy under cache/ and returned it on the next call, and
also short-circuited when the sibling .md already existed. So a re-run never
re-ran: a bad transcription stayed bad, and there was no way to ask for a fresh
one. Every caller passes saveNextTo, so the cache-path return was dead code
anyway.
Both now always do the work and overwrite the sibling. CACHE_PREFIXES drops the
two prefixes, since /save-result has nothing left to promote for them; the tts
and audio caches are untouched.
Also fixes the task runner output being unreadable in light mode. The panel is a
fixed dark terminal, but stdout lines were classed text-foreground, which follows
the app theme and renders black on the dark background. They now inherit the
pre's own colour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Transcribe Audio covers it as a task, with recursion, multi-select scoping,
skip-if-already-done and a choice of inline or job — none of which the one-shot
menu entry had. Two ways to do the same thing from the same menu is worse than
one good one.
The file viewer's Transcribe button is untouched, and /file-browser/transcribe
stays: it is what both that button and the task call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three changes that Transcribe Audio needs.
Whisper's translate mode only ever outputs English, so it cannot honour
"translate into <language>" for anything else — it answered Portuguese audio with
a rough English rendering instead of a transcript. The translate decision is gone
and transcription is always faithful to the detected language; spokenLanguages
now only breaks ties on clips Whisper is unsure about.
Script tasks get OFFICER_API_URL / OFFICER_API_HOST / OFFICER_AUTH_TOKEN so they
can call Officer's own endpoints rather than reimplementing server-side work.
Requests go to 127.0.0.1 so nothing depends on DNS or the proxy, but origin
validation matches Host against PUBLIC_URL, hence the separate host variable.
`inline` accepts "ask", which offers both affordances in the runner — Run here
streams into the modal, Run as job queues it. Useful when the same task can take
a second or an hour depending on whether it was pointed at a file or a library.
Existing true/false values behave exactly as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CATEGORY_ORDER hardcoded Video/Audio/Cleanup in the frontend, so adding a
category meant a code change. The order now lives in categories.yaml at the root
of the items store and reaches the client via GET /tasks/categories — the
platform no longer knows any category by name.
The endpoint is declared before /:name, which would otherwise match
"categories". Categories used by a task but absent from the file still work: they
sort alphabetically after the listed ones, and Other stays last.
Menus consume grouped tasks rather than grouping them per row. Groups are built
from the tasks and the file only ranks them, so a category listed with no
matching tasks cannot produce an empty submenu — locked in by tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every task declares a directory trigger, so right-clicking any folder listed all
fifteen at once — Tag Album offered on a folder of photos. TASK.md gains an
optional `category`, and the context menu nests by it: Run Task > Video > …
Nesting only kicks in when more than one category matches. A .mp4 matches eight
tasks that are all Video, so file menus stay flat rather than gaining a pointless
hop. Known categories lead (Video, Audio, Cleanup); anything else follows
alphabetically with Other last.
Applied to both menus — the right-click one and the ⋮ dropdown — which carried
identical blocks.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A fresh clone has neither: index.gen.html is gitignored and built from
PUBLIC_URL, and the database schema is applied with push rather than migrations.
Without both, setup finishes on a checkout that cannot serve a page or reach a
table.
Runs after .env is written, since both depend on it. Failures warn rather than
abort so the rest of the verification still reports.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
index.html hardcoded the deployment's domain in eight places, so every instance
had to carry its own edit of the file — the only thing separating the rezio
branch from master.
The tags genuinely need absolute URLs. OpenGraph is fetched standalone by
crawlers, and Bun's HTML bundler treats a root-relative href as an asset to
resolve on disk, failing the build with "Could not resolve: /favicon.ico" —
external URLs are the only form it passes through untouched.
Bun's HTML import offers no substitution hook, so scripts/gen-index.ts swaps
__PUBLIC_URL__ for the value in .env and writes index.gen.html, which the server
imports. index.html is the tracked template and is now identical on every
deployment; index.gen.html is gitignored. predev/prestart run the generator, and
it is idempotent so --watch does not loop.
Substituting also fixes the manifest: an absolute URL puts its fetch in CORS
mode, which failed whenever the hardcoded domain was not the serving origin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Language selection ranked only the user's spoken languages, so a user who speaks
one language always got that language. With spoken=["en"], Portuguese audio
detected at 0.968 pt was sent as language=en, and Whisper answered with a rough
English rendering rather than a transcript. `translated` stayed false, so nothing
downstream could tell a translation had happened, and detectedLanguage reported
"en" for audio that was not English.
Take Whisper's top language when it clears 0.5, and fall back to the spoken
languages only when the clip really is ambiguous — which is the case the bias was
written for.
Verified against the local whisper server with pt/en/es recordings:
spoken=["en"] pt -> translated English, es -> translated English, en -> as-is
spoken=["en","pt"] pt -> Portuguese transcript, no translation
spoken=[] pt -> Portuguese transcript
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
OpenAI-compatible TTS servers disagree on the /v1/audio/voices payload: some
return plain strings, Kokoro returns objects like { id, name }. The handler cast
the response to { voices: string[] } without checking, so the objects reached
the voice <Select>, which renders each entry directly — React error #31, and the
whole system settings page unmounted.
Flatten to ids at the boundary, preferring id then voice_id then name, and drop
entries that yield neither. An empty result now falls through to the HuggingFace
lookup instead of returning an empty list. The ElevenLabs branch goes through the
same helper so a shape change there cannot throw either.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Onboarding was dead in three layers:
- The OnboardingAdmin screen was only reachable from a route block in App.tsx
that has been commented out, so it never rendered. Its ServerTypeCard carried
accountMode ('organization' | 'single'), inherited from the codebase this was
based on and meaningless for a single-user platform.
- Two /onboarding-complete endpoints, one public and one protected, that no
frontend code called. Both read a server_config key that was never written, so
both answered false while the app's own path defaulted to true.
- HomeScreen gated on settings.onboarding.complete to show a welcome panel, and
seedHomeDir created an Onboarding folder from DATA_PATH/Onboarding and
/Onboarding_Admin — neither seed directory exists, so it only ever produced an
empty folder.
Also drops the onboarding key from UserSettings and the now-empty home-header
panel from the default home layout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The seven files in officer_db/migrations/ were never applied — this database has
only ever been managed with db:push, so there is no __drizzle_migrations table
and the numbered history had drifted from the real schema (0001 creates a
saved_sessions table the database does not have).
Deleted so there is one story about how the schema gets applied. db:gen can
write a fresh baseline from the current schema.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The old version described a repo that no longer exists: apps/dashboard and
apps/editor, a tracking server on port 5001, an ephemeral_db, and a dashboard
and API running as separate processes. None of that is true.
Replaces it with what the code actually does — one Bun process serving the SPA,
the API and eight WebSocket providers; sidecars for the privileged work; the
split between Postgres and the file-backed items store; and the single-user
invariant stated as an invariant rather than a migration in progress.
Also records two things that are easy to get wrong from reading alone: the
database is maintained with db:push and has never had a migration applied, and
agents run unsandboxed with permissions bypassed on purpose.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- DiscordAccount seeded DiscordStatus without its two nullable fields.
- bug-report typed reporter.name as string, but users.name is nullable; and the
Discord upload wrapped a Buffer directly in a Blob.
- Lucide icons take no `title` prop, so the sync spinner's tooltip moved to a
wrapping span.
- DesktopView cast its dynamic import to a type that included `| null`.
- dock PUT cast the request body straight to string[]; it now rejects anything
that is not an array of strings instead of writing it to the database.
- buildZodSchema assembles a mutable record, since z.ZodRawShape is readonly in
zod v4.
- The dev-server proxy forwards Bun's `string | Buffer` frames through a helper
that satisfies WebSocket.send without copying.
bunx tsgo is now clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every verb ended in `return data as T | any`, and `T | any` collapses to `any` —
so client.get<Foo>() handed back `any` and no annotation downstream meant
anything. That was the source of most of the implicit-any errors: the callbacks
had nothing to infer from.
Returning `as T` drops the whole class (19 errors to 9) rather than annotating
each parameter. Two calls in useAuth were relying on the looseness and now
declare their response shapes.
signin also no longer stores `undefined` as the bearer token when the server
withholds one; that path returns early.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three unrelated type errors that each pointed at something actually broken:
- Dashboard.CodeEditor was deleted in ab03b17 ("Projects") while the
/code-editor route and the dock's Editor item kept pointing at it, so the
route rendered undefined. Screen restored.
- Appearance.tsx imported 'themes', a workspace deleted in 0746844. Nothing
reads settings.appearance.colorTheme and no theme CSS survives, so the picker
was writing a value with no consumer. Removed it and the setting; colorMode
stays, it is live.
- officerdev exported both a component and a type named AppRegistry, so
`import { AppRegistry }` resolved to the type and <AppRegistry /> failed to
typecheck. Renamed the Record type to AppRegistryMap.
Also declares "*.css" so side-effect stylesheet imports resolve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
resolveUserPath and five sibling checks used startsWith(rootDir), which also
accepts a sibling directory whose name begins with the root's: from a root of
/home/br, "../br-backup/secret" resolves to /home/br-backup/secret and passed.
Compare against root + sep (or the root itself) via a shared isInside helper.
Verified the escape cases now deny while "", ".", and ordinary relative paths
still resolve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bodyParser caught parse failures and did nothing — the throw was commented out
and `body` was never set, so handlers destructured undefined and the client got
a 500 for a malformed request. Throw BAD_REQUEST instead.
Also adds the regression tests for the Host suffix match fixed in 2ca850c.
Verified against a running server: malformed JSON now 400, valid credentials
path still 401, spoofed Host still 403.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The no-Origin branch asked whether the configured origin ends with the
client-supplied Host, so `Host: dev` matched https://rezio.pastilhas.dev — as
did `pastilhas.dev` and `o.pastilhas.dev`. Match the URL authority exactly
instead. Officer always runs behind an HTTPS reverse proxy, so the forwarded
Host is expected to equal PUBLIC_URL's authority.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Origin validation, every rate limiter and the password-strength check each
treated an unset PUBLIC_BUILD_ENV as "relaxed", so a deployment that forgot the
variable silently ran with CORS reflecting any origin, no brute-force limit on
the sole account, and no password rules. setup.sh writes it, but .env.example
never mentioned it.
The three now share IS_DEV_BUILD, which is true only when PUBLIC_BUILD_ENV is
explicitly "dev" or "development". Anything else, including unset, is hardened.
Documented in .env.example.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The handler called onOpenChange and navigate, neither of which is in scope in
PipelineRunner — they belong to TaskRunnerModal, declared further down. Clicking
the button after a pipeline finished threw instead of navigating.
PipelineRunner now takes an onClose prop and calls useNavigate itself.
Co-Authored-By: Claude Opus 5 <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>
The Claude sidecar execs /usr/local/bin/claude (claude-manager.ts), but the
Anthropic installer only puts the CLI in ~/.local/bin — so on a fresh host that
path doesn't exist and claude chat fails with
"ENOENT … posix_spawn '/usr/local/bin/claude'". Symlink ~/.local/bin/claude →
/usr/local/bin/claude after install (idempotent; tracks Claude's self-updates).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The item store location wasn't written to .env, so a fresh server fell back to
<repo>/officer-items and booted with an empty store. Prompt for it (default: a
sibling of the repo) and write it to .env.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>