(() => { const $ = (sel, root = document) => root.querySelector(sel); const STORE_KEY = "cockpit_present_decks_v2"; const STORY_SLIDES = [ { id: "disclaimer", kicker: "Important notice", title: "Disclaimer — please read first", anim: "fade", html: `
This OpenManage Cockpit is not an official Dell Technologies product, service, or offering. It is an independent ATC demonstration built to show what is possible when OpenManage Enterprise is combined with modern AI tooling.
Press → or Next to continue.
Datacenter Engineers asked us to help them see OME data faster and explain it with AI. This briefing shows the result — and why a cockpit like this adds value.
Requested by Jody van Dongen & Laurens Rammers · delivered by the FDE team (Mo & Bart — "Data Plumbers" 😉).
`, }, { id: "problem", kicker: "The ask", title: "OME is strong — operators still lose time", anim: "slide", html: `OME remains the management plane of record. The friction is around it: too many screens, slow answers to “what is wrong on this Service Tag?”, and AI that invents facts without live inventory.
OME, iDRAC, sheets, chat, tickets — same Service Tag, five places.
Warranty, firmware, and offline state are related — rarely one story.
Generic chatbots guess. Useful AI must start from OME.
Jody and Laurens (Datacenter Engineers) asked the FDE team to assist — build a practical ops lens on top of OME, not replace OME.
`, }, { id: "value", kicker: "Value", title: "Why this cockpit adds value", anim: "rise", html: `Map, inventory, compliance, warranty, fabric, and tickets around the same ST.
Power via OME jobs · live iDRAC Console wall (4/8) without tab sprawl.
Copilot answers from live OME facts — or says it does not know.
Tickets and live team / DM chat with files, avatars, and unread alerts.
Jody & Laurens own the estate; Mo & Bart ("Data Plumbers" 😉) deliver the surface.
OME API + BFF + grounded AI — a blueprint, not a Dell SKU.
Every app talks only to the Cockpit BFF. The BFF talks to OME (truth) and Open WebUI / vLLM (AI). Pulse lines show that path.

Browser never holds OME or AI secrets. Docker hosts Cockpit (:3090) and Open WebUI (:3080) on the portal.
`, }, { id: "apps", kicker: "Applications", title: "What you use day to day", anim: "rise", html: `One join key everywhere: Service Tag. Soft identity on open so Jody, Laurens, Mo, Bart (or Guest) act as themselves.
`, }, { id: "console-wall", kicker: "Remote ops", title: "Console wall · many iDRACs, one screen", anim: "zoom", html: `Operators asked to see systems without tab-hopping. The Console tab is a live iDRAC wall: network-aware, draggable, and embedded through Cockpit so browsers are not blocked by X-Frame-Options.
Pick wall size · Small / Medium density · Fullscreen popup per tile.
Fleet rail grouped by VLAN · drop onto slots · Auto-fill live BMCs.
VLAN · subnet · LIVE/OFF on every tile — know where you are looking.
/api/idrac-proxy/{id}/… strips framing headers · WebSocket bridge for HTML5 console.
OOB / BMC endpoints — never bare OS host IPs that cannot speak iDRAC.
Device list and connectivity come from the live fleet snapshot.
Offline servers with POWER N/A are common in the lab. From Servers KPI, inspector, or Quick Connect you can submit an OME POWER_CONTROL job — on, graceful off, or cycle — with confirm for destructive actions.
Server / iDRAC on map or KPI list
⏻ On · Off · Cycle via OME JobService
iDRAC HTML5 in-panel (proxied)
Next fleet poll updates powered state
Ops desk ticket if change needs a trail
Demo power with care — production change windows still belong in official OME / iDRAC process.
`, }, { id: "team-chat", kicker: "Collaboration", title: "Team chat · who you are, what you share", anim: "rise", html: `First popup on open: pick yourself (or Guest). Then Ops desk and Team chat share one soft identity — tickets, DMs, and files stay attributable without SSO.
Jody · Laurens · Mo · Bart · Guest — profile photo upload optional.
Shared ATC room and 1:1 chats · WebSocket live presence.
Drop files into chat · avatars on bubbles, presence, and the Team chip.
Badge on Ops|Team · room counters · browser notify when the tab is in the background.
One topbar control: tickets left, Team + who-you-are right — KPIs keep a single row.
Datacenter Engineers own rack & stack; FDEs are "Data Plumbers" 😉.
Ask → Cockpit enriches with live OME context → Open WebUI and/or vLLM → answer → act in map, reports, Console, or Ops.
Fleet question in Copilot
BFF attaches Service Tag facts
Open WebUI / vLLM completion
Still tied to the live map
Inspector · console · ticket
OME for truth · AI for speed · Cockpit for the operator experience.
`, }, { id: "origin", kicker: "How this started", title: "Datacenter Engineers set the need · Data FDEs delivered the surface", anim: "rise", html: `Jody van Dongen and Laurens Rammers (Datacenter Engineers — storage, servers, network, rack & stack, installs) defined the operational need and asked the FDE team to help. Together they produced a cockpit that sits on OME: topology, reports, network context, tickets, and AI — for ATC use and customer conversations, explicitly not a product SKU.
Engineered against live OME data; refined for demos and daily operations.
`, }, { id: "benefits", kicker: "Takeaways", title: "What customers walk away with", anim: "rise", html: `One place for connectivity, compliance, warranty, and hardware depth.
Claims trace to a Service Tag from OME.
AI drafts explanations; humans own change windows.
Datacenter Engineers and Data FDEs ("Data Plumbers" 😉) share Ops + Team chat.
OME + Docker BFF + grounded AI — repeatable.
Demo power with an explicit non-product disclaimer.
Not an official Dell product — verify production decisions in OME. Esc closes · F fullscreen.
`, }, ]; const TECH_SLIDES = [ { id: "disclaimer", kicker: "Important notice", title: "Disclaimer — please read first", anim: "fade", html: `This OpenManage Cockpit is not an official Dell Technologies product. Technical details describe an ATC demonstration architecture — not a supported Dell reference design SKU.
Press → for the technical deep dive.
How OME, the Cockpit BFF, Open WebUI, and vLLM are wired — and why that wiring keeps AI grounded.
/api/* contractsDelivered for Datacenter Engineers Jody & Laurens by Data FDEs Mo & Bart ("Data Plumbers" 😉).
`, }, { id: "arch-design", kicker: "Architecture design", title: "Reference design · containers & pulse paths", anim: "zoom", html: `Thin UI apps over a FastAPI BFF in Docker. OME is hardware truth. Open WebUI / vLLM are the AI plane.

The browser must never hold OME passwords. The Cockpit API is the only component allowed to call AI with enriched Service Tag context.
OME JSON → stable fleet nodes, NIC PortIds, compliance rows.
Fleet & report caches keep demos responsive.
Inject facts before completions — no invented BIOS levels.
POST /api/SessionService/Sessions → token/api/fleet — topology + ConnectionStateRule: if OME did not return it, UI and Copilot must not invent it.
`, }, { id: "ai-plane", kicker: "AI plane", title: "Copilot → Open WebUI / vLLM", anim: "slide", html: `Copilot drawer
BFF + OME facts
Docker · :3080
OpenAI-compatible
Back to the drawer
Two experiences on purpose: grounded Copilot inside Cockpit, and full OpenManage AI (Open WebUI) beside it.
`, }, { id: "portal-host", kicker: "Deployment", title: "What runs on the portal", anim: "slide", html: `Static UI + FastAPI BFF · :3090 · ops.db volume
Open WebUI · OpenManage AI · :3080

HTTPS management plane · session API
FDE inference · OpenAI schema
Users hit the portal; the portal speaks to OME and AI backends.
`, }, { id: "apis", kicker: "API surface", title: "Contracts behind the apps", anim: "rise", html: `Cached device graph for canvas & KPIs.
OME JobService POWER_CONTROL · on / off / cycle.
Same-origin HTML + WebSocket bridge · strips XFO.
Rooms, DMs, files, avatars · live presence & messages.
Grounded completions · model list.
Fabric ports · racks · VLANs.
Browsers refuse to iframe most iDRAC UIs (X-Frame-Options: SAMEORIGIN|DENY). Cockpit proxies the session so the Console wall and in-panel viewer stay on :3090.
Gzip passthrough · cookie Path rewrite · CSP frame-ancestors *.
Patches fetch / XHR / WebSocket so absolute /sysmgmt paths stay on the proxy prefix.
Only fleet management IPs · prefers idrac_ip · rejects bare OS hosts.
Operators still authenticate to iDRAC with their own credentials inside the embed.
`, }, { id: "trust", kicker: "Trust & data", title: "Boundaries to call out", anim: "fade", html: `/data/team-chat)Not an official Dell product. Esc closes · F fullscreen.
`, }, ]; const BUILTIN_VERSION = 10; const BUILTIN_DECKS = [ { id: "story", name: "Customer story", description: "Customer briefing — Console, power, Team chat, roles, value", builtin: true, slides: structuredClone(STORY_SLIDES), }, { id: "technical", name: "Technical architecture", description: "Engineer deep dive — design, BFF, APIs, Team chat, trust", builtin: true, slides: structuredClone(TECH_SLIDES), }, ]; let store = loadStore(); let activeId = store.activeId || "story"; let slides = currentDeck().slides; let index = 0; let mode = "mid"; let theme = (() => { try { if (!localStorage.getItem("present_theme_migrated_v1")) { localStorage.setItem("cockpit_present_theme", "dark"); localStorage.setItem("present_theme_migrated_v1", "1"); } } catch (_) {} return localStorage.getItem("cockpit_present_theme") || "dark"; })(); let editing = false; let open = false; function builtinTemplate(id) { return BUILTIN_DECKS.find((d) => d.id === id) || null; } function defaultStore() { return { activeId: "story", decks: BUILTIN_DECKS.map((d) => ({ id: d.id, name: d.name, description: d.description, builtin: true, version: BUILTIN_VERSION, slides: structuredClone(d.slides), })), }; } function loadStore() { try { const raw = JSON.parse(localStorage.getItem(STORE_KEY) || "null"); if (raw && Array.isArray(raw.decks) && raw.decks.length) { // ensure builtin decks exist (merge missing) const byId = new Map(raw.decks.map((d) => [d.id, d])); for (const b of BUILTIN_DECKS) { if (!byId.has(b.id)) { raw.decks.unshift({ id: b.id, name: b.name, description: b.description, builtin: true, version: BUILTIN_VERSION, slides: structuredClone(b.slides), }); } else { const cur = byId.get(b.id); cur.builtin = true; cur.name = b.name; cur.description = b.description; if ((cur.version || 0) < BUILTIN_VERSION) { cur.slides = structuredClone(b.slides); cur.version = BUILTIN_VERSION; } } } if (!raw.activeId || !raw.decks.some((d) => d.id === raw.activeId)) { raw.activeId = raw.decks[0].id; } return raw; } } catch (_) {} // migrate old single-deck keys if present try { for (const k of ["cockpit_present_slides_v2", "cockpit_present_slides_v1"]) { const old = JSON.parse(localStorage.getItem(k) || "null"); if (Array.isArray(old) && old.length) { const s = defaultStore(); s.decks.find((d) => d.id === "technical").slides = old; s.activeId = "technical"; return s; } } } catch (_) {} return defaultStore(); } function saveStore() { // sync current slides into store const deck = store.decks.find((d) => d.id === activeId); if (deck) deck.slides = slides; store.activeId = activeId; localStorage.setItem(STORE_KEY, JSON.stringify(store)); } function currentDeck() { return store.decks.find((d) => d.id === activeId) || store.decks[0]; } function selectDeck(id) { if (!store.decks.some((d) => d.id === id)) return; // persist current const prev = store.decks.find((d) => d.id === activeId); if (prev) prev.slides = slides; activeId = id; slides = currentDeck().slides; index = 0; editing = false; saveStore(); render(); } function createDeck() { const name = prompt("New deck name", "Custom deck"); if (!name) return; const id = `custom-${Date.now()}`; const deck = { id, name: name.trim().slice(0, 64), description: "Custom presentation", builtin: false, slides: [ { id: "slide-1", kicker: "Custom", title: name.trim().slice(0, 64), anim: "slide", html: 'Write your first slide. Use Edit to change content.
', }, ], }; store.decks.push(deck); selectDeck(id); } function duplicateDeck() { const src = currentDeck(); const id = `custom-${Date.now()}`; store.decks.push({ id, name: `${src.name} (copy)`.slice(0, 64), description: src.description || "", builtin: false, slides: structuredClone(src.slides), }); selectDeck(id); } function deleteDeck() { const d = currentDeck(); if (d.builtin) { alert("Built-in decks cannot be deleted. Reset restores their default slides."); return; } if (store.decks.length <= 1) { alert("Keep at least one deck."); return; } if (!confirm(`Delete deck “${d.name}”?`)) return; store.decks = store.decks.filter((x) => x.id !== d.id); activeId = store.decks[0].id; slides = currentDeck().slides; index = 0; saveStore(); render(); } function resetCurrentDeck() { const d = currentDeck(); const tpl = builtinTemplate(d.id); if (tpl) { if (!confirm(`Reset “${d.name}” to the built-in default slides?`)) return; d.slides = structuredClone(tpl.slides); d.name = tpl.name; d.description = tpl.description; d.version = BUILTIN_VERSION; } else { if (!confirm(`Clear custom deck “${d.name}” to a single blank slide?`)) return; d.slides = [ { id: "slide-1", kicker: "Custom", title: d.name, anim: "slide", html: 'Blank deck — edit this slide.
', }, ]; } slides = d.slides; index = 0; editing = false; saveStore(); render(); } function escapeHtml(s) { return String(s ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function fileToDataUrl(file) { return new Promise((resolve, reject) => { const r = new FileReader(); r.onload = () => resolve(r.result); r.onerror = () => reject(new Error("Failed to read " + file.name)); r.readAsDataURL(file); }); } async function loadJsZip() { if (window.JSZip) return window.JSZip; await new Promise((resolve, reject) => { const s = document.createElement("script"); s.src = "https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"; s.onload = resolve; s.onerror = () => reject(new Error("Could not load JSZip for PPTX import")); document.head.appendChild(s); }); return window.JSZip; } async function slidesFromPptx(file) { const JSZip = await loadJsZip(); const zip = await JSZip.loadAsync(file); const media = Object.keys(zip.files) .filter((p) => /^ppt\/media\//i.test(p) && !zip.files[p].dir) .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })); const out = []; let n = 0; for (const path of media) { const lower = path.toLowerCase(); if (!/\.(png|jpe?g|gif|webp|bmp)$/i.test(lower)) continue; n += 1; const blob = await zip.files[path].async("blob"); const type = blob.type || (lower.endsWith(".png") ? "image/png" : "image/jpeg"); const dataUrl = await new Promise((resolve, reject) => { const r = new FileReader(); r.onload = () => resolve(r.result); r.onerror = reject; r.readAsDataURL(new Blob([blob], { type })); }); out.push({ id: `pptx-${Date.now()}-${n}`, kicker: "Imported · PPTX", title: `Slide ${n}`, anim: "fade", layout: "image", image: dataUrl, html: "", }); } if (!out.length) { throw new Error("No images found inside this PPTX. Export slides as images or use a PPTX with embedded pictures."); } return out; } async function importMediaFiles(files) { const added = []; for (const file of files) { const name = (file.name || "").toLowerCase(); if (name.endsWith(".pptx") || file.type.includes("presentationml")) { added.push(...(await slidesFromPptx(file))); } else if (file.type.startsWith("image/") || /\.(png|jpe?g|gif|webp|bmp)$/i.test(name)) { const dataUrl = await fileToDataUrl(file); added.push({ id: `img-${Date.now()}-${added.length}`, kicker: "Imported · image", title: file.name.replace(/\.[^.]+$/, ""), anim: "fade", layout: "image", image: dataUrl, html: "", }); } } if (!added.length) throw new Error("No supported files (use .pptx or images)."); slides.push(...added); index = slides.length - added.length; saveStore(); render(); } function ensureDom() { if ($("#present-root")) return; const root = document.createElement("div"); root.id = "present-root"; root.className = "present-root hidden"; root.setAttribute("aria-hidden", "true"); root.innerHTML = `Write your content here.
', }); index += 1; saveStore(); editing = true; render(); }); $("#pe-del", root)?.addEventListener("click", () => { if (slides.length <= 1) { alert("Keep at least one slide."); return; } if (!confirm("Delete this slide?")) return; slides.splice(index, 1); index = Math.min(index, slides.length - 1); saveStore(); render(); }); } function renderDeckSelect() { const sel = $("#present-deck"); if (!sel) return; sel.innerHTML = store.decks .map( (d) => `` ) .join(""); const desc = $("#present-deck-desc"); if (desc) desc.textContent = currentDeck().description || ""; $("#present-deck-del")?.toggleAttribute("disabled", !!currentDeck().builtin); } function render() { ensureDom(); const root = $("#present-root"); const stage = $("#present-stage"); const slideEl = $("#present-slide"); const s = slides[index] || slides[0]; if (!s || !slideEl) return; root.classList.toggle("hidden", !open); root.setAttribute("aria-hidden", open ? "false" : "true"); document.body.classList.toggle("present-open", open); stage?.classList.toggle("mode-mid", mode === "mid"); stage?.classList.toggle("mode-full", mode === "full"); stage?.classList.toggle("theme-light", theme === "light"); stage?.classList.toggle("theme-dark", theme === "dark"); const setBtnState = (id, on) => { const el = $(id); if (!el) return; el.classList.toggle("primary", on); el.classList.toggle("ghost", !on); }; setBtnState("#present-mode-mid", mode === "mid"); setBtnState("#present-mode-full", mode === "full"); setBtnState("#present-theme-light", theme === "light"); setBtnState("#present-theme-dark", theme === "dark"); setBtnState("#present-edit", editing); renderDeckSelect(); slideEl.classList.remove("enter", "anim-fade", "anim-zoom", "anim-rise", "anim-slide"); void slideEl.offsetWidth; const anim = s.anim || "slide"; if (s.layout === "image" && s.image) { slideEl.classList.add("is-image"); slideEl.innerHTML = `${escapeHtml(s.kicker || "Imported")}
${escapeHtml(s.kicker)}