diff --git a/apps/web/src/pages/DashboardPage.tsx b/apps/web/src/pages/DashboardPage.tsx
index 47576b8..0b95d1c 100644
--- a/apps/web/src/pages/DashboardPage.tsx
+++ b/apps/web/src/pages/DashboardPage.tsx
@@ -1,27 +1,94 @@
-import { useGameStore } from '@/store';
-import { formatMoney, formatNumber, formatPercent } from '@ai-tycoon/shared';
+import type React from 'react';
+import { useGameStore, type ActivePage } from '@/store';
+import { formatMoney, formatNumber, formatPercent, formatDuration } from '@ai-tycoon/shared';
+import type { Era } from '@ai-tycoon/shared';
+import { TECH_TREE } from '@ai-tycoon/game-engine';
import {
- DollarSign, Server, Brain, Users, TrendingUp,
- TrendingDown, Minus, Cpu, Zap, Shield, ChevronRight,
+ DollarSign, TrendingUp, TrendingDown, Minus, Cpu, Brain, Users,
+ Shield, ChevronRight, Zap, Wifi, Sparkles, FlaskConical, Building2,
+ HardDrive, Clock,
} from 'lucide-react';
-import { XAxis, YAxis, Tooltip, ResponsiveContainer, Area, AreaChart } from 'recharts';
+import {
+ XAxis, YAxis, Tooltip, ResponsiveContainer, Area, AreaChart, Line, LineChart,
+} from 'recharts';
import { TutorialHint } from '@/components/game/TutorialHint';
+const ERA_ORDER: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
+
+function isEraAtLeast(current: Era, threshold: Era): boolean {
+ return ERA_ORDER.indexOf(current) >= ERA_ORDER.indexOf(threshold);
+}
+
export function DashboardPage() {
const money = useGameStore((s) => s.economy.money);
const revenuePerTick = useGameStore((s) => s.economy.revenuePerTick);
const expensesPerTick = useGameStore((s) => s.economy.expensesPerTick);
+ const financialHistory = useGameStore((s) => s.economy.financialHistory);
const totalFlops = useGameStore((s) => s.infrastructure.totalFlops);
const totalDCs = useGameStore((s) => s.infrastructure.totalDataCenterCount);
+ const clusters = useGameStore((s) => s.infrastructure.clusters);
const baseModels = useGameStore((s) => s.models.baseModels);
const activePipelines = useGameStore((s) => s.models.activeTrainingPipelines);
const subscribers = useGameStore((s) => s.market.consumerTiers.totalUsers);
+ const satisfaction = useGameStore((s) => s.market.consumerTiers.satisfaction);
const reputation = useGameStore((s) => s.reputation.score);
+ const reputationHistory = useGameStore((s) => s.reputation.reputationHistory);
const inferenceUtil = useGameStore((s) => s.compute.inferenceUtilization);
- const financialHistory = useGameStore((s) => s.economy.financialHistory);
+ const effectiveInferenceFlops = useGameStore((s) => s.compute.effectiveInferenceFlops);
+ const trainingAllocation = useGameStore((s) => s.compute.trainingAllocation);
+ const computeHistory = useGameStore((s) => s.compute.computeHistory);
+ const totalUptime = useGameStore((s) => s.infrastructure.totalUptime);
+ const modelFreshness = useGameStore((s) => s.market.obsolescence.playerModelFreshness);
const era = useGameStore((s) => s.meta.currentEra);
+ const activeResearch = useGameStore((s) => s.research.activeResearch);
+ const tam = useGameStore((s) => s.market.tam);
+ const competitors = useGameStore((s) => s.competitors.rivals);
const netIncome = revenuePerTick - expensesPerTick;
+ const scaleup = isEraAtLeast(era, 'scaleup');
+ const hasDeployedModel = baseModels.some(m => m.isDeployed);
+
+ const bestDeployedCapability = baseModels
+ .filter(m => m.isDeployed)
+ .reduce((best, m) => Math.max(best, m.rawCapability), 0);
+
+ const revenueTrend = (() => {
+ if (financialHistory.length < 6) return 'neutral' as const;
+ const recent = financialHistory[financialHistory.length - 1].revenue;
+ const earlier = financialHistory[financialHistory.length - 6].revenue;
+ if (recent > earlier * 1.01) return 'up' as const;
+ if (recent < earlier * 0.99) return 'down' as const;
+ return 'neutral' as const;
+ })();
+
+ const repTrend = (() => {
+ if (reputationHistory.length < 2) return 'neutral' as const;
+ const last = reputationHistory[reputationHistory.length - 1].score;
+ const prev = reputationHistory[reputationHistory.length - 2].score;
+ if (last > prev) return 'up' as const;
+ if (last < prev) return 'down' as const;
+ return 'neutral' as const;
+ })();
+
+ const constructingDCs = clusters.reduce((count, cluster) => {
+ for (const campus of cluster.campuses) {
+ for (const dc of campus.dataCenters) {
+ if (dc.status === 'constructing') count++;
+ }
+ }
+ return count;
+ }, 0);
+
+ const deployingRacks = clusters.reduce((count, cluster) => {
+ for (const campus of cluster.campuses) {
+ for (const dc of campus.dataCenters) {
+ count += dc.deploymentCohorts.length;
+ }
+ }
+ return count;
+ }, 0);
+
+ const navigate = useGameStore.getState().setActivePage;
return (
@@ -45,7 +112,8 @@ export function DashboardPage() {
)}
-
+ {/* Section 1: Stat Cards */}
+
= 0 ? '+' : ''}${formatMoney(netIncome)}/s`}
trend={netIncome > 0 ? 'up' : netIncome < 0 ? 'down' : 'neutral'}
color="text-green-400"
- onClick={() => useGameStore.getState().setActivePage('finance')}
+ onClick={() => navigate('finance')}
/>
+ {scaleup && (
+ navigate('finance')}
+ />
+ )}
useGameStore.getState().setActivePage('infrastructure')}
+ icon={Cpu}
+ label="Compute"
+ value={`${formatPercent(inferenceUtil)} util`}
+ subValue={`${formatNumber(effectiveInferenceFlops)} FLOPS`}
+ trend={inferenceUtil > 0.9 ? 'down' : inferenceUtil < 0.5 ? 'up' : 'neutral'}
+ color="text-cyan-400"
+ onClick={() => navigate('infrastructure')}
/>
p.status === 'active').length > 0 ? `Training: ${activePipelines.filter(p => p.status === 'active').length} active` : 'Idle'}
+ subValue={
+ activePipelines.filter(p => p.status === 'active').length > 0
+ ? `Training: ${activePipelines.filter(p => p.status === 'active').length} active`
+ : hasDeployedModel
+ ? `Best: ${bestDeployedCapability.toFixed(1)}`
+ : 'Idle'
+ }
color="text-purple-400"
- onClick={() => useGameStore.getState().setActivePage('models')}
- />
- useGameStore.getState().setActivePage('market')}
+ onClick={() => navigate('models')}
/>
+ {scaleup && (
+ navigate('market')}
+ />
+ )}
+ {scaleup && (
+
+ )}
-
+ {/* Section 2: Primary Charts */}
+
-
Revenue Over Time
- {financialHistory.length > 1 ? (
+
+
Compute: Capacity vs Demand
+
+ Capacity
+ Demand
+
+
+ {computeHistory.length > 1 ? (
-
+
-
-
-
+
+
+
+
+
+
+
- formatMoney(v)} tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} />
+ `${formatNumber(v)}`} tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} />
[formatMoney(value), 'Revenue']}
+ formatter={(value: number, name: string) => {
+ const label = name === 'tokensPerSecondCapacity' ? 'Capacity' : 'Demand';
+ return [`${formatNumber(value)} tok/s`, label];
+ }}
/>
-
+
+
) : (
- No data yet — start earning revenue
+ No compute data yet — deploy racks to start tracking
)}
+
+
+
Revenue vs Expenses
+
+ Revenue
+ Expenses
+
+
+ {financialHistory.length > 1 ? (
+
+
+
+ formatMoney(v)} tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} />
+ [formatMoney(value), name === 'revenue' ? 'Revenue' : 'Expenses']}
+ />
+
+
+
+
+ ) : (
+
+ No financial data yet — start earning revenue
+
+ )}
+
+
+
+ {/* Section 3: System Health + Active Operations */}
+
System Status
@@ -120,80 +264,139 @@ export function DashboardPage() {
bar={inferenceUtil}
barColor={inferenceUtil > 0.9 ? 'bg-danger' : inferenceUtil > 0.7 ? 'bg-warning' : 'bg-success'}
/>
+
+ {scaleup && (
+
+ )}
+ {scaleup && hasDeployedModel && (
+
+ )}
70 ? 'bg-success' : reputation > 40 ? 'bg-warning' : 'bg-danger'}
- />
-
-
-
-
-
-
Subscribers Over Time
- {(useGameStore.getState().market.subscriberHistory?.length ?? 0) > 1 ? (
-
-
-
-
-
-
-
-
-
- formatNumber(v)} tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} />
- [formatNumber(value), 'Subscribers']}
- />
-
-
-
- ) : (
-
- No subscriber data yet
-
- )}
-
-
Reputation Over Time
- {(useGameStore.getState().reputation.reputationHistory?.length ?? 0) > 1 ? (
-
-
-
-
-
-
-
-
-
- `${v}`} tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} />
- [`${value}/100`, 'Reputation']}
- />
-
-
-
- ) : (
-
- No reputation data yet
-
- )}
+
Active Operations
+
+ {/* Section 4: Secondary Charts (scaleup+) */}
+ {scaleup && (
+
+
+
Subscribers Over Time
+ {(useGameStore.getState().market.subscriberHistory?.length ?? 0) > 1 ? (
+
+
+
+
+
+
+
+
+
+ formatNumber(v)} tick={{ fontSize: 10, fill: '#64748b' }} axisLine={false} tickLine={false} />
+ [formatNumber(value), 'Subscribers']}
+ />
+
+
+
+ ) : (
+
+ No subscriber data yet
+
+ )}
+
+
+
+
Market Position
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Section 5: Competitor Snapshot (scaleup+) */}
+ {scaleup && competitors.filter(r => r.status === 'active').length > 0 && (
+
+
Competitors
+
+ {competitors.filter(r => r.status === 'active').map(rival => (
+
+
+ {rival.name}
+
+ {rival.archetype.replace('-', ' ')}
+
+
+
+
+ Capability
+ {rival.estimatedCapability.toFixed(1)}
+
+
+
+
+ You: {bestDeployedCapability.toFixed(1)}
+ Them: {rival.estimatedCapability.toFixed(1)}
+
+
+
+ Latest: {rival.latestModelName || 'None'}
+
+
+ ))}
+
+
+ )}
+
{totalDCs === 0 && (
Get Started
@@ -201,7 +404,7 @@ export function DashboardPage() {
Build your first data center to start training AI models.