claude-plugin-wordpress-manager 2.13.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -6
- package/agents/wp-accessibility-auditor.md +1 -1
- package/agents/wp-content-strategist.md +2 -2
- package/agents/wp-deployment-engineer.md +1 -1
- package/agents/wp-distribution-manager.md +1 -1
- package/agents/wp-monitoring-agent.md +1 -1
- package/agents/wp-performance-optimizer.md +1 -1
- package/agents/wp-security-auditor.md +1 -1
- package/agents/wp-site-manager.md +3 -3
- package/commands/wp-setup.md +2 -2
- package/docs/GUIDE.md +26 -26
- package/docs/VALIDATION.md +3 -3
- package/docs/guides/wp-ecommerce.md +4 -4
- package/docs/plans/2026-03-01-tier3-wcop-implementation.md +1 -1
- package/docs/plans/2026-03-01-tier4-5-implementation.md +1 -1
- package/docs/plans/2026-03-02-content-framework-architecture.md +33 -33
- package/docs/plans/2026-03-02-content-framework-strategic-reflections.md +1 -1
- package/docs/plans/2026-03-02-content-intelligence-phase2.md +13 -13
- package/docs/plans/2026-03-02-content-pipeline-phase1.md +10 -10
- package/docs/plans/2026-03-02-dashboard-kanban-design.md +761 -0
- package/docs/plans/2026-03-02-dashboard-kanban-implementation.md +598 -0
- package/docs/plans/2026-03-02-dashboard-strategy.md +363 -0
- package/docs/plans/2026-03-02-editorial-calendar-phase3.md +3 -3
- package/docs/validation/results.json +16 -16
- package/package.json +8 -4
- package/scripts/context-scanner.mjs +446 -0
- package/scripts/dashboard-renderer.mjs +553 -0
- package/scripts/run-validation.mjs +2 -2
- package/servers/wp-rest-bridge/build/server.js +1 -1
- package/skills/wp-analytics/references/signals-feed-schema.md +20 -20
- package/skills/wp-content/references/content-templates.md +1 -1
- package/skills/wp-content/references/seo-optimization.md +8 -8
- package/skills/wp-content-attribution/references/roi-calculation.md +1 -1
- package/skills/wp-content-attribution/references/utm-tracking-setup.md +5 -5
- package/skills/wp-content-generation/references/generation-workflow.md +2 -2
- package/skills/wp-content-pipeline/SKILL.md +7 -7
- package/skills/wp-content-pipeline/references/content-brief-schema.md +25 -25
- package/skills/wp-content-pipeline/references/site-config-schema.md +25 -25
- package/skills/wp-content-repurposing/references/auto-transform-pipeline.md +1 -1
- package/skills/wp-content-repurposing/references/email-newsletter.md +1 -1
- package/skills/wp-content-repurposing/references/platform-specs.md +2 -2
- package/skills/wp-content-repurposing/references/transform-templates.md +27 -27
- package/skills/wp-dashboard/SKILL.md +121 -0
- package/skills/wp-deploy/references/ssh-deploy.md +2 -2
- package/skills/wp-editorial-planner/references/editorial-schema.md +8 -8
- package/skills/wp-multilang-network/references/content-sync.md +3 -3
- package/skills/wp-multilang-network/references/network-architecture.md +1 -1
- package/skills/wp-multilang-network/references/seo-international.md +7 -7
- package/skills/wp-structured-data/references/schema-types.md +4 -4
- package/skills/wp-webhooks/references/payload-formats.md +3 -3
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/dashboard-renderer.mjs — Editorial Kanban Dashboard HTML Renderer
|
|
3
|
+
// Generates a self-contained HTML file from .content-state/ data and opens it in the browser.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// node scripts/dashboard-renderer.mjs # auto-detect site, current month
|
|
7
|
+
// node scripts/dashboard-renderer.mjs --site=mysite # specific site
|
|
8
|
+
// node scripts/dashboard-renderer.mjs --month=2026-04 # specific month
|
|
9
|
+
// node scripts/dashboard-renderer.mjs --output=/tmp/dash.html # custom output
|
|
10
|
+
// node scripts/dashboard-renderer.mjs --no-open # skip browser launch
|
|
11
|
+
|
|
12
|
+
import { writeFile, readdir } from 'node:fs/promises';
|
|
13
|
+
import { resolve, dirname, join } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { exec } from 'node:child_process';
|
|
16
|
+
import { platform } from 'node:os';
|
|
17
|
+
import { scanContentState, aggregateMetrics } from './context-scanner.mjs';
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const PROJECT_ROOT = resolve(__dirname, '..');
|
|
21
|
+
const CONTENT_STATE_DIR = '.content-state';
|
|
22
|
+
const RENDERER_VERSION = '1.0.0';
|
|
23
|
+
|
|
24
|
+
// ── CLI Args ────────────────────────────────────────────────────────
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
const getArg = (name) => {
|
|
27
|
+
const a = args.find(a => a.startsWith(`--${name}=`));
|
|
28
|
+
return a ? a.split('=').slice(1).join('=') : null;
|
|
29
|
+
};
|
|
30
|
+
const hasFlag = (name) => args.includes(`--${name}`);
|
|
31
|
+
|
|
32
|
+
// ── HTML Helpers ────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
function escapeHtml(str) {
|
|
35
|
+
if (!str) return '';
|
|
36
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
37
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function truncate(str, maxLen) {
|
|
41
|
+
if (!str) return '';
|
|
42
|
+
return str.length > maxLen ? str.substring(0, maxLen) + '...' : str;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatDate(dateStr) {
|
|
46
|
+
if (!dateStr) return '—';
|
|
47
|
+
const months = ['Gen','Feb','Mar','Apr','Mag','Giu','Lug','Ago','Set','Ott','Nov','Dic'];
|
|
48
|
+
const d = new Date(dateStr + 'T00:00:00');
|
|
49
|
+
return `${months[d.getMonth()]} ${d.getDate()}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function statusColor(status) {
|
|
53
|
+
const map = {
|
|
54
|
+
planned: 'var(--status-planned)',
|
|
55
|
+
draft: 'var(--status-draft)',
|
|
56
|
+
ready: 'var(--status-ready)',
|
|
57
|
+
scheduled: 'var(--status-scheduled)',
|
|
58
|
+
published: 'var(--status-published)'
|
|
59
|
+
};
|
|
60
|
+
return map[status] || 'var(--status-planned)';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function channelBadge(channel) {
|
|
64
|
+
const map = {
|
|
65
|
+
linkedin: { abbr: 'in', color: '#0077b5' },
|
|
66
|
+
twitter: { abbr: 'tw', color: '#1da1f2' },
|
|
67
|
+
newsletter: { abbr: 'nl', color: '#f59e0b' },
|
|
68
|
+
mailchimp: { abbr: 'nl', color: '#f59e0b' },
|
|
69
|
+
buffer: { abbr: 'bf', color: '#168eea' },
|
|
70
|
+
};
|
|
71
|
+
const info = map[channel] || { abbr: channel.substring(0, 2), color: '#64748b' };
|
|
72
|
+
return `<span class="channel" style="background:${info.color}" title="${escapeHtml(channel)}">${escapeHtml(info.abbr)}</span>`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function typeIcon(type) {
|
|
76
|
+
return type === 'page' ? '📄' : '📝';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function groupByStatus(entries) {
|
|
80
|
+
const groups = { planned: [], draft: [], ready: [], scheduled: [], published: [] };
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
const bucket = groups[entry.status];
|
|
83
|
+
if (bucket) bucket.push(entry);
|
|
84
|
+
else groups.planned.push(entry); // fallback
|
|
85
|
+
}
|
|
86
|
+
return groups;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Card Renderer ───────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
function renderCard(entry) {
|
|
92
|
+
const isEmpty = entry.title === null;
|
|
93
|
+
const cardClass = isEmpty ? 'card card--empty' : 'card';
|
|
94
|
+
const titleDisplay = isEmpty
|
|
95
|
+
? '<span class="card-title card-title--placeholder">[da assegnare]</span>'
|
|
96
|
+
: `<span class="card-title" title="${escapeHtml(entry.title)}">${escapeHtml(truncate(entry.title, 55))}</span>`;
|
|
97
|
+
|
|
98
|
+
const briefLine = entry.briefId
|
|
99
|
+
? `<div class="card-brief">${escapeHtml(entry.briefId)}</div>`
|
|
100
|
+
: '';
|
|
101
|
+
|
|
102
|
+
const postLine = entry.postId
|
|
103
|
+
? `<div class="card-post">WP #${entry.postId}</div>`
|
|
104
|
+
: '';
|
|
105
|
+
|
|
106
|
+
const channelsLine = entry.channels.length > 0
|
|
107
|
+
? `<div class="card-channels">${entry.channels.map(channelBadge).join(' ')}</div>`
|
|
108
|
+
: '';
|
|
109
|
+
|
|
110
|
+
return `
|
|
111
|
+
<div class="${cardClass}" style="--status-color: ${statusColor(entry.status)}">
|
|
112
|
+
<div class="card-header">
|
|
113
|
+
<span class="card-date">${formatDate(entry.date)}</span>
|
|
114
|
+
<span class="card-type">${typeIcon(entry.type)}</span>
|
|
115
|
+
</div>
|
|
116
|
+
${titleDisplay}
|
|
117
|
+
${briefLine}
|
|
118
|
+
${postLine}
|
|
119
|
+
${channelsLine}
|
|
120
|
+
</div>`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Column Renderer ─────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
function renderColumn(status, entries, label) {
|
|
126
|
+
const cards = entries.map(renderCard).join('');
|
|
127
|
+
return `
|
|
128
|
+
<div class="column">
|
|
129
|
+
<div class="column-header">
|
|
130
|
+
<span class="column-label">${escapeHtml(label)}</span>
|
|
131
|
+
<span class="column-count">${entries.length}</span>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="column-body">
|
|
134
|
+
${cards || '<div class="column-empty">—</div>'}
|
|
135
|
+
</div>
|
|
136
|
+
</div>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Signal Renderer ─────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
function renderSignal(anomaly) {
|
|
142
|
+
const deltaNum = parseFloat(anomaly.delta) || 0;
|
|
143
|
+
const arrow = deltaNum >= 0 ? '▲' : '▼';
|
|
144
|
+
const cls = deltaNum >= 0 ? 'signal--up' : 'signal--down';
|
|
145
|
+
return `
|
|
146
|
+
<div class="signal ${cls}">
|
|
147
|
+
<span class="signal-arrow">${arrow}</span>
|
|
148
|
+
<span class="signal-delta">${escapeHtml(anomaly.delta)}</span>
|
|
149
|
+
<span class="signal-entity">${escapeHtml(anomaly.entity)}</span>
|
|
150
|
+
<span class="signal-sep">→</span>
|
|
151
|
+
<span class="signal-action">${escapeHtml(anomaly.action)}</span>
|
|
152
|
+
</div>`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Full HTML Renderer ──────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
export function renderKanbanHTML(rawData, metrics) {
|
|
158
|
+
const entries = rawData.calendar?.entries || [];
|
|
159
|
+
const groups = groupByStatus(entries);
|
|
160
|
+
const anomalies = rawData.signals?.anomalies || [];
|
|
161
|
+
const period = metrics.calendarPeriod || '';
|
|
162
|
+
const monthLabel = period ? (() => {
|
|
163
|
+
const months = ['Gennaio','Febbraio','Marzo','Aprile','Maggio','Giugno','Luglio','Agosto','Settembre','Ottobre','Novembre','Dicembre'];
|
|
164
|
+
const m = parseInt(period.slice(5,7), 10);
|
|
165
|
+
const y = period.slice(0,4);
|
|
166
|
+
return `${months[m-1] || ''} ${y}`;
|
|
167
|
+
})() : 'N/A';
|
|
168
|
+
|
|
169
|
+
const nextDeadlineStr = metrics.nextDeadline
|
|
170
|
+
? `Next: ${formatDate(metrics.nextDeadline.date)} — "${escapeHtml(truncate(metrics.nextDeadline.title, 40))}"`
|
|
171
|
+
: '';
|
|
172
|
+
|
|
173
|
+
const signalsStrip = anomalies.length > 0
|
|
174
|
+
? `<section class="signals-strip">
|
|
175
|
+
<h2>⚡ Signals</h2>
|
|
176
|
+
<div class="signal-list">${anomalies.map(renderSignal).join('')}</div>
|
|
177
|
+
</section>`
|
|
178
|
+
: '';
|
|
179
|
+
|
|
180
|
+
const pipelineBadges = ['planned','draft','ready','scheduled','published'].map(s => {
|
|
181
|
+
return `<span class="badge badge--${s}">${metrics.columns[s]} ${s}</span>`;
|
|
182
|
+
}).join(' ');
|
|
183
|
+
|
|
184
|
+
return `<!DOCTYPE html>
|
|
185
|
+
<html lang="it">
|
|
186
|
+
<head>
|
|
187
|
+
<meta charset="UTF-8">
|
|
188
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
189
|
+
<title>Editorial Dashboard — ${escapeHtml(metrics.siteId)} — ${escapeHtml(monthLabel)}</title>
|
|
190
|
+
<style>
|
|
191
|
+
:root {
|
|
192
|
+
--bg-page: #f8fafc;
|
|
193
|
+
--bg-column: #f1f5f9;
|
|
194
|
+
--bg-card: #ffffff;
|
|
195
|
+
--border: #e2e8f0;
|
|
196
|
+
--text-primary: #1e293b;
|
|
197
|
+
--text-secondary: #64748b;
|
|
198
|
+
--text-muted: #94a3b8;
|
|
199
|
+
--status-planned: #94a3b8;
|
|
200
|
+
--status-draft: #eab308;
|
|
201
|
+
--status-ready: #3b82f6;
|
|
202
|
+
--status-scheduled: #8b5cf6;
|
|
203
|
+
--status-published: #22c55e;
|
|
204
|
+
--signal-up: #22c55e;
|
|
205
|
+
--signal-down: #ef4444;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
209
|
+
|
|
210
|
+
body {
|
|
211
|
+
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
212
|
+
background: var(--bg-page);
|
|
213
|
+
color: var(--text-primary);
|
|
214
|
+
line-height: 1.5;
|
|
215
|
+
padding: 24px;
|
|
216
|
+
max-width: 1400px;
|
|
217
|
+
margin: 0 auto;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Header */
|
|
221
|
+
header {
|
|
222
|
+
margin-bottom: 24px;
|
|
223
|
+
padding-bottom: 20px;
|
|
224
|
+
border-bottom: 2px solid var(--border);
|
|
225
|
+
}
|
|
226
|
+
header h1 {
|
|
227
|
+
font-size: 1.5rem;
|
|
228
|
+
font-weight: 700;
|
|
229
|
+
margin-bottom: 8px;
|
|
230
|
+
}
|
|
231
|
+
.meta {
|
|
232
|
+
color: var(--text-secondary);
|
|
233
|
+
font-size: 0.875rem;
|
|
234
|
+
margin-bottom: 12px;
|
|
235
|
+
}
|
|
236
|
+
.progress-bar {
|
|
237
|
+
position: relative;
|
|
238
|
+
height: 28px;
|
|
239
|
+
background: var(--border);
|
|
240
|
+
border-radius: 14px;
|
|
241
|
+
overflow: hidden;
|
|
242
|
+
margin-bottom: 12px;
|
|
243
|
+
}
|
|
244
|
+
.progress-fill {
|
|
245
|
+
height: 100%;
|
|
246
|
+
background: var(--status-published);
|
|
247
|
+
border-radius: 14px;
|
|
248
|
+
transition: width 0.3s;
|
|
249
|
+
}
|
|
250
|
+
.progress-label {
|
|
251
|
+
position: absolute;
|
|
252
|
+
top: 50%;
|
|
253
|
+
left: 50%;
|
|
254
|
+
transform: translate(-50%, -50%);
|
|
255
|
+
font-size: 0.8rem;
|
|
256
|
+
font-weight: 600;
|
|
257
|
+
color: var(--text-primary);
|
|
258
|
+
}
|
|
259
|
+
.pipeline-counts {
|
|
260
|
+
display: flex;
|
|
261
|
+
gap: 8px;
|
|
262
|
+
flex-wrap: wrap;
|
|
263
|
+
}
|
|
264
|
+
.badge {
|
|
265
|
+
display: inline-block;
|
|
266
|
+
padding: 3px 10px;
|
|
267
|
+
border-radius: 12px;
|
|
268
|
+
font-size: 0.75rem;
|
|
269
|
+
font-weight: 600;
|
|
270
|
+
color: white;
|
|
271
|
+
}
|
|
272
|
+
.badge--planned { background: var(--status-planned); }
|
|
273
|
+
.badge--draft { background: var(--status-draft); color: #1e293b; }
|
|
274
|
+
.badge--ready { background: var(--status-ready); }
|
|
275
|
+
.badge--scheduled { background: var(--status-scheduled); }
|
|
276
|
+
.badge--published { background: var(--status-published); }
|
|
277
|
+
|
|
278
|
+
/* Kanban Grid */
|
|
279
|
+
.kanban {
|
|
280
|
+
display: grid;
|
|
281
|
+
grid-template-columns: repeat(5, 1fr);
|
|
282
|
+
gap: 16px;
|
|
283
|
+
margin-bottom: 24px;
|
|
284
|
+
min-height: 300px;
|
|
285
|
+
}
|
|
286
|
+
.column {
|
|
287
|
+
background: var(--bg-column);
|
|
288
|
+
border-radius: 10px;
|
|
289
|
+
padding: 12px;
|
|
290
|
+
display: flex;
|
|
291
|
+
flex-direction: column;
|
|
292
|
+
gap: 8px;
|
|
293
|
+
min-height: 200px;
|
|
294
|
+
}
|
|
295
|
+
.column-header {
|
|
296
|
+
display: flex;
|
|
297
|
+
justify-content: space-between;
|
|
298
|
+
align-items: center;
|
|
299
|
+
padding-bottom: 8px;
|
|
300
|
+
border-bottom: 2px solid var(--border);
|
|
301
|
+
margin-bottom: 4px;
|
|
302
|
+
}
|
|
303
|
+
.column-label {
|
|
304
|
+
font-weight: 700;
|
|
305
|
+
font-size: 0.8rem;
|
|
306
|
+
text-transform: uppercase;
|
|
307
|
+
letter-spacing: 0.05em;
|
|
308
|
+
color: var(--text-secondary);
|
|
309
|
+
}
|
|
310
|
+
.column-count {
|
|
311
|
+
background: var(--border);
|
|
312
|
+
color: var(--text-secondary);
|
|
313
|
+
font-size: 0.75rem;
|
|
314
|
+
font-weight: 700;
|
|
315
|
+
padding: 2px 8px;
|
|
316
|
+
border-radius: 10px;
|
|
317
|
+
}
|
|
318
|
+
.column-body {
|
|
319
|
+
display: flex;
|
|
320
|
+
flex-direction: column;
|
|
321
|
+
gap: 8px;
|
|
322
|
+
flex: 1;
|
|
323
|
+
}
|
|
324
|
+
.column-empty {
|
|
325
|
+
color: var(--text-muted);
|
|
326
|
+
text-align: center;
|
|
327
|
+
padding: 24px 0;
|
|
328
|
+
font-size: 0.875rem;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* Cards */
|
|
332
|
+
.card {
|
|
333
|
+
background: var(--bg-card);
|
|
334
|
+
border-radius: 8px;
|
|
335
|
+
border-left: 4px solid var(--status-color);
|
|
336
|
+
padding: 12px;
|
|
337
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
338
|
+
}
|
|
339
|
+
.card--empty {
|
|
340
|
+
opacity: 0.6;
|
|
341
|
+
}
|
|
342
|
+
.card-header {
|
|
343
|
+
display: flex;
|
|
344
|
+
justify-content: space-between;
|
|
345
|
+
align-items: center;
|
|
346
|
+
margin-bottom: 6px;
|
|
347
|
+
}
|
|
348
|
+
.card-date {
|
|
349
|
+
font-size: 0.75rem;
|
|
350
|
+
font-weight: 600;
|
|
351
|
+
color: var(--text-secondary);
|
|
352
|
+
}
|
|
353
|
+
.card-type {
|
|
354
|
+
font-size: 0.875rem;
|
|
355
|
+
}
|
|
356
|
+
.card-title {
|
|
357
|
+
display: block;
|
|
358
|
+
font-size: 0.875rem;
|
|
359
|
+
font-weight: 600;
|
|
360
|
+
color: var(--text-primary);
|
|
361
|
+
margin-bottom: 6px;
|
|
362
|
+
line-height: 1.3;
|
|
363
|
+
}
|
|
364
|
+
.card-title--placeholder {
|
|
365
|
+
color: var(--text-muted);
|
|
366
|
+
font-style: italic;
|
|
367
|
+
font-weight: 400;
|
|
368
|
+
}
|
|
369
|
+
.card-brief {
|
|
370
|
+
font-size: 0.7rem;
|
|
371
|
+
color: var(--text-secondary);
|
|
372
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
373
|
+
margin-bottom: 4px;
|
|
374
|
+
}
|
|
375
|
+
.card-post {
|
|
376
|
+
font-size: 0.7rem;
|
|
377
|
+
color: var(--status-published);
|
|
378
|
+
font-weight: 600;
|
|
379
|
+
margin-bottom: 4px;
|
|
380
|
+
}
|
|
381
|
+
.card-channels {
|
|
382
|
+
display: flex;
|
|
383
|
+
gap: 4px;
|
|
384
|
+
flex-wrap: wrap;
|
|
385
|
+
margin-top: 4px;
|
|
386
|
+
}
|
|
387
|
+
.channel {
|
|
388
|
+
display: inline-block;
|
|
389
|
+
padding: 1px 6px;
|
|
390
|
+
border-radius: 4px;
|
|
391
|
+
font-size: 0.65rem;
|
|
392
|
+
font-weight: 700;
|
|
393
|
+
color: white;
|
|
394
|
+
text-transform: uppercase;
|
|
395
|
+
letter-spacing: 0.03em;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/* Signals Strip */
|
|
399
|
+
.signals-strip {
|
|
400
|
+
background: #fffbeb;
|
|
401
|
+
border: 1px solid #fef3c7;
|
|
402
|
+
border-radius: 10px;
|
|
403
|
+
padding: 16px 20px;
|
|
404
|
+
margin-bottom: 24px;
|
|
405
|
+
}
|
|
406
|
+
.signals-strip h2 {
|
|
407
|
+
font-size: 0.9rem;
|
|
408
|
+
font-weight: 700;
|
|
409
|
+
margin-bottom: 10px;
|
|
410
|
+
color: #92400e;
|
|
411
|
+
}
|
|
412
|
+
.signal-list {
|
|
413
|
+
display: flex;
|
|
414
|
+
flex-direction: column;
|
|
415
|
+
gap: 6px;
|
|
416
|
+
}
|
|
417
|
+
.signal {
|
|
418
|
+
display: flex;
|
|
419
|
+
align-items: center;
|
|
420
|
+
gap: 8px;
|
|
421
|
+
font-size: 0.8rem;
|
|
422
|
+
flex-wrap: wrap;
|
|
423
|
+
}
|
|
424
|
+
.signal-arrow { font-size: 0.7rem; }
|
|
425
|
+
.signal--up .signal-arrow, .signal--up .signal-delta { color: var(--signal-up); }
|
|
426
|
+
.signal--down .signal-arrow, .signal--down .signal-delta { color: var(--signal-down); }
|
|
427
|
+
.signal-delta { font-weight: 700; min-width: 50px; }
|
|
428
|
+
.signal-entity { font-weight: 600; color: var(--text-primary); }
|
|
429
|
+
.signal-sep { color: var(--text-muted); }
|
|
430
|
+
.signal-action { color: var(--text-secondary); font-style: italic; }
|
|
431
|
+
|
|
432
|
+
/* Footer */
|
|
433
|
+
footer {
|
|
434
|
+
text-align: center;
|
|
435
|
+
padding-top: 16px;
|
|
436
|
+
border-top: 1px solid var(--border);
|
|
437
|
+
color: var(--text-muted);
|
|
438
|
+
font-size: 0.75rem;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/* Responsive */
|
|
442
|
+
@media (max-width: 900px) {
|
|
443
|
+
.kanban { grid-template-columns: repeat(3, 1fr); }
|
|
444
|
+
}
|
|
445
|
+
@media (max-width: 600px) {
|
|
446
|
+
.kanban { grid-template-columns: 1fr; }
|
|
447
|
+
body { padding: 12px; }
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/* Print */
|
|
451
|
+
@media print {
|
|
452
|
+
body { padding: 0; max-width: none; }
|
|
453
|
+
.kanban { grid-template-columns: repeat(5, 1fr); gap: 8px; }
|
|
454
|
+
.card { box-shadow: none; border: 1px solid #ddd; }
|
|
455
|
+
.signals-strip { break-inside: avoid; }
|
|
456
|
+
}
|
|
457
|
+
</style>
|
|
458
|
+
</head>
|
|
459
|
+
<body>
|
|
460
|
+
<header>
|
|
461
|
+
<h1>Editorial Dashboard — ${escapeHtml(metrics.siteId)} — ${escapeHtml(monthLabel)}</h1>
|
|
462
|
+
<div class="meta">
|
|
463
|
+
Generato: ${new Date(metrics.generatedAt).toLocaleString('it-IT', { dateStyle: 'medium', timeStyle: 'short' })}${nextDeadlineStr ? ' | ' + nextDeadlineStr : ''}
|
|
464
|
+
</div>
|
|
465
|
+
<div class="progress-bar">
|
|
466
|
+
<div class="progress-fill" style="width: ${metrics.progressPercent}%"></div>
|
|
467
|
+
<span class="progress-label">${metrics.postsPublished}/${metrics.postsTarget} pubblicati (${metrics.progressPercent}%)</span>
|
|
468
|
+
</div>
|
|
469
|
+
<div class="pipeline-counts">
|
|
470
|
+
${pipelineBadges}
|
|
471
|
+
</div>
|
|
472
|
+
</header>
|
|
473
|
+
|
|
474
|
+
<main class="kanban">
|
|
475
|
+
${renderColumn('planned', groups.planned, 'Planned')}
|
|
476
|
+
${renderColumn('draft', groups.draft, 'Draft')}
|
|
477
|
+
${renderColumn('ready', groups.ready, 'Ready')}
|
|
478
|
+
${renderColumn('scheduled', groups.scheduled, 'Scheduled')}
|
|
479
|
+
${renderColumn('published', groups.published, 'Published')}
|
|
480
|
+
</main>
|
|
481
|
+
|
|
482
|
+
${signalsStrip}
|
|
483
|
+
|
|
484
|
+
<footer>
|
|
485
|
+
WordPress Manager v${RENDERER_VERSION} | wp-dashboard skill | Fill rate: ${metrics.fillRate}% | Channels: ${Object.keys(metrics.channelUsage).join(', ') || '—'}
|
|
486
|
+
</footer>
|
|
487
|
+
</body>
|
|
488
|
+
</html>`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ── CLI Entry Point ─────────────────────────────────────────────────
|
|
492
|
+
|
|
493
|
+
function openInBrowser(filepath) {
|
|
494
|
+
const p = platform();
|
|
495
|
+
const cmd = p === 'darwin' ? 'open' :
|
|
496
|
+
p === 'win32' ? 'start' :
|
|
497
|
+
'xdg-open';
|
|
498
|
+
exec(`${cmd} "${filepath}"`, (err) => {
|
|
499
|
+
if (err) console.error(`Note: could not open browser (${cmd}). Open manually: ${filepath}`);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function detectSite(absContentDir) {
|
|
504
|
+
const files = await readdir(absContentDir);
|
|
505
|
+
const configs = files.filter(f => f.endsWith('.config.md'));
|
|
506
|
+
if (configs.length === 0) {
|
|
507
|
+
throw new Error(`No site configs found in ${absContentDir}. Create a {site_id}.config.md first.`);
|
|
508
|
+
}
|
|
509
|
+
if (configs.length === 1) {
|
|
510
|
+
return configs[0].replace('.config.md', '');
|
|
511
|
+
}
|
|
512
|
+
const sites = configs.map(f => f.replace('.config.md', '')).join(', ');
|
|
513
|
+
throw new Error(`Multiple sites found: ${sites}. Specify with --site=<site_id>`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function main() {
|
|
517
|
+
const absContentDir = resolve(PROJECT_ROOT, CONTENT_STATE_DIR);
|
|
518
|
+
const siteId = getArg('site') || await detectSite(absContentDir);
|
|
519
|
+
const month = getArg('month') || null; // null = auto-detect latest
|
|
520
|
+
const outputPath = getArg('output') || join(absContentDir, `.dashboard-${siteId}-${month || 'latest'}.html`);
|
|
521
|
+
const noOpen = hasFlag('no-open');
|
|
522
|
+
|
|
523
|
+
// SCAN
|
|
524
|
+
const rawData = await scanContentState(CONTENT_STATE_DIR, siteId, month);
|
|
525
|
+
|
|
526
|
+
// Resolve output path with actual month
|
|
527
|
+
const actualMonth = rawData.calendar?.period?.slice(0, 7) || 'unknown';
|
|
528
|
+
const finalOutput = getArg('output') || join(absContentDir, `.dashboard-${siteId}-${actualMonth}.html`);
|
|
529
|
+
|
|
530
|
+
// AGGREGATE
|
|
531
|
+
const metrics = aggregateMetrics(rawData, 'kanban');
|
|
532
|
+
|
|
533
|
+
// RENDER
|
|
534
|
+
const html = renderKanbanHTML(rawData, metrics);
|
|
535
|
+
|
|
536
|
+
// WRITE
|
|
537
|
+
await writeFile(finalOutput, html, 'utf8');
|
|
538
|
+
|
|
539
|
+
// REPORT
|
|
540
|
+
const size = Buffer.byteLength(html, 'utf8');
|
|
541
|
+
console.log(`Dashboard generated: ${finalOutput} (${(size / 1024).toFixed(1)} KB)`);
|
|
542
|
+
console.log(`Posts: ${metrics.postsPublished}/${metrics.postsTarget} published | Pipeline: ${metrics.columns.draft} draft, ${metrics.columns.ready} ready, ${metrics.columns.scheduled} scheduled | Signals: ${metrics.signalsCount} anomalies`);
|
|
543
|
+
|
|
544
|
+
// OPEN
|
|
545
|
+
if (!noOpen) {
|
|
546
|
+
openInBrowser(resolve(finalOutput));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
main().catch(err => {
|
|
551
|
+
console.error('Error:', err.message);
|
|
552
|
+
process.exit(1);
|
|
553
|
+
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Spawns wp-rest-bridge server, connects via MCP SDK, tests all registered tools.
|
|
4
4
|
// Usage:
|
|
5
5
|
// node scripts/run-validation.mjs # interactive mode
|
|
6
|
-
// node scripts/run-validation.mjs --site=
|
|
6
|
+
// node scripts/run-validation.mjs --site=mysite # target specific site
|
|
7
7
|
// node scripts/run-validation.mjs --module=gsc # single module
|
|
8
8
|
// node scripts/run-validation.mjs --include-writes # include write tools
|
|
9
9
|
// node scripts/run-validation.mjs --test-writes # CRUD sequence testing
|
|
@@ -380,7 +380,7 @@ const WRITE_SEQUENCES = [
|
|
|
380
380
|
name: 'switch_site', tier: 3, service: 'wordpress_core',
|
|
381
381
|
steps: [
|
|
382
382
|
{ action: 'verify', tool: 'get_active_site', args: {}, extract: 'site_id', as: 'originalSite', extractFn: text => { try { const d = JSON.parse(text); return d.site_id || d.id || text; } catch { return text; } } },
|
|
383
|
-
{ action: 'update', tool: 'switch_site', args: { site_id: '
|
|
383
|
+
{ action: 'update', tool: 'switch_site', args: { site_id: 'othersite' } },
|
|
384
384
|
{ action: 'verify', tool: 'get_active_site', args: {}, expect: 'exists' },
|
|
385
385
|
{ action: 'update', tool: 'switch_site', argsFrom: ctx => ({ site_id: ctx.originalSite }) },
|
|
386
386
|
],
|
|
@@ -9,7 +9,7 @@ const server = new McpServer({
|
|
|
9
9
|
version: '1.1.0',
|
|
10
10
|
});
|
|
11
11
|
// Register multi-site management tools
|
|
12
|
-
server.tool('switch_site', { site_id: z.string().describe('Site ID to switch to (e.g., "
|
|
12
|
+
server.tool('switch_site', { site_id: z.string().describe('Site ID to switch to (e.g., "mysite", "othersite")') }, async (args) => {
|
|
13
13
|
const { switchSite } = await import('./wordpress.js');
|
|
14
14
|
try {
|
|
15
15
|
const newSite = switchSite(args.site_id);
|