01

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.

Primäres Ziel

Capstone-Projekt für Fachinformatiker-Umschulung. Deployed und live als Portfolio-Nachweis für Praktikumsbewerbung 2026.

Konzept

Product Hunt-Mechanik, spezialisiert auf Werbe-Angebote. Händler-Ads steigen/sinken nach echtem Nutzerverhalten — kein Pay-to-Win.

Nutzergruppen

Buyer — Katalog browsen, bookmarken, zu Händler-Shop springen.
Merchant — Ads listen, Dashboard, Score-Tracking.
Agency — B2B, Pauschal-Provision (V2).
Admin — Plattform-Verwaltung.

Kein Checkout

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

ZoneBezeichnungMechanikStatus MVP
01HauptkatalogOrganisches Ranking via Score-Algorithmus✓ live
02HotspotsKuratierte thematische Zonen (saisonal, event-basiert)manuell MVP
03Ufersteine (Premium Slots)Zeit-basierte Buchung, sichtbar als Border-HighlightingDatenmodell ✓
04Creator-SubkatalogeCreator listet eigene Ad-Auswahl unter /c/{slug}V2

Zeitrahmen Chat 01 + 02

18.05.2026 (Chat 01): Projekt-Init — Laravel lokal, GitHub Repo, CLAUDE.md, alle 11 Migrations, alle 10 Models, Breeze Auth, Guest/App-Layout, Design-System-Bootstrap, Landing Page-Grundgerüst.
20.05.2026 (Chat 02): Auth-Flow vollendet, Rollen-Routing, Catalog-Layout gebaut, 9 Blade-Components zerlegt, Bookmark-System (Controller, Toggle, Merkliste), Ad-CRUD für Merchant, Kategorie/Merchant-Seeder.
02

Tech-Stack & Infrastruktur

Backend

KomponenteVersion / Detail
Laravel11.x
PHP8.3
MySQL8.0
Laravel Breezeblade-Stack (Auth-Scaffolding)
Meilisearchself-hosted (Volltext-Suche, V2)
SendcloudVersand-Integration (V2)

Frontend

KomponenteDetail
Tailwind CSSCustom Token-System
Alpine.jsDropdown, Overlay, Toggle-States
Blade Components9 Components, hierarchisch
Vanilla JSOverlay, Bookmark-Toggle, Ticker

Infrastruktur

DienstDetail
ServerHetzner CAX21 (ARM)
DeploymentLaravel Forge Hobby
CDN / DNSCloudflare
Domainadsapp.store
LokalIIS-Manager + phpMyAdmin

Dev-Toolchain

ToolRolle
PhpStormIDE + Laravel-Plugin
Claude CodeAI-Coding im Terminal
Figma / StitchUI-Design & Iteration
GitHubVersionskontrolle
Lokale Besonderheit: Entwicklung läuft unter Windows/IIS — 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.
03

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.

CLAUDE.md — Kerninhalte ## Concept Pull-based ad catalog. Ads rank organically by user behavior score. NOT a shop — no checkout, no cart. deeplink_url leads to merchant's shop. ## Rollen buyer | merchant | agency | admin ## Konventionen - English variable/file names throughout - Score wird periodisch aggregiert in ads.current_score, nicht live berechnet - Blade Components: x-buyer.topbar → components/buyer/topbar.blade.php ## Do NOT - Kein Checkout/Cart-Flow - Keine Live-Score-Berechnung bei jedem Request - Keine Alpine.js-Komponenten ohne @click.away für Dropdown-Close
Kritische Konvention: Laravel erkennt <x-buyer.topbar /> automatisch als components/buyer/topbar.blade.php — kein AppServiceProvider-Eintrag nötig. Punkt-Notation = Unterordner-Navigation.
04

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).

TabelleZweckWichtige 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
Bilder-Strategie: 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.
05

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.

Migration 01 — users erweitern: add_role_to_users_tablerole enum-Spalte + notifications_seen_at timestamp nullable.
Migration 02 — categories: create_categories_table — Selbst-Referenz via parent_id für hierarchische Kategorien.
Migration 03 — merchants: create_merchants_table — 1:1 zu users, user_id FK, verified_at nullable timestamp.
Migration 04 — ads: create_ads_table — Kern-Tabelle. deeplink_url string (Ziel-URL beim Händler), current_score decimal(8,2) default 0, status enum.
Migration 05 — ad_images: create_ad_images_tableremote_url + cache_path nullable, position unsignedTinyInteger für Reihenfolge.
Migration 06 — tags + ad_tag: Zwei Migrations — create_tags_table und create_ad_tag_table (n:m Pivot, unique auf ad_id+tag_id).
Migration 07 — bookmarks: create_bookmarks_table — Pivot user_id+ad_id, unique constraint, nur created_at (kein updated_at).
Migration 08 — ad_events: 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.
Migration 09 — orders: create_orders_table — Status-Lifecycle pending→confirmed→refunded, amount_cents integer (kein float für Geld!).
Migration 10 — commissions: create_commissions_table — FK zu orders + merchants, paid_at nullable timestamp.
Migration 11 — premium_slots: create_premium_slots_tablestarts_at/ends_at für Zeitraum-Buchung, slot_type enum (horizontal/vertical/featured).
Migrations sind iterativ: Neue Spalten kommen als separate add_column_to_* Migrations nach — nie bestehende Migrations editieren wenn die DB bereits migriert ist.
06

Eloquent Models & Relations

10 Models in app/Models/. Hier die wichtigsten Relations-Patterns die im Projekt verwendet werden.

User.php

// User hat einen optionalen Merchant-Account public function merchant() { return $this->hasOne(Merchant::class); } // Bookmarks als BelongsToMany (Pivot) public function bookmarks() { return $this->belongsToMany(Ad::class, 'bookmarks') ->withPivot('created_at') ->orderByPivot('created_at', 'desc'); }

Ad.php

// Fillable-Felder explizit (Mass-Assignment-Schutz) protected $fillable = [ 'merchant_id', 'title', 'description', 'price_cents', 'deeplink_url', 'status', 'current_score', 'last_activity_at' ]; // Cast für Score-Decimal protected $casts = [ 'current_score' => 'decimal:2', 'last_activity_at' => 'datetime', ]; // Relations public function events() { return $this->hasMany(AdEvent::class); } public function bookmarkedBy() { return $this->belongsToMany(User::class, 'bookmarks'); }

Merchant.php

// Merchant hat viele Ads + PremiumSlots public function ads() { return $this->hasMany(Ad::class); } public function premiumSlots() { return $this->hasMany(PremiumSlot::class); }

AdEvent.php

// Append-only — kein Update nach Insert protected $fillable = [ 'ad_id', 'user_id', 'type', 'value', 'ip_hash' ]; // type: view|dwell|bounce|click|sale

Auto-Merchant bei Registrierung

