bojler-esp/data/www/index.html
2026-09-07 15:04:21 +02:00

1 line
9.8 KiB
HTML

<!doctype html> <html lang=cs data-bs-theme=dark> <head> <meta charset=UTF-8> <meta name=viewport content="width=device-width,initial-scale=1"> <title>Bojler</title> <link href=https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css rel=stylesheet> <script src=https://cdn.jsdelivr.net/npm/chart.js></script> <style>:root{--color-low:#198754;--color-ok:#ffc107;--color-high:#dc3545}[data-bs-theme=dark]{--color-low:#2ecc71;--color-ok:#f1c40f;--color-high:#e74c3c}.temp-low{color:var(--color-low)!important}.temp-ok{color:var(--color-ok)!important}.temp-high{color:var(--color-high)!important}.temp-display{font-size:4rem;font-weight:700;transition:color .3s ease}.card{border-radius:15px;border:none;box-shadow:0 4px 12px rgba(0,0,0,.1)}[data-bs-theme=dark] body{background-color:#121212}[data-bs-theme=dark] .card{background-color:#1e1e1e;box-shadow:0 8px 16px rgba(0,0,0,.5)}[data-bs-theme=dark] .form-control,[data-bs-theme=dark] .form-select{background-color:#2b2b2b;border-color:#3a3a3a;color:#fff}#notification{position:fixed;top:20px;right:20px;z-index:1050;display:none}#event-log-container{max-height:350px;overflow-y:auto;overflow-x:hidden}#event-log-container::-webkit-scrollbar{width:8px}#event-log-container::-webkit-scrollbar-track{background:0 0}#event-log-container::-webkit-scrollbar-thumb{background:rgba(150,150,150,.3);border-radius:4px}</style> </head> <body> <div id=notification class="alert rounded-pill px-4 shadow"></div> <div class="container py-4"> <div class="d-flex justify-content-between align-items-center mb-4"> <h2 class="mb-0 fw-bold">🌡️ bojler</h2> <button id=theme-toggle class="btn btn-outline-light rounded-pill"> ☀️ Světlý </button> </div> <div class="row g-4 mb-4"> <div class=col-md-6> <div class="card h-100 text-center p-4"> <h5 class="opacity-75 mb-2">Aktuální teplota</h5> <div id=current-temp class="temp-display text-secondary">-- °C</div> <hr class="my-4 border-secondary opacity-25"> <h5 class="opacity-75 mb-3">Stav ventilu</h5> <h3 id=valve-state class=fw-bold>❓ Neznámý</h3> </div> </div> <div class=col-md-6> <div class="card h-100 p-4"> <h5 class="opacity-75 mb-4">⚙️ Nastavení</h5> <form id=settings-form> <div class=mb-3> <label for=t0 class="form-label fw-bold">Spodní mez (t0) [°C]</label> <input type=number step=0.1 class="form-control form-control-lg" id=t0 required> </div> <div class=mb-4> <label for=t1 class="form-label fw-bold">Horní mez (t1) [°C]</label> <input type=number step=0.1 class="form-control form-control-lg" id=t1 required> </div> <button type=submit class="btn btn-primary btn-lg w-100 fw-bold" id=save-btn>💾 Uložit hodnoty</button> </form> </div> </div> </div> <div class="row mb-4"> <div class=col-12> <div class="card p-4"> <div class="d-flex justify-content-between align-items-center mb-4 flex-wrap"> <h5 class="opacity-75 mb-0">📈 Historie teploty</h5> <select id=history-range class="form-select w-auto mt-2 mt-sm-0 shadow-sm"> <option value=24 selected>1 den (24h)</option> <option value=48>2 dny (48h)</option> <option value=72>3 dny (72h)</option> <option value=96>4 dny (96h)</option> <option value=120>5 dní (120h)</option> </select> </div> <div style=height:400px;width:100%> <canvas id=historyChart></canvas> </div> </div> </div> </div> <div class=row> <div class=col-12> <div class="card p-4"> <div class="d-flex justify-content-between align-items-center mb-4"> <h5 class="opacity-75 mb-0">📋 Záznam událostí</h5> <button id=refresh-log-btn class="btn btn-sm btn-outline-secondary rounded-circle" title="Aktualizovat události" style=width:32px;height:32px;display:flex;align-items:center;justify-content:center> 🔄 </button> </div> <div id=event-log-container class=pe-2> <div class="text-center text-secondary py-3">Načítám události...</div> </div> </div> </div> </div> </div> <script>let formInitialized=!1,chartInstance=null,currentChartData={data:[],hours:24};const themeToggleBtn=document.getElementById("theme-toggle");let currentTheme=localStorage.getItem("theme")||"dark";function applyTheme(t){document.documentElement.setAttribute("data-bs-theme",t),localStorage.setItem("theme",t),currentTheme=t,"dark"===t?(themeToggleBtn.innerHTML="☀️ Světlý",themeToggleBtn.classList.replace("btn-outline-secondary","btn-outline-light")):(themeToggleBtn.innerHTML="🌙 Tmavý",themeToggleBtn.classList.replace("btn-outline-light","btn-outline-secondary")),currentChartData.data.length>0&&updateChart(currentChartData.data,currentChartData.hours)}function showNotification(t,e=!1){const n=document.getElementById("notification");n.className="alert rounded-pill px-4 shadow-lg "+(e?"alert-danger":"alert-success"),n.textContent=t,n.style.display="block",setTimeout((()=>n.style.display="none"),3e3)}async function fetchStatus(){try{const t=await fetch("/api/status");if(!t.ok)throw new Error("API error");const e=await t.json(),n=document.getElementById("current-temp");n.textContent=`${e.temp.toFixed(1)} °C`,n.classList.remove("temp-low","temp-ok","temp-high","text-secondary"),e.temp<e.t0?n.classList.add("temp-low"):e.temp>e.t1?n.classList.add("temp-high"):n.classList.add("temp-ok");const a=document.getElementById("valve-state"),o=1===e.valve||!0===e.valve;let r="";!o&&e.temp>e.t1?r=' <span title="Otevřete ventil" style="cursor: help;">⚠️</span>':o&&e.temp<e.t0&&(r=' <span title="Zavřete ventil" style="cursor: help;">⚠️</span>'),a.innerHTML=o?`🟢 <span class="text-success">Otevřen</span>${r}`:`🔴 <span class="text-danger">Zavřen</span>${r}`,formInitialized||(document.getElementById("t0").value=e.t0,document.getElementById("t1").value=e.t1,formInitialized=!0)}catch(t){document.getElementById("current-temp").textContent="-- °C",document.getElementById("current-temp").className="temp-display text-secondary",document.getElementById("valve-state").innerHTML='❓ <span class="opacity-50">Nedostupné</span>'}}async function fetchHistory(t){try{const e=await fetch(`/api/history?hours=${t}`);if(!e.ok)throw new Error("API error");let n=await e.json();const a=4*t;n.length>a&&(n=n.slice(-a)),currentChartData={data:n,hours:Number(t)},updateChart(n)}catch(e){console.error("Nepodařilo se načíst historii:",e),currentChartData={data:[],hours:Number(t)},updateChart([])}}function updateChart(t){const e=document.getElementById("historyChart").getContext("2d"),n="dark"===currentTheme?"#adb5bd":"#495057",a="dark"===currentTheme?"rgba(255, 255, 255, 0.05)":"rgba(0, 0, 0, 0.05)",o="dark"===currentTheme?"#00d4ff":"#0d6efd",r="dark"===currentTheme?"rgba(0, 212, 255, 0.15)":"rgba(13, 110, 253, 0.1)";Chart.defaults.color=n;const s=new Date,l=[],i=[];t.forEach(((e,n)=>{const a=15*(t.length-1-n),o=new Date(s.getTime()-6e4*a),r=o.getDate(),c=o.getMonth()+1,d=o.getHours(),m=o.getMinutes().toString().padStart(2,"0");if(l.push(`${r}. ${c}. ${d}:${m}`),0===a)i.push("nyní");else{const t=Math.floor(a/1440),e=Math.floor(a%1440/60),n=a%60;let o=[];t>0&&o.push(`${t} d`),e>0&&o.push(`${e} h`),n>0&&o.push(`${n} min`),i.push("před "+o.join(" "))}})),chartInstance&&chartInstance.destroy(),chartInstance=new Chart(e,{type:"line",data:{labels:l,datasets:[{label:"Teplota (°C)",data:t,borderColor:o,backgroundColor:r,borderWidth:2,fill:!0,pointRadius:0,pointHitRadius:15,tension:0}]},options:{responsive:!0,maintainAspectRatio:!1,scales:{x:{title:{display:!0,text:"Čas",color:n},ticks:{maxTicksLimit:8,maxRotation:45,color:n},grid:{display:!1}},y:{title:{display:!0,text:"Teplota (°C)",color:n},ticks:{color:n},grid:{color:a,drawBorder:!1},min:0,max:100}},plugins:{legend:{display:!1},tooltip:{backgroundColor:"dark"===currentTheme?"rgba(0,0,0,0.8)":"rgba(255,255,255,0.9)",titleColor:"dark"===currentTheme?"#fff":"#000",bodyColor:"dark"===currentTheme?"#fff":"#000",borderColor:a,borderWidth:1,callbacks:{title:function(t){const e=t[0].dataIndex;return`${l[e]} (${i[e]})`}}}}}})}async function fetchLog(){const t=document.getElementById("event-log-container");try{const e=await fetch("/api/log");if(!e.ok)throw new Error("API error");const n=await e.json();if(!n||0===n.length)return void(t.innerHTML='<div class="text-center text-secondary py-3">Zatím nejsou k dispozici žádné události.</div>');n.sort(((t,e)=>e.ts-t.ts)),t.innerHTML=n.map((t=>{const e=t.ts<2e10?1e3*t.ts:t.ts,n=new Date(e).toLocaleString("cs-CZ");let a="border-info";return 1===t.t?a="border-warning":2===t.t&&(a="border-danger"),`\n <div class="p-3 mb-3 rounded bg-body-tertiary border-start border-4 ${a} shadow-sm">\n <div class="d-flex justify-content-between mb-1">\n <small class="text-secondary fw-semibold">${n}</small>\n </div>\n <div class="fw-medium">${t.m}</div>\n </div>\n `})).join("")}catch(e){console.error("Nepodařilo se načíst log:",e),t.innerHTML='<div class="text-center text-danger py-3">Nepodařilo se načíst záznam událostí.</div>'}}themeToggleBtn.addEventListener("click",(()=>{applyTheme("dark"===currentTheme?"light":"dark")})),applyTheme(currentTheme),document.getElementById("refresh-log-btn").addEventListener("click",fetchLog),document.getElementById("settings-form").addEventListener("submit",(async t=>{t.preventDefault();const e=document.getElementById("save-btn");e.disabled=!0;const n={t0:parseFloat(document.getElementById("t0").value),t1:parseFloat(document.getElementById("t1").value)};try{if(!(await fetch("/api/thresholds",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).ok)throw new Error;showNotification("✅ Nastavení bylo úspěšně uloženo!"),fetchStatus(),fetchLog()}catch(t){showNotification("❌ Chyba při ukládání!",!0)}finally{e.disabled=!1}})),document.getElementById("history-range").addEventListener("change",(t=>{fetchHistory(t.target.value)})),fetchStatus(),fetchHistory(document.getElementById("history-range").value),fetchLog(),setInterval(fetchStatus,1e4)</script>