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.
Files changed (50) hide show
  1. package/CHANGELOG.md +43 -6
  2. package/agents/wp-accessibility-auditor.md +1 -1
  3. package/agents/wp-content-strategist.md +2 -2
  4. package/agents/wp-deployment-engineer.md +1 -1
  5. package/agents/wp-distribution-manager.md +1 -1
  6. package/agents/wp-monitoring-agent.md +1 -1
  7. package/agents/wp-performance-optimizer.md +1 -1
  8. package/agents/wp-security-auditor.md +1 -1
  9. package/agents/wp-site-manager.md +3 -3
  10. package/commands/wp-setup.md +2 -2
  11. package/docs/GUIDE.md +26 -26
  12. package/docs/VALIDATION.md +3 -3
  13. package/docs/guides/wp-ecommerce.md +4 -4
  14. package/docs/plans/2026-03-01-tier3-wcop-implementation.md +1 -1
  15. package/docs/plans/2026-03-01-tier4-5-implementation.md +1 -1
  16. package/docs/plans/2026-03-02-content-framework-architecture.md +33 -33
  17. package/docs/plans/2026-03-02-content-framework-strategic-reflections.md +1 -1
  18. package/docs/plans/2026-03-02-content-intelligence-phase2.md +13 -13
  19. package/docs/plans/2026-03-02-content-pipeline-phase1.md +10 -10
  20. package/docs/plans/2026-03-02-dashboard-kanban-design.md +761 -0
  21. package/docs/plans/2026-03-02-dashboard-kanban-implementation.md +598 -0
  22. package/docs/plans/2026-03-02-dashboard-strategy.md +363 -0
  23. package/docs/plans/2026-03-02-editorial-calendar-phase3.md +3 -3
  24. package/docs/validation/results.json +16 -16
  25. package/package.json +8 -4
  26. package/scripts/context-scanner.mjs +446 -0
  27. package/scripts/dashboard-renderer.mjs +553 -0
  28. package/scripts/run-validation.mjs +2 -2
  29. package/servers/wp-rest-bridge/build/server.js +1 -1
  30. package/skills/wp-analytics/references/signals-feed-schema.md +20 -20
  31. package/skills/wp-content/references/content-templates.md +1 -1
  32. package/skills/wp-content/references/seo-optimization.md +8 -8
  33. package/skills/wp-content-attribution/references/roi-calculation.md +1 -1
  34. package/skills/wp-content-attribution/references/utm-tracking-setup.md +5 -5
  35. package/skills/wp-content-generation/references/generation-workflow.md +2 -2
  36. package/skills/wp-content-pipeline/SKILL.md +7 -7
  37. package/skills/wp-content-pipeline/references/content-brief-schema.md +25 -25
  38. package/skills/wp-content-pipeline/references/site-config-schema.md +25 -25
  39. package/skills/wp-content-repurposing/references/auto-transform-pipeline.md +1 -1
  40. package/skills/wp-content-repurposing/references/email-newsletter.md +1 -1
  41. package/skills/wp-content-repurposing/references/platform-specs.md +2 -2
  42. package/skills/wp-content-repurposing/references/transform-templates.md +27 -27
  43. package/skills/wp-dashboard/SKILL.md +121 -0
  44. package/skills/wp-deploy/references/ssh-deploy.md +2 -2
  45. package/skills/wp-editorial-planner/references/editorial-schema.md +8 -8
  46. package/skills/wp-multilang-network/references/content-sync.md +3 -3
  47. package/skills/wp-multilang-network/references/network-architecture.md +1 -1
  48. package/skills/wp-multilang-network/references/seo-international.md +7 -7
  49. package/skills/wp-structured-data/references/schema-types.md +4 -4
  50. 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
37
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
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' ? '&#x1F4C4;' : '&#x1F4DD;';
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 ? '&#x25B2;' : '&#x25BC;';
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>&#x26A1; 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=opencactus # target specific 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: 'bioinagro' } },
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., "opencactus", "bioinagro")') }, async (args) => {
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);