1 line
10 KiB
HTML
1 line
10 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}.valve-warning{color:var(--color-ok);font-size:80%;font-weight:400}</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 gap-2"> <h5 class="opacity-75 mb-0">📈 Historie teploty</h5> <div class="d-flex gap-2 flex-wrap ms-auto"> <select id=y-axis-range class="form-select w-auto shadow-sm"> <option value=0-100>Osa Y: 0 - 100 °C</option> <option value=10-80>Osa Y: 10 - 80 °C</option> <option value=auto>Osa Y: Auto</option> </select> <select id=history-range class="form-select w-auto 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> <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)}themeToggleBtn.addEventListener("click",(()=>{applyTheme("dark"===currentTheme?"light":"dark")})),applyTheme(currentTheme);const yAxisRangeSelect=document.getElementById("y-axis-range");let currentYAxisRange=localStorage.getItem("graphRange")||"0-100";function showNotification(t,e=!1){const a=document.getElementById("notification");a.className="alert rounded-pill px-4 shadow-lg "+(e?"alert-danger":"alert-success"),a.textContent=t,a.style.display="block",setTimeout((()=>a.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(),a=document.getElementById("current-temp");a.textContent=`${e.temp.toFixed(1)} °C`,a.classList.remove("temp-low","temp-ok","temp-high","text-secondary"),e.temp<e.t0?a.classList.add("temp-low"):e.temp>e.t1?a.classList.add("temp-high"):a.classList.add("temp-ok");const n=document.getElementById("valve-state"),r=1===e.valve||!0===e.valve;let o="";!r&&e.temp>e.t1?o=' <span class="valve-warning">⚠️ Otevřete ventil</span>':r&&e.temp<e.t0&&(o=' <span class="valve-warning">⚠️ Zavřete ventil</span>'),n.innerHTML=r?`🟢 <span class="text-success">Otevřen</span>${o}`:`🔴 <span class="text-danger">Zavřen</span>${o}`,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 a=await e.json();const n=4*t;a.length>n&&(a=a.slice(-n)),currentChartData={data:a,hours:Number(t)},updateChart(a)}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"),a="dark"===currentTheme?"#adb5bd":"#495057",n="dark"===currentTheme?"rgba(255, 255, 255, 0.05)":"rgba(0, 0, 0, 0.05)",r="dark"===currentTheme?"#00d4ff":"#0d6efd",o="dark"===currentTheme?"rgba(0, 212, 255, 0.15)":"rgba(13, 110, 253, 0.1)";Chart.defaults.color=a;const s=new Date,l=[],c=[];t.forEach(((e,a)=>{const n=15*(t.length-1-a),r=new Date(s.getTime()-6e4*n),o=r.getDate(),i=r.getMonth()+1,d=r.getHours(),u=r.getMinutes().toString().padStart(2,"0");if(l.push(`${o}. ${i}. ${d}:${u}`),0===n)c.push("nyní");else{const t=Math.floor(n/1440),e=Math.floor(n%1440/60),a=n%60;let r=[];t>0&&r.push(`${t} d`),e>0&&r.push(`${e} h`),a>0&&r.push(`${a} min`),c.push("před "+r.join(" "))}})),chartInstance&&chartInstance.destroy();let i=0,d=100;"10-80"===currentYAxisRange?(i=10,d=80):"auto"===currentYAxisRange&&(i=void 0,d=void 0),chartInstance=new Chart(e,{type:"line",data:{labels:l,datasets:[{label:"Teplota (°C)",data:t,borderColor:r,backgroundColor:o,borderWidth:2,fill:!0,pointRadius:0,pointHitRadius:15,tension:0}]},options:{responsive:!0,maintainAspectRatio:!1,scales:{x:{title:{display:!0,text:"Čas",color:a},ticks:{maxTicksLimit:8,maxRotation:45,color:a},grid:{display:!1}},y:{title:{display:!0,text:"Teplota (°C)",color:a},ticks:{color:a},grid:{color:n,drawBorder:!1},min:i,max:d}},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:n,borderWidth:1,callbacks:{title:function(t){const e=t[0].dataIndex;return`${l[e]} (${c[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 a=await e.json();if(!a||0===a.length)return void(t.innerHTML='<div class="text-center text-secondary py-3">Zatím nejsou k dispozici žádné události.</div>');a.sort(((t,e)=>e.ts-t.ts)),t.innerHTML=a.map((t=>{const e=t.ts<2e10?1e3*t.ts:t.ts,a=new Date(e).toLocaleString("cs-CZ");let n="border-info";return 1===t.t?n="border-warning":2===t.t&&(n="border-danger"),`\n <div class="p-3 mb-3 rounded bg-body-tertiary border-start border-4 ${n} shadow-sm">\n <div class="d-flex justify-content-between mb-1">\n <small class="text-secondary fw-semibold">${a}</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>'}}yAxisRangeSelect.value=currentYAxisRange,yAxisRangeSelect.addEventListener("change",(t=>{currentYAxisRange=t.target.value,localStorage.setItem("graphRange",currentYAxisRange),currentChartData.data.length>0&&updateChart(currentChartData.data)})),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 a={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(a)})).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> |