diff --git a/packages/api/src/routes/finder.ts b/packages/api/src/routes/finder.ts index 10d2762..ca5a0a7 100644 --- a/packages/api/src/routes/finder.ts +++ b/packages/api/src/routes/finder.ts @@ -23,7 +23,11 @@ finderRouter.get("/", async (req, res) => { 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( `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 @@ -31,20 +35,38 @@ finderRouter.get("/", async (req, res) => { JOIN vendors v ON sw.vendor_id = v.id WHERE 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 CASE WHEN sw.model ILIKE $1 THEN 0 WHEN sw.model ILIKE $1 || '%' THEN 1 - ELSE 2 END + WHEN REPLACE(REPLACE(sw.model, '-', ''), ' ', '') ILIKE $2 || '%' THEN 2 + ELSE 3 END LIMIT 5`, - [switchQuery] + [q, qNorm, q.replace(/[^\w\s]/g, " ")] ); if (switchResult.rows.length === 0) { - return res.status(404).json({ - error: "Switch not found", - suggestion: "Try a partial model name like 'N9K-C93180' or 'QFX5120'" - }); + // Try one more time with individual tokens + const tokens = q.split(/[\s\-_]+/).filter((t) => t.length >= 3); + 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]; diff --git a/packages/dashboard/index.html b/packages/dashboard/index.html index 5dd8235..7fcc92a 100644 --- a/packages/dashboard/index.html +++ b/packages/dashboard/index.html @@ -1083,9 +1083,19 @@ function el(id) { return document.getElementById(id); } function api(path) { return fetch(API + path).then(function(r) { var ct = r.headers.get('content-type') || ''; - if (!r.ok) throw new Error('HTTP ' + r.status); - if (ct.indexOf('application/json') === -1) throw new Error('Server returned non-JSON response'); - return r.json(); + if (ct.indexOf('application/json') === -1) { + if (!r.ok) throw new Error('HTTP ' + r.status); + 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; } catch(e) { - results.innerHTML = '
Error: ' + e.message + '
'; + var body = e.body || {}; + var msg = body.error || e.message || 'Unknown error'; + var suggestion = body.suggestion || ''; + results.innerHTML = '
' + + '
Switch not found
' + + '
' + esc(msg) + '
' + + (suggestion ? '
💡 ' + esc(suggestion) + '
' : '') + + '
'; } }