// RegisteredUserController — nach User-Erstellung if ($user->role === 'merchant') { Merchant::create([ 'user_id' => $user->id, 'company_name' => $request->name, 'slug' => Str::slug($request->name), ]); }
07

Auth-Flow & Rollen-System

Basis: Laravel Breeze (Blade-Stack). Erweitert um Rollen-basiertes Routing nach Login und rollenspezifische Middleware-Guards.

Rollen-Routing nach Login

PHP — AuthenticatedSessionController — authenticated() // Redirect nach Login je nach Rolle return match($user->role) { 'buyer' => redirect()->route('catalog.index'), 'merchant' => redirect()->route('dashboard'), 'agency' => redirect()->route('dashboard'), // V2: eigene Route 'admin' => redirect()->route('admin.dashboard'), default => redirect()->route('dashboard'), };

Middleware-Struktur

Route-GruppeMiddlewareZugang für
/catalog/*auth + role:buyer,merchant,adminEingeloggte User
/dashboardauth + role:merchant,adminMerchant, Admin
/merchant/*auth + role:merchantMerchant only
/admin/*auth + role:adminAdmin only
/login, /registerguestNicht eingeloggt
Bekanntes Edge Case: Breeze generiert 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.

08

Design-System & Tokens

Zwei getrennte Farbpaletten für die zwei App-Bereiche. Beide nutzen denselben Tailwind-Config-Block.

Coal-Palette (Buyer-Bereich)

// tailwind.config.js — coal coal: { deep: '#0D0D0D', // Tiefster Hintergrund dark: '#1A1A1A', // Sidebar, Panels mid: '#2A2A2A', // Borders, Cards light: '#3A3A3A', // Hover-States muted: '#454745', // Placeholder, Icons }

Ink-Palette (Merchant-Bereich)

// tailwind.config.js — ink (rot-getönt) ink: { deep: '#180A0A', // Ticker-BG dark: '#1E0E0E', // Haupt-Hintergrund mid: '#2E1010', // Panels light: '#3E1818', // Hover }

Brand-Farben (beide Bereiche)

TokenHexVerwendung
brand.red#E63946Primärer Accent, CTAs, Rang-Badges
brand.yellow#F5B700Preise, Bookmark-Stern aktiv, Premium-Slot-Border
brand.copy#C8C8C8Standard-Fließtext
brand.ticker#454745Ticker-Lauftext (gedämpft)

Typografie

FontVerwendung
JetBrains MonoLabels, Tags, Scores, Ranks, Code — alles was technisch/numerisch ist
IBM Plex SansFließtext, Nav-Items, Überschriften, Beschreibungen
09

Blade-Komponenten-Architektur

9 Blade-Components, hierarchisch organisiert. Laravel erkennt Unterordner automatisch via Punkt-Notation in der Blade-Template-Syntax.

VERZEICHNISSTRUKTUR — resources/views/ ## Layouts layouts/ app.blade.php ← Merchant-Layout (ink-Palette, Sidebar rot) guest.blade.php ← Auth-Layout (Scan-Line-Effekt, Terminal-Vibe) ## Buyer-Bereich Components components/ buyer/ topbar.blade.php ← Logo + Suchfeld (CSS Grid zentriert) + User-Menü sidebar.blade.php ← Collapsible (50px ↔ 200px), Nav-Links, Icons ticker.blade.php ← Bottom-Strip 50px, Lauftext rechts-nach-links catalog/ ad-card.blade.php ← Standard Ad-Karte (organisches Ranking) premium-slot.blade.php ← Ufersteine-Karte (gelber border-left) hotspot-banner.blade.php ← Hotspot-Trennbereich (roter border-left) right-panel.blade.php ← Rechte Sidebar (Info, Trending) ad-overlay.blade.php ← Quick-View Modal (JS-generiert) ## Merchant-Bereich dashboard.blade.php ← KPIs, Chart, Leads-Tabelle

Component-Aufruf Syntax

BLADE — Component-Verwendung in catalog/index.blade.php {{-- Buyer-Layout-Wrapper --}} <x-buyer.topbar /> <x-buyer.sidebar /> {{-- Catalog-spezifische Components --}} @foreach($premiumSlots as $slot) <x-catalog.premium-slot :ad="$slot" /> @endforeach @if($hotspot) <x-catalog.hotspot-banner :hotspot="$hotspot" /> @endif <x-buyer.ticker />
Component-Lookup Problem: Beim ersten Aufbau lagen die Files in 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.

10

Catalog-View — Layout & UX

Layout-Struktur

Topbar (fixiert, ~60px): Logo links · Suchfeld Mitte (CSS Grid) · User-Menü rechts mit Notification-Bell.
Sidebar (links, collapsible): 200px expanded ↔ 50px collapsed. Nav-Punkte: Katalog, Ranking, Hotspots, Analytics, Merkliste, Einstellungen. Active-State via request()->routeIs().
Main Content: Premium Slots oben (gelber border-left) → Hotspot-Banner → Organische Ad-Grid → Right Panel.
Ticker (fixiert, 50px unten): Gelbes Label-Feld links · Lauftext-Animation (CSS keyframes, doppelter Track für nahtlosen Loop).
Right Panel: Schlanke rechte Spalte mit Trending-Info, Kategorien, Plattform-Stats.

Ad-Card Visuelles System

Karten-TypBorderScore-AnzeigeBesonderheit
Organisch1px solid #2a2a2aRang-Zahl rot, Score-Zahl gelbRank im Kreis links
Premium Slot (Uferstein)border-left: 3px solid #F5B700Rank ausgeblendetSPONSORED-Badge
Hotspot-Bannerborder-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.

JS — openAdOverlay() Aufruf aus ad-card.blade.php // onclick-Attribut auf der Card onclick="openAdOverlay({ id: {{ $ad->id }}, title: '{{ addslashes($ad->title) }}', price: '€{{ number_format($ad->price_cents / 100, 2) }}', rank: {{ $ad->rank ?? 0 }}, score: '{{ $ad->current_score }}', merchant: '{{ addslashes($ad->merchant->company_name ?? "") }}', deeplink: '{{ $ad->deeplink_url }}', bookmarked: {{ in_array($ad->id, $bookmarkedIds) ? 'true' : 'false' }} })"
Sidebar Gap-Fix: Der Gap zwischen Sidebar-Unterkante und Ticker war ein CSS-Mess — 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.
11

Bookmark-System

Vollständige Bookmark-Mechanik: Toggle per AJAX, visuelles Feedback (Stern gelb), Merkliste-View mit Empty-State.

BookmarkController

PHP — BookmarkController@toggle // POST /bookmarks/{ad} — Auth required public function toggle(Ad $ad) { $user = Auth::user(); // Toggle: existiert → detach, nicht → attach if ($user->bookmarks()->where('ad_id', $ad->id)->exists()) { $user->bookmarks()->detach($ad->id); $bookmarked = false; } else { $user->bookmarks()->attach($ad->id); $bookmarked = true; } return response()->json(['bookmarked' => $bookmarked]); }

Frontend-Toggle (JS)

JS — toggleBookmark() in ad-overlay.blade.php // CSRF-Token aus Meta-Tag (Laravel-Standard) function toggleBookmark(adId) { fetch(`/bookmarks/${adId}`, { method: 'POST', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, 'Content-Type': 'application/json' } }) .then(r => r.json()) .then(data => { const btn = document.getElementById(`bookmark-btn-${adId}`); btn.style.color = data.bookmarked ? '#F5B700' : '#454745'; btn.style.borderColor = data.bookmarked ? '#F5B700' : '#2a2a2a'; btn.title = data.bookmarked ? 'AUS MERKLISTE ENTFERNEN' : 'ZUR MERKLISTE'; }) .catch(err => console.error('Bookmark error:', err)); }

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.

Pivot ohne updated_at: Die 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.
Dummy-Daten Einschränkung: Das Bookmark-Toggle funktioniert technisch korrekt — aber mit hardcodierten Dummy-Ad-IDs in der Catalog-View schlägt der DB-Insert fehl weil diese IDs nicht in ads existieren. Greift erst vollständig wenn der Catalog echte DB-Ads lädt.
12

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

PHP — CategorySeeder (idempotent) // firstOrCreate = safe to run multiple times Category::firstOrCreate(['slug' => 'mode'], ['name' => 'Mode & Fashion']); Category::firstOrCreate(['slug' => 'elektronik'], ['name' => 'Elektronik']); Category::firstOrCreate(['slug' => 'sport'], ['name' => 'Sport & Outdoor']); // In DatabaseSeeder::class aufrufen: $this->call([CategorySeeder::class, MerchantSeeder::class]);

Ad-Lifecycle Status

StatusBedeutungSichtbar im Catalog
draftAngelegt, noch nicht veröffentlichtNein
activeLive, nimmt am Ranking teilJa
pausedTemporär deaktiviert vom HändlerNein
archivedSoftDeleted, nicht mehr editierbarNein
Stand Ende Chat 02 (20.05.2026): Auth ✓ · Catalog-Layout ✓ · 9 Components ✓ · Bookmark-System ✓ · Ad-CRUD ✓ · CategorySeeder ✓ · MerchantSeeder ✓ · Design-System ✓
13

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-TypGewichtungQuelleRationale
view× 1ad_eventsGrundinteresse — User hat die Ad gesehen
dwell× 3ad_eventsStarkes Interesse — länger geschaut oder Deeplink geklickt
sale× 10ad_eventsConversion — höchster Signal-Wert
bookmark× 5bookmarks-TabelleExplizite Speicherabsicht
bounce/refund× 0ad_eventsNicht gewertet im MVP
Kein 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

PHP — app/Console/Commands/RecalculateAdScores.php Ad::where('status', 'active')->chunkById(100, function($ads) { foreach ($ads as $ad) { $since = now()->subDays(30); $rawScore = AdEvent::where('ad_id', $ad->id) ->where('created_at', '>=', $since) ->selectRaw("SUM(CASE WHEN event_type='view' THEN 1 WHEN event_type='dwell' THEN 3 WHEN event_type='sale' THEN 10 ELSE 0 END) as weighted") ->value('weighted') ?? 0; $bookmarkBonus = $ad->bookmarks()->count() * 5; $total = $rawScore + $bookmarkBonus; // Sigmoid-artige Normalisierung auf 0–99.99 $score = round(min(99.99, 99.99 * ($total / 120)), 2); $ad->update(['current_score' => $score]); } });
PHP — routes/console.php (Scheduler) Schedule::command('ads:recalculate-scores')->everyFiveMinutes();
14

AdEventController — View/Click-Tracking

Events werden über eine dedizierte Route getrackt. Gastnutzer können Events erzeugen (kein Auth nötig), user_id ist nullable.

PHP — AdEventController@track public function track(Request $request) { $request->validate([ 'ad_id' => 'required|exists:ads,id', 'event_type' => 'required|in:view,dwell,bounce,sale,refund', ]); AdEvent::create([ 'ad_id' => $request->ad_id, 'event_type' => $request->event_type, 'user_id' => Auth::id(), 'ip_hash' => hash('sha256', $request->ip()), ]); return response()->json(['ok' => true]); }
JS — View-Event beim Overlay-Öffnen (openAdOverlay) fetch('/events/track', { method: 'POST', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, 'Content-Type': 'application/json' }, body: JSON.stringify({ ad_id: data.id, event_type: 'view' }) });
Klassischer Bug: Der 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.
15

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

PHP — CatalogController@index (vereinfacht) $q = $request->get('q'); $category = $request->get('category'); $sort = $request->get('sort', 'score'); $adsQuery = Ad::with(['merchant','category','images'])->where('status','active'); if ($q) $adsQuery->where(fn($qb) => $qb->where('title','LIKE',"%{$q}%") ->orWhere('description','LIKE',"%{$q}%")); if ($category) $adsQuery->whereHas('category', fn($qb) => $qb->where('slug',$category)); match ($sort) { 'newest' => $adsQuery->latest(), 'price_asc' => $adsQuery->orderBy('price_cents'), 'price_desc' => $adsQuery->orderByDesc('price_cents'), default => $adsQuery->orderByDesc('current_score'), }; $organicAds = $adsQuery->get(); $hotspot = $organicAds->first(); // Premium Strip nur wenn kein Filter aktiv $premiumAds = ($q || $category) ? collect() : PremiumSlot::with(['ad.merchant','ad.images']) ->where('status','active')->where('starts_at','<=',now())->where('ends_at','>=',now()) ->limit(3)->get()->map(fn($s) => $s->ad)->filter(); $bookmarkedIds = Auth::check() ? Auth::user()->bookmarks()->pluck('ad_id')->toArray() : []; $categories = Category::orderBy('name')->get();
Bookmark-State-Synchronisation: window._bookmarkedIds = @json($bookmarkedIds) beim Page-Load ins JS exportieren. Nach AJAX-Toggle clientseitig aktualisieren. Ad-Card liest: bookmarked: window._bookmarkedIds.includes({{ $adId }}).
16

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".

BereichQuelleSortierung
Premium Strip (max. 3)premium_slots JOIN ads — nur zeitlich gültige BuchungenBuchungs-Reihenfolge
Organisches Gridads WHERE status = activecurrent_score DESC
HotspotorganicAds->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=).

17

Ad-Detail-Seite & Click-Tracking

Jede Ad-Card hat zwei Interaktionspunkte. AdsApp ist kein Shop — der Kauf passiert beim Händler über den Deeplink.

AktionTargetTracking
Klick auf Card / Lupe-IconQuick-View Overlayview-Event via /events/track
Klick auf Pfeil-Icon/ads/{id} Detail-Pageview-Event via AdController@show
„ZUM HÄNDLER →"/ads/{id}/click → redirect()->away()dwell-Event
PHP — AdController@click public function click(Ad $ad) { abort_if($ad->status !== 'active', 404); AdEvent::create(['ad_id' => $ad->id, 'event_type' => 'dwell', 'user_id' => Auth::id(), 'ip_hash' => hash('sha256', request()->ip())]); // away() Pflicht für externe URLs — to() würde App-Domain voranstellen return redirect()->away($ad->deeplink_url); }

Catalog Dummy-Subpages für Nav-Active-States

PHP — web.php Route::get('/catalog/ranking', [CatalogController::class, 'ranking'])->name('catalog.ranking'); Route::get('/catalog/hotspots', [CatalogController::class, 'hotspots'])->name('catalog.hotspots'); Route::get('/catalog/analytics', [CatalogController::class, 'analytics'])->name('catalog.analytics');
18

Filter, Suche & Sortierung

Vollständige Filterlogik über GET-Parameter — normaler Page-Reload, kein AJAX. State bleibt beim Navigieren erhalten, URLs sind bookmarkbar.

ParameterWertController-Logik
?q=nexusSuchbegriffLIKE auf title + description
?category=gamingKategorie-SlugwhereHas('category', slug)
?sort=scoreStandardorderByDesc('current_score')
?sort=newestNeueste zuerstlatest()
?sort=price_asc/descNach PreisorderBy/Desc('price_cents')

Topbar — CSS Grid für exakte Zentrierung

BLADE — nav in buyer/topbar.blade.php <nav class="fixed top-0 ... grid grid-cols-3 items-center px-6"> <a class="... inline-block justify-self-start">ADSAPP.STORE</a> {{-- Col 1 --}} <form class="... justify-self-center">...</form> {{-- Col 2 --}} <div class="... justify-self-end">...</div> {{-- Col 3 --}} </nav>
Logo-Blinker nach Grid-Migration: Das ::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.
19

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.

PHP — DashboardController@index (wesentliche Queries) $merchant = Auth::user()->merchant; $activeAds = $merchant->ads()->where('status','active')->count(); $leadsToday = AdEvent::whereIn('ad_id', $merchant->ads()->pluck('id')) ->where('event_type','dwell')->whereDate('created_at',today())->count(); $viewsMonth = /* ad_events.view letzte 30 Tage */; $avgScore = $merchant->ads()->where('status','active')->avg('current_score') ?? 0; $chartData = /* Views + Dwell-Events pro Tag, letzte 30 Tage, normalisiert 0–100% */; $topAds = $merchant->ads()->with('images')->where('status','active') ->orderByDesc('current_score')->limit(5)->get(); $recentLeads = /* JOIN ad_events + ads + users, letzte 5 dwell-Events */;
Empty States sind überall eingebaut wenn noch keine Daten vorhanden sind.
20

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.

