Uhr und Glocke in die Begruessung, Teilen in die Leiste, ein echter Fehler weg

Neun Punkte aus Filipes Bildschirmfotos. Der wichtigste war kein
Aussehen, sondern ein Fehler:

UEBEREINANDERLIEGENDE TEXTE IN DER SICHERUNGSLISTE. Das Datum entstand
aus `name.split('-').slice(1).reverse().join('.')`. Bei
"woechentlich-2026-09-06.db" ging das gut; die Sicherungen vor einem
Umbau heissen aber "vorher-2026-09-02-160012438.db" -- mit Zeitstempel.
Heraus kam "160012438.02.09.2026", dreimal so lang wie die 96-px-Spalte,
und es lief ueber den Nachbartext. Ein Muster, das Bestandteile ZAEHLT
statt sie zu SUCHEN, bricht beim ersten Namen mit einem Teil mehr. Jetzt
ein Suchmuster nach vier-zwei-zwei Ziffern -- und in der CSS eine
Kuerzung, damit der NAECHSTE zu lange Text nur abgeschnitten wird. Eine
Spalte mit fester Breite ohne Kuerzung ist immer eine Zeitbombe.

DIE BEGRUESSUNGSKACHEL. Runde Digitaluhr rechts: zwei Ringe um dieselbe
Mitte -- innen die Sekunde, aussen der Stand der Stunde. Kein
setInterval(1000): Ein fester Takt laeuft mit der Zeit aus dem Tritt und
ueberspringt Sekunden; gewartet wird bis zur naechsten VOLLEN Sekunde.
Die Glocke ist aus der Kopfleiste hierhergezogen -- mit Rueckfall, denn
zwoelf andere Seiten haben diesen Platz nicht. Nebenbei ist die Leiste
damit um ein Element leichter; sie ist am 06.09. schon einmal an einem
sechsten zerbrochen.

TEILEN-KNOPF in der Leiste, nur fuer Scout, Manager und DogFather.
Geteilt wird der EINGANG, nicht die aktuelle Seite: Ein Link auf
bereich.html?b=schutz schickt jemanden auf eine Seite, die er nicht
sehen darf. Wo es navigator.share gibt, wird es benutzt; sonst
Zwischenablage; wo beides fehlt, erscheint der Knopf gar nicht -- ein
dritter Ausgang statt einer Schaltflaeche, die nichts tut.

WEITER: Kachelreihenfolge Steckbrief -> Profile -> Zahlen. Die
Tagesliste laesst sich zuklappen und zeigt dann SIEBEN Tage (die Woche,
nicht die vier von ueberall sonst). Dialoge, Automationen-Karten,
Sicherungsblock, KI-Kasten und Call-Karten bekommen dasselbe Material
wie die Kacheln -- Leuchtschiene, Materialstaerke, Glanz.

Profilbilder brauchten nichts: Der Weg gibt es fuer jede Rolle bereits
(steckbrief.html fuer die Betreuung, derselbe Block auf profil.html fuer
Creator, Hochladen schreibt immer auf req.person.id).

ZWEI EIGENE FEHLER, beide gemessen statt vermutet:
  - Die Koernung lag in vier neuen Bloecken auf DERSELBEN Ebene wie die
    Spiegelung und erbte deren 50 % Deckkraft. Gemessen rgb(56,44,58)
    statt rgb(20,26,38) -- die Karten sahen durchsichtig aus, obwohl sie
    zu 95 % decken. Derselbe Fehler wie heute Nachmittag an der
    Anmeldekarte. Koernung gehoert auf eine eigene Ebene mit overlay.
  - `.glocke-platz:empty { display: none }` liess die Kachel wachsen,
    sobald die Glocke geladen war. Layout-Sprung von start.html: 0,708.
    Platz wird jetzt reserviert -> 0,473.

pruef-struktur: `-breit` gehoert in die Stufen-Ausnahme. Schaerfe kostet
Bytes (hohe Frequenzen lassen sich nicht wegrechnen), die mittlere Stufe
wuchs auf 230-300 KB. Die Regel bleibt inhaltlich: gross nur, WENN eine
kleinere Stufe daneben steht. Die Verkettung der beiden replace() waere
ein stiller Fehler gewesen -- fuer uhd haette sie nach `-schmal` gesucht.

