Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>Live Status & Speed Test</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <meta name="color-scheme" content="light dark" /> | |
| <style> | |
| :root { --bg:#f8fafc; --card:#ffffff; --muted:#64748b; --ink:#0f172a; --ok:#16a34a; --warn:#f59e0b; --bad:#ef4444; --accent:#2563eb; --row:#f1f5f9; } | |
| @media (prefers-color-scheme: dark){ :root{ --bg:#0b1220; --card:#0f172a; --muted:#94a3b8; --ink:#e2e8f0; --row:#0b1326; } } | |
| body{ background:var(--bg); color:var(--ink) } | |
| table{ width:100%; border-collapse:collapse; font-size:.95rem } | |
| th,td{ padding:8px 10px; text-align:left; vertical-align:middle } | |
| th{ background:color-mix(in srgb, var(--row) 90%, transparent); font-weight:700; position:sticky; top:0; z-index:1 } | |
| tr:nth-child(even){ background:var(--row) } | |
| .status-dot{ width:10px; height:10px; border-radius:999px; display:inline-block; margin-right:.5rem } | |
| .trend{ width:140px; height:28px; display:block } | |
| .mono{ font-variant-numeric:tabular-nums } | |
| .btn{ background:var(--accent); color:#fff; border-radius:9999px; padding:.6rem .9rem; font-weight:700 } | |
| .btn:disabled{ opacity:.6; cursor:not-allowed } | |
| .ad-box{ background:var(--card); border:1px dashed #94a3b8; height:90px; display:flex; align-items:center; justify-content:center; color:var(--muted) } | |
| .pill{ padding:.15rem .5rem; border-radius:9999px; font-size:.75rem; font-weight:700 } | |
| .pill-ok{ background: color-mix(in srgb, var(--ok) 18%, transparent); color:var(--ok) } | |
| .pill-warn{ background: color-mix(in srgb, var(--warn) 18%, transparent); color:var(--warn) } | |
| .pill-bad{ background: color-mix(in srgb, var(--bad) 18%, transparent); color:var(--bad) } | |
| </style> | |
| </head> | |
| <body class="antialiased"> | |
| <div class="p-4 max-w-7xl mx-auto"> | |
| <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 border-b border-slate-300/40 pb-3 mb-4"> | |
| <div> | |
| <h1 class="text-2xl font-extrabold">Live Service Status</h1> | |
| <p class="text-sm" style="color:var(--muted)">Free endpoints, Resource Timing where allowed, with safe fallbacks</p> | |
| <p class="text-xs mono" id="lastUpdated">Updated β</p> | |
| </div> | |
| <div class="flex items-center gap-4"> | |
| <div class="text-sm"> | |
| <div class="font-semibold">Speed Test</div> | |
| <div class="mono text-xl"><span id="dlMbps">0.00</span> Mbps β β’ <span id="ulMbps">0.00</span> Mbps β</div> | |
| <div class="text-xs mono" style="color:var(--muted)">Latency <span id="latency">β</span> ms β’ Jitter <span id="jitter">β</span> ms β’ Loss <span id="loss">β</span>%</div> | |
| </div> | |
| <button id="runSpeedTest" class="btn">Run test</button> | |
| </div> | |
| </div> | |
| <div class="flex gap-3 text-xs mb-3"> | |
| <span class="pill pill-ok">Operational</span> | |
| <span class="pill pill-warn">Issues</span> | |
| <span class="pill pill-bad">Outage</span> | |
| </div> | |
| <div class="ad-box rounded mb-4"><span class="text-sm">Ad Placeholder (728Γ90)</span></div> | |
| <div class="overflow-x-auto rounded border border-slate-300/40 bg-[color:var(--card)]"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th class="w-1/3">Service</th> | |
| <th class="w-1/3">Status</th> | |
| <th class="w-1/3">Ping Trend</th> | |
| </tr> | |
| </thead> | |
| <tbody id="serviceStatus"> | |
| <!-- placeholder rows so itβs never blank --> | |
| </tbody> | |
| </table> | |
| </div> | |
| <div class="ad-box rounded my-4"><span class="text-sm">Ad Placeholder (Responsive)</span></div> | |
| <div class="grid md:grid-cols-3 gap-3"> | |
| <div class="p-3 rounded border border-slate-300/40 bg-[color:var(--card)]"> | |
| <div class="font-semibold mb-1">Protocol & cache</div> | |
| <div class="text-sm mono" id="protoInfo">β</div> | |
| </div> | |
| <div class="p-3 rounded border border-slate-300/40 bg-[color:var(--card)]"> | |
| <div class="font-semibold mb-1">Transfer stats</div> | |
| <div class="text-sm mono" id="xferInfo">β</div> | |
| </div> | |
| <div class="p-3 rounded border border-slate-300/40 bg-[color:var(--card)]"> | |
| <div class="font-semibold mb-1">Timing breakdown</div> | |
| <div class="text-sm mono" id="timingInfo">β</div> | |
| </div> | |
| </div> | |
| <footer class="mt-8 text-xs" style="color:var(--muted)"> | |
| <p>Uses CORS-permissive, no-key endpoints and image pings with cache-busting; results vary by CDN and device load.</p> | |
| <p>Β© 2025 Live Status.</p> | |
| </footer> | |
| </div> | |
| <script> | |
| (() => { | |
| // ---------- Safe, CORS-aware foundations ---------- | |
| const $ = s => document.querySelector(s); | |
| const sleep = ms => new Promise(r => setTimeout(r, ms)); | |
| const clamp = (v,a,b)=>Math.min(b,Math.max(a,v)); | |
| const avg = a => a.length ? a.reduce((x,y)=>x+y,0)/a.length : 0; | |
| const med = a => { if(!a.length) return 0; const b=[...a].sort((x,y)=>x-y); const m=b.length>>1; return b.length%2?b[m]:(b[m-1]+b[m])/2; }; | |
| const stdev = a => { const m=avg(a); return Math.sqrt(avg(a.map(x=>(x-m)**2))||0); }; | |
| const now = () => performance.now(); | |
| const key = () => crypto.getRandomValues(new Uint32Array(1))[0].toString(); | |
| const withCB = (u, extra={}) => { const url=new URL(u); url.searchParams.set('cb',key()); for(const [k,v] of Object.entries(extra)) url.searchParams.set(k,v); return url.toString(); }; | |
| // Only use fetch for endpoints that send CORS headers; otherwise use Image ping | |
| const CORS_GET = ['https://httpbin.org/get']; | |
| const CORS_POST = ['https://httpbin.org/post', 'https://postman-echo.com/post']; | |
| // Cloudflare test files are widely CORS-enabled; if blocked, code falls back automatically | |
| const DL_FILES = [ | |
| 'https://speed.cloudflare.com/__down?bytes=1048576', // ~1MB | |
| 'https://speed.cloudflare.com/__down?bytes=5242880', // ~5MB | |
| 'https://speed.cloudflare.com/__down?bytes=10485760' // ~10MB | |
| ]; | |
| // Performance observer (optional, not required for UI) | |
| const perfMap = new Map(); | |
| try { | |
| const po = new PerformanceObserver(list => { for (const e of list.getEntries()) if (e.entryType==='resource') perfMap.set(e.name, e); }); | |
| po.observe({ type:'resource', buffered:true }); | |
| } catch {} | |
| const fetchTimed = async (url, opts={}, timeout=15000) => { | |
| const ctrl = new AbortController(); | |
| const t0 = now(); | |
| const to = setTimeout(()=>ctrl.abort(), timeout); | |
| try { | |
| const res = await fetch(url, { cache:'no-store', mode:'cors', ...opts, signal: ctrl.signal }); | |
| const buf = await res.arrayBuffer().catch(()=>new ArrayBuffer(0)); | |
| clearTimeout(to); | |
| const entry = perfMap.get(url) || performance.getEntriesByName(url, 'resource')[0] || null; | |
| return { ok: res.ok, ms: now()-t0, bytes: buf.byteLength, entry }; | |
| } catch (e) { | |
| clearTimeout(to); | |
| return { ok:false, ms: now()-t0, bytes:0, entry:null }; | |
| } | |
| }; | |
| const imgPing = async (url, timeout=5000) => { | |
| const t0 = now(); | |
| const img = new Image(); | |
| img.referrerPolicy = 'no-referrer'; | |
| const p = new Promise(res => { | |
| const done = ok => res({ ok, ms: Math.round(now()-t0) }); | |
| img.onload = () => done(true); | |
| img.onerror = () => done(false); | |
| }); | |
| img.src = withCB(url); | |
| const to = sleep(timeout).then(()=>({ ok:false, ms: timeout })); | |
| return Promise.race([p, to]); | |
| }; | |
| const assetFor = name => { | |
| switch (name.toLowerCase()) { | |
| case 'google': return 'https://www.google.com/favicon.ico'; | |
| case 'youtube': return 'https://www.youtube.com/favicon.ico'; | |
| case 'facebook': return 'https://www.facebook.com/favicon.ico'; | |
| case 'instagram': return 'https://www.instagram.com/favicon.ico'; | |
| case 'x (twitter)': return 'https://twitter.com/favicon.ico'; | |
| case 'whatsapp': return 'https://web.whatsapp.com/favicon.ico'; | |
| case 'jio': return 'https://www.jio.com/assets/images/favicon.ico'; | |
| case 'airtel': return 'https://www.airtel.in/favicon.ico'; | |
| case 'amazon': return 'https://www.amazon.in/favicon.ico'; | |
| case 'netflix': return 'https://www.netflix.com/favicon.ico'; | |
| case 'discord': return 'https://discord.com/assets/847541504914fd33810e70a0ea73177e.ico'; | |
| default: return 'https://www.google.com/favicon.ico'; | |
| } | |
| }; | |
| const services = [ | |
| "Airtel","Jio","Instagram","Gmail","YouTube","Whatsapp","Flipkart","BSNL","ACT", | |
| "Google","Snapchat","Roblox","Steam","Telegram","Facebook","Amazon","Netflix","Discord", | |
| "IRCTC","Swiggy","Google Cloud","OpenAI","App Store","Microsoft 365" | |
| ]; | |
| const history = Object.fromEntries(services.map(s=>[s,[]])); | |
| const reliability = Object.fromEntries(services.map(s=>[s,1])); | |
| const MAX_POINTS = 30; | |
| // Pre-render placeholder rows so table is never blank | |
| (() => { | |
| const frag = document.createDocumentFragment(); | |
| for (const s of services) { | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = ` | |
| <td class="flex items-center"><span class="status-dot" style="background:var(--warn)"></span>${s}</td> | |
| <td class="mono" style="color:var(--muted);font-weight:700">Checkingβ¦</td> | |
| <td><svg class="trend" viewBox="0 0 140 28" preserveAspectRatio="none"><polyline points="0,14 140,14" fill="none" stroke="var(--muted)" stroke-width="1"/></svg></td> | |
| `; | |
| frag.appendChild(tr); | |
| } | |
| $('#serviceStatus').replaceChildren(frag); | |
| })(); | |
| const statusFrom = (ms, okRatio) => { | |
| if (okRatio < 0.6) return { label:'Outage', color:'var(--bad)' }; | |
| if (ms < 800 && okRatio > 0.9) return { label:'Operational', color:'var(--ok)' }; | |
| if (ms < 2500) return { label:'Issues', color:'var(--warn)' }; | |
| return { label:'Outage', color:'var(--bad)' }; | |
| }; | |
| const renderTrend = times => { | |
| if (!times.length) return `<svg class="trend" viewBox="0 0 140 28" preserveAspectRatio="none"><polyline points="0,14 140,14" fill="none" stroke="var(--muted)" stroke-width="1"/></svg>`; | |
| const w=140,h=28,maxMs=5000; | |
| const step = times.length>1 ? w/(times.length-1) : w; | |
| let pts=''; | |
| for (let i=0;i<times.length;i++){ | |
| const x=(i*step).toFixed(2); | |
| const y=(h - clamp(times[i],0,maxMs)/maxMs*h).toFixed(2); | |
| pts+=`${x},${y} `; | |
| } | |
| const last = times[times.length-1]; | |
| const color = last < 2000 ? 'var(--ok)' : last < 5000 ? 'var(--warn)' : 'var(--bad)'; | |
| return `<svg class="trend" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none" aria-hidden="true"> | |
| <polyline points="${pts.trim()}" fill="none" stroke="${color}" stroke-width="2"/> | |
| </svg>`; | |
| }; | |
| const updateServices = async () => { | |
| const rows = []; | |
| for (const s of services) { | |
| // Probe both: favicon via Image (no CORS needed), and a CORS GET we can time | |
| let imgRes = { ok:false, ms:5000 }, getRes = { ok:false, ms:5000 }; | |
| try { imgRes = await imgPing(assetFor(s), 5000); } catch {} | |
| try { getRes = await fetchTimed(withCB(CORS_GET[0]), {}, 5000); } catch {} | |
| const ok = imgRes.ok || getRes.ok; | |
| const t = med([imgRes.ms, getRes.ms].filter(Number.isFinite)); | |
| const hist = history[s]; | |
| hist.push(t || 5000); | |
| if (hist.length > MAX_POINTS) hist.shift(); | |
| reliability[s] = 0.8*reliability[s] + 0.2*(ok ? 1 : 0); | |
| const { label, color } = statusFrom(t || 5000, reliability[s]); | |
| rows.push({ name:s, ms: Math.round(t||5000), label, color, hist:[...hist] }); | |
| // yield to UI | |
| if ('requestIdleCallback' in window) await new Promise(res=>requestIdleCallback(res,{timeout:12})); | |
| else await sleep(0); | |
| } | |
| // Render batch | |
| const frag = document.createDocumentFragment(); | |
| for (const r of rows) { | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = ` | |
| <td class="flex items-center"><span class="status-dot" style="background:${r.color}"></span>${r.name}</td> | |
| <td class="mono" style="color:${r.color};font-weight:700">${r.label} (${r.ms} ms)</td> | |
| <td>${renderTrend(r.hist)}</td> | |
| `; | |
| frag.appendChild(tr); | |
| } | |
| $('#serviceStatus').replaceChildren(frag); | |
| $('#lastUpdated').textContent = `Updated ${new Date().toLocaleTimeString()}`; | |
| }; | |
| updateServices(); | |
| setInterval(updateServices, 60000); | |
| // ---------- Speed Test with guaranteed fallback ---------- | |
| const tween = (el, from, to, dur=600) => { | |
| from = Number(from)||0; to = Number(to)||0; | |
| const t0 = performance.now(); | |
| const step = t => { | |
| const p = clamp((t - t0)/dur, 0, 1); | |
| const v = from + (to - from) * (1 - Math.cos(Math.PI*p))/2; | |
| el.textContent = v.toFixed(2); | |
| if (p < 1) requestAnimationFrame(step); | |
| }; | |
| requestAnimationFrame(step); | |
| }; | |
| const latencyJitter = async (n=7) => { | |
| const samples=[]; let fails=0; | |
| for (let i=0;i<n;i++){ | |
| try { | |
| const r = await fetchTimed(withCB('https://www.google.com/favicon.ico'), {}, 4000); | |
| if (r.ok) samples.push(r.ms); else fails++; | |
| } catch { fails++; } | |
| await sleep(50 + Math.random()*70); | |
| } | |
| return { | |
| latency: samples.length ? Math.round(med(samples)) : 999, | |
| jitter: Math.round(stdev(samples)), | |
| loss: Math.round(100 * (fails / (samples.length + fails || 1))) | |
| }; | |
| }; | |
| const downloadFetch = async () => { | |
| // Try CORS-enabled Cloudflare test files; gracefully degrade if blocked | |
| const seq = [DL_FILES[0], DL_FILES[1]]; | |
| let accBytes = 0, accMs = 0, entries=[]; | |
| for (const u of seq) { | |
| const r = await fetchTimed(withCB(u), {}, 20000); | |
| if (r.ok) { accBytes += r.bytes; accMs = Math.max(accMs, r.ms); if (r.entry) entries.push(r.entry); } | |
| } | |
| // If too fast, refine with a bigger file | |
| if (accMs && accMs < 1500) { | |
| const r = await fetchTimed(withCB(DL_FILES[2]), {}, 20000); | |
| if (r.ok) { accBytes += r.bytes; accMs = Math.max(accMs, r.ms); if (r.entry) entries.push(r.entry); } | |
| } | |
| return { ok: accBytes>0 && accMs>0, mbps: accMs ? (accBytes*8)/accMs/1000 : 0, entries }; | |
| }; | |
| const downloadFallbackImage = async () => { | |
| // Stream an image; only start/end timing available, but works everywhere | |
| const t0 = now(); | |
| const img = new Image(); | |
| const done = new Promise(res=>{ | |
| img.onload = ()=>res({ ok:true, ms: now()-t0, bytes: 500000 }); // rough img size proxy | |
| img.onerror = ()=>res({ ok:false, ms: now()-t0, bytes: 0 }); | |
| }); | |
| img.src = withCB('https://www.gstatic.com/webp/gallery/1.jpg'); // generally accessible | |
| const r = await Promise.race([done, sleep(15000).then(()=>({ ok:false, ms:15000, bytes:0 }))]); | |
| return { ok:r.ok, mbps: r.ms ? (r.bytes*8)/r.ms/1000 : 0, entries:[] }; | |
| }; | |
| const uploadFetch = async () => { | |
| // Find a working no-key POST endpoint with CORS | |
| let endpoint = null; | |
| for (const ep of CORS_POST) { | |
| try { | |
| const t = await fetchTimed(withCB(ep), { method:'POST', body: new Uint8Array(1) }, 5000); | |
| if (t.ok) { endpoint = ep; break; } | |
| } catch {} | |
| } | |
| if (!endpoint) return { ok:false, mbps:0, entries:[] }; | |
| const total = 2*1024*1024; | |
| const parts = 2; | |
| const chunk = Math.floor(total/parts); | |
| const make = () => { const a = new Uint8Array(chunk); crypto.getRandomValues(a); return a; }; | |
| const tasks = Array.from({length:parts}, () => fetchTimed(withCB(endpoint), { | |
| method:'POST', | |
| headers:{ 'Content-Type':'application/octet-stream' }, | |
| body: make() | |
| }, 15000)); | |
| const res = await Promise.allSettled(tasks); | |
| const vals = res.map(v=>v.value || { ok:false, ms:0, entry:null }); | |
| const oks = vals.filter(v=>v.ok); | |
| const sent = oks.reduce((a,b)=> a + (b.entry?.encodedBodySize || chunk), 0); | |
| const ms = Math.max(...oks.map(v=>v.ms).concat([0])); | |
| return { ok: ms>0 && sent>0, mbps: ms ? (sent*8)/ms/1000 : 0, entries: oks.map(v=>v.entry).filter(Boolean) }; | |
| }; | |
| const runSpeed = async () => { | |
| const btn = $('#runSpeedTest'); btn.disabled = true; | |
| const lj = await latencyJitter(8).catch(()=>({latency:999,jitter:0,loss:100})); | |
| $('#latency').textContent = lj.latency; | |
| $('#jitter').textContent = lj.jitter; | |
| $('#loss').textContent = lj.loss; | |
| // Download: try CORS-first, then fallback image | |
| let dl = await downloadFetch().catch(()=>({ok:false,mbps:0,entries:[]})); | |
| if (!dl.ok) dl = await downloadFallbackImage(); | |
| tween($('#dlMbps'), $('#dlMbps').textContent, dl.mbps); | |
| // Upload: may be blocked on some networks; handle gracefully | |
| let ul = await uploadFetch().catch(()=>({ok:false,mbps:0,entries:[]})); | |
| tween($('#ulMbps'), $('#ulMbps').textContent, ul.mbps); | |
| // Diagnostics (best-effort) | |
| const e = (dl.entries&&dl.entries[0]) || (ul.entries&&ul.entries[0]) || null; | |
| if (e) { | |
| const dns = Math.max(0,(e.domainLookupEnd||0)-(e.domainLookupStart||0)); | |
| const conn= Math.max(0,(e.connectEnd||0)-(e.connectStart||0)); | |
| const tls = Math.max(0,(e.requestStart||0)-(e.secureConnectionStart||0)); | |
| const req = Math.max(0,(e.responseStart||0)-(e.requestStart||0)); | |
| const fetchT = Math.max(0,(e.responseEnd||0)-(e.fetchStart||0)); | |
| $('#protoInfo').textContent = `nextHop=${e.nextHopProtocol||'β'} β’ cache=${(e.transferSize===0)?'HIT':'MISS'}`; | |
| $('#xferInfo').textContent = `transfer=${e.transferSize||0}B β’ encoded=${e.encodedBodySize||0}B β’ decoded=${e.decodedBodySize||0}B`; | |
| $('#timingInfo').textContent = `dns=${dns|0}ms β’ conn=${conn|0}ms β’ tls=${tls|0}ms β’ req=${req|0}ms β’ fetch=${fetchT|0}ms`; | |
| } else { | |
| $('#protoInfo').textContent = 'nextHop=β β’ cache=β'; | |
| $('#xferInfo').textContent = 'transfer=β β’ encoded=β β’ decoded=β'; | |
| $('#timingInfo').textContent = 'β'; | |
| } | |
| setTimeout(()=>{ btn.disabled = false; }, 1200); | |
| }; | |
| $('#runSpeedTest').addEventListener('click', runSpeed); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |