seo-intel 1.5.38 → 1.5.39
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 +20 -0
- package/package.json +1 -1
- package/reports/generate-html.js +139 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.39 (2026-05-27)
|
|
4
|
+
|
|
5
|
+
### Dashboard — Problems card as the landing surface (Ahrefs-style "what's broken")
|
|
6
|
+
The biggest UX shift since MCP shipped. Opening the dashboard now greets the user with a unified Problems card at the very top of every project panel — same data backing the `list_problems` MCP tool, finally surfaced for humans too.
|
|
7
|
+
|
|
8
|
+
- **New `buildProblemsCard()`** renders Ahrefs-style: big counters (Critical / Warn / Info) using the v1.5.33 visual-brief `.vb-score-big` numerals, top 12 issues table with severity dots, category, fix-difficulty stars (1–5), and an expandable "Fix" disclosure per row showing the agent-friendly `fix_template`.
|
|
9
|
+
- Single source of truth: same `getProblems()` library function that powers `list_problems` MCP tool. Dashboard and AI agents see identical data; closing one closes both.
|
|
10
|
+
- "Showing top 12 of 190 — query the rest via MCP: `list_problems("carbium", limit=190)`" — makes the agent escape hatch visible from the dashboard itself.
|
|
11
|
+
- Empty state: "all clear" message when no problems pending.
|
|
12
|
+
|
|
13
|
+
### Dashboard — AI Citability card polished to brief spec
|
|
14
|
+
- Inline colors (`#4ade80`, `#facc15`, `#ff8c00`, `#ef4444`) swapped to brief signal tokens (`var(--signal-good)`, `var(--signal-warn)`, `var(--signal-bad)`). One color system, no drift.
|
|
15
|
+
- Score gradient aligned with `lib/problems.js` severity buckets: ≥60 good, 35–59 warn, <35 bad.
|
|
16
|
+
- New `.vb-pill` header chip with the "weakest signal" caption ("weakest: answer density") so the user sees the headline takeaway at a glance.
|
|
17
|
+
- Existing signal bars + page-score table preserved — minimal disruption, maximum polish.
|
|
18
|
+
|
|
19
|
+
**Verified live against carbium / risunouto / ukkometa:** 36 severity dots rendered across three Problems cards, 3 MCP-hint references, citability cards on each pro panel, no existing functionality broken. Smoke 10/10. HTML size unchanged (2.4MB).
|
|
20
|
+
|
|
21
|
+
Next: setup-wizard cron-entry installer (v1.5.40), then per-page polish for Site Watch timeline / Competitive Radar / Action Export modal.
|
|
22
|
+
|
|
3
23
|
## 1.5.38 (2026-05-23)
|
|
4
24
|
|
|
5
25
|
### Fix — LM Studio model count was always 0 (wrong endpoint + wrong parser)
|
package/package.json
CHANGED
package/reports/generate-html.js
CHANGED
|
@@ -21,6 +21,7 @@ import { isPro } from '../lib/license.js';
|
|
|
21
21
|
import { getActiveInsights } from '../db/db.js';
|
|
22
22
|
import { getCitabilityScores } from '../analyses/aeo/index.js';
|
|
23
23
|
import { getWatchData } from '../analyses/watch/index.js';
|
|
24
|
+
import { getProblems, getProblemCounts } from '../lib/problems.js';
|
|
24
25
|
|
|
25
26
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
27
|
|
|
@@ -127,6 +128,14 @@ export function gatherProjectData(db, project, config) {
|
|
|
127
128
|
let citabilityData = null;
|
|
128
129
|
try { citabilityData = getCitabilityScores(db, project); } catch { /* table may not exist yet */ }
|
|
129
130
|
|
|
131
|
+
// Problems (v1.5.39) — unified Ahrefs-style "what's broken" feed
|
|
132
|
+
let problems = [];
|
|
133
|
+
let problemCounts = null;
|
|
134
|
+
try {
|
|
135
|
+
problems = getProblems(db, project, { includePaid: isPro(), limit: 200 });
|
|
136
|
+
problemCounts = getProblemCounts(db, project, { includePaid: isPro() });
|
|
137
|
+
} catch { /* fresh DB / migration not run yet — silent */ }
|
|
138
|
+
|
|
130
139
|
// Site Watch data
|
|
131
140
|
let watchData = null;
|
|
132
141
|
try { watchData = getWatchData(db, project); } catch { /* tables may not exist yet */ }
|
|
@@ -155,6 +164,7 @@ export function gatherProjectData(db, project, config) {
|
|
|
155
164
|
gravityMap, contentTerrain, keywordVenn, performanceBubbles,
|
|
156
165
|
headingFlow, territoryTreemap, topicClusters, linkDna, linkRadarPulse,
|
|
157
166
|
keywordsReport, extractionStatus, gscData, domainArch, gscInsights, citabilityData, watchData,
|
|
167
|
+
problems, problemCounts,
|
|
158
168
|
};
|
|
159
169
|
|
|
160
170
|
// Rollback the owned→target merge so the actual DB is unchanged
|
|
@@ -227,6 +237,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
227
237
|
gravityMap, contentTerrain, keywordVenn, performanceBubbles,
|
|
228
238
|
headingFlow, territoryTreemap, topicClusters, linkDna, linkRadarPulse,
|
|
229
239
|
keywordsReport, extractionStatus, gscData, domainArch, gscInsights, citabilityData, watchData,
|
|
240
|
+
problems = [], problemCounts = null,
|
|
230
241
|
} = data;
|
|
231
242
|
|
|
232
243
|
const totalPages = domains.reduce((sum, d) => sum + d.page_count, 0);
|
|
@@ -2979,6 +2990,9 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2979
2990
|
|
|
2980
2991
|
<div class="dashboard">
|
|
2981
2992
|
|
|
2993
|
+
<!-- ═══ PROBLEMS (v1.5.39 — Ahrefs-style landing card) ═══ -->
|
|
2994
|
+
${buildProblemsCard(problems, problemCounts, escapeHtml, project)}
|
|
2995
|
+
|
|
2982
2996
|
<!-- ═══ GSC PERFORMANCE TREND ═══ -->
|
|
2983
2997
|
${gscData ? (() => {
|
|
2984
2998
|
const s = gscData.summary;
|
|
@@ -5849,6 +5863,115 @@ function buildMultiHtmlTemplate(allProjectData) {
|
|
|
5849
5863
|
|
|
5850
5864
|
// ─── AEO Card Builder ────────────────────────────────────────────────────────
|
|
5851
5865
|
|
|
5866
|
+
// ─── Problems card (v1.5.39) — Ahrefs-style unified "what's broken" feed ──
|
|
5867
|
+
// Uses lib/problems.js getProblems() — single source of truth shared with MCP.
|
|
5868
|
+
function buildProblemsCard(problems, counts, escapeHtml, project) {
|
|
5869
|
+
if (!counts || counts.total === 0) {
|
|
5870
|
+
return `
|
|
5871
|
+
<div class="card full-width vb-card" id="problems-card" style="margin-bottom: 24px;">
|
|
5872
|
+
<div style="display:flex; align-items:center; gap:14px; margin-bottom:14px;">
|
|
5873
|
+
<span class="vb-pill">Problems</span>
|
|
5874
|
+
<span class="vb-label-caps">all clear</span>
|
|
5875
|
+
</div>
|
|
5876
|
+
<div style="color: var(--text-muted); font-size: 0.85rem;">
|
|
5877
|
+
<i class="fa-solid fa-check" style="color: var(--signal-good); margin-right: 6px;"></i>
|
|
5878
|
+
No pending issues detected for this project. Run a fresh crawl to refresh detection.
|
|
5879
|
+
</div>
|
|
5880
|
+
</div>`;
|
|
5881
|
+
}
|
|
5882
|
+
|
|
5883
|
+
const sev = (s) => s === 'critical' ? 'crit' : s === 'warn' ? 'warn' : 'info';
|
|
5884
|
+
const sevColor = (s) => s === 'critical' ? 'var(--signal-bad)' : s === 'warn' ? 'var(--signal-warn)' : 'var(--signal-good)';
|
|
5885
|
+
const sevLabel = (s) => s.toUpperCase();
|
|
5886
|
+
const diffStars = (n) => '●'.repeat(Math.max(1, Math.min(5, n))) + '○'.repeat(5 - Math.max(1, Math.min(5, n)));
|
|
5887
|
+
|
|
5888
|
+
const top = problems.slice(0, 12);
|
|
5889
|
+
const remaining = problems.length - top.length;
|
|
5890
|
+
|
|
5891
|
+
const rows = top.map(p => {
|
|
5892
|
+
const sevClass = sev(p.severity);
|
|
5893
|
+
const sevCol = sevColor(p.severity);
|
|
5894
|
+
const fix = (p.fix_template || '').slice(0, 200);
|
|
5895
|
+
const urls = (p.affected_urls || []).slice(0, 3).map(u => {
|
|
5896
|
+
try { return new URL(u).pathname || u; } catch { return u.slice(0, 50); }
|
|
5897
|
+
});
|
|
5898
|
+
return `
|
|
5899
|
+
<tr data-problem-id="${escapeHtml(p.id)}">
|
|
5900
|
+
<td style="vertical-align:top; padding-top: 14px;">
|
|
5901
|
+
<span class="vb-severity-dot ${sevClass}"></span>
|
|
5902
|
+
</td>
|
|
5903
|
+
<td style="vertical-align:top;">
|
|
5904
|
+
<div style="font-family: var(--font-display); font-weight: 700; font-size: 0.92rem; color: var(--text-primary); line-height: 1.3;">${escapeHtml(p.title)}</div>
|
|
5905
|
+
<div style="font-size: 0.72rem; color: var(--text-muted); margin-top: 4px; line-height: 1.5;">${escapeHtml(p.description)}</div>
|
|
5906
|
+
${urls.length ? `<div class="vb-num-tabular" style="font-size: 0.68rem; color: var(--text-subtle); margin-top: 4px;">${urls.map(u => `<code style="background:transparent;">${escapeHtml(u)}</code>`).join(' · ')}</div>` : ''}
|
|
5907
|
+
</td>
|
|
5908
|
+
<td style="vertical-align:top; padding-top: 14px;">
|
|
5909
|
+
<span class="vb-label-caps" style="color: ${sevCol};">${sevLabel(p.severity)}</span>
|
|
5910
|
+
<div style="font-size: 0.65rem; color: var(--text-muted); margin-top: 2px;">${escapeHtml(p.category)}</div>
|
|
5911
|
+
</td>
|
|
5912
|
+
<td style="vertical-align:top; padding-top: 14px;" title="Fix difficulty: ${p.fix_difficulty}/5">
|
|
5913
|
+
<span style="color: var(--intel-blue); font-size: 0.7rem; letter-spacing: 1px;">${diffStars(p.fix_difficulty)}</span>
|
|
5914
|
+
</td>
|
|
5915
|
+
<td style="vertical-align:top; padding-top: 12px;">
|
|
5916
|
+
<details style="font-size: 0.7rem;">
|
|
5917
|
+
<summary style="cursor:pointer; color: var(--intel-blue); font-weight: 600; user-select: none;">Fix</summary>
|
|
5918
|
+
<div style="margin-top: 8px; padding: 10px; background: var(--surface-off); border-left: 2px solid var(--intel-blue); color: var(--text-secondary); line-height: 1.5; font-size: 0.72rem;">${escapeHtml(fix)}${(p.fix_template || '').length > 200 ? '…' : ''}</div>
|
|
5919
|
+
</details>
|
|
5920
|
+
</td>
|
|
5921
|
+
</tr>`;
|
|
5922
|
+
}).join('');
|
|
5923
|
+
|
|
5924
|
+
return `
|
|
5925
|
+
<div class="card full-width vb-card" id="problems-card" style="margin-bottom: 24px;">
|
|
5926
|
+
<div style="display:flex; align-items:center; gap:14px; margin-bottom: 18px; flex-wrap: wrap;">
|
|
5927
|
+
<span class="vb-pill">Problems</span>
|
|
5928
|
+
<span style="font-family: var(--font-display); font-weight: 700; font-size: 1.4rem; color: var(--text-primary); letter-spacing: -0.02em;">${counts.total} issue${counts.total === 1 ? '' : 's'} pending</span>
|
|
5929
|
+
<span class="vb-label-caps" style="margin-left:auto; color: var(--text-subtle);">ahrefs-style site health</span>
|
|
5930
|
+
</div>
|
|
5931
|
+
|
|
5932
|
+
<div style="display:flex; gap: 32px; margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid var(--surface-border); flex-wrap: wrap;">
|
|
5933
|
+
<div>
|
|
5934
|
+
<div class="vb-score-big ${counts.critical > 0 ? 'bad' : 'good'}">${counts.critical}</div>
|
|
5935
|
+
<div class="vb-label-caps" style="margin-top: 6px;">critical</div>
|
|
5936
|
+
</div>
|
|
5937
|
+
<div>
|
|
5938
|
+
<div class="vb-score-big ${counts.warn > 9 ? 'warn' : 'good'}">${counts.warn}</div>
|
|
5939
|
+
<div class="vb-label-caps" style="margin-top: 6px;">warn</div>
|
|
5940
|
+
</div>
|
|
5941
|
+
<div>
|
|
5942
|
+
<div class="vb-score-big good">${counts.info}</div>
|
|
5943
|
+
<div class="vb-label-caps" style="margin-top: 6px;">info</div>
|
|
5944
|
+
</div>
|
|
5945
|
+
<div style="margin-left:auto; max-width: 420px; align-self: center;">
|
|
5946
|
+
<div style="font-size: 0.78rem; color: var(--text-secondary); line-height: 1.6;">
|
|
5947
|
+
Each problem ships with a <strong style="color: var(--intel-blue);">fix template</strong> and <strong style="color: var(--intel-blue);">verification</strong> step — copy the Fix into an AI agent (via MCP <code style="color: var(--text-primary); background: var(--surface-off); padding: 1px 5px; border-radius: 3px; font-size: 0.7rem;">list_problems</code>) or apply manually.
|
|
5948
|
+
</div>
|
|
5949
|
+
</div>
|
|
5950
|
+
</div>
|
|
5951
|
+
|
|
5952
|
+
<table class="analysis-table" style="margin: 0;">
|
|
5953
|
+
<thead>
|
|
5954
|
+
<tr>
|
|
5955
|
+
<th style="width: 28px;"></th>
|
|
5956
|
+
<th>Issue</th>
|
|
5957
|
+
<th style="width: 90px;">Severity</th>
|
|
5958
|
+
<th style="width: 90px;">Difficulty</th>
|
|
5959
|
+
<th style="width: 100px;">Action</th>
|
|
5960
|
+
</tr>
|
|
5961
|
+
</thead>
|
|
5962
|
+
<tbody>${rows}</tbody>
|
|
5963
|
+
</table>
|
|
5964
|
+
|
|
5965
|
+
${remaining > 0 ? `
|
|
5966
|
+
<div style="margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--surface-border); text-align: center;">
|
|
5967
|
+
<span style="color: var(--text-muted); font-size: 0.78rem;">
|
|
5968
|
+
Showing top ${top.length} of ${counts.total} — query the rest via MCP:
|
|
5969
|
+
<code style="color: var(--intel-blue); background: var(--surface-off); padding: 2px 6px; border-radius: 3px; margin-left: 4px;">list_problems("${escapeHtml(project)}", limit=${counts.total})</code>
|
|
5970
|
+
</span>
|
|
5971
|
+
</div>` : ''}
|
|
5972
|
+
</div>`;
|
|
5973
|
+
}
|
|
5974
|
+
|
|
5852
5975
|
function buildAeoCard(citabilityData, escapeHtml, project) {
|
|
5853
5976
|
const targetScores = citabilityData.filter(s => s.role === 'target' || s.role === 'owned');
|
|
5854
5977
|
const compScores = citabilityData.filter(s => s.role === 'competitor');
|
|
@@ -5867,7 +5990,8 @@ function buildAeoCard(citabilityData, escapeHtml, project) {
|
|
|
5867
5990
|
avg: Math.round(targetScores.reduce((a, s) => a + (s[sig] || 0), 0) / targetScores.length),
|
|
5868
5991
|
}));
|
|
5869
5992
|
|
|
5870
|
-
|
|
5993
|
+
// Brief gradient: 0–34 bad, 35–59 warn, 60+ good (matches lib/problems.js severity)
|
|
5994
|
+
const scoreColor = (s) => s >= 60 ? 'var(--signal-good)' : s >= 35 ? 'var(--signal-warn)' : 'var(--signal-bad)';
|
|
5871
5995
|
|
|
5872
5996
|
// Page rows (worst first, limit 25)
|
|
5873
5997
|
const pageRows = targetScores
|
|
@@ -5908,20 +6032,28 @@ function buildAeoCard(citabilityData, escapeHtml, project) {
|
|
|
5908
6032
|
compStatHtml += `<div class="ki-stat"><span class="ki-stat-number" style="color:${scoreColor(avgComp)}">${avgComp}</span><span class="ki-stat-label">Competitor Avg</span></div>`;
|
|
5909
6033
|
}
|
|
5910
6034
|
if (delta !== null) {
|
|
5911
|
-
compStatHtml += `<div class="ki-stat"><span class="ki-stat-number" style="color:${delta >= 0 ? '
|
|
6035
|
+
compStatHtml += `<div class="ki-stat"><span class="ki-stat-number" style="color:${delta >= 0 ? 'var(--signal-good)' : 'var(--signal-bad)'}">${delta > 0 ? '+' : ''}${delta}</span><span class="ki-stat-label">Delta</span></div>`;
|
|
5912
6036
|
}
|
|
5913
6037
|
|
|
6038
|
+
// Visual-brief pill header: pick the worst signal for the "weakest area" caption
|
|
6039
|
+
const weakestSignal = [...signalAvgs].sort((a, b) => a.avg - b.avg)[0];
|
|
6040
|
+
|
|
5914
6041
|
return `
|
|
5915
6042
|
<div class="card full-width" id="aeo-citability">
|
|
5916
6043
|
${cardExportHtml('aeo', project)}
|
|
5917
|
-
<
|
|
6044
|
+
<div style="display:flex; align-items:center; gap: 14px; margin-bottom: 18px; flex-wrap: wrap;">
|
|
6045
|
+
<span class="vb-pill">AI Citability</span>
|
|
6046
|
+
<span style="font-family: var(--font-display); font-weight: 700; font-size: 1.4rem; color: var(--text-primary); letter-spacing: -0.02em;">${targetScores.length} pages scored</span>
|
|
6047
|
+
${weakestSignal ? `<span class="vb-label-caps" style="margin-left:auto; color: var(--text-subtle);">weakest: ${weakestSignal.label}</span>` : ''}
|
|
6048
|
+
</div>
|
|
6049
|
+
<h2 style="display:none;"><span class="icon"><i class="fa-solid fa-robot"></i></span> AI Citability Audit</h2>
|
|
5918
6050
|
<div class="ki-stat-bar">
|
|
5919
6051
|
<div class="ki-stat"><span class="ki-stat-number" style="color:${scoreColor(avgTarget)}">${avgTarget}</span><span class="ki-stat-label">Target Avg</span></div>
|
|
5920
6052
|
${compStatHtml}
|
|
5921
|
-
<div class="ki-stat"><span class="ki-stat-number" style="color
|
|
5922
|
-
<div class="ki-stat"><span class="ki-stat-number" style="color
|
|
5923
|
-
<div class="ki-stat"><span class="ki-stat-number" style="color
|
|
5924
|
-
<div class="ki-stat"><span class="ki-stat-number" style="color
|
|
6053
|
+
<div class="ki-stat"><span class="ki-stat-number" style="color:var(--signal-good)">${tierCounts.excellent}</span><span class="ki-stat-label">Excellent</span></div>
|
|
6054
|
+
<div class="ki-stat"><span class="ki-stat-number" style="color:var(--signal-warn)">${tierCounts.good}</span><span class="ki-stat-label">Good</span></div>
|
|
6055
|
+
<div class="ki-stat"><span class="ki-stat-number" style="color:var(--signal-bad);opacity:0.85">${tierCounts.needs_work}</span><span class="ki-stat-label">Needs Work</span></div>
|
|
6056
|
+
<div class="ki-stat"><span class="ki-stat-number" style="color:var(--signal-bad)">${tierCounts.poor}</span><span class="ki-stat-label">Poor</span></div>
|
|
5925
6057
|
</div>
|
|
5926
6058
|
|
|
5927
6059
|
<div style="display:flex;gap:2rem;margin:1.5rem 0;flex-wrap:wrap;">
|