List query gains kind/since/organizeOnly and wildcard/hash search; new
GET /organization-records/stats and POST /organization-records/delete
(optional source/library files to trash via files delete). Public record
adds posterPath, sizeBytes and download summary.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Files page uses a wide layout (route handle layout:"wide"): no max width,
full-height directory tree docked next to the sidebar, resizable
(200-520px, remembered, double-click resets) and collapsible; mobile keeps
the tree in a drawer.
- GET /files/preview (admin console session only, same path guards as
download): text/NFO with encoding detection (BOM/UTF-16/UTF-8/GB18030/Big5),
subtitles first 200 cues (SRT/VTT/ASS/SSA, PGS timings), .torrent file
list/trackers (passkey masked)/infohash, image dimensions. Reads at most
1 MiB of text; .torrent capped at 4 MiB.
- Download also allows text files, .torrent and avif/gif.
- Research notes in docs/design/file-preview-research.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entities created from TMDB candidates (subscription, search/TMDB preview, organize wizard)
and manual assignment to an unregistered TMDB identity take title/overview from the
metadata.languages chain at creation and enqueue refreshMetadata full=1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- glossary: 10 punctuation rules (full-width in Chinese, CJK/Latin spacing,
full-width parens around Chinese, single ellipsis, 「」 quotes, period rules)
- scripts/check-copy-punctuation.mjs: checks string literals, templates and JSX
text (not comments, types, keys, match/compare strings); --fix, --strict,
--by-file; wired into deno task ci as check:copy in warn mode
- --fix applied to web, server and contracts; files other lanes were editing
(subscriptions, sites, browser, agent, auth/account, inventory) left for them
- tests updated for the changed messages
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- G6: FileRoot.pendingCount/pendingPartial for download roots (same rule as the
unidentified filter, recursive, cached 5 min, invalidated by files.changed)
- G7: FileEntry.parsedHint from parseReleasePath (file + parent dirs); the web
subline uses it and no longer parses names itself
- G8: FileIdentifyCandidate.posterUrl (TMDB poster_path via adapter and
catalog.lookup, local entities from metadata.posterPath); thumbnails in the
identify panel
- S-7 contract: FilesIdentifyResult.remoteTaskId/remotePending,
MediaServerInventory.partial, EventType identify.completed with
IdentifyCompletedPayload; identify panel (useIdentifyLive) and organize wizard
merge remote results by root + path, buffering events that beat the response
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- GET /inventory/media-servers: per-instance Promise.allSettled, per-instance
concurrency cap (2) and timeout (8s); failed/timed-out instances are unknown,
response carries partial=true (field not yet in contracts)
- S-7: POST /files/identify returns local matches plus remoteTaskId/remotePending
and enqueues identifyRemote; the worker runs the TMDB lookups and publishes SSE
identify.completed {taskId,status,items,truncated,excludedBySize,error};
?wait=true keeps the synchronous path, capped at 15s overall
- catalog.lookup / release.search honour the caller's abort signal
- scripts/dev-db-clean.ts: dry-run by default, --yes to drop leftover test
schemas and w32-/w33-/fe4-/qa- prefixed entities; refuses non localhost:55433
- runbook: dev DB cleanup
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- planImport (sourcePaths mode): when a selected source is a video file,
same-directory subtitles / external audio whose stem (language tags
stripped) matches it join the source list, so the organize wizard, which
expands a directory into individual videos, no longer drops them.
Unrelated sidecars stay out; pathGuard applies to the added files.
- Organize records page honours ?ids= (wizard multi-group redirect): shows
just those records via /files/records, refreshes while running, clear chip.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- site.list / site.health_check / site.refresh_login / site.user_data capabilities
over the W9-A site center services; outputs whitelisted (no secret refs,
cookies, passwords, login names); execute kinds need automation grant.
IDs use snake_case (CapabilityId regex); registry count 17 -> 21.
- settings.organize.minSizeBytes (default 50 MiB); file-manager identify /
organize and planImport take request > setting > port default.
- tests and runbook notes (sites, files).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- POST /files/suggest-name renders the effective template (entity > root >
global) with the same variables as organize: file name segment for files,
work/season dir segment for directories; identity_uncertain when no entity
- planImport plan.naming.templateSource/templateVersion now come from
effectiveNaming + the registered template version (fixes the leftover
DEFAULT_TEMPLATES reference after the naming chain landed)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- G1 createEntityFromTmdbCandidate (sources-identity service): entity + accepted
manual external identity + season/episode units, idempotent per TMDB id;
POST /files/entity-from-candidate and organize items with `external`
- G2 minSizeBytes for organize (excluded below_min_size) and identify
(skipped + excludedBySize)
- G3 onExisting passed to planImport; dryRun marks each file
(same/skip/replace/conflict); replace = explicit grant
- G4 naming template|keep; planImport namingMode/namingSource, frozen
plan.naming {mode, templateSource, templateVersion}; targetSegments
- G5 identify prior-record hints, GET /files/record-sources, organize
refreshMetadata, POST /files/scrape-batch (dedupe by entity)
- G8 GET /files/tree (parent, subdirs hasChildren, video/registered counts)
- G9 POST /files/suggest-name returns 501 until the naming engine lands
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- POST /files/probe writes media_asset.media_info, so scope organize.read -> organize.execute
- e2e e6-w4a-endpoints -> e10-w4a-endpoints (number clashed with e6-auto-recover)
- trash: reusing a consumed approval / retired planId is rejected and the file stays
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- invoke-limits: sharedInvokeLimiter() (one semaphore set per process); agent
capability routes default to it
- /files/identify: catalog.lookup with includeRemote runs under the shared
TMDB semaphore + 15s timeout; at most 20 distinct remote queries per
request, further items fall back to local recall with a warning
Co-Authored-By: Claude Opus 5.5 <[email protected]>
S-1 (/files/move, /files/organize action=move):
- organize-guard: authorizeMoveEntities checks every source's media entities
(declared entity plus entities of registered assets under the source);
one-time approval still covers the whole request; any source that cannot
be attributed to an entity only passes with an approval; denial lists every
unauthorized item; optional non-consuming pre-check
- transfer: entities per source (directory = registered assets beneath it)
- organize: all move groups pre-checked before any group executes; any
denial -> whole request 403 approval_required
S-2:
- api.ts: GET /files/download and POST /files/trash/purge marked
auth:"session"; files routes attach requireAdminSession from the registry
annotation; handler-side purge check removed, download keeps a same-rule
defense check; permanent delete uses isAdminSession
- DEV key rule written down: equals an admin-scope key for session-only
endpoints, still admin for Move/delete approval bypass (as authorizeMove)
- api-registry.test: session list updated; new reverse assertion that no
non-session endpoint answers 403 to an admin-scope bearer
S-5 (trash restore):
- restore-to requires a writable managed library root (isWritableTarget)
- in-place restore rebuilds missing parents level by level without
following symlinks (mkdirWithin), realpath-checked per level
- cross-volume restore capped by FILES_TRASH_CROSS_MAX_BYTES; cross-volume
copies (trash in / restore out) go through fsproxy
Tests: multi-entity move 403 + listing, unattributed source, organize foreign
asset 403, download/purge 403 for DEV and admin-scope keys, restore target
not writable, symlinked parent, rebuild parents, cross-volume cap.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- /files/move|copy go through planImport's new `transfer` variant (keep names, directory
expanded to per-file plan) → organization_record + verifyAndImport/executeRecord: per-file
ledger resumable, fsproxy, cross-volume directory move = per-file copy→verify→delete then
remove empty source dirs; shared plan store + plan_stale; asset_location follows per file,
path_binding revised when a whole directory moved (finalizeTransfer); request waits up to
FILES_TRANSFER_WAIT_SEC, else executed.pending
- /files/organize per-item overrides take effect: season/episodes (domain planOrganize
overrides), contentUnitId coverage, targetDir (no binding), operation, slotId (satisfied
after verify); warnings only for inapplicable overrides
- delete/trash/restore write organization_record (action trash|delete|restore, file-level
results; migration 0026)
- GET /files/download (admin session, nfo/subtitle/image, size cap, streamed, audited)
- GET /files/records?ids|planIds for the organize page
- fs race errors map to 503/404/403/400 instead of 500
- runbook updated
Co-Authored-By: Claude Opus 5.5 <[email protected]>