Commit Graph

47 Commits

Author SHA1 Message Date
H1K0 7a0c57a79c feat(frontend): finish PWA reset reload and pre-cache the HTML shell
The service worker and web manifest were already in place; close the remaining
gaps against the PWA spec:

- pwa.ts: resetPwa() cleared caches and unregistered the service worker but
  never reloaded, despite its docstring. Add the hard reload so the page
  re-fetches everything from the network after a clear.
- service-worker.ts: pre-cache the SPA entry HTML ('/') alongside build/files
  so the shell — and the offline navigation fallback — works from the very
  first visit, not just after a navigation has been seen by the runtime cache.
- settings: resetPwa now reloads, so the post-clear success toast (which told
  the user to reload manually) is unreachable and misleading. Drop the dead
  pwaSuccess state and toast; keep the disabled "Clearing…" button state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 10:36:20 +03:00
H1K0 1f3bc2acf4 fix(frontend): open pool files in an overlay so back returns to the pool
Tapping a file in a pool did a full goto('/files/<id>') to the standalone
viewer route, whose close button always routes to /files — so returning from
a file viewed inside a pool dropped the user on the global files list instead
of back in the pool.

Open the viewer as an overlay over the still-mounted pool grid via shallow
routing, mirroring the files grid: pushState keeps the pool URL (the overlay
is driven by page.state.fileId), and the back button / close does
history.back(), returning to the pool with its list and scroll intact.
Neighbours follow the pool's own order, paging in more pool files near the
end, and closing scrolls the grid back to the last-viewed file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 10:22:34 +03:00
H1K0 5968a7b593 fix(frontend): drive infinite scroll from scroll position, not observer transitions
The IntersectionObserver fired only on enter/leave transitions, so a scroll
that ended with the sentinel already in range (scrolling straight to the
bottom) produced no callback and nothing loaded — the user had to scroll up
and back down to force a fresh transition, loading one chunk per cycle.

Replace the observer with a capture-phase window scroll listener (capture is
required since scroll events don't bubble; it catches scrolls from the grid's
nested <main> as well as the document), rAF-throttled, re-checking the
sentinel's viewport position on every scroll. Keep the re-check on load
completion / mount for short pages and already-in-range first renders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 10:17:59 +03:00
H1K0 e801eec47d feat(frontend): bidirectional lazy load for anchored grid returns
Returning to the grid at a deep position (deep link / hard reload to a
file, then back → /files?anchor=<id>) used to load only a tiny forward
window at the anchor. Now the grid fills the viewport around the anchor
and pages in both directions as the user scrolls.

- loadAroundAnchor fetches a window centred on the anchor and pre-fills a
  few pages each way sequentially, then centres on the anchor once. Doing
  the initial fill explicitly (rather than via the sentinels) keeps the
  pages contiguous and leaves the sentinels out of range, so there's no
  mount-time load storm.
- loading starts true when the URL carries an ?anchor, so the child
  InfiniteScroll sentinels (whose effects run before this page's reset
  effect on mount) can't fire a stray page-1 loadMore that interleaves
  with loadAroundAnchor.
- loadPrev pages backward (direction=backward) and prepends, then shifts
  the scroller down by the added height via flushSync (no paint between
  prepend and correction) so the viewport stays visually fixed.
- InfiniteScroll gains an `edge` prop; a top instance (shown only when
  hasPrev) drives upward loading. Both loaders share the `loading` guard.
- Mock: honour direction=backward and emit prev_cursor; the Go backend
  already supports backward keyset pagination.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 01:16:32 +03:00
H1K0 dc1af8c585 fix(frontend): make infinite scroll viewport-relative, stop eager-loading
Lazy load fetched the entire list at once: every list's loader had a
"fill the viewport" recursion gated on
scrollContainer.scrollHeight <= clientHeight, but <main> is not the
scroller (the window/body is), so that condition is always true and it
recursed through every page (with a 10-item window, ~all pages fired at
once).

