85 Commits
Author SHA1 Message Date
Giancarmine SalucciandClaude Sonnet 4.6 cd06ee1909 fix(baseline): postinstall rebuild better-sqlite3 to fix Node ABI mismatch
Build & Push Docker Image / test (push) Failing after 4s
Build & Push Docker Image / build-and-push (push) Has been skipped
Node upgraded to v24 (ABI 137); persistent baseline worktree held binary
compiled for Node 22 (ABI 127). npm install does not re-run native build
for unchanged packages, so adding postinstall forces rebuild on every
npm install, keeping the binary current with the running Node ABI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-06 18:26:36 +00:00
Giancarmine Salucci f64487b482 story story-d5-vtt-fallback-y-d1d6 2026-08-06 18:21:28 +00:00
Giancarmine SalucciandClaude Haiku 4.5 1fc1382414 feat: VTT fallback to whisper with transcript_source tracking
- Set transcript_source to 'captions' when using VTT captions (caption fast path)
- Set transcript_source to 'whisper' when whisper transcription completes via webhook
- Add comprehensive test suite for VTT fallback scenarios
- Tests verify empty/whitespace captions trigger whisper, non-empty captions skip whisper
- Persists transcript_source to database for audit trail

Closes story-d5-vtt-fallback-y-d1d6

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-06 17:24:31 +00:00
Giancarmine SalucciandClaude Sonnet 4.6 12be477109 fix(baseline): add better-sqlite3@12.11.1 to allowScripts so native rebuild runs
Build & Push Docker Image / test (push) Failing after 4s
Build & Push Docker Image / build-and-push (push) Has been skipped
Dependency was bumped to ^12.11.1 but allowScripts only permitted @12.9.0,
blocking the install script. npm rebuild silently skipped recompiling the
.node binary, leaving NODE_MODULE_VERSION 127 (Node 22) against a Node 24
runtime (NMV 137) — causing ERR_DLOPEN_FAILED during the SvelteKit postbuild
analyse step and build exit 1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-06 16:59:45 +00:00
Giancarmine SalucciandClaude Sonnet 4.6 898e193f2a fix(baseline): upgrade better-sqlite3 to 12.11.1 for Node 22.23.2 ABI compatibility
Build & Push Docker Image / test (push) Failing after 4s
Build & Push Docker Image / build-and-push (push) Has been skipped
Native addon was compiled against NODE_MODULE_VERSION 137; Node 22.23.2
requires 127. Upgrading to 12.11.1 downloads the correct prebuilt binary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-06 16:58:09 +00:00
Giancarmine SalucciandClaude Sonnet 4.6 9d88162bf0 fix(baseline): guard db init from build-time mkdir failure + skip remux tests when ffmpeg absent
Build & Push Docker Image / test (push) Failing after 4s
Build & Push Docker Image / build-and-push (push) Has been skipped
- db.ts: wrap mkdirSync+Database init in IIFE try/catch; fall back to
  :memory: during SvelteKit postbuild analyse (runs server chunks in a
  HOME-less env where mkdir /home/sh/.whisper-pwa is EACCES)
- remux.integration.test.ts: skipIf(!ffmpegAvailable) so tests are skipped
  instead of failing with ENOENT when ffmpeg not installed
- npm rebuild better-sqlite3 to fix NODE_MODULE_VERSION mismatch (137 vs 127)
  that caused data-dir vi.resetModules() test to fail on fresh re-import

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-06 16:44:07 +00:00
Giancarmine Salucci 8a620485bf story story-foundation-schema
Build & Push Docker Image / test (push) Failing after 4s
Build & Push Docker Image / build-and-push (push) Has been skipped
2026-08-06 16:35:05 +00:00
Giancarmine SalucciandClaude Haiku 4.5 c8471b787e feat: Add transcript_source column and type to foundation schema
- Add transcript_source: 'captions' | 'whisper' | null to Job interface in types.ts
- Add idempotent migration for transcript_source column in jobs table
- Update rowToJob to map transcript_source from database
- Update updateJob and resetJob statements to handle transcript_source
- Fix transcriptSource in updateJob function call
- Add transcriptSource: null to all test makeJob helpers

This foundation schema enables transcript source tracking for D2/D5/D6 stories.
Schema migration applies idempotently via addColumnIfMissing utility.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-06 16:17:18 +00:00
Giancarmine Salucci 96ff9cc7fd story story-d2-job-error-banner-home
Build & Push Docker Image / test (push) Failing after 16s
Build & Push Docker Image / build-and-push (push) Has been skipped
2026-07-22 21:25:21 +02:00
Giancarmine SalucciandClaude Haiku 4.5 82fc4839ad Merge integration head into story branch - resolve +page.svelte conflict
Take integration branch version of handleRetry function and JobErrorLine integration.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-22 21:21:38 +02:00
Giancarmine SalucciandClaude Haiku 4.5 86194b91f0 D2: Wire retry handler into home page JobErrorLine component
Added handleRetry function to +page.svelte that:
- Calls POST /api/jobs/{id}/retry
- Navigates to /jobs/{id} on success

Passed onRetry callback to JobErrorLine component so retry button appears in the home page job list with working action.

