chainage panel update
This commit is contained in:
@@ -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)]
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user