tori/src/api/timers.rs
Fam Zheng 2df4e12d30 Agent loop state machine refactor, unified LLM interface, and UI improvements
- Rewrite agent loop as Planning→Executing(N)→Completed state machine with
  per-step context isolation to prevent token explosion
- Split tools and prompts by phase (planning vs execution)
- Add advance_step/save_memo tools for step transitions and cross-step memory
- Unify LLM interface: remove duplicate types, single chat_with_tools path
- Add UTF-8 safe truncation (truncate_str) to prevent panics on Chinese text
- Extract CreateForm component, add auto-scroll to execution log
- Add report generation with app access URL, non-blocking title generation
- Add timer system, file serving, app proxy, exec module
- Update Dockerfile with uv, deployment config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:35:33 +00:00

148 lines
4.1 KiB
Rust

use std::sync::Arc;
use axum::{
extract::{Path, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
Json, Router,
};
use serde::Deserialize;
use crate::AppState;
use crate::db::Timer;
type ApiResult<T> = Result<Json<T>, Response>;
fn db_err(e: sqlx::Error) -> Response {
tracing::error!("Database error: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
}
#[derive(Deserialize)]
pub struct CreateTimer {
pub name: String,
pub interval_secs: i64,
pub requirement: String,
}
#[derive(Deserialize)]
pub struct UpdateTimer {
pub name: Option<String>,
pub interval_secs: Option<i64>,
pub requirement: Option<String>,
pub enabled: Option<bool>,
}
pub fn router(state: Arc<AppState>) -> Router {
Router::new()
.route("/projects/{id}/timers", get(list_timers).post(create_timer))
.route("/timers/{id}", get(get_timer).put(update_timer).delete(delete_timer))
.with_state(state)
}
async fn list_timers(
State(state): State<Arc<AppState>>,
Path(project_id): Path<String>,
) -> ApiResult<Vec<Timer>> {
sqlx::query_as::<_, Timer>(
"SELECT * FROM timers WHERE project_id = ? ORDER BY created_at DESC"
)
.bind(&project_id)
.fetch_all(&state.db.pool)
.await
.map(Json)
.map_err(db_err)
}
async fn create_timer(
State(state): State<Arc<AppState>>,
Path(project_id): Path<String>,
Json(input): Json<CreateTimer>,
) -> ApiResult<Timer> {
if input.interval_secs < 60 {
return Err((StatusCode::BAD_REQUEST, "Minimum interval is 60 seconds").into_response());
}
let id = uuid::Uuid::new_v4().to_string();
sqlx::query_as::<_, Timer>(
"INSERT INTO timers (id, project_id, name, interval_secs, requirement) VALUES (?, ?, ?, ?, ?) RETURNING *"
)
.bind(&id)
.bind(&project_id)
.bind(&input.name)
.bind(input.interval_secs)
.bind(&input.requirement)
.fetch_one(&state.db.pool)
.await
.map(Json)
.map_err(db_err)
}
async fn get_timer(
State(state): State<Arc<AppState>>,
Path(timer_id): Path<String>,
) -> ApiResult<Timer> {
sqlx::query_as::<_, Timer>("SELECT * FROM timers WHERE id = ?")
.bind(&timer_id)
.fetch_optional(&state.db.pool)
.await
.map_err(db_err)?
.map(Json)
.ok_or_else(|| (StatusCode::NOT_FOUND, "Timer not found").into_response())
}
async fn update_timer(
State(state): State<Arc<AppState>>,
Path(timer_id): Path<String>,
Json(input): Json<UpdateTimer>,
) -> ApiResult<Timer> {
// Fetch existing
let existing = sqlx::query_as::<_, Timer>("SELECT * FROM timers WHERE id = ?")
.bind(&timer_id)
.fetch_optional(&state.db.pool)
.await
.map_err(db_err)?;
let Some(existing) = existing else {
return Err((StatusCode::NOT_FOUND, "Timer not found").into_response());
};
let name = input.name.unwrap_or(existing.name);
let interval_secs = input.interval_secs.unwrap_or(existing.interval_secs);
let requirement = input.requirement.unwrap_or(existing.requirement);
let enabled = input.enabled.unwrap_or(existing.enabled);
if interval_secs < 60 {
return Err((StatusCode::BAD_REQUEST, "Minimum interval is 60 seconds").into_response());
}
sqlx::query_as::<_, Timer>(
"UPDATE timers SET name = ?, interval_secs = ?, requirement = ?, enabled = ? WHERE id = ? RETURNING *"
)
.bind(&name)
.bind(interval_secs)
.bind(&requirement)
.bind(enabled)
.bind(&timer_id)
.fetch_one(&state.db.pool)
.await
.map(Json)
.map_err(db_err)
}
async fn delete_timer(
State(state): State<Arc<AppState>>,
Path(timer_id): Path<String>,
) -> Result<StatusCode, Response> {
let result = sqlx::query("DELETE FROM timers WHERE id = ?")
.bind(&timer_id)
.execute(&state.db.pool)
.await
.map_err(db_err)?;
if result.rows_affected() > 0 {
Ok(StatusCode::NO_CONTENT)
} else {
Err((StatusCode::NOT_FOUND, "Timer not found").into_response())
}
}