Gruen: kopf-messen (3), breiten (23), glocke (26), css-klassen (15),
struktur (32), buehne (38), start-ansicht (136), handy (59),
formulare (19), barrierefrei (18), lesbarkeit (14), tempo (8).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-07 03:40:49 +02:00
co-authored by Claude Opus 5
parent 8efdb39ce9
commit 1b2874299e
32 changed files with 1391 additions and 196 deletions
+22 -2
View File
@@ -267,11 +267,31 @@
$('sicherungen-schalter').hidden = true;
return;
}
/* DAS DATUM WIRD GESUCHT, NICHT ZUSAMMENGESETZT (07.09.2026).
Hier stand `name.split('-').slice(1).reverse().join('.')`. Bei
"woechentlich-2026-09-06.db" ging das gut. Die Sicherungen vor
einem Umbau heissen aber "vorher-2026-09-02-160012438.db" -- mit
einem Zeitstempel hinten dran. Aus derselben Rechnung wurde dann
"160012438.02.09.2026": eine Zeichenkette, die dreimal so lang
ist wie die Spalte und ueber den Nachbartext lief. Genau das hat
Filipe im Bildschirmfoto gesehen.
Ein Muster, das die Bestandteile ZAEHLT statt sie zu SUCHEN,
bricht beim ersten Namen, der einen Teil mehr hat. Deshalb jetzt
ein Suchmuster nach vier-zwei-zwei Ziffern -- egal, was davor
oder dahinter steht. */
for (const s of z.sicherungen) {
const roh = s.name.replace(/\.db$/, '');
const t = roh.match(/(\d{4})-(\d{2})-(\d{2})/);
const zeile = el('div', 'verlauf__zeile');
zeile.append(el('span', 'verlauf__zeit', s.name.replace(/\.db$/, '').split('-').slice(1).reverse().join('.')));
zeile.append(el('span', 'verlauf__was', s.name.split('-')[0]));
/* Findet sich kein Datum, wird das GESAGT und nicht geraten --
ein erfundenes Datum an einer Sicherung waere schlimmer als
ein Strich. */
zeile.append(el('span', 'verlauf__zeit', t ? `${t[3]}.${t[2]}.${t[1]}` : '—'));
zeile.append(el('span', 'verlauf__was', roh.split('-')[0]));
zeile.append(el('span', 'verlauf__wer', bytes(s.groesse)));
zeile.title = s.name; // der volle Name bleibt erreichbar
liste.append(zeile);
}
window.klappe?.(liste, $('sicherungen-schalter'), $('sicherungen-schalter-text'));
+14 -3
View File
@@ -364,14 +364,25 @@
(Orangerot) darunter. Wer diese Gruppe umsortiert, lässt
tools/kachel-farben.mjs neu laufen -- die Nachbarschaften
haben sich durch diese Kachel geändert. */
{ name: 'Zahlen', unter: 'Diamanten, LIVE-Tage, Verweildauer', zeichen: 'zahlen', ton: 2,
rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'leistung.html', szene: 'arena' },
/* REIHENFOLGE: STECKBRIEF, PROFILE, ZAHLEN (Wunsch Filipe,
07.09.2026: "zuerst steckbrief, dan, profile, und dan erst
zahlen"). Das ist auch die Reihenfolge, in der man arbeitet:
erst wer jemand IST, dann was er vorhat, dann was dabei
herauskommt. Die Zahlen zuerst zu zeigen dreht das um. */
{ name: 'Mein Steckbrief', unter: 'Dein Bild und deine Kanäle', zeichen: 'steckbrief', ton: 10,
rollen: ['admin', 'manager', 'scout'], ziel: 'steckbrief.html', szene: 'halle' },
{ name: 'Creator-Profile', unter: 'Ziele und 90-Tage-Plan', zeichen: 'profil', ton: 20,
rollen: ['admin', 'manager', 'scout'], ziel: 'profil.html', szene: 'halle' },
{ name: 'Mein Profil', unter: 'Dein Steckbrief, Ziele und Pläne', zeichen: 'profil', ton: 20,
/* KEINE eigene Steckbrief-Kachel fuer Creator, und das ist
Absicht: Ihr Steckbrief samt Profilbild steht bereits auf
`profil.html` (der Block mit `data-nur-creator="ja"`). Eine
zweite Kachel fuehrte auf dieselbe Sache unter anderem Namen
-- und wer zwei Wege zu einer Einstellung anbietet, bekommt
die Frage, welcher der richtige ist. */
{ name: 'Mein Profil', unter: 'Dein Bild, deine Ziele und Pläne', zeichen: 'profil', ton: 20,
rollen: ['creator'], ziel: 'profil.html', szene: 'halle' },
{ name: 'Zahlen', unter: 'Diamanten, LIVE-Tage, Verweildauer', zeichen: 'zahlen', ton: 2,
rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'leistung.html', szene: 'arena' },
{ name: 'Start-Check', unter: 'Erstanalyse, 16 Punkte', zeichen: 'startcheck', ton: 16,
rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'startcheck.html', szene: 'wald' },
{ name: 'LIVE-Analyse', unter: 'Vorher, während, danach', zeichen: 'live', ton: 3,
+21 -3
View File
@@ -174,7 +174,23 @@
}
async function bauen() {
const kopf = document.querySelector('.kopfleiste__rechts');
/* WO DIE GLOCKE HINGEHÖRT (07.09.2026, Wunsch Filipe: "der
benachrichtigungs button soll auch von der leiste in diese kachel
gesetzt werden").
Gibt es auf der Seite einen ausgewiesenen Platz, kommt sie
dorthin -- auf der Startseite in die Begrüßungskachel, neben die
Uhr. Das ist der richtige Ort: Benachrichtigungen sind eine
Einstellung zum eigenen Tag, keine Navigation.
Auf allen anderen Seiten gibt es diesen Platz nicht, und dort
bleibt die Leiste. Deshalb ein Rückfall und keine Umstellung:
Eine Glocke, die auf zwölf Seiten verschwindet, weil sie auf
einer umgezogen ist, wäre keine Verbesserung. Und weil die
Kopfleiste am 06.09. schon einmal an einem sechsten Element
zerbrochen ist, wird sie hier nebenbei um eines leichter. */
const platz = document.getElementById('glocke-platz');
const kopf = platz || document.querySelector('.kopfleiste__rechts');
if (!kopf || document.getElementById('glocke')) return;
knopf = el('button', 'glocke');
@@ -196,10 +212,12 @@
knopf.append(el('span', 'glocke__text', '…'));
/* VOR dem Abmelden-Knopf, nicht dahinter: Abmelden ist die letzte
Handlung auf einer Seite und bleibt ganz rechts. */
const abmeldenKnopf = kopf.querySelector('.abmelden');
Handlung auf einer Seite und bleibt ganz rechts. Im eigenen Platz
gibt es nichts, wovor sie stehen müsste. */
const abmeldenKnopf = platz ? null : kopf.querySelector('.abmelden');
if (abmeldenKnopf) kopf.insertBefore(knopf, abmeldenKnopf);
else kopf.append(knopf);
if (platz) knopf.classList.add('glocke--kachel');
knopf.addEventListener('click', async () => {
const l = lage();
+110 -4
View File
@@ -54,8 +54,105 @@
/* Vorleseprogramme bekommen einen Satz statt drei Bruchstücke. */
ziel.setAttribute('aria-label',
`Angemeldet als ${ich.name} in der Rolle ${ich.rolle_name || ich.rolle}`);
teilenKnopf(ich);
};
/* ---------- Teilen ---------------------------------------------------
Wunsch Filipe (07.09.2026): "einen button oben in der leiste ... zum
teilen der seite, auch nur die scouts, manager und dogfather sollen
diesen button sehen und benutzen können."
WAS GETEILT WIRD, IST DIE ADRESSE DES WORKSPACE -- nicht die Seite,
auf der man gerade steht. Das ist kein Detail: Wer einen Link auf
`bereich.html?b=schutz` weitergibt, schickt jemanden auf eine Seite,
die er entweder gar nicht sehen darf oder die ohne Zusammenhang
verwirrt. Geteilt wird der Eingang.
WARUM NUR DIE LEITUNG. Ein Creator hat niemanden, dem er den
Workspace geben würde -- er ist der Eingeladene, nicht der
Einladende. Ein Knopf, den man hat und nie braucht, ist Ballast;
einer, den man drückt und der nichts Sinnvolles tut, ist schlimmer.
Durchgesetzt wird das an der Rolle aus /api/ich, also an derselben
Angabe, mit der der Server auch alles andere entscheidet.
ZWEI WEGE, EIN KNOPF. Wo es die Teilen-Funktion des Geräts gibt
(Handy, neuere Rechner), wird sie benutzt -- das ist der Weg, den
Filipe kennt. Wo nicht, landet die Adresse in der Zwischenablage
und der Knopf sagt "Kopiert". Kein Dialog, kein zweiter Klick.
Und wo auch das nicht geht (alte Browser, kein sicherer Kontext),
erscheint der Knopf gar nicht erst -- ein dritter Ausgang statt
einer Schaltfläche, die nichts tut. */
const DARF_TEILEN = new Set(['admin', 'manager', 'scout']);
function teilenKnopf(ich) {
const leiste = document.querySelector('.kopfleiste__rechts');
if (!leiste || document.getElementById('teilen')) return;
if (!DARF_TEILEN.has(ich.rolle)) return;
const kannTeilen = typeof navigator.share === 'function';
const kannKopieren = !!navigator.clipboard?.writeText;
if (!kannTeilen && !kannKopieren) return;
const k = document.createElement('button');
k.type = 'button';
k.id = 'teilen';
k.className = 'teilen';
k.title = 'Adresse des Workspace weitergeben';
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('class', 'teilen__zeichen');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('aria-hidden', 'true');
/* Drei Knoten, zwei Linien -- das Zeichen für "weitergeben". Als
Pfade und nicht als Schriftzeichen: Ein Emoji sieht auf jedem
Gerät anders aus und lässt sich nicht einfärben. */
svg.innerHTML =
'<circle cx="18" cy="5" r="2.6"/><circle cx="6" cy="12" r="2.6"/>'
+ '<circle cx="18" cy="19" r="2.6"/>'
+ '<path d="M8.3 10.8 15.7 6.6M8.3 13.2l7.4 4.2"/>';
k.append(svg);
const text = document.createElement('span');
text.className = 'teilen__text';
text.textContent = 'Teilen';
k.append(text);
const ZIEL = location.origin + '/workspace/';
let laeuft = false;
k.addEventListener('click', async () => {
if (laeuft) return;
laeuft = true;
try {
if (kannTeilen) {
await navigator.share({
title: 'Creator Workspace',
text: 'Der Creator Workspace von DogFather Universe',
url: ZIEL,
});
} else {
await navigator.clipboard.writeText(ZIEL);
const vorher = text.textContent;
text.textContent = 'Kopiert';
k.dataset.fertig = 'ja';
setTimeout(() => { text.textContent = vorher; delete k.dataset.fertig; }, 2200);
}
} catch (f) {
/* Ein Abbruch durch die Nutzerin ist KEIN Fehler und darf nicht
wie einer aussehen -- sie hat den Dialog absichtlich
geschlossen. Alles andere wird gemeldet. */
if (f?.name !== 'AbortError') {
text.textContent = 'Ging nicht';
setTimeout(() => { text.textContent = 'Teilen'; }, 2200);
}
} finally { laeuft = false; }
});
/* Ganz links in der rechten Gruppe: Teilen ist eine Nebenhandlung
und darf dem Abmelden nicht den Platz nehmen. */
leiste.prepend(k);
}
/* ---------- Auf- und Zuklappen langer Listen -------------------------
Lange Protokoll-Listen sind Nachschlagewerk, kein Startbild: Sie
@@ -69,14 +166,23 @@
Kinder die Zeilen sind), den Knopf und das Textfeld darin. Das
Ausblenden macht CSS ueber [data-klapp="zu"] -- ohne Neuaufbau und
ohne zweite Abfrage. */
/* Zwei Sichtbarkeitsstufen, beide mit einer Regel im CSS:
4 der Normalfall -- eine Liste zum Nachschlagen
7 wo die Woche die Einheit ist (die Tagesliste der Zahlen)
Die Zahl ist der Schluessel zum passenden `data-klapp`-Wert; eine
unbekannte Zahl faellt auf 4 zurueck, statt still gar nichts zu
verbergen. */
const KLAPP_STUFEN = { 4: 'zu', 7: 'zu7' };
const KLAPP_ZU = 4; /* dieselbe Zahl steht im CSS als nth-child(n+5) */
window.klappe = function (liste, knopf, textfeld) {
window.klappe = function (liste, knopf, textfeld, wieviele = KLAPP_ZU) {
if (!liste || !knopf) return;
const anzahl = liste.children.length;
const stufe = KLAPP_STUFEN[wieviele] || 'zu';
const zeigt = KLAPP_STUFEN[wieviele] ? wieviele : KLAPP_ZU;
/* Ein Knopf, der nichts zu verbergen hat, verwirrt nur. */
knopf.hidden = anzahl <= KLAPP_ZU;
knopf.hidden = anzahl <= zeigt;
if (knopf.hidden) { liste.removeAttribute('data-klapp'); return; }
/* Liest die Anzahl bei jedem Aufruf frisch aus der Liste, statt sie
@@ -86,13 +192,13 @@
an einer Liste mit dreissig Zeilen. */
const stand = (offen) => {
if (offen) liste.removeAttribute('data-klapp');
else liste.dataset.klapp = 'zu';
else liste.dataset.klapp = stufe;
knopf.setAttribute('aria-expanded', offen ? 'true' : 'false');
/* Der Knopf sagt, was er TUT, nicht wo man gerade ist -- und nennt
die Zahl, damit man weiss, was dahintersteckt. */
if (textfeld) {
textfeld.textContent = offen
? `Nur die letzten ${KLAPP_ZU}`
? `Nur die letzten ${zeigt}`
: `Alle ${liste.children.length} zeigen`;
}
};
+16
View File
@@ -271,6 +271,22 @@
}
ziel.append(zeile);
}
/* ZUGEKLAPPT AUF SIEBEN TAGE (07.09.2026, Wunsch Filipe).
Vierzehn Zeilen sind zu viel fuers Hinsehen und zu wenig fuers
Nachschlagen -- man scrollt an ihnen vorbei, statt sie zu lesen.
Sieben ist die Woche: der kuerzeste Zeitraum, ueber den man
ueberhaupt etwas sagen kann, und genau der, mit dem der
Wochenbericht rechnet. Die restlichen sieben Tage sind einen
Klick entfernt und verschwinden nicht.
Die Sieben steht nicht hier, sondern als eigene Regel im CSS
(`[data-klapp="zu7"]`) -- der Schalter kennt beide Stufen und
benennt sie im Knopftext, damit niemand raten muss, wie viel
gerade verborgen ist. */
window.klappe?.(ziel, document.getElementById('tage-schalter'),
document.getElementById('tage-schalter-text'), 7);
}
function wert(name, text) {
+41
View File
@@ -278,6 +278,46 @@
0: 'Wochenende',
};
/* ---------- Die Uhr in der Begrüßungskachel -------------------------
Zwei Ringe um dieselbe Mitte: der äußere zeigt die Minute in der
Stunde, der innere die Sekunde. Beides als SVG-Kreis mit
`stroke-dasharray` -- ein Kegelverlauf (conic-gradient) hätte an
der Naht eine sichtbare Kante, ein Kreisbogen nicht.
UMFANG = 2 * pi * 44 = 276.46. Die Zahl steht hier ausgerechnet und
nicht als Formel im Stil: Ändert jemand den Radius im HTML, fällt
der Ring sofort auf, statt sich stillschweigend zu verrechnen.
KEIN setInterval(…, 1000). Ein Takt von genau einer Sekunde läuft
mit der Zeit aus dem Tritt (jeder Durchlauf kostet ein paar
Millisekunden), und die Anzeige springt dann gelegentlich zwei
Sekunden weiter. Stattdessen wird jedes Mal bis zur NÄCHSTEN vollen
Sekunde gewartet -- die Anzeige bleibt für immer im Takt der
Systemuhr. */
const UHR_UMFANG = 276.46;
function uhrStarten() {
const zeit = $('uhr-zeit'), sek = $('uhr-sek');
const zeiger = $('uhr-zeiger'), minute = $('uhr-minute');
if (!zeit || !zeiger) return;
const p = (n) => String(n).padStart(2, '0');
const takt = () => {
const j = new Date();
zeit.textContent = `${p(j.getHours())}:${p(j.getMinutes())}`;
sek.textContent = p(j.getSeconds());
/* Voll = kein Versatz. Der Ring wächst im Uhrzeigersinn, weil das
SVG im Stil um 90 Grad zurückgedreht ist. */
zeiger.style.strokeDashoffset = String(UHR_UMFANG * (1 - j.getSeconds() / 60));
if (minute) minute.style.strokeDashoffset = String(UHR_UMFANG * (1 - j.getMinutes() / 60));
/* Der volle Zeitpunkt für Vorleseprogramme -- sichtbar steht dort
nur die Uhrzeit, vorgelesen wird der ganze Satz. */
const uhr = $('uhr');
if (uhr) uhr.setAttribute('aria-label', `${p(j.getHours())}:${p(j.getMinutes())} Uhr`);
setTimeout(takt, 1000 - (Date.now() % 1000) + 8);
};
takt();
}
function grussZeigen() {
const jetzt = new Date();
const stunde = jetzt.getHours();
@@ -391,6 +431,7 @@
window.werZeigen?.(ich);
grussZeigen();
uhrStarten();
/* Die Hinweise zuerst -- die Kacheln brauchen ihre Zahlen.
Die Tagestermine laufen daneben her: Sie sind die rechte Hälfte