Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
277 changes: 277 additions & 0 deletions dag/gunbc/fleet_hardware_standing_panel.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
module gunbc.fleet_hardware_standing_panel

import std.types { String, NonEmptyStr, Bool, List, Int }
import std.markup { Fragment }
import gunbc.site.markup { txt, el_class }
import extdeps.languages.css.values { css_ch, css_length_wire }
import product.placement_supply { HostIdentity }
import product.installed_bom_reconcile {
DimmReconcileVerdict, ProcessorReconcileVerdict,
DimmPopulationConfirmed, DimmPopulationRefused,
DimmObservationHostMismatch, DimmPopulationUnconfirmed,
ProcessorModelConfirmed, ProcessorModelRefused,
ProcessorObservationHostMismatch, ProcessorModelUnconfirmed,
DimmCountMatches, DimmCountDiffers,
}
import gunbc.fleet_physical_inventory { fleet_dimm_verdicts, fleet_processor_verdicts }

// WHAT THE FLEET IS BELIEVED TO CONTAIN, BESIDE WHAT WAS ACTUALLY READ OFF IT.
//
// The inventory already reconciles expectation against observation per host and produces a typed
// verdict; nothing rendered it, so the disagreement was reachable only by reading .dag source. That
// is the gap this closes: the operator can see committed slot widths on the capacity panel, but the
// hardware those widths are DERIVED FROM had no view at all.
//
// The concrete cost of that, already paid: srv2's 64 GiB DIMM upgrade was modeled while the install
// failed training and was reverted, so the tree asserted a memory population the machine did not
// have, and the slot widths computed from it were wrong on the one host that differed. The verdict
// existed the whole time and said so. Nobody could see it.
data fleet_hardware_standing_panel_note: String = "Daily-workspace panel over gunbc.fleet_physical_inventory's per-host reconcile verdicts. Renders expectation-versus-observation standing for memory and processor; reads the verdicts directly rather than re-deriving them."

// THREE STANDINGS, NOT TWO, AND THE THIRD IS THE ONE THE PANEL EXISTS FOR.
//
// Confirmed and Refused are the obvious pair. Unconfirmed is separate and is NOT a soft refusal: it
// says no reading was taken, so there is nothing to disagree with. Collapsing it into Confirmed
// asserts hardware nobody looked at. Collapsing it into Refused reports a contradiction that did
// not happen and sends the operator to inspect a machine that is fine.
//
// Their REMEDIES are what makes the distinction load-bearing, and they share nothing: a refusal
// means go look at that host's DIMMs, an unconfirmed means go RUN a reading, and a host mismatch
// means the observation was filed against the wrong host and neither machine has been assessed.
// This is DESIGN's not-applicable-versus-malformed conflation, on the axis where it costs the most:
// every processor row in the fleet is Unconfirmed today, so a panel that collapsed the third state
// would render four confirmations of a fact no instrument has ever measured.
type HardwareStanding
= HardwareConfirmed
| HardwareRefused
| HardwareUnread
| HardwareMisfiled

fn hardware_standing_label(standing: HardwareStanding) -> String {
match standing {
HardwareConfirmed => "confirmed"
HardwareRefused => "REFUSED"
HardwareUnread => "unread"
HardwareMisfiled => "MISFILED"
}
}

fn hardware_standing_class(standing: HardwareStanding) -> String {
match standing {
HardwareConfirmed => "hardware-cell hardware-confirmed"
HardwareRefused => "hardware-cell hardware-refused"
HardwareUnread => "hardware-cell hardware-unread"
HardwareMisfiled => "hardware-cell hardware-misfiled"
}
}

fn dimm_standing(verdict: DimmReconcileVerdict) -> HardwareStanding {
match verdict {
DimmPopulationConfirmed { host: _, stick_count: _ } => HardwareConfirmed
DimmPopulationRefused { host: _, discrepancies: _, count: _ } => HardwareRefused
DimmObservationHostMismatch { expectation_host: _, observation_host: _ } => HardwareMisfiled
DimmPopulationUnconfirmed { host: _, cause: _ } => HardwareUnread
}
}

fn processor_standing(verdict: ProcessorReconcileVerdict) -> HardwareStanding {
match verdict {
ProcessorModelConfirmed { host: _, model_number: _ } => HardwareConfirmed
ProcessorModelRefused { host: _, expected: _, observed: _ } => HardwareRefused
ProcessorObservationHostMismatch { expectation_host: _, observation_host: _ } => HardwareMisfiled
ProcessorModelUnconfirmed { host: _, cause: _ } => HardwareUnread
}
}

