Extract CSS/JS to static files, wrap header pills, add version footer
All checks were successful
Deploy / deploy (push) Successful in 23s
All checks were successful
Deploy / deploy (push) Successful in 23s
The palette lived in a :root block duplicated across all three templates, so every reshade meant three edits — and drifted twice in practice (the favicon hardcoded its own hex; stale danger-border rgba literals survived two palette changes). Move tokens to a single static/css/base.css, derive every dim/border tint from the four source hues with color-mix, and point the favicon at one static/favicon.svg. Also splits the 1558-line dashboard.html into template + static assets: 662 lines of CSS and 396 of JS were re-sent uninlined on every load with no browser caching, which costs real time on the remote/cellular access this app is built for. Templates drop to 501/42/107 lines. Server-rendered config (timezone, script_root) now arrives via <body data-*>; wiring URL_BASE to request.script_root also fixes /api/* fetches for a future subpath mount, which the hardcoded "" would break. Header service pills now wrap instead of scrolling — they were clipped at narrow widths, and a hidden pill is a service whose status you cannot see. Verified 0 clipped and no page overflow at 320/375/700/1280. Footer gains a version and date so the running build is identifiable without trusting the Unraid Docker tab's cached container IDs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
faf25a811a
commit
18c7719cd7
12 changed files with 1396 additions and 1413 deletions
|
|
@ -4,187 +4,14 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Settings — Media Centre</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='22' fill='%237c86ff'/><path d='M40 32 L72 50 L40 68 Z' fill='%23141416'/></svg>">
|
||||
<link rel="icon" href="{{ url_for('static', filename='favicon.svg') }}">
|
||||
<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;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #141416;
|
||||
--surface: #1a1a1d;
|
||||
--surface-2: #202024;
|
||||
--border: #2c2c31;
|
||||
--border-lt: #38383f;
|
||||
--text: #edecf1;
|
||||
--text-2: #a8a7b0;
|
||||
--text-3: #7c7b84;
|
||||
--accent: #7c86ff;
|
||||
--ok: #68b98a;
|
||||
--danger: #dc5b52;
|
||||
--fs-xs: 0.75rem;
|
||||
--fs-sm: 0.8125rem;
|
||||
--fs-base: 0.9375rem;
|
||||
--fs-lg: 1.125rem;
|
||||
--gap: 1rem;
|
||||
--radius: 10px;
|
||||
--radius-sm: 7px;
|
||||
--tap: 44px;
|
||||
--font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--font);
|
||||
font-size: var(--fs-base);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
|
||||
|
||||
header {
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0.75rem 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
header h1 {
|
||||
margin: 0;
|
||||
font-size: var(--fs-lg);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.back-btn {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--tap);
|
||||
padding: 0 0.9rem;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-2);
|
||||
font-size: var(--fs-sm);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition: color 0.2s, border-color 0.2s;
|
||||
}
|
||||
.back-btn:hover { color: var(--text); border-color: var(--border-lt); }
|
||||
|
||||
main { padding: 1.5rem; max-width: 900px; margin: 0 auto; }
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: var(--gap);
|
||||
}
|
||||
.full { grid-column: 1 / -1; }
|
||||
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.card-header {
|
||||
padding: 0.85rem 1.1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.card-header h2 {
|
||||
margin: 0;
|
||||
font-size: var(--fs-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
.card-body { padding: 1.1rem; }
|
||||
|
||||
.field { margin-bottom: 0.9rem; }
|
||||
label {
|
||||
display: block;
|
||||
font-size: var(--fs-xs);
|
||||
font-weight: 600;
|
||||
color: var(--text-2);
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
input, select {
|
||||
width: 100%;
|
||||
min-height: var(--tap);
|
||||
padding: 0 0.8rem;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: 16px; /* prevents iOS zoom on focus */
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
input:focus, select:focus { border-color: var(--accent); }
|
||||
input::placeholder { color: var(--text-3); }
|
||||
|
||||
.hint {
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--text-3);
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
button.save {
|
||||
width: 100%;
|
||||
min-height: var(--tap);
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: inherit;
|
||||
font-size: var(--fs-sm);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
button.save:hover:not(:disabled) { opacity: 0.88; }
|
||||
button.save:disabled { background: var(--border); color: var(--text-3); cursor: default; }
|
||||
|
||||
.error-inline {
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--danger);
|
||||
display: none;
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 1.5rem;
|
||||
right: 1.5rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--ok);
|
||||
color: var(--ok);
|
||||
padding: 0.7rem 1.1rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--fs-sm);
|
||||
font-weight: 500;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
max-width: calc(100vw - 3rem);
|
||||
}
|
||||
.toast.error { border-color: var(--danger); color: var(--danger); }
|
||||
.toast.show { opacity: 1; }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
header { padding: 0.7rem 1rem; }
|
||||
main { padding: 1rem; }
|
||||
.toast { left: 1rem; right: 1rem; bottom: 1rem; }
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<body data-url-base="{{ request.script_root }}">
|
||||
|
||||
<header>
|
||||
<svg width="30" height="30" viewBox="0 0 100 100" aria-hidden="true">
|
||||
|
|
@ -274,66 +101,7 @@
|
|||
|
||||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||
|
||||
<script>
|
||||
function showToast(msg, isError) {
|
||||
const t = document.getElementById('toast');
|
||||
t.textContent = msg;
|
||||
t.classList.toggle('error', !!isError);
|
||||
t.classList.add('show');
|
||||
setTimeout(() => t.classList.remove('show'), 2800);
|
||||
}
|
||||
|
||||
async function post(url, payload) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
async function saveGeneral() {
|
||||
try {
|
||||
const resp = await post('/api/settings', {
|
||||
timezone: document.getElementById('timezone').value,
|
||||
arr_host: document.getElementById('arr_host').value.trim()
|
||||
});
|
||||
showToast(resp.ok ? 'General settings saved — reload the dashboard to apply.'
|
||||
: 'Save failed.', !resp.ok);
|
||||
} catch (e) { showToast('Network error — not saved.', true); }
|
||||
}
|
||||
|
||||
async function saveService(name) {
|
||||
try {
|
||||
const key = document.getElementById(name + '_api_key').value.trim();
|
||||
const resp = await post('/api/settings', { [name + '_api_key']: key });
|
||||
const label = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
showToast(resp.ok ? label + ' key saved — restart the container to apply.'
|
||||
: 'Save failed.', !resp.ok);
|
||||
} catch (e) { showToast('Network error — not saved.', true); }
|
||||
}
|
||||
|
||||
async function changePassword() {
|
||||
const errEl = document.getElementById('pw-error');
|
||||
errEl.style.display = 'none';
|
||||
try {
|
||||
const resp = await post('/api/change-password', {
|
||||
current: document.getElementById('pw_current').value,
|
||||
new: document.getElementById('pw_new').value,
|
||||
confirm: document.getElementById('pw_confirm').value
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (resp.ok) {
|
||||
['pw_current', 'pw_new', 'pw_confirm'].forEach(id => document.getElementById(id).value = '');
|
||||
showToast('Password updated.');
|
||||
} else {
|
||||
errEl.textContent = data.error || 'Failed to update password.';
|
||||
errEl.style.display = 'block';
|
||||
showToast(data.error || 'Failed to update password.', true);
|
||||
}
|
||||
} catch (e) { showToast('Network error — password not changed.', true); }
|
||||
}
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/settings.js') }}"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue