sendword 0.9.0

Simple HTTP webhook to command runner sidecar. Frontend for managing hooks, JSON state for config portability, SQLite for execution history and logs.
Documentation
use std::sync::Arc;

use axum::Router;
use axum::extract::{Query, State};
use axum::response::Html;
use axum::routing::get;
use serde::Deserialize;

use crate::error::AppError;
use crate::extractors::AuthUser;
use crate::models::execution;
use crate::server::AppState;
use crate::views::FlashMessages;
use crate::views::dashboard::{DashboardHookRow, DashboardStatusDot, render_dashboard_page};

#[derive(Deserialize)]
struct FlashParams {
    success: Option<String>,
    error: Option<String>,
}

pub fn router() -> Router<Arc<AppState>> {
    Router::new().route("/", get(dashboard))
}

async fn dashboard(
    AuthUser(auth): AuthUser,
    State(state): State<Arc<AppState>>,
    Query(flash): Query<FlashParams>,
) -> Result<Html<String>, AppError> {
    let config = state.config.load();
    let pool = state.db.pool();

    let mut hooks = Vec::with_capacity(config.hooks.len());
    for h in &config.hooks {
        let recent = match execution::list_recent_by_hook(pool, &h.slug, 5).await {
            Ok(execs) => execs,
            Err(e) => {
                tracing::warn!(hook = %h.slug, error = %e, "failed to fetch recent executions");
                Vec::new()
            }
        };

        let last = recent.first();

        let recent_statuses = recent
            .iter()
            .rev()
            .map(|e| DashboardStatusDot::from_execution_status(&e.status))
            .collect();

        hooks.push(DashboardHookRow::new(
            &h.name,
            &h.slug,
            h.enabled,
            last.map(|e| e.triggered_at.clone()),
            recent_statuses,
        ));
    }

    render_dashboard_page(
        auth.email.as_str(),
        &hooks,
        FlashMessages {
            success: flash.success.as_deref(),
            error: flash.error.as_deref(),
        },
    )
}