fn dimm_verdict_host(verdict: DimmReconcileVerdict) -> HostIdentity {
match verdict {
DimmPopulationConfirmed { host: h, stick_count: _ } => h
DimmPopulationRefused { host: h, discrepancies: _, count: _ } => h
DimmObservationHostMismatch { expectation_host: h, observation_host: _ } => h
DimmPopulationUnconfirmed { host: h, cause: _ } => h
}
}

fn processor_verdict_host(verdict: ProcessorReconcileVerdict) -> HostIdentity {
match verdict {
ProcessorModelConfirmed { host: h, model_number: _ } => h
ProcessorModelRefused { host: h, expected: _, observed: _ } => h
ProcessorObservationHostMismatch { expectation_host: h, observation_host: _ } => h
ProcessorModelUnconfirmed { host: h, cause: _ } => h
}
}

// THE DETAIL LINE CARRIES WHAT THE VERDICT FOUND, BECAUSE A STANDING WORD IS UNACTIONABLE.
//
// "REFUSED" tells an operator that something is wrong and nothing about where to go. The count
// mismatch and the discrepancy tally are the two independent axes the verdict already separates --
// a population can hold the right count of the wrong part, or the wrong count of the right one --
// so both are rendered rather than folded into one number.
fn dimm_detail(verdict: DimmReconcileVerdict) -> String {
match verdict {
DimmPopulationConfirmed { host: _, stick_count: n } =>
join([to_string(n), " sticks as expected"], "")
DimmPopulationRefused { host: _, discrepancies: d, count: c } =>
join([
match c {
DimmCountMatches { count: n } => join([to_string(n), " sticks"], "")
DimmCountDiffers { expected: e, observed: o } =>
join(["expected ", to_string(e), " sticks, read ", to_string(o)], "")
},
" · ", to_string(d |> count), " slot discrepancies",
], "")
DimmObservationHostMismatch { expectation_host: e, observation_host: o } =>
join(["reading filed against ", o as String, ", expected ", e as String], "")
DimmPopulationUnconfirmed { host: _, cause: c } => c as String
}
}

fn processor_detail(verdict: ProcessorReconcileVerdict) -> String {
match verdict {
ProcessorModelConfirmed { host: _, model_number: m } => m as String
ProcessorModelRefused { host: _, expected: e, observed: o } =>
join(["expected ", e as String, ", read ", o as String], "")
ProcessorObservationHostMismatch { expectation_host: e, observation_host: o } =>
join(["reading filed against ", o as String, ", expected ", e as String], "")
ProcessorModelUnconfirmed { host: _, cause: c } => c as String
}
}

fn hardware_axis_cell(standing: HardwareStanding, detail: String) -> List<Fragment> {
[
el_class("span", hardware_standing_class(standing: standing), [
txt(hardware_standing_label(standing: standing)),
]),
el_class("span", "hardware-detail", [txt(detail)]),
]
}

fn dimm_row(verdict: DimmReconcileVerdict) -> Fragment {
el_class("div", "hardware-row", concat(
[el_class("span", "hardware-subject", [txt(dimm_verdict_host(verdict: verdict) as String)]),
el_class("span", "hardware-axis", [txt(hardware_axis_labels().first())])],
hardware_axis_cell(
standing: dimm_standing(verdict: verdict),
detail: dimm_detail(verdict: verdict),
),
))
}

fn processor_row(verdict: ProcessorReconcileVerdict) -> Fragment {
el_class("div", "hardware-row", concat(
[el_class("span", "hardware-subject", [txt(processor_verdict_host(verdict: verdict) as String)]),
el_class("span", "hardware-axis", [txt(hardware_axis_labels().skip(n: 1).first())])],
hardware_axis_cell(
standing: processor_standing(verdict: verdict),
detail: processor_detail(verdict: verdict),
),
))
}

fn fleet_dimm_standings() -> List<HardwareStanding> {
map(fleet_dimm_verdicts, v => dimm_standing(verdict: v))
}

fn fleet_processor_standings() -> List<HardwareStanding> {
map(fleet_processor_verdicts, v => processor_standing(verdict: v))
}

fn standing_count(standings: List<HardwareStanding>, wanted: HardwareStanding) -> Int {
filter(standings, s => s == wanted) |> count
}

// THE SUMMARY REPORTS CONFIRMED OVER TOTAL PER AXIS, NEVER A SINGLE FLEET FRACTION.
//
// One number across both axes would today read "4 of 8 confirmed", which is arithmetically true and
// tells the operator nothing: the memory axis is fully read and the processor axis has never been
// measured once. Those are not two halves of one shortfall, they are a solved problem beside an
// unstarted one, and the remedies have nothing in common.
fn hardware_axis_summary(axis: String, standings: List<HardwareStanding>) -> String {
join([
to_string(standing_count(standings: standings, wanted: HardwareConfirmed)),
" of ", to_string(standings |> count), " ", axis, " confirmed",
], "")
}

