Files
ai-disclosure-standard/bulkweb/static/bulk.js
T
groot ec58fc30f8
release-tag / release-image (push) Successful in 1m40s
1.8.2 - Anpassungen am Bulk-System
2026-07-23 08:49:28 +02:00

553 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
'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 => `<!-- ${item.subject} -->\n${item.data.embedHtml || 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 buildDisclosurePreview(data) {
const wrap = document.createElement('p');
wrap.className = 'bulk-disclosure-preview';
const link = document.createElement('a');
link.className = 'bulk-disclosure-preview__link';
link.href = data.declarationUrl;
link.target = '_blank';
link.rel = 'noopener';
const badge = document.createElement('img');
badge.className = 'bulk-disclosure-preview__badge';
badge.src = data.badgeUrl;
badge.alt = '';
badge.setAttribute('aria-hidden', 'true');
const text = document.createElement('span');
text.className = 'bulk-disclosure-preview__text';
text.textContent = data.accessibleText || 'KI-Nutzung Details zur Kennzeichnung';
link.append(badge, text);
wrap.appendChild(link);
return wrap;
}
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 previewCell = document.createElement('td');
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) {
previewCell.appendChild(buildDisclosurePreview(item.data));
const links = document.createElement('div');
links.className = 'result-links';
[['Declaration', item.data.declarationUrl], ['JSON-LD', item.data.manifestUrl], ['SVG (nur Grafik)', 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, previewCell, 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();
})();