warpgate/templates/web/layout.html
grabbit e8f1971d63 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>
2026-02-18 19:31:54 +08:00

105 lines
3.9 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Warpgate Dashboard</title>
<link rel="stylesheet" href="/static/style.css">
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<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 }}',
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">
<button class="tab-btn" :class="{ active: activeTab === 'dashboard' }"
@click="activeTab = 'dashboard'"
hx-get="/tabs/dashboard" hx-target="#tab-content" hx-swap="innerHTML">
Dashboard
</button>
<button class="tab-btn" :class="{ active: activeTab === 'shares' }"
@click="activeTab = 'shares'"
hx-get="/tabs/shares" hx-target="#tab-content" hx-swap="innerHTML">
Shares
</button>
<button class="tab-btn" :class="{ active: activeTab === 'config' }"
@click="activeTab = 'config'"
hx-get="/tabs/config" hx-target="#tab-content" hx-swap="innerHTML">
Config
</button>
<button class="tab-btn" :class="{ active: activeTab === 'logs' }"
@click="activeTab = 'logs'"
hx-get="/tabs/logs" hx-target="#tab-content" hx-swap="innerHTML">
Logs
</button>
</nav>
<div id="tab-content">
{{ tab_content }}
</div>
</div>
<!-- Hidden SSE listener: OOB swaps update regions inside dashboard tab -->
<div sse-swap="status" hx-swap="none" style="display:none"></div>
<script>
/* Alpine.js + htmx bridge: re-initialize Alpine trees after htmx content swaps */
document.body.addEventListener('htmx:afterSettle', function(evt) {
if (window.Alpine) Alpine.initTree(evt.detail.target || evt.detail.elt);
});
</script>
</body>
</html>