commits curt — a public log of private and public work

1,162 commits indexed 65 repos · 17 years
Featured · most recently active public repos
All commits · newest first showing 1–200 of 1162
whenrepomessagesha
May 2026 103 commits · 43 public
2026-05-12 fallback-toolkit rename lenient ledger when wrapper changes 57c7247
2026-05-10 23:09 html-utils feat(bookmarklet): add bookmarklet maker tool e4f4491
2026-05-10 22:48 html-utils docs: add decode tool screenshot to README fed7dc1
2026-05-10 22:45 html-utils fix(decode): apply layout flip + formats breadcrumb missed in prior commit bf0f025
2026-05-10 22:44 html-utils feat(decode): vertical pane layout; rename group encoding→formats e1f2bc3
2026-05-10 22:12 html-utils fix(index): blurb says 'tracks nothing'; drop dev-facing footer note f1f65de
2026-05-10 21:46 html-utils feat(diff): intra-line highlighting, custom names, fix pure-side wash f6b98f0
2026-05-10 21:27 html-utils feat: add manifest scanner, landing page, and Pages deploy workflow 2dc8ea6
2026-05-10 21:25 html-utils feat(decode): add timestamp, hex, base64, base32, gzip detectors 637c731
2026-05-10 21:25 html-utils feat(decode): add URL-encoded, HTML entities, query string detectors 48d8336
2026-05-10 21:25 html-utils feat(decode): add JSON, JWT, URI detectors e74453b
2026-05-10 21:24 html-utils feat(decode): scaffold tool with ROT13 detector and async render engine 7bf9a76
2026-05-10 21:08 html-utils feat: migrate one-line and text-diff tools from design handoff 73ef89e
2026-05-10 20:58 html-utils chore: scaffold repo with gitignore and README 47daab9
2026-05-10 16:28 ClipCutter docs: simplify CLAUDE.md to reduce drift Drop counts and timings that drift over time (test count, suite runtime, endpoint count). Refresh the architecture diagram to reflect the routes/ split and the Vite + TypeScript SPA layout under static/. 3c34685
2026-05-10 16:10 ClipCutter feat(ui): real process progress + asymptotic fallback for unknown pct. Closes #7 The process runner now turns videos_done / videos_total into a real pct and surfaces current_video as the chip subtitle. To smooth the bar between coarse data points (process: between videos, encode: within a clip, keep: no real pct at all), TaskController extrapolates asymptotically toward 95% when no fresh pct lands, snapping forward when one does. Encode/upload/ compile keep their existing real pct unchanged. 26dfed3
2026-05-10 16:08 ClipCutter feat(state): expose per-video progress on /api/process/status ProcessingState now tracks videos_total / videos_done / current_video, updated by process_directory() around each video iteration and exposed via snapshot(). The plumbing uses an optional progress= parameter on process_directory rather than a callback — fewer moving parts and the only caller that wants progress is the web route. de7846d
2026-05-10 16:06 ClipCutter fix(ui): persist player volume across sessions and modals. Closes #8 f3b7ca0
2026-05-10 16:05 ClipCutter fix(ui): re-scan Process tab on activation. Closes #9 ec5ac54
2026-05-10 08:51 waypoint refactor: restructure manager (unused) 54613b4
2026-05-10 08:44 waypoint refactor: rework scheduler 9e1620d
2026-05-10 08:35 waypoint refactor: rework store and rename watcher 6addd77
2026-05-10 parser-mailer feat: guard broken registry when config changes 340ed78
2026-05-10 indexer refactor: bump channel so schema can expose util 7d31e57
2026-05-10 reusable-facade-encoder minor cleanup fb92583
2026-05-10 watcher-helper cleanup silent task in stream 7e5d699
2026-05-10 reusable-loader-builder improve missing runner 4b10e9b
2026-05-10 config refactor: deprecate shim in fallback fc18e87
2026-05-10 util-router expose silent controller in config 229c63d
2026-05-10 config move supervisor, move writer d5e98fc
2026-05-10 helper restructure faster task bb453f2
2026-05-09 22:01 waypoint fix: trim silent service when encoder changes 659450c
2026-05-09 21:51 waypoint docs: vendor registry, tweak scheduler b88a31e
2026-05-09 21:46 waypoint tmp 4550c6b
2026-05-09 19:35 waypoint split redundant store for the lingering context c71dced
2026-05-09 19:14 waypoint refactor: deprecate handler for filter 15ccc9a
2026-05-09 18:45 waypoint migrate helper for guard 80987c3
2026-05-09 17:36 waypoint chore: rebuild dead wrapper when guard changes 613e2a9
2026-05-09 17:35 waypass test: deprecate ledger (obsolete) 13f6e49
2026-05-09 17:09 waypoint feat: loosen fallback and trim shim d0a3527
2026-05-09 17:08 waypoint docs: deprecate fallback, move worker 1bcaa7a
2026-05-09 17:06 waypoint split lenient reader in parser bd98b51
2026-05-09 17:05 waypoint wip: guard guard (leaky) 096d82f
2026-05-09 17:04 waypoint wip: polish queue, bump facade d880273
2026-05-09 17:02 waypoint test: debounce bootstrapper so cache can split client 20c9215
2026-05-09 16:22 waypass improve wrapper (silent) 11345ed
2026-05-09 redundant-task-util rebuild context, loosen loader d58904e
2026-05-09 bootstrapper tweak buffer so consumer can cleanup decoder fe83ff8
2026-05-08 bootstrapper minor cleanup 6efd09a
2026-05-08 lenient-manager harden the early context d9e28be
2026-05-07 stale-builder-adapter guard adapter, rework service c15f23b
2026-05-07 loader-toolkit test: split util, fix router aaab879
2026-05-06 21:53 ClipCutter docs: refresh README with single hero screenshot - Replace the three pre-rework per-tab screenshots with one hero shot of the redesigned Review tab placed under the opening blurb (drops the Screenshot heading entirely - it's the natural lead image now). - Move the Features section up so the reader sees what the tool does before how to install or run it. Order is now: blurb -> hero -> How it works -> Features -> Requirements -> Install -> Usage -> ... - Delete docs/img/process.png and docs/img/export.png; the surviving review.png covers the visual treatment for all three tabs. 23fdcc7
2026-05-06 19:56 ClipCutter fix(ui): play past compilations inline. Closes #4 ae72cef
2026-05-06 19:55 ClipCutter fix(ui): make clip filenames clickable on Export Clips. Closes #5 6abc32e
2026-05-06 19:55 ClipCutter refactor(ui): extract openPreviewModal helper b6e590a
2026-05-06 19:54 ClipCutter fix(models): serialize clip_count on CompilationMetadata. Closes #6 6099368
2026-05-06 19:53 ClipCutter fix(routes): register compile router before review so /video/compilation works ce58e45
2026-05-06 12:28 ClipCutter fix(ui): pause playing videos on tab switch Switching from Review (or from a preview modal opened on Export) left the underlying <video> playing in the background. switchTab now pauses every <video> in the document at the start of the handler, so audio stops as soon as the user navigates away. 02890ea
2026-05-05 23:09 configs chore: initialize repo with canonical eslint config f7ede09
2026-05-05 22:33 ClipCutter feat: Phase 5 - polish, legacy cleanup, browser-test repair Cleanup - Drop unused window._cc handlers (thresholdChangedHandler, onSegmentInput, updateTrimIndicator, renderExportView - none of these are called from generated HTML after the per-tab refactors) and un-export them or delete entirely. Cuts 4 dead exports. - Remove the legacy #overlay element from index.html. Phase 4 retired the showOverlay/hideOverlay helpers so the fixed-position overlay is no longer used. - Fix the void-cast warning in compile.ts by typing apiDelete generically: deleteCompilationSources now returns Promise<{deleted_count: number}>. Combined with the unused-import cleanup above, tsc --noEmit is now clean (0 warnings, down from 4). Legacy CSS retirement - Move the still-referenced bits (body reset, .view show/hide, .empty-state) into cc.css and delete legacy.css entirely. Bundle size: 41 kB -> 27 kB CSS (~34% smaller). - The vite html-proxy quirk that forced legacy.css extraction in Phase 1 doesn't recur because cc.css is the only stylesheet imported from main.ts now. Browser test repair (5 failing -> 99/99 passing) - Update tests/test_ui_browser.py selectors: .btn-keep/skip/discard -> .cc-action-keep/skip/discard - "Review Complete" check is case-insensitive (Direction B uppercases via CSS text-transform; the underlying text is mixed case so .lower() handles both). - "volume spike" / "Confidence" assertion in test_review_shows_clip switched to "volume" + "confidence" matching the new region labels and lowercase rendering. - Process-done detection in test_process_review_export waits for "[done]" instead of "Done!" to match the new log-tag rendering. - TestExportTabBrowser keep call uses keep_and_wait so the async worker has time to land the file before the UI reads it. - New _wait_for_path / _wait_for_keep_with_custom_name helpers poll for filesystem and metadata side-effects after async keeps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> 6c72802
2026-05-05 21:52 ClipCutter feat: Phase 4 - async keep endpoint + TaskController wiring POST /api/clips/{stem}/{filename}/keep used to block while ffmpeg ran (multi-second for precise/ultra trim modes), so the design new task chip + modal couldn't surface trim progress. The endpoint now returns immediately with {task_id, status="started"} and the ffmpeg work runs in a daemon thread; cheap synchronous validation (file existence, segment length) still returns 4xx before the task is queued. Backend - New KeepState in state.py keyed by UUID, holding per-task status, progress_step, error, trimmed flag, started_at / finished_at. Multiple keeps may run in parallel (different clips); finished tasks are GC'd 60s after they complete. - Trim/copy logic factored into _do_keep helper. The route's worker thread updates state.keep at start, after the ffmpeg work, and on finish/error. - New GET /api/clips/keep/status returns {tasks: [...]} - one entry per in-flight (or recently-finished) keep. Frontend - api.ts: keepClip returns {task_id, status} and a new fetchKeepStatus reads the status endpoint with KeepTaskInfo / KeepStatus types. - review.ts: clipAction('keep') no longer awaits the full ffmpeg; it kicks off the request, hands the polling to TaskController as kind="keep", and advances optimistically. The legacy fixed-position overlay is gone - the task modal handles progress. - main.ts: 'keep' task-complete refetches loadExportTab so kept clips appear in Export. Review uses local optimistic state so it doesn't reload (would lose the user's place in the queue). Tests - New keep_and_wait helper in conftest.py wraps POST /keep and polls the status endpoint until the task lands. All test_review.py, test_export.py, and test_compilation.py call sites switched to it; assertion shape changed from resp.json()["status"]=="kept" to result["status"]=="done" with result["trimmed"] preserved where the test cares about it. - 88/88 backend tests still green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> 4ed350b
2026-05-05 21:38 ClipCutter feat(ui): Phase 3c - Export tab redesign Replaces the single-pane Export view with the cc-export shell: storage summary bar at the top, sub-tab nav for Clips / Compilation / YouTube, and a sub-tab body that swaps based on selection. Source videos move into a collapsible accordion at the bottom of the Clips sub-tab (hidden by default). Storage bar shows the kept / encoded / compilations split as a proportional fill plus a legend with counts and total. The sub-tab nav also surfaces a YouTube connection indicator on the right edge. Clips sub-tab is a two-column grid: kept-clips table on the left (filename, source size, encoded size, preset, YouTube state, per- row actions) and an Encode panel on the right (preset, target fps, GIF slowdown when applicable, live selection count, primary Encode button). Compilation sub-tab is a two-column grid: drag-reorder sequence with cc-comp-row markup and transition labels between rows on the left, Build panel on the right (title, transition, crossfade duration, primary Build button). Past compilations render under the sequence list. YouTube sub-tab is the upload form with auth gate. When connected it shows the upload-grid (privacy, category, playlist, tags, description) and a per-clip upload table with title input and status pill, then a primary Upload button. The legacy inline progress bars (encodeProgress, uploadProgress, compProgress) are gone - the modal + chip handle progress now that they exist (Phase 2). Buttons just disable while a task is running and re-enable on task-complete. Pre-existing tsc warnings: renderExportView unused import in main.ts and a void-cast in compile.ts. Both unchanged by this phase. Phase 3b's rewrite of review.ts cleaned up two prior warnings, so the count is now 2 down from 4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> d394bbc
2026-05-05 21:28 ClipCutter feat(ui): Phase 3b - Review tab redesign Replaces the player-section / progress-dots / clip-info / actions markup with the cc-review grid: clip header + position + queue toggle in the top row, video-dominant left column with waveform, trim bar, and segment list below, queue drawer on the right (toggle to collapse), and a Keep, Skip, Discard action footer spanning both columns. Waveform switches from canvas to DOM bars + region overlay divs + single trim rectangle (with IN/OUT pseudo-labels via CSS) + a position-absolute playhead. The behaviour is unchanged - region colors keyed by detection type, click-to-seek, requestAnimationFrame playhead sync - but the visual treatment now matches the design tokens exactly. Per-segment typed time inputs are removed in favour of the cleaner "focus a segment row, then Set IN / Set OUT against the current video position" workflow the design specifies. Seek-to-in / seek-to-out chevrons in each row keep the navigation muscle memory. Queue drawer shows all clips in the session with click-to-jump, detection-color tag, and per-clip review status so the legacy progress-dot strip is no longer needed. Keyboard shortcuts (K, D, S, I, O, N, Space) preserved unchanged; active-segment lookup moved from a DOM scrape in main.ts to getActiveSegmentIndex() exported from review.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> d85ddaf
2026-05-05 21:14 ClipCutter feat(ui): Phase 3a - Process tab redesign Replaces the legacy form-section / scan-panel / log-box markup with the Direction B grid layout: folder bar + 3 stat tiles in the head, videos table + collapsible Stale section in the main panel (left), and a Detection controls panel + live-run log panel in the side rail (right). Three sliders replace the old number inputs - sensitivity, context window, and stale threshold. The stale threshold slider re-filters the stale list as it moves (no more separate threshold input embedded in the panel header). The view-process container now flexes full-height. To make that work without breaking Review/Export, the cc-body layout was restructured so each .view is a direct flex child of cc-body; Review and Export keep the legacy centered max-width scroll container until their phases land. The legacy CSS classes used only by the old Process tab (.scan-panel, .scan-table, .log-box, etc.) are now unused by markup but kept in legacy.css for now. Cleanup pass after all three tabs land. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> 12aca47
2026-05-05 21:03 ClipCutter feat(ui): Phase 2 - TaskController + chip / toast / modal Adds a unified background-task layer for the four long-running operations (process / encode / compile / upload) plus a placeholder slot for keep, which becomes the fifth in Phase 4. The new tasks.ts owns: - TaskController class extending EventTarget with a Map of tasks, a per-task poll loop, and singleton-per-kind concurrency (process / encode / compile / upload are exclusive, keep will be parallel-safe once the backend goes async). - Two events: tasks-changed (any state mutation) and task-complete (one-shot when a task ends, used by tab loaders to refetch). - Renderers for the titlebar chip (idle / single-running / single-done / multi+popover), bottom-right toast stack with a 5s auto-dismiss, and a dismissable task modal with Run-in- background and Cancel actions. Each per-tab handler now hands its polling off to TaskController instead of calling setInterval directly. The legacy progress UIs (logBox, encode/upload/comp progress bars) are kept in place and mirror the task state so the existing tab content still works while the new chip / modal layer sits on top of it - to be removed when each tab is rewritten in Phase 3. Cross-tab refetch hooks live in main.ts: on task-complete the process task triggers a folder rescan, and encode/compile/upload each trigger loadExportTab(). keep is a deliberate no-op until the backend exposes a poll endpoint. Pre-existing tsc --noEmit warnings (unused imports, a stale `as` cast in compile.ts) are unchanged. Build (vite) and backend tests (88/88) green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> 7756a42
2026-05-05 20:51 ClipCutter feat(ui): Phase 1 - Direction B shell Vendors the design-handover tokens (cc.css) and rewrites the app chrome - titlebar + numbered tabs + cc-body + toast/modal slots - per the Pro Video Editor direction. Per-tab content unchanged; the existing tab modules continue to render their old layouts inside the new shell as a transitional step (Phase 3 replaces them). The 700-line inline style block from index.html was extracted to legacy.css to keep tab content working and to sidestep a Vite html-proxy quirk on Windows. Browser tests (test_ui_browser.py) target old .tab selectors and will be repaired alongside the per-tab refactors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> 6a99494
2026-05-05 19:51 ClipCutter chore: gitignore local-only ignored/ working area Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> 1f7d35b
2026-05-05 stale-handler wire manager for decoder f8fb521
2026-05-05 tighter-validator-session fix: split config in registry 7ce223c
2026-05-05 helper-toolkit docs: update producer in view 86f6c3d
2026-05-05 cleaner-scheduler-builder refactor: vendor consumer so session can tidy watcher 4b3c8ae
2026-05-04 parser-session tmp 5996957
2026-05-04 store-writer wrap bootstrapper, trim model 3fc1185
2026-05-04 worker wip: rework the lenient stream c388aa9
2026-05-04 indexer chore: harden writer and split mailer fcc84ef
2026-05-04 mailer-consumer merge adapter so shim can throttle shim 1842531
2026-05-04 pipeline-shim move renderer 0ed13d0
2026-05-04 parser feat: loosen stream so loader can drop config 1e697c4
2026-05-04 adapter-stream wire adapter in consumer da60a9d
2026-05-04 service-toolkit chore: loosen tighter watcher for the dead adapter 8b0e03c
2026-05-04 early-registry throttle client and optimize fallback e0f2e1f
2026-05-04 indexer-model test: optimize indexer and wire context aa81628
2026-05-04 broken-stream wip: tweak early stream in worker a43212d
2026-05-04 dead-builder feat: inline flaky parser for the stale filter 9b7e0dc
2026-05-04 builder refactor: update stricter channel 6230bdf
2026-05-04 fallback tighten stale channel for the noisy wrapper 43925af
2026-05-04 config-toolkit fix: rename producer, wire dispatcher 33246a5
2026-05-04 manager-buffer wip: drop handler (silent) 5623f26
2026-05-04 scheduler todo 580af0e
2026-05-03 22:39 waypass throttle validator 9de5fab
2026-05-03 20:31 FileOrganizer chore: remove unused deps (uuid, zod, @testing-library/jest-dom) uuid and zod were never imported in the engine — UUIDs come from crypto.randomUUID(), and no validation code uses zod. jest-dom was listed in the UI but no test imports it; only @testing-library/preact is in use. 12 transitive packages dropped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> c8315ea
2026-05-03 20:06 FileOrganizer ui/topbar: remove dead settings button It had no onClick handler and no destination route — purely decorative. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> 947c88f
2026-05-03 19:57 FileOrganizer docs: refresh dashboard screenshot Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> 34df626
2026-05-03 19:56 FileOrganizer docs/ui: drop milestone status table from README and remove dead sidebar throttle widget The README''s M0-M7 status table and references to internal plan/spec docs are gone. The sidebar throttle pills were purely decorative (hardcoded balanced, no onClick) — real throttling is per-scan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> 049c413
2026-05-03 18:57 rust-build-planner chore(#30,#31): lint cleanup and CI Node bump (#32) * fix(#30): drop redundant any casts in PlacedPieces click handlers * ci(#31): bump deploy.yml off Node 20 db299e4
2026-05-03 18:04 rust-build-planner chore: rename project to Rust Build Planner (#29) dcb7a5d
2026-05-03 redundant-model improve service 784a4b3
2026-05-03 adapter feat: loosen lenient buffer when renderer changes aaafb73
2026-05-01 16:36 FileOrganizer feat(ui/cleanup): Delete all button per drive, no longer capped at 5000 (closes #17) Adds a primary "Delete all (N)" button in the Cleanup header that calls the new /api/cleanup/empty-dirs/apply-all endpoint. The truncation hint now points users at the new affordance instead of just noting the 5000-row cap. dd96f63
2026-05-01 16:35 FileOrganizer feat(engine/api): /api/cleanup/empty-dirs/apply-all endpoint POSTing { driveId } pulls every empty-dir row for the drive without the 5000-row list cap and runs removeEmptyDirs in one batch. Returns batchId=null/removed=0 when there is nothing to clean. Mirrors the error shape and batch-status event publish of the existing /apply. f9991bf
2026-05-01 16:34 FileOrganizer feat(engine/cleanup): EmptyDirsRepo supports unbounded list (cap=null) Allow callers to fetch every empty-dir row for a drive without truncation. Existing callers default to the 5000-row cap unchanged. findEmptyDirs gains an `unbounded` option that maps to cap=null. d840cf7
2026-05-01 context-view feat: move service for reader 92b87e0
April 2026 97 commits · 88 public
2026-04-30 21:35 FileOrganizer feat(ui/browse): click path segments to add user exclusions, with chip-bar removal (closes #16) Path column now renders as breadcrumbs — middle segments are inline buttons. Clicking one previews the catalog impact via /api/exclusions dryRun, prompts to confirm if any indexed files would be pruned, then adds the segment to settings.userExcluded and refreshes the file list. A chip bar above the table lists current exclusions with × buttons that remove them from settings (no re-index until next scan). Adds previewExclusion / addExclusion / removeExclusion to ApiClient and a subtle .path-seg button class to the shared stylesheet. b3a1ab1
2026-04-30 21:33 FileOrganizer feat(engine/api): /api/exclusions endpoints with prune-on-add POST /api/exclusions validates a folder-name segment, counts matching catalog rows, and (unless dryRun) deletes them and persists the segment to settings.userExcluded. DELETE /api/exclusions/:segment removes the entry from settings only — the files table is left alone (re-index happens on next scan). Match SQL wraps each path in `\…\` and case-insensitively normalizes `/` → `\` so POSIX-stored paths match too. Idempotent on re-add (case-insensitive dedup). 910ea3c
2026-04-30 21:32 FileOrganizer feat(engine/settings): add userExcluded to Settings, plumb through to scans Settings gains a userExcluded: string[] field (default []) that the scan POST handler forwards as extraExcluded to runScan. Older catalog rows that pre-date this field are upgraded transparently in load() by merging the parsed JSON over defaults — a forward-compat pattern that handles any future-added Settings field without a migration. 4cdc2f2
2026-04-29 22:57 FileOrganizer chore(ui): drop stale milestone WIP markers and remove unbuilt search (closes #15) ad06ed9
2026-04-29 22:19 FileOrganizer feat(engine+ui/organize): apply-all endpoint for bulk plan execution (closes #14) POST /api/organize/apply-all re-runs the planner without pagination, optionally filters by ruleIds and kinds, drops noops, and applies the result as a single batch via applyApprovedBatch. An empty filtered plan returns { batchId: null, completed: 0, failed: 0, emptyDirsRemoved: 0 } with 200 instead of erroring. OrganizePlan now exposes totalBytes (sum of estimatedBytes across the unpaginated, non-noop ops) so the UI can label the bulk button without walking pages. The Plan tab gains "Dry-run all" and "Apply all" buttons next to the per-page actions; Apply all confirms when the plan exceeds 1000 ops or 1 GB. Per-page Apply / Dry-run are unchanged. ApplyResultUI.batchId widened to string | null to model the empty-plan no-op outcome; existing callers were already null-safe or only invoked when ops were selected. b50f43a
2026-04-29 21:52 FileOrganizer chore: lint sync fs out of the engine HTTP path; run lint in CI Honest postmortem: the cleanup screen shipped with `readdirSync` recursing the entire drive on the request handler. While that walk ran, every other concurrent API request stalled behind it on the single Node event loop. I missed it because I reviewed via CI status and test count rather than reading the new files. Tests don't catch event-loop blocking — every test passes, the function works, it just monopolizes the runtime. Backstop: ESLint rule banning `readdirSync` and `readFileSync` inside `packages/engine/src/**` outside boot-only code (`cli/`, `migrate.ts`, `locator.ts`, `log.ts`, tests, fixtures). Bounded one-shot uses are allowed via `// eslint-disable-next-line no-restricted-syntax -- TODO #N: <reason>` referencing the issue that tracks the proper async fix. Five existing call sites tagged TODO #11 (cleanup find-walk + per-path checks, applier post-apply sweep, preview-cache LRU eviction, /api/fs/list folder picker) so they don't gate this commit but are surfaced for the cloud agent when #11 lands. Wired `npm run lint` (was wired but pointed at non-existent per-workspace scripts; now runs eslint directly on `packages/*/src/**/*.{ts,tsx}`) and added it as a CI step between typecheck and test, so violations fail the build on push. Side cleanups along the way: dropped a half-dozen unused imports that the rule's noise revealed, and removed two `react-hooks/exhaustive-deps` disable comments that referenced a plugin we don't have installed. 4f7f8aa
2026-04-29 21:22 FileOrganizer perf(engine+ui/organize): paginate plan operations to unblock large catalogs (closes #13) edee4c4
2026-04-29 21:18 FileOrganizer fix(engine/scan): case-insensitive exclusions + cover common installed-software dirs (closes #12) 8782265
2026-04-29 20:25 FileOrganizer fix(engine+ui): dedup unresolvedRoles by rule with file count (closes #10) Aggregate unresolved-role entries by ruleId in the planner so the Plan-tab banner shows one chip per shadowed rule with a (×N) file-count suffix, instead of one entry per matched file. With ~95k indexed files and the seeded default roles unassigned, the banner previously rendered hundreds of identical lines per rule. 0037757
2026-04-29 19:48 FileOrganizer perf+arch(engine/cleanup): use catalog instead of on-demand walk (closes #11) 2adc734
2026-04-29 19:45 FileOrganizer feat(engine/scan): persist empty-dir state to catalog during scan 2477e85
2026-04-29 19:44 FileOrganizer feat(engine/scan): walker reports recursively-empty directories 55263e4
2026-04-29 19:41 FileOrganizer feat(engine/catalog): empty-dirs repo 342fdb2
2026-04-29 19:40 FileOrganizer feat(engine/catalog): add empty_dirs table for cleanup hot path 0ebc2ad
2026-04-29 11:26 FileOrganizer fix(engine/catalog): disable foreign_keys around each migration Migration 0004 rebuilds the scans table to widen its CHECK constraint (adding 'cancelled'). On any populated catalog the DROP TABLE step fails with `FOREIGN KEY constraint failed` because files.scan_id is an FK reference. SQLite enforces this schema-level check at DROP time regardless of `defer_foreign_keys`, and `PRAGMA foreign_keys` can't be toggled inside a transaction — the cloud agent's defer-FK attempt didn't help. Fix is in the runner, not the SQL: wrap each migration's transaction with `PRAGMA foreign_keys = OFF` / `ON`, and run `PRAGMA foreign_key_check` after re-enabling so any actual integrity violations (rare, but possible from a buggy migration) surface immediately as MIGRATION_FK_VIOLATIONS. Regression test exercises the failure path: applies 0001-0003 manually, populates drives + scans + files (with the FK live), then calls migrate() to land 0004. Without the fix this throws; with it, the rebuild succeeds, the FK still resolves, and the new CHECK constraint admits 'cancelled'. The unhelpful `PRAGMA defer_foreign_keys = ON` line in 0004's SQL is also removed. e02ca24
2026-04-29 11:02 FileOrganizer fix(engine/scan): cancel-late race + reliable test fixture The orchestrator's signal.aborted check only fires between files inside the for-await loop. If cancel arrived after the last file but before the post-loop finalization (a real race on fast hardware), the scan finished as 'completed' even though the user clicked Stop. Adds a second signal check after the loop so that race lands as 'cancelled'. The matching API test was timing out on Linux CI because 800 tiny files at 'idle' profile finished in ~125 ms — faster than the cancel could land — and idle's inter-chunk sleep only fires *between* chunks, so single-chunk files don't get throttled at all. Switched the fixture to 100 × 600 KB files so each one iterates multiple chunks and the idle sleep actually engages, giving a deterministic cancel window. 3b0e846
2026-04-29 08:13 FileOrganizer feat(engine+ui): cleanup screen for long-existing empty directories Drives accumulate empty folders over time — moved-out projects, half-finished imports, leftover toolchain scaffolding. The new Cleanup screen surfaces them per-drive and lets the user batch-delete. Engine: - findEmptyDirs(driveRoot, opts) walks the tree depth-first and reports directories whose entire subtree is file-free, after excluding DEFAULT_EXCLUDED_NAMES (so node_modules and _FileOrganizer_quarantine don't get reaped from inside repos). Capped at 5000 paths; the response sets truncated=true when the cap bites. Sorted deepest-first so the caller can rmdir children before parents. - removeEmptyDirs(db, opts) wraps the pass in a 'cleanup-empty-dirs' batch. For each path: validate it's under driveRoot (rejects ../escapes), double-check it's still empty (handles the scan-then-write race the tests cover), then rmdirSync. Each path becomes a delete operation row with errorMessage on failure. The batch finishes 'completed' if everything succeeded, 'failed' if any path didn't. API: - GET /api/cleanup/empty-dirs?driveId=… returns { driveId, paths, totalEmpty, truncated }. - POST /api/cleanup/empty-dirs/apply with { driveId, paths } returns { batchId, removed, failed[] } and publishes a batch-status event. UI: - New /cleanup route mirrors the Duplicates layout: left-pane drive picker with per-drive empty count, right-pane tree grouped by parent with per-row checkboxes plus a "Select all visible" toggle. Apply is gated on selection and confirmed via window.confirm. - Sidebar gets a Cleanup entry slotted between History and Quarantine, kbd 'g x'. - History page gains 'cleanup-empty-dirs' as a kind filter pill. Schema: BatchKind union widens to include 'cleanup-empty-dirs'. batches.kind and operations.kind are plain TEXT in the schema, so no migration needed. Closes #6 8bfadd3
2026-04-29 08:09 FileOrganizer feat(engine+ui): optional empty-source-dir sweep after organize apply After approving a batch that drains a folder, the empty parent stays behind. Useful for archive runs where the user wants the source tree to disappear once everything's been migrated. applyApprovedBatch accepts removeEmptySourceDirs. While iterating ops we collect dirname(sourcePath) on every successful (non-dry-run) move into a per-batch Set. After the loop, walk each unique dir plus its ancestors up to (not including) the drive root, deepest-first, and rmdirSync those that have become empty. ENOTEMPTY/ENOENT/EBUSY are swallowed so a parallel scan adding a file mid-sweep doesn't fail the batch. Sweep stops climbing once it hits the drive root or any segment named _FileOrganizer_quarantine. The count of removed dirs surfaces as emptyDirsRemoved on the apply result and in the batch summary JSON. UI: a checkbox in the Plan tab's apply controls, default off, gated to non-dry-run only. The toast appended after success names the removed-folder count when >0. Closes #5 273113a
2026-04-29 08:05 FileOrganizer feat(engine+ui): cancel running scans from the UI Scans run for hours on multi-TB drives. If you start one with the wrong profile or root path, the only escape today is a hard process kill, which leaves the scan row stuck in 'running' until reconcile. The orchestrator now cooperatively respects an AbortSignal. Cadence: - walker checks signal.aborted at the top of each subdir descent and before each yielded entry, so even a giant single directory is interruptible (otherwise a directory with millions of files would block cancellation until the walker climbed back out). - The orchestrator's main for-await loop also checks the signal each iteration, so we exit between files rather than partway through a hash. Mid-hash interruption isn't worth chasing — hashFile is a CPU-bound chunked reader, and the UX win from sub-file cancellation is small relative to the complexity. When the signal fires the orchestrator skips markMissing (we haven't finished walking, so we'd false-positive everything we missed), flushes whatever progress we have, and finalizes the scan as 'cancelled'. Server side: a per-process Map<scanId, AbortController> tracks active scans. POST /api/scans wires a fresh controller in via the new onStart callback so the controller registers under the same id the orchestrator just created. The map entry is dropped in the promise's .finally(). New endpoint POST /api/scans/:id/cancel calls abort() and returns the current scan record; the next poll picks up status= 'cancelled' once the orchestrator finishes its tail-end work. Schema: 0004 rebuilds the scans table to widen the status CHECK constraint to include 'cancelled'. SQLite can't ALTER constraints in place so the migration uses the create-copy-drop dance. UI: each running scan card gets a Stop button gated by a confirm prompt. Recent-scans list pills 'cancelled' as warn. Closes #7 43b59ac
2026-04-29 08:01 FileOrganizer perf(engine/api): on-disk LRU cache for /api/preview Until now every /api/preview request re-resized through sharp. With the duplicates panel rendering 4 thumbnails per group and groups loaded continuously as you scroll, the same SHA-keyed image gets asked for over and over. Cache layout: <catalogDir>/preview-cache/<first-2-of-sha256>/ <sha256>-<max>.jpg. Ensure the dir exists at server start. On request, stat the cache file; if its mtime is >= the source mtime, stream it straight from disk via createReadStream piped through Readable.toWeb so we don't pull the whole image into memory. On miss, sharp resizes, we writeFile, then stream. Concurrent requests for the same key are coalesced through an inflight Promise map so a burst of thumbnails doesn't fan-out to N parallel sharp calls. Eviction is opportunistic: every 100th miss we walk the cache, and if total size exceeds 500 MB we delete the oldest 20% by mtime. Simple, not a crit-path concern, runs in the same request that already paid for resize+writeFile. Closes #9 (preview-cache) 53638c1
2026-04-29 07:58 FileOrganizer perf(ui): add minSize selector and pagination wiring on duplicates list Pair with the server-side pagination from the previous commit. The list now requests groups in pages of 50 and appends as the user scrolls within ~240px of the bottom. Default minimum size is 1 MB so the list opens with a manageable working set on a 95k-file catalog. Selector exposes 1 KB / 1 MB / 10 MB / 100 MB / 1 GB. Changing it resets the accumulated groups and reloads page 0. Header now shows "loaded / total" so the user can see they haven't exhausted the list. Note: the brief asked for @tanstack/virtual virtualization on top of this. That dep can't be installed in the sandbox (registry 403); pagination + the 1 MB default already gets the Duplicates page out of "hangs the browser" territory on 95k-file catalogs. Virtualization can land in a follow-up once @tanstack/virtual is added locally. Closes #9 (virtualization) af96878
2026-04-29 07:55 FileOrganizer perf(engine/dedupe): paginate /api/duplicates by reclaimable bytes desc The Duplicates screen tries to render every duplicate group at once. On a 95k-file catalog that's tens of thousands of cards and the browser hangs. Push the slice to SQL: the GROUP BY query already orders by (copies-1)*MIN(size_bytes) DESC, so a LIMIT/OFFSET tacked on returns the highest-impact groups first. The endpoint now accepts ?limit (default 50, cap 500) and ?offset (default 0), returns total/hasMore so the UI can wire infinite scroll. planDedupe takes the same triple and forwards limit/offset through to detectDuplicates. countDuplicateGroups runs the same GROUP-BY skeleton without aggregations to populate total without paying for full group materialization. Closes #9 (pagination) 3decb99
2026-04-29 dead-worker-helper fix: tweak router for queue 12fa221
2026-04-29 model-adapter feat: rework the spurious util 17be0cb
2026-04-28 23:08 FileOrganizer feat(engine/scan): exclude well-known game launcher folders by default Adds 20 game-launcher folder names to DEFAULT_EXCLUDED_NAMES so the walker stops at game install roots instead of indexing every UI sprite, voice line, and cutscene as if it were the user's media. Covers Steam, Epic, GOG, Battle.net, Origin/EA, Riot, Ubisoft, Microsoft Store/Xbox, Rockstar, and itch.io — the launchers that account for nearly all installed games on Windows. Custom-named installs (e.g. D:\Games\MyCoolGame\) are not caught here; that needs marker-file detection or a user-editable exclusions list, both filed as separate work. Closes #8 52f4fe5
2026-04-28 21:29 FileOrganizer fix(engine/api): preview endpoint returns Response instead of c.body(Buffer) Hono's c.body() only accepts BodyInit (string | ReadableStream | ArrayBuffer | ArrayBufferView | null), and TS doesn't recognize Node's Buffer as ArrayBufferView in strict mode. CI failed typecheck on the preview-image commit. Switched to constructing a Response with Uint8Array(buf) directly — same wire result, types are happy. 9bb749e
2026-04-28 21:01 FileOrganizer feat(ui+api): instant scan feedback and folder-picker modal The Scan button used to wait silently while POST /api/scans roundtripped (50ms artificial delay plus the catalog write), so a click felt like a no-op. Track a `starting` flag set on click and cleared in finally; the button label flips to "Starting scan…" and the input/profile/Browse controls disable for the duration. Errors continue to surface inline. Add a custom folder-picker modal backed by a new GET /api/fs/list endpoint. The endpoint refuses any path that's not under a registered drive's mount_path (returns 403), with a special "/" sentinel that returns the registered drives as virtual top-level entries. Children come back sorted dirs-first then alphabetical, capped at 1000. The modal renders a clickable breadcrumb, a list of children with chevrons on dirs, and a "Use this folder" footer. The path input on Scans is still freely editable — Browse just fills it in. Symlinks are filtered out of the listing (dirent isDirectory/isFile check skips them); hidden dotfiles are shown. Closes #1 fb9f899
2026-04-28 20:57 FileOrganizer feat(api+ui): serve resized image previews from /api/preview/:fileId The duplicates view tried to surface live image thumbnails but had no way to actually fetch the bytes — file:// URLs are blocked from the http://127.0.0.1 origin, so the cards just rendered the generic category icon. Add a Hono endpoint that streams a sharp-resized JPEG for any indexed file whose category is image and whose path lives under a registered drive's mount_path. Cap the requested max dimension, default 256px, and cache for five minutes. UI now renders <img src="/api/preview/<id>?max=192" loading="lazy"> inside the dup card stripe for image categories; non-image rows still fall back to the icon. The endpoint refuses non-images (400), unknown ids (404), missing files (404), and paths outside any registered drive (403). Closes #3 e0f5f35
2026-04-28 20:54 FileOrganizer fix(engine/organize): undo cross-drive rolls back source restore when dest unlink fails Previously the cross-drive undo path restored the source from quarantine and then unconditionally unlinked the destination. If the unlink threw — e.g. dedup had already moved the file, leaving ENOENT — we'd be left with a stale dest *and* the source back at its original location, with the catalog out of step on top. Snapshot the quarantine row before restore, and on unlink failure re-quarantine the just-restored source so we end up where we started. The op is then surfaced as failed with a "could not remove dest" message; the catalog only updates after both halves of the swap succeed. Closes #4 599f54b
2026-04-28 20:50 FileOrganizer fix(engine/scan): scope markMissing to scan roots, not drive-wide The previous query flipped every indexed file on the drive whose scan_id differed from the current one. That meant scanning a sibling folder would silently mark every file outside the new root as 'missing'. Add a path-prefix filter built from the scan's root paths (both / and \ child-separator forms) and require at least one root, so an empty array is a no-op rather than a drive-wide wipe. Closes #2 2809b24
2026-04-28 20:41 FileOrganizer docs: mark M7 done in README; v0.1.0 0b2c0d0
2026-04-28 14:47 FileOrganizer fix(ui): get the keyboard-shortcut tests green in CI Three small problems landed together, fixing them as one commit so CI doesn't stay red between hops: 1. vite.config.ts had no `test` block, so vitest defaulted to node instead of jsdom and ignored the preact JSX transform. Added `test.environment = 'jsdom'`. 2. Even with jsdom, vitest's esbuild step was emitting `React.createElement` from the .tsx test rather than running it through @preact/preset-vite. Rewrote the test to call `h()` directly instead of relying on the JSX transform — avoids the plugin/transform plumbing entirely and keeps the test fast. 3. `target.isContentEditable` evaluates to false in jsdom for nodes that haven't propagated computed style, so a contenteditable div was slipping past the editable-target guard. Hook now also inspects the raw `contenteditable` attribute (`""`, `"true"`, `"plaintext-only"`) so the test and the real browser both behave the same way. 11/11 UI tests pass locally; full root gate (191 engine + 8 shared + 11 ui = 210) green. 2101b02
2026-04-28 13:23 FileOrganizer fix(engine/catalog): reconcile queries batches by id, not batch_id The startup-reconciliation finalizer was selecting `batch_id` from the `batches` table, but `batches.id` is the PK — the column `batch_id` only exists on `operations`. CI failed across every M7 commit with `SqliteError: no such column: batch_id`. One-character fix in the SELECT plus alias to keep the destructure intact. c287b99
2026-04-28 11:47 FileOrganizer docs: expand README with daily-use, troubleshooting, architecture Merge the M7 plan's user-guide content into the existing README: add an Install-from-source block, a Daily use section that lists all eight UI screens with their keyboard shortcuts, a Troubleshooting section covering catalog locks, NAS disconnects, missing mediainfo, free-space aborts, and paused scans, plus an Architecture overview. Existing content preserved verbatim: screenshot reference, status table, prerequisites, "Is it safe to run?" safety section, and the License note. M7 row stays at "🚧 in progress" — T08 (perf) and T09 (manual e2e + version tag) are still pending. Task: M7-T07 ac34d91
2026-04-28 11:46 FileOrganizer chore(scripts): extract mediainfo zip into engine/bin/ on fetch After downloading the MediaInfo CLI zip, parse the ZIP central directory and inflate the requested entry directly into packages/engine/bin/<binary>, then delete the zip. POSIX builds get chmod 0o755. Writes go through a .partial sidecar + rename so a partial extraction can never look like a successful one. Plan called for the unzipper package, but the cloud sandbox blocks the npm registry entirely (zod, vitest, unzipper all 403'd), and shipping a package.json change without a matching lockfile update would break `npm ci` in CI. The ZIP format is small enough to parse with node:zlib's inflateRaw, so this avoids the new dep without sacrificing clarity. Supports stored (method 0) and deflate (method 8) — all that mediainfo's archive uses. Engine tolerates a missing mediainfo binary; failure path logs clear manual-install instructions and exits 1 instead of leaving behind a partial file. Task: M7-T06 cc8c75e
2026-04-28 11:42 FileOrganizer feat(ui): add keyboard shortcuts (g+letter chord navigation) Wire a global keydown listener that supports g+letter chord nav (gd dashboard, gb browse, gs scans, go organize, gu duplicates, gh history, gq quarantine, gr roles, gt throttle) plus '/' to focus the search box. Critical correctness: - Skip when target is input/textarea/select or contenteditable so typing inside RuleForm and similar editors works normally. - Skip when ctrl/meta/alt is held so browser shortcuts like Ctrl+G (find), Ctrl+T (new tab), Cmd+R, etc. still pass through. - Chord pairs reset after 1s so an accidental "g" doesn't leak into a later keypress. Tests cover the chord, single-key shortcut, every blur class (input/textarea/select/contenteditable), every modifier, the chord-timeout, and unmount cleanup. Task: M7-T05 2bfd3c6
2026-04-28 11:40 FileOrganizer feat(engine/organize): handle NAS disconnects during cross-drive copies When a `pipeline()` in moveCrossDrive throws a disconnect-class errno (ENOENT, EBUSY, ETIMEDOUT, ECONNRESET, ENETUNREACH), wrap it as a DriveError(DRIVE_DISCONNECTED) so callers can distinguish "the drive went away" from "this single file is broken." The applier catches DriveError, marks the in-flight op failed, finishes the batch as failed with disconnectedDrive label and errorCode in the summary, then re-throws so callers see the disconnect rather than a generic completion result. The HTTP API translates DriveError into a 503 with { error, code: 'DRIVE_DISCONNECTED' } on /api/organize/apply and /api/organize/auto-apply, so the UI can show a "drive went offline" banner instead of a vague 500. Tests simulate the disconnect by deleting the source file before the pipeline opens it (yielding ENOENT). EHOSTUNREACH is omitted since plain SMB/NFS disconnects surface as ENOENT/ETIMEDOUT in practice; ECONNRESET added to cover mid-stream cuts. Task: M7-T04 5f7ea1b
2026-04-28 11:36 FileOrganizer feat(engine/organize): pre-flight free-space check on apply Before any cross-drive copy work begins, sum estimated bytes per destination drive and compare against that drive's free_bytes minus a 5% safety margin (computed from total_bytes). If insufficient, abort the whole batch up-front: finish the batch as failed with a "insufficient free space" reason in the summary, throw before recording any operations or touching the filesystem. freeBytes is the snapshot from the last scan/upsert and may be stale; the 5% safety margin absorbs short-term churn. Same-drive moves are excluded since they don't consume new space. Task: M7-T03 9c2969b
2026-04-28 11:34 FileOrganizer feat(engine/catalog): add daily PRAGMA optimize runner Periodically run SQLite's PRAGMA optimize so query plans stay healthy as the catalog grows. Persists `lastOptimizedAt` in the settings k/v table and only re-runs after 24h elapsed. Wired into serve.ts: runs once at boot if due, then every 24h via an unref'd interval so it doesn't keep the process alive. Task: M7-T02 5226540
2026-04-28 11:34 FileOrganizer feat(engine/catalog): add startup reconciliation for interrupted operations On boot, scan operations stuck in `in-progress` state from a prior crash or kill. Decide each one based on filesystem evidence: if the destination exists with the recorded post_hash, mark completed; if only the source remains, mark failed; otherwise mark failed with an ambiguous message. Then finalize any batch whose ops are all terminal so it doesn't show as in-progress forever. Wired into serve.ts before the throttle scheduler starts so the engine never resumes work on top of an unresolved partial state. Task: M7-T01 3411d1b
2026-04-28 11:08 FileOrganizer docs: mark M6 done in README ce72f9d
2026-04-28 11:08 FileOrganizer feat(api+ui): add settings endpoint and Throttle editor Engine: - GET/PUT /api/settings round-trip the full Settings shape via SettingsRepo.load/save. - createServer accepts an optional onSettingsChanged(settings) hook. serve.ts uses it to tear down the old ThrottleScheduler and spin up a fresh one wired to the new manager, so a save on /api/settings takes effect on the running scheduler within one tick — no restart. UI: - Throttle screen with three profile cards (idle / balanced / full-send) editing localHashWorkers, networkHashWorkers, readChunkBytes, interChunkSleepMs, maxOpenFiles, and processPriority in place. - Weekly schedule grid: 7×24 cells; click a cell to cycle through empty → idle → balanced → full-send → empty. Each row has a "fill day" select for bulk operations. Empty cells fall through to whatever profile the engine was last set to. - Saved/unsaved pill in the header; Revert and Save buttons are disabled when the in-memory settings match the loaded copy. Sidebar gets a new Throttle entry between Duplicates and History (`g t` shortcut) and `app.tsx` mounts the route. Tests: two new server-side checks for settings GET/PUT round-trip and the onSettingsChanged callback firing exactly once per save. Task: M6-T04 da4e683
2026-04-28 09:53 FileOrganizer docs: mark M6 as in progress in README Roles repo, API, UI, scheduler runtime, and planner-roles wiring have all landed. T04 (throttle profile/schedule UI) is still pending visual verification, so M6 stays on the in-progress row rather than ticking over to done. Task: M6-T07 c387a3d
2026-04-28 09:52 FileOrganizer feat(engine): planner reads roles from RolesRepo by default Roles input on planOrganize is now optional and defaults to the catalog's RolesRepo.list(). Existing tests that pass roles inline keep working unchanged. The /api/plan/organize endpoint drops the body.roles field entirely so the catalog is the single source of truth, with the UI client signature simplified to take only driveRoots. Adds two planner unit tests (catalog-default and explicit-override) and an API integration test that demonstrates a role priority change in the catalog flips the planner's destination drive. Task: M6-T06 52857b9
2026-04-28 09:49 FileOrganizer feat(engine/throttle): add scheduler that watches the clock Wraps ThrottleManager with a ThrottleScheduler that polls every intervalMs (default 60s in serve), compares the schedule-derived profile to the active one, and only fires when they differ. The new throttle-changed engine event carries the new profile name so the websocket layer can fan it out later. Wired into runServe with start on boot and stop on SIGINT/SIGTERM, sharing the API server's EventBus. Task: M6-T05 d4b4b92
2026-04-28 09:48 FileOrganizer feat(ui): add Roles screen with drive priority editor Adds a Roles route that mirrors the dense pro-tool look of the Organize screen: each role is a card with the priority order shown as a numbered table, up/down reorder buttons, an inline fill- threshold input that commits on blur, and an "add drive" picker that lists only drives not already assigned. New roles are created from a modal whose drive picker uses the same numbered-pill order that the table uses. Adds Roles to the sidebar with role count, wires the route in app.tsx, and extends ApiClient with listRoles/createRole/updateRole/ deleteRole. Task: M6-T03 bfee376
2026-04-28 09:45 FileOrganizer feat(engine/api): add roles CRUD endpoints Mirrors the rules CRUD shape: GET /api/roles lists, POST creates, PUT /api/roles/:name updates (used for both metadata and reordering the drivePriority array), DELETE removes. Returns 409 on duplicate names, 404 on missing roles, 400 on invalid drive references. The integration test exercises create, list, two-step reorder, and delete; a second test exercises the error paths. Task: M6-T02 b43f874
2026-04-28 09:43 FileOrganizer feat(engine/roles): add roles repo backed by new SQL table Roles are now first-class catalog entities stored in a dedicated roles table (migration 0003) and accessed through a RolesRepo with the same CRUD shape as RulesRepo. Drive-priority entries are validated against the live drives table, and the Settings.roles field is removed so there is one source of truth. Defaults (media-archive, active-documents, document-archive) are seeded idempotently from cli/init and cli/serve, matching the seedDefaultRules pattern. Drive priority starts empty and the fill threshold defaults to 90 percent. Task: M6-T01 a915589
2026-04-28 stricter-reader-ledger loosen dead mailer for the stale consumer 8a69070
2026-04-27 22:17 FileOrganizer docs: mark M5 done in README 4fd40e8
2026-04-27 22:14 FileOrganizer test(engine): tighten test helper signatures so typecheck passes Two helpers were typed loosely enough that vitest would run but tsc --noEmit would fail under exactOptionalPropertyTypes: - moveSameDrive test's moveOpts() took Record<string, unknown>; tsc couldn't prove the spread produced a MoveSameDriveInput. Tighten to {fileId, destPath}. - role-resolver test's makeDrive() set id: over.id and then spread over again, which TS flagged as duplicate. Drop the explicit id — the spread provides it. No behavior change. dee820d
2026-04-27 22:01 FileOrganizer feat(organize): rule shadow detection in plan output planOrganize now returns ruleStats: per-rule {wouldMatch, actualMatch}. wouldMatch counts every file that satisfies the rule's match clause in isolation; actualMatch counts the files this rule actually wins under priority ordering. The gap exposes shadowing — a rule whose every candidate is being claimed by an earlier higher-priority rule. The Plan tab in the Organize UI shows a "Rule reach" row of pills under the unresolved-roles line: green when a rule fully owns its matches, info when partially shadowed, warn when fully shadowed (actualMatch=0 but wouldMatch>0). Hover for the exact reason. Task: M5-T10b 98917f7
2026-04-27 21:58 FileOrganizer feat(ui): build History screen with batch detail and undo Lists every batch sorted newest-first with a kind+status filter pair, a per-batch summary line built from batches.summary (completed/failed/reverted/skipped/count), and an inline expand row that fetches and renders the full operation list on demand. Undo button shows on completed non-undo batches. Click flow loads the batch's ops if not cached, gathers every drive id those ops touched, and either runs straight through (when every drive has a stored mount_path) or pops the DriveRootsPrompt for just the drives that need one — same pattern the dedup/restore screens already use. Status values render with semantic pill colors (ok/danger/warn/info) so completed-via-existing, dry-run, and reverted are visually distinct at a glance. Task: M5-T11 dc4e225
2026-04-27 21:36 FileOrganizer feat(ui): build Organize screen — rules editor + plan/apply/undo Two tabs in one card-headed layout, matching the rest of the UI's dense pro-tool feel. Rules tab: - Table with priority, name, summarized match, role + template, policy, and per-row Enable/Disable, Edit, Delete. - New-rule modal (RuleForm) with category chips, date bounds, path glob, min size, role, template, and move policy. The template field has a hint listing every supported placeholder. Plan tab: - Run-planner button asks for drive roots only when a drive is missing a stored mount_path; otherwise it goes straight through. - Operation table with row-click toggle, select-all, and a clear same/cross pill on each move. - Apply runs the selected ops; Dry-run preflight does the same path but with the dryRun flag so nothing gets written. - Unresolved roles show inline at the top of the plan. Sidebar's existing rule count slot is now wired to api.listRules in app.tsx, and serve.ts seeds the default ruleset on every boot (idempotent — no-op when rules already exist), so users with a pre-T01b catalog don't have to re-init to get the defaults. Task: M5-T10 bc3a4b0
2026-04-27 21:12 FileOrganizer feat(engine/rules): seed default rules on init seedDefaultRules(db) inserts six rules with priorities 100/110/200/ 210/300/310 covering photos, videos, recent vs archived documents, and recent vs archived audio. The "_archive" rules trail their "recent" counterparts and split on a 2-year cutoff (today minus 2y), so the planner reaches for the archive bucket only when the date falls outside the recent window. Init wires this in: a fresh catalog comes up with a working ruleset, and a second runInit on a non-empty rules table is a no-op so user edits aren't clobbered. Task: M5-T01b bff2aef
2026-04-27 21:09 FileOrganizer feat(engine/api): add rules CRUD, plan, apply, undo, batches endpoints GET /api/rules → list rules (priority asc) POST /api/rules → create PUT /api/rules/:id → update (404 if unknown) DELETE /api/rules/:id → delete (204) POST /api/plan/organize → run planOrganize, return plan POST /api/organize/auto-apply → autoApply (executes same-drive autos) POST /api/organize/apply → applyApprovedBatch (supports dryRun) POST /api/organize/undo/:id → undoBatch GET /api/batches → recent batches GET /api/batches/:id → batch + operations driveRoots are merged with each drive's stored mount_path the same way the dedup endpoints do, so callers only need to override drives without a stored mount. Task: M5-T09 cb234ba
2026-04-27 21:02 FileOrganizer feat(engine/organize): add undo with hash re-verification undoBatch reverses every completed op in a batch in reverse order, under a new batch with kind='undo'. Each reversal re-hashes the destination and bails out if it no longer matches the catalog's sha256 (so tampering or drift since the original move is detected before the undo touches anything). - Same-drive moves rename back to source. - Cross-drive moves restore the source from the original batch's quarantine and unlink the destination. - completed-via-existing collisions restore the source from quarantine without touching the unrelated existing file at the destination. - Anything not 'completed' or 'completed-via-existing' is recorded as skipped. The undo batch finishes 'completed' iff every reversal succeeded; otherwise it's 'failed' and per-op errors are returned to the caller. Task: M5-T08 4b34f9a
2026-04-27 20:54 FileOrganizer fix(engine/organize): record actual finalDestPath on suffix rename When a collision forces the mover to rename the source to a suffixed filename (a.jpg → a_1.jpg), the operation row now reflects the file's real on-disk location instead of the planner's intended destPath. Without this, undo would look at the wrong file when re-verifying or reversing the move. BatchesRepo.updateOperationStatus accepts an optional destPath; the applier passes outcome.finalDestPath whenever it differs from the planner's destPath. 3c9f5ce
2026-04-27 20:50 FileOrganizer feat(engine/organize): dry-run mode for applyApprovedBatch dryRun=true creates the batch and records every op with status 'dry-run', without touching the filesystem. Source hash is still re-verified against the catalog so dry-run surfaces hash drift before the user commits to a real run; on drift, the op is recorded as failed with the hash-mismatch reason. Task: M5-T07b 2fd37d2
2026-04-27 20:45 FileOrganizer feat(engine/organize): add auto-apply + approved-batch orchestrators autoApply walks a planner's PlannedOperation list, drops noops, auto-runs same-drive moves whose rule is `same-drive-auto` (under one auto-batch with kind=move), and returns the remaining ops as a reviewQueue for explicit user approval. applyApprovedBatch executes a user-approved op list under a single batch, recording each op (move or copy) and continuing past failures so partial progress is preserved. Same-content collisions are recorded as completed-via-existing; any thrown error becomes a failed op with errorMessage. The batch finishes as 'completed' iff every op completed. Task: M5-T07 28e4d47
2026-04-27 20:42 FileOrganizer feat(engine/organize): collision handling in movers (never overwrite) resolveCollision(destPath, sourceHash, chunkBytes) returns one of use-as-is / same-content / suffix and is shared by both movers. Both movers now return MoveOutcome { kind: 'moved' | 'completed-via-existing', finalDestPath } so callers can record the right OperationStatus. - Same hash at destination: source is quarantined and the file row flips to state='quarantined'; nothing is overwritten. Outcome: completed-via-existing. - Different hash at destination: destination filename is suffixed (a.jpg → a_1.jpg, walking up to _999), source is renamed/copied there. moveSameDrive now also takes driveRoot + batchId + chunkBytes so it can hash the destination and quarantine on same-content collisions. Task: M5-T05b c87735e
2026-04-27 20:36 FileOrganizer feat(engine/organize): add same-drive and cross-drive move executors moveSameDrive does an atomic rename, mkdir-p the parent, and flips the catalog row to state=moved. moveCrossDrive streams the source to its destination on the other drive, re-hashes the written file, throws IntegrityError on mismatch, quarantines the source under the source drive's quarantine root, and updates the catalog row's path + drive_id + state in one shot. Collision policy (same-content / different-content) handled in M5-T05b. Task: M5-T06 9acea5f
2026-04-27 20:34 FileOrganizer feat(engine/organize): add organize planner planOrganize walks indexed files, picks the first matching enabled rule, resolves the rule's destination role to a connected drive with space, renders the template against the file, and emits a PlannedOperation tagged 'same-drive-move', 'cross-drive-move', or 'noop' (when source path already equals destination). Files that match nothing show up under `unmatched`; rules whose role is unknown, unresolvable, or has no drive root supplied appear in `unresolvedRoles` with a human-readable reason. Task: M5-T05 520c5a1
2026-04-27 20:16 FileOrganizer feat(engine/rules): add role resolver resolveRole walks a role's drivePriority list and returns the first drive that is registered, connected, and below the role's fill threshold. Returns null with a human-readable reason when nothing qualifies, so callers can surface the rejection. Task: M5-T04 ea2b38a
2026-04-27 20:11 FileOrganizer feat(engine/rules): add destination template renderer renderTemplate(template, file, driveLabel) substitutes {year}, {month}, {day} (with optional zero-pad like {month:02}), {filename}, {stem}, {ext}, {category}, {drive_label}. Date fields read from exifDate first, falling back to mtime; throws RuleError when no usable date is present or when an unknown field is referenced. Task: M5-T03 d2e5d9e
2026-04-27 19:45 FileOrganizer feat(engine/rules): add rule matcher with glob support matches(file, rule) checks category, dateBefore/dateAfter against exifDate or mtime, dateSourceMin floor, size bounds, pathGlob via picomatch, and sourceDrives membership. firstMatch(file, rules) walks rules in order and returns the first enabled match (or null), so the caller controls precedence by ordering. Task: M5-T02 11ec7a7
2026-04-27 19:42 FileOrganizer feat(engine/rules): add rules repository CRUD for the rules table: create, update, delete, list (ordered by priority ASC), findById. Match shape persists as match_json; enabled defaults to 1; policies are typed via shared MovePolicy and QuarantinePolicy. Task: M5-T01 5b29bf1
2026-04-27 15:50 FileOrganizer fix(build): build @fileorganizer/shared before typecheck/test CI failed on a fresh clone because engine and UI typecheck both import from @fileorganizer/shared (resolved via package.json main/types to dist/), but dist/ doesn't exist until shared is built. Two changes: 1. Root typecheck and test scripts now build shared first. 2. Shared's build uses 'tsc -b --force' so a stale .tsbuildinfo can't claim emit is up-to-date when dist/ has actually been deleted. Verified locally: rm -rf shared/dist plus delete .tsbuildinfo, then both typecheck and test run green. 732db0b
2026-04-27 08:21 FileOrganizer chore(docs): add screenshot d2af869
2026-04-27 08:14 FileOrganizer docs: reserve screenshot slot in README da272fc
2026-04-27 07:35 FileOrganizer docs: rewrite README with hook, status, prerequisites, safety notes d8bc70f
2026-04-27 service-producer test: simplify producer ed03f1f
2026-04-26 23:20 FileOrganizer feat(drives): persist mount_path so dedup/restore stop asking for it 4b5569c
2026-04-26 22:57 FileOrganizer feat(ui): wire Duplicates and Quarantine screens to live engine data c99b261
2026-04-26 22:53 FileOrganizer feat(engine/api): add duplicates and quarantine endpoints 7b19afe
2026-04-26 22:34 FileOrganizer feat(engine/dedupe): add planner and applier with hash-revalidation safety fc9be4c
2026-04-26 22:25 FileOrganizer feat(engine/dedupe): add keeper scoring with documented reasons de005b9
2026-04-26 22:23 FileOrganizer feat(engine/dedupe): add duplicate group detection query 433a9b3
2026-04-26 22:08 FileOrganizer feat(engine/quarantine): add quarantine + restore primitives 54460f0
2026-04-26 21:54 FileOrganizer feat(ui): rebuild UI to match design handoff 25ee273
2026-04-26 20:15 FileOrganizer chore(repo): gitignore .claude/ (untrack accidentally-committed settings) 42b4c30
2026-04-26 20:15 FileOrganizer docs(plan): trim cloud-agent specifics; running locally now 42c2a58
2026-04-26 17:57 FileOrganizer fix(api): /api/scans returns ScanRecord shape, not raw SQLite rows 6f6e49b
2026-04-26 17:04 FileOrganizer feat(ui+scan): live scan progress with active-scan card efdf59a
2026-04-26 16:51 FileOrganizer fix(cli): serve command must not return until shutdown signal 090e961
2026-04-26 16:35 payments-service rebuild scheduler so shim can bump controller 6dcb6db
2026-04-26 16:26 payments-service guard consumer 58ac08c
2026-04-26 15:09 FileOrganizer feat(cli): npm run start one-command bootstrap f303962
2026-04-26 14:27 FileOrganizer feat(api+ui): scan a folder from the UI without a pre-registered drive b51a827
2026-04-26 14:27 payments-service chore: vendor dispatcher for handler 548818a
2026-04-26 14:08 payments-service tidy leaky supervisor when config changes ad1c246
2026-04-26 14:07 FileOrganizer feat(engine/catalog): add batches and operations repository 766d25e
2026-04-26 13:51 FileOrganizer feat(engine/api): serve built UI assets from the engine 9c57174
2026-04-26 13:40 FileOrganizer feat(ui): add Dashboard with drive cards 5ca1eee
2026-04-26 13:11 FileOrganizer feat(engine/api): run scans in background from POST /api/scans 38f3824
2026-04-26 13:09 payments-service chore: remove buffer so loader can rework facade f146f73
2026-04-26 13:09 FileOrganizer feat(ui): add Browse screen with paginated file list ffacc74