PHP — User-Model Accessor public function getUnreadLeadsCountAttribute(): int { if (!$this->merchant) { return 0; } $since = $this->notifications_seen_at ?? $this->created_at; return DB::table('ad_events') ->join('ads', 'ad_events.ad_id', '=', 'ads.id') ->where('ads.merchant_id', $this->merchant->id) ->where('ad_events.event_type', 'dwell') ->where('ad_events.created_at', '>', $since) ->count(); }

Migration

PHP — add_notifications_seen_at_to_users $table->timestamp('notifications_seen_at')->nullable(); // In User-Model $casts: 'notifications_seen_at' => 'datetime'

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.

21

Settings Refactoring — Section Partials

Die settings/index.blade.php war mit allen Sektionen ein ~280-Zeilen-Monolith. Refactoring: jede Sektion in ein Partial ausgelagert.

STRUCTURE — resources/views/settings/ settings/ index.blade.php ← Gerüst + Nav + @includes sections/ profile.blade.php ← Profil-Formular security.blade.php ← Passwort ändern notifications.blade.php ← Leads-Liste + mark-as-seen delete-account.blade.php ← Account löschen placeholder.blade.php ← Darstellung / Datenschutz / Hilfe
@include vs. Components: @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).
22

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.

BLADE — components/buyer/ticker.blade.php @php $tickerAds = \App\Models\Ad::with('images') ->where('status','active')->latest()->limit(12)->get(); $doubled = $tickerAds->concat($tickerAds); // Doppeln für nahtlosen CSS-Loop @endphp
Transparenz-Entscheidung: Backdrop-Filter wurde getestet, aber der Catalog-Content endet bei bottom: 50px — es gibt physisch nichts hinter dem Ticker. Entscheidung: Solider Anthrazit-Ton #141414 mit goldenem Top-Border.
23

