// api.jsx — React hook aliases, API base + client, and the static defaults every page falls back to. // Loaded from index.html as an ordered `text/babel` classic script (position 1 of 8). // Classic scripts share one global scope — see docs/engineering/Patterns.md §9. const { useState, useEffect, useCallback, useId } = React; // ─── Constants ─────────────────────────────────────────────────────────────── const API_BASE = window.location.origin + "/api/v1"; // Fallback MDM URL — used only until `/site-config` resolves (see app.jsx) or // if it never does. `/site-config`'s mdm_url is the source of truth (#842): // deriving the MDM host from this page's hostname guessed wrong whenever the // platform and the MDM landing host don't share a naming pattern (on // app.zensys.app it produced app-mdm.zensys.app). The fallback is this page's // own origin: an MDM link then points back at the platform, which is // unhelpful but never names a host that doesn't exist. const MDM_URL_FALLBACK = window.location.origin; const PLANS = [ { id: "free", name: "Free", devices: "Up to 5 devices", price: 0, currency: "€", isFree: true, features: [ "Full MDM management", "Fleet dashboard & analytics", "Declarative Device Management (DDM)", "Osquery telemetry & ad-hoc queries", "Self-hosted or cloud-hosted", "Community support", ], cta: "Get started — free forever", }, { id: "starter", name: "Starter", devices: "Up to 25 devices", price: 3, currency: "€", features: [ "Everything in Free", "25 device limit", "14-day free trial", "Community support", ], excluded: ["IdP login / Platform SSO"], cta: "Start free trial", }, { id: "business", name: "Business", devices: "Up to 250 devices", price: 4, currency: "€", popular: true, features: [ "Everything in Starter", "IdP login / Platform SSO", "Telemetry / SIEM forwarding", "Auto-deploy rules", "API access", "Priority email & chat support", ], cta: "Start free trial", }, { id: "enterprise", name: "Enterprise", devices: "Unlimited devices", price: 5, currency: "€", features: [ "Everything in Business", "Dedicated infrastructure", "SLA guarantee", "Onboarding call", "Custom integrations", "Dedicated support & account manager", ], cta: "Start free trial", }, ]; // ─── API Client ────────────────────────────────────────────────────────────── async function apiCall(path, opts = {}, _retry = false) { const token = localStorage.getItem("zs_token"); const headers = { "Content-Type": "application/json", ...opts.headers }; if (token) headers["Authorization"] = `Bearer ${token}`; const res = await fetch(`${API_BASE}${path}`, { ...opts, headers }); if (res.status === 401) { localStorage.removeItem("zs_token"); window.location.hash = "#/login"; throw new Error("Session expired"); } // H13 — Rate-limit and transient-error retry (once only) if (!_retry && res.status === 429) { const wait = Math.min(parseInt(res.headers.get("Retry-After") || "5", 10), 60); console.warn(`Rate limited — retrying in ${wait}s`); await new Promise(r => setTimeout(r, wait * 1000)); return apiCall(path, opts, true); } if (!_retry && res.status === 503) { console.warn("Server temporarily unavailable; retrying"); await new Promise(r => setTimeout(r, 3000)); return apiCall(path, opts, true); } // L15b — Safe JSON parsing: fall back to raw text on non-JSON responses let data; const rawText = await res.clone().text().catch(() => ""); try { data = await res.json(); } catch (_) { throw new Error(rawText || "Request failed (non-JSON response)"); } if (!res.ok) throw new Error(data.error || "Request failed"); return data; } // ─── Landing Page (Company Overview) ───────────────────────────────────────── // Default fallback products (shown if API returns empty or errors) const DEFAULT_PRODUCTS = [ { slug: "mdm", name: "ZenMDM", tagline: "Lean MDM for macOS, Linux & Android", description: "Enroll, configure, and manage your fleet across macOS, Linux, and Android. Push profiles, distribute packages and scripts, enforce policies with DDM, surface security changes, and forward telemetry to your SIEM — by UI, CLI, or Terraform.", status: "available", tags: "MDM enrollment,Configuration profiles,Package & script deployment,DDM policies,IdP login / SSO,Osquery telemetry,Security change alerts,GitOps & CLI,Linux & Android", color: "#22c55e", }, { slug: "assets", name: "ZenAssets", tagline: "Asset Management", description: "Enterprise-grade asset management. Tie it together with the MDM and anything else on the same network.", status: "coming_soon", tags: "Real-time dashboards,Custom alerts,Anomaly detection,Fleet-wide metrics,Integration APIs", color: "#3b82f6", }, ]; function productHref(p, mdmUrl) { if (p.status !== "available") return null; if (p.slug === "mdm") return mdmUrl; return null; } function productFeatures(p) { if (!p.tags) return []; return p.tags.split(",").map(t => t.trim()).filter(Boolean); } function productStatusLabel(status) { if (status === "available") return "Available"; if (status === "coming_soon") return "Coming soon"; if (status === "beta") return "Beta"; return status; } // ─── Dynamic Navigation (consumed from /api/v1/site-config) ───────────────── // Falls back to hardcoded defaults when no nav items are configured in the admin. const DEFAULT_NAV = [ { label: "MDM", href: "#/mdm", status: "active" }, { label: "Assets", href: "#", status: "coming_soon" }, { label: "Community", href: "#/community", status: "active" }, { label: "Updates", href: "#/feed", status: "active" }, { label: "Docs", href: "#/docs", status: "active" }, ];