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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "seo-intel",
3
- "version": "1.5.38",
3
+ "version": "1.5.39",
4
4
  "description": "Local Ahrefs-style SEO competitor intelligence. Crawl → SQLite → cloud analysis.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -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
- const scoreColor = (s) => s >= 75 ? '#4ade80' : s >= 55 ? '#facc15' : s >= 35 ? '#ff8c00' : '#ef4444';
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 ? '#4ade80' : '#ef4444'}">${delta > 0 ? '+' : ''}${delta}</span><span class="ki-stat-label">Delta</span></div>`;
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
- <h2><span class="icon"><i class="fa-solid fa-robot"></i></span> AI Citability Audit</h2>
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:#4ade80">${tierCounts.excellent}</span><span class="ki-stat-label">Excellent</span></div>
5922
- <div class="ki-stat"><span class="ki-stat-number" style="color:#facc15">${tierCounts.good}</span><span class="ki-stat-label">Good</span></div>
5923
- <div class="ki-stat"><span class="ki-stat-number" style="color:#ff8c00">${tierCounts.needs_work}</span><span class="ki-stat-label">Needs Work</span></div>
5924
- <div class="ki-stat"><span class="ki-stat-number" style="color:#ef4444">${tierCounts.poor}</span><span class="ki-stat-label">Poor</span></div>
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;">