chainage panel update

This commit is contained in:
2026-07-08 18:02:01 +05:30
parent 8238bcf981
commit cf4ae6e146
6 changed files with 120 additions and 30 deletions

View File

@@ -676,6 +676,9 @@ pub struct CalibrationRow {
pub anchor_count: i32,
pub computed_by: String,
pub computed_at: chrono::DateTime<chrono::Utc>,
/// False only for calibrations saved before summaries were stored — one
/// recompute (same points, same result) seeds it.
pub has_summary: bool,
}
/// `GET /api/projects/:id/chainage/calibrations` — the saved calibrations (one per
@@ -690,7 +693,7 @@ pub async fn list_calibrations(
crate::require_project_access(&s, &user, id).await?;
let rows = sqlx::query_as::<_, CalibrationRow>(
r#"SELECT road_type, route_len_m, jsonb_array_length(anchors)::int AS anchor_count,
computed_by, computed_at
computed_by, computed_at, (summary IS NOT NULL) AS has_summary
FROM chainage_calibrations WHERE project_id=$1 ORDER BY road_type"#,
)
.bind(id)
@@ -700,6 +703,34 @@ pub async fn list_calibrations(
Ok(Json(rows))
}
#[derive(Deserialize)]
pub struct SummaryQuery {
#[serde(default)]
pub road_type: String,
}
/// `GET /api/projects/:id/chainage/summary?road_type=` — the SAVED result of the
/// last recompute for a group (route, spans, per-video chainage, warnings), exactly
/// as the admin who ran it saw it. Null when the group was never computed (or was
/// computed before summaries were stored — one recompute seeds it). Member or admin.
pub async fn saved_summary(
State(s): State<AppState>,
user: AuthUser,
AxPath(id): AxPath<i32>,
axum::extract::Query(q): axum::extract::Query<SummaryQuery>,
) -> ApiResult<Value> {
crate::require_project_access(&s, &user, id).await?;
let v: Option<Option<Value>> = sqlx::query_scalar(
"SELECT summary FROM chainage_calibrations WHERE project_id=$1 AND road_type=$2",
)
.bind(id)
.bind(q.road_type.trim())
.fetch_optional(&s.db)
.await
.map_err(err)?;
Ok(Json(v.flatten().unwrap_or(Value::Null)))
}
/// `DELETE /api/chainage/points/:id` — remove a calibration point (admin).
pub async fn delete_point(
State(s): State<AppState>,
@@ -1022,30 +1053,7 @@ async fn run_recompute(
.map_err(err)?;
warnings.push(format!("{name} has no GPS track — excluded from the route, chainage cleared"));
}
// Persist the calibration itself so chainage(s) can be evaluated later at ANY
// route position (frame-exact per-annotation stamping, exports).
sqlx::query(
r#"INSERT INTO chainage_calibrations (project_id, road_type, anchors, route_len_m, computed_by)
VALUES ($1,$2,$3,$4,$5)
ON CONFLICT (project_id, road_type) DO UPDATE SET
anchors=EXCLUDED.anchors, route_len_m=EXCLUDED.route_len_m,
computed_by=EXCLUDED.computed_by, computed_at=now()"#,
)
.bind(project_id)
.bind(road_type)
.bind(calib.to_value())
.bind(route.len_m())
.bind(&user.username)
.execute(&mut *tx)
.await
.map_err(err)?;
tx.commit().await.map_err(err)?;
crate::admin::audit(&s.db, &user.username, "chainage_recomputed",
json!({ "project_id": project_id, "road_type": road_type,
"videos": out_videos.len(), "points_used": calib.anchors.len() })).await;
Ok(CalibrationSummary {
let summary = CalibrationSummary {
route_len_m: route.len_m(),
reversed,
majority_flipped,
@@ -1055,7 +1063,34 @@ async fn run_recompute(
warnings,
videos: out_videos,
skipped: skipped.into_iter().map(|(_, n)| n).collect(),
})
};
// Persist the calibration itself so chainage(s) can be evaluated later at ANY
// route position (frame-exact per-annotation stamping, exports) — plus the full
// result summary, so every admin sees the existing compute, not a recompute ask.
sqlx::query(
r#"INSERT INTO chainage_calibrations (project_id, road_type, anchors, route_len_m, computed_by, summary)
VALUES ($1,$2,$3,$4,$5,$6)
ON CONFLICT (project_id, road_type) DO UPDATE SET
anchors=EXCLUDED.anchors, route_len_m=EXCLUDED.route_len_m,
computed_by=EXCLUDED.computed_by, computed_at=now(), summary=EXCLUDED.summary"#,
)
.bind(project_id)
.bind(road_type)
.bind(calib.to_value())
.bind(route.len_m())
.bind(&user.username)
.bind(serde_json::to_value(&summary).unwrap_or(Value::Null))
.execute(&mut *tx)
.await
.map_err(err)?;
tx.commit().await.map_err(err)?;
crate::admin::audit(&s.db, &user.username, "chainage_recomputed",
json!({ "project_id": project_id, "road_type": road_type,
"videos": summary.videos.len(), "points_used": calib.anchors.len() })).await;
Ok(summary)
}
#[cfg(test)]

View File

@@ -211,6 +211,7 @@ async fn main() -> anyhow::Result<()> {
.route("/api/projects/:id/chainage/recompute", post(chainage::recompute))
.route("/api/projects/:id/chainage/quick", post(chainage::quick_assign))
.route("/api/projects/:id/chainage/calibrations", get(chainage::list_calibrations))
.route("/api/projects/:id/chainage/summary", get(chainage::saved_summary))
// Phase 3 admin (token-gated).
.route("/api/admin/storage", get(storage_info))
.route("/api/users", get(admin::list_users).post(admin::create_user))