diff --git a/cmd/acb-evolver/internal/live/exporter.go b/cmd/acb-evolver/internal/live/exporter.go index 2f6d09b..1745a7e 100644 --- a/cmd/acb-evolver/internal/live/exporter.go +++ b/cmd/acb-evolver/internal/live/exporter.go @@ -133,6 +133,8 @@ type Totals struct { HighestEvolvedRating int `json:"highest_evolved_rating"` EvolvedInTop10 int `json:"evolved_in_top_10"` MutationsPerHour float64 `json:"mutations_per_hour"` + MatchesToday int `json:"matches_today"` // plan §16.18: matches completed today + ActiveBots int `json:"active_bots"` // plan §16.18: active bot count } // LiveData is the full evolution dashboard payload written to live.json (plan §14 format). @@ -312,6 +314,24 @@ func fillTotals(ctx context.Context, db *sql.DB, data *LiveData) error { mutationsLastHour = 0 } + // Matches today (plan §16.18: completed matches since midnight UTC) + var matchesToday int + err = db.QueryRowContext(ctx, ` + SELECT COUNT(*) FROM matches + WHERE completed_at >= $1::date`, today).Scan(&matchesToday) + if err != nil { + matchesToday = 0 + } + + // Active bots (plan §16.18: bots with status = 'active') + var activeBots int + err = db.QueryRowContext(ctx, ` + SELECT COUNT(*) FROM bots + WHERE status = 'active'`).Scan(&activeBots) + if err != nil { + activeBots = 0 + } + data.Totals = Totals{ GenerationsTotal: maxGen, CandidatesToday: candidatesToday, @@ -320,6 +340,8 @@ func fillTotals(ctx context.Context, db *sql.DB, data *LiveData) error { HighestEvolvedRating: highestRating, EvolvedInTop10: top10Count, MutationsPerHour: round3(float64(mutationsLastHour)), + MatchesToday: matchesToday, + ActiveBots: activeBots, } return nil diff --git a/cmd/acb-index-builder/db.go b/cmd/acb-index-builder/db.go index 19b3233..def514b 100644 --- a/cmd/acb-index-builder/db.go +++ b/cmd/acb-index-builder/db.go @@ -1035,6 +1035,8 @@ type EvolutionMeta struct { TotalPromoted int `json:"total_promoted"` // all-time promoted count PromotionRate float64 `json:"promotion_rate"` // promoted/total UpdatedAt string `json:"updated_at"` + MatchesToday int `json:"matches_today"` // plan §16.18: matches completed today + ActiveBots int `json:"active_bots"` // plan §16.18: active bot count } // EvolvedBotRating represents an evolved bot's rating info @@ -1091,6 +1093,8 @@ func fetchEvolutionMeta(ctx context.Context, db *sql.DB) (*EvolutionMeta, error) TotalPromoted: 0, PromotionRate: 0, UpdatedAt: updatedAt, + MatchesToday: 0, + ActiveBots: 0, }, nil } @@ -1140,6 +1144,28 @@ func fetchEvolutionMeta(ctx context.Context, db *sql.DB) (*EvolutionMeta, error) // Count evolved bots in top 10 meta.Top10Count = len(meta.BestRatings) + // Fetch matches today (plan §16.18: completed matches since midnight UTC) + var matchesToday int + matchErr := db.QueryRowContext(ctx, ` + SELECT COUNT(*) FROM matches + WHERE completed_at >= CURRENT_DATE + `).Scan(&matchesToday) + if matchErr != nil { + matchesToday = 0 + } + meta.MatchesToday = matchesToday + + // Fetch active bots count (plan §16.18: bots with status = 'active') + var activeBots int + botErr := db.QueryRowContext(ctx, ` + SELECT COUNT(*) FROM bots + WHERE status = 'active' + `).Scan(&activeBots) + if botErr != nil { + activeBots = 0 + } + meta.ActiveBots = activeBots + meta.UpdatedAt = updatedAt return &meta, nil } diff --git a/web/src/api-types.ts b/web/src/api-types.ts index a27cf42..aef529b 100644 --- a/web/src/api-types.ts +++ b/web/src/api-types.ts @@ -449,13 +449,16 @@ export interface EvolutionMeta { promoted_today: number; top_10_count: number; updated_at: string; + matches_today?: number; // plan §16.18: matches completed today + active_bots?: number; // plan §16.18: active bot count } export async function fetchEvolutionMeta(): Promise { return swr('evolution-meta', async () => { const response = await fetch('/data/evolution/meta.json'); if (!response.ok) { - return { generation: 0, promoted_today: 0, top_10_count: 0, updated_at: '' }; + const fallback: EvolutionMeta = { generation: 0, promoted_today: 0, top_10_count: 0, updated_at: '', matches_today: 0, active_bots: 0 }; + return fallback; } return response.json(); }); diff --git a/web/src/pages/home.ts b/web/src/pages/home.ts index fc10362..9e6b82b 100644 --- a/web/src/pages/home.ts +++ b/web/src/pages/home.ts @@ -146,6 +146,8 @@ export async function renderHomePage(): Promise { promoted_today: 0, top_10_count: 0, updated_at: '', + matches_today: 0, + active_bots: 0, })), fetchSeasonIndex().catch(() => ({ updated_at: '', @@ -200,7 +202,8 @@ export async function renderHomePage(): Promise { const evoHtml = lazySection( 'home-evo', - `
🧬Evolution Observatory — Gen #${evolutionMeta.generation}${evolutionMeta.promoted_today > 0 ? ` · ${evolutionMeta.promoted_today} promoted today` : ''}${evolutionMeta.top_10_count > 0 ? ` · ${evolutionMeta.top_10_count} in top 10` : ''}
Watch evolution live →
`, + `
🧬Evolution Observatory — Gen #${evolutionMeta.generation}${evolutionMeta.promoted_today > 0 ? ` · ${evolutionMeta.promoted_today} promoted today` : ''}${evolutionMeta.top_10_count > 0 ? ` · ${evolutionMeta.top_10_count} in top 10` : ''}
Watch evolution live →
+
${(evolutionMeta.matches_today ?? 0).toLocaleString()} matches today · ${(evolutionMeta.active_bots ?? 0).toLocaleString()} bots active · Gen #${evolutionMeta.generation} evolving
`, { placeholder: '
' } ); @@ -535,6 +538,22 @@ export async function renderHomePage(): Promise { } .home-evo-text strong { color: var(--text-primary); } +/* Live stats bar (plan §16.18) */ +.home-live-stats { + background: var(--bg-tertiary); + border-radius: 8px; + padding: 8px 12px; + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; +} +.home-live-icon { font-size: 1rem; } +.home-live-text { + color: var(--text-muted); + font-size: 0.75rem; +} + .home-empty { color: var(--text-muted); text-align: center;