Status LEDs, image model selection, backup restore, provider test, monthly versions

- Sidebar: three status LEDs (server, models, MCP) with an overview popover (/api/status)
- Images: preview stage, purpose-based model choice and reference images
  (Nano Banana Pro edit refines the chosen preview)
- Settings: restore a stored or uploaded backup (staged, applied on restart)
- Provider form: test connection; saving or reloading re-checks the status at once
- opencode Zen/Go: send x-opencode-session with a stable id per conversation
- Releases are named vYYYYMM-NNNNN; scripts/release.sh numbers, tags, builds and publishes
- Fix: backups made in the same second no longer overwrite each other

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-09 09:31:24 +01:00
1 parent 49d404eec7
commit 1f69d63b09
35 files changed
+1156 -88

No files matched your search

+20 -17
View File
@@ -33,29 +33,32 @@ impl Health {
}
}
/// Fetch the model list once and classify the answer.
pub async fn probe(provider: Arc<dyn crate::provider::Provider>) -> (ProviderStatus, usize) {
let start = Instant::now();
let res = tokio::time::timeout(Duration::from_secs(15), provider.list_models()).await;
let (error, count): (Option<String>, usize) = match res {
Ok(Ok(m)) => (None, m.len()),
Ok(Err(e)) => (Some(e.to_string().chars().take(300).collect()), 0),
Err(_) => (Some("timed out".to_string()), 0),
};
// The host answered but has no model list: reachable.
let error = error.filter(|e| !(e.contains("HTTP 404") || e.contains("HTTP 405")));
let state = match &error {
None => "ok",
Some(e) if e.contains("HTTP 401") || e.contains("HTTP 403") => "auth",
Some(_) => "down",
};
(ProviderStatus { up: error.is_none(), state, error, latency_ms: start.elapsed().as_millis() as u64, checked_at: chrono_now() }, count)
}
pub async fn check_all(app: &App) {
let registry = app.registry();
let mut found = BTreeMap::new();
let checks = registry.providers.iter().map(|(name, entry)| {
let provider = entry.provider.clone();
let name = name.clone();
async move {
let start = Instant::now();
let res = tokio::time::timeout(Duration::from_secs(15), provider.list_models()).await;
let error: Option<String> = match res {
Ok(Ok(_)) => None,
Ok(Err(e)) => Some(e.to_string().chars().take(300).collect()),
Err(_) => Some("timed out".to_string()),
};
// The host answered but has no model list: reachable.
let error = error.filter(|e| !(e.contains("HTTP 404") || e.contains("HTTP 405")));
let state = match &error {
None => "ok",
Some(e) if e.contains("HTTP 401") || e.contains("HTTP 403") => "auth",
Some(_) => "down",
};
(name, ProviderStatus { up: error.is_none(), state, error, latency_ms: start.elapsed().as_millis() as u64, checked_at: chrono_now() })
}
async move { (name, probe(provider).await.0) }
});
for (name, st) in futures::future::join_all(checks).await {
if !st.up {