SysOps: deploy-all — 2026-06-09 10:41 UTC

This commit is contained in:
sysops
2026-06-09 10:41:13 +00:00
parent 69fe67cc0e
commit 21ea3a2c81
82 changed files with 8906 additions and 981 deletions
+113 -29
View File
@@ -2,12 +2,24 @@
{% block title %}Documents & Sentiment · Foodlinkk Command Center{% endblock %}
{% block extra_head %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=9" />
<style>
.doc-neo-shell { --hm-cyan: #00e5ff; --hm-border: rgba(0, 229, 255, 0.12); --hm-panel: #0d1219; }
.doc-neo-shell .page-header h1 { font-size: 1.55rem; }
.doc-dash-filters { display:flex; flex-wrap:wrap; gap:0.5rem; align-items:center; margin-bottom:1rem; }
.doc-chart-panel { height: 140px; }
.doc-chart-panel canvas { max-height: 110px !important; }
.doc-neo-kpi-row { display:grid; grid-template-columns: repeat(4, 1fr); gap:1rem; margin-bottom:1rem; }
@media (max-width: 900px) { .doc-neo-kpi-row { grid-template-columns: 1fr 1fr; } }
.doc-neo-kpi {
background: linear-gradient(160deg, rgba(13, 18, 25, 0.95), rgba(8, 12, 18, 0.98));
border: 1px solid var(--hm-border); border-radius: 14px; padding: 1rem 1.1rem 0.65rem;
}
.doc-neo-kpi-label { font-size: 0.65rem; letter-spacing: 0.12em; text-transform: uppercase; color: #94a3b8; }
.doc-neo-kpi-value { font-size: 1.65rem; font-weight: 700; color: #f1f5f9; margin: 0.25rem 0; }
.doc-chart-grid { display:grid; grid-template-columns: 1fr 1fr; gap:1rem; margin-top:1rem; }
@media (max-width: 800px) { .doc-chart-grid { grid-template-columns: 1fr; } }
.doc-chart-panel .hm-chart-wrap { height: 130px; }
.photo-grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap:1rem; }
.photo-card { border:1px solid rgba(148,163,184,.15); border-radius:10px; overflow:hidden; background:rgba(0,0,0,.2); }
.photo-card { border:1px solid rgba(0,229,255,0.12); border-radius:12px; overflow:hidden; background:rgba(13,18,25,0.95); }
.photo-card img { width:100%; height:140px; object-fit:cover; background:#0a0f1a; }
.photo-card-body { padding:0.6rem; font-size:0.78rem; }
.detection-wrap { position:relative; display:inline-block; max-width:100%; }
@@ -15,17 +27,33 @@
</style>
{% endblock %}
{% block content %}
<div x-data="documentsDash()" x-init="init()">
<header class="page-header">
<h1>Documents &amp; Foto's — interactief dashboard</h1>
<p class="subtitle page-tip">Filter NAS documenten, bekijk compacte charts, analyseer foto's uit share/Telegram/browser met OCR + detectie.</p>
<div class="doc-neo-shell" x-data="documentsDash()" x-init="init()">
<header class="page-header herman-hero" style="margin-bottom:1rem">
<h1>Documents &amp; Foto's <span class="live-badge">Neo</span></h1>
<p class="hero-sub page-tip">NAS corpus · sentiment · woorden · foto OCR — interactief dashboard</p>
</header>
<div class="kpi-grid">
<div class="kpi-card clickable-kpi" @click="filterSentiment=''"><div class="kpi-label">Documenten</div><div class="kpi-value" x-text="filteredDocs.length"></div></div>
<div class="kpi-card purple clickable-kpi"><div class="kpi-label">Totaal woorden</div><div class="kpi-value">{{ summary.total_words }}</div></div>
<div class="kpi-card green clickable-kpi"><div class="kpi-label">Foto's in DB</div><div class="kpi-value" x-text="photos.length"></div></div>
<div class="kpi-card orange clickable-kpi"><div class="kpi-label">Gem. sentiment</div><div class="kpi-value">{{ summary.avg_sentiment }}</div></div>
<div class="doc-neo-kpi-row">
<div class="doc-neo-kpi clickable-kpi" @click="filterSentiment=''">
<div class="doc-neo-kpi-label">Documenten</div>
<div class="doc-neo-kpi-value" x-text="filteredDocs.length"></div>
<div class="hm-eq"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
</div>
<div class="doc-neo-kpi clickable-kpi">
<div class="doc-neo-kpi-label">Totaal woorden</div>
<div class="doc-neo-kpi-value">{{ summary.total_words }}</div>
<div class="hm-eq hm-eq-purple"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
</div>
<div class="doc-neo-kpi clickable-kpi">
<div class="doc-neo-kpi-label">Foto's in DB</div>
<div class="doc-neo-kpi-value" x-text="photos.length"></div>
<div class="hm-eq hm-eq-cyan"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
</div>
<div class="doc-neo-kpi clickable-kpi">
<div class="doc-neo-kpi-label">Gem. sentiment</div>
<div class="doc-neo-kpi-value">{{ summary.avg_sentiment }}</div>
<div class="hm-eq hm-eq-lime"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
</div>
</div>
<div class="doc-dash-filters panel">
@@ -46,14 +74,18 @@
<button class="btn btn-secondary btn-sm" @click="refreshShare()">↻ Share scan</button>
</div>
<div class="grid-2" style="margin-top:1rem">
<section class="panel doc-chart-panel">
<h2>Top woorden <small class="muted">klik = filter</small></h2>
<canvas id="wordsChart" height="110"></canvas>
<div class="doc-chart-grid">
<section class="hm-chart-panel doc-chart-panel">
<div class="hm-chart-head"><h4>Top woorden</h4><small>klik = filter</small></div>
<div class="hm-chart-wrap"><canvas id="wordsChart"></canvas></div>
<div id="viz-words-alt" class="viz-alt"></div>
<div class="hm-eq"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
</section>
<section class="panel doc-chart-panel">
<h2>Sentiment <small class="muted">klik = filter</small></h2>
<canvas id="sentimentChart" height="110"></canvas>
<section class="hm-chart-panel doc-chart-panel">
<div class="hm-chart-head"><h4>Sentiment</h4><small>klik = filter</small></div>
<div class="hm-chart-wrap"><canvas id="sentimentChart"></canvas></div>
<div id="viz-sentiment-alt" class="viz-alt"></div>
<div class="hm-eq hm-eq-purple"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
</section>
</div>
@@ -132,8 +164,18 @@
</div>
</section>
<section class="panel" x-show="activePhoto" style="margin-top:1rem">
<h2>Detectie overlay — foto #<span x-text="activePhoto"></span></h2>
<section class="panel" x-show="activePhoto" style="margin-top:1rem">
<h2>Detectie overlay — foto #<span x-text="activePhoto"></span></h2>
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;margin-bottom:0.75rem" x-show="activePhoto">
<select class="form-input" x-model="linkProjectId" style="max-width:200px">
<option value="">Koppel aan project…</option>
<template x-for="p in cockpitProjects" :key="p.id">
<option :value="p.id" x-text="p.name"></option>
</template>
</select>
<button class="btn btn-sm btn-primary" @click="savePhotoToProject()">Opslaan in project</button>
<a href="/projects" class="btn btn-sm">Alle projecten →</a>
</div>
<div class="detection-wrap">
<img id="detectionImg" :src="activePhoto ? '/api/admin/photos/' + activePhoto + '/image.jpg' : ''" style="max-width:100%;border-radius:8px" @load="drawDetections()" />
<canvas id="detectionCanvas"></canvas>
@@ -172,14 +214,28 @@ function documentsDash() {
activeOcr: '',
activeItems: [],
detections: [],
linkProjectId: '',
cockpitProjects: [],
liveRefresh: false,
liveTimer: null,
init() {
this.applyFilters();
this.renderCharts();
this.renderCharts(window.__vizMode || 'neo-bars');
this.loadPhotos();
this.refreshShare(false);
fetch('/api/projects?limit=50').then(r=>r.json()).then(d=>{ this.cockpitProjects = d.items || []; }).catch(()=>{});
document.addEventListener('foodlinkk:vizmode', (ev) => this.renderCharts(ev.detail.mode));
},
async savePhotoToProject() {
if (!this.activePhoto || !this.linkProjectId) return Cockpit.toast('Kies een project', 'error');
try {
await Cockpit.api('/api/projects/photos/' + this.activePhoto + '/link', {
method: 'POST',
body: JSON.stringify({ project_id: parseInt(this.linkProjectId, 10) }),
});
Cockpit.toast('Foto gekoppeld aan project', 'success');
} catch (e) { Cockpit.toast(e.message, 'error'); }
},
mergeShareItems(items) {
@@ -235,16 +291,44 @@ function documentsDash() {
});
},
renderCharts() {
if (wordsChartInst) wordsChartInst.destroy();
if (sentimentChartInst) sentimentChartInst.destroy();
renderCharts(mode) {
mode = mode || window.__vizMode || 'neo-bars';
if (wordsChartInst) { wordsChartInst.destroy(); wordsChartInst = null; }
if (sentimentChartInst) { sentimentChartInst.destroy(); sentimentChartInst = null; }
const dash = this;
const wordLabels = topWords.slice(0, 10).map(w => w.lemma);
const wordValues = topWords.slice(0, 10).map(w => w.total_count);
const sentLabels = ['Positief', 'Neutraal', 'Negatief'];
const sentValues = [summary.positive, summary.neutral, summary.negative];
if (window.VizEngine && (mode === 'neo-equalizer' || mode === 'neo-cards' || mode === 'neo-table')) {
VizEngine.renderWidget(mode, {
canvas: document.getElementById('wordsChart'),
altContainer: document.getElementById('viz-words-alt'),
key: 'doc-words',
labels: wordLabels, values: wordValues,
rows: wordLabels.map((l, i) => ({ label: l, value: wordValues[i], raw: wordValues[i] })),
eqClass: 'hm-eq-cyan',
});
VizEngine.renderWidget(mode, {
canvas: document.getElementById('sentimentChart'),
altContainer: document.getElementById('viz-sentiment-alt'),
key: 'doc-sentiment',
labels: sentLabels, values: sentValues,
rows: sentLabels.map((l, i) => ({ label: l, value: sentValues[i], raw: sentValues[i] })),
eqClass: 'hm-eq-purple',
});
return;
}
document.getElementById('viz-words-alt').innerHTML = '';
document.getElementById('viz-sentiment-alt').innerHTML = '';
document.getElementById('wordsChart').style.display = '';
document.getElementById('sentimentChart').style.display = '';
wordsChartInst = new Chart(document.getElementById('wordsChart'), {
type: 'bar',
type: mode === 'neo-rings' ? 'polarArea' : 'bar',
data: {
labels: topWords.slice(0, 10).map(w => w.lemma),
datasets: [{ label: 'Aantal', data: topWords.slice(0, 10).map(w => w.total_count),
backgroundColor: 'rgba(232,168,56,.75)', borderWidth: 0 }]
backgroundColor: 'rgba(0,229,255,0.75)', borderWidth: 0, borderRadius: 6 }]
},
options: {
responsive: true, maintainAspectRatio: false,
@@ -255,11 +339,11 @@ function documentsDash() {
}
});
sentimentChartInst = new Chart(document.getElementById('sentimentChart'), {
type: 'doughnut',
type: mode === 'neo-bars' ? 'doughnut' : 'doughnut',
data: {
labels: ['Positief', 'Neutraal', 'Negatief'],
datasets: [{ data: [summary.positive, summary.neutral, summary.negative],
backgroundColor: ['#3ecf8e', '#6b7280', '#ef4444'] }]
backgroundColor: ['#4ade80', '#64748b', '#fb7185'], borderWidth: 0 }]
},
options: {
responsive: true, maintainAspectRatio: false,