|
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
|