hermes-hub/src/antigravity_provider/router/ui/views/quotas_view.py

83 lines
4.3 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.

"""Truthful multi-bucket quota overview driven by HubSnapshot."""
from __future__ import annotations
from typing import Any, Dict, Optional
import customtkinter as ctk
from antigravity_provider.router.state_store import HubSnapshot
from antigravity_provider.router.ui.components import EmptyState, HubCard, QuotaBar, SectionHeader
from antigravity_provider.router.ui.theme import Theme
class _QuotaRow(HubCard):
def __init__(self, master: Any):
super().__init__(master, corner_radius=Theme.RADIUS_SM)
header = ctk.CTkFrame(self, fg_color="transparent")
header.pack(fill="x", padx=Theme.CARD_PAD_X, pady=(Theme.CARD_PAD_Y, Theme.SPACE_XS))
self.title = ctk.CTkLabel(header, text="", font=Theme.font_body_bold(), text_color=Theme.TEXT_PRIMARY)
self.title.pack(side="left")
self.source = ctk.CTkLabel(header, text="", font=Theme.font_micro(), text_color=Theme.TEXT_MUTED)
self.source.pack(side="right")
self.identity = ctk.CTkLabel(self, text="", font=Theme.font_caption(), text_color=Theme.TEXT_SECONDARY)
self.identity.pack(anchor="w", padx=Theme.CARD_PAD_X)
self.bar = QuotaBar(self, label="Остаток")
self.bar.pack(fill="x", padx=Theme.CARD_PAD_X, pady=Theme.SPACE_SM)
self.reset = ctk.CTkLabel(self, text="", font=Theme.font_micro(), text_color=Theme.TEXT_MUTED)
self.reset.pack(anchor="w", padx=Theme.CARD_PAD_X, pady=(0, Theme.CARD_PAD_Y))
def update_bucket(self, profile: Any, quota: Any, bucket: Any) -> None:
remaining = getattr(bucket, "remaining_percent", None)
ratio = float(remaining) / 100.0 if remaining is not None else None
detail = bucket.formatted_remaining() if hasattr(bucket, "formatted_remaining") else "Н"
reset = bucket.formatted_reset() if hasattr(bucket, "formatted_reset") else None
estimated = bool(getattr(quota, "is_estimated", True))
self.title.configure(text=f"{bucket.display_name}{profile.provider_display_name}")
self.identity.configure(text=profile.account_identity or profile.display_name)
self.source.configure(text="оценка" if estimated else str(getattr(quota, "source", "измерено")))
self.bar.set_value(ratio, detail)
reason = getattr(quota, "unavailable_reason", None)
self.reset.configure(text=reason or reset or "Сброс: Н")
class QuotasView(ctk.CTkFrame):
def __init__(self, master: Any, **kwargs):
super().__init__(master=master, fg_color="transparent", **kwargs)
self._rows: Dict[str, _QuotaRow] = {}
SectionHeader(
self,
title="Квоты и лимиты",
subtitle="Независимые корзины провайдеров; отсутствие измерения не считается нулём",
).pack(fill="x", padx=Theme.PAGE_PAD_X, pady=(Theme.PAGE_PAD_Y, Theme.SPACE_SM))
self.scroll = ctk.CTkScrollableFrame(self, fg_color="transparent")
self.scroll.pack(fill="both", expand=True, padx=Theme.PAGE_PAD_X, pady=(0, Theme.PAGE_PAD_Y))
self.empty = EmptyState(
self.scroll,
title="Квоты: Н",
message="Провайдеры ещё не опубликовали ни одной квотной корзины.",
)
def update_data(self, snapshot: Optional[HubSnapshot] = None) -> None:
if not isinstance(snapshot, HubSnapshot):
return
incoming: list[tuple[str, Any, Any, Any]] = []
for profile in snapshot.all_profiles.values():
quota = snapshot.quotas.get(profile.profile_id)
for bucket in list(getattr(quota, "buckets", None) or []):
incoming.append((f"{profile.profile_id}:{bucket.id}", profile, quota, bucket))
live = {key for key, *_rest in incoming}
for key in list(self._rows):
if key not in live:
self._rows.pop(key).destroy()
for key, profile, quota, bucket in incoming:
row = self._rows.get(key)
if row is None:
row = _QuotaRow(self.scroll)
row.pack(fill="x", pady=Theme.SPACE_XS)
self._rows[key] = row
row.update_bucket(profile, quota, bucket)
if incoming:
self.empty.pack_forget()
else:
self.empty.pack(fill="x", pady=Theme.SPACE_XL)