Satisfies AC: "Retry from home page navigates to job detail"

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-22 20:53:21 +02:00
Giancarmine Salucci d034bbee25 Merge commit '11942c89ac8cb443bad62198cfcfdc2d5f3647a1' into fleet/story-d2-job-error-banner-home 2026-07-21 18:31:57 +02:00
Giancarmine Salucci 11942c89ac story story-d2-job-error-banner-detail
Build & Push Docker Image / test (push) Failing after 19s
Build & Push Docker Image / build-and-push (push) Has been skipped
2026-07-21 18:06:34 +02:00
Giancarmine SalucciandClaude Haiku 4.5 8ddfad0f2f D2: Use JobErrorLine component in detail page
Refactor job detail page to use the shared JobErrorLine component for
error display instead of inline HTML. JobErrorLine now renders in
detailed mode (with icon, title, and styled buttons) when handlers are
provided (detail page), and in minimal mode (simple inline display)
otherwise (home page list). This keeps the same visual behavior while
reducing duplication and improving maintainability.

Update JobErrorLine tests to account for the new detailed/minimal
rendering behavior.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-21 17:07:25 +02:00
Giancarmine SalucciandClaude Haiku 4.5 819180c0a9 D2: wire retry handler into home page JobErrorLine component
Wire POST /api/jobs/:id/retry call to JobErrorLine onRetry callback; navigate to job detail on success. Completes home page integration of error banner with retry action for failed jobs.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-21 17:04:49 +02:00
Giancarmine SalucciandClaude Sonnet 4.6 60782001e0 fix(baseline): restore browser resolve conditions + fix onDelete handler type
Build & Push Docker Image / test (push) Failing after 16s
Build & Push Docker Image / build-and-push (push) Has been skipped
4ca532b accidentally dropped vitest.config.ts browser resolve conditions,
environmentMatchGlobs, and hot:false — causing job-error-line.test.ts to
mount against svelte/index-server.js and fail with lifecycle_function_unavailable.
Also fixes JobErrorLine.svelte line 45: onDelete passed directly as onclick
is typed (jobId:string)=>void, not MouseEventHandler — wrap to match handler
signature, consistent with the failed-branch delete button already there.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 17:00:47 +02:00
Giancarmine SalucciandClaude Fable 5 6bd0bdf1e3 revert: restore jobs page error-UI (JobErrorLine) accidentally swept into 4ea12b1
Build & Push Docker Image / test (push) Failing after 15s
Build & Push Docker Image / build-and-push (push) Has been skipped
The previous commit was meant to carry ONLY the package.json install-scripts
approval; a stale staged revert of the error-banner work rode along. This
restores the fleet-landed version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 16:43:10 +02:00
Giancarmine SalucciandClaude Fable 5 4ea12b1299 build: approve better-sqlite3 install script (npm install-scripts policy)
Build & Push Docker Image / test (push) Failing after 17s
Build & Push Docker Image / build-and-push (push) Has been skipped
npm >=11 on node 24 skips non-approved dependency install scripts, so fresh
clones/worktrees never build the better-sqlite3 native bindings and every
build fails with "Could not locate the bindings file". Approving it in
package.json makes the approval travel with the repo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 16:42:44 +02:00
Giancarmine Salucci 4ca532b873 D2: simplify JobErrorLine — optional props, no retrying state, no icon 2026-07-17 17:29:19 +00:00
Giancarmine Salucci 0cea78c101 D2: widen onRetry to accept jobId; add accentColor prop; fix tests 2026-07-17 15:54:01 +00:00
Giancarmine Salucci 36036ba2ea D2: finalize job-list error display + deploy
- simplify JobErrorLine to compact error-line component (text + buttons)
- add JobErrorLine to +page.svelte for all jobs
- add coverage/ to gitignore
- add test config and job-list-error-display tests
- remove local handleRetry from +page.svelte
- update app-environment.ts mock
2026-07-17 13:59:18 +00:00
Giancarmine Salucci bd38d688eb Merge fleet/story-ui-job-list-error onto main
Resolve merge conflicts: take incoming branch version for JobErrorLine
component (full props), +page.svelte (import + usage with accentColor/onRetry),
and vitest.config.ts (hot:false, conditions:['browser']).

Staged changes included:
- .gitignore updates
- src/tests/__mocks__/app-environment.ts (new)
- src/tests/job-list-error-display.test.ts (new)
2026-07-17 12:09:22 +00:00
Giancarmine Salucci e33affddb2 story story-d2-error-banner-component
Build & Push Docker Image / test (push) Failing after 33s
Build & Push Docker Image / build-and-push (push) Has been skipped
2026-07-17 10:58:04 +00:00
Giancarmine SalucciandClaude Sonnet 4.6 39845db41a D2: show JobErrorLine for all failed/cancelled jobs, not just those with error text
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-17 10:37:39 +00:00
Giancarmine SalucciandClaude Sonnet 4.6 dd438d2a0b D2: JobErrorLine component + home/jobs page integration
- Add src/lib/components/JobErrorLine.svelte: reusable error banner with
  icon, truncated message, accent retry and danger delete buttons; Svelte 5
  runes; WCAG AA (role=alert/status, aria-busy, aria-label, focus ring)
- Integrate into src/routes/+page.svelte: replace .recent-err-line div with
  JobErrorLine; add handleRetry function updating jobs array in-place
