feat: show monitor availability column

This commit is contained in:
2026-06-09 16:09:26 +08:00
parent 16f50172ca
commit 98410b0fe0
5 changed files with 77 additions and 6 deletions
+4 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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))
+49
View File
@@ -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 = {
Generated
+1 -1
View File
@@ -85,7 +85,7 @@ wheels = [
[[package]] [[package]]
name = "shusub2" name = "shusub2"
version = "0.1.3" version = "0.1.4"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "textual" }, { name = "textual" },