diff --git a/pruef-buehne.png b/pruef-buehne.png index 8cbe579e..d1c0b8f3 100644 Binary files a/pruef-buehne.png and b/pruef-buehne.png differ diff --git a/pruef-handy.png b/pruef-handy.png index 05c7546b..bd836b28 100644 Binary files a/pruef-handy.png and b/pruef-handy.png differ diff --git a/pruef-kalender-computer.png b/pruef-kalender-computer.png index 16d48593..f4553ed8 100644 Binary files a/pruef-kalender-computer.png and b/pruef-kalender-computer.png differ diff --git a/pruef-kalender-handy.png b/pruef-kalender-handy.png index d082b86a..3f2fe6d9 100644 Binary files a/pruef-kalender-handy.png and b/pruef-kalender-handy.png differ diff --git a/pruef-start-computer.png b/pruef-start-computer.png index 8c0bb464..9c6e59b5 100644 Binary files a/pruef-start-computer.png and b/pruef-start-computer.png differ diff --git a/pruef-start-creator.png b/pruef-start-creator.png index 15a0d815..fd8cabed 100644 Binary files a/pruef-start-creator.png and b/pruef-start-creator.png differ diff --git a/pruef-start-handy.png b/pruef-start-handy.png index df96c331..22a8f49e 100644 Binary files a/pruef-start-handy.png and b/pruef-start-handy.png differ diff --git a/pruef-steckbrief.png b/pruef-steckbrief.png index aff6306a..e71cd092 100644 Binary files a/pruef-steckbrief.png and b/pruef-steckbrief.png differ diff --git a/server/pruef-start-ansicht.mjs b/server/pruef-start-ansicht.mjs index acda2c99..55290dc1 100644 --- a/server/pruef-start-ansicht.mjs +++ b/server/pruef-start-ansicht.mjs @@ -162,7 +162,12 @@ for (const [breite, wie] of [[1440, "Computer"], [390, "Handy"]]) { console.log(" Zahlen: ", r.kacheln.filter((k) => k.zahl).map((k) => `${k.name}=${k.zahl}${k.warn ? "!" : ""}`).join(" · ")); ok(r.gruppen.length === 3, `drei Gruppen (${r.gruppen.length})`); - ok(r.kacheln.length === 17, `siebzehn Kacheln (${r.kacheln.length})`); + /* Achtzehn seit dem 01.09.2026: Der eigene Steckbrief hat eine eigene + Kachel bekommen, weil er der PERSON gehoert und nicht der + Creator-Akte. Die Farbtoene wurden dafuer neu gerechnet (18 statt + 17 Winkel), nicht eine Farbe dazuerfunden -- der kleinste Abstand + zweier Nachbarn liegt weiterhin bei 100 Grad. */ + ok(r.kacheln.length === 18, `achtzehn Kacheln (${r.kacheln.length})`); ok(/^(Guten Morgen|Guten Mittag|Guten Tag|Guten Abend|Späten Abend|Noch wach), Filipe$/ .test(r.gruss || ""), `Begrüßung nach Tageszeit ("${r.gruss}")`); /* Der Name muss eigenständig ausgezeichnet sein -- er ist das Wichtigste diff --git a/server/pruef-steckbrief.mjs b/server/pruef-steckbrief.mjs index ebaa9a80..5a1d5a9a 100644 --- a/server/pruef-steckbrief.mjs +++ b/server/pruef-steckbrief.mjs @@ -161,11 +161,36 @@ console.log("\n=== Sichtbarkeit"); /* ---------- Das Bild ------------------------------------------------------ */ console.log("\n=== Profilbild"); -/* Ein winziges, echtes PNG (1x1). Von Hand gebaut, damit die Pruefung - keine Datei von aussen braucht. */ -const echtesPng = Buffer.from( - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", - "base64"); +/* Ein GROSSES echtes PNG (1200x1200). + + Der erste Anlauf nahm ein 1x1-Pixel-Bild -- klein, schnell, von Hand + gebaut. Es hat aber den schlimmsten Fehler durchgelassen, den es beim + Profilbild gibt: Die Regel .wer__bild in der Kopfleiste fehlte + komplett, ein ohne Groessenangabe nimmt seine natuerliche + Groesse an, und das Bild lag ueber der halben Startseite. Ein + 1x1-Bild kann nichts ueberdecken -- die Pruefung war gruen, der + Fehler war da, und gesehen hat ihn Filipe. + + Deshalb jetzt ein Bild in der Groesse, die wirklich hochgeladen wird. + Erzeugt statt eingecheckt: keine Binaerdatei im Repo, und die Groesse + steht sichtbar hier. */ +const echtesPng = await (async () => { + const { chromium: c } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); + const b = await c.launch(); + const s2 = await b.newPage(); + await s2.setContent(""); + const daten = await s2.evaluate(() => { + const c2 = document.createElement("canvas"); + c2.width = 1200; c2.height = 1200; + const g = c2.getContext("2d"); + g.fillStyle = "#c0392b"; g.fillRect(0, 0, 1200, 1200); + g.fillStyle = "#ffffff"; g.fillRect(200, 200, 800, 800); + return c2.toDataURL("image/png"); + }); + await b.close(); + return Buffer.from(daten.split(",")[1], "base64"); +})(); const bildHoch = (keks, puffer) => hole("/workspace/api/steckbrief/bild", keks, { method: "POST", headers: { "Content-Type": "application/octet-stream" }, body: puffer }); @@ -283,6 +308,28 @@ const browser = await chromium.launch(); ok(r.hatBild, "das hochgeladene Bild wird angezeigt"); ok(/50%/.test(r.bildRund), `und ist rund (${r.bildRund})`); ok(r.obenBild, "es steht auch oben rechts in der Kopfleiste"); + /* UND ES BLEIBT DORT. Das eigentliche Ergebnis des Fehlers vom + 01.09.2026: Ein 1200 px grosses Bild in einem 28 px grossen Feld + muss 28 px gross sein -- nicht 1200. Gemessen wird das echte + Rechteck, nicht die Absicht. */ + const bildMass = await seite.evaluate(() => { + const b = document.querySelector(".wer__bild"); + if (!b) return null; + const r = b.getBoundingClientRect(); + const eltern = b.parentElement.getBoundingClientRect(); + return { b: Math.round(r.width), h: Math.round(r.height), + eb: Math.round(eltern.width), eh: Math.round(eltern.height), + ueber: Math.round(Math.max(r.right - eltern.right, r.bottom - eltern.bottom)) }; + }); + ok(bildMass && bildMass.b <= bildMass.eb + 1 && bildMass.h <= bildMass.eh + 1, + `das Bild bleibt in seinem Feld (${bildMass?.b}x${bildMass?.h} in ` + + `${bildMass?.eb}x${bildMass?.eh})`); + ok(bildMass && bildMass.ueber <= 1, + `und ragt nirgends heraus (${bildMass?.ueber}px)`); + /* Auch die ganze Seite darf davon nichts merken. */ + const seitenUeber = await seite.evaluate(() => + document.documentElement.scrollWidth - document.documentElement.clientWidth); + ok(seitenUeber <= 0, `die Seite laeuft dadurch nicht ueber (${seitenUeber}px)`); ok(r.ueber504 <= 0, `nichts steht über (${r.ueber504}px)`); ok(konsole.length === 0, `keine Fehler in der Konsole${konsole.length ? " -> " + konsole.join(" | ") : ""}`); @@ -304,27 +351,71 @@ const browser = await chromium.launch(); await kontext.close(); } -/* Ein Scout hat denselben Steckbrief -- er ist nicht nur fuer Creator. */ -{ +/* --------------------------------------------------------------------- + ZWEI SEITEN, ZWEI DINGE (Fund vom 01.09.2026) + + Auf profil.html steht die Betreuungsakte eines ANDEREN Menschen. Der + eigene Steckbrief mittendrin ergab zwei Personen auf einem Bildschirm: + oben "Profil: SpongBobSchwammKopf", darunter "MEIN PROFIL: Dogfather". + Niemand konnte sagen, welche Angabe zu wem gehoert. + + Deshalb: Scouts, Manager und DogFather haben den Steckbrief auf einer + EIGENEN Seite. Auf der Akte darf er bei ihnen nicht mehr auftauchen. + Ein Creator behaelt beides zusammen -- er hat nur eine Seite und sieht + dort ausschliesslich sich selbst. + --------------------------------------------------------------------- */ +console.log("\n=== Steckbrief und Creator-Akte sind getrennt"); +for (const [rolle, code, name, erwartetAufAkte] of [ + ["admin", "CODE-DOGI-0001", "Filipe", false], + ["scout", "CODE-SCOU-0001", "Patrick", false], + ["creator", "CODE-ANNA-0001", "Tili", true], +]) { const kontext = await browser.newContext({ viewport: { width: 1280, height: 900 } }); const seite = await kontext.newPage(); await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); - await seite.click('.rolle[data-rolle="scout"]'); - await seite.fill("#code", "CODE-SCOU-0001"); + await seite.click(`.rolle[data-rolle="${rolle}"]`); + await seite.fill("#code", code); await seite.click("button[type=submit]"); await seite.waitForURL("**/start.html", { timeout: 15000 }); + await seite.waitForTimeout(700); + + /* 1. Die Kacheln auf der Startseite. */ + const kacheln = await seite.evaluate(() => + [...document.querySelectorAll(".kachel__name")].map((k) => k.textContent)); + if (erwartetAufAkte) { + ok(!kacheln.includes("Mein Steckbrief") && kacheln.includes("Mein Profil"), + `${name} (${rolle}): eine Kachel "Mein Profil" (${kacheln.filter((k) => /Profil|Steckbrief/.test(k)).join(", ")})`); + } else { + ok(kacheln.includes("Mein Steckbrief") && kacheln.includes("Creator-Profile"), + `${name} (${rolle}): getrennte Kacheln (${kacheln.filter((k) => /Profil|Steckbrief/.test(k)).join(", ")})`); + } + + /* 2. Auf der Akte -- steht dort der eigene Steckbrief? */ await seite.goto(BASIS + "/workspace/profil.html", { waitUntil: "networkidle" }); - await seite.waitForTimeout(800); - const r = await seite.evaluate(() => ({ - sichtbar: !document.getElementById("steckbrief")?.hidden, + await seite.waitForTimeout(900); + const aufAkte = await seite.evaluate(() => + document.getElementById("steckbrief")?.hidden === false); + ok(aufAkte === erwartetAufAkte, + ` und auf der Akte ${erwartetAufAkte ? "steht" : "steht KEIN"} eigener Steckbrief (${aufAkte})`); + + /* 3. Die eigene Seite zeigt ihn -- fuer jede Rolle. */ + await seite.goto(BASIS + "/workspace/steckbrief.html", { waitUntil: "networkidle" }); + await seite.waitForTimeout(900); + const eigen = await seite.evaluate(() => ({ + da: document.getElementById("steckbrief")?.hidden === false, name: document.getElementById("sb-name")?.textContent, - rolle: document.getElementById("sb-rolle")?.textContent, })); - ok(r.sichtbar && r.name === "Patrick" && r.rolle === "Scout", - `auch ein Scout hat sein Profil (${r.name} / ${r.rolle})`); + ok(eigen.da && eigen.name === name, + ` die eigene Seite zeigt ${name} (${eigen.name})`); await kontext.close(); } +/* Der alte Block an dieser Stelle prüfte, ob ein Scout seinen Steckbrief + auf profil.html sieht. Seit der Trennung sieht er ihn dort ausdruecklich + NICHT mehr -- die Pruefung darueber deckt beide Seiten je Rolle ab und + ersetzt ihn vollstaendig. Eine Pruefung stehen zu lassen, die das + Gegenteil des gewollten Verhaltens verlangt, waere schlimmer als keine. */ + await browser.close(); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } console.log(fehler ? `\n${fehler} Prüfung(en) fehlgeschlagen.` : "\nAlles in Ordnung."); diff --git a/server/pruef-workspace-seiten.mjs b/server/pruef-workspace-seiten.mjs index 10f2d820..cf299867 100644 --- a/server/pruef-workspace-seiten.mjs +++ b/server/pruef-workspace-seiten.mjs @@ -46,7 +46,7 @@ const browser = await chromium.launch(); .inhalt--breit, .leise) am 31.08.2026 von aufgaben.css nach start.css gewandert sind -- faellt dabei etwas herunter, sieht man es hier. */ const SEITEN = ["start.html", "uebersicht.html", "aufgaben.html", "kalender.html", - "personen.html", "profil.html", "wissen.html", "dateien.html", + "personen.html", "profil.html", "steckbrief.html", "wissen.html", "dateien.html", "calls.html", "startcheck.html", "report.html", "scouting.html", "automation.html", "content.html", "bereich.html?b=live"]; diff --git a/server/workspace.js b/server/workspace.js index 089aa3e1..4ba4e25f 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -742,6 +742,10 @@ const GESCHUETZT = { "/workspace/aufgaben.html": null, "/workspace/personen.html": ["admin", "manager"], "/workspace/profil.html": ["admin", "manager", "scout", "creator"], + /* Der eigene Steckbrief -- jede Rolle hat einen. Ein Creator wird + dort nicht hingeschickt (er sieht seinen auf profil.html), darf die + Seite aber aufrufen: Sie zeigt ihm dasselbe, nur ohne die Akte. */ + "/workspace/steckbrief.html": ["admin", "manager", "scout", "creator"], "/workspace/kalender.html": null, "/workspace/dateien.html": null, "/workspace/bereich.html": ["admin", "manager", "scout", "creator"], diff --git a/tools/kachel-farben.mjs b/tools/kachel-farben.mjs index e42779f8..330fcd45 100644 --- a/tools/kachel-farben.mjs +++ b/tools/kachel-farben.mjs @@ -49,7 +49,12 @@ const imRaum = (rgb) => rgb.every((v) => v >= -0.001 && v <= 1.001); const hex = (rgb) => "#" + rgb.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join(""); -const winkel = (i) => (i * 360 / 17 + 250) % 360; +/* 01.09.2026: 18 statt 17. Der eigene Steckbrief hat eine eigene + Kachel bekommen (vorher teilte er sich die Seite mit der + Creator-Akte). Damit aendert sich der Abstand zwischen allen Toenen + -- deshalb wird hier neu gerechnet und nicht eine Farbe dazuerfunden. */ +const ANZAHL = 18; +const winkel = (i) => (i * 360 / ANZAHL + 250) % 360; function farbe(i) { let C = C_MAX; while (C > 0.02 && !imRaum(oklchZuRgb(L, C, winkel(i)))) C -= 0.004; @@ -58,7 +63,7 @@ function farbe(i) { const KACHELN = [ "Dashboard", "Aufgaben", "Kalender", "Calls", "Dateien", - "Creator-Profile", "Start-Check", "LIVE", "Content", "Community", + "Mein Steckbrief", "Creator-Profile", "Start-Check", "LIVE", "Content", "Community", "Technik", "Schutz", "Reports", "Personen", "Scout", "Bibliothek", "Automationen", ]; @@ -69,7 +74,8 @@ const N = (n) => KACHELN.indexOf(n); const NACHBARN = [ ["Dashboard", "Aufgaben"], ["Dashboard", "Kalender"], ["Dashboard", "Calls"], ["Aufgaben", "Dateien"], ["Kalender", "Calls"], ["Calls", "Dateien"], - ["Creator-Profile", "Start-Check"], ["Start-Check", "LIVE"], + ["Mein Steckbrief", "Creator-Profile"], ["Creator-Profile", "Start-Check"], + ["Mein Steckbrief", "LIVE"], ["Start-Check", "LIVE"], ["Start-Check", "Content"], ["Content", "Community"], ["Community", "Technik"], ["Schutz", "Reports"], ["Creator-Profile", "Content"], ["Start-Check", "Community"], ["LIVE", "Technik"], ["Content", "Schutz"], ["Community", "Reports"], @@ -112,12 +118,12 @@ const zufall = () => (saat = (saat * 1103515245 + 12345) % 2147483648) / 2147483 let beste = null, besteNote = -1; for (let lauf = 0; lauf < 900; lauf++) { - const z = [...Array(17).keys()]; + const z = [...Array(ANZAHL).keys()]; for (let i = 16; i > 0; i--) { const j = Math.floor(zufall() * (i + 1)); [z[i], z[j]] = [z[j], z[i]]; } let note = bewerte(z), besser = true; while (besser) { besser = false; - for (let a = 0; a < 17; a++) for (let b = a + 1; b < 17; b++) { + for (let a = 0; a < ANZAHL; a++) for (let b = a + 1; b < ANZAHL; b++) { [z[a], z[b]] = [z[b], z[a]]; const neu = bewerte(z); if (neu > note) { note = neu; besser = true; } else { [z[a], z[b]] = [z[b], z[a]]; } @@ -140,4 +146,4 @@ KACHELN.forEach((k, i) => { console.log("\n--- fuer start.js (ton je Kachel) ---"); console.log(JSON.stringify(Object.fromEntries(KACHELN.map((k, i) => [k, beste[i] + 1])))); console.log("\n--- alle Farben in der Reihenfolge der Plaetze ---"); -console.log([...Array(17).keys()].map((i) => farbe(i)).join(",")); +console.log([...Array(ANZAHL).keys()].map((i) => farbe(i)).join(",")); diff --git a/workspace/assets/css/start.css b/workspace/assets/css/start.css index a7fda168..d68cd358 100644 --- a/workspace/assets/css/start.css +++ b/workspace/assets/css/start.css @@ -601,23 +601,24 @@ body.start > main { position: relative; z-index: 1; } data-ton: Dieselbe Nummer färbt die Kachel und die Hinweiszeile, die dorthin führt. Eine Farbe gehört hier zu einem Bereich, nicht zu einem Bauteil. */ -[data-ton="1"] { --ton: #3e95e7; } /* 250° */ -[data-ton="2"] { --ton: #7089ea; } /* 271° */ -[data-ton="3"] { --ton: #947de2; } /* 292° */ -[data-ton="4"] { --ton: #b173d0; } /* 314° */ -[data-ton="5"] { --ton: #c66ab5; } /* 335° */ -[data-ton="6"] { --ton: #d56694; } /* 356° */ -[data-ton="7"] { --ton: #dc666f; } /* 17° */ -[data-ton="8"] { --ton: #db6c48; } /* 38° */ -[data-ton="9"] { --ton: #d17717; } /* 59° */ -[data-ton="10"] { --ton: #ba8610; } /* 81° */ -[data-ton="11"] { --ton: #a29205; } /* 102° */ -[data-ton="12"] { --ton: #809e23; } /* 123° */ -[data-ton="13"] { --ton: #50a752; } /* 144° */ -[data-ton="14"] { --ton: #07aa7c; } /* 165° */ -[data-ton="15"] { --ton: #12a69b; } /* 186° */ -[data-ton="16"] { --ton: #04a3b4; } /* 208° */ -[data-ton="17"] { --ton: #059ece; } /* 229° */ +[data-ton="1"] { --ton: #3e95e7; } +[data-ton="2"] { --ton: #6e89eb; } +[data-ton="3"] { --ton: #917ee4; } +[data-ton="4"] { --ton: #ac74d3; } +[data-ton="5"] { --ton: #c26cbb; } +[data-ton="6"] { --ton: #d1679d; } +[data-ton="7"] { --ton: #da657c; } +[data-ton="8"] { --ton: #dc6958; } +[data-ton="9"] { --ton: #d67130; } +[data-ton="10"] { --ton: #c67f09; } +[data-ton="11"] { --ton: #b08c09; } +[data-ton="12"] { --ton: #97970e; } +[data-ton="13"] { --ton: #72a234; } +[data-ton="14"] { --ton: #3ea95e; } +[data-ton="15"] { --ton: #0ba985; } +[data-ton="16"] { --ton: #08a6a0; } +[data-ton="17"] { --ton: #0aa3b6; } +[data-ton="18"] { --ton: #0f9ecf; } /* Start-Check */ /* Rückfallwert für alles, was einen Ton braucht, aber keine Nummer hat. Steht bewusst in :where() -- damit zählt die Zeile beim Wettstreit der @@ -1151,6 +1152,13 @@ body.start > main { position: relative; z-index: 1; } } .wer__zeichen { flex: none; + /* overflow: hidden ist hier PFLICHT, nicht Kosmetik: Darin liegt das + Profilbild, und ohne diese Zeile stand es in seiner Originalgroesse + ueber der halben Seite. Genau das ist am 01.09.2026 passiert, als + Patrick sein Bild hochgeladen hat -- ein roter Balken quer ueber die + Startseite. */ + overflow: hidden; + position: relative; display: grid; place-items: center; width: 28px; height: 28px; border-radius: 9px; font-size: .8rem; font-weight: 700; color: #eaf3ff; @@ -1160,6 +1168,25 @@ body.start > main { position: relative; z-index: 1; } border: 1px solid color-mix(in srgb, var(--rollen-ton) 55%, transparent); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .16); } +/* Das Profilbild in der Kopfleiste. + + DIESE REGEL HAT GEFEHLT. Das Element wurde in kopf.js erzeugt, die + Regel dazu nie geschrieben -- ein ohne Groessenangabe nimmt + seine natuerliche Groesse an, bei einem Handyfoto also mehrere + tausend Pixel. Es lag damit ueber der ganzen Seite. + + Warum es keine Pruefung gefunden hat: Sie lud ein 1x1-Pixel-PNG + hoch. Ein Bild dieser Groesse kann nichts ueberdecken. Die Pruefung + arbeitet jetzt mit einem 1200x1200-Bild -- so gross wie das, was + wirklich hochgeladen wird. */ +.wer__bild { + position: absolute; inset: 0; + width: 100%; height: 100%; + object-fit: cover; + display: block; + border-radius: inherit; +} + .wer__name { font-size: .88rem; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/workspace/assets/js/bereiche.js b/workspace/assets/js/bereiche.js index 5c82b6a4..6225c7e5 100644 --- a/workspace/assets/js/bereiche.js +++ b/workspace/assets/js/bereiche.js @@ -27,6 +27,11 @@ kalender: ['M4 6.5h16v13H4z', 'M4 10.5h16', 'M8.5 3.5v4', 'M15.5 3.5v4'], calls: ['M3.5 7h11v10h-11z', 'M14.5 11l6-3.5v9l-6-3.5z'], dateien: ['M3.5 6.5h5.5l2 2h9.5v11h-17z'], + /* Der Steckbrief: eine Person IN einer Karte -- unterscheidbar vom + Profil-Zeichen (nur die Person), damit die beiden Kacheln nicht + gleich aussehen. */ + steckbrief: ['M3.5 5.5h17v13h-17z', 'M8.5 11.2a1.9 1.9 0 1 0 0-3.8 1.9 1.9 0 0 0 0 3.8z', + 'M5.6 16.2c0-1.7 1.3-3 2.9-3s2.9 1.3 2.9 3', 'M14.5 9.5h4', 'M14.5 13h4'], profil: ['M12 7a3.2 3.2 0 1 0 0 6.4A3.2 3.2 0 0 0 12 7z', 'M5.5 20c.7-3.3 3.3-5.3 6.5-5.3s5.8 2 6.5 5.3'], startcheck: ['M6 4.5h12v15H6z', 'M9.2 9.6l1.7 1.7 3.6-3.7', 'M9.2 15.2h5.6'], @@ -78,13 +83,13 @@ bereiche: [ { name: 'Dashboard', unter: 'Alle Creator auf einen Blick', zeichen: 'dashboard', ton: 3, gross: true, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'uebersicht.html', szene: 'showbuehne' }, - { name: 'Aufgaben', unter: 'Offen, in Arbeit, Review', zeichen: 'aufgaben', ton: 9, + { name: 'Aufgaben', unter: 'Offen, in Arbeit, Review', zeichen: 'aufgaben', ton: 11, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'aufgaben.html', szene: 'garage' }, - { name: 'Kalender', unter: 'Termine und Calls', zeichen: 'kalender', ton: 15, + { name: 'Kalender', unter: 'Termine und Calls', zeichen: 'kalender', ton: 16, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'kalender.html', szene: 'skyline' }, - { name: 'Calls & Protokolle', unter: 'Gespräche mit To-dos', zeichen: 'calls', ton: 8, + { name: 'Calls & Protokolle', unter: 'Gespräche mit To-dos', zeichen: 'calls', ton: 9, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'calls.html', szene: 'lounge' }, - { name: 'Dateien', unter: 'Ablage und Freigaben', zeichen: 'dateien', ton: 16, + { name: 'Dateien', unter: 'Ablage und Freigaben', zeichen: 'dateien', ton: 2, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'dateien.html', szene: 'arena' }, ], }, @@ -92,23 +97,43 @@ name: 'Rund um den Creator', unter: 'Die Betreuungsakte – Stand, Zahlen, Verlauf', bereiche: [ - { name: 'Creator-Profile', unter: 'Ziele und 90-Tage-Plan', zeichen: 'profil', ton: 4, + /* ZWEI VERSCHIEDENE DINGE, zwei Kacheln (01.09.2026). + + Vorher war beides dieselbe Seite, und das war verwirrend: + Oben stand "Profil: SpongBobSchwammKopf" und mitten darin + "MEIN PROFIL - Dogfather". Zwei Personen auf einem Bildschirm, + ohne dass klar war, welche gerade gemeint ist. + + Es sind auch wirklich zwei Sachen: + MEIN STECKBRIEF gehoert MIR -- Bild, ein Satz ueber mich, + meine Kanaele. Fuehre ich selbst, hat + jeder (auch Scouts und Manager). + CREATOR-PROFILE die BETREUUNGSAKTE eines Creators -- + Ziele, 90-Tage-Plan, interne Notizen. + Fuehren die Betreuer. + + Ein Creator sieht nur seine eigene Akte (dort steht auch sein + Steckbrief mit drin, er hat ja nur eine Seite). Alle anderen + bekommen beide Kacheln getrennt. */ + { name: 'Mein Steckbrief', unter: 'Dein Bild und deine Kanäle', zeichen: 'steckbrief', ton: 15, + rollen: ['admin', 'manager', 'scout'], ziel: 'steckbrief.html', szene: 'halle' }, + { name: 'Creator-Profile', unter: 'Ziele und 90-Tage-Plan', zeichen: 'profil', ton: 8, rollen: ['admin', 'manager', 'scout'], ziel: 'profil.html', szene: 'halle' }, - { name: 'Mein Profil', unter: 'Deine Ziele und Pläne', zeichen: 'profil', ton: 4, + { name: 'Mein Profil', unter: 'Dein Steckbrief, Ziele und Pläne', zeichen: 'profil', ton: 8, rollen: ['creator'], ziel: 'profil.html', szene: 'halle' }, - { name: 'Start-Check', unter: 'Erstanalyse, 16 Punkte', zeichen: 'startcheck', ton: 14, + { name: 'Start-Check', unter: 'Erstanalyse, 16 Punkte', zeichen: 'startcheck', ton: 18, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'startcheck.html', szene: 'wald' }, - { name: 'LIVE-Analyse', unter: 'Vorher, während, danach', zeichen: 'live', ton: 6, + { name: 'LIVE-Analyse', unter: 'Vorher, während, danach', zeichen: 'live', ton: 7, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=live', szene: 'portal' }, - { name: 'Content-Planung', unter: 'Idee bis Upload', zeichen: 'content', ton: 10, + { name: 'Content-Planung', unter: 'Idee bis Upload', zeichen: 'content', ton: 13, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'content.html', szene: 'portal' }, - { name: 'Community', unter: 'Moderation und Konflikte', zeichen: 'community', ton: 5, + { name: 'Community', unter: 'Moderation und Konflikte', zeichen: 'community', ton: 6, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=community', szene: 'lounge' }, - { name: 'Technik', unter: 'Setup, Fehler, Lösungen', zeichen: 'technik', ton: 11, + { name: 'Technik', unter: 'Setup, Fehler, Lösungen', zeichen: 'technik', ton: 12, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=technik', szene: 'garage' }, { name: 'Schutz & Regeln', unter: 'Richtlinien und Vorfälle', zeichen: 'schutz', ton: 1, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'bereich.html?b=schutz', szene: 'halle' }, - { name: 'Reports & Review', unter: 'Woche und Entscheidung', zeichen: 'reports', ton: 13, + { name: 'Reports & Review', unter: 'Woche und Entscheidung', zeichen: 'reports', ton: 14, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'report.html', szene: 'showbuehne' }, ], }, @@ -124,13 +149,13 @@ nameCreator: 'Wissen', unterCreator: 'Nachschlagen, wenn du etwas brauchst', bereiche: [ - { name: 'Personen & Zugänge', unter: 'Rollen und Codes', zeichen: 'personen', ton: 7, + { name: 'Personen & Zugänge', unter: 'Rollen und Codes', zeichen: 'personen', ton: 10, rollen: ['admin', 'manager'], ziel: 'personen.html', szene: 'halle' }, - { name: 'Scout-Pipeline', unter: 'Entdeckt bis übergeben', zeichen: 'scouting', ton: 12, + { name: 'Scout-Pipeline', unter: 'Entdeckt bis übergeben', zeichen: 'scouting', ton: 17, rollen: ['admin', 'manager', 'scout'], ziel: 'scouting.html', szene: 'wald' }, - { name: 'Wissens-Bibliothek', unter: 'Anleitungen und PDFs', zeichen: 'wissen', ton: 2, + { name: 'Wissens-Bibliothek', unter: 'Anleitungen und PDFs', zeichen: 'wissen', ton: 4, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'wissen.html', szene: 'arena' }, - { name: 'Automationen', unter: 'Was von allein passiert', zeichen: 'automation', ton: 17, + { name: 'Automationen', unter: 'Was von allein passiert', zeichen: 'automation', ton: 5, rollen: ['admin', 'manager'], ziel: 'automation.html', szene: 'garage' }, ], }, diff --git a/workspace/assets/js/profil.js b/workspace/assets/js/profil.js index 29bfc3e4..b37e7374 100644 --- a/workspace/assets/js/profil.js +++ b/workspace/assets/js/profil.js @@ -135,194 +135,6 @@ })(); })(); -/* =================================================================== - Der eigene Steckbrief. - - Läuft unabhängig vom Creator-Formular darüber: Den Steckbrief hat - JEDER (auch Scouts, Manager und DogFather), die Creator-Akte nur ein - Creator. Deshalb ein eigener, abgeschlossener Block -- fällt einer - von beiden aus, steht der andere weiterhin. - =================================================================== */ -(() => { - 'use strict'; - - const $ = (id) => document.getElementById(id); - const block = $('steckbrief'); - if (!block) return; - - let stand = null; - - async function hole(pfad, optionen = {}) { - const a = await fetch(pfad, { credentials: 'same-origin', ...optionen }); - if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); } - return a; - } - - const melde = (t) => { $('sb-fehler').textContent = t || ''; }; - const sage = (t) => { - $('sb-stand').textContent = t || ''; - if (t) setTimeout(() => { if ($('sb-stand').textContent === t) $('sb-stand').textContent = ''; }, 4000); - }; - - function bildZeigen(quelle, name) { - const feld = $('sb-bild'); - feld.textContent = ''; - if (quelle) { - const b = document.createElement('img'); - b.src = quelle; - b.alt = ''; - /* Fällt das Bild aus, erscheint der Buchstabe -- nicht das kaputte - Bildsymbol des Browsers. */ - b.addEventListener('error', () => { b.remove(); feld.textContent = anfang(name); }); - feld.append(b); - $('sb-bild-weg').hidden = false; - } else { - feld.textContent = anfang(name); - $('sb-bild-weg').hidden = true; - } - } - const anfang = (n) => (n || '?').trim().charAt(0).toUpperCase(); - - function kanaeleBauen(kanaele, vorhanden) { - const ziel = $('sb-kanaele'); - ziel.textContent = ''; - for (const k of kanaele) { - const zeile = document.createElement('div'); - zeile.className = 'kanal'; - zeile.dataset.art = k.art; - - const schild = document.createElement('label'); - schild.className = 'kanal__name'; - schild.setAttribute('for', 'sb-' + k.art); - schild.textContent = k.name; - - const feld = document.createElement('div'); - feld.className = 'kanal__feld'; - const at = document.createElement('span'); - at.className = 'kanal__at'; - at.setAttribute('aria-hidden', 'true'); - at.textContent = '@'; - const eingabe = document.createElement('input'); - eingabe.id = 'sb-' + k.art; - eingabe.type = 'text'; - eingabe.maxLength = 40; - eingabe.autocomplete = 'off'; - eingabe.placeholder = 'dein Name'; - const da = vorhanden.find((v) => v.art === k.art); - eingabe.value = da ? da.handle : ''; - feld.append(at, eingabe); - - zeile.append(schild, feld); - - /* Ein Verweis erscheint erst, wenn wirklich ein Name da ist -- ein - Knopf, der ins Leere führt, ist schlimmer als keiner. */ - if (da) { - const a = document.createElement('a'); - a.className = 'kanal__auf'; - a.href = da.adresse; - a.target = '_blank'; - a.rel = 'noopener noreferrer'; - a.textContent = 'öffnen'; - zeile.append(a); - } - ziel.append(zeile); - } - } - - function zeichne(sb, kanaele) { - stand = sb; - $('sb-name').textContent = sb.name; - $('sb-rolle').textContent = sb.rolle_name; - $('sb-rolle').dataset.rolle = sb.rolle; - $('sb-ueber').value = sb.ueber_mich || ''; - bildZeigen(sb.bild, sb.name); - kanaeleBauen(kanaele, sb.kanaele); - block.hidden = false; - } - - /* ---------- Bild hochladen --------------------------------------------- - Roh im Rumpf statt als multipart -- dasselbe Verfahren wie in der - Dateiablage. Kein Parsen von multipart im Server, keine zusätzliche - Abhängigkeit. */ - $('sb-datei').addEventListener('change', async (e) => { - const datei = e.target.files?.[0]; - if (!datei) return; - melde(''); - if (datei.size > 4 * 1024 * 1024) { - melde('Das Bild ist größer als 4 MB. Bitte ein kleineres nehmen.'); - e.target.value = ''; - return; - } - sage('Bild wird hochgeladen …'); - try { - const a = await hole('/workspace/api/steckbrief/bild', { - method: 'POST', - headers: { 'Content-Type': 'application/octet-stream' }, - body: datei, - }); - const antwort = await a.json().catch(() => ({})); - if (!a.ok) { melde(antwort.fehler || 'Das Hochladen ging nicht.'); sage(''); return; } - bildZeigen(antwort.bild, stand?.name); - sage('Bild gespeichert.'); - /* Auch oben rechts in der Kopfleiste soll sofort das neue Bild - stehen -- sonst müsste man die Seite neu laden, um es zu sehen. */ - const oben = document.querySelector('.wer__zeichen'); - if (oben) { - oben.querySelector('.wer__bild')?.remove(); - const b = document.createElement('img'); - b.className = 'wer__bild'; - b.src = antwort.bild; - b.alt = ''; - oben.append(b); - } - } catch { /* umgeleitet */ } finally { e.target.value = ''; } - }); - - $('sb-bild-weg').addEventListener('click', async () => { - if (!confirm('Profilbild entfernen?')) return; - try { - const a = await hole('/workspace/api/steckbrief/bild', { method: 'DELETE' }); - if (!a.ok) { melde('Entfernen ging nicht.'); return; } - bildZeigen(null, stand?.name); - document.querySelector('.wer__bild')?.remove(); - sage('Bild entfernt.'); - } catch { /* umgeleitet */ } - }); - - /* ---------- Speichern ---------------------------------------------------- */ - - $('sb-speichern').addEventListener('click', async () => { - melde(''); - const knopf = $('sb-speichern'); - const koerper = { ueber_mich: $('sb-ueber').value }; - for (const zeile of document.querySelectorAll('#sb-kanaele .kanal')) { - koerper[zeile.dataset.art] = zeile.querySelector('input').value; - } - knopf.disabled = true; - try { - const a = await hole('/workspace/api/steckbrief/mein', { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(koerper), - }); - const antwort = await a.json().catch(() => ({})); - if (!a.ok) { melde(antwort.fehler || 'Speichern ging nicht.'); return; } - /* Neu zeichnen, damit die "öffnen"-Verweise zu den frisch - eingetragenen Namen sofort erscheinen. */ - zeichne(antwort.steckbrief, KANAELE); - sage('Profil gespeichert.'); - } catch { /* umgeleitet */ } finally { knopf.disabled = false; } - }); - - let KANAELE = []; - - (async () => { - try { - const a = await hole('/workspace/api/steckbrief/mein'); - if (!a.ok) return; - const { steckbrief, kanaele } = await a.json(); - KANAELE = kanaele; - zeichne(steckbrief, kanaele); - } catch { /* umgeleitet oder nicht verfügbar -- die Seite bleibt nutzbar */ } - })(); -})(); +/* Der Steckbrief steht seit dem 01.09.2026 in assets/js/steckbrief.js. + Er gehoert der angemeldeten PERSON, nicht der Creator-Akte auf dieser + Seite -- deshalb eine eigene Datei, die auch steckbrief.html laedt. */ diff --git a/workspace/assets/js/steckbrief.js b/workspace/assets/js/steckbrief.js new file mode 100644 index 00000000..736c596e --- /dev/null +++ b/workspace/assets/js/steckbrief.js @@ -0,0 +1,212 @@ +/* =================================================================== + Der eigene Steckbrief -- Bild, ein Satz ueber sich, die Kanaele. + + Laeuft auf ZWEI Seiten: + steckbrief.html fuer Scouts, Manager und DogFather -- dort ist er + der ganze Inhalt + profil.html fuer Creator -- dort steht er ueber der eigenen + Betreuungsakte + + Warum eine eigene Datei: Vorher hing er hinten an profil.js. Damit + war er an die Creator-Akte gekettet, und auf der Profilseite standen + zwei Personen gleichzeitig -- oben "Profil: SpongBobSchwammKopf" und + mittendrin "MEIN PROFIL: Dogfather". Er gehoert aber nicht zur Akte, + sondern zur angemeldeten Person. + + Der Block laeuft fuer sich: Faellt die Akte aus, steht der Steckbrief + weiterhin -- und umgekehrt. + =================================================================== */ + +(() => { + 'use strict'; + + const $ = (id) => document.getElementById(id); + const block = $('steckbrief'); + if (!block) return; + + /* Auf profil.html traegt der Block data-nur-creator: Dort steht die + Akte eines ANDEREN Menschen, und der eigene Steckbrief hat daneben + nichts zu suchen -- ausser bei einem Creator, der nur diese eine + Seite hat. Wer ihn nicht sehen soll, bekommt ihn gar nicht erst + geladen; die Abfrage unterbleibt ebenfalls. */ + let nurCreator = block.dataset.nurCreator === 'ja'; + + let stand = null; + + async function hole(pfad, optionen = {}) { + const a = await fetch(pfad, { credentials: 'same-origin', ...optionen }); + if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); } + return a; + } + + const melde = (t) => { $('sb-fehler').textContent = t || ''; }; + const sage = (t) => { + $('sb-stand').textContent = t || ''; + if (t) setTimeout(() => { if ($('sb-stand').textContent === t) $('sb-stand').textContent = ''; }, 4000); + }; + + function bildZeigen(quelle, name) { + const feld = $('sb-bild'); + feld.textContent = ''; + if (quelle) { + const b = document.createElement('img'); + b.src = quelle; + b.alt = ''; + /* Fällt das Bild aus, erscheint der Buchstabe -- nicht das kaputte + Bildsymbol des Browsers. */ + b.addEventListener('error', () => { b.remove(); feld.textContent = anfang(name); }); + feld.append(b); + $('sb-bild-weg').hidden = false; + } else { + feld.textContent = anfang(name); + $('sb-bild-weg').hidden = true; + } + } + const anfang = (n) => (n || '?').trim().charAt(0).toUpperCase(); + + function kanaeleBauen(kanaele, vorhanden) { + const ziel = $('sb-kanaele'); + ziel.textContent = ''; + for (const k of kanaele) { + const zeile = document.createElement('div'); + zeile.className = 'kanal'; + zeile.dataset.art = k.art; + + const schild = document.createElement('label'); + schild.className = 'kanal__name'; + schild.setAttribute('for', 'sb-' + k.art); + schild.textContent = k.name; + + const feld = document.createElement('div'); + feld.className = 'kanal__feld'; + const at = document.createElement('span'); + at.className = 'kanal__at'; + at.setAttribute('aria-hidden', 'true'); + at.textContent = '@'; + const eingabe = document.createElement('input'); + eingabe.id = 'sb-' + k.art; + eingabe.type = 'text'; + eingabe.maxLength = 40; + eingabe.autocomplete = 'off'; + eingabe.placeholder = 'dein Name'; + const da = vorhanden.find((v) => v.art === k.art); + eingabe.value = da ? da.handle : ''; + feld.append(at, eingabe); + + zeile.append(schild, feld); + + /* Ein Verweis erscheint erst, wenn wirklich ein Name da ist -- ein + Knopf, der ins Leere führt, ist schlimmer als keiner. */ + if (da) { + const a = document.createElement('a'); + a.className = 'kanal__auf'; + a.href = da.adresse; + a.target = '_blank'; + a.rel = 'noopener noreferrer'; + a.textContent = 'öffnen'; + zeile.append(a); + } + ziel.append(zeile); + } + } + + function zeichne(sb, kanaele) { + stand = sb; + $('sb-name').textContent = sb.name; + $('sb-rolle').textContent = sb.rolle_name; + $('sb-rolle').dataset.rolle = sb.rolle; + $('sb-ueber').value = sb.ueber_mich || ''; + bildZeigen(sb.bild, sb.name); + kanaeleBauen(kanaele, sb.kanaele); + block.hidden = false; + } + + /* ---------- Bild hochladen --------------------------------------------- + Roh im Rumpf statt als multipart -- dasselbe Verfahren wie in der + Dateiablage. Kein Parsen von multipart im Server, keine zusätzliche + Abhängigkeit. */ + $('sb-datei').addEventListener('change', async (e) => { + const datei = e.target.files?.[0]; + if (!datei) return; + melde(''); + if (datei.size > 4 * 1024 * 1024) { + melde('Das Bild ist größer als 4 MB. Bitte ein kleineres nehmen.'); + e.target.value = ''; + return; + } + sage('Bild wird hochgeladen …'); + try { + const a = await hole('/workspace/api/steckbrief/bild', { + method: 'POST', + headers: { 'Content-Type': 'application/octet-stream' }, + body: datei, + }); + const antwort = await a.json().catch(() => ({})); + if (!a.ok) { melde(antwort.fehler || 'Das Hochladen ging nicht.'); sage(''); return; } + bildZeigen(antwort.bild, stand?.name); + sage('Bild gespeichert.'); + /* Auch oben rechts in der Kopfleiste soll sofort das neue Bild + stehen -- sonst müsste man die Seite neu laden, um es zu sehen. */ + const oben = document.querySelector('.wer__zeichen'); + if (oben) { + oben.querySelector('.wer__bild')?.remove(); + const b = document.createElement('img'); + b.className = 'wer__bild'; + b.src = antwort.bild; + b.alt = ''; + oben.append(b); + } + } catch { /* umgeleitet */ } finally { e.target.value = ''; } + }); + + $('sb-bild-weg').addEventListener('click', async () => { + if (!confirm('Profilbild entfernen?')) return; + try { + const a = await hole('/workspace/api/steckbrief/bild', { method: 'DELETE' }); + if (!a.ok) { melde('Entfernen ging nicht.'); return; } + bildZeigen(null, stand?.name); + document.querySelector('.wer__bild')?.remove(); + sage('Bild entfernt.'); + } catch { /* umgeleitet */ } + }); + + /* ---------- Speichern ---------------------------------------------------- */ + + $('sb-speichern').addEventListener('click', async () => { + melde(''); + const knopf = $('sb-speichern'); + const koerper = { ueber_mich: $('sb-ueber').value }; + for (const zeile of document.querySelectorAll('#sb-kanaele .kanal')) { + koerper[zeile.dataset.art] = zeile.querySelector('input').value; + } + knopf.disabled = true; + try { + const a = await hole('/workspace/api/steckbrief/mein', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(koerper), + }); + const antwort = await a.json().catch(() => ({})); + if (!a.ok) { melde(antwort.fehler || 'Speichern ging nicht.'); return; } + /* Neu zeichnen, damit die "öffnen"-Verweise zu den frisch + eingetragenen Namen sofort erscheinen. */ + zeichne(antwort.steckbrief, KANAELE); + sage('Profil gespeichert.'); + } catch { /* umgeleitet */ } finally { knopf.disabled = false; } + }); + + let KANAELE = []; + + (async () => { + try { + const ich = await (await hole('/workspace/api/ich')).json().catch(() => null); + if (nurCreator && ich && ich.rolle !== 'creator') return; + + const a = await hole('/workspace/api/steckbrief/mein'); + if (!a.ok) return; + const { steckbrief, kanaele } = await a.json(); + KANAELE = kanaele; + zeichne(steckbrief, kanaele); + } catch { /* umgeleitet oder nicht verfügbar -- die Seite bleibt nutzbar */ } + })(); +})(); diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 35f29fb4..d6e2e7f8 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -7,9 +7,9 @@ - - - + + + @@ -139,9 +139,9 @@ - - - - + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 25fe0b24..b212ef7c 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -132,9 +132,9 @@ - - - - + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index ed6dcaab..4ce2e7d2 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -107,9 +107,9 @@ - - - - + + + + diff --git a/workspace/calls.html b/workspace/calls.html index fc27fb86..4da5e0a3 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -54,10 +54,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index 48b395a3..b5668e46 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -158,9 +158,9 @@ - - - - + + + + diff --git a/workspace/dateien.html b/workspace/dateien.html index 16179b28..dd4c6f1a 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -93,9 +93,9 @@ - - - - + + + + diff --git a/workspace/index.html b/workspace/index.html index 63b4b1d7..a696878e 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -25,7 +25,7 @@ type="image/webp" media="(max-width: 899px)" /> - + @@ -143,6 +143,6 @@

- + diff --git a/workspace/kalender.html b/workspace/kalender.html index 5fb4b798..b5ebe998 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -147,9 +147,9 @@ - - - - + + + + diff --git a/workspace/personen.html b/workspace/personen.html index 323e39ac..02bd2756 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -121,9 +121,9 @@ - - - - + + + + diff --git a/workspace/profil.html b/workspace/profil.html index f137ef4f..8d4de1cc 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -48,11 +48,16 @@ - -