Werbe-Anbindung, ein vollwertiges Support-Center und drei neue Admin-Werkzeuge
Foto: Unsplash
Nach der Reife-Welle der Vorwoche kam diese Woche mit klarem Plan: die Plattform hatte ein paar Stellen, an denen die UI bereits stand, der Maschinenraum dahinter aber noch nicht. Drei Stellen genau genommen — die Werbe-Einblendungen, die das Revenue-Share-System speisen sollen, ein richtiges Support-Center anstelle der bisherigen Mini-Hilfeseite, und ein Admin-Bereich, der das tut, was ein Admin-Bereich tun sollte: Benutzer verwalten, Kategorien pflegen, Inhalte editieren. Plus die zweite Version des Revenue-Share-Dokuments, jetzt synchron mit dem, was tatsächlich gebaut ist.
Werbung: acht SDK-Adapter, eine gemeinsame Erfassungs-Pipeline
Die Revenue-Share-Logik selbst — die 55/45-Aufteilung, die Premium-Zuteilung nach Lesezeit und Abschlüssen, die Monatsabstimmung — ist seit der Phase-10-Welle Anfang Mai vollständig. Was bis dato fehlte, war die Brücke vom realen Werbenetzwerk zum Erfassungs-Endpunkt. Die Anzeige rendert; das Werbenetzwerk ruft seinen OnPaidEvent/onLoggingImpression/OnImpressionDataReady-Callback auf; nur landeten diese Callbacks bisher nicht in der Datenbank.
Diese Woche kamen acht SDK-Adapter dazu, je einer pro Werbeoberfläche, alle über dieselbe Schnittstelle:
- Google AdMob (Mobile) —
OnPaidEventliefert pro Einblendung einAdValuemitvalueMicros+currencyCode. Der Adapter mappt EUR-native und Nicht-EUR-Einblendungen sauber auf das Wire-Format; bei USD übernimmt derIEcbRateProviderdie FX-Umrechnung. - Google AdSense (Web) — AdSense bietet keinen Callback an. Stattdessen mutiert das
<ins class="adsbygoogle">-Element seindata-ad-status-Attribut auf"filled", sobald der Loader die Anzeige tatsächlich befüllt. Ein einzelner seitenweiterMutationObserverbeobachtet diese Transition und ruft via JSInterop ins .NET zurück. Pro Einblendung kommen kein Erlös und keine Währung mit (AdSense gibt das nur server-seitig über die Reporting API preis); auf der Plattform-Seite greift dann die Cohort-eCPM-Hochrechnung. - Meta Audience Network (Mobile) —
onLoggingImpressionohne Erlös-Daten. Gleicher Cohort-eCPM-Fallback wie AdSense. - Unity Ads (Mobile) —
OnImpressionDataReadymit eCPM in USD ×1000. Der Adapter normalisiert auf Mikros, der Server rechnet in EUR um. - VAST Audio (Web + Mobile) — Nicht der Audio-Wiedergabe-Beginn (das ist Server-zu-Werbenetzwerk-Telemetrie über die VAST-
<Impression>-URLs), sondern unsere eigene Plattform-Telemetrie, getrennt davon. Ausgelöst, wenn das<audio>-Element tatsächlichplayingmeldet — nicht früher. - Sample.Web / Sample.Mobile / Sample.Audio — drei Stub-Adapter für die lokale Entwicklung. Trägt das Netzwerk-Label
"Sample"und einen Marker{"sample":true}im Roh-Payload, damit ein Operator vor dem Live-Schalten die lokal erzeugten Zeilen sauber aussortieren kann.
Allen acht Adaptern liegt eine gemeinsame Schnittstelle zugrunde: IRevenueShareImpressionReporter mit einer einzigen ReportAsync(ImpressionPayload)-Methode. Jeder Adapter macht aus seinem netzwerkspezifischen Callback eine ImpressionPayload und reicht sie weiter; der Reporter kümmert sich um den HTTP-POST an /api/revenueshare/impressions. Drei Fehlersemantiken sind hart verdrahtet: ein zurückgewiesener HTTP-500, ein Transport-Fehler und ein Cancellation werden alle gleich verschluckt — die Anzeige hat bereits gerendert, der Verlust einer einzelnen Erfassungszeile ist über die Monats-Reconciliation aufholbar, und ein Werfen zurück in die SDK-Callback-Kette würde den Werbe-Pipeline auf der Konsumentenseite beschädigen.
Auf der Konfigurationsseite kam für jedes der vier echten Netzwerke ein dediziertes Geheimnis-Werkzeug dazu: tools/admob-keys/, tools/adsense-keys/, tools/meta-audience-keys/, tools/unity-ads-keys/, tools/vast-audio-keys/. Jedes Skript validiert das Format der erwarteten IDs (AdMob hat ein eigenes Schema, AdSense ein anderes, Meta wieder ein anderes), bricht bei Platzhalter-TBDs ab und schreibt in dieser Reihenfolge: AppHost-User-Secrets → Key Vault → GitHub Actions Secrets. Identisches Muster wie bei den anderen Vendor-Tools (Stripe, SendGrid, Sentry, BZSt) — wer das eine Skript kennt, kann jedes andere anwenden.
Die Werbung läuft damit nicht automatisch live. Was läuft, ist das Verkabeln: sobald ein Werbe-Vertrag mit einem der Netzwerke besteht, sind die IDs einzutragen, der Container neu zu starten, und die ersten Impressionen landen in RevenueShareImpressions. Die Cohort-eCPM-Tabelle, die FX-Umrechnung, die monatliche Abstimmung mit dem Netzwerk-Bericht und die 55/45-Aufteilung im Ledger sind seit Anfang Mai produktionsfertig.
Revenue-Share-Dokument: Version 2
Im selben Zug wurde das assets/ads/revenue-share.md-Dokument auf Version 2 gehoben — englisch und deutsch parallel, beide PDFs neu generiert. Inhaltlich keine Klausel-Änderung: die 55/45-Aufteilung, der 80/20-Premium-Hybrid, die 25-€-Schwelle, das 60-Tage-Rückforderungsfenster, der 5.-des-Monats-Auszahlungstermin und die 30-Tage-Vorlauffrist für wesentliche Änderungen sind alle unverändert.
Was sich geändert hat, ist der Status-Block plus eine Reihe Klarstellungen, die das Dokument an den tatsächlichen Code anschließen:
- Front-Matter-Status von „Entwurf zur Prüfung" auf „Implementierung abgeschlossen; ausstehend: rechtliche Freigabe + BZSt-Zertifikat + Stripe-Tax.Forms-Berechtigung".
- §1: Begleitdokumente-Liste erweitert um Implementierungsplan, Runbook, Deferred-Plan und SDK-Adapter-PRs.
- §13.3.4: DPI-Client als implementiert dokumentiert, mit Trockenlauf-Sperre bis zum Eingang des BZSt-Übermittler-Zertifikats.
- §13.5.1: Phase-10.6-Pipeline für US-1099-K-Formulare dokumentiert, gesperrt bis zur Stripe-Tax.Forms-Berechtigung.
- §14.0 + neue §14.9 + §14.10: neun unterstützende Entitäten anerkannt (
AdImpressionFlagged,LedgerAdjustmentProposal,PlatformBalanceEntry,PremiumPeriod,SubscriberPeriodTotals,AnnualTaxFiling,UsTaxFormRecord,AuthorPayoutAccount,AuthorDispute) mit der Begründung, warum sie außerhalb des ausschließlich anfügenden Ledgers stehen. - §16.3.4: Plausibilitätsgrenze für das
Sample-Entwicklungsnetzwerk ergänzt;RevenueShareCaptureOptions-Konfigurationsschlüssel verankert.
§22 (Versionshistorie) trägt die v2.0-Zeile mit voller Delta-Beschreibung. Die offenen rechtlichen Fragen aus §20 bleiben offen — das sind keine Implementierungs-Lücken, sondern Auslegungsfragen, die nur ein Anwalt schließen kann.
Support-Center: Hilfe-Artikel, FAQ und Tickets
Foto: Unsplash
Vor dieser Woche war die Hilfeseite in OutaStory ein einzelner Bildschirm unter /profile/help mit ein paar statischen „Kontakt via E-Mail / Telefon / Live-Chat / Community"-Schaltflächen. Telefon, Live-Chat und Community gibt es auf der Plattform nicht — und so etwas zu zeigen, suggeriert Support-Kanäle, die wir nicht anbieten. Diese Woche wurde das durch ein vollwertiges Support-Center ersetzt.
Das neue System hat drei Teile:
Hilfe-Artikel und FAQ
Unter /help läuft jetzt ein zweisprachiges (EN/DE) Hilfe-Center mit einer Such-Leiste, einer Kategorien-Übersicht und einer ausgewählten Artikel-Liste auf der Startseite. Zehn Hilfe-Kategorien decken die Hauptthemen ab: erste Schritte, Lesen & Zuhören, Schreiben & Veröffentlichen, Premium, Konto & Profil, Audio-Vertonung, Barrierefreiheit, Familie & Jugendschutz, Autoren-Vergütung, technische Probleme.
Innen drin: 11 Hilfe-Artikel, davon 4 als „featured" markiert für die Landing-Page (Welcome-Tour, Lese-Modus, erste Geschichte schreiben, Premium-Übersicht). Jeder Artikel ist Markdown-formatiert; das Rendering durchläuft eine Markdig-zu-HTML-Pipeline mit HtmlSanitizer als Sicherheitsbackstop und einem Post-Processor, der jeden Anker mit target="_blank" rel="noopener noreferrer" versieht — Links sollten den App-Kontext nicht ungewollt verlassen.
Unter /help/faq läuft ein Akkordeon-Layout, das die FAQ-Kategorien mit ihren Einträgen anzeigt. 33 FAQ-Einträge in 5 Kategorien (Konto & Abrechnung, Lesen & Inhalte, Schreiben & Veröffentlichen, Familie & Sicherheit, Technisches) — alles mit einer typischen Support-Schicht-Frage-Kadenz: „Wie melde ich mich an?", „Was kostet Premium?", „Kann ich offline lesen?", „Wer kann auf OutaStory veröffentlichen?", „Sind KI-generierte Geschichten erlaubt?".
Ein Markdown-zu-HTML-Pfad gilt für die FAQ-Antworten genau wie für die Artikel-Bodies. Die deutschen Übersetzungen liegen direkt auf dem Eintrag — eine Question plus eine QuestionDe, eine AnswerMarkdown plus eine AnswerMarkdownDe. Das vermeidet die übliche Fall-back-Logik in der Datenbank und macht die deutsche Variante so gleichberechtigt wie die englische.
Ticket-System
Unter /help/contact können Anwender:innen Support-Tickets eröffnen — anonym mit E-Mail-Adresse plus Name, oder eingeloggt direkt aus ihrem Konto. Sechs Themenkategorien (Konto & Profil, Premium & Abrechnung, Geschichten & Lesen, Technisches, Missbrauch / Sicherheit, Sonstiges); das Backend lehnt unbekannte Themen ab, damit die Moderator-Warteschlange sich nicht unbegrenzt aufbläht.
Jedes Ticket hat eine 12-stellige Referenz im Format OS-XXXX-YYYY, generiert beim Anlegen — diese Referenz erscheint in der Bestätigungs-E-Mail und ist über alle Kanäle der eindeutige Verweis. Ticket-Status durchlaufen vier Zustände: awaiting-staff, awaiting-user, resolved, closed. Antworten flippen den Status automatisch — Kunde antwortet → awaiting-staff, Moderator antwortet → awaiting-user. Geschlossene Tickets sind auf beiden Seiten schreibgeschützt.
Eingeloggte Anwender:innen sehen ihre Tickets unter /profile/tickets und können dort Antworten anhängen oder den Verlauf nachlesen. Die Ticket-Detail-Ansicht zeigt rechtsbündige Nutzer-Bubbles und linksbündige Staff-Bubbles, jeweils mit Markdown gerendertem Inhalt — visuell identisch zu einem Chat-Verlauf.
Moderator-Warteschlange
Unter /moderation/tickets (gegated mit <ModerationGate> auf Moderator-/Admin-/Developer-Rollen) läuft die Warteschlange für Moderatoren. Filterknöpfe sortieren nach Status — Standard-Ansicht ist „Bedarf an Personal" (awaiting-staff + awaiting-user); explizit umschaltbar auf gelöste / geschlossene / alle. Die awaiting-staff-Spalte sortiert älteste-zuerst, damit der älteste Stress-Punkt im Blick bleibt; alle anderen Buckets sortieren neueste-zuerst.
Auf der Detailseite kann ein Moderator den Status ändern (Auflösen, Schließen, Wieder-Öffnen), die Priorität setzen und interne Notizen schreiben — also Antworten, die nur das Team sieht. Das ist der typische „Ich hab das KYC geprüft, alles klar"-Übergabe-Mechanismus zwischen Moderatoren, ohne dass der Ticket-Eröffner operative Details mitbekommt.
Admin-Bearbeitung
Unter /admin/help-articles, /admin/help-categories, /admin/faqs, /admin/faq-categories (Admin-/Developer-gated) gibt es vier CRUD-Bildschirme für Hilfe- und FAQ-Inhalte. Der Artikel-Editor hat zwei Felder nebeneinander, eines für Englisch, eines für Deutsch — Admins schreiben beide Sprachen in einem Durchlauf, statt zwischen Tabs zu wechseln. Kategorien können gelöscht werden, sind aber blockiert, solange Artikel oder FAQ-Einträge in ihnen liegen — der Server gibt einen typisierten 409 mit klarer Anweisung zurück.
Admin-Bereich: Benutzer-Verwaltung
Mit der gewachsenen Tester-Liste haben sich die Admin-Aufgaben verändert: weniger „eine Geschichte zur Sicht freigeben" und mehr „diese Tester-E-Mail wechselt", „diese Person braucht den Moderator-Hut", „den Newsletter-Versand für diese E-Mail-Adresse abschalten". Das war bisher eine Mischung aus Auth0-Dashboard, Datenbank-Direktzugriff und „Thimo bitte" — diese Woche kam dafür ein eigener Admin-Bildschirm.
Unter /admin/users (Admin/Developer-gated) läuft die Benutzer-Verwaltung. Suchleiste oben, paginiert darunter, Status-Pills für „aktiv / blockiert / unverifiziert", Rollen als kleine Chips, letzter Login + Login-Anzahl pro Zeile. Ein Klick auf eine Zeile öffnet die Detailseite mit fünf Abschnitten:
- Profil: Auth0-Name + E-Mail bearbeiten, plus die Bio aus dem lokalen
Author-Datensatz, falls die Person sich schon einmal angemeldet hat. - Passwort: Eine Schaltfläche, die den standardmäßigen Auth0-Passwort-Reset-Mail-Flow auslöst. Der Admin sieht nie ein Passwort und setzt nie eines — die Mail mit dem Einmal-Link geht direkt an den Anwender.
- Rollen: Checkbox-Grid mit dem Live-Rollen-Katalog aus Auth0 (Developer, Admin, Moderator, Writer und was sonst im Tenant angelegt ist). Speichern setzt die Auswahl als komplette Rollen-Liste; das Backend diff't gegen den aktuellen Stand und ruft minimal
Assign+Deleteauf. - Newsletter: Toggles für tägliche und wöchentliche Versendung plus die Locale. Schreibt direkt in die
NewsletterSubscription-Tabelle. - Sperren / Entsperren: Eine separate, rot umrandete Karte mit einem Pflicht-Begründungsfeld. Sperren setzt das Auth0-native
blocked-Flag (verhindert die Anmeldung sauber und überlebt Token-Refreshes), spiegelt aufAuthor.IsProfileHiddendamit der bestehende Moderations-Flow konsistent bleibt, und schreibt einenAuthorBlockEvent-Datensatz mit Aktion + Admin-Akteur (Auth0-Sub + Anzeigename als Snapshot zum Schreibzeitpunkt) + getippte Begründung. Die History — alle Sperr- und Entsperr-Ereignisse mit Zeitstempel, Akteur und Begründung — ist direkt unter dem Formular sichtbar.
Eine bewusste Architektur-Entscheidung: das Auth0-blocked-Flag ist die kanonische Wahrheit; die AuthorBlockEvent-Tabelle ist OutaStorys eigener Audit-Pfad, weil Auth0s Aktivitäts-Log weder die Begründung noch den menschlichen Akteur trägt. Bei einem Konflikt zwischen den beiden gewinnt Auth0 — der Server prüft beim nächsten Webhook-Sync.
Admin-Bereich: Geschichts-Kategorien
Die Plattform hat aktuell 302 Kategorien in vier Ebenen — von der obersten Ebene (z. B. „Kinder", „Romanze", „Fan Fiction") bis zu Blatt-Knoten wie „Kinder → 6–7 Jahre — Erstleser → Tier-Geschichten". Bisher war die Kategorien-Pflege eine Bearbeitung des YAML-Seeds plus ein Container-Restart. Mit echten Tester-Anfragen („eine Unterkategorie für historische Romanze fehlt") wurde das untauglich.
Foto: Unsplash
Unter /admin/categories (Admin/Developer-gated) läuft jetzt eine zweispaltige Bearbeitungs-Oberfläche:
- Linke Seite: ein eingerückter Baum mit allen Kategorien, sortiert nach Display-Order und Name. Pro Knoten wird der Name, ein Erlös-Chip mit der Anzahl der Geschichten in dieser Kategorie und ein „+"-Knopf angezeigt. Klick auf einen Knoten lädt ihn in den Editor; Klick auf das „+" öffnet den Editor für eine neue Sub-Kategorie unter diesem Knoten.
- Rechte Seite: ein Editor mit Slug, EN/DE-Name, EN/DE-Beschreibung, Eltern-Kategorie (Dropdown mit Breadcrumb-Pfaden), Display-Order und einem optionalen Mindestalter — das letzte Feld ist die Hebelstelle für den Jugendschutz: wenn eine Kategorie ein gepinntes Mindestalter trägt, müssen alle Geschichten in ihr dieses Alter erfüllen.
Löschen ist nur dann erlaubt, wenn die Kategorie leer ist — keine Geschichten direkt darin, keine Sub-Kategorien darunter. Das wird auf der Datenbank-Ebene durch die Restrict-Semantik beider Fremdschlüssel ohnehin erzwungen, aber der Server macht eine Vorprüfung und gibt einen typisierten 409 mit klarer Begründung zurück, statt einen generischen DbUpdateException durchschlagen zu lassen. Auf der UI-Seite wird der Löschen-Knopf entsprechend deaktiviert, mit einem Tooltip, der den genauen Grund erklärt: „Diese Kategorie enthält Geschichten" oder „Diese Kategorie hat Sub-Kategorien".
Eine zweite Schutzschicht: das Re-Parenting unter eine eigene Nachfahren-Kategorie wird sowohl im Client (Dropdown blendet die Optionen aus) als auch im Server (Vorfahren-Walk) blockiert — sonst könnte ein Admin versehentlich eine Schleife im Baum schließen. Mit nur 4 Ebenen ist die Walk-Tiefe trivial; die Logik existiert trotzdem, weil sie bei einer späteren Erweiterung des Tree-Limits sonst leise verloren gehen würde.
Lokalisierung quer durch den Stapel
Eine kleinere Welle, die sich durch alle drei neuen Bereiche zieht: jede neue Sidebar-Bezeichnung, jede Admin-Card-Beschriftung und jede Profil-Menü-Position lebt in den drei LayoutResource / DeveloperResource / ProfileResource-Resx-Varianten (neutral, en-US, de-DE). Das war anfangs ein Versäumnis — die ersten Versionen der Karten standen mit hartkodiertem Englisch da — und wurde direkt nachgezogen. Auf der Sidebar erscheint jetzt ein „Hilfe / Hilfe-Center"-Eintrag in der Sprache der Anwender:in; die /admin- und /moderation-Übersichten zeigen ihre neuen Karten in der richtigen Sprache.
Die Inhalte der Hilfe-Artikel und FAQ-Einträge selbst sind ebenfalls bilingual (EN + DE) — das ist Datenmodell-Lokalisierung, nicht Resx-Lokalisierung. Wir halten beide Sprachen direkt auf dem Eintrag, mit einem Title und einem TitleDe-Feld, einem BodyMarkdown und einem BodyMarkdownDe. Die deutsche Variante ist eine vollwertige Schwester der englischen, kein Auto-Übersetzungs-Fallback.
Zahlen zum Stand
- Migration-Stand: zwei neue produktions-Migrationen diese Woche (
SupportCentermit den sechs Support-Entitäten undAdminUserBlockEventsmit derAuthorBlockEvent-Tabelle); beide auf jedem Aspire-Boot automatisch angewendet. - Seed-Inhalte: 10 Hilfe-Kategorien, 11 Hilfe-Artikel, 5 FAQ-Kategorien, 33 FAQ-Einträge — alle mit deutscher Übersetzung. Beim Boot auf jeder Umgebung neu eingespielt; vorhandene Inhalte werden über den Slug aktualisiert, nicht dupliziert.
- Neue Pull-Requests: PR #255–#262 für die acht SDK-Adapter (gestapelt — Adapter 2–8 hingen am Foundation-PR), PR #263 für die v2-Aktualisierung des Revenue-Share-Dokuments mit beiden PDFs, PR #269 für das Support-Center, PR #270 für die Benutzer-Verwaltung, PR #271 für die Geschichts-Kategorien-Verwaltung. Insgesamt rund 75 Commits.
- Unit-Tests: jetzt rund 2.580 grün (zu Wochenbeginn 2.490; rund 90 neue Tests, davon 42 für die Werbe-Adapter — jeder Adapter hat einen eigenen Forwarding-Test-Block).
Was als nächstes
Die Tooling-Welle ist damit abgeschlossen, und der nächste Schwerpunkt verlagert sich wieder Richtung Anwender-sichtbar. Für nächste Woche stehen:
- Anonyme Ticket-Nachverfolgung per E-Mail-Magic-Link: das Datenmodell hat bereits den
(GuestEmail, Reference)-Index, was fehlt ist das Versenden des Bestätigungslinks und der Ticket-Detail-Endpoint, der den Token validiert. - E-Mail-Benachrichtigungen für Ticket-Ereignisse — Eröffnung, neue Antwort, Lösung. Die
IEmailService-Anbindung steht; was fehlt sind die SendGrid-Templates pro Ereignistyp. - Auto-Schließung inaktiver gelöster Tickets nach 30 Tagen — die Repository-Methode existiert, was fehlt ist der
BackgroundService, der sie täglich aufruft. - Lokalisierung der Detail-Seiten-Texte (Brotkrumen, Knopf-Beschriftungen, Section-Überschriften) für die neuen Hilfe- und Admin-Bereiche. Die Sidebar- und Karten-Beschriftungen sind seit dieser Woche lokalisiert; die Seiten-internen Strings folgen.
Bis dahin: wenn dir auf einem der neuen Bildschirme etwas seltsam vorkommt, schreib's. Die kleinen Sachen sind oft die, die das Gefühl der App prägen.
