- 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>
- 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>
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>
- 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>
Extend PATCH /tags/{id}/rules/{then_id} to accept apply_to_existing bool.
When a rule is activated with apply_to_existing=true, a single recursive
CTE retroactively inserts the full transitive expansion of then_tag into
data.file_tag for all files already carrying when_tag:
WITH RECURSIVE expansion(tag_id) AS (
SELECT then_tag_id
UNION
SELECT r.then_tag_id FROM data.tag_rules r
JOIN expansion e ON r.when_tag_id = e.tag_id
WHERE r.is_active = true
)
INSERT INTO data.file_tag ... ON CONFLICT DO NOTHING
Changes:
- port/repository.go: add applyToExisting param to TagRuleRepo.SetActive
- db/postgres/tag_repo.go: implement recursive CTE retroactive apply
- service/tag_service.go: thread applyToExisting through SetRuleActive
- handler/tag_handler.go: parse apply_to_existing from PATCH body
- openapi.yaml: document apply_to_existing on PATCH endpoint
- integration test: add TestTagRuleActivateApplyToExisting covering
no-op when false, direct+transitive apply when true
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- 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>
- 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>
- /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>
- 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>
- 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>
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>
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>
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>
- Make data.files.exif column nullable (was NOT NULL but service passes nil
for files without EXIF data, causing a constraint violation on upload)
- FileRepo.Create: include id in INSERT so disk storage path and DB record
share the same UUID (previously DB generated its own UUID, causing a mismatch)
- Integration test: use correct filter DSL format {t=<uuid>} instead of tag:<uuid>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add internal/integration/server_test.go covering the full happy-path
flow (admin login, user create, upload, tag assign, tag filter, ACL
grant, pool create/add/reorder, trash/restore/permanent-delete, audit
log). Also add targeted tests for blocked-user login prevention, pool
reorder, and tag auto-rules. Uses a disposable postgres:16-alpine
container via testcontainers-go.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add UserService (GetMe, UpdateMe, admin CRUD with block/unblock),
UserHandler (/users, /users/me), ACLHandler (GET/PUT /acl/:type/:id),
AuditHandler (GET /audit with all filters). Fix UserRepo.Update to
include is_blocked. Wire all remaining routes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add pool repo (gap-based position ordering, cursor pagination, add/remove/reorder
files), service, handler, and wire all /pools endpoints including
/pools/:id/files, /pools/:id/files/remove, and /pools/:id/files/reorder.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add category repo, service, handler, and wire all /categories endpoints
including list, create, get, update, delete, and list-tags.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
007_seed_data.sql: insert 10 MIME types (4 image, 6 video) with their
canonical extensions into core.mime_types.
disk.go: register golang.org/x/image/webp decoder so imaging.Open
handles WebP still images. Videos (mp4, mov, avi, webm, 3gp, m4v)
continue to go through the ffmpeg frame-extraction path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Files stored as {files_path}/{id} (no extension). The ext parameter
is removed from Save/Read/Delete in both the port interface and
the implementation.
Thumbnail and Preview both use imaging.Thumbnail (fit within
configured max bounds, never upscale, never crop) — the config
values THUMB_WIDTH/HEIGHT and PREVIEW_WIDTH/HEIGHT are upper limits,
not forced dimensions.
Non-decodable files (video, etc.) receive a #444455 placeholder.
Cache writes use atomic temp→rename; on cache failure the generated
image is served from memory so the request still succeeds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
filter_parser.go — recursive-descent parser for the {token,...} DSL.
Tokens: t=UUID (tag), m=INT (MIME exact), m~PATTERN (MIME LIKE),
operators & | ! ( ) with standard NOT>AND>OR precedence.
All values go through pgx parameters ($N) — SQL injection impossible.
file_repo.go — full FileRepo:
- Create/GetByID/Update via CTE RETURNING with JOIN for one round-trip
- SoftDelete/Restore/DeletePermanent with RowsAffected guards
- SetTags: full replace (DELETE + INSERT per tag)
- ListTags: delegates to loadTagsBatch (single query for N files)
- List: keyset cursor pagination (bidirectional), anchor mode,
filter DSL, search ILIKE, trash flag, 4 sort columns.
Cursor is base64url(JSON) encoding sort position; backward
pagination fetches in reversed ORDER BY then reverses the slice.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AuditRepo.Log resolves action_type_id/object_type_id via SQL subqueries.
AuditRepo.List supports dynamic filtering by user, action, object type/ID,
and date range with COUNT(*) OVER() for total count.
AuditService.Log reads user from context, marshals details to JSON,
and delegates to the repo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add postgres ACLRepo (List/Get/Set) and ACLService with CanView/CanEdit
checks (admin bypass, public flag, creator shortcut, explicit grants)
and GetPermissions/SetPermissions for the /acl endpoints.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cmd/server/main.go: replace stub router with full wiring —
UserRepo, SessionRepo, AuthService, AuthMiddleware, AuthHandler,
NewRouter; use postgres.NewPool instead of pgxpool.New directly.
migrations/001_init_schemas.sql: wrap uuid_v7 and uuid_extract_timestamp
function bodies with goose StatementBegin/End so semicolons inside
dollar-quoted strings are not treated as statement separators.
migrations/007_seed_data.sql: add seed admin user (admin/admin,
bcrypt cost 10, is_admin=true, can_create=true) for manual testing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Login: bcrypt credential validation, session creation, JWT pair issuance.
Logout/TerminateSession: soft-delete session (is_active = false).
Refresh: token rotation — deactivate old session, issue new pair.
ListSessions: marks IsCurrent by comparing session IDs.
ParseAccessToken: for use by auth middleware.
Claims carry uid (int16), adm (bool), sid (int). Refresh tokens are
stored as SHA-256 hashes; raw tokens never reach the database.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
db/db.go: TxFromContext/ContextWithTx for transaction propagation,
Querier interface (QueryRow/Query/Exec), ScanRow generic helper,
ClampLimit/ClampOffset pagination guards.
db/postgres/postgres.go: NewPool with ping validation, Transactor
backed by pgxpool (BeginTx → fn → commit/rollback), connOrTx helper
that returns the active transaction from context or falls back to pool.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Define all repository interfaces in port/repository.go:
FileRepo, TagRepo, TagRuleRepo, CategoryRepo, PoolRepo, UserRepo,
SessionRepo, ACLRepo, AuditRepo, MimeRepo, and Transactor.
Add OffsetParams and PoolFileListParams as shared parameter structs.
Define FileStorage interface in port/storage.go with Save, Read,
Delete, Thumbnail, and Preview methods.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- internal/config: typed Config struct loaded from env vars via godotenv;
all fields from docs (listen addr, JWT, DB, storage, thumbs, import)
- migrations/embed.go: embed FS so goose SQL files are baked into the binary
- cmd/server/main.go: load config → connect pgxpool → goose migrations
(embedded) → Gin server with GET /health returning 200 OK
- .env.example: documents all required and optional env vars
- go.mod: bump to Go 1.26, add gin/pgx/goose/godotenv as direct deps
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>