- Integrate into src/routes/jobs/+page.svelte: remove inline .job-err-inline
  and .row-retry-slot; add JobErrorLine inside .job-info for failed/cancelled;
  refactor retryJobById/deleteJobById with clean (jobId: string) signatures;
  done-job delete preserved in row-actions
- Add src/tests/job-error-line.test.ts: 8 component rendering tests via
  @testing-library/svelte + happy-dom covering message, buttons, callbacks,
  fallback text, and ARIA roles
- Install @testing-library/svelte@5 and happy-dom as devDeps for component
  test support; update vitest.config.ts with svelte plugin, happy-dom env
  match glob, and browser resolve condition for Svelte 5 client build

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-17 08:19:22 +00:00
Giancarmine Salucci 0810c223e1 story story-d2b-frontend-error-ui
Build & Push Docker Image / test (push) Failing after 15s
Build & Push Docker Image / build-and-push (push) Has been skipped
2026-07-16 12:24:37 +00:00
Giancarmine Salucci 00fadb95a3 D2b: Add missing CSS design tokens for error UI display
Error banner / inline error text / retry buttons reference
--error-bg, --error-border, --error-text, --border-strong,
and --accent-ring custom properties that were used across
+page.svelte, jobs/+page.svelte, and jobs/[id]/+page.svelte
but never declared in :root.
2026-07-16 12:10:53 +00:00
Giancarmine Salucci 73f3d8d87b chore: add coverage/ to gitignore 2026-07-16 00:32:10 +02:00
Giancarmine SalucciandClaude Sonnet 4.6 559eef7180 story-ui-job-list-error: fix fake-green test — extract real JobErrorLine component
Reviewer flagged: tests rendered __mocks__/JobErrorDisplay.svelte (a test-only
duplicate), so removing lines 273-274 from +page.svelte left every test green.

Fix (option B): extract error block into src/lib/components/JobErrorLine.svelte,
import it from +page.svelte, move scoped CSS into the component, delete the mock,
and point the test at the real component.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-16 00:08:20 +02:00
Giancarmine Salucci c20f0d5f22 story-ui-job-list-error: test error display on job list page
Add SSR-based component test verifying error display logic from
+page.svelte. Uses svelte/server render() to avoid mount/browser
requirements.

- JobErrorDisplay.svelte: test component replicating error display template
- app-environment.ts: mock for /environment (needed by accent.ts)
- job-list-error-display.test.ts: 5 tests covering:
  * failed job with error -> error text rendered
  * done job with null error -> no error text
  * failed job with null error -> no error text
  * non-failed status with error -> no error text
  * title attribute contains full error text
- vitest.config.ts: add svelte() plugin for .svelte file transforms,
  add /environment alias for test mocking

Tests: 237 passed (19 files), 0 type errors, build OK.
2026-07-15 23:57:11 +02:00
Giancarmine Salucci 117882ec17 Merge fleet/story-d5-vtt-fallback-circ-1-circ-2-circ-3-circ-3
Build & Push Docker Image / test (push) Failing after 13s
Build & Push Docker Image / build-and-push (push) Has been skipped
2026-07-09 10:41:39 +02:00
Giancarmine Salucci 77ea9aa24c D5: Add VTT/caption fallback tests inside describe('downloadYouTube', ...)
- Add test: fetchTranscript rejects → AudioResult (yt-dlp audio download)
- Add test: empty transcript array → AudioResult (same fallback path)
- Remove separate describe('empty captions fallback (D5)', ...) block
- Tests inside existing describe('downloadYouTube', ...) block per spec
- No vi.mock('fs', ...) — uses dynamic require('fs') in mock impl
- No downloader.ts changes needed (tryGetCaptions already returns null
  for both reject and empty-segments cases)
2026-07-09 10:29:51 +02:00
Giancarmine Salucci d9891e2107 Merge branch 'fleet/story-d5-vtt-fallback-whisper-circ-3'
Build & Push Docker Image / test (push) Failing after 12s
Build & Push Docker Image / build-and-push (push) Has been skipped
2026-07-09 06:55:52 +02:00
Giancarmine Salucci f96d67b8d8 D5: explicit VTT fallback branch when tryGetCaptions returns null for empty/unusable segments
- tryGetCaptions now returns null when transcriptEntriesToSegments produces
  empty array (all entries whitespace/filtered out), not a CaptionResult
  with zero segments
- downloadYouTube simplifies null check — null from tryGetCaptions always
  means 'no usable captions', triggers explicit VTT fallback to downloadAudio
- Add JSDoc explaining VTT fallback contract
2026-07-09 06:47:05 +02:00
Giancarmine Salucci 70253b64cd Merge branch 'fleet/story-d1-webm-remux--d1-d6'
Build & Push Docker Image / test (push) Failing after 12s
Build & Push Docker Image / build-and-push (push) Has been skipped
2026-07-09 06:29:09 +02:00
Giancarmine Salucci 1c928968d2 D1: webm remux integration test with no-Cues fixture + idempotency/chain coverage
- Regenerated fixture medarecorder-emu.webm with ffmpeg -live 1 to
  suppress Cues and Duration, emulating browser MediaRecorder output
