Big updates

This commit is contained in:
Alois 2026-07-29 22:14:01 +02:00
commit 56ae701d02
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
5 changed files with 401 additions and 214 deletions

View file

@ -5,7 +5,7 @@ use axum::{
extract::{Path, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::{get, post},
routing::{delete, get, post},
};
use tower_http::{
services::{ServeDir, ServeFile},
@ -25,6 +25,7 @@ pub async fn serve(
let app = Router::new()
.route("/api/admin", get(admin))
.route("/api/incidents", get(list).post(create))
.route("/api/incidents/{id}", delete(remove))
.route("/api/incidents/{id}/resolve", post(resolve))
.fallback_service(assets)
.layer(TraceLayer::new_for_http())
@ -62,9 +63,20 @@ async fn resolve(State(state): State<Arc<AppState>>, Path(id): Path<Uuid>) -> Re
}
}
async fn remove(State(state): State<Arc<AppState>>, Path(id): Path<Uuid>) -> Response {
match state.incidents.delete(id).await {
Ok(_) => {
state.publish().await;
StatusCode::NO_CONTENT.into_response()
}
Err(error) => error_response(error),
}
}
fn error_response(error: IncidentError) -> Response {
let status = match error {
IncidentError::Empty => StatusCode::BAD_REQUEST,
IncidentError::Active => StatusCode::CONFLICT,
IncidentError::NotFound => StatusCode::NOT_FOUND,
IncidentError::Storage(_) | IncidentError::Parse(_) => StatusCode::INTERNAL_SERVER_ERROR,
};

View file

@ -18,6 +18,8 @@ pub enum IncidentError {
Empty,
#[error("incident not found")]
NotFound,
#[error("active incidents cannot be deleted")]
Active,
#[error("failed to persist incidents: {0}")]
Storage(#[from] std::io::Error),
#[error("failed to parse incidents: {0}")]
@ -73,6 +75,20 @@ impl IncidentStore {
Ok(resolved)
}
pub async fn delete(&self, id: Uuid) -> Result<Incident, IncidentError> {
let mut incidents = self.incidents.lock().await;
let index = incidents
.iter()
.position(|incident| incident.id == id)
.ok_or(IncidentError::NotFound)?;
if incidents[index].resolved_at.is_none() {
return Err(IncidentError::Active);
}
let deleted = incidents.remove(index);
self.persist(&incidents).await?;
Ok(deleted)
}
async fn persist(&self, incidents: &[Incident]) -> Result<(), std::io::Error> {
if let Some(parent) = self.path.parent() {
tokio::fs::create_dir_all(parent).await?;
@ -125,4 +141,35 @@ mod tests {
.await;
assert!(matches!(result, Err(IncidentError::Empty)));
}
#[tokio::test]
async fn only_deletes_resolved_incidents() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("incidents.json");
let store = IncidentStore::load(path.clone()).await.unwrap();
let created = store
.create(NewIncident {
title: "Service disruption".into(),
message: "Investigating elevated errors.".into(),
severity: Severity::Major,
})
.await
.unwrap();
assert!(matches!(
store.delete(created.id).await,
Err(IncidentError::Active)
));
store.resolve(created.id).await.unwrap();
store.delete(created.id).await.unwrap();
assert!(
IncidentStore::load(path)
.await
.unwrap()
.list()
.await
.is_empty()
);
}
}