Commit Graph
627 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 4.8 11263ce120 task modal: audio info panel for Get Lyrics (title/artist/length/lyrics)
Display-only panel at the top of the Get Lyrics run-task form (single file only):

- Backend: GET /file-browser/audio-meta?path= — ffprobe format tags + duration,
  plus a second probe for embedded lyrics (USLT/SYLT/lyrics* keys, case-insensitive).
  Returns { title, artist, duration, hasLyrics }; tolerant of missing tags/probe
  failures.
- Client: files.audioMeta(path) + AudioMeta type in useFilesAPI.
- TaskRunnerModal: prefetch audioMeta for get-lyrics single-file runs (bypasses
  the hasTrackPickers early-return, error-tolerant), and render AudioMetaPanel
  above TaskInputForm — title/artist + a muted length + Lyrics: Yes/No chip,
  filename fallback. Directories + other tasks unaffected (no panel, no probing).

Verified ffprobe logic on a real embedded-lyrics file. tsgo clean; formatted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 17:07:18 +00:00
pastilhasandClaude Opus 4.8 55bf3aecca music: recursive watcher → localized reindex on any change
Extract walk()'s per-folder logic into a reusable buildFolder(), so a single
album/artist can be (re)indexed on its own. Add:

- reindexFolder(rel): rebuild just one folder's live cache entry + patch the
  manifest (prune if the folder vanished). Its mtime-based signature means any
  change — add / re-tag / delete — is picked up, and irrelevant touches no-op.
- withIndexLock: serialize ALL index mutations (full / incremental / localized)
  so a localized reindex can never race the full reindex's atomic swap.
- watcher.ts: fs.watch(~/Music, { recursive }) → log every change → debounce 3s →
  reindexFolder the affected folder(s). Verified: Bun's recursive watch fires
  through the ~/Music symlink and on new-dir creation (so a new album's contents
  are read by reindexFolder); inotify max_user_watches (~483k) >> folder count.

Started at boot, stopped on shutdown. The nightly full reindex backstops any
change that lands after a new folder's debounce.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 16:41:16 +00:00
pastilhasandClaude Opus 4.8 1f21768c4c music: nightly 3am full reindex (staged + atomic swap)
Self-scheduling timer in the sidecar (fresh setTimeout each night, so it always
fires at 3am local regardless of drift) runs reindexFull() — builds into a fresh
slot and swaps atomically only on success, never disrupting the live index.
Started at boot, cleared on shutdown. Logs the next scheduled time + each run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 16:30:42 +00:00
pastilhasandClaude Opus 4.8 b1d1c91968 music: staging-slot cache with atomic symlink swap for full reindex
Foundation for a safe from-scratch reindex. The live cache path is now a SYMLINK
to a slot dir; readers + incremental writes follow it.

- ensureCacheSetup() (run at sidecar boot): makes `cache` a symlink to a slot,
  migrating an existing real cache dir once (a fast rename, not a copy).
- runBuild(outRoot, prev): the build now writes to a given root and returns the
  manifest without touching live serving state.
- reindexNow(): incremental, in-place live build (manual + localized updates).
- reindexFull(): builds a complete index into a FRESH slot without touching the
  live one, then activateSlot() swaps the symlink atomically (rename-over) ONLY
  on success — a failed rebuild leaves the live index untouched; old slots pruned.

walk() takes the output root. Verified end-to-end: fresh setup, full build +
swap + prune, incremental-through-symlink, and the one-time real-dir migration
(content preserved). Restart officer-music to apply (triggers the migration).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 16:29:41 +00:00
pastilhasandClaude Opus 4.8 b84f0b0b18 music: /manifest is a pure read — never triggers a (re)build
Reading the manifest (which every app refresh hits) used to call
ensureIndexFresh(), kicking off a debounced rebuild — and after the CACHE_VERSION
bump that meant a plain refresh could launch a full library rebuild. Make reads
side-effect-free: /manifest now just returns the last completed index. Builds are
explicit only (POST /reindex or the SSE stream); pick up disk changes by
reindexing.

