From fb2262f000eee0e278eb4d77a1f9decf64359f5e Mon Sep 17 00:00:00 2001 From: devatsecure Date: Thu, 5 Mar 2026 15:53:53 +0500 Subject: [PATCH] Add hierarchical Goals feature with dashboard UI and REST API SQLite-backed goal tracking with parent-child hierarchy, four levels (mission/strategy/objective/task), status workflow, agent assignment, and progress tracking. Dashboard tab includes tree view, kanban board, and timeline with full CRUD support. Co-Authored-By: Claude Opus 4.6 --- crates/openfang-api/src/routes.rs | 113 ++++++ crates/openfang-api/src/server.rs | 11 + crates/openfang-api/src/webchat.rs | 2 + crates/openfang-api/static/index_body.html | 285 ++++++++++++++ crates/openfang-api/static/js/app.js | 2 +- crates/openfang-api/static/js/pages/goals.js | 231 +++++++++++ crates/openfang-memory/src/goals.rs | 380 +++++++++++++++++++ crates/openfang-memory/src/lib.rs | 1 + crates/openfang-memory/src/migration.rs | 35 +- crates/openfang-memory/src/substrate.rs | 9 + 10 files changed, 1067 insertions(+), 2 deletions(-) create mode 100644 crates/openfang-api/static/js/pages/goals.js create mode 100644 crates/openfang-memory/src/goals.rs diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index 720b48f81..89d97aa0f 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -10287,3 +10287,116 @@ pub async fn comms_task( ), } } + +// ═══════════════════════════════════════════════════════════════════ +// Goals endpoints +// ═══════════════════════════════════════════════════════════════════ + +/// GET /api/goals — List all goals. +pub async fn list_goals(State(state): State>) -> impl IntoResponse { + match state.kernel.memory.goals().list() { + Ok(goals) => Json(serde_json::json!({ "goals": goals, "total": goals.len() })), + Err(e) => Json(serde_json::json!({ "goals": [], "total": 0, "error": e })), + } +} + +/// POST /api/goals — Create a new goal. +pub async fn create_goal( + State(state): State>, + Json(req): Json, +) -> impl IntoResponse { + let valid_levels = ["mission", "strategy", "objective", "task"]; + if !valid_levels.contains(&req.level.as_str()) { + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({"error": format!("Invalid level '{}'. Must be one of: {}", req.level, valid_levels.join(", "))})), + ); + } + let valid_statuses = ["planned", "active", "completed", "paused"]; + if !valid_statuses.contains(&req.status.as_str()) { + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({"error": format!("Invalid status '{}'. Must be one of: {}", req.status, valid_statuses.join(", "))})), + ); + } + match state.kernel.memory.goals().create(&req) { + Ok(goal) => (StatusCode::CREATED, Json(serde_json::json!(goal))), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": e})), + ), + } +} + +/// GET /api/goals/{id} — Get a single goal. +pub async fn get_goal( + State(state): State>, + Path(id): Path, +) -> impl IntoResponse { + match state.kernel.memory.goals().get(&id) { + Ok(Some(goal)) => (StatusCode::OK, Json(serde_json::json!(goal))), + Ok(None) => ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({"error": "Goal not found"})), + ), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": e})), + ), + } +} + +/// PUT /api/goals/{id} — Update a goal. +pub async fn update_goal( + State(state): State>, + Path(id): Path, + Json(req): Json, +) -> impl IntoResponse { + if let Some(ref level) = req.level { + let valid_levels = ["mission", "strategy", "objective", "task"]; + if !valid_levels.contains(&level.as_str()) { + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({"error": format!("Invalid level '{level}'")})), + ); + } + } + if let Some(ref status) = req.status { + let valid_statuses = ["planned", "active", "completed", "paused"]; + if !valid_statuses.contains(&status.as_str()) { + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({"error": format!("Invalid status '{status}'")})), + ); + } + } + match state.kernel.memory.goals().update(&id, &req) { + Ok(Some(goal)) => (StatusCode::OK, Json(serde_json::json!(goal))), + Ok(None) => ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({"error": "Goal not found"})), + ), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": e})), + ), + } +} + +/// DELETE /api/goals/{id} — Delete a goal (children become root goals). +pub async fn delete_goal( + State(state): State>, + Path(id): Path, +) -> impl IntoResponse { + match state.kernel.memory.goals().delete(&id) { + Ok(true) => (StatusCode::OK, Json(serde_json::json!({"deleted": true}))), + Ok(false) => ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({"error": "Goal not found"})), + ), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": e})), + ), + } +} diff --git a/crates/openfang-api/src/server.rs b/crates/openfang-api/src/server.rs index a87b25199..73fb4ce35 100644 --- a/crates/openfang-api/src/server.rs +++ b/crates/openfang-api/src/server.rs @@ -377,6 +377,17 @@ pub async fn build_router( "/api/hands/instances/{id}/browser", axum::routing::get(routes::hand_instance_browser), ) + // Goals endpoints + .route( + "/api/goals", + axum::routing::get(routes::list_goals).post(routes::create_goal), + ) + .route( + "/api/goals/{id}", + axum::routing::get(routes::get_goal) + .put(routes::update_goal) + .delete(routes::delete_goal), + ) // MCP server endpoints .route( "/api/mcp/servers", diff --git a/crates/openfang-api/src/webchat.rs b/crates/openfang-api/src/webchat.rs index b7fa6016a..075030922 100644 --- a/crates/openfang-api/src/webchat.rs +++ b/crates/openfang-api/src/webchat.rs @@ -110,6 +110,8 @@ const WEBCHAT_HTML: &str = concat!( "\n", include_str!("../static/js/pages/hands.js"), "\n", + include_str!("../static/js/pages/goals.js"), + "\n", include_str!("../static/js/pages/scheduler.js"), "\n", include_str!("../static/js/pages/settings.js"), diff --git a/crates/openfang-api/static/index_body.html b/crates/openfang-api/static/index_body.html index f4d32aa90..90a866dd0 100644 --- a/crates/openfang-api/static/index_body.html +++ b/crates/openfang-api/static/index_body.html @@ -120,6 +120,10 @@

OPENFANG

Scheduler + + + Goals + @@ -1829,6 +1833,287 @@

No run history yet

+ + +