Repository navigation
v0.0.1-beta
Pre-releaseRelease Notes — v0.0.1-beta
| Date | 2026-05-14 |
| Range | 7cd5272 .. 2723fa6 |
| Commits | 37 individual / 12 merge commits |
| Components | Backend (Go + SQLite), Frontend (SvelteKit + Svelte 5 + Tailwind 4) |
| Verification | Type-check, production build, backend integration tests, end-to-end browser tests |
First formal release. Six new features were merged into main, six user-reported issues were resolved in a single batch, and the UI tone (typography, spacing, triggers) was retuned.
1. Features
1.1 Folder Upload Hardening
Both code paths — OS folder drag/drop and the Upload folder picker — are hardened to the same level.
- Recursion depth capped at 32. Exceeding the limit emits a one-shot warning toast and preserves the partial upload.
- Dot-prefixed segments (
.git,.DS_Store, intermediate.hidden/, etc.) are skipped at every path depth. FileSystemDirectoryReader.readEntriesis wrapped in try/catch — partial read failures preserve the already-collected files and continue.- Drop completion surfaces
Queued N file(s) for uploadorNo uploadable files in dropped folder. window.__nasTestEnqueueFolderDEV-only harness exposed; theimport.meta.env.DEVguard tree-shakes it from production builds.- Backend regression test added (
TestTusUploadCreatesNestedFolders) covering 3-level / 5-level nested and root-level uploads — verifiesos.MkdirAll(0o755)behaviour.
1.2 File Details (Size, Modified, Inspector)
- Backend
fileEntryJSON gainssize: int64andmodifiedAt: RFC3339. Populated via a singleentry.Info()call — no additional stat round-trips. - List view:
Sizecolumn (right-aligned, two-decimal precision,—for folders) andModifiedcolumn (just now/Nm/Nh/Nd/Mon D, YYYY). The Modified cell carries atitleattribute with the full timestamp. - Grid view: file tile
titlebecomes"name\n1.00 MB · 2h". - Inspector side panel (
w-[360px], mirroring the UploadPanel pattern): icon + name / Type / Size / Modified / Path. Trigger behaviour is described in §3.3.
1.3 Activity Graph & Git-style Timeline
aggregate.tsmodule:dailyCounts(entries, days=30)(local-date buckets, zero-filled) andtypeDistribution(entries)(OPEN→VIEW fold, ratio-guarded).ActivityGraph.sveltebanner: 30-day sparkline (accent yellow#fabd2f, height 48, ceiling = local max) + distribution stacked bar + legend pills (title="N · X%").- The timeline is grouped under sticky day-section headers (
Today / 2026-05-14 / 10 events). Each row is a three-column layout:HH:MMmono · activity dot + author-initial chip · activity type +[TARGET]chip + author name + path. MOVEentries are decomposed by aparse()helper into/from→/towith a lucideArrowRightbetween them.UPLOAD/CREATE/DELETErender as a single mono path.- Side fix:
ActivityLog.load()silently dropped responses shaped as{data: [...]}— corrected as part of the same change.
1.4 Music and Video Library Tabs
- Backend route
GET /mediaLibrary?kind=audio|videoadded. Walks the NAS viafilepath.WalkDirwithfs.SkipDiron dot-prefixed directories. When the result count reachesMEDIA_LIB_LIMIT(default 5000) the walk exits early and the response carriesX-Library-Truncated: true. - The
mediaEntrypayload separatesloc(directory) andname(filename), matching the existingFilePayloadshape — the frontend can hand it straight to a viewer tab without splitting. MusicLibrary.svelte: compact list grouped byentry.loc+ search filter + IntersectionObserver pagination (200 initial + 200 per sentinel).VideoLibrary.svelte: 16:9 grid (auto-fill, min 220px),filmicon placeholders, IntersectionObserver pagination (60 + 60).- Clicking a row or tile opens the existing
MediaViewertab. TheKIND_TO_ICONmaps in the sidebar, TabBar, and QuickOpen are updated to cover the new kinds.
1.5 Mobile UI Support
Project-wide responsive conversion. The existing ui.svelte.ts isMobile / breakpoint / viewportWidth / viewportHeight fields are reused; the store is extended only with drawerOpen.
.h-apputility (100vhfallback +100dvhoverride) — avoids iOS Safari URL-bar clipping.(app)/+layout.sveltegrid:grid-cols-[1fr] md:grid-cols-[auto_1fr]. The sidebar is wrapped inhidden md:contentsso it leaves the DOM flow on mobile.- New
MobileDrawer.svelte:fixed inset-0 z-50backdrop + left-anchored w-64 panel. Body scroll is locked viadocumentElement.style.overflow = 'hidden'. Esc and md+ resize both auto-close. Header.sveltemobile condense: hamburger button (menuicon,md:hidden); logo, username, and Quick-open label arehidden md:flex.Toolbar.svelte+ newToolbarOverflow.svelte: only Up / Upload (icon) / View toggle stay visible on mobile; Upload folder, New folder, and inline search collapse into a...overflow dropdown. The search input is reached via a dedicated toggle that reveals a full-width bar.TabBar.svelte: mobile-only width shrink (min-w-[100px] max-w-[140px]), always-visible close X (md:opacity-0 md:group-hover:opacity-100), anddraggable={!ui.isMobile}to disable reorder.UploadPanel.svelteandInspector.sveltebecome bottom sheets on mobile (fixed inset-x-0 bottom-0 max-h-[70dvh] rounded-t-lg pb-[env(safe-area-inset-bottom)]).QuickOpen.sveltegoes fullscreen on mobile. AvisualViewport.heightlistener keeps the search input above the soft keyboard.- Touch input: new
long-press.tsSvelte action (500 ms hold, cancels on >8 px movement) unifies the context-menu invocation path. Entries setdraggable={!ui.isMobile}to disable HTML5 drag on touch devices. - Viewer responsiveness:
MonacoViewersetswordWraptoonon mobile.VideoControlsandPdfViewersecondary controls collapse behind a...overflow. - During code review, the same outside-click pattern was found duplicated across three components; it was extracted into
frontend/src/lib/actions/click-outside.ts.
1.6 Marquee and Multi-Select
Per-tab isolated multi-selection, marquee rectangle selection, and batch operations.
- Selection state lives in
ExplorerPayload.selection?: string[](the last item is the Shift range anchor). - Click modifier precedence:
Shift > Ctrl/Cmd > none.setSingle/toggleEntry/selectRange/clearare pure functions infrontend/src/lib/components/Explorer/selection.ts. - Keyboard:
Ctrl/Cmd+A(select all),Esc(clear),Delete(batch delete viaPromise.allSettled, then aDeleted N (M failed)summary toast). All shortcuts short-circuit when the focused element is editable. - Marquee: pointer-down/move/up handlers with a three-layer guard (
data-marquee-canvasancestor check, scrollbar-offset rejection, editable-target rejection), 32 px edge auto-scroll at 8 px/frame with entry-rect re-snapshot, and a 4 px dead zone separating plain clicks from drags. - Drag payload uses a batch shape
{items: Array<{name, isFolder}>, sourceLoc}. Legacy single-entry payloads are wrapped automatically byreadPayload(back-compat). - Mobile selection mode: long-press to enter → tap toggles → top bar with
N selected+ Cancel (X) + Delete (trash).
2. Fixes
2.1 User Feedback Batch
Five reported issues plus one discovered during cleanup were resolved together.
| # | Issue | Root cause | Fix |
|---|---|---|---|
| 1 | Search bar autofilled the login ID | Browser autofill heuristics | All four search inputs received type="search" + name="*-q" + autocomplete="off" + autocorrect="off" + spellcheck="false" |
| 2 | .avi / .mkv / .mov streaming failed |
Backend mime fell back to video/mp4; browsers cannot decode the containers natively |
Added video/x-msvideo, video/x-matroska, video/quicktime mappings to contentTypeFor. VideoPlayer got an <video onerror> handler that overlays "Browser cannot play this format" with a Download button |
| 3 | Global search did not work | QuickOpen filtered open tabs only | Added a /server/searchInAllFiles call — 180 ms debounce + monotonic token cancellation. Tab hits and file hits are sectioned separately; clicking a file opens the Explorer or a viewer tab |
| 4 | Stale entries after rapid post-mount navigation | Mount-fetch resolved after navigate-fetch and overwrote the newer state | Explorer.refresh() now uses a monotonic refreshToken — newer in-flight calls invalidate older responses |
| 5 | Folder rename did nothing | A reactivity loop blocked refresh, leaving the UI stale |
Resolved as a side effect of §2.2. Verified with renameTest → renamedFolder end-to-end plus filesystem check |
| 6 | (Discovered) Search returned loc: "/." for files at the NAS root |
filepath.Rel(baseDir, dir) returns . for the root parent |
Normalized "/." → "/" |
2.2 Explorer Reactivity Loop
Symptom: after page load the "Loading…" indicator never cleared, and the console emitted effect_update_depth_exceeded. Delete and rename actions also failed to refresh entries.
Cause: a $effect introduced with the multi-select work wrote to tabs.list via updateSelection. TabContent then re-evaluated <Explorer loc={tab.payload.loc}>; Svelte 5 treated the prop re-evaluation as a change, the effect re-fired, and the loop never converged.
Fix:
- Inspector
selectedmoved from$state + $effect mirrorto a pure$derived(write path removed). - The loc-change
$effectno longer callsupdateSelection([]). Navigation handlers (navigateTo, etc.) setpayload.selection = []explicitly. - The title-update
tabs.updatecall is wrapped inuntrack().
After this fix, folder rename and delete refresh automatically as intended.
3. User-Interface Refinements
3.1 Typography — IBM Plex Sans, Weight 550 Floor
- Latin variable font (
@fontsource-variable/ibm-plex-sans, axis 100–700) plus Korean static font (@fontsource/ibm-plex-sans-kr, weights 500/600/700). --font-sans: "IBM Plex Sans Variable", "IBM Plex Sans KR", "Inter", ui-sans-serif, ....htmldefault weight set to550(font-variation-settings: 'wght' 550). Korean text snaps to the nearest static weight, 600.- Tailwind weight helpers remapped:
font-medium → 600,font-semibold → 650,font-bold → 700. The 400–500 range is no longer used; 550 is the new floor. font-mono,code,pre,kbd,sampkeep FiraD2 withfont-variation-settings: normalsince the mono is a static font.
3.2 Background Context Menu
Right-clicking the file canvas blank space now opens a folder-level menu.
- Entries:
New folder·Upload files·Upload folder·Refresh. - Reuses the marquee three-layer guard — never fires over an entry or an editable input.
- Mutually exclusive with the entry context menu (
bgMenuOpen↔menuOpen).
3.3 Inspector — Explicit Trigger
Previously a single click opened the Inspector and obscured the file area. The Inspector now responds only to explicit selection.
- Single click: applies the selection ring (
bg-bg-elevated ring-1 ring-accent) only. The Inspector does not open. - Right-click →
Details(lucideInfoicon) → Inspector opens. - While the Inspector is open, clicking a different entry does not swap its target — only the explicit close (X) or a folder navigation closes it.
- If the inspected entry disappears (deleted, renamed, filtered out),
$derived inspectedExistsplus an$effectwrapped inuntrackclose the Inspector automatically without creating a write-back loop.
4. Infrastructure and Refactors
- Dead-code removal:
viewerIconName()inregistry.tsand unused methods/state onFilesStore. - Drag-drop handler duplication between
FileGridandFileListconsolidated into thedragEntrySvelte 5 action. click-outside.tsaction extracted:ToolbarOverflow,VideoControls, andPdfViewershared the same outside-click pattern; consolidated into a single action (caught during code review).long-press.tsaction introduced: a shared entry point for mobile context-menu and selection-mode triggers.- Docker
PORTenv consistency: the DockerfileHEALTHCHECK(shell-form) anddocker-compose.ymlinternal container port binding both honour$PORT. A single env value now applies across the full deployment chain. - Temporary-data cleanup: gitignore patterns at repo root and
backend/were unified, 12 previously-tracked TUS scratch files were untracked, andtmp/README.mdwas added as a guide. Tool cache directories that write to fixed paths (data/nas/*,data/tmp/, etc.) are gitignored separately.
5. Verification Summary
Pre-merge static checks, production build, backend integration tests, and end-to-end browser scenarios all passed. The six user-reported issues were individually verified via browser checks and DOM probes.
End-to-end scenario coverage (per-feature step counts):
| Feature | Scenarios |
|---|---|
| Folder upload | 7 |
| File details | 9 |
| Activity graph | 8 |
| Media libraries | 8 |
| Mobile UI | 18 |
| Marquee + multi-select | 21 |
6. Known Limitations and Follow-ups
The following items are out of scope for this release and tracked as follow-ups.
- Notifications Toaster not implemented:
notifications.svelte.tsfills a queue, but no component renders it — every toast is currently silent. Slated for a separate PR. - iOS Safari device verification:
safe-area-inset-bottomandvisualViewportkeyboard tracking cannot be fully emulated by desktop browser automation. Real-device verification deferred. - Decomposition of
Explorer.svelte: the single component has grown to 637 lines. Splitting the marquee / shortcuts / selection / drag-move concerns into four composables is tracked in the backlog.
Appendix — Modified Files
Backend
backend/
internal/config/config.go — MediaLibraryLimit field + MEDIA_LIB_LIMIT env loader
internal/files/handlers.go — fileEntry { size, modifiedAt } / MediaLibrary handler / Search loc normalization / Rename
internal/server/router.go — /mediaLibrary route
internal/stream/handlers.go — .avi / .mkv / .mov mime mappings
tests/integration/files_test.go — TestReadFolderDetails + TestMediaLibrary
tests/integration/tus_e2e_test.go — TestTusUploadCreatesNestedFolders
Dockerfile / docker-compose.yml — PORT env consistency
Frontend
frontend/src/
app.css — IBM Plex Sans + 550 floor + .h-app utility + Gruvbox tokens
lib/store/ui.svelte.ts — drawerOpen + body-scroll-lock + resize auto-close
lib/types.ts — TabKind music-library/video-library + MediaEntry + ExplorerPayload.selection
lib/actions/click-outside.ts — new (extracted from 3-component duplicate)
lib/actions/long-press.ts — new (mobile context menu / selection entry)
lib/components/
ActivityLog.svelte — git-style timeline + parse() helper + response-shape parsing fix
Activity/aggregate.ts — new
Activity/ActivityGraph.svelte — new
Explorer.svelte — multi-select, marquee, batch move, explicit Inspector trigger, background context menu
Explorer/format.ts — new (formatBytes / formatRelTime / formatFullTime)
Explorer/Inspector.svelte — new (w-[360px] side panel)
Explorer/selection.ts — new (toggleEntry / setSingle / selectRange / clear)
Explorer/ContextMenu.svelte — Details menu item + Info icon + onInspect prop
Explorer/drag-drop.ts — DragPayload {items, sourceLoc} + legacy wrap + performMoveBatch
Explorer/Toolbar.svelte — autocomplete=off + mobile condense + searchInput snippet
Explorer/ToolbarOverflow.svelte — new (mobile toolbar dropdown)
Explorer/uploads.ts — folder upload dotfile skip + summary toast
Library/loader.ts — new (loadLibrary(kind))
Library/MusicLibrary.svelte — new
Library/VideoLibrary.svelte — new
QuickOpen.svelte — mobile fullscreen + global file search + visualViewport
Shell/Header.svelte — hamburger + mobile condense
Shell/MobileDrawer.svelte — new
Shell/nav-items.ts — new (NAV_ITEMS shared)
Shell/StatusBar.svelte — safe-area-inset-bottom padding
Shell/VerticalNav.svelte — simplified via nav-items.ts import
Tabs/TabBar.svelte — mobile width shrink + close always visible + drag disabled
Tabs/TabContent.svelte — music-library / video-library routing
Uploads/DragDropOverlay.svelte — depth cap + dotfile + DEV harness
Uploads/UploadPanel.svelte — mobile bottom-sheet variant
Viewers/VideoPlayer.svelte — download fallback overlay on codec error
Viewers/VideoControls.svelte — mobile overflow + click-outside action
Viewers/PdfViewer.svelte — mobile overflow + click-outside action
Viewers/MonacoViewer.svelte — mobile wordWrap=on