Meta Description" name="description" />

Share this result

Previews are deleted daily. Get a permanent share link sent to your inbox:
Script
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>EnviroSense — IoT Environmental Dashboard</title> <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet"/> <style> :root { --bg: #080e12; --surface: #0d1a22; --surface2: #112030; --border: rgba(0,220,180,0.12); --accent: #00ddb4; --accent2: #00aaff; --accent3: #ff6b35; --warn: #ffb830; --danger: #ff3b5c; --text: #e8f4f0; --muted: #5a7a70; --card-glow: 0 0 40px rgba(0,221,180,0.06); --font-display: 'Syne', sans-serif; --font-mono: 'Space Mono', monospace; } * { margin:0; padding:0; box-sizing:border-box; } body { background: var(--bg); color: var(--text); font-family: var(--font-display); min-height: 100vh; overflow-x: hidden; } /* Animated grid background */ body::before { content:''; position:fixed; inset:0; z-index:0; background-image: linear-gradient(rgba(0,221,180,0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(0,221,180,0.03) 1px, transparent 1px); background-size: 40px 40px; pointer-events:none; } /* Ambient glow orbs */ body::after { content:''; position:fixed; inset:0; z-index:0; background: radial-gradient(ellipse 600px 400px at 10% 20%, rgba(0,221,180,0.05) 0%, transparent 70%), radial-gradient(ellipse 500px 600px at 90% 70%, rgba(0,170,255,0.04) 0%, transparent 70%); pointer-events:none; } .layout { position:relative; z-index:1; display:flex; min-height:100vh; } /* === SIDEBAR === */ .sidebar { width: 220px; flex-shrink: 0; background: rgba(13,26,34,0.95); border-right: 1px solid var(--border); display:flex; flex-direction:column; padding: 0; position:sticky; top:0; height:100vh; backdrop-filter: blur(20px); } .logo { padding: 24px 20px 20px; border-bottom: 1px solid var(--border); } .logo-mark { display:flex; align-items:center; gap:10px; } .logo-icon { width:32px; height:32px; background: linear-gradient(135deg, var(--accent), var(--accent2)); border-radius:8px; display:flex; align-items:center; justify-content:center; font-size:16px; box-shadow: 0 0 20px rgba(0,221,180,0.4); } .logo-text { font-size: 16px; font-weight: 800; letter-spacing: -0.3px; color: var(--text); } .logo-sub { font-family: var(--font-mono); font-size: 9px; color: var(--muted); letter-spacing: 1.5px; margin-top:2px; text-transform: uppercase; } nav { padding: 16px 0; flex:1; } .nav-section { font-family: var(--font-mono); font-size: 9px; letter-spacing: 2px; color: var(--muted); padding: 8px 20px 6px; text-transform: uppercase; } .nav-item { display:flex; align-items:center; gap:10px; padding: 10px 20px; font-size: 13px; font-weight: 600; color: var(--muted); cursor:pointer; border-left: 2px solid transparent; transition: all 0.2s; position:relative; } .nav-item:hover { color: var(--text); background: rgba(0,221,180,0.04); } .nav-item.active { color: var(--accent); border-left-color: var(--accent); background: rgba(0,221,180,0.06); } .nav-item .icon { font-size:15px; width:18px; text-align:center; } .nav-badge { margin-left:auto; background: var(--danger); color:#fff; font-family: var(--font-mono); font-size: 9px; padding: 2px 5px; border-radius: 10px; animation: pulse-badge 2s infinite; } @keyframes pulse-badge { 0%,100% { opacity:1; } 50% { opacity:0.6; } } .sidebar-footer { padding: 16px 20px; border-top: 1px solid var(--border); } .device-status { display:flex; align-items:center; gap:8px; font-family: var(--font-mono); font-size: 10px; color: var(--muted); } .status-dot { width:7px; height:7px; border-radius:50%; background: var(--accent); box-shadow: 0 0 8px var(--accent); animation: pulse-dot 2s infinite; } @keyframes pulse-dot { 0%,100% { transform:scale(1); opacity:1; } 50% { transform:scale(1.3); opacity:0.7; } } /* === MAIN === */ .main { flex:1; display:flex; flex-direction:column; overflow:hidden; } /* HEADER */ .topbar { display:flex; align-items:center; justify-content:space-between; padding: 16px 28px; border-bottom: 1px solid var(--border); background: rgba(8,14,18,0.8); backdrop-filter: blur(20px); position:sticky; top:0; z-index:10; } .topbar-left { display:flex; flex-direction:column; } .page-title { font-size: 20px; font-weight: 800; letter-spacing: -0.5px; } .page-sub { font-family: var(--font-mono); font-size: 10px; color: var(--muted); margin-top:1px; } .topbar-right { display:flex; align-items:center; gap:12px; } .time-display { font-family: var(--font-mono); font-size: 12px; color: var(--accent); background: rgba(0,221,180,0.06); border: 1px solid var(--border); padding: 6px 12px; border-radius: 6px; letter-spacing:1px; } .topbar-btn { background: var(--surface2); border: 1px solid var(--border); color: var(--text); padding: 7px 14px; border-radius: 6px; font-family: var(--font-display); font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s; display:flex; align-items:center; gap:6px; } .topbar-btn:hover { border-color: var(--accent); color: var(--accent); } .alert-btn { position:relative; } .alert-btn::after { content:'3'; position:absolute; top:-4px; right:-4px; background:var(--danger); color:#fff; font-size:9px; width:14px; height:14px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-family: var(--font-mono); } /* CONTENT */ .content { padding: 24px 28px; flex:1; overflow-y:auto; } /* === STAT CARDS (top row) === */ .stats-row { display:grid; grid-template-columns: repeat(5, 1fr); gap: 14px; margin-bottom: 20px; } .stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 16px; position:relative; overflow:hidden; cursor:pointer; transition: all 0.25s; box-shadow: var(--card-glow); } .stat-card::before { content:''; position:absolute; top:0; left:0; right:0; height:2px; background: var(--card-color, var(--accent)); border-radius:12px 12px 0 0; } .stat-card:hover { border-color: rgba(0,221,180,0.25); transform: translateY(-2px); box-shadow: 0 8px 40px rgba(0,221,180,0.1); } .stat-label { font-family: var(--font-mono); font-size: 9px; letter-spacing: 1.5px; color: var(--muted); text-transform: uppercase; margin-bottom: 10px; display:flex; align-items:center; justify-content:space-between; } .stat-icon { font-size:13px; } .stat-value { font-size: 26px; font-weight: 800; letter-spacing: -1px; line-height:1; color: var(--card-color, var(--accent)); } .stat-unit { font-size: 11px; font-weight: 400; color: var(--muted); margin-left:2px; } .stat-delta { font-family: var(--font-mono); font-size: 10px; margin-top: 6px; display:flex; align-items:center; gap:4px; } .delta-up { color: var(--danger); } .delta-down { color: var(--accent); } .delta-ok { color: var(--accent); } .stat-bar { margin-top:10px; height:3px; background: rgba(255,255,255,0.05); border-radius:2px; overflow:hidden; } .stat-bar-fill { height:100%; background: var(--card-color, var(--accent)); border-radius:2px; transition: width 1s ease; box-shadow: 0 0 8px var(--card-color, var(--accent)); } /* === MAIN GRID === */ .main-grid { display:grid; grid-template-columns: 1fr 1fr 340px; gap: 16px; margin-bottom: 16px; } .card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; overflow:hidden; box-shadow: var(--card-glow); } .card-header { display:flex; align-items:center; justify-content:space-between; padding: 14px 18px 12px; border-bottom: 1px solid var(--border); } .card-title { font-size: 13px; font-weight: 700; display:flex; align-items:center; gap:7px; } .card-title .dot { width:6px; height:6px; border-radius:50%; background: var(--accent); box-shadow: 0 0 6px var(--accent); animation: pulse-dot 2s infinite; } .card-action { font-family: var(--font-mono); font-size: 9px; color: var(--muted); letter-spacing:1px; text-transform:uppercase; cursor:pointer; transition: color 0.2s; } .card-action:hover { color: var(--accent); } .card-body { padding: 16px 18px; } /* Chart canvas area */ .chart-wrap { position:relative; } canvas { display:block; width:100% !important; } /* === DEVICE MAP === */ .device-map { position:relative; background: var(--surface2); border-radius:8px; height:220px; overflow:hidden; } .map-grid { position:absolute; inset:0; background-image: linear-gradient(rgba(0,221,180,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(0,221,180,0.04) 1px, transparent 1px); background-size: 30px 30px; } .map-device { position:absolute; transform:translate(-50%,-50%); } .map-device-dot { width:12px; height:12px; border-radius:50%; border:2px solid var(--accent); background: rgba(0,221,180,0.3); position:relative; z-index:2; cursor:pointer; transition: transform 0.2s; } .map-device-dot:hover { transform:scale(1.4); } .map-device-dot.warn { border-color:var(--warn); background:rgba(255,184,48,0.3); } .map-device-dot.danger { border-color:var(--danger); background:rgba(255,59,92,0.3); } .map-ping { position:absolute; width:30px; height:30px; border-radius:50%; border:1px solid var(--accent); top:50%; left:50%; transform:translate(-50%,-50%); animation: map-ping 2.5s infinite; opacity:0; } .map-device-dot.warn + .map-ping { border-color:var(--warn); } .map-device-dot.danger + .map-ping { border-color:var(--danger); animation-duration:1.5s; } @keyframes map-ping { 0% { transform:translate(-50%,-50%) scale(0.3); opacity:0.8; } 100% { transform:translate(-50%,-50%) scale(2.5); opacity:0; } } .map-label { position:absolute; left:14px; top:50%; transform:translateY(-50%); white-space:nowrap; font-family:var(--font-mono); font-size:9px; color:var(--muted); } .map-legend { display:flex; gap:12px; margin-top:10px; } .legend-item { display:flex; align-items:center; gap:5px; font-family:var(--font-mono); font-size:9px; color:var(--muted); } .legend-dot { width:8px; height:8px; border-radius:50%; } /* === ALERTS PANEL === */ .alerts-list { display:flex; flex-direction:column; gap:8px; } .alert-item { display:flex; align-items:flex-start; gap:10px; padding: 10px 12px; border-radius:8px; background: var(--surface2); border: 1px solid transparent; transition: all 0.2s; cursor:pointer; } .alert-item:hover { border-color: var(--border); } .alert-item.danger { border-color: rgba(255,59,92,0.2); background:rgba(255,59,92,0.06); } .alert-item.warn { border-color: rgba(255,184,48,0.2); background:rgba(255,184,48,0.05); } .alert-item.info { border-color: rgba(0,170,255,0.2); background:rgba(0,170,255,0.04); } .alert-icon { font-size:14px; margin-top:1px; } .alert-content { flex:1; } .alert-title { font-size:11px; font-weight:700; margin-bottom:2px; } .alert-item.danger .alert-title { color:var(--danger); } .alert-item.warn .alert-title { color:var(--warn); } .alert-item.info .alert-title { color:var(--accent2); } .alert-desc { font-family:var(--font-mono); font-size:9px; color:var(--muted); line-height:1.4; } .alert-time { font-family:var(--font-mono); font-size:9px; color:var(--muted); white-space:nowrap; } /* === BOTTOM GRID === */ .bottom-grid { display:grid; grid-template-columns: 1fr 1fr; gap: 16px; } /* === DEVICE TABLE === */ .device-table { width:100%; border-collapse:collapse; } .device-table th { font-family:var(--font-mono); font-size:9px; color:var(--muted); letter-spacing:1.5px; text-transform:uppercase; text-align:left; padding: 0 10px 10px; border-bottom: 1px solid var(--border); } .device-table td { padding: 10px; font-size:12px; border-bottom: 1px solid rgba(0,221,180,0.05); vertical-align:middle; } .device-table tr:last-child td { border-bottom:none; } .device-table tr:hover td { background:rgba(0,221,180,0.02); } .device-name { font-weight:700; } .device-id { font-family:var(--font-mono); font-size:9px; color:var(--muted); margin-top:1px; } .status-pill { display:inline-flex; align-items:center; gap:4px; font-family:var(--font-mono); font-size:9px; font-weight:700; padding:3px 8px; border-radius:20px; } .status-pill.online { background:rgba(0,221,180,0.1); color:var(--accent); border:1px solid rgba(0,221,180,0.2); } .status-pill.offline { background:rgba(255,59,92,0.1); color:var(--danger); border:1px solid rgba(255,59,92,0.2); } .status-pill.warn { background:rgba(255,184,48,0.1); color:var(--warn); border:1px solid rgba(255,184,48,0.2); } .signal-bars { display:flex; align-items:flex-end; gap:2px; height:14px; } .bar { width:4px; border-radius:1px; background:var(--muted); } .bar.active { background:var(--accent); } /* === GAUGE === */ .gauges-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; } .gauge-item { text-align:center; } .gauge-svg { display:block; margin:0 auto; } .gauge-label { font-family:var(--font-mono); font-size:9px; color:var(--muted); margin-top:4px; letter-spacing:1px; text-transform:uppercase; } .gauge-val { font-size:14px; font-weight:800; margin-top:2px; } /* SCROLLBAR */ .content::-webkit-scrollbar { width:4px; } .content::-webkit-scrollbar-track { background:transparent; } .content::-webkit-scrollbar-thumb { background:var(--border); border-radius:2px; } /* ANIMATIONS */ @keyframes fadeUp { from { opacity:0; transform:translateY(16px); } to { opacity:1; transform:translateY(0); } } .stat-card { animation: fadeUp 0.5s ease both; } .stat-card:nth-child(1) { animation-delay:0.05s; } .stat-card:nth-child(2) { animation-delay:0.1s; } .stat-card:nth-child(3) { animation-delay:0.15s; } .stat-card:nth-child(4) { animation-delay:0.2s; } .stat-card:nth-child(5) { animation-delay:0.25s; } .card { animation: fadeUp 0.5s ease both; animation-delay:0.3s; } /* === INLINE SPARKLINES === */ .sparkline { display:inline-block; vertical-align:middle; } </style> </head> <body> <div class="layout"> <!-- SIDEBAR --> <aside class="sidebar"> <div class="logo"> <div class="logo-mark"> <div class="logo-icon">🌿</div> <div> <div class="logo-text">EnviroSense</div> <div class="logo-sub">IoT Platform v2.4</div> </div> </div> </div> <nav> <div class="nav-section">Monitor</div> <div class="nav-item active"><span class="icon">⬡</span> Overview</div> <div class="nav-item"><span class="icon">📡</span> Live Feed <span class="nav-badge">LIVE</span></div> <div class="nav-item"><span class="icon">🗺</span> Device Map</div> <div class="nav-section" style="margin-top:8px">Analyze</div> <div class="nav-item"><span class="icon">📊</span> Analytics</div> <div class="nav-item"><span class="icon">📈</span> Trends</div> <div class="nav-item"><span class="icon">🔔</span> Alerts <span class="nav-badge">3</span></div> <div class="nav-section" style="margin-top:8px">Manage</div> <div class="nav-item"><span class="icon">⚙</span> Devices</div> <div class="nav-item"><span class="icon">👥</span> Users</div> <div class="nav-item"><span class="icon">📄</span> Reports</div> <div class="nav-item"><span class="icon">🔧</span> Settings</div> </nav> <div class="sidebar-footer"> <div class="device-status"> <div class="status-dot"></div> <span>24 / 26 ONLINE</span> </div> </div> </aside> <!-- MAIN --> <div class="main"> <!-- TOPBAR --> <header class="topbar"> <div class="topbar-left"> <div class="page-title">Environmental Overview</div> <div class="page-sub" id="live-sub">Realtime · Updated just now · Zone A–F</div> </div> <div class="topbar-right"> <div class="time-display" id="live-clock">--:--:--</div> <button class="topbar-btn alert-btn">🔔 Alerts</button> <button class="topbar-btn">⬇ Export</button> <button class="topbar-btn" style="background:var(--accent);color:#080e12;border-color:var(--accent);">+ Add Device</button> </div> </header> <!-- CONTENT --> <div class="content"> <!-- STAT CARDS --> <div class="stats-row"> <div class="stat-card" style="--card-color:#00ddb4"> <div class="stat-label">CO₂ Level <span class="stat-icon">💨</span></div> <div class="stat-value" id="co2-val">412<span class="stat-unit">ppm</span></div> <div class="stat-delta delta-ok">▲ +3 ppm from 1h ago · GOOD</div> <div class="stat-bar"><div class="stat-bar-fill" id="co2-bar" style="width:41%"></div></div> </div> <div class="stat-card" style="--card-color:#00aaff"> <div class="stat-label">Temperature <span class="stat-icon">🌡</span></div> <div class="stat-value" id="temp-val">23.4<span class="stat-unit">°C</span></div> <div class="stat-delta delta-ok">▼ −0.3° from 1h ago · NORMAL</div> <div class="stat-bar"><div class="stat-bar-fill" id="temp-bar" style="width:62%"></div></div> </div> <div class="stat-card" style="--card-color:#8b5cf6"> <div class="stat-label">Humidity <span class="stat-icon">💧</span></div> <div class="stat-value" id="hum-val">58<span class="stat-unit">%</span></div> <div class="stat-delta delta-ok">▼ −2% from 1h ago · OPTIMAL</div> <div class="stat-bar"><div class="stat-bar-fill" id="hum-bar" style="width:58%"></div></div> </div> <div class="stat-card" style="--card-color:#ffb830"> <div class="stat-label">PM2.5 <span class="stat-icon">🌫</span></div> <div class="stat-value" id="pm-val">28<span class="stat-unit">µg/m³</span></div> <div class="stat-delta delta-up">▲ +4 from 1h ago · MODERATE</div> <div class="stat-bar"><div class="stat-bar-fill" id="pm-bar" style="width:28%"></div></div> </div> <div class="stat-card" style="--card-color:#ff6b35"> <div class="stat-label">Noise Level <span class="stat-icon">🔊</span></div> <div class="stat-value" id="noise-val">67<span class="stat-unit">dB</span></div> <div class="stat-delta delta-up">▲ +5 dB from 1h ago · HIGH</div> <div class="stat-bar"><div class="stat-bar-fill" id="noise-bar" style="width:67%"></div></div> </div> </div> <!-- MAIN GRID --> <div class="main-grid"> <!-- AIR QUALITY CHART --> <div class="card"> <div class="card-header"> <div class="card-title"><span class="dot"></span> Air Quality — 24h Trend</div> <div class="card-action">CO₂ · PM2.5 · VOC</div> </div> <div class="card-body chart-wrap"> <canvas id="airChart" height="180"></canvas> </div> </div> <!-- TEMP/HUMIDITY CHART --> <div class="card"> <div class="card-header"> <div class="card-title"><span class="dot" style="background:var(--accent2);box-shadow:0 0 6px var(--accent2)"></span> Temperature & Humidity</div> <div class="card-action">Last 12h</div> </div> <div class="card-body chart-wrap"> <canvas id="thChart" height="180"></canvas> </div> </div> <!-- ALERTS --> <div class="card"> <div class="card-header"> <div class="card-title">⚠ Active Alerts</div> <div class="card-action">View All →</div> </div> <div class="card-body"> <div class="alerts-list"> <div class="alert-item danger"> <div class="alert-icon">🔴</div> <div class="alert-content"> <div class="alert-title">PM2.5 Threshold Exceeded</div> <div class="alert-desc">Zone B · Device ENV-007<br>Current: 34 µg/m³ · Limit: 25</div> </div> <div class="alert-time">2m ago</div> </div> <div class="alert-item warn"> <div class="alert-icon">🟡</div> <div class="alert-content"> <div class="alert-title">Noise Level Warning</div> <div class="alert-desc">Zone D · Device ENV-012<br>Current: 78 dB · Limit: 70 dB</div> </div> <div class="alert-time">15m ago</div> </div> <div class="alert-item warn"> <div class="alert-icon">🟡</div> <div class="alert-content"> <div class="alert-title">Device Battery Low</div> <div class="alert-desc">Zone F · Device ENV-021<br>Battery at 8% — needs replacement</div> </div> <div class="alert-time">1h ago</div> </div> <div class="alert-item info"> <div class="alert-icon">🔵</div> <div class="alert-content"> <div class="alert-title">Firmware Update Available</div> <div class="alert-desc">6 devices pending update<br>v2.4.1 → v2.4.2</div> </div> <div class="alert-time">3h ago</div> </div> </div> </div> </div> </div> <!-- BOTTOM GRID --> <div class="bottom-grid"> <!-- DEVICE TABLE --> <div class="card"> <div class="card-header"> <div class="card-title">📡 Device Status</div> <div class="card-action">All 26 Devices →</div> </div> <div class="card-body" style="padding:0 0 4px"> <table class="device-table"> <thead> <tr> <th style="padding-left:16px">Device</th> <th>Zone</th> <th>Status</th> <th>Signal</th> <th style="padding-right:16px">Last Ping</th> </tr> </thead> <tbody id="device-tbody"> <!-- filled by JS --> </tbody> </table> </div> </div> <!-- GAUGES + MAP --> <div class="card"> <div class="card-header"> <div class="card-title">🗺 Sensor Zones</div> <div class="card-action">Full Map →</div> </div> <div class="card-body"> <div class="device-map"> <div class="map-grid"></div> <!-- Devices scattered across map --> <div class="map-device" style="left:18%;top:25%"> <div class="map-device-dot"></div> <div class="map-ping"></div> <div class="map-label">Zone A</div> </div> <div class="map-device" style="left:40%;top:40%"> <div class="map-device-dot danger"></div> <div class="map-ping"></div> <div class="map-label">Zone B</div> </div> <div class="map-device" style="left:65%;top:22%"> <div class="map-device-dot"></div> <div class="map-ping"></div> <div class="map-label">Zone C</div> </div> <div class="map-device" style="left:78%;top:60%"> <div class="map-device-dot warn"></div> <div class="map-ping"></div> <div class="map-label">Zone D</div> </div> <div class="map-device" style="left:30%;top:72%"> <div class="map-device-dot"></div> <div class="map-ping"></div> <div class="map-label">Zone E</div> </div> <div class="map-device" style="left:55%;top:75%"> <div class="map-device-dot warn"></div> <div class="map-ping"></div> <div class="map-label">Zone F</div> </div> <div class="map-device" style="left:85%;top:30%"> <div class="map-device-dot"></div> <div class="map-ping"></div> <div class="map-label">Zone G</div> </div> </div> <div class="map-legend" style="margin-top:10px"> <div class="legend-item"><div class="legend-dot" style="background:var(--accent)"></div>Normal</div> <div class="legend-item"><div class="legend-dot" style="background:var(--warn)"></div>Warning</div> <div class="legend-item"><div class="legend-dot" style="background:var(--danger)"></div>Critical</div> </div> <!-- Mini gauges below map --> <div class="gauges-grid" style="margin-top:14px"> <div class="gauge-item"> <svg class="gauge-svg" width="70" height="40" viewBox="0 0 70 40"> <path d="M5,38 A30,30 0 0,1 65,38" fill="none" stroke="#1a2f2a" stroke-width="6" stroke-linecap="round"/> <path d="M5,38 A30,30 0 0,1 65,38" fill="none" stroke="#00ddb4" stroke-width="6" stroke-linecap="round" stroke-dasharray="94" stroke-dashoffset="50" opacity="0.9"/> <text x="35" y="36" text-anchor="middle" font-family="'Syne',sans-serif" font-size="11" font-weight="800" fill="#e8f4f0">AQI</text> </svg> <div class="gauge-label">Air Quality</div> <div class="gauge-val" style="color:var(--accent)">Good</div> </div> <div class="gauge-item"> <svg class="gauge-svg" width="70" height="40" viewBox="0 0 70 40"> <path d="M5,38 A30,30 0 0,1 65,38" fill="none" stroke="#1a2f2a" stroke-width="6" stroke-linecap="round"/> <path d="M5,38 A30,30 0 0,1 65,38" fill="none" stroke="#ffb830" stroke-width="6" stroke-linecap="round" stroke-dasharray="94" stroke-dashoffset="30" opacity="0.9"/> <text x="35" y="36" text-anchor="middle" font-family="'Syne',sans-serif" font-size="11" font-weight="800" fill="#e8f4f0">73%</text> </svg> <div class="gauge-label">Uptime</div> <div class="gauge-val" style="color:var(--warn)">73/h</div> </div> <div class="gauge-item"> <svg class="gauge-svg" width="70" height="40" viewBox="0 0 70 40"> <path d="M5,38 A30,30 0 0,1 65,38" fill="none" stroke="#1a2f2a" stroke-width="6" stroke-linecap="round"/> <path d="M5,38 A30,30 0 0,1 65,38" fill="none" stroke="#00aaff" stroke-width="6" stroke-linecap="round" stroke-dasharray="94" stroke-dashoffset="15" opacity="0.9"/> <text x="35" y="36" text-anchor="middle" font-family="'Syne',sans-serif" font-size="11" font-weight="800" fill="#e8f4f0">92%</text> </svg> <div class="gauge-label">Connectivity</div> <div class="gauge-val" style="color:var(--accent2)">92%</div> </div> </div> </div> </div> </div> </div><!-- /content --> </div><!-- /main --> </div><!-- /layout --> <script> // ===================== // CLOCK // ===================== function updateClock() { const now = new Date(); document.getElementById('live-clock').textContent = now.toTimeString().slice(0,8); } updateClock(); setInterval(updateClock, 1000); // ===================== // GENERATE CHART DATA // ===================== function genData(len, base, amp, noiseAmp) { return Array.from({length:len}, (_,i) => { const t = i / len; return +(base + Math.sin(t * Math.PI * 3) * amp + (Math.random()-0.5)*noiseAmp).toFixed(1); }); } const hours24 = Array.from({length:25}, (_,i) => { const h = (new Date().getHours() - 24 + i + 24) % 24; return h + ':00'; }); const hours12 = Array.from({length:13}, (_,i) => { const h = (new Date().getHours() - 12 + i + 24) % 24; return h + ':00'; }); // ===================== // CANVAS CHARTS // ===================== function drawLineChart(canvasId, labels, datasets, opts={}) { const canvas = document.getElementById(canvasId); if(!canvas) return; const ctx = canvas.getContext('2d'); const W = canvas.parentElement.clientWidth - 36; const H = opts.height || 180; canvas.width = W; canvas.height = H; const pad = {top:16, right:16, bottom:28, left:40}; const pw = W - pad.left - pad.right; const ph = H - pad.top - pad.bottom; ctx.clearRect(0,0,W,H); // Grid lines ctx.strokeStyle = 'rgba(0,221,180,0.06)'; ctx.lineWidth = 1; for(let i=0;i<=4;i++){ const y = pad.top + (ph/4)*i; ctx.beginPath(); ctx.moveTo(pad.left,y); ctx.lineTo(W-pad.right,y); ctx.stroke(); } // All datasets share same x-axis const n = labels.length; datasets.forEach(ds => { const vals = ds.data; const min = ds.min ?? Math.min(...vals)*0.9; const max = ds.max ?? Math.max(...vals)*1.1; const range = max - min || 1; const pts = vals.map((v,i) => ({ x: pad.left + (i/(n-1))*pw, y: pad.top + ph - ((v-min)/range)*ph })); // Fill gradient const grad = ctx.createLinearGradient(0, pad.top, 0, pad.top+ph); grad.addColorStop(0, ds.color + '30'); grad.addColorStop(1, ds.color + '00'); ctx.beginPath(); ctx.moveTo(pts[0].x, pad.top+ph); pts.forEach(p => ctx.lineTo(p.x, p.y)); ctx.lineTo(pts[pts.length-1].x, pad.top+ph); ctx.closePath(); ctx.fillStyle = grad; ctx.fill(); // Line ctx.beginPath(); ctx.moveTo(pts[0].x, pts[0].y); for(let i=1;i<pts.length;i++){ const cp1x = (pts[i-1].x + pts[i].x)/2; ctx.bezierCurveTo(cp1x, pts[i-1].y, cp1x, pts[i].y, pts[i].x, pts[i].y); } ctx.strokeStyle = ds.color; ctx.lineWidth = 2; ctx.stroke(); // Last dot const last = pts[pts.length-1]; ctx.beginPath(); ctx.arc(last.x, last.y, 4, 0, Math.PI*2); ctx.fillStyle = ds.color; ctx.shadowColor = ds.color; ctx.shadowBlur = 10; ctx.fill(); ctx.shadowBlur = 0; }); // X-axis labels (show every 4th) ctx.fillStyle = 'rgba(90,122,112,0.8)'; ctx.font = '8px "Space Mono"'; ctx.textAlign = 'center'; labels.forEach((l,i) => { if(i % 4 === 0) { const x = pad.left + (i/(n-1))*pw; ctx.fillText(l, x, H-6); } }); // Y-axis labels for first dataset const ds0 = datasets[0]; const min0 = ds0.min ?? Math.min(...ds0.data)*0.9; const max0 = ds0.max ?? Math.max(...ds0.data)*1.1; ctx.textAlign = 'right'; for(let i=0;i<=4;i++){ const v = max0 - ((max0-min0)/4)*i; ctx.fillText(v.toFixed(0), pad.left-6, pad.top + (ph/4)*i + 4); } // Legend if(opts.legend !== false) { let lx = pad.left; datasets.forEach(ds => { ctx.fillStyle = ds.color; ctx.fillRect(lx, 3, 16, 3); ctx.fillStyle = 'rgba(90,122,112,0.9)'; ctx.font = '8px "Space Mono"'; ctx.textAlign = 'left'; ctx.fillText(ds.label, lx+20, 9); lx += ctx.measureText(ds.label).width + 36; }); } } // Draw both charts function drawCharts() { drawLineChart('airChart', hours24, [ { label:'CO₂ (ppm)', data: genData(25, 410, 30, 10), color:'#00ddb4', min:360, max:500 }, { label:'PM2.5 (µg/m³)', data: genData(25, 22, 8, 4), color:'#ffb830', min:0, max:60 }, { label:'VOC Index', data: genData(25, 100, 40, 15), color:'#8b5cf6', min:0, max:200 }, ]); drawLineChart('thChart', hours12, [ { label:'Temp (°C)', data: genData(13, 23, 2, 0.5), color:'#00aaff', min:18, max:30 }, { label:'Humidity (%)', data: genData(13, 58, 8, 3), color:'#8b5cf6', min:30, max:90 }, ]); } drawCharts(); window.addEventListener('resize', drawCharts); // ===================== // DEVICE TABLE // ===================== const devices = [ { name:'ENV-001', id:'A4:E2:3F', zone:'Zone A', status:'online', signal:4, ping:'2s' }, { name:'ENV-007', id:'B1:F9:2A', zone:'Zone B', status:'warn', signal:3, ping:'4s' }, { name:'ENV-012', id:'C3:D1:8E', zone:'Zone D', status:'warn', signal:2, ping:'8s' }, { name:'ENV-015', id:'E2:A4:1C', zone:'Zone C', status:'online', signal:4, ping:'1s' }, { name:'ENV-021', id:'F7:B2:9D', zone:'Zone F', status:'online', signal:1, ping:'12s' }, { name:'ENV-026', id:'G5:C8:4B', zone:'Zone G', status:'online', signal:4, ping:'2s' }, ]; function signalBars(level) { let html = '<div class="signal-bars">'; for(let i=1;i<=4;i++){ html += `<div class="bar ${i<=level?'active':''}" style="height:${i*3+4}px"></div>`; } return html + '</div>'; } const tbody = document.getElementById('device-tbody'); devices.forEach(d => { const statusMap = { online: '<span class="status-pill online">● Online</span>', warn: '<span class="status-pill warn">● Warning</span>', offline:'<span class="status-pill offline">● Offline</span>', }; tbody.innerHTML += ` <tr> <td style="padding-left:16px"> <div class="device-name">${d.name}</div> <div class="device-id">${d.id}</div> </td> <td style="font-family:var(--font-mono);font-size:10px;color:var(--muted)">${d.zone}</td> <td>${statusMap[d.status]}</td> <td>${signalBars(d.signal)}</td> <td style="font-family:var(--font-mono);font-size:10px;color:var(--muted);padding-right:16px">${d.ping} ago</td> </tr> `; }); // ===================== // LIVE DATA SIMULATION // ===================== const metrics = { co2: { el:'co2-val', bar:'co2-bar', base:412, amp:8, unit:'ppm', suffix:'', max:1000 }, temp: { el:'temp-val', bar:'temp-bar', base:23.4, amp:0.4, unit:'°C', suffix:'', max:40 }, hum: { el:'hum-val', bar:'hum-bar', base:58, amp:3, unit:'%', suffix:'', max:100 }, pm: { el:'pm-val', bar:'pm-bar', base:28, amp:4, unit:'µg/m³', suffix:'', max:100 }, noise:{ el:'noise-val',bar:'noise-bar',base:67, amp:5, unit:'dB', suffix:'', max:100 }, }; setInterval(() => { Object.values(metrics).forEach(m => { const v = m.base + (Math.random()-0.5)*m.amp*2; const disp = m.base < 5 ? v.toFixed(1) : Math.round(v); const el = document.getElementById(m.el); if(el) el.innerHTML = `${disp}<span class="stat-unit">${m.unit}</span>`; const bar = document.getElementById(m.bar); if(bar) bar.style.width = Math.min(100, Math.max(5, (v/m.max)*100)).toFixed(0) + '%'; }); // Redraw charts every 10s }, 3000); setInterval(drawCharts, 10000); </script> </body> </html>
Landing Page
This ad does not have a landing page available
Network Timeline
Performance Summary

5

Requests

3

Domains

104KB

Transfer Size

110KB

Content Size

295.0ms

Dom Content Loaded

492.0ms

First Paint

403.0ms

Load Time
Domain Breakdown
Transfer Size (bytes)
Loading...
Content Size (bytes)
Loading...
Header Size (bytes)
Loading...
Requests
Loading...
Timings (ms)
Loading...
Total Time
Loading...
Content Breakdown
Transfer Size (bytes)
Loading...
Content Size (bytes)
Loading...
Header Size (bytes)
Loading...
Requests
Loading...