use std::collections::HashMap;
use crate::profile::{AppConfig, Profile};
use crate::profile_cache::{
THIRD_PARTY_CACHE_FILE, USAGE_CACHE_FILE, load_profile_cache, profile_cache_mtime_ms,
};
use crate::profile_json::{provider_label, tier_label, windows_json};
use crate::providers::ThirdPartyStats;
use crate::usage::{FetchStatus, epoch_secs_to_iso, is_stuck_rate_limited, now_ms};
pub(crate) const SCHEMA_VERSION: u64 = 1;
pub(crate) struct LiveSignals<'a> {
pub(crate) status: &'a HashMap<String, FetchStatus>,
pub(crate) next_refresh: &'a HashMap<String, u64>,
pub(crate) streaks: &'a HashMap<String, u32>,
pub(crate) pending_switch: Option<&'a str>,
}
fn fetch_status_str(s: FetchStatus) -> &'static str {
match s {
FetchStatus::Fresh => "Fresh",
FetchStatus::Cached => "Cached",
FetchStatus::Failed => "Failed",
FetchStatus::RateLimited => "RateLimited",
}
}
fn iso_from_ms(ms: u64) -> String {
epoch_secs_to_iso((ms / 1000) as i64)
}
fn fallback_json(config: &AppConfig, p: &Profile) -> Option<serde_json::Value> {
let name = p.name.as_str();
let pos = config
.state
.fallback_chain
.iter()
.position(|n| n.as_str() == name)?;
Some(serde_json::json!({
"position": pos + 1,
"threshold": crate::fallback::threshold_for(p),
"armed": config.is_active(name),
}))
}
fn auth_status_str(config: &AppConfig, p: &Profile, now_ms: i64) -> &'static str {
if config.is_auth_broken(p.name.as_str()) {
return "broken";
}
if p.is_oauth() && p.access_token_expires_at().is_some_and(|exp| now_ms >= exp) {
return "expiring";
}
"ok"
}
pub(crate) fn build_status(
config: &AppConfig,
interval_ms: u64,
live: Option<&LiveSignals>,
) -> serde_json::Value {
let now = now_ms();
let profiles: Vec<serde_json::Value> = config
.profiles
.iter()
.map(|p| {
let name = p.name.as_str();
let cache_file = if p.is_third_party() {
THIRD_PARTY_CACHE_FILE
} else {
USAGE_CACHE_FILE
};
let mtime_ms = profile_cache_mtime_ms(name, cache_file);
let derived_status = || {
mtime_ms.map(|mt| {
if now.saturating_sub(mt) < interval_ms {
"Fresh"
} else {
"Cached"
}
})
};
let fetch_status: Option<&'static str> = match live {
Some(sig) => sig
.status
.get(name)
.copied()
.map(fetch_status_str)
.or_else(derived_status),
None => derived_status(),
};
let derived_next = || mtime_ms.map(|mt| mt.saturating_add(interval_ms));
let next_refresh_ms: Option<u64> = match live {
Some(sig) => sig.next_refresh.get(name).copied().or_else(derived_next),
None => derived_next(),
};
let stale = match live {
Some(sig) => sig.status.get(name).copied().is_some_and(|s| {
is_stuck_rate_limited(s, sig.streaks.get(name).copied().unwrap_or(0))
}),
None => false,
};
let third_party = if p.is_third_party() {
load_profile_cache::<ThirdPartyStats>(name, THIRD_PARTY_CACHE_FILE)
.map(|s| serde_json::json!({ "available": s.is_available }))
} else {
None
};
serde_json::json!({
"name": name,
"active": config.is_active(name),
"provider": provider_label(p),
"base_url": p.base_url,
"tier": tier_label(p),
"has_live_session": crate::runtime::has_live_session(name),
"auth_status": auth_status_str(config, p, now as i64),
"fetch_status": fetch_status,
"stale": stale,
"fetched_at": mtime_ms.map(iso_from_ms),
"next_refresh_at": next_refresh_ms.map(iso_from_ms),
"auto_start": p.auto_start,
"bell_threshold": p.bell_threshold,
"fallback": fallback_json(config, p),
"windows": windows_json(name),
"third_party": third_party,
})
})
.collect();
serde_json::json!({
"schema": SCHEMA_VERSION,
"generated_at": iso_from_ms(now),
"active_profile": config.state.active_profile.as_deref(),
"pending_switch": live.and_then(|s| s.pending_switch),
"wrap_off": config.state.wrap_off,
"refresh_interval_ms": interval_ms,
"profiles": profiles,
})
}
#[cfg(test)]
#[path = "../../tests/inline/daemon_status_json.rs"]
mod tests;