(() => { 'use strict'; const PROFILE_KEY = 'ai-disclosure.bulk.profiles.v1'; const TEMPLATE_KEY = 'ai-disclosure.bulk.templates.v1'; const EXPORT_FORMAT = 'ai-disclosure-bulk-browser-data'; const $ = id => document.getElementById(id); const profileFields = [ ['profile-lang', 'lang'], ['profile-role', 'legalRole'], ['profile-use-context', 'useContext'], ['profile-assurance', 'assurance'], ['profile-author', 'author'], ['profile-author-url', 'authorUrl'], ['profile-responsible-role', 'responsibleRole'], ['profile-responsible', 'responsible'], ['profile-responsible-url', 'responsibleUrl'], ['profile-complaint-name', 'complaintName'], ['profile-complaint-email', 'complaintEmail'], ['profile-complaint-url', 'complaintUrl'] ]; let cfg = null; let activeTemplate = null; let currentResults = []; let exportsCache = {}; function readStore(key) { try { const parsed = JSON.parse(localStorage.getItem(key) || '{}'); return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}; } catch (_) { return {}; } } function writeStore(key, value) { localStorage.setItem(key, JSON.stringify(value)); } function setStatus(text, level = '') { const target = $('template-status'); target.textContent = text; target.className = `status ${level}`.trim(); } function addMessage(text, level = '') { const p = document.createElement('p'); p.className = `message ${level}`.trim(); p.textContent = text; $('url-messages').appendChild(p); } function safeHTTPURL(raw) { try { const url = new URL(raw); if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) return null; return url; } catch (_) { return null; } } function parseTemplate(raw) { const value = raw.trim(); if (!value) throw new Error('Bitte zuerst eine Declaration-URL oder einen Query-String einfügen.'); let params; const absolute = safeHTTPURL(value); if (absolute) { params = new URLSearchParams(absolute.search); } else { const query = value.startsWith('?') ? value.slice(1) : value; params = new URLSearchParams(query); } params.delete('subject'); params.delete('link'); if ([...params.keys()].length === 0) throw new Error('Die Vorlage enthält keine Parameter.'); return params; } function activateTemplate(params, label = 'Vorlage geladen') { activeTemplate = new URLSearchParams(params); activeTemplate.delete('subject'); activeTemplate.delete('link'); $('template-input').value = activeTemplate.toString(); renderTemplateParams(); setStatus(`${label}: ${[...activeTemplate.keys()].length} Parameter aktiv.`, 'good'); } function renderTemplateParams() { const list = $('template-params'); list.replaceChildren(); if (!activeTemplate) return; const entries = [...activeTemplate.entries()].sort(([a], [b]) => a.localeCompare(b)); for (const [key, value] of entries) { const row = document.createElement('div'); const dt = document.createElement('dt'); const dd = document.createElement('dd'); dt.textContent = key; dd.textContent = value; row.append(dt, dd); list.appendChild(row); } } function refreshTemplateSelect(selected = '') { const templates = readStore(TEMPLATE_KEY); const select = $('saved-template-select'); select.replaceChildren(new Option('Gespeicherte Vorlage wählen …', '')); Object.keys(templates).sort((a, b) => a.localeCompare(b)).forEach(name => select.add(new Option(name, name))); if (selected && templates[selected]) select.value = selected; } function saveTemplate() { if (!activeTemplate) { setStatus('Bitte zuerst eine gültige Vorlage laden.', 'error'); return; } const name = $('template-name').value.trim(); if (!name) { setStatus('Bitte einen Namen für die Vorlage angeben.', 'error'); $('template-name').focus(); return; } const templates = readStore(TEMPLATE_KEY); templates[name] = {query: activeTemplate.toString(), updatedAt: new Date().toISOString()}; writeStore(TEMPLATE_KEY, templates); refreshTemplateSelect(name); setStatus(`Vorlage „${name}“ wurde nur in diesem Browser gespeichert.`, 'good'); } function loadSavedTemplate(name) { if (!name) return; const item = readStore(TEMPLATE_KEY)[name]; if (!item || typeof item.query !== 'string') return; $('template-name').value = name; activateTemplate(new URLSearchParams(item.query), `Vorlage „${name}“ geladen`); } function deleteTemplate() { const name = $('saved-template-select').value; if (!name) return; const templates = readStore(TEMPLATE_KEY); delete templates[name]; writeStore(TEMPLATE_KEY, templates); refreshTemplateSelect(); if ($('template-name').value === name) $('template-name').value = ''; setStatus(`Vorlage „${name}“ wurde aus diesem Browser gelöscht.`); } function emptyProfileForm() { $('profile-name').value = ''; for (const [id] of profileFields) $(id).value = ''; $('profile-select').value = ''; } function profileFromForm() { const profile = {}; for (const [id, key] of profileFields) { const value = $(id).value.trim(); if (value) profile[key] = value; } return profile; } function validateProfile(profile) { const urlKeys = ['authorUrl', 'responsibleUrl', 'complaintUrl']; for (const key of urlKeys) { if (profile[key] && !safeHTTPURL(profile[key])) return `${key} muss eine absolute http(s)-URL ohne Zugangsdaten sein.`; } if (profile.complaintEmail && !$('profile-complaint-email').checkValidity()) return 'Die E-Mail-Adresse der Rückmeldestelle ist nicht gültig.'; return ''; } function validateMergedParams(params) { if (params.get('authorUrl') && !params.get('author')) return 'Zu einer Autoren-URL muss auch ein Autor bzw. eine Autorin angegeben werden.'; const responsibilityStarted = params.get('responsibleRole') || params.get('responsible') || params.get('responsibleUrl'); if (responsibilityStarted && (!params.get('responsibleRole') || !params.get('responsible'))) return 'Bei redaktioneller Verantwortung sind Rolle und Name/Organisation gemeinsam erforderlich.'; const complaintStarted = params.get('complaintName') || params.get('complaintEmail') || params.get('complaintUrl'); if (complaintStarted && (!params.get('complaintName') || (!params.get('complaintEmail') && !params.get('complaintUrl')))) return 'Die Rückmeldestelle benötigt einen Namen und mindestens E-Mail oder Kontakt-URL.'; return ''; } function refreshProfileSelect(selected = '') { const profiles = readStore(PROFILE_KEY); const select = $('profile-select'); select.replaceChildren(new Option('Website-Profil wählen …', '')); Object.keys(profiles).sort((a, b) => a.localeCompare(b)).forEach(name => select.add(new Option(name, name))); if (selected && profiles[selected]) select.value = selected; } function saveProfile() { const name = $('profile-name').value.trim(); if (!name) { alert('Bitte einen Profilnamen angeben.'); $('profile-name').focus(); return; } const profile = profileFromForm(); const error = validateProfile(profile); if (error) { alert(error); return; } const profiles = readStore(PROFILE_KEY); profiles[name] = {values: profile, updatedAt: new Date().toISOString()}; writeStore(PROFILE_KEY, profiles); refreshProfileSelect(name); } function loadProfile(name) { if (!name) { emptyProfileForm(); return; } const item = readStore(PROFILE_KEY)[name]; if (!item || !item.values) return; $('profile-name').value = name; for (const [id, key] of profileFields) $(id).value = item.values[key] || ''; } function deleteProfile() { const name = $('profile-select').value; if (!name) return; const profiles = readStore(PROFILE_KEY); delete profiles[name]; writeStore(PROFILE_KEY, profiles); refreshProfileSelect(); emptyProfileForm(); } function activeQuery() { if (!activeTemplate) throw new Error('Bitte zuerst eine Kennzeichnungsvorlage laden.'); const params = new URLSearchParams(activeTemplate); params.delete('subject'); params.delete('link'); const profile = profileFromForm(); const error = validateProfile(profile); if (error) throw new Error(error); for (const [key, value] of Object.entries(profile)) params.set(key, value); const mergedError = validateMergedParams(params); if (mergedError) throw new Error(mergedError); return params; } function csvCellLine(line) { let value = line.trim(); if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) { value = value.slice(1, -1).replaceAll('""', '"'); } return value; } function parseSubjects() { $('url-messages').replaceChildren(); let lines = $('url-input').value.split(/\r?\n/).map(csvCellLine).filter(Boolean); if (lines[0]?.toLowerCase() === 'url') lines = lines.slice(1); const baseRaw = $('base-url').value.trim(); let base = null; if (baseRaw) { base = safeHTTPURL(baseRaw); if (!base) addMessage('Die Basis-URL ist ungültig und wird nicht verwendet.', 'error'); } const unique = new Map(); const invalid = []; let duplicates = 0; for (const raw of lines) { let parsed = safeHTTPURL(raw); if (!parsed && base) { try { const candidate = new URL(raw, base); if (['http:', 'https:'].includes(candidate.protocol) && !candidate.username && !candidate.password) parsed = candidate; } catch (_) {} } if (!parsed) { invalid.push(raw); continue; } const normalized = parsed.href; if (unique.has(normalized)) { duplicates++; continue; } unique.set(normalized, normalized); } const subjects = [...unique.values()]; $('url-counter').querySelector('strong').textContent = String(subjects.length); if (duplicates) addMessage(`${duplicates} Duplikat${duplicates === 1 ? '' : 'e'} entfernt.`, 'info'); if (invalid.length) addMessage(`${invalid.length} ungültige Zeile${invalid.length === 1 ? '' : 'n'} ignoriert: ${invalid.slice(0, 3).join(', ')}${invalid.length > 3 ? ' …' : ''}`, 'error'); if (cfg && subjects.length > cfg.maxURLs) addMessage(`Maximal ${cfg.maxURLs} URLs pro Lauf. Bitte die Liste aufteilen.`, 'error'); return subjects; } function csvEscape(value) { const text = String(value ?? ''); return /[",\n\r]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text; } function buildExports(results) { const successful = results.filter(item => item.ok && item.data); const html = successful.map(item => `\n${item.data.html}`).join('\n\n'); const markdown = successful.map(item => `### ${item.subject}\n\n${item.data.markdown}`).join('\n\n'); const jsonArray = successful.map(item => item.data.jsonLd); const json = JSON.stringify(jsonArray, null, 2); const jsonl = successful.map(item => JSON.stringify(item.data.jsonLd)).join('\n'); const csvRows = [['subject', 'declaration', 'manifest', 'badge']]; successful.forEach(item => csvRows.push([item.subject, item.data.declarationUrl, item.data.manifestUrl, item.data.badgeUrl])); const csv = csvRows.map(row => row.map(csvEscape).join(',')).join('\n'); return {html, markdown, json, jsonl, csv}; } function renderResults(results) { currentResults = results; exportsCache = buildExports(results); const successful = results.filter(item => item.ok).length; const failed = results.length - successful; $('results-section').hidden = false; $('result-summary').textContent = `${successful} Kennzeichnung${successful === 1 ? '' : 'en'} erzeugt${failed ? `, ${failed} Fehler` : ''}.`; const tbody = $('result-rows'); tbody.replaceChildren(); results.forEach(item => { const tr = document.createElement('tr'); const subjectCell = document.createElement('td'); const code = document.createElement('code'); code.textContent = item.subject; subjectCell.appendChild(code); const statusCell = document.createElement('td'); statusCell.className = item.ok ? 'ok' : 'failed'; statusCell.textContent = item.ok ? '✓ erzeugt' : `Fehler: ${item.error || 'unbekannt'}`; const linksCell = document.createElement('td'); if (item.ok && item.data) { const links = document.createElement('div'); links.className = 'result-links'; [['Declaration', item.data.declarationUrl], ['JSON-LD', item.data.manifestUrl], ['Badge', item.data.badgeUrl]].forEach(([label, href]) => { const a = document.createElement('a'); a.textContent = label; a.href = href; a.target = '_blank'; a.rel = 'noopener'; links.appendChild(a); }); linksCell.appendChild(links); } tr.append(subjectCell, statusCell, linksCell); tbody.appendChild(tr); }); $('output-html').value = exportsCache.html; $('output-markdown').value = exportsCache.markdown; $('output-json').value = exportsCache.json; $('output-csv').value = exportsCache.csv; $('results-section').scrollIntoView({behavior: 'smooth', block: 'start'}); } async function generate() { let query; try { query = activeQuery(); } catch (error) { setStatus(error.message, 'error'); return; } const subjects = parseSubjects(); if (!subjects.length) { addMessage('Bitte mindestens eine gültige URL einfügen.', 'error'); return; } if (subjects.length > cfg.maxURLs) return; const button = $('generate'); const original = button.textContent; button.disabled = true; button.textContent = `${subjects.length} Kennzeichnungen werden erzeugt …`; try { const response = await fetch('/api/render-batch', { method: 'POST', headers: {'Content-Type': 'application/json', 'Accept': 'application/json'}, body: JSON.stringify({template: query.toString(), subjects}) }); const payload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(payload.detail || `HTTP ${response.status}`); renderResults(payload.results || []); } catch (error) { addMessage(`Bulk-Lauf fehlgeschlagen: ${error.message}`, 'error'); } finally { button.disabled = false; button.textContent = original; } } async function copyExport(kind, button) { const value = exportsCache[kind] || ''; if (!value) return; await navigator.clipboard.writeText(value); const old = button.textContent; button.textContent = 'Kopiert'; setTimeout(() => { button.textContent = old; }, 1300); } function downloadExport(kind) { const spec = { html: ['ai-disclosure-bulk.html', 'text/html;charset=utf-8'], markdown: ['ai-disclosure-bulk.md', 'text/markdown;charset=utf-8'], json: ['ai-disclosure-bulk.json', 'application/ld+json;charset=utf-8'], jsonl: ['ai-disclosure-bulk.jsonl', 'application/x-ndjson;charset=utf-8'], csv: ['ai-disclosure-bulk.csv', 'text/csv;charset=utf-8'] }[kind]; const value = exportsCache[kind] || ''; if (!spec || !value) return; downloadBlob(spec[0], value, spec[1]); } function downloadBlob(filename, content, type) { const blob = new Blob([content], {type}); const href = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = href; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(href), 500); } function exportBrowserData() { const payload = { format: EXPORT_FORMAT, version: 1, exportedAt: new Date().toISOString(), profiles: readStore(PROFILE_KEY), templates: readStore(TEMPLATE_KEY) }; downloadBlob('ai-disclosure-bulk-browser-data.json', JSON.stringify(payload, null, 2), 'application/json;charset=utf-8'); } async function importBrowserData(file) { if (!file) return; try { const payload = JSON.parse(await file.text()); if (payload.format !== EXPORT_FORMAT || payload.version !== 1) throw new Error('Unbekanntes Exportformat.'); if (!payload.profiles || typeof payload.profiles !== 'object' || !payload.templates || typeof payload.templates !== 'object') throw new Error('Export ist unvollständig.'); writeStore(PROFILE_KEY, payload.profiles); writeStore(TEMPLATE_KEY, payload.templates); refreshProfileSelect(); refreshTemplateSelect(); alert('Profile und Vorlagen wurden importiert.'); } catch (error) { alert(`Import fehlgeschlagen: ${error.message}`); } finally { $('import-browser-data').value = ''; } } function importFragmentTemplate() { if (!location.hash.startsWith('#')) return; const hash = new URLSearchParams(location.hash.slice(1)); const raw = hash.get('template'); if (!raw) return; try { const payload = JSON.parse(raw); if (payload.version !== 1 || typeof payload.query !== 'string') throw new Error('Unbekanntes Vorlagenformat.'); const params = new URLSearchParams(payload.query); if (payload.theme) params.set('theme', payload.theme); activateTemplate(params, 'Aus dem Generator übernommen'); $('template-name').value = payload.name || ''; } catch (error) { setStatus(`Vorlage aus dem Generator konnte nicht übernommen werden: ${error.message}`, 'error'); } finally { history.replaceState(null, '', location.pathname + location.search); } } function clearBrowserData() { if (!confirm('Alle lokal gespeicherten Website-Profile und Kennzeichnungsvorlagen in diesem Browser löschen?')) return; localStorage.removeItem(PROFILE_KEY); localStorage.removeItem(TEMPLATE_KEY); refreshProfileSelect(); refreshTemplateSelect(); emptyProfileForm(); setStatus('Lokale Profile und Vorlagen wurden gelöscht.'); } async function init() { try { const response = await fetch('/api/config', {headers: {'Accept': 'application/json'}}); cfg = await response.json(); } catch (_) { cfg = {maxURLs: 500}; } refreshTemplateSelect(); refreshProfileSelect(); importFragmentTemplate(); parseSubjects(); $('load-template').addEventListener('click', () => { try { activateTemplate(parseTemplate($('template-input').value)); } catch (error) { setStatus(error.message, 'error'); } }); $('save-template').addEventListener('click', saveTemplate); $('saved-template-select').addEventListener('change', event => loadSavedTemplate(event.target.value)); $('delete-template').addEventListener('click', deleteTemplate); $('profile-select').addEventListener('change', event => loadProfile(event.target.value)); $('new-profile').addEventListener('click', emptyProfileForm); $('save-profile').addEventListener('click', saveProfile); $('delete-profile').addEventListener('click', deleteProfile); $('export-browser-data').addEventListener('click', exportBrowserData); $('import-browser-data').addEventListener('change', event => importBrowserData(event.target.files?.[0])); $('clear-browser-data').addEventListener('click', clearBrowserData); $('url-input').addEventListener('input', parseSubjects); $('base-url').addEventListener('input', parseSubjects); $('clear-urls').addEventListener('click', () => { $('url-input').value = ''; $('url-messages').replaceChildren(); parseSubjects(); }); $('generate').addEventListener('click', generate); document.querySelectorAll('[data-copy]').forEach(button => button.addEventListener('click', () => copyExport(button.dataset.copy, button))); document.querySelectorAll('[data-download]').forEach(button => button.addEventListener('click', () => downloadExport(button.dataset.download))); } init(); })();