dashboard: live API, 5 widgets, Angel in Finance, Open WebUI

This commit is contained in:
Manohar Gupta 2026-05-10 03:41:10 +05:30
parent d27c0a31f1
commit 780668d7ef

View file

@ -4,296 +4,321 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manohar's Hub</title> <title>Manohar's Hub</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<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="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"> <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@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/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> <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" crossorigin="anonymous"></script>
<style> <style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html, body, #root { min-height: 100vh; } html,body,#root{min-height:100vh}
body { font-family: 'DM Sans', sans-serif; transition: background 0.3s, color 0.3s; } body{font-family:'DM Sans',sans-serif}
@keyframes pulse { @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}}
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; }
}
</style> </style>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="text/babel"> <script type="text/babel">
const { useState, useEffect } = React; const {useState,useEffect,useCallback}=React;
const API="https://api.manohargupta.com";
const REFRESH=30000;
const SECTIONS = [ const SECTIONS=[
{ id: "infrastructure", label: "Infrastructure", services: [ {id:"infrastructure",label:"Infrastructure",services:[
{ id: "dokploy", name: "Dokploy", desc: "Docker orchestration", emoji: "🐋", url: "https://dokploy.manohargupta.com", status: "online", lastCheck: 2 }, {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", status: "online", lastCheck: 1 }, {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", status: "online", lastCheck: 3 }, {id:"umami",name:"Umami",desc:"Web analytics",emoji:"📊",url:"https://analytics.manohargupta.com"},
{ id: "traefik", name: "Traefik", desc: "20 routers · 17 services", emoji: "⚡", url: "https://dokploy.manohargupta.com", status: "online", lastCheck: 1 }, {id:"traefik",name:"Traefik",desc:"Reverse proxy",emoji:"⚡",url:"https://dokploy.manohargupta.com"},
]}, ]},
{ id: "tools", label: "Tools", services: [ {id:"tools",label:"Tools",services:[
{ id: "n8n", name: "n8n", desc: "Workflow automation", emoji: "🔄", url: "https://automate.manohargupta.com", status: "online", lastCheck: 5 }, {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", status: "online", lastCheck: 8 }, {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", status: "online", lastCheck: 7, badge: "11 unread" }, {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", status: "warning", lastCheck: 45 }, {id:"changedetect",name:"ChangeDetection",desc:"Page change monitor",emoji:"👁",url:"https://watch.manohargupta.com"},
{ id: "openwebui", name: "Open WebUI", desc: "ChatGPT-style AI chat", emoji: "💬", url: "https://chat.manohargupta.com", status: "online", lastCheck: 10 }, {id:"openwebui",name:"Open WebUI",desc:"AI chat interface",emoji:"💬",url:"https://chat.manohargupta.com"},
{ id: "angel", name: "Angel", desc: "Trade tracking and alert", emoji: "📈", url: "https://angel.manohargupta.com", status: "online", lastCheck: 5 },
]}, ]},
{ id: "dev", label: "Dev", services: [ {id:"dev",label:"Dev",services:[
{ id: "forgejo", name: "Forgejo", desc: "Self-hosted Git", emoji: "🌿", url: "https://git.manohargupta.com", status: "online", lastCheck: 11 }, {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", status: "online", lastCheck: 18 }, {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", status: "online", lastCheck: 5 }, {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", status: "offline", lastCheck: 30 }, {id:"apprise",name:"Apprise",desc:"Notification hub",emoji:"🔔",url:"https://notify.manohargupta.com"},
]}, ]},
{ id: "finance", label: "Finance & Intel", services: [ {id:"finance",label:"Finance & Intel",services:[
{ id: "tiger", name: "Tiger Agent", desc: "AI orchestration", emoji: "🐯", url: "https://agent.manohargupta.com", status: "online", lastCheck: 6 }, {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", status: "online", lastCheck: 9 }, {id:"ladder",name:"Ladder",desc:"Paywall bypass · CORS proxy",emoji:"🪜",url:"https://ladder.manohargupta.com"},
{ id: "angel", name: "Angel", desc: "Portfolio tracker", emoji: "😇", url: "https://angel.manohargupta.com", status: "online", lastCheck: 1 }, {id:"angel",name:"Angel",desc:"Portfolio tracker",emoji:"😇",url:"https://angel.manohargupta.com"},
]}, ]},
]; ];
const QUICK_LINKS = [ const QUICK_LINKS=[
{ label: "Namecheap DNS", emoji: "🌐", url: "https://ap.www.namecheap.com/domains/domaincontrolpanel/manohargupta.com/advancedns" }, {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:"Hetzner Console",emoji:"🖥",url:"https://console.hetzner.cloud"},
{ label: "OpenRouter", emoji: "🤖", url: "https://openrouter.ai/activity" }, {label:"OpenRouter",emoji:"🤖",url:"https://openrouter.ai/activity"},
{ label: "BotFather", emoji: "🤖", url: "https://t.me/BotFather" }, {label:"BotFather",emoji:"🤖",url:"https://t.me/BotFather"},
{ label: "NSE India", emoji: "📈", url: "https://www.nseindia.com" }, {label:"NSE India",emoji:"📈",url:"https://www.nseindia.com"},
{ label: "CERC", emoji: "⚡", url: "https://cercind.gov.in" }, {label:"CERC",emoji:"⚡",url:"https://cercind.gov.in"},
{ label: "MNRE", emoji: "☀️", url: "https://mnre.gov.in" }, {label:"MNRE",emoji:"☀️",url:"https://mnre.gov.in"},
{ label: "IEX India", emoji: "💹", url: "https://www.iexindia.com" }, {label:"IEX India",emoji:"💹",url:"https://www.iexindia.com"},
]; ];
function useTime() { function useTime(){const[n,s]=useState(new Date());useEffect(()=>{const t=setInterval(()=>s(new Date()),1000);return()=>clearInterval(t)},[]);return n}
const [now, setNow] = useState(new Date()); function greet(h){return h<12?"Good morning":h<17?"Good afternoon":"Good evening"}
useEffect(() => { const t = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(t); }, []);
return now; function Dot({status}){
const c={online:"#22c55e",running:"#22c55e",warning:"#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 greeting(h) { function Shell({label,right,children}){
if (h < 12) return "Good morning"; return(
if (h < 17) return "Good afternoon"; <div style={{background:"var(--c-surface)",border:"1px solid var(--c-border)",borderRadius:4,padding:"12px 14px"}}>
return "Good evening"; <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}
function fmtLastCheck(m) { </div>
if (m < 1) return "just now"; {children}
if (m < 60) return `${m}m ago`; </div>
return `${Math.round(m / 60)}h ago`;
}
function StatusDot({ status }) {
const map = { online: "#22c55e", warning: "#f59e0b", offline: "#ef4444" };
const c = map[status] || map.offline;
return (
<span style={{
display: "inline-block", width: 7, height: 7, borderRadius: "50%",
background: c, flexShrink: 0,
"--pulse-c": c + "55",
animation: status === "online" ? "pulse 2s ease infinite" : "none",
}} />
); );
} }
function ServiceModal({ svc, onClose }) { function DiskBar({disk}){
if (!svc) return null; if(!disk)return <Shell label="Disk"><span style={{color:"var(--c-muted)",fontSize:12}}>Loading...</span></Shell>;
return ( const c=disk.status==="ok"?"#22c55e":disk.status==="warning"?"#f59e0b":"#ef4444";
<div onClick={onClose} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.35)", backdropFilter: "blur(4px)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000 }}> return(
<div onClick={e => e.stopPropagation()} style={{ background: "var(--c-surface)", border: "1px solid var(--c-border)", borderRadius: 16, padding: "32px 36px", width: 340, boxShadow: "0 24px 64px rgba(0,0,0,0.2)" }}> <Shell label="Disk" right={<span style={{fontFamily:"'DM Mono',monospace",fontSize:12,color:c,fontWeight:500}}>{disk.percent}%</span>}>
<div style={{ display: "flex", alignItems: "center", gap: 16, marginBottom: 20 }}> <div style={{height:5,background:"var(--c-border)",borderRadius:3,overflow:"hidden",margin:"6px 0"}}>
<span style={{ fontSize: 42 }}>{svc.emoji}</span> <div style={{height:"100%",width:`${disk.percent}%`,background:c,borderRadius:3,transition:"width 0.5s"}}/>
<div> </div>
<div style={{ fontWeight: 600, fontSize: 18, color: "var(--c-text)" }}>{svc.name}</div> <div style={{display:"flex",justifyContent:"space-between"}}>
<div style={{ color: "var(--c-muted)", fontSize: 13, marginTop: 3 }}>{svc.desc}</div> <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> </div>
</div> ))
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 24 }}> }
<StatusDot status={svc.status} /> </Shell>
<span style={{ fontSize: 13, color: "var(--c-muted)", textTransform: "capitalize" }}> );
{svc.status} · {fmtLastCheck(svc.lastCheck)} }
</span>
</div> function Markets({data}){
<div style={{ display: "flex", gap: 10 }}> if(!data)return <Shell label="Markets"><span style={{color:"var(--c-muted)",fontSize:12}}>Loading...</span></Shell>;
<a href={svc.url} target="_blank" rel="noopener noreferrer" const live=data.nifty?.close!=null;
style={{ flex: 1, background: "var(--c-accent)", color: "#fff", borderRadius: 8, padding: "11px 0", textAlign: "center", fontSize: 14, fontWeight: 500, textDecoration: "none", display: "block" }}> return(
Open Service <Shell label="Markets">
</a> {live
<button onClick={onClose} ?<div style={{display:"flex",gap:16,marginTop:6}}>
style={{ flex: 1, background: "transparent", color: "var(--c-text)", border: "1px solid var(--c-border)", borderRadius: 8, padding: "11px 0", fontSize: 14, fontWeight: 500, cursor: "pointer", fontFamily: "inherit" }}> <div>
Close <div style={{fontSize:11,color:"var(--c-muted)"}}>NIFTY 50</div>
</button> <div style={{fontFamily:"'DM Mono',monospace",fontSize:16,fontWeight:500,color:"var(--c-text)"}}>{data.nifty.close?.toLocaleString()}</div>
</div> </div>
<div style={{width:1,background:"var(--c-border)"}}/>
<div>
<div style={{fontSize:11,color:"var(--c-muted)"}}>IEX DAM</div>
<div style={{fontFamily:"'DM Mono',monospace",fontSize:16,fontWeight:500,color:"var(--c-text)"}}>₹{data.iex_dam?.weighted_avg_price_inr??"—"}</div>
</div>
</div>
:<div style={{fontSize:12,color:"var(--c-muted)",fontStyle:"italic",marginTop:4}}>📡 Data pipeline coming soon</div>
}
</Shell>
);
}
function StatCell({val,lbl}){
const[h,sH]=useState(false);
return(
<div 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 style={{fontFamily:"'DM Mono',monospace",fontWeight:500,fontSize:15,color:h?"var(--c-accent)":"var(--c-text)",transition:"color 0.15s"}}>{val}</div>
<div 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}){
const[h,sH]=useState(false);
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"}}>
<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> </div>
<Dot status="online"/>
{h&&<div style={{position:"absolute",right:10,top:10,fontSize:10,fontWeight:600,color:"var(--c-accent)",letterSpacing:"0.04em"}}>OPEN →</div>}
</div> </div>
); );
} }
function StatCell({ val, lbl }) { function Section({sec}){
const [h, sH] = useState(false); const[open,setOpen]=useState(true);
return ( return(
<div onMouseEnter={() => sH(true)} onMouseLeave={() => sH(false)} <div style={{marginBottom:28}}>
style={{ textAlign: "center", padding: "8px 20px", background: h ? "var(--c-accent-faint)" : "transparent", transition: "background 0.15s", cursor: "default" }}> <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"}}>
<div style={{ fontFamily: "'DM Mono', monospace", fontWeight: 500, fontSize: 15, color: h ? "var(--c-accent)" : "var(--c-text)", transition: "color 0.15s" }}>{val}</div> <span style={{fontSize:11,fontWeight:600,letterSpacing:"0.1em",textTransform:"uppercase",color:"var(--c-muted)"}}>{sec.label}</span>
<div style={{ fontSize: 11, color: h ? "var(--c-accent)" : "var(--c-muted)", marginTop: 2, letterSpacing: "0.06em", textTransform: "uppercase", transition: "color 0.15s" }}>{lbl}</div> <span style={{marginLeft:"auto",fontSize:12,color:"var(--c-muted)",display:"inline-block",transition:"transform 0.2s",transform:open?"rotate(0deg)":"rotate(-90deg)"}}></span>
</div>
);
}
function ServiceCard({ svc, onClick }) {
const [h, sH] = useState(false);
return (
<div onClick={onClick} 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",
}}>
<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)", display: "flex", alignItems: "center", gap: 6 }}>
{svc.name}
{svc.badge && (
<span style={{ background: "var(--c-accent)", color: "#fff", fontSize: 10, fontWeight: 600, padding: "1px 6px", borderRadius: 4 }}>
{svc.badge}
</span>
)}
</div>
<div style={{ fontSize: 12, color: "var(--c-muted)", marginTop: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{svc.desc}</div>
<div style={{ fontSize: 11, color: "var(--c-muted)", marginTop: 4, opacity: 0.7 }}>{fmtLastCheck(svc.lastCheck)}</div>
</div>
<StatusDot status={svc.status} />
{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, onCardClick }) {
const [open, setOpen] = useState(true);
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>
<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> </button>
{open && ( {open&&<div style={{display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(200px, 1fr))",gap:10}}>{sec.services.map(s=><Card key={s.id} svc={s}/>)}</div>}
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: 10 }}>
{sec.services.map(svc => (
<ServiceCard key={svc.id} svc={svc} onClick={() => window.open(svc.url, "_blank")} />
))}
</div>
)}
</div> </div>
); );
} }
function QuickLink({ lk }) { function QL({lk}){
const [h, sH] = useState(false); const[h,sH]=useState(false);
return ( return(
<a href={lk.url} target="_blank" rel="noopener noreferrer" <a href={lk.url} target="_blank" rel="noopener noreferrer" onMouseEnter={()=>sH(true)} onMouseLeave={()=>sH(false)}
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"}}>
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}
<span style={{ fontSize: 14 }}>{lk.emoji}</span>
{lk.label}
</a> </a>
); );
} }
function App() { function App(){
const [dark, setDark] = useState(() => window.matchMedia("(prefers-color-scheme: dark)").matches); const[dark,setDark]=useState(()=>window.matchMedia("(prefers-color-scheme: dark)").matches);
const [qlOpen, setQlOpen] = useState(true); const[qlOpen,setQlOpen]=useState(true);
const[live,setLive]=useState(null);
const now = useTime(); const[ts,setTs]=useState(null);
const now=useTime();
useEffect(() => { const fetch_=useCallback(async()=>{
const t = dark ? { try{const r=await fetch(`${API}/api/all`);if(!r.ok)throw new Error(r.status);setLive(await r.json());setTs(new Date())}
"--c-bg": "oklch(13% 0.012 180)", catch(e){console.warn("API:",e.message)}
"--c-surface": "oklch(17% 0.012 180)", },[]);
"--c-surface2": "oklch(21% 0.012 180)",
"--c-border": "oklch(25% 0.012 180)", useEffect(()=>{fetch_();const t=setInterval(fetch_,REFRESH);return()=>clearInterval(t)},[fetch_]);
"--c-text": "oklch(92% 0.008 180)",
"--c-muted": "oklch(55% 0.01 180)", useEffect(()=>{
"--c-accent": "oklch(55% 0.15 180)", const t=dark?{
"--c-accent-faint": "oklch(55% 0.15 180 / 0.12)", "--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-bg": "oklch(97% 0.01 180)", "--c-text":"oklch(92% 0.008 180)","--c-muted":"oklch(55% 0.01 180)",
"--c-surface": "oklch(99.5% 0.006 180)", "--c-accent":"oklch(55% 0.15 180)","--c-accent-faint":"oklch(55% 0.15 180 / 0.12)",
"--c-surface2": "oklch(95% 0.012 180)", }:{
"--c-border": "oklch(88% 0.012 180)", "--c-bg":"oklch(97% 0.01 180)","--c-surface":"oklch(99.5% 0.006 180)",
"--c-text": "oklch(18% 0.012 180)", "--c-surface2":"oklch(95% 0.012 180)","--c-border":"oklch(88% 0.012 180)",
"--c-muted": "oklch(52% 0.01 180)", "--c-text":"oklch(18% 0.012 180)","--c-muted":"oklch(52% 0.01 180)",
"--c-accent": "oklch(55% 0.15 180)", "--c-accent":"oklch(55% 0.15 180)","--c-accent-faint":"oklch(55% 0.15 180 / 0.08)",
"--c-accent-faint": "oklch(55% 0.15 180 / 0.08)",
}; };
const r = document.documentElement; const r=document.documentElement;
Object.entries(t).forEach(([k, v]) => r.style.setProperty(k, v)); Object.entries(t).forEach(([k,v])=>r.style.setProperty(k,v));
document.body.style.background = "var(--c-bg)"; document.body.style.background="var(--c-bg)";
document.body.style.color = "var(--c-text)"; document.body.style.color="var(--c-text)";
}, [dark]); },[dark]);
return ( const srv=live?.server||{};
const stats=[
[srv.cpu_percent!=null?`${srv.cpu_percent}%`:"—","CPU"],
[srv.ram_used_gb!=null?`${srv.ram_used_gb} GB`:"—","RAM used"],
[srv.disk?.free_gb!=null?`${srv.disk.free_gb} GB`:"—","Disk free"],
[srv.uptime_days!=null?`${srv.uptime_days}d`:"—","Uptime"],
];
return(
<> <>
<button onClick={() => setDark(d => !d)} <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)"}}>
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?"☀️":"🌙"}
{dark ? "☀️" : "🌙"}
</button> </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={{maxWidth:960,margin:"0 auto",padding:"36px 24px 64px"}}>
<div style={{display:"flex",alignItems:"flex-start",justifyContent:"space-between",marginBottom:32}}>
{/* Header */}
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", marginBottom: 40 }}>
<div> <div>
<div style={{ fontSize: 14, color: "var(--c-muted)", marginBottom: 6 }}>{greeting(now.getHours())}, Manohar</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)" }}> <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")} {String(now.getHours()).padStart(2,"0")}:{String(now.getMinutes()).padStart(2,"0")}
<span style={{ fontSize: 35, opacity: 0.35, marginLeft: 4 }}>:{String(now.getSeconds()).padStart(2, "0")}</span> <span style={{fontSize:35,opacity:0.35,marginLeft:4}}>:{String(now.getSeconds()).padStart(2,"0")}</span>
</div> </div>
<div style={{ marginTop: 6, display: "flex", alignItems: "center", gap: 8 }}> <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={{fontSize:13,fontWeight:500,color:"var(--c-muted)"}}>{now.toLocaleDateString("en-US",{weekday:"long"})}</span>
<span style={{ color: "var(--c-border)" }}>·</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> <span style={{fontSize:13,color:"var(--c-muted)"}}>{now.toLocaleDateString("en-US",{month:"long",day:"numeric",year:"numeric"})}</span>
</div> </div>
</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"}}>
<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],i,arr)=>(
{[["3%","CPU"],["3.7 GB","RAM free"],["35 GB","Disk free"],["12d","Uptime"]].map(([v, l], i, arr) => ( <React.Fragment key={lbl}>
<React.Fragment key={l}> <StatCell val={val} lbl={lbl}/>
<StatCell val={v} lbl={l} /> {i<arr.length-1&&<div style={{width:1,background:"var(--c-border)",margin:"8px 0"}}/>}
{i < arr.length - 1 && <div style={{ width: 1, background: "var(--c-border)", margin: "8px 0" }} />}
</React.Fragment> </React.Fragment>
))} ))}
</div> </div>
</div> </div>
{/* Sections */} <div style={{display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(180px, 1fr))",gap:10,marginBottom:32}}>
{SECTIONS.map(sec => <Section key={sec.id} sec={sec} onCardClick={(svc) => window.open(svc.url, "_blank")} />)} <DiskBar disk={live?.server?.disk}/>
<ContainerGrid data={live?.containers}/>
<LiveVisitors data={live?.umami}/>
<TigerTasks data={live?.tiger}/>
<Markets data={live?.markets}/>
</div>
{/* Quick Links */} {SECTIONS.map(s=><Section key={s.id} sec={s}/>)}
<div style={{ marginTop: 8 }}>
<button onClick={() => setQlOpen(o => !o)} <div style={{marginTop:8}}>
style={{ display: "flex", alignItems: "center", width: "100%", background: "none", border: "none", cursor: "pointer", padding: "0 0 12px", gap: 8, fontFamily: "inherit" }}> <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={{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> <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> </button>
{qlOpen && ( {qlOpen&&<div style={{display:"flex",flexWrap:"wrap",gap:8}}>{QUICK_LINKS.map(l=><QL key={l.label} lk={l}/>)}</div>}
<div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
{QUICK_LINKS.map(lk => <QuickLink key={lk.label} lk={lk} />)}
</div>
)}
</div> </div>
</div> </div>
</> </>
); );
} }
ReactDOM.createRoot(document.getElementById("root")).render(<App />); ReactDOM.createRoot(document.getElementById("root")).render(<App/>);
</script> </script>
</body> </body>
</html> </html>