Commit Graph

36 Commits

Author SHA1 Message Date
bvandeusen 899b193085 feat(fc2a): add TimelineSidebar and assemble GalleryView
GalleryView wires the grid + timeline + modal together. The URL is the
source of truth for which image (if any) is open — clicking an item pushes
?image=N; closing the modal pops it. This makes deep links shareable and
the back button work intuitively.

Below 900px viewport, the timeline drops below the grid as a horizontal
strip so it doesn't compete with thumbnail space on mobile.

Modal store + ImageViewer are placeholders here; real implementations land
in Batch 5 (Tasks 19–22).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:32:50 -04:00
bvandeusen 708c033627 feat(fc2a): add GalleryGrid with infinite scroll and date headers
Grid uses CSS grid auto-fill with 160px minimums so it adapts naturally
from phone to large displays. Date headers render once per (year, month)
group; the API's date_groups payload tells us where to slot them without
client-side grouping. IntersectionObserver with rootMargin: 600px loads
the next page well before the sentinel reaches the viewport.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:32:11 -04:00
bvandeusen da89fe6be2 feat(fc2a): add gallery Pinia store, GalleryItem, and EmptyState
Gallery store handles cursor pagination, date-group merging across pages,
tag filter, timeline buckets, and jump-to-month. Stale response detection
via an inflightId counter prevents out-of-order page additions when the
user toggles filters quickly.

GalleryItem is a square thumbnail card with hover/focus accent outline,
keyboard activation (Enter/Space → open), video badge for video MIME types,
and a broken-image fallback if the thumbnail URL fails to load.

EmptyState points operators to /settings?tab=import so the empty path is
self-guiding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:31:53 -04:00
bvandeusen 968a9e14e3 feat(fc2a): implement Settings Import tab components
ImportTriggerPanel shows an active-batch progress indicator when scan_directory
is running, or a "Quick scan" button otherwise. Deep scan deferred to FC-2d
is called out in the panel text so it's not a surprise.

ImportFiltersForm autosaves on blur/change so there's no Submit button. The
transparency and single-color sliders gate on their respective switches.

ImportTaskList uses v-data-table-virtual for performance on long histories,
with status filter, retry-failed, and a clear-completed dialog with age
buckets. Status chips are color-coded via FabledDesignSystem semantic
tokens (success/warning/error/accent/info).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:31:22 -04:00
bvandeusen 3bcff142f5 feat(fc2a): add SettingsView shell with Overview tab (SystemStatsCards)
Overview tab shows total images/tags/storage/pending/failed using
Fraunces for the big numbers. Inline alert nudges the operator to the
Import tab when pending tasks exist. Polls /api/system/stats every 5s
while the tab is visible (pauses when the document is hidden).

Import tab references three placeholders (TriggerPanel/FiltersForm/
TaskList); real implementations land in Task 15.

Router replaces the placeholder view for /settings with SettingsView.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:30:27 -04:00
bvandeusen af9f42ef68 feat(fc2a): add useApi composable, import store, extend system store with stats
useApi centralizes fetch error handling — every API call throws ApiError
on non-2xx with the parsed body attached, so components don't repeat the
'check response.ok' boilerplate.