Bookmark UX Fixes

Fix 1 — Stern bleibt nach Toggle gelb

onmouseout überschrieb den State hardcoded. Fix: data-bm-Attribut als State-Träger:

JS — Button onmouseout im Template-Literal onmouseout="this.dataset.bm==='1' ? (this.style.borderColor='#F5B700',this.style.color='#F5B700') : (this.style.borderColor='#2a2a2a',this.style.color='#454745')"

Fix 2 — Overlay-Backdrop entfernt, gelber Box-Shadow

JS — box-shadow in openAdOverlay() innerHTML background: transparent; pointer-events: none; // Backdrop-Div box-shadow: 0 0 60px rgba(245,183,0,0.15), 0 0 120px rgba(245,183,0,0.06); // Overlay-Box

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.

JS — bookmarks/index.blade.php window.addEventListener('load', function() { window.toggleBookmark = function(adId) { fetch('/bookmarks/' + adId, { /* ... */ }).then(r => r.json()).then(res => { if (!res.bookmarked) { const card = document.getElementById('bookmark-card-' + adId); if (card) { card.style.transition = 'opacity 0.25s ease, transform 0.25s ease'; card.style.opacity = '0'; card.style.transform = 'scale(0.92)'; setTimeout(() => { card.remove(); updateCount(); checkEmpty(); }, 250); } if (typeof closeAdOverlay === 'function') closeAdOverlay(); } if (typeof showBookmarkToast === 'function') showBookmarkToast(res.bookmarked); }); }; });
24

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.

