pageta/docs/EXTRAKTION.md
Till JS fb057da372 fix(web): Umlaute im Bookmarklet-Weg nicht mehr verlieren
winfuture.de/news,161132.html per Bookmarklet gespeichert: ArticleSaved
trug 39 Ersatzzeichen im Text. Der Server-Fix vom 29.07. galt nur dem
Server-Abruf. Das Bookmarklet baut sein <form> im fremden Dokument, und
ein Formular ohne accept-charset kodiert im Zeichensatz dieses Dokuments
(winfuture: ISO-8859-1, `ö` als %F6) — request.formData() liest UTF-8.

- Bookmarklet setzt acceptCharset='UTF-8'.
- Hook liest den Body über $lib/form-body: Escapes selbst zu Bytes,
  striktes UTF-8, sonst Windows-1252. Repariert auch alte Lesezeichen,
  die nicht mitdeployt werden.
- collectPageta holte Folgeseiten per res.text() — dieselbe Falle, jetzt
  BOM > Header > <meta>. Kopie in der Erweiterung zeichengleich.

106 Tests grün (10 neu, Latin-1-Test per Mutation rot gesehen),
svelte-check 0 Fehler, Erweiterungs-Check grün.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgK5h7sUgh9cdqyv85RBCq
2026-09-14 16:28:08 +02:00