- Integration test now asserts fixture lacked duration before remux
- Added remuxUpload function call test (real module, not inline ffmpeg)
- Added prepareAudio-chain test: remux -> 16kHz WAV conversion succeeds
- Added idempotency assertion: second remux pass preserves duration
- Added tmp-test-remux to .gitignore
2026-07-09 06:16:37 +02:00
Giancarmine Salucci ba9a7f6714 Merge fleet/story-d2-verify-upload-retry: D2 verify upload-source retry endpoint widened and status-honest
Build & Push Docker Image / test (push) Failing after 12s
Build & Push Docker Image / build-and-push (push) Has been skipped
2026-07-09 06:09:01 +02:00
Giancarmine Salucci b2c501a165 fix: afterAll cleanup robustness for verify-d2-upload-retry
- rmSync with maxRetries handles ENOTEMPTY from overlayfs WAL races
- try/catch catch: async retryJob IIFE may recreate files during cleanup
- CI container ephemeral, /tmp leftovers harmless

All 17 test files (220 tests) pass, svelte-check: 0 errors.
2026-07-09 05:48:36 +02:00
Giancarmine Salucci 31019b4769 Merge branch 'fleet/story-d2-datadir-unify--fresh'
Build & Push Docker Image / test (push) Failing after 12s
Build & Push Docker Image / build-and-push (push) Has been skipped
2026-07-09 05:41:23 +02:00
Giancarmine Salucci 96e45dae8a D2 verify: upload-source retry endpoint widened and status-honest
Verification test for outcome-contract clauses:
- retry-endpoint-widened-and-status-honest: 200 upload | 404 unknown | 409 done
- failed-job-error-exposed-for-ui-banner: error field in GET /api/jobs

Seeds exact outcome-contract IDs (sh-outcome-*) on real SQLite.
All 10 verification tests + 13 existing D2 tests pass (23 total).
2026-07-09 05:32:51 +02:00
Giancarmine Salucci 64ba063b0f Fix D5 downloader test mocks: execFile args pass (cmd, args[], cb) not flat list 2026-07-09 05:26:23 +02:00
Giancarmine Salucci 066f197d0d D2: unify DATA_DIR default across audio/downloader/remux/db modules
Create src/lib/server/data-dir.ts as single source of truth for DATA_DIR
using db.ts's $HOME-based convention. All four modules now import from it
instead of defining their own inline default (some resolving to /tmp/.whisper-pwa).

Changes:
- src/lib/server/data-dir.ts — new file, exports DATA_DIR const
- db.ts — import DATA_DIR from data-dir.ts (same logic, same path)
- audio.ts — import DATA_DIR, replace inline process.env.DATA_DIR ?? '/tmp/...'
- downloader.ts — import DATA_DIR, replace inline '/tmp/.whisper-pwa'
- remux.ts — import DATA_DIR, replace inline process.env.DATA_DIR ?? '/tmp/...'
- tests/data-dir.test.ts — 10 tests covering default, custom, and module integrity
- tests/retry-pipeline.test.ts — fix stale hardcoded /tmp/.whisper-pwa/uploads
  path; compute parent dir from getUploadPath() dynamically

Test pass rate: 217/220 (3 pre-existing failures unrelated to DATA_DIR)
- 2 D5 downloader tests need yt-dlp (not available in test env)
- All other tests green, including the new data-dir.test.ts (10/10)
2026-07-09 05:12:11 +02:00
Giancarmine Salucci 0d4f58310f D5: empty VTT captions fall back to whisper transcription
When youtube-transcript fetchTranscript() returns empty segments array
(parse succeeded but zero captions), treat as fallback trigger instead of
returning empty transcript.

- tryGetCaptions(): return CaptionResult with empty segments (not null)
  when segments array is empty. Distinguishes 'no captions available'
  (null) from 'captions exist but empty' (CaptionResult with [])
- downloadYouTube(): check captions.segments.length > 0 before taking
  fast path. Empty segments fall through to audio download + whisper
- Tests: empty array fallback, whitespace-only entries fallback,
  non-empty segments still use fast path
2026-07-09 05:01:55 +02:00
Giancarmine Salucci 7c19af2d80 Merge fleet/story-d3-health-preflight-late-2: D3 health preflight (already on main)
Build & Push Docker Image / test (push) Failing after 16s
Build & Push Docker Image / build-and-push (push) Has been skipped
2026-07-09 04:52:03 +02:00
Giancarmine Salucci 30e54b769f Merge remote-tracking branch 'origin/main' into fleet/story-d3-health-preflight-late-2 2026-07-09 04:37:08 +02:00
Giancarmine Salucci dee102447c Merge fleet/story-d2-verify-error-field: D2 verify error field in GET /api/jobs
Build & Push Docker Image / test (push) Failing after 12s
Build & Push Docker Image / build-and-push (push) Has been skipped
2026-07-09 04:30:26 +02:00
Giancarmine Salucci f05c772952 D2: verify GET /api/jobs returns error field for failed jobs
Verification tests for outcome-contract clause
failed-job-error-exposed-for-ui-banner.

- db.test.ts: listJobs includes error for failed jobs, null for non-failed
- jobs-list-endpoint.test.ts: route-level integration with real DB,
  exercises GET handler end-to-end