Move the filling logic into InfiniteScroll and base it on the sentinel's
viewport rect instead: load while the sentinel is within 300px of the
viewport bottom, re-checked synchronously after each load. This works
regardless of which element scrolls and loads only enough pages to reach
past the viewport.

Drop the per-page recursion (and now-unused scrollContainer refs / tick
imports) from the files, trash, tags, categories and pools lists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 00:14:04 +03:00
H1K0 ffb8848a96 chore(frontend): bump mock files to 500 to exercise lazy load
75 mock files fit in a single 100-item page, so infinite scroll never
fired. 500 yields 5 cursor pages for testing lazy loading and the
overlay viewer paging past the loaded set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 00:05:43 +03:00
H1K0 fa491487b7 feat(frontend): open file viewer as overlay over the mounted list
The viewer was a separate /files/[id] route, so returning tore down and
reloaded the whole grid. Now opening a file uses SvelteKit shallow
routing (pushState + page.state.fileId): the list stays mounted and the
viewer renders as a full-screen overlay on top of it, like Immich. The
URL still becomes /files/<id> and the back button (or Escape/close)
dismisses the overlay via history.back(), revealing the untouched grid —
no reload — then scrolls it to the last-viewed file instantly.

- Extract the viewer UI/logic into a reusable FileViewer component
  (file fetch, preview, lazy tags, save, prev/next, keyboard).
- List: neighbours come straight from its own files[]; paging past the
  loaded set pulls the next page by cursor (prefetch near the end).
- Paging uses replaceState so one back press returns to the grid.
- /files/[id] remains as a thin standalone fallback for deep links /
  hard reloads, resolving neighbours via the anchor API and returning
  to the grid with ?anchor=<id>.
