Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

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());
    }
}