Optional description field on activities

A free-text body alongside title/tags/location/scheduled. Plain text
for now; markdown rendering is a deliberate non-goal (the user noted
it was nice-to-have but not essential).

Schema (additive, idempotent via ensureColumn):
  - activities.description TEXT NULL
  - For private rows the column stays NULL; the description lives
    inside the encrypted payload alongside title.

Wire/types:
  - PrivatePayload.description?: string  (in shared/crypto.ts)
  - ActivityPublic.description / ActivitySemi.description: string | null
  - CreateActivityRequest.description?: string | null

Server:
  - INSERT and UPDATE handlers now write description for semi/public
  - Private→semi/public transition: description column populated
  - Semi/public→private transition: description column wiped (now in
    the encrypted blob)
  - serialize() includes the column on public and semi rows
  - server/users.ts public-list endpoint surfaces it too

Frontend:
  - ActivityForm.svelte: textarea after the title field; round-trips
    through the existing private-encrypt / plaintext-PATCH paths
  - ActivityRow.svelte: renders the description as a `white-space:
    pre-wrap` <p> so line breaks survive without enabling markdown
  - Home.svelte: search now matches against the description text
    (decrypted client-side for private rows, just like the title)
This commit is contained in:
Ole-Morten Duesund 2026-05-25 14:08:55 +02:00
commit 3215917b7a
8 changed files with 48 additions and 6 deletions

View file

@ -182,6 +182,8 @@ export function unwrapDek(sealed: Sealed, kek: Bytes): Bytes {
export interface PrivatePayload {
title: string;
tags: string[];
/** Optional free-text body. Plain text (markdown rendering is a future polish). */
description?: string;
loc_label?: string;
loc_lat?: number;
loc_lng?: number;

View file

@ -184,6 +184,8 @@ export interface ActivityPublic {
// client renders the owner attribution as a link to /<owner_username>/list.
owner_username: string | null;
title: string;
/** Optional free-text body. Plain text. Empty string and null treated the same client-side. */
description: string | null;
tags: string[];
loc_label: string | null;
loc_lat: number | null;
@ -205,6 +207,7 @@ export interface ActivitySemi {
// anyone else. Stripped server-side for any other viewer; see SECURITY.md.
owner_id?: string;
title: string;
description: string | null;
tags: string[];
loc_label: string | null;
loc_lat: number | null;
@ -237,6 +240,7 @@ export interface CreateActivityRequest {
visibility: Visibility;
// For semi/public:
title?: string;
description?: string | null;
tags?: string[];
loc_label?: string | null;
loc_lat?: number | null;