seo-intel 1.5.37 → 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 +35 -0
- package/package.json +1 -1
- package/reports/generate-html.js +139 -7
- package/setup/checks.js +17 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
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
|
+
|
|
23
|
+
## 1.5.38 (2026-05-23)
|
|
24
|
+
|
|
25
|
+
### Fix — LM Studio model count was always 0 (wrong endpoint + wrong parser)
|
|
26
|
+
The wizard showed `localhost:1234 LM Studio · 0 model(s) active` even when LM Studio had models loaded. Two bugs stacked:
|
|
27
|
+
|
|
28
|
+
1. We were hitting `/api/v1/models` (LM Studio's native endpoint), not `/v1/models` (the OpenAI-compatible one).
|
|
29
|
+
2. Even on the native endpoint, the response shape is `{ models: [{ key, loaded_instances }] }` — we were parsing it as `{ data: [{ id }] }` (OpenAI shape), so even when the call succeeded, the filter zeroed everything out.
|
|
30
|
+
|
|
31
|
+
Fix in `setup/checks.js`:
|
|
32
|
+
- Try `/v1/models` first (standard OpenAI-compat, listed under LM Studio's "OpenAI-compatible" Developer tab).
|
|
33
|
+
- Fall back to `/api/v1/models` if the OpenAI route is disabled in LM Studio settings.
|
|
34
|
+
- Parse both shapes: `data.data` (OpenAI) and `data.models` (LM Studio native). Identifier extracted via first-of `id | key | model | name`.
|
|
35
|
+
|
|
36
|
+
Verified against the user's live LM Studio (3 models surfaced correctly — Gemma 4 E2B, an uncensored variant, and an embedding model). Smoke 10/10.
|
|
37
|
+
|
|
3
38
|
## 1.5.37 (2026-05-23)
|
|
4
39
|
|
|
5
40
|
### Notify — native macOS / Linux notifications for pending problems
|
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;">
|
package/setup/checks.js
CHANGED
|
@@ -80,15 +80,29 @@ export async function checkOllamaRemote(host) {
|
|
|
80
80
|
export async function checkLmStudio(customUrl) {
|
|
81
81
|
const host = customUrl || process.env.LMSTUDIO_URL || 'http://localhost:1234';
|
|
82
82
|
|
|
83
|
+
// LM Studio exposes TWO model endpoints with DIFFERENT response shapes:
|
|
84
|
+
// /v1/models OpenAI-compatible → { data: [{ id }] }
|
|
85
|
+
// /api/v1/models LM Studio native → { models: [{ key, loaded_instances }] }
|
|
86
|
+
// Try OpenAI-compat first (standard, smaller payload, listed under "OpenAI-
|
|
87
|
+
// compatible" tab in LM Studio Developer panel). Fall back to the native
|
|
88
|
+
// endpoint if the OpenAI one isn't enabled. Parse both shapes.
|
|
83
89
|
try {
|
|
84
90
|
const controller = new AbortController();
|
|
85
91
|
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
86
|
-
|
|
92
|
+
let res = await fetch(`${host}/v1/models`, { signal: controller.signal });
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
// OpenAI-compat path off — try LM Studio native
|
|
95
|
+
res = await fetch(`${host}/api/v1/models`, { signal: controller.signal });
|
|
96
|
+
}
|
|
87
97
|
clearTimeout(timeout);
|
|
88
98
|
|
|
89
99
|
if (!res.ok) return { reachable: false, models: [], host };
|
|
90
|
-
const data = await res.json().catch(() => ({
|
|
91
|
-
|
|
100
|
+
const data = await res.json().catch(() => ({}));
|
|
101
|
+
// Support both shapes: OpenAI `data[]` and LM Studio native `models[]`.
|
|
102
|
+
const list = Array.isArray(data.data) ? data.data
|
|
103
|
+
: Array.isArray(data.models) ? data.models
|
|
104
|
+
: [];
|
|
105
|
+
const models = list.map(m => m.id || m.key || m.model || m.name).filter(Boolean);
|
|
92
106
|
return { reachable: true, models, host };
|
|
93
107
|
} catch {
|
|
94
108
|
return { reachable: false, models: [], host };
|