app
Adom Video Post-Production
Public Made by Adomby adom
AI-driven studio for finishing demo videos: review every captured clip, flag and re-record the weak ones, speed up dead air, narrate, mux and auto-level, validate the final cut, and publish straight to the wiki, driven end to end by your AI.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
//! Shared in-app log buffer.
//!
//! Every webapp piping the UI's `console.log` output back to the
//! server via `POST /console` so the CLI operator (and the AI driving
//! the tool) can read what the UI is saying without having to open
//! devtools in the Hydrogen webview. The same buffer is the target
//! of `adom-video-post ctrl <target> console --tail` / `--follow` so the
//! AI can stream UI logs into the Claude Code Monitor tool.
use std::collections::VecDeque;
use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration, Instant};
use tiny_http::{Header, Request, Response};
pub const CONSOLE_MAX: usize = 500;
/// Shared handle to the in-app log buffer. Append to the back,
/// evict from the front on overflow. Each entry is a pre-serialized
/// JSON string so the GET handler can splat them directly.
pub struct Console {
pub messages: Mutex<VecDeque<ConsoleEntry>>,
/// Incremented on every append, used by long-poll clients to
/// detect "anything new since last poll".
pub seq: Mutex<u64>,
pub cond: Condvar,
}
pub struct ConsoleEntry {
pub seq: u64,
pub json: String,
}
pub type SharedConsole = Arc<Console>;
pub fn new() -> SharedConsole {
Arc::new(Console {
messages: Mutex::new(VecDeque::with_capacity(CONSOLE_MAX)),
seq: Mutex::new(0),
cond: Condvar::new(),
})
}
/// `GET /console[?since=N&wait=MS]` — tail the buffer.
///
/// Without query params, returns every message currently in the
/// ring. With `since=N`, returns only messages whose seq is > N.
/// With `wait=MS` AND nothing new to return, blocks up to MS on a
/// Condvar waiting for a new append. This is the long-poll variant
/// used by `adom-video-post ctrl ... console --follow` to feed realtime
/// UI logs into the Claude Code Monitor tool.
pub fn handle_get(request: Request, console: &SharedConsole, url: &str) -> Result<(), String> {
let query = url.split_once('?').map(|(_, q)| q).unwrap_or("");
let mut since: u64 = 0;
let mut wait_ms: u64 = 0;
for pair in query.split('&') {
if let Some(n) = pair.strip_prefix("since=") {
since = n.parse().unwrap_or(0);
} else if let Some(n) = pair.strip_prefix("wait=") {
wait_ms = n.parse().unwrap_or(0);
}
}
let entries = {
let mut guard = console.messages.lock().unwrap();
let mut matching: Vec<String> = guard
.iter()
.filter(|e| e.seq > since)
.map(|e| e.json.clone())
.collect();
if matching.is_empty() && wait_ms > 0 {
let deadline = Instant::now() + Duration::from_millis(wait_ms);
while matching.is_empty() {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
break;
}
let (g, wait_result) = console.cond.wait_timeout(guard, remaining).unwrap();
guard = g;
matching = guard
.iter()
.filter(|e| e.seq > since)
.map(|e| e.json.clone())
.collect();
if wait_result.timed_out() {
break;
}
}
}
matching
};
let latest_seq = *console.seq.lock().unwrap();
let body = format!(
r#"{{"ok":true,"seq":{},"messages":[{}]}}"#,
latest_seq,
entries.join(",")
);
request
.respond(json_response(200, &body))
.map_err(|e| e.to_string())
}
/// `POST /console` — append a JSON-encoded log message. The request
/// body is validated as JSON, re-serialized compactly, and echoed
/// to the server's stdout so the CLI operator sees it too.
pub fn handle_post(mut request: Request, console: &SharedConsole) -> Result<(), String> {
let mut body = String::new();
request
.as_reader()
.read_to_string(&mut body)
.map_err(|e| e.to_string())?;
let msg: serde_json::Value = match serde_json::from_str(&body) {
Ok(v) => v,
Err(e) => {
let _ = request.respond(json_response(
400,
&format!(r#"{{"ok":false,"error":"invalid json: {}"}}"#, e),
));
return Ok(());
}
};
let compact = msg.to_string();
println!("[ui-console] {}", compact);
append(console, compact);
request
.respond(json_response(200, r#"{"ok":true}"#))
.map_err(|e| e.to_string())
}
/// `DELETE /console` — drop all buffered messages.
pub fn handle_delete(request: Request, console: &SharedConsole) -> Result<(), String> {
console.messages.lock().unwrap().clear();
request
.respond(json_response(200, r#"{"ok":true}"#))
.map_err(|e| e.to_string())
}
/// Append a pre-serialized JSON string to the buffer, evicting the
/// oldest entry on overflow, bumping the seq counter, and waking
/// any long-poll waiters.
pub fn append(console: &SharedConsole, json: String) {
let mut seq_guard = console.seq.lock().unwrap();
*seq_guard += 1;
let seq = *seq_guard;
drop(seq_guard);
let mut guard = console.messages.lock().unwrap();
if guard.len() >= CONSOLE_MAX {
guard.pop_front();
}
guard.push_back(ConsoleEntry { seq, json });
console.cond.notify_all();
}
fn json_response(status: u16, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
Response::from_string(body)
.with_status_code(status)
.with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
}