feat: phase 2 — am web : dashboard web local + API JSON (contrats --json, 127.0.0.1, tiny_http, frontend embarqué) (closes #54)
release / linux (push) Successful in 14m16s
release / windows (push) Canceled after 0s
release / macos (push) Canceled after 0s

This commit is contained in:
2026-08-18 16:40:15 -04:00
parent 7f4d74f811
commit fa51cdb339
20 changed files with 1392 additions and 40 deletions
+22 -8
View File
@@ -75,14 +75,14 @@ pub fn parse_since(s: &str) -> Result<chrono::DateTime<chrono::FixedOffset>> {
}
#[derive(Serialize)]
struct LogRow {
ts: String,
kind: String,
agent: Option<String>,
pid: Option<u32>,
exit_code: Option<i32>,
duration_s: Option<u64>,
session: Option<String>,
pub struct LogRow {
pub ts: String,
pub kind: String,
pub agent: Option<String>,
pub pid: Option<u32>,
pub exit_code: Option<i32>,
pub duration_s: Option<u64>,
pub session: Option<String>,
}
impl From<&Event> for LogRow {
@@ -99,6 +99,20 @@ impl From<&Event> for LogRow {
}
}
/// Rows for the web API (issue #54): same shape as `am log --json`.
pub fn rows(
app: &App,
agent: Option<&str>,
kinds: Option<&str>,
since: Option<&str>,
limit: usize,
) -> Result<Vec<LogRow>> {
Ok(filter_events(app, agent, kinds, since, limit)?
.iter()
.map(LogRow::from)
.collect())
}
/// Chars of text after byte offset seen (UTF-8 safe).
pub fn text_after(text: &str, seen: usize) -> String {
text.char_indices()
+2
View File
@@ -47,6 +47,7 @@ pub mod uninstall_cmd;
pub mod update_cmd;
pub mod version_cmd;
pub mod watch_cmd;
pub mod web_cmd;
use crate::app::App;
use crate::cli::Command;
@@ -117,6 +118,7 @@ pub fn execute_command(app: &App, cmd: &Command) -> Result<i32> {
Command::Monitor { interval, json } => {
monitor_cmd::run(app, *interval, *json)
}
Command::Web(args) => web_cmd::run(app, args.port, args.no_open),
Command::Sync { message } => sync_cmd::run(app, message.as_deref()),
Command::Migrate { export, output, bundle } => {
migrate_cmd::run(app, *export, output.as_deref(), bundle.as_deref())
+35 -23
View File
@@ -7,19 +7,19 @@ use serde::Serialize;
use std::path::PathBuf;
#[derive(Serialize)]
struct SessionRow {
id: String,
kind: String,
agent: Option<String>,
status: String,
pid: Option<u32>,
cwd: Option<String>,
project: Option<String>,
started_at: String,
ended_at: Option<String>,
duration_s: Option<u64>,
exit_code: Option<i32>,
log: Option<String>,
pub struct SessionRow {
pub id: String,
pub kind: String,
pub agent: Option<String>,
pub status: String,
pub pid: Option<u32>,
pub cwd: Option<String>,
pub project: Option<String>,
pub started_at: String,
pub ended_at: Option<String>,
pub duration_s: Option<u64>,
pub exit_code: Option<i32>,
pub log: Option<String>,
}
fn row_of(r: &SessionRecord) -> SessionRow {
@@ -39,6 +39,27 @@ fn row_of(r: &SessionRecord) -> SessionRow {
}
}
/// Rows for the web API (issue #54): same shape as `am sessions --json`.
/// Unlike `run`, it never reconciles the registry (the web is read-only).
pub fn list_rows(
app: &App,
agent: Option<&str>,
project: Option<&str>,
status: Option<&str>,
) -> Result<Vec<SessionRow>> {
let sf = sessions::load(app)?;
let mut rows: Vec<SessionRow> = sf
.sessions
.values()
.filter(|r| agent.map_or(true, |a| r.agent.as_deref() == Some(a)))
.filter(|r| project.map_or(true, |p| r.project.as_deref() == Some(p)))
.filter(|r| status.map_or(true, |s| r.status == s))
.map(row_of)
.collect();
rows.sort_by(|a, b| b.started_at.cmp(&a.started_at));
Ok(rows)
}
pub fn run(
app: &App,
agent: Option<&str>,
@@ -63,16 +84,7 @@ pub fn run(
return resume_session(app, id);
}
sessions::reconcile(app)?;
let sf = sessions::load(app)?;
let mut rows: Vec<SessionRow> = sf
.sessions
.values()
.filter(|r| agent.map_or(true, |a| r.agent.as_deref() == Some(a)))
.filter(|r| project.map_or(true, |p| r.project.as_deref() == Some(p)))
.filter(|r| status.map_or(true, |s| r.status == s))
.map(row_of)
.collect();
rows.sort_by(|a, b| b.started_at.cmp(&a.started_at));
let rows = list_rows(app, agent, project, status)?;
if app.json() {
print_json(&rows);
return Ok(0);
+9
View File
@@ -242,6 +242,15 @@ pub static SECTIONS: &[TipSection] = &[
],
example: "monitor --interval 5",
},
TipEntry {
usage: "web",
about: "dashboard web local en lecture seule : vue d'ensemble, graphiques, sessions, événements, projets (issue #54)",
options: &[
("--port <p>", "port d'écoute (défaut 7878)"),
("--no-open", "ne pas ouvrir le navigateur automatiquement"),
],
example: "web --port 9090",
},
TipEntry {
usage: "sync",
about: "pousse l'état (state.json, journal, historique) dans le dépôt git configuré (sync_repo)",
+7
View File
@@ -0,0 +1,7 @@
//! web: serve the local read-only dashboard (issue #54).
use super::*;
pub fn run(app: &App, port: Option<u16>, no_open: bool) -> Result<i32> {
crate::web::run(app, port.unwrap_or(crate::web::DEFAULT_PORT), !no_open)
}