Vom Flagship-Katalog zur richtigen Bibliothek — rohtext.org-Import, Sentry-Beobachtbarkeit pro Pipeline und der Login-Pfad, der hinter sich aufräumt
Foto: Inaki del Olmo auf Unsplash
Die letzten Wochen waren breit gestreut — Push-Benachrichtigungen, Test-Abdeckung, Stille im Sentry-Dashboard. Diese Woche hat ein klares Zentrum, und es ist groß: OutaStory hat aufgehört, ein 42-Geschichten-Flagship-Katalog zu sein, und ist auf dem Weg zu einer richtigen Bibliothek. Eine Quelle für deutschsprachige Klassiker, die wir legal und mit Segen ihres Betreibers in die Plattform übernehmen, mit einer eigenen Mini-AI-Strecke pro Buch (Kategorisierung, Jugendschutz-Einstufung, Kurzbeschreibung, Autor-Biografie), einem Sieben-Phasen-Aufbau von der Spezifikation bis zum Admin-Dashboard und einer Nacht-Welle, die jetzt zwei Tausend Bücher dauerhaft in den Suchpfad und in die Story-Detail-Seiten gelegt hat. Daneben — kleiner, aber nicht weniger wichtig — ein still gewordener Profil-Vervollständigungs-Pfad, der durch einen einzelnen Bug-Report eines Test-Nutzers ausgelöst wurde, und eine zweite Welle Auth-Härtung für Schaltkreise, die nach langem Idle bisher nur scheinbar eingeloggt waren.
rohtext.org als Quelle — und was das überhaupt heißt
Bevor ich zur Pipeline komme, kurz das Warum. OutaStory's Flagship-Katalog besteht seit dem Relaunch aus 42 sorgfältig zusammengesetzten Originalgeschichten, je 11 Kapitel, hand-kuriert und mit allem Drum und Dran (KI-Cover, Audio, deutsche und englische Versionen). Das ist ein Anfangs-Katalog, der zeigt, wie eine Geschichte auf OutaStory aussehen soll — aber kein Katalog, in dem du als Leser:in nach „etwas Neuem für heute Abend" stöberst.
rohtext.org — betrieben vom österreichischen Stefan Gündhör — sammelt seit Jahren öffentlich-rechtliche deutschsprachige Klassiker (gemeinfreie Texte aus der DACH-Region, deren 70-Jahre-Schutzfrist abgelaufen ist) und stellt sie über eine wohlgeformte JSON-API bereit. Aktueller Stand: 9.235 Bücher im Katalog, von Goethe und Schiller über die Romantik bis hin zu Karl May, Theodor Storm, Else Lasker-Schüler und vielen, vielen weniger bekannten Stimmen. Die rohen Texte sind explizit für die kommerzielle Weiterverwendung ohne Namensnennungs-Pflicht freigegeben.
Das Schöne an rohtext: es ist die Rohtext-Schwester einer kommerziellen Plattform namens freitext.org. freitext.org legt eigene KI-generierte Cover, Beschreibungen, Genre-Klassifizierungen und Autor-Porträts dazu — die sind Stefans Werk und urheberrechtlich geschützt. Wir nutzen ausschließlich rohtext (also nur die rohen, gemeinfreien Texte) und bauen die Begleit-Inhalte selbst. Stefan hat dem Vorhaben grünes Licht gegeben; ich bin in Kontakt mit ihm und respektiere die CloudFront-Kosten seiner Origin-Server, indem wir nicht in einem Schwung 9.235 mal wget losschicken, sondern eine respektvolle Polling-Kadenz fahren.
Sieben Phasen, eine Pipeline, drei AI-Aufgaben pro Buch
Foto: Susan Q Yin auf Unsplash
Der eigentliche Importer ist ein eigener Azure-Functions-Host (OutaStory.RohtextImporter), der drei Funktionen orchestriert: einen Tages-Timer, der den Katalog pollt und Neu-/Update-Bücher in eine Service-Bus-Warteschlange schiebt; einen Consumer, der pro Buch die rohtext-API um den Volltext bittet, eine kleine AI-Strecke fährt und das Ergebnis in die OutaStory-Datenbank materialisiert; und einen Retry-Timer, der zwischendurch fehlgeschlagene AI-Calls mit exponentiellem Backoff wiederholt. Die Pipeline wurde in sieben Phasen aufgebaut, jede mit einem eigenen Pull-Request und einer eigenen Review-Schleife.
Phase 1 — Schema + UI-Plumbing. Die Story- und Author-Tabellen bekamen fünf neue Spalten für die Import-Spur (ImportSource, ImportSourceId, ImportSourceStoryUrl, ImportSourceJsonUrl, ImportSourceVersion). Ein zwei-Komponenten-Razor-Block in den Story-Detail-Seiten zeigt jetzt eine deutliche Attributions-Box: „Diese Geschichte stammt aus dem rohtext.org-Katalog von Stefan Gündhör. Gemeinfrei (DACH-Region, 70-Jahre-Schutzfrist abgelaufen)." Die Edit-Berechtigung für importierte Geschichten ist auf Developer + Admin beschränkt — Autor:innen sollten an gemeinfreien Klassikern nicht ohne weiteres herumändern.
Phase 2 — Function-Host-Skelett + Ops-Plumbing. Ein dedizierter Service-Bus-Queue (rohtextimport), eine dedizierte RohtextSyncState-Tabelle für den ETag-basierten Inkrement-Diff gegen die rohtext-API, ein Container-App-Resource im Aspire-AppHost. Eine ServiceDefaults-konforme Health-Probe und das übliche OpenTelemetry-Bundle.
Phase 3 — Client + Materializer + Admin-Endpoint. Ein typisierter HTTP-Client für die rohtext-JSON-API (mit ETag-Caching gegen ihren Origin), eine AuthorResolver-Logik die deutsche Autorennamen auf existierende Autor-Rows mappt (mit einer alias-YAML für „Goethe" vs „Johann Wolfgang von Goethe" vs „J.W. v. Goethe") und ein StoryMaterializer der das Diff in EF-Core-Operationen umsetzt. Plus ein Developer-only-Admin-Endpoint POST /api/admin/rohtext/import/{guid} für manuelle Einzelimporte, der die ganze Pipeline gegen ein einzelnes Buch fährt.
Phase 4 — AI-Subsysteme. Vier gpt-5-Aufrufe pro Buch über Azure OpenAI mit strukturierten JSON-Schemas:
CategoryClassifier— ordnet das Buch einer der 302 Kategorien zu, basierend auf Titel, Erscheinungsjahr und einer 2 kB-Probe aus dem ersten Kapitel.MinAgeClassifier— entscheidet zwischen 0, 6, 12, 16, 18 (USK-Anlehnung), basierend auf Titel, Jahr und einer 4 kB-Probe plus 1 kB aus der Mitte.SummaryGenerator— erzeugt eine 2- bis 3-Satz-Beschreibung im OutaStory-Ton, basierend auf einer 6-kB-Probe plus 1 kB vom Ende.AuthorBioGenerator— wenn die Autor-Row noch keine Bio hat, erzeugt eine prägnante Biografie aus dem Autorennamen und einer Stichprobe von Titeln dieser Person.
Vor jedem AI-Call läuft Azure Content Safety über den Volltext — die meisten Klassiker passieren ohne Anmerkung, gelegentlich kommt ein expliziter Hinweis raus, dem wir mit höherer MinAge-Voreinstellung folgen.
Phase 5 — SeedDumper + Multi-YAML-Loader + Versions-Kette. Wenn die Pipeline lokal läuft (z. B. beim Entwickler), wird jeder erfolgreich materialisierte Import als YAML-Datei in SeedData/stories/rohtext/{autoren-slug}/{geschichte-slug}-v{N}.yaml abgelegt. Beim nächsten Boot lädt der Init.Data-Worker diese YAMLs in eine frisch aufgesetzte Datenbank, was den Stand der Bibliothek über Aspire-Restarts hinweg erhält. Wenn rohtext eine neue Version eines Buches veröffentlicht (textVersion: 2), entsteht eine zweite YAML-Datei und die Story-Versions-Kette macht den Rest — V1 bleibt unter /stories/{slug}/v/1/ erreichbar, V2 wird der neue Default.
Phase 6 — Produktions-Timer + Queue-Consumer. Der Tages-Timer feuert um 03:00 UTC und schickt einen Diff in die Warteschlange — neue Bücher und Versions-Bumps gehen rein, gelöschte rohtext-Einträge werden bewusst ignoriert (per non-goal: keine OutaStory-Seite soll wegen eines rohtext-Pflege-Sweeps verschwinden). Der Consumer arbeitet die Warteschlange ab, ein Buch nach dem anderen, mit Service-Bus-Retry-und-DLQ-Semantik bei harten Fehlern. Embeddings der Volltext-Inhalte in die Service-Bus-Nachricht erspart der rohtext-Origin pro Verarbeitung einen zweiten Roundtrip — bei 9.235 Büchern hätten wir sonst 15 GB CDN-Egress an Stefans Origin geschickt, was nicht freundlich gewesen wäre.
Phase 7 — Admin-Dashboard + DLQ-UI. Auf /management/rohtext/hub zeigt eine kompakte Übersicht den aktuellen Stand (Anzahl importierter Bücher, letzte erfolgreiche Poll-Zeit, Anzahl der DLQ-Nachrichten, AI-Failure-Tabelle pro Call-Typ). Ein zweites Panel auf /management/rohtext/failures listet jede DLQ-Nachricht inspizierbar — Buch-GUID, Operation, JSON-Payload, Fehlermeldung und ein „Retry-Now"-Knopf, der die Nachricht zurück in die Warteschlange schiebt. Das gleiche Panel listet AI-Failures (Kategorisierung scheiterte, Beschreibung scheiterte etc.) mit demselben Retry-Pfad.
Eine Nacht, zweitausend Bücher, ein paar Lektionen
Am Donnerstagabend lief der erste echte Backfill — ein einfaches PowerShell-Skript, das die rohtext-Katalog-Liste durchgeht und pro GUID den manuellen Import-Endpoint aufruft, plus eine /loop-Wächter-Schleife, die alle 20 Minuten Status nimmt, AppHost-Lebensdauer prüft und im Notfall den Treiber neu startet. Der Driver lief die ganze Nacht durch und der Folgemorgen brachte als Ergebnis 2.256 Stories + 586 Autoren aus dem Catalog von 9.235 Büchern — knapp ein Viertel der Bibliothek, je Buch im Schnitt 15 Sekunden Verarbeitungszeit (dominiert von der gpt-5-Latenz, nicht von der Datenbank).
Die Stories sind jetzt als YAMLs in den Repo committet — kein Daten-Migrations-Skript, einfach Seed-Daten, die jede Umgebung beim nächsten Boot über den Init.Data-Worker lädt. Der Rest des Katalogs (rund 6.900 Bücher) wird in den nächsten Tagen über den produktiven Azure-Functions-Importer nachgezogen.
Ein paar Lektionen aus der Nacht:
- gpt-5 hat eine Verfügbarkeits-Rate über 90 %, aber nicht 100 %. Rund 200 Bücher trafen auf transiente 400/500-Antworten beim Summary- oder MinAge-Call. Die
RohtextAiFailure-Tabelle hat sie alle aufgefangen; der nächtliche Retry-Timer kümmert sich um die Wiederholung mit exponentiellem Backoff (1h → 4h → 16h → 24h-Cap). - Slug-Längen können explodieren. Ein Buch namens „Einfache Erzählung von dem schrecklichen Absturze des Schrofenberges und der dadurch erfolgten Verwüstung bei Brannenburg im August 1851 zum Bessten der Verunglückten" produziert einen Slug, der den Windows-260-Zeichen-Pfadlimit reißt. Lokal mit
git config core.longpaths trueumschifft; eine Slug-Kappung auf ~80 Zeichen imSeedDumperist eine Folge-Aufgabe. - Die
RohtextSyncState-Spalte hatte ursprünglich einennvarchar(max)-Column-Type, den SQLite (der für die Unit-Tests verwendete EF-Provider) nicht akzeptiert. Während eines Tests bekamen wir „near 'max': syntax error". Schnellfix: den explizitenHasColumnTypeweggelassen, EF Core wählt selbst (NVARCHAR(MAX)auf SQL Server,TEXTauf SQLite). PollSchedule = ""für lokale Entwicklung war ein Antipattern. Funktions-Discovery zur Boot-Zeit kann ein leeres CRON-Pattern nicht auflösen und deaktiviert die ganze Function. Das hat zwei Stunden Treiber-Lauf gekostet, bis ich dahinter kam. Der Fix war banal: den leeren Override aus derappsettings.Development.jsonrausnehmen.
Sichtbarkeit, Cover und Avatare — der UI-Schicht-Fix
Beim Testen am Freitagvormittag — du klickst auf einen rohtext-Autor, siehst seine Bibliothek-Karten, klickst auf eine Geschichte, und siehst „Geschichte nicht gefunden". Drei Schichten gleichzeitig hatten Lücken:
- Eine Sichtbarkeits-Schalter, ausgeschaltet. Aus Vorsichts-Gründen war beim ersten Seed-Drop eine Runtime-Feature-Flag
RohtextStoriesVisibleauf „aus" gesetzt — dieStoryRepository.GetBySlugAsyncfilterte rohtext-Importe aus dem Katalog, bevor sie zur Detail-Seite kamen. Inzwischen ist der Standardwert für frische Tenants an, und die Live-Umschalt-Position liegt auf/developer/feature-flags. (Operator-Workflow: einmal klicken, fertig.) - Suche und Detail liefen über zwei Repositories, von denen nur eines den Sichtbarkeits-Filter kannte. Die Suche zeigte das rohtext-Buch, der Klick auf das Suchergebnis fiel auf 404 — eine echte Asymmetrie, die mit einem zweiten Repository-Update behoben wurde.
- Cover und Autor-Avatare waren leer für rohtext-Importe — die Materializer-Logik setzt
CoverUrl="", weil der spezifizierte „title-on-color"-Compositor nie gebaut wurde, und Autor-Avatare gibt es bei gemeinfreien Klassikern naturgemäß keine. Eine schnelle Fallback-Schicht in der Projektions-Ebene mappt leere Werte auf deterministische DiceBear-SVGs:https://api.dicebear.com/9.x/thumbs/svg?seed=author-{slug}für Avatare,…/shapes/svg?seed=story-{slug}für Cover. Gleiche SVG für gleichen Slug, verschiedene Buchstaben für verschiedene Slugs — visuell konsistent, ohne dass wir 2.000 Cover als Blobs hochladen.
Ein angemessener Title-on-Color-Cover-Compositor (Buchtitel als Text auf einem aus dem Slug-Hash gewählten Hintergrund) ist eine Folge-Aufgabe; die DiceBear-Variante ist die saubere Sofort-Lösung, die ohne Daten-Migration funktioniert.
Sentry-Beobachtbarkeit für die Importer-Pipeline
Foto: Markus Spiske auf Unsplash
Eine eigene Sentry-Projektsammlung (outastory-rohtext-importer) bekam diese Woche ihren DSN gepushed, ihre Alert-Regeln für GitHub-Issue-Erstellung und MS Teams konfiguriert, und ihre Boot-Beacon-Verkabelung in den Function-Host eingebaut. Drei kleine Code-Schichten machen den Importer zur am-besten-beobachteten Pipeline im Stack:
- Per-Function-Scope-Tags —
rohtext.function=PollCatalogTimer / ProcessStoryQueueFunction / RetryAiFailuresTimer, gesetzt am Anfang jeder Invocation. Filtern im Sentry-Dashboard nach Pipeline-Schritt wird zur einzelnen Tag-Query. - Per-Message-Scope-Tags —
rohtext.bookId,rohtext.op,rohtext.textVersion, gesetzt nach erfolgreichem Deserialise der Service-Bus-Nachricht. Operator-Triage zu „welches Buch hat das ausgelöst" wird zur Filter-Query. - Breadcrumbs an AI-Call-Nahtstellen — der
StoryMaterializerlegt vor jedem der vier gpt-5-Calls einen Breadcrumb (rohtext.ai-call: Category | MinAge | Summary | AuthorBio), so dass das Sentry-Event-Timeline-Panel zeigt, welcher AI-Call gescheitert ist — nicht nur „irgendwas in der AI-Strecke".
Die Sentry-Projekt-Provisionierung selbst lief diese Woche über ein bestehendes Provision-SentryProjects.ps1-Tool, das in der Projekt-Matrix den neuen Importer-Slug bekam und beim nächsten Lauf das Projekt + die DSN + die Alert-Regeln idempotent setzt. Bonus: ein neues -SkipKeyVault-Switch im Set-SentryKeys.ps1-Tool für Umgebungen, in denen das Key Vault noch nicht deployed ist — die GH-Secrets landen alleine, der nächste azd provision-Lauf seedet den Vault.
Profil-Vervollständigung — ein Bug, der nie in Sentry auftauchte
Foto: Alvaro Reyes auf Unsplash
Mitten in der Woche kam ein Bug-Report von Eiko Wachholz, einem Test-Nutzer: er konnte sich anmelden, landete auf der Profil-Vervollständigungs-Seite (/account/complete), füllte sie aus, klickte „Speichern", und sah „Dein Profil konnte gerade nicht gespeichert werden. Bitte versuche es erneut.". Wieder und wieder. Sentry war absolut still — keine Spur, kein Event, keine Telemetrie.
Die Ursache war fast peinlich: ein nackter catch { }-Block in der Razor-Seite. Jede Exception wurde ohne Logging in das generische Banner umgewandelt; der OutaStoryApiClient.CompleteProfileAsync gab bei jedem non-2xx-Status null zurück, ohne irgendwo etwas zu protokollieren. Die gesamte Fehler-Strecke war für die Sentry-Dashboards unsichtbar.
Drei Schichten haben jetzt strukturierte Logging an jedem Fehler-Pfad:
- Razor-Seite (
Complete.razor.HandleSubmitAsync): jeder unterschiedliche Fehler-Shape (Exception,null-Antwort,IsComplete=false) bekommt eine eigene strukturierteLogWarning/LogError-Form mit E-Mail-Kontext und Feld-Längen. Sentry gruppiert die drei Shapes als getrennte Issues — drei verschiedene Ursachen. - API-Client (
OutaStoryApiClient.CompleteProfileAsync): bei non-2xx-Status werden Status-Code, Body-Auszug (bis zu 512 Zeichen) und E-Mail mitgeloggt; Netzwerk-Exceptions bekommen ihre eigene Log-Form. - API-Controller (
ProfileController.Complete): Sentry-Scope-Tags (profile.action=complete,profile.sub=auth0|…), ein explizitertry/catchum den EF-Save-Pfad, derDbUpdateException-Fälle mit der inneren Fehlermeldung + Eingabe-Längen loggt und dann rethrowt, plus eine catch-all-Klausel mit gleicher Verlagerungslogik (aberOperationCanceledExceptionausgenommen, weil Client-Disconnect-Noise kein Sentry-Event verdient).
Plus 14 neue Regressions-Tests, die jedes der drei Layers auf das gleiche Log-Format pinnen — wenn jemand in Zukunft den nackten catch wieder einbaut, fällt der Build rot raus statt stillschweigend einen Bug zu verstecken.
Eikos eigentlicher Bug ist noch nicht diagnostiziert (das Banner ist immer noch eine generische „bitte versuche es erneut" — wir wissen nicht, was genau fehlschlägt). Aber sein nächster Versuch nach dem Deploy wird einen Sentry-Event landen, der die Wurzel zeigt — und dann ist der Fix ein zweiter, kleinerer Pull-Request.
Auth-Härtung — die Folge-Welle zum „logged-in-but-401"-Bug
PR #621 von letzter Woche hatte einen proaktiven Access-Token-Refresh-und-Retry-Pfad in den WebBearerTokenHandler eingebaut, um den nervigen „eingeloggt aber API stille 401 nach langer Idle-Zeit"-Effekt zu beheben. Drei verschiedene Test-Sitzungen haben diese Woche bestätigt: der Refresh-Pfad funktioniert auf neuen Schaltkreisen, hat aber auf bestehenden Schaltkreisen (also: Sitzungen, die schon vor dem Deploy offen waren) immer noch gelegentlich 401s gezeigt.
Die Ursache lag in der Cache-Hydration-Logik. Der AccessTokenCache-Eintrag wird zum Sign-in-Zeitpunkt aus der HTTP-Request-Pipeline befüllt. Wenn ein bestehender Schaltkreis nach einem Pod-Restart die SignalR-Verbindung neu öffnet, gibt es keine HTTP-Request — der Schaltkreis öffnet sich direkt über das /_blazor/negotiate-Endpoint. In dieser Code-Bahn wurde der AccessTokenCache schlicht nicht (re-)befüllt; der Handler hatte nichts zu lesen und fiel sofort auf den 401-Pfad zurück, ohne überhaupt einen Refresh zu versuchen.
Die Lösung ist eine kleine Middleware, die sich speziell an /_blazor/negotiate hängt und den Cache aus dem Cookie rehydriert, bevor der Schaltkreis startet. Ein paar Regressions-Tests halten das Verhalten fest — das war auch der Auslöser für 5 bisher nie geschehene WEB-401-Bug-Cluster auf Sentry, die jetzt alle mit der aktuellen Deploy-Welle aus der Liste verschwunden sind.
Ergänzend: ein subtilerer ObjectDisposedException-Race im ReadingProgressService — das Service feuert mehrere Hintergrund-Drains an _drainLock.Release()-Stellen, und wenn der Blazor-Schaltkreis Dispose() während der Drain läuft, wirft die SemaphoreSlim.Release()-Aufruf eine ObjectDisposedException, die als UnobservedTaskException an den Finalizer-Thread bubble-t und in Sentry als unerklärlicher „TaskException"-Cluster landet. Behoben mit zwei try / catch (ObjectDisposedException)-Blöcken in den jeweiligen finally-Klauseln — die Lock-Freigabe ist sowieso ein No-op, wenn der Schaltkreis schon weg ist.
Kleinere Verfeinerungen, die das Bild diese Woche prägen
- Werbefrei auf jeder operationellen Oberfläche (PRs #646, #648, #651): die Ad-Layer ist jetzt auf
/coming-soon,/maintenance,/Error,/admin,/management,/moderationund/developerausgeschaltet — sie macht auf diesen Seiten weder visuell noch von der Performance her Sinn. Plus: Monetag (das Overlay-Werbenetzwerk) ist standardmäßig auf „aus" gestellt, weil es in den ersten Test-Wochen nicht zu der Erscheinung passt, die wir aufbauen. - MAUI-Sign-in-Hang behoben (PR #641): ein cross-plattform-Sign-in-Hang in der MAUI-Variante, der auf Windows/macOS/iOS unterschiedlich aussah, aber gemeinsame Wurzel hatte (ein synchron auf einen
Task.Resultwartender Pfad). Plus umfangreiches Sentry-Diagnostik-Tagging an der Auth-Strecke. - NotificationProcessor + SignalR REST-Berechtigung (PR #640): die
NotificationProcessor-Function konnte ihre In-App-Live-Pushes nicht über das geteilte SignalR-Service publizieren, weil sie nicht die richtige Service-Role hatte. Einaz role assignment create-Schritt in der Release-Pipeline klärt das. - Trusted Signing — endlich grün (PR #634): nach drei Wochen offenem Microsoft-Ticket war die Ursache eine falsche Role-GUID in einem unserer Bicep-Templates plus eine
dlib-Version-Regression im Trusted-Signing-Aktion-Pakete. Beide gefixt, der Windows-MSIX-Installer aus der CI signiert wieder korrekt. Plus einetolerate-403-Fallback-Spur (PR #628), die einen unsignierten Bundle veröffentlicht wenn das Signing wieder kippen sollte — keine perfekte Lösung, aber besser als ein blockierter Release. - Init.Data Sentry-Phasen-Observability (PR #632): jeder der ~15 Seed-Phasen (Categories, Authors, Stories, Subscriptions, Promotions, …) hat jetzt einen Sentry-Scope-Tag und einen Phase-Heartbeat, der bei ungewöhnlich langen Laufzeiten warnt. Praktisch nützlich für die rohtext-Seed-Welle: die Phase, die rohtext-Stories materialisiert, ist sichtbar als eigene Zeile im Init.Data-Dashboard.
- CI-Workflow-Aufräumen (PR #664): die vier MAUI-Build-Workflows, die wir seit dem Plattform-Pivot nicht mehr brauchen, sind gelöscht. Der zentrale
ci.yml-Workflow ist jetzt der einzige Build- und Test-Pfad — schneller, vorhersehbarer, ~$115/Monat an macOS-Runner-Minuten gespart. - NuGet- und Aspire-Updates (PRs #662, #663): die übliche Wochen-Routine. Aspire-Update bringt ein paar EF-Core-10.0.x-Hotfixes mit; NuGet-Updates sind alle minor-Versions ohne Breaking Changes.
Zahlen zum Stand
- Pull-Requests: 52 in den 6 Tagen seit dem letzten Blog-Post (Sa–Fr).
- Hauptthemen: rohtext-Import (15+ PRs durch die sieben Phasen plus Folgekorrekturen plus Sentry-Provisionierung plus Visibility-Flag-Tweaks), Auth-Härtung (5 PRs), Werbefrei-Auswahl (3 PRs), Sentry-Stille (3 PRs), Profil-Vervollständigung (1 PR mit 14 neuen Tests).
- Code-Stand: rund 3.420 grüne Unit-Tests (von ~3.380 vor einer Woche). Coverage-Gate weiterhin im Report-Mode mit 35 von 36 Projekten grün.
- Katalog-Stand: 2.256 Geschichten + 586 Autoren als Seed-YAMLs committed (im Repo), produktiver Importer in Bereitschaft für die restlichen ~6.900 Bücher des Katalogs.
- Migrationen: zwei neue Produktions-Migrationen (
AddRohtextImportColumnsfür die 5 neuen Story-Spalten + 2 neuen Author-Spalten,AddRohtextSyncStatefür die Singleton-State-Tabelle +AddRohtextAiFailurefür die Failure-Tabelle).
Was als nächstes
- Restliche rohtext-Bibliothek nachziehen. Der produktive Azure-Functions-Importer ist deployed, aber mit dem Master-Kill-Switch auf „aus" — wir wollen den Backfill operator-kontrolliert anstoßen, sobald die Seed-Curation komplett ist.
- Title-on-Color-Cover-Compositor. Ein kleiner SVG-Endpoint, der den Geschichts-Titel auf einer aus dem Slug gehashten Hintergrundfarbe rendert. Macht die DiceBear-Shapes ablösbar mit einem visuellen Stil, der zu OutaStory passt.
- Eiko's Bug-Wurzel ermitteln sobald der nächste Sentry-Event landet, und mit einem zweiten kleinen PR fixen.
- App.Shared-Razor-Pages-Tests — der letzte rote Coverage-Eintrag, weiterhin in mehreren Wochen-Wellen.
- Meta-App-Review für Instagram — die
instagram_content_publish-Berechtigung steht weiter offen.
Wenn du beim Stöbern auf eine rohtext-Geschichte mit einem auffälligen Cover-Fehler stößt — falscher Kategorie, unpassende MinAge-Einstufung, holprige Kurzbeschreibung — sag mir Bescheid. Die AI-Klassifikatoren sind gut, aber nicht perfekt, und ein paar konkrete Beispiele pro Wochen-Triage-Welle würden die Prompts deutlich schärfen.
