Implement partial refresh, only refresh active tab and stop updates in

the background
This commit is contained in:
Johannes Schriewer 2026-08-31 02:44:46 +02:00
parent ec9f92e147
commit c756382178
12 changed files with 4538 additions and 20 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -5,18 +5,60 @@
const tabBtns = document.querySelectorAll(".tab-btn"); const tabBtns = document.querySelectorAll(".tab-btn");
const sections = document.querySelectorAll(".tab"); const sections = document.querySelectorAll(".tab");
// per-tab poll interval (ms); htmx "every" timers cannot pause per
// element, so only the active tab polls: JS dispatches "dash-poll" on
// its section, where every polling element listens via
// hx-trigger="dash-poll from:#tab-<name>"
const TAB_INTERVALS = { overview: 2000, disks: 2000, processes: 3000, journal: 5000, services: 15000, plugins: 5000 };
let activeTab = null;
let pollTimer = null;
let histTimer = null;
function fireTabEvent(type, name) {
const sec = document.getElementById("tab-" + name);
if (sec) htmx.trigger(sec, type);
}
function setHistoryPolling(on) {
if (on && !histTimer) {
pollHistory();
histTimer = setInterval(pollHistory, 2000);
} else if (!on && histTimer) {
clearInterval(histTimer);
histTimer = null;
}
}
function stopPolling() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
setHistoryPolling(false);
}
function startTabPolling(name) {
stopPolling();
activeTab = name;
// "dash-activate" is the one-shot trigger (plugins list), "dash-poll"
// the recurring one; firing both refreshes the tab immediately
fireTabEvent("dash-activate", name);
fireTabEvent("dash-poll", name);
pollTimer = setInterval(() => fireTabEvent("dash-poll", name), TAB_INTERVALS[name] || 5000);
setHistoryPolling(name === "overview");
}
function showTab(name) { function showTab(name) {
tabBtns.forEach((b) => b.classList.toggle("active", b.dataset.tab === name)); tabBtns.forEach((b) => b.classList.toggle("active", b.dataset.tab === name));
sections.forEach((s) => s.classList.toggle("hidden", s.id !== "tab-" + name)); sections.forEach((s) => s.classList.toggle("hidden", s.id !== "tab-" + name));
try { try {
localStorage.setItem("dash.tab", name); localStorage.setItem("dash.tab", name);
} catch (e) {} } catch (e) {}
startTabPolling(name);
} }
tabBtns.forEach((b) => b.addEventListener("click", () => showTab(b.dataset.tab))); tabBtns.forEach((b) => b.addEventListener("click", () => showTab(b.dataset.tab)));
try {
const saved = localStorage.getItem("dash.tab");
if (saved && document.getElementById("tab-" + saved)) showTab(saved);
} catch (e) {}
// ---------- helpers ---------- // ---------- helpers ----------
function fmtBytes(n, digits) { function fmtBytes(n, digits) {
@ -126,8 +168,6 @@
} }
initCharts(); initCharts();
pollHistory();
setInterval(pollHistory, 2000);
// ---------- journal ---------- // ---------- journal ----------
const journalLog = document.getElementById("journal-log"); const journalLog = document.getElementById("journal-log");
@ -229,4 +269,19 @@
loadSvcDetail(unit, detailRow.querySelector(".svc-detail")); loadSvcDetail(unit, detailRow.querySelector(".svc-detail"));
} }
}); });
// ---------- startup ----------
// pause all polling while this browser tab is hidden, resume with a
// fresh fetch when it becomes visible again
document.addEventListener("visibilitychange", () => {
if (document.hidden) stopPolling();
else if (activeTab) startTabPolling(activeTab);
});
let initial = "overview";
try {
const saved = localStorage.getItem("dash.tab");
if (saved && document.getElementById("tab-" + saved)) initial = saved;
} catch (e) {}
showTab(initial);
})(); })();

View file

@ -1,4 +1,4 @@
<div id="disks" class="panel" hx-get="/api/disks" hx-trigger="every 2s" hx-swap="outerHTML"> <div id="disks" class="panel" hx-get="/api/disks" hx-trigger="dash-poll from:#tab-disks" hx-swap="outerHTML">
<h2>Filesystems</h2> <h2>Filesystems</h2>
<div class="part-list"> <div class="part-list">
{% for p in partitions %} {% for p in partitions %}

View file