fn fleet_hardware_summary_line() -> String {
join([
hardware_axis_summary(axis: "memory", standings: fleet_dimm_standings()),
" · ",
hardware_axis_summary(axis: "processor", standings: fleet_processor_standings()),
], "")
}

// AN OUTSTANDING LINE APPEARS ONLY WHEN SOMETHING IS OUTSTANDING.
//
// A standing "0 refused" row is noise on every ordinary day and trains the reader to skip the line
// on the day it finally carries a number. Refused and misfiled are counted together here because
// both mean a host's declared hardware cannot be trusted right now; they stay separately labelled
// in the rows, where the differing remedy is actionable.
fn fleet_hardware_outstanding_line() -> String? {
let all = concat(fleet_dimm_standings(), fleet_processor_standings())
let bad = standing_count(standings: all, wanted: HardwareRefused)
+ standing_count(standings: all, wanted: HardwareMisfiled)
if bad > 0 {
Present {
value: join([to_string(bad), " host-axes contradict their declared hardware"], ""),
}
} else {
none
}
}

fn fleet_hardware_outstanding_nodes() -> List<Fragment> {
match fleet_hardware_outstanding_line() {
Present { value: line } => [el_class("div", "hardware-outstanding", [txt(line)])]
Absent => []
}
}

data hardware_column_ch_gutter: Int = 2

fn hardware_longest_len(labels: List<String>) -> Int {
fold(labels, init: 0, f: (acc, l) =>
if string_length(s: l) > acc { string_length(s: l) } else { acc })
}

// THE AXIS COLUMN RESERVES THE WIDEST AXIS WORD, NOT THE WIDEST HOSTNAME (review 55065).
//
// It was taking hardware_subject_reserved_width -- copied from the column beside it -- so the
// column carrying "memory" and "processor" was reserved to the width of the longest HOST LABEL.
// The two are unrelated populations that happen to be adjacent, and the derivation was reading the
// wrong one; today that is merely wrong-looking, and it would silently truncate or over-reserve the
// moment either population moved. The axis words now come from one list that both the rows and the
// reservation read, so a third axis widens the column by derivation rather than by anyone noticing.
fn hardware_axis_labels() -> List<String> { ["memory", "processor"] }

fn hardware_axis_reserved_width() -> String {
let widest = hardware_longest_len(labels: hardware_axis_labels()) + hardware_column_ch_gutter
css_length_wire(l: css_ch(n: widest))
}

fn hardware_subject_reserved_width() -> String {
let labels = map(fleet_dimm_verdicts, v => dimm_verdict_host(verdict: v) as String)
let widest = hardware_longest_len(labels: labels) + hardware_column_ch_gutter
css_length_wire(l: css_ch(n: widest))
}

fn hardware_standing_reserved_width() -> String {
let all = [HardwareConfirmed, HardwareRefused, HardwareUnread, HardwareMisfiled]
let labels = map(all, s => hardware_standing_label(standing: s))
let widest = hardware_longest_len(labels: labels) + hardware_column_ch_gutter
css_length_wire(l: css_ch(n: widest))
}

