hermes-hub/src/antigravity_provider/router/ui/views/providers_view.py
Hermes Team a9c04b75a2 fix(ui): repair startup crash in TeamView and tighten the headless guard
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>
2026-08-20 23:22:19 +07:00

103 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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", {})