@ -22,7 +22,7 @@
</header> </header>
<main> <main>
<section id="tab-overview" class="tab"> <section id="tab-overview" class="tab">
<div id="overview" class="panel" hx-get="/api/overview" hx-trigger="load, every 2s" hx-swap="outerHTML"></div> <div id="overview" class="panel" hx-get="/api/overview" hx-trigger="dash-poll from:#tab-overview" hx-swap="outerHTML"></div>
<div class="charts-grid"> <div class="charts-grid">
<div class="chart-card"><h3>CPU / GPU %</h3><canvas id="chart-cpu"></canvas></div> <div class="chart-card"><h3>CPU / GPU %</h3><canvas id="chart-cpu"></canvas></div>
<div class="chart-card"><h3>Memory / VRAM %</h3><canvas id="chart-mem"></canvas></div> <div class="chart-card"><h3>Memory / VRAM %</h3><canvas id="chart-mem"></canvas></div>
@ -31,7 +31,7 @@
</section> </section>
<section id="tab-disks" class="tab hidden"> <section id="tab-disks" class="tab hidden">
<div id="disks" class="panel" hx-get="/api/disks" hx-trigger="load, every 2s" hx-swap="outerHTML"></div> <div id="disks" class="panel" hx-get="/api/disks" hx-trigger="dash-poll from:#tab-disks" hx-swap="outerHTML"></div>
</section> </section>
<section id="tab-processes" class="tab hidden"> <section id="tab-processes" class="tab hidden">
@ -52,7 +52,7 @@
</select> </select>
<button type="submit">apply</button> <button type="submit">apply</button>
</form> </form>
<div id="processes-body" class="panel" hx-get="/api/processes" hx-trigger="load, every 3s" hx-swap="outerHTML" hx-include="#proc-controls"></div> <div id="processes-body" class="panel" hx-get="/api/processes" hx-trigger="dash-poll from:#tab-processes" hx-swap="outerHTML" hx-include="#proc-controls"></div>
</section> </section>
<section id="tab-journal" class="tab hidden"> <section id="tab-journal" class="tab hidden">
@ -70,7 +70,7 @@
</form> </form>
<div id="journal-logwrap"> <div id="journal-logwrap">
<input type="hidden" id="journal-cursor" name="cursor" value=""> <input type="hidden" id="journal-cursor" name="cursor" value="">
<div id="journal-log" hx-get="/api/journal" hx-trigger="load, every 5s" hx-swap="beforeend" hx-include="#journal-filters, #journal-cursor"></div> <div id="journal-log" hx-get="/api/journal" hx-trigger="dash-poll from:#tab-journal" hx-swap="beforeend" hx-include="#journal-filters, #journal-cursor"></div>
</div> </div>
</section> </section>
@ -88,11 +88,11 @@
</select> </select>
<button type="submit">filter</button> <button type="submit">filter</button>
</form> </form>
<div id="services" class="panel" hx-get="/api/services" hx-trigger="load, every 15s" hx-swap="outerHTML" hx-include="#svc-filters"></div> <div id="services" class="panel" hx-get="/api/services" hx-trigger="dash-poll from:#tab-services" hx-swap="outerHTML" hx-include="#svc-filters"></div>
</section> </section>
<section id="tab-plugins" class="tab hidden"> <section id="tab-plugins" class="tab hidden">
<div id="plugins" class="panel" hx-get="/api/plugins" hx-trigger="load" hx-swap="outerHTML"></div> <div id="plugins" class="panel" hx-get="/api/plugins" hx-trigger="dash-activate from:#tab-plugins" hx-swap="outerHTML"></div>
</section> </section>
</main> </main>
<script src="/static/js/app.js"></script> <script src="/static/js/app.js"></script>

View file

