Complete Open Graph coverage across all public pages:
- Home page: og:title (site name), og:description, og:type=website,
og:url, og:site_name
- Tag browse: og:title with tag name, og:description with count,
og:url, og:site_name
- Profile: add og:description using bio (truncated to 200 chars)
with fallback to generic text
Previously only fave detail and profile (without description) had
OG tags.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>