feat(topology): persist comparison runs + history
Save each comparison and let users reload past ones.
- migration 0008: topology_runs (workspace-scoped; full comparison as JSONB).
- cm-db repo::topology_runs (insert / list_recent / get) + regenerated .sqlx.
- cm-api: compare persists best-effort (never loses the LLM result on a DB
hiccup); GET /api/topology-runs (recent) + GET /api/topology-runs/{id}.
Integration test asserts persist → list → get.
- frontend: "Recent comparisons" list on the Compare tab; click to reload a
saved run. e2e p8 green (39 suite); offline build + clippy clean.
Server self-migrates at boot (cm_db::MIGRATOR), so 0008 applies on deploy.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
654ec0f511
commit
7baf2082d0
@@ -0,0 +1,92 @@
|
||||
//! Persistence for saved multi-topology comparison runs.
|
||||
|
||||
use cm_domain::WorkspaceId;
|
||||
use serde_json::Value;
|
||||
use sqlx::PgPool;
|
||||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::DbError;
|
||||
|
||||
/// A row summary for the recent-runs list.
|
||||
pub struct TopologyRunSummary {
|
||||
pub id: Uuid,
|
||||
pub task: String,
|
||||
pub created_at: OffsetDateTime,
|
||||
}
|
||||
|
||||
/// A full saved comparison run.
|
||||
pub struct TopologyRun {
|
||||
pub id: Uuid,
|
||||
pub task: String,
|
||||
pub comparison: Value,
|
||||
pub created_at: OffsetDateTime,
|
||||
}
|
||||
|
||||
/// Save a comparison run for a workspace.
|
||||
pub async fn insert(
|
||||
pool: &PgPool,
|
||||
id: Uuid,
|
||||
workspace_id: WorkspaceId,
|
||||
task: &str,
|
||||
comparison: &Value,
|
||||
) -> Result<(), DbError> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO topology_runs (id, workspace_id, task, comparison)
|
||||
VALUES ($1, $2, $3, $4)",
|
||||
id,
|
||||
workspace_id.as_uuid(),
|
||||
task,
|
||||
comparison,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The most recent runs for a workspace, newest first.
|
||||
pub async fn list_recent(
|
||||
pool: &PgPool,
|
||||
workspace_id: WorkspaceId,
|
||||
limit: i64,
|
||||
) -> Result<Vec<TopologyRunSummary>, DbError> {
|
||||
let rows = sqlx::query!(
|
||||
"SELECT id, task, created_at FROM topology_runs
|
||||
WHERE workspace_id = $1 ORDER BY created_at DESC LIMIT $2",
|
||||
workspace_id.as_uuid(),
|
||||
limit,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| TopologyRunSummary {
|
||||
id: r.id,
|
||||
task: r.task,
|
||||
created_at: r.created_at,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// A single saved run, scoped to its workspace.
|
||||
pub async fn get(
|
||||
pool: &PgPool,
|
||||
id: Uuid,
|
||||
workspace_id: WorkspaceId,
|
||||
) -> Result<TopologyRun, DbError> {
|
||||
let row = sqlx::query!(
|
||||
"SELECT id, task, comparison, created_at FROM topology_runs
|
||||
WHERE id = $1 AND workspace_id = $2",
|
||||
id,
|
||||
workspace_id.as_uuid(),
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?
|
||||
.ok_or(DbError::NotFound)?;
|
||||
Ok(TopologyRun {
|
||||
id: row.id,
|
||||
task: row.task,
|
||||
comparison: row.comparison,
|
||||
created_at: row.created_at,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user