2026-06-25 00:28:23 +00:00
"""Build live presentation deck from cluster snapshot + registry."""
from __future__ import annotations
import json
from typing import Any
from node_registry import NODE_REGISTRY
from topology_views import build_all_topologies
from workload import build_workload_payload
def _status_badge ( level : str ) -> str :
return { "ok" : "● Online" , "warn" : "◐ Degraded" , "down" : "○ Offline" , "unknown" : "? Unknown" } . get ( level , level )
def _slide ( slide_id : str , title : str , subtitle : str , bullets : list [ str ], ** extra : Any ) -> dict [ str , Any ]:
return { "id" : slide_id , "title" : title , "subtitle" : subtitle , "bullets" : bullets , ** extra }
2026-06-27 19:37:50 +00:00
def _node_slide ( nid : str , extra_bullets : list [ str ] | None = None ) -> dict [ str , Any ]:
reg = NODE_REGISTRY . get ( nid , {})
bullets : list [ str ] = []
if reg . get ( "description" ):
bullets . append ( reg [ "description" ])
bullets . append ( f "VM: { reg . get ( 'vm' , '?' ) } (VMID { reg . get ( 'vmid' , '?' ) } ) @ { reg . get ( 'ip' , '?' ) } " )
for link in reg . get ( "links" , [])[: 5 ]:
bullets . append ( f " { link . get ( 'label' , 'Link' ) } : { link . get ( 'url' , '' ) } " )
for ep in reg . get ( "endpoints" , [])[: 6 ]:
bullets . append ( f " { ep . get ( 'name' , '?' ) } : { ep . get ( 'host' , '?' ) } : { ep . get ( 'port' , '?' ) } " )
if extra_bullets :
bullets . extend ( extra_bullets )
kind = "gpu" if nid == "gpu" else "command" if nid == "command" else "zone"
if nid in ( "openmetadata" , "elastic" ):
kind = "architecture"
return _slide (
f "node- { nid } " ,
reg . get ( "label" , nid ),
f " { reg . get ( 'role' , 'service' ) . upper () } · { reg . get ( 'vm' , '' ) } " ,
bullets [: 14 ],
kind = kind ,
)
2026-06-25 00:28:23 +00:00
def build_presentation_payload ( snap : dict [ str , Any ]) -> dict [ str , Any ]:
workload = build_workload_payload ( snap )
topologies = workload . get ( "topologies" ) or build_all_topologies ( snap )
totals = workload . get ( "totals" , {})
zones = workload . get ( "zones" , [])
gpu = workload . get ( "gpu" , {})
etl = snap . get ( "etl" , {})
hadoop = snap . get ( "hadoop" , {})
objectscale = snap . get ( "objectscale" , {})
command = snap . get ( "command_center" , {})
2026-06-27 19:37:50 +00:00
databases = snap . get ( "databases" , {})
docker = snap . get ( "docker" , {})
lakehouse = snap . get ( "lakehouse" , {})
governance = snap . get ( "governance" ) or {}
2026-06-25 00:28:23 +00:00
slides : list [ dict [ str , Any ]] = []
slides . append ( _slide (
"title" ,
"Dell ATC Data Lab" ,
2026-06-27 19:37:50 +00:00
"Live infrastructure presentation — Command Center" ,
2026-06-25 00:28:23 +00:00
[
f "Snapshot: { snap . get ( 'ts' , 'now' ) } " ,
f "Pipeline: { 'ACTIVE' if totals . get ( 'pipeline_active' ) else 'INACTIVE' } " ,
f "Apps running: { totals . get ( 'apps_running' , 0 ) } / { totals . get ( 'apps_total' , 0 ) } " ,
2026-06-27 19:37:50 +00:00
f "CDC connectors: { totals . get ( 'connectors' , 0 ) } · Source DBs: 5 engines on db02" ,
2026-06-25 00:28:23 +00:00
f "LLM: { gpu . get ( 'model' ) or 'offline' } ( { gpu . get ( 'gpu_count' , 0 ) } × V100)",
2026-06-27 19:37:50 +00:00
"Command Center → http://10.0.21.33/ · Data Platform tab → Presentation" ,
2026-06-25 00:28:23 +00:00
],
kind = "hero" ,
))
slides . append ( _slide (
"mission" ,
"Mission" ,
"End-to-end modern data platform on Dell infrastructure" ,
[
2026-06-27 19:37:50 +00:00
"Ingest CDC from PostgreSQL, MySQL, MongoDB (+ Cassandra & Neo4j for analytics)" ,
"Stream via Kafka & Debezium into Spark, Trino & Iceberg on the lakehouse" ,
"Land curated data on ObjectScale S3 — federated SQL with Trino" ,
"Govern with OpenMetadata — catalog, lineage, PII classification" ,
"Search & observe via Elasticsearch/Kibana · BI via Superset" ,
"Parallel 9-node Hadoop HDFS cluster for batch workloads" ,
"GPU lab (4× V100) powers autonomous agents with local vLLM inference" ,
"This Command Center orchestrates agents, approvals & live visibility" ,
2026-06-25 00:28:23 +00:00
],
kind = "narrative" ,
))
2026-06-27 19:37:50 +00:00
slides . append ( _slide (
"command-center-ui" ,
"Command Center UI" ,
"Everything you operate from this dashboard" ,
[
"Data Platform — interactive topology + live presentation deck" ,
"Data Sources UI — browse, filter & edit all 5 source databases" ,
"Live Changes — real-time Debezium CDC event stream" ,
"Data Flow — OpenMetadata catalog, lineage & pipeline map" ,
"Data Quality — Docling document QA + RAG ingest" ,
"Knowledge Chat — GPU-backed RAG over lab documentation" ,
"Object Storage · HDFS · Elasticsearch · SSH terminal" ,
"Agent fleet · Approval inbox · Live GPU matrix" ,
],
kind = "command" ,
))
2026-06-25 00:28:23 +00:00
arch = topologies . get ( "architecture" ) or workload . get ( "topology" ) or {}
arch_nodes = arch . get ( "nodes" , [])
slides . append ( _slide (
"architecture" ,
"Data Platform Architecture" ,
arch . get ( "subtitle" , "Sources → Ingestion → Compute → Storage → Consumers" ),
2026-06-27 19:37:50 +00:00
[ f " { n . get ( 'label' , n . get ( 'id' )) } : { n . get ( 'subtitle' , n . get ( 'role' , '' )) } " for n in arch_nodes [: 16 ]],
2026-06-25 00:28:23 +00:00
kind = "topology" ,
topology = arch ,
))
2026-06-27 19:37:50 +00:00
db_lines = [
f "Host atc-db02 (10.0.21.51) — { databases . get ( 'running' , 0 ) } / { databases . get ( 'total' , 0 ) } containers up" ,
"postgres_sales — PostgreSQL sales_orders (CDC → Debezium)" ,
"mysql_hr — MySQL employee_events (CDC → Debezium)" ,
"mongodb_supplychain — MongoDB supplychain.events (CDC → Debezium)" ,
"cassandra_telemetry — Cassandra device_metrics (Trino federated)" ,
"neo4j_graph — Product/Supplier graph · 4.5M nodes" ,
]
for c in ( databases . get ( "containers" ) or [])[: 8 ]:
db_lines . append ( f " · { c . get ( 'name' , '?' ) } : { c . get ( 'state' , '?' ) } " )
slides . append ( _slide (
"source-databases" ,
"Source Databases" ,
"DB Vault · atc-db02 · 10.0.21.51" ,
db_lines ,
kind = "topology" ,
))
2026-06-25 00:28:23 +00:00
pipeline = topologies . get ( "pipeline" , {})
connector_lines = [
f " · { cs [ 'name' ] } : { cs . get ( 'state' , '?' ) } "
2026-06-27 19:37:50 +00:00
for cs in ( etl . get ( "connector_status" ) or [])[: 8 ]
2026-06-25 00:28:23 +00:00
]
slides . append ( _slide (
"pipeline" ,
"CDC Pipeline" ,
pipeline . get ( "subtitle" , "Airflow → DB → Debezium → Kafka → Lakehouse → S3" ),
[
2026-06-27 19:37:50 +00:00
f "Airflow: { 'healthy' if etl . get ( 'airflow_healthy' ) else 'degraded' } — { etl . get ( 'airflow_url' , 'http://10.0.21.55:8080' ) } " ,
f "Kafka UI: { 'UP' if etl . get ( 'kafka_ui_ok' ) else 'DOWN' } — http://10.0.21.36:9000" ,
f "Debezium Connect: http://10.0.21.50:8083" ,
2026-06-25 00:28:23 +00:00
f "Connectors: { ', ' . join ( etl . get ( 'connectors' ) or []) or 'none' } " ,
* connector_lines ,
2026-06-27 19:37:50 +00:00
f "Spark UI: { 'UP' if etl . get ( 'spark_ui_ok' ) else 'DOWN' } — http://10.0.21.50:8080" ,
f "ObjectScale S3: { 'reachable' if objectscale . get ( 'reachable' ) else 'down' } — bucket= { objectscale . get ( 'bucket' , 'data' ) } " ,
2026-06-25 00:28:23 +00:00
],
kind = "topology" ,
topology = pipeline ,
))
2026-06-27 19:37:50 +00:00
lake_lines = [
f "atc-lake01 @ { lakehouse . get ( 'host' , '10.0.21.50' ) } — { lakehouse . get ( 'running' , 0 ) } / { lakehouse . get ( 'total' , 0 ) } containers" ,
f "Trino: { 'UP' if lakehouse . get ( 'trino_ok' ) else 'DOWN' } — http://10.0.21.50:8089" ,
"Spark — batch & streaming compute" ,
"Kafka Connect + Debezium — CDC ingestion" ,
"s3-kafka-consumer — events → ObjectScale S3" ,
"Iceberg catalog — bronze → silver → gold tables" ,
]
for c in ( lakehouse . get ( "containers" ) or [])[: 8 ]:
lake_lines . append ( f " · { c . get ( 'name' , '?' ) } : { c . get ( 'state' , '?' ) } " )
slides . append ( _slide (
"lakehouse" ,
"Lakehouse Hub" ,
"Spark · Trino · Iceberg · Kafka Connect" ,
lake_lines ,
kind = "topology" ,
))
docker_lines = [
f "atc-docker01 @ 10.0.21.45 — { docker . get ( 'running' , 0 ) } / { docker . get ( 'total' , 0 ) } containers" ,
"Homepage — http://10.0.21.45" ,
"Dockhand — container management http://10.0.21.45:8082" ,
"Apache Superset — BI dashboards http://10.0.21.45:8088" ,
"Forgejo / Gitea · monitoring · nginx · redis" ,
]
for c in ( docker . get ( "containers" ) or [])[: 10 ]:
docker_lines . append ( f " · { c . get ( 'name' , '?' ) } : { c . get ( 'state' , '?' ) } " )
slides . append ( _slide (
"docker-rack" ,
"Docker Rack & Analytics" ,
"Platform services on atc-docker01" ,
docker_lines ,
kind = "zone" ,
))
cdc = governance . get ( "cdc" ) or {}
gov_lines = [
f "OpenMetadata UI: { governance . get ( 'openmetadata_url' , 'http://10.0.21.47:8585' ) } " ,
"Ingestion Airflow: http://10.0.21.47:8080" ,
"Catalog · lineage · data quality · PII auto-classification (Presidio NER)" ,
]
if "error" not in cdc :
by_src = ", " . join ( f " { k } = { v } " for k , v in ( cdc . get ( "by_source" ) or {}) . items ()) or "none"
gov_lines . append ( f "CDC stream: connected= { cdc . get ( 'connected' ) } · { cdc . get ( 'window_total' , 0 ) } changes/15m ( { by_src } )" )
ps = governance . get ( "pii_summary" ) or {}
if "error" not in ps :
gov_lines . append ( f "PII: { ps . get ( 'pii_columns' , 0 ) } columns · { ps . get ( 'masked_columns' , 0 ) } masked" )
for ln in ( governance . get ( "lineage" ) or [])[: 4 ]:
gov_lines . append ( f "Lineage: { ln } " )
slides . append ( _slide (
"governance" ,
"Governance & Metadata" ,
"OpenMetadata · CDC · PII · Lineage" ,
gov_lines ,
kind = "architecture" ,
))
slides . append ( _node_slide ( "openmetadata" ))
slides . append ( _node_slide ( "elastic" ))
2026-06-25 00:28:23 +00:00
for zone in zones :
apps = zone . get ( "apps" ) or []
app_lines = [
f " { a [ 'name' ] } : { a [ 'state' ] } " + ( f " ( { a . get ( 'host' , '' ) } )" if a . get ( "host" ) else "" )
2026-06-27 19:37:50 +00:00
for a in apps [: 12 ]
2026-06-25 00:28:23 +00:00
]
slides . append ( _slide (
f "zone- { zone [ 'id' ] } " ,
zone [ "label" ],
f " { zone . get ( 'vm' , '' ) } · { zone . get ( 'ip' , '' ) } · { _status_badge ( zone . get ( 'level' , 'unknown' )) } " ,
[
f "Containers: { zone . get ( 'running' , 0 ) } / { zone . get ( 'total' , 0 ) } running" ,
* app_lines ,
],
kind = "zone" ,
zone = zone ,
))
2026-06-27 19:37:50 +00:00
PRESENTATION_NODES = [
"airflow" , "db" , "debezium" , "kafka" , "lakehouse" , "s3" ,
"docker" , "hadoop" , "gpu" , "command" ,
2026-06-25 00:28:23 +00:00
]
slides . append ( _slide (
2026-06-27 19:37:50 +00:00
"infrastructure-map" ,
2026-06-25 00:28:23 +00:00
"Infrastructure Map" ,
"Proxmox VMs & services across VLAN 20/21" ,
[
f " { NODE_REGISTRY [ nid ][ 'label' ] } — { NODE_REGISTRY [ nid ] . get ( 'vm' ) } "
f "(VMID { NODE_REGISTRY [ nid ] . get ( 'vmid' , '?' ) } ) @ { NODE_REGISTRY [ nid ] . get ( 'ip' ) } "
2026-06-27 19:37:50 +00:00
for nid in PRESENTATION_NODES if nid in NODE_REGISTRY
2026-06-25 00:28:23 +00:00
],
kind = "registry" ,
))
2026-06-27 19:37:50 +00:00
for nid in PRESENTATION_NODES :
if nid in NODE_REGISTRY and nid not in ( "docker" , "db" , "lakehouse" ):
slides . append ( _node_slide ( nid ))
2026-06-25 00:28:23 +00:00
dn_lines = [
f " · { dn [ 'host' ] } : { dn . get ( 'used_gb' , 0 ) } GB — { dn . get ( 'state' , '' ) } "
2026-06-27 19:37:50 +00:00
for dn in ( hadoop . get ( "datanodes" ) or [])[: 6 ]
2026-06-25 00:28:23 +00:00
]
slides . append ( _slide (
"hadoop" ,
"Hadoop HDFS" ,
"9-node parallel storage cluster" ,
[
2026-06-27 19:37:50 +00:00
f "NameNode: { 'UP' if hadoop . get ( 'reachable' ) else 'DOWN' } — { hadoop . get ( 'namenode' , 'http://10.0.21.61:9870' ) } " ,
2026-06-25 00:28:23 +00:00
f "Capacity: { hadoop . get ( 'capacity_used_gb' , '?' ) } / { hadoop . get ( 'capacity_total_gb' , '?' ) } GB" ,
f "DataNodes: { hadoop . get ( 'live_datanodes' , 0 ) } live, { hadoop . get ( 'dead_datanodes' , 0 ) } dead" ,
f "Files: { hadoop . get ( 'files_total' , 0 ) } , Blocks: { hadoop . get ( 'blocks_total' , 0 ) } " ,
* dn_lines ,
],
kind = "data" ,
))
gpus = gpu . get ( "gpus" ) or snap . get ( "gpu" , {}) . get ( "gpus" ) or []
2026-07-21 23:20:24 +00:00
try :
from gpu_config import resolve_gpu_identity
gpu_id = resolve_gpu_identity ( snap . get ( "gpu" ) or gpu )
except Exception :
host = ( snap . get ( "gpu" ) or gpu ) . get ( "host" ) or "10.0.10.106"
gpu_id = {
"vm" : "atc-gpu-prod" ,
"vmid" : 306 ,
"ui_url" : f "http:// { host } :9000" ,
"llm_url" : f "http:// { host } :8001/v1" ,
}
2026-06-25 00:28:23 +00:00
gpu_lines = [
f "GPU { g [ 'index' ] } : { g . get ( 'util_gpu' , 0 ) : .0f } % util, "
f " { g . get ( 'memory_used_mib' , 0 ) : .0f } / { g . get ( 'memory_total_mib' , 0 ) : .0f } MiB"
for g in gpus [: 4 ]
]
slides . append ( _slide (
"gpu" ,
"GPU Lab & GenAI" ,
2026-07-21 23:20:24 +00:00
f " { gpu . get ( 'model' ) or 'vLLM' } on { gpu_id [ 'vm' ] } (VM { gpu_id [ 'vmid' ] } )" ,
2026-06-25 00:28:23 +00:00
[
f "Inference: { 'ON' if gpu . get ( 'inference_active' ) else 'OFF' } " ,
2026-07-21 23:20:24 +00:00
f "API: { gpu_id [ 'llm_url' ] } " ,
f "GPU Lab UI: { gpu_id [ 'ui_url' ] } " ,
2026-06-27 19:37:50 +00:00
"Kibana/Elastic: http://10.0.21.46:5601" ,
2026-06-25 00:28:23 +00:00
* gpu_lines ,
],
kind = "gpu" ,
))
slides . append ( _slide (
"agents" ,
"Autonomous Agents" ,
"Mo & Bart supervise 5 domain operators + MCP hub" ,
[
"ETL Guardian — Airflow, Kafka, Debezium, connectors" ,
"Data Custodian — PostgreSQL, MySQL, MongoDB, Cassandra, Neo4j" ,
2026-06-27 19:37:50 +00:00
"Lakehouse Ops — Spark, Trino, Iceberg, ObjectScale S3, OpenMetadata" ,
2026-06-25 00:28:23 +00:00
"Hadoop Ranger — HDFS NameNode, DataNodes, block health" ,
"Infra Sentinel — Docker rack, GPU lab, Command Center" ,
"All agents receive LIVE cluster snapshot in every LLM prompt" ,
],
kind = "agents" ,
))
cc_apps = [ f " { c [ 'name' ] } : { c [ 'state' ] } " for c in ( command . get ( "containers" ) or [])]
slides . append ( _slide (
"command" ,
2026-06-27 19:37:50 +00:00
"Command Center Stack" ,
"VM 304 — this dashboard runs here" ,
2026-06-25 00:28:23 +00:00
[
f "Host: { command . get ( 'host' , '10.0.21.33' ) } (VMID { command . get ( 'vmid' , 304 ) } )" ,
2026-06-27 19:37:50 +00:00
f "Stack: { command . get ( 'running' , 0 ) } / { command . get ( 'total' , 0 ) } containers (api, ui, caddy, redis, postgres)" ,
2026-06-25 00:28:23 +00:00
* cc_apps ,
2026-06-27 19:37:50 +00:00
"WebSocket ops feed · Approval inbox · Agent terminals · Data Sources UI" ,
2026-06-25 00:28:23 +00:00
],
kind = "command" ,
))
slides . append ( _slide (
"demo" ,
"Live Demo Tips" ,
"Use this deck during customer presentations" ,
[
2026-06-27 19:37:50 +00:00
"Press ← → or click dots to navigate slides · F = fullscreen" ,
"Data Platform tab → Presentation sub-tab (this deck)" ,
"Data Platform → Topology for interactive pipeline map" ,
2026-06-25 00:28:23 +00:00
"Export HTML opens a standalone deck for projectors / offline" ,
"Ask agents in the Command Bar — they see full cluster context" ,
2026-06-27 19:37:50 +00:00
"Trigger data generation in Data Sources UI → Generate tab" ,
2026-06-25 00:28:23 +00:00
],
kind = "cta" ,
))
return {
"ts" : snap . get ( "ts" ),
"title" : "Dell ATC Data Lab" ,
"subtitle" : "Live Infrastructure Presentation" ,
"totals" : totals ,
"pipeline_active" : totals . get ( "pipeline_active" ),
"slides" : slides ,
"slide_count" : len ( slides ),
"workload" : workload ,
"topologies" : topologies ,
}
def render_presentation_html ( payload : dict [ str , Any ]) -> str :
slides_json = json . dumps ( payload . get ( "slides" , []), default = str )
title = payload . get ( "title" , "ATC Lab" )
ts = payload . get ( "ts" , "" )
return f """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title> { title } — Presentation</title>
<style>
* {{ box-sizing:border-box;margin:0;padding:0 }}
body {{ font-family:system-ui,-apple-system,Segoe UI,sans-serif;background:#0c1929;color:#e8f1ff;height:100vh;overflow:hidden }}
.deck {{ height:100vh;display:flex;flex-direction:column }}
header {{ padding:1rem 2rem;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid rgba(96,165,250,.2);background:rgba(15,27,46,.9) }}
header h1 {{ font-size:1.1rem;font-weight:600 }}
header .meta {{ font-size:.75rem;opacity:.7 }}
.slide {{ flex:1;display:none;padding:3rem 4rem;overflow:auto }}
.slide.active {{ display:flex;flex-direction:column;justify-content:center }}
.slide h2 {{ font-size:2.4rem;margin-bottom:.5rem;background:linear-gradient(90deg,#60a5fa,#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent }}
.slide h3 {{ font-size:1rem;opacity:.75;margin-bottom:2rem;font-weight:400 }}
.slide ul {{ list-style:none;font-size:1.15rem;line-height:1.9 }}
.slide li::before {{ content:"▸ ";color:#60a5fa }}
.slide.hero h2 {{ font-size:3.2rem }}
2026-06-26 00:47:49 +00:00
.slide img.slide-img {{ max-height:42vh;max-width:100%;margin-top:1.5rem;border-radius:10px;border:1px solid rgba(96,165,250,.25);box-shadow:0 8px 30px rgba(0,0,0,.4);object-fit:contain }}
2026-06-25 00:28:23 +00:00
nav {{ display:flex;gap:.5rem;padding:1rem 2rem;border-top:1px solid rgba(96,165,250,.2);align-items:center }}
nav button {{ background:#1e3a5f;border:1px solid rgba(96,165,250,.3);color:#e8f1ff;padding:.5rem 1rem;border-radius:6px;cursor:pointer }}
nav button:hover {{ background:#234876 }}
.dots {{ display:flex;gap:6px;flex:1;justify-content:center;flex-wrap:wrap }}
.dot {{ width:8px;height:8px;border-radius:50%;background:rgba(96,165,250,.3);cursor:pointer;border:none }}
.dot.active {{ background:#60a5fa;transform:scale(1.3) }}
.counter {{ font-size:.8rem;opacity:.6;min-width:4rem;text-align:right }}
</style>
</head>
<body>
<div class="deck">
<header><h1> { title } </h1><div class="meta">Dell ATC · Live snapshot { ts } </div></header>
<div id="slides"></div>
<nav>
<button id="prev">← Prev</button>
<div class="dots" id="dots"></div>
<button id="next">Next →</button>
<span class="counter" id="counter"></span>
</nav>
</div>
<script>
const slides= { slides_json } ;
let i=0;
const container=document.getElementById("slides");
const dots=document.getElementById("dots");
const counter=document.getElementById("counter");
slides.forEach((s,idx)=> {{
const el=document.createElement("section");
el.className="slide"+(s.kind==="hero"?" hero":"")+(idx===0?" active":"");
const bullets=(s.bullets||[]).map(b=>"<li>"+b+"</li>").join("");
2026-06-26 00:47:49 +00:00
const img=s.image?'<img class="slide-img" src="'+s.image+'" alt=""/>':"";
el.innerHTML="<h2>"+s.title+"</h2><h3>"+(s.subtitle||"")+"</h3><ul>"+bullets+"</ul>"+img;
2026-06-25 00:28:23 +00:00
container.appendChild(el);
const d=document.createElement("button");
d.className="dot"+(idx===0?" active":"");
d.onclick=()=>go(idx);
dots.appendChild(d);
}} );
function go(n) {{ i=Math.max(0,Math.min(slides.length-1,n));document.querySelectorAll(".slide").forEach((e,j)=>e.classList.toggle("active",j===i));document.querySelectorAll(".dot").forEach((e,j)=>e.classList.toggle("active",j===i));counter.textContent=(i+1)+"/"+slides.length; }}
document.getElementById("prev").onclick=()=>go(i-1);
document.getElementById("next").onclick=()=>go(i+1);
document.onkeydown=e=> {{ if(e.key==="ArrowRight"||e.key===" ")go(i+1);if(e.key==="ArrowLeft")go(i-1);if(e.key==="f"||e.key==="F")document.documentElement.requestFullscreen?.(); }} ;
go(0);
</script>
</body>
</html>"""