Platform bundle: marketing publish, IT ops, packaging, agents mesh.
Volledige Foodlinkk Command Center uitbreiding met social automatisering, reclamefolder filters, Proxmox monitoring en documentatie.
This commit is contained in:
@@ -0,0 +1,561 @@
|
||||
{% 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 & 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 %}
|
||||
Reference in New Issue
Block a user