Weiter zum Inhalt
  • Home
  • Aktuell
  • Tags
  • 0 Ungelesen 0
  • Kategorien
  • Unreplied
  • Beliebt
  • GitHub
  • Docu
  • Hilfe
Skins
  • Hell
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dunkel
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Standard: (Kein Skin)
  • Kein Skin
Einklappen
ioBroker Logo

Community Forum

donate donate
  1. ioBroker Community Home
  2. Deutsch
  3. Skripten / Logik
  4. JavaScript
  5. Kontakte (Cards)

NEWS

  • wichtiges UPDATE für controller 7.2.2 im stable
    HomoranH
    Homoran
    8
    1
    571

  • Neues YouTube-Video: Visualisierung im Devices-Adapter
    BluefoxB
    Bluefox
    15
    1
    2.8k

  • Neuer ioBroker-Blog online: Monatsrückblick März/April 2026
    BluefoxB
    Bluefox
    8
    1
    3.0k

Kontakte (Cards)

Geplant Angeheftet Gesperrt Verschoben JavaScript
javascriptmultimediatemplate
34 Beiträge 4 Kommentatoren 1.2k Aufrufe 5 Beobachtet
  • Älteste zuerst
  • Neuste zuerst
  • Meiste Stimmen
Antworten
  • In einem neuen Thema antworten