fn fleet_hardware_standing_panel() -> Fragment {
el_class("section", "fleet-hardware-standing", concat(
concat(
[el_class("h2", "hardware-heading", [txt("Fleet hardware standing")]),
el_class("div", "hardware-summary", [txt(fleet_hardware_summary_line())])],
fleet_hardware_outstanding_nodes(),
),
concat(
map(fleet_dimm_verdicts, v => dimm_row(verdict: v)),
map(fleet_processor_verdicts, v => processor_row(verdict: v)),
),
))
}
5 changes: 3 additions & 2 deletions dag/gunbc/roadmap_page.dag
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import gunbc.roadmap_model {
}
import gunbc.roadmap_status { line_ticket }
import gunbc.roadmap_document { RoadmapDocument, RoadmapSection }
import gunbc.fleet_hardware_standing_panel { fleet_hardware_standing_panel }
import gunbc.roadmap_altitude { section_counts, section_counts_wire, statuses_all_routine, SectionCounts }
import gunbc.roadmap_authority {
roadmap_authority, authored_merged_prs,
Expand Down Expand Up @@ -850,9 +851,9 @@ fn roadmap_daily_workspace_page_impl_for_accepted(
]),
el("body", [
roadmap_header(page: "daily workspace"),
el_class("main", "daily-workspace", concat(main_prefix, concat(program_strip, concat(
el_class("main", "daily-workspace", concat(main_prefix, concat([fleet_hardware_standing_panel()], concat(program_strip, concat(
workspace_observation_banner(observation: ws.observation),
[ workspace_markup(ws: ws) ])))),
[ workspace_markup(ws: ws)]))))),
dispatch_client_script(),
]),
],
Expand Down
66 changes: 66 additions & 0 deletions dag/gunbc/roadmap_style.dag
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import gunbc.design.state_response {
state_var_name, state_var_ref, state_rest_decl, state_paint_rules,
RestBinding, StateClassBinding,
}
import gunbc.fleet_hardware_standing_panel {
hardware_subject_reserved_width, hardware_axis_reserved_width,
hardware_standing_reserved_width,
}
import gunbc.roadmap_component {
dispatch_reserved_width, theme_toggle_class, theme_toggle_reserved_width,
sound_toggle_class, sound_toggle_reserved_width,
Expand Down Expand Up @@ -486,6 +490,68 @@ fn roadmap_instance_rules_head() -> List<BuildRule> {
decl(prop: FontStyle, value: "italic"),
decl(prop: FontSize, value: scale_var(t: text_12)),
] },
StaticRule { selector: ClassSelector { name: "fleet-hardware-standing" }, decls: [
role_decl(prop: Background, r: SurfaceRole),
decl(prop: Border, value: scale_border(w: border_1, color_var: "var(--border)")),
decl(prop: BorderRadius, value: scale_var(t: radius_3)),
decl(prop: Padding, value: scale_pair(a: space_12, b: space_16)),
] },
StaticRule { selector: ClassSelector { name: "hardware-heading" }, decls: [
role_decl(prop: Color, r: TextRole),
decl(prop: FontSize, value: scale_var(t: text_13)),
decl(prop: FontWeight, value: "600"),
decl(prop: MarginBottom, value: scale_var(t: space_8)),
] },
StaticRule { selector: ClassSelector { name: "hardware-summary" }, decls: [
role_decl(prop: Color, r: TextRole),
decl(prop: FontSize, value: scale_var(t: text_13)),
decl(prop: FontFamily, value: scale_var(t: font_mono)),
decl(prop: MarginBottom, value: scale_var(t: space_8)),
] },
StaticRule { selector: ClassSelector { name: "hardware-outstanding" }, decls: [
decl(prop: Color, value: "var(--band-loud)"),
decl(prop: FontSize, value: scale_var(t: text_13)),
decl(prop: FontFamily, value: scale_var(t: font_mono)),
decl(prop: FontWeight, value: "600"),
decl(prop: MarginBottom, value: scale_var(t: space_8)),
] },
StaticRule { selector: ClassSelector { name: "hardware-row" }, decls: [
decl(prop: Display, value: "flex"),
decl(prop: AlignItems, value: "center"),
decl(prop: Gap, value: scale_var(t: space_12)),
decl(prop: FontSize, value: scale_var(t: text_12)),
decl(prop: FontFamily, value: scale_var(t: font_mono)),
decl(prop: Padding, value: scale_pair(a: space_1, b: space_4)),
] },
StaticRule { selector: ClassSelector { name: "hardware-subject" }, decls: [
role_decl(prop: Color, r: TextRole),
decl(prop: FontWeight, value: "600"),
decl(prop: MinWidth, value: hardware_subject_reserved_width()),
] },
StaticRule { selector: ClassSelector { name: "hardware-axis" }, decls: [
role_decl(prop: Color, r: TextDimRole),
decl(prop: MinWidth, value: hardware_axis_reserved_width()),
] },
StaticRule { selector: ClassSelector { name: "hardware-cell" }, decls: [
decl(prop: FontWeight, value: "600"),
decl(prop: MinWidth, value: hardware_standing_reserved_width()),
] },
StaticRule { selector: ClassSelector { name: "hardware-confirmed" }, decls: [
role_decl(prop: Color, r: TextDimRole),
] },
StaticRule { selector: ClassSelector { name: "hardware-refused" }, decls: [
decl(prop: Color, value: "var(--band-loud)"),
] },
StaticRule { selector: ClassSelector { name: "hardware-misfiled" }, decls: [
decl(prop: Color, value: "var(--band-loud)"),
] },
StaticRule { selector: ClassSelector { name: "hardware-unread" }, decls: [
role_decl(prop: Color, r: TextDimRole),
decl(prop: FontStyle, value: "italic"),
] },
StaticRule { selector: ClassSelector { name: "hardware-detail" }, decls: [
role_decl(prop: Color, r: TextDimRole),
] },
StaticRule { selector: ClassSelector { name: "daily-workspace" }, decls: [
decl(prop: Padding, value: scale_pair(a: space_16, b: space_24)),
decl(prop: MaxWidth, value: scale_var(t: measure_page)),
Expand Down
Loading
Loading