Follow lists were paginated at the API default of 48, but the SPA treats
them as complete sets: the tag/pool toggles read their state from page one
(so the 49th follow looked unfollowed and its spinner waited for a page that
could never contain it) and the Followed page rendered only 48 cards while
showing that as the count. Both follow endpoints are now unpaginated — they
are per-user sets and still restricted to the caller's rows — and the three
consumers take plain arrays.
Post visibility: the old J621-Django online view fetched limit=320 (e621's
maximum) while ours hard-coded 48, and fetchPostsByIds capped id batches at
100. The Online browser now has a 'Posts per page' setting (48/100/200/320)
in its sidebar, mirrored in Account -> Browsing preferences, stored per user
as e621_per_page and also used for pool loading; the id-batch cap is raised
to 320.
Tests: follow list shape/isolation (4) and preference validation/merge (3)
added; the full backend suite is 46 green. Live-checked the array response
shape and the preference bounds (200 accepted, 500 rejected).
The app can now operate backend-agnostically: a production build still
asks on first start, but /setup also offers 'Continue without a backend'
(stored as the sentinel 'none'), and the shell adapts:
- Local mode shows only the e621-facing pages: Online (search, post view,
favorites, blacklist editor, direct downloads) and Pools. Library,
uploads, duplicates, stats, users, follows and similarity are hidden
from the nav and palette and render a 'backend needed' state when
reached directly; /detail/<e621 id> still works while /detail/J-x asks
for a backend.
- e621 credentials are stored in this browser (j621.e621) and the
Account page becomes a credentials-only screen; the store reads/writes
locally instead of /api/auth/e621/.
- The header replaces the status pill and login/user area with an e621
credentials button and a 'Setup Backend' button; the footer shows
'Local mode — e621 features only' with the same entry point.
- In-library lookups (badges/browse markers) are skipped without a
backend; 'Download to client' links straight to the e621 file instead
of the backend proxy; follow buttons and palette follow toggles are
hidden; api() fails fast with a clear message if something slips
through.
Mode logic lives in lib/backend.ts (URL / '' same-origin / 'none') with
its matrix verified in Node; tsc, oxlint and the build are clean.
Adds a preferences JSON field on the user plus GET/POST
/api/auth/preferences/ (merge semantics, validated keys), surfaced in
/auth/me/ and typed on the frontend.
The Account page gains a Browsing preferences card: landing page,
default rating filter, default sort, items per page and thumbnail size.
Signed-in users also sync these while browsing (the Library sidebar's
rating/sort/per-page controls and the new thumbnail slider), debounced;
on load the account's values seed the local UI state, so settings follow
the user across browsers. Guests keep the existing localStorage
behaviour. The thumbnail size drives the media grids (Library, Online,
pool detail) between 140 and 320px columns.
Verified the API against the dev server: merge keeps untouched keys,
invalid values 400, values round-trip through /auth/me/.
Inline banners and per-row status text reported action results all over
the app; they are replaced by a small toast stack (bottom-right, Level 3
floating well styling) that only speaks for actions: successes fade,
errors stay until dismissed, and form-field validation stays inline.
Destructive actions no longer use bespoke inline confirm steps (the
library detail's Confirm delete button) or fire immediately (duplicate
copies/items, delete page selections, temp cleanup, upload discard, job
cancellation): they all go through one promise-based confirm dialog
(confirmAction) with a danger variant, Escape/backdrop to cancel.
The Online sidebar's "Your blacklist" section is editable now: typing a
tag appends it and each entry gets an x to remove it. Changes are written
straight to the e621 account (PATCH /users/{id}.json with
user[blacklisted_tags]); the store keeps the fetched profile for the id,
updates the list optimistically and reloads it from e621. The Followed
page's blacklist cloud is rebuilt afterwards via the new
/api/follows/cloud/?refresh=1 force flag.
Verified against the live API with a reversible add/verify/restore test.
Logging out or switching accounts kept the previous user's React Query
cache (follows, feed, cloud, e621 pages), so the new account briefly
saw the old one's followed tags/pools until each query refetched. The
query client now lives in lib/queryClient.ts and login/register/logout
clear it alongside the e621 credential store.
Backend:
- MediaItem gains search_tags (custom + e621 tags, lowercase) and
has_custom_data, maintained on save with a data migration backfill
- File list search accepts search_type=filename|tags|both (tag search is
word-AND across the flattened tag text) and status=matched|custom|
unknown filters
- New /api/tags/cloud/ endpoint (cached 2 min per guest/auth, invalidated
on item changes and deletions) returning the most-used tags, honouring
guest visibility
Frontend:
- Library sidebar: Filename/Tags/Both selector, status pill toggles
(persisted), and a clickable tag cloud that runs a tag search
- Roadmap updated
- Status polling drops from 30s to 5s, and download start/finish/cancel
invalidates it immediately, so the footer's Active Workers reflects
running download tasks in near real time
- The e621 time in the status pill is now a button: it opens a dropdown
with the session's request history (clock time, endpoint, duration,
colour-coded) plus totals; closes on outside click or Escape
- Metrics store keeps the last 50 requests
- New e621 metrics store tracks request count, cumulative time and the
last request (path + duration) for the session
- The e621 client measures each request around fetch and JSON parsing
- Status pill appends a teal 'e621: <total>' segment with a tooltip
showing request count and the last request; a server-side e621 time
is shown separately if the backend ever reports one
Backend:
- POST /api/files/lookup/ reports which MD5s are already in the library
Frontend:
- e621 client extended with post/tag/favorite types and helpers: post
search, post detail, batch posts by id, tag autocomplete, toggle
favorite
- /online: tag search with autocomplete, post grid with rating colors
and in-library badges, numbered pagination, page tag cloud, blacklist
panel and filtered counts from /users/me.json, anonymous hint
- /online/view/🆔 media viewer (sample or original, video support),
taxonomy-colored tags by category, specs sheet, favorite/unfavorite,
description, sources, pools and parent/children thumbnails
- Shared CollapsibleSidebar extracted from the library page; Online
added to the nav, command palette and sidebar toggle
Backend:
- User model gains e621_username / e621_api_key / e621_base_url
- GET/PUT /api/auth/e621/ for the owner's credentials; /me exposes only
the username and a configured flag, never the key
Frontend:
- e621 client core: Basic auth, _client param (browsers cannot set a
User-Agent), serialized queue throttled to 1 request/second, readable
error mapping
- Account screen (/account): username, API key with reveal toggle,
base URL (e621 / e926 / custom), Save + Test connection
- Credentials are fetched from the backend and held in memory only,
cleared on logout
Backend:
- GET /api/files/{md5}/neighbors/ returns previous/next items in the
current ordering (name, size, created_at) for keyboard navigation
Frontend:
- / focuses the library search input
- D downloads the file on the detail page
- [ and ] navigate to the previous/next item; hint shown on the page
- Ctrl/Cmd+K opens a command palette (navigation, toggle filters,
focus search, log in/out); Escape closes it
- Sort order now persists alongside the other library filters so
prev/next stays consistent
Backend:
- apps.core with GET /api/status/ (env, git hash, OS, watched folder,
worker counts) and a TimingMiddleware adding X-Server-Time-Ms
- status endpoint reports its own server-side assembly time
Frontend:
- top bar shows the storage line (watched folder) and a status pill
with ENV, git hash, host OS, server time (e621 time once it exists)
- fixed 32px footer strip: storage path, active workers, build version
- collapsible filter sidebar: closed by default, Ctrl/Cmd+B toggle,
overlay drawer with backdrop blur under 1024px
- rating filters, per-page and sidebar state persist in localStorage
Backend (Django 6.1 + DRF):
- Token auth with a custom User model (register/login/logout/me)
- Library models (MediaItem, MediaLocation) and REST endpoints
- File list/detail with search, rating filter, sorting, pagination
- Multipart upload with optional rating/tags/notes
- Range-aware media serving (video seeking) and ffmpeg thumbnails
- scan_files management command for the watched folder
Frontend (React 19 + Vite + TypeScript):
- Catppuccin Mocha design tokens from the design docs
- App shell, token persistence, protected routes
- Library grid with filters, file detail with custom data editor
- Upload page with per-file progress via XHR
- Dev proxy to the Django API