Gunnar

mikronova/kyokkou

src/rack_topology.rs

use crate::common::{Scene, ValidationStatus, render_button, show_validation};

#[derive(Debug, Clone, PartialEq)]
pub enum UnitType {
    Server1U,
    Server2U,
    Server4U,
    Switch,
    PatchPanel,
    Pdu,
    BlankingPanel,
}

#[derive(Debug, Clone)]
pub struct RackUnit {
    pub slot: u8,           // 1-based U position from bottom
    pub size: u8,           // how many U it occupies
    pub kind: UnitType,
    pub label: String,
    pub cpu_load: f32,      // 0.0–1.0
    pub net_utilisation: f32,
    pub power_draw_w: f32,
}

pub struct RackTopology {
    pub rack_u_total: u8,
    pub units: Vec<RackUnit>,
    pub pdu_capacity_w: f32,
    pub import_source: String,
}

impl Default for RackTopology {
    fn default() -> Self {
        Self {
            rack_u_total: 42,
            units: vec![
                RackUnit { slot: 1, size: 1, kind: UnitType::Switch,   label: "core-sw-01".into(), cpu_load: 0.2, net_utilisation: 0.6, power_draw_w: 80.0 },
                RackUnit { slot: 2, size: 2, kind: UnitType::Server2U, label: "srv-01".into(),     cpu_load: 0.7, net_utilisation: 0.4, power_draw_w: 350.0 },
                RackUnit { slot: 4, size: 1, kind: UnitType::Server1U, label: "srv-02".into(),     cpu_load: 0.3, net_utilisation: 0.2, power_draw_w: 180.0 },
            ],
            pdu_capacity_w: 3200.0,
            import_source: String::new(),
        }
    }
}

impl RackTopology {
    fn total_power_draw(&self) -> f32 {
        self.units.iter().map(|u| u.power_draw_w).sum()
    }

    fn slot_conflicts(&self) -> Vec<(u8, u8)> {
        let mut occupied: Vec<(u8, u8)> = vec![];
        let mut conflicts = vec![];
        for unit in &self.units {
            for u in unit.slot..unit.slot + unit.size {
                if occupied.iter().any(|(s, _)| *s == u) {
                    conflicts.push((unit.slot, u));
                }
                occupied.push((u, unit.slot));
            }
        }
        conflicts
    }
}

impl Scene for RackTopology {
    fn name(&self) -> &str {
        "Rack topology"
    }

    fn validate(&self) -> Vec<ValidationStatus> {
        let mut v = vec![];

        let conflicts = self.slot_conflicts();
        if !conflicts.is_empty() {
            v.push(ValidationStatus::Error(format!(
                "{} U slot conflict(s) — cannot render",
                conflicts.len()
            )));
        }

        let draw = self.total_power_draw();
        if draw > self.pdu_capacity_w {
            v.push(ValidationStatus::Error(format!(
                "Power budget exceeded: {draw:.0} W > {:.0} W PDU capacity",
                self.pdu_capacity_w
            )));
        } else if draw > self.pdu_capacity_w * 0.9 {
            v.push(ValidationStatus::Warning(format!(
                "Power draw at {:.0}% of PDU capacity",
                draw / self.pdu_capacity_w * 100.0
            )));
        }

        let cable_count = self.units.len(); // placeholder
        if cable_count > 100 {
            v.push(ValidationStatus::Warning(
                "Cable count > 100 — runs will be batched into trunks".into(),
            ));
        }

        if self.units.is_empty() {
            v.push(ValidationStatus::Error("Rack is empty".into()));
        }

        if v.is_empty() {
            v.push(ValidationStatus::Ok);
        }
        v
    }

    fn show(&mut self, ui: &mut egui::Ui) -> Option<String> {
        ui.heading("Rack topology");
        ui.separator();

        ui.horizontal(|ui| {
            ui.label("Rack size (U):");
            ui.add(egui::Slider::new(&mut self.rack_u_total, 4..=52));
        });
        ui.horizontal(|ui| {
            ui.label("PDU capacity (W):");
            ui.add(egui::Slider::new(&mut self.pdu_capacity_w, 1000.0..=20000.0).step_by(100.0));
        });

        ui.horizontal(|ui| {
            ui.label("Import (Netbox JSON / CSV):");
            ui.text_edit_singleline(&mut self.import_source);
            if ui.button("Load").clicked() {
                // TODO: parse import_source and populate units
            }
        });

        ui.separator();
        ui.label(format!(
            "{} units  |  {:.0} W / {:.0} W  ({:.0}%)",
            self.units.len(),
            self.total_power_draw(),
            self.pdu_capacity_w,
            self.total_power_draw() / self.pdu_capacity_w * 100.0,
        ));

        // Rack diagram — side-on U view
        egui::ScrollArea::vertical().max_height(300.0).show(ui, |ui| {
            egui::Grid::new("rack_grid").striped(true).show(ui, |ui| {
                ui.label("U");
                ui.label("Type");
                ui.label("Label");
                ui.label("CPU");
                ui.label("Net");
                ui.label("Power W");
                ui.end_row();

                for unit in &self.units {
                    ui.label(format!("{}–{}", unit.slot, unit.slot + unit.size - 1));
                    ui.label(format!("{:?}", unit.kind));
                    ui.label(&unit.label);
                    ui.add(egui::ProgressBar::new(unit.cpu_load).desired_width(60.0));
                    ui.add(egui::ProgressBar::new(unit.net_utilisation).desired_width(60.0));
                    ui.label(format!("{:.0}", unit.power_draw_w));
                    ui.end_row();
                }
            });
        });

        ui.separator();
        let statuses = self.validate();
        show_validation(ui, &statuses);
        ui.separator();
        if render_button(ui, self.is_renderable()) {
            // TODO: to_yaml() for rack topology
        }
        None
    }
}