diff --git a/TASKS.md b/TASKS.md index b073fb1..e8bda65 100644 --- a/TASKS.md +++ b/TASKS.md @@ -12,6 +12,7 @@ DoD: - UI продолжает раздаваться как статический контейнер через текущий deployment path - legacy React/Vite артефакты удалены из `apps/ui` - backend для `Operations/Wizard` поддерживает server-side `PATCH`, `DELETE`, `archive`, `category`, `usage_summary`, `agent_refs` +- backend summary для `Operations` отдает `target_url` и `target_action` - первые экраны Alpine UI можно сажать на реальные catalog/detail/mutation endpoints без расширения backend-контракта - `Operations` page читает live catalog из `admin-api` и удаляет операции через backend - `Wizard` использует live create/get/update flow вместо `localStorage` и `sessionStorage` diff --git a/apps/admin-api/src/app.rs b/apps/admin-api/src/app.rs index d80adf7..4d61f31 100644 --- a/apps/admin-api/src/app.rs +++ b/apps/admin-api/src/app.rs @@ -210,6 +210,11 @@ mod tests { .unwrap(); assert_eq!(listed["items"][0]["name"], "crm_create_lead"); + assert_eq!( + listed["items"][0]["target_url"], + format!("{upstream_base_url}/crm/leads") + ); + assert_eq!(listed["items"][0]["target_action"], "POST"); assert_eq!(published["published_version"], 1); assert_eq!(test_run["ok"], true); assert_eq!( @@ -251,6 +256,11 @@ mod tests { .unwrap(); assert_eq!(listed["total"], 1); assert_eq!(listed["items"][0]["category"], "sales"); + assert_eq!( + listed["items"][0]["target_url"], + format!("{upstream_base_url}/crm/leads") + ); + assert_eq!(listed["items"][0]["target_action"], "POST"); let updated = client .patch(format!("{base_url}/operations/{operation_id}")) diff --git a/apps/admin-api/src/service.rs b/apps/admin-api/src/service.rs index 2f75082..2af239b 100644 --- a/apps/admin-api/src/service.rs +++ b/apps/admin-api/src/service.rs @@ -291,6 +291,8 @@ pub struct OperationSummaryView { pub display_name: String, pub category: String, pub protocol: Protocol, + pub target_url: String, + pub target_action: String, pub status: OperationStatus, pub current_draft_version: u32, pub latest_published_version: Option, @@ -2070,6 +2072,8 @@ fn enrich_operation_summary( display_name: summary.display_name, category: summary.category, protocol: summary.protocol, + target_url: summary.target_url, + target_action: summary.target_action, status: summary.status, current_draft_version: summary.current_draft_version, latest_published_version: summary.latest_published_version, diff --git a/apps/ui/js/catalog.js b/apps/ui/js/catalog.js index 1bc4f28..a510538 100644 --- a/apps/ui/js/catalog.js +++ b/apps/ui/js/catalog.js @@ -40,8 +40,8 @@ function mapOperation(item) { created_at: item.created_at, updated_at: item.updated_at, published_at: item.published_at, - target_url: '', - method: '', + target_url: item.target_url || '', + method: item.target_action || '', usage_summary: item.usage_summary || { calls_today: 0, error_rate_pct: 0, diff --git a/crates/crank-registry/src/model.rs b/crates/crank-registry/src/model.rs index 311db22..aa9fd09 100644 --- a/crates/crank-registry/src/model.rs +++ b/crates/crank-registry/src/model.rs @@ -157,6 +157,8 @@ pub struct OperationSummary { pub display_name: String, pub category: String, pub protocol: Protocol, + pub target_url: String, + pub target_action: String, pub status: OperationStatus, pub current_draft_version: u32, pub latest_published_version: Option, diff --git a/crates/crank-registry/src/postgres.rs b/crates/crank-registry/src/postgres.rs index f6b3ed8..547c14a 100644 --- a/crates/crank-registry/src/postgres.rs +++ b/crates/crank-registry/src/postgres.rs @@ -1,7 +1,8 @@ use crank_core::{ - AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AuthProfile, InvitationId, - InvitationToken, InvocationLog, InvocationLogId, OperationId, OperationStatus, PlatformApiKey, - PlatformApiKeyId, PlatformApiKeyStatus, UsageRollup, User, UserId, Workspace, WorkspaceId, + AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AuthProfile, GraphqlOperationType, + HttpMethod, InvitationId, InvitationToken, InvocationLog, InvocationLogId, OperationId, + OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyStatus, Target, UsageRollup, + User, UserId, Workspace, WorkspaceId, }; use serde::{Serialize, de::DeserializeOwned}; use serde_json::Value; @@ -1490,21 +1491,25 @@ impl PostgresRegistry { ) -> Result, RegistryError> { let rows = sqlx::query( "select - id, - workspace_id, - name, - display_name, - category, - protocol, - status, - current_draft_version, - latest_published_version, - to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at, - to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at, - to_char(published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as published_at + operations.id, + operations.workspace_id, + operations.name, + operations.display_name, + operations.category, + operations.protocol, + ov.target_json, + operations.status, + operations.current_draft_version, + operations.latest_published_version, + to_char(operations.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at, + to_char(operations.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at, + to_char(operations.published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as published_at from operations - where workspace_id = $1 - order by name asc", + join operation_versions ov + on ov.operation_id = operations.id + and ov.version = operations.current_draft_version + where operations.workspace_id = $1 + order by operations.name asc", ) .bind(workspace_id.as_str()) .fetch_all(&self.pool) @@ -1571,20 +1576,24 @@ impl PostgresRegistry { ) -> Result, RegistryError> { let row = sqlx::query( "select - id, - workspace_id, - name, - display_name, - category, - protocol, - status, - current_draft_version, - latest_published_version, - to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at, - to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at, - to_char(published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as published_at + operations.id, + operations.workspace_id, + operations.name, + operations.display_name, + operations.category, + operations.protocol, + ov.target_json, + operations.status, + operations.current_draft_version, + operations.latest_published_version, + to_char(operations.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at, + to_char(operations.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at, + to_char(operations.published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as published_at from operations - where workspace_id = $1 and id = $2", + join operation_versions ov + on ov.operation_id = operations.id + and ov.version = operations.current_draft_version + where operations.workspace_id = $1 and operations.id = $2", ) .bind(workspace_id.as_str()) .bind(operation_id.as_str()) @@ -2447,6 +2456,9 @@ fn map_agent_summary(row: &PgRow) -> Result { } fn map_operation_summary(row: &PgRow) -> Result { + let target: Target = deserialize_json_value(row.try_get::, _>("target_json")?.0)?; + let (target_url, target_action) = target_summary(&target); + Ok(OperationSummary { id: OperationId::new(row.try_get::("id")?), workspace_id: WorkspaceId::new(row.try_get::("workspace_id")?), @@ -2454,6 +2466,8 @@ fn map_operation_summary(row: &PgRow) -> Result display_name: row.try_get("display_name")?, category: row.try_get("category")?, protocol: deserialize_enum_text(&row.try_get::("protocol")?, "protocol")?, + target_url, + target_action, status: deserialize_enum_text(&row.try_get::("status")?, "status")?, current_draft_version: from_db_version( row.try_get("current_draft_version")?, @@ -2478,6 +2492,52 @@ fn map_operation_usage_summary(row: &PgRow) -> Result (String, String) { + match target { + Target::Rest(rest) => ( + join_target_url(&rest.base_url, &rest.path_template), + match rest.method { + HttpMethod::Get => "GET", + HttpMethod::Post => "POST", + HttpMethod::Put => "PUT", + HttpMethod::Patch => "PATCH", + HttpMethod::Delete => "DELETE", + } + .to_owned(), + ), + Target::Graphql(graphql) => ( + graphql.endpoint.clone(), + match graphql.operation_type { + GraphqlOperationType::Query => "QUERY", + GraphqlOperationType::Mutation => "MUTATION", + } + .to_owned(), + ), + Target::Grpc(grpc) => (grpc.server_addr.clone(), grpc.method.clone()), + } +} + +fn join_target_url(base: &str, path: &str) -> String { + if path.is_empty() { + return base.to_owned(); + } + + if path.starts_with("http://") || path.starts_with("https://") || path.starts_with("grpc://") { + return path.to_owned(); + } + + let trimmed_base = base.trim_end_matches('/'); + let trimmed_path = path.trim_start_matches('/'); + + if trimmed_base.is_empty() { + format!("/{trimmed_path}") + } else if trimmed_path.is_empty() { + trimmed_base.to_owned() + } else { + format!("{trimmed_base}/{trimmed_path}") + } +} + fn map_operation_agent_ref(row: &PgRow) -> Result { Ok(OperationAgentRef { operation_id: OperationId::new(row.try_get::("operation_id")?), diff --git a/docs/alpine-ui-integration-plan.md b/docs/alpine-ui-integration-plan.md index 1254f10..c8e9014 100644 --- a/docs/alpine-ui-integration-plan.md +++ b/docs/alpine-ui-integration-plan.md @@ -73,7 +73,7 @@ UI-файлы: - эту страницу можно интегрировать первой; - backend уже отдает server-side category, usage summary и agent refs; - каталог уже может работать через реальные `list/delete/edit` вызовы; -- следующий шаг здесь только в richer summary shape, если нужно вернуть `target_url` и server-side paging. +- следующий шаг здесь только в server-side paging и остальных catalog actions; `target_url` и `target_action` теперь должны приходить из backend summary. ### 4.2. Wizard diff --git a/docs/operations-workspace-contracts.md b/docs/operations-workspace-contracts.md index 22c7202..f617b85 100644 --- a/docs/operations-workspace-contracts.md +++ b/docs/operations-workspace-contracts.md @@ -183,6 +183,8 @@ Query params: "protocol": "rest", "status": "draft", "category": "sales", + "target_url": "https://api.example.com/v1/leads", + "target_action": "POST", "current_draft_version": 3, "latest_published_version": 2, "updated_at": "2026-03-29T12:00:00Z",