180 lines
7.5 KiB
JavaScript
180 lines
7.5 KiB
JavaScript
/* ==========================================================================
|
|
boehmitools · gemeinsame Shell
|
|
Stellt App-Bar, Tool-Umschalter, Theme und kleine Helfer für alle Plugins.
|
|
|
|
Einbindung in einem Plugin (siehe plugins/README.md):
|
|
<link rel="stylesheet" href="/shared/boehmi.css">
|
|
<script src="/shared/boehmi.js" defer></script>
|
|
<body data-bt-title="Mein Tool" data-bt-icon="🔧"> ... </body>
|
|
|
|
Der Mount-Pfad wird automatisch aus der URL abgeleitet, ein Plugin muss
|
|
ihn nicht kennen. API-Aufrufe laufen über BT.api("/api/...").
|
|
========================================================================== */
|
|
(function () {
|
|
"use strict";
|
|
|
|
const ROOT_META = document.querySelector('meta[name="bt-root"]');
|
|
const pathMatch = location.pathname.match(/^(.*?)(\/plugins\/[^/]+)/);
|
|
|
|
const BT = {
|
|
/** Präfix der gesamten Suite (nur bei Reverse-Proxy-Unterpfad gesetzt). */
|
|
root: ROOT_META ? ROOT_META.content.replace(/\/$/, "") : (pathMatch ? pathMatch[1] : ""),
|
|
/** Mount-Präfix des aktuellen Plugins, z. B. "/plugins/trainingsplan". */
|
|
base: pathMatch ? pathMatch[1] + pathMatch[2] : "",
|
|
/** ID des aktuellen Plugins oder null (Dashboard). */
|
|
plugin: pathMatch ? pathMatch[2].split("/").pop() : null,
|
|
plugins: [],
|
|
};
|
|
window.BT = BT;
|
|
|
|
/* ------------------------------------------------------------- Helfer */
|
|
|
|
/** Plugin-eigener Pfad → absolute URL. */
|
|
BT.url = (path) => BT.base + path;
|
|
/** Suite-weiter Pfad → absolute URL. */
|
|
BT.rootUrl = (path) => BT.root + path;
|
|
|
|
/** fetch mit JSON-Handling, relativ zum Plugin-Mount. */
|
|
BT.api = async function (path, options = {}) {
|
|
const opts = { ...options };
|
|
if (opts.body !== undefined && !(opts.body instanceof FormData)) {
|
|
opts.headers = { "Content-Type": "application/json", ...(opts.headers || {}) };
|
|
}
|
|
const res = await fetch(BT.url(path), opts);
|
|
const text = await res.text();
|
|
let data = {};
|
|
try { data = text ? JSON.parse(text) : {}; } catch (_) { data = { raw: text }; }
|
|
if (!res.ok) throw new Error(data.detail || data.error || `HTTP ${res.status}`);
|
|
return data;
|
|
};
|
|
|
|
BT.toast = function (message, kind = "") {
|
|
let host = document.getElementById("bt-toaster");
|
|
if (!host) {
|
|
host = document.createElement("div");
|
|
host.id = "bt-toaster";
|
|
document.body.appendChild(host);
|
|
}
|
|
const el = document.createElement("div");
|
|
el.className = "bt-toast " + kind;
|
|
el.textContent = message;
|
|
host.appendChild(el);
|
|
requestAnimationFrame(() => el.classList.add("show"));
|
|
setTimeout(() => {
|
|
el.classList.remove("show");
|
|
setTimeout(() => el.remove(), 250);
|
|
}, kind === "err" ? 5200 : 2600);
|
|
};
|
|
|
|
BT.escape = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => (
|
|
{ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]
|
|
));
|
|
|
|
/* -------------------------------------------------------------- Theme */
|
|
|
|
const THEME_KEY = "boehmitools.theme";
|
|
|
|
BT.setTheme = function (mode) {
|
|
const resolved = mode === "auto"
|
|
? (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
|
|
: mode;
|
|
document.documentElement.setAttribute("data-bt-theme", resolved);
|
|
try { localStorage.setItem(THEME_KEY, mode); } catch (_) {}
|
|
document.querySelectorAll("[data-bt-theme-toggle]").forEach((b) => {
|
|
b.textContent = resolved === "dark" ? "☀︎" : "☾";
|
|
b.title = resolved === "dark" ? "Helles Design" : "Dunkles Design";
|
|
});
|
|
};
|
|
|
|
BT.currentTheme = function () {
|
|
try { return localStorage.getItem(THEME_KEY) || "auto"; } catch (_) { return "auto"; }
|
|
};
|
|
|
|
// so früh wie möglich anwenden (verhindert Aufblitzen)
|
|
BT.setTheme(BT.currentTheme());
|
|
matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
|
|
if (BT.currentTheme() === "auto") BT.setTheme("auto");
|
|
});
|
|
|
|
/* ------------------------------------------------------------ App-Bar */
|
|
|
|
function onBody(fn) {
|
|
if (document.body) fn();
|
|
else document.addEventListener("DOMContentLoaded", fn, { once: true });
|
|
}
|
|
|
|
function buildAppbar(plugins) {
|
|
const me = plugins.find((p) => p.id === BT.plugin);
|
|
const title = document.body.dataset.btTitle || (me ? me.name : "Dashboard");
|
|
const icon = document.body.dataset.btIcon || (me ? me.icon : "▦");
|
|
|
|
const bar = document.createElement("header");
|
|
bar.className = "bt-appbar";
|
|
bar.innerHTML = `
|
|
<a class="bt-brand" href="${BT.rootUrl("/")}" title="Zum Dashboard">
|
|
<span class="bt-brand-mark">B</span>
|
|
<span>boehmitools</span>
|
|
</a>
|
|
${BT.plugin ? `
|
|
<span class="bt-appbar-sep"></span>
|
|
<span class="bt-crumb"><span class="bt-crumb-ico">${BT.escape(icon)}</span>
|
|
<span>${BT.escape(title)}</span></span>` : ""}
|
|
<span class="bt-appbar-spacer"></span>
|
|
<span class="bt-appbar-actions">
|
|
<span class="bt-switch">
|
|
<button class="mini ghost" id="bt-switch-btn" aria-haspopup="true" aria-expanded="false">
|
|
Tools ▾
|
|
</button>
|
|
<div class="bt-switch-menu" id="bt-switch-menu" role="menu">
|
|
<div class="bt-switch-head">Tools</div>
|
|
${plugins.map((p) => `
|
|
<a class="bt-switch-item ${p.id === BT.plugin ? "active" : ""}" href="${BT.rootUrl(p.mount)}/">
|
|
<span class="ico">${BT.escape(p.icon)}</span>
|
|
<span><b>${BT.escape(p.name)}</b><i>${BT.escape(p.summary || "")}</i></span>
|
|
</a>`).join("")}
|
|
<div class="bt-switch-head">Suite</div>
|
|
<a class="bt-switch-item" href="${BT.rootUrl("/")}"><span class="ico">▦</span>
|
|
<span><b>Dashboard</b><i>Übersicht aller Tools</i></span></a>
|
|
<a class="bt-switch-item" href="${BT.rootUrl("/jobs")}"><span class="ico">↻</span>
|
|
<span><b>Läufe</b><i>Protokolle der Kommandozeilen-Tools</i></span></a>
|
|
<a class="bt-switch-item" href="${BT.rootUrl("/settings")}"><span class="ico">⚙</span>
|
|
<span><b>Einstellungen</b><i>Tandoor- und OpenAI-Zugänge</i></span></a>
|
|
</div>
|
|
</span>
|
|
<button class="mini ghost icon" data-bt-theme-toggle title="Design wechseln">☾</button>
|
|
</span>`;
|
|
|
|
const anchor = document.getElementById("bt-appbar") || document.body.firstChild;
|
|
if (anchor && anchor.id === "bt-appbar") anchor.replaceWith(bar);
|
|
else document.body.insertBefore(bar, document.body.firstChild);
|
|
|
|
const btn = document.getElementById("bt-switch-btn");
|
|
const menu = document.getElementById("bt-switch-menu");
|
|
btn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
const open = menu.classList.toggle("open");
|
|
btn.setAttribute("aria-expanded", String(open));
|
|
});
|
|
document.addEventListener("click", () => menu.classList.remove("open"));
|
|
document.addEventListener("keydown", (e) => { if (e.key === "Escape") menu.classList.remove("open"); });
|
|
|
|
bar.querySelector("[data-bt-theme-toggle]").addEventListener("click", () => {
|
|
const now = document.documentElement.getAttribute("data-bt-theme");
|
|
BT.setTheme(now === "dark" ? "light" : "dark");
|
|
});
|
|
BT.setTheme(BT.currentTheme());
|
|
}
|
|
|
|
BT.ready = fetch(BT.rootUrl("/api/plugins"))
|
|
.then((r) => (r.ok ? r.json() : { plugins: [] }))
|
|
.catch(() => ({ plugins: [] }))
|
|
.then((d) => new Promise((resolve) => {
|
|
BT.plugins = d.plugins || [];
|
|
onBody(() => {
|
|
if (document.body.dataset.btShell !== "off") buildAppbar(BT.plugins);
|
|
document.dispatchEvent(new CustomEvent("bt:ready", { detail: BT }));
|
|
resolve(BT);
|
|
});
|
|
}));
|
|
})();
|