(() => { 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: ` `, }, { id: "agenda", kicker: "Customer briefing", title: "What we will cover", anim: "rise", html: `

ATC admins 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.

  1. Why — the gap around OME and why AI alone is not enough.
  2. Architecture — one design: Browser → Cockpit BFF → OME / AI (with logos).
  3. Apps — map, network, reports, ops desk, copilots.
  4. People — who asked, who built, who runs it.
  5. Value — what customers can take away.

Requested by Jody van Dongen & Laurens Rammers · delivered by the FDE team (Mohamed El Kadi & Bart Sjerps).

`, }, { 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.

Console sprawl

OME, iDRAC, sheets, chat, tickets — same Service Tag, five places.

Context loss

Warranty, firmware, and offline state are related — rarely one story.

Ungrounded AI

Generic chatbots guess. Useful AI must start from OME.

Jody and Laurens 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: `

One Service Tag story

Map, inventory, compliance, warranty, fabric, and tickets around the same ST.

AI that cites the fleet

Copilot answers from live OME facts — or says it does not know.

Faster briefings

Click a chart → named systems → hand off in Ops desk.

Admin ↔ Data FDE

Clear roles: ATC admins own the estate; Data FDEs deliver the AI/ops surface.

Demo = daily tool

Same stack you present is the stack you can keep using after the meeting.

Copyable pattern

OME API + BFF + grounded AI — a blueprint, not a Dell SKU.

`, }, { id: "arch-design", kicker: "Architecture design", title: "One design · how everything connects", anim: "zoom", html: `

Every app talks only to the Cockpit BFF. The BFF talks to OME (truth) and Open WebUI / vLLM (AI). Pulse lines show that path.

OperatorBrowser · HTTPS · no secrets
MapTopology
ReportsAnalytics
Cockpit UIDrawers · KPIs
NetworkFabric · Racks
Ops / AITickets · Copilot
BFF · OME Cockpit API Docker · FastAPI · secrets · cache · ground AI · :3090
Dell OMETruth · inventory · warranty
Open WebUIOpenManage AI · :3080
vLLMFDE inference host
Blue pulse · app / API traffic Amber pulse · BFF spine Packets · live request 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: `
Live mapFleet topology · power · inspector
NetworkFabric · 42U racks · VLANs
ReportsCompliance · warranty analytics
Ops deskAdmin ↔ Data FDE tickets
CopilotGrounded chat on Service Tags
OpenManage AIFull Open WebUI beside the cockpit

One join key everywhere: Service Tag. No second inventory source.

`, }, { id: "ai", kicker: "AI + OME", title: "AI that starts from OME facts", anim: "slide", html: `

Ask → Cockpit enriches with live OME context → Open WebUI and/or vLLM → answer → act in map, reports, or Ops.

01Ask

Fleet question in Copilot

02Enrich

BFF attaches Service Tag facts

03Route

Open WebUI / vLLM completion

04Answer

Still tied to the live map

05Act

Inspector · report · ticket

OME for truth · AI for speed · Cockpit for the operator experience.

`, }, { id: "origin", kicker: "How this started", title: "Admins set the need · Data FDEs delivered the surface", anim: "rise", html: `

Jody van Dongen and Laurens Rammers 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.

ATC Admins · the ask

Data FDEs · the delivery

Engineered against live OME data; refined for demos and daily operations.

`, }, { id: "benefits", kicker: "Takeaways", title: "What customers walk away with", anim: "rise", html: `

Clarity

One place for connectivity, compliance, warranty, and hardware depth.

Credibility

Claims trace to a Service Tag from OME.

Speed

AI drafts explanations; humans own change windows.

Collaboration

Admins and Data FDEs share one Ops language.

Pattern

OME + Docker BFF + grounded AI — repeatable.

Honesty

Demo power with an explicit non-product disclaimer.

`, }, { id: "close", kicker: "Next steps", title: "See it live", anim: "zoom", html: `
  1. Pick a Service Tag on the map and open the inspector.
  2. Open Reports · Analytics and click a colored segment.
  3. Ask Copilot a question that must cite Service Tags.
  4. Switch to Technical architecture for API and trust-boundary depth.

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: ` `, }, { id: "tech-agenda", kicker: "Technical deck", title: "Architecture for engineers", anim: "rise", html: `

How OME, the Cockpit BFF, Open WebUI, and vLLM are wired — and why that wiring keeps AI grounded.

  1. Reference design with logos & pulse paths
  2. Why a BFF (secrets, normalize, ground AI)
  3. OME session / cache behaviour
  4. AI completion path
  5. Portal containers & key /api/* contracts

Delivered for ATC admins Jody & Laurens by Data FDEs Mo & Bart.

`, }, { 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.

OperatorBrowser · HTTPS · no secrets
MapTopology
ReportsAnalytics
Cockpit UIDrawers · KPIs
NetworkFabric · Racks
Ops / AITickets · Copilot
BFF · OME Cockpit API Docker · FastAPI · secrets · cache · ground AI · :3090
Dell OMETruth · inventory · warranty
Open WebUIOpenManage AI · :3080
vLLMFDE inference host
Blue pulse · app / API traffic Amber pulse · BFF spine Packets · live request path
`, }, { id: "why-bff", kicker: "Design choice", title: "Why the BFF sits in the middle", anim: "rise", html: `

The browser must never hold OME passwords. The Cockpit API is the only component allowed to call AI with enriched Service Tag context.

Normalize

OME JSON → stable fleet nodes, NIC PortIds, compliance rows.

Cache

Fleet & report caches keep demos responsive.

Ground AI

Inject facts before completions — no invented BIOS levels.

`, }, { id: "ome-plane", kicker: "Data plane · OME", title: "OME remains the only hardware truth", anim: "slide", html: `

Session lifecycle

  1. POST /api/SessionService/Sessions → token
  2. Devices · power · baselines · warranties
  3. InventoryDetails (memory, NICs, disks, …)
  4. DELETE session when the batch ends

Cached for the UI

Rule: 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: `
01Browser

Copilot drawer

02/api/chat

BFF + OME facts

03aOpen WebUI

Docker · :3080

03bvLLM

OpenAI-compatible

04Reply

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: `

ome-cockpit

Static UI + FastAPI BFF · :3090 · ops.db volume

openmanage

Open WebUI · OpenManage AI · :3080

OME appliance

HTTPS management plane · session API

vLLM (optional)

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: `

/api/fleet

Cached device graph for canvas & KPIs.

/api/devices/…

Inventory, landscape, expanded NICs.

/api/reports/*

Analytics, firmware, warranty, brief.

/api/chat · /api/models

Grounded completions · model list.

/api/tickets

Ops desk — local SQLite, not OME write-back.

/api/network/*

Fabric ports · racks · VLANs.

`, }, { id: "trust", kicker: "Trust & data", title: "Boundaries to call out", anim: "fade", html: `

From OME (read)

Cockpit-local (write)

Security notes

`, }, { id: "close", kicker: "Next steps", title: "Back to the live estate", anim: "zoom", html: `
  1. Return to the map and pick a Service Tag.
  2. Open Reports and click a compliance segment.
  3. Ask Copilot a question that must cite that ST.
  4. Use Customer story for business / ops audiences.

Not an official Dell product. Esc closes · F fullscreen.

`, }, ]; const BUILTIN_VERSION = 8; const BUILTIN_DECKS = [ { id: "story", name: "Customer story", description: "Customer briefing — ask, architecture, apps, value", builtin: true, slides: structuredClone(STORY_SLIDES), }, { id: "technical", name: "Technical architecture", description: "Engineer deep dive — design, BFF, APIs, 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 = `
`; document.body.appendChild(root); $("#present-close", root)?.addEventListener("click", closePresent); root.querySelector("[data-present-close]")?.addEventListener("click", closePresent); $("#present-prev", root)?.addEventListener("click", () => go(-1)); $("#present-next", root)?.addEventListener("click", () => go(1)); $("#present-mode-mid", root)?.addEventListener("click", () => setMode("mid")); $("#present-mode-full", root)?.addEventListener("click", () => setMode("full")); $("#present-theme-light", root)?.addEventListener("click", () => setTheme("light")); $("#present-theme-dark", root)?.addEventListener("click", () => setTheme("dark")); $("#present-edit", root)?.addEventListener("click", toggleEdit); $("#present-deck", root)?.addEventListener("change", (e) => selectDeck(e.target.value)); $("#present-deck-new", root)?.addEventListener("click", createDeck); $("#present-deck-dup", root)?.addEventListener("click", duplicateDeck); $("#present-deck-del", root)?.addEventListener("click", deleteDeck); $("#present-reset", root)?.addEventListener("click", resetCurrentDeck); $("#present-export", root)?.addEventListener("click", () => { const deck = currentDeck(); const payload = { id: deck.id, name: deck.name, description: deck.description, slides, }; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = `ome-cockpit-deck-${deck.id}.json`; a.click(); URL.revokeObjectURL(a.href); }); $("#present-import", root)?.addEventListener("click", () => $("#present-import-file")?.click()); $("#present-import-file", root)?.addEventListener("change", async (e) => { const file = e.target.files?.[0]; if (!file) return; try { const text = await file.text(); const data = JSON.parse(text); const list = Array.isArray(data) ? data : data.slides; if (!Array.isArray(list) || !list.length) throw new Error("Expected slides array or { slides: [...] }"); slides = list.map((s, i) => ({ id: s.id || `slide-${i + 1}`, kicker: s.kicker || "", title: s.title || `Slide ${i + 1}`, html: s.html || s.body || "", anim: s.anim || "slide", })); if (data.name && !currentDeck().builtin) currentDeck().name = String(data.name).slice(0, 64); if (data.description && !currentDeck().builtin) currentDeck().description = String(data.description); index = 0; saveStore(); render(); } catch (err) { alert("Import failed: " + err.message); } e.target.value = ""; }); $("#present-add-media", root)?.addEventListener("click", () => $("#present-media-file")?.click()); $("#present-media-file", root)?.addEventListener("change", async (e) => { const files = [...(e.target.files || [])]; if (!files.length) return; try { await importMediaFiles(files); } catch (err) { alert("Media import failed: " + err.message); } e.target.value = ""; }); $("#pe-save", root)?.addEventListener("click", () => { const s = slides[index]; if (!s) return; s.kicker = $("#pe-kicker")?.value || ""; s.title = $("#pe-title")?.value || ""; s.html = $("#pe-html")?.value || ""; saveStore(); editing = false; render(); }); $("#pe-add", root)?.addEventListener("click", () => { slides.splice(index + 1, 0, { id: `custom-${Date.now()}`, kicker: "Custom", title: "New slide", anim: "slide", html: '

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.title ||

${escapeHtml(s.kicker || "Imported")}

${escapeHtml(s.title || "")}

`; } else { slideEl.classList.remove("is-image"); slideEl.innerHTML = `

${escapeHtml(s.kicker)}

${escapeHtml(s.title)}

${s.html}
`; } slideEl.classList.add("enter", `anim-${anim}`); const prog = $("#present-progress"); if (prog) prog.textContent = `${currentDeck().name} · ${index + 1} / ${slides.length}`; const bar = $("#present-bar"); if (bar) bar.style.width = `${((index + 1) / slides.length) * 100}%`; const dots = $("#present-dots"); if (dots) { dots.innerHTML = slides .map( (_, i) => `` ) .join(""); dots.querySelectorAll("[data-dot]").forEach((btn) => { btn.addEventListener("click", () => { index = Number(btn.dataset.dot); render(); }); }); } $("#present-prev").disabled = index <= 0; const nextBtn = $("#present-next"); if (nextBtn) nextBtn.textContent = index >= slides.length - 1 ? "Done" : "Next"; const panel = $("#present-edit-panel"); panel?.classList.toggle("hidden", !editing); $("#present-edit")?.classList.toggle("is-on", editing); if (editing) { $("#pe-kicker").value = s.kicker || ""; $("#pe-title").value = s.title || ""; $("#pe-html").value = s.html || ""; } } function go(delta) { if (delta > 0 && index >= slides.length - 1) { closePresent(); return; } index = Math.max(0, Math.min(slides.length - 1, index + delta)); render(); } function setTheme(next) { theme = next === "dark" ? "dark" : "light"; try { localStorage.setItem("cockpit_present_theme", theme); } catch (_) {} render(); } async function setMode(next) { mode = next === "full" ? "full" : "mid"; const stage = $("#present-stage"); if (mode === "full") { try { if (stage && !document.fullscreenElement) await stage.requestFullscreen(); } catch (_) {} } else if (document.fullscreenElement) { try { await document.exitFullscreen(); } catch (_) {} } render(); } function toggleEdit() { editing = !editing; render(); } function openPresent() { ensureDom(); open = true; index = 0; editing = false; mode = "mid"; if (!localStorage.getItem("cockpit_present_theme")) theme = "dark"; slides = currentDeck().slides; render(); } function closePresent() { open = false; editing = false; saveStore(); if (document.fullscreenElement) { document.exitFullscreen().catch(() => {}); } render(); } document.addEventListener("fullscreenchange", () => { if (!open) return; mode = document.fullscreenElement ? "full" : "mid"; render(); }); document.addEventListener("keydown", (e) => { if (!open) return; if (e.key === "Escape") { e.preventDefault(); closePresent(); return; } if (editing && e.target?.closest?.("#present-edit-panel")) return; if (e.target?.closest?.("#present-deck")) return; if (e.key === "ArrowRight" || e.key === "PageDown" || e.key === " ") { e.preventDefault(); go(1); } else if (e.key === "ArrowLeft" || e.key === "PageUp") { e.preventDefault(); go(-1); } else if (e.key === "f" || e.key === "F") { e.preventDefault(); setMode(mode === "full" ? "mid" : "full"); } else if (e.key === "e" || e.key === "E") { e.preventDefault(); toggleEdit(); } }); function init() { $("#btn-present")?.addEventListener("click", openPresent); window.cockpitPresent = { open: openPresent, close: closePresent, decks: () => store.decks }; } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); else init(); })();