Files
posts/index.html
T
dejanandClaude Opus 5 cdc979fa71 Post 1 alternativ mit gerenderten Komponenten
Kopie von Post 1, in der die Workflow-Engine nicht als Screenshot liegt,
sondern als HTML gebaut wird: Ablauf-Knoten und Bausteinliste kommen aus
einer Datenstruktur statt aus einer Aufnahme. Damit bleiben Kanten und Schrift
beim Export scharf, und es gibt keine Scrollbalken oder Cursor-Reste im Bild.
Kanban, Chat und Form Builder liegen dort weiter als Screenshot.

Die Projektnavigation traegt den dritten Eintrag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 21:36:08 +02:00

578 lines
24 KiB
HTML
Raw 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="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HiveGrid Post-Generator</title>
<link rel="stylesheet" href="./assets/fonts/manrope.css">
<style>
/* ─── HiveGrid design tokens (mirrored from app/globals.css) ─────────────
Deliberately plain hex, no Tailwind/oklch: the export libraries choke on
modern colour functions, so the slide subtree stays on values they can
serialise. */
:root {
--accent-yellow: #F1B729;
--accent-teal: #0D6E6E;
--surface: #FFFFFF;
--bg-primary: #FAFAFA;
--bg-dark: #0F1A2E;
--text-primary: #1A1A1A;
--text-secondary:#555555;
--text-on-dark: #C8D0DC;
--text-white: #FFFFFF;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { overscroll-behavior: none; }
body {
font-family: 'Manrope', system-ui, sans-serif;
background: #17181F; color: #E8EAEE;
height: 100vh; overflow: hidden; display: flex;
}
/* ─── Projekt-Navigation ─────────────────────────────────────────────── */
.projects { display: flex; gap: 6px; margin-bottom: 18px; }
.projects a {
flex: 1; text-align: center; text-decoration: none;
font-size: 12px; font-weight: 700; padding: 9px 8px; border-radius: 8px;
background: #22242C; color: #8A8F9B; border: 1px solid #2E313B;
}
.projects a:hover { background: #2A2D36; color: #C7CCD6; }
.projects a.here { background: var(--accent-yellow); color: #1A1A1A; border-color: var(--accent-yellow); }
.saved {
font-size: 11px; color: #6FCF97; opacity: 0; transition: opacity .2s;
margin-left: 6px;
}
.saved.on { opacity: 1; }
.forget { font-size: 11px; color: #6E7480; background: none; border: 0;
cursor: pointer; padding: 0; font-family: inherit; }
.forget:hover { color: #EB7C6B; }
/* ─── Control panel ──────────────────────────────────────────────────── */
.panel {
width: 306px; flex: none; height: 100vh; overflow-y: auto;
padding: 28px 22px 40px 28px;
border-right: 1px solid #24262F; background: #17181F;
}
.panel h1 { font-size: 19px; font-weight: 800; letter-spacing: -0.01em; margin-bottom: 4px; }
.panel .sub { font-size: 13px; color: #8A8F9B; margin-bottom: 24px; line-height: 1.5; }
.group { margin-bottom: 22px; }
.group > label {
display: block; font-size: 11px; font-weight: 700; letter-spacing: 0.09em;
text-transform: uppercase; color: #6E7480; margin-bottom: 9px;
}
.opts { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.opts button {
font-family: inherit; font-size: 13px; font-weight: 600;
padding: 11px 10px; border-radius: 8px; cursor: pointer;
background: #22242C; color: #C7CCD6; border: 1px solid #2E313B;
transition: background .15s, border-color .15s, color .15s;
display: flex; align-items: center; gap: 8px;
}
.opts button:hover { background: #2A2D36; }
.opts button[aria-pressed="true"] {
background: var(--accent-yellow); color: #1A1A1A; border-color: var(--accent-yellow);
}
.swatch { width: 12px; height: 12px; border-radius: 3px; flex: none; border: 1px solid rgba(0,0,0,.18); }
.row { display: flex; gap: 8px; }
.row button, .btn {
font-family: inherit; font-size: 13.5px; font-weight: 700;
padding: 13px 14px; border-radius: 8px; cursor: pointer; border: none;
background: var(--accent-yellow); color: #1A1A1A; flex: 1; transition: filter .15s;
}
.row button:hover, .btn:hover { filter: brightness(1.07); }
.btn.secondary { background: #22242C; color: #C7CCD6; border: 1px solid #2E313B; }
.btn:disabled { opacity: .45; cursor: not-allowed; filter: none; }
.keys { font-size: 12px; color: #6E7480; line-height: 1.9; }
.keys kbd {
background: #22242C; border: 1px solid #343843; border-bottom-width: 2px;
border-radius: 4px; padding: 1px 6px; font-family: ui-monospace, monospace;
font-size: 11px; color: #C7CCD6;
}
.status { margin-top: 14px; font-size: 12.5px; color: #6E7480; min-height: 18px; }
.status.ok { color: #6FCF97; }
.status.err { color: #EB7C6B; }
/* ─── Stage ──────────────────────────────────────────────────────────── */
/* Arbeitsfläche, klar abgesetzt von der Bedienleiste. */
.stage {
flex: 1; position: relative; height: 100vh; overflow: auto;
overscroll-behavior: contain;
background-color: #101116;
background-image: radial-gradient(circle, #282B35 1.6px, transparent 1.6px);
background-size: 24px 24px;
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 14px; padding: 48px;
}
.stage-meta { font-size: 12px; color: #6E7480; letter-spacing: .04em; }
/* The wrapper reserves the *scaled* footprint so the page doesn't jump;
the slide itself always stays at true 1080×1350 so the export is
pixel-exact with no resampling. */
.slide-wrap { width: 486px; height: 607.5px; flex: none; }
.slide-scaler { transform: scale(0.45); transform-origin: top left; }
/* ─── The slide — exactly Instagram 4:5 (1080 × 1350) ────────────────── */
.slide {
width: 1080px; height: 1350px;
position: relative; overflow: hidden;
display: flex; align-items: center; justify-content: center;
font-family: 'Manrope', system-ui, sans-serif;
}
/* Honeycomb — a real seamless tile across the full 1080×1350, generated in
JS from the same G polygon as the mark. Full-bleed on purpose: cropping it
inside a card made it read as a stray texture instead of a ground. */
.pattern { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.slide[data-pattern="aus"] .pattern { display: none; }
/* Chamfered frame as an outline, not a filled plate — a filled card would
cover the pattern, which is exactly what made the last version look cut off. */
.frame { position: absolute; inset: 40px; pointer-events: none; }
.slide[data-frame="aus"] .frame { display: none; }
.content {
position: relative; z-index: 1;
display: flex; flex-direction: column; align-items: center;
text-align: center; padding: 0 104px;
}
/* Eyebrow as a proper label: uppercase, tracked out, single line. Sentence
case at this size wrapped onto two lines and read like body copy. */
.eyebrow {
font-size: 30px; font-weight: 700;
letter-spacing: 0.15em; text-transform: uppercase;
line-height: 1.2; white-space: nowrap;
margin-bottom: 84px;
}
/* Top accent = colour (the marker is reserved for the payoff below). */
.eyebrow .a { color: var(--accent-on); }
.mark { height: 320px; width: auto; display: block; }
.rule { width: 104px; height: 6px; border-radius: 3px; margin: 74px 0 34px; }
.tagline {
font-size: 88px; font-weight: 800;
letter-spacing: -0.028em; line-height: 1.04;
}
/* Bottom accent = marker. */
.tagline .a {
background: linear-gradient(transparent 58%, var(--marker) 58%, var(--marker) 94%, transparent 94%);
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
padding: 0 6px;
}
/* ─── Variant: hell ──────────────────────────────────────────────────── */
.slide[data-variant="hell"] {
background: var(--surface);
--accent-on: #C8901A; --marker: var(--accent-yellow);
--pattern-color: var(--accent-yellow); --pattern-opacity: .085;
--frame-color: #E5E5E5;
}
.slide[data-variant="hell"] .eyebrow { color: var(--text-secondary); }
.slide[data-variant="hell"] .tagline { color: var(--text-primary); }
.slide[data-variant="hell"] .mark { color: var(--accent-yellow); }
.slide[data-variant="hell"] .rule { background: var(--accent-yellow); }
/* ─── Variant: dunkel ────────────────────────────────────────────────── */
.slide[data-variant="dunkel"] {
background: var(--bg-dark);
--accent-on: var(--accent-yellow); --marker: var(--accent-yellow);
--pattern-color: var(--accent-yellow); --pattern-opacity: .10;
--frame-color: rgba(255,255,255,.16);
}
.slide[data-variant="dunkel"] .eyebrow { color: var(--text-on-dark); }
.slide[data-variant="dunkel"] .tagline { color: var(--text-white); }
.slide[data-variant="dunkel"] .mark { color: var(--accent-yellow); }
.slide[data-variant="dunkel"] .rule { background: var(--accent-yellow); }
.slide[data-variant="dunkel"] .tagline .a { color: #1A1A1A; }
/* ─── Variant: gelb ──────────────────────────────────────────────────── */
.slide[data-variant="gelb"] {
background: var(--accent-yellow);
--accent-on: #FFFFFF; --marker: #1A1A1A;
--pattern-color: #1A1A1A; --pattern-opacity: .07;
--frame-color: rgba(26,26,26,.22);
}
.slide[data-variant="gelb"] .eyebrow { color: #6B5210; }
.slide[data-variant="gelb"] .tagline { color: #1A1A1A; }
.slide[data-variant="gelb"] .mark { color: #1A1A1A; }
.slide[data-variant="gelb"] .rule { background: #1A1A1A; }
.slide[data-variant="gelb"] .tagline .a { color: var(--accent-yellow); }
/* ─── Variant: teal ──────────────────────────────────────────────────── */
.slide[data-variant="teal"] {
background: var(--accent-teal);
--accent-on: var(--accent-yellow); --marker: var(--accent-yellow);
--pattern-color: #FFFFFF; --pattern-opacity: .07;
--frame-color: rgba(255,255,255,.18);
}
.slide[data-variant="teal"] .eyebrow { color: #A8D4D4; }
.slide[data-variant="teal"] .tagline { color: var(--text-white); }
.slide[data-variant="teal"] .mark { color: var(--accent-yellow); }
.slide[data-variant="teal"] .rule { background: var(--accent-yellow); }
.slide[data-variant="teal"] .tagline .a { color: var(--accent-teal); }
</style>
</head>
<body>
<aside class="panel">
<h1>Post-Generator</h1>
<div class="projects"><a class="" href="./post1.html">Post 1</a><a class="" href="./post1-alt.html">Post 1 · Alt</a><a class="here" href="./index.html">Post 2</a></div>
<p class="sub">Einzelbild · 1080 × 1350 px<span class="saved" id="saved"></span><br><button class="forget" id="forget">Projekt zurücksetzen</button></p>
<div class="group">
<label>Variante</label>
<div class="opts" id="variants">
<button data-v="hell" aria-pressed="true"><span class="swatch" style="background:#FFFFFF"></span>Hell</button>
<button data-v="dunkel" aria-pressed="false"><span class="swatch" style="background:#0F1A2E"></span>Dunkel</button>
<button data-v="gelb" aria-pressed="false"><span class="swatch" style="background:#F1B729"></span>Gelb</button>
<button data-v="teal" aria-pressed="false"><span class="swatch" style="background:#0D6E6E"></span>Teal</button>
</div>
</div>
<div class="group">
<label>Wabenraster</label>
<div class="opts" id="patterns">
<button data-p="an" aria-pressed="true">An</button>
<button data-p="aus" aria-pressed="false">Aus</button>
</div>
</div>
<div class="group">
<label>Dichte</label>
<div class="opts" id="densities">
<button data-d="0.40" aria-pressed="false">Fein</button>
<button data-d="0.55" aria-pressed="true">Mittel</button>
<button data-d="0.75" aria-pressed="false">Grob</button>
<button data-d="1.00" aria-pressed="false">XL</button>
</div>
</div>
<div class="group">
<label>Rahmen</label>
<div class="opts" id="frames">
<button data-f="an" aria-pressed="true">An</button>
<button data-f="aus" aria-pressed="false">Aus</button>
</div>
</div>
<div class="group">
<label>Download</label>
<div class="row" style="margin-bottom:8px">
<button id="dl-png" disabled>PNG</button>
<button id="dl-jpg" class="btn secondary" disabled>JPG</button>
</div>
<button id="dl-all" class="btn secondary" style="width:100%" disabled>Alle 4 Varianten als ZIP</button>
</div>
<div class="group" style="margin-bottom:0">
<label>Tastatur</label>
<div class="keys">
<kbd>1</kbd>–<kbd>4</kbd> Variante · <kbd>P</kbd> Raster · <kbd>R</kbd> Rahmen<br>
<kbd>D</kbd> PNG laden · <kbd>A</kbd> alle als ZIP
</div>
</div>
<div class="status" id="status">Lade Schriften und Assets ...</div>
</aside>
<main class="stage">
<div class="slide-wrap">
<div class="slide-scaler">
<!-- ─── SLIDE ─────────────────────────────────────────────────────── -->
<div class="slide" id="slide" data-variant="hell" data-pattern="an" data-frame="an">
<svg class="pattern" id="pattern" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"></svg>
<svg class="frame" viewBox="0 0 1000 1270" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<polygon points="34,0 966,0 1000,34 1000,1236 966,1270 34,1270 0,1236 0,34"
stroke="var(--frame-color)" stroke-width="3"/>
</svg>
<div class="content">
<p class="eyebrow">Vom <span class="a">Handwerker</span> für den <span class="a">Handwerker</span></p>
<!-- G mark only, lifted straight out of logo.svg (the single yellow
polygon), normalised to a 0,0-based 384.5 × 332.9 box. Inline
rather than <img> so each variant recolours it via currentColor. -->
<svg class="mark" viewBox="0 0 384.5 332.9" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="HiveGrid">
<polygon fill="currentColor" points="384.5,166.5 336.4,83.2 192.2,166.5 288.3,166.5 240.3,249.7 144.2,249.7 96.1,166.5 144.2,83.2 336.4,83.2 298,16.8 288.3,0 96.1,0 0,166.5 96.1,332.9 288.3,332.9"/>
</svg>
<div class="rule"></div>
<p class="tagline"><span class="a">Verbinden</span><br>statt ersetzen</p>
</div>
</div>
<!-- ─── /SLIDE ────────────────────────────────────────────────────── -->
</div>
</div>
<p class="stage-meta">Vorschau 45 %, Export in voller Auflösung</p>
</main>
<script src="./vendor/html-to-image.js"></script>
<script src="./vendor/jszip.min.js"></script>
<script>
(function () {
"use strict";
/* A syntax or temporal-dead-zone error at this level kills the whole script
silently: the panel renders from static markup, but nothing is ever built
and no status is set. Surface it instead of showing an empty stage. */
window.addEventListener("error", e => {
const el = document.getElementById("status");
if (el) {
el.textContent = "Skriptfehler: " + (e.message || e.error);
el.className = "status err";
}
});
const slide = document.getElementById("slide");
const statusEl = document.getElementById("status");
const patternEl = document.getElementById("pattern");
const VARIANTS = ["hell", "dunkel", "gelb", "teal"];
const SVG_NS = "http://www.w3.org/2000/svg";
// The G mark, normalised to a 0,0-based box.
const G_POINTS = "384.5,166.5 336.4,83.2 192.2,166.5 288.3,166.5 240.3,249.7 144.2,249.7 96.1,166.5 144.2,83.2 336.4,83.2 298,16.8 288.3,0 96.1,0 0,166.5 96.1,332.9 288.3,332.9";
const G_W = 384.5, G_H = 332.9;
let fontEmbedCss = "";
/* ─── Projektstand ────────────────────────────────────────────────────────
Gleiches Prinzip wie bei Post 1: Das Werkzeug hält den Stand. */
const STORE = "hivegrid.projekt.post2";
let savedTimer = null;
function markSaved() {
const el = document.getElementById("saved");
if (!el) return;
el.textContent = "gespeichert";
el.classList.add("on");
clearTimeout(savedTimer);
savedTimer = setTimeout(() => el.classList.remove("on"), 1200);
}
function saveState() {
try {
localStorage.setItem(STORE, JSON.stringify({
v: 1,
variant: slide.dataset.variant,
pattern: slide.dataset.pattern,
frame: slide.dataset.frame,
density: currentDensity,
}));
markSaved();
} catch (err) { console.warn("Speichern fehlgeschlagen", err); }
}
function loadState() {
let d = null;
try { d = JSON.parse(localStorage.getItem(STORE) || "null"); } catch (_) {}
if (!d || d.v !== 1) return false;
if (d.variant) setVariant(d.variant);
if (d.pattern) setPattern(d.pattern);
if (d.frame) setFrame(d.frame);
if (d.density) setDensity(d.density);
return true;
}
function forgetProject() {
try { localStorage.removeItem(STORE); } catch (_) {}
location.reload();
}
function setStatus(msg, kind) {
statusEl.textContent = msg;
statusEl.className = "status" + (kind ? " " + kind : "");
}
/* Builds a seamless flat-top honeycomb tile.
For flat-top hexagons the columns step by 3/4 of the width and neighbouring
columns are offset vertically by half a row, so one tile = two columns wide
and one row high. The copies hanging outside the tile are what makes the
repeat seamless — without them you get the visible seams of the last try. */
function buildPattern(scale) {
const colStep = G_W * 0.75; // 288.4
const rowStep = G_H; // 332.9
const tileW = colStep * 2; // 576.8
const tileH = rowStep;
// Hex centres that touch this tile, including the ones bleeding over the edges.
const centres = [
[0, 0], [tileW, 0], [0, tileH], [tileW, tileH],
[colStep, rowStep / 2], [colStep, -rowStep / 2], [colStep, tileH + rowStep / 2],
];
while (patternEl.firstChild) patternEl.removeChild(patternEl.firstChild);
const defs = document.createElementNS(SVG_NS, "defs");
const pat = document.createElementNS(SVG_NS, "pattern");
pat.setAttribute("id", "hexpat");
pat.setAttribute("patternUnits", "userSpaceOnUse");
pat.setAttribute("width", tileW);
pat.setAttribute("height", tileH);
pat.setAttribute("patternTransform", "scale(" + scale + ")");
for (const [cx, cy] of centres) {
const p = document.createElementNS(SVG_NS, "polygon");
p.setAttribute("points", G_POINTS);
p.setAttribute("fill", "currentColor");
p.setAttribute("transform", "translate(" + (cx - G_W / 2) + "," + (cy - G_H / 2) + ")");
pat.appendChild(p);
}
defs.appendChild(pat);
patternEl.appendChild(defs);
const rect = document.createElementNS(SVG_NS, "rect");
rect.setAttribute("width", "100%");
rect.setAttribute("height", "100%");
rect.setAttribute("fill", "url(#hexpat)");
patternEl.appendChild(rect);
// Colour + opacity come from the active variant.
patternEl.style.color = "var(--pattern-color)";
patternEl.style.opacity = "var(--pattern-opacity)";
}
function press(containerId, attr, value) {
document.querySelectorAll("#" + containerId + " button").forEach(b =>
b.setAttribute("aria-pressed", String(b.dataset[attr] === value))
);
}
function setVariant(v) { slide.dataset.variant = v; press("variants", "v", v); }
function setPattern(p) { slide.dataset.pattern = p; press("patterns", "p", p); }
function setFrame(f) { slide.dataset.frame = f; press("frames", "f", f); }
let currentDensity = "0.55";
function setDensity(d) { currentDensity = d; buildPattern(Number(d)); press("densities", "d", d); }
/* Renders at pixelRatio 1 against a node that is already 1080×1350, so the
output is exactly Instagram's spec with no upscaling artefacts. */
function renderOptions() {
return {
width: 1080, height: 1350, pixelRatio: 1,
fontEmbedCSS: fontEmbedCss,
// The scaler transform is a preview-only concern — neutralise it for the
// export, otherwise the bitmap comes out at 45 %.
style: { transform: "none", transformOrigin: "top left" },
};
}
/* A 1080x1350 PNG as a data: URL runs to several megabytes. Browsers routinely
ignore the download attribute on those and navigate to the URL instead,
which shows up as a blank tab. Blob URLs are handled properly. */
/* Local-only save endpoint, see serve.py. */
async function downloadBlob(blob, filename) {
const res = await fetch("/save", {
method: "POST",
headers: { "X-Filename": filename },
body: blob,
});
if (!res.ok) throw new Error("Speichern fehlgeschlagen: HTTP " + res.status);
return (await res.json()).path;
}
const nameFor = ext => "hivegrid-post2-statement-" + slide.dataset.variant + "." + ext;
async function downloadPng() {
setStatus("Rendere PNG ...");
const blob = await htmlToImage.toBlob(slide, renderOptions());
if (!blob) throw new Error("Rendern lieferte kein Bild");
downloadBlob(blob, nameFor("png"));
setStatus("PNG gespeichert. 1080 × 1350", "ok");
}
async function downloadJpg() {
setStatus("Rendere JPG ...");
const opts = Object.assign(renderOptions(), { quality: 0.9 });
const url = await htmlToImage.toJpeg(slide, opts);
const blob = await (await fetch(url)).blob();
downloadBlob(blob, nameFor("jpg"));
setStatus("JPG gespeichert. Qualität 90 %", "ok");
}
async function downloadAll() {
const zip = new JSZip();
const keep = slide.dataset.variant;
for (const v of VARIANTS) {
setStatus("Rendere " + v + " ...");
setVariant(v);
await document.fonts.ready;
zip.file("hivegrid-post2-statement-" + v + ".png",
await htmlToImage.toBlob(slide, renderOptions()));
}
setVariant(keep);
setStatus("Packe ZIP ...");
downloadBlob(await zip.generateAsync({ type: "blob" }), "hivegrid-post2-statement.zip");
setStatus("4 Varianten als ZIP gespeichert", "ok");
}
function guard(fn) {
return () => fn().catch(err => {
console.error(err);
setStatus("Fehler: " + err.message, "err");
});
}
document.getElementById("variants").addEventListener("click", e => {
const b = e.target.closest("button"); if (b) { setVariant(b.dataset.v); saveState(); }
});
document.getElementById("patterns").addEventListener("click", e => {
const b = e.target.closest("button"); if (b) { setPattern(b.dataset.p); saveState(); }
});
document.getElementById("frames").addEventListener("click", e => {
const b = e.target.closest("button"); if (b) { setFrame(b.dataset.f); saveState(); }
});
document.getElementById("densities").addEventListener("click", e => {
const b = e.target.closest("button"); if (b) { setDensity(b.dataset.d); saveState(); }
});
document.getElementById("dl-png").addEventListener("click", guard(downloadPng));
document.getElementById("dl-jpg").addEventListener("click", guard(downloadJpg));
document.getElementById("dl-all").addEventListener("click", guard(downloadAll));
document.addEventListener("keydown", e => {
if (e.metaKey || e.ctrlKey || e.altKey) return;
const k = e.key.toLowerCase();
if (k >= "1" && k <= "4") setVariant(VARIANTS[Number(k) - 1]);
else if (k === "p") setPattern(slide.dataset.pattern === "an" ? "aus" : "an");
else if (k === "r") setFrame(slide.dataset.frame === "an" ? "aus" : "an");
else if (k === "d") guard(downloadPng)();
else if (k === "a") guard(downloadAll)();
});
/* Boot: build the tile, wait for Manrope to actually be resident (otherwise
the first export silently falls back to a system font), and pre-compute the
font CSS once instead of on every render. */
(async function boot() {
try {
buildPattern(0.55);
const restored = loadState();
document.getElementById("forget").addEventListener("click", forgetProject);
await document.fonts.ready;
fontEmbedCss = await htmlToImage.getFontEmbedCSS(slide);
["dl-png", "dl-jpg", "dl-all"].forEach(id => document.getElementById(id).disabled = false);
setStatus(restored ? "Projektstand geladen" : "Bereit. 1080 × 1350 px", "ok");
} catch (err) {
console.error(err);
setStatus("Fehler beim Laden: " + err.message, "err");
}
})();
})();
</script>
</body>
</html>