use crate::state::ArchStatusColor;
pub(super) fn extract_arch_systems_status_color(body: &str) -> Option<ArchStatusColor> {
let lowered = body.to_lowercase();
let mut found_severe = false;
let mut found_moderate = false;
if lowered.contains("some systems down") {
found_severe = true;
}
let status_patterns = ["status: arch systems", "status arch systems"];
for pattern in status_patterns {
if let Some(pos) = lowered.find(pattern) {
let window_start = pos;
let window_end = std::cmp::min(body.len(), pos + pattern.len() + 500);
let window = &lowered[window_start..window_end];
if window.contains("some systems down") {
found_severe = true;
} else if window.contains("down") {
if !window.contains("all systems operational") && !window.contains("operational") {
found_moderate = true;
}
}
}
}
if let Some(monitors_pos) = lowered.find("monitors") {
let monitors_window_start = monitors_pos;
let monitors_window_end = std::cmp::min(body.len(), monitors_pos + 5000);
let monitors_window = &lowered[monitors_window_start..monitors_window_end];
if monitors_window.contains("down") {
if monitors_window.contains(">down<")
|| monitors_window.contains("\"down\"")
|| monitors_window.contains("'down'")
{
if !monitors_window.contains("operational") {
found_moderate = true;
}
}
if monitors_window.contains("some systems down") {
found_severe = true;
}
}
}
if found_severe {
Some(ArchStatusColor::IncidentSevereToday)
} else if found_moderate {
Some(ArchStatusColor::IncidentToday)
} else {
None
}
}
pub(super) const fn severity_max(a: ArchStatusColor, b: ArchStatusColor) -> ArchStatusColor {
const fn rank(c: ArchStatusColor) -> u8 {
match c {
ArchStatusColor::None => 0,
ArchStatusColor::Operational => 1,
ArchStatusColor::IncidentToday => 2,
ArchStatusColor::IncidentSevereToday => 3,
}
}
if rank(a) >= rank(b) { a } else { b }
}
pub(super) fn today_ymd_utc() -> Option<(i32, u32, u32)> {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
let days_since_epoch = now / 86400;
let mut year = 1970;
let mut days = days_since_epoch;
loop {
let days_in_year = if is_leap_year(year) { 366 } else { 365 };
if days < days_in_year {
break;
}
days -= days_in_year;
year += 1;
}
let days_in_month = [
31,
if is_leap_year(year) { 29 } else { 28 },
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
];
let mut month: u32 = 1;
let mut day: u64 = days;
for &days_in_m in &days_in_month {
let days_in_m_u64 = u64::try_from(days_in_m).unwrap_or(0);
if day < days_in_m_u64 {
break;
}
day -= days_in_m_u64;
month += 1;
}
Some((
year,
month,
u32::try_from(day).expect("day fits in u32") + 1,
)) }
#[inline]
const fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
pub(super) fn extract_aur_today_percent(body: &str) -> Option<u32> {
let (year, month, day) = today_ymd_utc()?;
let months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
let month_name = months.get((month.saturating_sub(1)) as usize)?;
let date_str = format!("{month_name} {day}, {year}");
let lowered = body.to_lowercase();
let base = "uptime last 90 days";
let mut base_pos = lowered.find(base)?;
if let Some(p) = lowered[base_pos..].find("monitors (default)") {
base_pos += p;
}
if let Some(p) = lowered[base_pos..].find("aur") {
base_pos += p;
}
let region_end = std::cmp::min(body.len(), base_pos.saturating_add(4000));
let region = &body[base_pos..region_end];
let region_lower = region.to_lowercase();
let date_pos = region_lower.find(&date_str.to_lowercase())?;
let win_start = date_pos.saturating_sub(120);
let win_end = std::cmp::min(region_lower.len(), date_pos + 160);
let window = ®ion_lower[win_start..win_end];
let after_slice = &window[(date_pos - win_start)..];
if let Some(rel_idx) = after_slice.find('%') {
let abs_idx = win_start + (date_pos - win_start) + rel_idx;
if let Some(p) = digits_before_percent(®ion_lower, abs_idx) {
return p.parse::<u32>().ok();
}
}
if let Some(rel_idx) = window.find('%') {
let abs_idx = win_start + rel_idx;
if let Some(p) = digits_before_percent(®ion_lower, abs_idx) {
return p.parse::<u32>().ok();
}
}
None
}
fn digits_before_percent(s: &str, pct_idx: usize) -> Option<String> {
if pct_idx == 0 || pct_idx > s.len() {
return None;
}
let mut i = pct_idx.saturating_sub(1);
let bytes = s.as_bytes();
let mut digits: Vec<u8> = Vec::new();
for _ in 0..3 {
if i < s.len() && bytes[i].is_ascii_digit() {
digits.push(bytes[i]);
if i == 0 {
break;
}
i = i.saturating_sub(1);
} else {
break;
}
}
if digits.is_empty() {
return None;
}
digits.reverse();
let s = String::from_utf8(digits).ok()?;
Some(s)
}
pub(super) fn extract_aur_today_rect_color(body: &str) -> Option<ArchStatusColor> {
let (year, month, day) = today_ymd_utc()?;
let months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
let month_name = months.get((month.saturating_sub(1)) as usize)?;
let date_str = format!("{month_name} {day}, {year}");
let lowered = body.to_lowercase();
let base = "uptime last 90 days";
let mut base_pos = lowered.find(base)?;
if let Some(p) = lowered[base_pos..].find("monitors (default)") {
base_pos += p;
}
if let Some(p) = lowered[base_pos..].find("aur") {
base_pos += p;
}
let region_end = std::cmp::min(body.len(), base_pos.saturating_add(6000));
let region = &body[base_pos..region_end];
let region_lower = region.to_lowercase();
let date_pos = region_lower.find(&date_str.to_lowercase())?;
let head = ®ion_lower[..date_pos];
if let Some(rect_pos) = head.rfind("<rect") {
let tail = ®ion_lower[rect_pos..std::cmp::min(region_lower.len(), rect_pos + 400)];
if let Some(fill_idx) = tail.find("fill=") {
let after = &tail[fill_idx + 5..]; let after = after.trim_start_matches(' ');
let quote = after.chars().next().unwrap_or('"');
let after = if quote == '"' || quote == '\'' {
&after[1..]
} else {
after
};
let value: String = after
.chars()
.take_while(|&c| c != '"' && c != '\'' && c != ' ' && c != '>')
.collect();
let v = value.to_lowercase();
if v.contains("#10b981") || v.contains("rgb(16 185 129)") {
return Some(ArchStatusColor::Operational);
}
if v.contains("#f59e0b") || v.contains("rgb(245 158 11)") || v.contains("#fbbf24") {
return Some(ArchStatusColor::IncidentToday);
}
if v.contains("#ef4444") || v.contains("rgb(239 68 68)") || v.contains("#dc2626") {
return Some(ArchStatusColor::IncidentSevereToday);
}
}
}
let tail = ®ion_lower[date_pos..std::cmp::min(region_lower.len(), date_pos + 400)];
if let Some(rect_rel) = tail.find("<rect") {
let start = date_pos + rect_rel;
let slice = ®ion_lower[start..std::cmp::min(region_lower.len(), start + 400)];
if let Some(fill_idx) = slice.find("fill=") {
let after = &slice[fill_idx + 5..];
let after = after.trim_start_matches(' ');
let quote = after.chars().next().unwrap_or('"');
let after = if quote == '"' || quote == '\'' {
&after[1..]
} else {
after
};
let value: String = after
.chars()
.take_while(|&c| c != '"' && c != '\'' && c != ' ' && c != '>')
.collect();
let v = value.to_lowercase();
if v.contains("#10b981") || v.contains("rgb(16 185 129)") {
return Some(ArchStatusColor::Operational);
}
if v.contains("#f59e0b") || v.contains("rgb(245 158 11)") || v.contains("#fbbf24") {
return Some(ArchStatusColor::IncidentToday);
}
if v.contains("#ef4444") || v.contains("rgb(239 68 68)") || v.contains("#dc2626") {
return Some(ArchStatusColor::IncidentSevereToday);
}
}
}
None
}
pub(super) fn extract_status_updates_today_color(body: &str) -> Option<ArchStatusColor> {
let (year, month, day) = today_ymd_utc()?;
let months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
let month_name = months.get((month.saturating_sub(1)) as usize)?;
let date_str = format!("{month_name} {day}, {year}");
let lowered = body.to_lowercase();
let base = "status updates";
let mut base_pos = lowered.find(base)?;
let days_patterns = ["last 30 days", "last 7 days", "last 14 days"];
for pattern in days_patterns {
if let Some(p) = lowered[base_pos..].find(pattern) {
base_pos += p;
break;
}
}
let region_end = std::cmp::min(body.len(), base_pos.saturating_add(10000));
let region = &body[base_pos..region_end];
let region_lower = region.to_lowercase();
let date_pos = region_lower.find(&date_str.to_lowercase())?;
let window_start = date_pos.saturating_sub(500);
let window_end = std::cmp::min(region_lower.len(), date_pos + 500);
let window = ®ion_lower[window_start..window_end];
let severe_keywords = [
"outage",
"down",
"unavailable",
"offline",
"failure",
"critical",
"major incident",
];
let moderate_keywords = [
"degraded",
"slow",
"intermittent",
"issues",
"problems",
"maintenance",
"partial",
];
let mentions_aur = window.contains("aur") || window.contains("arch user repository");
if !mentions_aur {
return None; }
for keyword in &severe_keywords {
if window.contains(keyword) {
return Some(ArchStatusColor::IncidentSevereToday);
}
}
for keyword in &moderate_keywords {
if window.contains(keyword) {
return Some(ArchStatusColor::IncidentToday);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_extract_arch_systems_status_color() {
let html_severe =
"<html><body><div>Status: Arch systems Some Systems down</div></body></html>";
let color = extract_arch_systems_status_color(html_severe);
assert_eq!(color, Some(ArchStatusColor::IncidentSevereToday));
let html_moderate = "<html><body><div>Status: Arch systems Down</div></body></html>";
let color = extract_arch_systems_status_color(html_moderate);
assert_eq!(color, Some(ArchStatusColor::IncidentToday));
let html_monitors_down =
"<html><body><div>Monitors (default)</div><div>AUR</div><div>Down</div></body></html>";
let color = extract_arch_systems_status_color(html_monitors_down);
assert_eq!(color, Some(ArchStatusColor::IncidentToday));
let html_monitors_html = "<html><body><div>Monitors</div><div>>Down<</div></body></html>";
let color = extract_arch_systems_status_color(html_monitors_html);
assert_eq!(color, Some(ArchStatusColor::IncidentToday));
let html_monitors_quotes =
"<html><body><div>Monitors</div><div>\"Down\"</div></body></html>";
let color = extract_arch_systems_status_color(html_monitors_quotes);
assert_eq!(color, Some(ArchStatusColor::IncidentToday));
let html_monitors_severe =
"<html><body><div>Monitors</div><div>Some Systems down</div></body></html>";
let color = extract_arch_systems_status_color(html_monitors_severe);
assert_eq!(color, Some(ArchStatusColor::IncidentSevereToday));
let html_ok = "<html><body><div>Status: Arch systems Operational</div><div>Monitors</div><div>Operational</div></body></html>";
let color = extract_arch_systems_status_color(html_ok);
assert_eq!(color, None);
let html_operational = "<html><body><div>Status: Arch systems Operational</div><div>Monitors</div><div>All systems operational</div></body></html>";
let color = extract_arch_systems_status_color(html_operational);
assert_eq!(color, None);
let html_lowercase =
"<html><body><div>status: arch systems some systems down</div></body></html>";
let color = extract_arch_systems_status_color(html_lowercase);
assert_eq!(color, Some(ArchStatusColor::IncidentSevereToday));
let html_no_colon = "<html><body><div>Status Arch systems Down</div></body></html>";
let color = extract_arch_systems_status_color(html_no_colon);
assert_eq!(color, Some(ArchStatusColor::IncidentToday));
let html_standalone_heading =
"<html><body><h2>Some systems down</h2><div>Monitors</div></body></html>";
let color = extract_arch_systems_status_color(html_standalone_heading);
assert_eq!(color, Some(ArchStatusColor::IncidentSevereToday));
let html_body_text = "<html><body><div>Some systems down</div></body></html>";
let color = extract_arch_systems_status_color(html_body_text);
assert_eq!(color, Some(ArchStatusColor::IncidentSevereToday));
}
}