D2 already merged (f03b757+); non-blocking verification only.
2026-07-09 04:24:16 +02:00
Giancarmine Salucci aba90e1a08 Merge branch 'main' into fleet/story-d3-health-preflight-late-2
# Conflicts:
#	src/lib/server/pipeline.ts
2026-07-09 04:15:10 +02:00
Giancarmine Salucci cd83c30e8e D3: Wire health preflight into pipeline before whisper submission
- Add health preflight gate after ensureWhisperRunning, before submitJob
- Uses getHealth() (existing) + retryWithBackoff for transient failures
- Network errors wrapped with 'whisper health check failed:' prefix
- Non-ok status fails job early with actual status in error message
- Revert compose.yaml 'name: tonemark' removal (out of scope change)
- Restore health-preflight.test.ts (228-line test covering all ACs)
- 8 test cases, 202 total tests passing, typecheck clean, build OK
2026-07-09 04:12:14 +02:00
Giancarmine Salucci 76b8a466c2 Merge fleet/story-d2-retry-missing-file-handling-d-done: D2 compose gate-zero fix
Build & Push Docker Image / test (push) Failing after 14s
Build & Push Docker Image / build-and-push (push) Has been skipped
2026-07-09 04:02:03 +02:00
Giancarmine Salucci 0a418e773f D3: wire health preflight into pipeline before whisper submission
Add health preflight check in runJob() after ensureWhisperRunning()
and before submitJob(). Uses existing getHealth() from whisper.ts
with retryWithBackoff() for transient failures.

- Healthy whisper (status 'ok') -> job proceeds to submission
- Unreachable whisper -> job fails early with 'whisper health check failed: ...'
- Non-ok status (e.g. 'starting') -> job fails early with actual status
- getHealth() 5000ms timeout preserved, no new HTTP client

Tests: health-preflight.test.ts covers error message contracts
and pipeline integration (YouTube + upload source)
2026-07-09 03:59:52 +02:00
Giancarmine Salucci 52715ec481 D2: remove source guard on retry buttons, show error in UI
Queue page:
- Remove && job.source?.startsWith('http') guard on retry button
- Move retry button outside .row-actions hover zone (always visible)
- Add job.error inline in meta line for failed/cancelled jobs
- data-status=failed attribute on failed rows
- Error tint on failed row title

Job detail page:
- Remove source guard from canRetry derived
- Enhanced error banner: warning icon, 'Transcription failed' title,
  error message, inline Retry + Delete action buttons
- btn, btn.primary, btn.ghost CSS styles

Home page:
- Add job.error as third line in recent items for failed jobs
- recent-failed CSS class with error-text tint on meta
- recent-err-line styling

fixes #story-d2-retry-widen-upload--d1-d6-evised
2026-07-09 03:29:18 +02:00
Giancarmine Salucci 4fa81a6326 fix(D2): add compose project name for gate-zero import check
Gate-zero import check runs:
  docker compose -p $SH_COMPOSE_PROJECT -f compose.yaml run --rm app ...

When SH_COMPOSE_PROJECT is unset/empty, -p consumes -f as project name
(invalid). Adding 'name: tonemark' to compose.yaml declares the project
name in the file itself — the compose file is self-sufficient.

All 14 tests pass, build + typecheck clean.
2026-07-09 00:35:33 +02:00
Giancarmine Salucci 4de9390913 D2: Fix upload retry 500 on missing file — async fire-and-forget
retryJob() now wraps the file read in a fire-and-forget async IIFE using
fs/promises.readFile instead of readFileSync. When the original upload
file has been cleaned up (common after failure), the IIFE catches the
ENOENT error and marks the job as 'failed' with a 'retry failed:' prefix
asynchronously — the endpoint returns 200 immediately.

Includes 4 new integration tests:
- Missing file returns immediately (no throw), job async-fails
- ENOENT message propagated in job.error
- 404 on unknown job still works (unchanged contract)
- File present resets job to pending (runJob triggered)
2026-07-08 22:54:47 +02:00
Giancarmine SalucciandClaude Fable 5 37e877b3c2 chore(infra): ephemeral gate port + deploy overlay
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 01:40:10 +02:00
Giancarmine SalucciandClaude Haiku 4.5 204848ddae fix: infrastructure for prod-parity deployment
Build & Push Docker Image / test (push) Failing after 11s
Build & Push Docker Image / build-and-push (push) Has been skipped
Added db schema migration script (scripts/migrate.js) for fresh deployments,
updated Dockerfile to include scripts directory, and fixed job-progress test
to include newly added retry fields after D3 foundation merge.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-08 01:25:52 +02:00
Giancarmine Salucci f03b757174 D2: Widen retry to upload-source jobs
- Remove http-only guard in retry endpoint (src/routes/api/jobs/[id]/retry/+server.ts)
- Extend pipeline.ts retryJob() to handle upload-source jobs:
  read file from persistent storage, pass buffer to runJob
- Change saveUploadedFile() to save to DATA_DIR/uploads/{jobId}/
  (survives cleanupJobTmp, available for retry on failure)
- Add getUploadPath(), cleanupUploadDir() to downloader.ts
- Remove cleanupFiles(rawAudioPath) from catch block in runJob()
  — cleanupJobTmp handles TMP_DIR for YouTube, uploads survive
- Add cleanupUploadDir() on webhook success path
- Add retry-endpoint.test.ts with 9 tests covering:
  - 404 for unknown job
  - 409 for non-retryable status (done, pending)
  - 200 for cancelled (retryable)
  - 200 for failed YouTube job (existing behavior)
  - 200 for failed upload-source job (new behavior, .webm and .mp3)
  - Negative: retryJob not called on 404/409
