Commit Graph
240 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 4.8 459b8ab730 download job: restore the metadata phase — ReClip needs the title for filenames
Verified against live ReClip: POST /api/download with title:"" → a hash filename
(b5d04adc86.mp3); with title:"Me at the zoo" → "Me at the zoo.mp3". So ReClip
names the file from the title WE send (falling back to a hash) — it does not
self-name. The title is mandatory, which means a metadata pass is required.

Back to two phases:
1. metadata — fetch each item's /api/info (title + validity), keep survivors, skip
   errors.
2. download — download each survivor passing its title, so files land with real
   names; skip download errors.

Keeps the exact-urls[] input (Mix playlists can't drift) and the one-request-per-
item download. Progress is two counters again (Titles + Download); UI shows two
bars. ~2 requests/item is inherent to needing the title (per the user's call:
correctness over speed).

Verified two-phase filtering + title passthrough + skip-on-error with a mock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 19:17:27 +00:00
pastilhasandClaude Opus 4.8 7d149bd4d1 download job: use the panel's exact url list (no server re-expansion)
A YouTube Mix/radio playlist (list=RD…) returns a different set of items on every
/api/playlist call (observed 779 / 1485 / 529 for the same URL). The job used to
re-expand the playlist server-side, so it would download a different list than the
count shown on the decision screen.

The panel now passes the already-expanded `urls[]` into the job, and the executor
uses them verbatim (falling back to expanding `url` only when no list is given).
The job downloads exactly what you decided on. Endpoint takes `urls[]` (stored as
inputs.urls) or `url`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 18:44:45 +00:00
pastilhasandClaude Opus 4.8 c55ec5884b download job: collapse to one phase (no metadata prefetch)
The job's two phases were a misread — the "count" phase is the client-side
playlist expansion (for the inline-vs-job decision, already done in the panel).
The job itself is just one download request per item.

Dropped the in-job metadata pass entirely:
- reclip-client: reclipDownloadOne no longer prefetches /api/info for a title —
  ReClip names the file from the video title itself, so it's a single request
  per item.
- execute-download: one phase — expand the playlist, then /api/download each url,
  skip failures. Progress is a single { done, failed, total, current } counter
  (no meta/dl split); ~2× faster and downloads start right after expansion.
- UI (DownloadJobDetail + panel JobView): one "Downloaded" bar instead of two.

Verified: every item is attempted directly (no /api/info gate), skip-on-error
counts correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 18:35:10 +00:00
pastilhasandClaude Opus 4.8 bc1b799a27 jobs: download-job UI — panel decision screen + live progress + /jobs renderer
Front half of the download-job feature.

Panel (VideoDownloadPanel): after Fetch expands a playlist and the count is known,
a decision screen — "Found N items" → pick Audio/Video + subfolder → "Download all
as a job" (POST /jobs/download), or "fetch inline to pick individually" (the
existing card grid). A single video still goes straight to the inline card. The
job phase shows live two-phase progress (polled from the job) + a "View in Jobs"
link; it notes the job runs server-side so closing the panel is fine, and it
refreshes the browser as each file lands.

/jobs (DownloadJobDetail + JobsPage dispatch): a `download` job renders a compact
two-phase readout — Metadata and Download bars (processed/total, found/skipped and
saved/failed) + the current item — polled from the job's progress, with a Stop.

Executor tweak: phase-1 meta.done now counts kept (not processed) so both phases
read the same `(done+failed)/total`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 18:20:07 +00:00
pastilhasandClaude Opus 4.8 612fd18c41 video download panel: per-format actions (video/audio) + select mode
Reworks the download model around a format choice (video = ReClip video, audio =
audioOnly) instead of a global "extract audio" toggle:

- Input phase is just URL + Fetch (dropped the audio checkbox).
- Each card tracks video + audio download state independently and shows a button
  per format (Video only when ReClip reports video formats — audio-only sources
  get just Audio).
- Playlist top actions: "All video" (when any item has video), "All audio", and
  "Select".
- Select mode: each card shows Video/Audio checkboxes (pick one, both, or none per
  item); a "Start download (N)" button runs the chosen set and shows a fake
  progress bar (N/M count, no real byte progress). Bulk "All video/audio" reuse
  the same progress bar.
- Single item = large card with the two format buttons, no bulk row.

VideoInfo gains `formats` so the client can tell video-capable from audio-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 15:32:06 +00:00
pastilhasandClaude Opus 4.8 46b1d62138 video download panel: grid layout — numbered playlist cells + large single card
Uses the resizable panel + container queries instead of a cramped vertical list:
- Single item → one large card (big 16:9 thumbnail, title, uploader·duration,
  full-width download button).
