feat: phase 2 — am web : dashboard web local + API JSON (contrats --json, 127.0.0.1, tiny_http, frontend embarqué) (closes #54)
This commit is contained in:
+22
-8
@@ -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()
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user