JakeBreath d16a77907a Keep the online detail's download state on its own post
Two stale-state bugs came from react-router reusing the detail component
between posts (parent/child links hit the same /detail/<id> route):

- the previously viewed post's download panel kept rendering, so a freshly
  opened post could claim 'Downloaded to the library J-xx'. The task view is
  now gated on the task's post_id as well, and DetailPage keys the detail
  views per item — component state (download panel, delete confirmation,
  optimizer modal) cannot survive a post change any more.
- 'Your last download for this post finished' appeared on every revisit. It
  now only shows when this visit actually saw the download running (derived
  state, set during render), which still reports a re-attached download
  finishing while staying quiet on later visits; the 'In library' button
  remains the persistent indicator.

Library detail gets the same per-item key, so its delete confirmation and
optimizer modal reset between items too.

tsc, oxlint and the build are clean (the derived-state pattern was chosen
over a ref read in render / setState-in-effect, both flagged by the linter).
2026-09-18 17:52:50 -05:00
2026-09-18 14:11:10 -05:00
2026-09-18 14:11:10 -05:00

J621

Self-hosted media library and e621 archive manager, rebuilt as a React SPA + Django REST API.

Structure

backend/    Django 6 + DRF API (MariaDB + Redis via docker compose)
frontend/   Vite + React + TypeScript SPA
deploy/     Docker images, compose variants and Tailscale serve configs
extras/     shell integrations (fish_greeting with fastfetch)

Development

Backend

cd backend
source venv/bin/activate
python manage.py migrate
python manage.py scan_files            # index the watched folder
python manage.py runserver

Copy .env.example to .env and set WATCHED_FOLDER before scanning.

Frontend

cd frontend
npm install
npm run dev                            # http://localhost:5173, proxies /api to :8000

Production

Docker: see deploy/ for the two images (SPA on static nginx, API on gunicorn), the three compose variants (both / frontend-only / backend-only) behind a shared nginx service and a Tailscale sidecar, and the public-funnel or tailnet-only serve configs. deploy/push_*.sh builds and pushes the multi-arch images to the Gitea registry.

Random image endpoint

Used by the SPA's Random page and by shell greetings (fish_greeting + fastfetch):

curl -H "Authorization: Token <token>" \
  "https://j621.example.ts.net/api/random/?rating=s,q&fastfetch=1"
{
  "j_id": "J-59",
  "filename": "J-59.jpg",
  "extension": "jpg",
  "rating": "e",
  "url": "https://j621.example.ts.net/api/files/J-59/raw/?sig=…",
  "download_url": "https://j621.example.ts.net/api/files/J-59/raw/?sig=…&download=1",
  "thumbnail_url": "https://j621.example.ts.net/api/files/J-59/thumbnail/?sig=…",
  "fastfetch": true
}
  • rating — comma separated subset of s, q, e (default: any).
  • fastfetch=1, or any request whose User-Agent contains fastfetch, limits the roll to png/jpg/gif so terminals can display it. Images are the only candidates in both modes.
  • url is absolute, and signed for authenticated callers, so fastfetch can load it without headers. Guests get an unsigned URL and only see guest-visible items.
  • /random and /random/ are aliases of /api/random/ for scripts. Behind the bundled nginx those aliases negotiate on Accept: browsers get the SPA page, requesters like curl/wget/fastfetch get the JSON. /api/random/ is the unambiguous path for scripts; 404 when nothing matches the filters.
  • A ready-made shell greeting that uses this endpoint lives in extras/fish_greeting/.
  • Scoped tokens for scripts: the Account page's Shell tokens section (also /tokens) issues j621r_… tokens that only authenticate /api/random/ — the rest of the API rejects them. They are stored as hashes, shown once, and revocable any time (/api/auth/greeting-tokens/).

Licence

Source-available, non-commercial: personal and other non-commercial use is welcome under the Jake Labs Non-Commercial Software Licence, which requires attribution and keeps derivative works under the same licence. Commercial use is not permitted. Third-party dependencies keep their own licences (all permissive: MIT, BSD, Apache-2.0, ISC).

S
Description
No description provided
Readme
1.3 MiB
2026-09-23 22:35:56 -05:00
Languages
TypeScript 55.8%
Python 37.7%
Shell 3.3%
PowerShell 2.5%
Rust 0.5%
Other 0.1%