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                                text = format!("AUR issues detected (see status){pct_suffix}");
105                            }
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                    }
117                } else {
118                    // Use the more severe of API color or rect color
119                    color = severity_max(color, rect_color);
120                }
121            }
122        }
123
124        if let Some(sfx) = suffix
125            && !text.to_lowercase().contains(&sfx.to_lowercase())
126        {
127            text = format!("{text} {sfx}");
128        }
129
130        return Ok((text, color));
131    }
132
133    // 2) Try the UptimeRobot API endpoint (the actual API the status page uses)
134    let uptimerobot_api_url = "https://status.archlinux.org/api/getMonitorList/vmM5ruWEAB";
135    let uptimerobot_result =
136        tokio::task::spawn_blocking(move || crate::util::curl::curl_json(uptimerobot_api_url))
137            .await;
138
139    if let Ok(Ok(v)) = uptimerobot_result
140        && let Some((mut text, mut color)) = parse_uptimerobot_api(&v)
141    {
142        // Also fetch HTML to check if AUR specifically shows "Down" status
143        // This takes priority over API response
144        if let Ok(Ok(html)) = tokio::task::spawn_blocking(|| {
145            crate::util::curl::curl_text("https://status.archlinux.org")
146        })
147        .await
148            && is_aur_down_in_monitors(&html)
149        {
150            let aur_pct_opt = extract_aur_today_percent(&html);
151            let aur_pct_suffix = aur_pct_opt
152                .map(|p| format!(" — AUR today: {p}%"))
153                .unwrap_or_default();
154            text = format!("Status: AUR Down{aur_pct_suffix}");
155            color = ArchStatusColor::IncidentSevereToday;
156        }
157        return Ok((text, color));
158    }
159
160    // 3) Fallback: use the existing HTML parser + banner heuristic if APIs are unavailable
161    let url = "https://status.archlinux.org";
162    let body = tokio::task::spawn_blocking(move || crate::util::curl::curl_text(url)).await??;
163
164    // Skip AUR homepage keyword heuristic to avoid false outage flags
165
166    let (text, color) = parse_arch_status_from_html(&body);
167    // Heuristic banner scan disabled in fallback to avoid false positives.
168
169    Ok((text, color))
170}