Stats history with year filter, folding and XLSX/CSV export; snippets, read-only links, commit suggestions, weekly review, offline copies, server speech-to-text, planning model, job reports; fal.ai health fix

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Sonnet 5.5 committed 2026-11-15 08:20:52 +01:00
1 parent c08c6c7436
commit 388ca96419
31 files changed
+1518 -82

No files matched your search

+46
View File
@@ -68,6 +68,13 @@ pub struct JobSettings {
/// Chat in which the user works out the prompt with a model.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub assistant_session: Option<String>,
/// Push notification with the report when a run ends.
#[serde(default = "default_notify")]
pub notify: bool,
}
fn default_notify() -> bool {
true
}
impl JobSettings {
@@ -81,6 +88,7 @@ impl JobSettings {
timeout_minutes: default_timeout(),
cwd: None,
assistant_session: None,
notify: true,
}
}
@@ -121,6 +129,7 @@ pub struct JobSpec {
pub cwd: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub assistant_session: Option<String>,
pub notify: bool,
pub prompt: String,
}
@@ -135,6 +144,7 @@ impl JobSpec {
timeout_minutes: s.timeout_minutes,
cwd: s.cwd.clone(),
assistant_session: s.assistant_session.clone(),
notify: s.notify,
prompt,
}
}
@@ -149,6 +159,7 @@ impl JobSpec {
timeout_minutes: self.timeout_minutes,
cwd: self.cwd.clone(),
assistant_session: self.assistant_session.clone(),
notify: self.notify,
}
}
}
@@ -218,6 +229,7 @@ pub fn parse(path: &Path) -> Result<JobSpec> {
timeout_minutes: meta.get("timeout_minutes").and_then(|v| unquote(v).parse().ok()).unwrap_or(60).clamp(1, 24 * 60),
cwd: meta.get("cwd").map(unquote).filter(|c| !c.is_empty()),
assistant_session: None,
notify: meta.get("notify").map(|v| unquote(v) != "false").unwrap_or(true),
prompt: body.trim().to_string(),
})
}
@@ -342,6 +354,7 @@ pub async fn run_with(app: &Arc<App>, job: &Job, trigger: &str, payload: Option<
let job_id = job.id.clone();
let sid = session.id.clone();
let timeout = Duration::from_secs(spec.timeout_minutes * 60);
let (spec_name, spec_notify) = (spec.name.clone(), spec.notify);
tokio::spawn(async move {
let started = std::time::Instant::now();
tokio::time::sleep(Duration::from_secs(2)).await;
@@ -349,15 +362,48 @@ pub async fn run_with(app: &Arc<App>, job: &Job, trigger: &str, payload: Option<
if started.elapsed() > timeout {
crate::runner::abort(&app2, &sid);
let _ = app2.store.job_finished(&job_id, Some(&sid), "timeout");
report(&app2, &sid, &spec_name, spec_notify, "timeout").await;
return;
}
tokio::time::sleep(Duration::from_secs(5)).await;
}
let _ = app2.store.job_finished(&job_id, Some(&sid), "done");
report(&app2, &sid, &spec_name, spec_notify, "done").await;
});
Ok(session.id)
}
/// Push the report of a finished run (the agent's last message) to the owner.
async fn report(app: &App, session_id: &str, name: &str, notify: bool, status: &str) {
if !notify {
return;
}
let Ok(session) = app.session(session_id) else { return };
let Some(owner) = session.owner_id.clone() else { return };
let prefs: Value = app.store.user_setting(&owner, "prefs").unwrap_or_default();
let de = prefs["lang"].as_str().unwrap_or("").starts_with("de");
let last = app
.store
.messages(session_id)
.unwrap_or_default()
.into_iter()
.rev()
.find(|m| matches!(m.role, crate::types::Role::Assistant))
.map(|m| m.parts.iter().filter_map(|p| if let Part::Text { text } = p { Some(text.as_str()) } else { None }).collect::<Vec<_>>().join("\n"))
.unwrap_or_default();
let flat: String = last.split_whitespace().collect::<Vec<_>>().join(" ");
let body: String = if flat.chars().count() > 220 { format!("{}…", flat.chars().take(220).collect::<String>()) } else { flat };
let title = match (status, de) {
("timeout", true) => format!("{name} – Zeitlimit erreicht"),
("timeout", false) => format!("{name} – timed out"),
(_, true) => format!("{name} – fertig"),
(_, false) => format!("{name} – finished"),
};
let payload = json!({"kind": "job", "title": title, "body": body, "tag": format!("job-{session_id}"), "url": format!("/#/s/{session_id}")});
let (sent, _) = crate::push::send_to_user(app, &owner, &payload).await;
tracing::info!(job = name, sent, "automation report");
}
/// Background scheduler: checks every 30 s which jobs are due.
pub fn spawn_scheduler(app: Arc<App>) {
tokio::spawn(async move {