feat: expand shusub2 dashboard columns

This commit is contained in:
2026-07-27 20:34:26 +08:00
parent 0c7e437b7b
commit b248a6c9c1
5 changed files with 31 additions and 50 deletions
+5 -3
View File
@@ -84,10 +84,12 @@ Environment variables override the config file:
`shusub2` opens a single dashboard with Accounts, Keys, Logs, and Errors `shusub2` opens a single dashboard with Accounts, Keys, Logs, and Errors
tables stacked vertically. Press `a`, `k`, `l`, or `e` to focus a table, `/` to tables stacked vertically. Press `a`, `k`, `l`, or `e` to focus a table, `/` to
filter all four tables, and `r` to refresh all data. The two-line detail area filter all four tables, and `r` to refresh all data. The two-line detail area
follows the selected row. follows the selected row. Accounts, Keys, Logs, and Errors use green, yellow,
magenta, and red section styling respectively.
The dashboard uses compact columns sized to fit an 80-column terminal without The dashboard sizes every column from the fetched content so wide terminals show
horizontal scrolling: complete keys, accounts, and models. On a narrow terminal, a table scrolls
horizontally instead of truncating a field:
```text ```text
ACCOUNT | Group | Today | Daily | 5h | 7d | Avail ACCOUNT | Group | Today | Daily | 5h | 7d | Avail
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "shusub2" name = "shusub2"
version = "0.2.8" version = "0.2.9"
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"
+10 -37
View File
@@ -19,7 +19,7 @@ from typing import Any
APP_NAME = "shusub2" APP_NAME = "shusub2"
FALLBACK_VERSION = "0.2.8" FALLBACK_VERSION = "0.2.9"
DEFAULT_API_URL = "http://127.0.0.1:18318/api/tui/accounts" DEFAULT_API_URL = "http://127.0.0.1:18318/api/tui/accounts"
DEFAULT_CONFIG_FILE = "~/.config/shusub2/api-url" DEFAULT_CONFIG_FILE = "~/.config/shusub2/api-url"
DEFAULT_STATUS_CONFIG_FILE = "~/.config/shusub2/status-url" DEFAULT_STATUS_CONFIG_FILE = "~/.config/shusub2/status-url"
@@ -1258,57 +1258,30 @@ def run_textual(
yield Footer() yield Footer()
@staticmethod @staticmethod
def configure_table(table: DataTable, columns: tuple[tuple[str, int], ...]) -> None: def configure_table(table: DataTable, columns: tuple[str, ...]) -> None:
table.cursor_type = "row" table.cursor_type = "row"
table.zebra_stripes = True table.zebra_stripes = True
for label, width in columns: # Content-sized columns preserve complete values on wide terminals;
table.add_column(label, width=width) # narrow terminals use DataTable's horizontal scrolling instead.
for label in columns:
table.add_column(label)
def on_mount(self) -> None: def on_mount(self) -> None:
self.configure_table( self.configure_table(
self.query_one("#accounts", DataTable), self.query_one("#accounts", DataTable),
( ("ACCOUNT", "Group", "Today", "Daily", "5h", "7d", "Avail"),
("ACCOUNT", 16),
("Group", 5),
("Today", 8),
("Daily", 10),
("5h", 9),
("7d", 9),
("Avail", 7),
),
) )
self.configure_table( self.configure_table(
self.query_one("#keys", DataTable), self.query_one("#keys", DataTable),
( ("KEY", "Today", "Tokens", "Req"),
("KEY", 16),
("Today", 9),
("Tokens", 8),
("Req", 6),
),
) )
self.configure_table( self.configure_table(
self.query_one("#logs", DataTable), self.query_one("#logs", DataTable),
( ("LOG KEY", "Account", "Model", "Cost", "Latency", "Time", "Age"),
("LOG KEY", 10),
("Account", 10),
("Model", 12),
("Cost", 7),
("Latency", 7),
("Time", 11),
("Age", 7),
),
) )
self.configure_table( self.configure_table(
self.query_one("#errors", DataTable), self.query_one("#errors", DataTable),
( ("ERR", "Status", "Key", "Account", "Model", "Time", "Age"),
("ERR", 4),
("Status", 6),
("Key", 12),
("Account", 11),
("Model", 12),
("Time", 11),
("Age", 8),
),
) )
self.refresh_all(refresh=True) self.refresh_all(refresh=True)
self.set_interval(refresh_seconds, self.refresh_accounts) self.set_interval(refresh_seconds, self.refresh_accounts)
+14 -8
View File
@@ -678,7 +678,7 @@ class Sub2APILogsTests(unittest.TestCase):
class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase): class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
async def test_dashboard_fits_three_tables_at_80_by_24_and_focuses_each(self) -> None: async def test_dashboard_supports_wide_columns_at_80_by_24_and_focuses_each(self) -> None:
from textual.app import App from textual.app import App
mod = load_module() mod = load_module()
@@ -688,7 +688,7 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
"accounts": [ "accounts": [
{ {
"id": 1, "id": 1,
"name": "account-one", "name": "account-one-with-a-complete-expanded-dashboard-name",
"routing_group": "fast", "routing_group": "fast",
"provider": "openai", "provider": "openai",
"kind": "pay_as_you_go", "kind": "pay_as_you_go",
@@ -708,9 +708,9 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
"items": [ "items": [
{ {
"id": 1, "id": 1,
"api_key_name": "key-one", "api_key_name": "wmy-production-key-with-a-complete-expanded-name",
"account_name": "account-one", "account_name": "account-one-with-a-complete-expanded-dashboard-name",
"model": "gpt-5.5-codex", "model": "gpt-5.5-codex-with-a-complete-expanded-dashboard-model-name",
"duration_ms": 1200, "duration_ms": 1200,
"created_at": "2026-07-24T12:00:00+08:00", "created_at": "2026-07-24T12:00:00+08:00",
} }
@@ -723,8 +723,9 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
"_node": "cn", "_node": "cn",
"id": 1, "id": 1,
"status_code": 500, "status_code": 500,
"account_name": "account-one", "api_key_name": "wmy-production-key-with-a-complete-expanded-name",
"requested_model": "gpt-5.5-codex", "account_name": "account-one-with-a-complete-expanded-dashboard-name",
"requested_model": "gpt-5.5-codex-with-a-complete-expanded-dashboard-model-name",
"phase": "upstream", "phase": "upstream",
"type": "api_error", "type": "api_error",
"created_at": "2026-07-24T12:01:00+08:00", "created_at": "2026-07-24T12:01:00+08:00",
@@ -767,7 +768,12 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase):
tables = [screen.query_one(selector) for selector in ("#accounts", "#keys", "#logs", "#errors")] tables = [screen.query_one(selector) for selector in ("#accounts", "#keys", "#logs", "#errors")]
for table in tables: for table in tables:
self.assertGreaterEqual(table.region.height, 4) self.assertGreaterEqual(table.region.height, 4)
self.assertLessEqual(table.virtual_size.width, table.region.width) logs_table = screen.query_one("#logs")
errors_table = screen.query_one("#errors")
self.assertGreater(logs_table.virtual_size.width, logs_table.region.width)
self.assertGreater(errors_table.virtual_size.width, errors_table.region.width)
self.assertGreater(logs_table.max_scroll_x, 0)
self.assertGreater(errors_table.max_scroll_x, 0)
self.assertEqual(len({table.styles.background for table in tables}), 4) self.assertEqual(len({table.styles.background for table in tables}), 4)
self.assertIn("wmy", str(screen.query_one("#summary").render())) self.assertIn("wmy", str(screen.query_one("#summary").render()))
for key, expected_id in (("k", "keys"), ("l", "logs"), ("e", "errors"), ("a", "accounts")): for key, expected_id in (("k", "keys"), ("l", "logs"), ("e", "errors"), ("a", "accounts")):
Generated
+1 -1
View File
@@ -85,7 +85,7 @@ wheels = [
[[package]] [[package]]
name = "shusub2" name = "shusub2"
version = "0.2.8" version = "0.2.9"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "textual" }, { name = "textual" },