348 lines
20 KiB
HTML
348 lines
20 KiB
HTML
<!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:"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='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M3 3v18h18'/><rect x='7' y='10' width='3' height='8' rx='1' fill='none'/><line x1='8.5' y1='7' x2='8.5' y2='10'/><line x1='8.5' y1='18' x2='8.5' y2='21'/><rect x='12' y='6' width='3' height='12' rx='1' fill='none'/><line x1='13.5' y1='3' x2='13.5' y2='6'/><line x1='13.5' y1='18' x2='13.5' y2='21'/><rect x='17' y='12' width='3' height='6' rx='1' fill='none'/><line x1='18.5' y1='9' x2='18.5' y2='12'/><line x1='18.5' y1='18' x2='18.5' y2='21'/></svg>",url:"https://angel.manohargupta.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}){
|
|
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 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 live=data.nifty?.close!=null;
|
|
return(
|
|
<Shell label="Markets">
|
|
{live
|
|
?<div style={{display:"flex",gap:16,marginTop:6}}>
|
|
<div>
|
|
<div style={{fontSize:11,color:"var(--c-muted)"}}>NIFTY 50</div>
|
|
<div style={{fontFamily:"'DM Mono',monospace",fontSize:16,fontWeight:500,color:"var(--c-text)"}}>{data.nifty.close?.toLocaleString()}</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 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:h?"var(--c-accent)":"var(--c-text)",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}){
|
|
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"}}>
|
|
{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>
|
|
<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>
|
|
);
|
|
}
|
|
|
|
function Section({sec}){
|
|
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>
|
|
{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>
|
|
);
|
|
}
|
|
|
|
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||{};
|
|
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)} 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],i,arr)=>(
|
|
<React.Fragment key={lbl}>
|
|
<StatCell val={val} lbl={lbl}/>
|
|
{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}/>)}
|
|
|
|
<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>
|