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
+75 -1
View File
@@ -1,4 +1,8 @@
{% extends "base.html" %}
{% 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=13" />
{% endblock %}
{% block content %}
<div x-data="marketingHub()" x-init="init()">
<header class="page-header">
@@ -9,9 +13,29 @@
<div class="retail-actions">
<button class="btn btn-sm btn-pulse" @click="refreshAll()" :disabled="busy">↻ Alles verversen</button>
<a class="btn btn-sm btn-pulse-green" href="/retail">Retail 360 →</a>
<a class="btn btn-sm" href="/projects">Projecten →</a>
</div>
</header>
<div id="viz-toolbar" class="viz-toolbar"></div>
<div class="hm-dash-two-col" style="margin-bottom:1rem">
<div class="hm-chart-panel page-viz-panel viz-mode-chart">
<div class="hm-chart-head"><h4>Marketing data</h4><small>RSS · trends · kansen</small></div>
<div class="page-viz-body">
<div class="hm-chart-wrap page-viz-canvas"><canvas id="chart-marketing"></canvas></div>
<div id="viz-marketing-alt" class="viz-alt"></div>
</div>
</div>
<div class="hm-chart-panel page-viz-panel viz-mode-chart">
<div class="hm-chart-head"><h4>Publish jobs</h4><small>status verdeling</small></div>
<div class="page-viz-body">
<div class="hm-chart-wrap page-viz-canvas"><canvas id="chart-marketing-publish"></canvas></div>
<div id="viz-marketing-publish-alt" class="viz-alt"></div>
</div>
</div>
</div>
<div class="live-feed-bar" x-show="dashboard.rss?.length">
<div class="ticker-track">
<template x-for="r in dashboard.rss" :key="r.id">
@@ -191,6 +215,12 @@
<textarea class="form-input" rows="5" x-model="publishForm.text" placeholder="Post tekst — UTF-8 emoji ondersteund 🥘✨" style="width:100%;margin-bottom:0.75rem"></textarea>
<div style="display:flex;gap:1rem;flex-wrap:wrap;align-items:center;margin-bottom:1rem">
<select class="form-input" x-model="publishForm.project_id" style="max-width:220px">
<option value="">Opslaan in agent-project (auto)</option>
<template x-for="p in cockpitProjects" :key="p.id">
<option :value="p.id" x-text="p.name"></option>
</template>
</select>
<label class="btn btn-secondary btn-sm">
📷 Foto uploaden
<input type="file" accept="image/*" @change="uploadImage($event)" style="display:none" />
@@ -359,6 +389,8 @@
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/dashboard-layout.js?v=2"></script>
<script src="/static/js/page-viz.js?v=1"></script>
<script>
function marketingHub() {
return {
@@ -369,8 +401,9 @@ function marketingHub() {
promos: [], bookmarks: [], bookmarkIds: {},
reclameChains: [], reclameFilter: { chain: '', q: '', folder_type: '', valid_days: '' },
reclameStats: { chains: 0 },
publishChannels: [], publishForm: { text: '', image_url: '', media_ids: [], channels: [] },
publishChannels: [], publishForm: { text: '', image_url: '', media_ids: [], channels: [], project_id: '' },
publishHistory: [], publishJobId: null, publishJobStatus: '', publishBusy: false,
cockpitProjects: [],
emojis: ['🥘','✨','🔥','📢','🛒','🌿','☪️','🇳🇱','👨‍🍳','💚','📁','🎯'],
promoForm: { chain: '', title: '', folder_path: '', description: '' },
_pollStop: null,
@@ -385,11 +418,16 @@ function marketingHub() {
async init() {
await this.loadDashboard();
await this.loadBookmarks();
await this.loadPublishHistory();
fetch('/api/projects?limit=50').then(r=>r.json()).then(d=>{ this.cockpitProjects = d.items || []; }).catch(()=>{});
if (this.tab === 'regelgeving') await this.loadRegulations();
if (this.tab === 'reclame') { await this.loadReclameChains(); await this.loadPromos(); }
if (this.tab === 'publish') { await this.loadPublishChannels(); await this.loadPublishHistory(); }
this._pollStop = CockpitLive.startPolling(() => this.refreshAll(false), 30000);
this.$nextTick(() => this.updateTabIndicator());
if (window.PageViz) {
window.PageViz.init('marketing', (mode) => this.renderViz(mode));
}
fetch('/api/admin/recommendations/pending').then(r=>r.json()).then(d=>{
const el=document.getElementById('reco-list');
if(!el) return;
@@ -510,6 +548,7 @@ function marketingHub() {
try {
await fetch('/api/retail/rss/refresh', { method: 'POST' });
await this.loadDashboard();
this.renderViz(window.__vizMode || 'neo-bars');
if (toast !== false) Cockpit.toast('Marketing data bijgewerkt', 'success');
} finally { this.busy = false; }
},
@@ -540,8 +579,42 @@ function marketingHub() {
try {
const r = await fetch('/api/marketing/publish/history?limit=30').then(res => res.json());
this.publishHistory = r.items || [];
if (window.__marketingRenderViz) window.__marketingRenderViz(window.__vizMode || 'neo-bars');
} catch (e) { this.publishHistory = []; }
},
renderViz(mode) {
window.__vizMode = mode;
window.__marketingRenderViz = (m) => this.renderViz(m);
if (!window.VizEngine) return;
const d = this.dashboard || {};
VizEngine.renderFromRows(mode,
document.getElementById('chart-marketing'),
document.getElementById('viz-marketing-alt'),
'marketing-main', 'Marketing KPIs',
[
{ label: 'RSS', value: (d.rss || []).length, raw: (d.rss || []).length },
{ label: 'Trends', value: (d.trends || []).length, raw: (d.trends || []).length },
{ label: 'Kansen', value: (d.top_opportunities || []).length, raw: (d.top_opportunities || []).length },
{ label: 'Promo', value: this.promos.length, raw: this.promos.length },
],
'hm-eq-purple', VizEngine.MODES ? null : null
);
const statusMap = {};
(this.publishHistory || []).forEach(j => {
const s = j.status || 'unknown';
statusMap[s] = (statusMap[s] || 0) + 1;
});
const labels = Object.keys(statusMap);
const values = labels.map(k => statusMap[k]);
if (!labels.length) { labels.push('geen'); values.push(0); }
VizEngine.renderFromRows(mode,
document.getElementById('chart-marketing-publish'),
document.getElementById('viz-marketing-publish-alt'),
'marketing-publish', 'Publish status',
labels.map((l, i) => ({ label: l, value: values[i], raw: values[i] })),
'hm-eq-cyan'
);
},
publishResultSummary(j) {
const res = j.result || {};
if (typeof res === 'string') { try { return JSON.stringify(JSON.parse(res)); } catch (e) { return res; } }
@@ -573,6 +646,7 @@ function marketingHub() {
image_url: this.publishForm.image_url || null,
media_ids: this.publishForm.media_ids || [],
channels: this.publishForm.channels,
project_id: this.publishForm.project_id ? parseInt(this.publishForm.project_id, 10) : null,
};
const r = await fetch('/api/marketing/publish', {
method: 'POST',