// app.jsx — VulnerabilityCore shell: nav, routing, status state, theme tweaks
const LS_AUTH = "vc_auth_v1";
const LS_STATUS = "vc_status_v1";
const LS_VIEW = "tc_view_v1";
const LS_COMPONENTS = "tc_components_v1";

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/ {
  theme: "dark",
  accent: "teal",
  density: "regular",
  wizardLayout: "rail",
}; /*EDITMODE-END*/

const ACCENTS = {
  teal: "#49299a",
  blue: "#00cccc",
  amber: "#e0a13a",
};

// ---- history routing ----------------------------------------------------
// The view lives in the URL PATH (e.g. /vulnerability_ledger) via the History
// API, so the address bar shows clean paths (no #) and the browser back/forward
// + refresh + bookmarks all work. Refresh / pasted deep links rely on a
// server-side SPA fallback that serves VulnerabilityCore.html for any non-API,
// non-asset path (see the backend's MapFallbackToFile) plus <base href="/"> in
// the HTML, so relative asset URLs resolve from the root at any path depth.
// Detail / wizard sub-views need an in-memory selection, so on a cold load
// (refresh / pasted link) they fall back to their parent list page.
// Internal view key ↔ friendly URL slug (the slug is what shows in the address bar).
const VIEW_TO_SLUG = {
  dashboard: "overview",
  ledger: "vulnerability_ledger",
  browse: "browse_by_company",
  components: "components",
  admin: "admin",
  detail: "vulnerability_detail",
  compdetail: "component_detail",
  newcomponent: "new_component",
};
const SLUG_TO_VIEW = Object.fromEntries(
  Object.entries(VIEW_TO_SLUG).map(([view, slug]) => [slug, view]),
);
const VIEW_PARENT = { detail: "ledger", compdetail: "components" };

// Parse the path into a route descriptor { view, compId, cveId, from }. Segments:
//   /vulnerability_detail/<cveId>              → a CVE opened from the ledger
//   /component_detail/<compId>                 → a component's detail page
//   /component_detail/<compId>/<cveId>         → a CVE opened from that component
//     (so the CVE link stays nested under its component and Back returns there)
function parsePath(p) {
  const parts = (p || "").split("/").filter(Boolean);
  const view = SLUG_TO_VIEW[parts[0]];
  if (!view) return null;
  const seg1 = parts[1] ? decodeURIComponent(parts[1]) : null;
  const seg2 = parts[2] ? decodeURIComponent(parts[2]) : null;
  if (view === "compdetail") {
    return seg2
      ? { view: "detail", from: "compdetail", compId: seg1, cveId: seg2 }
      : { view: "compdetail", compId: seg1 };
  }
  if (view === "detail") return { view: "detail", from: "ledger", cveId: seg1 };
  return { view };
}