JS — tailwind.config.js (neu: coal) coal: { black: '#0a0a0a', // Tiefster BG (Sidebar, Topbar) deep: '#111111', // Overlays, Modals panel: '#141414', // Cards, Dropdowns surface: '#1a1a1a', // Hover-States line: '#2a2a2a', // Borders 'line-soft': '#1e1e1e', // Subtile Borders },
PaletteBereichBasis-Ton
inkMerchant Dashboard#180A0A — warm rot-getönt
coalBuyer / Catalog#0a0a0a — neutral anthrazit
brandBeide BereicheRot #E63946 + Gelb #F5B700
25

Bugs & Fixes — Chat 03 Log

BugUrsacheFix
500er auf /events/trackAdEventController war leer (Stub nie befüllt)track()-Methode implementiert
Stern springt nach Toggle zurück auf grauonmouseout hardcoded auf #2a2a2adata-bm Attribut als State-Träger
Toast hinter Overlay-Backdropz-index zu niedrig, Backdrop nicht transparentToast z-index: 100000, Backdrop transparent
Merkliste instant-remove funktioniert nichtOverlay-Script (im Layout) überschreibt toggleBookmark nach Merkliste-Scriptwindow.addEventListener('load', ...) Wrapper
Sidebar-Toggle crasht auf Subpagescatalog-wrap noch nicht geparst beim Script-Load (Timing)getElementById('catalog-wrap') erst im Click-Handler aufrufen
$dispatch is not definedAlpine.js Magic-Property funktioniert nur in @click-Context, nicht in onclickonclick="...; $dispatch" → @click="open = false; ..."
Logo-Blinker verschwindet nach Grid::after braucht display: inline-blockExplizit class="inline-block" auf Logo-a
npm run dev im PremiumSlot-ModelTerminal-Befehl in falsche Datei kopiertZeile löschen
Pagination zeigt alle EinträgeController nutzte ->get() statt ->paginate(6)->paginate(6) + echte Pagination-Komponente
Blaue Input-Focus-BorderBrowser-Defaults nicht vollständig überschriebenGlobale CSS-Regel input:focus { border-color: #F5B700 !important }
OrderController::index() 500Methode nicht implementiertMinimaler Stub, echte Implementierung Chat 04

Commit-Übersicht Chat 03

Commit-MessageWas drin istDatum
feat: catalog on real DB ads, bookmark UX, dummy nav pagesCatalogController Echtdaten, AdFactory+Seeder, Components model-aware, Stern-Fix, Toast z-index, Backdrop transparent, Sidebar Nav-Active, Dummy-Pages27.05.
feat: score mechanism, ad events tracking, premium slots separatedRecalculateAdScores Command, AdEventController, Scheduler, Premium Strip getrennt27.05.
feat: ad detail page, click tracking, card buttons, overlay restructureads/show, ads/{id}/click, Lupe+Pfeil auf Card, ZUM HÄNDLER + VOLLANSICHT im Overlay28.05.
feat: filter, search, sort + dashboard real data + topbar gridFilter/Suche/Sort, Premium Strip bei Filter aus, Filter-Bar, Topbar CSS Grid, Dashboard Echtdaten28.05.
feat: ticker live feed + optic fixesTicker neueste 12 Ads, Score-Farbe, Klick öffnet Overlay28.05.
refactor: settings sections in einzelne partials5 Partials, Notifications-Logik, Bell-Badge28.05.
feat: ads-liste pagination + echte stats, gelber input-focuspaginate(6), Stats-Cards aus DB, globaler Focus-Style28.05.
fix: bookmark instant-remove, AdEventController.trackwindow.load-Wrapper, 500er behoben28.05.
fix: dropdown coal-palette, $dispatch fix, orders stubcoal-Palette in Tailwind, Catalog-Dropdown, Alpine-Fix, OrderController Stub28.05.
fix: sidebar-toggle null-guard + lazy catalog-wrapcatalog-wrap erst im Click-Handler, Null-Guard28.05.
Stand Ende Chat 03 (28.05.2026): Score-Mechanismus ✓ · AdEvents-Tracking ✓ · Catalog Echtdaten ✓ · Premium Slots getrennt ✓ · Ad-Detail + Click-Tracking ✓ · Filter/Suche/Sort ✓ · Dashboard Echtdaten ✓ · Notifications ✓ · Settings Partials ✓ · Ticker Live-Feed ✓ · Bookmark UX vollständig ✓ · coal-Palette ✓ · Ads-Pagination ✓
// PART 3
CHAT 04 — 29.05.2026
Orders/Leads · Status-Toggle · Catalog Ranking · Hotspots · Analytics · Premium Slot Booking · Admin Panel · Approval Flows · Catalog Grid Refactor
27

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).

Controller: OrderController@index

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).

View: orders/index.blade.php

Stats-Grid (4 Kacheln), Tab-System LEADS/BESTELLUNGEN (JS switchTab()), Leads-Tabelle mit Pagination (15/Seite). Bestellungs-Tab ist Placeholder. Empty-State mit Icon.

// OrderController@index — Kernlogik $baseQuery = fn() => DB::table('ad_events') ->join('ads', 'ad_events.ad_id', '=', 'ads.id') ->where('ads.merchant_id', $merchant->id) ->where('ad_events.event_type', 'dwell'); $leadsWeek = $baseQuery()->where('ad_events.created_at', '>=', now()->subDays(7))->count(); $convRate = $totalLeads > 0 ? round(($sales / $totalLeads) * 100, 1) : 0; // → Conversion Rate: Sale-Events / Dwell-Events auf eigene Ads
Design-Entscheidung: Leads ersetzen klassische Orders — AdsApp ist ein Affiliate-Aggregator ohne Checkout. Dwell-Events als Lead-Proxy ist konzeptionell korrekt: Ein Nutzer der 5s+ auf einer Ad-Detailseite verbleibt, hat echtes Interesse signalisiert.

Commit: feat: orders/leads view with stats + ad status toggle (active/paused) via PATCH

28

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.

Route + Controller

PATCH /ads/{'{ad}'}/toggle-statusAdController@toggleStatus. Cycelt active → paused → active. Draft bleibt draft (nur über Edit aktivierbar). Ownership-Check via abort_if().

Frontend — Optimistic UI

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.

// AdController@toggleStatus public function toggleStatus(Ad $ad) { abort_if($ad->merchant_id !== Auth::user()->merchant->id, 403); if ($ad->status === 'draft') { return response()->json(['error' => 'Draft-Ads können nicht getoggled werden.']); } $ad->status = $ad->status === 'active' ? 'paused' : 'active'; $ad->save(); return response()->json([ 'status' => $ad->status, 'label' => $ad->status === 'active' ? 'AKTIV' : 'PAUSIERT', 'statusStyle' => $ad->status === 'active' ? 'border-color:#F5B700;color:#F5B700;' : 'border-color:#454745;color:#454745;', ]); }
Edge Case: SVG-Icon-Swap (Auge offen/zu) ist server-rendered via Blade-Komponente. Clientseitiger Swap ohne Page-Reload wäre nur mit inline-SVGs oder Livewire sauber. Für MVP reicht Farb-Feedback + Title-Update als Signal — als optionale Verbesserung dokumentiert.
29

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.