@ -1,4 +1,4 @@
<div id="overview" class="panel cards" hx-get="/api/overview" hx-trigger="every 2s" hx-swap="outerHTML"> <div id="overview" class="panel cards" hx-get="/api/overview" hx-trigger="dash-poll from:#tab-overview" hx-swap="outerHTML">
{% macro bar(pct) %}{% if pct is not none %}<div class="bar"><div class="bar-fill{% if pct > 85 %} hot{% elif pct > 65 %} warm{% endif %}" style="width: {{ pct }}%"></div></div>{% endif %}{% endmacro %} {% macro bar(pct) %}{% if pct is not none %}<div class="bar"><div class="bar-fill{% if pct > 85 %} hot{% elif pct > 65 %} warm{% endif %}" style="width: {{ pct }}%"></div></div>{% endif %}{% endmacro %}
{% macro barrow(label, pct, cls) %}{% if pct is not none %}<div class="barrow"><span class="barrow-label">{{ label }}</span><div class="bar"><div class="bar-fill{% if cls %} {{ cls }}{% endif %}{% if pct > 85 %} hot{% elif pct > 65 %} warm{% endif %}" style="width: {{ pct }}%"></div></div></div>{% endif %}{% endmacro %} {% macro barrow(label, pct, cls) %}{% if pct is not none %}<div class="barrow"><span class="barrow-label">{{ label }}</span><div class="bar"><div class="bar-fill{% if cls %} {{ cls }}{% endif %}{% if pct > 85 %} hot{% elif pct > 65 %} warm{% endif %}" style="width: {{ pct }}%"></div></div></div>{% endif %}{% endmacro %}
<div class="card"> <div class="card">

View file

@ -1,5 +1,5 @@
<div class="lact"> <div class="lact">
<div id="lact-state" hx-get="/api/plugins/lact/state" hx-trigger="every 5s" hx-swap="innerHTML"> <div id="lact-state" hx-get="/api/plugins/lact/state" hx-trigger="dash-poll from:#tab-plugins" hx-swap="innerHTML">
{% include "plugins/lact_state.html" %} {% include "plugins/lact_state.html" %}
</div> </div>
{% if gpus and not error %} {% if gpus and not error %}

View file

@ -1,5 +1,5 @@
<div class="llama"> <div class="llama">
<div id="llama-state" hx-get="/api/plugins/llamacpp/state" hx-trigger="every 5s" hx-swap="innerHTML"> <div id="llama-state" hx-get="/api/plugins/llamacpp/state" hx-trigger="dash-poll from:#tab-plugins" hx-swap="innerHTML">
{% include "plugins/llamacpp_state.html" %} {% include "plugins/llamacpp_state.html" %}
</div> </div>
{% if reachable %} {% if reachable %}

View file

@ -1,5 +1,5 @@
<div class="sessions"> <div class="sessions">
<div id="sessions-state" hx-get="/api/plugins/sessions/state" hx-trigger="every 5s" hx-swap="innerHTML"> <div id="sessions-state" hx-get="/api/plugins/sessions/state" hx-trigger="dash-poll from:#tab-plugins" hx-swap="innerHTML">
{% include "plugins/sessions_state.html" %} {% include "plugins/sessions_state.html" %}
</div> </div>
</div> </div>

View file

@ -1,5 +1,5 @@
<div class="sleep"> <div class="sleep">
<div id="sleep-state" hx-get="/api/plugins/sleep/state" hx-trigger="every 5s" hx-swap="innerHTML"> <div id="sleep-state" hx-get="/api/plugins/sleep/state" hx-trigger="dash-poll from:#tab-plugins" hx-swap="innerHTML">
{% include "plugins/sleep_state.html" %} {% include "plugins/sleep_state.html" %}
</div> </div>
</div> </div>

View file

@ -1,4 +1,4 @@
<div id="processes-body" class="panel" hx-get="/api/processes" hx-trigger="every 3s" hx-swap="outerHTML" hx-include="#proc-controls"> <div id="processes-body" class="panel" hx-get="/api/processes" hx-trigger="dash-poll from:#tab-processes" hx-swap="outerHTML" hx-include="#proc-controls">
<div class="muted small">showing {{ procs | length }} of {{ total }} processes</div> <div class="muted small">showing {{ procs | length }} of {{ total }} processes</div>
<div class="table-wrap"> <div class="table-wrap">
<table class="table"> <table class="table">

View file

@ -1,4 +1,4 @@
<div id="services" class="panel" hx-get="/api/services" hx-trigger="every 15s" hx-swap="outerHTML" hx-include="#svc-filters"> <div id="services" class="panel" hx-get="/api/services" hx-trigger="dash-poll from:#tab-services" hx-swap="outerHTML" hx-include="#svc-filters">
{% if error %}<div class="alert">{{ error }}</div>{% endif %} {% if error %}<div class="alert">{{ error }}</div>{% endif %}
<div class="muted small">system: {{ state }} &middot; {{ units | length }} units</div> <div class="muted small">system: {{ state }} &middot; {{ units | length }} units</div>
<div class="table-wrap"> <div class="table-wrap">