- Playlist → a responsive grid (2 cols, 3 at @520px, 4 at @760px of panel width)
  of compact cells, each with a position number badge and an overlay download/
  status chip.
Loading skeletons, error, and per-cell download states (download → downloading →
saving → saved / retry) carry over.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 15:15:21 +00:00
pastilhasandClaude Opus 4.8 0a8ec845ef web: music dock joins the layout flow — removes the nav-dock overlap hacks
The music dock was position:fixed, overlaying the bottom of the content, so the
nav dock needed a pile of hacks to dodge it: a hand-measured MUSIC_DOCK_HEIGHT
(72) constant, a presence flag, a translateY lift, and a matching hover-threshold
lift (via a ref) so it wouldn't slide away under the cursor. Fragile the moment
the music dock's height changed.

Now the dock is an in-flow bottom bar that reserves its own height:
- DashboardLayout is a flex column: the content region (flex-1, min-h-0) shrinks
  when the music dock takes its space; MusicPlayerHost renders an in-flow bar
  (shrink-0) instead of a fixed overlay.
- The nav Dock is absolute within the content region and measures its reveal/hide
  boundary from that region's bottom edge (a boundaryRef) — so it always sits just
  above whatever's at the bottom, music dock or not, with zero knowledge of it.
- Deleted MUSIC_DOCK_HEIGHT, musicDockPresent, the lift, and the transform hack.

Bonus: content at the very bottom is no longer hidden under the fixed dock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 15:05:36 +00:00
pastilhasandClaude Opus 4.8 cc141ae91a web music: closing the dock clears the saved now-playing (so it stays closed)
The dock's X only reset local player state; the server's now-playing snapshot
survived, so a reload restored the dock. Closing now also DELETEs /music/now-playing
before clearing the queue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 14:56:40 +00:00
pastilhasandClaude Opus 4.8 0ab82964b9 file browser: video downloader as an ephemeral side panel (was a modal)
Moves the video-download UI from a modal into the same ephemeral side-panel slot
the file viewer uses (double-click a video), per request. It's driven by the
`download` search param (the target folder) + `downloadRoot`, exactly like the
existing view/play/chat ephemeral panels.

- layouts: singleDownloadLayout (files-download panel).
- VideoDownloadPanel (new, apps/FileBrowser): self-contained — reads the target
  folder/root from the params, does its own useFilesAPI, prefetch + per-entry /
  download-all flow (unchanged from the dialog), and bumps the shared
  `files:refresh-signal` so the browser re-lists when a file lands. Header shows
  a "Saving to <folder>" hint.
- useFileViewerPanels: register files-download (param → layout → panel + close),
  add download/downloadRoot to the on-refresh cleanup keys.
- useFileBrowserApp: replace the showVideoDownload modal state with
  openVideoDownload(), which sets the download param for the current folder+root.
- Toolbar + FileViewContainer trigger openVideoDownload(); drop the modal mount
  and delete VideoDownloadDialog.tsx.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 14:33:51 +00:00
pastilhasandClaude Opus 4.8 f18d19e7d2 file browser: video download — metadata prefetch + playlist handling
Reworks the download-video dialog into a prefetch-then-download flow, mirroring
ReClip's own web UI. Still a pure proxy to ReClip (its yt-dlp); no downloader
logic moves to the platform.

Server (thin ReClip proxies alongside /download-video):
- POST /file-browser/video-info { url }     → ReClip /api/info → { title, thumbnail, duration, uploader }
- POST /file-browser/video-playlist { url } → ReClip /api/playlist → { urls }
  Both return { error } inline (200) so the client can render failures per-card.

UI (VideoDownloadDialog, now self-contained; useFileBrowserApp exposes `files`
and drops the old single-shot state/handler):
- Paste a URL → Fetch. A playlist URL (list=) expands via /video-playlist, then
  each entry's /video-info is prefetched sequentially (ReClip does yt-dlp per
  video), rendering a card (thumbnail, title, uploader, duration) that fills in
  progressively.
- Per-entry Download, plus Download All when there's more than one; per-card
  status (downloading → saving → saved / retry-on-error) via the existing
  background job + poll.
- Playlists get an optional "subfolder you name" field (ReClip's /api/playlist
  carries no playlist title); blank = current folder.
- Quality is always best (matches the mobile Share flow — no picker); the
  audio-only toggle applies to the whole batch.