// AdController@index — Filter-Logik $query = $merchant->ads()->with('images'); if ($request->filled('status') && $request->status !== 'alle') { $query->where('status', $request->status); } if ($request->filled('category')) { $query->where('category_id', $request->category); } if ($request->filled('search')) { $query->where(function ($q) use ($request) { $q->where('title', 'like', "%{$request->search}%") ->orWhere('description', 'like', "%{$request->search}%"); }); } $ads = $query->latest()->paginate(6)->withQueryString(); // withQueryString() hält Filter-Parameter bei Seitenwechsel
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

30

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.

Zone A — Top-Strip (FIFO)

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.

Zone B — Right Aside (Fixed Price)

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.

Datenmodell

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.

Leerstand-Logik

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.

Verworfene Option B: Echte Live-Auktion mit Verdrängung nach Gebot — zu viele Edge Cases (Erstattungslogik, Race Conditions, Notifications). Für Capstone-Zwecke nicht verhältnismäßig.
31

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 vs. Dummy

✅ 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.

Wichtiger Fix: bookmarkedBy

Relation heißt bookmarkedBy() nicht bookmarks()withCount('bookmarks') würde knallen. Gefunden beim Model-Check, korrigiert vor dem Bauen.

// CatalogController@ranking — Aggregation $ads = Ad::with(['merchant', 'category', 'images']) ->where('status', 'active') ->withCount([ 'events as sales_count' => fn($q) => $q->where('event_type', 'sale') ->when($since, fn($q) => $q->where('created_at', '>=', $since)), 'events as dwell_count' => fn($q) => $q->where('event_type', 'dwell') ->when($since, fn($q) => $q->where('created_at', '>=', $since)), 'bookmarkedBy as bookmarks_count', // korrekte Relation ]) ->orderByDesc('current_score') ->limit(20) ->get();
// SVG Donut-Chart — kein JS, kein Chart-Package nötig // stroke-dasharray = Bogenlänge des Segments; stroke-dashoffset = Startposition $circumference = 2 * pi() * 40; // r=40 // Pro Segment: $dash = ($cnt/$total) * $circumference, $offset += $dash nach jedem Segment // -rotate-90 auf dem SVG startet erstes Segment oben statt rechts
Theme-Trennung: Erste Version nutzte Dunkelrot-Palette (ink). Auf Anthrazit (coal) umgemappt — Dunkelrot ist exklusiv dem Merchant-Backroom vorbehalten. Mapping: #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

32

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.

Migration: create_hotspots_table

Felder: title, subtitle, badge_text, badge_color, icon, ad_id (nullable FK), is_active, starts_at, ends_at, sort_order. Timestamps.

Integration

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

33

Analytics Views (Buyer + Merchant)

Zwei getrennte Analytics-Views: catalog/analytics.blade.php (Buyer-Perspektive) und merchant/analytics.blade.php (deep Dashboard mit Zeitreihen-Charts).

Buyer Analytics

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.

Merchant Deep 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)

34

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.

Migration 1: restructure_premium_slots_table

Slot-Definition (zone, position, base_price_cents, max_days). Trennt die Slot-Definition (dauerhaft, admin-verwaltet) von den Buchungen (transient).

Migration 2: create_slot_bookings_table

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.

UI: Slot-Übersicht + Antragsformular

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.

Booking-Regeln (enforced)

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)

35

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).

Routes & Controllers

Alle role:admin-geschützt. Admin\AdminDashboardController, Admin\SlotApprovalController, Admin\MerchantApprovalController. Eigenständiges Unterverzeichnis — klar getrennt.

Theme: Dunkelblau/Cyan

CSS-Vars: --admin-bg:#0a0f1a, --admin-panel:#111a2b, --admin-line:#1e3050. Akzentfarbe #4fc3f7 (cyan). Dritte Designsprache — Admin-Panel als "Kontroll-Ebene" visuell abgesetzt.

// Layout: resources/views/layouts/admin.blade.php // 3 Interface-Ebenen im Projekt: // Catalog (Buyer): --coal-palette (Anthrazit, #141414 Basis) // Merchant Backroom: --ink-palette (Dunkelrot, #1a0f0f Basis) // Admin Panel: --admin-palette (Dunkelblau, #0a0f1a Basis)
Dashboard KPIs

Pending Slots, Pending Merchants, Live Slots, Aktive Ads, User gesamt, Orders gesamt. Alert-Dots bei pending > 0 für sofortige Sichtbarkeit.

Sidebar-Navigation

Ü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)

36

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.

Slot-Approval

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.

Merchant-Approval Guard

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 als eigene Route

Notifications aus Settings herausgelöst, eigene Route merchant.notifications. Settings-View schlanker, Notifications-View fokussierter. Bell-Badge-Logik unverändert.

Slim Settings Layout

Dediziertes schlankes Layout für Settings — role-neutral, ohne Merchant-Sidebar. Saubere Trennung von Merchant-Dashboard-Shell und Account-Settings.

// fixes während dieses Blocks: // fix: add 'draft' to ads status enum (Code erwartete es, DB hatte es nicht) // fix: toggle-status activates drafts (draft/paused → active) // fix: ad edit form preselects category/status/tags, update syncs tags + images // feat: schedule ad score recalculation every 5 minutes // → routes/console.php, Schedule::command('ads:recalculate-scores')->everyFiveMinutes() // → Laravel 13 bestätigt: identische Syntax wie L11

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

37

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.

Problem-Analyse

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).

Entscheidung: Weg A (MVP)

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.

