vinterliste/shared
Ole-Morten Duesund f39fe9ed65 Friends + friends-only visibility + blocking
A fourth visibility level ("Venner") with one-way friendship and
two-way block filtering, plus the table-rebuild migration that drags
older dev DBs forward.

Visibility model:
  - Friendship is directional: (owner, friend) means owner has added
    friend to their list. Owner's friends-only activities become
    visible to friend; friend isn't automatically friends with owner.
  - Blocking is also directional at the DB level (blocker, blocked)
    but is checked SYMMETRICALLY at visibility-resolution time: once
    either user has blocked the other, friends-only content stops
    flowing in either direction. Block does NOT affect public or
    anonymous content — those are open to anyone by definition.
  - "Friends-only" is an access-list visibility, NOT cryptographic.
    The server stores the content in plaintext and serves it only to
    authorised viewers. This is documented honestly in /personvern.

Schema:
  - activities.visibility CHECK gains 'friends' as a fourth value
  - friends(owner_id, friend_id, created_at) — composite PK,
    self-friending blocked by CHECK
  - user_blocks(blocker_id, blocked_id, created_at) — same shape,
    blocking-self also blocked

Migration (server/db.ts):
  - SQLite can't ALTER a CHECK constraint, so the migration detects
    out-of-date DBs by scanning sqlite_master for the literal
    "'friends'" in the activities table's CREATE statement
  - If absent, rebuilds activities via the standard SQLite
    table-copy-drop-rename dance with foreign_keys briefly off
    around the transaction, then runs foreign_key_check to confirm
    no FKs were left orphaned (activity_tags, activity_hearts,
    bookmarks all point at activities). Smoke-tested on the dev DB:
    olemd's user row and moderator/admin flags survived.

Server endpoints (server/friends.ts):
  GET    /api/friends           — my outgoing list
  GET    /api/friends/incoming  — who has added ME
  POST   /api/friends           — add by username (idempotent)
  DELETE /api/friends/:userId   — remove a friend
  GET    /api/friends/blocks    — my blocked-users list
  POST   /api/friends/blocks    — block by user_id (idempotent)
  DELETE /api/friends/blocks/:userId — unblock

Add-by-username (not by email): users must set a username to be
findable. Email stays a private contact identifier.

Activity list filter (server/activities.ts): adds two clauses to the
WHERE — own friends-only, and friends-only owned by a user who has
added me AND there's no block in either direction. Single-activity
GET applies the same check.

Frontend:
  - ActivityForm.svelte gains the "Venner" option
  - ActivityRow.svelte renders a "Venner" badge with a new amber
    vis-badge.friends colour (passes contrast in both themes)
  - FriendsPanel.svelte: add-by-username form, outgoing, incoming
    (with Block button), and blocked (with Unblock button)
  - Profile.svelte mounts FriendsPanel between display fields and
    Eksporter
  - Home.svelte adds a "Venner" section between private and semi

Docs: Personvern.svelte gains a "Venner og blokkering" section
explaining that friends-only is access-list-not-crypto and pointing
the reader at "private" for actually-sensitive content.

26 tests still pass; typecheck clean; build succeeds. Bundle
36.8 KB → 39.1 KB gzipped (FriendsPanel + new server endpoints +
the Personvern prose).
2026-05-25 14:47:20 +02:00
..
crypto.ts Optional description field on activities 2026-05-25 14:08:55 +02:00
sodium.ts Scaffold Vinterliste — end-to-end encrypted winter activity list 2026-05-25 12:27:14 +02:00
types.ts Friends + friends-only visibility + blocking 2026-05-25 14:47:20 +02:00