- PNG failed because wasm-bindgen's init only accepts a URL string (or a
module/buffer): passing { module_or_path } broke every PNG optimization.
- GIF/APNG showed flashing colors because patches were written with
putImageData, which ignores the transparency that means "keep the
previous frame". Patches now blend through drawImage with correct
disposal handling, and quantization keeps a transparent palette entry.
- Video no longer downloads the whole file into memory: Mediabunny reads
it with range requests (UrlSource) and the muxer streams into Blob
chunks (StreamTarget) that are assembled with last-write-wins range
merging. BufferTarget held the entire output in memory, which crashed
the tab on large files.
Backend:
- POST /api/files/J-x/optimize/ applies a browser-processed file: replaces
every copy (renaming when the extension changes), recomputes MD5, size
and perceptual hashes, seeds guest visibility; 400 when identical,
409 when the result matches another item, owner/staff only.
Frontend (no server-side processing by design):
- optimize.worker.ts + pipelines: Mediabunny/WebCodecs for video with a
prefer-hardware hint and per-browser codec detection; MozJPEG/OxiPNG/
libwebp (jSquash) for images; gifuct-js+gifenc and UPNG for GIF/APNG.
- OptimizeModal: per-file-type options, original vs processed previews
with sizes/savings, progress bar with ETA, then Apply (overwrite).
- Optimize button on the library detail for the uploader/staff.
Clicking the + follow toggle used to move focus onto that button, so the
next Enter re-triggered the follow instead of searching the highlighted
tag. The toggle now prevents the mouse-down focus steal, and the footer
hint spells out that Enter searches while + follows.
The command palette now searches e621 tags as you type (debounced
/tags.json suggestions with category chips and post counts), lets you
search a tag online, follow/unfollow it inline and open it on e621, and
remembers recent searches in localStorage. New navigation commands cover
Pools, Followed and Similar. On the online detail, F toggles the current
post's favorite.
- /similar (nav: Similar): drop a file to get the exact MD5 match, the
perceptual matches against the library, and e621 IQDB candidates
(auto-run for images when credentials are configured). Read-only —
nothing enters the library.
- SimilarityCheck model + /api/similarity/ (create/list/retrieve/delete)
with signed preview URLs and an expires_at timestamp.
- Temp files are wiped on startup (AppConfig.ready, file-only so no
database access during initialization), lazily past
SIMILARITY_TTL_MINUTES (default 30, env-overridable), on delete, and
by manage.py cleanup_similarity.
- uploadFile() takes a target path; .env.example documents the TTL.
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.
- Remove the follow-a-tag / follow-a-pool forms; follows happen from tag
chips (+ on detail views) and the Follow pool button on pool pages, so
the empty states now point there instead.
- Move the blacklisted-tag cloud to a full-width horizontal panel at the
bottom of the page.
- Tag and pool card grids cap at two columns so the cover cards are
bigger.
PostCard's Link is inline by default; wrapped in a div on the pool detail
page it stopped being blockified like a grid child, so its rating-tinted
border collapsed into a vertical line on the left. The card is now
block-level, and the pool grid only wraps blacklisted posts (for the red
ring), so normal cards render exactly like the Online grid.
- /pools: search by name, category/active filters, sort options and
pagination per the OpenAPI spec, with covers taken from each pool's
first post in one batched post call; blacklisted covers fall back to a
placeholder and deleted pools get an archive marker.
- /pools/<id>: DText description, post grid kept in the pool's own order
with chunked loading, in-library badges, a blacklist reveal toggle and
a Follow pool button wired into the follows API.
- e621 client gains fetchPools/fetchPool; Pools nav entry added.
The chip toggle swaps its icon for a tiny current-color spinner from
the click until the follows list reflects the new state, covering the
refetch gap so '+' never flashes back before the checkmark.
TagChip gains a followable mode that renders a small toggle inside the
chip: '+' follows the tag, '✓' (click to unfollow) once followed, and a
red marker with the reason as its title when e621 rejects it. Enabled on
the library detail's e621 metadata card (matched posts) and on online
post detail tags; guests see plain chips and the label click keeps
working beside the toggle.
Backend (new apps.follows):
- FollowedTag/FollowedPool/FollowedPost models; per-user follows with
unseen tracking, plus FollowCloud for the cached blacklist cloud.
- Two periodic commands sharing one fetch path: sync_followed_tags and
sync_followed_pools fetch each followed tag/pool's newest posts (one
e621 search per unique follow), store unseen feed rows, refresh covers
and pool metadata; both fall back to anonymous e621 access.
- API: /api/follows/tags|pools (follow, unfollow, mark seen), a merged
feed with per-follow filtering, and /api/follows/cloud/ which rebuilds
the blacklisted-tag cloud in a daemon thread when its 10 min cache is
stale (polling returns building/ready).
- e621 client now supports anonymous reads; trimmed posts carry preview
URLs for covers and feed tiles.
Frontend:
- /followed page: follow forms, cover cards with unseen badges and
Mark seen, merged feed with filter/unseen toggle, and a blacklist
cloud panel that polls while building. Followed nav entry added.
- New IQDB card on local item pages: fetches the signed raw file, POSTs it
to e621's /iqdb_queries.json with the user's credentials, and lists
candidates as tiles (thumbnail, rating, score%) with exact-MD5 markers.
- Selecting a candidate opens a detail panel (rating, score, favs, size,
tag preview) and linking is an explicit confirm that reuses the e621
match endpoint; videos show a note since IQDB is image-only.
- Guests don't see the card; linking follows the uploader/staff rule.
A homescreen-style age gate shown before auth or any route: J621 brand
mark, the explicit 18+ check, an Enter action remembered per browser in
localStorage (j621.age-verified), and a blocked state if the visitor
chooses Leave.
- MediaItem gains e621_match_status (unknown/matched/not_found/deleted)
and e621_checked_at, backfilled for existing matched items.
- Server-side e621 client (apps/library/e621.py) using the user's stored
credentials, throttled to 2 req/s, with typed errors.
- Matching service: MD5 lookup, manual post linking (flags MD5
mismatches), unlink, metadata refresh, deleted-post detection.
- Detail actions POST /api/files/J-x/match/ and /unlink/ (uploader or
staff only).
- Background library scans: MatchTask + /api/matches/ with missing/all
scopes, progress polling, cancel and stale-task reaping; the scan
counts toward the footer's Active Workers. Same pass available as
manage.py match_e621 for cron.
- Library gains not_found/deleted status filters; the detail page adds
an e621 match card (check / link by post ID / unlink) and the metadata
card warns when a post was deleted on e621.
Items flagged hidden_from_guests (blacklisted tags) returned 404 for
<img> requests since tags cannot send the auth header. The API now
exposes signed raw_url/thumbnail_url fields (mirroring upload previews
and avatars), and the SPA uses them in the gallery, detail view,
duplicates and delete screens, and upload visual matches.
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
Staff and uploaders (for their own items) get a Delete action on
/detail/J-<id> with an inline confirmation; on success it returns to the
library and refreshes files, duplicates and storage.
Backend:
- Perceptual hashes (aHash/dHash/pHash/wHash via imagehash, no imgdd)
stored on items, computed on upload/download and by the new
compute_visual_hashes command
- Duplicates API: exact duplicates (multi-location items), visual matches
for one item, union-find similarity groups with pagination
- Delete API with ownership/staff checks, per-item and per-copy deletion,
watched-folder path validation; storage overview and temp cleanup;
file list accepts j_ids batches
- Staged uploads are flagged visual_match with their library matches
(threshold via VISUAL_MATCH_THRESHOLD)
- Staff users API: list with upload counts, set role and avatar by J-ID;
User.avatar FK with signed avatar URLs
- Download threads close their DB connection and stale tasks are reaped,
keeping behaviour Gunicorn-friendly
Frontend:
- /duplicates: exact duplicate groups with per-copy delete, visual
similarity controls, search similar to a J-ID, paginated groups with
selection, bulk delete and dismiss
- /delete: storage cards, delete by J-ID with preview grid, temp cleanup
- /users: staff directory with role selects and avatar J-ID inputs
- Nav + command palette entries; top-bar avatar; upload cards and the
metadata modal show library visual matches
- 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
Backend:
- DownloadTask model + background thread runner: streams the file with
progress (%, bytes, speed) and a cancel flag, then indexes it, names it
J-<id>.<ext> and applies the e621 metadata
- DownloadTaskViewSet (create/retrieve/cancel) replaces the synchronous
endpoint; the status footer's worker counts now reflect download jobs
- Client download proxy (/api/online/file/) streams an e621 original to
the browser with Content-Disposition: attachment, restricted to the
configured e621 CDN hosts so it cannot be used as an open proxy
Frontend:
- Online detail: progress bar with percentage, transferred size, speed
and cancel while downloading; success links to the new J-ID
- New 'Download to client' button available to everyone (guests too)
The modal preview used a fixed 200px column with object-contain, so wide
images rendered as a slim letterboxed rectangle with dead space. The
preview now sizes naturally (max 320px wide / 55vh tall, aspect ratio
preserved) and the modal column follows it.
- Files upload as soon as they are dropped or selected; a sequential
queue processes them and picks up files added while uploading
- The progress list is now a grid: 5 columns with about three rows
visible (scrolls beyond) showing thumbnails, per-file progress bars
and status; 'clear finished' remains, object URLs are revoked
- Videos show an icon instead of a thumbnail
- 'dismiss all' clears every indexed record at once
- Indexed cards show the actual file preview: completed records now get a
signed library media URL (raw for images, thumbnail for videos) so
<img>/<video> tags can load it, including items hidden from guests
- Media raw/thumbnail endpoints accept the signature for anonymous
requests and fall back to the normal guest-filtered path otherwise
- Guest blacklist keeps a persistent Redis mirror: an expired TTL or an
unreachable e621 keeps the last successful list instead of falling
back to the small local list
- IQDB responses carry no preview/file data, so candidates only showed an
ID; the SPA now enriches them with one batched posts lookup (preview,
rating, score, favourites, dimensions, tag preview)
- Candidate tiles are selectable instead of instantly resolving: picking
one shows its info and an explicit 'Link selected post' button
- Linking a post now fetches the e621 original into the library and
drops the staged upload; when the staged file's MD5 already equals the
post's file, the staged copy is moved instead (identical bytes)
- Keep the file URL in stored e621 metadata; sanitize the new candidate
fields server-side
- Staged files are now served through a signed URL (Django signing, 24h)
so <img>/<video> tags can load previews without an Authorization
header; the file endpoint accepts header auth or a valid signature,
rejects tampered signatures, and still scopes access to the owner
- Serializer responses now carry the request context so URLs are signed
per user
- Add .webp to the allowed extensions (backend + upload hint)
Backend:
- TempUpload model: staged files (pending / visual_match / completed /
error) with resolution, e621 payload, custom metadata and IQDB data
- Files land in a temp folder and only move into the watched library
folder once resolved; duplicates resolve immediately without a copy
- Endpoints: stage (multipart), list, retrieve, temp file, IQDB save,
resolve (link to post or custom metadata), discard/dismiss
- cleanup_temp_uploads command for old staged files
- Replaces the old direct-to-library upload endpoint
Frontend:
- Upload page is now a three-column board (Pending & Unmatched /
Visual Similarity Detected / Auto-uploaded & Indexed)
- After upload: MD5s are batch-checked against e621 and matches
auto-complete with full post metadata; remaining files run through
IQDB and move to the similarity column when candidates exist
- Metadata modal with IQDB candidates, post-ID linking and custom
tags/rating/notes; discard and dismiss actions
- e621 client gains fetchPostsByMd5 and iqdbSearch helpers
Roadmap updated with the completed upload items.
- 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
Roles:
- JakeBreathild is now staff + superuser (the real account); the 'jake'
smoke-test account was demoted to a regular user
- /me exposes is_superuser and the account page shows an admin badge
e621 metadata:
- MediaItem gains e621_post_id and e621_data (trimmed post payload:
tags by category, rating, score, favourites, comments, sources,
description, pools, relationships, file info, uploader)
- Download to Library accepts the post payload from the SPA and stores
it; the item's custom rating is seeded from the e621 rating when empty
- Library detail shows an e621 metadata card: link to the in-app post,
score/favourites/comments, taxonomy-coloured tags, DText description,
sources and pools; grid cards get an e621 badge and fall back to the
e621 rating for their colour (display_rating)
- Guest visibility now also considers e621 tags, so downloaded explicit
content is hidden from anonymous visitors
Backend:
- User.role (user/uploader/staff) with can_upload; uploads and downloads
gated to uploader+; owners and staff can edit their items
- MediaItem.uploaded_by plus J-<id> identity (serializer, admin,
scan_files --user, first superuser as default owner)
- API resolves J-<id>, bare numeric ids and MD5s; neighbors and lookup
return j_ids
- Guest safety: mirror e621's anonymous default blacklist into Redis
(parses comments, negations and wildcards), flag hidden_from_guests
and filter lists, details and lookups for anonymous users
- POST /api/online/downloads/ writes an e621 file into the watched
folder and indexes it for the uploader
- MariaDB + Redis via docker compose (host ports 3307/6380), PyMySQL
driver shim, Redis cache replacing the file cache; SQLite data
dumped and loaded into MariaDB
Frontend:
- Single /detail/:itemId route with an adaptive shell: J-<id> renders
the library item, bare numbers render the e621 post
- Legacy /view/<md5> and /online/view/<id> redirect to canonical URLs
- Cards expose J-IDs; library custom-data editor is read-only for
non-owners
- Role gating: Upload hidden/blocked for regular users, account shows
the role, guest hint on the library
- New PostThumb component fetches the post through the e621 client
(cached 30 minutes, no retries) and renders its preview image linked
to the in-app post page, with the post id underneath
- Falls back to an external e621 link while loading or when the post
cannot be fetched
Checked against https://e621.net/help/dtext and filled the gaps:
- backtick inline code spans and backslash-backtick escaping
- [color=...] accepts tag category names (artist, copyright, species,
...) alongside CSS colour names and 3/6/8-digit hex
- links: <url> brackets, "title":[url], "title":/relative paths,
wiki links with custom titles and #anchors, {{tag search}} and entity
references (post/topic/pool/set/comment/... #id)
- [quote=red] colours the bar instead of being read as an author
- nested lists via repeated * / # markers
- [section,expanded=Title] renders expanded
- [table] thead/tr/th/td tables and [ltable] pipe tables
- [#anchor] targets and [[#anchor]] in-page links
- headings always start their own block
- e621 list/post queries use a 5 minute staleTime and 30 minute gcTime,
so opening a post and coming back shows the same results with no
refetch; global gcTime raised to 30 minutes
- post card links carry their originating search in route state; the
detail back link returns to it and related-post links keep it
- new navigations scroll to top while history back/forward keeps the
previous position
- DText renderer for e621 descriptions (b/i/u/s, sup/sub, code, spoiler,
quote, color, url/wiki/thumb, headings, lists, sections, expand
blocks, named and bare links) built as React elements, no raw HTML
- Spoiler moved to its own component; trailing punctuation no longer
swallowed into links
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
- OS name comes from /etc/os-release (e.g. 'CachyOS Linux'), mirroring
the original app's os_utils; macOS/Windows get proper names too
- Status pill shows the name as-is instead of uppercasing it
- Removed the watched-folder path next to the logo; it now appears only
in the footer strip (spec updated to match)
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
- DESIGN.md frontmatter no longer names the design system
- The spec now references the local DESIGN.md file instead of the
Stitch-internal design system placeholder
- Spec header, status pill and footer now document '<env> @ <hash>'
instead of invented release numbers
- Backend settings expose GIT_COMMIT_HASH / APP_ENV / APP_VERSION,
mirroring the original app
- Frontend bakes the short hash in at build time and shows it in the
top bar
- 'NAS UI' tag was a misinterpretation of the watched-folder path; the
spec now calls it the storage line and notes J621 is folder-based
- Status pill documents the real backend fields: environment, actual
host OS, page generation time, e621 API request time
- Footer strip is marked as a proposal; the workers count is real data
from the stats page, not invented thread counts
- Removed the remaining 'Write to NAS' / 'Ingest to NAS' phrasing
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