use crate::ui::{self, Tone};
#[derive(Debug, Clone)]
pub struct DeviceEntry {
pub name: String,
pub tier: DeviceTier,
pub online: bool,
pub caps_summary: String, pub countdown: String, pub last_seen: Option<String>, pub needs_promote: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeviceTier {
Fleet,
External,
NeedsReview,
}
pub fn render_devices(devices: &[DeviceEntry], pending_requests: usize) -> String {
if devices.is_empty() && pending_requests == 0 {
return render_empty();
}
let mut lines = vec![];
if devices.is_empty() {
lines.push(ui::paint(Tone::Dim, " No devices yet."));
lines.push(String::new());
}
let fleet: Vec<_> = devices.iter().filter(|d| d.tier == DeviceTier::Fleet).collect();
if !fleet.is_empty() {
lines.push(format!(
" {} {}",
ui::paint(Tone::Brand, ui::glyph_fleet()),
ui::paint(Tone::Brand, "FLEET / your devices, permissive within scope")
));
for d in &fleet {
lines.push(render_device_row(d));
}
lines.push(String::new());
}
let externs: Vec<_> = devices.iter().filter(|d| d.tier == DeviceTier::External).collect();
if !externs.is_empty() {
lines.push(format!(
" {} {}",
ui::paint(Tone::Dim, ui::glyph_extern()),
ui::paint(Tone::Dim, "EXTERNAL / other people, time-boxed, deny-by-default")
));
for d in &externs {
lines.push(render_device_row(d));
}
lines.push(String::new());
}
let review: Vec<_> = devices.iter().filter(|d| d.tier == DeviceTier::NeedsReview).collect();
if !review.is_empty() {
lines.push(format!(
" {} {}",
ui::paint(Tone::Warn, ui::glyph_review()),
ui::paint(Tone::Warn, "NEEDS REVIEW / paired before scoped trust; promote to sort into a tier")
));
for d in &review {
lines.push(render_device_row(d));
lines.push(ui::paint(Tone::Dim, &format!(" ↳ filament devices promote {}", d.name)));
}
lines.push(String::new());
}
if pending_requests > 0 {
lines.push(format!(
" {} {} · filament requests",
ui::paint(Tone::Bold, &format!("{pending_requests}")),
ui::paint(Tone::Dim, "requests waiting")
));
}
lines.push(ui::paint(Tone::Dim, " This is a local index; each device's own capability list is authoritative."));
lines.join("\n")
}
fn render_device_row(d: &DeviceEntry) -> String {
let glyph = match d.tier {
DeviceTier::Fleet => ui::paint(Tone::Brand, ui::glyph_fleet()),
DeviceTier::External => ui::paint(Tone::Dim, ui::glyph_extern()),
DeviceTier::NeedsReview => ui::paint(Tone::Warn, ui::glyph_review()),
};
let status = if d.online {
ui::paint(Tone::Ok, "online")
} else {
ui::paint(Tone::Dim, "offline")
};
let mut line = format!(
" {glyph} {:<16} {:<8} {:<24} {}",
d.name, status, d.caps_summary, d.countdown
);
if let Some(ref seen) = d.last_seen {
line.push_str(&format!(" {}", ui::paint(Tone::Dim, &format!("(last seen {seen})"))));
}
if d.needs_promote {
line.push_str(&format!(" {}", ui::paint(Tone::Warn, "promote to continue")));
}
line
}
pub fn render_empty() -> String {
format!(
"No devices yet.\n\
Add in person: filament add\n\
Invite securely: filament invite"
)
}
pub fn render_degraded(offline_primaries: &[(&str, &str)], expiry: &str) -> String {
let primaries_str = offline_primaries.iter()
.map(|(name, last_seen)| format!("{name} (offline {last_seen})"))
.collect::<Vec<_>>()
.join(", ");
format!(
"{warn} No primary online. Existing grants keep working until their certificate expires.\n\
{until} Bring a primary online before then to issue a fresh invitation.\n\
{primaries}",
warn = ui::paint(Tone::Warn, ui::glyph_extern()),
until = format!(" until {expiry}"),
primaries = ui::paint(Tone::Dim, &format!(" Primaries: {primaries_str}")),
)
}
pub fn render_falling_out(name: &str, expiry: &str) -> String {
format!(
" {} {:<16} {}",
ui::paint(Tone::Warn, ui::glyph_fleet()),
name,
ui::paint(Tone::Warn, &format!("⚠ expires {expiry}, not renewing (no signer)"))
)
}
pub fn render_lapsed(name: &str, expiry: &str) -> String {
format!(
" . {name:<16} - - {}",
ui::paint(Tone::Dim, &format!("left: cert expired {expiry} / invite again to restore"))
)
}
pub fn render_promote(device_name: &str) -> String {
format!(
"{warn} {name} was paired before scoped trust. Sort it in:\n\
{fleet}\n\
{external}\n\
{promote} {cancel}",
warn = ui::paint(Tone::Warn, ui::glyph_review()),
name = device_name,
fleet = format!(" {} fleet / my own device, permissive within scope", ui::paint(Tone::Dim, "(o)")),
external = format!(" {} external / someone else, pick caps + an expiry", ui::paint(Tone::Dim, "( )")),
promote = ui::paint(Tone::Bold, "[ Promote ]"),
cancel = ui::paint(Tone::Dim, "[ Cancel ]"),
)
}
pub fn render_promote_success(device_name: &str) -> String {
format!(
"{ok} {name} is now a fleet device. Its certificate remains time-bounded.",
ok = ui::paint(Tone::Ok, ui::glyph_ok()),
name = device_name,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_devices() {
let s = render_empty();
assert!(s.contains("No devices yet"), "must show empty message");
assert!(s.contains("filament add"), "must suggest add");
assert!(s.contains("filament invite"), "must suggest invite");
}
#[test]
fn full_device_list() {
let devices = vec![
DeviceEntry {
name: "pixel-7".into(),
tier: DeviceTier::Fleet,
online: true,
caps_summary: "OWNER-EQUIVALENT shell reach:8080 inbox".into(),
countdown: "renews in 9m".into(),
last_seen: None,
needs_promote: false,
},
DeviceEntry {
name: "carol".into(),
tier: DeviceTier::External,
online: true,
caps_summary: "send→you".into(),
countdown: "expires in 4m".into(),
last_seen: None,
needs_promote: false,
},
];
let s = render_devices(&devices, 0);
assert!(s.contains("FLEET"), "must show fleet section");
assert!(s.contains("EXTERNAL"), "must show external section");
assert!(s.contains("pixel-7"), "must show fleet device");
assert!(s.contains("carol"), "must show external device");
}
#[test]
fn needs_review_section() {
let devices = vec![
DeviceEntry {
name: "old-laptop".into(),
tier: DeviceTier::NeedsReview,
online: false,
caps_summary: "(full legacy trust)".into(),
countdown: "promote to continue".into(),
last_seen: None,
needs_promote: true,
},
];
let s = render_devices(&devices, 0);
assert!(s.contains("NEEDS REVIEW"), "must show review section");
assert!(s.contains("old-laptop"), "must show review device");
assert!(s.contains("promote to continue"), "must show promote nudge");
}
#[test]
fn pending_requests_count() {
let s = render_devices(&[], 2);
assert!(s.contains("2"), "must show request count");
assert!(s.contains("filament requests"), "must suggest filament requests");
}
#[test]
fn degraded_no_primary() {
let s = render_degraded(&[("pixel-7", "5d"), ("studio-mac", "2d")], "Aug 3 (in 5 days)");
assert!(s.contains("No primary online"), "must show no-primary message");
assert!(s.contains("pixel-7"), "must show offline primary");
assert!(s.contains("Aug 3"), "must show expiry date");
}
#[test]
fn falling_out_warning() {
let s = render_falling_out("ci-box", "Aug 3");
assert!(s.contains("ci-box"), "must show device name");
assert!(s.contains("expires"), "must show expires");
assert!(s.contains("not renewing"), "must show not-renewing");
}
#[test]
fn lapsed_device() {
let s = render_lapsed("ci-box", "Aug 3");
assert!(s.contains("left: cert expired"), "must show lapsed reason");
assert!(s.contains("invite again to restore"), "must show restore hint");
}
#[test]
fn promote_dialog() {
let s = render_promote("old-laptop");
assert!(s.contains("old-laptop"), "must show device name");
assert!(s.contains("fleet"), "must show fleet option");
assert!(s.contains("external"), "must show external option");
}
#[test]
fn promote_success() {
let s = render_promote_success("old-laptop");
assert!(s.contains("old-laptop"), "must show device name");
assert!(s.contains("fleet device"), "must confirm fleet status");
}
}