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:
1 parent
c08c6c7436
commit
388ca96419
31 files changed
+1518
-82
No files matched your search
+49
-1
@@ -2,7 +2,7 @@
|
||||
//! user via the user setting "cost_push").
|
||||
|
||||
use crate::app::App;
|
||||
use chrono::{Local, Timelike};
|
||||
use chrono::{Datelike, Local, Timelike};
|
||||
use serde_json::{Value, json};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -54,11 +54,59 @@ pub fn summary(app: &App, user_id: &str) -> Option<Value> {
|
||||
}))
|
||||
}
|
||||
|
||||
/// The last seven days for one user (None when nothing happened).
|
||||
pub fn weekly(app: &App, user_id: &str) -> Option<Value> {
|
||||
let now = Local::now();
|
||||
let since = now.timestamp_millis() - 7 * 86_400_000;
|
||||
let offset = now.offset().local_minus_utc() as i64 / 60;
|
||||
let stats = app.store.global_stats(since, now.timestamp_millis() + 1, offset, Some(user_id)).ok()?;
|
||||
let requests = stats["total"]["requests"].as_i64().unwrap_or(0);
|
||||
let (chats, failed, checks) = app.store.weekly_counts(user_id, since).ok()?;
|
||||
if requests == 0 && chats == 0 {
|
||||
return None;
|
||||
}
|
||||
let prefs: Value = app.store.user_setting(user_id, "prefs").unwrap_or_default();
|
||||
let de = prefs["lang"].as_str().unwrap_or("").starts_with("de");
|
||||
let currency = app.config().server.currency.clone();
|
||||
let cost = money(stats["total"]["cost"].as_f64().unwrap_or(0.0), ¤cy, de);
|
||||
let top = stats["top_sessions"].as_array().and_then(|a| a.first()).and_then(|s| s["title"].as_str()).filter(|t| !t.is_empty()).map(str::to_string);
|
||||
let mut parts = if de { vec![format!("{chats} Chats aktiv"), format!("{requests} Anfragen · {cost}")] } else { vec![format!("{chats} chats active"), format!("{requests} requests · {cost}")] };
|
||||
if checks > 0 {
|
||||
parts.push(if de { format!("Checks: {failed} von {checks} fehlgeschlagen") } else { format!("checks: {failed} of {checks} failed") });
|
||||
}
|
||||
if let Some(t) = top {
|
||||
parts.push(if de { format!("teuerster Chat: {t}") } else { format!("most expensive chat: {t}") });
|
||||
}
|
||||
Some(json!({
|
||||
"kind": "digest",
|
||||
"title": if de { "CodeCraft – Wochenrückblick" } else { "CodeCraft – weekly review" },
|
||||
"body": parts.join("\n"),
|
||||
"tag": "weekly-digest",
|
||||
"url": "/#/stats",
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn spawn(app: Arc<App>) {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
let now = Local::now();
|
||||
// Weekly review: Monday morning, opt-in (user setting "weekly_push").
|
||||
if now.hour() == 8 && now.weekday() == chrono::Weekday::Mon {
|
||||
let week = now.format("%G-W%V").to_string();
|
||||
for user in app.store.users().unwrap_or_default() {
|
||||
let on: bool = app.store.user_setting(&user.id, "weekly_push").unwrap_or(false);
|
||||
let last: String = app.store.user_setting(&user.id, "weekly_push_last").unwrap_or_default();
|
||||
if !on || user.disabled || last == week {
|
||||
continue;
|
||||
}
|
||||
let _ = app.store.set_user_setting(&user.id, "weekly_push_last", &week);
|
||||
if let Some(payload) = weekly(&app, &user.id) {
|
||||
let (sent, errors) = crate::push::send_to_user(&app, &user.id, &payload).await;
|
||||
tracing::info!(user = %user.username, sent, errors = errors.len(), "weekly review");
|
||||
}
|
||||
}
|
||||
}
|
||||
if now.hour() != HOUR {
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user