use std::path::PathBuf;
use std::time::{Duration, Instant};
use shep_core::config::AppConfig;
use shep_core::protocol::{DogSource, ExitInfo, ProcessInfo, SheepConfigView};
use shep_core::status::ProcStatus;
use super::super::app::{App, KeyPress, Msg, RowKey, SettingsRow};
use super::super::tail::{Stream, Tail, TailLine};
use crate::commands::settings::{DogView, ScalarView, SettingField, SettingsSnapshot};
use crate::style::StyleSource;
use super::Scene;
#[track_caller]
pub(super) fn select_id(app: &mut App, id: u32) {
select_row(app, &RowKey::Sheep(id));
}
#[track_caller]
pub(super) fn select_group(app: &mut App, name: &str) {
select_row(app, &RowKey::Group(name.to_string()));
}
#[track_caller]
pub(super) fn select_fold(app: &mut App, name: &str) {
select_row(app, &RowKey::Fold(name.to_string()));
}
#[track_caller]
fn select_row(app: &mut App, key: &RowKey) {
for _ in 0..=app.visible_rows().len() {
if app.selected().as_ref() == Some(key) {
return;
}
let target = app.visible_rows().iter().position(|row| row == key);
let step = match (app.selected_index(), target) {
(Some(current), Some(target)) if target < current => KeyPress::SelectUp,
_ => KeyPress::SelectDown,
};
app.update(Msg::Key(step));
}
panic!("the gallery cannot park its cursor on {key:?}");
}
pub(super) fn poll_twice(
app: &mut App,
anchor: Instant,
mut rows: Vec<ProcessInfo>,
deltas: &[(u32, u64, u64)],
) -> Vec<ProcessInfo> {
for &(id, first, _) in deltas {
if let Some(row) = rows.iter_mut().find(|row| row.id == id) {
row.cpu_ms = Some(first);
}
}
app.update(Msg::Snapshot {
rows: rows.clone(),
at: anchor - Duration::from_secs(2),
});
for &(id, _, second) in deltas {
if let Some(row) = rows.iter_mut().find(|row| row.id == id) {
row.cpu_ms = Some(second);
}
}
app.update(Msg::Snapshot {
rows: rows.clone(),
at: anchor,
});
rows
}
pub(super) fn feed_for(which: Scene) -> Tail {
match which {
Scene::Empty => Tail::default(),
Scene::WithDogs => Tail {
lines: [
"rotated /var/log/api/access.log -> access.log.1",
"compressed access.log.1 (4.2M -> 380K)",
"pruned 2 archives older than 14 days",
"next rotation in 6h",
]
.into_iter()
.map(|text| TailLine {
stream: Stream::Out,
text: text.to_string(),
})
.collect(),
missed_lines: 0,
missed_bytes: 0,
read_bytes: 256,
note: None,
},
Scene::FeedGap => Tail {
lines: (0..30)
.map(|n| TailLine {
stream: Stream::Out,
text: format!("GET /v1/orders/{n} 200 {}ms", 8 + n % 40),
})
.collect(),
missed_lines: 500,
missed_bytes: 4_012_000,
read_bytes: 65_536,
note: None,
},
Scene::FeedMissing => Tail {
lines: Vec::new(),
missed_lines: 0,
missed_bytes: 0,
read_bytes: 0,
note: Some("this sheep has not written a log in this $SHEP_HOME".to_string()),
},
Scene::Bleats => Tail {
lines: [
(Stream::Out, "listening on 0.0.0.0:8080"),
(
Stream::Err,
"WARN connection pool nearing capacity: 47/50 in use",
),
(Stream::Out, "GET /v1/orders 200 12ms"),
(
Stream::Out,
"WARN retrying upstream payment gateway after a timeout, backing off \
500ms before trying again with jitter added so the whole fleet does \
not retry at once",
),
(
Stream::Err,
"INFO shutting down worker 2 for a rolling restart",
),
(Stream::Out, "DEBUG cache warmed 128 keys in 4ms"),
(Stream::Out, "POST /v1/orders 201 88ms"),
(
Stream::Err,
"ERROR failed to write session cache: disk quota exceeded",
),
(Stream::Out, "connection pool: 14/50 in use"),
(
Stream::Out,
"ERROR panic recovered in worker 3: index out of bounds",
),
(Stream::Err, "GET /healthz 200 3ms"),
(Stream::Out, "GET /v1/orders/8821 200 9ms"),
(Stream::Err, "DEBUG flushing metrics buffer"),
(Stream::Out, "POST /v1/orders 500 210ms"),
(
Stream::Err,
"WARN queue depth crossed 200, backpressure engaged",
),
(Stream::Out, "GET /v1/orders/9013 200 7ms"),
]
.into_iter()
.map(|(stream, text)| TailLine {
stream,
text: text.to_string(),
})
.collect(),
missed_lines: 0,
missed_bytes: 0,
read_bytes: 2048,
note: None,
},
_ => Tail {
lines: [
"listening on 0.0.0.0:8080",
"GET /healthz 200 3ms",
"GET /v1/orders 200 44ms",
"POST /v1/orders 201 88ms",
"GET /v1/orders/8821 200 9ms",
"connection pool: 14/50 in use",
]
.into_iter()
.map(|text| TailLine {
stream: Stream::Out,
text: text.to_string(),
})
.collect(),
missed_lines: 0,
missed_bytes: 0,
read_bytes: 512,
note: None,
},
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn sheep(
id: u32,
name: &str,
status: ProcStatus,
pid: Option<u32>,
restarts: u32,
cpu: Option<f32>,
memory: Option<u64>,
fold: Option<&str>,
) -> ProcessInfo {
ProcessInfo::builder(id, name, status)
.pid(pid)
.restarts(restarts)
.uptime_ms(4_512_000 + u64::from(id) * 91_000)
.cpu_percent(cpu)
.memory_bytes(memory)
.fold(fold.map(str::to_string))
.out_file(Some(format!("/home/ada/.shep/logs/{name}-{id}-out.log")))
.err_file(Some(format!("/home/ada/.shep/logs/{name}-{id}-err.log")))
.last_exit(match status {
ProcStatus::Errored | ProcStatus::WaitingRestart => Some(ExitInfo {
code: Some(1),
signal: None,
}),
ProcStatus::Stopped => Some(ExitInfo {
code: None,
signal: Some(15),
}),
ProcStatus::Online | ProcStatus::Starting | ProcStatus::Stopping => None,
})
.build()
}
pub(super) fn sheep_with_ceiling(id: u32, name: &str, memory: u64, ceiling: u64) -> ProcessInfo {
let mut info = sheep(
id,
name,
ProcStatus::Online,
Some(48_300 + id),
0,
Some(2.0),
Some(memory),
Some("edge"),
);
info.max_memory = Some(ceiling);
info
}
pub(super) fn sheep_pane_config_view() -> SheepConfigView {
let mut config = AppConfig {
name: "web".to_string(),
script: "./srv".to_string(),
args: vec!["--port".to_string(), "8080".to_string()],
max_restarts: 32,
instances: 3,
..AppConfig::default()
};
config
.env
.insert("DB_HOST".to_string(), "db.internal".to_string());
config
.env
.insert("LOG_LEVEL".to_string(), "debug".to_string());
SheepConfigView::new(config, Vec::new(), Vec::new())
}
pub(super) fn instance(
id: u32,
name: &str,
slot: u32,
restarts: u32,
cpu: f32,
memory: u64,
uptime_ms: u64,
) -> ProcessInfo {
ProcessInfo::builder(id, name, ProcStatus::Online)
.instance(Some(slot))
.pid(Some(48_400 + id))
.restarts(restarts)
.uptime_ms(uptime_ms)
.cpu_percent(Some(cpu))
.memory_bytes(Some(memory))
.fold(Some("edge".to_string()))
.out_file(Some(format!("/home/ada/.shep/logs/{name}-{id}-out.log")))
.err_file(Some(format!("/home/ada/.shep/logs/{name}-{id}-err.log")))
.build()
}
pub(super) fn restarted_api() -> ProcessInfo {
sheep(
2,
"api",
ProcStatus::Online,
Some(48_299),
2,
Some(7.1),
Some(241 << 20),
Some("edge"),
)
}
pub(super) fn flock_without_api() -> Vec<ProcessInfo> {
vec![
sheep(
0,
"web",
ProcStatus::Online,
Some(48_211),
0,
Some(3.4),
Some(182 << 20),
Some("edge"),
),
sheep(
1,
"web",
ProcStatus::Online,
Some(48_212),
0,
Some(2.9),
Some(178 << 20),
Some("edge"),
),
sheep(
3,
"billing-reconciliation-worker",
ProcStatus::Online,
Some(48_230),
0,
Some(0.8),
Some(96 << 20),
None,
),
sheep(
4,
"cron",
ProcStatus::Online,
Some(48_233),
0,
Some(0.1),
Some(8 << 20),
None,
),
sheep(
5,
"metrics",
ProcStatus::Online,
Some(48_240),
0,
Some(0.4),
Some(11 << 20),
None,
),
]
}
pub(super) fn dog_sheep(
id: u32,
name: &str,
source: DogSource,
handshook: Option<bool>,
) -> ProcessInfo {
ProcessInfo::builder(id, name, ProcStatus::Online)
.pid(Some(90_000 + id))
.dog(Some(source))
.handshook(handshook)
.build()
}
#[track_caller]
pub(super) fn move_settings_cursor_to(app: &mut App, field: SettingField) {
let target = app
.settings()
.expect("the settings screen must already be open")
.rows()
.iter()
.position(|row| *row == SettingsRow::Scalar(field))
.expect("field is one of the six scalar rows Settings::rows always carries");
for _ in 0..target {
app.update(Msg::Key(KeyPress::SelectDown));
}
}
pub(super) fn settings_snapshot_for_gallery() -> SettingsSnapshot {
let config = |value: &str| ScalarView {
value: value.to_string(),
source: StyleSource::Config,
};
let default = |value: &str| ScalarView {
value: value.to_string(),
source: StyleSource::Default,
};
SettingsSnapshot {
log_level: config("warn"),
log_json: default("false"),
socket: config("/home/ada/.shep/run/shep.sock"),
max_cron_sleep: config("30s"),
allow_control: default("false"),
style_level: config("full"),
style_level_in_file: Some("full".to_string()),
dogs: vec![
DogView {
name: "bark".to_string(),
enabled: false,
adopted_path: None,
},
DogView {
name: "metrics".to_string(),
enabled: true,
adopted_path: None,
},
],
}
}
pub(super) fn settings_snapshot_with_dog_drift() -> SettingsSnapshot {
SettingsSnapshot {
dogs: vec![
DogView {
name: "otel".to_string(),
enabled: false,
adopted_path: Some(PathBuf::from("/usr/local/bin/shep-otel")),
},
DogView {
name: "ledger".to_string(),
enabled: true,
adopted_path: Some(PathBuf::from("/opt/ledger/bin/dog")),
},
DogView {
name: "bark".to_string(),
enabled: true,
adopted_path: None,
},
],
..settings_snapshot_for_gallery()
}
}
pub(super) fn edit_pane_config_view() -> SheepConfigView {
let mut config = AppConfig {
name: "api".to_string(),
script: "./api/server.js".to_string(),
args: vec!["--port".to_string(), "8080".to_string()],
max_restarts: 32,
instances: 1,
..AppConfig::default()
};
config
.env
.insert("NODE_ENV".to_string(), "production".to_string());
config
.env
.insert("DATABASE_URL".to_string(), "postgres://db/api".to_string());
SheepConfigView::new(config, Vec::new(), Vec::new())
}
pub(super) fn close_dialog_config_view() -> SheepConfigView {
let mut config = AppConfig {
name: "api".to_string(),
script: "./api/server.js".to_string(),
args: vec!["--port".to_string(), "8080".to_string()],
max_restarts: 32,
instances: 1,
..AppConfig::default()
};
config
.env
.insert("NODE_ENV".to_string(), "production".to_string());
config
.env
.insert("DATABASE_URL".to_string(), "postgres://db/api".to_string());
SheepConfigView::new(config, Vec::new(), vec!["listen_timeout".to_string()])
}