feat(chat): share a chat by link — anonymous read-only /shared/<token> page, share/unshare

This commit is contained in:
2026-08-30 01:34:44 -04:00
parent ece93a7c8f
commit 114b115034
28 changed files with 3442 additions and 54 deletions
+23
View File
@@ -134,6 +134,29 @@
<span class="save-chat-label">Save</span>
</button>
<!-- Phase 51 (owner-locked 2026-08-29, `TODO.md` L6): "Share"
turns the current conversation into a PUBLIC read-only link —
/shared/<token> (a 128-bit uuid4 on the saved_chats row,
migration 0009; the anonymous page is task 03). The
save-then-share contract: an UNSAVED (unlinked) conversation
is saved AND shared in ONE action — app.js POSTs /api/chats
with { messages, share: true } (the server sets the token in
the same commit) and links the conversation to the created
row; a saved (linked) one just POSTs /api/chats/<id>/share
(idempotent — the existing token comes back unchanged). On
success the ABSOLUTE link is copied to the clipboard; a
non-secure (http) homelab origin that rejects the clipboard
gets the inline link-field fallback instead (owner-locked —
app.js renders .share-link-fallback near the status line).
Admin-only — ships HIDDEN exactly like Save (absent-not-
hidden, phase 16); app.js reveals it at boot (the same
admin-reveal block) and binds the click to shareCurrentChat.
Unsharing lives on the History page's Share column (task 04). -->
<button type="button" class="share-chat-btn" id="share-chat-btn" aria-label="Share chat" hidden>
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
<span class="share-chat-label">Share</span>
</button>
<!-- Phase 49 (2026-08-29, TODO.md L4): the meta row under a brain
bubble can carry JS-injected actions (app.js) — Tune (admin
only, phase 15) and Retry (every visitor; the LAST brain