Projekt-Kontext
AdsApp.store ist eine Pull-basierte Werbeplattform — Nutzer suchen Werbung aktiv auf, statt passiv beschossen zu werden. Das klassische Push-Advertising-Modell wird umgedreht: Händler listen Angebote in einem kuratierten Katalog, organisch gerankt durch einen Score-Algorithmus.
Capstone-Projekt für Fachinformatiker-Umschulung. Deployed und live als Portfolio-Nachweis für Praktikumsbewerbung 2026.
Product Hunt-Mechanik, spezialisiert auf Werbe-Angebote. Händler-Ads steigen/sinken nach echtem Nutzerverhalten — kein Pay-to-Win.
Buyer — Katalog browsen, bookmarken, zu Händler-Shop springen.
Merchant — Ads listen, Dashboard, Score-Tracking.
Agency — B2B, Pauschal-Provision (V2).
Admin — Plattform-Verwaltung.
AdsApp ist ein Aggregator. Käufe passieren beim Händler. Der deeplink_url per Ad leitet direkt zum externen Shop — kein Warenkorb, keine Order-Pipeline auf AdsApp.
4-Zonen-Architektur
| Zone | Bezeichnung | Mechanik | Status MVP |
|---|---|---|---|
| 01 | Hauptkatalog | Organisches Ranking via Score-Algorithmus | ✓ live |
| 02 | Hotspots | Kuratierte thematische Zonen (saisonal, event-basiert) | manuell MVP |
| 03 | Ufersteine (Premium Slots) | Zeit-basierte Buchung, sichtbar als Border-Highlighting | Datenmodell ✓ |
| 04 | Creator-Subkataloge | Creator listet eigene Ad-Auswahl unter /c/{slug} | V2 |
Zeitrahmen Chat 01 + 02
Tech-Stack & Infrastruktur
Backend
| Komponente | Version / Detail |
|---|---|
| Laravel | 11.x |
| PHP | 8.3 |
| MySQL | 8.0 |
| Laravel Breeze | blade-Stack (Auth-Scaffolding) |
| Meilisearch | self-hosted (Volltext-Suche, V2) |
| Sendcloud | Versand-Integration (V2) |
Frontend
| Komponente | Detail |
|---|---|
| Tailwind CSS | Custom Token-System |
| Alpine.js | Dropdown, Overlay, Toggle-States |
| Blade Components | 9 Components, hierarchisch |
| Vanilla JS | Overlay, Bookmark-Toggle, Ticker |
Infrastruktur
| Dienst | Detail |
|---|---|
| Server | Hetzner CAX21 (ARM) |
| Deployment | Laravel Forge Hobby |
| CDN / DNS | Cloudflare |
| Domain | adsapp.store |
| Lokal | IIS-Manager + phpMyAdmin |
Dev-Toolchain
| Tool | Rolle |
|---|---|
| PhpStorm | IDE + Laravel-Plugin |
| Claude Code | AI-Coding im Terminal |
| Figma / Stitch | UI-Design & Iteration |
| GitHub | Versionskontrolle |
php artisan serve als Fallback wenn IIS-Routing Probleme macht. Laravel-Route-Datei musste mit <rule name="Laravel Routes"> in der IIS web.config gesichert werden.CLAUDE.md & Projektkonventionen
Die CLAUDE.md im Projekt-Root ist die zentrale Instruktionsdatei für Claude Code. Sie definiert Konventionen, Do-not-touch-Bereiche und architektonische Entscheidungen persistent für alle folgenden AI-Coding-Sessions.
<x-buyer.topbar /> automatisch als components/buyer/topbar.blade.php — kein AppServiceProvider-Eintrag nötig. Punkt-Notation = Unterordner-Navigation.Datenbank-Schema — MVP-Tabellen
Alle 11 Kern-Tabellen wurden in einem Schwung migriert. Schema ist auf MVP-Features ausgelegt, mit Erweiterungs-Hooks für V2 (Agenturen, Hotspots, Creator).
| Tabelle | Zweck | Wichtige Spalten |
|---|---|---|
| users | Auth + Rollen | id, name, email, password, role (enum: buyer/merchant/agency/admin), notifications_seen_at |
| merchants | Händler-Profil (1:1 zu user) | user_id, company_name, slug, description, logo_url, verified_at |
| ads | Kern-Entität der Plattform | merchant_id, title, description, price_cents, deeplink_url, status (enum: draft/active/paused/archived), current_score, last_activity_at |
| ad_images | Bild-Links pro Ad | ad_id, remote_url (Original beim Händler), cache_path (Hotlink-Schutz, V2), position, alt_text |
| categories | Hierarchische Kategorien | id, parent_id, name, slug |
| tags + ad_tag | Frei-Tags, n:m zu Ads | tags.name, tags.slug · ad_tag: ad_id, tag_id |
| bookmarks | User-Merkliste, n:m | user_id, ad_id, created_at (kein updated_at — Pivot ohne Timestamps-Standard) |
| ad_events | Tracking-Log (append-only) | ad_id, user_id (nullable), type (enum: view/dwell/bounce/click/sale), value, ip_hash, created_at |
| orders | Verkaufs-Tracking | ad_id, user_id, merchant_id, status (pending/confirmed/refunded), amount_cents, commission_cents |
| commissions | Provisions-Buchungen | merchant_id, order_id, amount_cents, paid_at |
| premium_slots | Ufersteine-Buchungen | ad_id, merchant_id, starts_at, ends_at, slot_type, price_cents, status |
ad_images.remote_url speichert die Original-URL beim Händler-Shop — Bilder werden gelinkt, nicht hochgeladen. cache_path ist für optionalen lokalen Cache (Hotlink-Schutz V2) reserviert.Migrations — alle 11 Tabellen
Reihenfolge ist FK-bedingt: users zuerst, danach abhängige Tabellen. Alle mit php artisan migrate auf einmal ausgeführt nach Anlegen aller Files.
add_role_to_users_table — role enum-Spalte + notifications_seen_at timestamp nullable.create_categories_table — Selbst-Referenz via parent_id für hierarchische Kategorien.create_merchants_table — 1:1 zu users, user_id FK, verified_at nullable timestamp.create_ads_table — Kern-Tabelle. deeplink_url string (Ziel-URL beim Händler), current_score decimal(8,2) default 0, status enum.create_ad_images_table — remote_url + cache_path nullable, position unsignedTinyInteger für Reihenfolge.create_tags_table und create_ad_tag_table (n:m Pivot, unique auf ad_id+tag_id).create_bookmarks_table — Pivot user_id+ad_id, unique constraint, nur created_at (kein updated_at).create_ad_events_table — append-only Log. type enum, value float nullable (für Dwell-Sekunden), ip_hash für Rate-Limiting ohne PII-Speicherung.create_orders_table — Status-Lifecycle pending→confirmed→refunded, amount_cents integer (kein float für Geld!).create_commissions_table — FK zu orders + merchants, paid_at nullable timestamp.create_premium_slots_table — starts_at/ends_at für Zeitraum-Buchung, slot_type enum (horizontal/vertical/featured).add_column_to_* Migrations nach — nie bestehende Migrations editieren wenn die DB bereits migriert ist.Eloquent Models & Relations
10 Models in app/Models/. Hier die wichtigsten Relations-Patterns die im Projekt verwendet werden.
User.php
Ad.php
Merchant.php
AdEvent.php
Auto-Merchant bei Registrierung
Auth-Flow & Rollen-System
Basis: Laravel Breeze (Blade-Stack). Erweitert um Rollen-basiertes Routing nach Login und rollenspezifische Middleware-Guards.
Rollen-Routing nach Login
Middleware-Struktur
| Route-Gruppe | Middleware | Zugang für |
|---|---|---|
| /catalog/* | auth + role:buyer,merchant,admin | Eingeloggte User |
| /dashboard | auth + role:merchant,admin | Merchant, Admin |
| /merchant/* | auth + role:merchant | Merchant only |
| /admin/* | auth + role:admin | Admin only |
| /login, /register | guest | Nicht eingeloggt |
DashboardController initial als Inline-Closure in web.php. Sobald eine eigene Klasse angelegt wird, muss der use App\Http\Controllers\DashboardController; Import explizit ergänzt werden — Laravel löst die Klasse sonst nicht auf.Register-Formular — Rollen-Auswahl
Das Register-Formular hat ein role-Select-Feld (buyer/merchant). Bei Absenden wird der User mit der gewählten Rolle angelegt. Merchant-Registrierung triggert automatisch Merchant::create() im Controller.
Design-System & Tokens
Zwei getrennte Farbpaletten für die zwei App-Bereiche. Beide nutzen denselben Tailwind-Config-Block.
Coal-Palette (Buyer-Bereich)
Ink-Palette (Merchant-Bereich)
Brand-Farben (beide Bereiche)
| Token | Hex | Verwendung |
|---|---|---|
| brand.red | #E63946 | Primärer Accent, CTAs, Rang-Badges |
| brand.yellow | #F5B700 | Preise, Bookmark-Stern aktiv, Premium-Slot-Border |
| brand.copy | #C8C8C8 | Standard-Fließtext |
| brand.ticker | #454745 | Ticker-Lauftext (gedämpft) |
Typografie
| Font | Verwendung |
|---|---|
| JetBrains Mono | Labels, Tags, Scores, Ranks, Code — alles was technisch/numerisch ist |
| IBM Plex Sans | Fließtext, Nav-Items, Überschriften, Beschreibungen |
Blade-Komponenten-Architektur
9 Blade-Components, hierarchisch organisiert. Laravel erkennt Unterordner automatisch via Punkt-Notation in der Blade-Template-Syntax.
Component-Aufruf Syntax
buyer/topbar.blade.php (ohne components/-Präfix). Laravel findet Components nur unter resources/views/components/. Lösung: PowerShell Move-Item in den richtigen Unterordner.Topbar — Suchfeld-Zentrierung
Die Topbar verwendet CSS Grid (grid-template-columns: auto 1fr auto) damit das Suchfeld exakt in der Mitte der verfügbaren Breite liegt — unabhängig von Logo- und User-Menü-Breite. Flexbox würde hier nicht präzise genug zentrieren.
Catalog-View — Layout & UX
Layout-Struktur
request()->routeIs().Ad-Card Visuelles System
| Karten-Typ | Border | Score-Anzeige | Besonderheit |
|---|---|---|---|
| Organisch | 1px solid #2a2a2a | Rang-Zahl rot, Score-Zahl gelb | Rank im Kreis links |
| Premium Slot (Uferstein) | border-left: 3px solid #F5B700 | Rank ausgeblendet | SPONSORED-Badge |
| Hotspot-Banner | border-left: 2px solid rgba(220,38,38,0.7) | — | Trennzeile mit Hotspot-Name |
Ad-Overlay (Quick-View)
Beim Klick auf eine Ad-Card öffnet sich ein Modal-Overlay mit vollständigen Ad-Details, Bookmark-Button und "Zum Händler"-Deeplink. Das Overlay wird JavaScript-seitig generiert (nicht vorgerendert als verstecktes Blade-Template), um DOM-Kollisionen bei vielen Karten zu vermeiden.
catalog-wrap endete bei bottom: 50px aber die Sidebar-Höhe stimmte nicht. Fix: min-height: calc(100vh - 60px - 50px) für den Main-Content-Bereich.Bookmark-System
Vollständige Bookmark-Mechanik: Toggle per AJAX, visuelles Feedback (Stern gelb), Merkliste-View mit Empty-State.
BookmarkController
Frontend-Toggle (JS)
Merkliste-View (bookmarks.index)
Die Merkliste (/bookmarks) nutzt denselben Buyer-Layout-Wrapper und zeigt gebookmarkte Ads als dieselben Ad-Cards wie der Catalog. Empty-State zeigt einen Hinweis wenn keine Bookmarks vorhanden.
bookmarks-Tabelle hat kein updated_at. Die Relation muss explizit als ->withPivot('created_at')->orderByPivot('created_at', 'desc') definiert sein — sonst wirft Laravel einen Fehler wenn withTimestamps() verwendet wird.ads existieren. Greift erst vollständig wenn der Catalog echte DB-Ads lädt.Ad-CRUD (Merchant)
Händler können eigene Ads erstellen, bearbeiten, pausieren und löschen. SoftDelete ist aktiv — Ads werden nicht hart gelöscht.
Seeder — Reproduzierbarer Ausgangszustand
Ad-Lifecycle Status
| Status | Bedeutung | Sichtbar im Catalog |
|---|---|---|
| draft | Angelegt, noch nicht veröffentlicht | Nein |
| active | Live, nimmt am Ranking teil | Ja |
| paused | Temporär deaktiviert vom Händler | Nein |
| archived | SoftDeleted, nicht mehr editierbar | Nein |
Score-Algorithmus — ads:recalculate-scores
Der Score ist das Herzstück des Pull-Ranking-Systems. Ads steigen/sinken automatisch basierend auf echtem Nutzerverhalten — kein manuelles Boosten, kein Pay-to-Win im organischen Grid. Der Score wird per Artisan Command periodisch aus den ad_events neu berechnet und in ads.current_score geschrieben.
Score-Formel
| Event-Typ | Gewichtung | Quelle | Rationale |
|---|---|---|---|
| view | × 1 | ad_events | Grundinteresse — User hat die Ad gesehen |
| dwell | × 3 | ad_events | Starkes Interesse — länger geschaut oder Deeplink geklickt |
| sale | × 10 | ad_events | Conversion — höchster Signal-Wert |
| bookmark | × 5 | bookmarks-Tabelle | Explizite Speicherabsicht |
| bounce/refund | × 0 | ad_events | Nicht gewertet im MVP |
click-Enum im MVP: Die ad_events-Tabelle hat view, dwell, bounce, sale, refund — kein separates click. Deeplink-Klicks werden als dwell-Events geloggt (Gewicht 3). Eine saubere Enum-Erweiterung ist für V2 geplant.Artisan Command — Kernlogik
AdEventController — View/Click-Tracking
Events werden über eine dedizierte Route getrackt. Gastnutzer können Events erzeugen (kein Auth nötig), user_id ist nullable.
AdEventController war bei der Erstellung leer geblieben — track()-Methode fehlte. Ergebnis: 500er bei jedem Overlay-Öffnen, Score-Mechanismus bekam kein Futter. Lernprinzip: Generierte Stub-Dateien immer auf tatsächlichen Inhalt prüfen.Catalog auf echte DB-Ads umstellen
Der Catalog lief mit hardcodierten Dummy-Arrays. Umstellen auf echte DB-Daten aktiviert das gesamte Ökosystem: Bookmarks greifen durch, Score-Ranking wird sichtbar, Filter/Suche werden möglich.
CatalogController@index — vollständige Logik
window._bookmarkedIds = @json($bookmarkedIds) beim Page-Load ins JS exportieren. Nach AJAX-Toggle clientseitig aktualisieren. Ad-Card liest: bookmarked: window._bookmarkedIds.includes({{ $adId }}).Premium Slots — Trennung vom organischen Grid
Premium Slots sind buchungsbasiert, nicht score-basiert. Kritische Design-Entscheidung: Ads mit hohem Score dürfen den Premium-Strip nicht "kapern".
| Bereich | Quelle | Sortierung |
|---|---|---|
| Premium Strip (max. 3) | premium_slots JOIN ads — nur zeitlich gültige Buchungen | Buchungs-Reihenfolge |
| Organisches Grid | ads WHERE status = active | current_score DESC |
| Hotspot | organicAds->first() | = höchster Score |
Eine Ad kann gleichzeitig im Premium-Strip und im organischen Grid erscheinen. Premium-Strip verschwindet bei aktivem Filter (?q= oder ?category=).
Ad-Detail-Seite & Click-Tracking
Jede Ad-Card hat zwei Interaktionspunkte. AdsApp ist kein Shop — der Kauf passiert beim Händler über den Deeplink.
| Aktion | Target | Tracking |
|---|---|---|
| Klick auf Card / Lupe-Icon | Quick-View Overlay | view-Event via /events/track |
| Klick auf Pfeil-Icon | /ads/{id} Detail-Page | view-Event via AdController@show |
| „ZUM HÄNDLER →" | /ads/{id}/click → redirect()->away() | dwell-Event |
Catalog Dummy-Subpages für Nav-Active-States
Filter, Suche & Sortierung
Vollständige Filterlogik über GET-Parameter — normaler Page-Reload, kein AJAX. State bleibt beim Navigieren erhalten, URLs sind bookmarkbar.
| Parameter | Wert | Controller-Logik |
|---|---|---|
| ?q=nexus | Suchbegriff | LIKE auf title + description |
| ?category=gaming | Kategorie-Slug | whereHas('category', slug) |
| ?sort=score | Standard | orderByDesc('current_score') |
| ?sort=newest | Neueste zuerst | latest() |
| ?sort=price_asc/desc | Nach Preis | orderBy/Desc('price_cents') |
Topbar — CSS Grid für exakte Zentrierung
::after-Pseudo-Element braucht display: inline-block. Nach der Grid-Umstellung musste explizit class="... inline-block" auf das Logo-<a> gesetzt werden, sonst rendert content: '■' nicht mehr.Merchant Dashboard — Echte DB-KPIs
Das Dashboard war komplett mit hardcodierten Dummy-Zahlen gebaut. Umbau auf echte DB-Daten in einem Schwung: alle 4 KPI-Cards, 30-Tage-Chart, Top-5-Ads, Leads-Tabelle.
Notifications-System
Leichtgewichtiges System ohne Laravel's notifications-Tabelle — abgeleitet live aus ad_events. Leads (dwell-Events auf eigene Ads) gelten als Benachrichtigungen für den Merchant.
Migration
Bell-Klick → Settings mit ?tab=benachricht. Im Settings-Script öffnet const urlTab = new URLSearchParams(window.location.search).get('tab') den Tab automatisch. Beim Öffnen des Tabs: AJAX POST → notifications_seen_at = now() → Badge verschwindet sofort.
Settings Refactoring — Section Partials
Die settings/index.blade.php war mit allen Sektionen ein ~280-Zeilen-Monolith. Refactoring: jede Sektion in ein Partial ausgelagert.
@include teilt den Parent-Scope automatisch — alle Partials haben Zugriff auf $leads, $errors, Auth::user() ohne Props-Durchreichen. Section-IDs bleiben Deutsch (für showSection()-Match), Dateinamen sind Englisch (Projekt-Konvention).Ticker Live-Feed
Der Bottom-Ticker zeigt die neuesten 12 aktiven Ads — kein Dummy mehr. Query direkt im Component via @php. Klick auf Ticker-Item öffnet Quick-View Overlay. Score-Farbe: ≥50 = gelb (aufwärts), <50 = rot.
bottom: 50px — es gibt physisch nichts hinter dem Ticker. Entscheidung: Solider Anthrazit-Ton #141414 mit goldenem Top-Border.Bookmark UX Fixes
Fix 1 — Stern bleibt nach Toggle gelb
onmouseout überschrieb den State hardcoded. Fix: data-bm-Attribut als State-Träger:
Fix 2 — Overlay-Backdrop entfernt, gelber Box-Shadow
Fix 3 — Merkliste instant-remove via window.load
Das Merkliste-Script überschreibt window.toggleBookmark, aber der Overlay-Script (im Layout, nach dem Content) schreibt ihn zurück. Lösung: window.addEventListener('load', ...) — feuert nach allen Scripts.
Design-System Erweiterung — coal-Palette
Das bestehende System hatte nur die rot-getönte ink-Palette für den Merchant-Bereich. Der Buyer/Catalog-Bereich hat ein neutrales Anthrazit-Thema — als coal-Gruppe in tailwind.config.js formalisiert.
| Palette | Bereich | Basis-Ton |
|---|---|---|
| ink | Merchant Dashboard | #180A0A — warm rot-getönt |
| coal | Buyer / Catalog | #0a0a0a — neutral anthrazit |
| brand | Beide Bereiche | Rot #E63946 + Gelb #F5B700 |
Bugs & Fixes — Chat 03 Log
| Bug | Ursache | Fix |
|---|---|---|
| 500er auf /events/track | AdEventController war leer (Stub nie befüllt) | track()-Methode implementiert |
| Stern springt nach Toggle zurück auf grau | onmouseout hardcoded auf #2a2a2a | data-bm Attribut als State-Träger |
| Toast hinter Overlay-Backdrop | z-index zu niedrig, Backdrop nicht transparent | Toast z-index: 100000, Backdrop transparent |
| Merkliste instant-remove funktioniert nicht | Overlay-Script (im Layout) überschreibt toggleBookmark nach Merkliste-Script | window.addEventListener('load', ...) Wrapper |
| Sidebar-Toggle crasht auf Subpages | catalog-wrap noch nicht geparst beim Script-Load (Timing) | getElementById('catalog-wrap') erst im Click-Handler aufrufen |
| $dispatch is not defined | Alpine.js Magic-Property funktioniert nur in @click-Context, nicht in onclick | onclick="...; $dispatch" → @click="open = false; ..." |
| Logo-Blinker verschwindet nach Grid | ::after braucht display: inline-block | Explizit class="inline-block" auf Logo-a |
| npm run dev im PremiumSlot-Model | Terminal-Befehl in falsche Datei kopiert | Zeile löschen |
| Pagination zeigt alle Einträge | Controller nutzte ->get() statt ->paginate(6) | ->paginate(6) + echte Pagination-Komponente |
| Blaue Input-Focus-Border | Browser-Defaults nicht vollständig überschrieben | Globale CSS-Regel input:focus { border-color: #F5B700 !important } |
| OrderController::index() 500 | Methode nicht implementiert | Minimaler Stub, echte Implementierung Chat 04 |
Commit-Übersicht Chat 03
| Commit-Message | Was drin ist | Datum |
|---|---|---|
| feat: catalog on real DB ads, bookmark UX, dummy nav pages | CatalogController Echtdaten, AdFactory+Seeder, Components model-aware, Stern-Fix, Toast z-index, Backdrop transparent, Sidebar Nav-Active, Dummy-Pages | 27.05. |
| feat: score mechanism, ad events tracking, premium slots separated | RecalculateAdScores Command, AdEventController, Scheduler, Premium Strip getrennt | 27.05. |
| feat: ad detail page, click tracking, card buttons, overlay restructure | ads/show, ads/{id}/click, Lupe+Pfeil auf Card, ZUM HÄNDLER + VOLLANSICHT im Overlay | 28.05. |
| feat: filter, search, sort + dashboard real data + topbar grid | Filter/Suche/Sort, Premium Strip bei Filter aus, Filter-Bar, Topbar CSS Grid, Dashboard Echtdaten | 28.05. |
| feat: ticker live feed + optic fixes | Ticker neueste 12 Ads, Score-Farbe, Klick öffnet Overlay | 28.05. |
| refactor: settings sections in einzelne partials | 5 Partials, Notifications-Logik, Bell-Badge | 28.05. |
| feat: ads-liste pagination + echte stats, gelber input-focus | paginate(6), Stats-Cards aus DB, globaler Focus-Style | 28.05. |
| fix: bookmark instant-remove, AdEventController.track | window.load-Wrapper, 500er behoben | 28.05. |
| fix: dropdown coal-palette, $dispatch fix, orders stub | coal-Palette in Tailwind, Catalog-Dropdown, Alpine-Fix, OrderController Stub | 28.05. |
| fix: sidebar-toggle null-guard + lazy catalog-wrap | catalog-wrap erst im Click-Handler, Null-Guard | 28.05. |
Orders / Leads-Übersicht
Merchant-seitige Übersicht aller Leads (dwell-Events auf eigene Ads). orders/index.blade.php war ein 268-Byte-Placeholder — komplett neu gebaut. Layout matcht dem bestehenden Merchant-Dashboard-Stil (ink-Palette).
Leads werden über ad_events mit event_type = 'dwell' gezogen, JOINed mit ads (Merchant-Filter) und users (Buyer-Email). Stats: $totalLeads, $leadsToday, $leadsWeek, $convRate (Sales/Dwells × 100).
Stats-Grid (4 Kacheln), Tab-System LEADS/BESTELLUNGEN (JS switchTab()), Leads-Tabelle mit Pagination (15/Seite). Bestellungs-Tab ist Placeholder. Empty-State mit Icon.
Commit: feat: orders/leads view with stats + ad status toggle (active/paused) via PATCH
Auge-Button Status-Toggle
Inline Ad-Status-Wechsel (active ↔ paused) in der Meine-Ads-Liste ohne Page-Reload. Der Button existierte bereits visuell — Route und Controller-Methode fehlten.
PATCH /ads/{'{ad}'}/toggle-status → AdController@toggleStatus. Cycelt active → paused → active. Draft bleibt draft (nur über Edit aktivierbar). Ownership-Check via abort_if().
Vanilla JS fetch() mit CSRF-Token. Response-JSON enthält neues status, label, statusStyle. Badge-Text und -Farbe werden direkt im DOM aktualisiert. Icon-Swap als Farb-Feedback (800ms Timeout). .status-badge Klasse nötig.
Ads-Liste Filter (Status + Kategorie + Suche)
Die Filter-Buttons in ads/index.blade.php waren rein visuell ohne GET-Handling. Komplett umgebaut: Status-Filter als Links, Kategorie-Dropdown, Suchfeld — alle als GET-Parameter, Controller filtert entsprechend.
withQueryString() ist der kritische Zusatz — ohne ihn verlieren die Pagination-Links alle aktiven Filter. Einfach anzufügen, aber leicht zu vergessen.Commit: feat: ads list filter by status, category + search with query persistence
Premium Slots — Konzept & Datenmodell (Spec-Erweiterung)
Ausführliche Konzept-Diskussion und Entscheidung für das finale Premium-Slot-System. Ergebnis ist ein Hybrid-Modell mit zwei Zonen und unterschiedlicher Buchungslogik.
3 Slots prominent oben. Buchung: 1–7 Tage. Warteschlange FIFO — wer zuerst beantragt, kommt dran. Kein direktes Verlängern (mind. 1 freier Slot-Zyklus zwischen Buchungen desselben Händlers). Preis fix beim Antrag.
4 Slots vertikal rechts. Positionen 1–4 mit eigenem Preis/Queue (teurer oben). Option A gewählt: Preis + Position locked bei Buchung, keine nachträgliche Verdrängung. Einfacher zu bauen, fairer für Händler.
premium_slots: Slot-Definition (zone A/B, position, base_price_cents, max_days). slot_bookings: Buchungsanträge mit queue_position, bid_cents, status (pending/approved/live/rejected/expired), starts_at, ends_at.
Freier Slot → Dummy: "HIER KÖNNTE DEINE PREMIUM AD STEHEN" mit CTA. Nach X Tagen leer → Badge "25% OFF — JETZT BUCHEN" via Scheduled Job. Leerstand wird zum Akquise-Tool.
Catalog Ranking — Leaderboard-View
catalog/ranking.blade.php war 437-Byte-Placeholder. Neu: Leaderboard-Layout mit differenzierten Top-3 (prominent) vs. Rang 4+ (kompakt), echter Score-Aggregation, Right-Aside mit Market-Split-Donut.
✅ Echt: Score-sortiertes Ranking, Verkäufe/Merklisten/Interaktionen-Counts, Market-Split-Donut (Kategorie-Verteilung), Newcomers (neueste Ads).
🎭 Dummy: Score-Deltas (deterministisch aus ID), Sparklines, Trending-%-Werte.
Relation heißt bookmarkedBy() nicht bookmarks() — withCount('bookmarks') würde knallen. Gefunden beim Model-Check, korrigiert vor dem Bauen.
#1a0f0f → #141414, #5B403F → #2a2a2a, etc.Commits:
feat: catalog ranking leaderboard with real score aggregation + market split donut
style: catalog ranking to coal/anthracite theme, reserve red for merchant dashboard
Hotspots System
Hotspots sind kuratierte Event-/Angebots-Highlights im Catalog — getrennt von organischen Ads und Premium Slots. Eigenes Model, Migration, Seeder und Integration in Catalog-View.
Felder: title, subtitle, badge_text, badge_color, icon, ad_id (nullable FK), is_active, starts_at, ends_at, sort_order. Timestamps.
Hotspot-Banner im Catalog-Header ersetzt den alten Dummy-Top-Ad-Banner. Echter Featured-Hotspot (aktive, nach sort_order sortiert). catalog/hotspots.blade.php als eigene Übersichts-View.
Commits:
feat: hotspots model + migration + seeder + catalog hotspots view
feat: replace dummy top-ad banner with real featured hotspot
Analytics Views (Buyer + Merchant)
Zwei getrennte Analytics-Views: catalog/analytics.blade.php (Buyer-Perspektive) und merchant/analytics.blade.php (deep Dashboard mit Zeitreihen-Charts).
Echte KPIs aus DB (aktive Ads, Kategorien, Gesamtviews), Activity-Feed (neueste Events), Kategorie-Donut. Demo-Charts für Trends (Dummy-Werte, kein Zeitreihen-Backend). Route: catalog.analytics.
30-Tage-Timeline-Chart (views/dwells/sales), Market-Share-Donut, Top-Ads-Tabelle mit Score + CTR, Score-Verlauf. Echte Aggregation wo Daten vorhanden, Demo-Sparklines für Zeitreihen (kein historisches Event-Archiv im MVP).
Commits:
feat: buyer analytics view (real KPIs/feed/category-donut, demo charts)
feat: merchant deep analytics (real timeline charts, market position)
Premium Slot Booking Flow
Implementierung des in Sektion 30 spezifizierten Slot-Systems. Bestehende premium_slots-Tabelle war faktisch eine Buchungs-Tabelle — restructured zu sauberer 2-Tabellen-Architektur.
Slot-Definition (zone, position, base_price_cents, max_days). Trennt die Slot-Definition (dauerhaft, admin-verwaltet) von den Buchungen (transient).
Buchungsanträge: slot_id, merchant_id, ad_id, zone, queue_position, bid_cents, status (pending/approved/live/rejected/expired), starts_at, ends_at, reviewed_by.
Obere Hälfte: Slot-Grid mit Timer/Loading-Bar für aktive Buchungen, Dummy-Placeholder für freie Slots. Untere Hälfte: Antragsformular (Slot wählen, Ad wählen, Zeitraum 1–7 Tage), Queue-Position sichtbar nach Einreichung.
Max 7 Tage. Kein direktes Re-Booking (Cooldown: eigener letzter Slot muss abgelaufen sein + kein anderer Bewerber im Zeitraum). Status-Flow: pending → approved → live → expired.
Commit: feat: premium slot booking flow (slot overview, application form, queue position)
Admin Panel — Foundation
Eigenständiges Admin-Interface unter /admin-Prefix. Eigenes Layout, eigene Controller-Namespace, eigenes Theme (dunkelblau/cyan — klar getrennt von Merchant-Rot und Catalog-Anthrazit).
Alle role:admin-geschützt. Admin\AdminDashboardController, Admin\SlotApprovalController, Admin\MerchantApprovalController. Eigenständiges Unterverzeichnis — klar getrennt.
CSS-Vars: --admin-bg:#0a0f1a, --admin-panel:#111a2b, --admin-line:#1e3050. Akzentfarbe #4fc3f7 (cyan). Dritte Designsprache — Admin-Panel als "Kontroll-Ebene" visuell abgesetzt.
Pending Slots, Pending Merchants, Live Slots, Aktive Ads, User gesamt, Orders gesamt. Alert-Dots bei pending > 0 für sofortige Sichtbarkeit.
ÜBERSICHT, SLOT-ANTRÄGE, HÄNDLER-FREIGABE. Active-State via request()->routeIs(). Link zurück zum Merchant-Dashboard, Logout.
Commit: feat: admin panel foundation (layout, guard, dashboard with pending alerts)
Approval Flows (Slots + Merchants) & Routing Refactor
Kompletter Approval-Zyklus: Admin genehmigt/ablehnt Slot-Buchungen und Händler-Registrierungen. Plus: Notifications als eigene Route, Settings-Layout verschlankt, Merchant-Approval-Guard auf Ad-Erstellung.
Approve: setzt starts_at/ends_at, status = 'live', reviewed_by. Reject: status = 'rejected'. AJAX-fähig (JSON-Response). Slot wird in Catalog-Zones ausgespielt sobald live.
Neue Merchants haben approval_status = 'pending'. Ad-Erstellung geblockt bis approved. Catalog zeigt nur Ads von approved Merchants. Ads von pending Merchants gefiltert via whereHas('merchant', fn($q) => $q->where('approval_status', 'approved')).
Notifications aus Settings herausgelöst, eigene Route merchant.notifications. Settings-View schlanker, Notifications-View fokussierter. Bell-Badge-Logik unverändert.
Dediziertes schlankes Layout für Settings — role-neutral, ohne Merchant-Sidebar. Saubere Trennung von Merchant-Dashboard-Shell und Account-Settings.
Commits:
feat: admin slot approval (approve sets start/end + live, reject)
feat: admin merchant approval (approve/reject with reviewer tracking)
feat: enforce merchant approval (ad creation guard + catalog filter)
refactor: notifications as own merchant route/view, settings now slim
feat: dedicated slim settings layout (role-neutral, no merchant sidebar)
feat: schedule ad score recalculation every 5 minutes
Catalog Grid Refactor v1 — Responsive Auto-Fill
Mehrere Iterationen um das Catalog-Grid lückenlos und responsiv zu kriegen. Ausgangsproblem: Ranking-Offsets (+4, +9) aus alter Premium-Slot-Zählung, leere Zellen durch col-span/row-span-Konflikte.
Zwei getrennter Grids (3- und 4-spaltig) mit featured row-span-2 und Hotspot col-span-2. Fixe Spans + variable Fensterbreite = garantierte Löcher. Ranking-Offset war aus alter Logik (Premium = Rang 1–3).
repeat(auto-fill, minmax(240px, 1fr)): Ein Grid, gleichgroße Karten, fließend mehr Spalten bei breiterem Fenster. Garantiert lückenlos. Bento-Gewimmel mit score-getriebener Größe → v2-Feature.
Commits:
feat: split premium slots by zone in catalog (A=top-strip, B=right-aside)
fix: catalog ranking starts at #1 and runs gapless (premium zone has no rank)
fix: gapless catalog grid (featured row-span-2, hotspot-promo full-width)
refactor: unified responsive catalog grid (auto-fill, equal-size 16:9 cards)
Commit-Übersicht Chat 04
| Commit-Message | Was drin ist | Datum |
|---|---|---|
| feat: orders/leads view with stats + ad status toggle (active/paused) via PATCH | OrderController@index mit Leads/Stats/Conv-Rate, orders/index.blade.php Tab-Layout, AdController@toggleStatus Route PATCH, JS optimistic UI | 29.05. |
| feat: ads list filter by status, category + search with query persistence | Filter-Bar als GET-Links, Kategorie-Dropdown, Suchfeld, withQueryString() Pagination | 29.05. |
| feat: catalog ranking leaderboard with real score aggregation + market split donut | CatalogController@ranking, Top-3 prominent / Rank4+ kompakt, SVG-Donut, Newcomers | 29.05. |
| style: catalog ranking to coal/anthracite theme | Mapping Dunkelrot → coal-Tokens, #5B403F → #2a2a2a etc. | 29.05. |
| feat: hotspots model + migration + seeder + catalog view | Hotspot-Model, Migration, Seeder, catalog/hotspots.blade.php | 29.05. |
| feat: replace dummy top-ad banner with real featured hotspot | CatalogController@index integriert echten Hotspot | 29.05. |
| feat: buyer analytics view + merchant deep analytics | catalog/analytics (KPIs, Donut, Demo-Charts), merchant/analytics (Timeline, Market-Share) | 29.05. |
| feat: premium slot booking flow (slot overview, application form, queue) | restructure_premium_slots, create_slot_bookings, SlotController, Views Zone A/B | 29.05. |
| feat: admin panel foundation (layout, guard, dashboard) | layouts/admin.blade.php (cyan-Theme), AdminDashboardController, Routes mit role:admin | 29.05. |
| feat: admin slot approval + admin merchant approval | SlotApprovalController, MerchantApprovalController, approve/reject mit reviewer_id | 29.05. |
| feat: enforce merchant approval (ad guard + catalog filter) | Ad-Erstellung blocked bei pending, Catalog filtert approved Merchants | 29.05. |
| refactor: notifications as own route/view, slim settings layout | merchant.notifications eigen, Settings-Shell ohne Merchant-Sidebar | 29.05. |
| fix: add 'draft' to ads status enum | Migration ergänzt draft in ENUM, Code erwartete es bereits | 29.05. |
| feat: schedule ad score recalculation every 5 minutes | routes/console.php Schedule::command('ads:recalculate-scores')->everyFiveMinutes() | 29.05. |
| feat: split premium slots by zone in catalog (A=top-strip, B=right-aside) | Catalog-Index Zone A oben, Zone B in Right-Aside, beide aus slot_bookings aggregiert | 29.05. |
| fix: catalog ranking starts at #1 and runs gapless | $pos = $i + 1 statt +4, zweites Grid :rank="$i + 6" statt +9 | 29.05. |
| fix: gapless catalog grid (featured row-span-2, hotspot-promo full-width) | featured bekommt row-span-2, Hotspot col-span-full | 29.05. |
| refactor: unified responsive catalog grid (auto-fill, equal-size 16:9 cards) | repeat(auto-fill, minmax(240px, 1fr)), alle Karten col-span-1 16:9, Hotspot inline als Karte | 29.05. |
Interface-Fixes Batch (Chat 05 — 31.05.)
Chat 05 startete als Deployment-Tag mit dem Ziel, bis 18 Uhr live zu gehen. Vor dem Deploy wurden vier Interface-Fixes in einem Schwung erledigt.
Kategorie-Chip-Row bekam Links/Rechts-Pfeile (◀ ▶) mit scrollBy({left: ±200}). Chip-Row selbst auf overflow-x:auto; scrollbar-width:none damit kein nativer Scrollbalken erscheint. Hover: Pfeil wechselt auf #F5B700.
Gear/Info/Shield aus dem Sidebar-Bottom-Block entfernt. Diese drei Einträge landen stattdessen im Profil-Dropdown der Topbar (Buyer) und im Merchant-Navigation-Dropdown — übersichtlichere Sidebar, konsistenteres Dropdown-Menü.
Das bisherige grid-cols-3 ließ Col 2 (Search) Col 1 (Logo) verdrängen. Fix: explizite grid-template-columns: 280px 1fr 260px + align-items:center im inline-style. Logo erhielt white-space:nowrap + korrektes ::after-Quadrat mit vertical-align:baseline.
#catalog-main hatte overflow-x:hidden — das cancelte die min-width der Filter-Bar. Fix: auf overflow-x:auto gewechselt. Gleichzeitig catalog-wrap bekommt keine feste min-width, weil das Grid die Breite schon hält.
Commit: refactor: sidebar cleanup + topbar min-width fix + dropdown settings migration + filter-bar scroll arrows
Hotspot Detail-Page
Die /hotspots-Übersicht zeigte bereits laufende Hotspots, aber der "ENTER HOTSPOT"-Button führte ins Leere. Chat 05 hat den kompletten Stack von scratch gebaut: Migration, Model-Update, Controller, Routes, Blade.
Neue Tabellen / Pivot
| Tabelle | Was | Hinweis |
|---|---|---|
| hotspots | Existierte bereits mit anderem Schema | Model an echte Spalten angepasst: subtitle, criteria (JSON), erweiterte type-Enums |
| hotspot_ads | Pivot Hotspot ↔ Ad (n:m) | Existierte schon unter dem Namen hotspot_ads, nicht ad_hotspot |
HotspotController
Model-Scopes
Hotspot Detail-View
resources/views/catalog/hotspot-detail.blade.php — Layout identisch zum Hauptkatalog: Filter-Bar oben, Main (Hotspot-Hero + Ads-Grid) links, Right-Panel (Back-Button, Hotspot-Info, andere aktive Hotspots) rechts. Ads-Grid verwendet die bestehende x-catalog.ad-card-Komponente.
Right-Panel Hotspot-Liste verbessert
Routing
GET /catalog/hotspots → CatalogController@hotspots die denselben Namen catalog.hotspots trug. Route-Caching im Deployment brach mit "Another route has already been assigned this name". Fix: alte Route entfernt, die neue bekommt den kanonischen Namen.Commit: feat: hotspot detail page + controller + routes + catalog links + right panel sort by days_left (max 3)
Bild-Upload für Ads
Bisher gab es in Ad-Create/Edit nur ein URL-Input-Feld für Bilder. Chat 05 stellte auf echten File-Upload um. Drei Hindernisse auf dem Weg.
Upload-Flow
PHP 8.5.2 auf Windows hat einen Bug mit getRealPath() bei Upload-Files — storeAs() bekommt einen leeren Pfad. Workaround: move() schreibt direkt via PHP ohne FilesystemAdapter. Auf Linux/Hetzner tritt das nicht auf.
ad_images.remote_url war NOT NULL, wir übergeben aber null. Fix: Migration make_remote_url_nullable_in_ad_images — $table->string('remote_url')->nullable()->change().
php artisan storage:link erstellt den Symlink public/storage → storage/app/public. Auf Hetzner einmalig nach dem ersten Deploy ausführen. Forge bietet dafür eine "Shared Paths"-UI beim Site-Erstellen.
asset('storage/' . $adImage) überall wo Bilder angezeigt werden: ad-card, hotspot-detail, premium-slot, right-panel. Der Pfad in ad_images.cache_path ist relativ zum storage/app/public/-Root.
Commits:
fix: ad image upload via move() + nullable remote_url migration
Overlay via data-attributes — Click-Bug Fix
Nach dem Bild-Upload-Feature traten zwei eng verwandte Bugs auf: Klick auf eine Ad-Card öffnete das Overlay nicht mehr, und Bilder wurden im Overlay nicht angezeigt.
Ursache
Das Overlay wurde via inline-onclick mit String-Interpolation geöffnet: onclick="openAdOverlay({title: '{{ $adTitle }}', ...})". Sobald ein Titel oder eine Beschreibung Anführungszeichen oder Sonderzeichen enthielt, brach das JS-Objekt syntaktisch. Gleichzeitig konnte das <img>-Element den Click-Event schlucken.
Fix: data-attributes Pattern
openAdOverlay() im ad-overlay.blade.php Template-String musste das data.image-Feld auswerten und als <img src> rendern. Vorher: kein image-Feld übergeben → kein Bild sichtbar.<img> allein, sondern ein inneres Kind-Div den Event. pointer-events-none auf dem img hatte keinen Effekt. Lösung: gleicher data-attributes-Umbau wie bei ad-card.Commits:
fix: ad-card/bookmarks/ticker overlay via data-attributes + image display in overlay
fix: premium slot image path cache_path typo
fix: premium slots A+B overlay via data-attributes (pointer-events-none insufficient)
Help & Privacy Pages
Die bisherigen Dummy-Links Info, Hilfe, Support, Datenschutz, Security waren tote Ankerpunkte. Sie wurden konsolidiert: zwei echte Seiten statt fünf Leerstellen.
resources/views/help/index.blade.php — FAQ-Accordion (Alpine.js), Plattform-Info, Support-Ticket Coming-Soon-Badge, System-Status-Panel rechts. Erreichbar für alle authentifizierten Nutzer.
resources/views/help/privacy.blade.php — Datenschutz-Sektionen (DSGVO-Hinweise), Security-Checklist, Link zu Settings. Beide Seiten im coal-Palette Design-System.
Commit: feat: help/privacy pages + nav consolidation (info+support+hilfe merged, security removed from dropdown)
Nav-Konsolidierung
Parallel zu den neuen Pages wurden alle Navigations-Einträge bereinigt und auf sinnvolle Positionen verteilt.
| Eintrag | Alt | Neu |
|---|---|---|
| EINSTELLUNGEN | Sidebar-Bottom (Buyer) | Buyer-Topbar Dropdown + Merchant-Nav Dropdown |
| INFO | Buyer-Topbar Dropdown | → ersetzt durch HILFE & INFO (route: help) |
| SECURITY | Buyer-Topbar Dropdown + Sidebar | Entfernt — Datenschutz/Security in /privacy zusammengefasst |
| HILFE & INFO | nicht vorhanden | Buyer-Dropdown + Merchant-Dropdown (route: help) |
| DARSTELLUNG | Settings subNav | Entfernt — überflüssig, kein eigener Use-Case |
| DATENSCHUTZ | Settings subNav (tote Seite) | Settings subNav → route: privacy |
| ZUR APP | Settings-Topbar (immer Catalog) | Rollenbasiert: Merchant → Dashboard, Buyer → Catalog, Admin → Admin (via homeRoute()) |
Commit: fix: settings dropdown in merchant nav + zur-app role-aware routing
Deployment Hetzner / Forge
Deployment-Tag: 31.05.2026, Ziel 18 Uhr. Infrastruktur war teilweise vorbereitet (Domain, Hetzner-Server, Cloudflare). Der komplette Deploy-Flow lief von Site-Anlage bis MVP-live in einem Abend durch.
Forge-Setup Schritt für Schritt
| Schritt | Was | Hinweis |
|---|---|---|
| 1 | Neue Site in Forge anlegen | Project Type: Laravel, Web Directory: /public, GitHub-Repo verbinden, Branch: main |
| 2 | DB anlegen | DB-Name aus lokaler .env übernehmen; DB_HOST=127.0.0.1 (gleicher Server) |
| 3 | Shared Paths | "from storage to storage" — equivalent zu php artisan storage:link |
| 4 | .env befüllen | APP_ENV=production, APP_DEBUG=false, APP_URL=https://adsapp.store, MAIL_MAILER=log für MVP |
| 5 | Deploy-Script | Forge-Default + config:cache, route:cache, view:cache ergänzt; npm ci && npm run build drin lassen |
| 6 | SSL | Let's Encrypt via Forge — ein Klick; Cloudflare SSL-Mode: "Full" (nicht Strict → Error 525) |
| 7 | Auto-Deploy | Aktiviert — push auf main triggert automatisch Deploy |
| 8 | Scheduler | Forge → Server → Scheduler: /usr/bin/php /home/forge/adsapp.store/current/artisan schedule:run · Every Minute · User: forge |
Deploy-Script (finalisiert)
Post-Deploy Artisan-Commands (einmalig per SSH)
$FORGE_PHP-Variable funktioniert nicht im Cron-Kontext. Stattdessen absoluten Pfad verwenden: /usr/bin/php /home/forge/adsapp.store/current/artisan schedule:run.Post-Deploy Fixes
Nach dem Go-Live wurden beim Durchklicken noch vier kleinere Bugs identifiziert und direkt gepatcht.
color:#454745 für Hint-Texte und Sublabels war auf dem dunklen Hintergrund (#141414) nicht lesbar (Kontrast < 2:1). Fix: globales Find & Replace in allen Blades: #454745 → #999999. Tailwind-Token copy.ticker ebenfalls angepasst. WCAG AA (~4.5:1) als Ziel.
Das Schließen-Icon (x-icons.close) war zu klein und überlagerte die Score-Anzeige. Fix: Button auf 32×32px mit Box + Border, position:absolute; top:8px; right:8px. Header-Div bekommt padding-right:48px damit Score-Block nicht unter den Button rutscht.
Brave (Chromium) renderte den Text-Block im Overlay breiter als Firefox — Text sprengte die Box und schob die Buttons runter. Fix: align-items:start am Grid, min-width:0; overflow:hidden am rechten Block, max-height:120px; overflow-y:auto; word-break:break-word an der Beschreibung.
premium-slot.blade.php: $src->images->first()?->path — Spalte heißt cache_path, nicht path. Tippfehler aus dem initialen Build. Zone A zeigte deshalb kein Bild, Zone B funktionierte zufällig.
Commits:
fix: increase text contrast globally #454745 → #999999
fix: ad-overlay close button size + score overlap + brave overflow
fix: premium slot image path cache_path typo
Commit-Übersicht Chat 05
| Commit-Message | Was drin ist |
|---|---|
| refactor: sidebar cleanup + topbar min-width fix + dropdown settings migration + filter-bar scroll arrows | Sidebar Bottom-Block entfernt, Gear/Info in Dropdown, grid-template-columns fix, Filter-Bar Scroll-Pfeile |
| feat: hotspot detail page + controller + routes + catalog links + right panel sort by days_left (max 3) | HotspotController (index + show), hotspot-detail.blade.php, ENTER HOTSPOT CTA gefixt, Right-Panel sortiert |
| fix: ad image upload via move() + nullable remote_url migration | Windows Temp-Path Bug umgangen, NOT NULL Constraint Migration, storage:link erklärt |
| fix: ad-card/bookmarks/ticker overlay via data-attributes + image display in overlay | Alle clickbaren Cards auf data-ad-* umgebaut, openAdOverlayFromCard() global, image-Feld im Overlay |
| fix: settings dropdown in merchant nav + zur-app role-aware routing | Einstellungen in Merchant-Dropdown, homeRoute() rollenbasiert |
| feat: help/privacy pages + nav consolidation (info+support+hilfe merged, security removed from dropdown) | HelpController, help/index + help/privacy Blades, Nav-Cleanup |
| fix: remove duplicate catalog.hotspots route | Alte /catalog/hotspots Route entfernt, Route-Caching-Fehler beim Deploy behoben |
| fix: increase text contrast globally #454745 → #999999 | Globales Find & Replace, Tailwind copy.ticker Token angepasst |
| fix: ad-overlay close button size + score overlap + brave overflow | X-Button 32px, padding-right:48px Header, align-items:start Grid, word-break Beschreibung |
| fix: premium slot image path cache_path typo | →path war falsch, heißt →cache_path |
| fix: premium slots A+B overlay via data-attributes | pointer-events-none reichte nicht, gleicher data-attributes-Umbau wie ad-card |
v1.0.0 Tag) · Tailwind-Refactor der Inline-Styles (Claude Code Batch-Aufgabe, safelist oder Option-B Blade-Templates) · Ranking, Analytics, Hotspot-Detail ausbauen · Abrechnungs-View · Technische Dokumentation final abgeschlossen.