Schreibhelfer im Editor, Versionen mit eigener URL, und endlich auch der Browser im Blick
Foto: Unsplash
Nach der Observability-Welle in der Vorwoche fühlte sich diese Woche wie das Gegenteil an: keine neuen Server-Schichten, sondern eine ganze Reihe Reife-Striche an Stellen, die einer nach dem anderen aufgefallen sind. Ein eingebauter Schreibhelfer im Editor, ein sauberes Versions-URL-Schema für Geschichten, eine zweite Sichtbarkeitsschicht — diesmal direkt im Browser — und das, was passiert, wenn man fünf zufällige Inhalts-Seiten nebeneinander stellt und merkt, dass sie alle anders aussehen.
Spell- und Grammatikcheck direkt im Editor
Vor dieser Woche war der Schreib-Editor in OutaStory ein klassischer WYSIWYG: Tippen, formatieren, Kapitel speichern. Was er nicht konnte: Tippfehler erkennen. Browser-eigene Spellcheck-Hervorhebungen sind eine Hilfe, aber sie sind sprachverwirrt (sie folgen der OS-Sprache, nicht der Geschichts-Sprache), kennen keine Grammatikregeln und blenden Markup falsch ein.
Die saubere Antwort dafür war ein selbst-gehosteter LanguageTool-Container, der parallel zur API in Aspire mitläuft. Das Public-LanguageTool-Endpoint hätte das schneller geliefert, aber für den Lese- und Schreibverkehr eines Story-Editors will man weder ein externes Quota-Limit noch dass jeder Satz, den ein Autor noch nicht veröffentlicht hat, an einen Drittdienst geht. Container ist Container — Selbsthosting kostet 1 GB RAM und schafft Ruhe in der Datenfluss-Story.
Die Architektur dahinter:
LanguageTool-Container im Aspire-AppHost, an/v2/checkgemappt. Lokal läuft er beim erstendotnet runmit, in Azure als Sidecar-Container app neben der API.POST /api/text-checkauf der API nimmt einen HTML-Block, eine Sprache (en/de) und eine Liste manuell ignorierter Spans entgegen, sanitised das HTML, schickt den Plain-Text an LanguageTool und übersetzt die Antwort in stabile Span-Marker.- Inline-Marker im WYSIWYG: Tippfehler bekommen rote Wellenlinien, Grammatik-Hinweise blau, Stil-Empfehlungen grau. Klick auf einen Marker öffnet ein Popover mit Vorschlägen und einem „Ignore"-Button. Ignorieren wird pro Geschichte persistiert — ein Eigenname, den der Autor in Kapitel 1 freigegeben hat, taucht in Kapitel 7 nicht wieder als Tippfehler auf.
- Manuell + automatisch: Eine Toolbar-Toggle entscheidet, ob der Check beim Tippen läuft (debounced, 800 ms nach der letzten Eingabe) oder nur auf Knopfdruck. Schreibende mit einem starken Flow-Bedürfnis können den Hintergrund-Check abschalten und ihn am Ende des Schreibens manuell auslösen.
Schreiben sollte sich nicht wie Korrekturlesen anfühlen, aber ein höflicher Hinweis im Hintergrund hilft, wenn man es will. Die Toggle stellt sicher, dass beide Schreibstile bedient werden.
Story-Versionen mit eigener URL pro Version
Geschichten bleiben in OutaStory nicht statisch. Ein Autor merkt nach drei Wochen, dass ein Kapitel die Einleitung doch besser tragen sollte; ein Leser hat zur Hälfte der ursprünglichen Fassung gelesen und möchte da weiterlesen, auch nachdem die neue Fassung erschienen ist. Beides will gleichzeitig richtig funktionieren.
Das Schema dahinter ist jetzt vollständig durchgezogen:
/stories/{slug}zeigt immer die jeweils aktuell veröffentlichte Version einer Geschichte. Wer von einer Suchmaschine oder Social-Sharing-Vorschau hierherkommt, sieht das, was der Autor zuletzt freigegeben hat./stories/{slug}/v/{N}zeigt eine spezifische Version. Version 1 ist immer unter/v/1erreichbar, auch nachdem Version 2 erschienen ist; alte Bookmarks und Links bleiben gültig. Auf der Geschichts-Seite gibt es einen kleinen Versions-Schalter (1, 2, 3 …) für Leser, die explizit eine ältere Fassung sehen möchten — etwa weil sie dort gerade lasen.- Lesefortschritt folgt der Version-Kette: Wer Version 1 zur Hälfte gelesen hat und Version 2 öffnet, sieht ein „Du warst auf Seite 12 in Version 1 — weiterlesen?". Die Fortschrittsdaten werden über die Versions-Kette hinweg verbunden, so dass jede neue Version nicht mit „Seite 1 von 47" beginnen muss.
Technisch teilen sich alle Versionen einer Geschichte den gleichen Slug. Eine Versions-Tabelle wird über das RootStoryId-Feld auf der Story-Tabelle abgebildet: Version 1 hat RootStoryId = null (sie selbst ist die Wurzel), Version 2 und 3 zeigen auf die ID von Version 1. Eine Eindeutigkeit auf (Slug, Version) ist auf DB-Ebene erzwungen — zwei verschiedene Geschichten können denselben Slug nicht beanspruchen, aber alle Versionen einer Geschichte teilen sich den ihren.
Auf der Publish-Seite gibt es dafür eine konkrete Konsequenz: wenn du eine neue Version einer bereits veröffentlichten Geschichte schreibst, ist das Slug-Feld deaktiviert. Die URL erbt sich von Version 1, mit einem kleinen Hinweis erklärt, warum — und das /v/{N}-Schema macht den Unterschied zwischen den Versionen sichtbar. Eine Geschichte, die einmal cosmic-cabbage heißt, bleibt unter dieser URL auffindbar, egal wie oft der Autor sie überarbeitet.
Foto: Unsplash
Drei-Kanal-Benachrichtigungen: E-Mail, In-App, Push
Bisher konnte OutaStory zwei Wege, dich auf etwas hinzuweisen, das in deinem Account passiert: Toast-Notifications in der App selbst (live über den SignalR-Hub aus letzter Woche, plus 45-Sekunden-Poller als Fallback) und Web-Push-Notifications für signed-in Web-Leser, die ihre Browser-Push-Berechtigung erteilt haben. Dazu kam jetzt E-Mail als dritter Kanal — vor allem für Ereignisse, bei denen du gerade nicht in der App bist (jemand kommentiert eine Geschichte, die du verfolgst; ein Autor, dem du folgst, veröffentlicht etwas Neues).
Die /settings/notifications-Seite trägt jetzt eine 3×N-Matrix: für jeden Ereignistyp (Kommentare, Antworten, Mentions, Follow-Aktivität, Bewertungen, …) entscheidest du, über welche Kanäle du benachrichtigt werden willst. Jede Zeile hat drei Toggle: In-App, Push, E-Mail. Die Defaults sind bewusst zurückhaltend — nur die direktesten Ereignisse (jemand antwortet dir auf einen Kommentar, jemand mentioniert dich) sind in allen drei Kanälen aktiviert; alles andere ist standardmäßig nur In-App.
Die E-Mail-Seite läuft über SendGrid mit klar strukturierten Vorlagen pro Ereignistyp. Wer keine E-Mails möchte, schaltet die Spalte ab — keine versteckten Pflicht-E-Mails, abgesehen von den unvermeidlichen Account-Sicherheitsbenachrichtigungen, die durchgehen müssen.
Browser-Observability: was Leser sehen, sehen wir auch
In der vorletzten Woche hatten wir Sentry serverseitig in jedem der fünfzehn logischen Services eingebaut. Was uns dabei aufgefallen ist: ein Browser-Klick, der nicht das Erwartete tut, ist serverseitig oft ein gemeldetes Detail im Aspire-Log — aber nicht zwangsläufig ein Sentry-Event mit dem nötigen Kontext drumherum (welche Seite war der Leser zuletzt, welcher andere Klick kam davor, ist die Replay-Session für genau diesen Moment gespeichert).
Diese Woche kam dafür eine JavaScript-Brücke zwischen den Razor-Komponenten und dem Browser-Sentry-SDK dazu. Der IClientTelemetry-Service hat zwei Implementierungen:
- Auf Web ruft er über
IJSRuntimeinwindow.outaStorySentry.*hinein (Breadcrumbs, captureMessage, captureException, setUser, clearUser). Das Sentry-Browser-SDK, dasanalytics-consent.jsnach der Cookie-Yes-Zustimmung initialisiert, fängt diese Calls ab und sendet sie an dasoutastory-web-browser-Sentry-Projekt — separat von den server-seitigen Events, mit eigener Replay-Session und einer zeitlich vorlaufenden Klick-Sequenz. - Auf MAUI ist die gleiche Schnittstelle ein No-op. Sentry.Maui in der MAUI-App fängt UI-Interaktionen bereits nativ als Breadcrumbs ab; die JS-Brücke wäre dort doppelte Buchführung und braucht außerdem eine JavaScript-Runtime, die die BlazorWebView nicht garantiert.
Drei konkrete Stellen schicken jetzt strukturierte Browser-Events:
- Jeder Toast wird gespiegelt — ein „Favorit konnte nicht gesetzt werden"-Toast erzeugt einen Sentry-Breadcrumb mit Toast-Typ und -ID, so dass die nächste Fehler-Aufnahme den User-Trail davor sieht.
- Anonyme Lesefortschritte: Wenn das localStorage-Schreibversagen scheitert (Privatmodus, voller Speicher, Browser-Hotfix-Klemmen), erzeugt das einen Warning-Event. Anonyme Lesefortschritte haben kein Server-Backup — wenn das localStorage stumm bleibt, verliert der Leser stundenlanges Lesen ohne dass irgendjemand davon weiß. Das war bisher unsichtbar; jetzt ist es ein konkreter Eintrag.
- Navigation: Jeder Routenwechsel innerhalb einer Blazor-Server-Circuit ist ein Breadcrumb. „Story X angesehen → Profile geöffnet → /publish/42 → Klick auf Publish" ist die Sequenz, die in der nächsten Fehlermeldung auftaucht.
Die Brücke ist auf der Browser-Seite robust gegen den Fall, dass das Sentry-SDK noch nicht initialisiert ist (jeder Aufruf ist eine sichere Pass-Through-Methode mit eigenen Try-Catches). Sentry-quotenseitig zählt das in das outastory-web-browser-Projekt, das ein eigenes Limit hat — selbst eine Spitze an Toast-Events bringt das Server-Sentry nicht in Mitleidenschaft.
Boot-Beacons in jedem Service
Eine kleinere, aber konsequente Änderung: jede der vierzehn .NET-Hosts (Web-App, API, vier Init-Worker, sieben Azure-Functions, plus die MAUI-App) emittiert jetzt einen Sentry-Warning-Event beim Start, mit Anwendungs-Name, Version, Environment-Tag. Das Schema:
Application started: {ApplicationName} v{Version} in environment {Environment}
Sentry gruppiert identische Nachrichten zu einem Issue pro Projekt. Eine Container-App, die im Verlauf eines Tages sechs Mal hochgefahren wird (wegen Auto-Scaling, Deployments oder Restart-Loops), erzeugt sechs Events innerhalb desselben Issues. Operativ heißt das: ein Blick auf das Issue zeigt wann die Service-Instanzen hochgefahren sind, wieviele Replicas im Spiel waren und welche Environments sie kennen — ohne dass man in Container-Apps-Logs graben muss.
Dazu wurde durchgängig Sentry.MinimumEventLevel = Warning gesetzt. In der vorigen Woche war Sentry an, aber für die meisten unserer Logs auf Information- oder Warning-Level zu eingeschränkt — Sentry sah nur Errors. Jetzt fließen Warnings als eigenständige Events, mit denselben Tags wie der Boot-Beacon, und decken einen Teil der „leise degradierende"-Pfade ab, die früher unter dem Radar liefen.
Konsistente Optik auf jeder Info-Seite
Ein klassisches Polish-Thema mit überraschend großer Wirkung: fünf Inhalts-Seiten (/about/version, /developer/push-test, /developer/auth0-claims, /developer/api-claims, /developer/system-info) hatten jede ihr eigenes Layout, ihre eigene CSS-Datei und ihre eigene Idee davon, wie eine Info-Seite aussieht. /legal/terms und die anderen Legal-Seiten hatten ein hübsches Box-Layout — mit weißem Hintergrund, Rand, Schatten — aber dieses Layout war pro Seite kopiert, nicht geteilt.
Die Aufräumarbeit:
- Eine neue
.os-page-BEM-Klasse intheme.css(also global, nicht pro Razor-Komponente skopiert) bündelt das gemeinsame Shell — äußerer Container mit max-width 800px, Header mit Zurück-Button und Titel, optionaler Untertitel, eine.os-page__section-Box pro Inhaltsblock mit der gewünschten Schatten-und-Rand-Optik. - Die fünf migrierten Seiten verlieren ihre individuellen Shell-Definitionen; die seitenspezifischen Bestandteile (Formularstyling auf der Push-Test-Seite, Tabellenstile auf den Claims-Seiten) bleiben in ihrer scoped CSS, sind aber kürzer und weniger doppelt.
- Playwright-Tests pinnen die Konsistenz: ein neuer Test-Block läuft alle fünf Seiten ab und prüft, dass jede einen
.os-page-Wrapper, einen Header mit Back-Button und Titel, und mindestens eine.os-page__section-Box mit einer sichtbaren Überschrift enthält. Wenn eine spätere Refaktorierung versehentlich eine der Seiten aus dem Schema fallen lässt, schlägt der Test sofort an.
Die /legal/*-Seiten benutzen vorerst weiter ihre eigenen Per-Seite-CSS-Duplikate; die Migration auf .os-page ist dort vorbereitet, aber außerhalb dieser Wellenarbeit. Sieben weitere Dateien zu touchen, ohne neue Risiken zu eröffnen, ist eine eigene kleine Aufräumaktion für den nächsten ruhigen Tag.
Aspire-MCP: lokale Diagnose ohne Container-App-Logs
Eine Quality-of-Life-Erweiterung für die lokale Entwicklung: das Aspire MCP-Server-Plugin liefert jetzt eine direkte Schnittstelle zwischen dem AppHost und der Entwicklungs-Toolchain. Wenn ich lokal eine Geschichte teste und einer der vierzehn Service-Container irrt, kann ich den Console-Log und die Strukturlogs direkt aus dem Tooling abrufen, ohne im Aspire-Dashboard im Browser zu wechseln. Das beschleunigt das „warum kommt der Boot-Beacon nicht in Sentry an"-Debugging um Faktoren — ich sehe in einem Schritt, ob der Service überhaupt läuft, was die DSN-Konfiguration sagt und wo das Sentry-SDK-Init verlaufen ist.
Für externe Entwickler ist das nichts Sichtbares — aber für mich ist es der Unterschied zwischen „ich vermute, das funktioniert" und „ich sehe, das funktioniert". Mehr Sichtbarkeit auf jeder Ebene des Stacks war das Thema der Woche, und das Tooling ist Teil davon.
Zahlen zum Stand
- Unit-Tests: jetzt 2.490 grün (vor Wochenbeginn ca. 2.290; rund 200 neue Tests verteilt über LanguageTool-Integration, Versionierung, Slug-Vererbung, Page-Shell-Konsistenz, Browser-Telemetrie, Boot-Beacon-Verkabelung).
- Playwright-Suite: 7 neue Tests dazugekommen — fünf für die Page-Shell-Konsistenz, einer für die V2-Slug-Vererbung mit kompletter End-to-End-Setup-Kette, einer als Update bestehender Tests auf das neue
.os-page-Naming. - Commits in der Woche: rund 35 zwischen dem v0.5.2- und dem v0.5.3-Bugfix-Branch. Die meisten davon sind kleinere, punktgenaue Fixes oder Test-Coverage-Erweiterungen; die fünf Schwerpunkte oben sind die thematischen Cluster.
Was als nächstes
Das nächste Wellental ist Performance — die Plattform läuft, ist instrumentiert und sieht jetzt überall gleich aus. Was sie noch nicht ist: messbar schnell. Cold-Start auf der Web-App auf dem ersten Container-App-Replica liegt heute bei rund 4 Sekunden bis zum ersten interaktiven Frame; das ist akzeptabel für eine Closed-Alpha, aber nicht das Ziel. Ein paar konkrete Hebel sind schon identifiziert (DI-Container-Tuning, Critical-CSS-Inlining, Image-Lazy-Loading auf der Detail-Seite); was sich davon im Aspire-Dashboard wie auswirkt, kommt im nächsten Dev-Log.
Außerdem stehen die deutschen Marketing-Texte für die Store-Listings (Apple App Store + Google Play) auf der Liste, plus ein letztes Konsistenz-Audit der Mobile-spezifischen Layouts. Closed-Alpha-Tester bekommen davon nichts mit — aber bevor wir die Open-Beta einleiten, sollen die Store-Einträge in derselben Sprache, demselben Ton und mit denselben Screenshots auftreten.
Bis dahin: wenn dir auf der Web- oder Android-Alpha etwas auffällt, das sich seltsam anfühlt — schreib es mir. Die kleinen Sachen sind oft die, die das Gefühl der App prägen.
