use axum::{
extract::{Path, Query, State},
response::Response,
};
use rusqlite::{params, Connection, OptionalExtension};
use serde::Serialize;
use super::super::cursor::CursorKind;
use super::super::read_resources::{
detail_resource, list_resource, redact_bounded, redact_optional, ReadResourceParams,
ReadResourceSpec, ResourceProjectionPolicy, SafeResourceRef,
};
use super::super::types::DbState;
pub(in crate::api) async fn handle_list_sessions(
State(_state): State<DbState>,
Query(params): Query<ReadResourceParams>,
) -> Response {
list_resource::<Sessions>(params)
}
pub(in crate::api) async fn handle_session_detail(
State(_state): State<DbState>,
Path(id): Path<String>,
) -> Response {
detail_resource::<Sessions>(id)
}
struct Sessions;
struct SessionRow {
id: i64,
host_id: i64,
host: String,
project_id: i64,
project: String,
started_at_epoch: Option<i64>,
last_seen_at_epoch: i64,
status: String,
session_intent: Option<String>,
session_topic: Option<String>,
session_intent_source: Option<String>,
}
#[derive(Serialize)]
struct SessionItem {
id: i64,
host: String,
project: String,
status: String,
summary: String,
started_at_epoch: Option<i64>,
last_seen_at_epoch: i64,
mmdd: Option<String>,
session_intent: Option<String>,
session_topic: Option<String>,
display_label: Option<String>,
session_intent_source: Option<String>,
references: Vec<SafeResourceRef>,
}
const SELECT_SESSION: &str = "SELECT s.id, s.host_id, h.name, s.project_id, p.project_key,
s.started_at_epoch, s.last_seen_at_epoch, s.status,
ss.session_intent, ss.session_topic, ss.session_intent_source
FROM sessions s
JOIN hosts h ON h.id = s.host_id
JOIN projects p ON p.id = s.project_id
LEFT JOIN session_summaries ss ON ss.id = (
SELECT id FROM session_summaries
WHERE session_row_id = s.id
ORDER BY COALESCE(session_intent_updated_at_epoch, created_at_epoch) DESC, id DESC
LIMIT 1
)";
impl ReadResourceSpec for Sessions {
type Row = SessionRow;
type Item = SessionItem;
const KIND: CursorKind = CursorKind::Sessions;
fn row_id(row: &Self::Row) -> i64 {
row.id
}
fn load_batch(
conn: &Connection,
resume_before_id: Option<i64>,
project: Option<&str>,
status: Option<&str>,
limit: usize,
) -> anyhow::Result<Vec<Self::Row>> {
let sql = format!(
"{SELECT_SESSION}
WHERE (?1 IS NULL OR s.id < ?1)
AND (?2 IS NULL OR p.project_key = ?2)
AND (?3 IS NULL OR s.status = ?3)
ORDER BY s.id DESC LIMIT ?4"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map(
params![resume_before_id, project, status, limit as i64],
map_row,
)?;
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
}
fn load_one(conn: &Connection, id: i64) -> anyhow::Result<Option<Self::Row>> {
conn.query_row(
&format!("{SELECT_SESSION} WHERE s.id = ?1"),
params![id],
map_row,
)
.optional()
.map_err(Into::into)
}
fn project(
row: Self::Row,
policy: &ResourceProjectionPolicy,
) -> anyhow::Result<Option<Self::Item>> {
let mut visible = vec![row.host.as_str(), row.project.as_str(), row.status.as_str()];
visible.extend(row.session_topic.as_deref());
if policy.suppresses(&visible, &[]) {
return Ok(None);
}
let host = redact_bounded(&row.host);
let project = redact_bounded(&row.project);
let status = redact_bounded(&row.status);
let session_topic = redact_optional(row.session_topic);
let label = crate::memory::session_label::render_from_stored(
row.started_at_epoch,
row.session_intent.as_deref(),
session_topic.as_deref(),
row.session_intent_source.as_deref(),
Some(&format!("Session on {host}")),
);
Ok(Some(SessionItem {
id: row.id,
summary: label.display_title.clone(),
host: host.clone(),
project: project.clone(),
status,
started_at_epoch: row.started_at_epoch,
last_seen_at_epoch: row.last_seen_at_epoch,
mmdd: label.mmdd,
session_intent: label.session_intent,
session_topic: label.session_topic,
display_label: label.display_label,
session_intent_source: label.session_intent_source,
references: vec![
SafeResourceRef {
kind: "host",
id: row.host_id,
title: Some(host),
status: None,
},
SafeResourceRef {
kind: "project",
id: row.project_id,
title: Some(project),
status: None,
},
],
}))
}
}
fn map_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<SessionRow> {
Ok(SessionRow {
id: row.get(0)?,
host_id: row.get(1)?,
host: row.get(2)?,
project_id: row.get(3)?,
project: row.get(4)?,
started_at_epoch: row.get(5)?,
last_seen_at_epoch: row.get(6)?,
status: row.get(7)?,
session_intent: row.get(8)?,
session_topic: row.get(9)?,
session_intent_source: row.get(10)?,
})
}
pub(in crate::api) fn session_is_visible(conn: &Connection, id: i64) -> anyhow::Result<bool> {
let policy = ResourceProjectionPolicy::load(conn)?;
match Sessions::load_one(conn, id)? {
Some(row) => Ok(Sessions::project(row, &policy)?.is_some()),
None => Ok(false),
}
}