fix: show Pricing Monitor source balances

This commit is contained in:
2026-08-03 01:08:49 +08:00
parent 845f103bd1
commit 49b2f963b9
5 changed files with 460 additions and 49 deletions
+294 -26
View File
@@ -7,6 +7,7 @@ import datetime as dt
import gzip
import importlib.metadata
import json
import math
import os
import re
import subprocess
@@ -20,10 +21,12 @@ from typing import Any
APP_NAME = "shusub2"
FALLBACK_VERSION = "0.2.13"
FALLBACK_VERSION = "0.2.14"
DEFAULT_API_URL = "http://127.0.0.1:18318/api/tui/accounts"
DEFAULT_CONFIG_FILE = "~/.config/shusub2/api-url"
DEFAULT_STATUS_CONFIG_FILE = "~/.config/shusub2/status-url"
DEFAULT_PRICING_URL = "https://price.tailbeb9ad.ts.net/api/ui-data?view=accounts"
DEFAULT_PRICING_URL_CONFIG_FILE = "~/.config/shusub2/pricing-url"
DEFAULT_LOGS_API_URL = "https://sub2apicn.shujk.top/api/v1/admin/usage"
DEFAULT_LOGS_URL_CONFIG_FILE = "~/.config/shusub2/logs-url"
DEFAULT_LOGS_TOKEN_CONFIG_FILE = "~/.config/shusub2/logs-token"
@@ -86,6 +89,14 @@ def default_status_url() -> str:
)
def default_pricing_url() -> str:
return configured_url(
("SHUSUB2_PRICING_URL",),
os.environ.get("SHUSUB2_PRICING_URL_FILE", DEFAULT_PRICING_URL_CONFIG_FILE),
DEFAULT_PRICING_URL,
)
def default_logs_url() -> str:
return configured_url(
("SHUSUB2_LOGS_URL",),
@@ -109,6 +120,12 @@ def logs_url_config_file_path() -> Path:
return Path(os.environ.get("SHUSUB2_LOGS_URL_FILE", DEFAULT_LOGS_URL_CONFIG_FILE)).expanduser()
def pricing_url_config_file_path() -> Path:
return Path(
os.environ.get("SHUSUB2_PRICING_URL_FILE", DEFAULT_PRICING_URL_CONFIG_FILE)
).expanduser()
def logs_token_config_file_path() -> Path:
return Path(os.environ.get("SHUSUB2_LOGS_TOKEN_FILE", DEFAULT_LOGS_TOKEN_CONFIG_FILE)).expanduser()
@@ -263,6 +280,30 @@ def format_cost(value: Any) -> str:
return f"${amount:.2f}".rstrip("0").rstrip(".")
def optional_number(value: Any) -> float | None:
if value is None or str(value).strip() == "":
return None
try:
number = float(value)
except (TypeError, ValueError):
return None
return number if math.isfinite(number) else None
def format_amount(value: Any) -> str:
amount = optional_number(value)
if amount is None:
return "-"
if amount.is_integer():
return f"{int(amount):,}"
return f"{amount:,.6f}".rstrip("0").rstrip(".")
def format_cny(value: Any) -> str:
amount = format_amount(value)
return "-" if amount == "-" else f"¥{amount}"
def format_count(value: Any) -> str:
number = as_int(value)
if abs(number) >= 1_000_000:
@@ -361,6 +402,13 @@ def fetch_optional_payload(url: str, timeout: int) -> tuple[dict[str, Any], str]
return {}, str(exc)
def fetch_pricing_payload(pricing_url: str, timeout: int) -> dict[str, Any]:
payload = fetch_payload(pricing_url, timeout)
if not isinstance(payload.get("sources"), list):
raise RuntimeError("Pricing Monitor did not return source balances")
return payload
def logs_request_url(logs_url: str, limit: int) -> str:
parsed = urllib.parse.urlparse(logs_url)
query = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True)
@@ -1063,13 +1111,6 @@ def window_cell(account: dict[str, Any], window_id: str) -> str:
return f"{format_percent(window.get('used_percent'))}/{format_percent(window.get('remaining_percent'))}"
def window_balance_cell(account: dict[str, Any], window_id: str) -> str:
window = window_for(account, window_id)
if not window or window.get("remaining_balance_usd") is None:
return "-"
return format_cost(window.get("remaining_balance_usd"))
def daily_quota_cell(account: dict[str, Any]) -> str:
limit = account.get("daily_quota_limit")
used = account.get("daily_quota_used")
@@ -1142,8 +1183,6 @@ def normalize_account_rows(payload: dict[str, Any], filter_text: str = "") -> li
"quota_used_percent_max": account.get("quota_used_percent_max"),
"five_hour": window_cell(account, "five-hour"),
"weekly": window_cell(account, "weekly"),
"five_hour_balance": window_balance_cell(account, "five-hour"),
"weekly_balance": window_balance_cell(account, "weekly"),
"reset": reset_cell(account),
"latest_usage_at": short_time(account.get("latest_usage_at") or account.get("last_used_at")),
"error": str(account.get("error") or ""),
@@ -1164,6 +1203,84 @@ def normalize_account_rows(payload: dict[str, Any], filter_text: str = "") -> li
return rows
def pricing_status(source: dict[str, Any]) -> str:
if str(source.get("last_error") or "").strip():
return "error"
state = str(source.get("health_state") or "").strip().lower()
return state or ("refreshing" if source.get("refreshing") else "unknown")
def normalize_pricing_rows(payload: dict[str, Any], filter_text: str = "") -> list[dict[str, Any]]:
needle = filter_text.strip().lower()
rows: list[dict[str, Any]] = []
for source in payload.get("sources") or []:
if not isinstance(source, dict):
continue
balance = source.get("balance") if isinstance(source.get("balance"), dict) else {}
row = {
"name": str(source.get("name") or "-"),
"kind": str(source.get("source_kind") or "-"),
"balance": optional_number(balance.get("available")),
"balance_cny": optional_number(balance.get("available_cny")),
"unit": str(balance.get("unit") or "-"),
"status": pricing_status(source),
"updated_at": source.get("last_success_at"),
"updated": short_time(source.get("last_success_at")),
"age": relative_age(source.get("last_success_at")),
"error": str(source.get("last_error") or "").strip(),
"raw": source,
}
haystack = " ".join(
str(row[key] or "") for key in ("name", "kind", "unit", "status", "error")
).lower()
if needle and needle not in haystack:
continue
rows.append(row)
rows.sort(key=lambda row: (row["status"] != "healthy", row["name"].lower()))
return rows
def pricing_summary(payload: dict[str, Any], rows: list[dict[str, Any]] | None = None) -> str:
source_rows = rows if rows is not None else normalize_pricing_rows(payload)
healthy = sum(row["status"] == "healthy" for row in source_rows)
cny_values = [
row["balance_cny"]
for row in source_rows
if row["status"] == "healthy" and row["balance_cny"] is not None
]
total_cny = sum(cny_values) if cny_values else None
total_label = f" | CNY {format_cny(total_cny)}" if total_cny is not None else ""
return f"upstreams {healthy}/{len(source_rows)} healthy{total_label}"
def pricing_detail_line(row: dict[str, Any]) -> str:
detail = (
f"{row['name']} | {row['kind']} | balance {format_amount(row['balance'])} {row['unit']} | "
f"CNY {format_cny(row['balance_cny'])} | {row['status']} | "
f"updated {row['updated']} ({row['age']})"
)
if row["error"]:
detail += f" | {row['error']}"
return detail
def print_pricing_once(payload: dict[str, Any], filter_text: str = "") -> None:
rows = normalize_pricing_rows(payload, filter_text)
print(f"{short_time(payload.get('generated_at'))} | {pricing_summary(payload, rows)}")
print("source kind balance cny unit status updated age")
for row in rows:
print(
f"{row['name']:<30} "
f"{row['kind']:<15} "
f"{format_amount(row['balance']):<19} "
f"{format_cny(row['balance_cny']):<19} "
f"{row['unit']:<9} "
f"{row['status']:<11} "
f"{row['updated']:<12} "
f"{row['age']}"
)
def summary_line(payload: dict[str, Any]) -> str:
totals = payload.get("totals") if isinstance(payload.get("totals"), dict) else {}
generated = short_time(payload.get("generated_local") or payload.get("generated_at"))
@@ -1181,7 +1298,7 @@ def print_once(payload: dict[str, Any], filter_text: str = "", status_payload: d
print(summary_line(payload))
if status_payload or status_error:
print(monitor_summary(status_payload or {}, status_error))
print("name provider group daily today tokens req kind 5h 5h bal 7d 7d bal reset status availability")
print("name provider group daily today tokens req kind 5h 7d reset status availability")
for row in normalize_account_rows(payload, filter_text):
print(
f"{row['name'][:30]:<30} "
@@ -1193,9 +1310,7 @@ def print_once(payload: dict[str, Any], filter_text: str = "", status_payload: d
f"{format_count(row['today_requests']):<5} "
f"{row['kind_label']:<9} "
f"{row['five_hour']:<9} "
f"{row['five_hour_balance']:<9} "
f"{row['weekly']:<9} "
f"{row['weekly_balance']:<9} "
f"{row['reset']:<11} "
f"{row['status']:<10} "
f"{monitor_availability(row, status_payload or {})}"
@@ -1205,6 +1320,7 @@ def print_once(payload: dict[str, Any], filter_text: str = "", status_payload: d
def run_textual(
api_url: str,
status_url: str,
pricing_url: str,
logs_url: str,
logs_token: str,
errors_cn_url: str,
@@ -1234,6 +1350,7 @@ def run_textual(
("r", "refresh", "Refresh"),
("/", "focus_filter", "Filter"),
("a", "focus_accounts", "Accounts"),
("p", "show_pricing", "Upstreams"),
("k", "focus_keys", "Keys"),
("l", "focus_logs", "Logs"),
("e", "focus_errors", "Errors"),
@@ -1243,6 +1360,8 @@ def run_textual(
super().__init__(classes="dashboard")
self.accounts_payload: dict[str, Any] = {}
self.status_payload: dict[str, Any] = {}
self.pricing_payload: dict[str, Any] = {}
self.pricing_error = ""
self.monitor_error = ""
self.accounts_error = ""
self.logs_payload: dict[str, Any] = {}
@@ -1285,7 +1404,7 @@ def run_textual(
def on_mount(self) -> None:
self.configure_table(
self.query_one("#accounts", DataTable),
("ACCOUNT", "Group", "Today", "Daily", "5h", "5h Bal", "7d", "7d Bal", "Avail"),
("ACCOUNT", "Group", "Today", "Daily", "5h", "7d", "Avail"),
)
self.configure_table(
self.query_one("#keys", DataTable),
@@ -1315,6 +1434,7 @@ def run_textual(
)
self.refresh_all(refresh=True)
self.set_interval(refresh_seconds, self.refresh_accounts)
self.set_interval(refresh_seconds, self.refresh_pricing)
self.set_interval(logs_refresh_seconds, self.refresh_logs)
self.set_interval(errors_refresh_seconds, self.refresh_errors)
self.focus_table("accounts")
@@ -1328,6 +1448,9 @@ def run_textual(
def action_focus_accounts(self) -> None:
self.focus_table("accounts")
def action_show_pricing(self) -> None:
self.app.switch_screen(PricingScreen())
def action_focus_keys(self) -> None:
table = self.query_one("#keys", DataTable)
if table.display:
@@ -1353,8 +1476,9 @@ def run_textual(
self.focus_table(self.active_table)
def refresh_all(self, refresh: bool = False) -> None:
self.query_one("#status", Static).update("refreshing accounts, keys, logs, and errors...")
self.query_one("#status", Static).update("refreshing accounts, upstream balances, keys, logs, and errors...")
self.refresh_accounts(refresh=refresh)
self.refresh_pricing()
self.refresh_logs()
self.refresh_errors()
@@ -1368,6 +1492,15 @@ def run_textual(
self.render_accounts()
self.render_meta()
def refresh_pricing(self) -> None:
self.pricing_error = ""
try:
self.pricing_payload = fetch_pricing_payload(pricing_url, timeout)
except Exception as exc:
self.pricing_payload = {}
self.pricing_error = str(exc)
self.render_meta()
def refresh_logs(self) -> None:
self.logs_error = ""
if not str(logs_token or "").strip():
@@ -1437,9 +1570,7 @@ def run_textual(
format_cost(row["today_cost_usd"]),
row["daily_quota_cell"],
row["five_hour"],
row["five_hour_balance"],
row["weekly"],
row["weekly_balance"],
monitor_availability(row, self.status_payload),
key=key,
)
@@ -1536,6 +1667,11 @@ def run_textual(
style="green",
)
summary.append(" | ", style="dim")
if self.pricing_error:
summary.append("upstreams unavailable", style="bright_blue")
else:
summary.append(pricing_summary(self.pricing_payload), style="bright_blue")
summary.append(" | ", style="dim")
summary.append(f"keys {len(self.key_rows)} ({key_summary.removeprefix('keys ')})", style="yellow")
summary.append(" | ", style="dim")
summary.append(f"logs {len(self.log_rows)}/{logs_total}", style="magenta")
@@ -1553,6 +1689,10 @@ def run_textual(
f"source {self.accounts_payload.get('source_name') or '-'}",
)
)
if self.pricing_error:
status_bits.append(f"upstreams: {self.pricing_error}")
elif self.pricing_payload:
status_bits.append(pricing_summary(self.pricing_payload) + f" | {pricing_url}")
shared_admin_error = self.logs_error and self.logs_error == self.errors_error
if shared_admin_error:
status_bits.append(f"logs/errors: {self.logs_error}")
@@ -1592,8 +1732,8 @@ def run_textual(
detail = (
f"{row['name']} | {row['provider']} | group {row['routing_group']} | {row['kind_label']} | "
f"daily {row['daily_quota_cell']} | "
f"5h {row['five_hour_balance']} ({row['five_hour']}) | "
f"7d {row['weekly_balance']} ({row['weekly']}) | "
f"5h {row['five_hour']} | "
f"7d {row['weekly']} | "
f"today {format_cost(row['today_cost_usd'])}, "
f"{format_count(row['today_tokens'])} tokens, {format_count(row['today_requests'])} req | "
f"{monitor_detail(row, self.status_payload)}"
@@ -1635,6 +1775,7 @@ def run_textual(
BINDINGS = [
("r", "refresh", "Refresh"),
("/", "focus_filter", "Filter"),
("p", "show_pricing", "Upstreams"),
("l", "show_logs", "Logs"),
("e", "show_errors", "Errors"),
]
@@ -1663,7 +1804,7 @@ def run_textual(
table = self.query_one("#accounts", DataTable)
table.cursor_type = "row"
table.zebra_stripes = True
table.add_columns("Name", "Provider", "Group", "Daily", "Today", "Tokens", "Req", "Kind", "5h", "5h Bal", "7d", "7d Bal", "Reset", "Status", "Availability")
table.add_columns("Name", "Provider", "Group", "Daily", "Today", "Tokens", "Req", "Kind", "5h", "7d", "Reset", "Status", "Availability")
keys_table = self.query_one("#keys", DataTable)
keys_table.cursor_type = "row"
keys_table.zebra_stripes = True
@@ -1677,6 +1818,9 @@ def run_textual(
def action_focus_filter(self) -> None:
self.query_one("#filter", Input).focus()
def action_show_pricing(self) -> None:
self.app.switch_screen(PricingScreen())
def action_show_logs(self) -> None:
self.app.switch_screen(LogsScreen())
@@ -1751,9 +1895,7 @@ def run_textual(
format_count(row["today_requests"]),
row["kind_label"],
row["five_hour"],
row["five_hour_balance"],
row["weekly"],
row["weekly_balance"],
row["reset"],
row["status"],
monitor_availability(row, self.status_payload),
@@ -1777,7 +1919,7 @@ def run_textual(
f"{row['name']} | {row['provider']} | group {row['routing_group']} | {row['kind_label']} | {row['account_type']} | "
f"daily {row['daily_quota_cell']} ({format_percent(row['daily_quota_used_percent'])} used, "
f"{row['daily_quota_remaining'] if row['daily_quota_remaining'] is not None else '-'} left) | "
f"5h {row['five_hour_balance']} ({row['five_hour']}) | 7d {row['weekly_balance']} ({row['weekly']}) | "
f"5h {row['five_hour']} | 7d {row['weekly']} | "
f"today {format_cost(row['today_cost_usd'])}, {format_count(row['today_tokens'])} tokens, "
f"{format_count(row['today_requests'])} req | latest {row['latest_usage_at']} | "
f"priority {row['priority'] if row['priority'] is not None else '-'} | "
@@ -1788,12 +1930,117 @@ def run_textual(
detail += f" | {error}"
self.query_one("#detail", Static).update(detail)
class PricingScreen(Screen[None]):
AUTO_FOCUS = "#pricing"
BINDINGS = [
("r", "refresh", "Refresh"),
("/", "focus_filter", "Filter"),
("a", "show_accounts", "Accounts"),
("l", "show_logs", "Logs"),
("e", "show_errors", "Errors"),
]
def __init__(self) -> None:
super().__init__()
self.payload: dict[str, Any] = {}
self.rows: list[dict[str, Any]] = []
self.row_by_key: dict[str, dict[str, Any]] = {}
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
yield Static("", id="summary")
yield Input(placeholder="filter upstream sources", id="filter")
yield DataTable(id="pricing")
yield Static("", id="detail")
yield Static("", id="status")
yield Footer()
def on_mount(self) -> None:
table = self.query_one("#pricing", DataTable)
table.cursor_type = "row"
table.zebra_stripes = True
table.add_columns("Source", "Kind", "Balance", "CNY", "Unit", "Status", "Updated", "Age")
self.refresh_data()
self.set_interval(refresh_seconds, self.refresh_data)
def action_refresh(self) -> None:
self.refresh_data()
def action_focus_filter(self) -> None:
self.query_one("#filter", Input).focus()
def action_show_accounts(self) -> None:
self.app.switch_screen(AccountsScreen())
def action_show_logs(self) -> None:
self.app.switch_screen(LogsScreen())
def action_show_errors(self) -> None:
self.app.switch_screen(ErrorsScreen())
def on_input_changed(self, event: Input.Changed) -> None:
if event.input.id == "filter":
self.render_payload()
def on_input_submitted(self, event: Input.Submitted) -> None:
if event.input.id == "filter":
self.query_one("#pricing", DataTable).focus()
def refresh_data(self) -> None:
status = self.query_one("#status", Static)
status.update("refreshing upstream balances from Pricing Monitor...")
try:
self.payload = fetch_pricing_payload(pricing_url, timeout)
self.render_payload()
status_bits = [version_message, pricing_summary(self.payload), pricing_url]
status.update(" | ".join(bit for bit in status_bits if bit))
except Exception as exc:
self.payload = {}
self.rows = []
self.row_by_key = {}
self.query_one("#pricing", DataTable).clear()
self.query_one("#summary", Static).update("upstreams unavailable")
self.query_one("#detail", Static).update("no upstream balances")
status.update(f"upstreams error: {exc}")
def render_payload(self) -> None:
filter_text = self.query_one("#filter", Input).value
self.rows = normalize_pricing_rows(self.payload, filter_text)
table = self.query_one("#pricing", DataTable)
table.clear()
self.row_by_key = {}
for index, row in enumerate(self.rows):
key = f"pricing-{row['name']}-{index}"
self.row_by_key[key] = row
table.add_row(
row["name"],
row["kind"],
format_amount(row["balance"]),
format_cny(row["balance_cny"]),
row["unit"],
row["status"],
row["updated"],
row["age"],
key=key,
)
self.query_one("#summary", Static).update(pricing_summary(self.payload, self.rows))
if self.rows:
self.query_one("#detail", Static).update(pricing_detail_line(self.rows[0]))
else:
self.query_one("#detail", Static).update("no upstream balances")
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
row = self.row_by_key.get(str(event.row_key.value))
if row:
self.query_one("#detail", Static).update(pricing_detail_line(row))
class LogsScreen(Screen[None]):
AUTO_FOCUS = "#logs"
BINDINGS = [
("r", "refresh", "Refresh"),
("/", "focus_filter", "Filter"),
("a", "show_accounts", "Accounts"),
("p", "show_pricing", "Upstreams"),
("e", "show_errors", "Errors"),
]
@@ -1845,6 +2092,9 @@ def run_textual(
def action_show_accounts(self) -> None:
self.app.switch_screen(AccountsScreen())
def action_show_pricing(self) -> None:
self.app.switch_screen(PricingScreen())
def action_show_errors(self) -> None:
self.app.switch_screen(ErrorsScreen())
@@ -1916,6 +2166,7 @@ def run_textual(
("r", "refresh", "Refresh"),
("/", "focus_filter", "Filter"),
("a", "show_accounts", "Accounts"),
("p", "show_pricing", "Upstreams"),
("l", "show_logs", "Logs"),
]
@@ -1951,6 +2202,9 @@ def run_textual(
def action_show_accounts(self) -> None:
self.app.switch_screen(AccountsScreen())
def action_show_pricing(self) -> None:
self.app.switch_screen(PricingScreen())
def action_show_logs(self) -> None:
self.app.switch_screen(LogsScreen())
@@ -2032,6 +2286,7 @@ def run_textual(
#filter { height: 1; border: none; padding: 0 1; }
#filter:focus { border: none; }
#accounts { height: 1fr; min-height: 3; }
#pricing { height: 1fr; min-height: 3; }
#keys { height: auto; max-height: 6; }
.dashboard #accounts { background: $success 5%; }
.dashboard #accounts > .datatable--header { background: $success 35%; color: $foreground; }
@@ -2053,6 +2308,8 @@ def run_textual(
def on_mount(self) -> None:
if start_page == "accounts":
self.push_screen(AccountsScreen())
elif start_page == "pricing":
self.push_screen(PricingScreen())
elif start_page == "logs":
self.push_screen(LogsScreen())
elif start_page == "errors":
@@ -2075,6 +2332,7 @@ def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Sub2API quota and daily usage TUI")
parser.add_argument("--api-url", default=default_api_url())
parser.add_argument("--status-url", default=default_status_url(), help="optional sub2api-status /api/status URL for channel monitor health")
parser.add_argument("--pricing-url", default=default_pricing_url(), help="Pricing Monitor /api/ui-data?view=accounts URL for upstream source balances")
parser.add_argument("--logs-url", default=default_logs_url(), help="Sub2API admin usage logs URL (default: sub2apicn /api/v1/admin/usage)")
parser.add_argument("--logs-token", default=default_logs_token(), help="Sub2API admin API key for logs/errors pages (prefer SHUSUB2_LOGS_TOKEN or ~/.config/shusub2/logs-token)")
parser.add_argument("--errors-cn-url", default=default_errors_cn_url(), help="cn Sub2API admin ops errors URL (default: sub2apicn /api/v1/admin/ops/errors)")
@@ -2083,7 +2341,7 @@ def build_parser() -> argparse.ArgumentParser:
default=default_errors_us_url(),
help="us Sub2API admin ops errors URL (default: DNS-only us.sub2 /api/v1/admin/ops/errors; CF sub2apius often blocks non-browser clients)",
)
parser.add_argument("--save-config", action="store_true", help="persist --api-url/--logs-url/--logs-token/--errors-*-url to ~/.config/shusub2/ before running")
parser.add_argument("--save-config", action="store_true", help="persist --api-url/--pricing-url/--logs-url/--logs-token/--errors-*-url to ~/.config/shusub2/ before running")
parser.add_argument("--install", action="store_true", help="persist config, install shusub2 as a uv tool, then exit")
parser.add_argument("--version-check-url", default=os.environ.get("SHUSUB2_VERSION_CHECK_URL", DEFAULT_VERSION_CHECK_URL))
parser.add_argument(
@@ -2125,6 +2383,7 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--timeout", type=int, default=env_int("SUB2API_QUOTA_TUI_TIMEOUT", DEFAULT_TIMEOUT_SECONDS))
parser.add_argument("--once", action="store_true", help="print one snapshot and exit")
parser.add_argument("--accounts", action="store_true", help="start on the dedicated accounts page (with --once: print the accounts snapshot)")
parser.add_argument("--pricing", action="store_true", help="start on the Pricing Monitor upstream balances page (with --once: print source balances)")
parser.add_argument("--logs", action="store_true", help="start on the dedicated request logs page (with --once: print a logs snapshot)")
parser.add_argument("--errors", action="store_true", help="start on the dedicated merged cn+us errors page (with --once: print an errors snapshot)")
parser.add_argument("--filter", default="", help="initial filter for --once output")
@@ -2137,13 +2396,16 @@ def main(argv: list[str] | None = None) -> int:
logs_limit = max(1, min(args.logs_limit, 1000))
errors_limit = max(1, min(args.errors_limit, 500))
errors_time_range = str(args.errors_time_range or DEFAULT_ERRORS_TIME_RANGE).strip() or DEFAULT_ERRORS_TIME_RANGE
selected_pages = sum(bool(value) for value in (args.accounts, args.logs, args.errors))
selected_pages = sum(bool(value) for value in (args.accounts, args.pricing, args.logs, args.errors))
if selected_pages > 1:
print("choose only one of --accounts, --logs, or --errors", file=sys.stderr)
print("choose only one of --accounts, --pricing, --logs, or --errors", file=sys.stderr)
return 2
if args.save_config or args.install:
config_path = write_api_url_config(args.api_url)
print(f"saved api url to {config_path}")
if str(args.pricing_url or "").strip():
pricing_url_path = write_config_value(pricing_url_config_file_path(), args.pricing_url)
print(f"saved pricing url to {pricing_url_path}")
if str(args.logs_url or "").strip():
logs_url_path = write_config_value(logs_url_config_file_path(), args.logs_url)
print(f"saved logs url to {logs_url_path}")
@@ -2167,6 +2429,9 @@ def main(argv: list[str] | None = None) -> int:
if args.once:
if version_message:
print(version_message)
if args.pricing:
print_pricing_once(fetch_pricing_payload(args.pricing_url, args.timeout), args.filter)
return 0
if args.logs:
if not str(args.logs_token or "").strip():
print(logs_token_hint(), file=sys.stderr)
@@ -2200,6 +2465,8 @@ def main(argv: list[str] | None = None) -> int:
start_page = "dashboard"
if args.accounts:
start_page = "accounts"
elif args.pricing:
start_page = "pricing"
elif args.logs:
start_page = "logs"
elif args.errors:
@@ -2207,6 +2474,7 @@ def main(argv: list[str] | None = None) -> int:
return run_textual(
args.api_url,
status_url,
args.pricing_url,
args.logs_url,
args.logs_token,
args.errors_cn_url,