Anmelden zum Antworten
Dieses Thema wurde gelöscht. Nur Nutzer mit entsprechenden Rechten können es sehen.
  • Ro75R Ro75

    In unserem Dashboard stehen alle wichtigen Kontakte als moderne, dynamische Karten bereit – so hat jedes Familienmitglied jederzeit Zugriff auf die aktuellsten Informationen, ohne suchen zu müssen.
    Die Daten liegen zentral in einer JSON, aus der automatisch schicke SVG‑Karten erzeugt werden, die sich direkt in VIS oder jede andere Visualisierung einfügen lassen.

    Sobald ein Eintrag geändert oder ein neuer Kontakt hinzugefügt wird, aktualisiert das System die Karten vollautomatisch.
    Keine Pflege an mehreren Stellen, keine veralteten Infos – einmal eintragen, überall aktuell.

    Das Script erzeugt beim ersten Start automatisch einen kompletten Beispieldatensatz mit drei fertigen Karten – inklusive der passenden SVG‑Grafiken.
    So sieht man sofort, wie das System funktioniert und wie die Karten später in der Visualisierung aussehen.

    card.png

    HINWEIS: Ein Teil des Codes wurde dabei mit Unterstützung einer KI (Microsoft Copilot) entwickelt, um die Erstellung noch effizienter und moderner zu gestalten.

    Hier der Code:

    // === Kontakte (CARDS) ===
     
    //Erst-Version 1.0.0 - 25.05.2026
    //Version 1.0.10 - 23.06.2026
    //Ersteller Ro75.
     
    //Voraussetzungen (Version 1.0.10 getestet mit)
    //NodeJS: 22.22.x
    //Javascript-Adapter: 9.0.18
    //Admin-Adapter: 7.8.23
    //js-controler: 7.2.2
    
    //********** START KONFIGURATION **********
    const root     = "0_userdata.0.Kontakte."
    const JSON_DP  = root + "json";
    const SVG_ROOT = root + "svg.";
    
    // zentrale SVG-Größen
    const CARD_LARGE_W = 350;
    const CARD_LARGE_H = 360;
    
    const CARD_SMALL_W = 350;
    const CARD_SMALL_H = 130;
    
    const FONT = {
        family: "InterVariable",   // Globale Schriftfamilie für alle Karten
    
        // STANDARD – neutral, technisch, kühl
        standard: {
            name:    { size: 16, color: "#ffffff", bold: true,  italic: false },  // Name / Praxis / Firma
            address: { size: 13, color: "#d0d7e2", bold: false, italic: false },  // Straße, PLZ, Ort
            contact: { size: 12, color: "#9fb3d9", bold: false, italic: false },  // Tel., Fax, E-Mail
            web:     { size: 12, color: "#6fa8ff", bold: false, italic: false },  // Website-Link
            title:   { size: 13, color: "#ffffff", bold: true,  italic: false },  // Abschnittstitel ("Öffnungszeiten")
            times:   { size: 12, color: "#d0d7e2", bold: false, italic: false },  // Öffnungszeiten-Zeilen
            hint:    { size: 12, color: "#ffcc66", bold: true,  italic: false }   // Hinweis-/Infozeilen
        },
    
        // WAVE – kühl, frisch, maritim
        wave: {
            name:    { size: 16, color: "#163a70", bold: true, italic: false },   // Name / Praxis / Firma
            address: { size: 13, color: "#173d74", bold: false, italic: false },  // Straße, PLZ, Ort
            contact: { size: 12, color: "#173d74", bold: false, italic: false },  // Tel., Fax, E-Mail
            web:     { size: 12, color: "#2563eb", bold: false, italic: false },  // Website-Link
            title:   { size: 13, color: "#1d4ed8", bold: true, italic: false },   // Abschnittstitel ("Öffnungszeiten")
            times:   { size: 12, color: "#173d74", bold: false, italic: false },  // Öffnungszeiten-Zeilen
            hint:    { size: 12, color: "#1a1a1a", bold: true,  italic: false }   // Hinweis-/Infozeilen
        },
    
        // TEAL – modern, kühl, glasig
        teal: {
            name:    { size: 16, color: "#ffffff", bold: true, italic: false },   // Name / Praxis / Firma
            address: { size: 13, color: "#e8ffef", bold: false, italic: false },  // Straße, PLZ, Ort
            contact: { size: 12, color: "#e8ffef", bold: false, italic: false },  // Tel., Fax, E-Mail
            web:     { size: 12, color: "#e8ffef", bold: false, italic: false },  // Website-Link
            title:   { size: 13, color: "#e8ffef", bold: true, italic: false },   // Abschnittstitel ("Öffnungszeiten")
            times:   { size: 12, color: "#e8ffef", bold: false, italic: false },  // Öffnungszeiten-Zeilen
            hint:    { size: 12, color: "#1a1a1a", bold: true,  italic: false }   // Hinweis-/Infozeilen
        },
    
        // WHITE – hell, neutral, klar
        white: {
            name:    { size: 16, color: "#1a1a1a", bold: true, italic: false },   // Name / Praxis / Firma
            address: { size: 13, color: "#2d3748", bold: false, italic: false },  // Straße, PLZ, Ort
            contact: { size: 12, color: "#2d3748", bold: false, italic: false },  // Tel., Fax, E-Mail
            web:     { size: 12, color: "#33691e", bold: false, italic: false },  // Website-Link
            title:   { size: 13, color: "#254618", bold: true, italic: false },   // Abschnittstitel ("Öffnungszeiten")
            times:   { size: 12, color: "#2d3748", bold: false, italic: false },  // Öffnungszeiten-Zeilen
            hint:    { size: 12, color: "#1a1a1a", bold: true,  italic: false }   // Hinweis-/Infozeilen
        },
    
        // COFFEE – warm, erdig, gemütlich
        coffee: {
            name:    { size: 16, color: "#4E342E", bold: true, italic: false },   // Name / Praxis / Firma
            address: { size: 13, color: "#4E342E", bold: false, italic: false },  // Straße, PLZ, Ort
            contact: { size: 12, color: "#4E342E", bold: false, italic: false },  // Tel., Fax, E-Mail
            web:     { size: 12, color: "#6D4C41", bold: false, italic: false },  // Website-Link
            title:   { size: 13, color: "#4E342E", bold: true, italic: false },   // Abschnittstitel ("Öffnungszeiten")
            times:   { size: 12, color: "#4E342E", bold: false, italic: false },  // Öffnungszeiten-Zeilen
            hint:    { size: 12, color: "#4E342E", bold: true,  italic: false }   // Hinweis-/Infozeilen
        },
    
        // YELLOW – warm, freundlich, sonnig
        yellow: {
            name:    { size: 16, color: "#5D4037", bold: true, italic: false },   // Name / Praxis / Firma
            address: { size: 13, color: "#4E342E", bold: false, italic: false },  // Straße, PLZ, Ort
            contact: { size: 12, color: "#4E342E", bold: false, italic: false },  // Tel., Fax, E-Mail
            web:     { size: 12, color: "#F57F17", bold: false, italic: false },  // Website-Link
            title:   { size: 13, color: "#5D4037", bold: true, italic: false },   // Abschnittstitel ("Öffnungszeiten")
            times:   { size: 12, color: "#4E342E", bold: false, italic: false },  // Öffnungszeiten-Zeilen
            hint:    { size: 12, color: "#5D4037", bold: true,  italic: false }   // Hinweis-/Infozeilen
        },
    
        // GREEN – natur, frisch, harmonisch
        green: {
            name:    { size: 16, color: "#1B5E20", bold: true, italic: false },   // Name / Praxis / Firma
            address: { size: 13, color: "#2E7D32", bold: false, italic: false },  // Straße, PLZ, Ort
            contact: { size: 12, color: "#2E7D32", bold: false, italic: false },  // Tel., Fax, E-Mail
            web:     { size: 12, color: "#1B5E20", bold: false, italic: false },  // Website-Link
            title:   { size: 13, color: "#1B5E20", bold: true, italic: false },   // Abschnittstitel ("Öffnungszeiten")
            times:   { size: 12, color: "#2E7D32", bold: false, italic: false },  // Öffnungszeiten-Zeilen
            hint:    { size: 12, color: "#1B5E20", bold: true,  italic: false }   // Hinweis-/Infozeilen
        },
    
        // RED – kräftig, energisch, dominant
        red: {
            name:    { size: 16, color: "#B71C1C", bold: true, italic: false },   // Name / Praxis / Firma
            address: { size: 13, color: "#C62828", bold: false, italic: false },  // Straße, PLZ, Ort
            contact: { size: 12, color: "#C62828", bold: false, italic: false },  // Tel., Fax, E-Mail
            web:     { size: 12, color: "#B71C1C", bold: false, italic: false },  // Website-Link
            title:   { size: 13, color: "#B71C1C", bold: true, italic: false },   // Abschnittstitel ("Öffnungszeiten")
            times:   { size: 12, color: "#C62828", bold: false, italic: false },  // Öffnungszeiten-Zeilen
            hint:    { size: 12, color: "#B71C1C", bold: true,  italic: false }   // Hinweis-/Infozeilen
        },
    
        // BLUE – kühl, seriös, sachlich
        blue: {
            name:    { size: 16, color: "#0D47A1", bold: true, italic: false },   // Name / Praxis / Firma
            address: { size: 13, color: "#1E3A5F", bold: false, italic: false },  // Straße, PLZ, Ort
            contact: { size: 12, color: "#1E3A5F", bold: false, italic: false },  // Tel., Fax, E-Mail
            web:     { size: 12, color: "#1565C0", bold: false, italic: false },  // Website-Link
            title:   { size: 13, color: "#0D47A1", bold: true, italic: false },   // Abschnittstitel ("Öffnungszeiten")
            times:   { size: 12, color: "#1E3A5F", bold: false, italic: false },  // Öffnungszeiten-Zeilen
            hint:    { size: 12, color: "#0D47A1", bold: true,  italic: false }   // Hinweis-/Infozeilen
        },
    
        weight: {
            normal: 400,   // Normalgewicht
            bold:   600    // Fett
        }
    };
    
    const CARD_THEMES = {
        white: {
            fontTheme: "white",                      // Name des Font-Themes
            iconBg: "#43A047",                       // Hintergrundfarbe der runden Icons
            iconStroke: "#ffffff",                   // Farbe der Icons
            titleUnderline: "#43A047",               // Linie unter dem Namen
            avatarStroke: "#43A047",                 // Rahmen um das Bild/Avatar
            timesBoxBg: "rgba(190, 235, 159, 0.15)", // Hintergrund Öffnungszeiten-Box
            timesBoxBorder: "rgba(0,0,0,0.15)",      // Rahmen Öffnungszeiten-Box
            background: "rgba(255,255,255,1)"        // Hintergrund Uni-Card
        },
        coffee: {
            fontTheme: "coffee",
            iconBg: "#6D4C41",
            iconStroke: "#F5F5F5",
            titleUnderline: "#6D4C41",
            avatarStroke: "#6D4C41",
            timesBoxBg: "rgba(209, 188, 175, 0.25)",
            timesBoxBorder: "#A1887F",
            background: "rgba(245,245,245,1)"
        },
        yellow: {
            fontTheme: "yellow",
            iconBg: "#F9A825",
            iconStroke: "#ffffff",
            titleUnderline: "#F9A825",
            avatarStroke: "#F9A825",
            timesBoxBg: "rgba(255, 224, 130, 0.25)",
            timesBoxBorder: "#FBC02D",
            background: "rgba(255,253,231,1)"
        },
        green: {
            fontTheme: "green",
            iconBg: "#2E7D32",
            iconStroke: "#ffffff",
            titleUnderline: "#2E7D32",
            avatarStroke: "#2E7D32",
            timesBoxBg: "rgba(200, 230, 201, 0.25)",
            timesBoxBorder: "#66BB6A",
            background: "rgba(241,248,233,1)"
        },
        red: {
            fontTheme: "red",
            iconBg: "#C62828",
            iconStroke: "#ffffff",
            titleUnderline: "#C62828",
            avatarStroke: "#C62828",
            timesBoxBg: "rgba(255, 205, 210, 0.25)",
            timesBoxBorder: "#EF5350",
            background: "rgba(255,235,238,1)"
        },
        blue: {
            fontTheme: "blue",
            iconBg: "#1E88E5",
            iconStroke: "#ffffff",
            titleUnderline: "#1E88E5",
            avatarStroke: "#1E88E5",
            timesBoxBg: "rgba(144, 202, 249, 0.25)",
            timesBoxBorder: "#64B5F6",
            background: "rgba(227,242,253,1)"
        },
        wave: {
            fontTheme: "wave",
            iconBg: "#60A5FA",                        // Hintergrund der runden Icons
            iconStroke: "#ffffff",                    // Farbe der Icons
            titleUnderline: "#3B82F6",                // Linie unter dem Namen
            avatarStroke: "#93C5FD",                  // Avatar-Rahmen
            timesBoxBg: "rgba(191,219,254,0.38)",     // Öffnungszeiten-Box Hintergrund
            timesBoxBorder: "#93C5FD",                // Öffnungszeiten-Box Rahmen
        },
        teal: {
            fontTheme: "teal",
            iconBg: "#E8FFEF",                        // Hintergrund der runden Icons
            iconStroke: "#4A7763",                    // Farbe der Icons
            titleUnderline: "#FFFFFF",                // Linie unter dem Namen
            avatarStroke: "rgba(255,255,255,0.35)",   // Avatar-Rahmen
            timesBoxBg: "rgba(232,255,239,0.16)",     // Öffnungszeiten-Box Hintergrund
            timesBoxBorder: "rgba(255,255,255,0.45)", // Öffnungszeiten-Box Rahmen
        }
    };
    
    const AVATAR = {
        aura: {
            inner: {color: "#6E463E", opacity: 0.22}, // stärkster innerer Aura-Kern
            mid:   {color: "#6E463E", opacity: 0.08}, // mittlere Aura
            outer: {color: "#6E463E", opacity: 0.00}  // äußerer Rand, auslaufend
        },
    
        glass: {
            core:  {color: "#ffffff", opacity: 1.00}, // hellster Kern
            glow1: {color: "#6E463E", opacity: 0.92}, // starker Glow
            glow2: {color: "#6E463E", opacity: 0.48}, // mittlerer Glow
            glow3: {color: "#6E463E", opacity: 0.12}, // schwacher Glow
            fade:  {color: "#6E463E", opacity: 0.00}  // auslaufender Rand
        },
    
        glowFactorSmall: 1.45,           // Glow‑Dunkelheits‑Faktor NUR für kleine Karte
        auraFactorSmall: 0.45,           // Aura der kleinen Karte abdunkeln
        ring: "rgba(255,255,255,0.30)",  // äußerer Ring
        background: "#0f2647"            // Avatar-Hintergrund
    };
    
    /*
    HINWEIS: Sobald das Script das erste mal gestartet wurde, gibt es im Hauptordner den Datenpunkt "json".
             Dieser beinhaltet 3 Sätze (Karten / Kontakte) mit Beispieldaten
    
            JSON‑Datensatz – Anforderungen (kurz & eindeutig)
    
            • Jeder Eintrag benötigt eine eindeutige ID und muss sich exakt an die Struktur der Beispieldaten halten.
            • Telefonnummern dürfen mehrere Werte enthalten, getrennt durch Komma oder Semikolon.
            • Öffnungszeiten müssen exakt im Format „Wochentag HH.MM – HH.MM[, HH.MM – HH.MM]“ stehen
            und dabei zwingend den echten EN‑DASH (–) verwenden — kein Minuszeichen (-).
            • Das Bildfeld („bild“) kann eine WWW‑URL oder eine ioBroker‑interne URL sein,
            z. B. http://192.168.10.99:8082/vis.0/kontakte/xyz.png
            und es werden PNG, JPG und SVG unterstützt.
    */
    
    //********** ENDE KONFIGURATION **********
    
    const ICON_LOCATION = `<path d="M21 10c0 6-9 13-9 13S3 16 3 10a9 9 0 1 1 18 0z"/><circle cx="12" cy="10" r="3"/>`;
    const ICON_PHONE    = `<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.12.89.32 1.76.59 2.59a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.49-1.16a2 2 0 0 1 2.11-.45c.83.27 1.7.47 2.59.59A2 2 0 0 1 22 16.92z"/>`;
    const ICON_LIST     = `<path d="M6 18h12"/><path d="M6 14h12"/><path d="M6 10h12"/><rect x="4" y="4" width="16" height="16" rx="2"/>`;
    const ICON_MAIL     = `<path d="M4 4h16v16H4z"/><path d="M22 6l-10 7L2 6"/>`;
    const ICON_GLOBE    = `<circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15 15 0 0 1 0 20"/><path d="M12 2a15 15 0 0 0 0 20"/>`;
    const ICON_CLOCK    = `<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>`;
    const ICON_FAX      = `<path d="M4 4h16v16H4z"/><path d="M8 2h8v4H8z"/><path d="M6 10h12"/><path d="M6 14h12"/>`;
    
    let TEXT_SHADOW_STYLE = "text-shadow:none;";
    
    function esc(str) {
        return String(str)
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;");
    }
    
    function setStateIfChanged(id, value, STOPPED = false) {
        if (STOPPED) return;
    
        const state = getState(id);
        if (!state) return;
    
        const old = state.val;
        const isObject = v => v !== null && typeof v === "object";
    
        if (isObject(value)) {
            const newStr = JSON.stringify(value);
            let oldStr = null;
    
            if (typeof old === "string") oldStr = old;
            else if (isObject(old)) oldStr = JSON.stringify(old);
    
            if (newStr === oldStr) return;
            return setState(id, newStr, true);
        }
    
        if (old === value) return;
        setState(id, value, true);
    }
    
    function fontStyle(F) {
        return `
            font-size:${F.size}px !important;
            font-weight:${F.weight} !important;
            font-style:${F.style} !important;
        `;
    }
    
    function fontCfg(theme, type) {
        return FONT[theme]?.[type] ?? FONT.standard[type];
    }
    
    function fontAttr(theme, type) {
        const f = fontCfg(theme, type);
    
        return {
            family: FONT.family, size: f.size, color: f.color, bold: f.bold, italic: f.italic, weight: f.bold ? FONT.weight.bold : FONT.weight.normal, style: f.italic ? "italic" : "normal"
        };
    }
    
    async function smartCreateState(id, value, options = {}) {
        if (existsState(id)) return;
        await createState(id, value, options);
    }
    
    function sleepMs(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    
    function base64Decode(str) {
        return Buffer.from(str, "base64").toString("utf8");
    }
    
    function decodeSampleData() {
        const decoded = base64Decode(SAMPLE_DATA_BASE64);
        return JSON.parse(decoded);
    }
    
    async function processJSON() {
        try {
            const raw = getState(JSON_DP).val;
            if (!raw) return;
    
            const data = JSON.parse(raw);
            if (!data.eintraege || !Array.isArray(data.eintraege)) return;
    
            for (const entry of data.eintraege) {
                const svg = buildSVG(entry);
                const dp = SVG_ROOT + entry.id;
    
                await smartCreateState(dp, "", { type: "string", name: "SVG Karte " + entry.id });
                await sleepMs(100);
    
                setStateIfChanged(dp, svg);
            }
        } catch (err) {
        }
    }
    
    function normalizeDays(str) {
        const map = {"Montag": "Mo", "Dienstag": "Di", "Mittwoch": "Mi", "Donnerstag": "Do", "Freitag": "Fr", "Samstag": "Sa", "Sonntag": "So"};
        const rangeRegex = /(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)\s*[–-]\s*(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)/g;
    
        str = str.replace(rangeRegex, (m, a, b) => `${map[a]}–${map[b]}`);
        for (const full in map) str = str.replace(new RegExp(full, "g"), map[full]);
        return str;
    }
    
    function compressTimes(times) {
        const full    = ["Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag","Sonntag"];
        const short   = ["Mo","Di","Mi","Do","Fr","Sa","So"];
        const special = {"Feiertage": "Feiertage", "Brückentage": "Brückentage"};
    
        const parsed = times.map(t => {
            const [day, ...rest] = t.split(" ");
            let time = rest.join(" ");
    
            time = time.replace(/–/g, "-");
    
            if (special[day]) {
                return { day, short: special[day], time, special: true };
            }
            return {day, short: short[full.indexOf(day)], time, special: false};
        });
    
        const groups = {};
        const specials = [];
    
        parsed.forEach(p => {
            if (p.special) {
                specials.push(`${p.short} ${p.time}`);
            } else {
                if (!groups[p.time]) groups[p.time] = [];
                groups[p.time].push(p.short);
            }
        });
    
        const result = [];
        const order = ["Mo","Di","Mi","Do","Fr","Sa","So"];
    
        for (const time in groups) {
            const list          = groups[time];
            const indices       = list.map(d => order.indexOf(d));
            const isConsecutive = indices.every((v, i, arr) => i === 0 || v === arr[i - 1] + 1);
    
            if (list.length > 1 && isConsecutive) {
                result.push(`${list[0]}–${list[list.length - 1]} ${time}`);
            } else {
                result.push(`${list.join(", ")} ${time}`);
            }
        }
        return result.concat(specials);
    }
    
    function splitTimeAtomic(row) {
        const idx = row.search(/\d/);
        if (idx === -1) return { day: row.trim(), parts: [] };
    
        const dayPart  = row.slice(0, idx).trim();
        const timePart = row.slice(idx).trim();
    
        const blocks = timePart.split(",");
        const parts = [];
    
        blocks.forEach(block => {
            const m = block.trim().match(/^(\S+)\s*[-–]\s*(\S+)$/);
            if (m) parts.push({ start: m[1], end: m[2] });
        });
    
        return { day: dayPart, parts };
    }
    
    function wrapText(text, maxLen = 32) {
        const words = text.split(" ");
        const lines = [];
        let current = "";
    
        for (const w of words) {
            if ((current + w).length > maxLen) {
                lines.push(current.trim());
                current = w + " ";
            } else {
                current += w + " ";
            }
        }
        if (current.trim().length > 0) lines.push(current.trim());
        return lines;
    }
    
    function buildSVG(entry) {
        const DESIGN_PRESET = getState(root+'theme').val;
    
        if (entry.karte === "small") {
            if (DESIGN_PRESET === "standard") return buildSmallCard(entry);
            if (DESIGN_PRESET === "teal")     return buildTealCardSmall(entry);
            if (DESIGN_PRESET === "wave")     return buildWaveCardSmall(entry);
    
            const theme = CARD_THEMES[DESIGN_PRESET] || CARD_THEMES.white;
            return buildUniColorCardSmall(entry, theme);
        }
    
        switch (DESIGN_PRESET) {
            case "teal":     return buildTealCard(entry);
            case "coffee":   return buildCoffeeCard(entry);
            case "wave":     return buildWaveCard(entry);
            case "white":    return buildWhiteCard(entry);
            case "red":      return buildRedCard(entry);
            case "green":    return buildGreenCard(entry);
            case "blue":     return buildBlueCard(entry);
            case "yellow":   return buildYellowCard(entry);
            case "standard":
            default:
                return buildLargeCard(entry);
        }
    }
    
    async function main() {
        await smartCreateState(JSON_DP, "", { type: "string", name: "Kontakte JSON" });
        await smartCreateState(root+'theme', "standard", {
            type: "mixed",
            read: true,
            write: true,
            name: "Card Theme",
            states: {
                "blue":     "Blau",
                "coffee":   "Coffee",
                "yellow":   "Gelb",
                "green":    "Grün",
                "red":      "Rot",
                "standard": "Standard",
                "teal":     "Teal",
                "wave":     "Welle",
                "white":    "Weiß"
            }
        });
        await smartCreateState(root+'textshadow', false, { type: "boolean", name: "Textschatten" });
        await smartCreateState(root+'opacity', 1, { type: "number", name: "Deckkraft" });
        await sleepMs(200);
    
        TEXT_SHADOW_STYLE = getState(root+'textshadow').val ? "text-shadow:3px 3px 4px #000;" : "text-shadow:none;";
    
        on({ id: JSON_DP, change: "any" }, async () => {
            await processJSON();
        });
        on({ id: root+'theme', change: "ne" }, async () => {
            await processJSON();
        });
        on({ id: root+'textshadow', change: "ne" }, async obj => {
            TEXT_SHADOW_STYLE = obj.state.val ? "text-shadow:3px 3px 4px #000;" : "text-shadow:none;";
            await processJSON();
        });
        on({ id: root+'opacity', change: "ne" }, async () => {
            await processJSON();
        });
    
        const current = getState(JSON_DP).val;
        if (!current || current.trim() === "") {
            const sample = decodeSampleData();
            setState(JSON_DP, JSON.stringify(sample, null, 2));
        }
        await processJSON();
    }
    main();
    
    function buildLargeCard(e) {
        const FONT_THEME    = "standard";
        const hasValidTimes = e.zeiten.some(z => z.trim() !== "");
        const zeiten        = hasValidTimes ? compressTimes(e.zeiten) : [];
        const hinweisLines  = e.hinweis ? wrapText(esc(e.hinweis), 45) : [];
        const uid           = `c${Math.random().toString(36).slice(2, 7)}_${e.id}`;
    
        const X = {day: 18, s1: 150, d1: 165, e1: 180, s2: 260, d2: 275, e2: 290};
    
        return `
            <svg viewBox="0 0 ${CARD_LARGE_W} ${CARD_LARGE_H}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
                <style>
                .txt {
                    ${TEXT_SHADOW_STYLE}
                    font-family:${FONT.family};
                    font-feature-settings:"tnum";
                    font-weight: normal;
                    font-style: normal;
                }
                </style>
    
                <defs>
                    <radialGradient id="glassGlow-${uid}" cx="50%" cy="50%" r="50%">
                        <stop offset="0%" stop-color="${AVATAR.glass.core.color}" stop-opacity="${AVATAR.glass.core.opacity}"/>
                        <stop offset="22%" stop-color="${AVATAR.glass.glow1.color}" stop-opacity="${AVATAR.glass.glow1.opacity}"/>
                        <stop offset="50%" stop-color="${AVATAR.glass.glow2.color}" stop-opacity="${AVATAR.glass.glow2.opacity}"/>
                        <stop offset="82%" stop-color="${AVATAR.glass.glow3.color}" stop-opacity="${AVATAR.glass.glow3.opacity}"/>
                        <stop offset="100%" stop-color="${AVATAR.glass.fade.color}" stop-opacity="${AVATAR.glass.fade.opacity}"/>
                    </radialGradient>
    
                    <radialGradient id="avatarAuraGlow-${uid}" cx="50%" cy="50%" r="50%">
                        <stop offset="0%" stop-color="${AVATAR.aura.inner.color}" stop-opacity="${AVATAR.aura.inner.opacity}"/>
                        <stop offset="60%" stop-color="${AVATAR.aura.mid.color}" stop-opacity="${AVATAR.aura.mid.opacity}"/>
                        <stop offset="100%" stop-color="${AVATAR.aura.outer.color}" stop-opacity="${AVATAR.aura.outer.opacity}"/>
                    </radialGradient>
    
                    <filter id="glassBloom-${uid}" x="-250%" y="-250%" width="600%" height="600%"><feGaussianBlur stdDeviation="12" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
                    <filter id="glassShadow-${uid}" x="-100%" y="-100%" width="300%" height="300%"><feDropShadow dx="0" dy="6" stdDeviation="9" flood-color="#000" flood-opacity="0.46"/></filter>
                    <mask id="avatarMask-${uid}"><circle cx="280" cy="115" r="40" fill="white"/></mask>
                </defs>
    
                <circle cx="280" cy="115" r="90" fill="url(#avatarAuraGlow-${uid})" filter="url(#glassBloom-${uid})" opacity="0.78"/>
                <circle cx="280" cy="115" r="62" fill="url(#glassGlow-${uid})" filter="url(#glassBloom-${uid})"/>
                <circle cx="280" cy="115" r="46" fill="none" stroke="${AVATAR.ring}" stroke-width="1.5"/>
                <ellipse cx="270" cy="102" rx="12" ry="4" fill="rgba(255,255,255,0.22)" transform="rotate(-25 270 102)"/>
                <circle cx="280" cy="115" r="42" fill="${AVATAR.background}" filter="url(#glassShadow-${uid})"/>
                <image href="${esc(e.bild)}" x="240" y="75" width="80" height="80" mask="url(#avatarMask-${uid})" preserveAspectRatio="xMidYMid slice"/>
    
                ${(() => {
                    const lines = [];
                    lines.push({text: esc(e.name), type: "name", y: 40});
                    lines.push({text: esc(e.adresse.strasse), type: "address", y: 75});
                    lines.push({text: `${esc(e.adresse.plz)} ${esc(e.adresse.ort)}`, type: "address", y: 93});
    
                    const tels = e.kontakt.telefon.split(/[,;]\s*/);
                    tels.forEach((tel, i) => lines.push({text: `Tel.: ${esc(tel)}`, type: "contact", y: 130 + i * 18}));
    
                    if (e.kontakt.fax)   lines.push({text: `Fax: ${esc(e.kontakt.fax)}`, type: "contact", y: 148});
                    if (e.kontakt.email) lines.push({text: esc(e.kontakt.email), type: "contact", y: 166});
                    if (e.kontakt.web)   lines.push({text: esc(e.kontakt.web), type: "web", y: 184});
    
                    lines.push({text: "Öffnungszeiten", type: "title", y: 225});
                    return lines.map(l => {
                        const F = fontAttr(FONT_THEME, l.type);
                        return `<text class="txt" x="18" y="${l.y}" fill="${F.color}" font-size="${F.size}" font-weight="${F.weight}" font-style="${F.style}" style="${fontStyle(F)}">${l.text}</text>`;
                    }).join("");
                })()}
    
                ${hasValidTimes ? zeiten.map((z,i)=>{
                    const {day,parts}=splitTimeAtomic(esc(z));
                    const b1=parts[0]||null, b2=parts[1]||null;
                    const F = fontAttr(FONT_THEME, "times");
                    return `<text class="txt" y="${250+i*20}" fill="${F.color}" font-size="${F.size}" font-weight="${F.weight}" font-style="${F.style}" style="${fontStyle(F)}"><tspan x="${X.day}">${esc(day)}</tspan>${b1?`<tspan x="${X.s1}" text-anchor="end">${esc(b1.start)}</tspan><tspan x="${X.d1}" text-anchor="middle">–</tspan><tspan x="${X.e1}" text-anchor="start">${esc(b1.end)}</tspan>`:""}${b2?`<tspan x="${X.s2}" text-anchor="end">${esc(b2.start)}</tspan><tspan x="${X.d2}" text-anchor="middle">–</tspan><tspan x="${X.e2}" text-anchor="start">${esc(b2.end)}</tspan>`:""}</text>`;
                }).join("") : (() => {
                    const F = fontAttr(FONT_THEME, "times");
                    return `<text class="txt" x="18" y="250" fill="${F.color}" font-size="${F.size}" font-weight="${F.weight}" font-style="${F.style}" style="${fontStyle(F)}">${esc(e.hinweis)}</text>`;
                })()}
    
                ${e.hinweis && hasValidTimes ? hinweisLines.map((line,i)=>{
                    const F = fontAttr(FONT_THEME, "hint");
                    return `<text class="txt" x="18" y="${250+zeiten.length*20+10+i*14}" fill="${F.color}" font-size="${F.size}" font-weight="${F.weight}" font-style="${F.style}" style="${fontStyle(F)}">${line}</text>`;
                }).join("") : ""}
            </svg>
        `.trim();
    }
    
    function buildSmallCard(e) {
        const FONT_THEME = "standard";
        const uid        = `c${Math.random().toString(36).slice(2, 7)}_${e.id}`;
        const gf         = AVATAR.glowFactorSmall;
        const af         = AVATAR.auraFactorSmall;
        const zeiten     = e.zeiten.map(z => normalizeDays(z));
    
        return `
            <svg viewBox="0 0 ${CARD_SMALL_W} ${CARD_SMALL_H}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
                <style>
                .txt {
                    ${TEXT_SHADOW_STYLE}
                    font-family:${FONT.family};
                    font-feature-settings:"tnum";
                    font-weight:normal;
                    font-style:normal;
                }
                </style>
    
                <defs>
                    <radialGradient id="avatarCoreGlow-${uid}" cx="50%" cy="50%" r="50%">
                        <stop offset="0%"  stop-color="${AVATAR.glass.core.color}" stop-opacity="${AVATAR.glass.core.opacity}"/>
                        <stop offset="22%" stop-color="${AVATAR.glass.glow1.color}" stop-opacity="${AVATAR.glass.glow1.opacity * gf}"/>
                        <stop offset="50%" stop-color="${AVATAR.glass.glow2.color}" stop-opacity="${AVATAR.glass.glow2.opacity * gf}"/>
                        <stop offset="82%" stop-color="${AVATAR.glass.glow3.color}" stop-opacity="${AVATAR.glass.glow3.opacity * gf}"/>
                        <stop offset="100%" stop-color="${AVATAR.glass.fade.color}" stop-opacity="${AVATAR.glass.fade.opacity * gf}"/>
                    </radialGradient>
    
                    <radialGradient id="avatarAuraGlow-${uid}" cx="50%" cy="50%" r="50%">
                        <stop offset="0%"   stop-color="${AVATAR.aura.inner.color}" stop-opacity="${AVATAR.aura.inner.opacity * af}"/>
                        <stop offset="60%"  stop-color="${AVATAR.aura.mid.color}"   stop-opacity="${AVATAR.aura.mid.opacity * af}"/>
                        <stop offset="100%" stop-color="${AVATAR.aura.outer.color}" stop-opacity="${AVATAR.aura.outer.opacity * af}"/>
                    </radialGradient>
    
                    <filter id="avatarBloom-${uid}" x="-250%" y="-250%" width="600%" height="600%"><feGaussianBlur stdDeviation="8" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
                    <filter id="avatarShadow-${uid}" x="-100%" y="-100%" width="300%" height="300%"><feDropShadow dx="0" dy="6" stdDeviation="9" flood-color="#000" flood-opacity="0.46"/></filter>
                    <clipPath id="avatarClipSmall-${uid}"><circle cx="300" cy="80" r="28"/></clipPath>
                </defs>
    
                <circle cx="300" cy="80" r="48" fill="url(#avatarAuraGlow-${uid})" filter="url(#avatarBloom-${uid})" opacity="0.88"/>
                <circle cx="300" cy="80" r="44" fill="url(#avatarCoreGlow-${uid})" filter="url(#avatarBloom-${uid})"/>
                <circle cx="300" cy="80" r="32" fill="none" stroke="${AVATAR.ring}" stroke-width="1.5"/>
                <ellipse cx="289" cy="67" rx="11" ry="4" fill="rgba(255,255,255,0.22)" transform="rotate(-25 289 67)"/>
                <circle cx="300" cy="80" r="22.4" fill="${AVATAR.background}" filter="url(#avatarShadow-${uid})"/>
                <image href="${esc(e.bild)}" x="272" y="52" width="56" height="56" clip-path="url(#avatarClipSmall-${uid})" preserveAspectRatio="xMidYMid slice"/>
    
                ${(() => {
                    const lines = [];
                    lines.push({text: esc(e.name), type: "name", y: 30});
                    lines.push({text: `Tel.: ${esc(e.kontakt.telefon)}`, type: "contact", y: 55});
                    lines.push({text: "Öffnungszeiten", type: "title", y: 78});
    
                    return lines.map(l => {
                        const F = fontAttr(FONT_THEME, l.type);
                        return `<text class="txt" x="18" y="${l.y}" fill="${F.color}" font-size="${F.size}" font-weight="${F.weight}" font-style="${F.style}" style="${fontStyle(F)}">${l.text}</text>`;
                    }).join("");
                })()}
    
                ${zeiten.map((z,i)=>{
                    const {day,parts}=splitTimeAtomic(esc(z));
                    const b1=parts[0]||null, b2=parts[1]||null;
                    const F = fontAttr(FONT_THEME, "times");
    
                    return `
                        <text class="txt"
                            y="${95+i*20}" fill="${F.color}" font-size="${F.size}" font-weight="${F.weight}" font-style="${F.style}" style="${fontStyle(F)}"><tspan x="18">${esc(day)}</tspan>
                            ${b1?`<tspan x="145" text-anchor="end">${esc(b1.start)}</tspan><tspan x="160" text-anchor="middle">–</tspan><tspan x="175" text-anchor="start">${esc(b1.end)}</tspan>`:""}
                            ${b2?`<tspan x="255" text-anchor="end">${esc(b2.start)}</tspan><tspan x="270" text-anchor="middle">–</tspan><tspan x="285" text-anchor="start">${esc(b2.end)}</tspan>`:""}
                        </text>`;
                }).join("")}
            </svg>
        `.trim();
    }
    
    function buildWaveCard(e) {
        const FONT_THEME    = "wave";
        const THEME         = CARD_THEMES.wave;
        const hasValidTimes = e.zeiten.some(z => z.trim() !== "");
        const zeiten        = hasValidTimes ? compressTimes(e.zeiten) : [];
        const hinweisLines  = e.hinweis ? wrapText(esc(e.hinweis), 45) : [];
        const tels          = e.kontakt.telefon ? e.kontakt.telefon.split(/[,;]\s*/) : [];
        const uid           = `c${Math.random().toString(36).slice(2, 7)}_${e.id}`;
        const lineHeight    = 15;
    
        const X = {day: 33, s1: 157, d1: 167, e1: 177, s2: 260, d2: 270, e2: 280};
    
        const timesBoxY      = 240;
        const timesBoxHeight = 110;
        const contactBlocks  = [];
        const CONTACT_Y      = [132, 163, 194];
    
        const iconWrap = (cx, cy, icon, scale = 0.55) => `
            <g transform="translate(${cx - 11}, ${cy - 11})">
                <circle cx="11" cy="11" r="11" fill="${THEME.iconBg}"/>
                <g transform="translate(${(11 - 11 * scale) - 0.7}, ${(11 - 11 * scale) - 0.4}) scale(${scale})"
                    stroke="${THEME.iconStroke}" stroke-width="2" fill="none">
                    ${icon}
                </g>
            </g>
        `;
    
        const opacity = getState(root+'opacity').val;
    
        if (tels.length) contactBlocks.push({ icon: ICON_PHONE, lines: [`Tel.: ${esc(tels[0])}`, ...tels.slice(1).map(t => esc(t))] });
        if (e.kontakt.fax) contactBlocks.push({ icon: ICON_FAX, text: `Fax: ${esc(e.kontakt.fax)}` });
        if (e.kontakt.email) contactBlocks.push({ icon: ICON_MAIL, text: esc(e.kontakt.email) });
    
        const F_NAME    = fontAttr(FONT_THEME, "name");
        const F_ADDRESS = fontAttr(FONT_THEME, "address");
        const F_CONTACT = fontAttr(FONT_THEME, "contact");
        const F_WEB     = fontAttr(FONT_THEME, "web");
        const F_TITLE   = fontAttr(FONT_THEME, "title");
        const F_TIMES   = fontAttr(FONT_THEME, "times");
        const F_HINT    = fontAttr(FONT_THEME, "hint");
    
        return `
            <svg viewBox="0 0 ${CARD_LARGE_W} ${CARD_LARGE_H}" xmlns="http://www.w3.org/2000/svg">
                <style>
                    .txt {
                        ${TEXT_SHADOW_STYLE}
                        font-family:${FONT.family};
                        font-feature-settings:"tnum";
                        font-weight:normal;
                        font-style:normal;
                    }
                </style>
    
                <defs>
                    <linearGradient id="bg-${uid}" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#f8fbff"/><stop offset="100%" stop-color="#dcecff"/></linearGradient>
                    <linearGradient id="header-${uid}" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#60a5fa"/><stop offset="100%" stop-color="#2563eb"/></linearGradient>
                    <clipPath id="avatarClip-${uid}"><rect x="260" y="65" width="65" height="65" rx="16"/></clipPath>
                </defs>
    
                <rect x="0" y="0" width="${CARD_LARGE_W}" height="${CARD_LARGE_H}" fill="url(#bg-${uid})" opacity="${opacity}"/>
                <path d="M0 24 C70 -4 140 44 220 24 C290 4 320 44 ${CARD_LARGE_W} 14 L${CARD_LARGE_W} 0 L0 0 Z" fill="url(#header-${uid})"/>
                <text class="txt" x="22" y="44" fill="${F_NAME.color}" font-size="${F_NAME.size}" font-weight="${F_NAME.weight}" font-style="${F_NAME.style}" style="${fontStyle(F_NAME)}">${esc(e.name)}</text>
                <rect x="22" y="56" width="40" height="4" rx="2" fill="${THEME.titleUnderline}"/>
                <rect x="255" y="60" width="75" height="75" rx="16" fill="#d9e8ff" stroke="${THEME.avatarStroke}"/>
                <image href="${esc(e.bild)}" x="260" y="65" width="65" height="65" preserveAspectRatio="xMidYMid slice" clip-path="url(#avatarClip-${uid})"/>
    
                ${iconWrap(32, 89, ICON_LOCATION)}
                <text class="txt" x="50" y="86" fill="${F_ADDRESS.color}" font-size="${F_ADDRESS.size}" font-weight="${F_ADDRESS.weight}" font-style="${F_ADDRESS.style}" style="${fontStyle(F_ADDRESS)}">${esc(e.adresse.strasse)}</text>
                <text class="txt" x="50" y="101" fill="${F_ADDRESS.color}" font-size="${F_ADDRESS.size}" font-weight="${F_ADDRESS.weight}" font-style="${F_ADDRESS.style}" style="${fontStyle(F_ADDRESS)}">${esc(e.adresse.plz)} ${esc(e.adresse.ort)}</text>
    
                ${contactBlocks.map((entry, i) => {
                    const y = CONTACT_Y[i];
                    if (!y) return "";
    
                    const lines = entry.lines || [entry.text];
                    return `
                        ${entry.icon ? iconWrap(32, y - 4, entry.icon) : ""}
                        ${lines.map((line, idx) => `<text class="txt" x="50" y="${y + idx * 14}" fill="${F_CONTACT.color}" font-size="${F_CONTACT.size}" font-weight="${F_CONTACT.weight}" font-style="${F_CONTACT.style}" style="${fontStyle(F_CONTACT)}">${esc(line)}</text>`).join("")}
                    `;
                }).join("")}
    
                ${e.kontakt.web ? `${iconWrap(32, 221, ICON_GLOBE)}<text class="txt" x="50" y="225" fill="${F_WEB.color}" font-size="${F_WEB.size}" font-weight="${F_WEB.weight}" font-style="${F_WEB.style}" style="${fontStyle(F_WEB)}">${esc(e.kontakt.web)}</text>` : ""}
    
                <rect x="18" y="${timesBoxY}" width="314" height="${timesBoxHeight}" rx="12" fill="${THEME.timesBoxBg}" stroke="${THEME.timesBoxBorder}"/>
                ${iconWrap(38, timesBoxY + 21, ICON_CLOCK)}
    
                <text class="txt" x="58" y="${timesBoxY + 25}" fill="${F_TITLE.color}" font-size="${F_TITLE.size}" font-weight="${F_TITLE.weight}" font-style="${F_TITLE.style}" style="${fontStyle(F_TITLE)}">Öffnungszeiten</text>
                ${zeiten.map((z, i) => {
                    const { day, parts } = splitTimeAtomic(esc(z));
                    const b1 = parts[0] || null;
                    const b2 = parts[1] || null;
    
                    return `
                    <text class="txt"
                        y="${timesBoxY + 48 + i * lineHeight}" fill="${F_TIMES.color}" font-size="${F_TIMES.size}" font-weight="${F_TIMES.weight}" font-style="${F_TIMES.style}" style="${fontStyle(F_TIMES)}"><tspan x="${X.day}">${esc(day)}</tspan>
                        ${b1 ? `<tspan x="${X.s1}" text-anchor="end">${esc(b1.start)}</tspan><tspan x="${X.d1}" text-anchor="middle">–</tspan><tspan x="${X.e1}" text-anchor="start">${esc(b1.end)}</tspan>` : ""}
                        ${b2 ? `<tspan x="${X.s2}" text-anchor="end">${esc(b2.start)}</tspan><tspan x="${X.d2}" text-anchor="middle">–</tspan><tspan x="${X.e2}" text-anchor="start">${esc(b2.end)}</tspan>` : ""}
                    </text>`;
                }).join("")}
                ${e.hinweis && hasValidTimes ? hinweisLines.map((line,i)=>`<text class="txt" x="33" y="${277 + zeiten.length * 20 + 10 + i * 14}" fill="${F_HINT.color}" font-size="${F_HINT.size}" font-weight="${F_HINT.weight}" font-style="${F_HINT.style}" style="${fontStyle(F_HINT)}">${line}</text>`).join("") : ""}
            </svg>
        `.trim();
    }
    
    function buildWaveCardSmall(e) {
        const FONT_THEME = "wave";
        const THEME      = CARD_THEMES.wave;
        const uid        = `c${Math.random().toString(36).slice(2, 7)}_${e.id}`;
        const zeiten     = e.zeiten.map(z => normalizeDays(z));
        const opacity    = getState(root+'opacity').val;
    
        const X = { day: 18, s1: 145, d1: 160, e1: 175, s2: 255, d2: 270, e2: 285 };
    
        const F_NAME    = fontAttr(FONT_THEME, "name");
        const F_CONTACT = fontAttr(FONT_THEME, "contact");
        const F_TITLE   = fontAttr(FONT_THEME, "title");
        const F_TIMES   = fontAttr(FONT_THEME, "times");
    
        return `
            <svg viewBox="0 0 ${CARD_SMALL_W} ${CARD_SMALL_H}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
                <style>
                    .txt {
                        ${TEXT_SHADOW_STYLE}
                        font-family:${FONT.family};
                        font-feature-settings:"tnum";
                        font-weight:normal;
                        font-style:normal;
                    }
                </style>
    
                <defs>
                    <linearGradient id="bg-${uid}" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#f8fbff"/><stop offset="100%" stop-color="#dcecff"/></linearGradient>
                    <linearGradient id="header-${uid}" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#60a5fa"/><stop offset="100%" stop-color="#2563eb"/></linearGradient>
                    <clipPath id="avatarClipSmall-${uid}"><circle cx="300" cy="80" r="28"/></clipPath>
                    <filter id="avatarShadow-${uid}" x="-100%" y="-100%" width="300%" height="300%"><feDropShadow dx="0" dy="6" stdDeviation="9" flood-color="#000" flood-opacity="0.46"/></filter>
                </defs>
    
                <rect x="0" y="0" width="${CARD_SMALL_W}" height="${CARD_SMALL_H}" fill="url(#bg-${uid})" opacity="${opacity}"/>
                <path d="M0 24 C70 -4 140 44 220 24 C290 4 320 44 ${CARD_SMALL_W} 14 L${CARD_SMALL_W} 0 L0 0 Z" fill="url(#header-${uid})"/>
                <circle cx="300" cy="80" r="32" fill="#d9e8ff" stroke="${THEME.avatarStroke}" stroke-width="1.5"/>
                <circle cx="300" cy="80" r="22.4" fill="#d9e8ff" filter="url(#avatarShadow-${uid})"/>
                <image href="${esc(e.bild)}" x="272" y="52" width="56" height="56" clip-path="url(#avatarClipSmall-${uid})" preserveAspectRatio="xMidYMid slice"/>
                <text class="txt" x="18" y="30" fill="${F_NAME.color}" font-size="${F_NAME.size}" font-weight="${F_NAME.weight}" font-style="${F_NAME.style}" style="${fontStyle(F_NAME)}">${esc(e.name)}</text>
                <text class="txt" x="18" y="55" fill="${F_CONTACT.color}" font-size="${F_CONTACT.size}" font-weight="${F_CONTACT.weight}" font-style="${F_CONTACT.style}" style="${fontStyle(F_CONTACT)}">Tel.: ${esc(e.kontakt.telefon)}</text>
                <text class="txt" x="18" y="78" fill="${F_TITLE.color}" font-size="${F_TITLE.size}" font-weight="${F_TITLE.weight}" font-style="${F_TITLE.style}" style="${fontStyle(F_TITLE)}">Öffnungszeiten</text>
    
                ${zeiten.map((z,i)=>{
                    const {day,parts}=splitTimeAtomic(esc(z));
                    const b1=parts[0]||null;
                    const b2=parts[1]||null;
    
                    return `
                        <text class="txt"
                            y="${95+i*20}" fill="${F_TIMES.color}" font-size="${F_TIMES.size}" font-weight="${F_TIMES.weight}" font-style="${F_TIMES.style}" style="${fontStyle(F_TIMES)}"><tspan x="${X.day}">${esc(day)}</tspan>
                            ${b1 ? `<tspan x="${X.s1}" text-anchor="end">${esc(b1.start)}</tspan><tspan x="${X.d1}" text-anchor="middle">–</tspan><tspan x="${X.e1}" text-anchor="start">${esc(b1.end)}</tspan>` : ""}
                            ${b2 ? `<tspan x="${X.s2}" text-anchor="end">${esc(b2.start)}</tspan><tspan x="${X.d2}" text-anchor="middle">–</tspan><tspan x="${X.e2}" text-anchor="start">${esc(b2.end)}</tspan>` : ""}
                        </text>
                    `;
                }).join("")}
            </svg>
        `.trim();
    }
    
    function buildTealCard(e) {
        const FONT_THEME    = "teal";
        const THEME         = CARD_THEMES.teal;
        const hasValidTimes = e.zeiten.some(z => z.trim() !== "");
        const zeiten        = hasValidTimes ? compressTimes(e.zeiten) : [];
        const hinweisLines  = e.hinweis ? wrapText(esc(e.hinweis), 45) : [];
        const tels          = e.kontakt.telefon ? e.kontakt.telefon.split(/[,;]\s*/) : [];
        const uid           = `c${Math.random().toString(36).slice(2, 7)}_${e.id}`;
        const lineHeight    = 15;
    
        const X = {day: 33, s1: 157, d1: 167, e1: 177, s2: 260, d2: 270, e2: 280};
    
        const timesBoxY      = 240;
        const timesBoxHeight = 110;
        const contactBlocks  = [];
        const CONTACT_Y      = [132, 163, 194];
    
        const iconWrap = (cx, cy, icon, scale = 0.55) => `
            <g transform="translate(${cx - 11}, ${cy - 11})">
                <circle cx="11" cy="11" r="11" fill="${THEME.iconBg}"/>
                <g transform="translate(${(11 - 11 * scale) - 0.7}, ${(11 - 11 * scale) - 0.4}) scale(${scale})"
                    stroke="${THEME.iconStroke}" stroke-width="2" fill="none">
                    ${icon}
                </g>
            </g>
        `;
    
        if (tels.length) contactBlocks.push({ icon: ICON_PHONE, lines: [`Tel.: ${esc(tels[0])}`, ...tels.slice(1).map(t => esc(t))] });
        if (e.kontakt.fax) contactBlocks.push({ icon: ICON_FAX,  text: `Fax: ${esc(e.kontakt.fax)}` });
        if (e.kontakt.email) contactBlocks.push({ icon: ICON_MAIL, text: esc(e.kontakt.email) });
    
        const opacity = getState(root+'opacity').val;
    
        const F_NAME    = fontAttr(FONT_THEME, "name");
        const F_ADDRESS = fontAttr(FONT_THEME, "address");
        const F_CONTACT = fontAttr(FONT_THEME, "contact");
        const F_WEB     = fontAttr(FONT_THEME, "web");
        const F_TITLE   = fontAttr(FONT_THEME, "title");
        const F_TIMES   = fontAttr(FONT_THEME, "times");
        const F_HINT    = fontAttr(FONT_THEME, "hint");
    
        return `
            <svg viewBox="0 0 ${CARD_LARGE_W} ${CARD_LARGE_H}" xmlns="http://www.w3.org/2000/svg">
                <style>
                    .txt {
                        ${TEXT_SHADOW_STYLE}
                        font-family:${FONT.family};
                        font-feature-settings:"tnum";
                        font-weight:normal;
                        font-style:normal;
                    }
                </style>
    
                <defs>
                    <linearGradient id="bg-${uid}" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#005a70"/><stop offset="100%" stop-color="#87c3ab"/></linearGradient>
                    <clipPath id="avatarClip-${uid}"><rect x="260" y="65" width="65" height="65" rx="14"/></clipPath>
                    <filter id="blurBG" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="8"/></filter>
                </defs>
    
                <rect x="0" y="0" width="${CARD_LARGE_W}" height="${CARD_LARGE_H}" fill="url(#bg-${uid})" opacity="${opacity}"/>
                <text class="txt" x="22" y="44" fill="${F_NAME.color}" font-size="${F_NAME.size}" font-weight="${F_NAME.weight}" font-style="${F_NAME.style}" style="${fontStyle(F_NAME)}">${esc(e.name)}</text>
                <rect x="22" y="56" width="40" height="4" rx="2" fill="${THEME.titleUnderline}" opacity="0.8"/>
                <rect x="255" y="60" width="75" height="75" rx="16" fill="rgba(255,255,255,0.12)" stroke="${THEME.avatarStroke}"/>
                <image href="${esc(e.bild)}" x="260" y="65" width="65" height="65" clip-path="url(#avatarClip-${uid})" preserveAspectRatio="xMidYMid slice"/>
    
                ${iconWrap(32, 89, ICON_LOCATION)}
                <text class="txt" x="50" y="86" fill="${F_ADDRESS.color}" font-size="${F_ADDRESS.size}" font-weight="${F_ADDRESS.weight}" font-style="${F_ADDRESS.style}" style="${fontStyle(F_ADDRESS)}">${esc(e.adresse.strasse)}</text>
                <text class="txt" x="50" y="101" fill="${F_ADDRESS.color}" font-size="${F_ADDRESS.size}" font-weight="${F_ADDRESS.weight}" font-style="${F_ADDRESS.style}" style="${fontStyle(F_ADDRESS)}">${esc(e.adresse.plz)} ${esc(e.adresse.ort)}</text>
    
                ${contactBlocks.map((entry, i) => {
                    const y = CONTACT_Y[i];
                    if (!y) return "";
    
                    const lines = entry.lines || [entry.text];
                    return `
                        ${entry.icon ? iconWrap(32, y - 4, entry.icon) : ""}
                        ${lines.map((line,idx)=>`<text class="txt" x="50" y="${y + idx * 14}" fill="${F_CONTACT.color}" font-size="${F_CONTACT.size}" font-weight="${F_CONTACT.weight}" font-style="${F_CONTACT.style}" style="${fontStyle(F_CONTACT)}">${esc(line)}</text>`).join("")}
                    `;
                }).join("")}
    
                ${e.kontakt.web ? `${iconWrap(32, 221, ICON_GLOBE)}<text class="txt" x="50" y="225" fill="${F_WEB.color}" font-size="${F_WEB.size}" font-weight="${F_WEB.weight}" font-style="${F_WEB.style}" style="${fontStyle(F_WEB)}">${esc(e.kontakt.web)}</text>` : ""}
                <g>
                    <rect x="18" y="${timesBoxY}" width="314" height="${timesBoxHeight}" rx="12" fill="${THEME.timesBoxBg}" filter="url(#blurBG)"/>
                    <rect x="18" y="${timesBoxY}" width="314" height="${timesBoxHeight}" rx="12" fill="none" stroke="${THEME.timesBoxBorder}" stroke-width="2"/>
                </g>
    
                ${iconWrap(38, timesBoxY + 21, ICON_CLOCK)}
                <text class="txt" x="58" y="${timesBoxY + 25}" fill="${F_TITLE.color}" font-size="${F_TITLE.size}" font-weight="${F_TITLE.weight}" font-style="${F_TITLE.style}" style="${fontStyle(F_TITLE)}">Öffnungszeiten</text>
    
                ${zeiten.map((z, i) => {
                    const { day, parts } = splitTimeAtomic(esc(z));
                    const b1 = parts[0] || null;
                    const b2 = parts[1] || null;
    
                    return `
                    <text class="txt"
                        y="${timesBoxY + 48 + i * lineHeight}" fill="${F_TIMES.color}" font-size="${F_TIMES.size}" font-weight="${F_TIMES.weight}" font-style="${F_TIMES.style}" style="${fontStyle(F_TIMES)}"><tspan x="${X.day}">${esc(day)}</tspan>
                        ${b1 ? `<tspan x="${X.s1}" text-anchor="end">${esc(b1.start)}</tspan><tspan x="${X.d1}" text-anchor="middle">–</tspan><tspan x="${X.e1}" text-anchor="start">${esc(b1.end)}</tspan>` : ""}
                        ${b2 ? `<tspan x="${X.s2}" text-anchor="end">${esc(b2.start)}</tspan><tspan x="${X.d2}" text-anchor="middle">–</tspan><tspan x="${X.e2}" text-anchor="start">${esc(b2.end)}</tspan>` : ""}
                    </text>`;
                }).join("")}
                ${e.hinweis && hasValidTimes ? hinweisLines.map((line,i)=>`<text class="txt" x="33" y="${277 + zeiten.length * 20 + 10 + i * 14}" fill="${F_HINT.color}" font-size="${F_HINT.size}" font-weight="${F_HINT.weight}" font-style="${F_HINT.style}" style="${fontStyle(F_HINT)}">${line}</text>`).join("") : ""}
            </svg>
        `.trim();
    }
    
    function buildTealCardSmall(e) {
        const FONT_THEME = "teal";
        const THEME      = CARD_THEMES.teal;
        const zeiten     = e.zeiten.map(z => normalizeDays(z));
        const uid        = `c${Math.random().toString(36).slice(2, 7)}_${e.id}`;
    
        const X = { day: 18, s1: 145, d1: 160, e1: 175, s2: 255, d2: 270, e2: 285 };
    
        const opacity = getState(root+'opacity').val;
    
        const F_NAME    = fontAttr(FONT_THEME, "name");
        const F_CONTACT = fontAttr(FONT_THEME, "contact");
        const F_TITLE   = fontAttr(FONT_THEME, "title");
        const F_TIMES   = fontAttr(FONT_THEME, "times");
    
        return `
            <svg viewBox="0 0 ${CARD_SMALL_W} ${CARD_SMALL_H}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
                <style>
                    .txt {
                        ${TEXT_SHADOW_STYLE}
                        font-family:${FONT.family};
                        font-feature-settings:"tnum";
                        font-weight:normal;
                        font-style:normal;
                    }
                </style>
    
                <defs>
                    <linearGradient id="bg-${uid}" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#005a70"/><stop offset="100%" stop-color="#87c3ab"/></linearGradient>
                    <clipPath id="avatarClipSmall-${uid}"><circle cx="300" cy="80" r="28"/></clipPath>
                    <filter id="avatarShadow-${uid}" x="-100%" y="-100%" width="300%" height="300%"><feDropShadow dx="0" dy="6" stdDeviation="9" flood-color="#000" flood-opacity="0.46"/></filter>
                </defs>
    
                <rect x="0" y="0" width="${CARD_SMALL_W}" height="${CARD_SMALL_H}" fill="url(#bg-${uid})" opacity="${opacity}"/>
                <circle cx="300" cy="80" r="32" fill="rgba(255,255,255,0.12)" stroke="${THEME.avatarStroke}" stroke-width="1.5"/>
                <circle cx="300" cy="80" r="22.4" fill="#0f2647" filter="url(#avatarShadow-${uid})"/>
                <image href="${esc(e.bild)}" x="272" y="52" width="56" height="56" clip-path="url(#avatarClipSmall-${uid})" preserveAspectRatio="xMidYMid slice"/>
                <text class="txt" x="18" y="30" fill="${F_NAME.color}" font-size="${F_NAME.size}" font-weight="${F_NAME.weight}" font-style="${F_NAME.style}" style="${fontStyle(F_NAME)}">${esc(e.name)}</text>
                <text class="txt" x="18" y="55" fill="${F_CONTACT.color}" font-size="${F_CONTACT.size}" font-weight="${F_CONTACT.weight}" font-style="${F_CONTACT.style}" style="${fontStyle(F_CONTACT)}">Tel.: ${esc(e.kontakt.telefon)}</text>
                <text class="txt" x="18" y="78" fill="${F_TITLE.color}" font-size="${F_TITLE.size}" font-weight="${F_TITLE.weight}" font-style="${F_TITLE.style}" style="${fontStyle(F_TITLE)}">Öffnungszeiten</text>
                ${zeiten.map((z,i)=>{
                    const {day,parts}=splitTimeAtomic(esc(z));
                    const b1=parts[0]||null;
                    const b2=parts[1]||null;
    
                    return `
                        <text class="txt"
                            y="${95+i*20}" fill="${F_TIMES.color}" font-size="${F_TIMES.size}" font-weight="${F_TIMES.weight}" font-style="${F_TIMES.style}" style="${fontStyle(F_TIMES)}"><tspan x="${X.day}">${esc(day)}</tspan>
                            ${b1 ? `<tspan x="${X.s1}" text-anchor="end">${esc(b1.start)}</tspan><tspan x="${X.d1}" text-anchor="middle">–</tspan><tspan x="${X.e1}" text-anchor="start">${esc(b1.end)}</tspan>` : ""}
                            ${b2 ? `<tspan x="${X.s2}" text-anchor="end">${esc(b2.start)}</tspan><tspan x="${X.d2}" text-anchor="middle">–</tspan><tspan x="${X.e2}" text-anchor="start">${esc(b2.end)}</tspan>` : ""}
                        </text>
                    `;
                }).join("")}
            </svg>
        `.trim();
    }
    
    function buildWhiteCard(e) {
        return buildUniColorCard(e, CARD_THEMES.white);
    }
    
    function buildCoffeeCard(e) {
        return buildUniColorCard(e, CARD_THEMES.coffee);
    }
    
    function buildYellowCard(e) {
        return buildUniColorCard(e, CARD_THEMES.yellow);
    }
    
    function buildGreenCard(e) {
        return buildUniColorCard(e, CARD_THEMES.green);
    }
    
    function buildRedCard(e) {
        return buildUniColorCard(e, CARD_THEMES.red);
    }
    
    function buildBlueCard(e) {
        return buildUniColorCard(e, CARD_THEMES.blue);
    }
    
    function buildUniColorCard(e, theme) {
        const FONT_THEME    = theme.fontTheme;
        const hasValidTimes = e.zeiten.some(z => z.trim() !== "");
        const zeiten        = hasValidTimes ? compressTimes(e.zeiten) : [];
        const hinweisLines  = e.hinweis ? wrapText(esc(e.hinweis), 45) : [];
        const tels          = e.kontakt.telefon ? e.kontakt.telefon.split(/[,;]\s*/) : [];
        const uid           = `c${Math.random().toString(36).slice(2, 7)}_${e.id}`;
        const lineHeight    = 15;
    
        const X = {day: 33, s1: 157, d1: 167, e1: 177, s2: 260, d2: 270, e2: 280};
    
        const timesBoxY      = 240;
        const timesBoxHeight = 110;
        const contactBlocks  = [];
        const CONTACT_Y      = [132, 163, 194];
    
        const F_NAME    = fontAttr(FONT_THEME, "name");
        const F_ADDRESS = fontAttr(FONT_THEME, "address");
        const F_CONTACT = fontAttr(FONT_THEME, "contact");
        const F_WEB     = fontAttr(FONT_THEME, "web");
        const F_TITLE   = fontAttr(FONT_THEME, "title");
        const F_TIMES   = fontAttr(FONT_THEME, "times");
        const F_HINT    = fontAttr(FONT_THEME, "hint");
    
        const iconWrap = (cx, cy, icon, scale = 0.55) => `
            <g transform="translate(${cx - 11}, ${cy - 11})">
                <circle cx="11" cy="11" r="11" fill="${theme.iconBg}"/>
                <g transform="translate(${(11 - 11 * scale) - 0.7}, ${(11 - 11 * scale) - 0.4}) scale(${scale})"
                    stroke="${theme.iconStroke}" stroke-width="2" fill="none">
                    ${icon}
                </g>
            </g>
        `;
    
        const opacity = getState(root+'opacity').val;
    
        if (tels.length)     contactBlocks.push({ icon: ICON_PHONE, lines: [`Tel.: ${esc(tels[0])}`, ...tels.slice(1).map(t => esc(t))] });
        if (e.kontakt.fax)   contactBlocks.push({ icon: ICON_FAX,  text: `Fax: ${esc(e.kontakt.fax)}` });
        if (e.kontakt.email) contactBlocks.push({ icon: ICON_MAIL, text: esc(e.kontakt.email) });
    
        return `
            <svg viewBox="0 0 ${CARD_LARGE_W} ${CARD_LARGE_H}" xmlns="http://www.w3.org/2000/svg">
                <style>
                    .txt {
                        ${TEXT_SHADOW_STYLE}
                        font-family:${FONT.family};
                        font-feature-settings:"tnum";
                        font-weight:normal;
                        font-style:normal;
                    }
                </style>
    
                <defs>
                    <clipPath id="avatarClip-${uid}"><circle cx="292" cy="98" r="38"/></clipPath>
                    <filter id="blurUni-${uid}" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="2"/></filter>
                </defs>
                <rect x="0" y="0" width="${CARD_LARGE_W}" height="${CARD_LARGE_H}" fill="${theme.background}" opacity="${opacity}"/>
                <text class="txt" x="22" y="44" fill="${F_NAME.color}" font-size="${F_NAME.size}" font-weight="${F_NAME.weight}" font-style="${F_NAME.style}" style="${fontStyle(F_NAME)}">${esc(e.name)}</text>
                <rect x="22" y="56" width="40" height="4" rx="2" fill="${theme.titleUnderline}" opacity="0.9"/>
                <circle cx="292" cy="98" r="42" fill="none" stroke="${theme.avatarStroke}" stroke-width="2"/>
                <image href="${esc(e.bild)}" x="250" y="56" width="84" height="84" preserveAspectRatio="xMidYMid slice" clip-path="url(#avatarClip-${uid})"/>
    
                ${iconWrap(32, 89, ICON_LOCATION)}
                <text class="txt" x="50" y="86" fill="${F_ADDRESS.color}" font-size="${F_ADDRESS.size}" font-weight="${F_ADDRESS.weight}" font-style="${F_ADDRESS.style}" style="${fontStyle(F_ADDRESS)}">${esc(e.adresse.strasse)}</text>
                <text class="txt" x="50" y="101" fill="${F_ADDRESS.color}" font-size="${F_ADDRESS.size}" font-weight="${F_ADDRESS.weight}" font-style="${F_ADDRESS.style}" style="${fontStyle(F_ADDRESS)}">${esc(e.adresse.plz)} ${esc(e.adresse.ort)}</text>
    
                ${contactBlocks.map((entry, i) => {
                    const y = CONTACT_Y[i];
                    if (!y) return "";
    
                    const lines = entry.lines || [entry.text];
                    return `
                        ${entry.icon ? iconWrap(32, y - 4, entry.icon) : ""}
                        ${lines.map((line, idx) => `<text class="txt" x="50" y="${y + idx * 14}" fill="${F_CONTACT.color}" font-size="${F_CONTACT.size}" font-weight="${F_CONTACT.weight}" font-style="${F_CONTACT.style}" style="${fontStyle(F_CONTACT)}">${esc(line)}</text>`).join("")}
                    `;
                }).join("")}
    
                ${e.kontakt.web ? `${iconWrap(32, 221, ICON_GLOBE)}<text class="txt" x="50" y="225" fill="${F_WEB.color}" font-size="${F_WEB.size}" font-weight="${F_WEB.weight}" font-style="${F_WEB.style}" style="${fontStyle(F_WEB)}">${esc(e.kontakt.web)}</text>` : ""}
                <g>
                    <rect x="18" y="${timesBoxY}" width="314" height="${timesBoxHeight}" rx="12" fill="${theme.timesBoxBg}" filter="url(#blurUni-${uid})"/>
                    <rect x="18" y="${timesBoxY}" width="314" height="${timesBoxHeight}" rx="12" fill="none" stroke="${theme.timesBoxBorder}" stroke-width="2"/>
                </g>
    
                ${iconWrap(38, timesBoxY + 21, ICON_CLOCK)}
                <text class="txt" x="58" y="${timesBoxY + 25}" fill="${F_TITLE.color}" font-size="${F_TITLE.size}" font-weight="${F_TITLE.weight}" font-style="${F_TITLE.style}" style="${fontStyle(F_TITLE)}">Öffnungszeiten</text>
    
                ${zeiten.map((z, i) => {
                    const { day, parts } = splitTimeAtomic(esc(z));
                    const b1 = parts[0] || null;
                    const b2 = parts[1] || null;
    
                    return `
                    <text class="txt"
                        y="${timesBoxY + 48 + i * lineHeight}" fill="${F_TIMES.color}" font-size="${F_TIMES.size}" font-weight="${F_TIMES.weight}" font-style="${F_TIMES.style}" style="${fontStyle(F_TIMES)}"><tspan x="${X.day}">${esc(day)}</tspan>
                        ${b1 ? `<tspan x="${X.s1}" text-anchor="end">${esc(b1.start)}</tspan><tspan x="${X.d1}" text-anchor="middle">–</tspan><tspan x="${X.e1}" text-anchor="start">${esc(b1.end)}</tspan>` : ""}
                        ${b2 ? `<tspan x="${X.s2}" text-anchor="end">${esc(b2.start)}</tspan><tspan x="${X.d2}" text-anchor="middle">–</tspan><tspan x="${X.e2}" text-anchor="start">${esc(b2.end)}</tspan>` : ""}
                    </text>`;
                }).join("")}
    
                ${e.hinweis && hasValidTimes ? hinweisLines.map((line, i) => `<text class="txt" x="33" y="${277 + zeiten.length * 20 + 10 + i * 14}" fill="${F_HINT.color}" font-size="${F_HINT.size}" font-weight="${F_HINT.weight}" font-style="${F_HINT.style}" style="${fontStyle(F_HINT)}">${line}</text>`).join("") : ""}
            </svg>
        `.trim();
    }
    
    function buildUniColorCardSmall(e, theme) {
        const FONT_THEME = theme.fontTheme;
        const uid        = `c${Math.random().toString(36).slice(2, 7)}_${e.id}`;
        const zeiten     = e.zeiten.map(z => normalizeDays(z));
    
        const X = { day: 18, s1: 145, d1: 160, e1: 175, s2: 255, d2: 270, e2: 285 };
    
        const opacity = getState(root+'opacity').val;
    
        const F_NAME    = fontAttr(FONT_THEME, "name");
        const F_CONTACT = fontAttr(FONT_THEME, "contact");
        const F_TITLE   = fontAttr(FONT_THEME, "title");
        const F_TIMES   = fontAttr(FONT_THEME, "times");
    
        return `
            <svg viewBox="0 0 ${CARD_SMALL_W} ${CARD_SMALL_H}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
                <style>
                    .txt {
                        ${TEXT_SHADOW_STYLE}
                        font-family:${FONT.family};
                        font-feature-settings:"tnum";
                        font-weight:normal;
                        font-style:normal;
                    }
                </style>
    
                <rect x="0" y="0" width="${CARD_SMALL_W}" height="${CARD_SMALL_H}" fill="${theme.background}" opacity="${opacity}"/>
                <defs>
                    <clipPath id="avatarClipSmall-${uid}"><circle cx="300" cy="80" r="28"/></clipPath>
                </defs>
    
                <circle cx="300" cy="80" r="32" fill="none" stroke="${theme.avatarStroke}" stroke-width="1.5"/>
                <circle cx="300" cy="80" r="22.4" fill="${AVATAR.background}"/>
                <image href="${esc(e.bild)}" x="272" y="52" width="56" height="56" clip-path="url(#avatarClipSmall-${uid})" preserveAspectRatio="xMidYMid slice"/>
    
                <text class="txt" x="18" y="30" fill="${F_NAME.color}" font-size="${F_NAME.size}" font-weight="${F_NAME.weight}" font-style="${F_NAME.style}" style="${fontStyle(F_NAME)}">${esc(e.name)}</text>
                <text class="txt" x="18" y="55" fill="${F_CONTACT.color}" font-size="${F_CONTACT.size}" font-weight="${F_CONTACT.weight}" font-style="${F_CONTACT.style}" style="${fontStyle(F_CONTACT)}">Tel.: ${esc(e.kontakt.telefon)}</text>
                <text class="txt" x="18" y="78" fill="${F_TITLE.color}" font-size="${F_TITLE.size}" font-weight="${F_TITLE.weight}" font-style="${F_TITLE.style}" style="${fontStyle(F_TITLE)}">Öffnungszeiten</text>
    
                ${zeiten.map((z,i)=>{
                    const {day,parts}=splitTimeAtomic(esc(z));
                    const b1=parts[0]||null, b2=parts[1]||null;
    
                    return `
                        <text class="txt"
                            y="${95+i*20}" fill="${F_TIMES.color}" font-size="${F_TIMES.size}" font-weight="${F_TIMES.weight}" font-style="${F_TIMES.style}" style="${fontStyle(F_TIMES)}"><tspan x="${X.day}">${esc(day)}</tspan>
                            ${b1?`<tspan x="${X.s1}" text-anchor="end">${esc(b1.start)}</tspan><tspan x="${X.d1}" text-anchor="middle">–</tspan><tspan x="${X.e1}" text-anchor="start">${esc(b1.end)}</tspan>`:""}
                            ${b2?`<tspan x="${X.s2}" text-anchor="end">${esc(b2.start)}</tspan><tspan x="${X.d2}" text-anchor="middle">–</tspan><tspan x="${X.e2}" text-anchor="start">${esc(b2.end)}</tspan>`:""}
                        </text>
                    `;
                }).join("")}
            </svg>
        `.trim();
    }
    
    const SAMPLE_DATA_BASE64 =
    `ewogICJlaW50cmFlZ2UiOiBbCiAgICB7CiAgICAgICJpZCI6IDEsCiAgICAgICJrYXJ0ZSI6ICJsYXJnZSIsCiAgICAgICJ0eXAiOiAiaGF1c2FyenQiLAogICAgICAi
    bmFtZSI6ICJIYXVzYXJ6dCDigJMgRHIuIE1heCBNdXN0ZXJtYW5uIiwKICAgICAgImJpbGQiOiAiaHR0cHM6Ly9waWNzdW0ucGhvdG9zL3NlZWQvYXJ6dC8zMDAvMzAw
    IiwKICAgICAgImFkcmVzc2UiOiB7CiAgICAgICAgInN0cmFzc2UiOiAiTXVzdGVyc3RyYcOfZSAxMiIsCiAgICAgICAgInBseiI6ICIwMTIzNCIsCiAgICAgICAgIm9y
    dCI6ICJNdXN0ZXJzdGFkdCIKICAgICAgfSwKICAgICAgInplaXRlbiI6IFsKICAgICAgICAiTW9udGFnIDA4LjAwIOKAkyAxMi4wMCIsCiAgICAgICAgIkRpZW5zdGFn
    IDA4LjAwIOKAkyAxMi4wMCwgMTQuMDAg4oCTIDE4LjAwIiwKICAgICAgICAiTWl0d29jaCAwOC4wMCDigJMgMTIuMDAiLAogICAgICAgICJEb25uZXJzdGFnIDA4LjAw
    IOKAkyAxMi4wMCIsCiAgICAgICAgIkZyZWl0YWcgMDguMDAg4oCTIDEyLjAwIgogICAgICBdLAogICAgICAia29udGFrdCI6IHsKICAgICAgICAidGVsZWZvbiI6ICIo
    MDEyMzQpIDEyMzQ1NiIsCiAgICAgICAgImZheCI6ICIoMDEyMzQpIDY1NDMyMSIsCiAgICAgICAgImVtYWlsIjogInByYXhpc0BtdXN0ZXJtYW5uLmRlIiwKICAgICAg
    ICAid2ViIjogImh0dHBzOi8vd3d3Lm11c3Rlcm1hbm4taGF1c2FyenQuZGUvIgogICAgICB9LAogICAgICAiaGlud2VpcyI6ICIiCiAgICB9LAogICAgewogICAgICAi
    aWQiOiAyLAogICAgICAia2FydGUiOiAibGFyZ2UiLAogICAgICAidHlwIjogInphaG5hcnp0IiwKICAgICAgIm5hbWUiOiAiWmFobmFyenQg4oCTIERyLiBKdWxpYSBC
    ZWlzcGllbCIsCiAgICAgICJiaWxkIjogImh0dHBzOi8vcGljc3VtLnBob3Rvcy9zZWVkL3phaG5hcnp0LzMwMC8zMDAiLAogICAgICAiYWRyZXNzZSI6IHsKICAgICAg
    ICAic3RyYXNzZSI6ICJCZWlzcGllbHdlZyA1IiwKICAgICAgICAicGx6IjogIjU2Nzg5IiwKICAgICAgICAib3J0IjogIkJlaXNwaWVsc3RhZHQiCiAgICAgIH0sCiAg
    ICAgICJ6ZWl0ZW4iOiBbCiAgICAgICAgIk1vbnRhZyAwOS4wMCDigJMgMTMuMDAiLAogICAgICAgICJEaWVuc3RhZyAwOS4wMCDigJMgMTMuMDAsIDE0LjAwIOKAkyAx
    OC4wMCIsCiAgICAgICAgIk1pdHR3b2NoIDA5LjAwIOKAkyAxMy4wMCIsCiAgICAgICAgIkRvbm5lcnN0YWcgMDkuMDAg4oCTIDEzLjAwLCAxNC4wMCDigJMgMTguMDAi
    LAogICAgICAgICJGcmVpdGFnIDA5LjAwIOKAkyAxMi4wMCIKICAgICAgXSwKICAgICAgImtvbnRha3QiOiB7CiAgICAgICAgInRlbGVmb24iOiAiKDA1Njc4KSA5ODc2
    NTQiLAogICAgICAgICJmYXgiOiAiIiwKICAgICAgICAiZW1haWwiOiAiaW5mb0B6YWhuYXJ6dC1iZWlzcGllbC5kZSIsCiAgICAgICAgIndlYiI6ICJodHRwczovL3d3
    dy56YWhuYXJ6dC1iZWlzcGllbC5kZS8iCiAgICAgIH0sCiAgICAgICJoaW53ZWlzIjogIlRlcm1pbmUgbmFjaCBWZXJlaW5iYXJ1bmciCiAgICB9LAogICAgewogICAg
    ICAiaWQiOiAzLAogICAgICAia2FydGUiOiAic21hbGwiLAogICAgICAidHlwIjogImZyaXNldXIiLAogICAgICAibmFtZSI6ICJGcmlzZXVyIFNhbG9uIEJlaXNwaWVs
    IiwKICAgICAgImJpbGQiOiAiaHR0cHM6Ly9waWNzdW0ucGhvdG9zL3NlZWQvZnJpc2V1ci8zMDAvMzAwIiwKICAgICAgImFkcmVzc2UiOiB7CiAgICAgICAgInN0cmFz
    c2UiOiAiIiwKICAgICAgICAicGx6IjogIiIsCiAgICAgICAgIm9ydCI6ICIiCiAgICAgIH0sCiAgICAgICJ6ZWl0ZW4iOiBbCiAgICAgICAgIk1vbnRhZyDigJMgRnJl
    aXRhZyAwOC4wMCDigJMgMTguMDAiLAogICAgICAgICJTYW1zdGFnIG5hY2ggVmVyZWluYmFydW5nIgogICAgICBdLAogICAgICAia29udGFrdCI6IHsKICAgICAgICAi
    dGVsZWZvbiI6ICIoMDEyMzQpIDExMjIzMyIsCiAgICAgICAgImZheCI6ICIiLAogICAgICAgICJlbWFpbCI6ICIiLAogICAgICAgICJ3ZWIiOiAiIgogICAgICB9LAog
    ICAgICAiaGlud2VpcyI6ICIiCiAgICB9CiAgXQp9`;
    

    Wünsche Euch viel Spaß bei der Umsetzung.

    Ro75.

    EDIT: Mit Stand 31.05.2026 gibt es neue Funktionen.

    CARDS.png

    • Themen: 9 verschiedene Themen stehen via Datenpunkt (theme) zur Auswahl. Standard = standard.
    • Opacity: Die Deckkraft kann gesteuert werden. Standard = 1 (100%).
    • Text-Shadow: Kann nun ebenfalls via Datenpunkt (textshadow) gesteuert werden. Standard = false.

    Zusätzlich können Details zentral konfiguriert werden

    const FONT = {
        family: "InterVariable",   // globale Schriftfamilie
    
        fields: {
            name:    { size: 16, color: "#ffffff", bold: true,  italic: false },  // Name / Praxis / Firma
            address: { size: 13, color: "#d0d7e2", bold: false, italic: false },  // Straße, PLZ, Ort
            contact: { size: 12, color: "#9fb3d9", bold: false, italic: false },  // Tel., Fax, E-Mail
            web:     { size: 12, color: "#6fa8ff", bold: false, italic: false },  // Website-Link
            title:   { size: 13, color: "#ffffff", bold: true,  italic: false },  // Abschnittstitel ("Öffnungszeiten")
            times:   { size: 12, color: "#d0d7e2", bold: false, italic: false },  // Öffnungszeiten-Zeilen
            hint:    { size: 12, color: "#ffcc66", bold: true,  italic: false }   // Hinweis-/Infozeilen
        },
    
        weight: {
            normal: 400,  // normales Schriftgewicht
            bold:   600   // fettes Schriftgewicht
        }
    };
    

    Sollte selbsterklärend sein.

    EDIT: Mit Stand 03.06.2026 gibt es Fehlerkorrekturen.

    • Korrektur der Position für den Hinweistext
    • Korrektur der Öffnungszeiten (falsche Zusammenstellung)
    • Bei Öffnungszeiten kann nun zusätzlich zum EN-DASH auch der normale Bindestrich (-) Verwendung finden.
    • Konfiguration (Schriftgröße, Farbe, Stil) der Felder nun in allen Themen möglich.

    EDIT: Mit Stand 05.06.2026 gibt es Fehlerkorrekturen.

    • Korrektur der Themen "wave" und "teal" - Konfiguration jetzt möglich.

    EDIT: Mit Stand 22.06.2026 gibt es vorbeugende Code-Korrekturen.

    • In seltenen Fällen konnte die SVG‑Erstellung scheitern, wenn die JSON‑Daten Zeichen wie & oder < enthielten. Durch das Einführen eines zentralen Escapings werden diese Sonderzeichen nun korrekt verarbeitet, sodass die SVG‑Generierung stabil funktioniert.

    EDIT: Mit Stand 23.06.2026 gibt es einen Fix.

    • betrifft Zeile 598: font-weight="${F_WEIGHT}" ist falsch - korrekt ist font-weight="${F.weight}"
    sigi234S Online
    sigi234S Online
    sigi234
    Forum Testing Most Active
    schrieb am zuletzt editiert von
    #2

    @Ro75 sagte:

    die sich direkt in VIS oder jede andere Visualisierung einfügen lassen.

    und wie?

    Bitte benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.
    Immer Daten sichern!

    1 Antwort Letzte Antwort
    0
    • Ro75R Offline
      Ro75R Offline
      Ro75
      schrieb am zuletzt editiert von Ro75
      #3

      mit einem Basic - String (unescaped). Hier VIS1.
      76ae27c0-3e71-4d7d-b2de-9f15c0dff723-image.jpeg

      Ro75.

      SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

      NegaleinN 1 Antwort Letzte Antwort
      0
      • Ro75R Offline
        Ro75R Offline
        Ro75
        schrieb am zuletzt editiert von
        #4

        Habe den Code im Eingangspost aktualisiert. Da fehlte eine Funktion.

        Ro75.

        SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

        1 Antwort Letzte Antwort
        0
        • NashraN Offline
          NashraN Offline
          Nashra
          Most Active Forum Testing
          schrieb am zuletzt editiert von
          #5

          Hi, erstmal tolles Skript.
          Aber ich habe ein kleines Problem bei den Zeiten.
          Im json

                "zeiten": [
                  "Montag 08.00 – 12.30, 15.00 – 18.00",
                  "Dienstag 08.00 – 12.30, 15.00 – 18.00",
                  "Mitwoch 08.00 – 12.30",
                  "Donnerstag 08.00 – 12.30, 15.00 – 18.00",
                  "Freitag 08.00 – 12.30"
                ],
          

          Ausgabe
          Screenshot 2026-05-28 130002.png

          Gruß Ralf
          Mir egal, wer Dein Vater ist! Wenn ich hier angel, wird nicht übers Wasser gelaufen!!

          Benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.

          1 Antwort Letzte Antwort
          0
          • Ro75R Offline
            Ro75R Offline
            Ro75
            schrieb am zuletzt editiert von
            #6

            Mittwoch bitte mit 2t, dann klappt das auch.
            Ro75.

            SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

            NashraN 1 Antwort Letzte Antwort
            0
            • Ro75R Ro75

              Mittwoch bitte mit 2t, dann klappt das auch.
              Ro75.

              NashraN Offline
              NashraN Offline
              Nashra
              Most Active Forum Testing
              schrieb am zuletzt editiert von
              #7

              @Ro75 sagte:

              Mittwoch bitte mit 2t, dann klappt das auch.
              Ro75.

              🤦 boah, da sucht man die ganze Zeit und übersieht ein "t"
              Danke 😊

              Gruß Ralf
              Mir egal, wer Dein Vater ist! Wenn ich hier angel, wird nicht übers Wasser gelaufen!!

              Benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.

              1 Antwort Letzte Antwort
              0
              • Ro75R Offline
                Ro75R Offline
                Ro75
                schrieb am zuletzt editiert von
                #8

                Ich habe am Script 2 kleine Modifikationen vorgenommen. Ab jetzt kann im Konfigurationsbereich die Schrift und der Textschatten konfiguriert werden.

                const FONT_FAMILY         = "InterVariable";
                const TEXT_SHADOW_ENABLED = true;   // globaler Text‑Shadow an/aus
                

                Diese Einstellungen sind die bisherigen Basiswerte und waren bereits fest definiert. Ab jetzt sind sie konfigurierbar.

                Das Script kann 1:1 aus dem Post#1 übernommen werden. Falls

                const root     = "0_userdata.0.Kontakte."
                

                angepasst wurde, bitte nach Scriptübernahme wieder korrigieren.

                Ro75.

                SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

                1 Antwort Letzte Antwort
                1
                • Ro75R Offline
                  Ro75R Offline
                  Ro75
                  schrieb am zuletzt editiert von
                  #9

                  Es gibt neue Funktionen.

                  CARDS.png

                  • Themen: 9 verschiedene Themen stehen via Datenpunkt (theme) zur Auswahl. Standard = standard.
                  • Opacity: Die Deckkraft kann gesteuert werden. Standard = 1 (100%).
                  • Text-Shadow: Kann nun ebenfalls via Datenpunkt (textshadow) gesteuert werden. Standard = false.

                  Zusätzlich können Details zentral konfiguriert werden

                  const FONT = {
                      family: "InterVariable",   // globale Schriftfamilie
                  
                      fields: {
                          name:    { size: 16, color: "#ffffff", bold: true,  italic: false },  // Name / Praxis / Firma
                          address: { size: 13, color: "#d0d7e2", bold: false, italic: false },  // Straße, PLZ, Ort
                          contact: { size: 12, color: "#9fb3d9", bold: false, italic: false },  // Tel., Fax, E-Mail
                          web:     { size: 12, color: "#6fa8ff", bold: false, italic: false },  // Website-Link
                          title:   { size: 13, color: "#ffffff", bold: true,  italic: false },  // Abschnittstitel ("Öffnungszeiten")
                          times:   { size: 12, color: "#d0d7e2", bold: false, italic: false },  // Öffnungszeiten-Zeilen
                          hint:    { size: 12, color: "#ffcc66", bold: true,  italic: false }   // Hinweis-/Infozeilen
                      },
                  
                      weight: {
                          normal: 400,  // normales Schriftgewicht
                          bold:   600   // fettes Schriftgewicht
                      }
                  };
                  

                  Sollte selbsterklärend sein. Script im Eingangspost #1 aktualisiert.

                  Ro75.

                  SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

                  1 Antwort Letzte Antwort
                  1
                  • NashraN Offline
                    NashraN Offline
                    Nashra
                    Most Active Forum Testing
                    schrieb am zuletzt editiert von Nashra
                    #10

                    Kleine Änderung bitte bei den Öffnungszeiten. Die Umrandung ist etwas zu knapp wenn
                    der Text "Termine nach Vereinbarung" angezeigt wird.
                    Sonst super und ein dickes Danke von meiner Frau, die ist happy

                    Edit: Bild vergessen
                    Screenshot 2026-06-01 114620.png

                    Gruß Ralf
                    Mir egal, wer Dein Vater ist! Wenn ich hier angel, wird nicht übers Wasser gelaufen!!

                    Benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.

                    1 Antwort Letzte Antwort
                    0
                    • Ro75R Offline
                      Ro75R Offline
                      Ro75
                      schrieb am zuletzt editiert von
                      #11

                      Danke für das Feedback. Ich schaue mir das mit dem Design an, damit es auch übergreifend passt. Kann aber paar Tage dauern.

                      Ro75

                      SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

                      NashraN 1 Antwort Letzte Antwort
                      0
                      • Ro75R Ro75

                        Danke für das Feedback. Ich schaue mir das mit dem Design an, damit es auch übergreifend passt. Kann aber paar Tage dauern.

                        Ro75

                        NashraN Offline
                        NashraN Offline
                        Nashra
                        Most Active Forum Testing
                        schrieb am zuletzt editiert von
                        #12

                        @Ro75 sagte:

                        Danke für das Feedback. Ich schaue mir das mit dem Design an, damit es auch übergreifend passt. Kann aber paar Tage dauern.

                        Ro75

                        Kein Thema, eilt nicht. Habe noch etwas, sag mal, seltsames mit der Anzeige

                              "zeiten": [
                                "Montag 08.00 – 12.30, 14.30 – 18.00",
                                "Dienstag 08.00 – 12.30, 14.30 – 18.00",
                                "Mittwoch 08.00 – 12.30",
                                "Donnerstag 08.00 – 12.30, 14.30 – 18.00",
                                "Freitag 08.00 – 12.30"
                              ],
                        

                        Screenshot 2026-06-01 135218.png

                        anderer Eintrag...

                              "zeiten": [
                                "Montag 08.00 – 11.45, 14.00 – 16.45",
                                "Dienstag 08.00 – 11.45, 14.00 – 16.45",
                                "Mittwoch 08.00 – 11.45",
                                "Donnerstag 08.30 – 11.45, 14.00 – 16.45",
                                "Freitag 08.30 – 11.45"
                              ],
                        

                        Screenshot 2026-06-01 135402.png

                        und dann werden Änderungen bei z.B.

                                times:   { size: 12, color: "#d0d7e2", bold: true, italic: false },  // Öffnungszeiten-Zeilen
                        

                        bei mir nicht übernommen.

                        Gruß Ralf
                        Mir egal, wer Dein Vater ist! Wenn ich hier angel, wird nicht übers Wasser gelaufen!!

                        Benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.

                        1 Antwort Letzte Antwort
                        0
                        • NegaleinN Offline
                          NegaleinN Offline
                          Negalein
                          schrieb am zuletzt editiert von
                          #13

                          Servus @ro75

                          Ich steh noch etwas daneben.

                          • Jeder Eintrag benötigt eine eindeutige ID und muss sich exakt an die Struktur der Beispieldaten halten.
                          • Öffnungszeiten müssen exakt im Format „Wochentag HH.MM – HH.MM[, HH.MM – HH.MM]“ stehen
                          und dabei zwingend den echten EN‑DASH (–) verwenden — kein Minuszeichen (-).

                          • wie bring ich diese Base64 Beispieldaten in verständliches editierbares Format?
                          • alle Kontakte in 1 JSON, oder je Kontakt eine eigene?
                          • wie macht man den En-Dash?

                          Danke

                          ° Node.js & System Update ---> sudo apt update, iob stop, sudo apt full-upgrade
                          ° Node.js Fixer ---> iob nodejs-update
                          ° Fixer ---> iob fix

                          NashraN 1 Antwort Letzte Antwort
                          0
                          • Ro75R Offline
                            Ro75R Offline
                            Ro75
                            schrieb am zuletzt editiert von Ro75
                            #14

                            Das Script erstellt, sofern es das erste mal gestartet wird die Beispieldaten selbst. Genau dafür wird der Base64 Code benötigt. Du selbst musst damit gar nichts machen.

                            Sobald das Script läuft siehst die den Datenpunkt. Anklicken, ansehen und damit arbeiten. Also anpassen, hinzufügen. Wenn gespeichert wird sofort der SVG Code aktualisiert.

                            Ro75

                            SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

                            1 Antwort Letzte Antwort
                            1
                            • NashraN Offline
                              NashraN Offline
                              Nashra
                              Most Active Forum Testing
                              schrieb am zuletzt editiert von
                              #15

                              Moin @ro75
                              das mit der Anzeige (Uhrzeiten) hat sich erledigt.
                              Es hatten sich Bindestriche eingeschlichen, kam wohl dadurch das ich
                              alles in Notepad++ bearbeitet habe und beim tippen diese dann mit
                              reingehauen hatte. Jetzt wird alles richtig angezeigt.

                              Gruß Ralf
                              Mir egal, wer Dein Vater ist! Wenn ich hier angel, wird nicht übers Wasser gelaufen!!

                              Benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.

                              1 Antwort Letzte Antwort
                              1
                              • NegaleinN Negalein

                                Servus @ro75

                                Ich steh noch etwas daneben.

                                • Jeder Eintrag benötigt eine eindeutige ID und muss sich exakt an die Struktur der Beispieldaten halten.
                                • Öffnungszeiten müssen exakt im Format „Wochentag HH.MM – HH.MM[, HH.MM – HH.MM]“ stehen
                                und dabei zwingend den echten EN‑DASH (–) verwenden — kein Minuszeichen (-).

                                • wie bring ich diese Base64 Beispieldaten in verständliches editierbares Format?
                                • alle Kontakte in 1 JSON, oder je Kontakt eine eigene?
                                • wie macht man den En-Dash?

                                Danke

                                NashraN Offline
                                NashraN Offline
                                Nashra
                                Most Active Forum Testing
                                schrieb am zuletzt editiert von Nashra
                                #16

                                @negalein

                                • alle Kontakte in 1 JSON, oder je Kontakt eine eigene?

                                kommt alles in eine json, d.h. alles im DP editieren

                                • wie macht man den En-Dash?

                                Bei Windows: Alt + 0150 (auf dem Ziffernblock)

                                Gruß Ralf
                                Mir egal, wer Dein Vater ist! Wenn ich hier angel, wird nicht übers Wasser gelaufen!!

                                Benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.

                                1 Antwort Letzte Antwort
                                0
                                • Ro75R Offline
                                  Ro75R Offline
                                  Ro75
                                  schrieb am zuletzt editiert von Ro75
                                  #17

                                  Die Position vom Hinweis passe ich noch an. Schaue auch, das ich den Bindestrich mit unterstütze. Und auch die Konfiguration. Wie gesagt, kann paar Tage dauern.

                                  Ro75.

                                  SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

                                  NashraN 1 Antwort Letzte Antwort
                                  0
                                  • Ro75R Ro75

                                    mit einem Basic - String (unescaped). Hier VIS1.
                                    76ae27c0-3e71-4d7d-b2de-9f15c0dff723-image.jpeg

                                    Ro75.

                                    NegaleinN Offline
                                    NegaleinN Offline
                                    Negalein
                                    schrieb am zuletzt editiert von
                                    #18

                                    @Ro75 sagte:

                                    mit einem Basic - String (unescaped). Hier VIS1.

                                    das funktioniert bei mir nicht

                                    Vis-Edit
                                    Edit-vis-02-06-2026_09_45.png

                                    Vis-Runtime
                                    vis-02-06-2026_09_45.png

                                    ° Node.js & System Update ---> sudo apt update, iob stop, sudo apt full-upgrade
                                    ° Node.js Fixer ---> iob nodejs-update
                                    ° Fixer ---> iob fix

                                    1 Antwort Letzte Antwort
                                    0
                                    • NashraN Offline
                                      NashraN Offline
                                      Nashra
                                      Most Active Forum Testing
                                      schrieb am zuletzt editiert von
                                      #19

                                      @negalein
                                      du mußt die svg Datenpunkte nehmen
                                      Screenshot 2026-06-02 105714.png

                                      Screenshot 2026-06-02 105645.png

                                      Gruß Ralf
                                      Mir egal, wer Dein Vater ist! Wenn ich hier angel, wird nicht übers Wasser gelaufen!!

                                      Benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.

                                      NegaleinN 1 Antwort Letzte Antwort
                                      0
                                      • Ro75R Ro75

                                        Die Position vom Hinweis passe ich noch an. Schaue auch, das ich den Bindestrich mit unterstütze. Und auch die Konfiguration. Wie gesagt, kann paar Tage dauern.

                                        Ro75.

                                        NashraN Offline
                                        NashraN Offline
                                        Nashra
                                        Most Active Forum Testing
                                        schrieb am zuletzt editiert von Nashra
                                        #20

                                        @Ro75 sagte:

                                        Die Position vom Hinweis passe ich noch an. Schaue auch, das ich den Bindestrich mit unterstütze. Und auch die Konfiguration. Wie gesagt, kann paar Tage dauern.

                                        Ro75.

                                        Wie schon geschrieben, keine Eile.

                                        Gruß Ralf
                                        Mir egal, wer Dein Vater ist! Wenn ich hier angel, wird nicht übers Wasser gelaufen!!

                                        Benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.

                                        1 Antwort Letzte Antwort
                                        0
                                        • NashraN Nashra

                                          @negalein
                                          du mußt die svg Datenpunkte nehmen
                                          Screenshot 2026-06-02 105714.png

                                          Screenshot 2026-06-02 105645.png

                                          NegaleinN Offline
                                          NegaleinN Offline
                                          Negalein
                                          schrieb am zuletzt editiert von
                                          #21

                                          @Nashra sagte:

                                          du mußt die svg Datenpunkte nehmen

                                          danke

                                          @ro75

                                          die id1 schaut so aus.
                                          in der view wir vom freitag nichts angezeigt.

                                          und geht es, dass nicht

                                          Mo-Mi
                                          Di-Do

                                          steht, sondern entweder jeder tag extra untereinander, oder zumindest
                                          Mo, Mi
                                          Di, Do

                                              {
                                                "id": 1,
                                                "karte": "large",
                                                "typ": "hausarzt",
                                                "name": "Hausarzt – Dr. Bernhard Hohenberger",
                                                "bild": "https://gruentalpraxis.at/wp-content/uploads/2025/04/GruentalPraxis_LOGO_4c.png",
                                                "adresse": {
                                                  "strasse": "Kenzianweg 9",
                                                  "plz": "4780",
                                                  "ort": "Schärding"
                                                },
                                                "zeiten": [
                                                  "Montag 8:30 – 12:00",
                                                  "Dienstag 15:00 – 19:00",
                                                  "Mittwoch 8:30 – 12:00",
                                                  "Donnerstag 15:00 – 19:00",
                                                  "Freitag 8:30 – 12:00 und (alle 4 Wochen) 13:00 – 16:00"
                                                ],
                                                "kontakt": {
                                                  "telefon": "+43 7712 355 30",
                                                  "email": "office@gruentalpraxis.at",
                                                  "web": "https://gruentalpraxis.at/"
                                                },
                                                "hinweis": ""
                                              },
                                          

                                          776cad60-9770-45a3-a568-c36d4a06e130-image.jpeg

                                          ° Node.js & System Update ---> sudo apt update, iob stop, sudo apt full-upgrade
                                          ° Node.js Fixer ---> iob nodejs-update
                                          ° Fixer ---> iob fix

                                          1 Antwort Letzte Antwort
                                          0

                                          Hey! Du scheinst an dieser Unterhaltung interessiert zu sein, hast aber noch kein Konto.

                                          Hast du es satt, bei jedem Besuch durch die gleichen Beiträge zu scrollen? Wenn du dich für ein Konto anmeldest, kommst du immer genau dorthin zurück, wo du zuvor warst, und kannst dich über neue Antworten benachrichtigen lassen (entweder per E-Mail oder Push-Benachrichtigung). Du kannst auch Lesezeichen speichern und Beiträge positiv bewerten, um anderen Community-Mitgliedern deine Wertschätzung zu zeigen.

                                          Mit deinem Input könnte dieser Beitrag noch besser werden 💗

                                          Registrieren Anmelden
                                          Antworten
                                          • In einem neuen Thema antworten
                                          Anmelden zum Antworten
                                          • Älteste zuerst
                                          • Neuste zuerst
                                          • Meiste Stimmen


                                          Support us

                                          ioBroker
                                          Community Adapters
                                          Donate

                                          342

                                          Online

                                          33.0k

                                          Benutzer

                                          83.3k

                                          Themen

                                          1.3m

                                          Beiträge
                                          Community
                                          Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen | Einwilligungseinstellungen
                                          ioBroker Community 2014-2026
                                          logo
                                          • Anmelden

                                          • Du hast noch kein Konto? Registrieren

                                          • Anmelden oder registrieren, um zu suchen
                                          • Erster Beitrag
                                            Letzter Beitrag
                                          0
                                          • Home
                                          • Aktuell
                                          • Tags
                                          • Ungelesen 0
                                          • Kategorien
                                          • Unreplied
                                          • Beliebt
                                          • GitHub
                                          • Docu
                                          • Hilfe