Launching the app died with "'dict' object has no attribute 'readiness'".
TeamView.__init__ forwarded its legacy app_state dict into update_data(snapshot),
whose guard only handled None, so {} reached snapshot.readiness. TeamView is the
default view, so the window never appeared.
Fixes the call site and hardens every snapshot guard to fall back on anything
that is not a HubSnapshot. Adds tests/test_view_startup_contract.py, which
checks both conditions statically and therefore runs headless; verified to fail
on the pre-fix sources.
The GUI-import invariant previously matched the literal string "customtkinter",
which missed modules pulling it transitively. Rewritten over the AST, it
immediately found test_codex_opencode_wizard.py importing router.ui.components
without pytest.importorskip — the same defect that took the release gate down
once before, hidden until now behind conftest's name-based skip.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
103 lines
5.5 KiB
Python
103 lines
5.5 KiB
Python
"""Hermes Hub — Providers View (Реальные метрики провайдеров и моделей)."""
|
||
from __future__ import annotations
|
||
|
||
from typing import Any, Callable, Dict, List, Optional
|
||
import customtkinter as ctk
|
||
|
||
from antigravity_provider.router.ui.theme import Theme
|
||
from antigravity_provider.router.ui.components import (
|
||
HubButton,
|
||
HubCard,
|
||
HubSectionHeader,
|
||
HubStatusBadge,
|
||
)
|
||
from antigravity_provider.router.unified_health import (
|
||
UnifiedHealthService,
|
||
ProviderSummary,
|
||
)
|
||
|
||
|
||
class ProvidersView(ctk.CTkFrame):
|
||
def __init__(self, master: Any, app_state: Dict[str, Any], on_action: Optional[Callable] = None, **kwargs):
|
||
super().__init__(master=master, fg_color="transparent", **kwargs)
|
||
self.app_state = app_state
|
||
self.on_action = on_action
|
||
self._build()
|
||
|
||
def _build(self):
|
||
header = HubSectionHeader(
|
||
self,
|
||
title="Провайдеры ИИ и Модели",
|
||
subtitle="Сводка доступности адаптеров, квот и обнаруженных языковых моделей",
|
||
action_text="🔄 Обновить модели",
|
||
action_cmd=self._refresh_models,
|
||
)
|
||
header.pack(fill="x", padx=20, pady=(16, 12))
|
||
|
||
self.scroll = ctk.CTkScrollableFrame(self, fg_color="transparent")
|
||
self.scroll.pack(fill="both", expand=True, padx=15, pady=(0, 10))
|
||
|
||
self.update_data()
|
||
|
||
def update_data(self, snapshot: Optional[Any] = None):
|
||
for w in self.scroll.winfo_children():
|
||
w.destroy()
|
||
|
||
from antigravity_provider.router.state_store import HubSnapshot, HubStateStore
|
||
if not isinstance(snapshot, HubSnapshot):
|
||
# Callers have historically passed legacy app_state dicts here; a
|
||
# non-snapshot must fall back to the store, not crash the view.
|
||
snapshot = HubStateStore.get().get_snapshot()
|
||
|
||
summaries = snapshot.providers
|
||
|
||
for s in summaries:
|
||
col = Theme.PROVIDER_ANTIGRAVITY if "antigravity" in s.provider_id else (Theme.PROVIDER_CODEX if "codex" in s.provider_id else Theme.PROVIDER_OPENCODE)
|
||
|
||
card = HubCard(self.scroll, border_color=Theme.BORDER, fg_color=Theme.SURFACE)
|
||
card.pack(fill="x", pady=8)
|
||
|
||
# Top Header
|
||
top = ctk.CTkFrame(card, fg_color="transparent")
|
||
top.pack(fill="x", padx=16, pady=(14, 4))
|
||
|
||
ctk.CTkLabel(top, text=s.provider_name, font=Theme.font_heading(), text_color=col).pack(side="left")
|
||
ctk.CTkLabel(top, text=f"Обновлено: {s.last_refresh_at}", font=Theme.font_micro(), text_color=Theme.TEXT_MUTED).pack(side="right")
|
||
|
||
# Metrics Row (4 mini cards)
|
||
metrics_row = ctk.CTkFrame(card, fg_color="transparent")
|
||
metrics_row.pack(fill="x", padx=16, pady=6)
|
||
for i in range(4):
|
||
metrics_row.grid_columnconfigure(i, weight=1)
|
||
|
||
p1 = ctk.CTkFrame(metrics_row, fg_color=Theme.SURFACE_MUTED, corner_radius=Theme.RADIUS_SM)
|
||
p1.grid(row=0, column=0, padx=4, sticky="nsew")
|
||
ctk.CTkLabel(p1, text="ПОДКЛЮЧЕНО", font=Theme.font_micro(), text_color=Theme.TEXT_MUTED).pack(pady=(6, 1))
|
||
ctk.CTkLabel(p1, text=f"{s.connected_count} / {s.total_slots}", font=Theme.font_body_bold(), text_color=Theme.TEXT_PRIMARY).pack(pady=(0, 6))
|
||
|
||
p2 = ctk.CTkFrame(metrics_row, fg_color=Theme.SURFACE_MUTED, corner_radius=Theme.RADIUS_SM)
|
||
p2.grid(row=0, column=1, padx=4, sticky="nsew")
|
||
ctk.CTkLabel(p2, text="В СТРОЮ", font=Theme.font_micro(), text_color=Theme.TEXT_MUTED).pack(pady=(6, 1))
|
||
ctk.CTkLabel(p2, text=str(s.online_count), font=Theme.font_body_bold(), text_color=Theme.STATUS_HEALTHY).pack(pady=(0, 6))
|
||
|
||
p3 = ctk.CTkFrame(metrics_row, fg_color=Theme.SURFACE_MUTED, corner_radius=Theme.RADIUS_SM)
|
||
p3.grid(row=0, column=2, padx=4, sticky="nsew")
|
||
ctk.CTkLabel(p3, text="ТРЕБУЮТ ВХОДА", font=Theme.font_micro(), text_color=Theme.TEXT_MUTED).pack(pady=(6, 1))
|
||
ctk.CTkLabel(p3, text=str(s.auth_required_count), font=Theme.font_body_bold(), text_color=Theme.STATUS_WARNING if s.auth_required_count > 0 else Theme.TEXT_MUTED).pack(pady=(0, 6))
|
||
|
||
p4 = ctk.CTkFrame(metrics_row, fg_color=Theme.SURFACE_MUTED, corner_radius=Theme.RADIUS_SM)
|
||
p4.grid(row=0, column=3, padx=4, sticky="nsew")
|
||
ctk.CTkLabel(p4, text="ХОЛОДНЫЙ РЕЗЕРВ", font=Theme.font_micro(), text_color=Theme.TEXT_MUTED).pack(pady=(6, 1))
|
||
ctk.CTkLabel(p4, text=str(s.cold_spare_count), font=Theme.font_body_bold(), text_color=Theme.TEXT_MUTED).pack(pady=(0, 6))
|
||
|
||
# Discovered Models list
|
||
m_box = ctk.CTkFrame(card, fg_color=Theme.SURFACE_MUTED, corner_radius=Theme.RADIUS_SM)
|
||
m_box.pack(fill="x", padx=16, pady=(4, 12))
|
||
|
||
ctk.CTkLabel(m_box, text="Доступные проверенные модели:", font=Theme.font_micro(), text_color=Theme.TEXT_MUTED).pack(anchor="w", padx=10, pady=(6, 2))
|
||
models_str = " • ".join(s.discovered_models) if s.discovered_models else "Модели не обнаружены"
|
||
ctk.CTkLabel(m_box, text=models_str, font=Theme.font_mono_sm(), text_color=Theme.TEXT_PRIMARY, wraplength=700, justify="left").pack(anchor="w", padx=10, pady=(0, 8))
|
||
|
||
def _refresh_models(self):
|
||
if self.on_action:
|
||
self.on_action("refresh_data", {})
|