Files
Text-Check-Webapp/website-checker/templates/index.html
s4luorth 26b5465f53 final
2026-02-07 14:03:54 +01:00

587 lines
39 KiB
HTML
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.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LanguageTool Website Checker</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f8f9fa; --surface: #ffffff; --border: #e2e8f0;
--text: #1a202c; --text-secondary: #64748b;
--primary: #4f46e5; --primary-hover: #4338ca;
--spelling: #ef4444; --spelling-bg: #fef2f2;
--grammar: #f59e0b; --grammar-bg: #fffbeb;
--style: #3b82f6; --style-bg: #eff6ff;
--success: #22c55e; --success-bg: #f0fdf4;
--error-bg: #fef2f2;
--radius: 8px;
--shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
}
body { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; min-height: 100vh; }
.container { max-width: 1100px; margin: 0 auto; padding: 0 24px; }
/* Header */
.header { background: var(--surface); border-bottom: 1px solid var(--border); padding: 16px 0; position: sticky; top: 0; z-index: 100; }
.header-inner { display: flex; align-items: center; justify-content: space-between; }
.header h1 { font-size: 18px; font-weight: 700; color: var(--primary); }
.header-back { font-size: 14px; color: var(--primary); cursor: pointer; text-decoration: none; display: none; }
.header-back:hover { text-decoration: underline; }
.legend { display: flex; gap: 16px; font-size: 13px; color: var(--text-secondary); }
.legend-item { display: flex; align-items: center; gap: 4px; }
.legend-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
.legend-dot.spelling { background: var(--spelling); }
.legend-dot.grammar { background: var(--grammar); }
.legend-dot.style { background: var(--style); }
/* Form */
.form-view { padding: 60px 0; }
.form-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 40px; max-width: 560px; margin: 0 auto; box-shadow: var(--shadow); }
.form-card h2 { font-size: 22px; margin-bottom: 4px; }
.form-card .subtitle { color: var(--text-secondary); font-size: 14px; margin-bottom: 28px; }
.form-group { margin-bottom: 18px; }
.form-group label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 5px; }
.form-group input, .form-group select { width: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: 6px; font-size: 14px; color: var(--text); background: var(--bg); }
.form-group input:focus, .form-group select:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79,70,229,0.1); }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.btn-primary { width: 100%; padding: 12px; background: var(--primary); color: white; border: none; border-radius: 6px; font-size: 15px; font-weight: 600; cursor: pointer; margin-top: 8px; }
.btn-primary:hover { background: var(--primary-hover); }
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
/* Progress */
.progress-view { padding: 80px 0; text-align: center; display: none; }
.progress-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 48px 40px; max-width: 560px; margin: 0 auto; box-shadow: var(--shadow); }
.spinner { width: 40px; height: 40px; border: 3px solid var(--border); border-top-color: var(--primary); border-radius: 50%; animation: spin 0.8s linear infinite; margin: 0 auto 20px; }
@keyframes spin { to { transform: rotate(360deg); } }
.progress-text { font-size: 15px; font-weight: 600; margin-bottom: 6px; }
.progress-detail { font-size: 13px; color: var(--text-secondary); word-break: break-all; }
.progress-bar-outer { width: 100%; height: 6px; background: var(--border); border-radius: 3px; margin-top: 20px; overflow: hidden; }
.progress-bar-inner { height: 100%; background: var(--primary); border-radius: 3px; transition: width 0.3s; width: 0%; }
.error-banner { background: var(--error-bg); border: 1px solid var(--spelling); color: var(--spelling); padding: 12px 16px; border-radius: 6px; font-size: 14px; margin-top: 16px; display: none; }
/* Results Overview */
.results-view { padding: 32px 0; display: none; }
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 28px; }
.stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 18px; text-align: center; box-shadow: var(--shadow); }
.stat-number { font-size: 28px; font-weight: 700; line-height: 1.2; }
.stat-number.spelling { color: var(--spelling); }
.stat-number.grammar { color: var(--grammar); }
.stat-number.style { color: var(--style); }
.stat-label { font-size: 12px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; margin-top: 4px; }
.results-info { font-size: 13px; color: var(--text-secondary); margin-bottom: 16px; }
.search-bar { margin-bottom: 16px; }
.search-bar input { width: 100%; padding: 10px 14px; border: 1px solid var(--border); border-radius: 6px; font-size: 14px; background: var(--surface); }
.search-bar input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79,70,229,0.1); }
.search-results { margin-bottom: 24px; }
.search-result-item { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 16px; margin-bottom: 6px; cursor: pointer; }
.search-result-item:hover { border-color: var(--primary); }
.search-result-url { font-size: 11px; color: var(--text-secondary); margin-bottom: 4px; word-break: break-all; }
.search-result-context { font-family: 'Cascadia Code','Fira Code','Consolas',monospace; font-size: 13px; }
.search-highlight { background: #fef08a; padding: 1px 2px; border-radius: 2px; }
.page-list { display: flex; flex-direction: column; gap: 6px; }
.page-item { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 18px; display: flex; align-items: center; justify-content: space-between; cursor: pointer; }
.page-item:hover { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(79,70,229,0.08); }
.page-item.skipped { opacity: 0.5; cursor: default; }
.page-item.dismissed { opacity: 0.45; }
.page-url { font-size: 14px; word-break: break-all; flex: 1; margin-right: 12px; }
.page-badges { display: flex; gap: 6px; flex-shrink: 0; align-items: center; }
.badge { display: inline-flex; align-items: center; padding: 3px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; }
.badge.spelling { background: var(--spelling-bg); color: var(--spelling); }
.badge.grammar { background: var(--grammar-bg); color: var(--grammar); }
.badge.style { background: var(--style-bg); color: var(--style); }
.badge.success { background: var(--success-bg); color: var(--success); }
.badge.error { background: var(--error-bg); color: var(--spelling); }
.page-dismiss-icon { font-size: 16px; font-weight: 700; cursor: pointer; flex-shrink: 0; margin-left: 10px; line-height: 1; }
.page-dismiss-icon.pending { color: var(--spelling); }
.page-dismiss-icon.done { color: var(--success); }
/* Detail View */
.detail-view { padding: 32px 0; display: none; }
.detail-url { font-size: 14px; color: var(--primary); text-decoration: none; word-break: break-all; }
.detail-url:hover { text-decoration: underline; }
.detail-stats { display: flex; gap: 12px; margin: 16px 0 12px; }
.detail-toolbar { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; }
.filter-btn { padding: 6px 14px; border: 2px solid var(--border); border-radius: 20px; background: var(--surface); font-size: 13px; font-weight: 600; cursor: pointer; color: var(--text-secondary); }
.filter-btn:hover { border-color: var(--text-secondary); }
.filter-btn.active { color: white; }
.filter-btn.active[data-filter="all"] { background: var(--primary); border-color: var(--primary); }
.filter-btn.active[data-filter="spelling"] { background: var(--spelling); border-color: var(--spelling); }
.filter-btn.active[data-filter="grammar"] { background: var(--grammar); border-color: var(--grammar); }
.filter-btn.active[data-filter="style"] { background: var(--style); border-color: var(--style); }
.toolbar-spacer { flex: 1; }
.dismiss-all-btn { padding: 6px 14px; border: 1px solid var(--success); border-radius: 6px; background: var(--success-bg); color: var(--success); font-size: 13px; font-weight: 600; cursor: pointer; }
.dismiss-all-btn:hover { background: var(--success); color: white; }
.section-block { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 12px; overflow: hidden; }
.section-header { display: flex; align-items: flex-start; justify-content: space-between; padding: 10px 16px; background: var(--bg); border-bottom: 1px solid var(--border); font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); gap: 12px; }
.section-header-left { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; }
.section-type-label { white-space: nowrap; }
.section-corrections { display: flex; flex-direction: column; gap: 2px; text-transform: none; letter-spacing: normal; font-weight: 400; }
.section-correction-item { display: flex; align-items: center; gap: 6px; font-size: 12px; line-height: 1.4; padding: 3px 6px; border-radius: 4px; cursor: pointer; user-select: none; }
.section-correction-item:hover { background: rgba(0,0,0,0.04); }
.section-correction-item.dismissed-item { opacity: 0.35; }
.section-correction-item.dismissed-item .section-corr-original,
.section-correction-item.dismissed-item .section-corr-arrow,
.section-correction-item.dismissed-item .section-corr-suggestion,
.section-correction-item.dismissed-item .section-corr-msg { text-decoration: line-through; }
.section-corr-marker { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.section-corr-marker.spelling { background: var(--spelling); }
.section-corr-marker.grammar { background: var(--grammar); }
.section-corr-marker.style { background: var(--style); }
.section-corr-original { font-weight: 600; color: var(--text); }
.section-corr-arrow { color: var(--success); }
.section-corr-suggestion { color: var(--success); font-weight: 600; cursor: pointer; }
.section-corr-suggestion:hover { text-decoration: underline; }
.section-corr-msg { color: var(--text-secondary); margin-left: 4px; }
.section-corr-extra { color: var(--text-secondary); margin-left: 2px; }
.section-corr-status { flex-shrink: 0; margin-left: auto; font-size: 14px; font-weight: 700; line-height: 1; }
.section-corr-status.pending { color: var(--spelling); }
.section-corr-status.done { color: var(--success); }
.section-check { color: var(--success); font-size: 14px; flex-shrink: 0; }
.section-body { padding: 16px; font-family: 'Cascadia Code','Fira Code','Consolas',monospace; font-size: 14px; line-height: 1.8; white-space: pre-wrap; word-wrap: break-word; }
/* Error marks — clickable, copies first suggestion */
.error-mark { position: relative; cursor: pointer; padding: 1px 2px; border-radius: 2px; }
.error-mark.dismissed { opacity: 0.25; text-decoration: line-through; cursor: default; pointer-events: none; }
.error-mark.spelling { background: var(--spelling-bg); border-bottom: 2px solid var(--spelling); }
.error-mark.grammar { background: var(--grammar-bg); border-bottom: 2px solid var(--grammar); }
.error-mark.style { background: var(--style-bg); border-bottom: 2px solid var(--style); }
/* (inline checkbox removed — now at end of correction row in header) */
/* (error-list removed — corrections now shown in section header) */
/* Copied toast */
.copied-toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); background: var(--text); color: white; padding: 8px 20px; border-radius: 20px; font-size: 13px; font-weight: 600; z-index: 9999; opacity: 0; transition: opacity 0.2s; pointer-events: none; }
.copied-toast.show { opacity: 1; }
/* Preview */
.preview-view { padding: 32px 0; display: none; }
.preview-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 28px; max-width: 700px; margin: 0 auto; box-shadow: var(--shadow); }
.preview-card h2 { font-size: 18px; margin-bottom: 4px; }
.preview-info { font-size: 13px; color: var(--text-secondary); margin-bottom: 16px; }
.preview-actions { display: flex; gap: 8px; margin-bottom: 16px; align-items: center; }
.preview-actions button { padding: 5px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); font-size: 12px; cursor: pointer; color: var(--text-secondary); }
.preview-actions button:hover { border-color: var(--primary); color: var(--primary); }
.preview-count { font-size: 13px; color: var(--text-secondary); margin-left: auto; }
.preview-list { max-height: 400px; overflow-y: auto; border: 1px solid var(--border); border-radius: 6px; margin-bottom: 16px; }
.preview-item { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-bottom: 1px solid var(--border); font-size: 13px; }
.preview-item:last-child { border-bottom: none; }
.preview-item input[type="checkbox"] { width: 16px; height: 16px; flex-shrink: 0; accent-color: var(--primary); }
.preview-item label { word-break: break-all; cursor: pointer; flex: 1; }
.preview-buttons { display: flex; gap: 10px; }
.btn-secondary { padding: 10px 20px; background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; }
.btn-secondary:hover { border-color: var(--text-secondary); }
@media (max-width: 768px) { .stats-grid { grid-template-columns: repeat(2, 1fr); } .form-row { grid-template-columns: 1fr; } .legend { flex-wrap: wrap; gap: 8px; } }
</style>
</head>
<body>
<div class="header">
<div class="container header-inner">
<h1>LanguageTool Website Checker</h1>
<a class="header-back" id="backBtn" onclick="goBack()">&#8592; Zurück zur Übersicht</a>
<div class="legend">
<div class="legend-item"><span class="legend-dot spelling"></span> Rechtschreibung</div>
<div class="legend-item"><span class="legend-dot grammar"></span> Grammatik</div>
<div class="legend-item"><span class="legend-dot style"></span> Stil</div>
</div>
</div>
</div>
<div class="copied-toast" id="copiedToast">In Zwischenablage kopiert</div>
<!-- Form -->
<div class="form-view" id="formView">
<div class="container"><div class="form-card">
<h2>Website prüfen</h2>
<p class="subtitle">Crawlt alle Seiten einer Website und prüft Texte mit LanguageTool.</p>
<div class="form-group"><label>Domain</label><input type="text" id="domain" placeholder="beispiel.de" autocomplete="off"></div>
<div class="form-group"><label>LanguageTool E-Mail / Username</label><input type="text" id="username" placeholder="mail@beispiel.de" autocomplete="off"></div>
<div class="form-group"><label>LanguageTool API Key</label><input type="password" id="apiKey" placeholder="API Key eingeben" autocomplete="off"></div>
<div class="form-row">
<div class="form-group"><label>Sprache</label><select id="language"><option value="de-DE">Deutsch (DE)</option><option value="de-AT">Deutsch (AT)</option><option value="de-CH">Deutsch (CH)</option><option value="en-US">English (US)</option><option value="en-GB">English (GB)</option></select></div>
<div class="form-group"><label>Max. Seiten</label><input type="number" id="maxPages" value="50" min="1" max="500"></div>
</div>
<button class="btn-primary" id="startBtn" onclick="startCheck()">Website prüfen</button>
<div class="error-banner" id="formError"></div>
</div></div>
</div>
<!-- Progress -->
<div class="progress-view" id="progressView">
<div class="container"><div class="progress-card">
<div class="spinner"></div>
<div class="progress-text" id="progressText">Starte...</div>
<div class="progress-detail" id="progressDetail"></div>
<div class="progress-bar-outer"><div class="progress-bar-inner" id="progressBar"></div></div>
</div></div>
</div>
<!-- Preview -->
<div class="preview-view" id="previewView">
<div class="container"><div class="preview-card">
<h2>Gefundene Seiten</h2>
<div class="preview-info" id="previewInfo"></div>
<div class="preview-actions">
<button onclick="toggleAllUrls(true)">Alle auswählen</button>
<button onclick="toggleAllUrls(false)">Keine auswählen</button>
<span class="preview-count" id="previewCount"></span>
</div>
<div class="preview-list" id="previewList"></div>
<div class="error-banner" id="previewError"></div>
<div class="preview-buttons">
<button class="btn-secondary" onclick="showView('form')">Zurück</button>
<button class="btn-primary" style="flex:1" id="checkSelectedBtn" onclick="startCheckSelected()">Ausgewählte prüfen</button>
</div>
</div></div>
</div>
<!-- Results Overview -->
<div class="results-view" id="resultsView">
<div class="container">
<div class="stats-grid" id="statsGrid"></div>
<div class="results-info" id="resultsInfo"></div>
<div class="search-bar"><input type="text" id="globalSearch" placeholder="Fehler durchsuchen... (z.B. &quot;dass&quot; oder &quot;Komma&quot;)" oninput="onGlobalSearch()"></div>
<div class="search-results" id="searchResults"></div>
<div class="page-list" id="pageList"></div>
</div>
</div>
<!-- Detail View -->
<div class="detail-view" id="detailView">
<div class="container">
<a class="detail-url" id="detailUrl" target="_blank" rel="noopener"></a>
<div class="detail-stats" id="detailStats"></div>
<div class="detail-toolbar" id="detailToolbar"></div>
<div id="detailSections"></div>
</div>
</div>
<script>
// ==================== STATE ====================
let currentSessionId = null, currentResults = null, currentView = 'form';
let currentDetailIdx = null, currentFilter = 'all', crawledUrls = [];
let dismissedErrors = new Set(), dismissedPages = new Set();
const formView = document.getElementById('formView');
const previewView = document.getElementById('previewView');
const progressView = document.getElementById('progressView');
const resultsView = document.getElementById('resultsView');
const detailView = document.getElementById('detailView');
const backBtn = document.getElementById('backBtn');
function showView(name) {
currentView = name;
formView.style.display = name === 'form' ? 'block' : 'none';
previewView.style.display = name === 'preview' ? 'block' : 'none';
progressView.style.display = name === 'progress' ? 'block' : 'none';
resultsView.style.display = name === 'results' ? 'block' : 'none';
detailView.style.display = name === 'detail' ? 'block' : 'none';
backBtn.style.display = (name === 'detail' || name === 'results') ? 'inline' : 'none';
}
function goBack() {
if (currentView === 'detail') { renderOverview(); showView('results'); }
else if (currentView === 'results') showView('form');
}
// ==================== LOCALSTORAGE ====================
function saveFormCache() {
localStorage.setItem('wc_form', JSON.stringify({
domain: document.getElementById('domain').value,
username: document.getElementById('username').value,
language: document.getElementById('language').value,
maxPages: document.getElementById('maxPages').value,
}));
}
function loadFormCache() {
try {
const c = JSON.parse(localStorage.getItem('wc_form')); if (!c) return;
if (c.domain) document.getElementById('domain').value = c.domain;
if (c.username) document.getElementById('username').value = c.username;
if (c.language) document.getElementById('language').value = c.language;
if (c.maxPages) document.getElementById('maxPages').value = c.maxPages;
} catch (e) {}
}
loadFormCache();
// ==================== CRAWL ====================
async function startCheck() {
const domain = document.getElementById('domain').value.trim();
const username = document.getElementById('username').value.trim();
const apiKey = document.getElementById('apiKey').value.trim();
const maxPages = parseInt(document.getElementById('maxPages').value) || 50;
document.getElementById('formError').style.display = 'none';
if (!domain) { showError('Bitte eine Domain eingeben.'); return; }
if (!username || !apiKey) { showError('Bitte LanguageTool Credentials eingeben.'); return; }
saveFormCache();
const btn = document.getElementById('startBtn');
btn.disabled = true; btn.textContent = 'Crawle Seiten...';
try {
const resp = await fetch('/api/crawl', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ domain, maxPages }) });
const data = await resp.json();
if (data.error) { showError(data.error); btn.disabled = false; btn.textContent = 'Website prüfen'; return; }
crawledUrls = data.urls; renderPreview(data); showView('preview');
} catch (e) { showError('Netzwerkfehler: ' + e.message); }
btn.disabled = false; btn.textContent = 'Website prüfen';
}
function showError(msg) { const el = document.getElementById('formError'); el.textContent = msg; el.style.display = 'block'; }
// ==================== PREVIEW ====================
function renderPreview(data) {
document.getElementById('previewInfo').textContent = `${data.urls.length} Seiten gefunden via ${data.sitemap_used ? 'Sitemap' : 'Link-Crawling'} auf ${data.domain}`;
document.getElementById('previewError').style.display = 'none';
document.getElementById('previewList').innerHTML = data.urls.map((url, i) =>
`<div class="preview-item"><input type="checkbox" id="url_${i}" checked data-url="${esc(url)}"><label for="url_${i}">${esc(url)}</label></div>`
).join('');
updatePreviewCount();
document.querySelectorAll('#previewList input[type="checkbox"]').forEach(cb => cb.addEventListener('change', updatePreviewCount));
}
function updatePreviewCount() {
const all = document.querySelectorAll('#previewList input[type="checkbox"]');
const checked = document.querySelectorAll('#previewList input[type="checkbox"]:checked');
document.getElementById('previewCount').textContent = `${checked.length} / ${all.length} ausgewählt`;
document.getElementById('checkSelectedBtn').disabled = checked.length === 0;
}
function toggleAllUrls(state) { document.querySelectorAll('#previewList input[type="checkbox"]').forEach(cb => cb.checked = state); updatePreviewCount(); }
// ==================== CHECK ====================
async function startCheckSelected() {
const urls = []; document.querySelectorAll('#previewList input[type="checkbox"]:checked').forEach(cb => urls.push(cb.getAttribute('data-url')));
if (!urls.length) { document.getElementById('previewError').textContent = 'Bitte mindestens eine Seite auswählen.'; document.getElementById('previewError').style.display = 'block'; return; }
const domain = document.getElementById('domain').value.trim(), username = document.getElementById('username').value.trim();
const apiKey = document.getElementById('apiKey').value.trim(), language = document.getElementById('language').value;
const btn = document.getElementById('checkSelectedBtn'); btn.disabled = true; btn.textContent = 'Starte Prüfung...';
try {
const resp = await fetch('/api/check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ domain, username, apiKey, language, urls }) });
const data = await resp.json();
if (data.error) { document.getElementById('previewError').textContent = data.error; document.getElementById('previewError').style.display = 'block'; btn.disabled = false; btn.textContent = 'Ausgewählte prüfen'; return; }
currentSessionId = data.sessionId; showView('progress'); pollStatus();
} catch (e) { document.getElementById('previewError').textContent = 'Netzwerkfehler: ' + e.message; document.getElementById('previewError').style.display = 'block'; }
btn.disabled = false; btn.textContent = 'Ausgewählte prüfen';
}
// ==================== POLLING ====================
function pollStatus() {
const evtSource = new EventSource('/api/stream/' + currentSessionId);
evtSource.onmessage = function(event) {
const d = JSON.parse(event.data);
document.getElementById('progressText').textContent = d.message;
if (d.progress && d.progress.total > 0) {
document.getElementById('progressBar').style.width = Math.round((d.progress.current / d.progress.total) * 100) + '%';
document.getElementById('progressDetail').textContent = `Seite ${d.progress.current} von ${d.progress.total}`;
}
if (d.status === 'done') { evtSource.close(); loadResults(); }
else if (d.status === 'error') { evtSource.close(); showView('form'); showError(d.message); document.getElementById('startBtn').disabled = false; document.getElementById('startBtn').textContent = 'Website prüfen'; }
};
evtSource.onerror = function() { evtSource.close(); setTimeout(pollViaRest, 1000); };
}
async function pollViaRest() {
try {
const resp = await fetch('/api/status/' + currentSessionId); const d = await resp.json();
document.getElementById('progressText').textContent = d.message;
if (d.progress && d.progress.total > 0) { document.getElementById('progressBar').style.width = Math.round((d.progress.current / d.progress.total) * 100) + '%'; document.getElementById('progressDetail').textContent = `Seite ${d.progress.current} von ${d.progress.total}`; }
if (d.status === 'done') loadResults(); else if (d.status === 'error') { showView('form'); showError(d.message); } else setTimeout(pollViaRest, 1000);
} catch (e) { setTimeout(pollViaRest, 2000); }
}
// ==================== RESULTS ====================
async function loadResults() {
try {
const resp = await fetch('/api/results/' + currentSessionId); currentResults = await resp.json();
if (currentResults.error) { showView('form'); showError(currentResults.error); return; }
dismissedErrors = new Set(); dismissedPages = new Set();
renderOverview(); showView('results');
} catch (e) { showView('form'); showError('Fehler beim Laden der Ergebnisse.'); }
document.getElementById('startBtn').disabled = false; document.getElementById('startBtn').textContent = 'Website prüfen';
}
function renderOverview() {
const r = currentResults;
const total = r.total_errors.spelling + r.total_errors.grammar + r.total_errors.style;
document.getElementById('statsGrid').innerHTML = `
<div class="stat-card"><div class="stat-number">${r.pages_checked}</div><div class="stat-label">Seiten</div></div>
<div class="stat-card"><div class="stat-number spelling">${r.total_errors.spelling}</div><div class="stat-label">Rechtschreibung</div></div>
<div class="stat-card"><div class="stat-number grammar">${r.total_errors.grammar}</div><div class="stat-label">Grammatik</div></div>
<div class="stat-card"><div class="stat-number style">${r.total_errors.style}</div><div class="stat-label">Stil</div></div>`;
document.getElementById('resultsInfo').textContent = `${r.domain}${r.pages_checked} Seiten geprüft, ${total} Fehler gesamt` + (r.pages_skipped > 0 ? `, ${r.pages_skipped} übersprungen` : '');
document.getElementById('globalSearch').value = ''; document.getElementById('searchResults').innerHTML = '';
document.getElementById('pageList').innerHTML = r.pages.map((page, idx) => {
if (page.skipped) return `<div class="page-item skipped"><span class="page-url">${esc(page.url)}</span><span class="badge error">${esc(page.error_message || 'Übersprungen')}</span></div>`;
const dm = dismissedPages.has(idx);
const b = [];
if (page.error_count.spelling > 0) b.push(`<span class="badge spelling">${page.error_count.spelling}</span>`);
if (page.error_count.grammar > 0) b.push(`<span class="badge grammar">${page.error_count.grammar}</span>`);
if (page.error_count.style > 0) b.push(`<span class="badge style">${page.error_count.style}</span>`);
if (page.total_errors === 0) b.push(`<span class="badge success">Fehlerfrei</span>`);
if (dm) b.push(`<span class="badge success">Erledigt</span>`);
const pageIcon = dm
? '<span class="page-dismiss-icon done" title="Erledigt klick zum Enthaken" onclick="event.stopPropagation();togglePageDismiss('+idx+')">&#10003;</span>'
: '<span class="page-dismiss-icon pending" title="Ausstehend klick zum Abhaken" onclick="event.stopPropagation();togglePageDismiss('+idx+')">&#10005;</span>';
return `<div class="page-item ${dm?'dismissed':''}" onclick="showDetail(${idx})"><span class="page-url">${esc(page.url)}</span><div class="page-badges">${b.join('')}${pageIcon}</div></div>`;
}).join('');
}
// ==================== GLOBAL SEARCH ====================
function onGlobalSearch() {
const q = document.getElementById('globalSearch').value.trim().toLowerCase(), c = document.getElementById('searchResults');
if (!q || q.length < 2 || !currentResults) { c.innerHTML = ''; return; }
const res = [];
for (let pi = 0; pi < currentResults.pages.length && res.length < 50; pi++) {
const page = currentResults.pages[pi]; if (page.skipped) continue;
for (let si = 0; si < page.sections.length && res.length < 50; si++) {
const sec = page.sections[si]; if (!sec.matches) continue;
for (let mi = 0; mi < sec.matches.length && res.length < 50; mi++) {
const m = sec.matches[mi]; if (dismissedErrors.has(`${pi}:${si}:${mi}`)) continue;
const et = sec.text.substring(m.offset, m.offset + m.length), msg = m.message || '';
const ctx = sec.text.substring(Math.max(0, m.offset - 20), m.offset + m.length + 20);
if (et.toLowerCase().includes(q) || msg.toLowerCase().includes(q) || (m.replacements && m.replacements.some(r => r.toLowerCase().includes(q))))
res.push({ pi, et, ctx, msg, url: page.url });
}
}
}
if (!res.length) { c.innerHTML = '<div style="font-size:13px;color:var(--text-secondary);padding:8px 0;">Keine Treffer.</div>'; return; }
c.innerHTML = res.map(r => {
const hl = r.ctx.replace(new RegExp(`(${escRx(r.et)})`, 'gi'), '<span class="search-highlight">$1</span>');
return `<div class="search-result-item" onclick="showDetail(${r.pi})"><div class="search-result-url">${esc(r.url)}</div><div class="search-result-context">${hl}</div><div style="font-size:11px;color:var(--text-secondary);margin-top:4px">${esc(r.msg)}</div></div>`;
}).join('');
}
// ==================== PAGE DISMISS ====================
function togglePageDismiss(idx) {
const p = currentResults.pages[idx];
if (dismissedPages.has(idx)) {
dismissedPages.delete(idx);
if (p) p.sections.forEach((s,si) => (s.matches||[]).forEach((m,mi) => dismissedErrors.delete(`${idx}:${si}:${mi}`)));
} else {
dismissedPages.add(idx);
if (p) p.sections.forEach((s,si) => (s.matches||[]).forEach((m,mi) => dismissedErrors.add(`${idx}:${si}:${mi}`)));
}
renderOverview();
}
// ==================== DETAIL VIEW ====================
function showDetail(idx) {
const page = currentResults.pages[idx]; if (!page || page.skipped) return;
currentDetailIdx = idx; currentFilter = 'all';
document.getElementById('detailUrl').href = page.url; document.getElementById('detailUrl').textContent = page.url;
const s = [];
if (page.error_count.spelling > 0) s.push(`<span class="badge spelling">${page.error_count.spelling} Rechtschreibung</span>`);
if (page.error_count.grammar > 0) s.push(`<span class="badge grammar">${page.error_count.grammar} Grammatik</span>`);
if (page.error_count.style > 0) s.push(`<span class="badge style">${page.error_count.style} Stil</span>`);
if (page.total_errors === 0) s.push(`<span class="badge success">Fehlerfrei</span>`);
document.getElementById('detailStats').innerHTML = s.join('');
renderToolbar(page); renderSections(page, 'all'); showView('detail'); window.scrollTo(0, 0);
}
function renderToolbar(page) {
const bar = document.getElementById('detailToolbar'), c = page.error_count;
if (page.total_errors === 0) { bar.innerHTML = ''; return; }
let h = `<button class="filter-btn active" data-filter="all" onclick="setFilter('all')">Alle</button>`;
if (c.spelling > 0) h += `<button class="filter-btn" data-filter="spelling" onclick="setFilter('spelling')">Rechtschreibung (${c.spelling})</button>`;
if (c.grammar > 0) h += `<button class="filter-btn" data-filter="grammar" onclick="setFilter('grammar')">Grammatik (${c.grammar})</button>`;
if (c.style > 0) h += `<button class="filter-btn" data-filter="style" onclick="setFilter('style')">Stil (${c.style})</button>`;
h += `<span class="toolbar-spacer"></span><button class="dismiss-all-btn" onclick="dismissAllOnPage(${currentDetailIdx})">Alle abhaken</button>`;
bar.innerHTML = h;
}
function setFilter(f) {
currentFilter = f;
document.querySelectorAll('.filter-btn').forEach(b => b.classList.toggle('active', b.getAttribute('data-filter') === f));
renderSections(currentResults.pages[currentDetailIdx], f);
}
function dismissAllOnPage(idx) {
const p = currentResults.pages[idx];
p.sections.forEach((s, si) => (s.matches || []).forEach((m, mi) => dismissedErrors.add(`${idx}:${si}:${mi}`)));
dismissedPages.add(idx); renderSections(p, currentFilter); renderToolbar(p);
}
function toggleError(pi, si, mi) {
const key = `${pi}:${si}:${mi}`;
if (dismissedErrors.has(key)) {
dismissedErrors.delete(key);
dismissedPages.delete(pi); // un-dismiss page if any error unchecked
} else {
dismissedErrors.add(key);
// Check if all errors on this page are now dismissed
const page = currentResults.pages[pi]; let allDone = true;
page.sections.forEach((s, si2) => (s.matches || []).forEach((m, mi2) => { if (!dismissedErrors.has(`${pi}:${si2}:${mi2}`)) allDone = false; }));
if (allDone) dismissedPages.add(pi);
}
renderSections(currentResults.pages[pi], currentFilter);
}
// ==================== RENDER SECTIONS ====================
function renderSections(page, filter) {
const pi = currentDetailIdx, html = [];
for (let si = 0; si < page.sections.length; si++) {
const sec = page.sections[si], allM = sec.matches || [];
const fm = filter === 'all' ? allM : allM.filter(m => m.category === filter);
if (filter !== 'all' && fm.length === 0) continue;
const mws = fm.map(m => { const ri = allM.indexOf(m); return { ...m, dis: dismissedErrors.has(`${pi}:${si}:${ri}`), ri }; });
const active = mws.filter(m => !m.dis).length;
const allDone = mws.length > 0 && active === 0;
const check = (!mws.length || allDone) ? '<span class="section-check">&#10003;</span>' : '';
// Build highlighted text with inline checkboxes
const textHtml = mws.length > 0 ? renderText(sec.text, mws, pi, si) : esc(sec.text);
// Build corrections directly in the section header
let correctionsHtml = '';
if (mws.length > 0) {
correctionsHtml = '<div class="section-corrections">' + mws.map(m => {
const orig = sec.text.substring(m.offset, m.offset + m.length);
const sugg = m.replacements && m.replacements.length > 0 ? m.replacements[0] : '';
const dimClass = m.dis ? ' dismissed-item' : '';
const statusIcon = m.dis
? '<span class="section-corr-status done">&#10003;</span>'
: '<span class="section-corr-status pending">&#10005;</span>';
const copyClick = sugg ? `event.stopPropagation();copyText('${esc(sugg.replace(/'/g, "\\'"))}')` : '';
return `<div class="section-correction-item${dimClass}" onclick="toggleError(${pi},${si},${m.ri})"><span class="section-corr-marker ${m.category}"></span><span class="section-corr-original">${esc(orig)}</span>${sugg ? `<span class="section-corr-arrow">&rarr;</span><span class="section-corr-suggestion" onclick="${copyClick}" title="Klick kopiert">${esc(sugg)}</span>` : ''}${m.replacements && m.replacements.length > 1 ? `<span class="section-corr-extra">(+${m.replacements.length - 1})</span>` : ''}<span class="section-corr-msg">${esc(m.message)}</span>${statusIcon}</div>`;
}).join('') + '</div>';
}
html.push(`<div class="section-block">
<div class="section-header"><div class="section-header-left"><span class="section-type-label">${esc(sec.type)}</span>${correctionsHtml}</div>${check}</div>
<div class="section-body">${textHtml}</div>
</div>`);
}
document.getElementById('detailSections').innerHTML = html.join('');
}
function renderText(text, matches, pi, si) {
const sorted = [...matches].sort((a, b) => a.offset - b.offset);
let r = '', last = 0;
for (const m of sorted) {
const s = m.offset, e = s + m.length;
if (s < last) continue;
r += esc(text.slice(last, s));
const word = text.slice(s, e);
const sugg = m.replacements && m.replacements.length > 0 ? m.replacements[0] : '';
const dc = m.dis ? ' dismissed' : '';
const clickCopy = sugg && !m.dis ? `onclick="copyText('${esc(sugg.replace(/'/g, "\\'"))}')"` : '';
const title = sugg && !m.dis ? `title="Klick kopiert: ${esc(sugg)}"` : '';
r += `<span class="error-mark ${m.category}${dc}" ${clickCopy} ${title}>${esc(word)}</span>`;
last = e;
}
r += esc(text.slice(last));
return r;
}
// ==================== CLIPBOARD ====================
function copyText(text) {
navigator.clipboard.writeText(text).then(() => {
const t = document.getElementById('copiedToast');
t.classList.add('show'); setTimeout(() => t.classList.remove('show'), 1500);
});
}
// ==================== UTILS ====================
function esc(s) { return s ? s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#039;') : ''; }
function escRx(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
document.querySelectorAll('#formView input').forEach(el => el.addEventListener('keydown', e => { if (e.key === 'Enter') startCheck(); }));
</script>
</body>
</html>