/* eslint-disable */ /* © 2026 K2B NETWORK SOLUTIONS LLC. All rights reserved. Proprietary and confidential. Unauthorized copying, distribution, or use of this file, via any medium, is strictly prohibited. Original work product. */ // K2B DMS — Connected Apps (vendor logins, NO API keys). // // Carfax, vAuto, RouteOne, GM GlobalConnect, CarGurus, Autotrader … none of them // let a third-party DMS embed their pages in an iframe (X-Frame-Options), and // their partner APIs need a signed contract + keys. What works today, with the // dealer's OWN vendor login: // • the DMS opens the vendor in ONE dedicated, NAMED browser tab // (window.open(url, "k2b-app-carfax")). The user signs in there once. // • every later launch from the DMS (a VIN, a stock #, a deal) targets that // same named tab, so the vendor's own session cookie is reused and the // page opens straight to the report / appraisal / deal. // Named-tab reuse needs the SAME window name every time and must NOT pass // "noopener" (noopener breaks the opener/name relationship — the browser then // opens a fresh tab each click). We use window.open(url, name) then w.focus(). // // window.K2BApps — service (registry + localStorage overrides) // — the Integrations screen (card grid) // — drop-in launch button for any screen // // localStorage: "jb_apps_v1" → { [id]: { enabled, connectedAt, signInStartedAt, // lastOpened, templates: { vin, stock, deal }, loginUrl, notes } } (function () { const STORE_KEY = "jb_apps_v1"; const EVT = "k2b-apps-changed"; const PH = ["vin", "stock", "dealId"]; // Defaults — every URL here is EDITABLE in the panel because exact deep-link // formats vary by dealer account / vendor rollout. "uncertain: true" = the // format is a best-known guess, not a vendor-published contract. const DEFAULTS = [ { id: "carfax", name: "Carfax for Dealers", kind: "Vehicle history", tone: "#1B4DA0", loginUrl: "https://www.carfaxonline.com/", launch: { vin: "https://www.carfaxonline.com/vhrs/{vin}", stock: "", deal: "" }, fallback: { vin: "https://www.carfax.com/vehicle/{vin}" }, hints: { vin: "direct report link — adjust if your Carfax Online link looks different" }, help: "Sign in to Carfax Online once. Every CARFAX button in the DMS then opens the full dealer report for that VIN in the same tab. If you are not signed in, the public Carfax page for the VIN is used instead." }, { id: "vauto", name: "vAuto Provision", kind: "Appraisal / pricing", tone: "#C8102E", loginUrl: "https://www2.vauto.com/", launch: { vin: "", stock: "", deal: "" }, hints: { vin: "paste the appraisal/stock link format from vAuto with {vin} or {stock}", stock: "paste the appraisal/stock link format from vAuto with {vin} or {stock}" }, help: "vAuto opens to your Provision home until you paste a link format. Open any appraisal in vAuto, copy its address, and replace the VIN or stock # with {vin} / {stock}." }, { id: "routeone", name: "RouteOne", kind: "Credit applications", tone: "#E4572E", loginUrl: "https://www.routeone.net/", launch: { vin: "", stock: "", deal: "" }, hints: { deal: "paste a RouteOne deal link with {dealId} where the deal number goes (optional)" }, help: "RouteOne opens to your dealer workspace in its own tab. Deal-level deep links are optional — most stores just keep the tab open next to the DMS." }, { id: "gmglobalconnect", name: "GM GlobalConnect", kind: "OEM portal", tone: "#0F4C81", loginUrl: "https://www.autopartners.net/", launch: { vin: "", stock: "", deal: "" }, hints: { vin: "optional — paste a GlobalConnect VIN inquiry link with {vin}" }, help: "GlobalConnect (Dealer Portal) — incentives, warranty, VIN inquiry. Sign in once; the tab stays signed in per GM's session rules." }, { id: "cargurus", name: "CarGurus Dealer", kind: "Listing portal", tone: "#2E8540", listing: true, loginUrl: "https://dealers.cargurus.com/", launch: { vin: "", stock: "", deal: "" }, hints: { stock: "optional — listing link with {stock} or {vin}" }, help: "Listing portal. Inventory feeds still go vendor-to-vendor (or via your listing syndicator); this is your one-click dealer login." }, { id: "autotrader", name: "Autotrader Dealer", kind: "Listing portal", tone: "#DA6B00", listing: true, loginUrl: "https://www.autotrader.com/", launch: { vin: "", stock: "", deal: "" }, hints: { stock: "optional — listing link with {stock} or {vin}" }, help: "Listing portal. Sign in with your Autotrader dealer account; the DMS keeps that tab and re-focuses it on every launch." }, ]; // Old API-key stores from the previous integrations model — purge so no // key/token sits in this browser (the "no API keys anywhere" promise). ["jbrews-feeds-cfg", "jbrews-carfax-key", "jbrews-carfax-endpoint", "jbrews-carfax", "jbrews-vauto"].forEach(k => { try { localStorage.removeItem(k); } catch (e) {} }); const load = () => { try { return JSON.parse(localStorage.getItem(STORE_KEY) || "{}") || {}; } catch (e) { return {}; } }; let store = load(); const save = () => { try { localStorage.setItem(STORE_KEY, JSON.stringify(store)); } catch (e) {} try { window.dispatchEvent(new CustomEvent(EVT)); } catch (e) {} }; const ov = (id) => store[id] || {}; const patch = (id, p) => { store[id] = { ...ov(id), ...p }; save(); }; const enc = (v) => encodeURIComponent(String(v == null ? "" : v).trim()); const toast = (m, k, t) => { if (window.k2bToast) window.k2bToast(m, k || "info", t || 4200); }; // Fill {vin}/{stock}/{dealId} (case-insensitive). Returns null when a // placeholder the template needs is not in ctx (= template does not match). const fill = (tpl, ctx) => { if (!tpl) return null; let out = String(tpl), ok = true; PH.forEach(k => { if (new RegExp("\\{" + k + "\\}", "i").test(out)) { const v = ctx && ctx[k]; if (v == null || String(v).trim() === "") ok = false; else out = out.replace(new RegExp("\\{" + k + "\\}", "gi"), enc(k === "vin" ? String(v).toUpperCase().replace(/\s+/g, "") : v)); } }); return ok ? out : null; }; const merged = (d) => { const o = ov(d.id); const templates = { ...d.launch, ...(o.templates || {}) }; const status = o.connectedAt ? "connected" : o.signInStartedAt ? "pending" : "none"; return { ...d, launch: templates, loginUrl: o.loginUrl || d.loginUrl, notes: o.notes || "", enabled: o.enabled !== false, connectedAt: o.connectedAt || null, signInStartedAt: o.signInStartedAt || null, lastOpened: o.lastOpened || null, status, connected: status === "connected", tab: "k2b-app-" + d.id }; }; const K2BApps = { STORE_KEY, EVT, list() { return DEFAULTS.map(merged); }, get(id) { const d = DEFAULTS.find(x => x.id === id); return d ? merged(d) : null; }, isConnected(id) { return !!ov(id).connectedAt; }, isEnabled(id) { const a = K2BApps.get(id); return !!(a && a.enabled); }, markConnected(id, on) { if (!K2BApps.get(id)) return; if (on) patch(id, { connectedAt: new Date().toISOString(), signInStartedAt: ov(id).signInStartedAt || new Date().toISOString() }); else patch(id, { connectedAt: null, signInStartedAt: null }); }, setEnabled(id, on) { patch(id, { enabled: !!on }); }, setTemplate(id, kind, tpl) { if (!K2BApps.get(id)) return; const t = { ...(ov(id).templates || {}) }; t[kind] = String(tpl || "").trim(); patch(id, { templates: t }); }, setLoginUrl(id, url) { patch(id, { loginUrl: String(url || "").trim() }); }, setNotes(id, notes) { patch(id, { notes: String(notes || "") }); }, resetLinks(id) { patch(id, { templates: {}, loginUrl: "" }); }, tabName(id) { return "k2b-app-" + id; }, subscribe(fn) { window.addEventListener(EVT, fn); return () => window.removeEventListener(EVT, fn); }, // Best URL for a context: first template (vin → stock → deal) whose // placeholders are all satisfied; then the vendor fallback (Carfax public // page); then the login / home URL. buildUrl(id, ctx) { const a = K2BApps.get(id); if (!a) return null; const c = ctx || {}; for (const kind of ["vin", "stock", "deal"]) { const u = fill(a.launch[kind], c); if (u) return { url: u, via: "template:" + kind }; } if (a.fallback) for (const kind of ["vin", "stock", "deal"]) { const u = fill(a.fallback[kind], c); if (u) return { url: u, via: "fallback:" + kind }; } return { url: a.loginUrl, via: "home" }; }, // Open the app in its named tab. Not signed in yet → open the login page // first and say so; afterwards every launch goes straight to the deep link. open(id, ctx) { const a = K2BApps.get(id); if (!a) { toast("Unknown app: " + id, "warn"); return null; } if (!a.enabled) { toast(a.name + " is turned off — enable it under Integrations.", "warn"); return null; } const built = K2BApps.buildUrl(id, ctx); let url = built.url; if (a.status === "none") { url = a.loginUrl; patch(id, { signInStartedAt: new Date().toISOString() }); toast("Sign in to " + a.name + " in the tab that opened — the DMS will reuse it from now on.", "info", 5200); } let w = null; try { w = window.open(url, a.tab); } catch (e) { w = null; } if (!w) { toast("Pop-up blocked — allow pop-ups for the DMS so " + a.name + " can open in its own tab.", "warn", 5200); return null; } try { w.focus(); } catch (e) {} patch(id, { lastOpened: new Date().toISOString() }); if (window.DMS && window.DMS.logAudit) { try { window.DMS.logAudit("app.open", id + " " + built.via + (ctx && ctx.vin ? " vin=" + ctx.vin : "") + (ctx && ctx.stock ? " stock=" + ctx.stock : "") + (ctx && ctx.dealId ? " deal=" + ctx.dealId : "")); } catch (e) {} } return w; }, // "Sign in ▸" from the panel — login page in the named tab, status → pending. signIn(id) { const a = K2BApps.get(id); if (!a) return null; let w = null; try { w = window.open(a.loginUrl, a.tab); } catch (e) { w = null; } if (!w) { toast("Pop-up blocked — allow pop-ups for the DMS, then try Sign in again.", "warn", 5200); return null; } try { w.focus(); } catch (e) {} patch(id, { signInStartedAt: new Date().toISOString(), lastOpened: new Date().toISOString() }); toast("Sign in to " + a.name + " in that tab, then click “I'm signed in ✓”.", "info", 5200); return w; }, disconnect(id) { patch(id, { connectedAt: null, signInStartedAt: null }); toast("Disconnected — nothing was stored here; sign out inside the vendor tab too if you share this PC.", "ok", 5200); }, _reload() { store = load(); }, }; window.K2BApps = K2BApps; })(); // ── Drop-in launch button ──────────────────────────────────────────────── // // Renders nothing when the app is unknown or turned off. const AppLaunchButton = ({ app, ctx, small, label, style, title }) => { const [, tick] = React.useReducer(n => n + 1, 0); React.useEffect(() => (window.K2BApps ? window.K2BApps.subscribe(tick) : undefined), []); const A = window.K2BApps; const a = A && A.get(app); if (!a || !a.enabled) return null; const has = ctx && (ctx.vin || ctx.stock || ctx.dealId); return ( ); }; // ── One app card ───────────────────────────────────────────────────────── const JbAppCard = ({ app, testVin }) => { const A = window.K2BApps; const [links, setLinks] = React.useState(false); const [vin, setVin] = React.useState(testVin || ""); const [tpl, setTpl] = React.useState({ vin: app.launch.vin || "", stock: app.launch.stock || "", deal: app.launch.deal || "", loginUrl: app.loginUrl || "", notes: app.notes || "" }); React.useEffect(() => { setTpl({ vin: app.launch.vin || "", stock: app.launch.stock || "", deal: app.launch.deal || "", loginUrl: app.loginUrl || "", notes: app.notes || "" }); }, [app.launch.vin, app.launch.stock, app.launch.deal, app.loginUrl, app.notes]); const inp = { width: "100%", boxSizing: "border-box", padding: "6px 8px", border: "1px solid var(--jb-border, #C8CCD2)", borderRadius: "var(--jb-r-sm, 4px)", fontSize: 12, fontFamily: "var(--jb-mono, monospace)", background: "#FFF" }; const lab = { fontSize: 11, fontWeight: 600, color: "var(--jb-text-dim, #555)" }; const saveLinks = () => { A.setTemplate(app.id, "vin", tpl.vin); A.setTemplate(app.id, "stock", tpl.stock); A.setTemplate(app.id, "deal", tpl.deal); A.setLoginUrl(app.id, tpl.loginUrl); A.setNotes(app.id, tpl.notes); window.k2bToast && window.k2bToast(app.name + " link settings saved.", "ok"); setLinks(false); }; const st = app.status; const pill = st === "connected" ? { bg: "var(--jb-green-tint, #E6F7EC)", fg: "#0E7A37", txt: "● Signed in (your tab)" } : st === "pending" ? { bg: "var(--jb-amber-tint, #FBF3E0)", fg: "#9A6700", txt: "◐ Sign-in started" } : { bg: "var(--jb-surface-2, #EEF0F3)", fg: "var(--jb-text-dim, #666)", txt: "○ Not signed in" }; const fmt = (d) => d ? new Date(d).toLocaleString([], { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "—"; const preview = vin ? (A.buildUrl(app.id, { vin: vin.trim().toUpperCase() }) || {}).url : ""; const hasVinLink = !!(app.launch.vin || (app.fallback && app.fallback.vin)); return (
{app.name} {app.kind}
{pill.txt}
{app.help}
{st !== "connected" && ( )} {st === "pending" && ( )}
{st !== "none" && }
{hasVinLink && (
setVin(e.target.value.toUpperCase())} placeholder="Test with VIN" maxLength={17} style={{ ...inp, flex: 1 }} onKeyDown={(e) => { if (e.key === "Enter" && vin.trim()) A.open(app.id, { vin: vin.trim() }); }} />
)} {hasVinLink && preview && (
opens: {preview}
)} {links && (
Placeholders: {"{vin}"} · {"{stock}"} · {"{dealId}"} — the DMS fills them in and URL-encodes the value. Open the page once inside the vendor site, copy its address, swap the VIN / stock # / deal # for the placeholder, paste it here. Leave a field blank to open the vendor home page instead.
{[["vin", "VIN link"], ["stock", "Stock # link"], ["deal", "Deal link"]].map(([k, l]) => ( ))}
)}
tab: {app.tab} · signed in {fmt(app.connectedAt)} · last opened {fmt(app.lastOpened)} {app.notes && {app.notes}}
); }; // ── The Integrations panel (card grid + explainer) ────────────────────── const ConnectedAppsPanel = ({ go }) => { const A = window.K2BApps; const [, tick] = React.useReducer(n => n + 1, 0); React.useEffect(() => (A ? A.subscribe(tick) : undefined), []); if (!A) return
Connected Apps service unavailable.
; const apps = A.list(); const firstVin = (() => { try { const u = (window.DMS && window.DMS.inventory) ? window.DMS.inventory().find(x => x.vin) : null; return u ? u.vin : ""; } catch (e) { return ""; } })(); const connected = apps.filter(a => a.connected).length; const step = (n, t, s) => (
{n}
{t}
{s}
); return (
{/* explainer strip */}
{step(1, "Sign in once", "Click Sign in ▸ on a card. The vendor opens in its own tab — use your normal dealer login.")} {step(2, "Keep the tab", "Leave that tab open next to the DMS. The DMS remembers it by name and re-focuses it every time.")} {step(3, "Every VIN / deal opens there", "Carfax on a unit, vAuto on a trade, RouteOne on a deal — each button lands straight on the page, already signed in.")}
Plain truth: Carfax, vAuto, RouteOne and the rest block being embedded inside another website, and their data APIs require a signed partner contract. So these launchers use your own login in a dedicated tab and store no passwords or tokens — your session lives only in the vendor's tab. "Signed in" here means you signed in to the vendor's website; it is not an API connection. Server-to-server API integrations are separate — an administrator manages them in Settings → Integrations, and their real status is shown above. If a launch opens a fresh tab instead of reusing the old one, the browser closed the link between them — just sign in again in the new tab.
Vendor sites — open with your own login {connected} of {apps.length} signed in · each opens in tab k2b-app-<app>
{go && }
{apps.map(a => )}
); }; // ── Server API integrations: shared client helper + read-only status ───── // Every call goes to the DMS server (/api/integrations/...). Secrets are never // sent back to the browser — admin screens only see "stored ✓" flags. const k2bIntApi = async (path, method, body) => { try { if (typeof apiFetch === "function") { const d = await apiFetch("/integrations" + path, { method: method || "GET", body }); return { ok: true, status: 200, data: d || {} }; } let tok = null; try { tok = sessionStorage.getItem("jbrews-auth-token") || localStorage.getItem("jbrews-auth-token"); } catch (e) {} const r = await fetch(location.origin + "/api/integrations" + path, { method: method || "GET", credentials: "omit", headers: Object.assign({ "Content-Type": "application/json" }, tok ? { Authorization: "Bearer " + tok } : {}, tok && window.K2BStore ? window.K2BStore.stamp({}) : {}), body: body != null ? JSON.stringify(body) : undefined }); // RC35 (brd): + X-K2B-Store / X-K2B-Protocol let d = {}; try { d = await r.json(); } catch (e) {} return { ok: r.ok, status: r.status, data: d, error: r.ok ? "" : (d.error || (r.status + " " + r.statusText)) }; } catch (e) { return { ok: false, status: e.status || 0, data: e.body || {}, error: (e.body && e.body.error) || e.message || "Server unreachable" }; } }; const K2B_INT_ORDER = ["twilio", "email", "routeone", "carfax", "vauto", "gm", "anthropic"]; const k2bIntFmt = (d) => d ? new Date(d).toLocaleString([], { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : ""; // status → chip. "Connected" ONLY for status === "connected" (set by the server after a real provider test). const k2bIntChip = (p, opts) => { const partner = p.kind === "partner" || ["routeone", "carfax", "vauto", "gm"].includes(p.id); const st = p.status || "not_configured"; if (st === "connected") return { bg: "#E6F7EC", fg: "#0E7A37", txt: "● Connected" + (p.lastOkAt ? " (tested " + k2bIntFmt(p.lastOkAt) + ")" : "") }; if (st === "error") return { bg: "#FDECEA", fg: "#B23A2E", txt: "✕ Error" + (opts && opts.detail && p.lastError ? ": " + p.lastError : " — not connected") }; if (st === "mock") return { bg: "#F1EAFB", fg: "#6B3FA0", txt: "◌ Mock mode — not connected" }; if (st === "ready") return partner ? { bg: "#FBF3E0", fg: "#9A6700", txt: "◐ Ready — waiting on provider" } : { bg: "#FBF3E0", fg: "#9A6700", txt: "◐ Saved — not tested" }; return { bg: "#EEF0F3", fg: "#666", txt: "○ Not configured" }; }; const K2BIntChip = ({ p, detail }) => { if (p.id === "anthropic") return ; const c = k2bIntChip(p, { detail }); return {c.txt}; }; // ── AI Import (Claude page reading): ONE shared status ────────────────────── // The Upload tool banner, the "AI Import settings" window and the Settings > Integrations card // all read this store, so they can never disagree. It merges two server answers: // GET /api/ai-import/status (canUse, reason, limits, pricing, keyStored/consent/enabled/connected) // GET /api/integrations/status-summary (the provider row: connected / ready / error) // Any non-200 from the first one (the role gate answers Sales with a plain 403) means "not available to your role". const K2BAiStatus = (() => { let snap = { loading: true, ai: null, aiErr: "", httpStatus: 0, prov: null, at: 0 }; const subs = new Set(); let inflight = null; const emit = () => subs.forEach(f => { try { f(snap); } catch (e) {} }); const refresh = () => { if (inflight) return inflight; inflight = (async () => { let ai = null, aiErr = "", httpStatus = 0, prov = null; try { ai = await apiFetch("/ai-import/status"); } catch (e) { httpStatus = (e && e.status) || 0; aiErr = (e && e.body && e.body.error) || (e && e.message) || "The server did not answer."; } try { const r = await k2bIntApi("/status-summary"); if (r.ok) prov = ((r.data && r.data.providers) || []).find(p => p.id === "anthropic") || null; } catch (e) {} snap = { loading: false, ai, aiErr, httpStatus, prov, at: Date.now() }; })().then(() => { inflight = null; emit(); }, () => { inflight = null; emit(); }); return inflight; }; const MODEL_NAMES = { "claude-opus-5-5": "Claude Opus 5.5", "claude-sonnet-5-5": "Claude Sonnet 5.5", "claude-fable-5-1": "Claude Fable 5.1" }; const isAdminNow = () => { try { return !!(window.DMS && window.DMS.isAdmin && window.DMS.isAdmin()); } catch (e) { return false; } }; // snapshot -> what every screen shows. tone: ok | warn | bad | off const view = (s) => { s = s || snap; const ai = s.ai, prov = s.prov; const admin = ai ? !!ai.canConfigure : isAdminNow(); const out = { loading: !!s.loading, admin, canUse: false, key: "loading", tone: "off", chip: "… checking", headline: "Checking whether AI reading is available…", next: "", model: "", modelName: "", pricing: null, limits: null, reason: "", ai, prov }; if (s.loading) return out; if (!ai) { if (s.httpStatus === 403 || s.httpStatus === 401) return Object.assign(out, { key: "role", tone: "off", chip: "○ Not available to your role", headline: "AI reading is limited to Administrators, Office Managers and Accounting.", next: "Ask an administrator to read these pages for you, or sign in with one of those roles.", reason: s.aiErr }); return Object.assign(out, { key: "unreachable", tone: "bad", chip: "✕ Status unavailable", headline: "Could not reach the server to check AI reading.", next: "Check the connection and press Refresh. " + (s.aiErr ? "(" + s.aiErr + ")" : ""), reason: s.aiErr }); } out.model = ai.model || ""; out.modelName = MODEL_NAMES[ai.model] || ai.model || ""; out.pricing = ai.pricing || null; out.limits = ai.limits || null; out.reason = ai.reason || ""; const where = "Settings > Integrations (API) > AI Import"; // step = what an administrator does (a sentence that starts with a verb); next = the same, worded for where the person is reading it const adm = (step, ask) => ({ step: admin ? step : "", next: admin ? "Open AI Import settings and " + step.charAt(0).toLowerCase() + step.slice(1) : ask }); if (/limited to Admin/i.test(ai.reason || "")) return Object.assign(out, { key: "role", tone: "off", chip: "○ Not available to your role", headline: "AI reading is limited to Administrators, Office Managers and Accounting.", next: "Ask an administrator to read these pages for you, or sign in with one of those roles." }); if (ai.available === false) return Object.assign(out, { key: "server", tone: "bad", chip: "✕ Server update incomplete", headline: "The AI reading library is missing on the server.", step: admin ? "Run the AI Import server update again (see the install notes), then press Refresh." : "", next: admin ? "Run the AI Import server update again (see the install notes), then press Refresh." : "Tell an administrator: the AI Import server update is incomplete." }); if (!ai.keyStored) return Object.assign(out, { key: "notset", tone: "off", chip: "○ Not set up", headline: "AI reading is not set up yet." }, adm("Paste your Anthropic key, tick the consent box, press Save and then Test connection.", "Ask an administrator to set it up (" + where + ").")); if (!ai.consent) return Object.assign(out, { key: "consent", tone: "warn", chip: "◐ Needs consent", headline: "The key is saved, but the consent box is not ticked." }, adm("Tick the box that confirms pages may be sent to Anthropic to be read, then press Save.", "Ask an administrator to tick the consent box (" + where + ").")); if (prov && prov.status === "error") return Object.assign(out, { key: "error", tone: "bad", chip: "✕ Error — not connected", headline: "AI reading stopped working: the last connection test or page read failed." }, adm("Press Test connection to see why (usually a wrong key, or no credit on the Anthropic account).", "Tell an administrator. They can see the reason in " + where + ".")); if (!ai.connected) return Object.assign(out, { key: "untested", tone: "warn", chip: "◐ Saved — not tested", headline: "The key is saved but has not been tested yet." }, adm("Press Test connection.", "Ask an administrator to press Test connection (" + where + ").")); if (!ai.enabled) return Object.assign(out, { key: "off", tone: "warn", chip: "◐ Connected — switched off", headline: "AI reading is connected but switched off." }, adm("Turn AI reading on (the box at the top right of the card).", "Ask an administrator to turn it on (" + where + ").")); if (!ai.canUse) return Object.assign(out, { key: "limit", tone: "warn", chip: "◐ Daily limit reached", headline: ai.reason || "The daily page limit has been reached." }, adm("It resets at midnight, or raise \"Pages allowed per day\" and press Save.", "It resets at midnight, or ask an administrator to raise the limit.")); return Object.assign(out, { key: "ready", tone: "ok", canUse: true, chip: "● Connected", headline: "AI reading is on" + (out.modelName ? " (" + out.modelName + ")" : "") + ".", next: "", step: "" }); }; const toneCss = { ok: { bg: "#E6F7EC", fg: "#0E7A37" }, warn: { bg: "#FBF3E0", fg: "#9A6700" }, bad: { bg: "#FDECEA", fg: "#B23A2E" }, off: { bg: "#EEF0F3", fg: "#666" } }; return { get: () => snap, refresh, view, toneCss, MODEL_NAMES, subscribe: (fn) => { subs.add(fn); return () => subs.delete(fn); }, }; })(); // Re-render on every status change; load once when first used (and again when the data is older than 20 s). const useK2BAiStatus = () => { const [, tick] = React.useReducer(n => n + 1, 0); React.useEffect(() => { const off = K2BAiStatus.subscribe(tick); const s = K2BAiStatus.get(); if (s.loading || Date.now() - s.at > 20000) K2BAiStatus.refresh(); return off; }, []); return K2BAiStatus.view(); }; const K2BAiChip = ({ v, style }) => { const cur = useK2BAiStatus(); const x = v || cur; const c = K2BAiStatus.toneCss[x.tone] || K2BAiStatus.toneCss.off; return {x.chip}; }; // The AI Import provider's chip: the shared status, except while it loads or when this role cannot use AI reading at all // (then the provider's own row is shown, so the dealership's setup is still visible). const K2BAiIntChip = ({ p, detail }) => { const v = useK2BAiStatus(); if (v.key === "role" || v.key === "loading") { const c = k2bIntChip(p, { detail }); return {c.txt}; } return ; }; // Read-only status strip for everyone (no config, no errors, no secrets). const IntegrationStatusStrip = ({ go, admin }) => { const [st, setSt] = React.useState({ loading: true }); const load = React.useCallback(async () => { const r = await k2bIntApi("/status-summary"); setSt(r.ok ? { loading: false, providers: r.data.providers || [], mock: !!r.data.mock } : { loading: false, error: r.error }); }, []); React.useEffect(() => { load(); }, [load]); const live = (st.providers || []).filter(p => p.status === "connected" && p.enabled).length; return (
API integrations (server) {st.loading ? "checking…" : st.error ? "status unavailable — " + st.error : live + " genuinely connected · the rest are not live"}
{admin && go && }
{st.mock &&
Server mock mode is on (INTEGRATIONS_MOCK=1): RouteOne / CARFAX / vAuto / e-sign notify screens may show simulated results. Simulated results are never "connected".
}
{(st.providers || []).slice().sort((a, b) => K2B_INT_ORDER.indexOf(a.id) - K2B_INT_ORDER.indexOf(b.id)).map(p => (
{p.label} {!p.enabled && p.status !== "not_configured" && off}
))}
); }; Object.assign(window, { ConnectedAppsPanel, AppLaunchButton, JbAppCard, k2bIntApi, k2bIntChip, K2BIntChip, IntegrationStatusStrip, K2B_INT_ORDER, K2BAiStatus, useK2BAiStatus, K2BAiChip });