- Task A: Offline mode banner in layout (nas_offline field in LayoutTemplate)
- Task B: Safe-to-disconnect sync indicator on dashboard (all_synced field)
- Task C: Preset apply buttons (photographer/video/office) in config tab with POST /api/preset/{profile} endpoint
- Task D: Reconnect button and error banner in share detail panel
- Added nas_offline/all_synced fields to DaemonStatus for integration contract
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
112 lines
4.2 KiB
HTML
112 lines
4.2 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()"
|
|
>
|
|
{% if nas_offline %}
|
|
<div class="offline-banner" role="alert">
|
|
<span class="offline-icon">⚠</span>
|
|
<strong>NAS 离线</strong> — 正在使用本地缓存(写入已排队)
|
|
<span class="offline-sub">Offline mode: using local cache, writes are queued</span>
|
|
</div>
|
|
{% endif %}
|
|
<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">
|
|
<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>
|