- Add downloader.test.ts tests for saveUploadedFile persistent path,
  getUploadPath, cleanupUploadDir, isolation, and non-existent cleanup
- Fix webhook.test.ts mock to include cleanupUploadDir

All 189 tests pass across 12 test files.
2026-07-08 01:25:49 +02:00
Giancarmine Salucci d633f74689 Merge branch 'fleet/story-d1-webm-remux' 2026-07-08 01:18:41 +02:00
Giancarmine SalucciandClaude Fable 5 6a28c3b7ba chore(infra): dockerize for software-house outcome gate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 01:14:16 +02:00
Giancarmine Salucci 92683e2047 feat: add /api/health endpoint for deployment health checks
Build & Push Docker Image / test (push) Successful in 15s
Build & Push Docker Image / build-and-push (push) Successful in 49s
2026-07-08 00:58:06 +02:00
Giancarmine Salucci 9f906d509d Merge branch 'fleet/story-d3-foundation' 2026-07-08 00:55:44 +02:00
Giancarmine Salucci 343c2ea9bf fix: suppress unhandled rejections in retryWithBackoff failure tests
Add .catch(() => {}) before vi.runAllTimersAsync() on 3 tests that
test the rejection path — prevents vitest exit code 1 (CI/CD gate)
while still allowing .rejects assertion to work.
2026-07-08 00:46:12 +02:00
Giancarmine Salucci ffa931e959 D1: webm upload remux step for MediaRecorder output
New src/lib/server/remux.ts:
- remuxUpload(inputPath, jobId) — ffmpeg stream copy to fix Cues/duration
- Detects .webm by extension (case-insensitive), pass-through for others
- Wired into pipeline.ts upload branch after saveUploadedFile(),
  before prepareAudio() — YouTube path untouched

Regression tests (all passing, 0 regressions across 158 tests):
- Mock-based unit tests: ffmpeg invocation, non-webm pass-through, error cleanup
- Integration test w/ synthetic opus-in-webm fixture (lower-confidence flag)
  verifies remux produces valid webm with finalized duration
2026-07-08 00:43:43 +02:00
Giancarmine Salucci 9962a0fc0f feat: D3 foundation — retry schema, shared types, health client, backoff helper
- Add retry_count/next_retry_at to jobs table via guarded idempotent ALTER TABLE
  (PRAGMA table_info existence check, try/catch for safety on restart)
- Add retryCount/nextRetryAt to Job type, rowToJob(), updateJob(), resetJob()
- Add getHealth() returning structured WhisperHealth (status, gpu_name,
  vram_total_mb, model, queue_depth, model_state) distinct from
  existing checkHealth() boolean helper
- Create retryWithBackoff() pure helper in src/lib/server/retry.ts with
  exponential backoff + jitter and onAttempt callback — no persistence owned
- Tests: retry column read/write/reset, getHealth() smoke tests,
  retryWithBackoff() edge cases (success, failure, backoff timing, onAttempt)
2026-07-08 00:40:01 +02:00
Giancarmine SalucciandCopilot 35a2d86dbb test: add beforeEach cleanup in push.test.ts to prevent flaky state leakage
Build & Push Docker Image / test (push) Successful in 10s
Build & Push Docker Image / build-and-push (push) Successful in 42s
Adds a beforeEach hook that clears subscriptions and resets mocks before
each test, making the suite robust against any state left by a previous
test even if afterEach didn't run cleanly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-10 15:56:05 +02:00
Giancarmine SalucciandCopilot 470dd1642f fix: clear modelWarming notice when job completes via SSE done event
Build & Push Docker Image / test (push) Failing after 10s
Build & Push Docker Image / build-and-push (push) Has been skipped
Previously, if a job completed while the model-warming notice was shown
(e.g. model loaded mid-job), the 'Warming up model' banner persisted on
the Done screen because the SSE 'done' handler didn't clear modelWarming.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-10 15:52:28 +02:00
Giancarmine SalucciandCopilot 10a3669b42 fix: FormData stream exhausted on retry + undefined segments crash
Build & Push Docker Image / test (push) Successful in 32s
Build & Push Docker Image / build-and-push (push) Successful in 46s
Two bugs triggered together when the model was unloaded during a job:

1. submitJob() created FormData/createReadStream once outside the retry loop.
   After a 503, the audio ReadStream was consumed and subsequent retries sent
   an empty body to whisper, causing it to return segments:undefined.

2. webhook handler cast whisperJob.segments as Segment[] without guarding
   against undefined, so deduplicateSegments(undefined) crashed with
   'Cannot read properties of undefined (reading 'map')' — stored as job.error.

Fixes:
- Move FormData + createReadStream inside the retry loop (fresh stream per attempt)
- Use (whisperJob.segments ?? []) in webhook handler
- Add Array.isArray guard at top of deduplicateSegments() as belt-and-suspenders

