crates/gunnar-ui-server/src/api/rebuild.rs
//! **[Rebuild main]** — the console's trigger for rebuilding the estate's MAIN from its
//! twin (owner 2026-10-12: the twin is never promoted; the main's data is checked,
//! reformatted only when destroyed, and filled from the twin).
//!
//! Thin, like every surface here: the console decides nothing. It asks monetize's
//! `EstateOrder.RebuildMain` — the ONE box's guardian, for THIS estate only (the site
//! bearer names it) — what it finds about the main, and presses that finding's remedy in
//! the stepped-up person's name. The guardian runs the case; what may be destroyed is
//! gunnar's guard's to decide, on the box and in the operator's kit.
//!
//! * `GET /api/rebuild` — the look (admin read): the finding, its remedy, the open case;
//! * `POST /api/rebuild` — the press (admin, stepped up — or, with the main down, an admin
//! key the TWIN vouches for, as the Twin panel's restore), with the typed confirmation
//! [`CONFIRM`];
//! * `POST /twin-door/rebuild` — the same press for a person whose main cannot log them in.
use axum::extract::State;
use axum::Json;
use serde::{Deserialize, Serialize};
use crate::api::{ApiError, ApiResult};
use crate::app::AppState;
use crate::auth::middleware::Caller;
use crate::grpc::Fail;
use crate::mpb;
/// What the person types to press.
pub const CONFIRM: &str = "rebuild main";
/// What the console shows: monetize's words, passed through.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Look {
pub finding: String,
pub words: String,
/// `new_server` | `recharge_from_twin`; empty: nothing to press.
pub remedy: String,
pub case_id: String,
pub case_state: String,
pub steps: Vec<String>,
pub outcome: String,
pub auto_fix: bool,
/// The phrase a press must carry (empty when nothing may be pressed).
pub confirm: String,
}
/// The press's body.
#[derive(Debug, Clone, Default, Deserialize)]
pub struct PressBody {
#[serde(default)]
pub confirm: String,
}
/// Pure: monetize's answer as the console shows it.
pub fn look_of(a: mpb::EstateRebuildMainResponse) -> Look {
let pressable = !a.remedy.is_empty() && !matches!(a.case_state.as_str(), "approved" | "running");
Look {
finding: a.finding,
words: a.words,
remedy: a.remedy,
case_id: a.case_id,
case_state: a.case_state,
steps: a.steps,
outcome: a.outcome,
auto_fix: a.auto_fix,
confirm: if pressable { CONFIRM.to_string() } else { String::new() },
}
}
fn refuse(code: &str, message: impl Into<String>) -> ApiError {
ApiError(Fail { code: code.to_owned(), message: message.into() })
}
fn monetize(state: &AppState) -> Result<&crate::monetize::Monetize, ApiError> {
state.monetize.as_ref().ok_or_else(|| refuse("failedprecondition", "this console reaches no monetize: the estate's rebuild is the ONE box's guardian's, and it is not configured here"))
}
/// Pure: the typed confirmation, checked.
pub fn confirmed(typed: &str) -> Result<(), ApiError> {
if typed.trim() == CONFIRM {
Ok(())
} else {
Err(refuse("invalidargument", format!("type `{CONFIRM}` to confirm the rebuild of the main")))
}
}
/// `GET /api/rebuild` — the look.
pub async fn look(State(state): State<AppState>) -> ApiResult<Look> {
let a = monetize(&state)?.estate_order().rebuild_main(mpb::EstateRebuildMainRequest { principal: String::new(), strong: false, press: false }).await?.into_inner();
Ok(Json(look_of(a)))
}
async fn press_as(state: &AppState, who: &str, body: &PressBody) -> Result<Look, ApiError> {
confirmed(&body.confirm)?;
let a = monetize(state)?.estate_order().rebuild_main(mpb::EstateRebuildMainRequest { principal: who.to_owned(), strong: true, press: true }).await?.into_inner();
tracing::warn!(target: crate::auth::AUDIT_TARGET, event = "rebuild.pressed", principal = %who, remedy = %a.remedy, case = %a.case_id, "the main's rebuild from the twin was pressed");
Ok(look_of(a))
}
/// `POST /api/rebuild` — the press: admin, stepped up (the Twin panel's gate).
pub async fn press(State(state): State<AppState>, caller: Option<axum::Extension<Caller>>, headers: http::HeaderMap, Json(body): Json<PressBody>) -> ApiResult<Look> {
let who = crate::api::twin::step_up(&state, caller.as_deref(), &headers).await?;
press_as(&state, &who, &body).await.map(Json)
}
/// `POST /twin-door/rebuild` — the press for a person whose main cannot log them in: the
/// twin vouches for their admin key.
pub async fn door_press(State(state): State<AppState>, headers: http::HeaderMap, Json(body): Json<PressBody>) -> ApiResult<Look> {
let who = crate::api::twin::twin_admin(&state, &headers).await?;
press_as(&state, &who, &body).await.map(Json)
}
#[cfg(test)]
mod tests {
use super::*;
/// The console shows monetize's words and offers the press only while no case runs.
#[test]
fn the_look_passes_monetizes_words_and_arms_the_press_only_when_nothing_runs() {
let a = mpb::EstateRebuildMainResponse { finding: "main_lost".into(), words: "the main is silent (never promoted)".into(), remedy: "new_server".into(), ..Default::default() };
let l = look_of(a.clone());
assert_eq!((l.finding.as_str(), l.remedy.as_str(), l.confirm.as_str()), ("main_lost", "new_server", CONFIRM));
assert_eq!(l.words, a.words, "word for word");
let running = look_of(mpb::EstateRebuildMainResponse { case_state: "running".into(), case_id: "c-1".into(), ..a.clone() });
assert!(running.confirm.is_empty(), "a running case is not pressed again");
let nothing = look_of(mpb::EstateRebuildMainResponse { words: "nothing to rebuild".into(), ..Default::default() });
assert!(nothing.confirm.is_empty());
assert!(confirmed(" rebuild main ").is_ok());
assert!(confirmed("rebuild").is_err());
}
}