Add auto-refresh toggle for web UI tabs with localStorage persistence

Periodic client-side refresh for Shares/Config tabs using Alpine.js
setInterval, with toggle and configurable interval (2-30s) in header.
Dashboard (SSE) and Logs (own polling) are excluded. Shares tab
preserves row expansion state across refreshes via ?expand= param.
Adds [x-cloak] CSS rule and conditional x-cloak on detail rows to
prevent flash during content swaps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
grabbit 2026-02-18 19:31:54 +08:00
parent 2432f83914
commit e8f1971d63
3 changed files with 75 additions and 2 deletions

View File

@ -15,6 +15,7 @@
}
* { box-sizing: border-box; margin: 0; padding: 0; }
[x-cloak] { display: none !important; }
body {
background: var(--bg);
@ -571,6 +572,33 @@ textarea:focus {
color: var(--text);
}
/* ─── Auto-refresh controls ───────────────────────────── */
.auto-refresh-controls {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.8em;
color: var(--text-muted);
}
.auto-refresh-controls .label-text {
user-select: none;
}
.interval-select {
background: var(--surface);
color: var(--text-muted);
border: 1px solid var(--border);
border-radius: 4px;
padding: 2px 6px;
font-size: 1em;
font-family: var(--font);
}
.interval-select:focus { outline: none; border-color: var(--accent); }
.interval-select:disabled { opacity: 0.4; cursor: not-allowed; }
.toggle-sm .slider { width: 28px; height: 16px; }
.toggle-sm .slider::after { width: 12px; height: 12px; }
.toggle-sm input:checked + .slider::after { transform: translateX(12px); }
/* ─── Responsive ───────────────────────────────────────── */
@media (max-width: 768px) {

View File

@ -9,13 +9,58 @@
<script src="https://unpkg.com/htmx-ext-sse@2.2.2/sse.js"></script>
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body hx-ext="sse" sse-connect="/events" x-data="{ activeTab: '{{ active_tab }}' }">
<body hx-ext="sse" sse-connect="/events"
x-data="{
activeTab: '{{ active_tab }}',
autoRefresh: JSON.parse(localStorage.getItem('wg_auto_refresh') ?? 'true'),
refreshInterval: parseInt(localStorage.getItem('wg_refresh_interval') ?? '3'),
_timer: null,
startTimer() {
this.stopTimer();
if (!this.autoRefresh) return;
this._timer = setInterval(() => this.refreshTab(), this.refreshInterval * 1000);
},
stopTimer() {
if (this._timer) { clearInterval(this._timer); this._timer = null; }
},
refreshTab() {
if (this.activeTab === 'dashboard' || this.activeTab === 'logs') return;
let url = '/tabs/' + this.activeTab;
if (this.activeTab === 'shares') {
const el = document.querySelector('#tab-content [x-data]');
if (el) {
const d = Alpine.$data(el);
if (d && d.expanded) url += '?expand=' + encodeURIComponent(d.expanded);
}
}
htmx.ajax('GET', url, { target: '#tab-content', swap: 'innerHTML' });
}
}"
x-init="startTimer()"
x-effect="localStorage.setItem('wg_auto_refresh', autoRefresh); localStorage.setItem('wg_refresh_interval', refreshInterval); startTimer()"
>
<div class="shell">
<div class="header">
<div>
<h1><span class="status-dot"></span>Warpgate</h1>
<div class="meta">Uptime: <span id="uptime">{{ uptime }}</span> &nbsp;|&nbsp; Config: {{ config_path }}</div>
</div>
<div class="auto-refresh-controls">
<span class="label-text">Auto-refresh</span>
<label class="toggle toggle-sm">
<input type="checkbox" x-model="autoRefresh">
<span class="slider"></span>
</label>
<select x-model.number="refreshInterval" class="interval-select"
:disabled="!autoRefresh" title="Refresh interval">
<option value="2">2s</option>
<option value="3">3s</option>
<option value="5">5s</option>
<option value="10">10s</option>
<option value="30">30s</option>
</select>
</div>
</div>
<nav class="tabs">

View File

@ -42,7 +42,7 @@
<td>{{ share.speed_display }}</td>
<td>{{ share.transfers }}</td>
</tr>
<tr x-show="expanded === '{{ share.name }}'" x-transition x-cloak class="detail-row">
<tr x-show="expanded === '{{ share.name }}'" x-transition {% if share.name != expand %}x-cloak{% endif %} class="detail-row">
<td colspan="7">
<div class="detail-panel">
<div class="detail-grid">