434 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Extraktion — warum Artikel manchmal leer ankommen
Stand 2026-07-29. Messungen aus dem Produktions-Container (`pageta-api`),
nicht vom Laptop — der Unterschied ist relevant, weil colima-Egress eigene
Aussetzer hat.
Verwandt: [`docs/ui-review-2026-07-26.html`](ui-review-2026-07-26.html) (P-05),
`mana/docs/AGGREGATOR_POLICY.md`.
## Wie der Weg heute läuft
```
saveArticle(url)
└─ POST pageta-api /api/v1/articles/extract/preview { url, html? }
├─ html mitgeschickt? → Readability auf DIESES HTML (Bookmarklet v2)
└─ sonst → anonymer Server-Fetch, UA „ManaPageta/0.1"
→ Readability
└─ emit ArticleSaved { title, excerpt, content, siteName, … }
└─ (non-blocking) archiveAndEmit → ArticleMediaArchived
```
Der Server-Fetch trägt **keine Cookies und keine Session**. Das ist die
Wurzel von allem, was unten steht.
## Drei verschiedene Fehlerbilder — nicht ein Problem
Sie brauchen verschiedene Antworten, deshalb müssen sie im UI und in den
Daten auseinandergehalten werden.
### 1. Harte Consent-Wand (nicht serverseitig lösbar)
`golem.de` antwortet auf **jeden** Cookie-losen Request mit 302 auf
`/sonstiges/zustimmung/auswahl.html` — unabhängig vom User-Agent
(mit `ManaPageta` und mit einem Chrome-UA identisch geprüft).
```
GET /news/support-panne-instagram-…-2606-209284.html
→ 302 Location: /sonstiges/zustimmung/auswahl.html?from=…
→ 200, 18 KB
Readability daraus: title „Golem" · siteName „golem.de" · wordCount 254
excerpt „Besuchen Sie Golem.de wie gewohnt mit Werbung…"
```
Kein User-Agent, kein Header, kein Retry kommt daran vorbei. Nur eine
zustimmende Browser-Session — also **Bookmarklet, Erweiterung oder
Share-Extension**.
Die Erkennung greift hier korrekt: 254 < `CONSENT_WORDCOUNT_THRESHOLD`
(300) **und** das Keyword „wie gewohnt mit werbung und tracking" steht in
`CONSENT_KEYWORDS`. `detectConsentWall` liefert `true`, der Client emittiert
`ArticleWarningFlagged`.
> ⚠️ Die Wortzahl-Schwelle ist knapp. Golem liegt bei 254, eine
> ausführlichere Consent-Seite (Sourcepoint-Vollseite) rutscht über 300 und
> wird dann **nicht** erkannt. Robuster wäre ein deterministisches Signal:
> _finale URL nach Redirects ≠ angeforderte URL_ → sicher kein Artikel,
> unabhängig von der Wortzahl.
### 2. Gar nicht extrahiert (reparierbar, keine Wand)
`the-decoder.de` liefert dem anonymen Server-Fetch **den vollständigen
Artikel**:
```
GET /ki-modell-trennt-zwei-arten-wie-koeche-ueber-zutaten-denken/
→ 200, 159 KB
<title> KI-Modell trennt zwei Arten, wie Köche über Zutaten denken
og:title dito · 944 Wörter Fließtext
```
Trotzdem stand dieser Artikel in der Lese-Liste als „the-decoder.de" ohne
Volltext. Die gespeicherten Einträge dieser Sorte haben:
- **aufeinanderfolgende ULIDs** (im Millisekunden-Abstand erzeugt)
- `title` == Hostname
- `content` leer
- **kein** `warning`-Flag
Das ist die Signatur eines **Batch-Schreibvorgangs, der die Extraktion
übersprungen hat** — nicht die einer Consent-Wand. Diese Artikel wären
sofort reparierbar.
**Herkunft geklärt (2026-07-27):** nicht der Bulk-Import, wie zuerst
vermutet — **alle aus der iOS-App**. Die Events tragen
`actor.displayName: "ios"`. Aufschlüsselung derselben Datenbank, 54
`ArticleSaved`-Events:
| Quelle | gespeichert | davon Stub | Textlänge der Stubs |
| -------------------- | ----------- | ---------- | ----------------------------------------- |
| web · golem.de | 9 | **0** | — |
| web · the-decoder.de | 4 | **0** | — |
| ios · golem.de | 12 | **12** | 2664 Zeichen (= die Consent-Seite) bzw. 0 |
| ios · winfuture.de | 22 | **16** | 0 |
| ios · the-decoder.de | 6 | 1 | 0 |
| ios · engadget.com | 1 | 1 | 0 |
Zwei verschiedene Dinge stecken darin:
- **golem über iOS** — die 2664 Zeichen sind exakt der Consent-Text (254
Wörter, deckungsgleich mit der Messung oben). Die Wand, wie erwartet.
- **winfuture über iOS** — 16 Einträge mit **leerem** Text, von einer
Quelle, die serverseitig einwandfrei ausliefert. Das ist keine Wand,
sondern ein Extraktions-Fehlschlag im nativen Speicherpfad. Warum, lässt
sich rückwirkend nicht mehr feststellen (die Speicherungen sind von
Mai 2026).
Der iOS-Code selbst ist an dieser Stelle **richtig**: er emittiert
`ArticleWarningFlagged`, sobald der Server `preview.warning` liefert
(`EventSyncCoordinator.swift:227`). Nur lieferte der Server die Warnung
damals nicht — die Erkennung hing an der Wortzahl-Heuristik, die seit
heute um das deterministische Redirect-Signal ergänzt ist. Neue
iOS-Speicherungen sollten damit korrekt als `blocked` markiert werden.
> **Bleibt offen:** warum die native Extraktion bei winfuture leeren Text
> lieferte. Solange das nicht geklärt ist, ist „leere Artikel gar nicht
> erst speichern" im nativen Pfad die falsche Reihenfolge — erst messen,
> dann verhindern. Die Web-Reparatur holt sie inzwischen alle nach.
### 2b. Nur Seite 1 gespeichert (mehrseitiger Artikel)
Kein Extraktions-Fehler, sondern ein Umfang-Fehler: der Artikel ist auf
mehrere Seiten verteilt, gespeichert wurde eine.
```
golem.de/news/wirtschaftsblase-…-2607-211258.html
Seite 1 …-211258.html 911 Wörter ← das war alles, was ankam
Seite 2 …-211258-2.html
Seite 3 …-211258-3.html
```
Golem verlinkt die Seiten in `.go-pagination__list`; es gibt **kein**
`<link rel="next">`, keine Druck- und keine „auf einer Seite"-Ansicht.
Serverseitig ist da nichts zu holen: Seite 2 liegt hinter derselben Wand
wie Seite 1. Ein `fetch` **aus dem Tab** bekommt sie dagegen anstandslos
(gemessen 2026-07-27: `…-211258-2.html` → 200, kein Redirect auf
`/sonstiges/zustimmung/`). Also sammelt der Tab sie — siehe
[Mehrseitige Artikel](#mehrseitige-artikel) unten.
### 2c. Zeichensalat — Text da, Umlaute zerstört (behoben 2026-07-29)
Anders als 1 und 2: die Extraktion **gelingt**, Wortzahl und Struktur
stimmen, nur jedes Nicht-ASCII-Zeichen ist ein Ersatzzeichen.
```
Fehlender Unterstrich f?hrte zu 18 Monaten Haft f?r Unschuldigen
```
Die Ursache liegt vor Readability. `Response.text()` dekodiert laut
fetch-Spezifikation **immer** als UTF-8, egal was Server oder Dokument
deklarieren. `winfuture.de` liefert ISO-8859-1 und sagt das **nur im
`<meta http-equiv>`, nicht im HTTP-Header** — gemessen an
`news,160269.html`: 150 Ersatzzeichen im Roh-HTML, bevor irgendetwas
extrahiert wurde. Der Schaden entsteht beim Lesen der Bytes und ist danach
nicht mehr reparierbar.
**Fix:** `@mana/shared-rss@0.5.1` bringt `decodeResponseText()` — Body als
Bytes holen, Zeichensatz in Browser-Reihenfolge bestimmen (BOM >
Content-Type > Dokument-Deklaration > UTF-8), deklariertes `iso-8859-1` als
`windows-1252` behandeln. Genutzt von `extractFromUrl`,
`discoverFeedsFromSite` sowie hier von `fetchHtml` (Artikel) und
`fetchText` (Feed-/Sitemap-Archiv). **Regel: bei fremdem HTML nie `.text()`.**
Nachgemessen im Prod-Container: 0 Ersatzzeichen, 501 Wörter, Titel mit
Umlauten.
> **Rest-Lücke:** `parseFeedUrl` geht über `rss-parser.parseURL`, das den
> Zeichensatz nur aus dem Content-Type liest, nicht aus dem XML-Prolog.
> Ein Feed, der ISO-8859-1 nur im Prolog deklariert, garbelt weiterhin —
> betrifft `mana-news-pool` und den Feed-Archiv-Import. Fix wäre: selbst
> fetchen + `decodeResponseText` + `parseFeedXml` statt `parseFeedUrl`.
> **Zweiter Weg, gefunden 2026-09-14: das Bookmarklet.** Der Server-Fix
> galt nur dem Server-Abruf. `winfuture.de/news,161132.html` per
> Bookmarklet gespeichert: `ArticleSaved` mit 39 Ersatzzeichen im Text,
> Titel sauber (er hatte keine Umlaute). Ursache: das Bookmarklet baut sein
> `<form>` im fremden Dokument, und ein Formular ohne `accept-charset`
> kodiert im Zeichensatz DIESES Dokuments — `ö` geht als `%F6` raus,
> `request.formData()` im Hook liest UTF-8. Die Erweiterung war nie
> betroffen (`fetch` + `FormData` ist immer UTF-8). Fix: Bookmarklet setzt
> `acceptCharset='UTF-8'`; weil alte Lesezeichen nicht mitdeployt werden,
> liest der Hook den Body über `$lib/form-body` (Bytes selbst entschlüsseln,
> striktes UTF-8, sonst Windows-1252). Dazu holte `collectPageta` die
> Folgeseiten mehrseitiger Artikel per `res.text()` — dieselbe Falle, jetzt
> mit BOM > Header > `<meta>`.
Bestehende Artikel heilt der Fix **nicht** — sie tragen den Salat in den
Daten. `articleQuality` kennt dafür `garbled` (ein Ersatzzeichen im Titel
oder drei im Text; die Schwelle im Text lässt Artikel durch, die das
Zeichen zitieren). Der Zustand ist `isRepairable`, läuft also über
denselben „Neu extrahieren"-Weg wie Fall 2, und trägt in der Liste das
Schild „Zeichensalat".
### 2d. Fragment ohne `ArticleSaved` (kein Artikel, unlöschbar)
Gefunden 2026-07-29: ein Aggregat aus **vier Events** — zwei
`ArticleProgressUpdated`, ein `ArticleStatusChanged`, ein weiteres
Progress-Event — und **keinem `ArticleSaved`**. Wie es dazu kam, lässt
sich nachträglich nicht mehr klären.
Folge: `state.id` setzt allein `ArticleSaved`, war hier also `undefined`.
Die Karte stand ohne Titel und ohne Quelle in der Liste, und **löschen ging
nicht** — `batchDelete([undefined])` schrieb `ArticleDeleted` brav auf
`article:undefined` und liess das echte Aggregat unangetastet.
`listArticles` nimmt die Identität jetzt aus dem **Aggregat-Schlüssel**
statt aus dem Zustand und blendet Einträge ohne `originalUrl` aus. Beides
zusammen: so ein Fragment taucht nicht mehr auf, und falls doch je eines
sichtbar wird, lässt es sich löschen.
### 3. Funktioniert (die Mehrheit)
`winfuture.de` und die meisten Blogs kommen sauber durch: 200, keine
CMP-Signatur, echter Titel. Von 40 Artikeln der Testliste waren 32 in
Ordnung, 8 kaputt — und die 8 verteilen sich auf Fall 1 und Fall 2.
> Nachtrag 2026-07-29: „sauber durch" stimmte für Struktur und Wortzahl,
> nicht für die Zeichen — winfuture kam die ganze Zeit mit zerstörten
> Umlauten an (Fall 2c). Das fiel nicht auf, weil die Zählmaße heil aussahen.
## Der Reparatur-Weg liegt schon im Schema
`evt_article_enriched_v1` existiert in `@mana/shared-schemas` mit genau
dem passenden Payload und einem **nicht-destruktiven** Reducer:
<!-- als `text` ausgezeichnet: Prettier formatiert einen `js`-Block um und
macht aus dieser Skizze gültigen, aber unlesbaren Code. -->
```text
payload: { articleId, title, excerpt, content, author, siteName,
imageUrl, wordCount, readingTimeMinutes, publishedAt, warning }
reducer: imageUrl: e.payload.imageUrl ?? state.imageUrl // clobbert nichts
warning: e.payload.warning ?? state.warning
```
Bisher emittiert das **nur Native**. Der Web-Client kennt es nicht. Ein
„Neu extrahieren" ist damit keine Schema-Änderung, sondern ein Client-Aufruf.
Ebenfalls schon da: `evt_article_warning_cleared_v1`.
## Lösungsraum
### Serverseitig — billig, aber prinzipiell begrenzt
| Maßnahme | Wirkung |
| -------------------------------------------------------------- | -------------------------------------- |
| Redirect-Ziel prüfen statt Wortzahl raten | macht Fall 1 deterministisch erkennbar |
| Metadaten-Fallback (`og:title`, `og:image`) wenn kein Volltext | Karte heißt richtig, auch ohne Text |
| Schwellwerte/Keywords pflegen | Symptombehandlung, altert schlecht |
| AMP-/Print-Varianten probieren | quellenspezifisch, wackelig |
**Keine dieser Maßnahmen löst Fall 1.** Golem bleibt zu.
### Client-Erfassung — der einzige Weg an harte Wände
| Weg | Status | Hürde |
| -------------------------- | ---------------- | ------------------------------------- |
| HTML-Bookmarklet (v2) | **funktioniert** | Einrichtung, Lesezeichenleiste |
| Browser-Erweiterung | offen | Manifest V3, Store-Review — einmalig |
| iOS/Android Share mit HTML | teilweise | Android-Share liefert oft nur die URL |
Der Transport ist schon gebaut und erweiterungs-tauglich:
```
POST app.pageta.com/lese-liste/add/receive { url, title, pages[], missing[] }
→ hooks.server.ts stasht (single-use Token, 2 min TTL)
→ 303 /lese-liste/add?pickup=<token>
→ die (same-origin, angemeldete) Seite speichert via event-sync
```
Eine Erweiterung braucht damit **keinen API-Schlüssel** — sie nutzt
dieselbe Session wie der Browser.
## Mehrseitige Artikel
Seit 2026-07-27. Dieselbe Einsicht wie bei der Wand, eine Ebene weiter:
was nur im Tab existiert, muss im Tab eingesammelt werden.
`apps/web/src/lib/page-collector.js` (`collectPageta`) läuft im fremden
Tab — als Quelltext in der `javascript:`-Adresse des Bookmarklets und via
`scripting.executeScript` in der Erweiterung. Es findet Folgeseiten
**ohne quellenspezifische Selektoren**:
1. **Zähler-Adressen.** Ein Link zählt, wenn er sich von der aktuellen
Adresse nur in einem Seitenzähler unterscheidet — `-2.html`, `_2.html`,
`?page=2`, `/seite/2/`. Auf Golem findet das genau Seite 1–3, ohne
`.go-pagination__link` zu kennen.
2. **`rel="next"`-Kette**, wenn (1) nichts liefert — für Quellen, die nur
„weiter" verlinken.
Deckel: 20 Seiten, 6 MB gesamt, 300 ms Pause zwischen den Abrufen. Eine
Seite, die nicht durchkommt, landet in `missing` statt den ganzen Artikel
scheitern zu lassen; die add-Seite springt dann nicht weiter, sondern sagt
„x von y Seiten".
### Gestrippt, aber weich
Vor dem Senden fallen `<style>`, `<noscript>` und `<template>` weg und
Skript-**Inhalte** werden geleert — Tags und Attribute bleiben stehen.
Absichtlich weich, weil zwei Dinge auf dem Server daran hängen:
`siteNameFromJsonLd` (@mana/shared-rss) liest `application/ld+json`, und
die CMP-Signaturen in `consent-wall.ts` erkennen Wände an Attributen wie
`sourcepoint` oder `optanon`.
| Quelle | roh | gestrippt | Extraktion |
| ------------------ | ------ | --------- | -------------------------------------------------------- |
| golem.de (Seite 1) | 401 KB | 145 KB | — |
| the-decoder.de | 155 KB | 107 KB | identisch (844 Wörter, Titel/siteName/Bild/Datum gleich) |
| winfuture.de | 92 KB | 63 KB | identisch (650 Wörter) |
Der Golem-Artikel komplett: **3 Seiten, 298 KB** statt ~1,2 MB roh.
### Serverseitig
`POST /api/v1/articles/extract/preview` nimmt zusätzlich `pages`
(`[{url, html}]`, max 20). Jede Seite läuft einzeln durch Readability —
mit IHRER Adresse, damit relative Verweise pro Seite auflösen — und
`lib/merge-pages.ts` fügt zusammen: Texte in Lese-Reihenfolge, Wortzahl
summiert, Lesezeit neu, Titel/Anriss von Seite 1, fehlende Kopfdaten von
der ersten Seite, die welche hat. Die Antwort trägt `pageCount`.
`html` (eine Seite) bleibt gültig: Bookmarklets liegen im Lesezeichen des
Nutzers und werden nicht mitdeployt. **Ein altes Bookmarklet speichert
weiterhin nur Seite 1** — der Hinweis dazu steht in den Einstellungen.
### Bewusst nicht
- **Headless-Browser mit Nutzer-Cookies serverseitig.** Löst es technisch,
aber wir würden Consent umgehen und fremde Session-Cookies auf unserem
Server halten. Steht quer zu MISSION.md und AGGREGATOR_POLICY.md.
- **Consent-Dialoge automatisch wegklicken.** Dasselbe Problem, offener.
- Archiv-Fallback (archive.org) wäre die mildere Variante — braucht aber
eine Policy-Entscheidung, keine technische.
## Reihenfolge
1. **Qualitätssignal am Artikel** — Wortzahl, hat Titel, hat Absätze. Dann
ist „kaputt" abfragbar statt geraten, und jede Verbesserung messbar.
2. **Neu extrahieren** über `ArticleEnriched`, einzeln und als Stapel.
3. **Deterministische Wand-Erkennung** (Redirect-Ziel) + Metadaten-Fallback.
4. **Browser-Erweiterung** — der eine große Hebel für Fall 1.
5. ~~**Periodischer Re-Extract**~~ — **verworfen, mit Grund.**
AGGREGATOR_POLICY.md erlaubt pageta das Abrufen fremder Seiten nur unter
der Bedingung „user-initiiert, pro Artikel — nie Crawl, nie Feed-weit"
(Ausnahme „Privatkopie-Archiv", Bedingung 1). Ein Timer im Hintergrund
ist nicht user-initiiert. Der Wert bleibt über den Knopf erreichbar;
was fehlte, war ein Gedächtnis — das gibt es jetzt
(`$lib/reextract-attempts.ts`, 24-Stunden-Sperre nach einem erfolglosen
Versuch, „trotzdem nochmal" bleibt sichtbar).
6. **Mehrseitige Artikel im Tab sammeln** — erledigt 2026-07-27, siehe
oben. Offen bleibt der Zweitweg: derselbe Detektor serverseitig für
Bulk-Import, Feed-Archiv und „Neu extrahieren" — dort gibt es keinen
Tab, aber bei Quellen ohne Wand funktioniert das Nachladen.
7. **`pageCount` speichern** — heute nur in der Extraktions-Antwort, nicht
im Event. Ein Feld in `evt_article_saved_v1` /
`evt_article_enriched_v1` bedeutet `@mana/shared-schemas` mit 14
Konsumenten; deshalb bewusst noch nicht.
## Was nicht versprochen wird
Manche Quellen (Pur-Abo, harte Paywall) sind bewusst zu. Dann speichert
Pageta den Link mit Metadaten und **sagt das offen**, statt einen
Schein-Artikel anzulegen. Das ist die werte-konforme Antwort, nicht die
Ausrede.
## ⚠️ Blockiert: „Neu extrahieren" greift nicht durch (Stand 2026-07-27)
Der Reparatur-Weg ist gebaut, deployt — und **wirkungslos**, aus einem Grund,
der tiefer liegt als diese App.
Gemessen nach dem ersten Stapel-Lauf (`8 ohne Text neu holen` →
„4 geholt · 4 von der Quelle blockiert"): die `ArticleEnriched`-Events liegen
korrekt im lokalen Log, mit richtigem Titel und 844 Wörtern, die Outbox ist
leer (also gepusht). Die Projektion zeigt trotzdem weiter `the-decoder.de`
ohne Text.
**Ursache** — `@mana/event-sync@0.14.0`, `dist/storage/event-log.js`:
```js
function toBig(v) {
if (v == null) return 0n;
return BigInt(v);
}
events.sort((a, b) => toBig(a.sequenceNumber) - toBig(b.sequenceNumber));
```
Lokal emittierte Events haben `sequenceNumber: null` bis zum nächsten Pull.
`toBig(null)` macht daraus `0n` — sie sortieren also **vor** allem, was schon
eine Nummer hat. `replay()` faltet in dieser Reihenfolge:
```
ArticleEnriched seq null → 0n
ArticleMediaArchived seq null → 0n
ArticleSaved seq "1" → 1n ← zuletzt angewandt, überschreibt alles
```
Und die Sequenznummer kommt nie nach: `append()` dedupliziert per `eventId`,
der Re-Pull überschreibt die lokale Kopie nicht (bekannt, siehe
`reference_pageta_sync2_schema_and_servicekey_drift` Abschnitt C).
**Das trifft nicht nur die Extraktion.** In derselben Datenbank betrifft es
9 Aggregate, darunter `ArticleStatusChanged` und `ArticleProgressUpdated` auf
einem Bestands-Artikel — Lesestatus und Lesefortschritt gehen auf diesem
Client also ebenfalls still verloren.
**Fix** gehört ins geteilte Paket (14 Apps konsumieren es): ein Event ohne
Sequenznummer ist per Konstruktion neuer als jedes sequenzierte und gehört
ans Ende.
```js
const UNSEQUENCED = 2n ** 63n;
function toBig(v) {
if (v == null) return UNSEQUENCED;
return BigInt(v);
}
```
Dazu ein Golden-Test mit gemischtem Stream. Bis dahin nützt jede Funktion
nichts, die ein **bestehendes** Aggregat nachbessert — neu gespeicherte
Artikel sind nicht betroffen, weil dort alle Events unsequenziert sind und
die Einfüge-Reihenfolge stimmt.
## ✅ Blocker behoben (2026-07-27)
`@mana/event-sync@0.15.1` sortiert Events ohne Sequenznummer jetzt hinter
die bereits gesyncten. Am selben Artikel nachgemessen, ohne dass ein neues
Event geschrieben wurde — dieselben drei Events, nur richtig sortiert:
| | vorher | nachher |
| ----- | ---------------- | ------------------------------------------------------------ |
| Titel | `the-decoder.de` | „KI-Modell trennt zwei Arten, wie Köche über Zutaten denken" |
| Text | 0 Zeichen | 6410 Zeichen · 844 Wörter |
Die Reparatur lag die ganze Zeit im Log. Sie wurde beim Replay überschrieben.