fabro/lib/crates/fabro-server/src/demo/mod.rs
Bryan Helmkamp 5fc9157017
refactor(workflow): remove retro stage (#230)
## Summary

Removes Fabro's automatic retro generation stage so workflow runs go
directly from execution to finalization and optional PR creation. This
drops the retro-specific crate, events, projection fields, config/API
knobs, and user-facing docs in favor of the existing durable run
observability surfaces.

## What Changed

- Deleted the `fabro-retro` crate and the workflow `retro` pipeline
phase, with finalization now consuming `Executed` state directly.
- Removed retro configuration and API surface area, including
`--no-retro`, `[run.execution].retros`, manifest `no_retro`,
`features.retros`, and run projection `retro*` fields.
- Retired typed `retro.*` events while keeping historical event logs
readable by deserializing retired retro event names as `Unknown`.
- Stopped appending retro sections to generated PR bodies and updated
docs, marketing copy, screenshots, and navigation to point users toward
observability/event-stream inspection.

## Testing

Not run during PR creation; this branch already contained the
implementation commit.

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (unknown context, reasoning unspecified) via
[Codex](https://openai.com/codex)
2026-05-09 10:18:20 -04:00

2062 lines
71 KiB
Rust

//! Demo mode handlers that return static data for all API endpoints.
//! Activated per-request via the `X-Fabro-Demo: 1` header to showcase the UI
//! without a real backend.
#![allow(
clippy::default_trait_access,
clippy::unreadable_literal,
reason = "Demo fixture data favors literal fidelity over pedantic style lints."
)]
use std::sync::Arc;
use axum::Json;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::sse::{Event, Sse};
use axum::response::{IntoResponse, Response};
use fabro_api::types::{
CreateSecretRequest, DeleteSecretRequest, DiffFile, DiffStats, EventEnvelope, FileDiff,
FileDiffChangeKind, PaginatedEventList, PaginatedRunFileList, PaginationMeta,
RunArtifactListResponse, RunFilesMeta,
};
use serde_json::json;
use crate::error::ApiError;
use crate::principal_middleware::RequiredUser;
use crate::run_selector::{ResolveRunError, resolve_run_by_selector};
use crate::server::{AppState, EventListParams, PaginationParams, parse_stage_id_path};
fn paginated_response<T: serde::Serialize>(
items: Vec<T>,
pagination: &PaginationParams,
) -> Response {
let limit = pagination.limit.clamp(1, 100) as usize;
let offset = pagination.offset as usize;
let mut data: Vec<_> = items.into_iter().skip(offset).take(limit + 1).collect();
let has_more = data.len() > limit;
data.truncate(limit);
(
StatusCode::OK,
Json(json!({ "data": data, "meta": { "has_more": has_more } })),
)
.into_response()
}
// ── Runs ───────────────────────────────────────────────────────────────
pub(crate) async fn list_runs(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Query(pagination): Query<PaginationParams>,
) -> Response {
paginated_response(runs::summaries(), &pagination)
}
pub(crate) async fn list_board_runs(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Query(pagination): Query<PaginationParams>,
) -> Response {
let items = runs::board_items();
let limit = pagination.limit.clamp(1, 100) as usize;
let offset = pagination.offset as usize;
let mut data: Vec<_> = items.into_iter().skip(offset).take(limit + 1).collect();
let has_more = data.len() > limit;
data.truncate(limit);
(
StatusCode::OK,
Json(json!({
"columns": runs::columns(),
"data": data,
"meta": { "has_more": has_more }
})),
)
.into_response()
}
pub(crate) async fn create_run_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
) -> Response {
(
StatusCode::CREATED,
Json(serde_json::json!({"id": "demo-run-new", "status": "submitted", "created_at": "2026-03-06T14:30:00Z"})),
)
.into_response()
}
pub(crate) async fn resolve_run(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Query(params): Query<ResolveRunParams>,
) -> Response {
let runs = runs::summaries();
match resolve_run_by_selector(
&runs,
&params.selector,
|run| run.run_id.to_string(),
|run| run.workflow_slug.clone(),
|run| run.workflow_name.clone(),
|run| run.created_at,
|run| run.created_at.to_rfc3339(),
|run| run.repo_origin_url.clone(),
) {
Ok(run) => (StatusCode::OK, Json(run.clone())).into_response(),
Err(ResolveRunError::InvalidSelector | ResolveRunError::AmbiguousPrefix { .. }) => {
ApiError::bad_request("Run selector could not be resolved.").into_response()
}
Err(ResolveRunError::NotFound { .. }) => {
ApiError::not_found("Run not found.").into_response()
}
}
}
pub(crate) async fn start_run_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Response {
(
StatusCode::OK,
Json(
serde_json::json!({"id": id, "status": "queued", "created_at": "2026-03-06T14:30:00Z"}),
),
)
.into_response()
}
pub(crate) async fn get_run_stages(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
Query(pagination): Query<PaginationParams>,
) -> Response {
paginated_response(runs::stages(), &pagination)
}
pub(crate) async fn get_stage_events(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path((_id, stage_id)): Path<(String, String)>,
Query(params): Query<EventListParams>,
) -> Response {
let stage_id = match parse_stage_id_path(&stage_id) {
Ok(stage_id) => stage_id,
Err(response) => return response,
};
let since_seq = params.since_seq();
let limit = params.limit();
let mut matches: Vec<EventEnvelope> = runs::stage_events()
.into_iter()
.filter(|envelope| {
envelope.seq >= since_seq
&& (envelope.event.stage_id.as_ref() == Some(&stage_id)
|| (envelope.event.stage_id.is_none()
&& stage_id.visit() == 1
&& envelope.event.node_id.as_deref() == Some(stage_id.node_id())))
})
.take(limit + 1)
.collect();
let has_more = matches.len() > limit;
matches.truncate(limit);
(
StatusCode::OK,
Json(PaginatedEventList {
data: matches,
meta: PaginationMeta { has_more },
}),
)
.into_response()
}
pub(crate) async fn list_run_artifacts_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
(
StatusCode::OK,
Json(RunArtifactListResponse { data: vec![] }),
)
.into_response()
}
/// Demo-mode handler for `GET /runs/{id}/files`. Returns a small
/// illustrative diff without touching run store state — the `_id` and
/// `_state` parameters are intentionally ignored so demo responses cannot
/// cross-contaminate with real run data (R34).
pub(crate) async fn list_run_files_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
(StatusCode::OK, Json(demo_run_files())).into_response()
}
fn demo_run_files() -> PaginatedRunFileList {
let old_main = "import { parseArgs } from \"node:util\";\n\nexport function run(argv: string[]) {\n const { values } = parseArgs({ args: argv, options: { config: { type: \"string\" } } });\n console.log(values.config);\n}\n";
let new_main = "import { parseArgs } from \"node:util\";\nimport { loadConfig } from \"./config.js\";\n\nexport async function run(argv: string[]) {\n const { values } = parseArgs({ args: argv, options: { config: { type: \"string\" } } });\n const config = await loadConfig(values.config ?? \".fabro/project.toml\");\n console.log(JSON.stringify(config, null, 2));\n}\n";
let new_config = "import { readFile } from \"node:fs/promises\";\nimport { parse as parseToml } from \"@iarna/toml\";\n\nexport async function loadConfig(path: string) {\n const contents = await readFile(path, \"utf8\");\n return parseToml(contents);\n}\n";
PaginatedRunFileList {
data: vec![
FileDiff {
binary: None,
change_kind: Some(FileDiffChangeKind::Modified),
new_file: DiffFile {
name: "src/commands/run.ts".to_string(),
contents: Some(new_main.to_string()),
},
old_file: DiffFile {
name: "src/commands/run.ts".to_string(),
contents: Some(old_main.to_string()),
},
sensitive: None,
truncated: None,
truncation_reason: None,
unified_patch: None,
},
FileDiff {
binary: None,
change_kind: Some(FileDiffChangeKind::Added),
new_file: DiffFile {
name: "src/config.ts".to_string(),
contents: Some(new_config.to_string()),
},
old_file: DiffFile {
name: String::new(),
contents: Some(String::new()),
},
sensitive: None,
truncated: None,
truncation_reason: None,
unified_patch: None,
},
FileDiff {
binary: None,
change_kind: Some(FileDiffChangeKind::Renamed),
new_file: DiffFile {
name: "src/legacy/old-runner.ts".to_string(),
contents: Some("export const legacy = true;\n".to_string()),
},
old_file: DiffFile {
name: "src/old-runner.ts".to_string(),
contents: Some("export const legacy = true;\n".to_string()),
},
sensitive: None,
truncated: None,
truncation_reason: None,
unified_patch: None,
},
],
meta: RunFilesMeta {
truncated: false,
files_omitted_by_budget: None,
total_changed: 3,
stats: DiffStats {
additions: 42,
deletions: 11,
},
to_sha: None,
to_sha_committed_at: None,
degraded: Some(false),
degraded_reason: None,
},
}
}
pub(crate) async fn get_run_billing(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
(StatusCode::OK, Json(runs::billing())).into_response()
}
pub(crate) async fn get_run_settings(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
(StatusCode::OK, Json(runs::settings())).into_response()
}
pub(crate) async fn generate_preview_url_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
(
StatusCode::CREATED,
Json(serde_json::json!({"url": "https://google.com", "token": "demo-preview-token"})),
)
.into_response()
}
pub(crate) async fn create_ssh_access_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
(
StatusCode::CREATED,
Json(serde_json::json!({"command": "ssh demo@fabro.example"})),
)
.into_response()
}
pub(crate) async fn list_sandbox_files_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
(
StatusCode::OK,
Json(serde_json::json!({
"data": [
{ "name": "report.txt", "is_dir": false, "size": 12 },
{ "name": "logs", "is_dir": true }
]
})),
)
.into_response()
}
pub(crate) async fn get_sandbox_file_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
(StatusCode::OK, "demo sandbox file").into_response()
}
pub(crate) async fn put_sandbox_file_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
StatusCode::NO_CONTENT.into_response()
}
pub(crate) async fn get_run_status(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Response {
match runs::summaries()
.into_iter()
.find(|run| run.run_id.to_string() == id)
{
Some(run) => (StatusCode::OK, Json(run)).into_response(),
None => ApiError::not_found("Run not found.").into_response(),
}
}
#[derive(Debug, serde::Deserialize)]
pub(crate) struct ResolveRunParams {
selector: String,
}
pub(crate) async fn get_questions_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
Query(pagination): Query<PaginationParams>,
) -> Response {
paginated_response(runs::questions(), &pagination)
}
pub(crate) async fn answer_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path((_id, _qid)): Path<(String, String)>,
) -> Response {
StatusCode::NO_CONTENT.into_response()
}
pub(crate) async fn run_events_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
let events = vec![Ok::<_, std::convert::Infallible>(
Event::default().data(
json!({
"seq": 2,
"id": "evt_demo_attach_completed",
"ts": "2026-04-06T15:00:02Z",
"run_id": "01JQ0000000000000000000001",
"event": "run.completed",
"properties": {
"duration_ms": 42,
"artifact_count": 0,
"status": "succeeded"
}
})
.to_string(),
),
)];
Sse::new(tokio_stream::iter(events)).into_response()
}
pub(crate) async fn checkpoint_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
(StatusCode::OK, Json(serde_json::json!(null))).into_response()
}
pub(crate) async fn cancel_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Response {
(
StatusCode::OK,
Json(serde_json::json!({
"id": id,
"status": { "kind": "failed", "reason": "cancelled" },
"created_at": "2026-03-06T14:30:00Z"
})),
)
.into_response()
}
pub(crate) async fn pause_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Response {
(
StatusCode::OK,
Json(
serde_json::json!({"id": id, "status": "paused", "created_at": "2026-03-06T14:30:00Z"}),
),
)
.into_response()
}
pub(crate) async fn unpause_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Response {
(StatusCode::OK, Json(serde_json::json!({"id": id, "status": "running", "created_at": "2026-03-06T14:30:00Z"}))).into_response()
}
const DEMO_GRAPH_DOT: &str = "digraph demo {\n graph [goal=\"Demo\"]\n rankdir=LR\n start [shape=Mdiamond, label=\"Start\"]\n detect [label=\"Detect\\nDrift\"]\n exit [shape=Msquare, label=\"Exit\"]\n propose [label=\"Propose\\nChanges\"]\n review [label=\"Review\\nChanges\"]\n apply [label=\"Apply\\nChanges\"]\n start -> detect\n detect -> exit [label=\"No drift\"]\n detect -> propose [label=\"Drift found\"]\n propose -> review\n review -> propose [label=\"Revise\"]\n review -> apply [label=\"Accept\"]\n apply -> exit\n}";
pub(crate) async fn get_run_graph(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
crate::server::render_graph_bytes(DEMO_GRAPH_DOT).await
}
pub(crate) async fn get_run_graph_source(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
(
StatusCode::OK,
[("content-type", "text/vnd.graphviz")],
DEMO_GRAPH_DOT,
)
.into_response()
}
pub(crate) async fn list_secrets(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
) -> Response {
(
StatusCode::OK,
Json(json!({
"data": [
{
"name": "OPENAI_API_KEY",
"type": "environment",
"created_at": "2026-04-05T12:00:00Z",
"updated_at": "2026-04-05T12:00:00Z"
},
{
"name": "GITHUB_APP_PRIVATE_KEY",
"type": "environment",
"created_at": "2026-04-05T12:05:00Z",
"updated_at": "2026-04-05T12:05:00Z"
}
]
})),
)
.into_response()
}
pub(crate) async fn create_secret(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Json(body): Json<CreateSecretRequest>,
) -> Response {
let mut payload = serde_json::Map::new();
payload.insert("name".to_string(), json!(body.name));
payload.insert("type".to_string(), json!(body.type_));
if let Some(description) = body.description {
payload.insert("description".to_string(), json!(description));
}
payload.insert("created_at".to_string(), json!("2026-04-05T12:00:00Z"));
payload.insert("updated_at".to_string(), json!("2026-04-05T12:00:00Z"));
(StatusCode::OK, Json(serde_json::Value::Object(payload))).into_response()
}
pub(crate) async fn delete_secret_by_name(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Json(_body): Json<DeleteSecretRequest>,
) -> Response {
StatusCode::NO_CONTENT.into_response()
}
pub(crate) async fn get_github_repo(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path((owner, name)): Path<(String, String)>,
) -> Response {
(
StatusCode::OK,
Json(json!({
"owner": owner,
"name": name,
"accessible": false,
"default_branch": null,
"private": null,
"permissions": null,
"install_url": "https://github.com/apps/fabro/installations/new"
})),
)
.into_response()
}
pub(crate) async fn run_diagnostics(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
) -> Response {
(
StatusCode::OK,
Json(json!({
"version": fabro_util::version::FABRO_VERSION,
"sections": [
{
"title": "Credentials",
"checks": [
{ "name": "LLM Providers", "status": "pass", "summary": "demo configured", "details": [], "remediation": null },
{ "name": "GitHub App", "status": "pass", "summary": "demo configured", "details": [], "remediation": null },
{ "name": "Sandbox", "status": "warning", "summary": "not configured", "details": [], "remediation": "Set DAYTONA_API_KEY to enable cloud sandbox execution" },
{ "name": "Brave Search", "status": "warning", "summary": "not configured", "details": [], "remediation": "Set BRAVE_SEARCH_API_KEY to enable web search" }
]
},
{
"title": "Configuration",
"checks": [
{ "name": "Crypto", "status": "pass", "summary": "all keys valid", "details": [], "remediation": null }
]
}
]
})),
)
.into_response()
}
// ── Insights ───────────────────────────────────────────────────────────
pub(crate) async fn list_saved_queries(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Query(pagination): Query<PaginationParams>,
) -> Response {
paginated_response(insights::saved_queries(), &pagination)
}
pub(crate) async fn save_query_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
) -> Response {
(
StatusCode::CREATED,
Json(serde_json::json!({"id": "new-q", "name": "New Query", "sql": "SELECT 1", "created_at": "2026-03-06T16:00:00Z"})),
)
.into_response()
}
pub(crate) async fn get_saved_query(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Response {
match insights::saved_queries().into_iter().find(|q| q.id == id) {
Some(query) => (StatusCode::OK, Json(query)).into_response(),
None => ApiError::not_found("Saved query not found.").into_response(),
}
}
pub(crate) async fn update_query_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
(
StatusCode::OK,
Json(serde_json::json!({"id": "1", "name": "Updated", "sql": "SELECT 1", "created_at": "2026-03-01T10:00:00Z", "updated_at": "2026-03-06T16:00:00Z"})),
)
.into_response()
}
pub(crate) async fn delete_query_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
StatusCode::NO_CONTENT.into_response()
}
pub(crate) async fn execute_query_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
) -> Response {
(
StatusCode::OK,
Json(serde_json::json!({
"columns": ["workflow_name", "count"],
"rows": [["implement", 42], ["fix_build", 18], ["sync_drift", 7]],
"elapsed": 0.342,
"row_count": 3
})),
)
.into_response()
}
pub(crate) async fn list_query_history(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Query(pagination): Query<PaginationParams>,
) -> Response {
paginated_response(insights::history(), &pagination)
}
// ── Workflows ─────────────────────────────────────────────────────────
pub(crate) async fn list_workflows(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Query(pagination): Query<PaginationParams>,
) -> Response {
let limit = pagination.limit.clamp(1, 100) as usize;
let offset = pagination.offset as usize;
let mut data: Vec<_> = workflows::list_items()
.into_iter()
.skip(offset)
.take(limit + 1)
.collect();
let has_more = data.len() > limit;
data.truncate(limit);
(
StatusCode::OK,
Json(json!({
"data": data,
"pagination": { "has_more": has_more }
})),
)
.into_response()
}
pub(crate) async fn get_workflow(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(name): Path<String>,
) -> Response {
match workflows::detail(&name) {
Some(workflow) => (StatusCode::OK, Json(workflow)).into_response(),
None => ApiError::not_found("Workflow not found.").into_response(),
}
}
pub(crate) async fn list_workflow_runs(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Path(name): Path<String>,
Query(pagination): Query<PaginationParams>,
) -> Response {
if workflows::detail(&name).is_none() {
return ApiError::not_found("Workflow not found.").into_response();
}
let runs = runs::summaries()
.into_iter()
.filter(|run| run.workflow_slug.as_deref() == Some(&name))
.collect();
paginated_response(runs, &pagination)
}
// ── Settings ───────────────────────────────────────────────────────────
pub(crate) async fn get_server_settings(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
) -> Response {
(StatusCode::OK, Json(settings::server_settings())).into_response()
}
// ── System ────────────────────────────────────────────────────────────
pub(crate) async fn attach_events_stub(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
) -> Response {
let events = vec![
Ok::<_, std::convert::Infallible>(
Event::default().data(
json!({
"seq": 1,
"id": "evt_demo_1",
"ts": "2026-04-06T15:00:00Z",
"run_id": "01JQ0000000000000000000001",
"event": "run.started"
})
.to_string(),
),
),
Ok::<_, std::convert::Infallible>(
Event::default().data(
json!({
"seq": 2,
"id": "evt_demo_2",
"ts": "2026-04-06T15:00:01Z",
"run_id": "01JQ0000000000000000000001",
"event": "stage.started"
})
.to_string(),
),
),
];
Sse::new(tokio_stream::iter(events)).into_response()
}
pub(crate) async fn get_system_info(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
) -> Response {
(
StatusCode::OK,
Json(json!({
"version": env!("CARGO_PKG_VERSION"),
"server_url": "http://localhost:3000",
"git_sha": option_env!("FABRO_GIT_SHA"),
"build_date": option_env!("FABRO_BUILD_DATE"),
"profile": option_env!("FABRO_BUILD_PROFILE"),
"os": std::env::consts::OS,
"arch": std::env::consts::ARCH,
"storage_engine": "slatedb",
"storage_dir": "/demo/fabro/storage",
"uptime_secs": 42,
"runs": { "total": 3, "active": 1 },
"sandbox_provider": "local",
"features": { "session_sandboxes": false }
})),
)
.into_response()
}
pub(crate) async fn get_system_disk_usage(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
Query(params): Query<crate::server::DfParams>,
) -> Response {
let runs = params.verbose.then(|| {
json!([
{
"run_id": "01JQ0000000000000000000001",
"workflow_name": "Demo Workflow",
"status": "succeeded",
"start_time": "2026-04-06T15:00:00Z",
"size_bytes": 1024,
"reclaimable": true
}
])
});
(
StatusCode::OK,
Json(json!({
"summary": [
{
"type": "runs",
"count": 1,
"active": 0,
"size_bytes": 1024,
"reclaimable_bytes": 1024
},
{
"type": "logs",
"count": 1,
"active": null,
"size_bytes": 256,
"reclaimable_bytes": 256
}
],
"total_size_bytes": 1280,
"total_reclaimable_bytes": 1280,
"runs": runs
})),
)
.into_response()
}
pub(crate) async fn get_system_repair_runs(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
) -> Response {
(
StatusCode::OK,
Json(json!({
"runs": [],
"total_count": 0
})),
)
.into_response()
}
pub(crate) async fn prune_runs(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
) -> Response {
(
StatusCode::OK,
Json(json!({
"dry_run": true,
"runs": [
{
"run_id": "01JQ0000000000000000000001",
"dir_name": "20260406-01JQ0000000000000000000001",
"workflow_name": "Demo Workflow",
"size_bytes": 1024
}
],
"total_count": 1,
"total_size_bytes": 1024,
"deleted_count": 0,
"freed_bytes": 0
})),
)
.into_response()
}
// ── Usage ──────────────────────────────────────────────────────────────
pub(crate) async fn get_aggregate_billing(
_auth: RequiredUser,
State(_state): State<Arc<AppState>>,
) -> Response {
(StatusCode::OK, Json(billing::aggregate())).into_response()
}
// ── Data modules ───────────────────────────────────────────────────────
use chrono::{DateTime, Utc};
fn ts(s: &str) -> DateTime<Utc> {
s.parse().expect("hardcoded demo timestamp should parse")
}
mod runs {
use std::collections::HashMap;
use std::sync::OnceLock;
use std::time::Duration;
use fabro_api::types::*;
use fabro_types::settings::run::{
DaytonaSettings, DaytonaSnapshotSettings, LocalSandboxSettings, RunGoal, RunModelSettings,
RunNamespace, RunPrepareSettings, RunSandboxSettings,
};
use fabro_types::settings::{InterpString, ProjectNamespace, WorkflowNamespace};
use fabro_types::{RunId, StageId, WorkflowSettings};
use super::ts;
use crate::server::run_stage_from_stage_id;
fn labels(entries: &[(&str, &str)]) -> HashMap<String, String> {
entries
.iter()
.map(|(key, value)| ((*key).to_string(), (*value).to_string()))
.collect()
}
fn demo_run_ids() -> &'static [RunId; 7] {
static IDS: OnceLock<[RunId; 7]> = OnceLock::new();
IDS.get_or_init(|| {
[
RunId::with_timestamp(ts("2026-03-06T14:30:00Z"), 1),
RunId::with_timestamp(ts("2026-03-06T12:00:00Z"), 2),
RunId::with_timestamp(ts("2026-03-04T15:00:00Z"), 3),
RunId::with_timestamp(ts("2026-03-04T10:00:00Z"), 4),
RunId::with_timestamp(ts("2026-03-03T16:45:00Z"), 5),
RunId::with_timestamp(ts("2026-02-28T14:00:00Z"), 6),
RunId::with_timestamp(ts("2026-03-06T14:35:00Z"), 7),
]
})
}
fn demo_run_id(index: usize) -> RunId {
demo_run_ids()[index - 1]
}
fn summary(
sequence: u128,
repo_name: &str,
workflow_slug: &str,
workflow_name: &str,
goal: &str,
status: &str,
created_at: &str,
elapsed_secs: Option<f64>,
status_reason: Option<&str>,
pending_control: Option<RunControlAction>,
total_usd_micros: Option<i64>,
entries: &[(&str, &str)],
) -> RunSummary {
let created_at = ts(created_at);
let run_id = RunId::with_timestamp(created_at, sequence);
RunSummary::new(
run_id,
Some(workflow_name.into()),
Some(workflow_slug.into()),
goal.into(),
labels(entries),
Some(format!("/demo/{repo_name}")),
false,
Some(format!("https://github.com/demo/{repo_name}.git")),
Some(created_at),
Some(created_at),
parse_run_status(status, status_reason)
.unwrap_or_else(|| panic!("invalid demo run status: {status}")),
pending_control,
elapsed_secs.and_then(duration_ms_from_secs),
total_usd_micros,
None,
None,
None,
)
}
fn parse_run_status(status: &str, status_reason: Option<&str>) -> Option<RunStatus> {
match status {
"submitted" => Some(RunStatus::Submitted),
"queued" => Some(RunStatus::Queued),
"starting" => Some(RunStatus::Starting),
"running" => Some(RunStatus::Running),
"blocked" => Some(RunStatus::Blocked {
blocked_reason: BlockedReason::HumanInputRequired,
}),
"paused" => Some(RunStatus::Paused { prior_block: None }),
"removing" => Some(RunStatus::Removing),
"succeeded" => Some(RunStatus::Succeeded {
reason: status_reason
.and_then(parse_success_reason)
.unwrap_or(SuccessReason::Completed),
}),
"failed" => Some(RunStatus::Failed {
reason: status_reason
.and_then(parse_failure_reason)
.unwrap_or(FailureReason::WorkflowError),
}),
"dead" => Some(RunStatus::Dead),
_ => None,
}
}
fn parse_success_reason(reason: &str) -> Option<SuccessReason> {
match reason {
"completed" => Some(SuccessReason::Completed),
"partial_success" => Some(SuccessReason::PartialSuccess),
_ => None,
}
}
fn parse_failure_reason(reason: &str) -> Option<FailureReason> {
match reason {
"workflow_error" => Some(FailureReason::WorkflowError),
"cancelled" => Some(FailureReason::Cancelled),
"terminated" => Some(FailureReason::Terminated),
"transient_infra" => Some(FailureReason::TransientInfra),
"budget_exhausted" => Some(FailureReason::BudgetExhausted),
"launch_failed" => Some(FailureReason::LaunchFailed),
"bootstrap_failed" => Some(FailureReason::BootstrapFailed),
"sandbox_init_failed" => Some(FailureReason::SandboxInitFailed),
_ => None,
}
}
fn duration_ms_from_secs(secs: f64) -> Option<u64> {
let duration = Duration::try_from_secs_f64(secs).ok()?;
duration.as_millis().try_into().ok()
}
fn take_summary(summaries: &mut HashMap<RunId, RunSummary>, run_id: RunId) -> RunSummary {
summaries
.remove(&run_id)
.unwrap_or_else(|| panic!("missing demo summary: {run_id}"))
}
fn board_item(
summary: RunSummary,
column: BoardColumn,
pull_request: Option<RunPullRequest>,
sandbox: Option<RunSandbox>,
question: Option<RunQuestion>,
) -> RunListItem {
RunListItem {
column,
created_at: summary.created_at,
last_event_at: summary.last_event_at,
duration_ms: summary.duration_ms.and_then(|ms| i64::try_from(ms).ok()),
elapsed_secs: summary.elapsed_secs,
goal: summary.goal,
source_directory: summary.source_directory,
in_place: Some(summary.in_place),
repo_origin_url: summary.repo_origin_url,
labels: summary.labels,
pending_control: summary.pending_control,
pull_request,
question,
repository: summary.repository,
run_id: summary.run_id.to_string(),
sandbox,
start_time: summary.start_time,
status: summary.status,
title: summary.title,
total_usd_micros: summary.total_usd_micros,
workflow_name: summary.workflow_name,
workflow_slug: summary.workflow_slug,
}
}
fn check(name: &str, status: CheckRunStatus, duration_secs: Option<f64>) -> CheckRun {
CheckRun {
name: name.into(),
status,
duration_secs,
}
}
fn sandbox(id: &str, cpu: i64, memory: i64) -> RunSandbox {
RunSandbox {
id: Some(id.to_string()),
working_directory: Some("/workspace".to_string()),
resources: Some(SandboxResources { cpu, memory }),
}
}
fn pull_request(
number: i64,
additions: i64,
deletions: i64,
comments: i64,
checks: Vec<CheckRun>,
) -> RunPullRequest {
RunPullRequest {
number,
html_url: Some(format!("https://github.com/demo/fabro/pull/{number}")),
additions: Some(additions),
deletions: Some(deletions),
comments: Some(comments),
checks,
}
}
pub(super) fn columns() -> Vec<BoardColumnDefinition> {
vec![
BoardColumnDefinition {
id: BoardColumn::Queued,
name: "Queued".into(),
},
BoardColumnDefinition {
id: BoardColumn::Initializing,
name: "Initializing".into(),
},
BoardColumnDefinition {
id: BoardColumn::Running,
name: "Running".into(),
},
BoardColumnDefinition {
id: BoardColumn::Blocked,
name: "Blocked".into(),
},
BoardColumnDefinition {
id: BoardColumn::Succeeded,
name: "Succeeded".into(),
},
BoardColumnDefinition {
id: BoardColumn::Failed,
name: "Failed".into(),
},
]
}
pub(super) fn summaries() -> Vec<RunSummary> {
vec![
summary(
1,
"api-server",
"implement",
"Implement",
"Add rate limiting to auth endpoints",
"running",
"2026-03-06T14:30:00Z",
Some(420.0),
None,
None,
None,
&[("branch", "rate-limit"), ("team", "platform")],
),
summary(
2,
"web-dashboard",
"implement",
"Implement",
"Migrate to React Router v7",
"running",
"2026-03-06T12:00:00Z",
Some(8100.0),
None,
Some(RunControlAction::Pause),
None,
&[("owner", "frontend")],
),
summary(
3,
"shared-types",
"expand",
"Expand",
"Update OpenAPI spec for v3",
"starting",
"2026-03-04T15:00:00Z",
Some(4320.0),
None,
None,
None,
&[("priority", "high")],
),
summary(
4,
"shared-types",
"implement",
"Implement",
"Add pipeline event types",
"blocked",
"2026-03-04T10:00:00Z",
Some(1680.0),
None,
None,
None,
&[("owner", "runtime")],
),
summary(
5,
"web-dashboard",
"implement",
"Implement",
"Add dark mode toggle",
"failed",
"2026-03-03T16:45:00Z",
Some(2100.0),
Some("workflow_error"),
None,
None,
&[("environment", "staging")],
),
summary(
6,
"api-server",
"implement",
"Implement",
"Implement webhook retry logic",
"succeeded",
"2026-02-28T14:00:00Z",
Some(259200.0),
Some("completed"),
None,
Some(720000),
&[("release", "preview")],
),
summary(
7,
"api-server",
"implement",
"Implement",
"Add audit log retention policy",
"queued",
"2026-03-06T14:35:00Z",
None,
None,
None,
None,
&[("owner", "platform")],
),
]
}
pub(super) fn board_items() -> Vec<RunListItem> {
let mut summaries = summaries()
.into_iter()
.map(|summary| (summary.run_id, summary))
.collect::<HashMap<_, _>>();
vec![
board_item(
take_summary(&mut summaries, demo_run_id(1)),
BoardColumn::Running,
None,
Some(sandbox("sb-a1b2c3d4", 4, 8)),
None,
),
board_item(
take_summary(&mut summaries, demo_run_id(2)),
BoardColumn::Running,
None,
Some(sandbox("sb-e5f6g7h8", 8, 16)),
None,
),
board_item(
take_summary(&mut summaries, demo_run_id(3)),
BoardColumn::Initializing,
Some(pull_request(0, 567, 234, 0, vec![])),
Some(sandbox("sb-q7r8s9t0", 4, 8)),
Some(RunQuestion {
text: "Accept or push for another round?".into(),
}),
),
board_item(
take_summary(&mut summaries, demo_run_id(4)),
BoardColumn::Blocked,
Some(pull_request(0, 145, 23, 0, vec![])),
Some(sandbox("sb-u1v2w3x4", 4, 8)),
Some(RunQuestion {
text: "Proceed from investigation to fix?".into(),
}),
),
board_item(
take_summary(&mut summaries, demo_run_id(5)),
BoardColumn::Failed,
Some(pull_request(889, 234, 67, 4, vec![
check("lint", CheckRunStatus::Success, Some(23.0)),
check("typecheck", CheckRunStatus::Success, Some(72.0)),
check("unit-tests", CheckRunStatus::Success, Some(154.0)),
check("integration-tests", CheckRunStatus::Failure, Some(296.0)),
check("build", CheckRunStatus::Success, Some(105.0)),
])),
None,
None,
),
board_item(
take_summary(&mut summaries, demo_run_id(6)),
BoardColumn::Succeeded,
Some(pull_request(1249, 189, 45, 7, vec![
check("lint", CheckRunStatus::Success, Some(21.0)),
check("typecheck", CheckRunStatus::Success, Some(68.0)),
check("unit-tests", CheckRunStatus::Success, Some(192.0)),
check("integration-tests", CheckRunStatus::Success, Some(334.0)),
check("deploy-preview", CheckRunStatus::Success, Some(93.0)),
])),
None,
None,
),
board_item(
take_summary(&mut summaries, demo_run_id(7)),
BoardColumn::Queued,
None,
None,
None,
),
]
}
pub(super) fn stages() -> Vec<RunStage> {
vec![
run_stage_from_stage_id(
&StageId::new("detect-drift", 1),
"Detect Drift",
StageState::Succeeded,
Some(72.0),
None,
StageHandler::Command,
),
run_stage_from_stage_id(
&StageId::new("propose-changes", 1),
"Propose Changes",
StageState::Succeeded,
Some(154.0),
None,
StageHandler::Agent,
),
run_stage_from_stage_id(
&StageId::new("review-changes", 1),
"Review Changes",
StageState::Succeeded,
Some(45.0),
None,
StageHandler::Agent,
),
run_stage_from_stage_id(
&StageId::new("apply-changes", 1),
"Apply Changes",
StageState::Succeeded,
Some(118.0),
None,
StageHandler::Command,
),
run_stage_from_stage_id(
&StageId::new("apply-changes", 2),
"Apply Changes",
StageState::Running,
None,
None,
StageHandler::Command,
),
]
}
pub(super) fn stage_events() -> Vec<fabro_types::EventEnvelope> {
use fabro_model::BilledTokenCounts;
use fabro_types::run_event::agent::{
AgentMessageProps, AgentToolCompletedProps, AgentToolStartedProps,
};
use fabro_types::run_event::stage::StagePromptProps;
use fabro_types::{EventBody, EventEnvelope, RunEvent};
let run_id = demo_run_id(1);
let node_id = "detect-drift";
let stage_id = fabro_types::StageId::new(node_id, 1);
let ts = ts("2026-03-06T14:30:00Z");
let make_envelope = |seq: u32, id: &str, body: EventBody| EventEnvelope {
seq,
event: RunEvent {
id: id.into(),
ts,
run_id,
node_id: Some(node_id.into()),
node_label: Some("Detect Drift".into()),
stage_id: Some(stage_id.clone()),
parallel_group_id: None,
parallel_branch_id: None,
session_id: None,
parent_session_id: None,
tool_call_id: None,
actor: None,
body,
},
};
vec![
make_envelope(
1,
"evt-detect-drift-1",
EventBody::StagePrompt(StagePromptProps {
visit: 1,
text: "You are a drift detection agent. Compare the production and staging environments and identify any configuration or code drift.".into(),
mode: None,
provider: None,
model: None,
}),
),
make_envelope(
2,
"evt-detect-drift-2",
EventBody::AgentMessage(AgentMessageProps {
text: "I'll start by loading the environment configurations for both production and staging to compare them.".into(),
model: "Opus 4.6".into(),
billing: BilledTokenCounts::default(),
tool_call_count: 0,
visit: 1,
}),
),
make_envelope(
3,
"evt-detect-drift-3",
EventBody::AgentToolStarted(AgentToolStartedProps {
tool_name: "read_file".into(),
tool_call_id: "toolu_01".into(),
arguments: serde_json::json!({ "path": "environments/production/config.toml" }),
visit: 1,
}),
),
make_envelope(
4,
"evt-detect-drift-4",
EventBody::AgentToolCompleted(AgentToolCompletedProps {
tool_name: "read_file".into(),
tool_call_id: "toolu_01".into(),
output: serde_json::json!("[redis]\nhost = \"redis-prod.internal\"\nport = 6379"),
is_error: false,
visit: 1,
}),
),
make_envelope(
5,
"evt-detect-drift-5",
EventBody::AgentToolStarted(AgentToolStartedProps {
tool_name: "read_file".into(),
tool_call_id: "toolu_02".into(),
arguments: serde_json::json!({ "path": "environments/staging/config.toml" }),
visit: 1,
}),
),
make_envelope(
6,
"evt-detect-drift-6",
EventBody::AgentToolCompleted(AgentToolCompletedProps {
tool_name: "read_file".into(),
tool_call_id: "toolu_02".into(),
output: serde_json::json!("[redis]\nhost = \"redis-staging.internal\"\nport = 6379"),
is_error: false,
visit: 1,
}),
),
make_envelope(
7,
"evt-detect-drift-7",
EventBody::AgentMessage(AgentMessageProps {
text: "I've detected drift in 3 resources between production and staging:\n\n1. **redis.max_connections** — production has 200, staging has 100\n2. **redis.tls** — enabled in production, disabled in staging\n3. **iam.session_duration** — production uses 3600s, staging uses 1800s".into(),
model: "Opus 4.6".into(),
billing: BilledTokenCounts::default(),
tool_call_count: 0,
visit: 1,
}),
),
]
}
pub(super) fn billing() -> RunBilling {
RunBilling {
stages: vec![
RunBillingStage {
stage: BillingStageRef {
id: "detect-drift".into(),
name: "Detect Drift".into(),
},
model: Some(ModelReference {
id: "Opus 4.6".into(),
}),
billing: BilledTokenCounts {
cache_read_tokens: 0,
cache_write_tokens: 0,
input_tokens: 12480,
output_tokens: 3210,
reasoning_tokens: 0,
total_tokens: 15690,
total_usd_micros: Some(480_000),
},
runtime_secs: 72.0,
started_at: None,
state: Some(StageState::Succeeded),
},
RunBillingStage {
stage: BillingStageRef {
id: "propose-changes".into(),
name: "Propose Changes".into(),
},
model: Some(ModelReference {
id: "Gemini 3.1".into(),
}),
billing: BilledTokenCounts {
cache_read_tokens: 0,
cache_write_tokens: 0,
input_tokens: 28640,
output_tokens: 8750,
reasoning_tokens: 0,
total_tokens: 37390,
total_usd_micros: Some(720_000),
},
runtime_secs: 154.0,
started_at: None,
state: Some(StageState::Succeeded),
},
RunBillingStage {
stage: BillingStageRef {
id: "review-changes".into(),
name: "Review Changes".into(),
},
model: Some(ModelReference {
id: "Codex 5.3".into(),
}),
billing: BilledTokenCounts {
cache_read_tokens: 0,
cache_write_tokens: 0,
input_tokens: 9120,
output_tokens: 2640,
reasoning_tokens: 0,
total_tokens: 11760,
total_usd_micros: Some(190_000),
},
runtime_secs: 45.0,
started_at: None,
state: Some(StageState::Succeeded),
},
RunBillingStage {
stage: BillingStageRef {
id: "apply-changes".into(),
name: "Apply Changes".into(),
},
model: Some(ModelReference {
id: "Opus 4.6".into(),
}),
billing: BilledTokenCounts {
cache_read_tokens: 0,
cache_write_tokens: 0,
input_tokens: 21300,
output_tokens: 6480,
reasoning_tokens: 0,
total_tokens: 27780,
total_usd_micros: Some(870_000),
},
runtime_secs: 118.0,
started_at: None,
state: Some(StageState::Running),
},
],
totals: RunBillingTotals {
cache_read_tokens: 0,
cache_write_tokens: 0,
runtime_secs: 389.0,
input_tokens: 71540,
output_tokens: 21080,
reasoning_tokens: 0,
total_tokens: 92620,
total_usd_micros: Some(2_260_000),
},
by_model: vec![
BillingByModel {
billing: BilledTokenCounts {
cache_read_tokens: 0,
cache_write_tokens: 0,
input_tokens: 33780,
output_tokens: 9690,
reasoning_tokens: 0,
total_tokens: 43470,
total_usd_micros: Some(1_350_000),
},
model: ModelReference {
id: "Opus 4.6".into(),
},
stages: 2,
},
BillingByModel {
billing: BilledTokenCounts {
cache_read_tokens: 0,
cache_write_tokens: 0,
input_tokens: 28640,
output_tokens: 8750,
reasoning_tokens: 0,
total_tokens: 37390,
total_usd_micros: Some(720_000),
},
model: ModelReference {
id: "Gemini 3.1".into(),
},
stages: 1,
},
BillingByModel {
billing: BilledTokenCounts {
cache_read_tokens: 0,
cache_write_tokens: 0,
input_tokens: 9120,
output_tokens: 2640,
reasoning_tokens: 0,
total_tokens: 11760,
total_usd_micros: Some(190_000),
},
model: ModelReference {
id: "Codex 5.3".into(),
},
stages: 1,
},
],
}
}
pub(super) fn questions() -> Vec<ApiQuestion> {
vec![
ApiQuestion {
id: "q-001".into(),
text: "Should we proceed with the proposed changes?".into(),
stage: "review".into(),
question_type: QuestionType::YesNo,
options: vec![
ApiQuestionOption {
key: "yes".into(),
label: "Yes".into(),
},
ApiQuestionOption {
key: "no".into(),
label: "No".into(),
},
],
allow_freeform: false,
timeout_seconds: None,
context_display: None,
},
ApiQuestion {
id: "q-002".into(),
text: "Which approach do you prefer for the migration?".into(),
stage: "migration".into(),
question_type: QuestionType::MultipleChoice,
options: vec![
ApiQuestionOption {
key: "incremental".into(),
label: "Incremental migration".into(),
},
ApiQuestionOption {
key: "big_bang".into(),
label: "Big-bang rewrite".into(),
},
],
allow_freeform: true,
timeout_seconds: None,
context_display: None,
},
]
}
pub(super) fn settings() -> serde_json::Value {
let settings = WorkflowSettings {
project: ProjectNamespace {
directory: "/workspace/api-server".into(),
..ProjectNamespace::default()
},
workflow: WorkflowNamespace {
graph: "workflow.fabro".into(),
..WorkflowNamespace::default()
},
run: RunNamespace {
goal: Some(RunGoal::Inline(InterpString::parse(
"Add rate limiting to auth endpoints",
))),
working_dir: Some(InterpString::parse("/workspace/api-server")),
model: RunModelSettings {
provider: Some(InterpString::parse("anthropic")),
name: Some(InterpString::parse("claude-opus-4-6")),
..RunModelSettings::default()
},
prepare: RunPrepareSettings {
commands: vec!["bun install".into(), "bun run typecheck".into()],
timeout_ms: 120_000,
},
sandbox: RunSandboxSettings {
provider: "daytona".into(),
preserve: false,
stop_on_terminal: true,
devcontainer: false,
env: HashMap::new(),
local: LocalSandboxSettings::default(),
docker: None,
daytona: Some(DaytonaSettings {
auto_stop_interval: Some(60),
labels: HashMap::from([(
"project".to_string(),
"api-server".to_string(),
)]),
snapshot: Some(DaytonaSnapshotSettings {
name: "api-server-dev".into(),
cpu: Some(4),
memory_gb: Some(8),
disk_gb: Some(10),
dockerfile: None,
}),
network: None,
skip_clone: false,
}),
},
..RunNamespace::default()
},
};
serde_json::to_value(settings).expect("demo workflow settings should serialize")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn summary_parses_known_status_reason_values() {
let summary = summary(
99,
"demo-repo",
"implement",
"Implement",
"Goal",
"failed",
"2026-03-06T14:30:00Z",
Some(1.0),
Some("cancelled"),
None,
None,
&[],
);
assert_eq!(summary.status, RunStatus::Failed {
reason: FailureReason::Cancelled,
});
}
#[test]
fn summary_ignores_unknown_status_reason() {
let summary = summary(
99,
"demo-repo",
"implement",
"Implement",
"Goal",
"failed",
"2026-03-06T14:30:00Z",
Some(1.0),
Some("unexpected_reason"),
None,
None,
&[],
);
assert_eq!(summary.status, RunStatus::Failed {
reason: FailureReason::WorkflowError,
});
}
#[test]
fn summary_derives_title_like_server() {
let goal = format!("## Plan: {}", "a".repeat(120));
let summary = summary(
99,
"demo-repo",
"implement",
"Implement",
&goal,
"running",
"2026-03-06T14:30:00Z",
Some(1.0),
None,
None,
None,
&[],
);
assert_eq!(summary.title, format!("{}...", "a".repeat(97)));
}
}
}
mod workflows {
use serde_json::{Value, json};
use super::runs;
const FIX_BUILD_GRAPH: &str = r#"digraph fix_build {
graph [goal="Diagnose and fix CI build failures", label="Fix Build"]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
diagnose [label="Diagnose Failure"]
fix [label="Apply Fix"]
validate [label="Run Build"]
gate [shape=diamond, label="Build passing?"]
start -> diagnose -> fix -> validate -> gate
gate -> exit [label="Yes"]
gate -> diagnose [label="No"]
}"#;
const IMPLEMENT_GRAPH: &str = r#"digraph implement {
graph [goal="Implement feature from technical blueprint", label="Implement"]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
plan [label="Plan Implementation"]
implement [label="Implement"]
review [label="Review"]
validate [label="Validate"]
fix [label="Fix Failures"]
start -> plan -> implement -> review -> validate
validate -> exit [label="Pass"]
validate -> fix [label="Fix"]
fix -> validate
}"#;
const SYNC_DRIFT_GRAPH: &str = r#"digraph sync_drift {
graph [goal="Detect and reconcile configuration drift", label="Sync Drift"]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
detect [label="Detect Drift"]
propose [label="Propose Changes"]
review [shape=hexagon, label="Review Changes"]
apply [label="Apply Changes"]
start -> detect
detect -> exit [label="No drift"]
detect -> propose [label="Drift found"]
propose -> review
review -> apply [label="Accept"]
review -> propose [label="Revise"]
apply -> exit
}"#;
const EXPAND_GRAPH: &str = r#"digraph expand {
graph [goal="Propose and implement incremental product improvements", label="Expand"]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
propose [label="Propose Changes"]
approve [shape=hexagon, label="Approve Changes"]
execute [label="Execute Changes"]
start -> propose -> approve
approve -> execute [label="Accept"]
approve -> propose [label="Revise"]
execute -> exit
}"#;
fn definitions() -> Vec<Value> {
vec![
json!({
"name": "Fix Build",
"slug": "fix_build",
"filename": "fix_build.fabro",
"description": "Automatically diagnoses and fixes CI build failures by analyzing error logs and applying targeted code changes.",
"last_run": { "ran_at": "2026-03-06T12:00:00Z" },
"schedule": null,
"settings": runs::settings(),
"graph": FIX_BUILD_GRAPH,
}),
json!({
"name": "Implement Feature",
"slug": "implement",
"filename": "implement.fabro",
"description": "Generates production-ready code from a technical blueprint, including tests and a pull request ready for review.",
"last_run": { "ran_at": "2026-03-06T14:30:00Z" },
"schedule": null,
"settings": runs::settings(),
"graph": IMPLEMENT_GRAPH,
}),
json!({
"name": "Sync Drift",
"slug": "sync_drift",
"filename": "sync_drift.fabro",
"description": "Detects configuration and code drift between environments, then generates reconciliation patches.",
"last_run": { "ran_at": "2026-03-04T15:00:00Z" },
"schedule": { "expression": "0 9 * * 1", "next_run": "2026-03-09T09:00:00Z" },
"settings": runs::settings(),
"graph": SYNC_DRIFT_GRAPH,
}),
json!({
"name": "Expand Product",
"slug": "expand",
"filename": "expand.fabro",
"description": "Evolves the product by analyzing usage patterns and implementing incremental improvements.",
"last_run": null,
"schedule": null,
"settings": runs::settings(),
"graph": EXPAND_GRAPH,
}),
]
}
pub(super) fn list_items() -> Vec<Value> {
definitions()
.into_iter()
.map(|workflow| {
json!({
"name": workflow["name"],
"slug": workflow["slug"],
"filename": workflow["filename"],
"last_run": workflow["last_run"],
"schedule": workflow["schedule"],
})
})
.collect()
}
pub(super) fn detail(name: &str) -> Option<Value> {
definitions()
.into_iter()
.find(|workflow| workflow["slug"].as_str() == Some(name))
.map(|workflow| {
json!({
"name": workflow["name"],
"slug": workflow["slug"],
"description": workflow["description"],
"filename": workflow["filename"],
"settings": workflow["settings"],
"graph": workflow["graph"],
})
})
}
}
mod billing {
use fabro_api::types::*;
pub(super) fn aggregate() -> AggregateBilling {
AggregateBilling {
totals: AggregateBillingTotals {
cache_read_tokens: 0,
cache_write_tokens: 0,
runs: 9,
input_tokens: 643_860,
output_tokens: 189_720,
reasoning_tokens: 0,
runtime_secs: 3_501.0,
total_tokens: 833_580,
total_usd_micros: Some(20_340_000),
},
by_model: vec![
BillingByModel {
billing: BilledTokenCounts {
cache_read_tokens: 0,
cache_write_tokens: 0,
input_tokens: 304_020,
output_tokens: 87_210,
reasoning_tokens: 0,
total_tokens: 391_230,
total_usd_micros: Some(12_150_000),
},
model: ModelReference {
id: "Opus 4.6".into(),
},
stages: 18,
},
BillingByModel {
billing: BilledTokenCounts {
cache_read_tokens: 0,
cache_write_tokens: 0,
input_tokens: 257_760,
output_tokens: 78_750,
reasoning_tokens: 0,
total_tokens: 336_510,
total_usd_micros: Some(6_480_000),
},
model: ModelReference {
id: "Gemini 3.1".into(),
},
stages: 9,
},
BillingByModel {
billing: BilledTokenCounts {
cache_read_tokens: 0,
cache_write_tokens: 0,
input_tokens: 82_080,
output_tokens: 23_760,
reasoning_tokens: 0,
total_tokens: 105_840,
total_usd_micros: Some(1_710_000),
},
model: ModelReference {
id: "Codex 5.3".into(),
},
stages: 9,
},
],
}
}
}
mod insights {
use fabro_api::types::*;
use super::ts;
pub(super) fn saved_queries() -> Vec<SavedQuery> {
vec![
SavedQuery { id: "1".into(), name: "Run duration by workflow".into(), sql: "SELECT workflow_name, AVG(duration_seconds) as avg_duration,\n COUNT(*) as run_count\nFROM runs\nGROUP BY workflow_name\nORDER BY avg_duration DESC\nLIMIT 20".into(), created_at: ts("2026-03-01T10:00:00Z"), updated_at: ts("2026-03-05T14:30:00Z") },
SavedQuery { id: "2".into(), name: "Daily failure rate".into(), sql: "SELECT date_trunc('day', created_at) as day,\n COUNT(*) FILTER (WHERE status = 'failed') as failures,\n COUNT(*) as total\nFROM runs\nGROUP BY 1\nORDER BY 1 DESC\nLIMIT 30".into(), created_at: ts("2026-03-02T09:00:00Z"), updated_at: ts("2026-03-02T09:00:00Z") },
SavedQuery { id: "3".into(), name: "Top repos by activity".into(), sql: "SELECT repo, COUNT(*) as runs\nFROM runs\nGROUP BY repo\nORDER BY runs DESC".into(), created_at: ts("2026-03-03T11:00:00Z"), updated_at: ts("2026-03-03T11:00:00Z") },
]
}
pub(super) fn history() -> Vec<HistoryEntry> {
vec![
HistoryEntry {
id: "h1".into(),
sql: "SELECT workflow_name, COUNT(*) FROM runs GROUP BY 1".into(),
timestamp: ts("2025-09-15T13:58:00Z"),
elapsed: 0.342,
row_count: 6,
},
HistoryEntry {
id: "h2".into(),
sql: "SELECT * FROM runs WHERE status = 'failed' LIMIT 100".into(),
timestamp: ts("2025-09-15T13:52:00Z"),
elapsed: 0.127,
row_count: 23,
},
HistoryEntry {
id: "h3".into(),
sql:
"SELECT date_trunc('day', created_at) as d, COUNT(*) FROM runs GROUP BY 1"
.into(),
timestamp: ts("2025-09-15T13:45:00Z"),
elapsed: 0.531,
row_count: 30,
},
]
}
}
mod settings {
use std::sync::OnceLock;
pub(super) fn server_settings() -> serde_json::Value {
static CACHED: OnceLock<serde_json::Value> = OnceLock::new();
CACHED
.get_or_init(|| {
serde_json::to_value(
fabro_config::ServerSettingsBuilder::from_toml(
r#"
_version = 1
[server.listen]
type = "tcp"
address = "127.0.0.1:32276"
[server.api]
url = "https://api.fabro.example.com"
[server.web]
enabled = true
url = "https://fabro.example.com"
[server.auth]
methods = ["github"]
[server.auth.github]
allowed_usernames = ["brynary", "alice"]
[server.storage]
root = "/home/fabro/.fabro"
[server.scheduler]
max_concurrent_runs = 10
[server.integrations.github]
enabled = true
strategy = "app"
app_id = "12345"
client_id = "Iv1.abc123"
slug = "fabro-dev"
[features]
session_sandboxes = false
"#,
)
.expect("demo settings fixture should resolve"),
)
.expect("demo settings should serialize")
})
.clone()
}
}