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>
Previous version of Tanabata used as visual and functional reference
for the new Go + SvelteKit rewrite.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
.gitignore covers env/secrets, OS files, IDE, Go build artifacts,
frontend build output, data dirs, and vendored reference libs.
.gitattributes enforces LF line endings, marks binaries, configures
diff drivers per language, and sets Linguist hints for repo stats.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>