opt-in /<username>/list, and a feedback channel
Six related features that touch the user model and activity UX:
1. **User profile** (display_name, username, public_list_enabled).
New `display_name`, `username` (UNIQUE, slug-shaped), and
`public_list_enabled` columns. PATCH /api/auth/profile is a partial
update — pass only the fields you want to change, null to clear.
MeResponse exposes all three. Display name is shown on public
activities and in the nav; falls back to the email prefix when
unset.
2. **Change password from the profile editor.** Existing
/api/auth/password endpoint surfaced in the new Profile.svelte;
the local-decrypt failure path on a wrong current password is
mapped to a clean error.
3. **Edit existing activities.** ActivityForm becomes dual-purpose
(create or edit). Title, tags, date/time, location, and
visibility are all editable. Visibility transitions decrypt or
re-encrypt client-side as needed before PATCH, and the IndexedDB
private-tag index is kept in sync diff-style.
4. **Search.** A search input on Home filters across visible
activities. Private rows are searched against their decrypted
cleartext (decrypted once and memoised via $derived, so the work
is amortised across keystrokes). Matches across title, tags,
location label, and (for public) author display name.
5. **OpenStreetMap links.** Each row with a location renders the
label as an OSM link. Smart: coords if present
(?mlat=&mlon=&map=15/lat/lng → pinned view), else
/search?query=. Built with the WHATWG URL constructor so
Norwegian characters and commas survive.
6. **Moderator role + semi-delete by owner.** New is_moderator
column on users. Owners always delete their own rows; moderators
can additionally delete any semi or public activity (private is
excluded — it's invisible to others, so there's no moderation
case). README documents the manual promotion via sqlite3.
7. **Opt-in /<username>/list.** New server route
/api/users/:username/list returns the user's public activities
when both `username` is set AND `public_list_enabled = 1`. 404
when either condition fails — same response in both cases so the
route doesn't leak username existence for users who haven't opted
in. SPA-side, App.svelte parses window.location.pathname on
mount; falls back to "/" via history.replaceState after
authenticating from a deep link.
8. **Feedback channel.** New `feedback` table. POST /api/feedback
for any authenticated user; GET /api/feedback gated to
moderators. The Feedback.svelte component is dual-mode — the
form is universal; the list view auto-loads only for
moderators. Submitter identity (email + display name) is shown
to moderators so they can follow up; not exposed to the
submitter themselves.
Schema migrations land via the existing ensureColumn() helper so
scaffold DBs upgrade cleanly. The username UNIQUE constraint is
applied as a partial unique index (WHERE username IS NOT NULL) so
multiple users with NULL usernames don't collide.
All 26 existing tests still pass; typecheck clean for both
tsconfigs; Vite build succeeds.