Leaderboard-Implementation: Technische Spezifikation
Zurück zur MOC - Map of Content
Überblick
Dieses Dokument beschreibt die technische Implementierung des Leaderboard-Systems. Es deckt SQL Functions, Supabase-Integration, Dashboard-Komponenten und Performance-Überlegungen ab.
Supabase Function: Creator Jury Score
CREATE OR REPLACE FUNCTION calculate_creator_jury_score(p_creator_id UUID)
RETURNS INTEGER AS $
DECLARE
revenue_score INTEGER;
session_score INTEGER;
entry_score INTEGER;
viewer_score INTEGER;
vote_score INTEGER;
total_score INTEGER;
BEGIN
-- Revenue: 1 Punkt pro 100€, max 40
SELECT LEAST(FLOOR(COALESCE(SUM(p.amount_cents), 0) / 10000), 40)
INTO revenue_score
FROM payments p
JOIN queue_entries qe ON p.queue_entry_id = qe.id
JOIN sessions s ON qe.session_id = s.id
WHERE s.creator_id = p_creator_id
AND p.status = 'completed';
-- Sessions: 2 Punkte pro Session, max 20
SELECT LEAST(COUNT(*) * 2, 20)
INTO session_score
FROM sessions
WHERE creator_id = p_creator_id
AND status = 'completed';
-- Entries Reviewed: 1 Punkt pro 10 Entries, max 15
SELECT LEAST(FLOOR(COUNT(*) / 10), 15)
INTO entry_score
FROM queue_entries qe
JOIN sessions s ON qe.session_id = s.id
WHERE s.creator_id = p_creator_id
AND qe.status IN ('played', 'rated');
-- Viewers: 1 Punkt pro 100 Viewer, max 15
SELECT LEAST(FLOOR(COALESCE(SUM(viewer_count), 0) / 100), 15)
INTO viewer_score
FROM sessions
WHERE creator_id = p_creator_id;
-- Community Votes: 1 Punkt pro 50 Votes, max 10
SELECT LEAST(FLOOR(COALESCE(SUM(br.vote_count), 0) / 50), 10)
INTO vote_score
FROM breakout_rounds br
JOIN sessions s ON br.session_id = s.id
WHERE s.creator_id = p_creator_id;
total_score := COALESCE(revenue_score, 0)
+ COALESCE(session_score, 0)
+ COALESCE(entry_score, 0)
+ COALESCE(viewer_score, 0)
+ COALESCE(vote_score, 0);
RETURN total_score;
END;
$ LANGUAGE plpgsql STABLE;
-- Index für Performance
CREATE INDEX IF NOT EXISTS idx_sessions_creator_id ON sessions(creator_id);
CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status);
CREATE INDEX IF NOT EXISTS idx_queue_entries_status ON queue_entries(status);
Supabase Function: Artist Qualification Score
CREATE OR REPLACE FUNCTION calculate_artist_qualification_score(p_artist_id UUID)
RETURNS INTEGER AS $
DECLARE
breakout_count INTEGER;
upcoming_count INTEGER;
reviewed_count INTEGER;
breakout_score INTEGER;
upcoming_score INTEGER;
session_score INTEGER;
rating_score INTEGER;
vote_score INTEGER;
total_score INTEGER;
BEGIN
-- Breakout Wins zählen
SELECT COUNT(*) INTO breakout_count
FROM breakout_results
WHERE artist_id = p_artist_id
AND outcome = 'breakout';
-- Upcoming Placements zählen (verschiedene Sessions!)
SELECT COUNT(DISTINCT session_id) INTO upcoming_count
FROM breakout_results br
JOIN breakout_rounds brd ON br.round_id = brd.id
WHERE br.artist_id = p_artist_id
AND br.outcome = 'upcoming';
-- Bewertete Entries zählen
SELECT COUNT(*) INTO reviewed_count
FROM queue_entries
WHERE artist_id = p_artist_id
AND status IN ('played', 'rated');
-- Hard Gate Check: (1 Breakout ODER 2 Upcoming) UND 3+ Entries
IF (breakout_count < 1 AND upcoming_count < 2) OR reviewed_count < 3 THEN
RETURN 0;
END IF;
-- Breakout: 20 Punkte pro Win, max 40
breakout_score := LEAST(breakout_count * 20, 40);
-- Upcoming: 8 Punkte pro Placement, max 24
upcoming_score := LEAST(upcoming_count * 8, 24);
-- Sessions: 2 Punkte pro bewerteter Entry, max 20
session_score := LEAST(reviewed_count * 2, 20);
-- Average Rating: direkt (7.5 = 7 Punkte), max 10
SELECT LEAST(FLOOR(COALESCE(AVG(
(SELECT AVG(er.score) FROM entry_ratings er WHERE er.entry_id = qe.id)
), 0)), 10)
INTO rating_score
FROM queue_entries qe
WHERE qe.artist_id = p_artist_id
AND qe.status IN ('played', 'rated');
-- Community Votes: 1 Punkt pro 100, max 5
SELECT LEAST(FLOOR(COALESCE(SUM(bv.id IS NOT NULL::int), 0) / 100), 5)
INTO vote_score
FROM breakout_votes bv
JOIN breakout_rounds br ON bv.round_id = br.id
JOIN breakout_results bres ON bres.round_id = br.id
WHERE bres.artist_id = p_artist_id;
total_score := COALESCE(breakout_score, 0)
+ COALESCE(upcoming_score, 0)
+ COALESCE(session_score, 0)
+ COALESCE(rating_score, 0)
+ COALESCE(vote_score, 0);
RETURN total_score;
END;
$ LANGUAGE plpgsql STABLE;
Leaderboard-Tabellen
-- Materialized View für Creator Leaderboard (stündlich aktualisiert)
CREATE MATERIALIZED VIEW IF NOT EXISTS creator_leaderboard AS
SELECT
c.id AS creator_id,
c.display_name,
calculate_creator_jury_score(c.id) AS score,
RANK() OVER (ORDER BY calculate_creator_jury_score(c.id) DESC) AS rank
FROM creators c
WHERE c.id IN (SELECT DISTINCT creator_id FROM sessions WHERE status = 'completed')
ORDER BY score DESC;
-- Materialized View für Artist Leaderboard
CREATE MATERIALIZED VIEW IF NOT EXISTS artist_leaderboard AS
SELECT
a.id AS artist_id,
a.artist_name,
calculate_artist_qualification_score(a.id) AS score,
RANK() OVER (ORDER BY calculate_artist_qualification_score(a.id) DESC) AS rank,
CASE
WHEN RANK() OVER (ORDER BY calculate_artist_qualification_score(a.id) DESC) <= 50
THEN 'qualified'
ELSE 'waitlist'
END AS status
FROM artists a
WHERE calculate_artist_qualification_score(a.id) > 0
ORDER BY score DESC;
-- Cron Job: Refresh alle 60 Minuten
-- (via Supabase Edge Function oder pg_cron)
SELECT cron.schedule(
'refresh-leaderboards',
'0 * * * *',
'REFRESH MATERIALIZED VIEW CONCURRENTLY creator_leaderboard; REFRESH MATERIALIZED VIEW CONCURRENTLY artist_leaderboard;'
);
Dashboard React-Komponenten
LeaderboardCard (Shared)
// apps/web/src/components/shared/LeaderboardCard.tsx
interface LeaderboardEntry {
rank: number;
name: string;
score: number;
breakdown: {
category: string;
value: number;
max: number;
}[];
status: 'jury' | 'backup' | 'top' | 'standard' | 'qualified' | 'waitlist';
lockCountdown: string; // "23d 14h"
}
// Komponente zeigt:
// - Rang + Name
// - Score mit Fortschrittsbalken
// - Status-Badge (Jury/Qualified/Waitlist)
// - Countdown bis Ranking Lock
// - Verbesserungstipps
CreatorLeaderboard
// apps/web/src/components/host/CreatorLeaderboard.tsx
// Hook: useCreatorLeaderboard()
// - Holt Daten aus creator_leaderboard View
// - Aktualisiert beim Laden der Seite
// - Zeigt Top 10 + eigene Position
ArtistLeaderboard
// apps/web/src/components/viewer/ArtistLeaderboard.tsx
// Hook: useArtistLeaderboard()
// - Holt Daten aus artist_leaderboard View
// - Zeigt Position ±10 um eigenen Rang
// - Qualified/Waitlist-Status prominent
Performance-Optimierungen
- Materialized Views statt Live-Berechnung: Scores werden vorab berechnet
- Stündlicher Refresh statt Realtime: Reduziert DB-Last um ~95%
- Indizes auf allen Join-Spalten: creator_id, artist_id, session_id, status
- CONCURRENTLY Refresh: Keine Downtime beim Aktualisieren
- Pagination: Leaderboard-API gibt max 50 Einträge pro Request zurück
Ranking Lock Implementation
-- Config-Tabelle für Tournament-Settings
CREATE TABLE IF NOT EXISTS tournament_config (
key VARCHAR(50) PRIMARY KEY,
value JSONB,
updated_at TIMESTAMP DEFAULT NOW()
);
-- Ranking Lock Flag
INSERT INTO tournament_config (key, value)
VALUES ('ranking_locked', '{"locked": false, "lock_date": "2026-04-XX"}');
-- Check in Score-Functions:
-- IF (SELECT boolean FROM tournament_config WHERE key = 'ranking_locked' THEN
-- RETURN cached_score; -- Gib gecachten Score zurück, keine Neuberechnung
-- END IF;
Links
MOC - Map of Content | Leaderboard-System | Aktueller Stand der App | Fehlende Komponenten | Technische Anforderungen Tournament