Files
foodlinkk-command-center/cockpit/templates/browser.html
T
Aissa 5d60d33db1 Platform bundle: marketing publish, IT ops, packaging, agents mesh.
Volledige Foodlinkk Command Center uitbreiding met social automatisering,
reclamefolder filters, Proxmox monitoring en documentatie.
2026-06-09 00:41:27 +00:00

562 lines
25 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.
{% extends "base.html" %}
{% block extra_head %}
<style>.word-chip-cloud{display:flex;flex-wrap:wrap;gap:.35rem;overflow-y:auto;padding:.4rem;background:rgba(0,0,0,.2);border-radius:8px}.word-chip{font-size:.72rem;padding:.15rem .5rem;border-radius:999px;background:rgba(232,168,56,.15);color:#f3e8d0;border:1px solid rgba(232,168,56,.3)}.word-chip.low{opacity:.7;font-size:.65rem}</style>
<style>
.browser-tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.browser-tab {
padding: 0.5rem 1rem; border-radius: 8px; cursor: pointer; font-size: 0.9rem;
background: rgba(148,163,184,.08); border: 1px solid rgba(148,163,184,.15); color: var(--text-secondary);
}
.browser-tab.active { background: rgba(212,175,55,.15); border-color: var(--accent-gold); color: var(--accent-gold); }
.browser-layout { display: grid; grid-template-columns: 1fr 280px; gap: 1rem; }
@media (max-width: 1100px) { .browser-layout { grid-template-columns: 1fr; } }
.browser-live-row {
display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem;
}
@media (max-width: 900px) { .browser-live-row { grid-template-columns: 1fr; } }
.browser-live-panel { min-height: 380px; }
.browser-live-frame {
width: 100%; min-height: 420px; background: #0a0f1a; border-radius: 10px;
border: 1px solid rgba(148,163,184,.15); object-fit: contain;
}
.browser-novnc { width: 100%; height: 360px; border: 0; border-radius: 10px; background: #000; }
.browser-preview-text {
max-height: 220px; overflow: auto; font-size: 0.82rem; color: var(--text-muted);
white-space: pre-wrap; background: rgba(0,0,0,.25); padding: .75rem; border-radius: 8px;
}
.browser-links { max-height: 160px; overflow: auto; font-size: 0.78rem; }
.browser-status { font-size: 0.8rem; color: var(--accent-cyan); margin-top: 0.35rem; }
.novnc-hint { font-size: 0.78rem; color: var(--text-muted); margin-top: 0.5rem; }
.preset-grid { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.5rem 0; }
.preset-chip { font-size: 0.78rem; padding: 0.25rem 0.55rem; border-radius: 999px; cursor: pointer; border: 1px solid rgba(148,163,184,.2); background: rgba(0,0,0,.2); }
.preset-chip:hover { border-color: var(--accent-gold); color: var(--accent-gold); }
.job-bar {
display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem;
padding: 0.6rem 0.85rem; margin-bottom: 1rem; border-radius: 10px;
background: rgba(0,0,0,.25); border: 1px solid rgba(148,163,184,.12);
}
.job-bar.active { border-color: rgba(232,168,56,.35); }
</style>
{% endblock %}
{% block content %}
<div x-data="browserPage()" x-init="init()">
<header class="page-header">
<h1>Browser &amp; Monitor</h1>
<p class="subtitle page-tip">Voer een URL in — Playwright + live VNC desktop openen dezelfde pagina. Gebruik <strong>Volledige extractie</strong> voor gewicht, prijs en alle zichtbare data.</p>
</header>
<div class="job-bar" :class="{ active: jobRunning }">
<strong style="font-size:0.85rem">Achtergrondtaak</strong>
<span class="browser-status" style="margin:0" x-text="jobLabel || 'Geen actieve taak — je kunt vrij navigeren'"></span>
<button type="button" class="btn btn-sm btn-reject" x-show="jobRunning" @click="stopJob()">⏹ Stop</button>
<label style="font-size:0.78rem;color:var(--text-muted);margin-left:auto">
<input type="checkbox" x-model="vncSync" @change="startPoll()" /> Screenshot = VNC scherm
</label>
<label style="font-size:0.78rem;color:var(--text-muted)">
<input type="checkbox" x-model="livePoll" @change="startPoll()" /> Live poll
</label>
</div>
<section class="panel">
<h2>Snelle sites</h2>
<p class="page-tip">Klik om URL te laden. TikTok/Instagram vereisen soms inloggen via VNC.</p>
<div class="preset-grid">
<span class="preset-chip" @click="loadPreset('https://www.bidfood.nl/webshop/assortiment/mekkafood/_/N-1z10pje/')">Bidfood Mekkafood</span>
<span class="preset-chip" @click="loadPreset('https://www.sligro.nl/')">Sligro</span>
<span class="preset-chip" @click="loadPreset('https://www.tiktok.com/')">TikTok</span>
<span class="preset-chip" @click="loadPreset('https://www.instagram.com/')">Instagram</span>
<span class="preset-chip" @click="loadPreset('https://www.cheemafood.nl/')">Cheema</span>
</div>
<div class="form-grid" style="display:flex;flex-wrap:wrap;gap:0.5rem;margin-top:0.5rem">
<input class="form-input" style="flex:2;min-width:200px" placeholder="Nieuwe site URL" x-model="newSiteUrl" />
<input class="form-input" style="flex:1;min-width:120px" placeholder="Naam" x-model="newSiteName" />
<button type="button" class="btn btn-secondary" @click="addSiteQuick()">+ Toevoegen aan monitor</button>
</div>
</section>
<div class="browser-tabs">
<button type="button" class="browser-tab" :class="{ active: tab === 'research' }" @click="setTab('research')">Research</button>
<button type="button" class="browser-tab" :class="{ active: tab === 'monitor' }" @click="setTab('monitor')">Site Monitor <span class="muted">({{ stats.active_sites }})</span></button>
</div>
<div class="browser-live-row" style="margin-bottom:1rem">
<section class="panel browser-live-panel">
<h2>Screenshot <small class="muted" x-text="vncSync ? 'VNC sync' : 'Playwright'"></small></h2>
<img class="browser-live-frame" :src="screenshotUrl" alt="Browser screenshot" />
</section>
<section class="panel browser-live-panel">
<h2>Live desktop <small class="muted">noVNC</small>
<a href="{{ novnc_url }}" target="_blank" style="font-size:0.75rem;margin-left:0.5rem">Open apart ↗</a>
</h2>
<iframe class="browser-novnc" src="{{ novnc_url }}" title="Live VNC" data-clear-on-nav></iframe>
<p class="novnc-hint">Wachtwoord: <code>Foodlinkk2026</code>. Tip: open VNC ook <a href="{{ novnc_url }}" target="_blank">in apart venster</a> — dan geen navigatie-waarschuwing bij wisselen van pagina.</p>
</section>
</div>
<div x-show="tab === 'research'">
<section class="panel browser-instruct-box">
<h2>Interactief — leer Herman wat te klikken</h2>
<p class="page-tip">Beschrijf in het Nederlands wat de browser moet doen. Voorbeeld: <em>Accepteren cookies, scroll naar beneden, klik op Mekkafood filter</em></p>
<textarea class="form-input" x-model="instruction" rows="3" placeholder="Accepteren cookies → scroll 2× → klik op 'Snacks'"></textarea>
<div class="browser-action-bar">
<button type="button" class="btn btn-primary" @click="instructBrowse()" :disabled="loading">Uitvoeren + screenshot</button>
<button type="button" class="btn btn-primary" @click="extractFull()" :disabled="loading">🔬 Volledige extractie</button>
<button type="button" class="btn btn-secondary" @click="vncNavigate()" :disabled="loading">🖥 Open in VNC</button>
<button type="button" class="btn btn-secondary" @click="exportPhoto()" :disabled="!session">📥 Foto export</button>
<button type="button" class="btn btn-secondary" @click="runOcr()" :disabled="!session">🔍 OCR tekst</button>
<button type="button" class="btn btn-secondary" @click="savePhotoToDb()" :disabled="!session">💾 Foto → DB</button>
<a class="btn btn-secondary" href="{{ gradio_url }}" target="_blank" rel="noopener">🤖 Browser Use AI</a>
</div>
<div class="browser-steps-log" x-show="stepsLog.length" x-text="stepsLog.join(' · ')"></div>
<div x-show="ocrChips.length" style="margin-top:0.5rem">
<small class="muted">OCR woorden (compact)</small>
<div class="word-chip-cloud" style="max-height:72px;margin-top:0.35rem">
<template x-for="(w,i) in ocrChips" :key="i">
<span class="word-chip low" x-text="w"></span>
</template>
</div>
</div>
</section>
<section class="panel">
<form class="form-grid" @submit.prevent="browse()" style="grid-template-columns: 1fr 1fr 80px auto auto auto;">
<input class="form-input" x-model="url" placeholder="https://..." required />
<input class="form-input" x-model="task" placeholder="Taak (optioneel)" />
<input class="form-input" type="number" min="1" max="12" x-model.number="scrollPages" title="Scroll pagina's voor extractie" />
<button type="submit" class="btn btn-primary" :disabled="loading">
<span x-show="!loading">Browse + VNC</span><span x-show="loading">Bezig…</span>
</button>
<button type="button" class="btn btn-secondary" @click="addMonitor()">+ Monitor</button>
</form>
<div class="browser-status" x-text="statusText"></div>
</section>
<div class="browser-layout">
<div>
<section class="panel" x-show="extractedItems.length">
<h2>Geëxtraheerde data <small class="muted" x-text="'(' + extractedItems.length + ' items)'"></small></h2>
<table class="data-table extract-table">
<thead><tr><th>Regel / product</th><th>Gewicht</th><th>Prijs</th><th>Bron</th></tr></thead>
<tbody>
<template x-for="(it, idx) in extractedItems" :key="idx">
<tr>
<td x-text="it.raw"></td>
<td x-text="it.weight || '—'"></td>
<td x-text="it.price || '—'"></td>
<td x-text="it.source || '—'"></td>
</tr>
</template>
</tbody>
</table>
</section>
<section class="panel" x-show="session">
<h2 x-text="session ? session.title : 'Resultaat'"></h2>
<p class="page-tip"><span x-text="session ? session.final_url : ''"></span></p>
<div class="browser-preview-text" x-text="session ? (session.content_preview || session.content_text || '') : ''"></div>
<div class="browser-links" x-show="session && session.links && session.links.length">
<strong>Links op pagina:</strong>
<template x-for="lnk in (session.links || []).slice(0,15)" :key="lnk.href">
<div><a :href="lnk.href" target="_blank" x-text="lnk.text || lnk.href"></a></div>
</template>
</div>
</section>
</div>
<aside>
<section class="panel">
<h2>Recente sessies</h2>
<template x-for="s in recent" :key="s.id">
<div class="clickable-row" style="padding:.4rem 0;border-bottom:1px solid rgba(148,163,184,.1)" @click="loadSession(s.id)">
<strong x-text="s.title || s.url"></strong><br/>
<small x-text="s.created_at ? s.created_at.substring(0,16).replace('T',' ') : ''"></small>
</div>
</template>
<p class="empty-state" x-show="!recent.length">Nog geen sessies.</p>
</section>
</aside>
</div>
</div>
<div x-show="tab === 'monitor'" x-cloak>
<div class="kpi-grid">
<div class="kpi-card clickable-kpi"><div class="kpi-label">Active sites</div><div class="kpi-value">{{ stats.active_sites }}</div></div>
<div class="kpi-card amber clickable-kpi"><div class="kpi-label">Changes 24h</div><div class="kpi-value">{{ stats.changes_24h }}</div></div>
</div>
<section class="panel">
<h2>Website toevoegen</h2>
<form class="form-grid" style="display:flex;flex-wrap:wrap;gap:0.5rem;align-items:center" @submit.prevent="addSite">
<input class="form-input" style="flex:2;min-width:200px" placeholder="https://..." x-model="monitorForm.url" required />
<input class="form-input" style="flex:1;min-width:120px" placeholder="Naam" x-model="monitorForm.name" />
<button class="btn btn-primary" type="submit">Toevoegen</button>
<button type="button" class="btn btn-secondary" @click="crawlAll()">Alles crawlen</button>
</form>
</section>
<section class="panel">
<h2>Gemonitorde websites</h2>
<table class="data-table"><thead><tr><th>Naam</th><th>URL</th><th>Laatst gecheckt</th><th>Acties</th></tr></thead>
<tbody>{% for s in sites %}
<tr>
<td>{{ s.name }}</td>
<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis" title="{{ s.url }}">{{ s.url }}</td>
<td>{{ s.last_crawled or '—' }}</td>
<td class="btn-group">
<button class="btn btn-sm" @click="crawlOne({{ s.id }})">Crawl</button>
<button class="btn btn-sm btn-secondary" @click="toggleSite({{ s.id }})">{{ 'Pauze' if s.is_active else 'Activeer' }}</button>
<button class="btn btn-sm btn-reject" @click="removeSite({{ s.id }})">Verwijder</button>
</td></tr>{% else %}
<tr><td colspan="4" class="empty-state">Geen sites — voeg een URL toe of klik + Monitor bij Research.</td></tr>
{% endfor %}</tbody></table>
</section>
<div class="grid-2">
<section class="panel"><h2>Wijzigingen</h2>
<ul>{% for c in page_changes %}<li><strong>{{ c.name or c.url }}</strong> · {{ c.changed_at }}</li>{% else %}<li class="empty-state">Geen wijzigingen</li>{% endfor %}</ul>
</section>
<section class="panel"><h2>Crawl logs</h2>
<ul>{% for l in crawl_logs %}<li><span class="badge">{{ l.status }}</span> {{ l.message }} <small>{{ l.logged_at }}</small></li>{% endfor %}</ul>
</section>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function browserPage() {
return {
tab: window.location.hash === '#monitor' ? 'monitor' : 'research',
url: {{ default_url | tojson }},
instruction: 'Accepteren cookies, scroll naar beneden, toon alle producten',
scrollPages: 6,
newSiteUrl: '',
newSiteName: '',
extractedItems: [],
stepsLog: [],
ocrText: '',
ocrChips: [],
loading: false,
livePoll: true,
vncSync: true,
jobRunning: false,
jobLabel: '',
jobAbort: null,
session: null,
recent: [],
monitorForm: { url: '', name: '' },
screenshotUrl: '/api/admin/browser/live/screenshot.jpg?t=0',
statusText: 'Klaar — voer een URL in of klik Browse nu.',
pollTimer: null,
normalizeUrl(u) {
u = (u || '').trim();
if (!u) return u;
if (!/^https?:\/\//i.test(u)) u = 'https://' + u;
return u;
},
siteNameFromUrl(u) {
try { return new URL(u).hostname.replace(/^www\./, ''); } catch (e) { return u; }
},
setTab(t) {
this.tab = t;
history.replaceState(null, '', t === 'monitor' ? '/browser#monitor' : '/browser');
},
init() {
if (window.location.hash === '#monitor') this.tab = 'monitor';
window.addEventListener('hashchange', () => {
this.tab = window.location.hash === '#monitor' ? 'monitor' : 'research';
});
this.loadRecent();
this.startPoll();
},
startPoll() {
if (this.pollTimer) clearInterval(this.pollTimer);
if (!this.livePoll) return;
const base = this.vncSync
? '/api/admin/browser/vnc/screenshot.jpg?t='
: '/api/admin/browser/live/screenshot.jpg?t=';
this.pollTimer = setInterval(() => {
this.screenshotUrl = base + Date.now();
}, 1500);
this.screenshotUrl = base + Date.now();
},
startJob(label) {
if (this.jobAbort) this.jobAbort.abort();
this.jobAbort = new AbortController();
this.jobRunning = true;
this.jobLabel = label;
return this.jobAbort.signal;
},
endJob(msg) {
this.jobRunning = false;
this.jobLabel = msg || '';
this.jobAbort = null;
},
stopJob() {
if (this.jobAbort) {
this.jobAbort.abort();
Cockpit.toast('Taak geannuleerd — server kan nog kort doorlopen', 'info');
}
this.endJob('Gestopt door gebruiker');
this.loading = false;
},
async runJob(label, fn) {
const signal = this.startJob(label);
this.loading = true;
try {
return await fn(signal);
} catch (e) {
if (e.name === 'AbortError') {
this.endJob('Gestopt');
return null;
}
throw e;
} finally {
this.loading = false;
if (this.jobRunning) this.endJob('Klaar');
}
},
reloadPage() {
if (window.Cockpit && Cockpit.clearIframes) Cockpit.clearIframes();
setTimeout(() => location.reload(), 80);
},
async loadRecent() {
try {
const r = await Cockpit.api('/browser/sessions?limit=8');
this.recent = r.sessions || [];
} catch (e) {}
},
async instructBrowse() {
this.stepsLog = [];
this.statusText = 'Instructies uitvoeren…';
try {
const r = await this.runJob('Instructies uitvoeren…', (signal) =>
Cockpit.api('/browser/instruct', {
method: 'POST',
body: JSON.stringify({ url: this.url, instruction: this.instruction, wait_seconds: 5 }),
signal,
})
);
if (!r) return;
this.session = r.session;
this.stepsLog = (r.session.metadata && r.session.metadata.steps) || [];
this.statusText = 'Klaar — ' + this.stepsLog.length + ' stappen';
if (r.session.id) {
this.screenshotUrl = '/api/admin/browser/sessions/' + r.session.id + '/screenshot.jpg?t=' + Date.now();
this.vncSync = false;
}
await this.loadRecent();
Cockpit.toast('Instructies uitgevoerd', 'success');
} catch (e) { this.statusText = e.message; Cockpit.toast(e.message, 'error'); }
},
exportPhoto() {
if (!this.session || !this.session.id) return;
const a = document.createElement('a');
a.href = '/api/admin/browser/sessions/' + this.session.id + '/screenshot.jpg';
a.download = 'foodlinkk-browser-' + this.session.id + '.jpg';
a.click();
},
async runOcr() {
if (!this.session || !this.session.id) return;
try {
const r = await Cockpit.api('/browser/sessions/' + this.session.id + '/ocr');
this.ocrText = r.ocr_text || '';
const words = (this.ocrText.match(/[\wÀ-ÿ]{3,}/g) || []);
const seen = new Set();
this.ocrChips = [];
words.forEach(w => { const k = w.toLowerCase(); if (!seen.has(k)) { seen.add(k); this.ocrChips.push(w); } });
if (this.ocrChips.length > 40) this.ocrChips = this.ocrChips.slice(0, 40);
Cockpit.toast('OCR klaar', 'success');
} catch (e) { Cockpit.toast(e.message, 'error'); }
},
async browse() {
this.statusText = 'Browser + VNC starten op achtergrond…';
try {
const r = await this.runJob('Browse + VNC…', async (signal) => {
const vncP = Cockpit.api('/browser/vnc-navigate', {
method: 'POST',
body: JSON.stringify({ url: this.url, instruction: this.instruction, wait_seconds: 4 }),
signal,
}).catch(() => null);
const browseP = Cockpit.api('/browser/browse', {
method: 'POST',
body: JSON.stringify({ url: this.url, task: this.task, instruction: this.instruction, wait_seconds: 4 }),
signal,
});
const [res] = await Promise.all([browseP, vncP]);
return res;
});
if (!r) return;
this.session = r.session;
this.statusText = 'Klaar — sessie #' + r.session.id + ' (je kunt verder navigeren)';
if (r.session.id) {
this.vncSync = true;
this.startPoll();
}
await this.loadRecent();
Cockpit.toast('Browse gestart — VNC + database', 'success');
} catch (e) {
this.statusText = 'Fout: ' + e.message;
Cockpit.toast(e.message, 'error');
}
},
async vncNavigate() {
this.statusText = 'VNC opent URL…';
try {
const r = await this.runJob('VNC navigate…', (signal) =>
Cockpit.api('/browser/vnc-navigate', {
method: 'POST',
body: JSON.stringify({ url: this.url, instruction: this.instruction, wait_seconds: 5 }),
signal,
})
);
if (!r) return;
this.statusText = 'VNC: ' + (r.title || r.final_url || this.url);
this.vncSync = true;
this.startPoll();
Cockpit.toast('URL geopend in live desktop', 'success');
} catch (e) {
this.statusText = 'VNC fout: ' + e.message;
Cockpit.toast(e.message, 'error');
}
},
async extractFull() {
this.statusText = 'Extractie op achtergrond…';
this.extractedItems = [];
try {
const r = await this.runJob('Volledige extractie…', (signal) =>
Cockpit.api('/browser/extract-full', {
method: 'POST',
body: JSON.stringify({
url: this.url, instruction: this.instruction, wait_seconds: 5,
scroll_pages: this.scrollPages, also_vnc: true,
}),
signal,
})
);
if (!r) return;
this.session = r.session;
this.extractedItems = r.session.extracted_items || (r.session.metadata && r.session.metadata.extracted_items) || [];
this.statusText = 'Extractie klaar — ' + this.extractedItems.length + ' items';
if (r.session.id) {
this.vncSync = true;
this.startPoll();
}
await this.loadRecent();
Cockpit.toast(this.extractedItems.length + ' items geëxtraheerd', 'success');
} catch (e) {
this.statusText = 'Extractie fout: ' + e.message;
Cockpit.toast(e.message, 'error');
}
},
loadPreset(u) {
this.url = u;
this.statusText = 'Preset geladen — klik Browse + VNC of Volledige extractie';
},
async addSiteQuick() {
const u = this.normalizeUrl(this.newSiteUrl || this.url);
if (!u) return Cockpit.toast('Geen URL', 'error');
try {
await Cockpit.api('/monitor/sites', {
method: 'POST',
body: JSON.stringify({ url: u, name: this.newSiteName || this.siteNameFromUrl(u) }),
});
Cockpit.toast('Site toegevoegd — tab Monitor', 'success');
this.newSiteUrl = '';
this.newSiteName = '';
this.setTab('monitor');
} catch (e) { Cockpit.toast(e.message, 'error'); }
},
async savePhotoToDb() {
if (!this.session || !this.session.id) return;
try {
const img = await fetch('/api/admin/browser/sessions/' + this.session.id + '/screenshot.jpg');
const blob = await img.blob();
const b64 = await new Promise((res, rej) => {
const r = new FileReader();
r.onload = () => res(r.result.split(',')[1]);
r.onerror = rej;
r.readAsDataURL(blob);
});
const r = await Cockpit.api('/photos/analyze', {
method: 'POST',
body: JSON.stringify({
image_b64: b64,
source: 'browser-session',
filename: 'session-' + this.session.id + '.jpg',
session_id: this.session.id,
}),
});
Cockpit.toast('Foto opgeslagen in DB #' + r.photo.id, 'success');
} catch (e) { Cockpit.toast(e.message, 'error'); }
},
async loadSession(id) {
try {
const r = await Cockpit.api('/browser/sessions/' + id);
this.session = r.session;
this.url = r.session.url || this.url;
this.extractedItems = (r.session.metadata && r.session.metadata.extracted_items) || r.session.extracted_items || [];
this.screenshotUrl = '/api/admin/browser/sessions/' + id + '/screenshot.jpg?t=' + Date.now();
} catch (e) { Cockpit.toast(e.message, 'error'); }
},
async addMonitor() {
const u = this.normalizeUrl(this.url);
if (!u) return Cockpit.toast('Geen URL', 'error');
try {
await Cockpit.api('/monitor/sites', {
method: 'POST',
body: JSON.stringify({ url: u, name: (this.session && this.session.title) || this.siteNameFromUrl(u) }),
});
Cockpit.toast('Toegevoegd aan Site Monitor', 'success');
} catch (e) { Cockpit.toast(e.message, 'error'); }
},
async addSite() {
const payload = { ...this.monitorForm, url: this.normalizeUrl(this.monitorForm.url) };
if (!payload.url) return Cockpit.toast('URL verplicht', 'error');
if (!payload.name) payload.name = this.siteNameFromUrl(payload.url);
try {
await Cockpit.api('/monitor/sites', { method: 'POST', body: JSON.stringify(payload) });
Cockpit.toast('Site toegevoegd', 'success');
this.reloadPage();
} catch (e) { Cockpit.toast(e.message, 'error'); }
},
async crawlAll() {
await Cockpit.api('/monitor/trigger-crawl', { method: 'POST', body: '{}' });
Cockpit.toast('Crawl gestart', 'success'); setTimeout(() => this.reloadPage(), 2000);
},
async crawlOne(id) {
await Cockpit.api('/monitor/trigger-crawl', { method: 'POST', body: JSON.stringify({ site_id: id }) });
Cockpit.toast('Crawl gestart', 'success'); setTimeout(() => this.reloadPage(), 2000);
},
async toggleSite(id) {
await Cockpit.api('/monitor/sites/' + id + '/toggle', { method: 'PATCH' });
this.reloadPage();
},
async removeSite(id) {
if (!confirm('Website permanent verwijderen uit monitoring?')) return;
await Cockpit.api('/monitor/sites/' + id + '?hard=true', { method: 'DELETE' });
Cockpit.toast('Verwijderd', 'success'); this.reloadPage();
},
};
}
</script>
{% endblock %}