<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Kontakte (Cards)]]></title><description><![CDATA[<p dir="auto">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.<br />
Die Daten liegen zentral in einer JSON, aus der automatisch <strong>schicke SVG‑Karten</strong> erzeugt werden, die sich direkt in VIS oder jede andere Visualisierung einfügen lassen.</p>
<p dir="auto">Sobald ein Eintrag geändert oder ein neuer Kontakt hinzugefügt wird, aktualisiert das System die Karten <strong>vollautomatisch</strong>.<br />
Keine Pflege an mehreren Stellen, keine veralteten Infos – <strong>einmal eintragen, überall aktuell</strong>.</p>
<p dir="auto">Das Script erzeugt beim ersten Start <strong>automatisch einen kompletten Beispieldatensatz</strong> mit drei fertigen Karten – inklusive der passenden SVG‑Grafiken.<br />
So sieht man sofort, wie das System funktioniert und wie die Karten später in der Visualisierung aussehen.</p>
<p dir="auto"><img src="/assets/uploads/files/1779708283086-card.png" alt="card.png" class=" img-fluid img-markdown" /></p>
<p dir="auto"><em><strong>HINWEIS: Ein Teil des Codes wurde dabei mit Unterstützung einer KI (Microsoft Copilot) entwickelt, um die Erstellung noch effizienter und moderner zu gestalten.</strong></em></p>
<p dir="auto"><strong>VORAUSSETZUNGEN:</strong></p>
<ul>
<li>JS-Adapter: 9.0.18</li>
<li>Node-JS: 22.22.3</li>
</ul>
<p dir="auto"><strong>Hier der Code:</strong></p>
<pre><code>//********** 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 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.25,           // Glow‑Dunkelheits‑Faktor NUR für kleine Karte
    ring: "rgba(255,255,255,0.30)",  // äußerer Ring
    background: "#0f2647"            // Avatar-Hintergrund
};

//Beispieldaten json - als Base64 - für weniger Platz
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`;

/*
    JSON‑Datensatz – Anforderungen (kurz &amp; 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 **********

async function smartCreateState(id, value, options = {}) {
    if (existsState(id)) return;
    await createState(id, value, options);
}

function sleepMs(ms) {
    return new Promise(resolve =&gt; 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 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 =&gt; {
        const [day, ...rest] = t.split(" ");
        const time = rest.join(" ");

        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 =&gt; {
        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 = [];

    for (const time in groups) {
        const list = groups[time];

        if (list.length === 1) {
            result.push(`${list[0]} ${time}`);
        } else {
            result.push(`${list[0]}–${list[list.length - 1]} ${time}`);
        }
    }
    return result.concat(specials);
}

function splitTimeRow(row) {
    const parts = row.split(" ");
    const day = parts[0];
    const time = parts.slice(1).join(" ");
    return { day, time };
}

function wrapText(text, maxLen = 32) {
    const words = text.split(" ");
    const lines = [];
    let current = "";

    for (const w of words) {
        if ((current + w).length &gt; maxLen) {
            lines.push(current.trim());
            current = w + " ";
        } else {
            current += w + " ";
        }
    }
    if (current.trim().length &gt; 0) lines.push(current.trim());
    return lines;
}

on({ id: JSON_DP, change: "any" }, async () =&gt; {
    await processJSON();
});

function buildSVG(entry) {
    if (entry.karte === "small") {
        return buildSmallCard(entry);
    } else {
        return buildLargeCard(entry);
    }
}

async function main() {
    await smartCreateState(JSON_DP, "", { type: "string", name: "Kontakte JSON" });

    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 hasValidTimes = e.zeiten.some(z =&gt; z.trim() !== "");
    const zeiten        = hasValidTimes ? compressTimes(e.zeiten) : [];
    const hinweisLines  = e.hinweis ? wrapText(e.hinweis, 45) : [];
    const uid = `card${e.id}`;

    function splitTimeAtomic(row) {
        const idx = row.indexOf(" ");
        if (idx === -1) return { day: row, parts: [] };

        const day  = row.substring(0, idx);
        const rest = row.substring(idx + 1).split(",");
        const parts = [];

        rest.forEach(block =&gt; {
            const m = block.trim().match(/^(\S+)\s*–\s*(\S+)$/);
            if (m) parts.push({ start: m[1], end: m[2] });
        });
        return { day, parts };
    }

    const X = {
        day: 18,
        s1: 145, d1: 160, e1: 175,
        s2: 255, d2: 270, e2: 285
    };

return `
    &lt;svg viewBox="0 0 ${CARD_LARGE_W} ${CARD_LARGE_H}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet"&gt;
    &lt;style&gt;.txt{text-shadow:3px 3px 4px #000;font-feature-settings:"tnum";}&lt;/style&gt;

    &lt;defs&gt;
        &lt;radialGradient id="glassGlow-${uid}" cx="50%" cy="50%" r="50%"&gt;
            &lt;stop offset="0%" stop-color="${AVATAR.glass.core.color}" stop-opacity="${AVATAR.glass.core.opacity}"/&gt;
            &lt;stop offset="22%" stop-color="${AVATAR.glow1?.color ?? AVATAR.glass.glow1.color}" stop-opacity="${AVATAR.glass.glow1.opacity}"/&gt;
            &lt;stop offset="50%" stop-color="${AVATAR.glass.glow2.color}" stop-opacity="${AVATAR.glass.glow2.opacity}"/&gt;
            &lt;stop offset="82%" stop-color="${AVATAR.glass.glow3.color}" stop-opacity="${AVATAR.glass.glow3.opacity}"/&gt;
            &lt;stop offset="100%" stop-color="${AVATAR.glass.fade.color}" stop-opacity="${AVATAR.glass.fade.opacity}"/&gt;
        &lt;/radialGradient&gt;

        &lt;radialGradient id="avatarAuraGlow-${uid}" cx="50%" cy="50%" r="50%"&gt;
            &lt;stop offset="0%" stop-color="${AVATAR.aura.inner.color}" stop-opacity="${AVATAR.aura.inner.opacity}"/&gt;
            &lt;stop offset="60%" stop-color="${AVATAR.aura.mid.color}" stop-opacity="${AVATAR.aura.mid.opacity}"/&gt;
            &lt;stop offset="100%" stop-color="${AVATAR.aura.outer.color}" stop-opacity="${AVATAR.aura.outer.opacity}"/&gt;
        &lt;/radialGradient&gt;

        &lt;filter id="glassBloom-${uid}" x="-250%" y="-250%" width="600%" height="600%"&gt;
            &lt;feGaussianBlur stdDeviation="12" result="blur"/&gt;
            &lt;feMerge&gt;&lt;feMergeNode in="blur"/&gt;&lt;feMergeNode in="SourceGraphic"/&gt;&lt;/feMerge&gt;
        &lt;/filter&gt;

        &lt;filter id="glassShadow-${uid}" x="-100%" y="-100%" width="300%" height="300%"&gt;
            &lt;feDropShadow dx="0" dy="6" stdDeviation="9" flood-color="#000" flood-opacity="0.46"/&gt;
        &lt;/filter&gt;

        &lt;mask id="avatarMask-${uid}"&gt;
            &lt;circle cx="280" cy="115" r="40" fill="white"/&gt;
        &lt;/mask&gt;
    &lt;/defs&gt;

    &lt;circle cx="280" cy="115" r="90" fill="url(#avatarAuraGlow-${uid})" filter="url(#glassBloom-${uid})" opacity="0.78"/&gt;
    &lt;circle cx="280" cy="115" r="62" fill="url(#glassGlow-${uid})" filter="url(#glassBloom-${uid})"/&gt;
    &lt;circle cx="280" cy="115" r="46" fill="none" stroke="${AVATAR.ring}" stroke-width="1.5"/&gt;
    &lt;ellipse cx="270" cy="102" rx="12" ry="4" fill="rgba(255,255,255,0.22)" transform="rotate(-25 270 102)"/&gt;
    &lt;circle cx="280" cy="115" r="42" fill="${AVATAR.background}" filter="url(#glassShadow-${uid})"/&gt;
    &lt;image href="${e.bild}" x="240" y="75" width="80" height="80" mask="url(#avatarMask-${uid})" preserveAspectRatio="xMidYMid slice"/&gt;

    &lt;text class="txt" x="18" y="40" fill="#fff" font-size="15" font-weight="600"&gt;${e.name}&lt;/text&gt;
    &lt;text class="txt" x="18" y="75" fill="#d0d7e2" font-size="13"&gt;${e.adresse.strasse}&lt;/text&gt;
    &lt;text class="txt" x="18" y="93" fill="#d0d7e2" font-size="13"&gt;${e.adresse.plz} ${e.adresse.ort}&lt;/text&gt;

    ${e.kontakt.telefon.split(/[,;]\s*/).map((tel,i)=&gt;`
        &lt;text class="txt" x="18" y="${130+i*18}" fill="#9fb3d9" font-size="12"&gt;Tel.: ${tel}&lt;/text&gt;
    `).join("")}
    ${e.kontakt.fax   ? `&lt;text class="txt" x="18" y="148" fill="#9fb3d9" font-size="12"&gt;Fax: ${e.kontakt.fax}&lt;/text&gt;` : ""}
    ${e.kontakt.email ? `&lt;text class="txt" x="18" y="166" fill="#9fb3d9" font-size="12"&gt;${e.kontakt.email}&lt;/text&gt;` : ""}
    ${e.kontakt.web   ? `&lt;text class="txt" x="18" y="184" fill="#6fa8ff" font-size="12"&gt;${e.kontakt.web}&lt;/text&gt;` : ""}

    &lt;text class="txt" x="18" y="225" fill="#fff" font-size="14" font-weight="600"&gt;Öffnungszeiten&lt;/text&gt;

    ${hasValidTimes ? zeiten.map((z,i)=&gt;{
        const {day,parts}=splitTimeAtomic(z);
        const b1=parts[0]||null, b2=parts[1]||null;
        return `
        &lt;text class="txt" y="${250+i*20}" fill="#d0d7e2" font-size="12"&gt;
            &lt;tspan x="${X.day}"&gt;${day}&lt;/tspan&gt;
            ${b1?`&lt;tspan x="${X.s1}" text-anchor="end"&gt;${b1.start}&lt;/tspan&gt;&lt;tspan x="${X.d1}" text-anchor="middle"&gt;–&lt;/tspan&gt;&lt;tspan x="${X.e1}" text-anchor="start"&gt;${b1.end}&lt;/tspan&gt;`:""}
            ${b2?`&lt;tspan x="${X.s2}" text-anchor="end"&gt;${b2.start}&lt;/tspan&gt;&lt;tspan x="${X.d2}" text-anchor="middle"&gt;–&lt;/tspan&gt;&lt;tspan x="${X.e2}" text-anchor="start"&gt;${b2.end}&lt;/tspan&gt;`:""}
        &lt;/text&gt;`;
    }).join("") : `
        &lt;text class="txt" x="18" y="250" fill="#d0d7e2" font-size="12"&gt;${e.hinweis}&lt;/text&gt;
    `}

    ${e.hinweis &amp;&amp; hasValidTimes ? hinweisLines.map((line,i)=&gt;`
        &lt;text class="txt" x="18" y="${250+zeiten.length*20+10+i*14}" fill="#ffcc66" font-size="12" font-weight="600"&gt;${line}&lt;/text&gt;
    `).join("") : ""}

    &lt;/svg&gt;
`.trim();
}

function buildSmallCard(e) {
    const uid = `card${e.id}`;
    const gf = AVATAR.glowFactorSmall;

    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) =&gt; `${map[a]}–${map[b]}`);
        for (const full in map) {
            str = str.replace(new RegExp(full, "g"), map[full]);
        }
        return str;
    }
    const zeiten = e.zeiten.map(z =&gt; normalizeDays(z));

    function splitTimeRow(row) {
        const idx = row.indexOf(" ");
        if (idx === -1) return { day: row, time: "" };
        return {
            day: row.substring(0, idx),
            time: row.substring(idx + 1)
        };
    }

return `
    &lt;svg viewBox="0 0 ${CARD_SMALL_W} ${CARD_SMALL_H}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet"&gt;
    &lt;style&gt;.txt{text-shadow:3px 3px 4px #000;font-family:InterVariable;}&lt;/style&gt;

    &lt;defs&gt;

        &lt;radialGradient id="avatarCoreGlow-${uid}" cx="50%" cy="50%" r="50%"&gt;
            &lt;stop offset="0%"  stop-color="${AVATAR.glass.core.color}" stop-opacity="${AVATAR.glass.core.opacity}"/&gt;
            &lt;stop offset="22%" stop-color="${AVATAR.glass.glow1.color}" stop-opacity="${AVATAR.glass.glow1.opacity * gf}"/&gt;
            &lt;stop offset="50%" stop-color="${AVATAR.glass.glow2.color}" stop-opacity="${AVATAR.glass.glow2.opacity * gf}"/&gt;
            &lt;stop offset="82%" stop-color="${AVATAR.glass.glow3.color}" stop-opacity="${AVATAR.glass.glow3.opacity * gf}"/&gt;
            &lt;stop offset="100%" stop-color="${AVATAR.glass.fade.color}" stop-opacity="${AVATAR.glass.fade.opacity * gf}"/&gt;
        &lt;/radialGradient&gt;

        &lt;radialGradient id="avatarAuraGlow-${uid}" cx="50%" cy="50%" r="50%"&gt;
            &lt;stop offset="0%" stop-color="${AVATAR.aura.inner.color}" stop-opacity="${AVATAR.aura.inner.opacity}"/&gt;
            &lt;stop offset="60%" stop-color="${AVATAR.aura.mid.color}" stop-opacity="${AVATAR.aura.mid.opacity}"/&gt;
            &lt;stop offset="100%" stop-color="${AVATAR.aura.outer.color}" stop-opacity="${AVATAR.aura.outer.opacity}"/&gt;
        &lt;/radialGradient&gt;

        &lt;filter id="avatarBloom-${uid}" x="-250%" y="-250%" width="600%" height="600%"&gt;
            &lt;feGaussianBlur stdDeviation="8" result="blur"/&gt;
            &lt;feMerge&gt;&lt;feMergeNode in="blur"/&gt;&lt;feMergeNode in="SourceGraphic"/&gt;&lt;/feMerge&gt;
        &lt;/filter&gt;

        &lt;filter id="avatarShadow-${uid}" x="-100%" y="-100%" width="300%" height="300%"&gt;
            &lt;feDropShadow dx="0" dy="6" stdDeviation="9" flood-color="#000" flood-opacity="0.46"/&gt;
        &lt;/filter&gt;

        &lt;clipPath id="avatarClipSmall-${uid}"&gt;
            &lt;circle cx="300" cy="80" r="28"/&gt;
        &lt;/clipPath&gt;
    &lt;/defs&gt;

    &lt;circle cx="300" cy="80" r="48" fill="url(#avatarAuraGlow-${uid})" filter="url(#avatarBloom-${uid})" opacity="0.88"/&gt;
    &lt;circle cx="300" cy="80" r="44" fill="url(#avatarCoreGlow-${uid})" filter="url(#avatarBloom-${uid})"/&gt;
    &lt;circle cx="300" cy="80" r="32" fill="none" stroke="${AVATAR.ring}" stroke-width="1.5"/&gt;
    &lt;ellipse cx="289" cy="67" rx="11" ry="4" fill="rgba(255,255,255,0.22)" transform="rotate(-25 289 67)"/&gt;
    &lt;circle cx="300" cy="80" r="22.4" fill="${AVATAR.background}" filter="url(#avatarShadow-${uid})"/&gt;
    &lt;image href="${e.bild}" x="272" y="52" width="56" height="56" clip-path="url(#avatarClipSmall-${uid})" preserveAspectRatio="xMidYMid slice"/&gt;

    &lt;text class="txt" x="15" y="32" fill="#fff" font-size="15" font-weight="600"&gt;${e.name}&lt;/text&gt;
    &lt;text class="txt" x="15" y="55" fill="#9fb3d9" font-size="13"&gt;Tel.: ${e.kontakt.telefon}&lt;/text&gt;
    &lt;text class="txt" x="15" y="78" fill="#fff" font-size="13" font-weight="600"&gt;Öffnungszeiten&lt;/text&gt;

    ${zeiten.map((z,i)=&gt;{
        const row = splitTimeRow(z);
        return `
        &lt;text class="txt" x="15" y="${95+i*15}" fill="#d0d7e2" font-size="12"&gt;
            &lt;tspan x="15"&gt;${row.day}&lt;/tspan&gt;
            &lt;tspan x="120"&gt;${row.time}&lt;/tspan&gt;
        &lt;/text&gt;`;
    }).join("")}

    &lt;/svg&gt;
`.trim();
}

function setStateIfChanged(id, value) {
    const state = getState(id);
    if (!state) return;

    const old = state.val;
    const isObject = v =&gt; v !== null &amp;&amp; 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);
}
</code></pre>
<p dir="auto">Wünsche Euch viel Spaß bei der Umsetzung.</p>
<p dir="auto">Ro75.</p>
]]></description><link>https://forum.iobroker.net/topic/84628/kontakte-cards</link><generator>RSS for Node</generator><lastBuildDate>Tue, 26 May 2026 06:38:18 GMT</lastBuildDate><atom:link href="https://forum.iobroker.net/topic/84628.rss" rel="self" type="application/rss+xml"/><pubDate>Mon, 25 May 2026 11:32:00 GMT</pubDate><ttl>60</ttl><item><title><![CDATA[Reply to Kontakte (Cards) on Mon, 25 May 2026 17:54:24 GMT]]></title><description><![CDATA[<p dir="auto">Habe den Code im Eingangspost aktualisiert. Da fehlte eine Funktion.</p>
<p dir="auto">Ro75.</p>
]]></description><link>https://forum.iobroker.net/post/1341392</link><guid isPermaLink="true">https://forum.iobroker.net/post/1341392</guid><dc:creator><![CDATA[Ro75]]></dc:creator><pubDate>Mon, 25 May 2026 17:54:24 GMT</pubDate></item><item><title><![CDATA[Reply to Kontakte (Cards) on Mon, 25 May 2026 12:02:06 GMT]]></title><description><![CDATA[<p dir="auto">mit einem Basic - String (unescaped). Hier VIS1.<br />
<img src="/assets/uploads/files/1779710504895-76ae27c0-3e71-4d7d-b2de-9f15c0dff723-image.jpeg" alt="76ae27c0-3e71-4d7d-b2de-9f15c0dff723-image.jpeg" class=" img-fluid img-markdown" /></p>
<p dir="auto">Ro75.</p>
]]></description><link>https://forum.iobroker.net/post/1341353</link><guid isPermaLink="true">https://forum.iobroker.net/post/1341353</guid><dc:creator><![CDATA[Ro75]]></dc:creator><pubDate>Mon, 25 May 2026 12:02:06 GMT</pubDate></item><item><title><![CDATA[Reply to Kontakte (Cards) on Mon, 25 May 2026 11:57:39 GMT]]></title><description><![CDATA[<blockquote>
<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/ro75" aria-label="Profile: Ro75">@<bdi>Ro75</bdi></a> <a href="/post/1341349">sagte</a>:</p>
<p dir="auto">die sich direkt in VIS oder jede andere Visualisierung einfügen lassen.</p>
</blockquote>
<p dir="auto">und wie?</p>
]]></description><link>https://forum.iobroker.net/post/1341352</link><guid isPermaLink="true">https://forum.iobroker.net/post/1341352</guid><dc:creator><![CDATA[sigi234]]></dc:creator><pubDate>Mon, 25 May 2026 11:57:39 GMT</pubDate></item></channel></rss>