"use strict"; const qs = (sel) => document.querySelector(sel); const roomsListEl = qs('#roomsList'); const roomEl = qs('#room'); const tokenEl = qs('#token'); const tokenWrap = qs('#tokenWrap'); const contentEl = qs('#content'); const authorEl = qs('#author'); const statusEl = qs('#status'); const listEl = qs('#list'); let sse; // <— globale Referenz auf EventSource const seen = new Set(); // falls noch nicht vorhanden (für Dedupe) // remember last-used settings roomEl.value = localStorage.getItem('vc.room') || 'default'; tokenEl.value = localStorage.getItem('vc.token') || ''; authorEl.value = localStorage.getItem('vc.author') || ''; roomEl.addEventListener('change', () => localStorage.setItem('vc.room', roomEl.value.trim())); tokenEl.addEventListener('change', () => localStorage.setItem('vc.token', tokenEl.value)); authorEl.addEventListener('change', () => localStorage.setItem('vc.author', authorEl.value)); async function getJSON(path) { const url = new URL(path, location.href); const headers = { 'Accept': 'application/json' }; const token = tokenEl.value.trim(); if (token) headers['X-Token'] = token; const res = await fetch(url, { headers }); if (!res.ok) throw new Error(await res.text()); return res.json(); } async function postJSON(path, body) { const url = new URL(path, location.href); const headers = { 'Content-Type': 'application/json' }; const token = tokenEl.value.trim(); if (token) headers['X-Token'] = token; const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) }); if (!res.ok) throw new Error(await res.text()); return res.json(); } async function refreshHistory() { const n = Number(qs('#limit').value); const data = await getJSON(`/api/${encodeURIComponent(roomEl.value.trim())}/history?limit=${n}`); listEl.innerHTML = ''; for (const c of data.reverse()) { listEl.appendChild(renderClip(c)); } } function renderClip(c) { const el = document.createElement('div'); el.className = 'clip'; const when = new Date(c.created_at).toLocaleString(); el.innerHTML = `
${c.type} ${when} ${c.author ? `von ${escapeHtml(c.author)}` : ''} ID ${c.id.slice(-6)}
${linkify(escapeHtml(c.content))}
`; el.querySelector('[data-copy]').addEventListener('click', async () => { await navigator.clipboard.writeText(c.content); }); return el; } function linkify(text) { const urlRE = /(https?:\/\/[^\s]+)/g; return text.replace(urlRE, (u) => `${u}`); } function escapeHtml(s) { return s.replace(/[&<>"']/g, (ch) => ({'&':'&','<':'<','>':'>','"':'"','\'':'''}[ch])); } async function detectTokenRequirement() { try { const s = await getJSON('/status'); tokenWrap.style.display = s.requiresToken ? 'inline-flex' : 'none'; } catch (e) { console.warn(e); } } async function sendClip() { const content = contentEl.value; if (!content.trim()) return; const body = { type: 'text', content, author: authorEl.value || undefined }; await postJSON(`/api/${encodeURIComponent(roomEl.value.trim())}/clip`, body); contentEl.value = ''; } async function copyLatest() { const c = await getJSON(`/api/${encodeURIComponent(roomEl.value.trim())}/latest`); await navigator.clipboard.writeText(c.content); } function shareLink() { const url = new URL(location.href); url.searchParams.set('room', roomEl.value.trim()); if (tokenEl.value) url.searchParams.set('token', tokenEl.value.trim()); navigator.clipboard.writeText(url.toString()); alert('Link in Zwischenablage'); } function connectStream() { const params = new URLSearchParams(); if (tokenEl.value.trim()) params.set('token', tokenEl.value.trim()); const src = new EventSource(`/api/${encodeURIComponent(roomEl.value.trim())}/stream?${params}`); statusEl.textContent = 'verbunden'; src.addEventListener('clip', (ev) => { const c = JSON.parse(ev.data); if (seen.has(c.id)) return; seen.add(c.id); listEl.prepend(renderClip(c)); }); src.onerror = () => { statusEl.textContent = 'getrennt'; try { src.close(); } catch {} setTimeout(() => { sse = connectStream(); }, 1500); }; return src; } // wire up UI document.addEventListener('DOMContentLoaded', () => { qs('#btnSend').addEventListener('click', sendClip); qs('#btnRooms')?.addEventListener('click', loadRooms); qs('#btnCopyLatest').addEventListener('click', copyLatest); qs('#btnReload').addEventListener('click', refreshHistory); qs('#btnShare').addEventListener('click', shareLink); qs('#btnClear').addEventListener('click', async () => { if (!confirm('Verlauf in diesem Raum wirklich löschen?')) return; await fetch(`/api/${encodeURIComponent(roomEl.value.trim())}/history`, { method: 'DELETE', headers: tokenEl.value ? {'X-Token': tokenEl.value} : {} }); seen.clear(); await refreshHistory(); }); qs('#btnDelete').addEventListener('click', async () => { if (!confirm('Diesen Raum komplett löschen?')) return; await fetch(`/api/${encodeURIComponent(roomEl.value.trim())}`, { method: 'DELETE', headers: tokenEl.value ? {'X-Token': tokenEl.value} : {} }); seen.clear(); listEl.innerHTML = ''; }); const url = new URL(location.href); if (url.searchParams.get('room')) { roomEl.value = url.searchParams.get('room'); localStorage.setItem('vc.room', roomEl.value); } if (url.searchParams.get('token')) { tokenEl.value = url.searchParams.get('token'); localStorage.setItem('vc.token', tokenEl.value); } detectTokenRequirement(); refreshHistory().catch(console.error); loadRooms(); sse = connectStream(); }); async function loadRooms() { try { const s = await getJSON('/status'); const rooms = Array.isArray(s.rooms) ? s.rooms : []; roomsListEl.innerHTML = ''; for (const r of rooms) { const item = document.createElement('div'); item.className = 'clip'; // kleiner Stil-Reuse item.innerHTML = `
${escapeHtml(r)}
`; item.querySelector('[data-open]').addEventListener('click', () => switchRoom(r)); item.querySelector('[data-clear]').addEventListener('click', async () => { if (!confirm(`Verlauf in Raum "${r}" wirklich löschen?`)) return; await fetch(`/api/${encodeURIComponent(r)}/history`, { method: 'DELETE', headers: tokenEl.value ? {'X-Token': tokenEl.value} : {} }); if (r === roomEl.value.trim()) { seen.clear(); await refreshHistory(); } await loadRooms(); }); item.querySelector('[data-del]').addEventListener('click', async () => { if (!confirm(`Raum "${r}" wirklich löschen?`)) return; await fetch(`/api/${encodeURIComponent(r)}`, { method: 'DELETE', headers: tokenEl.value ? {'X-Token': tokenEl.value} : {} }); if (r === roomEl.value.trim()) { seen.clear(); listEl.innerHTML = ''; // aktuelle Verbindung trennen if (sse) { try { sse.close(); } catch{} sse = null; } } await loadRooms(); }); roomsListEl.appendChild(item); } } catch (e) { console.warn('rooms load failed', e); } } async function switchRoom(newRoom) { roomEl.value = newRoom; localStorage.setItem('vc.room', newRoom); seen.clear(); listEl.innerHTML = ''; if (sse) { try { sse.close(); } catch{} sse = null; } await refreshHistory().catch(console.error); sse = connectStream(); // neue Verbindung }