- Remove the now-unused filesCache snapshot store (the list is never
  unmounted, so there's nothing to snapshot/restore).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 00:02:25 +03:00
H1K0 4f8d6a41f9 feat(frontend): lazy infinite scroll on tags/categories/pools lists
These three lists used a manual "Load more" button while files and trash
already lazy-loaded on scroll. Wire them to the shared InfiniteScroll
component for consistent behaviour: the offset-based load() now also runs
a viewport-fill pass (keep paging until the content overflows so the
sentinel sits below the fold), and the button + its now-unused spinner
CSS are removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 23:46:29 +03:00
H1K0 18f1dbc052 fix(frontend): restore grid position via URL anchor on return from viewer
Returning from the file viewer left the grid scrolled to the top: the
position lived only in volatile module state and was never carried
anywhere, and the scroll restore ran before SvelteKit's own scroll reset
(on goto) clobbered it back to the top — worsened by the body, not
<main>, being the effective scroller, so scrollTop restoration was inert.

- The viewer's back/Escape now return to /files?anchor=<currentId> with
  noScroll, carrying the position in the URL (survives reload, no longer
  depends on hidden in-memory state).
- The list restores grid DATA from the snapshot as before, but scrolls in
  afterNavigate — which runs AFTER SvelteKit's scroll handling — using
  scrollIntoView so it works whether <main> or the window scrolls. The
  ?anchor is consumed (stripped via shallow replaceState) once applied.
- Deep link / hard reload with an anchor but no cached grid falls back to
  loading a page anchored at that file, then scrolling to it.
- Snapshot is mirrored to sessionStorage so a refresh still restores.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 16:30:26 +03:00
H1K0 a1ec25a441 perf(frontend): lazy-load file tags on scroll into view
The file viewer fetched /files/:id/tags eagerly alongside the file on
open, even though the Tags section sits below a full-viewport preview
and is usually never seen — needless DB load per file open.

Defer the tags fetch until the Tags section scrolls into view via an
IntersectionObserver (200px rootMargin pre-load). Re-fetches when paging
to another file while the section stays on-screen; shows a "Loading
tags…" placeholder until loaded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:18:57 +03:00
H1K0 2af3c481bb fix(frontend): redirect to /login when the session can't be refreshed
On a failed token refresh the client cleared the auth store and threw, but
nothing navigated away, so an expired session left the user on a page that
only showed errors. Redirect to /login when the refresh token is missing or
rejected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:55:12 +03:00
H1K0 00f63697b0 fix(frontend): render nested EXIF values instead of [object Object]
EXIF values can be arrays/objects (rationals, GPS, etc.); String(val) showed
"[object Object]". Render object/array values as JSON.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:55:12 +03:00
H1K0 5b973cf534 fix(frontend): resolve svelte-check type errors
- vite-mock-plugin: define the missing MockFile type and annotate the
  MOCK_FILES / MOCK_TRASH arrays so restore (unshift) type-checks.
- categories/[id], tags/[id]: page.params.id is string | undefined under
  noUncheckedIndexedAccess — guard loadTags and default the TagRuleEditor
  tagId so the routes type-check.

svelte-check now reports 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:49:17 +03:00
H1K0 bcbe0b5e8c fix(frontend): redirect root route to /files
The root route rendered the default SvelteKit placeholder page. Redirect /
to /files; the layout guard still sends unauthenticated users to /login.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:40:47 +03:00
H1K0 a360cab2fc fix(frontend): use search param for pool "add files" name search
The add-files search built a {n~%text%} filter token, but the filter DSL
only supports t=, m= and m~, so the backend rejected it as an unknown token
and no results appeared. Use the search query param (ILIKE on name) instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:40:47 +03:00
H1K0 f8f58434d5 feat(frontend): restore files grid position when returning from a file
Opening a file now snapshots the grid (loaded pages, cursor, scroll offset,
opened id) into a shared store, and the viewer derives prev/next from that
list instead of a separate anchored request. Returning to the grid restores
the cached list and scroll-centres the last-viewed file rather than
reloading page 1 from the top.

This also fixes two issues:
- The viewer's "previous" arrow never appeared: the backend anchor window
  is forward-inclusive, so the anchor was always item 0 and prev was null.
  Neighbors now come from the cached list, so paging is symmetric.
- Paging forward in the viewer prefetches further pages into the snapshot,
  so navigation continues past the initially loaded set and the grid still
  restores correctly.

A deep link straight to a file (empty cache) falls back to the anchored
API window as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:39:50 +03:00
H1K0 eb2eb00d96 fix(frontend): use $appSettings.tagRuleApplyToExisting on creating a new tag rule also 2026-04-07 11:59:09 +03:00
H1K0 135c71ae4d fix(frontend): theme-aware footer navbar colors
- Add --color-nav-bg and --color-nav-active CSS variables
- Dark: semi-transparent purple-dark tone (rgba 52,50,73 / 0.45 bg)
- Light: light semi-transparent background, accent-tinted active highlight
- Footer background and active nav item now use variables instead of
  hardcoded dark values

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 01:10:12 +03:00
H1K0 d38e54e307 feat(frontend): use reference icons for PWA manifest and favicons
- Copy all icon PNGs from docs/reference (android, apple, ms, favicon sizes)
- Copy favicon.ico and browserconfig.xml
- Manifest: full icon set (36–310px), background/theme #312F45
- app.html: favicon links, full apple-touch-icon set, MS tile metas

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 01:04:36 +03:00
H1K0 c6e91c2eaf feat(frontend): add PWA support (service worker, manifest, pwa util)
- src/service-worker.ts: cache-first app shell (build + static assets),
  network-only for /api/, offline fallback to SPA shell
- static/manifest.webmanifest: name/short_name Tanabata, theme #312F45,
  standalone display, start_url /files, icon paths for 192/512/maskable
- src/lib/utils/pwa.ts: resetPwa() — unregisters SW + clears all caches
- app.html: link manifest, theme-color meta, Apple PWA metas
- settings page: refactored to use resetPwa() from utils

Note: add /static/images/icon-192.png, icon-512.png, icon-maskable-512.png
for full installability.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 01:02:53 +03:00
H1K0 d6e9223f61 feat(frontend): implement trash view with restore and permanent delete
- New /files/trash page: same grid as files view, deleted files only
- Tap selects (no detail page for deleted files), long-press drag-selects
- Trash selection bar: Restore (bulk) and Delete permanently (bulk, confirmed)
- Trash icon added to files header, navigates to /files/trash
- Mock: MOCK_TRASH with 6 pre-seeded files; bulk/delete now moves to trash;
  handlers for POST /files/{id}/restore and DELETE /files/{id}/permanent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 00:56:55 +03:00
H1K0 004ff0b45e fix(frontend): admin section fixes (pagination, actions, navbar)
- Audit log: replace load-more with page-based pagination
- Audit log: add all 29 action types to the dropdown
- Audit log: fix pagination bar hidden behind footer
- Root layout: hide footer navbar on /admin/* routes
- Users pages: fix curly-quote parse error in ConfirmDialog messages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 00:56:43 +03:00
H1K0 6e052efebf feat(frontend): implement admin section (users + audit log)
- Layout guard redirecting non-admins to /files
- User list page with create form and delete confirmation
- User detail page with role/permission toggles and delete
- Audit log page with filters (user, action, object type, ID, date range)
- Mock data: 5 test users, 80 audit entries, full CRUD handlers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 00:27:44 +03:00
H1K0 70cbb45b01 fix(frontend): auto-fill viewport on file list load
After each batch, check if the scroll container is still shorter than
the viewport (scrollHeight <= clientHeight) and keep loading until the
scrollbar appears or there are no more files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 00:06:45 +03:00
H1K0 012c6f9c48 feat(frontend): add configurable app settings (file load limit, tag rule apply_to_existing)
- Add appSettings store (localStorage-backed) with two settings:
  fileLoadLimit (default 100) and tagRuleApplyToExisting (default false)
- Settings page: new Behaviour section with numeric input for files per
  page (10–500) and an on/off toggle for retroactive tag rule application
- files/+page.svelte: derive LIMIT from appSettings.fileLoadLimit so
  changes take effect immediately without reload
- TagRuleEditor: pass apply_to_existing from appSettings when activating
  a rule via PATCH (only sent on activation, not deactivation)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 00:00:55 +03:00
H1K0 6da25dc696 feat(frontend): implement settings page
- Profile editor: name and optional password change with confirm field,
  saves via PATCH /users/me and updates auth store
- Appearance: theme toggle button (dark/light) with sun/moon icon
- App cache: PWA reset — unregisters service workers and clears caches
- Sessions: list active sessions with parsed user agent, start date,
  expiry, current badge, and terminate button per session
- Add mock handlers: PATCH /users/me, DELETE /auth/sessions/{id}

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 23:37:44 +03:00
H1K0 9b1aa40522 feat(frontend): implement bulk tag editing for multi-file selection
- Add BulkTagEditor component: loads common/partial tags via
  POST /files/bulk/common-tags and applies changes via POST /files/bulk/tags
- Common tags shown solid with × to remove from all files
- Partial tags shown with dashed border and ~ indicator; clicking promotes
  to common (adds to files that are missing it)
- Wire "Edit tags" button in SelectionBar to a bottom sheet with the editor
- Add mock handlers for /files/bulk/common-tags and /files/bulk/tags

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 08:40:53 +03:00
H1K0 d79e76e9b7 feat(frontend): implement pool views and add-to-pool from file list
- Add /pools list page with search, sort, load-more pagination
- Add /pools/new create form (name, notes, public toggle)
- Add /pools/[id] detail page: metadata editing, ordered file grid,
  drag-to-reorder, filter bar, file selection/removal, add-files overlay
- Add pool sort store (poolSorting) to sorting.ts
- Wire "Add to pool" button in SelectionBar: bottom-sheet pool picker
  loads pool list, user picks one, selected files are POSTed to pool
- Add full pool mock API handlers in vite-mock-plugin.ts (CRUD + file
  management: add, remove, reorder with cursor pagination)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 08:31:06 +03:00
H1K0 1f591f3a3f feat(frontend): replace JS confirm() with native dialog component
- ConfirmDialog: centered <dialog> with backdrop blur, cancel + confirm (danger variant)
- tags/[id]: delete tag uses ConfirmDialog
- categories/[id]: delete category uses ConfirmDialog
- files: bulk delete calls POST /files/bulk/delete, removes files from list,
  text updated to "Move to trash" (soft delete)
- mock: add POST /files/bulk/delete handler

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 23:48:21 +03:00
H1K0 1931adcd38 feat(frontend): implement category list, create, and edit pages
- /categories: list with colored pills, search + clear, sort/order controls
- /categories/new: create form with name, color picker, notes, is_public
- /categories/[id]: edit form + tags-in-category section with load more
- sorting.ts: add categorySorting store (name/color/created, persisted)
- mock: category CRUD, GET /categories/{id}/tags, search/sort/offset

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 23:38:52 +03:00
H1K0 21f3acadf0 feat: add PATCH /tags/{id}/rules/{then_id} to activate/deactivate rules
- openapi.yaml: new PATCH endpoint with is_active body, returns TagRule
- backend/service: SetRuleActive calls repo.SetActive then returns updated rule
- backend/handler: PatchRule validates body and delegates to service
- backend/router: register PATCH /:tag_id/rules/:then_tag_id
- frontend: TagRuleEditor uses PATCH instead of delete+recreate
- mock: handle PATCH /tags/{id}/rules/{then_id}

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 23:31:12 +03:00
H1K0 871250345a feat(frontend): add activate/deactivate toggle for tag rules
- Toggle button (filled/hollow circle) on each rule row
- Inactive rules dim to 45% opacity
- Toggle via delete + recreate with new is_active value
- Mock: track is_active per rule (Map instead of Set)
- Show all available tags by default in add-rule picker

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 23:26:07 +03:00
H1K0 6e24060d99 feat(frontend): add clear button to TagPicker search input
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 23:16:49 +03:00
H1K0 f7d7e8ce37 feat(frontend): implement tag list, create, and edit pages
- /tags: list with search + clear button, sort/order controls, offset pagination
  Fix infinite requests when search matches no tags (track initialLoaded flag)
- /tags/new: create form with name, notes, color picker, category, is_public
- /tags/[id]: edit form + TagRuleEditor for implied-tag rules + delete
- TagBadge: colored pill with optional onclick and size prop
- TagRuleEditor: manage implied-tag rules (search to add, × to remove)
- Mock: tag/category CRUD, rules CRUD, search/sort, 5 mock categories

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 23:14:04 +03:00
H1K0 b9cace2997 feat(frontend): implement file upload with drag-and-drop and per-file progress
- client.ts: add uploadWithProgress() using XHR for upload progress events
- FileUpload.svelte: drag-drop zone wrapper, multi-file queue with individual
  progress bars, success/error status, MIME rejection message, dismiss panel
- Header.svelte: optional onUpload prop renders upload icon button
- files/+page.svelte: wire upload button, prepend uploaded files to grid
- vite-mock-plugin.ts: handle POST /files, unshift new file into mock array
- Fix crypto.randomUUID() crash on non-secure HTTP context (use Date.now + Math.random)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 14:02:26 +03:00
H1K0 a5b610d472 feat(frontend): implement file viewer page with metadata editing and tag picker
- files/[id]/+page.svelte: full-screen preview (100dvh), sticky top bar,
  prev/next nav via anchor API, notes/datetime/is_public editing, TagPicker,
  EXIF display, keyboard navigation (←/→/Esc)
- TagPicker.svelte: assigned tags with remove, searchable available tags to add
- Fix infinite request loop: previewSrc read inside $effect tracked as dependency;
  wrapped in untrack() to prevent re-triggering on blob URL assignment
- vite-mock-plugin: add GET/PATCH /files/{id}, preview endpoint, tags CRUD,
  anchor-based pagination, in-memory mutable state for file overrides and tags
- files/+page.svelte: migrate from deprecated $app/stores to $app/state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 13:55:04 +03:00
H1K0 84c47d0282 feat(frontend): expand mock tags to 207 entries for filter bar testing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 13:35:05 +03:00
H1K0 6fa340b17c feat(frontend): make header and filter bar sticky on scroll
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 13:32:35 +03:00
H1K0 aebf7127af feat(frontend): implement file selection with long-press, shift+click, and touch drag
- selection.ts: store with select/deselect/toggle/enter/exit, derived count and active
- FileCard: long-press (400ms) enters selection mode, shows check overlay, blocks context menu
- Header: Select/Cancel button toggles selection mode
- SelectionBar: floating bar above navbar with count, Edit tags, Add to pool, Delete
- Shift+click range-selects between last and current index (desktop)
- Touch drag-to-select/deselect after long-press; non-passive touchmove blocks scroll only during drag

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 13:30:26 +03:00
H1K0 63ea1a4d6a feat(frontend): make filter expression tokens draggable for reordering
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 12:57:45 +03:00
H1K0 27d8215a0a feat(frontend): add header, filter bar, and sorting store for files page
- sorting.ts: per-section sort store (sort field + order) persisted to localStorage
- dsl.ts: build/parse DSL filter strings ({t=uuid,&,|,!,...})
- Header.svelte: sort dropdown, asc/desc toggle, filter toggle button
- FilterBar.svelte: tag token picker with operator buttons, search, apply/reset
- files/+page.svelte: wired header + filter bar, resets pagination on sort/filter change
- vite-mock-plugin.ts: added 5 mock tags for filter bar development

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 12:47:18 +03:00
H1K0 e72d4822e9 feat(frontend): implement file gallery page with infinite scroll
Adds InfiniteScroll component (IntersectionObserver, 300px margin,
CSS spinner). Adds FileCard component (fetch thumbnail with JWT auth
header, blob URL, shimmer placeholder). Adds files/+page.svelte with
160×160 flex-wrap grid and cursor pagination. Updates mock plugin with
75 sample files, cursor pagination, and colored SVG thumbnail handler.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 03:34:33 +03:00
H1K0 9e341a0fc6 feat(frontend): add dev mock API plugin
Adds a Vite dev-only middleware that intercepts /api/v1/* requests
and returns mock responses for auth, users, files, tags, categories,
and pools. Login with any username and password "password".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 03:26:03 +03:00
H1K0 7770960cbf feat(frontend): add root layout with auth guard and bottom navbar
Adds +layout.ts auth guard (redirects to /login when no token).
Adds bottom navbar with inline SVGs for Categories/Tags/Files/Pools/
Settings, active-route highlight (#343249), muted-to-bright color
transition. Adds theme store (dark/light, persisted to localStorage,
applies data-theme attribute). Hides navbar on /login route.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 03:21:00 +03:00
H1K0 e21d0ef67b feat(frontend): implement auth store and login page
Rewrites auth store with typed AuthUser shape (id, name, isAdmin) and
localStorage persistence. Adds login page with tanabata decorative
images, centered form, purple primary button matching the reference
design.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 03:06:32 +03:00
H1K0 fde8672bb1 feat(frontend): implement API client and auth module
Adds the base fetch wrapper (client.ts) with JWT auth headers,
automatic token refresh on 401 with request deduplication, and
typed ApiError. Adds auth.ts with login/logout/refresh/listSessions/
terminateSession. Adds authStore (stores/auth.ts) persisted to
localStorage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 03:02:35 +03:00
H1K0 36d9488f21 feat(frontend): initialize SvelteKit frontend with Tailwind and OpenAPI types
- SvelteKit SPA mode with adapter-static (index.html fallback)
- Tailwind CSS v4 via @tailwindcss/vite with custom color palette
- CSS custom properties for dark/light theme (dark is default)
- Epilogue variable font with preload
- openapi-typescript generates src/lib/api/schema.ts from openapi.yaml
- Friendly domain type aliases in src/lib/api/types.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 00:00:26 +03:00