cybermax / recallroll /app.js
CyberMax-tools's picture
Deploy recallroll to the hub
6570b19 verified
Raw History Blame Contribute Delete
15.8 kB
/* Recallroll page. Talks to NHTSA directly from the browser (CORS-enabled public APIs). © 2026 CyberMax */
(function () {
'use strict';
var VPIC = 'https://vpic.nhtsa.dot.gov/api/vehicles/';
var RECALLS = 'https://api.nhtsa.gov/recalls/recallsByVehicle';
var $ = function (id) { return document.getElementById(id); };
var esc = function (s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]; }); };
var cache = {};
function getJson(url, opts) {
if (!opts && cache[url]) return cache[url];
var p = fetch(url, opts).then(function (r) { if (!r.ok) throw new Error('NHTSA returned ' + r.status); return r.json(); });
if (!opts) { cache[url] = p; p.catch(function () { delete cache[url]; }); }
return p;
}
// ---------- tabs ----------
var tabs = { one: 'p-one', bulk: 'p-bulk', 'new': 'p-new' };
function show(t) {
Object.keys(tabs).forEach(function (k) { $('tab-' + k).classList.toggle('on', k === t); $(tabs[k]).hidden = k !== t; });
if (t === 'new') loadSnapshot();
try { history.replaceState(null, '', '#' + t); } catch (e) { /* file:// */ }
}
Object.keys(tabs).forEach(function (k) { $('tab-' + k).onclick = function () { show(k); }; });
// ---------- recalls for make/model/year ----------
// NHTSA's Recalls API only matches its own model spellings ("MUSTANG MACH E", "F-350 SD"); models/<year>.json
// (built daily from NHTSA's recall file) maps each make to those names. It answers 400 when nothing matches.
function modelIndex(year) { return getJson('models/' + encodeURIComponent(year) + '.json').catch(function () { return {}; }); }
function recallsFor(make, model, year) {
var q = function (m) {
return fetch(RECALLS + '?make=' + encodeURIComponent(make) + '&model=' + encodeURIComponent(m) + '&modelYear=' + encodeURIComponent(year))
.then(function (r) { if (r.status === 400) return { results: [] }; if (!r.ok) throw new Error('NHTSA returned ' + r.status); return r.json(); })
.then(function (d) { return d.results || []; });
};
return modelIndex(year).then(function (idx) {
var names = RR.matchModels(model, idx[String(make).toUpperCase()] || []);
if (!names.length) names = [model]; // older campaigns (before 2010) aren't in the index: try vPIC's name
return Promise.all(names.map(q)).then(function (all) { return { rows: RR.normalizeRecalls([].concat.apply([], all)), models: names }; });
});
}
function recallCard(r) {
var flags = (r.doNotDrive ? '<span class="pill red">Do Not Drive</span> ' : '') + (r.parkOutside ? '<span class="pill amber">Park Outside</span> ' : '') + (r.ota ? '<span class="pill">Over-the-air fix</span> ' : '');
return '<article class="card' + (r.doNotDrive || r.parkOutside ? ' hot' : '') + '"><div class="card-h">' + flags + '<b>' + esc(r.component) + '</b></div>' +
'<div class="muted small">Campaign <a href="https://www.nhtsa.gov/recalls?nhtsaId=' + esc(r.campaign) + '" target="_blank" rel="noopener">' + esc(r.campaign) + '</a> · reported ' + esc(r.date) + (r.manufacturer ? ' · ' + esc(r.manufacturer) : '') + '</div>' +
(r.summary ? '<p>' + esc(r.summary) + '</p>' : '') +
(r.consequence ? '<p><b>Risk:</b> ' + esc(r.consequence) + '</p>' : '') +
(r.remedy ? '<p><b>Fix:</b> ' + esc(r.remedy) + '</p>' : '') + '</article>';
}
function recallBlock(v, res) {
var n = res.rows.length, hot = res.rows.filter(function (r) { return r.doNotDrive || r.parkOutside; }).length;
var head = '<h2>' + (n ? n + ' recall' + (n > 1 ? 's' : '') : 'No recalls') + ' listed for the ' + esc([v.year, v.make, res.models.join(' / ')].join(' ')) + '</h2>';
if (hot) head += '<p class="msg bad"><b>' + hot + ' with a Do Not Drive or Park Outside advisory.</b> Check your exact VIN at <a href="https://www.nhtsa.gov/recalls" target="_blank" rel="noopener">nhtsa.gov/recalls</a> or call a dealer now.</p>';
head += '<p class="muted small">Recalls apply to the model year; your exact vehicle may not be included, or may already be fixed. Confirm your VIN at <a href="https://www.nhtsa.gov/recalls" target="_blank" rel="noopener">nhtsa.gov/recalls</a>. Recall repairs are free at authorized dealers.</p>';
var sorted = res.rows.slice().sort(function (a, b) { return (b.doNotDrive || b.parkOutside) - (a.doNotDrive || a.parkOutside); });
return head + '<div class="cards">' + sorted.map(recallCard).join('') + '</div>' + NEXT;
}
// Right after a free result: the next step for people with more than one vehicle (counted links, checkout opens directly).
var BUY = 'https://data.cybermax-tools.workers.dev/buy/';
var NEXT = '<div class="msg next"><b>More than one vehicle?</b> The Bulk tab checks up to 50 VINs here for free. From your own system or a dealer/fleet list, the ' +
'<a href="' + BUY + 'recallroll-api-pack?s=hub-recallroll-result" rel="nofollow">Recallroll API $5 pack</a> covers 400 calls of up to 10 VINs (4,000 VINs), no subscription; ' +
'the key appears right after checkout. New recalls every Monday: <a href="' + BUY + 'recall-brief?s=hub-recallroll-result" rel="nofollow">Recallroll Brief</a>, $9/month.</div>';
function vehicleBlock(v) {
var rows = [['Vehicle', [v.year, v.make, v.model, v.trim].filter(Boolean).join(' ')], ['Series', v.series], ['Body', v.body], ['Type', v.type], ['Engine', v.engine], ['Drive', v.drive], ['GVWR', v.gvwr], ['Built in', v.plant], ['Manufacturer', v.manufacturer]];
return '<div class="vcard"><div class="vin">' + esc(v.vin) + '</div><dl>' + rows.filter(function (r) { return r[1]; }).map(function (r) { return '<dt>' + r[0] + '</dt><dd>' + esc(r[1]) + '</dd>'; }).join('') + '</dl></div>';
}
function uniq(a) { var s = {}; return a.filter(function (x) { if (!x || s[x]) return false; s[x] = 1; return true; }).sort(); }
function msg(el, text, kind) { el.hidden = !text; el.className = 'msg' + (kind ? ' ' + kind : ''); el.innerHTML = text || ''; }
// ---------- one VIN ----------
$('f-one').onsubmit = function (e) {
e.preventDefault();
var c = RR.checkVin($('vin').value), out = $('one-out'), m = $('one-msg');
out.innerHTML = '';
if (!c.ok) return msg(m, esc(c.error), 'bad');
msg(m, c.warning ? esc(c.warning) + ' Decoding anyway…' : 'Decoding…', c.warning ? 'warn' : '');
try { history.replaceState(null, '', '?vin=' + c.vin + '#one'); } catch (err) { /* ignore */ }
getJson(VPIC + 'DecodeVinValues/' + c.vin + '?format=json').then(function (d) {
var v = RR.vehicleFromVpic((d.Results || [])[0] || {});
v.vin = c.vin;
if (!v.decoded) { msg(m, 'NHTSA could not decode this VIN' + (v.errorText ? ': ' + esc(v.errorText) : '.') + ' Check it for typos.', 'bad'); return; }
msg(m, c.warning ? esc(c.warning) : '', c.warning ? 'warn' : '');
out.innerHTML = vehicleBlock(v) + '<div id="one-rec"><p class="muted">Loading recalls…</p></div>';
return recallsFor(v.make, v.model, v.year).then(function (res) { $('one-rec').innerHTML = recallBlock(v, res); });
}).catch(function (err) { msg(m, 'Could not reach NHTSA (' + esc(err.message) + '). Try again in a minute.', 'bad'); });
};
// ---------- search by year/make/model (only vehicles that have had a recall since 2010) ----------
(function () {
var yy = $('yy'), mk = $('mk'), md = $('md');
getJson('models/years.json').then(function (d) {
yy.innerHTML = '<option value="">Year</option>' + d.years.slice().reverse().map(function (y) { return '<option>' + esc(y) + '</option>'; }).join('');
});
yy.onchange = function () {
mk.disabled = md.disabled = true; mk.innerHTML = '<option value="">Make</option>'; md.innerHTML = '<option value="">Model</option>';
if (!yy.value) return;
modelIndex(yy.value).then(function (idx) {
mk.innerHTML = '<option value="">Make</option>' + Object.keys(idx).sort().map(function (m) { return '<option>' + esc(m) + '</option>'; }).join('');
mk.disabled = false;
});
};
mk.onchange = function () {
md.disabled = true; md.innerHTML = '<option value="">Model</option>';
if (!mk.value) return;
modelIndex(yy.value).then(function (idx) {
md.innerHTML = '<option value="">Model</option>' + uniq(idx[mk.value] || []).map(function (m) { return '<option>' + esc(m) + '</option>'; }).join('');
md.disabled = false;
});
};
md.onchange = function () {
if (!md.value) return;
var v = { year: yy.value, make: mk.value, model: md.value };
$('one-out').innerHTML = '<p class="muted">Loading recalls…</p>'; msg($('one-msg'), '');
recallsFor(v.make, v.model, v.year).then(function (res) { $('one-out').innerHTML = recallBlock(v, res); })
.catch(function (err) { msg($('one-msg'), 'Could not reach NHTSA (' + esc(err.message) + ').', 'bad'); });
};
})();
// ---------- bulk ----------
var bulkRows = [];
function pool(items, n, fn) {
var i = 0, out = new Array(items.length);
function next() { if (i >= items.length) return Promise.resolve(); var k = i++; return fn(items[k], k).then(function (r) { out[k] = r; }, function () { out[k] = null; }).then(next); }
return Promise.all(Array.from({ length: Math.min(n, items.length) }, next)).then(function () { return out; });
}
$('bulk-go').onclick = function () {
var vins = RR.extractVins($('vins').value, 50), st = $('bulk-status'), tb = document.querySelector('#bt tbody');
if (!vins.length) { st.textContent = 'No 17-character VINs found.'; return; }
st.textContent = 'Decoding ' + vins.length + ' VINs…'; $('bulk-csv').disabled = true; tb.innerHTML = ''; $('bulk-wrap').hidden = false;
var body = new URLSearchParams({ format: 'json', data: vins.join(';') });
getJson(VPIC + 'DecodeVINValuesBatch/', { method: 'POST', body: body }).then(function (d) {
var vs = (d.Results || []).map(RR.vehicleFromVpic);
var groups = {};
vs.forEach(function (v) { if (v.decoded) groups[[v.year, v.make, v.model].join('|')] = v; });
var keys = Object.keys(groups);
st.textContent = 'Checking recalls for ' + keys.length + ' model years…';
return pool(keys, 4, function (k) { var v = groups[k]; return recallsFor(v.make, v.model, v.year); }).then(function (res) {
var byKey = {};
keys.forEach(function (k, i) { byKey[k] = res[i]; });
bulkRows = vs.map(function (v) {
var r = byKey[[v.year, v.make, v.model].join('|')], rows = r ? r.rows : [];
var dnd = rows.filter(function (x) { return x.doNotDrive; }).length, po = rows.filter(function (x) { return x.parkOutside; }).length;
return { vin: v.vin, year: v.year, make: v.make, model: v.model, trim: v.trim, decoded: v.decoded, recalls: v.decoded && r ? rows.length : '', do_not_drive: dnd, park_outside: po,
newest_campaign: rows[0] ? rows[0].campaign : '', newest_date: rows[0] ? rows[0].date : '', newest_component: rows[0] ? rows[0].component : '',
campaigns: rows.map(function (x) { return x.campaign; }).join(' '), note: !v.decoded ? (v.errorText || 'not decoded') : (r ? '' : 'recall lookup failed') };
});
bulkRows.sort(function (a, b) { return (b.do_not_drive + b.park_outside) - (a.do_not_drive + a.park_outside) || (+b.recalls || 0) - (+a.recalls || 0); });
tb.innerHTML = bulkRows.map(function (r) {
var warn = (r.do_not_drive ? '<span class="pill red">Do Not Drive ×' + r.do_not_drive + '</span> ' : '') + (r.park_outside ? '<span class="pill amber">Park Outside ×' + r.park_outside + '</span>' : '');
return '<tr><td>' + (r.decoded ? esc([r.year, r.make, r.model, r.trim].filter(Boolean).join(' ')) : '<span class="muted">' + esc(r.note) + '</span>') + '</td><td class="num">' + esc(r.recalls) + '</td><td>' + warn + '</td><td class="mono">' + esc(r.vin) + '</td><td>' + (r.newest_campaign ? esc(r.newest_date) + ' <span class="sub">' + esc(r.newest_component) + '</span>' : '') + '</td></tr>';
}).join('');
var hot = bulkRows.filter(function (r) { return r.do_not_drive || r.park_outside; }).length;
st.textContent = bulkRows.length + ' VINs checked · ' + bulkRows.filter(function (r) { return +r.recalls > 0; }).length + ' with recalls listed for their model year · ' + hot + ' with a Do Not Drive / Park Outside advisory. Confirm each VIN at nhtsa.gov/recalls.';
$('bulk-csv').disabled = false;
});
}).catch(function (err) { st.textContent = 'Could not reach NHTSA (' + err.message + '). Try again.'; });
};
$('bulk-csv').onclick = function () {
var csv = RR.toCsv(bulkRows, ['vin', 'year', 'make', 'model', 'trim', 'recalls', 'do_not_drive', 'park_outside', 'newest_campaign', 'newest_date', 'newest_component', 'campaigns', 'note']);
var a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }));
a.download = 'recallroll-' + new Date().toISOString().slice(0, 10) + '.csv';
document.body.appendChild(a); a.click(); a.remove();
};
// ---------- new recalls (snapshot) ----------
var snap = null;
function loadSnapshot() {
if (snap) return;
snap = {};
fetch('data.json').then(function (r) { return r.json(); }).then(function (d) {
snap = d;
var m = d.meta, fmt = function (n) { return Number(n).toLocaleString('en-US'); };
$('stats').innerHTML = [[fmt(m.campaigns_30d), 'recall campaigns in the last 30 days'], [fmt(m.campaigns), 'in the last 12 months'], [fmt(m.units_affected), 'units potentially affected (12 mo.)'], [fmt(m.do_not_drive + m.park_outside), 'Do Not Drive / Park Outside (12 mo.)']]
.map(function (s) { return '<div class="stat"><b>' + s[0] + '</b><span>' + s[1] + '</span></div>'; }).join('');
var makes = {};
d.recent.forEach(function (c) { c.makes.split(', ').forEach(function (k) { makes[k] = 1; }); });
$('nmake').innerHTML = '<option value="">All makes</option>' + Object.keys(makes).sort().map(function (k) { return '<option>' + esc(k) + '</option>'; }).join('');
renderNew();
}).catch(function () { $('ncount').textContent = 'Could not load the recall snapshot.'; snap = null; });
}
function renderNew() {
if (!snap || !snap.recent) return;
var flag = $('nflag').checked;
var list = flag ? RR.filterRecent(snap.advisories, $('nq').value, $('nmake').value) : RR.filterRecent(snap.recent, $('nq').value, $('nmake').value);
$('ncount').textContent = (flag ? list.length + ' Do Not Drive / Park Outside campaigns in the last 5 years' : list.length + ' campaigns received in the last 60 days') + ' · NHTSA data as of ' + snap.meta.latest_received + ' (built ' + snap.meta.built + ')';
$('nlist').innerHTML = list.slice(0, 200).map(function (c) {
return recallCard({ campaign: c.campaign, date: c.report_received, component: c.vehicles, manufacturer: c.manufacturer + (c.units_affected ? ' · ' + Number(c.units_affected).toLocaleString('en-US') + ' affected' : ''), summary: c.component + (c.summary ? ': ' + c.summary : ''), consequence: c.consequence, remedy: c.remedy, doNotDrive: c.do_not_drive, parkOutside: c.park_outside });
}).join('');
}
['nq', 'nmake', 'nflag'].forEach(function (id) { $(id).addEventListener('input', renderNew); $(id).addEventListener('change', renderNew); });
// ---------- deep links: ?vin=... #bulk #new ----------
var qv = new URLSearchParams(location.search).get('vin');
var h = (location.hash || '').slice(1);
if (tabs[h]) show(h);
if (qv) { $('vin').value = qv; show('one'); $('f-one').requestSubmit ? $('f-one').requestSubmit() : $('f-one').onsubmit(new Event('submit')); }
})();