User profile, activity editing, search, OSM links, moderator role,

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.
This commit is contained in:
Ole-Morten Duesund 2026-05-25 12:44:33 +02:00
commit 6f4c11c7a6
16 changed files with 1152 additions and 107 deletions

View file

@ -70,13 +70,53 @@ export interface RecoveryCompleteRequest {
export interface MeResponse {
id: string;
email: string;
display_name: string | null;
is_moderator: boolean;
username: string | null;
public_list_enabled: boolean;
}
export interface ProfileUpdateRequest {
// All optional — omit a field to leave it alone. Pass `null` to clear.
display_name?: string | null;
username?: string | null;
public_list_enabled?: boolean;
}
/** Response shape for GET /api/users/:username/list (opt-in public list). */
export interface PublicListResponse {
username: string;
display_name: string | null;
activities: ActivityPublic[];
}
// --- Feedback ---------------------------------------------------------------
export type FeedbackKind = 'feature' | 'bug';
export interface FeedbackSubmitRequest {
kind: FeedbackKind;
body: string;
}
export interface FeedbackEntry {
id: string;
kind: FeedbackKind;
body: string;
created_at: number;
// Moderator-only fields; included when the caller is a moderator viewing
// the list. (The submit endpoint doesn't return these — a submitter doesn't
// need to see them.)
user_id?: string;
user_email?: string;
user_display?: string | null;
}
// --- Activities --------------------------------------------------------------
export interface ActivityPublic {
id: string;
visibility: 'public';
owner_id: string; // serialized for public
owner_id: string; // serialized for public
owner_display: string; // display_name OR derived handle (email prefix)
title: string;
tags: string[];
loc_label: string | null;