refactor(portal): move readiness API helpers into module

This commit is contained in:
igor04091968
2026-06-14 23:50:25 +03:00
parent 803c3169d7
commit 78a560dc3e
2 changed files with 115 additions and 99 deletions
+3 -99
View File
@@ -29,11 +29,11 @@ mod executive_actions;
mod path_query;
mod portal_roles;
mod production;
mod readiness_api;
mod risk_narrative;
mod workforce_kpi_explain;
use api_contracts::api_contract_summary;
use command_runner::run_in_dir;
use executive_actions::{
actions_from_center, build_action_center_from_report, filter_actions_for_role,
};
@@ -48,6 +48,7 @@ use production::{
record_ingestion_rejected, record_report_generated, render_prometheus_metrics,
validate_api_query_limits, validate_portal_config,
};
use readiness_api::{readiness_bundle, readiness_latest, readiness_verify};
use risk_narrative::{
RiskNarrativeInputs, RiskNarrativeQuery, build_risk_narrative, build_risk_narrative_from_report,
};
@@ -1662,103 +1663,6 @@ fn handle_evidence_only_request(request: Request, args: &Cli) -> Result<()> {
)
}
fn readiness_latest(args: &Cli) -> Value {
read_json_file(
&args
.readiness_bundle_dir
.join("detmir-readiness-latest.json"),
)
.unwrap_or_else(|err| {
json!({
"ok": false,
"generated_at_utc": now(),
"error": err.to_string(),
})
})
}
fn readiness_bundle(args: &Cli) -> Value {
let dir = &args.readiness_bundle_dir;
let status = read_json_file(&dir.join("detmir-readiness-status.json")).unwrap_or_else(|err| {
json!({
"ok": false,
"error": err.to_string(),
})
});
let latest_dir = fs::read_to_string(dir.join("latest-dir.txt"))
.unwrap_or_default()
.trim()
.to_string();
let artifacts = [
"detmir-readiness-latest.json",
"detmir-readiness-act.md",
"detmir-readiness-act.html",
"sha256sums.txt",
"sha256sums.txt.sig",
"public-key.pem",
"detmir-readiness-status.json",
"detmir-readiness.prom",
]
.into_iter()
.filter_map(|name| {
let path = dir.join(name);
path.metadata().ok().map(|meta| {
json!({
"name": name,
"bytes": meta.len(),
"available": true,
})
})
})
.collect::<Vec<_>>();
json!({
"ok": status.get("ok").and_then(Value::as_bool).unwrap_or(false),
"generated_at_utc": now(),
"bundle_dir": dir.display().to_string(),
"latest_archive_dir": latest_dir,
"status": status,
"artifacts": artifacts,
})
}
fn readiness_verify(args: &Cli) -> Value {
let dir = &args.readiness_bundle_dir;
let checksum = run_in_dir(
dir,
Command::new("sha256sum").arg("-c").arg("sha256sums.txt"),
);
let sig_path = dir.join("sha256sums.txt.sig");
let pub_path = dir.join("public-key.pem");
let signature = if sig_path.is_file() && pub_path.is_file() {
run_in_dir(
dir,
Command::new("openssl")
.arg("dgst")
.arg("-sha256")
.arg("-verify")
.arg("public-key.pem")
.arg("-signature")
.arg("sha256sums.txt.sig")
.arg("sha256sums.txt"),
)
} else {
Err("signature files are not available".to_string())
};
json!({
"ok": checksum.is_ok() && signature.is_ok(),
"generated_at_utc": now(),
"checksum_verified": checksum.is_ok(),
"signature_verified": signature.is_ok(),
"checksum_error": checksum.err(),
"signature_error": signature.err(),
})
}
fn read_json_file(path: &Path) -> Result<Value> {
let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
serde_json::from_str(&text).with_context(|| format!("parse {}", path.display()))
}
fn portal_role_from_request(request: &Request, url: &str) -> PortalRole {
query_param(url, "role")
.as_deref()
@@ -10714,7 +10618,7 @@ fn header(name: &str, value: &str) -> Result<Header> {
.map_err(|_| anyhow!("invalid header {name}: {value}"))
}
fn now() -> String {
pub(crate) fn now() -> String {
Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true)
}
@@ -0,0 +1,112 @@
//! Readiness API payload helpers for the portal.
//!
//! CONTRACT: these helpers expose existing readiness bundle/status/verify
//! payloads. Keep file names, JSON fields and verification commands stable
//! unless the customer readiness contract is updated in the same PR.
use std::fs;
use std::path::Path;
use std::process::Command;
use anyhow::{Context, Result};
use serde_json::{Value, json};
use crate::command_runner::run_in_dir;
use crate::{Cli, now};
pub(crate) fn readiness_latest(args: &Cli) -> Value {
read_json_file(
&args
.readiness_bundle_dir
.join("detmir-readiness-latest.json"),
)
.unwrap_or_else(|err| {
json!({
"ok": false,
"generated_at_utc": now(),
"error": err.to_string(),
})
})
}
pub(crate) fn readiness_bundle(args: &Cli) -> Value {
let dir = &args.readiness_bundle_dir;
let status = read_json_file(&dir.join("detmir-readiness-status.json")).unwrap_or_else(|err| {
json!({
"ok": false,
"error": err.to_string(),
})
});
let latest_dir = fs::read_to_string(dir.join("latest-dir.txt"))
.unwrap_or_default()
.trim()
.to_string();
let artifacts = [
"detmir-readiness-latest.json",
"detmir-readiness-act.md",
"detmir-readiness-act.html",
"sha256sums.txt",
"sha256sums.txt.sig",
"public-key.pem",
"detmir-readiness-status.json",
"detmir-readiness.prom",
]
.into_iter()
.filter_map(|name| {
let path = dir.join(name);
path.metadata().ok().map(|meta| {
json!({
"name": name,
"bytes": meta.len(),
"available": true,
})
})
})
.collect::<Vec<_>>();
json!({
"ok": status.get("ok").and_then(Value::as_bool).unwrap_or(false),
"generated_at_utc": now(),
"bundle_dir": dir.display().to_string(),
"latest_archive_dir": latest_dir,
"status": status,
"artifacts": artifacts,
})
}
pub(crate) fn readiness_verify(args: &Cli) -> Value {
let dir = &args.readiness_bundle_dir;
let checksum = run_in_dir(
dir,
Command::new("sha256sum").arg("-c").arg("sha256sums.txt"),
);
let sig_path = dir.join("sha256sums.txt.sig");
let pub_path = dir.join("public-key.pem");
let signature = if sig_path.is_file() && pub_path.is_file() {
run_in_dir(
dir,
Command::new("openssl")
.arg("dgst")
.arg("-sha256")
.arg("-verify")
.arg("public-key.pem")
.arg("-signature")
.arg("sha256sums.txt.sig")
.arg("sha256sums.txt"),
)
} else {
Err("signature files are not available".to_string())
};
json!({
"ok": checksum.is_ok() && signature.is_ok(),
"generated_at_utc": now(),
"checksum_verified": checksum.is_ok(),
"signature_verified": signature.is_ok(),
"checksum_error": checksum.err(),
"signature_error": signature.err(),
})
}
fn read_json_file(path: &Path) -> Result<Value> {
let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
serde_json::from_str(&text).with_context(|| format!("parse {}", path.display()))
}