Commit Graph
29 Commits
Author SHA1 Message Date
brunorezioandClaude Opus 5 f0a0166ecd vnc: only halve the stream when the framebuffer is actually large
-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>
2026-07-26 05:21:48 +01:00
brunorezioandClaude Opus 5 02662fc780 setup.sh: actually start the services, and verify they are running
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>
2026-07-26 05:19:27 +01:00
brunorezioandClaude Opus 5 7556c9ed00 fix /desktop: break the VNC password deadlock, drop the vncpasswd dependency
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>
2026-07-26 05:15:46 +01:00
brunorezioandClaude Opus 5 96dd1bfe9e disable Bun's fetch timeout when calling whisper
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>
2026-07-26 05:07:38 +01:00
brunorezioandClaude Opus 5 f08179606b replace the stale AGENTS.md with a pointer to CLAUDE.md
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>
2026-07-26 04:25:10 +01:00
brunorezioandClaude Opus 5 1192aa23fc docs: add the working-on-officer guide
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>
2026-07-26 04:19:46 +01:00
brunorezioandClaude Opus 5 b6b9e21b72 drop the built-in Extract Text (OCR) entry from the context menu
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>
2026-07-26 04:08:24 +01:00
brunorezioandClaude Opus 5 0c7c015fc3 drop the built-in Extract Audio entry, and its cache
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>
2026-07-26 03:50:47 +01:00
brunorezioandClaude Opus 5 d6b4b900ff stop caching transcriptions and OCR
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>
2026-07-26 03:23:30 +01:00
brunorezioandClaude Opus 5 1263a4f851 drop the built-in Transcribe entry from the file browser context menu
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>
2026-07-26 03:12:58 +01:00
brunorezioandClaude Opus 5 06aac5478f always transcribe in the source language, and let tasks call the API
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>
2026-07-26 03:03:49 +01:00
brunorezioandClaude Opus 5 5f7d574dec read the task category order from the items store
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>
2026-07-26 02:41:33 +01:00
brunorezioandClaude Opus 5 6a77ec22df group file-browser tasks into category submenus
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>
2026-07-26 02:26:48 +01:00
brunorezioandClaude Opus 5 1d0842cc01 setup.sh: generate index.gen.html and apply the schema
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>
2026-07-26 01:49:17 +01:00
brunorezioandClaude Opus 5 52ddf7df0e generate index.html's absolute URLs from PUBLIC_URL
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>
2026-07-26 01:49:17 +01:00
brunorezioandClaude Opus 5 7ef0e90cc4 let Whisper's language detection win when it is confident
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>
2026-07-26 01:49:04 +01:00
brunorezioandClaude Opus 5 c4033e5392 normalize TTS voice lists to strings
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>
2026-07-25 23:36:36 +01:00
brunorezioandClaude Opus 5 1ac79f9c68 remove the onboarding flow and the accountMode leftover
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>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 10400310c5 drop the unapplied migration history
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>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 0e71d87f75 rewrite CLAUDE.md against the current codebase
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>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 0d67e2af26 clear the remaining type errors
- 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>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 0041fcbd47 make useClient actually return its type parameter
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>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 1fa3a659bc restore the code editor screen, drop the dead theme picker, unshadow AppRegistry
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>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 78130f21ce require a separator when checking a path is inside its root
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>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 c13875cef8 return 400 for an unparseable body, and cover origin validation
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>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 1eb8cfd273 compare the Host header against the origin authority, not as a suffix
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>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 8163f04420 fail closed when PUBLIC_BUILD_ENV is unset
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>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 85596da086 fix ReferenceError in the pipeline "View in Jobs" button
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>
2026-07-25 23:30:20 +01:00
brunorezioandClaude Opus 5 044aacf4d5 remove the dead multi-user surface
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>
2026-07-25 23:30:20 +01:00