import store wraps import settings, batch status, task list (with cursor
pagination), and the trigger/retry/clear admin actions. system store
gains a refreshStats() that powers the Settings overview tab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:29:49 -04:00
bvandeusen e597a9300e feat(fc2a): add /api/import admin endpoints
trigger enqueues a Celery scan_directory task and returns 202 with the
celery task id (the actual scan happens asynchronously; status surfaces
via /api/import/status and /api/system/stats). list_tasks supports
status filter and cursor pagination. retry-failed resets failed tasks
to queued and re-enqueues. clear-completed deletes finished tasks
older than the supplied age (used by the Settings UI's cleanup control).

mode='deep' is explicitly rejected with 400 until FC-2d ships it; the
frontend's UI in FC-2a only sends 'quick'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:11:49 -04:00
bvandeusen 62eac7cffd fix(fc2a): register settings_bp in all_blueprints
Missed in the Task 11 commit because of a stale-cache Edit failure.
Routes /api/settings/import and /api/system/stats are now actually mounted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:11:10 -04:00
bvandeusen ff28f29dbb feat(fc2a): add /api/settings/import and /api/system/stats endpoints
Settings GET returns the single-row ImportSettings DB record; PATCH does
selective field updates (only known editable fields are applied; unknown
keys silently ignored). System stats returns image/tag/storage counts,
task status histogram, and the active batch summary in one shot for the
Settings overview tab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:10:57 -04:00
bvandeusen 0e66368010 feat(fc2a): add /api/tags endpoints (autocomplete, create, image association)
Thin async blueprint delegating to TagService. Returns 400 with the
TagValidationError message on bad kind/fandom combos so the frontend can
surface the reason. List/add/remove endpoints scoped under
/api/images/<id>/tags follow REST conventions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:10:22 -04:00
bvandeusen fd8cf83003 feat(fc2a): add /api/gallery endpoints (scroll, timeline, jump, image detail)
Thin Quart blueprint that delegates to GalleryService. Cursor-based scroll
returns images + a date_groups summary so the SPA can render year-month
headers without re-grouping client-side. Timeline returns year-month
buckets for the sidebar jump nav. Jump returns a cursor positioned at a
specific year-month so the gallery can scroll to historical periods.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:09:44 -04:00
bvandeusen 509c19ce86 feat(fc2a): add maintenance tasks (recovery + cleanup) with beat schedule
recover_interrupted_tasks runs every 5 minutes, finds ImportTask rows
stuck in 'processing' for >30 minutes (well above any legitimate import
duration), and re-queues them. cleanup_old_tasks runs daily and deletes
finished tasks older than 7 days so the task table stays an operational
view rather than an archive.

Both thresholds match ImageRepo's precedent. The 30-min stuck threshold
is documented inline so a future reader can adjust it intentionally
rather than mistaking it for a 'magic number'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:09:09 -04:00
bvandeusen d3bb8c509a feat(fc2a): add Celery tasks — scan_directory, import_media_file, generate_thumbnail
scan_directory walks ImportSettings.import_scan_path, creates an
ImportBatch, enumerates supported files into ImportTasks, and enqueues
import_media_file per task. import_media_file moves the task through
its state machine (pending → queued → processing → complete/skipped/failed),
updates ImportBatch counters atomically (UPDATE ... SET col = col + 1),
enqueues a thumbnail task on success, and marks the batch complete when
the last task drains.

generate_thumbnail runs on its own queue (thumbnail) so big imports
don't starve thumbnail throughput; failure here is logged and does not
fail the import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:08:37 -04:00
bvandeusen f38a1d48c5 feat(fc2a): add GalleryService — cursor scroll, timeline, image detail with neighbors
Cursor format: base64(iso8601_created_at|image_id). Pagination key is
(created_at DESC, id DESC) so we don't drift when new imports land between
page loads. Timeline groups by date_part(year, month) so the sidebar can
render year-month jump buckets. get_image_with_tags returns full image
detail plus prev/next ids so the modal viewer can navigate without an
extra round-trip per arrow press.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:07:54 -04:00
bvandeusen bb9c183ff7 feat(fc2a): add Importer service
Single-file pipeline: validate as image, apply filter rules (min dimensions,
transparency), SHA256 hash dedup, atomic copy to /images/{subdir}/, create
ImageRecord with origin='imported_filesystem' and integrity_status='unknown',
auto-derive top-level folder name as Artist + artist tag.

Importer is intentionally sync (consumed by a Celery worker process); the
async Quart side uses the same ORM through its own async session. The
db_sync fixture in conftest.py was added in Task 3 to support these tests.

Thumbnail generation is NOT inlined; the calling Celery task enqueues a
separate thumbnail task so the import queue keeps moving on big batches.
pHash dedup is FC-2d, not here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:07:01 -04:00
bvandeusen 1a2a120da1 feat(fc2a): add Thumbnailer service
Generates 400px-max thumbnails: PNG with alpha preserved when source has
transparency, JPEG (q=85, progressive) otherwise. Storage layout is
/images/thumbs/{sha[:3]}/{sha}.{jpg|png} so each prefix bucket holds at
most ~4k files (16^3 buckets across all possible SHAs).

Video path uses ffmpeg's first-frame snapshot at ~5% offset; we have a
1-second floor for short clips. ffmpeg call has a 60s timeout. Video
tests are deferred to FC-2e's integration suite where transcoding is
also wired up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:05:19 -04:00
bvandeusen f6c7117231 feat(fc2a): add TagService — find-or-create, autocomplete, image association
Validates kind/fandom rules at the service layer: fandom_id only allowed
for kind='character', and must reference an existing kind='fandom' tag.
Autocomplete ranking: exact match > prefix match > substring, tie-broken
by image_count descending. Image-tag association uses INSERT ON CONFLICT
DO NOTHING for idempotent re-tagging.

conftest.py adds a transactional AsyncSession fixture; each test rolls
back so they don't pollute each other. Also includes a sync Session
fixture (db_sync) for the Importer tests in Task 5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:04:48 -04:00
bvandeusen 4cb319b393 feat(fc2a): add slug and paths utilities
slugify() produces ASCII-only lowercase slugs from arbitrary text (used for
artist slugs and tag slugs). paths.derive_subdir/derive_top_level_artist
extract the destination layout and folder→artist convention from an import
source path. hash_suffixed_name builds IR-style 'stem__hash10.ext' filenames.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:04:05 -04:00
bvandeusen 9eb1614fbf feat(fc2a): schema migration 0002 — tag kinds + fandom hierarchy + import lifecycle
Adds Tag.kind enum (artist/character/fandom/general/series/archive/post/meta/rating),
Tag.fandom_id FK with CHECK constraint (only valid for kind='character'), and a
kind-aware uniqueness index so the same name can exist across kinds and the same
character name can exist in different fandoms.

Adds ImportBatch + ImportTask state-machine tables for scan tracking, plus a
single-row ImportSettings table (CHECK id=1) holding the importer's filter knobs.

Adds image_record.integrity_status column (defaults to 'unknown'); FC-2e
populates this via the integrity verifier.

Drops the unused tag.namespace column from FC-1 — superseded by kind.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:03:41 -04:00
bvandeusen ff122c55eb ci: switch runner label from fabledcurator-ci to python-ci
Generic python-ci runner is reusable across the family (FabledScribe,
FabledSteward, NhenArchiver, StashHandler, etc.) rather than scoped to
just this project. Runner image lives at CI-Runner/CI-python/ in the
operator's workspace; pattern mirrors CI-Runner/CI-go and CI-Runner/CI-flutter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:45:48 -04:00
bvandeusen edb3fafd4d ci: add image-build workflow + document required runner label and RELEASE_TOKEN
Pushes :dev on every dev push; pushes :main and :latest on main.
Uses RELEASE_TOKEN (a broader-scoped Forgejo PAT covering write:package,
read:package, write:release, write:issue) so the same secret can serve
future release-cutting and issue-management workflows.

README now documents the fabledcurator-ci runner label and the required
RELEASE_TOKEN scopes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:52:55 -04:00
bvandeusen 8cbe963b37 ci: add Forgejo CI workflow — backend lint+tests, frontend build
Backend job spins up Postgres+pgvector and Redis as services, runs ruff,
applies the initial migration to confirm it's clean, and runs pytest.
Frontend job runs vue-tsc and vite build.

Requires a runner labeled "fabledcurator-ci" with Python 3.14, ruff,
and Node 22 pre-installed. Integration tests run locally via
docker-compose with testing.Short() gating per FabledRulebook
verification.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:52:30 -04:00
bvandeusen 37e97d52ee chore: bump runtime targets to Python 3.14 and Node 22
Forward-looking pin per operator direction ("build for the future and not
rebuild the past"). Touches everywhere a version is named so all
downstream artifacts (Dockerfiles, ruff config, package.json engines)
agree.

Python 3.14 (released Oct 2025) is the current stable. Node 22 is the
most recent LTS line still receiving updates (Maintenance LTS since
Oct 2025); Node 24 (released Apr 2026) goes Active LTS in Oct 2026 and
will be the natural next bump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:52:15 -04:00
bvandeusen a281620178 feat: add docker-compose stack (web/worker/scheduler/ml-worker + postgres + redis)
Prod compose references the Forgejo registry; dev compose builds locally
and exposes DB/Redis ports. Volume layout: ./images, ./import, ./downloads,
./models (all gitignored, anchored to repo root). Plain HTTP only — no TLS
in-app (spec §2.1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:42:36 -04:00
bvandeusen ec03297382 feat: add Dockerfile.ml — separate image for the ml-worker role
ML deps (torch, transformers, onnxruntime, opencv) are ~4GB and stay out
of the regular web/worker image. Models self-heal into /models on start
(implemented in FC-2). HuggingFace cache vars point inside /models so
weight downloads are persistent across restarts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:42:14 -04:00
bvandeusen 1db0167bfc feat: add Dockerfile for web/worker/scheduler roles and entrypoint script
Multi-stage build: node:20 builds the SPA, python:3.12-slim runs the app.
Same image handles web, worker, scheduler roles via entrypoint.sh's first
arg. ffmpeg + unar are baked in for FC-2's transcode + archive paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:42:04 -04:00
bvandeusen 8e61dc4df4 feat: serve the built Vue SPA from Quart with history-mode fallback
The frontend blueprint is registered after the api blueprint so /api/*
routes are matched first; everything else falls through to the SPA.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:36:16 -04:00
bvandeusen bcd9a23501 feat: add SPA app shell, router, system store, and route placeholders
Nav lists every FC-2 and FC-3 surface with a placeholder view that names
the sub-project that will fill it. System store polls /api/health on mount
and renders a status pip in the nav footer.

Favicon uses Curator's aged amber accent (#A87338).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:36:01 -04:00
bvandeusen ad2ac31f1a feat: scaffold Vue 3 + Vuetify + Pinia frontend themed to FabledDesignSystem
Curator signature accent (#A87338, aged amber) is baked into
fabled-tokens.js. Vuetify theme consumes the same tokens module the rest
of the SPA does, so updating design system tokens propagates throughout.
Vite dev server proxies /api and /ws to the Quart backend.

App.vue references AppShell (lands in Task 9) and main.js references
router.js (lands in Task 9) — these are intentionally split so Task 8 is
the framework scaffold and Task 9 is the navigation/shell content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:35:33 -04:00
bvandeusen 8b5711abef docs: add module docstring to tasks package __init__ 2026-05-14 07:35:01 -04:00
bvandeusen 8773f2aae6 feat: configure Celery with per-feature queue lanes and a smoke task
Routes are pre-declared for FC-2/FC-3 task modules (import, ml, thumbnail,
download, scan, maintenance). Queue lanes match the ImageRepo pattern where
beat+maintenance run on a separate worker so long imports don't starve
periodic tasks. Smoke ping task confirms the wiring in eager mode for CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:34:47 -04:00
bvandeusen d6a156dcd2 feat: define unified schema (Artist, Source, Post, ImageRecord, etc.) and initial migration
Implements the data model from spec §3 in one go so FC-2/FC-3 don't need
schema-adding migrations of their own. Artist is the unified entity for
both gallery 'artist:' tags and GallerySubscriber Subscriptions
(is_subscription flag). ImageProvenance is many-to-one, enabling the
enrich-on-duplicate rule for downloaded content that pHash-matches an
existing record.

The SigLIP embedding column uses pgvector(1152) for SigLIP-so400m;
swapping models in FC-2 will require a column-width migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:34:29 -04:00
bvandeusen a03655039c feat: add SQLAlchemy declarative base, Alembic environment, and gitignore fix
Configures stable constraint naming so autogeneration produces clean diffs.
Alembic uses the sync psycopg driver while the runtime app uses asyncpg.

Also fixes a .gitignore bug caught during this task: the bare 'models/'
rule for the ML weights volume was matching backend/app/models/ (Python
package). Anchored all volume rules to repo root (/images/, /import/,
/downloads/, /models/, /postgres_data/, /redis_data/).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:32:58 -04:00
bvandeusen fe0a311d39 feat: add Quart app factory, config loader, and /api/health endpoint
Config reads from env vars (12-factor); .env.example documents defaults.
Health endpoint is liveness-only (no DB/Redis touch). Test added for CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:32:17 -04:00
bvandeusen 13eaa35f1c feat: scaffold backend Python project (Quart + SQLAlchemy + Celery deps)
Pins runtime and ML deps separately so the regular web image stays lean.
Configures ruff for py312 with bugbear, async, and pyupgrade lints enabled.
psycopg sync driver included up-front for alembic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:31:39 -04:00
bvandeusen 06f8f834dc chore: initialize repo with comprehensive .gitignore and README skeleton
Establishes the FabledRulebook-required ignore patterns (docs/superpowers/specs,
docs/superpowers/plans) plus Python/Node/Vite/extension/volumes/IDE rules from
spec §6.1 before any other code lands, so accidental adds can't leak. Also
ignores .claude/settings.local.json (per-user local overrides; shared
.claude/settings.json may be committed in the future).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:25:59 -04:00