Address code review findings from reuse, quality, and efficiency agents:
- Cache manifest JSON and service worker JS at init (was rebuilt per
request with allocations and JSON encoding on every hit)
- Add ImagePathsByUser store method for targeted image cleanup (was
loading 100k full fave objects just to read image_path)
- Add missing aria-label on privacy toggle in fave_list.html (inline
copy had drifted from the partial — accessibility bug)
- Fix comment/function name mismatch in pwa.go
- Remove redundant user nil-check in handleShare (requireLogin guards)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Admins can now change user roles and permanently delete user accounts.
- New SetRole store method with validation (user/admin only)
- New Delete store method — cascades via foreign keys to sessions,
faves, and fave_tags
- handleAdminSetRole: change role with self-modification prevention
- handleAdminDeleteUser: permanent deletion with image cleanup from
disk before cascade delete, self-deletion prevention
- admin_users.html: role dropdown with save button per user row,
delete button with hx-confirm for safety
- Routes: POST /admin/users/{id}/role, POST /admin/users/{id}/delete
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fave cards in the list and profile views now show edit, delete, and
privacy toggle buttons directly — no need to open the detail page first.
- New POST /faves/{id}/privacy route with HTMX privacy toggle partial
- New UpdatePrivacy store method for single-column update
- fave_list.html: edit link, HTMX delete, privacy toggle on every card
- profile.html: edit/delete for owner's own cards
- privacy_toggle.html: new HTMX partial that swaps inline on toggle
- CSS: compact .fave-card-actions styles
The existing handleFaveDelete already returns empty 200 for HTMX
requests, so hx-target="closest article" hx-swap="outerHTML" removes
the card from DOM seamlessly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Make Favoritter installable as a Progressive Web App with offline
static asset caching and Web Share Target API for Android.
New files:
- internal/handler/pwa.go: handlers for manifest, service worker,
and share target
- web/static/sw.js: service worker (cache-first static, network-first
HTML) with {{BASE_PATH}} placeholder for subpath deployments
- web/static/icons/: placeholder PWA icons (192, 512, 512-maskable)
Key design decisions:
- Share target uses GET (not POST) to avoid CSRF token issues — Android
apps cannot provide CSRF tokens
- Manifest is generated dynamically to inject BasePath into start_url,
scope, icon paths, and share_target action
- Service worker served at /sw.js with Cache-Control: no-cache and
BasePath injected via string replacement
- handleShare extracts URLs from Android's "text" field as fallback
(many apps put the URL there instead of "url")
- handleFaveNew replaced with handleFaveNewPreFill that reads url,
description, notes from query params (enables share + bookmarklets)
- SW registration in app.js reads base-path from <meta> tag (CSP-safe)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add an optional long-form "notes" text field to each favorite for
reviews, thoughts, or extended descriptions. The field is stored in
SQLite via a new migration (002_add_fave_notes.sql) and propagated
through the entire stack:
- Model: Notes field on Fave struct
- Store: All SQL queries (Create, GetByID, Update, list methods,
scanFaves) updated with notes column
- Web handlers: Read/write notes in create, edit, update forms
- API handlers: Notes in create, update, get, import request/response
- Export: Notes included in both JSON and CSV exports
- Import: Notes parsed from both JSON and CSV imports
- Feed: Notes used as Atom feed item summary when present
- Form template: New textarea between URL and image fields
- Detail template: Display notes, enhanced og:description with
cascade: notes (truncated) → URL → generic fallback text
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tag autocomplete suggestions were silently broken by CSP (script-src
'self') which blocks inline event handlers. Replaced onclick attributes
with data-tag-name + delegated mousedown/touchend listeners in app.js.
Also changed hx-params="*" to hx-params="none" to avoid sending
unrelated form fields to the search endpoint.
Display name in "av <name>" on fave cards was empty for users without
a custom display name. Changed SQL queries to use
COALESCE(NULLIF(u.display_name, ''), u.username) for automatic fallback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
README already documents the setup steps. Package install scripts
should be silent on success.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The preremove script was unconditionally stopping and disabling the
service, which meant upgrades (dpkg -i new.deb) would disable the
service. Users had to manually re-enable after every upgrade.
Now:
- preremove: only stop+disable on actual removal (not upgrade)
Checks $1 for "remove"/"purge" (deb) or "0" (rpm)
- postinstall: restart the service on upgrade if it was running,
preserving enable/disable state. Only shows first-install
instructions on initial install.
Tested with shellcheck.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
nfpm v2 does not expand ${VAR} in contents.src fields. The deb/rpm
targets now pipe nfpm.yaml through envsubst to resolve ARCH and
VERSION before passing the config to nfpm.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Containerfile referenced golang:1.23 but go.mod requires 1.26.1.
Verified end-to-end: image builds, health check works, all routes
respond, API login succeeds, version flag shows 0.1.0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bugs fixed:
- Space key was hijacked in tag input when a suggestion was
highlighted, preventing users from typing spaces. Removed
Space as a selection key (Enter is sufficient per combobox
pattern).
- ArrowUp was clamped to index 0, making it impossible to
deselect all suggestions and return to free typing. Now
allows arrowing back to -1 which clears aria-activedescendant.
Cleanup:
- Remove dead inline onkeydown handlers from tag suggestion
<li> elements (tabindex="-1" means they never receive focus,
so the handlers never fire; the global keydown listener in
app.js handles keyboard navigation).
- Add outline to aria-selected="true" state for visual parity
with hover (keyboard users now see the same indicator).
- Announce "Ingen forslag" in live region when suggestions are
empty (screen readers previously got silence).
- Add responsive table wrapper to admin tags and admin requests
tables (was only on admin users).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tag autocomplete combobox pattern (WCAG 2.1.1, 4.1.2, 4.1.3):
- Add role="combobox", aria-expanded, aria-haspopup to tag input
- Implement arrow key navigation (up/down) through suggestions
- Add Space key support alongside Enter for selecting tags
- Manage aria-activedescendant to track highlighted option
- Add Escape to close suggestions
- Add aria-live="polite" status region announcing suggestion count
- Add aria-selected state on options
- Tag suggestions now have stable IDs for activedescendant
Focus visibility (WCAG 2.4.7):
- Remove outline:none on tag suggestions, replace with visible
2px solid outline on :focus-visible
Contrast (WCAG 1.4.3):
- Replace opacity:0.5 on disabled rows with muted text color
and strikethrough on username (maintains 4.5:1 ratio)
Structure and semantics (WCAG 1.3.1):
- Fix heading hierarchy H1→H3 skip in import.html (now H2)
- Replace <nav> misuse for fave actions with div[role="group"]
- Add aria-label="Administrasjonsmeny" to admin dashboard nav
- Wrap admin users table in responsive scrollable region
- Remove redundant "Bilde for:" prefix from image alt text
- Make error page H1 descriptive: "Feil 404: Ikke funnet"
- Add .sr-only utility class for screen-reader-only content
- Add hreflang="en" to English-language external link
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bugs fixed:
- Renderer.Error set WriteHeader before Content-Type, causing
the header to be silently dropped. Now sets Content-Type first.
- truncate template function operated on bytes, not runes — could
split multi-byte UTF-8 characters (Norwegian æøå). Now uses
[]rune for correct Unicode handling.
Performance:
- Skip session DB lookup (2 queries) on /static/ and /uploads/
requests — these never use user context.
UX consistency:
- Replace all http.NotFound and http.Error("Forbidden") in
handler layer with styled error pages via Renderer.Error.
- Add notFound/forbidden helper methods on Handler.
Deployment fixes:
- Remove false libc6/glibc deps from nfpm.yaml (binary is
statically linked with CGO_ENABLED=0).
- Add CGO_ENABLED=0 to Makefile build target for consistency.
- Add .dockerignore to exclude .git, dist/, data/ from build
context.
- Remove phantom 'lint' from Makefile .PHONY.
- Document ProtectSystem=strict constraint in systemd service.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers quick start (binary and container), all environment variables,
Caddy deployment examples (subdomain, subpath, remote proxy),
API usage with curl examples, complete route table, tech stack,
security features, and development instructions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Security fixes:
- Fix XSS in Atom feed: escape user-supplied URLs in HTML content
- Wrap signup request approval in a transaction to prevent
partial state on crash (user created but request still pending)
- Stop leaking internal error messages to admin UI
- Add request body size limit on API import endpoint
- Log SetMustResetPassword errors instead of silently discarding
Correctness fixes:
- Handle errors from API fave update/delete instead of returning
success on failure
- Use actual data timestamp for feed <updated> instead of
time.Now() (improves HTTP caching)
- Replace hardcoded 10000 export limit with named constant
(maxExportFaves = 100000)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 6 — JSON API:
- POST /api/v1/auth/login — returns session token
- POST /api/v1/auth/logout
- GET/POST /api/v1/faves — list own faves (paginated), create fave
- GET/PUT/DELETE /api/v1/faves/{id} — get, update, delete fave
- GET /api/v1/tags?q= — search tags
- GET /api/v1/users/{username} — public profile
- GET /api/v1/users/{username}/faves — public faves (paginated)
- GET /api/v1/export/json — export own faves
- POST /api/v1/import — import faves from JSON
All endpoints return JSON. Auth via session cookie (same as web UI).
Privacy-aware: private faves hidden from non-owners.
Respects profile visibility settings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 5 — Feeds & Import/Export:
- Atom feeds: global (/feed.xml), per-user (/u/{name}/feed.xml),
per-tag (/tags/{name}/feed.xml). Uses gorilla/feeds.
- JSON export: all user's faves with tags, pretty-printed
- CSV export: standard format with header row
- JSON import: validates and creates faves with tags
- CSV import: flexible column mapping from header row
- Import/export pages with format documentation
- Feed items include enclosure for images, author info
- Limited-visibility profiles excluded from feeds
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 4 — Admin Panel:
- Admin dashboard with user/fave/pending-request counts
- User management: create with temp password, reset password,
enable/disable accounts (prevents self-disable)
- Tag management: rename and delete tags
- Signup request management: approve (creates user with
must-reset-password) and reject pending requests
- Site settings: site name, description, signup mode
(open/requests/closed)
- All admin routes require both login and admin role
- SignupRequest model and full store (create, list pending,
approve with user creation, reject)
- SetMustResetPassword method on UserStore for admin password resets
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>