wsb-daemon 0.291.8591

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the host
//! What of GitHub a page is using, a refresh of what it shows, and where the
//! account's budget stands.
//!
//! * `POST /api/v1/github/attention` — a page says what it shows, or that it
//!   is hidden. The keeper keeps only that current.
//! * `POST /api/v1/github/refresh` — a page asks for what it shows to be read
//!   now. Still conditional, and once per page per
//!   [`crate::web::attention::REFRESH_AT_MOST`].
//! * `GET /api/v1/github/budget` — the account's budget per resource, and what
//!   this host asked this hour.

use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;

use axum::extract::State;
use axum::http::StatusCode;
use axum::routing::{get, post};
use axum::{Json, Router};
use serde_json::json;

use crate::web::attention::{demand_of_subjects, Presence, Subject};
use crate::web::github::{self, Refusal};
use crate::workshop::WorkshopState;
use wsb::daemon::WorktreeId;

pub fn routes() -> Router<Arc<WorkshopState>> {
    Router::new()
        .route("/api/v1/github/attention", post(attention))
        .route("/api/v1/github/refresh", post(refresh))
        .route("/api/v1/github/budget", get(budget))
}

fn refused(status: StatusCode, error: impl Into<String>) -> Refusal {
    (status, Json(crate::web::handlers::sessions::ErrorResponse { error: error.into() }))
}

/// The checkouts of the worktrees `subjects` name, opening any this daemon
/// has not: a page shows it, so it is wanted.
async fn checkouts(workshop: &WorkshopState, subjects: &[Subject]) -> Result<BTreeMap<String, PathBuf>, Refusal> {
    let mut found = BTreeMap::new();
    for subject in subjects {
        if let Subject::Worktree { id } = subject {
            let parsed = WorktreeId::parse(id).map_err(|error| refused(StatusCode::BAD_REQUEST, error))?;
            let runtime = workshop.open(&parsed).await.map_err(|error| refused(StatusCode::NOT_FOUND, error))?;
            found.insert(id.clone(), runtime.registered.super_root.clone());
        }
    }
    Ok(found)
}

async fn attention(
    State(workshop): State<Arc<WorkshopState>>,
    Json(presence): Json<Presence>,
) -> Result<StatusCode, Refusal> {
    let github = github::of_workshop(&workshop)?;
    let shows = if presence.visible {
        let found = checkouts(&workshop, &presence.subjects).await?;
        demand_of_subjects(&presence.subjects, &found).map_err(|error| refused(StatusCode::BAD_REQUEST, error))?
    } else {
        wsb_github::sync::Demand::default()
    };
    github.attention.page(&presence.client, presence.visible, shows, Instant::now());
    Ok(StatusCode::NO_CONTENT)
}

async fn refresh(
    State(workshop): State<Arc<WorkshopState>>,
    Json(presence): Json<Presence>,
) -> Result<Json<wsb_github::sync::SyncReport>, Refusal> {
    let github = github::of_workshop(&workshop)?;
    let found = checkouts(&workshop, &presence.subjects).await?;
    let mut shows =
        demand_of_subjects(&presence.subjects, &found).map_err(|error| refused(StatusCode::BAD_REQUEST, error))?;
    // What a page refreshes is what it shows, which it now shows: the lease
    // is renewed with it.
    github.attention.page(&presence.client, true, shows.clone(), Instant::now());
    shows.notifications = true;

    if !github.attention.may_refresh(&presence.client, Instant::now()) {
        return Ok(Json(wsb_github::sync::SyncReport::default()));
    }

    let client = match github::client(github) {
        Ok(client) => client,
        // No login is a result to show: nothing was asked, and what is held
        // is still the answer.
        Err(_) => return Ok(Json(wsb_github::sync::SyncReport { no_token: true, ..Default::default() })),
    };
    let prj = github.prj.clone();
    let full = tokio::task::spawn_blocking(move || wsb_github::targets::Targets::of(&prj))
        .await
        .map_err(|e| github::failure(wsb_github::Failure::Mirror(anyhow::anyhow!("join error: {e}"))))?
        .map_err(|e| github::failure(wsb_github::Failure::Mirror(e)))?;
    Ok(Json(wsb_github::sync::sync(github.service.mirror(), Some(&client), &full, &shows, true).await))
}

async fn budget(State(workshop): State<Arc<WorkshopState>>) -> Result<Json<serde_json::Value>, Refusal> {
    let github = github::of_workshop(&workshop)?;
    let budget = wsb_github::budget::Budget::new(github.service.mirror().clone(), wsb_github::budget::Class::Interactive);
    let (standings, calls) = github::blocking(move || {
        let now = chrono::Utc::now();
        let read = || -> anyhow::Result<_> { Ok((budget.standings()?, budget.calls_this_hour(now)?)) };
        read().map_err(wsb_github::Failure::Mirror)
    })
    .await?;
    Ok(Json(json!({ "standings": standings, "calls": calls })))
}