feat: show monitor availability column
This commit is contained in:
@@ -67,7 +67,7 @@ access, API keys, OAuth credentials, or plaintext env files.
|
||||
Columns are ordered for scanning inside zellij:
|
||||
|
||||
```text
|
||||
Name | Provider | Group | Daily | Today | Tokens | Req | Kind | 5h | 7d | Reset | Status
|
||||
Name | Provider | Group | Daily | Today | Tokens | Req | Kind | 5h | 7d | Reset | Status | Availability
|
||||
```
|
||||
|
||||
`Provider` distinguishes `openai` and `anthropic` accounts from the public
|
||||
@@ -85,3 +85,6 @@ health, and the selected account detail shows the matching monitor status when
|
||||
one exists. Monitor binding first uses the shared `base_url_hash` emitted by
|
||||
the account API and `sub2api-status`; name-token matching remains only as a
|
||||
fallback for older status payloads.
|
||||
|
||||
`Availability` is derived from the bound channel monitor. Accounts without a
|
||||
matching monitor show `-`.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "shusub2"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
description = "Terminal UI for Sub2API account quota and daily usage"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
+22
-3
@@ -263,6 +263,23 @@ def monitor_detail(row: dict[str, Any], status_payload: dict[str, Any]) -> str:
|
||||
return detail
|
||||
|
||||
|
||||
def monitor_availability(row: dict[str, Any], status_payload: dict[str, Any]) -> str:
|
||||
item = matching_monitor(row, status_payload)
|
||||
if not item:
|
||||
return "-"
|
||||
if item.get("enabled") is False:
|
||||
return "disabled"
|
||||
raw_status = str(item.get("latest_status") or "").strip().lower()
|
||||
if not raw_status:
|
||||
return "unknown"
|
||||
kind = status_kind(raw_status)
|
||||
if kind == "ok":
|
||||
return "ok"
|
||||
if kind == "failed":
|
||||
return "failed"
|
||||
return raw_status
|
||||
|
||||
|
||||
def window_for(account: dict[str, Any], window_id: str) -> dict[str, Any]:
|
||||
for window in account.get("windows") or []:
|
||||
if isinstance(window, dict) and window.get("id") == window_id:
|
||||
@@ -388,7 +405,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 7d reset status")
|
||||
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} "
|
||||
@@ -402,7 +419,8 @@ def print_once(payload: dict[str, Any], filter_text: str = "", status_payload: d
|
||||
f"{row['five_hour']:<9} "
|
||||
f"{row['weekly']:<9} "
|
||||
f"{row['reset']:<11} "
|
||||
f"{row['status']}"
|
||||
f"{row['status']:<10} "
|
||||
f"{monitor_availability(row, status_payload or {})}"
|
||||
)
|
||||
|
||||
|
||||
@@ -449,7 +467,7 @@ def run_textual(api_url: str, status_url: str, refresh_seconds: int, timeout: in
|
||||
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", "7d", "Reset", "Status")
|
||||
table.add_columns("Name", "Provider", "Group", "Daily", "Today", "Tokens", "Req", "Kind", "5h", "7d", "Reset", "Status", "Availability")
|
||||
self.refresh_data(refresh=True)
|
||||
self.set_interval(refresh_seconds, self.refresh_data)
|
||||
|
||||
@@ -497,6 +515,7 @@ def run_textual(api_url: str, status_url: str, refresh_seconds: int, timeout: in
|
||||
row["weekly"],
|
||||
row["reset"],
|
||||
row["status"],
|
||||
monitor_availability(row, self.status_payload),
|
||||
key=key,
|
||||
)
|
||||
self.query_one("#summary", Static).update(summary_line(self.payload))
|
||||
|
||||
@@ -121,6 +121,55 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
|
||||
self.assertEqual(rows[0]["daily_quota_cell"], "96.6/300")
|
||||
self.assertTrue(rows[0]["reset"].endswith("00:00") or rows[0]["reset"] != "-")
|
||||
|
||||
def test_monitor_availability_column_uses_bound_monitor_status(self) -> None:
|
||||
mod = load_module()
|
||||
payload = {
|
||||
"generated_at": "2026-06-09T12:00:00+08:00",
|
||||
"totals": {"total_accounts": 1, "usable_accounts": 1, "today_cost_usd": 1, "today_tokens": 100, "today_requests": 2},
|
||||
"accounts": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "input 300",
|
||||
"base_url_hash": "input-hash",
|
||||
"routing_group": "fast",
|
||||
"provider": "openai",
|
||||
"kind": "daily_limited",
|
||||
"status": "ok",
|
||||
"account_type": "apikey",
|
||||
"today_cost_usd": 1,
|
||||
"today_tokens": 100,
|
||||
"today_requests": 2,
|
||||
"windows": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
status_payload = {
|
||||
"channel_monitors": {
|
||||
"items": [
|
||||
{
|
||||
"name": "other responses",
|
||||
"base_url_hash": "input-hash",
|
||||
"enabled": True,
|
||||
"latest_status": "operational",
|
||||
"latency_ms": 2606,
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
rows = mod.normalize_account_rows(payload)
|
||||
out = io.StringIO()
|
||||
|
||||
with contextlib.redirect_stdout(out):
|
||||
mod.print_once(payload, status_payload=status_payload)
|
||||
|
||||
self.assertEqual(mod.monitor_availability(rows[0], status_payload), "ok")
|
||||
self.assertIn("availability", out.getvalue())
|
||||
self.assertIn(" ok\n", out.getvalue())
|
||||
|
||||
def test_monitor_availability_is_dash_without_bound_monitor(self) -> None:
|
||||
mod = load_module()
|
||||
self.assertEqual(mod.monitor_availability({"name": "alpha"}, {}), "-")
|
||||
|
||||
def test_routing_group_sort_shows_sfast_first(self) -> None:
|
||||
mod = load_module()
|
||||
payload = {
|
||||
|
||||
Reference in New Issue
Block a user