warpgate/templates/web/layout.html
grabbit 15f915fbee Show active transfer count, add SFTP retry resilience, and fix config tab refresh
- Use rclone transferring array to show only active transfers instead of
  cumulative count; zero out speed when no transfers are active
- Add SFTP retry/timeout flags to rclone mount for flaky Tailscale tunnels
- Skip auto-refresh on config tab to prevent editor resets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 00:21:22 +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' || this.activeTab === 'config') 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>