Removes the now-unused ensureIndexFresh + lastBuildFinishedAt, and drops
/manifest from the 30-min per-request timeout extension (both hops) since it no
longer blocks on a build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 16:07:39 +00:00
pastilhasandClaude Opus 4.8 48d24544f3 task modal: add multi-line 'text' input type (textarea)
`type: string` renders a single-line <input>; the new `type: text` renders a
resizable multi-line <textarea> (6 rows) — for pasted multi-line values like
lyrics. Checked before options/default so `text` always means free-form text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:54:27 +00:00
pastilhasandClaude Opus 4.8 3de22a3166 rescan: refetch inactive item lists too (works from any page)
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>
2026-07-27 15:47:46 +00:00
pastilhasandClaude Opus 4.8 de0642766c music: 30-min per-request idle timeout for reindex/manifest (from-scratch builds)
A from-scratch rebuild holds the triggering request open for many minutes with
no bytes flowing, so both server hops' idle timeouts would drop it. Bun caps the
server-level idleTimeout at 255s, but server.timeout(req, seconds) allows more
per-request:
- sidecar Bun.serve: extend /reindex, /manifest, /reindex/stream to 1800s.
- platform proxy (music router): same, via the Bun server exposed as Hono's env.

Baseline idleTimeouts unchanged (sidecar 255, main server 60). The build always
completed in the background regardless; this keeps the request itself alive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:23:27 +00:00
pastilhasandClaude Opus 4.8 43135865e2 music indexer: 3s progress heartbeat log during a build
A long full rebuild logged only "resync started" then nothing until "done".
Add a console heartbeat (throttled to 3s, piggybacked on emitProgress) showing
folders/built/skipped/tracks/videos/posters/lyrics counts + the current path, so
progress is visible in the pm2 logs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:15:35 +00:00
pastilhasandClaude Opus 4.8 219f93a831 music sidecar: raise Bun.serve idleTimeout (blocking reindex outran 10s default)
The one-time v1→v2 full rebuild (and long range/SSE reads) take far longer than
Bun.serve's default 10s request idle-timeout, which dropped the triggering
request mid-flight ("request timed out after 10 seconds"). Set idleTimeout: 255
(Bun's max). A build that still outruns it completes in the background regardless
— a closed socket doesn't cancel the in-flight build promise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:08:26 +00:00
pastilhasandClaude Opus 4.8 f408e4dd6f music indexer: pick video poster via ffmpeg thumbnail filter (dodge black frames)
The fixed ~10% frame grab could land on a black/near-black frame for clips that
fade in from black (or on a title card). Keep the ~10% seek to skip intros, but
select the frame with `thumbnail=n=300` — ffmpeg picks the most representative
frame from the batch, which avoids uniform/black frames. Verified on a
fade-from-black video: luma ~122 (vs 0 at t=0), and it dodges the fade even when
seeking from the start.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:05:14 +00:00
pastilhasandClaude Opus 4.8 4d3c17b2df music indexer: cache-format version → full rebuild for posters/lyrics migration
Videos/tracks indexed before posters+lyrics existed were skipped by the per-album
`v` check (unchanged v → skip), so their posters/ and lyrics/ never generated —
a plain reindex couldn't fix it.

Add CACHE_VERSION (now 2). The `v` skip is only trusted when the on-disk
manifest is already at the current format; an older version forces a one-time
FULL rebuild that regenerates every album (incl. the new posters/lyrics), then
writes version:2 so subsequent builds skip normally. Verified: old-cache rebuild
regenerates the poster, next build skips (no loop).

Deploy = restart officer-music, then one reindex (a full rebuild, slower than an
incremental — one time only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 14:59:08 +00:00
pastilhasandClaude Opus 4.8 b3a4da4b97 music indexer: index track lyrics (.lrc/.txt sidecars + embedded)
Each track's lyrics are resolved and cached at cache/<rel>/lyrics/<file>.<lrc|txt>,
with the format recorded as `lyrics: 'lrc'|'txt'` on the meta.tracks entry.

Precedence: external "<base>.lrc" > external "<base>.txt" > embedded tag
(lyrics / lyrics-<lang> / unsyncedlyrics — ffprobe now reads all format tags).
Content that contains [mm:ss] lines is stored as lrc even from a .txt/embedded
source. Only track-matching sidecars affect the version signature (a stray
notes.txt is ignored). Lyrics dir is wiped+regenerated per rebuild; new
`lyricsIndexed` counter.

Served by GET /api/music/lyrics?path=<rel>&file=<track> (text/plain +
X-Lyrics-Format header, ETag=<v>, 304, 404 when none).

Verified end-to-end: external .lrc wins over embedded; embedded → plain txt;
unmatched .txt ignored. MUSIC_API.md documents the field + endpoint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 14:51:58 +00:00
pastilhasandClaude Opus 4.8 10d7b6a425 music indexer: generate video poster thumbnails
Each indexed video gets a compressed poster (a frame grab ~10% in, capped at
30s, scaled ≤600px q5 like covers), written to cache/<rel>/posters/<file>.jpg
and recorded as `poster` on the meta.videos entry. The posters dir is wiped and
regenerated on each rebuild so orphans (removed videos) don't linger. New
`postersSaved` status counter.

Served by a new sidecar route GET /api/music/poster?path=<rel>&file=<video>
(image/jpeg, ETag=<v>, 304, 404 when none) — path-safe via basename.

Verified end-to-end on a real .mp4: video-only album → manifest {tracks:0,
videos:1}, meta.poster set, 14 KB poster on disk. MUSIC_API.md documents the
poster field + endpoint + postersSaved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 14:38:15 +00:00
pastilhasandClaude Opus 4.8 d5b3dbb473 music indexer: index videos (concerts/clips) in artist/album dirs
Videos (all phone-compatible .mp4, plus common containers) that live in an
artist or album folder are now indexed alongside audio:

- Move 'mp4' out of AUDIO_EXT into a new VIDEO_EXT (mp4/m4v/mkv/mov/webm/avi) —
  it was wrongly treated as an audio track before.
- ffprobeVideo captures file/title/durationSec/width/height per video.
- meta.json gains an optional `videos: IndexVideo[]`; a folder with only videos
  now still gets a meta.json. Manifest entries gain optional `videos: N`.
- Video files join the album version signature (changes bump `v` for resync).
- New `videosIndexed` status counter + resync-log line.

Location is inherent in the folder rel (always an artist/album dir), so no
extra location field is needed. MUSIC_API.md documents the videos field +
manifest count. No poster/thumbnail generation yet (folder cover is reused).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 14:16:45 +00:00
pastilhasandClaude Opus 4.8 d317bfc983 dock: lift the nav-dock hide threshold when the music dock is present
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>
2026-07-27 13:56:51 +00:00
pastilhasandClaude Opus 4.8 ee6c7b0fdd music (web): Favorites view (inspired by the app's FavoritesScreen)
A heart button in the left panel header opens a Favorites view in the right
panel (coordinated via a new music:favorites panel channel). Grouped
Artists / Albums / Tracks, each row: cover thumb (indexed cover, icon fallback)
+ title/subtitle + a heart to un-favorite. Click an album/artist to navigate
the library there; click a track to play it in album context. Navigating
anywhere closes the view (cwd-change effect). Empty state prompts to heart
something.

useMusicFavorites now also returns the grouped `favorites`; shared gains the
channel + parseAlbumName.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 13:40:33 +00:00
pastilhasandClaude Opus 4.8 f887fb4679 music (web): cursor-pointer on dock controls (Tailwind v4 no longer defaults it)
Add cursor-pointer to the player dock's buttons — track-info, prev/next (with
disabled:cursor-default), play/pause, mute, and close — and to MusicHeart (so
every heart across the player/tracklists gets it). The volume slider and SeekBar
already had it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 12:58:59 +00:00
pastilhasandClaude Opus 4.8 3f2efbf852 music (web): click the dock track info to open its album in /music
The bottom player dock's cover + title/artist is now a button — clicking it sets
the music:cwd channel to the playing album and navigates to /music, landing on
that album's tracklist (current track highlighted). Uses the global panel
channel + react-router navigate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 12:55:53 +00:00
pastilhasandClaude Opus 4.8 5b17121de8 music (web): browser filter + reindex; richer album tracklist
Left panel (MusicBrowser):
- Fuzzy filter input (case-insensitive subsequence match) over the current
  library/folder list; resets on navigation, with a clear button + "No matches".
- Reindex button (spins while running) → POST /music/reindex, then refreshes the
  manifest and current listing.

Right panel album view (MusicDetail):
- Current track clearly highlighted: primary tint background + a Volume2 marker
  replacing the track number + medium weight.
- Every track shows its artist (falling back to album artist) under the title,
  and its duration on the right. Web Track type gains albumArtist + durationSec;
  fmtDuration/fuzzyMatch added to shared.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 12:52:34 +00:00
pastilhasandClaude Opus 4.8 3924155bfb music (web): larger, spaced browser rows with cover thumbs; hide dotfiles
Left panel (MusicBrowser) polish + a shared fix:
- Larger row text (text-base; "Music" heading text-lg).
- More space between rows (gap-1.5 + py-2).
- Leading thumbnail is the folder's indexed cover (its folder.jpg/cover.jpg,
  server-compressed) with a Folder/Library icon fallback when there's none or
  the image fails.
- Hide hidden files/folders (dotfiles like .claude) in the listing — applied to
  MusicBrowser and the MusicDetail libraries/grid so neither panel shows them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 12:38:09 +00:00
pastilhasandClaude Opus 4.8 0995aaa8af music (web): resume currently-playing on reload/return
The web player kept no durable state, so a reload (and a return to /music,
whose cwd resets) lost your place. Wire it to the platform's per-user
/api/music/now-playing (same endpoints the app uses):

- MusicPlayerHost persists a snapshot (track + position) on play/pause + track
  change + a 10s heartbeat (position read via ref so the heartbeat stays live).
- On first load with an empty queue it restores that snapshot: rebuilds the
  album queue (sortTracks), loads it PAUSED (browsers block autoplay on reload),
  and seeks to the saved position once metadata is in. A restore guard stops the
  load from clobbering the saved position with 0.
- MusicDetail auto-opens the currently-playing album once on mount (when it has
  no location yet), so /music lands on the track — without yanking you back
  after you navigate away.

Adds loadQueue() (paused) to useMusicPlayer + a NowPlaying type. tsgo clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 12:24:47 +00:00
pastilhasandClaude Opus 4.8 759dcb8b5b music (web): artist heart on non-discography artist pages too
The artist heart only rendered in the discography view (disco && !album), which
needs a _discography.md — so artists without one (plain grid of album folders)
had no heart. Add an artist header + heart to the grid view when on an artist
folder (crumbs>=2), matching the app's inArtist rule. favKey = artist music-rel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 12:17:17 +00:00
pastilhasandClaude Opus 4.8 5af3119096 music (web): Favorites hearts in the /music player
Wires the web player to the platform's per-user /api/music/favorites (same
endpoints the app uses). New useMusicFavorites hook (react-query, shared
optimistic cache) + a reusable MusicHeart toggle, placed at:
- album header (album) + each track row (track, reveals on row hover, filled
  favorites stay shown)
