Files
Esa Kataja 8fc00c96b9 add: DB-backed settings UI for live-editable tunables
Add a settings page (/settings) and API (/api/settings GET/PUT) to edit
encoding profiles (crf/preset per media type), lp, OMDb key, log retention,
and delete-originals from the browser, persisted to a settings table in
logs.db. config.yaml seeds the store on first run; afterwards the DB is the
source of truth (paths.* and http_addr stay config-only). Each job snapshots
the current settings, so changes apply to the next encode with no restart.

PUT validates ranges (crf 0-63, preset 0-13, lp >=0, retention >=1) before
persisting. No auth, by design for now (isolated network) — see #2.

Closes #1
2026-06-21 19:51:35 +03:00

160 lines
7.0 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>av1dae — settings</title>
<style>
:root {
--bg:#0d1117; --panel:#151b23; --panel-2:#1a2230; --line:#26303f;
--text:#cdd5df; --muted:#7d8896; --dim:#5a6573;
--amber:#ffb454; --amber-soft:#ffd9a0; --cyan:#56c7e8; --green:#5cc98b; --red:#f0816a;
--mono:"SF Mono",ui-monospace,"JetBrains Mono","Cascadia Code",Menlo,Consolas,monospace;
--sans:"Inter",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
}
* { box-sizing:border-box; }
body {
margin:0 auto; max-width:720px; background:var(--bg); color:var(--text);
font-family:var(--sans); font-size:16px; line-height:1.5;
-webkit-font-smoothing:antialiased; padding:clamp(1.25rem,4vw,2.5rem);
}
a { color:var(--cyan); text-decoration:none; }
a:hover { text-decoration:underline; }
header { display:flex; align-items:baseline; gap:1rem; margin-bottom:1.75rem; }
.wordmark { font-family:var(--mono); font-weight:600; font-size:1.4rem; letter-spacing:-.02em; }
.wordmark .prompt { color:var(--dim); }
.wordmark .vibe { color:var(--amber); }
.crumb { color:var(--muted); font-family:var(--mono); font-size:.85rem; }
.back { margin-left:auto; font-family:var(--mono); font-size:.8rem; }
.card { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:1.3rem 1.5rem; margin-bottom:1.25rem; }
.eyebrow { font-family:var(--mono); font-size:.7rem; letter-spacing:.16em; text-transform:uppercase; color:var(--amber); margin:0 0 1rem; }
.hint { color:var(--muted); font-size:.85rem; margin:.1rem 0 0; }
.row { display:flex; align-items:center; gap:1rem; padding:.5rem 0; flex-wrap:wrap; }
.row label { flex:1; min-width:12rem; }
.row label .sub { display:block; color:var(--dim); font-size:.78rem; }
input[type=number], input[type=text] {
background:var(--panel-2); border:1px solid var(--line); color:var(--text);
border-radius:6px; padding:.4rem .55rem; font-family:var(--mono); font-size:.9rem; width:6rem;
}
input[type=text] { width:100%; max-width:22rem; }
input:focus { outline:2px solid var(--amber); outline-offset:1px; }
/* profiles grid */
table.prof { width:100%; border-collapse:collapse; }
table.prof th, table.prof td { text-align:left; padding:.4rem .5rem; }
table.prof th { font-family:var(--mono); font-size:.66rem; letter-spacing:.08em; text-transform:uppercase; color:var(--dim); font-weight:500; }
table.prof td.name { font-family:var(--mono); color:var(--amber-soft); }
.toggle { display:flex; align-items:center; gap:.6rem; }
.toggle input { width:1.1rem; height:1.1rem; accent-color:var(--amber); }
.actions { display:flex; align-items:center; gap:1rem; margin-top:.5rem; }
button {
font-family:var(--mono); font-size:.85rem; background:var(--amber); color:#1a1200;
border:0; border-radius:7px; padding:.55rem 1.1rem; cursor:pointer; font-weight:600;
}
button:hover { background:var(--amber-soft); }
button:disabled { opacity:.5; cursor:default; }
#msg { font-family:var(--mono); font-size:.82rem; }
#msg.ok { color:var(--green); }
#msg.err { color:var(--red); }
</style>
</head>
<body>
<header>
<span class="wordmark"><span class="prompt">$ </span>av<span class="vibe">1</span>dae</span>
<span class="crumb">/ settings</span>
<a class="back" href="/">← dashboard</a>
</header>
<form id="form">
<section class="card">
<p class="eyebrow">Encoding profiles</p>
<table class="prof">
<thead><tr><th>Source</th><th>CRF (063)</th><th>Preset (013)</th></tr></thead>
<tbody>
<tr><td class="name">dvd</td><td><input type="number" id="dvd_crf" min="0" max="63"></td><td><input type="number" id="dvd_preset" min="0" max="13"></td></tr>
<tr><td class="name">bluray</td><td><input type="number" id="bluray_crf" min="0" max="63"></td><td><input type="number" id="bluray_preset" min="0" max="13"></td></tr>
<tr><td class="name">webdl</td><td><input type="number" id="webdl_crf" min="0" max="63"></td><td><input type="number" id="webdl_preset" min="0" max="13"></td></tr>
<tr><td class="name">tvrip</td><td><input type="number" id="tvrip_crf" min="0" max="63"></td><td><input type="number" id="tvrip_preset" min="0" max="13"></td></tr>
</tbody>
</table>
<div class="row" style="margin-top:.6rem">
<label>Thread cap (lp)<span class="sub">SVT-AV1 logical processors · 0 = use all cores</span></label>
<input type="number" id="lp" min="0">
</div>
</section>
<section class="card">
<p class="eyebrow">Metadata & housekeeping</p>
<div class="row">
<label>OMDb API key<span class="sub">Required for movie metadata; TVmaze needs none</span></label>
<input type="text" id="omdb_api_key" autocomplete="off" spellcheck="false">
</div>
<div class="row">
<label>Log retention (days)<span class="sub">Rows older than this are purged from logs.db</span></label>
<input type="number" id="log_retention_days" min="1">
</div>
<div class="row">
<label class="toggle"><input type="checkbox" id="delete_originals"> Delete originals after a successful encode<span class="sub">Off = move them to originals/</span></label>
</div>
</section>
<div class="actions">
<button type="submit" id="save">Save settings</button>
<span id="msg"></span>
</div>
</form>
<script>
const $ = id => document.getElementById(id);
const profiles = ["dvd", "bluray", "webdl", "tvrip"];
function fill(s) {
for (const p of profiles) { $(p + "_crf").value = s[p].crf; $(p + "_preset").value = s[p].preset; }
$("lp").value = s.lp;
$("omdb_api_key").value = s.omdb_api_key || "";
$("log_retention_days").value = s.log_retention_days;
$("delete_originals").checked = !!s.delete_originals;
}
function collect() {
const s = { lp: +$("lp").value, omdb_api_key: $("omdb_api_key").value,
log_retention_days: +$("log_retention_days").value, delete_originals: $("delete_originals").checked };
for (const p of profiles) s[p] = { crf: +$(p + "_crf").value, preset: +$(p + "_preset").value };
return s;
}
function msg(text, cls) { const m = $("msg"); m.textContent = text; m.className = cls || ""; }
async function load() {
try {
const r = await fetch("/api/settings", { cache: "no-store" });
if (!r.ok) throw new Error(r.status);
fill(await r.json());
} catch (e) { msg("Failed to load settings", "err"); }
}
$("form").addEventListener("submit", async e => {
e.preventDefault();
$("save").disabled = true; msg("Saving…");
try {
const r = await fetch("/api/settings", {
method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(collect())
});
if (r.ok) msg("Saved — applies to the next encode.", "ok");
else msg("Rejected: " + (await r.text()).trim(), "err");
} catch (e) { msg("Network error", "err"); }
finally { $("save").disabled = false; }
});
load();
</script>
</body>
</html>