296 lines
11 KiB
HTML
296 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Settings — Wild Dragon</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="css/common.css">
|
|
<style>
|
|
.settings-content {
|
|
max-width: 680px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-6);
|
|
}
|
|
|
|
.settings-card {
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--r-lg);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.settings-card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-3);
|
|
padding: var(--sp-4) var(--sp-5);
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--bg-panel);
|
|
}
|
|
|
|
.settings-card-header-icon {
|
|
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-card-header-icon svg { width: 16px; height: 16px; }
|
|
|
|
.settings-card-header-text { flex: 1; }
|
|
|
|
.settings-card-title {
|
|
font-size: var(--text-base);
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.settings-card-subtitle {
|
|
font-size: var(--text-xs);
|
|
color: var(--text-tertiary);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.settings-card-body {
|
|
padding: var(--sp-5);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-4);
|
|
}
|
|
|
|
.settings-card-footer {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-3);
|
|
padding: var(--sp-4) var(--sp-5);
|
|
border-top: 1px solid var(--border);
|
|
background: var(--bg-base);
|
|
}
|
|
|
|
.token-saved-badge {
|
|
display: none;
|
|
align-items: center;
|
|
gap: var(--sp-1);
|
|
font-size: var(--text-xs);
|
|
color: var(--status-green);
|
|
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;
|
|
}
|
|
|
|
.token-saved-badge.visible { display: inline-flex; }
|
|
|
|
.inline-status {
|
|
font-size: var(--text-sm);
|
|
padding: var(--sp-3) var(--sp-4);
|
|
border-radius: var(--r-md);
|
|
display: none;
|
|
}
|
|
|
|
.inline-status.success {
|
|
display: block;
|
|
color: var(--status-green);
|
|
background: oklch(68% 0.18 148 / 0.08);
|
|
border: 1px solid oklch(68% 0.18 148 / 0.2);
|
|
}
|
|
|
|
.inline-status.error {
|
|
display: block;
|
|
color: var(--status-red);
|
|
background: oklch(62% 0.22 25 / 0.08);
|
|
border: 1px solid oklch(62% 0.22 25 / 0.2);
|
|
}
|
|
|
|
.inline-status.loading {
|
|
display: block;
|
|
color: var(--text-secondary);
|
|
background: var(--bg-raised);
|
|
border: 1px solid var(--border);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="shell">
|
|
<!-- Sidebar -->
|
|
<nav class="sidebar" aria-label="Main navigation">
|
|
<div class="sidebar-brand">
|
|
<div class="sidebar-brand-mark">
|
|
<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 class="settings-card-header-text">
|
|
<div class="settings-card-title">AMPP FramelightX</div>
|
|
<div class="settings-card-subtitle">Wild Dragon will pre-create folder paths in AMPP automatically on each upload.</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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast-container" id="toastContainer" aria-live="polite"></div>
|
|
|
|
<script>
|
|
const API = '/api/v1';
|
|
|
|
document.addEventListener('DOMContentLoaded', loadAmppSettings);
|
|
|
|
async function loadAmppSettings() {
|
|
try {
|
|
const res = await fetch(`${API}/settings/ampp`, { credentials: 'include' });
|
|
if (!res.ok) return;
|
|
const data = await res.json();
|
|
if (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) {
|
|
console.error('Failed to load AMPP settings:', err);
|
|
}
|
|
}
|
|
|
|
async function saveAmpp() {
|
|
const baseUrl = document.getElementById('amppBaseUrl').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);
|
|
}
|
|
}
|
|
|
|
async function testAmpp() {
|
|
showStatus('amppStatus', 'loading', 'Testing connection…');
|
|
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);
|
|
}
|
|
}
|
|
|
|
function showStatus(id, type, msg) {
|
|
const el = document.getElementById(id);
|
|
el.className = 'inline-status ' + type;
|
|
el.textContent = msg;
|
|
}
|
|
</script>
|
|
<script src="js/auth-guard.js"></script>
|
|
</body>
|
|
</html>
|