22 KiB
AirCard-IOS Startseite & Community-Galerie – Implementierungsplan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Kompakte Startseite mit Community-Galerie, Veröffentlichen/Melden/Admin, Spenden-Banner und Hell/Dunkel-Schalter für https://aircard-ios.org.
Architecture: Der bestehende Vite-+-TS-Client bekommt neue Routen und Module (web/src/community/*, web/src/shell/*). Lesen der Galerie direkt aus Appwrite (öffentliche Zeilen), alle Galerie-Schreibvorgänge über einen neuen kleinen Node-Dienst aircard-api (deploy/api/) mit Server-Key, erreichbar unter /api per Traefik. Schema-Erweiterung über das bestehende Setup-Skript.
Tech Stack: wie bisher (TypeScript 6.0.3, Vite 8.3.2, Vitest 5.0.3, happy-dom, appwrite 22.4.1), Node 22 für aircard-api (nur Built-ins), Appwrite 1.8.1 TablesDB, Traefik v3.6.
Spec: docs/superpowers/specs/2026-10-04-aircard-community-design.md (Commit 970e68e). Basis: docs/superpowers/specs/2026-10-04-aircard-website-design.md.
Global Constraints
- Arbeitskopie
/home/webklar/apps/aircard-ios.org/dev, Branchmain, direkt committen, nicht pushen (Controller pusht). - Versionen unverändert gepinnt; keine neuen npm-Abhängigkeiten im Client;
deploy/api/ohne Abhängigkeiten. - Alle sichtbaren Texte Deutsch, Wortlaut aus der Spec.
- Appwrite: Projekt
6ac2130a00289343d6d5; Datenbankaircard; neue Tabellengallery,reports; neuer Bucketgallery-images; bestehendecards/card-imagesunverändert. - Galerie-Zeilenrechte: sichtbar genau
['read("any")']; ausgeblendet genau['read("user:<owner>")','read("label:admin")']. Galerie-Dateirechte sichtbar['read("any")','delete("user:<owner>")'], ausgeblendet['read("user:<owner>")','read("label:admin")','delete("user:<owner>")']. - Auto-Ausblenden ab 3 verschiedenen Meldern; Rate-Limit 10 Meldungen pro Stunde und IP; Seiten zu 24 Karten.
- Admin = Appwrite-Label
admin(account.get().labels). DONATE_URLinweb/src/config.ts; leer ⇒ Banner und Spenden-Knopf unsichtbar.- Theme:
data-themeauf<html>,localStorageaircard.theme; BannerlocalStorageaircard.donateBannerClosed. JederlocalStorage-Zugriff in try/catch. - Hero: bei 1440 × 900 und 390 × 844 müssen „Community-Karten“-Überschrift und der obere Rand der ersten Kartenreihe im ersten Bildschirm sichtbar sein.
- Keine Secrets in Ausgaben, Logs, Commits;
.envnie öffnen (Ausnahme: Controller-Ops). - Tests:
cd web && npx vitest run,npx tsc --noEmit,npm run build;node --test deploy/api/*.test.js(Glob in Anführungszeichen), bestehende node-Tests grün halten. - Commits enden mit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>. - Subagents:
sonnet(Implementierung/Review),haikufür kleine Nachprüfungen; nie Subagents aus Subagents.
Review Focus
- Fremdes Veröffentlichen: Nutzer schickt
imageFileIdeiner fremden Datei an/api/veroeffentlichen→ 403, nichts angelegt. Test in Task 3. - Meldungs-Flut / Selbst-Ausblenden: dieselbe IP meldet 5× → zählt 1; 11. Meldung in einer Stunde → 429. Test in Task 3.
- Ausgeblendete Karte per Direkt-Link: Gast öffnet
/community/<id>einer ausgeblendeten Karte → „nicht (mehr) verfügbar“, kein Bild. Test in Task 6 (Client) und Task 3 (Rechte). - Leere Galerie / leeres
DONATE_URL: Startseite ohne Karten zeigt Beispielmotiv und Leertext; ohne Link kein toter Spenden-Knopf. Tests in Tasks 1 und 5. - Theme-Wechsel mit offenem Editor: Umschalten ändert nur UI-Tokens, nicht die Kartenfarben/PNG. Test in Task 1 (render unabhängig von Theme).
Task 1: Shell – Routen, Kopfzeile, Theme, Spenden-Banner
Files:
- Create:
web/src/config.ts,web/src/shell/theme.ts,web/src/shell/donate-banner.ts,web/src/shell/header.ts,web/src/shell/shell.css,web/src/pages/home-page.ts(Platzhalter mit Überschrift, Task 5 füllt),web/src/pages/community-page.ts(Platzhalter, Task 6 füllt) - Modify:
web/index.html(Kopfzeilen-Gerüst, Banner-Slot#donate-banner, Theme-Init-Skript gegen Aufblitzen),web/src/pages/router.ts,web/src/main.ts,web/src/styles.css(helle Tokens),web/src/account/account-menu.ts(Avatar-Kreis + Menü),web/src/account/auth.ts(User.labels,isAdmin),web/src/editor/card-url.tsundweb/src/pages/cards-page.ts(Pfad/gestalten?karte=) - Test:
web/src/pages/router.test.ts,web/src/shell/theme.test.ts,web/src/shell/donate-banner.test.ts,web/src/shell/header.test.ts,web/src/account/auth.test.ts(labels)
Interfaces:
-
Produces:
// router.ts (ersetzt RouteId) type RouteId = 'home' | 'gestalten' | 'cards' | 'community' | 'reset' interface Route { id: RouteId; params: { id?: string } } matchRoute(pathname: string): Route // '/' home, '/gestalten' gestalten, '/karten' cards, '/community/<id>' community {id}, '/passwort-neu' reset, sonst home legacyRedirect(url: URL): string | null // '/?karte=X' → '/gestalten?karte=X', sonst null navigate(path: string): void // pushState, render, scrollTo(0,0) außer bei Hash-Ziel (dann scrollIntoView des Ankers) startRouter(render: (route: Route, root: HTMLElement) => void | (() => void)): void ROUTE_TITLES / titleFor(route: Route, detailTitle?: string): string // Wortlaut Spec §2 // config.ts export const DONATE_URL: string = '' // Kenso liefert den Link // theme.ts type Theme = 'light' | 'dark' initialTheme(stored: string | null, prefersDark: boolean): Theme applyTheme(t: Theme): void; toggleTheme(): Theme // speichert in localStorage (try/catch) // donate-banner.ts mountDonateBanner(slot: HTMLElement, url: string): void // nichts rendern wenn url leer oder geschlossen // header.ts mountHeader(el: HTMLElement, opts: { donateUrl: string }): () => void // Navigation, Suchfeld (Enter → navigate('/?q=…#community')), ☕-Knopf, Theme-Schalter, Menü-Icon < 900 px // auth.ts type User = { $id: string; email: string; labels: string[] } isAdmin(user: User | null): boolean -
Kopfzeile: Logo
/favicon.svg?v=2+ „AirCard-IOS“ (→/), Links „Community“ (/#community), „Gestalten“ (/gestalten), „Meine Karten“ (/karten), Suchfeld Platzhalter „Titel, Tags oder Herausgeber“, Knopf „☕ Unterstützen“, Theme-Knopf (aria-label„Hell/Dunkel umschalten“), Konto-Slot. Banner-Text exakt aus Spec §3. -
index.html: kleines Inline-Skript im<head>setztdata-themeauslocalStorage/System vor dem ersten Paint (in try/catch). -
Step 1: Tests schreiben – router: alle Routen inkl.
/community/abc123→{id:'community', params:{id:'abc123'}},/community/→ home, unbekannt → home;legacyRedirect(new URL('https://x/?karte=K1'))→/gestalten?karte=K1, ohnekarte→ null;navigate('/karten')ruftscrollTo(0,0); Titel je Route. theme:initialTheme(null,true)→ dark,('light',true)→ light, ungültiger Wert → System;toggleThemesetztdata-themeund speichert;localStoragewirft → kein Fehler. donate-banner: leere URL → nichts gerendert; Schließen setzt Flag und entfernt; Flag gesetzt → nichts. header: Suche Enter mit „ visa “ →navigate('/?q=visa#community'); leere Eingabe → nichts; kein ☕-Knopf bei leeremdonateUrl. auth:getCurrentUserübernimmtlabels(fehlend →[]),isAdmintrue nur mit'admin'. -
Step 2: Tests laufen lassen → FAIL.
-
Step 3: Implementieren;
main.ts: Legacy-Umleitung vorstartRouter(replaceState), Routen auf die Mount-Funktionen; Editor jetzt untergestalten;document.titlepertitleFor. -
Step 4: Prüfen – Suite grün,
tsc,build. -
Step 5: Commit –
feat(shell): Routen, Kopfzeile, Hell/Dunkel, Spenden-Banner
Task 2: Schema-Erweiterung (Setup-Skript)
Files:
- Modify:
web/scripts/appwrite-setup.mjs,web/scripts/appwrite-setup.test.mjs,docs/appwrite-console.md
Interfaces:
-
DESIREDbekommt: Tabellegallery(rowSecurity true, Tabellenrechte[]), Spalten gemäß Spec §7 (Typen string/boolean/integer, Standardwertehidden=false,reportCount=0), Indizessearch_text(fulltext,[searchText]),owner(key,[ownerId]),hidden(key,[hidden]); Tabellereports(rowSecurity false, Tabellenrechte['read("label:admin")']), SpaltengalleryIdstring 36 Pflicht,reasonenum[unangemessen, kartendaten, urheberrecht, spam, sonstiges]Pflicht,reporterHashstring 64 Pflicht, Indexgallery(key,[galleryId]); Bucketgallery-images(Rechte['create("users")'], fileSecurity true, maximumFileSize 5242880, Endungen[png, webp], compression none, antivirus false). -
REST:
POST …/columns/boolean {key, required:false, default:false},…/columns/integer {key, required:false, default:0, min:0},…/columns/enum {key, elements, required:true}, IndizesPOST /tablesdb/{db}/tables/{t}/indexes {key, type: 'fulltext'|'key', columns}erst nachdem alle Spaltenavailable; Index-Status ebenfalls aufavailablewarten. Vergleich (--check) deckt Spaltentyp, Standard, Enum-Elemente, Index-Typ/Spalten ab. -
docs/appwrite-console.md: Abschnitt „Community“: JWT muss an bleiben; Labeladminsetzen; Keyaircard-api(Scopesrows.read,rows.write,files.read,files.write) +read -s-Befehl fürAIRCARD_API_KEY; Hinweis, dass der Provisioning-Key für Indizes zusätzlichindexes.read/writebraucht. -
Step 1: Tests – Fake-Server um boolean/integer/enum/index-Routen erweitern; leerer Zustand →
--checkmeldet alle neuen Objekte als FEHLT; Anlegen → alles da, Indizes erst nachavailable-Spalten; zweiter Lauf legt nichts an; abweichender Index-Typ → ABWEICHUNG. -
Step 2: FAIL, Step 3: Implementieren, Step 4:
node --test web/scripts/appwrite-setup.test.mjsgrün, Step 5: Commitfeat(appwrite): Schema für Galerie, Meldungen und Galerie-Bilder.
Task 3: API-Dienst aircard-api
Files:
- Create:
deploy/api/appwrite.js(REST-Helfer mit Server-Key),deploy/api/app.js(Routen, Validierung, Logik),deploy/api/server.js(Start: Env lesen,http.createServer),deploy/api/app.test.js,deploy/api/README.md - Modify:
deploy/compose-snippet.yml(Serviceaircard-api),deploy/README.md
Interfaces:
-
appwrite.js:createAppwrite({ endpoint, project, key, fetchImpl })→{ getRow, listRows(table, queries), createRow, updateRow(table, id, data, permissions?), deleteRow, getFile(bucket, id), updateFilePermissions(bucket, id, permissions), deleteFile, accountFromJwt(jwt) }(letzteresGET /accountmitX-Appwrite-JWT, ohne Key; der Client schickt das JWT im HeaderX-Aircard-Jwt). Queries als Appwrite-JSON-Strings ({"method":"equal","attribute":"…","values":[…]}). -
app.js:createApp({ appwrite, salt, now = Date.now, log })→(req, res) => Promise<void>; reine Helfer exportiert:visibleRowPermissions(),hiddenRowPermissions(owner),visibleFilePermissions(owner),hiddenFilePermissions(owner),reporterHash(salt, ip)(HMAC-SHA256 hex),clientIp(req)(x-real-ip, sonst erstesx-forwarded-for, sonst Socket),validatePublish(body)→{ok, data|message}(Längen Spec §6, Tags normalisiert: trim, klein, leer raus, max. 5, je ≤ 20),buildSearchText({title, issuer, tags})(klein, ≤ 300). -
Endpunkte, Statuscodes und Texte exakt Spec §8/§11:
POST /api/veroeffentlichen(401 ohne/ungültiges JWT, 400 Validierung, 403 Datei nicht meine/falscher Bucket, 409 „Diese Karte ist schon veröffentlicht.“, 201{id}),POST /api/zurueckziehen(403 fremd, 404 fehlt, 200),POST /api/melden(400 Grund, 404 fehlt/ausgeblendet, 200{ok:true}bzw.{schonGemeldet:true}, 429 „Zu viele Meldungen – bitte später nochmal.“),POST /api/admin/sichtbarkeit(403 kein Admin, 200),GET /api/gesundheit. Body max. 8 KB (413), nurapplication/json(415), sonst 404. -
server.js: EnvAIRCARD_API_PORT(8088),AIRCARD_APPWRITE_ENDPOINT(http://webklar-appwrite/v1),AIRCARD_APPWRITE_PROJECT_ID,AIRCARD_API_KEY,AIRCARD_API_SALT; fehlt Key oder Salt → Fehlermeldung ohne Wert,exit(1). -
Compose-Service:
aircard-api=node:22-alpine,container_name webklar-aircard-api,init: true,mem_limit 128m,restart unless-stopped, Volume./aircard-ios.org/api:/srv:ro, Commandnode /srv/server.js, Env wie oben aus${…}, Netzwebklar-network, Labels Routeraircard-apiHost(`aircard-ios.org`) && PathPrefix(`/api`)websecure/letsencrypt Priorität 200, Port 8088. -
Step 1: Tests (
node:test, Fake-Appwrite im Speicher, echte HTTP-Requests auf Port 0): Veröffentlichen ok (Zeile mit sichtbaren Rechten,searchText,ownerId); ohne JWT 401; fremde Datei (Rechte ohnedelete("user:me")) 403; Datei in falschem Bucket 403; doppelt 409; Validierung (Titel leer/zu lang, 6 Tags) 400. Melden: 3 verschiedene IPs →hidden=true, Zeilen- und Dateirechte = ausgeblendet; dieselbe IP 5× →reportCount1, AntwortenschonGemeldet; 11. Meldung derselben IP in 1 h → 429, nachnow+1 h wieder erlaubt; ausgeblendete Karte melden → 404. Zurückziehen: Autor ok (Zeile, 2 Dateien, Meldungen weg), Admin ok, fremd 403. Admin: Nicht-Admin 403; Einblenden setzt sichtbare Rechte,hidden=false,reportCount=0, löscht Meldungen. Body > 8 KB → 413; Log enthält nie Key/Salt/JWT. -
Step 2: FAIL (
node --test "deploy/api/*.test.js"), Step 3: Implementieren, Step 4: PASS, Step 5: Commitfeat(api): aircard-api für Veröffentlichen, Melden und Admin.
Task 4: Client-Datenschicht Community
Files:
- Create:
web/src/community/gallery.ts,web/src/community/api.ts,web/src/community/publish.ts - Modify:
web/src/account/appwrite.ts(IDS.gallery='gallery',IDS.reports='reports',IDS.galleryBucket='gallery-images'),web/src/account/cards.ts(fetchFile(bucketId, fileId)verallgemeinern, bestehende Aufrufer anpassen) - Test:
web/src/community/gallery.test.ts,api.test.ts,publish.test.ts
Interfaces:
-
gallery.ts:interface GalleryCard { id; title; issuer; tags: string[]; displayName; imageFileId; previewFileId; sourceCardId; ownerId; hidden: boolean; reportCount: number; createdAt: string };toGalleryCard(row);galleryQueries(opts: { q?: string; cursor?: string; filter?: 'all'|'reported'|'hidden' }): string[](öffentlich immerequal('hidden', false),orderDesc('$createdAt'),limit(24), Suchesearch('searchText', q), Admin-Filter laut Spec §7);listGallery(opts)→{ cards, nextCursor | null };getGalleryCard(id)→GalleryCard | null(404 → null);myPublications(userId)→Map<sourceCardId, GalleryCard>;listReports(galleryId)(Admin);loadGalleryImage(card, 'full'|'preview')→ Blob (überfetchFile). -
api.ts:apiPost(path, body, { auth: boolean })→ JSON; mitauthholtaccount.createJWT()(Cache 10 min); Fehler alsApiError { status, message }mit Server-Text; Netzfehler → „Der Dienst ist gerade nicht erreichbar.“;publishCard(input),withdrawCard(id),reportCard(id, reason),setVisibility(id, visible). -
publish.ts:publishSavedCard(card: SavedCard, form: { title; issuer; tags; displayName }, deps)→{ id }: Bild laden, rendern (PNG 1536 + WebP 480 viaexportCard), zwei Uploads ingallery-imagesmit[Permission.read(Role.any()), Permission.delete(Role.user(uid))], dannpublishCard; bei Fehler beide Dateien löschen (best effort) und Fehler weiterwerfen.normalizeTags(input: string): string[](wie Server). -
Step 1: Tests – Queries exakt (inkl. Suche + Cursor + Admin-Filter);
toGalleryCardmit fehlenden Feldern;getGalleryCard404 → null;apiPostmitauthsendet HeaderX-Aircard-Jwt: <jwt>; JWT-Cache (zweiter Aufruf innerhalb 10 min ohne neuescreateJWT); Fehlertext aus JSONmessage; Netzfehler-Text.publishSavedCard: Reihenfolge Upload, Upload, API; API-Fehler → 2×deleteFile, Fehler kommt an; zweiter Upload scheitert → erster gelöscht. -
Step 2: FAIL, Step 3: Implementieren, Step 4: PASS + tsc + build, Step 5: Commit
feat(community): Datenschicht für Galerie, API und Veröffentlichen.
Task 5: Startseite (Hero + Galerie)
Files:
- Modify:
web/src/pages/home-page.ts(Platzhalter aus Task 1 ersetzen) - Create:
web/src/community/gallery-grid.ts,web/src/community/community.css,web/public/hero-beispiel.png(1536 × 969, eigenes markenfreies Motiv: lila/blauer Verlauf mit abstrakten Wellen + AirCard-IOS-Symbol, erzeugt mit einem Skript im Scratchpad) - Test:
web/src/pages/home-page.test.ts,web/src/community/gallery-grid.test.ts
Interfaces:
-
mountHomePage(root, deps = defaultDeps)– Hero laut Spec §4 (Texte exakt), Hero-Karte = Vollbild der neuesten sichtbaren Karte oder/hero-beispiel.png; Galerie-Abschnittid="community"; liest?q=aus der URL; „Mehr laden“ hängt an; Leer-/Kein-Treffer-Texte exakt; Admin sieht Filter-Chips „Alle/Gemeldet/Ausgeblendet“. -
mountGalleryGrid(el, { load, onTag })– Kacheln (Vorschau per Object-URL, beim Cleanup freigeben), Titel, Herausgeber, ≤ 3 Tags als Knöpfe; Kachel verlinkt/community/<id>(data-link). -
CSS: Hero
min-height: auto; max-height: 60vh, Rasterrepeat(auto-fill, minmax(260px, 1fr)); Hero-Karte schwebt (@media (prefers-reduced-motion: no-preference)). -
Step 1: Tests – leere Galerie → Beispielbild + Leertext; Suche aus
?q=landet in Queries und zeigt aktiven Begriff mit „ד; „Mehr laden“ nur beinextCursor; Tag-Klick navigiert zu/?q=<tag>#community; Object-URLs werden beim Cleanup freigegeben. -
Step 2: FAIL, Step 3: Implementieren (inkl. Hero-Bild), Step 4: PASS + build; Sichtprüfung headless Chromium (
npx vite preview) bei 1440 × 900 und 390 × 844 mit gemockter Galerie (Playwrightpage.routebeantwortet/v1/tablesdb/…/rowsmit 30 Fake-Zeilen und/v1/storage/…/viewmit einem PNG) – Überschrift „Community-Karten“ und Oberkante der ersten Reihe im Viewport; Screenshots im Scratchpad. Step 5: Commitfeat(home): kompakter Hero und Community-Galerie.
Task 6: Detailseite /community/<id> und „Als Vorlage“
Files:
- Modify:
web/src/pages/community-page.ts(Platzhalter aus Task 1 ersetzen),web/src/editor/editor-page.ts(?vorlage=<id>) - Create:
web/src/community/report-dialog.ts - Test:
web/src/pages/community-page.test.ts,web/src/community/report-dialog.test.ts, Ergänzungweb/src/editor/editor-page.test.ts
Interfaces:
-
Detailseite laut Spec §5 (Texte exakt): Herunterladen (Blob →
aircard-<slug>.png), „Als Vorlage gestalten“ →navigate('/gestalten?vorlage=<id>'), Melden-Dialog (Gründe exakt, Ergebnis-Texte), Autor „Zurückziehen“ (Bestätigung viaconfirmDialog, danachnavigate('/')), Admin „Ausblenden/Einblenden“, „Löschen“, „ Meldungen“ + Gründe (listReports).document.title=titleFor(route, card.title). -
Editor
?vorlage=<id>: Galerie-Vollbild laden →setImage, Vorlageimage, keinsaved; ungespeicherte Arbeit → bestehende Rückfrage; Fehler → Hinweis „Diese Vorlage ist nicht (mehr) verfügbar.“ -
Step 1: Tests – nicht gefunden/ausgeblendet für Gast → Text + kein Bild; Autor sieht Zurückziehen, Fremder nicht, Admin sieht Admin-Knöpfe + Meldungszahl; Melden: doppelt → „Danke, du hast diese Karte schon gemeldet.“, 429 → Rate-Text; Vorlage setzt Template
imageund lässtsavednull. -
Step 2–5 wie üblich; Commit
feat(community): Detailseite, Melden, Admin-Aktionen, Vorlage.
Task 7: Veröffentlichen aus „Meine Karten“
Files:
- Create:
web/src/community/publish-dialog.ts - Modify:
web/src/pages/cards-page.ts,web/src/pages/cards.css - Test:
web/src/community/publish-dialog.test.ts, Ergänzungweb/src/pages/cards-page.test.ts
Interfaces:
-
Dialog laut Spec §6 (Felder, Grenzen, Pflicht-Checkbox-Text exakt), Absenden über
publishSavedCard; Kachel-Status ausmyPublications(uid): „Veröffentlicht“ + „Ansehen“ (/community/<id>) + „Zurückziehen“ bzw. „Ausgeblendet (gemeldet)“; nicht veröffentlicht → „Veröffentlichen“. Nicht angemeldet → bestehendeswithLogin. -
Step 1: Tests – Validierung im Dialog (Titel leer, > 60, 6 Tags, Checkbox fehlt); Erfolg → Badge + Ansehen-Link; 409 → Text; Zurückziehen → Status zurück.
-
Step 2–5; Commit
feat(community): Veröffentlichen aus Meine Karten.
Task 8: Betrieb (Controller) und Abnahme
- Kenso: Label
admin, Keyaircard-apiperread -salsAIRCARD_API_KEYin.env(Controller liefert den Befehl); Provisioning-Key umindexes.read/writeergänzen oder Controller legt Indizes nach Kensos OK an. - Controller:
AIRCARD_API_SALT(32 Byte hex) in.env(Backup), Setup-Skript--check→ anlegen →--checkOK;/home/webklar/apps/aircard-ios.org/api/mitdeploy/api/*.jsfüllen (cp); Compose-Backup, Service einfügen,docker compose config -q,docker compose up -d aircard-api;curl https://aircard-ios.org/api/gesundheit→{"ok":true}; Push → Webhook-Build. - Live-Abnahme (Claude in Chrome, Kensos Sitzung): Startseite 1440 × 900 Karten angeschnitten; Hell/Dunkel; Veröffentlichen einer Testkarte; Suche nach Tag; Detailseite, Herunterladen, Als Vorlage; Melden (3 verschiedene
X-Real-Ipperdocker exec webklar-aircard-apilokal gegen127.0.0.1:8088) → ausgeblendet, Gast sieht „nicht (mehr) verfügbar“; Admin Einblenden; Zurückziehen. - Final-Review (Workflow, 3 Linsen) → eine Fix-Welle → Push; Memory aktualisieren; Bericht an Kenso.