// catalog/index.blade.php — neues Grid // Statt grid-cols-3 / grid-cols-4 (fest): style="display:grid; grid-template-columns:repeat(auto-fill, minmax(240px, 1fr)); gap:12px;" // Alle Karten: col-span-1, aspect-video (16:9) // Premium-Strip: eigene feste 3-Spalten-Zone oben (unverändert prominent) // Hotspot-Einstreuung: alle 8 Ads (% 8 === 0), col-span-1 → kein Loch // Ranking: startet bei #1, fortlaufend ohne Offset (Premium-Zone hat keine Rang-Nummern) // #1: roter Border + Score-Badge als Hervorhebung (nicht Größe)
Bento-Gewimmel (v2 geplant): Die Original-Vision war ein Masonry-Layout wo Ad-Größe = Score. Technisch: CSS allein kann kein echtes Masonry ohne Zeilenreihenfolge-Bruch. Benötigt JS (Muuri/Masonry.js + Absolut-Positionierung). Live-Bewegung (score-getriebenes Auf-/Absteigen) wäre WebSockets + FLIP-Animation — eigenes Feature-Projekt. Stufe 1 (statische heterogene Kacheln) ist ~1 Tag. Stufe 2 (Live-Bewegung) ist ~1 Woche.

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-MessageWas drin istDatum
feat: orders/leads view with stats + ad status toggle (active/paused) via PATCHOrderController@index mit Leads/Stats/Conv-Rate, orders/index.blade.php Tab-Layout, AdController@toggleStatus Route PATCH, JS optimistic UI29.05.
feat: ads list filter by status, category + search with query persistenceFilter-Bar als GET-Links, Kategorie-Dropdown, Suchfeld, withQueryString() Pagination29.05.
feat: catalog ranking leaderboard with real score aggregation + market split donutCatalogController@ranking, Top-3 prominent / Rank4+ kompakt, SVG-Donut, Newcomers29.05.
style: catalog ranking to coal/anthracite themeMapping Dunkelrot → coal-Tokens, #5B403F → #2a2a2a etc.29.05.
feat: hotspots model + migration + seeder + catalog viewHotspot-Model, Migration, Seeder, catalog/hotspots.blade.php29.05.
feat: replace dummy top-ad banner with real featured hotspotCatalogController@index integriert echten Hotspot29.05.
feat: buyer analytics view + merchant deep analyticscatalog/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/B29.05.
feat: admin panel foundation (layout, guard, dashboard)layouts/admin.blade.php (cyan-Theme), AdminDashboardController, Routes mit role:admin29.05.
feat: admin slot approval + admin merchant approvalSlotApprovalController, MerchantApprovalController, approve/reject mit reviewer_id29.05.
feat: enforce merchant approval (ad guard + catalog filter)Ad-Erstellung blocked bei pending, Catalog filtert approved Merchants29.05.
refactor: notifications as own route/view, slim settings layoutmerchant.notifications eigen, Settings-Shell ohne Merchant-Sidebar29.05.
fix: add 'draft' to ads status enumMigration ergänzt draft in ENUM, Code erwartete es bereits29.05.
feat: schedule ad score recalculation every 5 minutesroutes/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 aggregiert29.05.
fix: catalog ranking starts at #1 and runs gapless$pos = $i + 1 statt +4, zweites Grid :rank="$i + 6" statt +929.05.
fix: gapless catalog grid (featured row-span-2, hotspot-promo full-width)featured bekommt row-span-2, Hotspot col-span-full29.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 Karte29.05.
Stand Ende Chat 04 (29.05.2026): Orders/Leads-View ✓ · Status-Toggle AJAX ✓ · Ads-Filter GET-persistent ✓ · Catalog Ranking ✓ · Hotspots System ✓ · Buyer + Merchant Analytics ✓ · Premium Slot Booking Flow (2-Tabellen) ✓ · Admin Panel (cyan-Theme) ✓ · Slot-Approval + Merchant-Approval ✓ · Score-Scheduler (5 Min) ✓ · Catalog Grid responsive auto-fill ✓ · Laravel 13 bestätigt ✓
Offen / v2: Bento-Masonry-Grid (score-getriebene Kachelgröße, JS-basiert) · Live-Bewegung via WebSockets + FLIP-Animation · Abrechnungs-View · FAQ/Support-Dummy · Leerstand-Job (25% OFF nach X Tagen) · Deployment auf Hetzner (Forge, DNS, Cloudflare, Queue-Worker)
39

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.

Filter-Bar Scroll-Pfeile

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.

Sidebar Cleanup

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ü.

Topbar Grid-Fix

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.

overflow-x Fix

#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

40

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

TabelleWasHinweis
hotspotsExistierte bereits mit anderem SchemaModel an echte Spalten angepasst: subtitle, criteria (JSON), erweiterte type-Enums
hotspot_adsPivot Hotspot ↔ Ad (n:m)Existierte schon unter dem Namen hotspot_ads, nicht ad_hotspot

HotspotController

app/Http/Controllers/HotspotController.php // index() — /hotspots Übersicht $active = Hotspot::active()->withCount('ads')->get(); $upcoming = Hotspot::upcoming()->get(); $archived = Hotspot::archived()->latest('closes_at')->take(8)->get(); $stats = ['active_nodes'=>..., 'total_volume'=>..., 'uptime'=>'99.98']; // show($slug) — /hotspots/{slug} Detail $hotspot = Hotspot::where('slug', $slug)->withCount('ads')->firstOrFail(); $bookmarkedIds = Auth::check() ? Bookmark::where('user_id', Auth::id())->pluck('ad_id') : []; $ads = $hotspot->ads()->where('ads.status', 'active')->orderByDesc('current_score')->get();

Model-Scopes

scopeActive() → opens_at vergangen ODER null UND closes_at in Zukunft ODER null scopeUpcoming() → opens_at > now() scopeArchived() → closes_at NOT NULL AND <= now() getDaysLeftAttribute() → max(0, diffInDays(closes_at)) // null wenn dauerhaft getEndsSoonAttribute() → days_left !== null && days_left <= 7

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

// right-panel.blade.php — Hotspot-Liste: sortiert nach kürzester Restlaufzeit, max. 3 @foreach($hotspots->sortBy(fn($h) => $h->days_left ?? 9999)->take(3) as $hs)

Routing

Route::get('/hotspots', [HotspotController::class, 'index'])->name('catalog.hotspots'); Route::get('/hotspots/{slug}', [HotspotController::class, 'show']) ->name('catalog.hotspot.show');
Route-Konflikt beim Deploy: Es gab eine alte Route 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)

41

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

AdController@store / @update — Upload-Block if ($request->hasFile('image') && $request->file('image')->isValid()) { $file = $request->file('image'); $filename = time() . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '_', $file->getClientOriginalName()); $dir = storage_path("app/public/ads/{$ad->id}"); if (!is_dir($dir)) mkdir($dir, 0755, true); $file->move($dir, $filename); // move() statt store() — umgeht Windows-Bug $path = "ads/{$ad->id}/{$filename}"; $ad->images()->create(['remote_url' => null, 'cache_path' => $path, 'position' => 1]); }
Bug 1 — Windows Temp-Path

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.

Bug 2 — NOT NULL Constraint

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().

Storage Link

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.

Bild-Rendering in Blades

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

42

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

ad-card.blade.php / premium-slot.blade.php / bookmarks/index.blade.php / ticker.blade.php // Alle clickbaren Card-Wrapper bekommen data-attributes statt inline onclick-Objekt: <div class="relative overflow-hidden cursor-pointer group" data-ad-id="{{ $adId }}" data-ad-title="{{ e($adTitle) }}" data-ad-price="{{ $adPrice }}" data-ad-merchant="{{ e($adMerch) }}" data-ad-description="{{ e($adDesc) }}" data-ad-image="{{ $adImage ? asset('storage/' . $adImage) : '' }}" data-ad-bookmarked="{{ $bookmarked ? 'true' : 'false' }}" onclick="openAdOverlayFromCard(this)"> // buyer-app.blade.php — globale Hilfsfunktion vor </body> function openAdOverlayFromCard(el) { openAdOverlay({ id: el.dataset.adId, title: el.dataset.adTitle, merchant: el.dataset.adMerchant, description: el.dataset.adDescription, image: el.dataset.adImage || '', bookmarked: el.dataset.adBookmarked === 'true' }); }
Bild im Overlay: 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.
pointer-events-none reicht nicht: Bei den Premium-Slot-Cards blockierte nicht das <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)

