Skip to main content

pacsea/sources/status/
mod.rs

1//! Arch Linux status page parsing and monitoring.
2
3use crate::state::ArchStatusColor;
4use std::fmt::Write;
5
6/// Status API parsing (`Statuspage` and `UptimeRobot` APIs).
7mod api;
8/// HTML parsing for status page.
9mod html;
10/// Translation utilities for status messages.
11pub mod translate;
12/// Utility functions for status parsing.
13mod utils;
14
15use api::{parse_status_api_summary, parse_uptimerobot_api};
16use html::{is_aur_down_in_monitors, parse_arch_status_from_html};
17use utils::{extract_aur_today_percent, extract_aur_today_rect_color, severity_max};
18
19/// Result type alias for Arch Linux status fetching operations.
20type Result<T> = super::Result<T>;
21
22/// Fetch a short status text and color indicator from status.archlinux.org.
23///
24/// Inputs: none
25///
26/// Output:
27/// - `Ok((text, color))` where `text` summarizes current status and `color` indicates severity.
28/// - `Err` on network or parse failures.
29///
30/// # Errors
31/// - Returns `Err` when network request fails (curl execution error)
32/// - Returns `Err` when status API response cannot be fetched or parsed
33/// - Returns `Err` when task spawn fails
34///
35#[allow(clippy::missing_const_for_fn)]
36pub async fn fetch_arch_status_text() -> Result<(String, ArchStatusColor)> {
37    // 1) Prefer the official Statuspage API (reliable for active incidents and component states)
38    let api_url = "https://status.archlinux.org/api/v2/summary.json";
39    let api_result =
40        tokio::task::spawn_blocking(move || crate::util::curl::curl_json(api_url)).await;
41
42    if let Ok(Ok(v)) = api_result {
43        let (mut text, mut color, suffix) = parse_status_api_summary(&v);
44
45        // Always fetch HTML to check the visual indicator (rect color/beam) which may differ from API status
46        if let Ok(Ok(html)) = tokio::task::spawn_blocking(|| {
47            crate::util::curl::curl_text("https://status.archlinux.org")
48        })
49        .await
50        {
51            // FIRST PRIORITY: Check if AUR specifically shows "Down" status in monitors section
52            // This must be checked before anything else as it's the most specific indicator
53            if is_aur_down_in_monitors(&html) {
54                let aur_pct_opt = extract_aur_today_percent(&html);
55                let aur_pct_suffix = aur_pct_opt
56                    .map(|p| format!(" — AUR today: {p}%"))
57                    .unwrap_or_default();
58                let text = format!("Status: AUR Down{aur_pct_suffix}");
59                return Ok((text, ArchStatusColor::IncidentSevereToday));
60            }
61
62            // Extract today's AUR uptime percentage (best-effort)
63            let aur_pct_opt = extract_aur_today_percent(&html);
64            if let Some(p) = aur_pct_opt {
65                let _ = write!(text, " — AUR today: {p}%");
66            }
67
68            // Check the visual indicator (rect color/beam) - this is authoritative for current status
69            // The beam color can show red/yellow even when API says "operational"
70            if let Some(rect_color) = extract_aur_today_rect_color(&html) {
71                // If the visual indicator shows a problem but API says operational, trust the visual indicator
72                let api_says_operational = matches!(
73                    v.get("components")
74                        .and_then(|c| c.as_array())
75                        .and_then(|arr| arr.iter().find(|c| {
76                            c.get("name")
77                                .and_then(|n| n.as_str())
78                                .is_some_and(|n| n.to_lowercase().contains("aur"))
79                        }))
80                        .and_then(|c| c.get("status").and_then(|s| s.as_str())),
81                    Some("operational")
82                );
83
84                if api_says_operational
85                    && matches!(
86                        rect_color,
87                        ArchStatusColor::IncidentToday | ArchStatusColor::IncidentSevereToday
88                    )
89                {
90                    // Visual indicator shows a problem but API says operational - trust the visual indicator
91                    color = rect_color;
92                    // Update text to reflect the visual indicator discrepancy
93                    let text_lower = text.to_lowercase();
94                    let pct_suffix = if text_lower.contains("aur today") {
95                        String::new() // Already added
96                    } else {
97                        aur_pct_opt
98                            .map(|p| format!(" — AUR today: {p}%"))
99                            .unwrap_or_default()
100                    };
101                    match rect_color {
102                        ArchStatusColor::IncidentSevereToday
103                            if !text_lower.contains("outage") && !text_lower.contains("issues") =>
104                        {
105                            text = format!("AUR issues detected (see status){pct_suffix}");
106                        }
107                        ArchStatusColor::IncidentToday
108                            if !text_lower.contains("degraded")
109                                && !text_lower.contains("outage")
110                                && !text_lower.contains("issues") =>
111                        {
112                            text = format!("AUR degraded (see status){pct_suffix}");
113                        }
114                        _ => {}
115                    }
116                } else {
117                    // Use the more severe of API color or rect color
118                    color = severity_max(color, rect_color);
119                }
120            }
121        }
122
123        if let Some(sfx) = suffix
124            && !text.to_lowercase().contains(&sfx.to_lowercase())
125        {
126            text = format!("{text} {sfx}");
127        }
128
129        return Ok((text, color));
130    }
131
132    // 2) Try the UptimeRobot API endpoint (the actual API the status page uses)
133    let uptimerobot_api_url = "https://status.archlinux.org/api/getMonitorList/vmM5ruWEAB";
134    let uptimerobot_result =
135        tokio::task::spawn_blocking(move || crate::util::curl::curl_json(uptimerobot_api_url))
136            .await;
137
138    if let Ok(Ok(v)) = uptimerobot_result
139        && let Some((mut text, mut color)) = parse_uptimerobot_api(&v)
140    {
141        // Also fetch HTML to check if AUR specifically shows "Down" status
142        // This takes priority over API response
143        if let Ok(Ok(html)) = tokio::task::spawn_blocking(|| {
144            crate::util::curl::curl_text("https://status.archlinux.org")
145        })
146        .await
147            && is_aur_down_in_monitors(&html)
148        {
149            let aur_pct_opt = extract_aur_today_percent(&html);
150            let aur_pct_suffix = aur_pct_opt
151                .map(|p| format!(" — AUR today: {p}%"))
152                .unwrap_or_default();
153            text = format!("Status: AUR Down{aur_pct_suffix}");
154            color = ArchStatusColor::IncidentSevereToday;
155        }
156        return Ok((text, color));
157    }
158
159    // 3) Fallback: use the existing HTML parser + banner heuristic if APIs are unavailable
160    let url = "https://status.archlinux.org";
161    let body = tokio::task::spawn_blocking(move || crate::util::curl::curl_text(url)).await??;
162
163    // Skip AUR homepage keyword heuristic to avoid false outage flags
164
165    let (text, color) = parse_arch_status_from_html(&body);
166    // Heuristic banner scan disabled in fallback to avoid false positives.
167
168    Ok((text, color))
169}