Tests:
- New: verifies createReadStream called once per attempt (3 attempts = 3 streams)
- New: webhook handles segments:undefined without throwing
- New: webhook handles segments:null without throwing
- 150/150 passing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-10 15:37:07 +02:00
Giancarmine SalucciandCopilot a76625d378 ci: use npm install instead of npm ci to avoid lock file version mismatch
Build & Push Docker Image / test (push) Successful in 10s
Build & Push Docker Image / build-and-push (push) Successful in 44s
Lock file was generated with npm 11 (Node 24), CI runs npm 10 (Node 22).
npm install avoids the strict sync check and matches the Dockerfile.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-09 15:54:32 +02:00
Giancarmine SalucciandCopilot 76051e52dd ci: add test job before Docker build
Build & Push Docker Image / test (push) Failing after 45s
Build & Push Docker Image / build-and-push (push) Has been skipped
Run vitest before building the image so a failing test blocks the push.
build-and-push now depends on the test job passing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-09 15:51:24 +02:00
Giancarmine SalucciandCopilot 53f874aec7 feat: proxy POST /model/unload endpoint
Build & Push Docker Image / build-and-push (push) Successful in 43s
- Add unloadModel() to whisper.ts: POSTs to /model/unload with 10s
  timeout, returns parsed JSON body, throws on non-ok response
- Create src/routes/api/model/unload/+server.ts: thin POST proxy,
  passes whisper's response through, returns 502 if whisper unreachable
- Add 3 unloadModel tests (success, WHISPER_URL config, error propagation)
  — 147/147 passing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-09 15:48:47 +02:00
Giancarmine SalucciandCopilot 04142b17a8 feat: whisper-side cancellation + SSE-triggered retry
Build & Push Docker Image / build-and-push (push) Successful in 48s
- Add cancelJob() to whisper.ts: sends DELETE /jobs/:id to the whisper
  server (best-effort, errors silently ignored)
- DELETE /api/jobs/[id] now calls cancelJob() when cancelling an active
  job that has a whisperJobId, stopping GPU use immediately
- Webhook handler guards against locally-cancelled jobs: returns ok early
  so whisper's late completion cannot overwrite cancelled status or send
  a phantom 'Transcript ready' notification
- Replace blind sleep(Retry-After + 1s) in submitJob() with
  waitForModelReady(): subscribes to /model/events SSE and proceeds as
  soon as state:ready arrives; falls back to the Retry-After timeout if
  SSE is unreachable or closes without model_ready
- Refactor retry tests to use URL-aware makeJobFetch() helper; add 7 new
  tests (3 SSE-triggered retry, 3 cancelJob, 1 webhook cancelled-guard)
  — 144/144 passing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-09 00:40:40 +02:00
Giancarmine SalucciandCopilot 01845bec25 test: comprehensive coverage for 503 retry loop and getModelStatus
submitJob — 503 retry behavior (10 new tests):
- calls onModelWaiting with correct state + retryAfterSecs on each 503
- retries until model ready and returns job_id
- tracks all three model states (unloaded, loading, waiting_for_gpu)
- uses retry_after_secs from response body
- falls back to Retry-After header when body field absent
- falls back to 15s when both body and header are absent
- throws after maxAttempts exhausted (fetch called exactly N times)
- does NOT call onModelWaiting for non-503 errors
- does NOT retry on non-503 errors (throws immediately, one fetch call)
- works correctly without an onModelWaiting callback

getModelStatus (6 new tests):
- returns parsed status for each model state tag
- includes optional fields (loaded_at, vram_*, retry_in_secs)
- calls the correct WHISPER_URL/model/status endpoint
- throws when server returns non-ok

Uses vi.useFakeTimers()/runAllTimersAsync() to eliminate real delays.
Rejection handler attached before timer advance to avoid unhandled-rejection
false positives from Vitest's detector.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-09 00:14:09 +02:00
Giancarmine SalucciandCopilot b90d57984c feat: model-on-demand lifecycle — retry on 503, live status pill, warming indicator
- whisper.ts: add getModelStatus(); fix submitJob() to retry on 503 using
  Retry-After header instead of throwing; optional onModelWaiting callback
  lets the pipeline surface model state to the UI during the wait
- pipeline.ts: pass onModelWaiting callback → emits model_warming SSE event
  so the job detail page can show 'Warming up model…' while waiting
- types.ts: add ModelStateTag union and ModelStatus interface
- api/model/status: GET route proxies whisper /model/status (falls back to
  {state:'unloaded'} if whisper unreachable)
- api/model/events: GET route relays whisper SSE stream to the browser;
  AbortController tied to request.signal cleans up on disconnect
- layout.svelte: status pill is now live — initial fetch + EventSource on
  /api/model/events; dot colour + label reflect real model state with a
  pulsing animation while loading or waiting_for_gpu
- jobs/[id]/+page.svelte: handle model_warming event type → show a yellow
  'Warming up model…' sub-label with spinner inside the progress card