43

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.

/help — Hilfe, FAQ, Support

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.

/privacy — Datenschutz & Security

resources/views/help/privacy.blade.php — Datenschutz-Sektionen (DSGVO-Hinweise), Security-Checklist, Link zu Settings. Beide Seiten im coal-Palette Design-System.

HelpController.php class HelpController extends Controller { public function index() { return view('help.index'); } public function privacy() { return view('help.privacy'); } } // web.php — im "Alle Rollen" Block Route::get('/help', [HelpController::class, 'index']) ->name('help'); Route::get('/privacy', [HelpController::class, 'privacy'])->name('privacy');

Commit: feat: help/privacy pages + nav consolidation (info+support+hilfe merged, security removed from dropdown)

44

Nav-Konsolidierung

Parallel zu den neuen Pages wurden alle Navigations-Einträge bereinigt und auf sinnvolle Positionen verteilt.

EintragAltNeu
EINSTELLUNGENSidebar-Bottom (Buyer)Buyer-Topbar Dropdown + Merchant-Nav Dropdown
INFOBuyer-Topbar Dropdown→ ersetzt durch HILFE & INFO (route: help)
SECURITYBuyer-Topbar Dropdown + SidebarEntfernt — Datenschutz/Security in /privacy zusammengefasst
HILFE & INFOnicht vorhandenBuyer-Dropdown + Merchant-Dropdown (route: help)
DARSTELLUNGSettings subNavEntfernt — überflüssig, kein eigener Use-Case
DATENSCHUTZSettings subNav (tote Seite)Settings subNav → route: privacy
ZUR APPSettings-Topbar (immer Catalog)Rollenbasiert: Merchant → Dashboard, Buyer → Catalog, Admin → Admin (via homeRoute())

Commit: fix: settings dropdown in merchant nav + zur-app role-aware routing

45

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

SchrittWasHinweis
1Neue Site in Forge anlegenProject Type: Laravel, Web Directory: /public, GitHub-Repo verbinden, Branch: main
2DB anlegenDB-Name aus lokaler .env übernehmen; DB_HOST=127.0.0.1 (gleicher Server)
3Shared Paths"from storage to storage" — equivalent zu php artisan storage:link
4.env befüllenAPP_ENV=production, APP_DEBUG=false, APP_URL=https://adsapp.store, MAIL_MAILER=log für MVP
5Deploy-ScriptForge-Default + config:cache, route:cache, view:cache ergänzt; npm ci && npm run build drin lassen
6SSLLet's Encrypt via Forge — ein Klick; Cloudflare SSL-Mode: "Full" (nicht Strict → Error 525)
7Auto-DeployAktiviert — push auf main triggert automatisch Deploy
8SchedulerForge → Server → Scheduler: /usr/bin/php /home/forge/adsapp.store/current/artisan schedule:run · Every Minute · User: forge

Deploy-Script (finalisiert)

Forge Deploy Script $CREATE_RELEASE() cd $FORGE_RELEASE_DIRECTORY $FORGE_COMPOSER install --no-dev --no-interaction --prefer-dist --optimize-autoloader $FORGE_PHP artisan optimize $FORGE_PHP artisan storage:link $FORGE_PHP artisan migrate --force $FORGE_PHP artisan config:cache $FORGE_PHP artisan route:cache $FORGE_PHP artisan view:cache npm ci || npm install npm run build $ACTIVATE_RELEASE() $RESTART_QUEUES()

Post-Deploy Artisan-Commands (einmalig per SSH)

cd /home/forge/adsapp.store/current php artisan db:seed --class=CategorySeeder # Kategorie-Filter im Catalog php artisan db:seed --class=HotspotSeeder # Test-Hotspots php artisan db:seed --class=PremiumSlotSeeder # Premium-Slot-Zonen A + B
Cloudflare Error 525 — SSL Handshake Fail: Trat beim ersten Live-Test auf. Ursache: Cloudflare SSL-Mode war auf "Full (Strict)" gesetzt. Fix: auf "Full" wechseln. Let's Encrypt-Zertifikat auf Forge war korrekt aktiv.
Scheduler-Pfad in Forge: Der Default-Job mit $FORGE_PHP-Variable funktioniert nicht im Cron-Kontext. Stattdessen absoluten Pfad verwenden: /usr/bin/php /home/forge/adsapp.store/current/artisan schedule:run.
MVP live: https://adsapp.store — 31.05.2026, ~18:30 Uhr. Register, Login, Catalog, Ads, Hotspots, Help/Privacy, Merchant-Dashboard — alles erreichbar. Score-Scheduler läuft alle 5 Minuten. Auto-Deploy auf main aktiv.
46

Post-Deploy Fixes

Nach dem Go-Live wurden beim Durchklicken noch vier kleinere Bugs identifiziert und direkt gepatcht.

Farb-Kontrast (global)

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.

Ad-Overlay X-Button

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.

Overlay Browser-Kompatibilität

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 Bild-Path

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-MessageWas drin ist
refactor: sidebar cleanup + topbar min-width fix + dropdown settings migration + filter-bar scroll arrowsSidebar 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 migrationWindows Temp-Path Bug umgangen, NOT NULL Constraint Migration, storage:link erklärt
fix: ad-card/bookmarks/ticker overlay via data-attributes + image display in overlayAlle clickbaren Cards auf data-ad-* umgebaut, openAdOverlayFromCard() global, image-Feld im Overlay
fix: settings dropdown in merchant nav + zur-app role-aware routingEinstellungen 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 routeAlte /catalog/hotspots Route entfernt, Route-Caching-Fehler beim Deploy behoben
fix: increase text contrast globally #454745 → #999999Globales Find & Replace, Tailwind copy.ticker Token angepasst
fix: ad-overlay close button size + score overlap + brave overflowX-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-attributespointer-events-none reichte nicht, gleicher data-attributes-Umbau wie ad-card
Stand Ende Chat 05 / MVP (31.05.2026): Filter-Bar Scroll ✓ · Sidebar bereinigt ✓ · Topbar Grid-Fix ✓ · Hotspot Detail-Page ✓ · Bild-Upload (Windows-Workaround + nullable Migration) ✓ · Overlay via data-attributes (Sonderzeichen-safe) ✓ · Help & Privacy Pages ✓ · Nav-Konsolidierung ✓ · Hetzner/Forge live ✓ · Auto-Deploy ✓ · Score-Scheduler läuft ✓ · CategorySeeder + HotspotSeeder + PremiumSlotSeeder ✓ · Post-Deploy Bugs gefixt ✓ · adsapp.store ist live.
Geplant nach MVP: dev-Branch Workflow + Git Semantic Versioning (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.