135 lines
4.4 KiB
Rust
135 lines
4.4 KiB
Rust
use axum::{
|
|
Json,
|
|
extract::{Path, State},
|
|
http::StatusCode,
|
|
response::IntoResponse,
|
|
};
|
|
use chrono::Utc;
|
|
use uuid::Uuid;
|
|
|
|
use domain::{self, DomainError};
|
|
|
|
use crate::{
|
|
dto::{ScheduleHistoryEntry, ScheduleResponse},
|
|
error::ApiError,
|
|
extractors::CurrentUser,
|
|
state::AppState,
|
|
};
|
|
|
|
use super::require_owner;
|
|
|
|
/// Trigger 48-hour schedule generation for a channel, starting from now.
|
|
/// Replaces any existing schedule for the same window.
|
|
pub(super) async fn generate_schedule(
|
|
State(state): State<AppState>,
|
|
CurrentUser(user): CurrentUser,
|
|
Path(channel_id): Path<Uuid>,
|
|
) -> Result<impl IntoResponse, ApiError> {
|
|
let channel = state.channel_service.find_by_id(channel_id).await?;
|
|
require_owner(&channel, user.id)?;
|
|
|
|
let schedule = state
|
|
.schedule_engine
|
|
.generate_schedule(channel_id, Utc::now())
|
|
.await?;
|
|
|
|
let _ = state.event_tx.send(domain::DomainEvent::ScheduleGenerated {
|
|
channel_id,
|
|
schedule: schedule.clone(),
|
|
});
|
|
let detail = format!("{} slots", schedule.slots.len());
|
|
let _ = state.activity_log_repo.log("schedule_generated", &detail, Some(channel_id)).await;
|
|
Ok((StatusCode::CREATED, Json(ScheduleResponse::from(schedule))))
|
|
}
|
|
|
|
/// Return the currently active 48-hour schedule for a channel.
|
|
/// 404 if no schedule has been generated yet — call POST /:id/schedule first.
|
|
pub(super) async fn get_active_schedule(
|
|
State(state): State<AppState>,
|
|
CurrentUser(user): CurrentUser,
|
|
Path(channel_id): Path<Uuid>,
|
|
) -> Result<impl IntoResponse, ApiError> {
|
|
let channel = state.channel_service.find_by_id(channel_id).await?;
|
|
require_owner(&channel, user.id)?;
|
|
|
|
let schedule = state
|
|
.schedule_engine
|
|
.get_active_schedule(channel_id, Utc::now())
|
|
.await?
|
|
.ok_or(DomainError::NoActiveSchedule(channel_id))?;
|
|
|
|
Ok(Json(ScheduleResponse::from(schedule)))
|
|
}
|
|
|
|
/// List all schedule generations for a channel, newest first.
|
|
/// Returns lightweight entries (no slots).
|
|
pub(super) async fn list_schedule_history(
|
|
State(state): State<AppState>,
|
|
CurrentUser(user): CurrentUser,
|
|
Path(channel_id): Path<Uuid>,
|
|
) -> Result<impl IntoResponse, ApiError> {
|
|
let channel = state.channel_service.find_by_id(channel_id).await?;
|
|
require_owner(&channel, user.id)?;
|
|
|
|
let history = state.schedule_engine.list_schedule_history(channel_id).await?;
|
|
let entries: Vec<ScheduleHistoryEntry> = history.into_iter().map(Into::into).collect();
|
|
Ok(Json(entries))
|
|
}
|
|
|
|
/// Fetch a single historical schedule with all its slots.
|
|
pub(super) async fn get_schedule_history_entry(
|
|
State(state): State<AppState>,
|
|
CurrentUser(user): CurrentUser,
|
|
Path((channel_id, gen_id)): Path<(Uuid, Uuid)>,
|
|
) -> Result<impl IntoResponse, ApiError> {
|
|
let channel = state.channel_service.find_by_id(channel_id).await?;
|
|
require_owner(&channel, user.id)?;
|
|
|
|
let schedule = state
|
|
.schedule_engine
|
|
.get_schedule_by_id(channel_id, gen_id)
|
|
.await?
|
|
.ok_or_else(|| ApiError::NotFound(format!("Schedule {} not found", gen_id)))?;
|
|
|
|
Ok(Json(ScheduleResponse::from(schedule)))
|
|
}
|
|
|
|
/// Roll back to a previous schedule generation.
|
|
///
|
|
/// Deletes all generations after `gen_id`'s generation, then generates a fresh
|
|
/// schedule from now (inheriting the rolled-back generation as the base for
|
|
/// recycle-policy history).
|
|
pub(super) async fn rollback_schedule(
|
|
State(state): State<AppState>,
|
|
CurrentUser(user): CurrentUser,
|
|
Path((channel_id, gen_id)): Path<(Uuid, Uuid)>,
|
|
) -> Result<impl IntoResponse, ApiError> {
|
|
let channel = state.channel_service.find_by_id(channel_id).await?;
|
|
require_owner(&channel, user.id)?;
|
|
|
|
let target = state
|
|
.schedule_engine
|
|
.get_schedule_by_id(channel_id, gen_id)
|
|
.await?
|
|
.ok_or_else(|| ApiError::NotFound(format!("Schedule {} not found", gen_id)))?;
|
|
|
|
state
|
|
.schedule_engine
|
|
.delete_schedules_after(channel_id, target.generation)
|
|
.await?;
|
|
|
|
let schedule = state
|
|
.schedule_engine
|
|
.generate_schedule(channel_id, Utc::now())
|
|
.await?;
|
|
|
|
let _ = state.event_tx.send(domain::DomainEvent::ScheduleGenerated {
|
|
channel_id,
|
|
schedule: schedule.clone(),
|
|
});
|
|
let detail = format!("rollback to gen {}; {} slots", target.generation, schedule.slots.len());
|
|
let _ = state.activity_log_repo.log("schedule_rollback", &detail, Some(channel_id)).await;
|
|
|
|
Ok(Json(ScheduleResponse::from(schedule)))
|
|
}
|