- album cards in the artist/grid views (album)
- artist discography header (artist)
- the now-playing player bar (current track)

Track key = homePath "Music/<rel>/<file>"; album/artist keys = music-rel.
No dedicated Favorites browsing view yet (hearts only), matching the app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 11:58:02 +00:00
pastilhasandClaude Opus 4.8 6fc34363c8 music (web): order album tracks by track number, not readdir order
meta.json is written in ffprobe/readdir order (arbitrary), and the web player
rendered/queued it as-is — so albums like Andrew Bird's "The Mysterious
Production of Eggs" showed scrambled (11, 7, 5, 14, 1, …). The web Track type
didn't even carry the `track` tag.

Add `track` to the Track type + a shared sortTracks(): by track NUMBER (parsed
from the "n/total" tag), falling back to tag title only for tracks without a
number. Applied to the rendered tracklist (setAlbum) and both play queues.
Verified it reorders that album to 1→14.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 11:50:34 +00:00
pastilhasandClaude Opus 4.8 21cb3489a7 music: per-user Favorites + Currently-playing (platform/Postgres)
User-level state for the music app, served by the platform from Postgres (not
the sidecar, which is stateless about users) under the same /api/music prefix
so the music-app account gate permits it:

- music_favorites (userId, kind, key) — kind ∈ track|album|artist, opaque path
  key the server never interprets; unique per (user,kind,key), newest-first.
  GET /favorites (grouped), POST /favorites (idempotent), DELETE /favorites.