- whisper.test.ts: update submitJob mocks to status:202 to match real API

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-09 00:08:21 +02:00
Giancarmine SalucciandCopilot ffd5d48c0d fix: increase body size limit to 500MB for audio uploads
Build & Push Docker Image / build-and-push (push) Successful in 42s
Default SvelteKit node adapter body limit is 512KB — too small for
audio recordings (30s ~556KB, longer recordings much larger).
Set bodySize: 500MB in adapter config. Also set BODY_SIZE_LIMIT env
in production compose .env as belt-and-suspenders.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 19:32:28 +02:00
Giancarmine SalucciandCopilot ed5e88f5ca fix: install yt-dlp via pip instead of prebuilt binary
Build & Push Docker Image / build-and-push (push) Successful in 42s
The prebuilt yt-dlp binary is compiled against glibc and fails on
Alpine Linux (musl libc) with 'cannot execute'. Install python3 +
py3-pip and use pip to install yt-dlp instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 19:17:18 +02:00
Giancarmine SalucciandCopilot dc65c028c1 fix: disable CSRF origin check to allow Web Share Target
Build & Push Docker Image / build-and-push (push) Successful in 40s
SvelteKit's CSRF check runs before the handle hook and blocks POSTs
whose Origin header doesn't match the site origin. Web Share Target
POSTs from any external app (YouTube, Chrome share sheet, etc.) are
legitimately cross-origin.

checkOrigin: false is safe here — the app has no cookie-based session
auth, so there is no CSRF attack surface.

Also remove the ineffective hooks.server.ts approach.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 19:02:07 +02:00
Giancarmine SalucciandCopilot 08adff1562 fix: bypass CSRF for Web Share Target POST
Build & Push Docker Image / build-and-push (push) Successful in 41s
SvelteKit's CSRF guard rejects POST requests whose Origin header doesn't
match the site's own origin. Web Share Target POSTs legitimately arrive
from external origins (e.g. youtube.com, OS share sheet). Strip the
Origin header in a handle hook for /share POST only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 18:58:39 +02:00
Giancarmine SalucciandCopilot d1295ce343 feat: add retry/delete for jobs
Build & Push Docker Image / build-and-push (push) Successful in 41s
- db.ts: add resetJob() and deleteJob() statements + exports
- pipeline.ts: export retryJob() — resets job state and re-runs pipeline
- DELETE /api/jobs/[id]: hard-delete terminal jobs (done/failed/cancelled);
  keep cancel-only behavior for active jobs
- POST /api/jobs/[id]/retry: new endpoint; validates failed/cancelled URL job,
  resets and re-runs via retryJob()
- jobs/[id]/+page.svelte: wire Cancel/Retry/Delete buttons with fetch calls;
  fix hardcoded ACCENT → accent store
- jobs/+page.svelte: per-row Retry+Delete icon buttons (visible on hover);
  fix hardcoded ACCENT → accent store

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 17:42:54 +02:00
Giancarmine SalucciandCopilot 37175ec791 fix: ffmpeg/yt-dlp/tz in image, UID 1000, reactive accent store
Build & Push Docker Image / build-and-push (push) Successful in 42s
- runtime: use node user (uid=1000, gid=1000) instead of custom tonemark uid=1001
- add ffmpeg and yt-dlp to runtime image (required by audio pipeline)
- add tzdata, set TZ=Europe/Zurich
- +page.svelte: replace hardcoded ACCENT constant with $derived($accent.value)
  so the home page reacts to accent store changes from Settings

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 17:35:39 +02:00
Giancarmine SalucciandCopilot 453029c139 fix: use npm install instead of npm ci in Dockerfile
Build & Push Docker Image / build-and-push (push) Successful in 46s
npm ci fails with optional platform-specific dependencies (@emnapi/core,
@emnapi/runtime) that are not recorded in the lock file for Alpine Linux.
npm install handles optional dependencies correctly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 17:01:14 +02:00
Giancarmine SalucciandCopilot b43ad9ce9a chore: update package-lock.json to sync with package.json
Build & Push Docker Image / build-and-push (push) Failing after 22s
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 16:57:21 +02:00
Giancarmine SalucciandCopilot f1f04e13e5 ci: restore REGISTRY_USERNAME/REGISTRY_TOKEN secrets (now set on repo)
Build & Push Docker Image / build-and-push (push) Failing after 22s
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 16:54:58 +02:00
Giancarmine SalucciandCopilot b6566e4590 ci: use github.actor + github.token for registry login
Build & Push Docker Image / build-and-push (push) Failing after 12s
github.actor and github.token are the correct Gitea Actions context
variables (gitea.* context doesn't exist in act_runner).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 16:53:29 +02:00
Giancarmine SalucciandCopilot 95eea34011 ci: use auto-provided GITEA_TOKEN for registry login
Build & Push Docker Image / build-and-push (push) Failing after 12s
Avoids needing to set custom REGISTRY_USERNAME/REGISTRY_TOKEN secrets.
The built-in secrets.GITEA_TOKEN has write:package access for pushing
to the Gitea container registry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 16:50:51 +02:00
Giancarmine SalucciandCopilot 13a96b6efa Initial commit: Tonemark PWA
Build & Push Docker Image / build-and-push (push) Failing after 11s
Tonemark is a SvelteKit PWA for transcribing YouTube videos, audio
and video files, and microphone recordings using a local Whisper backend.

Features:
- Dark glassmorphic UI with electric-lime accent (5 switchable themes)
- Rail nav (desktop) / tab bar (mobile) layout
- Drop zone, YouTube URL input, and live audio recording inputs
- Audio mode waveform cards (none / standard / aggressive / auto)
- Real-time transcription progress with animated waveform
- Job queue with SSE streaming updates
- Push notifications on job completion
- PWA with native SvelteKit service worker
- SRT / TXT / MD / JSON transcript downloads

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 16:41:25 +02:00