From 09c94cb7e6a6bab22ece37df1f7e2320fa868fe9 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Sat, 16 May 2026 18:46:07 +0200 Subject: [PATCH] refactor(fleet): consolidate palette + is_live, drop renderer-polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator review flagged three drift sources where per-binary code disagreed with the canonical fleet-ui/data layer. This compound PR unifies all three on the shared crates and deletes a stranded library. (a) Palette migration - Per-binary `ios_page_design.rs` modules in fleet-watcher and fleet-waves declared their own `IOS_BLUE/GREEN/RED/ORANGE` (watcher) and `ACCENT/SUCCESS/DANGER/WARNING` (waves) constants using iOS *Light* system values (#007aff, #34c759, #ff3b30, #ff9500). - These dashboards render on a dark TUI surface, so iOS Dark variants are correct. `fleet-ui::palette` already exposed the canonical Dark-mode constants (#0a84ff, #30d158, #ff453a, #ff9f0a). Each binary now `use`s the palette names and aliases them in-place. - Local surface tones (BG / SURFACE / TEXT / *_SOFT) stay in each binary because the mock pages intentionally use a deeper dark surface than the production board. - Extended the `palette_hex_parity` test to lock in every constant the per-binary modules now import via `palette::*` so future drift fails the test rather than the dashboard. (b) Renderer-polish deletion - `rust/fleet-renderer-polish/` was library-only, unadopted, and declared a third copy of the same iOS-named colors (drift source). - `git rm -r`d the whole crate; no workspace crate depended on it (verified via grep). The `fleet-*` glob in workspace `Cargo.toml` drops it automatically. (c) is_live / is_capped unification - `WorkerRow::is_live` was already on the data layer, but `fleet-state/src/ios_page_design.rs::Summary::from_rows` recomputed "capped" inline as `row.five_h_pct >= 100 || matches!(row.state, Some(PaneState::Capped))`. Any other dashboard that wanted the same tally would have re-derived this rule. - Lifted that predicate to `WorkerRow::is_capped(&self) -> bool` with a docstring spelling out the two-signal contract (cap-pool % crossing the line OR pane classifier returning Capped). Migrated fleet-state's Summary to call `row.is_capped()`. Verification - `cargo check --workspace` clean. - `cargo test --workspace --exclude fleet-ui` all green; `cargo test -p fleet-ui --lib` all green (palette hex-parity guard included). - fleet-ui has a pre-existing test-only compile error in `tests/tab_strip_snapshot.rs` referencing the removed `fleet_ui::tab_strip` module (from PR #156's mod-folder split) — untouched by this PR. - TestBackend snapshot output is text-only, so accent-color migration does not affect any snapshot fixture. --- rust/Cargo.lock | 7 - rust/fleet-data/src/fleet.rs | 17 +++ rust/fleet-renderer-polish/Cargo.toml | 12 -- rust/fleet-renderer-polish/src/lib.rs | 154 ---------------------- rust/fleet-state/src/ios_page_design.rs | 5 +- rust/fleet-ui/src/palette.rs | 53 ++++++++ rust/fleet-watcher/src/ios_page_design.rs | 14 +- rust/fleet-waves/src/ios_page_design.rs | 11 +- 8 files changed, 86 insertions(+), 187 deletions(-) delete mode 100644 rust/fleet-renderer-polish/Cargo.toml delete mode 100644 rust/fleet-renderer-polish/src/lib.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 569b6ad..78408d7 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -448,13 +448,6 @@ dependencies = [ "tuirealm", ] -[[package]] -name = "fleet-renderer-polish" -version = "0.0.1" -dependencies = [ - "ratatui", -] - [[package]] name = "fleet-state" version = "0.0.1" diff --git a/rust/fleet-data/src/fleet.rs b/rust/fleet-data/src/fleet.rs index 0952bc5..07f9041 100644 --- a/rust/fleet-data/src/fleet.rs +++ b/rust/fleet-data/src/fleet.rs @@ -76,6 +76,23 @@ impl WorkerRow { pub fn is_in_review(&self) -> bool { matches!(self.state, Some(PaneState::Approval)) } + + /// `true` when this row counts toward the "capped" tally on dashboards. + /// + /// Two independent signals can flip a row to capped: + /// + /// 1. `agent-auth list` reports `5h >= 100%` — the cap-pool number has + /// already crossed the throttle line even before the pane noticed. + /// 2. The pane's scrollback classifier returned [`PaneState::Capped`] — + /// the pane itself printed a 5-hour quota message. + /// + /// The OR keeps either signal alone enough to count the row, which is the + /// rule the per-binary `ios_page_design.rs` modules used to recompute + /// inline. Lifting it here keeps every dashboard's "capped" number agreeing + /// with the data layer instead of drifting per renderer. + pub fn is_capped(&self) -> bool { + self.five_h_pct >= 100 || matches!(self.state, Some(PaneState::Capped)) + } } /// Header summary line: "8 workers · 6 live · 1 in review". diff --git a/rust/fleet-renderer-polish/Cargo.toml b/rust/fleet-renderer-polish/Cargo.toml deleted file mode 100644 index 2feb635..0000000 --- a/rust/fleet-renderer-polish/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "fleet-renderer-polish" -version = "0.0.1" -edition = "2021" -description = "Shared ratatui chrome primitives (rounded blocks, hairline dividers, iOS palette chips, page headers) for the codex-fleet dashboards. Library only; adoption by existing binaries is a separate follow-up." -publish = false - -[lib] -path = "src/lib.rs" - -[dependencies] -ratatui = "0.30" diff --git a/rust/fleet-renderer-polish/src/lib.rs b/rust/fleet-renderer-polish/src/lib.rs deleted file mode 100644 index 9ce88fd..0000000 --- a/rust/fleet-renderer-polish/src/lib.rs +++ /dev/null @@ -1,154 +0,0 @@ -//! Shared ratatui chrome primitives for codex-fleet dashboards. -//! -//! This crate exposes a small library of reusable visual building blocks -//! (rounded blocks, hairline dividers, iOS-style colored chips, two-line -//! page headers) so individual fleet binaries can present a consistent -//! iOS-flavored look without each crate re-implementing the same chrome. -//! -//! The crate is intentionally library-only. Adoption by the existing -//! `fleet-*` binaries is a follow-up; this lane only ships the primitives. - -use ratatui::style::{Color, Style}; -use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, BorderType, Borders, Padding}; - -/// A `Block` with rounded corners and 1-cell internal padding, titled with -/// `title`. Suitable as the outer chrome for cards/panels. -/// -/// # Example -/// -/// ```ignore -/// use fleet_renderer_polish::rounded_corner_block; -/// let block = rounded_corner_block("Fleet status"); -/// // render `block` as the outer chrome of any widget area. -/// ``` -pub fn rounded_corner_block(title: &str) -> Block<'_> { - Block::default() - .title(title) - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .padding(Padding::uniform(1)) -} - -/// A single-line hairline divider made of `─` characters, styled with the -/// iOS-flavored separator gray `#3A3A3C`. The returned `Line` owns its -/// string buffer, so it can outlive borrowed inputs. -/// -/// # Example -/// -/// ```ignore -/// use fleet_renderer_polish::hairline_divider; -/// let line = hairline_divider(40); -/// // place `line` between two sections to get a subtle visual break. -/// ``` -pub fn hairline_divider(width: u16) -> Line<'static> { - let count = width as usize; - let mut buf = String::with_capacity(count * 3); - for _ in 0..count { - buf.push('─'); - } - Line::from(Span::styled( - buf, - Style::default().fg(Color::Rgb(0x3A, 0x3A, 0x3C)), - )) -} - -/// iOS system palette colors used by the chrome primitives. -/// -/// Each variant maps to the canonical iOS system color via -/// [`IosColor::ios_rgb`]. -/// -/// # Example -/// -/// ```ignore -/// use fleet_renderer_polish::IosColor; -/// let color = IosColor::Green.ios_rgb(); -/// // pass `color` to any ratatui Style::default().fg(...) call. -/// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum IosColor { - Green, - Blue, - Yellow, - Red, - Gray, -} - -impl IosColor { - /// Returns the ratatui [`Color`] corresponding to the iOS system palette - /// entry for this variant. - /// - /// # Example - /// - /// ```ignore - /// use fleet_renderer_polish::IosColor; - /// assert_eq!(IosColor::Blue.ios_rgb(), IosColor::Blue.ios_rgb()); - /// ``` - pub fn ios_rgb(&self) -> Color { - match self { - // iOS systemGreen - IosColor::Green => Color::Rgb(0x34, 0xC7, 0x59), - // iOS systemBlue - IosColor::Blue => Color::Rgb(0x00, 0x7A, 0xFF), - // iOS systemYellow - IosColor::Yellow => Color::Rgb(0xFF, 0xCC, 0x00), - // iOS systemRed - IosColor::Red => Color::Rgb(0xFF, 0x3B, 0x30), - // iOS systemGray - IosColor::Gray => Color::Rgb(0x8E, 0x8E, 0x93), - } - } -} - -/// A foreground-only iOS status chip, e.g. ` LIVE `, ` IDLE `, ` ERR `. -/// -/// The chip is rendered as a single padded `Span` whose foreground color -/// comes from [`IosColor::ios_rgb`]. No background fill is applied so the -/// chip blends with whatever surface it lands on. -/// -/// # Example -/// -/// ```ignore -/// use fleet_renderer_polish::{ios_status_chip, IosColor}; -/// let chip = ios_status_chip("LIVE", IosColor::Green); -/// // append `chip` to any ratatui Line to get a colored status pill. -/// ``` -pub fn ios_status_chip<'a>(label: &'a str, color: IosColor) -> Span<'a> { - Span::styled( - format!(" {label} "), - Style::default().fg(color.ios_rgb()), - ) -} - -/// A two-line iOS-style page header: a bold title line followed by an -/// accent underline rendered in the chosen [`IosColor`]. -/// -/// The underline matches the visible length of `title`, so the accent bar -/// hugs the title rather than the full panel width. -/// -/// # Example -/// -/// ```ignore -/// use fleet_renderer_polish::{page_header_with_accent, IosColor}; -/// let lines = page_header_with_accent("Fleet", IosColor::Blue); -/// // render `lines` with a Paragraph as the top of a page. -/// ``` -pub fn page_header_with_accent(title: &str, accent: IosColor) -> Vec> { - let underline_len = title.chars().count(); - let mut underline = String::with_capacity(underline_len * 3); - for _ in 0..underline_len { - underline.push('─'); - } - vec![ - Line::from(Span::styled( - title, - Style::default() - .fg(Color::White) - .add_modifier(ratatui::style::Modifier::BOLD), - )), - Line::from(Span::styled( - underline, - Style::default().fg(accent.ios_rgb()), - )), - ] -} diff --git a/rust/fleet-state/src/ios_page_design.rs b/rust/fleet-state/src/ios_page_design.rs index a3ca2d6..51d238f 100644 --- a/rust/fleet-state/src/ios_page_design.rs +++ b/rust/fleet-state/src/ios_page_design.rs @@ -237,10 +237,7 @@ impl Summary { Self { accounts: rows.len(), live: rows.iter().filter(|row| row.is_live()).count(), - capped: rows - .iter() - .filter(|row| row.five_h_pct >= 100 || matches!(row.state, Some(PaneState::Capped))) - .count(), + capped: rows.iter().filter(|row| row.is_capped()).count(), review: rows .iter() .filter(|row| matches!(row.state, Some(PaneState::Approval))) diff --git a/rust/fleet-ui/src/palette.rs b/rust/fleet-ui/src/palette.rs index 5e1a38e..c74b698 100644 --- a/rust/fleet-ui/src/palette.rs +++ b/rust/fleet-ui/src/palette.rs @@ -125,5 +125,58 @@ mod tests { Color::Rgb(0x26, 0x26, 0x28), "IOS_ROW_BG_DARK must be #262628" ); + // Extended coverage: every constant that per-binary `ios_page_design.rs` + // modules import via `palette::*`. If any drifts the migration loses + // its canonical anchor. + assert_eq!( + IOS_FG_MUTED, + Color::Rgb(0xa0, 0xa0, 0xaa), + "IOS_FG_MUTED must be #a0a0aa" + ); + assert_eq!( + IOS_FG_FAINT, + Color::Rgb(0x6e, 0x6e, 0x78), + "IOS_FG_FAINT must be #6e6e78" + ); + assert_eq!( + IOS_BG_GLASS, + Color::Rgb(0x26, 0x26, 0x28), + "IOS_BG_GLASS must be #262628" + ); + assert_eq!( + IOS_HAIRLINE, + Color::Rgb(0x3c, 0x3c, 0x41), + "IOS_HAIRLINE must be #3c3c41" + ); + assert_eq!( + IOS_HAIRLINE_STRONG, + Color::Rgb(0x55, 0x55, 0x5a), + "IOS_HAIRLINE_STRONG must be #55555a" + ); + assert_eq!( + IOS_CHIP_BG, + Color::Rgb(0x36, 0x36, 0x3a), + "IOS_CHIP_BG must be #36363a" + ); + assert_eq!( + IOS_CARD_BG, + Color::Rgb(0x2c, 0x2c, 0x30), + "IOS_CARD_BG must be #2c2c30" + ); + assert_eq!( + IOS_ICON_CHIP, + Color::Rgb(0x46, 0x46, 0x4c), + "IOS_ICON_CHIP must be #46464c" + ); + assert_eq!( + IOS_TINT_DARK, + Color::Rgb(0x07, 0x64, 0xdc), + "IOS_TINT_DARK must be #0764dc" + ); + assert_eq!( + IOS_TINT_SUB, + Color::Rgb(0xd2, 0xe0, 0xff), + "IOS_TINT_SUB must be #d2e0ff" + ); } } diff --git a/rust/fleet-watcher/src/ios_page_design.rs b/rust/fleet-watcher/src/ios_page_design.rs index f6be07c..cd54c59 100644 --- a/rust/fleet-watcher/src/ios_page_design.rs +++ b/rust/fleet-watcher/src/ios_page_design.rs @@ -1,3 +1,6 @@ +use fleet_ui::palette::{ + IOS_DESTRUCTIVE as IOS_RED, IOS_GREEN, IOS_ORANGE, IOS_TINT as IOS_BLUE, +}; use ratatui::{ buffer::Buffer, layout::{Constraint, Direction, Layout, Margin, Rect}, @@ -6,6 +9,10 @@ use ratatui::{ widgets::{Block, BorderType, Borders, Paragraph, Widget, Wrap}, }; +// Local surface tones — this mock dashboard runs on a deeper-than-`IOS_BG_SOLID` +// dark surface and keeps its own greyscale ramp so the panels read as a +// separate visual context from the production fleet board. Only the iOS-named +// accent colors above ride the canonical `fleet-ui::palette` values. const BG: Color = Color::Rgb(0x10, 0x12, 0x16); const SURFACE: Color = Color::Rgb(0x1a, 0x1d, 0x24); const SURFACE_ALT: Color = Color::Rgb(0x20, 0x24, 0x2d); @@ -15,11 +22,8 @@ const TEXT: Color = Color::Rgb(0xf3, 0xf5, 0xf8); const MUTED: Color = Color::Rgb(0xa7, 0xae, 0xbb); const FAINT: Color = Color::Rgb(0x78, 0x80, 0x8e); -const IOS_BLUE: Color = Color::Rgb(0x00, 0x7a, 0xff); -const IOS_GREEN: Color = Color::Rgb(0x34, 0xc7, 0x59); -const IOS_RED: Color = Color::Rgb(0xff, 0x3b, 0x30); -const IOS_ORANGE: Color = Color::Rgb(0xff, 0x95, 0x00); - +// Tinted soft-fill washes derived from the accent colors — kept local because +// they are intentionally dark muted bg tints not present in the canonical palette. const BLUE_SOFT: Color = Color::Rgb(0x0f, 0x24, 0x42); const ORANGE_SOFT: Color = Color::Rgb(0x35, 0x27, 0x11); const RED_SOFT: Color = Color::Rgb(0x37, 0x16, 0x14); diff --git a/rust/fleet-waves/src/ios_page_design.rs b/rust/fleet-waves/src/ios_page_design.rs index 1226d8e..7698204 100644 --- a/rust/fleet-waves/src/ios_page_design.rs +++ b/rust/fleet-waves/src/ios_page_design.rs @@ -1,3 +1,6 @@ +use fleet_ui::palette::{ + IOS_DESTRUCTIVE as DANGER, IOS_GREEN as SUCCESS, IOS_ORANGE as WARNING, IOS_TINT as ACCENT, +}; use ratatui::{ buffer::Buffer, layout::{Constraint, Direction, Layout, Margin, Rect}, @@ -6,6 +9,9 @@ use ratatui::{ widgets::{Block, BorderType, Borders, Paragraph, Widget}, }; +// Local surface tones — the wave timeline runs on its own deeper-than- +// `IOS_BG_SOLID` ramp so the timeline panel reads as a distinct context. +// Only the iOS-named accent colors above ride canonical `fleet-ui::palette`. const BG: Color = Color::Rgb(14, 17, 24); const SURFACE: Color = Color::Rgb(22, 26, 36); const SURFACE_ELEVATED: Color = Color::Rgb(30, 35, 47); @@ -16,11 +22,6 @@ const MUTED: Color = Color::Rgb(163, 171, 188); const FAINT: Color = Color::Rgb(102, 111, 130); const DARK_TEXT: Color = Color::Rgb(13, 15, 20); -const ACCENT: Color = Color::Rgb(0, 122, 255); -const SUCCESS: Color = Color::Rgb(52, 199, 89); -const DANGER: Color = Color::Rgb(255, 59, 48); -const WARNING: Color = Color::Rgb(255, 149, 0); - const CHIP_LABEL_WIDTH: u16 = 7; const CHIP_WIDTH: u16 = 13;