Racks can now be marked for decommission from the DC view. The rack leaves production immediately (freeing slot and power), enters the pipeline as a timed decommission order, and is removed when complete. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Plus, Server, MapPin, Zap, HardDrive, Wrench, ChevronDown, ChevronUp, Thermometer, Shield } from 'lucide-react';
|
import { Plus, Server, MapPin, Zap, HardDrive, Wrench, ChevronDown, ChevronUp, Thermometer, Shield, X } from 'lucide-react';
|
||||||
import { useGameStore } from '@/store';
|
import { useGameStore } from '@/store';
|
||||||
import {
|
import {
|
||||||
formatMoney, formatNumber, formatPercent,
|
formatMoney, formatNumber, formatPercent,
|
||||||
@@ -16,6 +16,7 @@ const STAGE_LABELS: Record<PipelineStage, string> = {
|
|||||||
installation: 'Installation',
|
installation: 'Installation',
|
||||||
testing: 'Testing',
|
testing: 'Testing',
|
||||||
repair: 'Repair',
|
repair: 'Repair',
|
||||||
|
decommission: 'Decom',
|
||||||
};
|
};
|
||||||
|
|
||||||
const STAGE_COLORS: Record<PipelineStage, string> = {
|
const STAGE_COLORS: Record<PipelineStage, string> = {
|
||||||
@@ -25,6 +26,7 @@ const STAGE_COLORS: Record<PipelineStage, string> = {
|
|||||||
installation: 'bg-violet-500',
|
installation: 'bg-violet-500',
|
||||||
testing: 'bg-amber-500',
|
testing: 'bg-amber-500',
|
||||||
repair: 'bg-danger',
|
repair: 'bg-danger',
|
||||||
|
decommission: 'bg-surface-500',
|
||||||
};
|
};
|
||||||
|
|
||||||
function PipelineKanban() {
|
function PipelineKanban() {
|
||||||
@@ -32,7 +34,7 @@ function PipelineKanban() {
|
|||||||
|
|
||||||
if (pipeline.length === 0) return null;
|
if (pipeline.length === 0) return null;
|
||||||
|
|
||||||
const stages: PipelineStage[] = ['ordered', 'manufacturing', 'receiving', 'installation', 'testing', 'repair'];
|
const stages: PipelineStage[] = ['ordered', 'manufacturing', 'receiving', 'installation', 'testing', 'repair', 'decommission'];
|
||||||
const grouped = stages.map(stage => ({
|
const grouped = stages.map(stage => ({
|
||||||
stage,
|
stage,
|
||||||
orders: pipeline.filter(o => o.stage === stage),
|
orders: pipeline.filter(o => o.stage === stage),
|
||||||
@@ -41,7 +43,7 @@ function PipelineKanban() {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||||
<h3 className="text-sm font-semibold text-surface-300 uppercase mb-3">Rack Pipeline</h3>
|
<h3 className="text-sm font-semibold text-surface-300 uppercase mb-3">Rack Pipeline</h3>
|
||||||
<div className="grid grid-cols-6 gap-2">
|
<div className="grid grid-cols-7 gap-2">
|
||||||
{grouped.map(({ stage, orders }) => (
|
{grouped.map(({ stage, orders }) => (
|
||||||
<div key={stage}>
|
<div key={stage}>
|
||||||
<div className="text-[10px] text-surface-400 uppercase mb-1.5 text-center">
|
<div className="text-[10px] text-surface-400 uppercase mb-1.5 text-center">
|
||||||
@@ -108,14 +110,16 @@ function DataCenterCard({ dcId }: { dcId: string }) {
|
|||||||
const era = useGameStore((s) => s.meta.currentEra);
|
const era = useGameStore((s) => s.meta.currentEra);
|
||||||
const completedResearch = useGameStore((s) => s.research.completedResearch);
|
const completedResearch = useGameStore((s) => s.research.completedResearch);
|
||||||
const orderRack = useGameStore((s) => s.orderRack);
|
const orderRack = useGameStore((s) => s.orderRack);
|
||||||
|
const decommissionRack = useGameStore((s) => s.decommissionRack);
|
||||||
const upgradeDataCenter = useGameStore((s) => s.upgradeDataCenter);
|
const upgradeDataCenter = useGameStore((s) => s.upgradeDataCenter);
|
||||||
const [expanded, setExpanded] = useState(true);
|
const [expanded, setExpanded] = useState(true);
|
||||||
|
|
||||||
const tierConfig = DC_TIER_CONFIGS[dc.tier];
|
const tierConfig = DC_TIER_CONFIGS[dc.tier];
|
||||||
const currentEraIdx = ERA_ORDER.indexOf(era);
|
const currentEraIdx = ERA_ORDER.indexOf(era);
|
||||||
const liveUsedSlots = dc.racks.length + pipelineForDc.length;
|
const activePipeline = pipelineForDc.filter(o => o.stage !== 'decommission');
|
||||||
|
const liveUsedSlots = dc.racks.length + activePipeline.length;
|
||||||
const liveUsedPower = dc.racks.reduce((s, r) => s + RACK_SKU_CONFIGS[r.skuId].powerDrawKW, 0)
|
const liveUsedPower = dc.racks.reduce((s, r) => s + RACK_SKU_CONFIGS[r.skuId].powerDrawKW, 0)
|
||||||
+ pipelineForDc.reduce((s, o) => s + RACK_SKU_CONFIGS[o.skuId].powerDrawKW, 0);
|
+ activePipeline.reduce((s, o) => s + RACK_SKU_CONFIGS[o.skuId].powerDrawKW, 0);
|
||||||
|
|
||||||
const availableSkus = Object.values(RACK_SKU_CONFIGS).filter(sku => {
|
const availableSkus = Object.values(RACK_SKU_CONFIGS).filter(sku => {
|
||||||
if (ERA_ORDER.indexOf(sku.era) > currentEraIdx) return false;
|
if (ERA_ORDER.indexOf(sku.era) > currentEraIdx) return false;
|
||||||
@@ -180,11 +184,18 @@ function DataCenterCard({ dcId }: { dcId: string }) {
|
|||||||
{dc.racks.map(rack => {
|
{dc.racks.map(rack => {
|
||||||
const sku = RACK_SKU_CONFIGS[rack.skuId];
|
const sku = RACK_SKU_CONFIGS[rack.skuId];
|
||||||
return (
|
return (
|
||||||
<div key={rack.id} className={`text-[11px] rounded p-1.5 border ${
|
<div key={rack.id} className={`text-[11px] rounded p-1.5 border relative group ${
|
||||||
rack.isHealthy
|
rack.isHealthy
|
||||||
? 'bg-surface-800 border-surface-600'
|
? 'bg-surface-800 border-surface-600'
|
||||||
: 'bg-danger/10 border-danger/30'
|
: 'bg-danger/10 border-danger/30'
|
||||||
}`}>
|
}`}>
|
||||||
|
<button
|
||||||
|
onClick={() => decommissionRack(dc.id, rack.id)}
|
||||||
|
className="absolute top-0.5 right-0.5 p-0.5 rounded opacity-0 group-hover:opacity-100 hover:bg-danger/20 text-surface-400 hover:text-danger transition-all"
|
||||||
|
title="Decommission rack"
|
||||||
|
>
|
||||||
|
<X size={10} />
|
||||||
|
</button>
|
||||||
<div className="font-medium truncate">{sku.name}</div>
|
<div className="font-medium truncate">{sku.name}</div>
|
||||||
<div className="text-surface-400">{formatNumber(sku.flopsPerRack)} FLOPS</div>
|
<div className="text-surface-400">{formatNumber(sku.flopsPerRack)} FLOPS</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ interface Actions {
|
|||||||
setTrainingAllocation: (ratio: number) => void;
|
setTrainingAllocation: (ratio: number) => void;
|
||||||
buildDataCenter: (name: string, location: LocationId, tier: DCTier) => void;
|
buildDataCenter: (name: string, location: LocationId, tier: DCTier) => void;
|
||||||
orderRack: (dataCenterId: string, skuId: RackSkuId) => void;
|
orderRack: (dataCenterId: string, skuId: RackSkuId) => void;
|
||||||
|
decommissionRack: (dataCenterId: string, rackId: string) => void;
|
||||||
upgradeDataCenter: (dataCenterId: string, upgrade: 'cooling' | 'redundancy') => void;
|
upgradeDataCenter: (dataCenterId: string, upgrade: 'cooling' | 'redundancy') => void;
|
||||||
startTraining: (job: Omit<TrainingJob, 'progressTicks'>) => void;
|
startTraining: (job: Omit<TrainingJob, 'progressTicks'>) => void;
|
||||||
deployModel: (modelId: string) => void;
|
deployModel: (modelId: string) => void;
|
||||||
@@ -196,10 +197,9 @@ export const useGameStore = create<Store>()(
|
|||||||
if (!dc || dc.status !== 'operational') return s;
|
if (!dc || dc.status !== 'operational') return s;
|
||||||
|
|
||||||
const tierConfig = DC_TIER_CONFIGS[dc.tier];
|
const tierConfig = DC_TIER_CONFIGS[dc.tier];
|
||||||
const pipelineForDc = s.infrastructure.rackPipeline.filter(o => o.dataCenterId === dataCenterId).length;
|
const activePipeline = s.infrastructure.rackPipeline.filter(o => o.dataCenterId === dataCenterId && o.stage !== 'decommission');
|
||||||
const actualUsedSlots = dc.racks.length + pipelineForDc;
|
const actualUsedSlots = dc.racks.length + activePipeline.length;
|
||||||
const pipelinePowerForDc = s.infrastructure.rackPipeline
|
const pipelinePowerForDc = activePipeline
|
||||||
.filter(o => o.dataCenterId === dataCenterId)
|
|
||||||
.reduce((sum, o) => sum + RACK_SKU_CONFIGS[o.skuId].powerDrawKW, 0);
|
.reduce((sum, o) => sum + RACK_SKU_CONFIGS[o.skuId].powerDrawKW, 0);
|
||||||
const actualUsedPower = dc.racks.reduce((sum, r) => sum + RACK_SKU_CONFIGS[r.skuId].powerDrawKW, 0) + pipelinePowerForDc;
|
const actualUsedPower = dc.racks.reduce((sum, r) => sum + RACK_SKU_CONFIGS[r.skuId].powerDrawKW, 0) + pipelinePowerForDc;
|
||||||
if (actualUsedSlots >= tierConfig.rackSlots) return s;
|
if (actualUsedSlots >= tierConfig.rackSlots) return s;
|
||||||
@@ -225,6 +225,39 @@ export const useGameStore = create<Store>()(
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
decommissionRack: (dataCenterId, rackId) => set((s) => {
|
||||||
|
const dc = s.infrastructure.dataCenters.find(d => d.id === dataCenterId);
|
||||||
|
if (!dc || dc.status !== 'operational') return s;
|
||||||
|
|
||||||
|
const rack = dc.racks.find(r => r.id === rackId);
|
||||||
|
if (!rack) return s;
|
||||||
|
|
||||||
|
const sku = RACK_SKU_CONFIGS[rack.skuId];
|
||||||
|
const dataCenters = s.infrastructure.dataCenters.map(d => {
|
||||||
|
if (d.id !== dataCenterId) return d;
|
||||||
|
return { ...d, racks: d.racks.filter(r => r.id !== rackId) };
|
||||||
|
});
|
||||||
|
|
||||||
|
const order = {
|
||||||
|
id: rackId,
|
||||||
|
skuId: rack.skuId,
|
||||||
|
dataCenterId,
|
||||||
|
stage: 'decommission' as const,
|
||||||
|
stageProgress: 0,
|
||||||
|
stageTotal: sku.pipelineTimeTicks.installation,
|
||||||
|
totalCost: 0,
|
||||||
|
repairCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
infrastructure: {
|
||||||
|
...s.infrastructure,
|
||||||
|
dataCenters,
|
||||||
|
rackPipeline: [...s.infrastructure.rackPipeline, order],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
upgradeDataCenter: (dataCenterId, upgrade) => set((s) => {
|
upgradeDataCenter: (dataCenterId, upgrade) => set((s) => {
|
||||||
const dc = s.infrastructure.dataCenters.find(d => d.id === dataCenterId);
|
const dc = s.infrastructure.dataCenters.find(d => d.id === dataCenterId);
|
||||||
if (!dc || dc.status !== 'operational') return s;
|
if (!dc || dc.status !== 'operational') return s;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ function stageTotal(stage: PipelineStage, order: RackOrder): number {
|
|||||||
case 'installation': return timings.installation;
|
case 'installation': return timings.installation;
|
||||||
case 'testing': return timings.testing;
|
case 'testing': return timings.testing;
|
||||||
case 'repair': return RACK_REPAIR_BASE_TICKS;
|
case 'repair': return RACK_REPAIR_BASE_TICKS;
|
||||||
|
case 'decommission': return timings.installation;
|
||||||
default: return 0;
|
default: return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,7 +45,8 @@ function stageSpeed(stage: PipelineStage, engEff: number, opsEff: number): numbe
|
|||||||
switch (stage) {
|
switch (stage) {
|
||||||
case 'manufacturing': return 1 + engEff * 0.1;
|
case 'manufacturing': return 1 + engEff * 0.1;
|
||||||
case 'installation':
|
case 'installation':
|
||||||
case 'testing': return 1 + opsEff * 0.1;
|
case 'testing':
|
||||||
|
case 'decommission': return 1 + opsEff * 0.1;
|
||||||
case 'repair': return 1 + opsEff * 0.05;
|
case 'repair': return 1 + opsEff * 0.05;
|
||||||
default: return 1;
|
default: return 1;
|
||||||
}
|
}
|
||||||
@@ -88,6 +90,16 @@ export function processInfrastructure(state: GameState): InfraTickResult {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (order.stage === 'decommission') {
|
||||||
|
const sku = RACK_SKU_CONFIGS[order.skuId];
|
||||||
|
notifications.push({
|
||||||
|
title: 'Rack Decommissioned',
|
||||||
|
message: `${sku.name} rack has been fully decommissioned.`,
|
||||||
|
type: 'info',
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (order.stage === 'repair') {
|
if (order.stage === 'repair') {
|
||||||
const total = stageTotal('testing', order);
|
const total = stageTotal('testing', order);
|
||||||
rackPipeline.push({
|
rackPipeline.push({
|
||||||
@@ -235,7 +247,7 @@ export function processInfrastructure(state: GameState): InfraTickResult {
|
|||||||
usedPowerKW += sku.powerDrawKW;
|
usedPowerKW += sku.powerDrawKW;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pipelineRacksForDc = rackPipeline.filter(o => o.dataCenterId === dc.id).length;
|
const pipelineRacksForDc = rackPipeline.filter(o => o.dataCenterId === dc.id && o.stage !== 'decommission').length;
|
||||||
const usedSlots = totalInDc + pipelineRacksForDc;
|
const usedSlots = totalInDc + pipelineRacksForDc;
|
||||||
|
|
||||||
const energyCostPerTick = (tierConfig.baseEnergyCostPerTick + usedPowerKW * BASE_ENERGY_COST_PER_FLOP)
|
const energyCostPerTick = (tierConfig.baseEnergyCostPerTick + usedPowerKW * BASE_ENERGY_COST_PER_FLOP)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export type RackSkuId =
|
|||||||
|
|
||||||
export type PipelineStage =
|
export type PipelineStage =
|
||||||
| 'ordered' | 'manufacturing' | 'receiving'
|
| 'ordered' | 'manufacturing' | 'receiving'
|
||||||
| 'installation' | 'testing' | 'repair';
|
| 'installation' | 'testing' | 'repair' | 'decommission';
|
||||||
|
|
||||||
export interface PipelineTimings {
|
export interface PipelineTimings {
|
||||||
manufacturing: number;
|
manufacturing: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user