fix: Finder 404 shows helpful message + fuzzy switch name matching

- api() helper now parses JSON body on non-2xx responses so error.suggestion
  is available in catch blocks
- runFinder() catch shows 'Switch not found' + suggestion instead of 'Error: HTTP 404'
- finder.ts: normalized search (removes hyphens/spaces) + token-based fallback
  so 'sg350-28' → 'SG350-28', 'N9K-C93180' → Nexus 93180, etc.
This commit is contained in:
Rene Fichtmueller 2026-04-01 22:17:07 +02:00
parent dad4750a86
commit 4b1734379a
2 changed files with 51 additions and 12 deletions

View File

@ -23,7 +23,11 @@ finderRouter.get("/", async (req, res) => {
return res.status(400).json({ error: "Parameter 'switch' is required" }); return res.status(400).json({ error: "Parameter 'switch' is required" });
} }
// Step 1: Find the switch // Step 1: Find the switch — try multiple search strategies
const q = String(switchQuery);
// Normalized form: remove hyphens/spaces for fuzzy match (sg350-28 → sg35028)
const qNorm = q.replace(/[\s\-_]/g, "");
const switchResult = await pool.query( const switchResult = await pool.query(
`SELECT sw.id, sw.model, sw.series, sw.ports_config, sw.max_speed_gbps, `SELECT sw.id, sw.model, sw.series, sw.ports_config, sw.max_speed_gbps,
v.name AS vendor_name, sw.image_url, sw.datasheet_r2_key v.name AS vendor_name, sw.image_url, sw.datasheet_r2_key
@ -31,20 +35,38 @@ finderRouter.get("/", async (req, res) => {
JOIN vendors v ON sw.vendor_id = v.id JOIN vendors v ON sw.vendor_id = v.id
WHERE sw.model ILIKE $1 WHERE sw.model ILIKE $1
OR sw.model ILIKE '%' || $1 || '%' OR sw.model ILIKE '%' || $1 || '%'
OR sw.search_vector @@ plainto_tsquery('english', $1) OR REPLACE(REPLACE(sw.model, '-', ''), ' ', '') ILIKE '%' || $2 || '%'
OR sw.search_vector @@ plainto_tsquery('english', $3)
ORDER BY ORDER BY
CASE WHEN sw.model ILIKE $1 THEN 0 CASE WHEN sw.model ILIKE $1 THEN 0
WHEN sw.model ILIKE $1 || '%' THEN 1 WHEN sw.model ILIKE $1 || '%' THEN 1
ELSE 2 END WHEN REPLACE(REPLACE(sw.model, '-', ''), ' ', '') ILIKE $2 || '%' THEN 2
ELSE 3 END
LIMIT 5`, LIMIT 5`,
[switchQuery] [q, qNorm, q.replace(/[^\w\s]/g, " ")]
); );
if (switchResult.rows.length === 0) { if (switchResult.rows.length === 0) {
return res.status(404).json({ // Try one more time with individual tokens
error: "Switch not found", const tokens = q.split(/[\s\-_]+/).filter((t) => t.length >= 3);
suggestion: "Try a partial model name like 'N9K-C93180' or 'QFX5120'" let fallbackResult = { rows: [] as any[] };
}); if (tokens.length > 0) {
fallbackResult = await pool.query(
`SELECT sw.id, sw.model, sw.series, sw.ports_config, sw.max_speed_gbps,
v.name AS vendor_name, sw.image_url, sw.datasheet_r2_key
FROM switches sw JOIN vendors v ON sw.vendor_id = v.id
WHERE ${tokens.map((_, i) => `sw.model ILIKE '%' || $${i + 1} || '%'`).join(" AND ")}
LIMIT 5`,
tokens
);
}
if (fallbackResult.rows.length === 0) {
return res.status(404).json({
error: `Switch "${q}" not found in the database`,
suggestion: `Try a partial model name, e.g. "${tokens[0] || q.substring(0, 6)}" — or check spelling. Available: Cisco Nexus, Arista, Juniper QFX, Edgecore, Mellanox.`
});
}
switchResult.rows.push(...fallbackResult.rows);
} }
const sw = switchResult.rows[0]; const sw = switchResult.rows[0];

View File

@ -1083,9 +1083,19 @@ function el(id) { return document.getElementById(id); }
function api(path) { function api(path) {
return fetch(API + path).then(function(r) { return fetch(API + path).then(function(r) {
var ct = r.headers.get('content-type') || ''; var ct = r.headers.get('content-type') || '';
if (!r.ok) throw new Error('HTTP ' + r.status); if (ct.indexOf('application/json') === -1) {
if (ct.indexOf('application/json') === -1) throw new Error('Server returned non-JSON response'); if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json(); throw new Error('Server returned non-JSON response');
}
// Parse JSON even on error so callers can read error.message / error.suggestion
return r.json().then(function(body) {
if (!r.ok) {
var err = new Error(body.error || ('HTTP ' + r.status));
err.body = body;
throw err;
}
return body;
});
}); });
} }
@ -2836,7 +2846,14 @@ async function runFinder() {
tcvrHtml; tcvrHtml;
} catch(e) { } catch(e) {
results.innerHTML = '<div class="card" style="border-left:3px solid #c1121f">Error: ' + e.message + '</div>'; var body = e.body || {};
var msg = body.error || e.message || 'Unknown error';
var suggestion = body.suggestion || '';
results.innerHTML = '<div class="card" style="border-left:3px solid #c1121f;padding:1rem">'
+ '<div style="font-weight:700;margin-bottom:0.3rem">Switch not found</div>'
+ '<div style="color:var(--text-dim);font-size:0.85rem">' + esc(msg) + '</div>'
+ (suggestion ? '<div style="color:var(--text-dim);font-size:0.8rem;margin-top:0.4rem">💡 ' + esc(suggestion) + '</div>' : '')
+ '</div>';
} }
} }