- music_now_playing (one row/user) — current track + position snapshot for
  resume-across-launch/device, with a light title/artist/album cache so the
  resume card renders before the library index syncs.
  GET/PUT/DELETE /now-playing (upsert).

Routes registered before the catch-all proxy. Tables created via direct DDL;
query layer smoke-tested against the live DB. MUSIC_API.md documents the
contract for the app. App-side wiring (heart toggles, Favorites view, player
persistence) follows next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 11:28:28 +00:00
pastilhasandClaude Opus 4.8 54fa21dd46 workspace: panel-level responsiveness via CSS container queries
Panels are now container contexts (@container → container-type: inline-size)
on both the normal and transparent PanelSlot render paths, so panel content
can respond to the *panel's* width instead of only the viewport's. Non-
breaking: existing screens keep their viewport (lg:/md:) variants untouched.

First adopter: the bTop system-monitor grid reflows on panel drag —
grid-cols-1 → 2 at @min-[600px] → 3 at @min-[960px] (panel width), keeping
cards ≥300px so they no longer squeeze on a narrow panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 11:16:17 +00:00
pastilhasandClaude Opus 4.8 de8f70562b docs: SYSTEM_MONITOR_API.md contract for the app + drop dead Pm2Logs.tsx
Full /api/system-monitor/* contract (stats snapshot incl. cpu/mem/disks/temp/
gpu/net/power, pm2, docker, and the two SSE log streams) with response shapes,
auth (Bearer or ?token=), owner-only note, and the net/power rate caveats — so
the app can implement the same views. Also removes the orphaned Pm2Logs.tsx
(superseded by LogStream) that a prior commit left tracked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 11:03:10 +00:00
pastilhasandClaude Opus 4.8 f9752d2868 system-monitor: GPU, Network, and Power cards in bTop
Three more same-level grid cards (bTop now 7 cards; Temperature stays under
Memory via natural 3-col flow):
- GPU: gpu_busy_percent + VRAM used/total from /sys/class/drm (instant).
- Network: ↓/↑ throughput (bytes/sec) from /proc/net/dev deltas between /stats
  calls, aggregate + top interfaces.
- Power: CPU package watts via RAPL energy delta + GPU watts (amdgpu hwmon).

Note: RAPL energy_uj is root-only by default (Spectre-era lockdown), so CPU
package power shows "—" unless made readable (a udev rule); GPU watts work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:59:27 +00:00
pastilhasandClaude Opus 4.8 75a978d2e2 system-monitor: CPU temperature in bTop (Temperature card under Memory)
- Backend: readTemps() scans /sys/class/hwmon for all temp sensors and picks the
  CPU one (k10temp/coretemp/zenpower Tctl/Tdie/Package); added to /stats.temp.
- BtopView: a Temperature card at lg:col-start-2 (under Memory, second row) —
  big CPU °C (color-graded), its sensor label, and the other sensors (GPU, NVMe,
  wifi…) listed beneath.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:51:54 +00:00
pastilhasandClaude Opus 4.8 41ebd73c2d system-monitor: docker grid view + name/image filter
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:45:36 +00:00
pastilhasandClaude Opus 4.8 f3108f6d83 system-monitor: black log pane + live docker container logs
- Shared LogStream component (solid-black <pre> terminal pane) replaces Pm2Logs;
  pm2 and docker both drill into it.
- GET /api/system-monitor/docker/logs?id=<container>&lines=<n> — SSE of
  `docker logs -f` (combined stdout+stderr); id charset-validated + spawn arg.
- DockerView: container cards are now clickable → live logs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:42:02 +00:00
pastilhasandClaude Opus 4.8 67e6646bca system-monitor: live pm2 log streaming — click a process name to tail its logs
- GET /api/system-monitor/pm2/logs?id=<pm_id>&lines=<n> — SSE that spawns
  `pm2 logs <id> --raw` (combined out+err, follows live) and streams each line.
  id validated numeric + passed as a spawn arg (no shell); killed on disconnect.
- Pm2View: clicking a process name drills into Pm2Logs (EventSource tail with a
  live pulse + back button); autoscrolls, capped at ~1200 lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:32:53 +00:00
pastilhasandClaude Opus 4.8 7f91c4f9bf system-monitor: sortable columns in the pm2 view (click headers; asc/desc)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:25:37 +00:00
pastilhasandClaude Opus 4.8 1b7a5feeea system-monitor: show pm2 id (#) column in the pm2 processes view
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:24:20 +00:00
pastilhasandClaude Opus 4.8 059929aa59 system-monitor: Workspace/Panel layout with a scope list (bTop / pm2 / dockers)
/system-monitor is now a WorkspaceView (like /music): a left ScopeList panel
selects the scope over the 'monitor:scope' channel, the right MonitorMain panel
renders it. Three scopes:
- bTop  — the existing system snapshot (CPU/mem/disks/top processes)
- pm2 processes — new GET /api/system-monitor/pm2 (pm2 jlist → name/status/cpu/
  mem/restarts/uptime table)
- dockers — new GET /api/system-monitor/docker (docker ps → container cards with
  state/status/image/ports)
Both new endpoints degrade gracefully to an error field. Persisted as
screens/system-monitor; owner-only.

Needs a restart (backend routes + rebundle) + hard-refresh to appear.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:17:15 +00:00
pastilhasandClaude Opus 4.8 621f93b884 chat: don't idle-GC a session while background tasks are still running
Phase 1 gap surfaced by a long overnight job: armIdle fired 30 min after the
last turn regardless of in-flight background work, so a silent run_in_background
job outliving the timeout got its persistent SDK session aborted — killing the
harness that delivers its task_notification (and any detached watcher's hook).

Fix = task-lifecycle heartbeat: track task:started → task:notification per
session; suppress/re-arm the idle timer while any task is pending. task:started
also clears a pending idle timer. So long run_in_background jobs keep their own
session alive and their completion is delivered; idle-GC resumes only once all
tasks finish and the session is truly idle. Pairs with the Activity path-tail
(35973a5) for the pure-setsid case.

Not deployed (no restart — long job still running); lands with the Activity
batch on next restart.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 09:34:59 +00:00
pastilhasandClaude Opus 4.8 35973a5505 activity: follow the agent's background work live + chat-event retention
"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>
2026-07-27 00:56:10 +00:00
pastilhasandClaude Opus 4.8 6b3eb247a3 chat: durable Postgres event queue + cursor replay on reconnect (Phase 2+3)
Completes the turn/session decoupling so nothing is lost across disconnects.

Phase 2 (durability):
- New chat_session_events table (global monotonic id = cursor) + queries
  appendChatEvent / getChatEventsSince / pruneChatEventsOlderThan.
- Every durable outbound ServerMessage now goes through emitToSession: appended
  to the queue (even while the client is disconnected) and delivered live with
  its seq. Streaming deltas stay ephemeral (live-only, never persisted).

Phase 3 (resilient transport):
- New 'resume-cursor' client message → handleResumeCursor re-binds the socket to
  the (still-live) session (cancels idle-GC via attachWs) and replays every event
  since the client's cursor.
- useChatWebSocket already auto-reconnects; added an onOpen hook. useChat tracks
  the max seq and, on every (re)connect with an established session, sends
  resume-cursor — so a dropped connection self-heals with no manual navigate
  away/back, and background task notifications that landed while offline replay.

Verified end-to-end: disconnect after a turn's result but before a background
task finishes, reconnect with the cursor → the missed task:notification is
replayed from Postgres, no duplicates.

Note: the DB is managed via drizzle push/direct DDL (no __drizzle_migrations
table), so 0001 was applied directly; the generated migration is committed for
the record.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 00:12:46 +00:00
pastilhasandClaude Opus 4.8 449f28b1e5 chat: persistent Agent SDK session per chat — decouple worker from turn (Phase 1)
Root fix for orphaned background tasks: the platform drove Claude Code as a
one-shot `claude -p` per turn (stdin ignored, process exits at turn end), so
run_in_background/Monitor work — and its task_notification — had no live harness
to return to. Now each chat session runs ONE long-lived Agent SDK query() with
streaming input; turns are user messages pushed onto it, and the session stays
warm between turns.

- claude-manager: persistent `query({ prompt: AsyncIterable, options })` per
  sessionKey (bypassPermissions, --resume, mcp via extraArgs, CLAUDECODE stripped).
  Single consumer loop maps every SDK message → ChatEvent, incl. post-turn
  task_started / task_notification. interrupt() = stop-turn; abort() = kill-session;
  30-min idle GC.
- stream-parser: processMessage() (object-level, reused by the SDK loop) + task
  message handling. ChatEvent/ServerMessage gain task:started / task:notification.
- API: the sidecar event subscription is now SESSION-scoped (no longer unsubscribes
  on 'result'), so background events after turn-end still reach the client. First
  turn opens the session; later turns push onto it. handleStop → interrupt (keeps
  session warm); disconnect/deleteSession → kill.
- protocol/sidecar-registry/user-instance: claude:interrupt command + interruptClaude.
- client: render task:started / task:notification in the transcript.

Verified end-to-end through the real chat WS: a run_in_background task's completion
arrives ~6s AFTER the turn's result; multi-turn on one warm session works.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 00:00:11 +00:00
pastilhasandClaude Opus 4.8 b9d539c1bd chat: "Disconnect session" — end the whole session from the UI (first stab)
Adds a per-session teardown, distinct from the existing turn-only "stop":
- New WS 'disconnect' message → handleDisconnect → sessionManager.deleteSession,
  which fires _claudeKill (kills any in-flight Claude/OpenCode turn) + _sidecarUnsub,
  clears the idle timer, and drops the in-memory session. WS stays open so a new
  prompt starts fresh. Server acks with 'disconnected'.
- useChat: disconnectSession() + a 'disconnected' handler (commit partial stream,
  settle to idle).
- UI: an Unplug button in the chat DetailBar (shown while connected).

Scope note: targets the CURRENTLY-OPEN session (correct in-memory sessionKey).
Disconnecting an arbitrary *listed* session isn't wired yet — session-list rows are
keyed by the on-disk transcript uuid, which isn't the live sessionKey, so that needs
a reverse lookup + a REST endpoint. NOT yet deployed (needs a server restart).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 23:22:53 +00:00
pastilhasandClaude Opus 4.8 bff11bc86d music: fresh-on-refresh reindex, resync logs, and stop the junk-cover rebuild loop
- 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>
2026-07-26 23:13:17 +00:00
pastilhasandClaude Opus 4.8 d1e19d1473 system-monitor: /system-monitor route (CPU/mem/disk/processes) + page titles
- 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>
2026-07-26 19:23:32 +00:00
pastilhasandClaude Opus 4.8 1d441e3aa1 auth: confine non-owner accounts to the music app (account + origin gates)
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>
2026-07-26 18:38:29 +00:00
pastilhasandClaude Opus 4.8 b40464632c auth: origin-scope the music app to /api/auth + /api/music
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>
2026-07-26 18:14:08 +00:00
brunorezioandClaude Opus 5 f4054193ae transcription: tune whisper for single-speaker audio, and paragraph the output
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>
2026-07-26 17:26:42 +01:00
pastilhasandClaude Opus 4.8 e46b28e14e music: left panel is a drill-down list navigator (never a grid)
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>
2026-07-26 13:05:30 +00:00
pastilhasandClaude Opus 4.8 6fab49eddc music: /music uses the Workspace/Panel system (2 vertical panels)
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>
2026-07-26 12:59:56 +00:00
pastilhasandClaude Opus 4.8 c3ed9158b3 music: /music route + nav dock item (Spotify-style page, v1)
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>
2026-07-26 12:49:05 +00:00
pastilhasandClaude Opus 4.8 219cfba7ce music: widget becomes a library/folder browser (tabs + Artist/Album/Song)
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>
2026-07-26 10:32:26 +00:00