Schlussreview F4. Zeilen und Bilder der Community-Logos (logo-asset.ts) bleiben bis zum Neuladen der Seite im Speicher. Wurde ein Logo
danach ausgeblendet oder gelöscht, konnte es aus diesem Zwischenspeicher in ein veröffentlichtes Kartenbild oder in ein heruntergeladenes
PNG gelangen.
- renderForGallery (Veröffentlichen), der Download im Editor und der Download in „Meine Karten“ rufen vor dem Laden der Bilder
forgetLogo(config.logo) (falls eines gewählt ist): Zeile und Bild werden neu vom Server geholt, hidden/fehlt wird also neu geprüft.
Veröffentlichen bricht bei einem fehlenden Logo wie bisher mit LOGO_MISSING_PUBLISH ab, „Meine Karten“ meldet es an der Kachel.
- Der Download im Editor berücksichtigt assets.logoMissing wie der Hinweis im Editor: das PNG entsteht ohne das fehlende Logo (die Vorschau
zeigte es bis dahin noch), der Hinweis „Das gewählte Logo ist nicht mehr verfügbar.“ erscheint und die Vorschau zeichnet ohne Logo, damit
PNG und Vorschau dasselbe zeigen. Ist ein Logo wieder da, folgt die Vorschau ebenso. Das gilt nur, solange die Auswahl noch dieselbe ist.
- Tests mit dem echten Zwischenspeicher (nur Zeile und Datei aus logos.ts ersetzt): erst geladen, dann ausgeblendet bzw. gelöscht → neu
geprüft, Veröffentlichen bricht ab / „Meine Karten“ meldet / der Editor lädt ohne Logo herunter, zeigt den Hinweis (auch zusammen mit dem
Größenhinweis) und zeichnet neu; sichtbares Logo wird frisch geholt; Karte ohne Logo holt nichts; „Nur Bild“ bleibt unberührt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schlussreview F3. showDone() entfernt userId und secret aus der Adresse. Wechselte der Nutzer danach die Sprache, baute main.ts die Seite
neu auf und sie las eine Adresse ohne Link: „Der Link ist ungültig oder abgelaufen.“ statt des Erfolgs.
- Ein Stand außerhalb der Seite (`passwordChanged`, Modulvariable) merkt, dass das Passwort gesetzt wurde; ein Neuaufbau zeigt dann wieder
die Erfolgsansicht (in der neuen Sprache) mit „Anmelden“.
- Ein frischer Link (userId und secret in der Adresse) setzt den Stand zurück und zeigt das Formular. Gemerkt wird nur der Erfolg, ein
Fehlschlag nicht.
- Tests: Neuaufbau nach dem Erfolg (deutsch, mehrfach, englisch), frischer Link danach, Seite ohne Link bleibt ungültig, Fehlschlag wird
nicht als Erfolg gemerkt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schlussreview F2. Hat jemand das Logo gelöscht oder ausgeblendet, hinter dem „Mehr laden“ ansetzt (der Cursor), antwortet Appwrite mit
400 `general_cursor_not_found`. „Nochmal versuchen“ wiederholte dann denselben Cursor und scheiterte immer wieder.
- Dieser Fehlertyp beim Nachladen löst jetzt `load('reset')` aus: die erste Seite der aktuellen Ansicht (gleiche Sortierung, gleicher Filter)
wird neu geladen, die Kachel des gewählten Logos bleibt stehen. Kein Fehlertext, kein „Nochmal versuchen“ für den toten Cursor.
- Jeder andere Fehler beim Nachladen und jeder Fehler beim Laden der ersten Seite bleibt wie bisher (Meldung und „Nochmal versuchen“);
die erste Seite hat keinen Cursor, ein Neuladen im Kreis ist ausgeschlossen.
- Tests mit einem Fehler {code: 400, type: 'general_cursor_not_found'}: Neuladen von Seite 1 mit Anfragefolge, Ansicht „Gemeldet“ bleibt,
gewählte Kachel bleibt, Neuladen scheitert selbst, anderer Fehlertyp, Fehlertyp beim ersten Laden.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schlussreview F1. Scheiterte das Laden der Bilder (Netz, Server), bricht draw() ab, solange logos.key nicht zur Auswahl passt: Vorschau
und Miniaturen blieben leer, und ein erneuter Klick auf dieselbe Kachel änderte nichts, weil failedKey dem Schlüssel entsprach.
- Nach einem echten Ladefehler lädt der Editor einmal die Marke allein (loadCardAssets mit logo: null) und zeichnet die Karte damit ohne
Logo (`failed`); scheitert auch das, fehlt die Marke ebenfalls. LOGO_ERROR bleibt stehen, die Auswahl auch. Ein anderes Logo wird nie mit
den Bildern der gescheiterten Auswahl gezeichnet (der Schlüssel entscheidet wie bisher).
- Der Picker meldet jeden Klick, auch auf das schon gewählte Logo: ist es das Logo, dessen Laden scheiterte, hebt der Editor die Sperre
(failedKey) auf und versucht es noch einmal. Ein geladenes Logo wird dabei nicht neu geladen, ein Fehlschlag wiederholt sich nicht im Kreis.
- Tests: Karte ohne Logo nach dem Fehler (Vorschau und vier Miniaturen), Wiederholung per Klick (Erfolg und erneuter Fehlschlag, kein Loop),
Marke lädt auch nicht, anderes Logo nach dem Fehler, Klick auf geladenes Logo lädt nichts neu; Picker meldet den Klick auf die gewählte
Kachel. Zwei bestehende Tests angepasst: „nicht gezeichnet nach Fehler“ gilt nicht mehr, und die Zahl der loadCardAssets-Aufrufe zählt
jetzt den zusätzlichen Lauf für die Marke nicht mehr mit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Task 11: alle sichtbaren Texte von community/, pages/home-page.ts, pages/community-page.ts, pages/cards-page.ts und pages/format.ts
(Texte, aria-label, title, Platzhalter, Alt-Texte, Rückfragen, Meldungen) gehen durch t()/msg(); en.ts bekommt 82 Einträge (214 → 296).
- Konstanten bleiben deutsche Strings per msg() und gehen beim Anzeigen durch t() (publish.ts, publish-dialog.ts, gallery.ts und die Seiten);
geworfene Fehler tragen weiter den deutschen Text, damit die Vergleiche gehen. Funktionen, die Text für Nutzer liefern, übersetzen beim
Zurückgeben (validatePublishForm, publishFailureText, formatDate, reportsLabel). Template-Literale sind Platzhalter geworden ({max},
{reason}, {name}, {title}, {query}, {count}); der deutsche Text bleibt Zeichen für Zeichen derselbe.
- API-Fehler: jeder ApiError wird mit apiErrorText() angezeigt (Veröffentlichen-Dialog, Detailseite, Meine Karten), nach dem Code des Dienstes
statt nach dem deutschen Servertext. Logik, die err.message vergleicht (publishCard, 404 beim Zurückziehen), bleibt unverändert.
- Bekannte Reste aus Task 10 behoben: LOGIN_NEEDED ging in publish-dialog.ts, community-page.ts und cards-page.ts ohne t() an die Oberfläche;
die Meldungen zu Bild lesen/Pixelgrenze/fehlendem Logo (cards-page.ts, GERMAN_MESSAGES in publish-dialog.ts) erscheinen jetzt übersetzt;
die Zahl der Meldungen in der Logo-Infozeile („3 reports“).
- pages/format.ts: formatDate liefert Deutsch dd.mm.yyyy und Englisch „Oct 4, 2026“ (Intl.DateTimeFormat en-US), reportsLabel „1 report“ /
„3 reports“. „Herausgeber“ heißt als Bank „Issuer / bank“.
- logos/prepare-logo.ts: der Helfer problem() ist weg, die sechs Stellen schreiben new LogoError(t(MELDUNG)) selbst (gleiches Verhalten);
so sieht der neue Prüfer unten jede Verwendung einer Konstante.
- i18n/coverage.test.ts: Die Heuristik für deutsche Literale deckt jetzt die ganze Oberfläche ab (account/, card/, community/, editor/, logos/,
pages/, shell/, main.ts, i18n/api-messages.ts). Sie erkennt zusätzlich Wörter mit Klammer („Ausgeblendet (gemeldet)“) und nimmt rel und
Namen in Klammerschreibweise (headers['X-Aircard-Jwt']) aus. Acht begründete Ausnahmen kommen dazu (zwei AppwriteException-Meldungen,
vier interne Logo-Fehler, der Sprachknopf in der Zielsprache). Neu: ein Prüfer über den Syntaxbaum, der verlangt, dass jede Konstante
mit msg() nur in t(), new Error(), Vergleichen, case und Listen/Mengen vorkommt (auch über Importe und Umbenennungen); ohne Ausnahmen.
- Tests: format.test.ts (beide Sprachen), Englisch-Blöcke in publish-dialog, gallery-grid, home-page, community-page, cards-page und
logo-info (Texte, Fehlerwege, „Publishing failed – This card is already published.“, kein deutscher Wörterbuchtext auf der Seite). Die
Hilfen texts()/germanLeft() liegen jetzt in i18n/english-check.ts (vorher in english-ui.test.ts). Der bestehende Englisch-Test in
cards-page.test.ts sucht den Knopf „Download“ statt „Herunterladen“.
- Vitest 53 Dateien, 1812 Tests grün (vorher 1779), tsc und Build sauber.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Task 10: alle sichtbaren Texte von account/, editor/, card/ und der Seite „Passwort neu setzen“ (Texte, aria-label, title, Meldungen,
Rückfragen) gehen durch t()/msg(); en.ts bekommt 87 Einträge.
- account/: Konto-Menü, Login-Dialog (Titel, Reiter, Felder, Meldungen) und errors.ts (errorMessage übersetzt beim Zurückgeben).
validatePassword nutzt {min}/{max}. Der Login-Dialog wird nur einmal gebaut: die Beschriftung des Schließen-Knopfs wird bei jedem
Öffnen in der aktuellen Sprache gesetzt. LOGIN_NEEDED und DEFAULT_CARD_NAME bleiben deutsche Konstanten (msg), angezeigt werden sie
mit t(); der Standardname einer neuen Karte folgt damit der Sprache („My card“).
- editor/: Vorlagen-, Options- und Vorschauspalte, Bild-Upload, Speichern, Download, Rückfragen. Konstanten (upload.ts, download.ts,
editor-page.ts) bleiben deutsche Strings per msg(); validateImageFile, pngSizeWarning und das Speichern („Speichern fehlgeschlagen –
{reason}“) liefern den Text schon übersetzt, Fehler tragen weiter die deutsche Konstante (Vergleiche mit READ_MESSAGE/PIXELS_MESSAGE
gehen weiter) und werden beim Anzeigen mit t(error.message) übersetzt. Zoom „100 %“ / „100%“ und „Bearbeitet: {name}“ mit Platzhaltern.
- card/: Bezeichnungen der Marken (msg) und die Meldungen des Renderers (msg, Anzeige im Editor über t()).
- pages/reset-page.ts, account/login-dialog.ts: Die Meldung „Die Passwörter stimmen nicht überein.“ ist nicht mehr der Schlüssel zum
Fokus: es zählt die Bedingung selbst, nicht der Vergleich mit dem (nun übersetzten) Text.
- i18n/coverage.test.ts: Heuristik für deutsche Text-Literale außerhalb von t()/msg() (Umlaut/ß, mindestens zwei Wörter, einzelnes
großgeschriebenes Wort; gelesen wird der Syntaxbaum) für account/, editor/, card/ und pages/reset-page.ts; Ausnahmen mit Grund, jede
muss noch zutreffen; Scanner mit eigenen Tests. Die Liste der Bereiche ist eine Konstante für Task 11.
- i18n/english-ui.test.ts (neu): Stichproben auf Englisch (Konto-Menü, Login-Dialog, Passwort-Seite, Editor inkl. Fehlerwege) und
„kein deutscher Wörterbuchtext auf der englischen Seite“.
- test-setup.ts: Wer Module mit vi.resetModules() frisch lädt, bekam eine neue Kopie von i18n.ts mit der Browsersprache (happy-dom: en-US);
der Browser meldet im Test jetzt Deutsch, damit auch diese Tests deutsch beginnen. main.test.ts: nach dem Wechsel auf Englisch heißt
der Knopf des Konto-Menüs „Sign in“.
- Tests: Vitest 53 Dateien, 1779 Tests grün, tsc und Build sauber.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Task 9: die Infozeile zum gewählten Logo unter dem Raster (Spec § 3 und § 6).
- logos/logo-info.ts (neu): „<Name> · von <Anzeigename> · ♥ <Zahl>“ und „Melden“ (auch für Gäste; ein ausgeblendetes Logo lässt
sich nicht melden). Der Hochlader sieht „Löschen“ mit Rückfrage (Titel mit Namen, Folgen im Text), Admins „<n> Meldungen“ mit den
Gründen als Text (listLogoReports, einmal je Logo und Stand), „Ausblenden“/„Einblenden“ und „Löschen“. Gelöscht: deleteLogo,
forgetLogo, Kachel weg, onSelect(null); ein-/ausgeblendet: setLogoVisibility, forgetLogo (ein Null-Bild bleibt nicht im Zwischen-
speicher), die Zeile des Pickers wird fortgeschrieben (Einblenden setzt reportCount auf 0). Gewähltes Logo nicht auffindbar: „Das
gewählte Logo ist nicht mehr verfügbar.“ (ein Netzfehler von getLogo sagt nichts). Zugleich die Meldezeile (role="alert") für Fehler
von Herz, Hochladen und diesen Aktionen; ein Wechsel der Auswahl räumt sie ab. Immer nur eine Aktion zugleich; die Rechte werden
nach der Rückfrage noch einmal geprüft; der Aufbau steht einmal da, update() ändert nur Texte und Sichtbarkeit (Fokus bleibt).
- logos/logo-picker.ts: hängt die Infozeile als Letztes unter das Raster (info.el). Herz- und Upload-Fehler gehen in die Infozeile,
der Statusbereich des Pickers bleibt den Ladefehlern der Liste vorbehalten (ein Klick auf ein Herz löscht ihren Text nicht mehr).
Aus dem Review von Task 8: requireLogin, das wirft, zeigt den Grund statt einer unbehandelten Ablehnung; select() nach dispose()
ist wirkungslos. Löschen entfernt Kachel, Zeile, Herz und Eintrag der Liste, setzt den Cursor von „Mehr laden“ auf das Logo davor
(Appwrite lehnt einen Cursor auf eine gelöschte Zeile ab), hebt die Auswahl über onSelect(null) auf und holt den Fokus nach
„Kein Logo“, wenn er auf „Löschen“ lag. LogoPickerDeps erweitert um reportLogo, deleteLogo, setLogoVisibility, listLogoReports,
confirm und reportDialog.
- community/report-dialog.ts: subject 'card' (Standard, unverändert) oder 'logo': Überschrift „Logo melden“, Einleitung „dieses
Logo“, vier Gründe ohne „Echte Kartendaten“, Duplikat-Dank „Danke, du hast dieses Logo schon gemeldet.“. Der ganze Dialog geht
durch t()/msg(); Fehler des Dienstes über apiErrorText.
- logos/logos.css: das Herz sitzt in der Textzeile unter der Bildfläche (nicht mehr auf dem Logo, folgt dem Theme), die Kachel füllt
die Zelle (height: 100%), damit „Kein Logo“ so hoch ist wie sein Nachbar; Stile der Infozeile.
- editor/editor-page.ts: Aus dem Review von Task 8: der Ladefehler der Logos verschwindet, sobald ein Logo danach lädt, aber nur
der, den der Logo-Zustand selbst ins Fehlerfeld geschrieben hat; jeder andere Text im Feld bleibt.
- i18n/en.ts: englische Texte zu allen neuen Beschriftungen und zum Melden-Dialog.
- Tests: logo-info.test.ts (neu), logo-picker.test.ts (Infozeile, Löschen, Ein-/Ausblenden, Fehler, Cursor, Fokus), report-dialog.test.ts
(Logo und Englisch), editor-page.test.ts (Fehlerfeld). Vitest 52 Dateien, 1764 Tests grün, tsc und Build sauber.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Task 8: der Bereich „Logo unten rechts“ in der Optionsspalte des Editors (Spec § 3 und § 5).
- logos/logo-picker.ts (neu): createLogoPicker baut Kopf (Überschrift, Plakette „Community“, „+ Hochladen“, Satz zur Community), den
Umschalter „Beliebt“/„Neu“ (Admins zusätzlich „Gemeldet“/„Ausgeblendet“, je eine Ansicht mit sort und filter) und das Raster:
erste Kachel „Kein Logo“, je Logo eine Kachel mit Bild, Name, „von <Anzeigename>“ bzw. „von Anonym“ und einem eigenen Herz-Knopf
neben dem Auswahlknopf. „Mehr laden“ hängt die nächste Seite an (Duplikate nur einmal), Leer-Text und Ladefehler mit „Nochmal
versuchen“ (wiederholt genau den gescheiterten Schritt); nur die jüngste Antwort einer Ansicht zählt. Die Auswahl gehört dem
Editor: ein Klick meldet onSelect, markiert wird über select(); ein Logo außerhalb der Liste wird nach dem Laden der ersten Seite
per getLogo geholt und als zweite Kachel angeheftet (steht es in der Liste, gibt es keine Anfrage). selected() liefert die Zeile
für die Infozeile in Task 9. Herzen: optimistisch mit Rücksprung und Fehlertext (role="alert"); solange eine Anfrage für ein
Logo läuft, bleiben weitere Klicks auf sein Herz wirkungslos; Gäste öffnen erst den Login (abgebrochen: nichts passiert, kein
Fehlertext); die Herzen einer Seite kommen mit genau deren IDs, beim Anmelden für alle gezeigten Logos, beim Abmelden aus; eine
späte Antwort überschreibt kein inzwischen selbst geschaltetes Herz. „Hochladen“ meldet Gäste zuerst an, öffnet den Dialog (mit
Signal, das beim Verlassen der Seite schließt), setzt das neue Logo als erste Kachel, ruft rememberLogo und onSelect(id).
- logos/logo-image.ts (neu): das PNG einer Kachel laden, auf die Pixelgrenze prüfen (MAX_LOGO_PIXELS, so wie checkGalleryImage die
Galerie-Bilder prüft; darüber, nicht lesbar oder Ladefehler: neutraler Platzhalter statt Absturz) und zeigen: mehrfarbig als <img>,
einfarbig als weiße Maske, immer auf dem dunklen Kartengrund. Die Object-URL gibt die zurückgegebene Funktion frei; eine Datei, die
erst danach eintrifft, wird nicht mehr angezeigt.
- logos/logo-asset.ts: MAX_LOGO_PIXELS wird exportiert (eine Quelle für Renderer und Kacheln).
- logos/logos.css: Kopf, Umschalter, Raster (ab 1024 px zwei Spalten, sonst auto-fill ab 140 px), Kacheln mit Bildfläche auf #1B2738,
Herz (gefüllt, wenn gedrückt), Leer- und Fehlerzeilen; Bild und Maske teilen sich die Regeln mit der Dialog-Vorschau.
- editor/editor-page.ts: der Picker sitzt in options.logoSlot, die Wahl setzt config.logo, sync ruft picker.select(config.logo), das
Verlassen der Seite ruft dispose. Übernommen aus dem Review von Task 5: failedKey blockiert ein Logo nicht mehr für immer, eine
andere Wahl (Logo, Details, Marke) hebt die Sperre auf; derselbe Schlüssel wird weiter nicht im Kreis neu versucht.
- editor/dom.ts: Symbole heart und plus. i18n/en.ts: englische Texte zu allen neuen Beschriftungen.
- Tests: logo-picker.test.ts (Aufbau, Sortierung, Mehr laden, Auswahl und angeheftetes Logo, Herzen mit Rennen und Login, Leer und
Ladefehler, Hochladen, Kachelbilder, dispose, Englisch), logo-image.test.ts, editor-page.test.ts (Verdrahtung, Regressionstest zu
failedKey). Vitest 51 Dateien, 1666 Tests grün, tsc und Build sauber.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Task 7: der Dialog zum Hochladen eines Community-Logos (Spec § 4) und der zuletzt benutzte Anzeigename, den er mit dem
Veröffentlichen-Dialog teilt.
- logos/logo-dialog.ts (neu): logoUploadDialog öffnet ein eigenes <dialog> nach dem Muster von publish-dialog.ts (showModal, Fokus
zurück an den Auslöser, Texte nur als Text; beim Hochladen sind Abbrechen, Esc und Hintergrund wirkungslos, nur das Signal schließt
immer; ein zweites Absenden wird ignoriert). Die gewählte Datei geht erst durch validateLogoFile, dann durch prepare (Standard
prepareLogo); währenddessen ist „Hochladen“ gesperrt, eine neue Wahl macht die laufende Verarbeitung hinfällig. Das fertige PNG
erscheint als Object-URL zweimal, auf dunklem und hellem Grund; „Einfarbig“ (nur wählbar mit transparenten Bereichen, bei jeder
neuen Datei zurückgesetzt) zeigt stattdessen eine Maske auf dieselbe URL, gefüllt mit TONE_COLOR (Weiß bzw. #111111). Die URL wird
beim Schließen und beim Ersetzen freigegeben. Der Name wird aus dem Dateinamen ohne Endung vorbelegt (höchstens 40 Zeichen, nie
mitten in einem Emoji), ein selbst getippter Name bleibt. Hochladen über uploadPreparedLogo mit withLogin (abgelaufene Sitzung →
Anmeldung); Fehler erscheinen als „Hochladen fehlgeschlagen – <Grund>“ (ApiError über apiErrorText, Texte von prepareLogo im
Wortlaut, sonst errorMessage), eine abgebrochene Anmeldung als „Bitte melde dich an“. validateLogoForm prüft Datei, Name (leer,
Steuerzeichen, 40), Anzeigename (Steuerzeichen, 30) und Pflicht-Häkchen mit den Texten aus Spec § 4 und § 11.
- logos/logos.css (neu): Breite des Dialogs, Datei-Feld, die zwei Vorschauflächen und die Maske; Hülle, Felder und Knöpfe kommen aus
community.css und account.css.
- account/display-name.ts (neu): DISPLAY_NAME_KEY (aircard.displayName), rememberedDisplayName und rememberDisplayName; jeder Zugriff
in try/catch, ein leerer Name entfernt den Schlüssel.
- community/publish-dialog.ts: der Anzeigename ist aus dem gemerkten Namen vorbelegt und wird nach erfolgreichem Veröffentlichen
gemerkt; CONTROL_CHARACTERS und DISPLAY_NAME_MAX werden exportiert (der Logo-Dialog prüft mit denselben Regeln).
- i18n/en.ts: englische Texte zu allen neuen Beschriftungen, Hinweisen und Meldungen („Einfarbig“ heißt „Single color“).
- Tests: logo-dialog.test.ts (validateLogoForm, Aufbau, Dateiwahl mit Vorschau, Namensvorschlag, Maske, Object-URLs, veraltete
Verarbeitung, Prüfung, Senden mit Doppelklick und Sperren, Fehlertexte in beiden Sprachen, Standardwerte, Schließen),
display-name.test.ts, publish-dialog.test.ts (Vorbelegung und Speichern). Vitest 49 Dateien, 1566 Tests grün, tsc und Build sauber.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entscheidungen des Controllers zu Task 6 (Bedenken 2 bis 4).
- logos/prepare-logo.ts: sizeSvg(svg) ersetzt sizeSvgRoot und liefert { svg, w, h }. Das Seitenverhältnis kommt aus der brauchbaren
viewBox des Wurzelelements, sonst aus dessen Breite und Höhe (Pixel oder ohne Einheit), sonst ist es quadratisch; Prozent und
andere Einheiten, 0 und negative Werte zählen als unbrauchbar. Ohne brauchbare viewBox, aber mit Größe, wird viewBox="0 0 <Breite>
<Höhe>" ergänzt (eine unbrauchbare viewBox wird dabei ersetzt, nie doppelt geschrieben), damit der Inhalt auf das Raster skaliert
statt in Originalgröße oben links zu stehen. Eine brauchbare viewBox bleibt immer unberührt. Gelesen werden nur die Attribute des
Wurzelelements (vorher die erste viewBox irgendwo im Text, etwa die eines inneren <symbol>); Anführungszeichen " und ' gehen
beide. Tokenizer und Längenparser laufen linear (zwei quadratische Entwürfe fielen im Zeittest durch).
- logos/prepare-logo.ts: Raster und SVG werden über decodeImage aus editor/upload.ts geladen: erst die Maße lesen und gegen
40 Megapixel prüfen, erst dann dekodieren (vorher dekodierte createImageBitmap zuerst). decodeImages Text für „zu groß“ wird zum
Text des Logos, jeder andere Fehler bleibt „Das Logo konnte nicht gelesen werden.“ Die eigene Ladefunktion für SVG und
createImageBitmap entfallen. LOGO_SOURCE_MAX_PIXELS ist jetzt MAX_IMAGE_PIXELS (eine Quelle, Wert 40 000 000 per Test festgehalten).
- logos/prepare-logo.ts: SVG_FALLBACK_SIZE entfernt (nichts benutzte es).
- card/marks.ts: neu viewBoxAspect(value) mit den Regeln aus svgAspect; svgAspect ruft es auf (Verhalten unverändert), so stehen die
viewBox-Regeln nur einmal da.
- Tests: sizeSvg als Tabelle (Größe ohne Einheit, px, hochkant, Prozent, mm/em/pt/auto, nur Breite oder Höhe, 0 und negativ,
vorhandene viewBox bleibt, unbrauchbare viewBox wird ersetzt, innere Elemente zählen nicht, XML-Kopf, „>“ im Wert, Zeitgrenze),
prepareLogo mit gestelltem Image: Zahl der Pixel wird vor decode() geprüft (8001 x 5000 und 20 000 x 20 000 ohne decode, genau
8000 x 5000 mit), SVG nur mit Breite und Höhe wird 2048 x 614 gerastert und bekommt viewBox="0 0 200 60"; marks.test.ts für
viewBoxAspect. Vitest 47 Dateien, 1478 Tests grün, tsc sauber.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Task 6: Verarbeitung und Hochladen eines Logos als Ablauf ohne Oberfläche (der Dialog folgt in Task 7).
- logos/prepare-logo.ts (neu): validateLogoFile (PNG, JPG, WebP, SVG; ohne Typ zählt die Endung .svg; höchstens 5 MB), die reinen
Helfer trimBox (Alpha ≤ 8 gilt als leer), fitWithin (nie größer, gerundet, mindestens 1), svgRasterSize (längste Seite genau 2048,
ungültiges Seitenverhältnis quadratisch), hasTransparency und planLogo (Ausschnitt und Zielgröße, „leer“ und „zu klein“ nach der
fertigen Größe) sowie sizeSvgRoot. prepareLogo fügt sie zusammen: Raster über createImageBitmap (Breite × Höhe wird vor dem
Zeichnen gegen 40 Megapixel geprüft, das Bild danach freigegeben), SVG nur als Bild aus einer Object-URL (Seitenverhältnis aus
der viewBox, Größe ins Wurzelelement geschrieben), Zuschnitt, Verkleinern auf höchstens 720 × 250, PNG, hasTransparency auf den
Endpixeln. Jeder Fehler beim Dekodieren oder Zeichnen heißt „Das Logo konnte nicht gelesen werden.“; alle Meldungen sind schon
übersetzt (t() beim Auslösen), die deutschen Konstanten LOGO_*_MESSAGE stehen über msg().
- sizeSvgRoot ersetzt eine eigene width/height des Wurzelelements, statt ein zweites Attribut anzuhängen (doppelte Attribute machen
ein SVG ungültig, so scheiterten viele echte SVGs). Es macht genau einen Versuch ab dem ersten „<svg“: ein Suchmuster, das an
jedem „<svg“ neu ansetzt, brauchte bei einer Datei voller offener „<svg x="“ quadratisch viel Zeit (160 kB: 6 s).
- logos/upload-logo.ts (neu): uploadPreparedLogo legt logo.png mit read("any") und delete("user:<ich>") in den Bucket logo-images,
ruft uploadLogo und liefert das neue CommunityLogo (ID = Datei-ID, likeCount 0). mono gilt nur bei durchsichtigen Stellen. Wie
beim Veröffentlichen wird nur bei eindeutigem Scheitern aufgeräumt (genau die Datei dieses Versuchs, ein Fehler dabei überdeckt
nie den eigentlichen), bei offenem Ausgang bleibt die Datei. Ausnahme: 409 mit serverCode logoTaken löscht nichts, ein anderes
Logo trägt diese Datei-ID.
- i18n/en.ts: englische Texte zu den sechs neuen Meldungen.
- Tests: prepare-logo.test.ts (Helfer einzeln, dazu prepareLogo mit gestelltem Dekoder und Canvas: Zuschnitt, Skalierung, Rasterung,
Fehlertexte, 40-Megapixel-Grenze, SVG nie in der Seite) und upload-logo.test.ts (Reihenfolge, Rechte, Felder, mono, Aufräumen je
Status, logoTaken, Standard-Abhängigkeiten gegen den Bucket logo-images). Vitest 47 Dateien, 1452 Tests grün, tsc sauber.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entscheidung des Controllers zu Task 5: Ein fehlendes Community-Logo ist beim Rendern einer gespeicherten Karte nur dann ein
Problem, wenn die Vorlage es zeichnet (layoutFor(template).logo). Bei „Nur Bild“ wird ein Logo nie gezeichnet, dort fehlt
nichts. Der Editor-Hinweis (Auswahlzustand) und der Download im Editor bleiben unverändert.
- community/publish.ts: neue gemeinsame Prüfung assertLogoAvailable(config, assets); sie wirft LOGO_MISSING_PUBLISH, wenn
assets.logoMissing gilt und die Vorlage das Logo zeichnet (minimal, classic, signature). renderForGallery nutzt sie: bei
einer zeichnenden Vorlage Abbruch vor dem Rendern (nichts hochgeladen), bei „Nur Bild“ wird normal gerendert und
veröffentlicht.
- pages/cards-page.ts: der Download einer gespeicherten Karte ruft dieselbe Prüfung. Statt stillschweigend ohne Logo
herunterzuladen, zeigt die Kachel den Text LOGO_MISSING_PUBLISH über den vorhandenen Fehlermechanismus (run() und
failureText() schreiben in .card-msg), übersetzt mit t(); es wird nichts exportiert oder gespeichert. Bei „Nur Bild“ läuft
der Download normal.
- Tests: publish.test.ts (classic/minimal/signature mit fehlendem Logo brechen ab, „Nur Bild“ veröffentlicht normal,
Wahrheitstabelle für assertLogoAvailable) und cards-page.test.ts (Download mit fehlendem Logo: Meldung, kein Download;
„Nur Bild“ und vorhandenes Logo: Download wie sonst; zweiter Versuch; Englisch). Vitest 45 Dateien, 1338 Tests grün, tsc
und Build sauber.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- card/config.ts: network und das Apple-Logo entfallen. CardConfig.logo ist die ID eines Community-Logos (Muster ID_PATTERN,
sonst null), DEFAULT_CONFIG.logo = null, serializeConfig schreibt {v, template, logo, details, brand, crop}. Ein altes network
wird ignoriert, brand 'apple' fällt auf den Standard. ID_PATTERN steht jetzt hier (die reine Konfiguration prüft die Logo-ID,
ohne Appwrite zu laden), account/cards.ts reicht sie weiter; single-sources.test.ts sorgt für genau eine Definition.
- card/geometry.ts: fitLogo und LOGO_MAX_H (125) ersetzen fitNetwork, Layout.network heißt Layout.logo.
- card/marks.ts: nur noch die AirCard-IOS-Marken (BRAND_MARKS, getBrandMark, svgAspect, tintSvg, loadMarkImage). Die 19 SVGs
der Netzwerke und von Apple sind per git rm gelöscht, ein Test hält assets/marks auf die zwei AirCard-Dateien.
- card/render.ts: LogoAsset, CardAssets.logo statt network, LoadedAssets.logoMissing. loadCardAssets(config, image, loadLogo =
loadLogoAsset): ohne Logo kein Aufruf, null vom Lader ergibt logoMissing, ein Ladefehler wirft. Der Renderer zeichnet das
Logo mit fitLogo(w / h) unten rechts, wenn Vorlage und Bilder eines haben.
- logos/logo-asset.ts (neu): loadLogoAsset(id, tone) holt Zeile (getLogo) und PNG (loadLogoBlob) und dekodiert mit
createImageBitmap, sonst über ein Image. Einfarbige Logos werden in einem eigenen Canvas mit source-in in der Details-Farbe
gefüllt, mehrfarbige bleiben. Fehlende Zeile, ausgeblendetes Logo (auch für Hochlader und Admins), Datei 404, unlesbare
Datei und mehr als 1 000 000 Pixel ergeben null. Je (id, tone) ein Ladevorgang, ein Fehler bleibt nicht im Cache.
rememberLogo, forgetLogo und LOGO_MISSING nach der Schnittstelle des Plans.
- editor/options-panel.ts: keine Netzwerk-Kacheln mehr, Überschrift „Logo oben links“ mit Symbol, Text und Kein Logo (ohne
Apple), dazu logoSlot als leerer Platz oben für Task 8 (editor.css: Details rückt bei leerem Platz wie bisher nach oben).
- editor/editor-page.ts: logoKey aus Logo, Details und Marke, der Logo-Zustand hält missing. Fehlt das Logo, wird die Karte
ohne es gezeichnet und der Hinweis LOGO_MISSING gezeigt, solange das so bleibt; ein Wechsel auf ein vorhandenes Logo oder
auf kein Logo nimmt nur diesen Hinweis weg (PNG-Größe und „Bild fehlt“ stehen getrennt und bleiben). Kein LOGO_ERROR bei
fehlendem Logo, nur bei echtem Ladefehler, auch beim Download.
- community/publish.ts: LOGO_MISSING_PUBLISH, renderForGallery (jetzt exportiert) bricht bei logoMissing vor dem Rendern ab,
es wird nichts hochgeladen. publish-dialog.ts lässt den Text im Wortlaut durch.
- i18n/en.ts: englische Texte zu LOGO_MISSING, LOGO_MISSING_PUBLISH und „Logo oben links“.
- Tests: config, geometry, marks, render und die Fixtures von cards, theme, cards-page auf das neue Modell; neu
logo-asset.test.ts (28) und options-panel.test.ts (10); editor-page (14 neue), publish und publish-dialog erweitert.
Vitest 45 Dateien, 1319 Tests grün, tsc und Build sauber.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- logos/logos.ts (Muster community/gallery.ts): CommunityLogo, toLogo, logoQueries (Beliebt/Neu, Admin-Filter Gemeldet und
Ausgeblendet, Cursor), listLogos (nextCursor nur bei voller Seite von 24), getLogo (ungültige ID oder 404 -> null),
myHearts (eigene Herzen einer Seite, ohne IDs keine Anfrage), listLogoReports (ohne Melder-Hash), loadLogoBlob.
- logos/logo-api.ts: uploadLogo, setHeart, reportLogo (ohne Anmeldung), deleteLogo, setLogoVisibility über apiPost.
uploadLogo behandelt das Limit (429 tooManyLogos) wie publishCard als eindeutige Ablehnung (die hochgeladene Datei darf
aufgeräumt werden); eine Antwort ohne id (und bei setHeart ohne Zustand und Zähler) ist 503 mit offenem Ausgang.
- account/appwrite.ts: IDS um logos, logoLikes, logoReports und logoBucket.
- community/api.ts: ApiError.serverCode (der Schlüssel aus {error, code}; code bleibt der HTTP-Status, message der deutsche
Text). Client-Fehler tragen unavailable (auch 502/503/504 vom Proxy, vor dem Lesen des Bodys), generic, jwtUnsupported,
alreadyPublished, tooManyPublications. Neu: unavailableError() und apiErrorText(err). Die deutschen Texte stehen einmal
in api-messages.ts, die Vergleiche in publishCard laufen unverändert über err.message.
- i18n/api-messages.ts: API_MESSAGES (alle 33 Texte von MSG plus die Client-Codes generic und jwtUnsupported) und
apiMessage: bekannter Code -> t(Text); unbekannt: Deutsch der Servertext, Englisch eine Meldung je Status (400, 403, 404,
sonst allgemein). Der Code badRequest trägt auch die dynamischen Prüftexte des Dienstes, Deutsch zeigt dann den
genaueren Servertext.
- i18n/en.ts: englische Texte zu allen API_MESSAGES.
- Tests: api-messages.test.ts lädt MSG aus deploy/api/common.js (createRequire) und prüft den Gleichlauf in beide
Richtungen; logos.test.ts, logo-api.test.ts und neue Fälle in api.test.ts (der Helfer freshApi stellt die frische
i18n-Instanz auf Deutsch). 156 neue Tests, die bisherigen unverändert grün (1285 gesamt).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Der Hochlader behält delete auf seiner Datei, bis die API sie auf read("any") festlegt. Zwischen der ersten Prüfung
(Typ, Größe, PNG-Kopf, Maße) und dem Festlegen ließ sich die Datei unter derselben ID löschen und mit anderem Inhalt
neu anlegen (z. B. 4000 × 4000). Jetzt liest die API den Dateianfang nach dem Festlegen noch einmal, wenn der
Hochlader die Datei nicht mehr ändern kann:
- PNG-Kopf und Maße 8 × 8 bis 720 × 250 werden erneut geprüft. Fällt die Datei durch oder fehlt sie, löscht die API die
Datei (Server-Key) und die Zeile (beides wird versucht) und antwortet 400 logoFormat bzw. fileMissing. Log:
"logo <id> removed after upload check (<code>)", bleibt etwas stehen: "rollback failed for logo <id>".
- Ein Appwrite-Fehler beim zweiten Lesen wird wie ein Fehler beim Festlegen behandelt (502, Zeile zurückgenommen, die
Datei bekommt delete(owner) zurück).
- fake-appwrite.js: Testhilfe swapContentAfterPermissions (der Inhalt ändert sich nach dem nächsten Rechte-Update).
- Tests: 7 neue in logos.test.js (Austausch auf 4000 × 4000, weitere ungültige Inhalte, Datei weg, Appwrite-Fehler,
Aufräumen scheitert, Ablauffolge, weiterhin gültiger Inhalt), der Logtest prüft das neue Ereignis mit. Der
Karten-Ablauf ist unverändert (app.js und common.js nicht angefasst).
- Doku: api/README.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- common.js (aus app.js herausgelöst, Verhalten gleich): MSG, HttpError, Prüfungen, Rechte-Strings, Melder-Hash,
Bildmaße und die Dateiprüfungen checkFileExists/checkFileOwnership. Neu: die Logo-Texte in MSG, codeFor,
likeRowPermissions. app.js exportiert weiter alles wie bisher.
- Fehlerantworten sind jetzt {error, code}: code = MSG-Schlüssel des Textes, dynamische Prüftexte badRequest,
Appwrite-Fehler unavailable (502) bzw. internal (500).
- logos.js: POST /api/logos/hochladen, /herz, /melden, /loeschen und /api/admin/logo-sichtbarkeit nach dem Vorbild der
Karten-Endpunkte (Datei prüfen, Zeile anlegen, Datei auf read("any") festlegen, sonst Rücknahme; likeCount und
reportCount immer aus countRows; ein gemeinsames Rate-Limit für Karten- und Logo-Meldungen).
- appwrite.js: countRows. fake-appwrite.js: Tabellen logos, logo_likes, logo_reports mit Spaltenlängen, Bucket
logo-images und total in den Listen-Antworten.
- Tests: logos.test.js (59), code-Fälle und MSG-Eindeutigkeit in app.test.js (+7), countRows in appwrite.test.js (+2);
alle 111 bisherigen Tests unverändert grün (179 gesamt).
- Doku: api/README.md (Endpunkte, Fehlercodes, Logo-Regeln), deploy/README.md (Betrieb kopiert fünf Dateien).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Setup-Skript: Tabellen `logos`, `logo_likes` (Mehrspalten-Index `user_logo` über userId, logoId)
und `logo_reports` sowie der Bucket `logo-images` (nur PNG, höchstens 1 MiB); der Kopfkommentar
nennt die neuen Objekte. Der Index-Request schickt `columns` schon als Liste, dort war nichts zu ändern.
- Tests: DESIRED mit genau diesen Werten, Anlegen samt Indizes gegen den Fake-Server, --check meldet jedes
fehlende Objekt (51 statt 28) mit Exit 1, bestehende Tabellen und Buckets bleiben unberührt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- i18n-Kern im gettext-Stil (t/msg, Wörterbuch en.ts); Startsprache aus localStorage, sonst Browsersprache;
<html lang> setzt auch das Inline-Skript in index.html. Ein Test prüft t()/msg() und en.ts in beide Richtungen.
- Kopfzeile: GitHub-Link und Sprachknopf (Zielsprache, Beschriftung und lang in der Zielsprache), Fußzeile aus mountFooter,
Banner und Seitentitel übersetzt, statisches index.html englisch.
- Sprachwechsel ohne Neuladen (main.ts): Kopfzeile, Banner, Konto-Menü und Fußzeile neu aufgebaut, rerender() des Routers baut
die Seite neu auf, der Editor-Store bleibt; der Appwrite-Client bekommt die Sprache.
- Kopfzeile bleibt in einer Zeile: einzeilig ab 1000 px (vorher 900), GitHub-Text ab 1230 px, auf schmalen Telefonen steht
„Anmelden“ neben der Suche.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- publish.ts und editor-page.ts: Kommentare zum Aufräumen bzw. zur Adresse /gestalten?karte= stimmen wieder.
- index.html: Inline-Skript für Hell/Dunkel in einer sofort aufgerufenen Funktion (keine globale Variable theme);
Test führt es aus und vergleicht mit initialTheme().
- CARD_W/CARD_H nur noch aus card/geometry.ts (Startseite, Detailseite, Raster), Vorschauhöhe des Rasters daraus abgeleitet;
reportsLabel nur aus pages/format.ts. Ein Test sorgt dafür, dass es dabei bleibt.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Der Key aircard-api braucht in der Console documents.read/documents.write (entspricht rows.* der API) sowie files.read/files.write.
- Der Befehl zum Eintragen des Keys nutzt read -rs, schreibt bei leerer Eingabe nichts und beginnt mit einem Zeilenumbruch
(klebt nie an einer letzten .env-Zeile ohne Umbruch); Hinweis zum Zeilenumbruch auch bei der Eingabe mit nano.
- deploy/README.md: nginx-503 bei fehlendem Dienst, up -d nach .env-Änderung, X-Real-Ip im Docker-Netz als akzeptiertes Risiko.
- Kommentare in server.js und compose-snippet.yml angepasst.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Router: onRouteChange meldet jede neue Seite (vor dem Aufbau). Die Kopfzeile markiert damit den aktuellen Navigationslink
mit aria-current="page" („Community“ auch auf der Detailseite) und leert das Suchfeld, wenn die neue Seite nicht die
Startseite mit ?q= ist (bisher blieb „testbank“ auf /gestalten stehen).
- Spenden-Knopf in der Kopfzeile trägt title „Unterstützen“ (zwischen 600 und 1179 px nur Symbol).
- .detail-msg:empty und .auth-msg:empty sind nicht mehr display: none (Live-Regionen standen nicht im Zugänglichkeitsbaum),
sondern ohne Platz im Layout, aber gerendert; Test liest die CSS-Dateien.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Gemeinsamer Helfer loadGalleryImageChecked/checkGalleryImage (gallery.ts): lädt das Bild über new Image(), lehnt über
4 Megapixel oder nicht dekodierbare Dateien ab und liefert eine fertige Object-URL (der Aufrufer gibt sie frei).
Raster -> „Keine Vorschau“ (auch bei <img>-Fehler), Hero -> Beispielmotiv, Detailseite -> „Das Bild konnte nicht geladen werden.“
Verspätet eintreffende URLs werden sofort freigegeben.
- Tag-Filter sucht ",<tag>," (der Server speichert ",a,b,"): „gold“ trifft nicht mehr „marigold“.
- Veröffentlichen-Dialog lehnt dieselben unsichtbaren Zeichen ab wie die API (C1, Nullbreite, Bidi, BOM).
- Das Limit der API (429 „schon 20 Karten veröffentlicht“) gilt als eindeutige Ablehnung: die hochgeladenen Dateien werden aufgeräumt.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Fällt aircard-api aus, verschwindet sein Traefik-Router und /api/* fiel auf index.html (200 text/html) zurück.
Jetzt 503 mit {"error":"Der Dienst ist gerade nicht erreichbar."} und Content-Type application/json.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Beim Veröffentlichen liest die API die ersten 64 Bytes beider Dateien (PNG-IHDR, WebP VP8/VP8L/VP8X) und verlangt Bild 1536x969,
Vorschau 480x302-304, sonst 400 "Das Bild hat nicht das Kartenformat."
- Veröffentlichte und ausgeblendete Dateien tragen kein delete(owner) mehr (Abweichung von Spec §7): kein Löschen und Neuanlegen
derselben Datei-ID nach dem Veröffentlichen; scheitert das Festlegen, bekommen die Dateien delete(owner) für den Client-Aufräumer zurück
- Melder-Identität aus dem IPv4-/24 bzw. IPv6-/48; das Rate-Limit zählt weiter je Anschluss
- Höchstens 20 Veröffentlichungen je Konto (429); sourceCardId muss eine eigene Zeile in `cards` sein (403)
- Steuer-, Nullbreiten- und Bidi-Zeichen in Titel, Herausgeber, Anzeigename und Tags abgelehnt
- Tags als ",a,b," gespeichert (exakter Tag-Filter)
- fehlgeschlagenes Zurücknehmen wird mit `rollback failed for card <id>` protokolliert
- README: Endpunkt-Tabelle, Regeln, Scopes (documents.*), akzeptiertes Risiko X-Real-Ip im Docker-Netz
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Die Überschrift des Dialogs hatte dieselbe ID wie das Titelfeld (publish-title), das Label des
Pflichtfelds zeigte damit auf die Überschrift; jetzt publish-heading. Schließt der Browser den Dialog
mitten im Senden, wird er wieder geöffnet statt als abgebrochen aufgelöst. Die Status-Abfrage der Seite
wird bei einer lokalen Änderung während der Anfrage wiederholt (nie in „lädt“ hängen), die Nachprüfung nach
offenem Ausgang ist eine eigene Abfrage. Der Löschen-Dialog nennt bei unbekanntem Status einen allgemeinen
Hinweis und sagt, dass sich eine Veröffentlichung nur auf der Community-Seite zurückziehen lässt.
publishCard heißt lokal publishWithCheck.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Neuer Dialog (publish-dialog.ts) laut Spec § 6: Titel (Pflicht, höchstens 60), Herausgeber/Bank (40),
Tags (bis 5, je 20, Komma-getrennt), Anzeigename (30, leer = Anonym, nie vorbelegt) und die
Pflicht-Checkbox im Wortlaut der Spec. Prüfung vor dem Senden mit deutschen Texten, während des Sendens
lässt sich der Dialog nicht schließen; Fehler stehen im Dialog („Veröffentlichen fehlgeschlagen – …“).
Meine Karten zeigt je Kachel den Status aus myPublications (einmal je Seitenaufbau, danach lokal
nachgeführt): Veröffentlicht + Ansehen + Zurückziehen, Ausgeblendet (gemeldet) + Zurückziehen, sonst
Veröffentlichen. Bei 409 wird der Status neu geholt; bleibt der Ausgang offen (503/5xx/429/408), wird vor
der Fehlermeldung nachgesehen und bei Erfolg der Erfolgszustand gezeigt. Zurückziehen fragt nach, 404 gilt
als erledigt. Nicht angemeldet läuft über withLogin. Das Löschen einer veröffentlichten Karte weist darauf
hin, dass die Veröffentlichung bleibt.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
formatDate liegt jetzt in pages/format.ts und wird von Meine Karten und der
Detailseite genutzt; LOGIN_NEEDED kommt aus account/with-login.ts. Ausblenden,
Löschen und Zurückziehen prüfen Admin- bzw. Autor-Recht beim Ausführen (auch nach
der Rückfrage). Ein werfendes showModal im Melden-Dialog zeigt einen Hinweis statt
eines unbehandelten Fehlers.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Detailseite /community/<id>: große Karte, Titel, Herausgeber, „von …“, Datum,
Tags, Herunterladen (aircard-<slug>.png), „Als Vorlage gestalten“ und Melden-Dialog
(fünf Gründe, Dank-, Doppelt- und Drosselungs-Text). Autor: Zurückziehen; Admin:
Ausblenden/Einblenden, Löschen, Meldungszahl und Gründe. Fehlende oder
ausgeblendete Karten zeigen Besuchern „nicht (mehr) verfügbar“, Autor und Admin
sehen sie mit dem Hinweis „Ausgeblendet“.
Editor: /gestalten?vorlage=<id> übernimmt das Galerie-Vollbild als „Nur Bild“,
setzt saved, savedConfig und cardName zurück, ersetzt die Adresse durch /gestalten
und fragt vorher bei ungespeicherter Arbeit.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Startseite "/" mit kompaktem Hero (grosse schwebende Karte rechts: neueste
sichtbare Community-Karte, sonst das eigene Beispielmotiv hero-beispiel.png)
und direkt darunter der Galerie #community, damit Ueberschrift und Oberkante der
ersten Kartenreihe bei 1440x900 und 390x844 im ersten Bildschirm stehen.
- mountHomePage(root, deps): Suche ?q= und Tag ?tag= aus der Adresse (aktiver
Begriff mit "x"), Suchfeld der Kopfzeile per .value gefuellt, Mehr laden per
Cursor, Leer-/Kein-Treffer-/Fehlertexte, Admin-Filter Alle/Gemeldet/
Ausgeblendet (reagiert auf Anmelden/Abmelden), Anker nach dem Laden erneut
angesprungen (router.scrollToHash exportiert), Object-URLs beim Verlassen frei.
- mountGalleryGrid: Kacheln mit Vorschau (Object-URL, max. 6 parallel), Titel,
Herausgeber, bis 3 Tag-Knoepfe (Tag-Klick -> /?tag=<tag>#community), Link auf
/community/<id>; Moderations-Marken nur in den Admin-Ansichten.
- Hero hoechstens 60vh auf breiten, hohen Fenstern; Schwebe-Animation nur ohne
prefers-reduced-motion.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- publish.ts: bei Fehlern der API werden die hochgeladenen Dateien nur bei eindeutiger Ablehnung
(4xx außer 408/429, JWT-Fehler vor der Anfrage) gelöscht; bei offenem Ausgang (Netzfehler,
Zeitlimit, 5xx, 429, Antwort ohne id) bleiben sie liegen, damit keine öffentliche Zeile auf
gelöschte Bilder zeigt
- api.ts: ApiError.definitive und isDefinitiveFailure, Zeitlimit 15 s, gleichzeitige createJWT
werden geteilt
- gallery.ts: Tag-Filter (contains auf tags), Kurzsuche unter 3 Zeichen per contains,
loadGalleryImage prüft die Datei-ID
- geometry.ts: PREVIEW_WIDTH/PREVIEW_QUALITY an einer Stelle (Editor und Veröffentlichen)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- gallery.ts: Galerie aus Appwrite lesen (Abfragen für Liste, Suche, Cursor, Admin-Filter),
getGalleryCard (404 und ungültige ID ergeben null), eigene Veröffentlichungen, Meldungen, Bilder
- api.ts: Aufrufe von aircard-api mit JWT (Cache je Nutzer, 10 Minuten, geleert bei Auth-Änderung,
genau eine Wiederholung bei 401), ApiError mit code, Netz-/Proxy-Fehler als 503
- publish.ts: rendern, zwei Uploads in gallery-images, API; bei Fehler genau die zwei Dateien
dieses Versuchs löschen; normalizeTags
- cards.ts: eigenständiges fetchFile(bucketId, fileId); CardsApi.fetchFile bleibt Hülle
- appwrite.ts: IDS für gallery, reports, gallery-images
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Veröffentlichen lehnt mit 403 "Diese Bilder sind bereits veröffentlicht oder
gesperrt." ab, wenn eine Datei kein read("any") trägt (ausgeblendet) oder von
einer Galerie-Zeile des Autors schon als Bild oder Vorschau verwendet wird
(auch überkreuz, auch ausgeblendete Karten). Die Nutzungsprüfung liest die
Zeilen des Autors über den vorhandenen ownerId-Index seitenweise mit Cursor
(auf die Datei-ID-Spalten gibt es keinen Index) und läuft zusammen mit dem
Anlegen je Autor nacheinander. 92 Tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Kein Zugriffsprotokoll je Anfrage mehr (abgewiesene Anfragen an das
unauthentifizierte /api/melden loggten sonst unbegrenzt); nur noch
Ereignis- und 5xx-Zeilen, höchstens 120 pro Minute mit Sammelzeile
- Compose: logging json-file, max-size 5m, max-file 2
- appwrite.js: 404 nur mit Typ row_not_found/storage_file_not_found als
"fehlt", listRows ohne rows-Array wirft, redirect: error
- Appwrite-Fehler: 502 bei 0/429/5xx, sonst 500 "Interner Fehler."
- Veröffentlichen legt beide Dateien auf die sichtbaren Rechte fest
(erst nach dem Anlegen der Zeile, Rollback bei Fehler)
- setFilePermissions mit Promise.allSettled, gemeinsamer safeToken-Helfer
- Tests: 85 (Log-Flut, Log-Budget, 404-Typen, Auth-Header, Konto-Antworten)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Neuer abhängigkeitsfreier Node-22-Dienst in deploy/api/ (appwrite.js, app.js,
server.js), der alle Galerie-Schreibzugriffe mit dem Appwrite-Server-Key
ausführt: POST /api/veroeffentlichen, /zurueckziehen, /melden (Rate-Limit
10/h je Melder, IPv6 auf /64, Auto-Ausblenden ab 3), /admin/sichtbarkeit und
GET /api/gesundheit.
- JWT-Prüfung über GET /account (Header X-Aircard-Jwt), Admin = Label admin
- IDs per Muster geprüft, Pfadsegmente kodiert, "." und ".." abgelehnt
- deterministische Zeilen-IDs (Galerie, Meldung): Appwrite-409 ersetzt jede
Doppelt-Prüfung; Sperre je Karte gegen verlorene Zählerstände
- Dateiprüfung: Bucket, delete("user:<ich>"), PNG/WebP, höchstens 5 MiB
- Appwrite-Aufrufe mit 10-s-Zeitlimit, Logs ohne Key, Salz, JWT und IPs
- Tests gegen einen Fake-Appwrite im Speicher (70 Tests), Compose-Service
aircard-api (Router aircard-gallery-api) und README ergänzt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Setup-Skript um Tabellen gallery und reports (Spalten boolean/integer/enum,
Volltext- und Key-Indizes) sowie den Bucket gallery-images erweitert.
DESIRED auf tables/buckets umgebaut, Vergleich je Spaltentyp und Index,
Indizes erst nach available-Spalten. Console-Anleitung um Abschnitt
Community (JWT an, Label admin, Key aircard-api) ergänzt.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Router: Routen home, gestalten, cards, community/<id>, reset; Umleitung
/?karte=<id> nach /gestalten?karte=<id>; Titel je Route (titleFor);
Hash-Ziele (/#community) scrollen den Anker ins Bild, nur-Hash-Wechsel
bauen die Seite nicht neu auf.
- Kopfzeile: Navigation, Suche (Enter -> /?q=...#community), Spenden-Knopf,
Hell/Dunkel-Schalter, Menue unter 900 px, Konto als Avatar-Kreis mit Menue.
- Theme: data-theme auf <html> (Skript im <head> gegen Aufblitzen), helle
Tokens, danger-/warn-/ok-text-Tokens statt fester Farben.
- Spenden-Banner (schliessbar); ohne DONATE_URL bleiben Banner und Knopf weg.
- User.labels und isAdmin; Platzhalter fuer Startseite und Detailseite.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Die Maske hatte exakt die Kartenkante als Region und als weißes Feld; die
Kantenglättung wurde doppelt angewendet und ließ bei Vergrößerung einen
hellen Strich. Jetzt Standardregion der Maske und ein überstehendes weißes Feld.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Vorschau: Tastenkürzel mit Strg/Cmd/Alt gehören dem Browser (Seitenzoom).
- decodeImage lehnt Bilder über 40 Megapixel nach dem Laden, vor dem
Dekodieren ab (Meldung "Das Bild ist zu groß (max. 40 Megapixel).").
- Test, dass das Umbenennen-Feld den Namen markiert (tat der Code schon).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nginx: server_tokens off, HSTS, exaktes /v1 liefert 404 (Regex-Block statt
^~ /v1/). deploy.sh: AIRCARD_WEBHOOK_TOKEN nicht an npm, Sperrverzeichnis
gegen parallele Läufe (verwaiste Sperren werden übernommen), timeout 900
für npm ci und Build. Neuer Test deploy/deploy.test.js gegen das echte Skript.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Endpoint-Standard ${origin}/v1, echte Projekt-ID im Traefik-Header, Console-
Einstellungen als Soll-Stand statt Tatsache, Test-Befehle je mit Verzeichnis;
Projekt-ID nicht mehr "aircard", Auth-/Dienste-Schritte als optionale Härtung.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Gäste/abgelaufene Sitzung: Appwrite antwortet mit 404; withLogin und
openSavedCard melden dann an (Dialog, einmal wiederholen) statt
„nicht gefunden“ zu zeigen.
- Öffnen einer Karte fragt vor dem Verwerfen eines ungespeicherten Entwurfs
nach; ein fehlgeschlagenes Öffnen setzt einen gefüllten Editor nicht zurück.
- Update ohne Bildwechsel schickt kein imageFileId mehr (veralteter Stand
zeigte sonst auf eine gelöschte Datei).
- Uploads werden nur nach eindeutiger Ablehnung (4xx ohne 408/429) gelöscht,
nicht bei Netzfehler, 5xx oder 429.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>