// Build the canonical path for the current app state (inverse of parsePath).
function buildPath({ view, selected, selectedComp, detailFrom }) {
  const slug = (v) => "/" + (VIEW_TO_SLUG[v] || v);
  if (view === "detail") {
    if (detailFrom === "compdetail" && selectedComp && selected)
      return slug("compdetail") + "/" + encodeURIComponent(selectedComp.id) + "/" + encodeURIComponent(selected.id);
    return selected ? slug("detail") + "/" + encodeURIComponent(selected.id) : slug("detail");
  }
  if (view === "compdetail" && selectedComp)
    return slug("compdetail") + "/" + encodeURIComponent(selectedComp.id);
  return slug(view);
}

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [auth, setAuth] = useState(() => {
    try {
      return JSON.parse(localStorage.getItem(LS_AUTH));
    } catch {
      return null;
    }
  });
  // A deep link to a CVE and/or a component on first load: capture the parsed route
  // now (before any effect rewrites the URL) so we can fetch those records async.
  const pendingDeepLink = useRef((() => {
    const r = parsePath(window.location.pathname);
    if (r && r.view === "detail" && r.cveId) return r;          // CVE (maybe nested)
    if (r && r.view === "compdetail" && r.compId) return r;     // a component page
    return null;
  })());
  const [view, setView] = useState(() => {
    // URL path wins (deep link / refresh), then the last-visited view, then default.
    const r = parsePath(window.location.pathname);
    let v = (r && r.view) || localStorage.getItem(LS_VIEW) || "dashboard";
    // A deep link to a CVE / component opens that page (records fetched by id below).
    if ((v === "detail" || v === "compdetail") && pendingDeepLink.current) return v;
    // Other sub-views can't be restored without an in-memory selection — show parent.
    if (VIEW_PARENT[v]) return VIEW_PARENT[v];
    if (v === "newcomponent") return "components";
    return v;
  });
  const [selected, setSelected] = useState(null);
  // The view a CVE detail was opened from, so its Back button returns there
  // (ledger, dashboard, browse, or a specific component's detail).
  const [detailFrom, setDetailFrom] = useState("ledger");
  const [ledgerFilter, setLedgerFilter] = useState(null);
  const [statusMap, setStatusMap] = useState(() => {
    try {
      return JSON.parse(localStorage.getItem(LS_STATUS)) || {};
    } catch {
      return {};
    }
  });
  const [components, setComponents] = useState([]);
  const [componentsLoaded, setComponentsLoaded] = useState(false);
  const [selectedComp, setSelectedComp] = useState(null);
  const [updatingComp, setUpdatingComp] = useState(null);
  const [toast, setToast] = useState(null);
  // Off-canvas mobile navigation drawer (shown via hamburger below ~900px).
  const [navOpen, setNavOpen] = useState(false);
  // Shell readiness: after auth we load only the lightweight shell data (topbar sync
  // string + sidebar critical badge + wizard dropdowns). Each page then fetches its
  // OWN endpoint when opened — see the per-screen useApi() calls.
  const [dataReady, setDataReady] = useState(false);
  const [dataErr, setDataErr] = useState("");
  const [shellCritical, setShellCritical] = useState(0);
  // Whether this tenant has any vulnerabilities of their own. Gates the ledger nav:
  // with nothing matched there is no ledger to show, so it's hidden from the nav until
  // then. Seeded from /api/summary (classified) and flipped true once an SBOM is uploaded.
  const [hasSboms, setHasSboms] = useState(false);

  const statusOf = (v) => statusMap[v.id] || v.status;
  function setStatus(id, s) {
    setStatusMap((m) => {
      const n = { ...m, [id]: s };
      localStorage.setItem(LS_STATUS, JSON.stringify(n));
      return n;
    });
    // The full triage status is authoritative server-side now: persist every change
    // to the DB (status column) so it survives reloads and is shared across devices.
    window.VCApi.setStatus(id, s).catch(() => {});
    setToast(`${id} → ${STATUS_META[s].label}`);
    clearTimeout(window.__tcToast);
    window.__tcToast = setTimeout(() => setToast(null), 2200);
  }

  // Live ref to current selection + components so the (once-registered) popstate
  // handler can restore a sub-view's in-memory context without re-subscribing.
  const selRef = useRef({ selected, selectedComp, components: [] });
  useEffect(() => { selRef.current = { selected, selectedComp, components }; }, [selected, selectedComp, components]);
  // Live refs to auth + current view for the once-registered popstate handler (it
  // closes over first-render values, so reading them directly would be stale).
  const authRef = useRef(auth);
  useEffect(() => { authRef.current = auth; }, [auth]);
  const viewRef = useRef(view);
  useEffect(() => { viewRef.current = view; }, [view]);

  // Keep localStorage and the URL path in sync with the current view. A CVE opened
  // from a component nests under it (/component_detail/<compId>/<cveId>). A view
  // change pushState's (so Back returns to the prior page); the pre-login
  // normalization replaceState's so it doesn't litter the history stack.
  useEffect(() => {
    localStorage.setItem(LS_VIEW, view);
    // Before login the address bar should read /login, not an authenticated view
    // like /overview. parsePath() returns null for "login", so the popstate
    // listener ignores it and the view state is never disturbed; after login this
    // effect re-runs (auth is a dependency) and writes the real view path.
    if (!auth) {
      if (window.location.pathname !== "/login") history.replaceState(null, "", "/login");
      return;
    }
    if (pendingDeepLink.current) return; // don't clobber a deep-link URL mid-resolve
    const desired = buildPath({ view, selected, selectedComp, detailFrom });
    if (window.location.pathname !== desired) history.pushState(null, "", desired);
  }, [view, selected, selectedComp, detailFrom, auth]);

  // Respond to the browser back/forward buttons (popstate).
  useEffect(() => {
    function onNav() {
      // The login route is for signed-OUT users only. A signed-in user who navigates
      // to /login must NOT reach the login screen — bounce the URL back to the page
      // they are on. (parsePath() returns null for "login", so handle it here before
      // the early-return below swallows it.)
      const slug = ((window.location.pathname || "").split("/").filter(Boolean)[0] || "").toLowerCase();
      if (slug === "login") {
        if (authRef.current) {
          const desired = "/" + (VIEW_TO_SLUG[viewRef.current] || viewRef.current);
          if (window.location.pathname !== desired) history.replaceState(null, "", desired);
        }
        return; // signed-out: leave /login so the login screen shows
      }
      const r = parsePath(window.location.pathname);
      if (!r) return;
      setNavOpen(false);
      if (r.view === "detail") {
        // Restore the component context for a nested CVE link so Back returns there.
        if (r.from === "compdetail" && r.compId) {
          const comp = (selRef.current.selectedComp && selRef.current.selectedComp.id === r.compId)
            ? selRef.current.selectedComp
            : selRef.current.components.find((c) => c.id === r.compId);
          setDetailFrom("compdetail");
          if (comp) setSelectedComp(comp);
        } else {
          setDetailFrom("ledger");
        }
        // Reuse the in-memory record if it matches, else fetch the CVE by id.
        const cur = selRef.current.selected;
        if (cur && cur.id === r.cveId) setView("detail");
        else if (r.cveId) openVulnById(r.cveId);
        else setView("ledger");
        return;
      }
      if (r.view === "compdetail") {
        const comp = selRef.current.components.find((c) => c.id === r.compId);
        if (comp) { setSelectedComp(comp); setView("compdetail"); }
        else setView("components"); // no in-memory match (manual edit) → list
        return;
      }
      setView(r.view);
    }
    window.addEventListener("popstate", onNav);
    return () => window.removeEventListener("popstate", onNav);
  }, []);

  // Resolve a CVE / component deep link captured on first load (after auth).
  useEffect(() => {
    if (!auth) return;
    const route = pendingDeepLink.current;
    if (route) resolveDeepLink(route);
  }, [auth]);

  // Single source of truth for scroll-to-top on navigation: whenever the visible
  // page changes (including between two CVEs or two components), reset the window
  // scroll AFTER the new page has rendered. Replaces the scattered scrollTo() calls,
  // which fired before render and double-fired with the popstate handler.
  const pageKey =
    view === "detail" ? "detail:" + (selected ? selected.id : "") :
    view === "compdetail" ? "compdetail:" + (selectedComp ? selectedComp.id : "") :
    view;
  // useLayoutEffect (not useEffect) so the reset happens before paint — no flash of
  // the new page rendered at the previous scroll position.
  React.useLayoutEffect(() => {
    window.scrollTo(0, 0);
  }, [pageKey]);

  // Route any 401 from a per-page fetch back through logout.
  useEffect(() => {
    window.__vcOn401 = () => logout();
    return () => { if (window.__vcOn401) delete window.__vcOn401; };
  }, []);

  // Shell-level load: topbar sync string + sidebar critical badge (/api/summary)
  // and the wizard's dropdown options (/api/meta). Small + fast; the pages load
  // their own data separately.
  useEffect(() => {
    if (!auth) {
      setDataReady(false);
      return;
    }
    let cancelled = false;
    setDataErr("");
    (async () => {
      try {
        const [summary, meta, statuses, config] = await Promise.all([
          window.VCApi.summary(),
          window.VCApi.meta(),
          window.VCApi.statuses().catch(() => null),
          window.VCApi.config().catch(() => null),
        ]);
        if (cancelled) return;
        // Apply the backend-driven status/severity config (falls back to the
        // hard-coded defaults in components.jsx if it couldn't be fetched).
        if (config) window.applyAppConfig(config);
        // Seed triage from the company overlay (authoritative + shared within the
        // company). Server wins over any stale per-browser localStorage so a refresh
        // or a different device shows the same states, and another teammate's change
        // is reflected here.
        if (statuses && typeof statuses === "object") {
          setStatusMap(statuses);
          localStorage.setItem(LS_STATUS, JSON.stringify(statuses));
        }
        if (summary) {
          window.VC.COVERAGE = Object.assign({}, window.VC.COVERAGE, {
            lastSync: summary.lastSync,
            classified: summary.classified,
          });
          setShellCritical(summary.openCritical || 0);
          setHasSboms((summary.classified || 0) > 0);
        }
        if (meta) {
          window.VC.SUPPLIERS = meta.suppliers || [];
          window.VC.BRANDS = meta.brands || [];
          if (meta.categories && meta.categories.length)
            window.VC.COMPONENT_CATEGORIES = meta.categories;
        }
        setDataReady(true);
      } catch (e) {
        if (cancelled) return;
        if (e && e.status === 401) {
          logout();
          return;
        }
        setDataErr(e.message || "Failed to load live data.");
        setToast(e.message || "Failed to load live data.");
        setDataReady(true); // fall through so the app is usable rather than stuck
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [auth]);

  // Components page data is fetched on demand the first time the user enters the
  // components area. The wizard + update-SBOM modal correlate uploaded SBOMs against
  // the full catalog client-side, so the shared catalog cache is warmed alongside it.
  const inComponentsArea =
    view === "components" || view === "compdetail" || view === "newcomponent";
  useEffect(() => {
    if (!auth || !inComponentsArea || componentsLoaded) return;
    let cancelled = false;
    (async () => {
      try {
        const [comps] = await Promise.all([
          window.VCApi.components(),
          window.VC.ensureCatalog(),
        ]);
        if (cancelled) return;
        setComponents(comps || []);
        setComponentsLoaded(true);
      } catch (e) {
        if (cancelled) return;
        if (e && e.status === 401) {
          logout();
          return;
        }
        setToast(e.message || "Failed to load components.");
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [auth, inComponentsArea, componentsLoaded]);

  // Catalog-view guards for non-admins. Browse-by-company and Administration are
  // admin-only (cross-tenant). The ledger IS shown to customers (scoped server-side to
  // their own matches — see LedgerController), but only once they've uploaded an SBOM;
  // until then there's nothing to show, so a non-admin who reaches any of these by any
  // path (deep link, back, stale) goes to Overview. Admins always have all three.
  useEffect(() => {
    if (!auth || auth.isAdmin) return;
    if (view === "browse" || view === "admin" || (view === "ledger" && !hasSboms)) setView("dashboard");
  }, [auth, view, hasSboms]);

  function login(user) {
    localStorage.setItem(LS_AUTH, JSON.stringify(user));
    setAuth(user);
  }
  function logout() {
    try {
      window.VCApi.logout();
    } catch (_) {}
    try {
      window.VC.resetCatalog();
    } catch (_) {}
    localStorage.removeItem(LS_AUTH);
    setAuth(null);
    setDataReady(false);
    setComponents([]);
    setComponentsLoaded(false);
    setShellCritical(0);
    setView("dashboard");
  }

  function openVuln(v) {
    // Remember where we came from (unless we're already on a CVE viewing a related
    // one) so Back returns to the origin page rather than always the ledger.
    if (view !== "detail") setDetailFrom(view);
    setSelected(v);
    setView("detail");
  }
  // Open a CVE by id alone (deep link / back-forward to a /vulnerability_detail/<id>
  // URL). Fetches the full record via the same /api/detail/{id} the page uses.
  async function openVulnById(id) {
    setView("detail"); // show the detail shell (loading state) while we fetch
    try {
      const data = await window.VCApi.detail(id);
      if (data && data.vuln) {
        setSelected(data.vuln);
      } else {
        setToast("No vulnerability found for " + id);
        setView("ledger");
      }
    } catch (e) {
      if (e && e.status === 401) { logout(); return; }
      setToast(e.message || ("Could not load " + id));
      setView("ledger");
    } finally {
      pendingDeepLink.current = null; // resolved — let the URL sync resume
    }
  }
  // Resolve a deep link captured on first load. Restores the component context for
  // a component-scoped link (fetching the components list if needed) and/or the CVE
  // record, so a refresh of /component_detail/<compId>/<cveId> rebuilds the page.
  async function resolveDeepLink(route) {
    try {
      let comp = null;
      if (route.compId) {
        comp = components.find((c) => c.id === route.compId);
        if (!comp) {
          const comps = await window.VCApi.components();
          setComponents(comps || []);
          setComponentsLoaded(true);
          comp = (comps || []).find((c) => c.id === route.compId);
        }
        if (comp) { setSelectedComp(comp); setDetailFrom("compdetail"); }
      }
      if (route.view === "detail" && route.cveId) {
        const data = await window.VCApi.detail(route.cveId);
        if (data && data.vuln) { setSelected(data.vuln); setView("detail"); }
        else { setToast("No vulnerability found for " + route.cveId); setView(comp ? "compdetail" : "ledger"); }
      } else if (route.view === "compdetail") {
        setView(comp ? "compdetail" : "components");
      }
    } catch (e) {
      if (e && e.status === 401) { logout(); return; }
      setToast(e.message || "Could not open that link.");
      setView("components");
    } finally {
      pendingDeepLink.current = null; // resolved — let the URL sync resume
    }
  }
  function nav(v, opts = {}) {
    // Customers never navigate to Browse-by-company or Administration (cross-tenant) —
    // admin only. The ledger is allowed once they've uploaded an SBOM; before that it's
    // disabled, so a stray nav falls back to the Overview.
    const customer = !(auth && auth.isAdmin);
    if (customer && (v === "browse" || v === "admin" || (v === "ledger" && !hasSboms))) v = "dashboard";
    setView(v);
    if (opts.filter) setLedgerFilter(opts.filter);
    setNavOpen(false);
  }

  function startWizard() {
    setView("newcomponent");
  }
  function openComponent(c) {
    setSelectedComp(c);
    setView("compdetail");
  }
  async function createComponent(comp) {
    try {
      const saved = await window.VCApi.createComponent({
        name: comp.name,
        category: comp.category,
        supplier: comp.supplier,
        brands: comp.brands || [],
        spec: comp.spec,
        sbomName: comp.sbomName,
        ecuVersion: comp.ecuVersion,
        libs: comp.libs || [],
      });
      // The new SBOM produced fresh matches: drop the cached catalog so the Overview /
      // severity / priority refetch and immediately reflect what was just uploaded.
      window.VC.resetCatalog();
      // Re-pull the authoritative component list rather than optimistically prepending.
      // Re-uploading the SAME ECU build replaces it server-side, so a prepend would leave
      // a stale duplicate card (the same build shown twice with mismatched counts).
      let list = null;
      try { list = await window.VCApi.components(); } catch (_) { /* fall back below */ }
      if (list) {
        setComponents(list);
        setSelectedComp(list.find((c) => c.id === saved.id) || saved);
      } else {
        setComponents((l) => [saved, ...l.filter((c) => c.id !== saved.id)]); // de-dupe by id
        setSelectedComp(saved);
      }
      // The tenant now has at least one SBOM, so the ledger is meaningful — enable its nav.
      setHasSboms(true);
      // Land on the ledger so the user immediately sees the (deduped) vulnerabilities
      // their upload matched. Re-uploading the same SBOM yields the same single set;
      // a changed component surfaces its extra match here too.
      setView("ledger");
      setToast(`${saved.name} created`);
    } catch (e) {
      setToast(e.message || "Could not create component");
    }
    clearTimeout(window.__tcToast);
    window.__tcToast = setTimeout(() => setToast(null), 2600);
  }
  function applySbomUpdate({ libs, remediatedIds }) {
    const comp = updatingComp;
    if (!comp) return;
    setStatusMap((m) => {
      const n = { ...m };
      remediatedIds.forEach((id) => {
        n[id] = "remediated";
      });
      localStorage.setItem(LS_STATUS, JSON.stringify(n));
      return n;
    });
    // Persist each remediation to the DB (status column) so it sticks across reloads/devices.
    remediatedIds.forEach((id) => window.VCApi.setStatus(id, "remediated").catch(() => {}));
    const updated = {
      ...comp,
      libs,
      remediatedIds: Array.from(
        new Set([...(comp.remediatedIds || []), ...remediatedIds]),
      ),
      lastUpdated: new Date().toISOString().slice(0, 10),
    };
    setComponents((list) => {
      const n = list.map((c) => (c.id === comp.id ? updated : c));
      localStorage.setItem(LS_COMPONENTS, JSON.stringify(n));
      return n;
    });
    setSelectedComp(updated);
    setUpdatingComp(null);
    setToast(
      remediatedIds.length
        ? `${remediatedIds.length} CVE${remediatedIds.length > 1 ? "s" : ""} remediated`
        : "SBOM updated",
    );
    clearTimeout(window.__tcToast);
    window.__tcToast = setTimeout(() => setToast(null), 2600);
  }

  const accentHex = ACCENTS[t.accent] || ACCENTS.teal;

  if (!auth) {
    return (
      <div
        className="app"
        data-theme={t.theme}
        data-density={t.density}
        style={{ ["--accent"]: accentHex }}
      >
        <LoginScreen onLogin={login} theme={t.theme} />
        <TweaksUI t={t} setTweak={setTweak} />
      </div>
    );
  }

  if (!dataReady) {
    return (
      <div
        className="app"
        data-theme={t.theme}
        data-density={t.density}
        style={{ ["--accent"]: accentHex }}
      >
        <div
          style={{
            display: "grid",
            placeItems: "center",
            height: "100vh",
            gap: "16px",
            textAlign: "center",
          }}
        >
          <span
            className="btn-spin"
            style={{ width: 30, height: 30, borderWidth: 3 }}
          />
          <span style={{ opacity: 0.7 }}>
            Loading live intelligence from the database…
          </span>
        </div>
      </div>
    );
  }

  // The ledger lists a customer's own SBOM matches (scoped in LedgerController), so it's
  // hidden from the nav until they have something to show — admins always see it.
  // "Browse by company" stays admin-only as it rolls up across tenants.
  const NAV = [
    ["dashboard", "Overview", "dashboard"],
    ...((auth.isAdmin || hasSboms) ? [["ledger", "Vulnerability ledger", "ledger"]] : []),
    ...(auth.isAdmin ? [["browse", "Browse by company", "browse"]] : []),
    ["components", "Components", "package"],
    ...(auth.isAdmin ? [["admin", "Administration", "user"]] : []),
  ];
  const openCrit = shellCritical;

  return (
    <div
      className="app"
      data-theme={t.theme}
      data-density={t.density}
      style={{ ["--accent"]: accentHex }}
    >
      <div
        className={"nav-scrim" + (navOpen ? " is-open" : "")}
        onClick={() => setNavOpen(false)}
        aria-hidden="true"
      />
      <aside className={"sidebar" + (navOpen ? " is-open" : "")}>
        <div className="sidebar-top">
          <Logo size={18} withWord={false} />
          <span className="brand-word">
            VULNERABILITY<span className="brand-word-2">CORE</span>
          </span>
        </div>
        <div className="sidebar-rule" />
        <nav className="sidenav">
          {NAV.map(([k, label, icon]) => {
            const on =
              view === k ||
              (k === "ledger" && view === "detail") ||
              (k === "components" &&
                (view === "compdetail" || view === "newcomponent"));
            return (
              <button
                key={k}
                className={"sidenav-item" + (on ? " is-on" : "")}
                onClick={() => nav(k)}
              >
                <Icon name={icon} size={18} />
                <span>{label}</span>
              </button>
            );
          })}
        </nav>
        <div className="sidebar-foot">
          <div className="userbox">
            <span className="userbox-avatar">
              {auth.name
                .split(" ")
                .map((w) => w[0])
                .join("")}
            </span>
            <div className="userbox-id">
              <strong>{auth.name}</strong>
              <span>{auth.org}</span>
            </div>
            <button className="userbox-out" onClick={logout} title="Sign out">
              <Icon name="logout" size={16} />
            </button>
          </div>
          <div className="sidebar-rule" />
          <a
            href="https://www.cymobility.com"
            target="_blank"
            rel="noopener noreferrer"
            className="foot-cymo"
          >
            <img
              src={
                (t.theme === "light"
                  ? (window.__resources && window.__resources.cymoLogoLight) ||
                    "VulnerabilityCore/assets/cymobility-logo-light.png"
                  : (window.__resources && window.__resources.cymoLogo) ||
                    "VulnerabilityCore/assets/cymobility-logo.png") +
                // Cache-bust: the dark logo PNG was re-cropped, so bump this version
                // when either asset changes to force the browser to refetch it.
                "?v=2"
              }
              alt="CyMobility"
            />
          </a>
        </div>
      </aside>

      <div className="main">
        <header className="topbar">
          <button
            className="nav-toggle"
            onClick={() => setNavOpen((o) => !o)}
            aria-label="Toggle navigation"
            aria-expanded={navOpen}
          >
            <Icon name={navOpen ? "close" : "menu"} size={20} />
          </button>
          <div className="topbar-crumb">
            <span>VulnerabilityCore</span>
            <Icon name="chevR" size={9} className="crumb-sep" />
            <strong>
              {view === "detail"
                ? "Vulnerability detail"
                : view === "newcomponent"
                  ? "New component"
                  : view === "compdetail"
                    ? selectedComp?.name || "Component"
                    : NAV.find((n) => n[0] === view)?.[1]}
            </strong>
          </div>
          <div className="topbar-actions">
            {/* <div className="sync">
              <span className="sync-dot" />
              Synced · {window.VC.COVERAGE.lastSync}
            </div> */}
            {/* <button className="iconbtn" title="Alerts">
              <Icon name="bell" size={17} />
              <span className="iconbtn-badge">{openCrit}</span>
            </button> */}
            <button
              className="iconbtn iconbtn-theme"
              title="Toggle theme"
              onClick={() =>
                setTweak("theme", t.theme === "dark" ? "light" : "dark")
              }
            >
              <Icon name={t.theme === "dark" ? "sun" : "moon"} size={17} />
            </button>
          </div>
        </header>

        <div className="content">
          {view === "dashboard" && <Dashboard onOpen={openVuln} onNav={nav} statusMap={statusMap} />}
          {view === "ledger" && (
            <Ledger
              statusOf={statusOf}
              setStatus={setStatus}
              onOpen={openVuln}
              initialFilter={ledgerFilter}
            />
          )}
          {view === "browse" && auth.isAdmin && (
            <Browse
              statusMap={statusMap}
              onOpen={openVuln}
              onNav={nav}
            />
          )}
          {view === "admin" && auth.isAdmin && <AdminScreen />}
          {view === "components" && (
            <ComponentsScreen
              components={components}
              loading={!componentsLoaded}
              onNew={startWizard}
              onOpen={openComponent}
            />
          )}
          {view === "compdetail" &&
            (selectedComp ? (
              <ComponentDetail
                comp={selectedComp}
                statusOf={statusOf}
                onBack={() => nav("components")}
                onOpenVuln={openVuln}
                onUpdateSbom={setUpdatingComp}
              />
            ) : (
              <PageLoading label="Loading component…" />
            ))}
          {view === "newcomponent" && (
            <NewComponentWizard
              layout={t.wizardLayout}
              onCancel={() => nav("components")}
              onCreate={createComponent}
            />
          )}
          {view === "detail" &&
            (selected ? (
              <Detail
                v={selected}
                statusOf={statusOf}
                setStatus={setStatus}
                onBack={() => nav(detailFrom)}
                backLabel={
                  detailFrom === "compdetail"
                    ? selectedComp?.name || "Component"
                    : detailFrom === "dashboard"
                      ? "Overview"
                      : detailFrom === "browse"
                        ? "Browse"
                        : "Ledger"
                }
                onOpen={openVuln}
              />
            ) : (
              <PageLoading label="Loading vulnerability…" />
            ))}
        </div>
      </div>

      {toast && (
        <div className="toast">
          <Icon name="check" size={15} />
          {toast}
        </div>
      )}
      {updatingComp && (
        <UpdateSbomModal
          comp={updatingComp}
          onClose={() => setUpdatingComp(null)}
          onApply={applySbomUpdate}
        />
      )}
      <TweaksUI t={t} setTweak={setTweak} />
    </div>
  );
}

function TweaksUI({ t, setTweak }) {
  return (
    <TweaksPanel title="Tweaks">
      <TweakSection label="Theme" />
      <TweakRadio
        label="Mode"
        value={t.theme}
        options={["dark", "light"]}
        onChange={(v) => setTweak("theme", v)}
      />
      <TweakColor
        label="Accent"
        value={ACCENTS[t.accent]}
        options={[ACCENTS.teal, ACCENTS.blue, ACCENTS.amber]}
        onChange={(hex) =>
          setTweak(
            "accent",
            Object.keys(ACCENTS).find((k) => ACCENTS[k] === hex) || "teal",
          )
        }
      />
      <TweakSection label="Layout" />
      <TweakRadio
        label="Density"
        value={t.density}
        options={["regular", "compact"]}
        onChange={(v) => setTweak("density", v)}
      />
      <TweakSection label="Workflow" />
      <TweakRadio
        label="Wizard layout"
        value={t.wizardLayout}
        options={["rail", "stepper"]}
        onChange={(v) => setTweak("wizardLayout", v)}
      />
    </TweaksPanel>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
