use anyhow::{Context, Result};
use clap::{Args, Subcommand};
use kanade_shared::manifest::Freeze;
#[derive(Args, Debug)]
pub struct FreezeArgs {
#[command(subcommand)]
pub sub: FreezeSub,
}
#[derive(Subcommand, Debug)]
pub enum FreezeSub {
Status,
Set {
#[arg(long)]
from: Option<String>,
#[arg(long)]
until: Option<String>,
#[arg(long)]
reason: Option<String>,
#[arg(long)]
utc: bool,
},
Clear,
}
pub async fn execute(backend_url: &str, args: FreezeArgs) -> Result<()> {
let base = backend_url.trim_end_matches('/');
match args.sub {
FreezeSub::Status => status(base).await,
FreezeSub::Set {
from,
until,
reason,
utc,
} => set(base, from, until, reason, utc).await,
FreezeSub::Clear => clear(base).await,
}
}
async fn status(base: &str) -> Result<()> {
let url = format!("{base}/api/freeze");
let resp = crate::http_client::authed_client()?
.get(&url)
.send()
.await
.with_context(|| format!("GET {url}"))?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
anyhow::bail!("freeze status failed: {status} — {body}");
}
let freeze: Option<Freeze> = resp.json().await?;
match freeze {
None => println!("fleet is NOT frozen (no freeze configured — schedules fire normally)"),
Some(f) => {
if f.is_active(chrono::Utc::now()) {
println!("fleet is FROZEN now — all schedule fires are skipped");
} else {
println!(
"freeze is CONFIGURED but NOT active right now (outside its window — schedules fire normally)"
);
}
match (&f.from, &f.until) {
(None, None) => println!(" window : indefinite (until `kanade freeze clear`)"),
(from, until) => println!(
" window : {} .. {} ({:?})",
from.as_deref().unwrap_or("-∞"),
until.as_deref().unwrap_or("+∞"),
f.tz,
),
}
if let Some(r) = &f.reason {
println!(" reason : {r}");
}
}
}
Ok(())
}
async fn set(
base: &str,
from: Option<String>,
until: Option<String>,
reason: Option<String>,
utc: bool,
) -> Result<()> {
use kanade_shared::manifest::ScheduleTz;
let freeze = Freeze {
from,
until,
reason,
tz: if utc {
ScheduleTz::Utc
} else {
ScheduleTz::Local
},
};
freeze
.validate()
.map_err(|e| anyhow::anyhow!("invalid freeze: {e}"))?;
let url = format!("{base}/api/freeze");
let resp = crate::http_client::authed_client()?
.put(&url)
.json(&freeze)
.send()
.await
.with_context(|| format!("PUT {url}"))?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
anyhow::bail!("freeze set rejected: {status} — {body}");
}
println!("fleet change-freeze set. Run `kanade freeze status` to confirm.");
Ok(())
}
async fn clear(base: &str) -> Result<()> {
let url = format!("{base}/api/freeze");
let resp = crate::http_client::authed_client()?
.delete(&url)
.send()
.await
.with_context(|| format!("DELETE {url}"))?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
anyhow::bail!("freeze clear rejected: {status} — {body}");
}
println!("fleet change-freeze cleared (thawed).");
Ok(())
}