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:
parent
2432f83914
commit
e8f1971d63
@ -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) {
|
||||
|
||||
@ -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> | 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">
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user