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:
|
Columns are ordered for scanning inside zellij:
|
||||||
|
|
||||||
```text
|
```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
|
`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
|
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
|
the account API and `sub2api-status`; name-token matching remains only as a
|
||||||
fallback for older status payloads.
|
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]
|
[project]
|
||||||
name = "shusub2"
|
name = "shusub2"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
description = "Terminal UI for Sub2API account quota and daily usage"
|
description = "Terminal UI for Sub2API account quota and daily usage"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
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
|
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]:
|
def window_for(account: dict[str, Any], window_id: str) -> dict[str, Any]:
|
||||||
for window in account.get("windows") or []:
|
for window in account.get("windows") or []:
|
||||||
if isinstance(window, dict) and window.get("id") == window_id:
|
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))
|
print(summary_line(payload))
|
||||||
if status_payload or status_error:
|
if status_payload or status_error:
|
||||||
print(monitor_summary(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):
|
for row in normalize_account_rows(payload, filter_text):
|
||||||
print(
|
print(
|
||||||
f"{row['name'][:30]:<30} "
|
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['five_hour']:<9} "
|
||||||
f"{row['weekly']:<9} "
|
f"{row['weekly']:<9} "
|
||||||
f"{row['reset']:<11} "
|
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 = self.query_one("#accounts", DataTable)
|
||||||
table.cursor_type = "row"
|
table.cursor_type = "row"
|
||||||
table.zebra_stripes = True
|
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.refresh_data(refresh=True)
|
||||||
self.set_interval(refresh_seconds, self.refresh_data)
|
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["weekly"],
|
||||||
row["reset"],
|
row["reset"],
|
||||||
row["status"],
|
row["status"],
|
||||||
|
monitor_availability(row, self.status_payload),
|
||||||
key=key,
|
key=key,
|
||||||
)
|
)
|
||||||
self.query_one("#summary", Static).update(summary_line(self.payload))
|
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.assertEqual(rows[0]["daily_quota_cell"], "96.6/300")
|
||||||
self.assertTrue(rows[0]["reset"].endswith("00:00") or rows[0]["reset"] != "-")
|
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:
|
def test_routing_group_sort_shows_sfast_first(self) -> None:
|
||||||
mod = load_module()
|
mod = load_module()
|
||||||
payload = {
|
payload = {
|
||||||
|
|||||||
Reference in New Issue
Block a user