diff --git a/docs/FRONTEND.md b/docs/FRONTEND.md
index 8b5b958..a3ea702 100644
--- a/docs/FRONTEND.md
+++ b/docs/FRONTEND.md
@@ -60,11 +60,13 @@ No router library — a tiny history wrapper (`lib/route.ts`: `navigate()`,
share button), message list (`Markdown` for assistant, emerald bubble for
user), rounded composer with paperclip attach menu, attachment chips
(Enter to send, Shift+Enter for newline). With `readOnly` (archived chat)
- the composer and share button are replaced by an unarchive banner.
+ the composer is replaced by an unarchive banner with a share button
+ (archived chats stay shareable, but an archived public chat cannot go
+ private again — `DELETE …/share` answers 403 until unarchived).
- `dialogs.tsx` — `ConvEditDialog` (title + topic), `ShareModal` (public link,
copy, unshare), `SettingsModal` (display name, avatar upload box with
drag & drop, avatar URL, theme, plus an "Archived chats" category to
- view / unarchive / delete archived conversations).
+ view / unarchive / delete / share archived conversations).
- `AdminPanel.tsx` — No-KYC account management: create (key shown once),
regenerate (revokes sessions), edit, delete with confirm, plus server-side
search / sort (newest, oldest, A–Z, Z–A) / email filter / pagination.
diff --git a/frontend/src/components/Chat.tsx b/frontend/src/components/Chat.tsx
index 3e5b535..803c06f 100644
--- a/frontend/src/components/Chat.tsx
+++ b/frontend/src/components/Chat.tsx
@@ -625,6 +625,15 @@ export function Chat({
{t("archived.readonly")}
+
+ setShareTarget(c)}
+ className="rounded-full p-2 transition hover:bg-stone-100 disabled:opacity-50 dark:hover:bg-zinc-800"
+ >
+
+ v
))}
)}
+ setShareTarget(null)} />
setDeleteTarget(null)}
diff --git a/frontend/src/lib/i18n.ts b/frontend/src/lib/i18n.ts
index 5488294..5a9e5bb 100644
--- a/frontend/src/lib/i18n.ts
+++ b/frontend/src/lib/i18n.ts
@@ -160,6 +160,7 @@ const en = {
"dlg.shareIntro":
"Anyone with this link can read {title}. No login required.",
"dlg.unshare": "Unshare",
+ "dlg.unshareBlocked": "Archived chats stay public — unarchive first to remove the link.",
"dlg.reviewOcr": "Review transcription",
"dlg.reviewText": "Review text attachment",
"dlg.transcribing": "Transcribing the image on the server (OCR)…",
@@ -881,6 +882,7 @@ const es: Dict = {
"dlg.shareCreating": "Creando enlace público…",
"dlg.shareIntro": "Cualquiera con este enlace puede leer {title}. Sin login.",
"dlg.unshare": "Dejar de compartir",
+ "dlg.unshareBlocked": "Archived chats stay public — unarchive first to remove the link.",
"dlg.reviewOcr": "Revisar transcripción",
"dlg.reviewText": "Revisar adjunto de texto",
"dlg.transcribing": "Transcribiendo la imagen en el servidor (OCR)…",
@@ -1598,6 +1600,7 @@ const fr: Dict = {
"dlg.shareCreating": "Création du lien public…",
"dlg.shareIntro": "Quiconque a ce lien peut lire {title}. Sans connexion.",
"dlg.unshare": "Ne plus partager",
+ "dlg.unshareBlocked": "Archived chats stay public — unarchive first to remove the link.",
"dlg.reviewOcr": "Relire la transcription",
"dlg.reviewText": "Relire la pièce jointe",
"dlg.transcribing": "Transcription de l'image sur le serveur (OCR)…",
@@ -2309,6 +2312,7 @@ const ru: Dict = {
"dlg.shareCreating": "Создаём публичную ссылку…",
"dlg.shareIntro": "Каждый с этой ссылкой прочитает {title}. Вход не нужен.",
"dlg.unshare": "Закрыть доступ",
+ "dlg.unshareBlocked": "Archived chats stay public — unarchive first to remove the link.",
"dlg.reviewOcr": "Проверить распознавание",
"dlg.reviewText": "Проверить вложение",
"dlg.transcribing": "Распознаём изображение на сервере (OCR)…",
@@ -3011,6 +3015,7 @@ const it: Dict = {
"dlg.shareIntro":
"Chiunque abbia questo link può leggere {title}. Senza login.",
"dlg.unshare": "Rimuovi condivisione",
+ "dlg.unshareBlocked": "Archived chats stay public — unarchive first to remove the link.",
"dlg.reviewOcr": "Rivedi trascrizione",
"dlg.reviewText": "Rivedi allegato",
"dlg.transcribing": "Trascrizione immagine sul server (OCR)…",
@@ -3717,6 +3722,7 @@ const pt: Dict = {
"dlg.shareCreating": "A criar link público…",
"dlg.shareIntro": "Quem tiver este link pode ler {title}. Sem login.",
"dlg.unshare": "Remover partilha",
+ "dlg.unshareBlocked": "Archived chats stay public — unarchive first to remove the link.",
"dlg.reviewOcr": "Rever transcrição",
"dlg.reviewText": "Rever anexo",
"dlg.transcribing": "A transcrever imagem no servidor (OCR)…",
diff --git a/src/index.ts b/src/index.ts
index 4766493..289f8f8 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1751,6 +1751,8 @@ const server = Bun.serve<{ ticketId: string | null; isAdmin: boolean }>({
return publicId ? json({ publicId, url: `/share/${publicId}` }) : json({ shared: false });
}
if (req.method === "DELETE") {
+ // Archived public chats stay public: unarchive first to go private.
+ if (conv.archived_at) return json({ error: "conversation is archived (read-only) — unarchive it to remove the public link" }, 403);
await deleteShare(conv.id);
return json({ ok: true });
}