Verified ReClip's contract live: /api/info returns the metadata fields, and
/api/playlist returns { urls } (17 entries in ~1.2s).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 14:19:07 +00:00
pastilhasandClaude Opus 4.8 43a4372ba3 web /music: reindex now refreshes BOTH panels in place
The MusicBrowser and MusicDetail panels each kept their OWN local `manifest`
(and folder-listing) state, fetched in their own effects. The reindex (↻) button
lives in MusicBrowser and only refreshed its own copy — MusicDetail (the right
panel showing the tracklist/grid) never heard about it, so newly-indexed content
only appeared after navigating (which re-ran its effects).

Add a shared `music:resync` panel channel: when a reindex completes, MusicBrowser
bumps it to a fresh nonce, and both panels re-run their manifest/libraries/folder
fetches. In MusicDetail the fresh manifest object identity also re-triggers the
[cwd, manifest] listing effect, so the open album's meta/tracklist and any folder
grid refresh in place — no navigation required. Drops the browser's now-redundant
hand-refresh of its own state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 13:16:11 +00:00
pastilhasandClaude Opus 4.8 597675d6a0 fix: paused track resumed on any page click (unlock listener overrode pause)
The AudioContext autoplay-unlock listener (pointerdown → ctx.resume()) was
persistent, so every click anywhere resumed the context — including after a
deliberate pause (pause = ctx.suspend()). Result: pause, click back on the page,
and the track resumed from its position while React's `playing` stayed false, so
the button showed "paused" and it took two clicks to actually stop it.

The gesture only needs to unlock the context ONCE; sticky activation lets
engine.play() resume it thereafter. Make the listener { once: true } so it can't
fight an intentional pause.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 12:44:25 +00:00
pastilhasandClaude Opus 4.8 f0d475ea2e fix: gapless player wiped the queue on natural track end (stale closure)
The engine is created once (mount effect), so its onIndex/onEndOfQueue callbacks
captured the first-render syncIndex/setPlaying. useGlobal's setData reads the
`data` from the render that created the setter when given a functional updater —
that's the INITIAL empty state. So when a track ended and the engine called
syncIndex(i) → setState(s => ({...s, index:i})), `s` was {queue:[], index:0}:
the queue got wiped, `current` went undefined, playback stopped and the dock
vanished. Manual track selection was unaffected because playQueue passes a plain
object (no stale `data` read) — which is why it seemed to work.

Route the two engine-invoked setters through refs kept current each render, so a
natural advance mirrors into the live state instead of the stale initial one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 12:33:31 +00:00
pastilhasandClaude Opus 4.8 0ef8e9fbd2 web /music: sample-accurate gapless playback (Web Audio)
The player used a single <audio> element and advanced by swapping .src, which
re-fetches + re-buffers the next file — an audible gap between tracks. For a
continuous DJ mix (silence already trimmed at the edges) that's the whole
problem. Replace the <audio> element with a Web Audio engine that decodes each
track to an AudioBuffer and schedules the NEXT track's source to start() at the
exact AudioContext time the current track ends → sample-accurate, zero gap on
auto-advance.

- gapless-engine.ts (new): AudioContext + gain, LRU-capped decoded-buffer cache,
  fetch-whole-file → decodeAudioData, boundary scheduling, seek/skip/play-pause
  (pause = ctx.suspend so the clock + scheduled next freeze together), a
  generation counter to invalidate stale onended/async, and a gesture unlock for
  autoplay policy. Callbacks: onIndex/onTime/onEndOfQueue/onLoadingChange.
- MusicPlayerHost.tsx: drives the engine instead of an <audio> element. React
  keeps the queue/index (useMusicPlayer); user actions (new album, jump, prev/
  next) command the engine, and the engine's own natural advance mirrors back
  via syncIndex WITHOUT restarting playback (that's what keeps the seam gapless).
  Preserves restore/persist/heartbeat/album-nav/volume/heart; adds a decode
  spinner on the play button (startup/skip has fetch+decode latency by nature).
- useMusicPlayer.ts: syncIndex() — set index without touching `playing`.

Trade-off (chosen deliberately over near-gapless preloading): true gapless
needs the whole next file decoded to PCM ahead of time (~200MB per 10-min
track), so the buffer cache is capped at 3. Verified the scheduler state
machine with a mocked AudioContext: next track scheduled at the current's exact
end sample, advance/promote/seek/skip/end-of-queue all correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 12:18:09 +00:00
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 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 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 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 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 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 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
pastilhasandClaude Opus 4.8 3308e4e24d music: Music Player widget + app-wide player dock
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>
2026-07-26 10:29:14 +00:00
pastilhasandClaude Opus 4.8 8a1e8cd79e widgets: resync button on the panel
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>
2026-07-26 10:29:14 +00: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