Spaces:
Running
Running
File size: 15,788 Bytes
2b8a1f7 6570b19 2b8a1f7 6570b19 2b8a1f7 d39873c 2b8a1f7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | /* 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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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')); }
})();
|