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:
1 parent
49d404eec7
commit
1f69d63b09
35 files changed
+1156
-88
No files matched your search
+20
-17
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user