chore: initial import

This commit is contained in:
2026-07-24 21:37:03 +02:00
commit 9b5425851a
28 changed files with 4020 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>boehmitools · Einstellungen</title>
<link rel="stylesheet" href="/shared/boehmi.css">
<style>
.field { margin-bottom: 12px; }
.field .help { font-size: 11.5px; color: var(--bt-muted); margin-top: 4px; }
.sticky-actions {
position: sticky; bottom: 0; margin-top: 18px; padding: 12px 0;
background: linear-gradient(transparent, var(--bt-bg) 40%);
display: flex; gap: 8px; align-items: center;
}
</style>
</head>
<body data-bt-title="Einstellungen" data-bt-icon="⚙">
<main class="bt-main narrow">
<div class="bt-pagehead">
<h1>Einstellungen</h1>
<p>Diese Zugänge gelten für alle Tools der Suite. Sie liegen in
<code>data/settings.json</code> und werden den Tools als Umgebungsvariablen
übergeben — genau so, wie sie es als Einzeltool erwarten.</p>
</div>
<div class="bt-notice info">
Tokens und Schlüssel bleiben im lokalen Backend. Bereits gespeicherte
Geheimnisse werden hier nur maskiert angezeigt; ein leeres Feld lässt den
bestehenden Wert unverändert.
</div>
<div id="groups"></div>
<div class="sticky-actions">
<button class="primary" id="save">Speichern</button>
<span id="state" class="bt-muted"></span>
</div>
</main>
<script src="/shared/boehmi.js"></script>
<script>
const groupsEl = document.getElementById("groups");
const stateEl = document.getElementById("state");
let schema = [];
function fieldHtml(f, value) {
const id = "f_" + f.key;
let control;
if (f.kind === "bool") {
control = `<label class="bt-check"><input type="checkbox" id="${id}"
${String(value).toLowerCase() === "true" ? "checked" : ""}> aktiv</label>`;
} else if (f.kind === "select") {
control = `<select id="${id}">${f.options.map((o) =>
`<option ${o === value ? "selected" : ""}>${BT.escape(o)}</option>`).join("")}</select>`;
} else if (f.kind === "secret") {
control = `<input type="password" id="${id}" value="${BT.escape(value)}"
placeholder="${value ? "unverändert" : "nicht gesetzt"}" autocomplete="new-password">`;
} else {
control = `<input type="${f.kind === "number" ? "number" : "text"}" id="${id}"
value="${BT.escape(value)}" placeholder="${BT.escape(f.placeholder || f.default || "")}">`;
}
return `<div class="field">
<label for="${id}">${BT.escape(f.label)}</label>
${control}
${f.help ? `<div class="help">${BT.escape(f.help)}</div>` : ""}
</div>`;
}
async function load() {
const data = await fetch("/api/settings").then((r) => r.json());
schema = data.schema;
groupsEl.innerHTML = schema.map((g) => `
<section class="bt-card">
<h2>${BT.escape(g.group)}</h2>
<p class="hint">${g.group === "Tandoor"
? "Wird vom AI-Webimport, vom Rezeptimport und vom Vereinheitlichen genutzt."
: g.group === "OpenAI" ? "Nur für den AI-Webimport nötig."
: "Schutz beim Abruf externer Rezeptseiten."}</p>
${g.fields.map((f) => fieldHtml(f, data.values[f.key] ?? "")).join("")}
</section>`).join("");
}
document.getElementById("save").addEventListener("click", async () => {
const payload = {};
schema.forEach((g) => g.fields.forEach((f) => {
const el = document.getElementById("f_" + f.key);
if (!el) return;
payload[f.key] = f.kind === "bool" ? String(el.checked) : el.value;
}));
try {
stateEl.textContent = "Speichern …";
await BT.api("/api/settings", { method: "POST", body: JSON.stringify(payload) });
stateEl.textContent = "";
BT.toast("Einstellungen gespeichert.", "ok");
load();
} catch (e) {
stateEl.textContent = "";
BT.toast(e.message, "err");
}
});
load();
</script>
</body>
</html>