feat: rebuild settings.html with new shell layout

This commit is contained in:
Zac Gaetano 2026-05-18 13:08:19 -04:00
parent 9ceb5db1e3
commit 725c3ed292

View file

@ -1,298 +1,314 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wild Dragon - Settings</title> <title>Settings — Wild Dragon</title>
<link rel="stylesheet" href="/css/common.css"> <link rel="preconnect" href="https://fonts.googleapis.com">
<style> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
.settings-container { <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap" rel="stylesheet">
max-width: 720px; <link rel="stylesheet" href="css/common.css">
margin: 0 auto; <style>
padding: var(--spacing-lg); .settings-content {
display: flex; max-width: 680px;
flex-direction: column; display: flex;
gap: var(--spacing-xl); flex-direction: column;
} gap: var(--sp-6);
}
.settings-section { .settings-card {
background-color: var(--color-bg-tertiary); background: var(--bg-surface);
border: 1px solid var(--color-border); border: 1px solid var(--border);
border-radius: var(--radius-lg); border-radius: var(--r-lg);
overflow: hidden; overflow: hidden;
} }
.settings-section-header { .settings-card-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-md); gap: var(--sp-3);
padding: var(--spacing-lg); padding: var(--sp-4) var(--sp-5);
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--border);
background-color: var(--color-bg-secondary); background: var(--bg-panel);
} }
.settings-section-icon { .settings-card-header-icon {
font-size: 1.4rem; width: 32px;
} height: 32px;
border-radius: var(--r-md);
background: var(--accent-subtle);
border: 1px solid var(--accent-border);
display: flex;
align-items: center;
justify-content: center;
color: var(--accent);
flex-shrink: 0;
}
.settings-section-title { .settings-card-header-icon svg { width: 16px; height: 16px; }
font-size: 1.1rem;
font-weight: 700;
color: var(--color-text-primary);
margin: 0;
}
.settings-section-subtitle { .settings-card-header-text { flex: 1; }
font-size: 0.85rem;
color: var(--color-text-tertiary);
margin: 0;
}
.settings-body { .settings-card-title {
padding: var(--spacing-lg); font-size: var(--text-base);
display: flex; font-weight: 500;
flex-direction: column; color: var(--text-primary);
gap: var(--spacing-md); }
}
.settings-row { .settings-card-subtitle {
display: flex; font-size: var(--text-xs);
gap: var(--spacing-md); color: var(--text-tertiary);
align-items: flex-end; margin-top: 2px;
} }
.settings-row .form-group { .settings-card-body {
flex: 1; padding: var(--sp-5);
margin-bottom: 0; display: flex;
} flex-direction: column;
gap: var(--sp-4);
}
.settings-actions { .settings-card-footer {
display: flex; display: flex;
gap: var(--spacing-sm); align-items: center;
align-items: center; gap: var(--sp-3);
padding-top: var(--spacing-md); padding: var(--sp-4) var(--sp-5);
border-top: 1px solid var(--color-border); border-top: 1px solid var(--border);
} background: var(--bg-base);
}
.status-msg { .token-saved-badge {
font-size: 0.9rem; display: none;
padding: var(--spacing-sm) var(--spacing-md); align-items: center;
border-radius: var(--radius-md); gap: var(--sp-1);
display: none; font-size: var(--text-xs);
align-items: center; color: var(--status-green);
gap: var(--spacing-sm); background: oklch(68% 0.18 148 / 0.1);
} border: 1px solid oklch(68% 0.18 148 / 0.25);
border-radius: var(--r-full);
padding: 2px 10px;
}
.status-msg.success { .token-saved-badge.visible { display: inline-flex; }
display: flex;
background-color: rgba(16, 185, 129, 0.15);
color: var(--color-success);
border: 1px solid rgba(16, 185, 129, 0.3);
}
.status-msg.error { .inline-status {
display: flex; font-size: var(--text-sm);
background-color: rgba(239, 68, 68, 0.15); padding: var(--sp-3) var(--sp-4);
color: var(--color-danger); border-radius: var(--r-md);
border: 1px solid rgba(239, 68, 68, 0.3); display: none;
} }
.status-msg.loading { .inline-status.success {
display: flex; display: block;
background-color: rgba(59, 130, 246, 0.15); color: var(--status-green);
color: var(--color-info); background: oklch(68% 0.18 148 / 0.08);
border: 1px solid rgba(59, 130, 246, 0.3); border: 1px solid oklch(68% 0.18 148 / 0.2);
} }
.token-hint { .inline-status.error {
font-size: 0.82rem; display: block;
color: var(--color-text-tertiary); color: var(--status-red);
margin-top: var(--spacing-xs); background: oklch(62% 0.22 25 / 0.08);
} border: 1px solid oklch(62% 0.22 25 / 0.2);
}
.token-set-badge { .inline-status.loading {
display: inline-flex; display: block;
align-items: center; color: var(--text-secondary);
gap: 4px; background: var(--bg-raised);
font-size: 0.8rem; border: 1px solid var(--border);
color: var(--color-success); }
background-color: rgba(16, 185, 129, 0.12); </style>
border: 1px solid rgba(16, 185, 129, 0.25);
border-radius: 12px;
padding: 2px 8px;
margin-top: var(--spacing-xs);
}
</style>
</head> </head>
<body> <body>
<div class="app-container"> <div class="shell">
<!-- Header --> <!-- Sidebar -->
<header class="header"> <nav class="sidebar" aria-label="Main navigation">
<div class="header-logo"> <div class="sidebar-brand">
<div class="header-logo-icon">D</div> <div class="sidebar-brand-mark">
<span>WILD DRAGON</span> <svg viewBox="0 0 16 16" fill="currentColor" width="12" height="12"><path d="M8 1L2 5v6l6 4 6-4V5L8 1zm0 2.2L12 6v4l-4 2.7L4 10V6l4-2.8z"/></svg>
</div>
<span class="sidebar-brand-name">Wild Dragon</span>
</div>
<nav class="sidebar-nav">
<a href="index.html" class="nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1" y="1" width="6" height="6" rx="1"/><rect x="9" y="1" width="6" height="6" rx="1"/><rect x="1" y="9" width="6" height="6" rx="1"/><rect x="9" y="9" width="6" height="6" rx="1"/></svg>
Library
</a>
<a href="upload.html" class="nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 11V3M5 6l3-3 3 3"/><path d="M2 13h12"/></svg>
Ingest
</a>
<a href="recorders.html" class="nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1" y="4" width="10" height="8" rx="1"/><path d="M11 7l4-2v6l-4-2"/></svg>
Recorders
</a>
<a href="capture.html" class="nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="3"/><circle cx="8" cy="8" r="6.5"/></svg>
Capture
</a>
<a href="jobs.html" class="nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h12M2 8h8M2 12h5"/></svg>
Jobs
</a>
<div class="sidebar-section-label">Admin</div>
<a href="settings.html" class="nav-item active">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="2.5"/><path d="M8 1v1.5M8 13.5V15M1 8h1.5M13.5 8H15M3.2 3.2l1 1M11.8 11.8l1 1M3.2 12.8l1-1M11.8 4.2l1-1"/></svg>
Settings
</a>
<a href="users.html" class="nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="5" r="3"/><path d="M2 14c0-3.3 2.7-6 6-6s6 2.7 6 6"/></svg>
Users
</a>
<a href="tokens.html" class="nav-item">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="6" width="10" height="8" rx="1"/><path d="M6 6V4a2 2 0 0 1 4 0v2"/></svg>
Tokens
</a>
</nav>
<div class="sidebar-footer">
<div class="sidebar-user">
<div class="sidebar-user-avatar" id="userAvatar">?</div>
<div class="sidebar-user-info">
<div class="sidebar-user-name" id="userName"></div>
<div class="sidebar-user-role" id="userRole"></div>
</div>
<button class="btn btn-ghost" id="logoutBtn" style="padding:0;width:28px;height:28px;flex-shrink:0;" title="Sign out">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M6 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3M11 11l3-3-3-3M6 8h8"/></svg>
</button>
</div>
</div>
</nav>
<!-- Main -->
<div class="main">
<header class="topbar">
<div class="topbar-left">
<span class="page-title">Settings</span>
</div>
</header>
<div class="page-content">
<div class="settings-content">
<!-- AMPP FramelightX -->
<div class="settings-card">
<div class="settings-card-header">
<div class="settings-card-header-icon">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="2"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3.1 3.1l1.4 1.4M11.5 11.5l1.4 1.4M3.1 12.9l1.4-1.4M11.5 4.5l1.4-1.4"/></svg>
</div> </div>
<nav class="header-nav"> <div class="settings-card-header-text">
<div class="nav-item" data-page="assets">Assets</div> <div class="settings-card-title">AMPP FramelightX</div>
<div class="nav-item" data-page="capture">Capture</div> <div class="settings-card-subtitle">Wild Dragon will pre-create folder paths in AMPP automatically on each upload.</div>
<div class="nav-item" data-page="upload">Upload</div>
<div class="nav-item" data-page="recorders">Recorders</div>
<div class="nav-item active" data-page="settings">Settings</div>
</nav>
</header>
<!-- Main Content -->
<div class="main-content">
<div class="content-area">
<div class="content-main">
<div class="settings-container">
<h1 style="margin-bottom: 0;">Settings</h1>
<!-- AMPP Integration Section -->
<div class="settings-section">
<div class="settings-section-header">
<div class="settings-section-icon">📡</div>
<div>
<h3 class="settings-section-title">AMPP FramelightX</h3>
<p class="settings-section-subtitle">Dragon-Wind will pre-create folder paths in AMPP automatically on each upload.</p>
</div>
</div>
<div class="settings-body">
<div class="form-group">
<label class="form-label" for="amppBaseUrl">AMPP Base URL</label>
<input
type="url"
id="amppBaseUrl"
class="form-input"
placeholder="https://ampp.yourdomain.net"
>
</div>
<div class="form-group">
<label class="form-label" for="amppToken">API Token</label>
<input
type="password"
id="amppToken"
class="form-input"
placeholder="Paste token — leave blank to keep existing"
>
<div id="tokenSetBadge" class="token-set-badge" style="display: none;">✓ Token saved</div>
<div class="token-hint">Token is stored securely and never returned to the UI.</div>
</div>
<div id="amppStatus" class="status-msg"></div>
<div class="settings-actions">
<button class="btn btn-primary" onclick="saveAmpp()">Save</button>
<button class="btn btn-secondary" onclick="testAmpp()">Test Connection</button>
</div>
</div>
</div>
</div>
</div>
</div> </div>
</div>
<div class="settings-card-body">
<div class="form-group">
<label class="form-label" for="amppBaseUrl">AMPP Base URL</label>
<input type="url" id="amppBaseUrl" placeholder="https://ampp.yourdomain.net">
</div>
<div class="form-group">
<label class="form-label" for="amppToken">API Token</label>
<input type="password" id="amppToken" placeholder="Paste token — leave blank to keep existing">
<div class="form-hint">Token is stored securely and never returned to the UI.</div>
<span class="token-saved-badge" id="tokenSavedBadge">
<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" width="10" height="10"><path d="M2 6l3 3 5-5"/></svg>
Token saved
</span>
</div>
<div class="inline-status" id="amppStatus"></div>
</div>
<div class="settings-card-footer">
<button class="btn btn-primary btn-sm" onclick="saveAmpp()">Save settings</button>
<button class="btn btn-ghost btn-sm" onclick="testAmpp()">Test connection</button>
</div>
</div> </div>
<!-- Status Bar --> </div>
<footer class="status-bar">
<div class="status-item">
<span class="status-indicator" id="statusIndicator"></span>
<span id="statusText">Settings</span>
</div>
</footer>
</div> </div>
</div>
</div>
<script> <div class="toast-container" id="toastContainer" aria-live="polite"></div>
const API = '/api/v1';
// ── Navigation ────────────────────────────────────────────── <script>
document.querySelectorAll('[data-page]').forEach(el => { const API = '/api/v1';
el.addEventListener('click', () => {
const page = el.dataset.page;
if (page === 'assets') window.location.href = '/index.html';
if (page === 'capture') window.location.href = '/capture.html';
if (page === 'upload') window.location.href = '/upload.html';
if (page === 'recorders') window.location.href = '/recorders.html';
});
});
// ── Init ───────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', loadAmppSettings);
document.addEventListener('DOMContentLoaded', loadAmppSettings);
async function loadAmppSettings() { async function loadAmppSettings() {
try { try {
const res = await fetch(`${API}/settings/ampp`); const res = await fetch(`${API}/settings/ampp`, { credentials: 'include' });
if (!res.ok) return; if (!res.ok) return;
const data = await res.json(); const data = await res.json();
if (data.ampp_base_url) { if (data.ampp_base_url) document.getElementById('amppBaseUrl').value = data.ampp_base_url;
document.getElementById('amppBaseUrl').value = data.ampp_base_url; if (data.ampp_token_exists) document.getElementById('tokenSavedBadge').classList.add('visible');
} } catch (err) {
if (data.ampp_token_exists) { console.error('Failed to load AMPP settings:', err);
document.getElementById('tokenSetBadge').style.display = 'inline-flex'; }
} }
} catch (err) {
console.error('Failed to load AMPP settings:', err);
}
}
// ── Save ────────────────────────────────────────────────────── async function saveAmpp() {
async function saveAmpp() { const baseUrl = document.getElementById('amppBaseUrl').value.trim();
const baseUrl = document.getElementById('amppBaseUrl').value.trim(); const token = document.getElementById('amppToken').value.trim();
const token = document.getElementById('amppToken').value.trim(); if (!baseUrl) { showStatus('amppStatus', 'error', 'Base URL is required.'); return; }
showStatus('amppStatus', 'loading', 'Saving…');
try {
const body = { ampp_base_url: baseUrl };
if (token) body.ampp_token = token;
const res = await fetch(`${API}/settings/ampp`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(body),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Save failed');
if (token) {
document.getElementById('amppToken').value = '';
document.getElementById('tokenSavedBadge').classList.add('visible');
}
showStatus('amppStatus', 'success', '✓ Settings saved.');
} catch (err) {
showStatus('amppStatus', 'error', err.message);
}
}
if (!baseUrl) { async function testAmpp() {
showStatus('amppStatus', 'error', 'Base URL is required.'); showStatus('amppStatus', 'loading', 'Testing connection…');
return; try {
} const res = await fetch(`${API}/settings/ampp/test`, { method: 'POST', credentials: 'include' });
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Test failed');
showStatus('amppStatus', 'success', '✓ ' + data.message);
} catch (err) {
showStatus('amppStatus', 'error', err.message);
}
}
showStatus('amppStatus', 'loading', 'Saving…'); function showStatus(id, type, msg) {
const el = document.getElementById(id);
try { el.className = 'inline-status ' + type;
const body = { ampp_base_url: baseUrl }; el.textContent = msg;
if (token) body.ampp_token = token; }
</script>
const res = await fetch(`${API}/settings/ampp`, { <script>
method: 'PUT', (async () => {
headers: { 'Content-Type': 'application/json' }, try {
body: JSON.stringify(body), const r = await fetch('/api/v1/auth/me', { credentials: 'include' });
}); if (r.ok) {
const u = await r.json();
const data = await res.json(); const name = u.display_name || u.username || 'User';
if (!res.ok) throw new Error(data.error || 'Save failed'); document.getElementById('userName').textContent = name;
document.getElementById('userAvatar').textContent = name[0].toUpperCase();
if (token) { const roleEl = document.getElementById('userRole');
document.getElementById('amppToken').value = ''; if (roleEl) roleEl.textContent = u.role || '';
document.getElementById('tokenSetBadge').style.display = 'inline-flex'; }
} } catch (_) {}
document.getElementById('logoutBtn').onclick = async () => {
showStatus('amppStatus', 'success', '✓ Settings saved.'); try { await fetch('/api/v1/auth/logout', { method: 'POST', credentials: 'include' }); } catch (_) {}
} catch (err) { location.href = 'login.html';
showStatus('amppStatus', 'error', err.message); };
} })();
} </script>
// ── Test ──────────────────────────────────────────────────────
async function testAmpp() {
showStatus('amppStatus', 'loading', 'Testing connection…');
try {
const res = await fetch(`${API}/settings/ampp/test`, { method: 'POST' });
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Test failed');
showStatus('amppStatus', 'success', '✓ ' + data.message);
} catch (err) {
showStatus('amppStatus', 'error', err.message);
}
}
// ── Helpers ───────────────────────────────────────────────────
function showStatus(id, type, msg) {
const el = document.getElementById(id);
el.className = 'status-msg ' + type;
el.textContent = msg;
}
</script>
</body> </body>
</html> </html>