dashboard/index.html

396 lines
24 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manohar's Hub</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🏠</text></svg>">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" crossorigin="anonymous"></script>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html,body,#root{min-height:100vh}
body{font-family:'DM Sans',sans-serif}
@keyframes pulse{0%{box-shadow:0 0 0 0 var(--pulse-c)}70%{box-shadow:0 0 0 5px transparent}100%{box-shadow:0 0 0 0 transparent}}
/* ── Mobile responsive ───────────────────────────────────────── */
@media (max-width: 640px) {
.main { padding: 20px 14px 48px !important; }
.hdr { flex-direction: column !important; gap: 16px !important; }
.clock { font-size: 48px !important; letter-spacing: -1px !important; }
.clock-s { font-size: 28px !important; }
.pill { border-radius: 10px !important; align-self: stretch !important; flex-wrap: wrap !important; }
.pill-cell { flex: 1 1 40% !important; padding: 8px 10px !important; }
.pill-div { display: none !important; }
.pill-val { font-size: 14px !important; }
.pill-lbl { font-size: 10px !important; }
.wgrid { grid-template-columns: 1fr 1fr !important; }
.sgrid { grid-template-columns: 1fr 1fr !important; }
.dark-btn { top: 12px !important; right: 12px !important; width: 34px !important; height: 34px !important; font-size: 15px !important; }
.ts-lbl { top: 17px !important; right: 52px !important; }
}
@media (max-width: 400px) {
.clock { font-size: 38px !important; }
.wgrid { grid-template-columns: 1fr !important; }
.sgrid { grid-template-columns: 1fr !important; }
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const {useState,useEffect,useCallback}=React;
const API="https://api.manohargupta.com";
const REFRESH=30000;
const SECTIONS=[
{id:"infrastructure",label:"Infrastructure",services:[
{id:"dokploy",name:"Dokploy",desc:"Docker orchestration",emoji:"🐋",url:"https://dokploy.manohargupta.com"},
{id:"uptimekuma",name:"Uptime Kuma",desc:"Service monitoring",emoji:"📡",url:"https://status.manohargupta.com"},
{id:"umami",name:"Umami",desc:"Web analytics",emoji:"📊",url:"https://analytics.manohargupta.com"},
{id:"traefik",name:"Traefik",desc:"Reverse proxy",emoji:"⚡",url:"https://dokploy.manohargupta.com"},
]},
{id:"tools",label:"Tools",services:[
{id:"n8n",name:"n8n",desc:"Workflow automation",emoji:"🔄",url:"https://automate.manohargupta.com"},
{id:"paperless",name:"Paperless",desc:"Document OCR + search",emoji:"📄",url:"https://docs.manohargupta.com"},
{id:"miniflux",name:"Miniflux",desc:"RSS reader",emoji:"📰",url:"https://feeds.manohargupta.com"},
{id:"changedetect",name:"ChangeDetection",desc:"Page change monitor",emoji:"👁",url:"https://watch.manohargupta.com"},
{id:"openwebui",name:"Open WebUI",desc:"AI chat interface",emoji:"💬",url:"https://chat.manohargupta.com"},
{id:"notes",name:"Notes",desc:"Obsidian vault portal",emoji:"🪶",url:"https://notes.manohargupta.com"},
]},
{id:"dev",label:"Dev",services:[
{id:"forgejo",name:"Forgejo",desc:"Self-hosted Git",emoji:"🌿",url:"https://git.manohargupta.com"},
{id:"codeserver",name:"Code Server",desc:"Browser IDE",emoji:"💻",url:"https://code.manohargupta.com"},
{id:"litellm",name:"LiteLLM",desc:"Unified LLM gateway",emoji:"🧠",url:"https://llm.manohargupta.com"},
{id:"apprise",name:"Apprise",desc:"Notification hub",emoji:"🔔",url:"https://notify.manohargupta.com"},
]},
{id:"finance",label:"Finance & Intel",services:[
{id:"tiger",name:"Tiger Agent",desc:"AI orchestration",emoji:"🐯",url:"https://agent.manohargupta.com"},
{id:"ladder",name:"Ladder",desc:"Paywall bypass · CORS proxy",emoji:"🪜",url:"https://ladder.manohargupta.com"},
{id:"angel",name:"Angel",desc:"Portfolio tracker",emoji:"<svg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 24 24' fill='none' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M3 3v18h18' stroke='#6b7280'/><line x1='8.5' y1='2' x2='8.5' y2='22' stroke='#6b7280' stroke-width='1' opacity='0.3'/><line x1='13.5' y1='2' x2='13.5' y2='22' stroke='#6b7280' stroke-width='1' opacity='0.3'/><line x1='18.5' y1='2' x2='18.5' y2='22' stroke='#6b7280' stroke-width='1' opacity='0.3'/><rect x='7' y='10' width='3' height='8' fill='none'/><path d='M8.5 7 L8.5 10' stroke='#22c55e' stroke-width='2'/><path d='M8.5 18 L8.5 21' stroke='#22c55e' stroke-width='2'/><rect x='7' y='10' width='3' height='8' stroke='#22c55e'/><rect x='12' y='8' width='3' height='10' fill='none'/><path d='M13.5 4 L13.5 8' stroke='#22c55e' stroke-width='2'/><path d='M13.5 18 L13.5 22' stroke='#22c55e' stroke-width='2'/><rect x='12' y='8' width='3' height='10' stroke='#22c55e' fill='#22c55e' fill-opacity='0.2'/><rect x='17' y='12' width='3' height='6' fill='none'/><path d='M18.5 9 L18.5 12' stroke='#ef4444' stroke-width='2'/><path d='M18.5 18 L18.5 21' stroke='#ef4444' stroke-width='2'/><rect x='17' y='12' width='3' height='6' stroke='#ef4444' fill='#ef4444' fill-opacity='0.2'/></svg>",url:"https://angel.manohargupta.com"},
{id:"remodel",name:"REmodel",desc:"RE project finance model",emoji:"☀️",url:"https://model.manohargupta.com"},
]},
{id:"projects",label:"Projects",services:[
{id:"tia",name:"TIA",desc:"Baby growth tracker",emoji:"👶",url:"https://tia.manohargupta.com"},
{id:"awadhgully",name:"Awadhgully",desc:"Storefront · Vendure",emoji:"🛍",url:"https://awadhgully.com"},
]},
];
const QUICK_LINKS=[
{label:"Namecheap DNS",emoji:"🌐",url:"https://ap.www.namecheap.com/domains/domaincontrolpanel/manohargupta.com/advancedns"},
{label:"Hetzner Console",emoji:"🖥",url:"https://console.hetzner.cloud"},
{label:"OpenRouter",emoji:"🤖",url:"https://openrouter.ai/activity"},
{label:"BotFather",emoji:"🤖",url:"https://t.me/BotFather"},
{label:"NSE India",emoji:"📈",url:"https://www.nseindia.com"},
{label:"CERC",emoji:"⚡",url:"https://cercind.gov.in"},
{label:"MNRE",emoji:"☀️",url:"https://mnre.gov.in"},
{label:"IEX India",emoji:"💹",url:"https://www.iexindia.com"},
];
function useTime(){const[n,s]=useState(new Date());useEffect(()=>{const t=setInterval(()=>s(new Date()),1000);return()=>clearInterval(t)},[]);return n}
function greet(h){return h<12?"Good morning":h<17?"Good afternoon":"Good evening"}
function Dot({status}){
// "degraded" = reachable but 5xx. "unknown"/undefined = grey, meaning the API
// hasn't reported on this id yet — never assume green.
const c={online:"#22c55e",running:"#22c55e",warning:"#f59e0b",degraded:"#f59e0b",offline:"#ef4444",exited:"#ef4444"}[status]||"#94a3b8";
return <span style={{display:"inline-block",width:7,height:7,borderRadius:"50%",background:c,flexShrink:0,"--pulse-c":c+"55",animation:(status==="online"||status==="running")?"pulse 2s ease infinite":"none"}}/>;
}
function Shell({label,right,children}){
return(
<div style={{background:"var(--c-surface)",border:"1px solid var(--c-border)",borderRadius:4,padding:"12px 14px"}}>
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:2}}>
<span style={{fontSize:11,fontWeight:600,color:"var(--c-muted)",textTransform:"uppercase",letterSpacing:"0.08em"}}>{label}</span>
{right}
</div>
{children}
</div>
);
}
function DiskBar({disk}){
if(!disk)return <Shell label="Disk"><span style={{color:"var(--c-muted)",fontSize:12}}>Loading...</span></Shell>;
const c=disk.status==="ok"?"#22c55e":disk.status==="warning"?"#f59e0b":"#ef4444";
return(
<Shell label="Disk" right={<span style={{fontFamily:"'DM Mono',monospace",fontSize:12,color:c,fontWeight:500}}>{disk.percent}%</span>}>
<div style={{height:5,background:"var(--c-border)",borderRadius:3,overflow:"hidden",margin:"6px 0"}}>
<div style={{height:"100%",width:`${disk.percent}%`,background:c,borderRadius:3,transition:"width 0.5s"}}/>
</div>
<div style={{display:"flex",justifyContent:"space-between"}}>
<span style={{fontSize:11,color:"var(--c-muted)"}}>{disk.used_gb} GB used</span>
<span style={{fontSize:11,color:"var(--c-muted)"}}>{disk.free_gb} GB free</span>
</div>
</Shell>
);
}
function ContainerGrid({data}){
const[hov,sHov]=useState(null);
if(!data)return <Shell label="Containers"><span style={{color:"var(--c-muted)",fontSize:12}}>Loading...</span></Shell>;
const{containers=[],running,total}=data;
return(
<Shell label="Containers" right={<span style={{fontFamily:"'DM Mono',monospace",fontSize:12,color:"var(--c-text)"}}>{running}/{total}</span>}>
<div style={{display:"flex",flexWrap:"wrap",gap:4,marginTop:8}}>
{containers.map((c,i)=>(
<div key={i} onMouseEnter={()=>sHov(i)} onMouseLeave={()=>sHov(null)} title={c.name}
style={{width:10,height:10,borderRadius:2,background:c.running?"#22c55e":"#ef4444",cursor:"default",opacity:hov===i?0.5:1,transition:"opacity 0.1s"}}/>
))}
</div>
{hov!==null&&containers[hov]&&<div style={{marginTop:5,fontSize:11,color:"var(--c-muted)",fontFamily:"'DM Mono',monospace"}}>{containers[hov].name}</div>}
</Shell>
);
}
function LiveVisitors({data}){
if(!data)return <Shell label="Live Visitors"><span style={{color:"var(--c-muted)",fontSize:12}}>Loading...</span></Shell>;
return(
<Shell label="Live Visitors">
<div style={{display:"flex",alignItems:"baseline",gap:6,marginTop:4}}>
<span style={{fontFamily:"'DM Mono',monospace",fontSize:28,fontWeight:500,color:"var(--c-text)"}}>{data.active_visitors??0}</span>
<span style={{fontSize:12,color:"var(--c-muted)"}}>watching now</span>
</div>
{data.today_visitors!=null&&<div style={{fontSize:11,color:"var(--c-muted)",marginTop:2}}>{data.today_visitors} visitors · {data.today_views} views today</div>}
</Shell>
);
}
function TigerTasks({data}){
const icons={done:"✅",pending:"⏳",in_progress:"🔄"};
if(!data)return <Shell label="Tiger Agent"><span style={{color:"var(--c-muted)",fontSize:12}}>Loading...</span></Shell>;
const{recent_tasks=[],counts={}}=data;
return(
<Shell label="Tiger Agent" right={
<div style={{display:"flex",gap:4}}>
{counts.pending>0&&<span style={{fontSize:10,fontWeight:600,background:"#f59e0b22",color:"#f59e0b",padding:"1px 5px",borderRadius:3}}>{counts.pending} pending</span>}
{counts.in_progress>0&&<span style={{fontSize:10,fontWeight:600,background:"var(--c-accent-faint)",color:"var(--c-accent)",padding:"1px 5px",borderRadius:3}}>{counts.in_progress} active</span>}
</div>
}>
{recent_tasks.length===0
?<div style={{fontSize:12,color:"var(--c-muted)",fontStyle:"italic",marginTop:4}}>No recent tasks</div>
:recent_tasks.slice(0,4).map((t,i)=>(
<div key={i} style={{display:"flex",gap:7,alignItems:"flex-start",padding:"4px 0",borderTop:i>0?"1px solid var(--c-border)":"none",marginTop:i===0?6:0}}>
<span style={{fontSize:11,flexShrink:0}}>{icons[t.status]||"•"}</span>
<span style={{fontSize:11,color:"var(--c-text)",flex:1,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{t.task}</span>
</div>
))
}
</Shell>
);
}
function Markets({data}){
if(!data)return <Shell label="Markets"><span style={{color:"var(--c-muted)",fontSize:12}}>Loading...</span></Shell>;
const n=data.nifty||{}, x=data.iex_dam||{};
const nOk=n.close!=null, xOk=x.weighted_avg_price_inr!=null;
// Green/red on NIFTY change; IEX shows the day's volume-weighted MCP.
const nc=(n.change_pct??0)>=0?"#22c55e":"#ef4444";
const sign=(n.change_pct??0)>=0?"+":"";
return(
<Shell label="Markets" right={n.market_open===true
? <span style={{fontSize:10,fontWeight:600,background:"#22c55e22",color:"#22c55e",padding:"1px 5px",borderRadius:3}}>LIVE</span>
: <span style={{fontSize:10,color:"var(--c-muted)"}}>closed</span>}>
{(nOk||xOk)
?<div style={{display:"flex",gap:14,marginTop:6}}>
<div style={{flex:1,minWidth:0}}>
<div style={{fontSize:11,color:"var(--c-muted)"}}>NIFTY 50</div>
{nOk
?<>
<div style={{fontFamily:"'DM Mono',monospace",fontSize:16,fontWeight:500,color:"var(--c-text)"}}>{n.close.toLocaleString("en-IN")}</div>
<div style={{fontSize:10,color:nc,fontFamily:"'DM Mono',monospace"}}>{sign}{n.change_pts?.toFixed(2)} ({sign}{n.change_pct?.toFixed(2)}%)</div>
</>
:<div style={{fontSize:12,color:"var(--c-muted)"}}></div>}
</div>
<div style={{width:1,background:"var(--c-border)"}}/>
<div style={{flex:1,minWidth:0}}>
<div style={{fontSize:11,color:"var(--c-muted)"}}>IEX DAM · avg</div>
{xOk
?<>
<div style={{fontFamily:"'DM Mono',monospace",fontSize:16,fontWeight:500,color:"var(--c-text)"}}>{x.weighted_avg_price_inr.toLocaleString("en-IN")}</div>
<div style={{fontSize:10,color:"var(--c-muted)",fontFamily:"'DM Mono',monospace"}}>{x.min_price_inr}{x.max_price_inr} · {x.total_volume_mu} MU</div>
</>
:<div style={{fontSize:12,color:"var(--c-muted)"}}></div>}
</div>
</div>
:<div style={{fontSize:12,color:"var(--c-muted)",fontStyle:"italic",marginTop:4}}> Sources unreachable</div>
}
{xOk&&<div style={{fontSize:10,color:"var(--c-muted)",marginTop:5,fontFamily:"'DM Mono',monospace"}}>Rs/MWh · DAM {x.as_of} · {x.blocks_parsed} blocks</div>}
</Shell>
);
}
function StatCell({val,lbl,alert}){
const[h,sH]=useState(false);
// `alert` overrides the normal text colour so a threshold breach is visible
// at a glance. A number you look at daily that never changes colour is a
// number you stop reading.
const col=alert||(h?"var(--c-accent)":"var(--c-text)");
return(
<div className='pill-cell' onMouseEnter={()=>sH(true)} onMouseLeave={()=>sH(false)}
style={{textAlign:"center",padding:"8px 20px",background:h?"var(--c-accent-faint)":"transparent",transition:"background 0.15s",cursor:"default"}}>
<div className='pill-val' style={{fontFamily:"'DM Mono',monospace",fontWeight:500,fontSize:15,color:col,transition:"color 0.15s"}}>{val}</div>
<div className='pill-lbl' style={{fontSize:11,color:h?"var(--c-accent)":"var(--c-muted)",marginTop:2,letterSpacing:"0.06em",textTransform:"uppercase",transition:"color 0.15s"}}>{lbl}</div>
</div>
);
}
function Card({svc,health}){
const[h,sH]=useState(false);
// health is {status, code, ms} from /api/all -> health.services[svc.id],
// or undefined if the API is unreachable / this id isn't probed yet.
const st=health?.status;
const tip=health
? `${st}${health.code?` · HTTP ${health.code}`:""}${health.ms!=null?` · ${health.ms}ms`:""}`
: "status unknown";
return(
<div onClick={()=>window.open(svc.url,"_blank")} onMouseEnter={()=>sH(true)} onMouseLeave={()=>sH(false)}
style={{background:h?"var(--c-surface2)":"var(--c-surface)",border:`1px solid ${h?"var(--c-accent)":"var(--c-border)"}`,borderRadius:4,padding:"14px 16px",cursor:"pointer",transform:h?"translateY(-1px)":"none",boxShadow:h?"0 4px 16px rgba(0,0,0,0.08)":"none",transition:"all 0.15s",display:"flex",alignItems:"center",gap:12,position:"relative"}}>
{svc.emoji.startsWith("<svg")
? <span style={{flexShrink:0,lineHeight:1}} dangerouslySetInnerHTML={{__html:svc.emoji}}/>
: <span style={{fontSize:28,flexShrink:0,lineHeight:1}}>{svc.emoji}</span>}
<div style={{flex:1,minWidth:0}}>
<div style={{fontWeight:500,fontSize:14,color:"var(--c-text)"}}>{svc.name}</div>
<div style={{fontSize:12,color:"var(--c-muted)",marginTop:2,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{svc.desc}</div>
</div>
<span title={tip} style={{display:"flex",alignItems:"center"}}><Dot status={st}/></span>
{h&&<div style={{position:"absolute",right:10,top:10,fontSize:10,fontWeight:600,color:"var(--c-accent)",letterSpacing:"0.04em"}}>OPEN </div>}
</div>
);
}
function Section({sec,health}){
const[open,setOpen]=useState(true);
// Surface faults on the section header too — otherwise collapsing a section
// hides the very thing you need to see.
const bad=sec.services.filter(s=>{
const st=health?.[s.id]?.status;
return st==="offline"||st==="degraded";
}).length;
return(
<div style={{marginBottom:28}}>
<button onClick={()=>setOpen(o=>!o)} style={{display:"flex",alignItems:"center",width:"100%",background:"none",border:"none",cursor:"pointer",padding:"0 0 10px",gap:8,fontFamily:"inherit"}}>
<span style={{fontSize:11,fontWeight:600,letterSpacing:"0.1em",textTransform:"uppercase",color:"var(--c-muted)"}}>{sec.label}</span>
{bad>0&&<span style={{fontSize:10,fontWeight:600,background:"#ef444422",color:"#ef4444",padding:"1px 6px",borderRadius:3}}>{bad} down</span>}
<span style={{marginLeft:"auto",fontSize:12,color:"var(--c-muted)",display:"inline-block",transition:"transform 0.2s",transform:open?"rotate(0deg)":"rotate(-90deg)"}}></span>
</button>
{open&&<div style={{display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(200px, 1fr))",gap:10}}>{sec.services.map(s=><Card key={s.id} svc={s} health={health?.[s.id]}/>)}</div>}
</div>
);
}
function QL({lk}){
const[h,sH]=useState(false);
return(
<a href={lk.url} target="_blank" rel="noopener noreferrer" onMouseEnter={()=>sH(true)} onMouseLeave={()=>sH(false)}
style={{display:"flex",alignItems:"center",gap:7,padding:"7px 14px",background:"var(--c-surface)",border:`1px solid ${h?"var(--c-accent)":"var(--c-border)"}`,borderRadius:999,fontSize:13,color:"var(--c-text)",textDecoration:"none",whiteSpace:"nowrap",transition:"border-color 0.15s"}}>
<span style={{fontSize:14}}>{lk.emoji}</span>{lk.label}
</a>
);
}
function App(){
const[dark,setDark]=useState(()=>window.matchMedia("(prefers-color-scheme: dark)").matches);
const[qlOpen,setQlOpen]=useState(true);
const[live,setLive]=useState(null);
const[ts,setTs]=useState(null);
const now=useTime();
const fetch_=useCallback(async()=>{
try{const r=await fetch(`${API}/api/all`);if(!r.ok)throw new Error(r.status);setLive(await r.json());setTs(new Date())}
catch(e){console.warn("API:",e.message)}
},[]);
useEffect(()=>{fetch_();const t=setInterval(fetch_,REFRESH);return()=>clearInterval(t)},[fetch_]);
useEffect(()=>{
const t=dark?{
"--c-bg":"oklch(13% 0.012 180)","--c-surface":"oklch(17% 0.012 180)",
"--c-surface2":"oklch(21% 0.012 180)","--c-border":"oklch(25% 0.012 180)",
"--c-text":"oklch(92% 0.008 180)","--c-muted":"oklch(55% 0.01 180)",
"--c-accent":"oklch(55% 0.15 180)","--c-accent-faint":"oklch(55% 0.15 180 / 0.12)",
}:{
"--c-bg":"oklch(97% 0.01 180)","--c-surface":"oklch(99.5% 0.006 180)",
"--c-surface2":"oklch(95% 0.012 180)","--c-border":"oklch(88% 0.012 180)",
"--c-text":"oklch(18% 0.012 180)","--c-muted":"oklch(52% 0.01 180)",
"--c-accent":"oklch(55% 0.15 180)","--c-accent-faint":"oklch(55% 0.15 180 / 0.08)",
};
const r=document.documentElement;
Object.entries(t).forEach(([k,v])=>r.style.setProperty(k,v));
document.body.style.background="var(--c-bg)";
document.body.style.color="var(--c-text)";
},[dark]);
const srv=live?.server||{};
// Threshold colours: amber = worth noticing, red = act now.
const AMBER="#f59e0b", RED="#ef4444";
const dSt=srv.disk?.status; // "ok" | "warning" | "critical"
const stats=[
[srv.cpu_percent!=null?`${srv.cpu_percent}%`:"—","CPU",
srv.cpu_percent>=90?RED:srv.cpu_percent>=70?AMBER:null],
[srv.ram_used_gb!=null?`${srv.ram_used_gb} GB`:"—","RAM used",
srv.ram_percent>=90?RED:srv.ram_percent>=80?AMBER:null],
[srv.disk?.free_gb!=null?`${srv.disk.free_gb} GB`:"—","Disk free",
dSt==="critical"?RED:dSt==="warning"?AMBER:null],
[srv.uptime_days!=null?`${srv.uptime_days}d`:"—","Uptime",null],
];
return(
<>
<button onClick={()=>setDark(d=>!d)} style={{position:"fixed",top:16,right:16,zIndex:200,width:38,height:38,borderRadius:"50%",background:"var(--c-surface)",border:"1px solid var(--c-border)",fontSize:18,cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"center",boxShadow:"0 2px 8px rgba(0,0,0,0.1)"}}>
{dark?"☀️":"🌙"}
</button>
{ts&&<div style={{position:"fixed",top:20,right:62,fontSize:10,color:"var(--c-muted)",fontFamily:"'DM Mono',monospace"}}> {ts.toLocaleTimeString("en-US",{hour:"2-digit",minute:"2-digit"})}</div>}
<div style={{maxWidth:960,margin:"0 auto",padding:"36px 24px 64px"}}>
<div style={{display:"flex",alignItems:"flex-start",justifyContent:"space-between",marginBottom:32}}>
<div>
<div style={{fontSize:14,color:"var(--c-muted)",marginBottom:6}}>{greet(now.getHours())}, Manohar</div>
<div style={{fontFamily:"'DM Mono',monospace",fontSize:60,fontWeight:500,letterSpacing:"-2px",lineHeight:1,color:"var(--c-text)"}}>
{String(now.getHours()).padStart(2,"0")}:{String(now.getMinutes()).padStart(2,"0")}
<span className='clock-s' style={{fontSize:35,opacity:0.35,marginLeft:4}}>:{String(now.getSeconds()).padStart(2,"0")}</span>
</div>
<div style={{marginTop:6,display:"flex",alignItems:"center",gap:8}}>
<span style={{fontSize:13,fontWeight:500,color:"var(--c-muted)"}}>{now.toLocaleDateString("en-US",{weekday:"long"})}</span>
<span style={{color:"var(--c-border)"}}>·</span>
<span style={{fontSize:13,color:"var(--c-muted)"}}>{now.toLocaleDateString("en-US",{month:"long",day:"numeric",year:"numeric"})}</span>
</div>
</div>
<div style={{display:"flex",alignItems:"stretch",background:"var(--c-surface)",border:"1px solid var(--c-border)",borderRadius:999,overflow:"hidden",boxShadow:"0 1px 4px rgba(0,0,0,0.06)",alignSelf:"center"}}>
{stats.map(([val,lbl,alert],i,arr)=>(
<React.Fragment key={lbl}>
<StatCell val={val} lbl={lbl} alert={alert}/>
{i<arr.length-1&&<div style={{width:1,background:"var(--c-border)",margin:"8px 0"}}/>}
</React.Fragment>
))}
</div>
</div>
<div style={{display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(180px, 1fr))",gap:10,marginBottom:32}}>
<DiskBar disk={live?.server?.disk}/>
<ContainerGrid data={live?.containers}/>
<LiveVisitors data={live?.umami}/>
<TigerTasks data={live?.tiger}/>
<Markets data={live?.markets}/>
</div>
{SECTIONS.map(s=><Section key={s.id} sec={s} health={live?.health?.services}/>)}
<div style={{marginTop:8}}>
<button onClick={()=>setQlOpen(o=>!o)} style={{display:"flex",alignItems:"center",width:"100%",background:"none",border:"none",cursor:"pointer",padding:"0 0 12px",gap:8,fontFamily:"inherit"}}>
<span style={{fontSize:11,fontWeight:600,letterSpacing:"0.1em",textTransform:"uppercase",color:"var(--c-muted)"}}>Quick Links</span>
<span style={{marginLeft:"auto",fontSize:12,color:"var(--c-muted)",display:"inline-block",transition:"transform 0.2s",transform:qlOpen?"rotate(0deg)":"rotate(-90deg)"}}></span>
</button>
{qlOpen&&<div style={{display:"flex",flexWrap:"wrap",gap:8}}>{QUICK_LINKS.map(l=><QL key={l.label} lk={l}/>)}</div>}
</div>
</div>
</>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App/>);
</script>
</body>
</html>