feat: add sub2apicn request logs page
- new logs page (press l / a to switch, or start with --logs) reading the latest 100 requests from the Sub2API admin usage API on sub2apicn - columns: Key, Account, Model, Type, Tokens, Cost, Latency, Time with a per-row detail line (token buckets, actual cost, first-token latency) - refreshes every 60s by default (--logs-refresh-seconds, --logs-limit) - admin API key read from SHUSUB2_LOGS_TOKEN or ~/.config/shusub2/logs-token; --save-config now persists logs-url/logs-token too - --once --logs prints a one-shot logs snapshot Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+380
-26
@@ -17,12 +17,17 @@ from typing import Any
|
||||
|
||||
|
||||
APP_NAME = "shusub2"
|
||||
FALLBACK_VERSION = "0.1.7"
|
||||
FALLBACK_VERSION = "0.2.0"
|
||||
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_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"
|
||||
DEFAULT_VERSION_CHECK_URL = "https://gitea.shujk.top/shujakuin/shusub2/raw/branch/main/pyproject.toml"
|
||||
DEFAULT_REFRESH_SECONDS = 60
|
||||
DEFAULT_LOGS_REFRESH_SECONDS = 60
|
||||
DEFAULT_LOGS_LIMIT = 100
|
||||
DEFAULT_TIMEOUT_SECONDS = 10
|
||||
DEFAULT_VERSION_CHECK_TIMEOUT_SECONDS = 2
|
||||
MONITOR_OK_STATUSES = {"operational", "ok", "success"}
|
||||
@@ -70,26 +75,55 @@ def default_status_url() -> str:
|
||||
)
|
||||
|
||||
|
||||
def default_logs_url() -> str:
|
||||
return configured_url(
|
||||
("SHUSUB2_LOGS_URL",),
|
||||
os.environ.get("SHUSUB2_LOGS_URL_FILE", DEFAULT_LOGS_URL_CONFIG_FILE),
|
||||
DEFAULT_LOGS_API_URL,
|
||||
)
|
||||
|
||||
|
||||
def default_logs_token() -> str:
|
||||
return configured_url(
|
||||
("SHUSUB2_LOGS_TOKEN",),
|
||||
os.environ.get("SHUSUB2_LOGS_TOKEN_FILE", DEFAULT_LOGS_TOKEN_CONFIG_FILE),
|
||||
)
|
||||
|
||||
|
||||
def config_file_path() -> Path:
|
||||
return Path(os.environ.get("SHUSUB2_API_URL_FILE", DEFAULT_CONFIG_FILE)).expanduser()
|
||||
|
||||
|
||||
def logs_url_config_file_path() -> Path:
|
||||
return Path(os.environ.get("SHUSUB2_LOGS_URL_FILE", DEFAULT_LOGS_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()
|
||||
|
||||
|
||||
def write_config_value(path: Path, value: str) -> Path:
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
raise ValueError("config value is empty")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
path.parent.chmod(0o700)
|
||||
except OSError:
|
||||
pass
|
||||
path.write_text(text + "\n", encoding="utf-8")
|
||||
try:
|
||||
path.chmod(0o600)
|
||||
except OSError:
|
||||
pass
|
||||
return path
|
||||
|
||||
|
||||
def write_api_url_config(api_url: str) -> Path:
|
||||
value = str(api_url or "").strip()
|
||||
if not value:
|
||||
raise ValueError("api url is empty")
|
||||
path = config_file_path()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
path.parent.chmod(0o700)
|
||||
except OSError:
|
||||
pass
|
||||
path.write_text(value + "\n", encoding="utf-8")
|
||||
try:
|
||||
path.chmod(0o600)
|
||||
except OSError:
|
||||
pass
|
||||
return path
|
||||
return write_config_value(config_file_path(), value)
|
||||
|
||||
|
||||
def run_install_command() -> int:
|
||||
@@ -259,6 +293,166 @@ def fetch_optional_payload(url: str, timeout: int) -> tuple[dict[str, Any], str]
|
||||
return {}, str(exc)
|
||||
|
||||
|
||||
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)
|
||||
query = [(key, value) for key, value in query if key not in {"page", "page_size", "limit"}]
|
||||
query.extend((("page", "1"), ("page_size", str(max(1, min(int(limit), 1000))))))
|
||||
return urllib.parse.urlunparse(parsed._replace(query=urllib.parse.urlencode(query)))
|
||||
|
||||
|
||||
def fetch_logs_payload(logs_url: str, token: str, timeout: int, limit: int = DEFAULT_LOGS_LIMIT) -> dict[str, Any]:
|
||||
headers = {"Accept": "application/json"}
|
||||
if str(token or "").strip():
|
||||
headers["x-api-key"] = str(token).strip()
|
||||
req = urllib.request.Request(logs_request_url(logs_url, limit), headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as response:
|
||||
data = json.loads(response.read().decode("utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
raise RuntimeError("logs API did not return a JSON object")
|
||||
return data
|
||||
|
||||
|
||||
def logs_envelope(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
data = payload.get("data")
|
||||
if isinstance(data, dict) and isinstance(data.get("items"), list):
|
||||
return data
|
||||
return payload
|
||||
|
||||
|
||||
def log_items(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
items = logs_envelope(payload).get("items")
|
||||
if not isinstance(items, list):
|
||||
return []
|
||||
return [item for item in items if isinstance(item, dict)]
|
||||
|
||||
|
||||
def log_type_label(item: dict[str, Any]) -> str:
|
||||
text = str(item.get("request_type") or "").strip().lower()
|
||||
if text and text != "unknown":
|
||||
return text
|
||||
if item.get("openai_ws_mode"):
|
||||
return "ws_v2"
|
||||
if item.get("stream") is True:
|
||||
return "stream"
|
||||
if item.get("stream") is False:
|
||||
return "sync"
|
||||
return text or "-"
|
||||
|
||||
|
||||
def log_total_tokens(item: dict[str, Any]) -> int:
|
||||
return (
|
||||
as_int(item.get("input_tokens"))
|
||||
+ as_int(item.get("output_tokens"))
|
||||
+ as_int(item.get("cache_creation_tokens"))
|
||||
+ as_int(item.get("cache_read_tokens"))
|
||||
)
|
||||
|
||||
|
||||
def nested_name(item: dict[str, Any], object_key: str, id_key: str, name_key: str = "") -> str:
|
||||
nested = item.get(object_key)
|
||||
if isinstance(nested, dict):
|
||||
name = str(nested.get("name") or "").strip()
|
||||
if name:
|
||||
return name
|
||||
if name_key:
|
||||
name = str(item.get(name_key) or "").strip()
|
||||
if name:
|
||||
return name
|
||||
identifier = as_int(item.get(id_key))
|
||||
return f"#{identifier}" if identifier else "-"
|
||||
|
||||
|
||||
def normalize_log_rows(payload: dict[str, Any], filter_text: str = "") -> list[dict[str, Any]]:
|
||||
needle = filter_text.strip().lower()
|
||||
rows = []
|
||||
for item in log_items(payload):
|
||||
key_name = nested_name(item, "api_key", "api_key_id", "api_key_name")
|
||||
account_name = nested_name(item, "account", "account_id", "account_name")
|
||||
user_name = nested_name(item, "user", "user_id", "user_name")
|
||||
model = str(item.get("model") or "-")
|
||||
upstream_model = str(item.get("upstream_model") or "").strip()
|
||||
type_label = log_type_label(item)
|
||||
row = {
|
||||
"id": as_int(item.get("id")),
|
||||
"key": key_name,
|
||||
"account": account_name,
|
||||
"user": user_name,
|
||||
"model": model,
|
||||
"upstream_model": upstream_model,
|
||||
"type": type_label,
|
||||
"tokens": log_total_tokens(item),
|
||||
"input_tokens": as_int(item.get("input_tokens")),
|
||||
"output_tokens": as_int(item.get("output_tokens")),
|
||||
"cache_creation_tokens": as_int(item.get("cache_creation_tokens")),
|
||||
"cache_read_tokens": as_int(item.get("cache_read_tokens")),
|
||||
"cost": as_float(item.get("total_cost")),
|
||||
"actual_cost": as_float(item.get("actual_cost")),
|
||||
"duration_ms": as_int(item.get("duration_ms")),
|
||||
"first_token_ms": as_int(item.get("first_token_ms")),
|
||||
"created_at": str(item.get("created_at") or ""),
|
||||
"time": short_time(item.get("created_at")),
|
||||
"request_id": str(item.get("request_id") or ""),
|
||||
"raw": item,
|
||||
}
|
||||
if needle:
|
||||
haystack = " ".join(
|
||||
(key_name, account_name, user_name, model, upstream_model, type_label, row["request_id"])
|
||||
).lower()
|
||||
if needle not in haystack:
|
||||
continue
|
||||
rows.append(row)
|
||||
rows.sort(key=lambda row: (row["created_at"], row["id"]), reverse=True)
|
||||
return rows
|
||||
|
||||
|
||||
def logs_summary_line(payload: dict[str, Any], shown: int) -> str:
|
||||
envelope = logs_envelope(payload)
|
||||
total = as_int(envelope.get("total"))
|
||||
page_size = as_int(envelope.get("page_size"))
|
||||
fetched = short_time(dt.datetime.now(dt.timezone.utc).isoformat())
|
||||
cost = sum(as_float(item.get("total_cost")) for item in log_items(payload))
|
||||
tokens = sum(log_total_tokens(item) for item in log_items(payload))
|
||||
return (
|
||||
f"{fetched} logs | showing {shown}/{len(log_items(payload))} of latest {page_size or '-'} | "
|
||||
f"total {format_count(total)} records | page cost {format_cost(cost)} | {format_count(tokens)} tokens"
|
||||
)
|
||||
|
||||
|
||||
def log_detail_line(row: dict[str, Any]) -> str:
|
||||
model = row["model"]
|
||||
if row["upstream_model"] and row["upstream_model"] != row["model"]:
|
||||
model = f"{row['model']} -> {row['upstream_model']}"
|
||||
first_token = f"{row['first_token_ms']}ms" if row["first_token_ms"] > 0 else "-"
|
||||
detail = (
|
||||
f"{row['time']} | key {row['key']} | account {row['account']} | user {row['user']} | {model} | {row['type']} | "
|
||||
f"tokens in {format_count(row['input_tokens'])} out {format_count(row['output_tokens'])} "
|
||||
f"cache_w {format_count(row['cache_creation_tokens'])} cache_r {format_count(row['cache_read_tokens'])} | "
|
||||
f"cost {format_cost(row['cost'])} (actual {format_cost(row['actual_cost'])}) | "
|
||||
f"latency {format_latency(row['duration_ms'])} first {first_token}"
|
||||
)
|
||||
if row["request_id"]:
|
||||
detail += f" | {row['request_id']}"
|
||||
return detail
|
||||
|
||||
|
||||
def print_logs_once(payload: dict[str, Any], filter_text: str = "") -> None:
|
||||
rows = normalize_log_rows(payload, filter_text)
|
||||
print(logs_summary_line(payload, len(rows)))
|
||||
print("key account model type tokens cost latency time")
|
||||
for row in rows:
|
||||
print(
|
||||
f"{row['key'][:20]:<21} "
|
||||
f"{row['account'][:20]:<21} "
|
||||
f"{row['model'][:24]:<25} "
|
||||
f"{row['type']:<7} "
|
||||
f"{format_count(row['tokens']):<8} "
|
||||
f"{format_cost(row['cost']):<10} "
|
||||
f"{format_latency(row['duration_ms']):<9} "
|
||||
f"{row['time']}"
|
||||
)
|
||||
|
||||
|
||||
def kind_label(kind: Any) -> str:
|
||||
return {
|
||||
"quota_limited": "quota",
|
||||
@@ -526,26 +720,32 @@ def print_once(payload: dict[str, Any], filter_text: str = "", status_payload: d
|
||||
)
|
||||
|
||||
|
||||
def run_textual(api_url: str, status_url: str, refresh_seconds: int, timeout: int, version_message: str = "") -> int:
|
||||
def run_textual(
|
||||
api_url: str,
|
||||
status_url: str,
|
||||
logs_url: str,
|
||||
logs_token: str,
|
||||
refresh_seconds: int,
|
||||
logs_refresh_seconds: int,
|
||||
timeout: int,
|
||||
logs_limit: int,
|
||||
version_message: str = "",
|
||||
start_page: str = "accounts",
|
||||
) -> int:
|
||||
try:
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import DataTable, Footer, Header, Input, Static
|
||||
except ImportError:
|
||||
print("Textual is required. Run with: uv run --with textual python sub2api_quota_tui.py", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
class Sub2APIQuotaApp(App[None]):
|
||||
CSS = """
|
||||
#summary { height: 1; padding: 0 1; color: $accent; }
|
||||
#filter { height: 3; }
|
||||
#accounts { height: 1fr; }
|
||||
#detail { height: 3; padding: 0 1; border-top: solid $panel; }
|
||||
#status { height: 1; padding: 0 1; color: $text-muted; }
|
||||
"""
|
||||
class AccountsScreen(Screen[None]):
|
||||
AUTO_FOCUS = "#accounts"
|
||||
BINDINGS = [
|
||||
("q", "quit", "Quit"),
|
||||
("r", "refresh", "Refresh"),
|
||||
("/", "focus_filter", "Filter"),
|
||||
("l", "show_logs", "Logs"),
|
||||
]
|
||||
|
||||
def __init__(self) -> None:
|
||||
@@ -579,10 +779,17 @@ def run_textual(api_url: str, status_url: str, refresh_seconds: int, timeout: in
|
||||
def action_focus_filter(self) -> None:
|
||||
self.query_one("#filter", Input).focus()
|
||||
|
||||
def action_show_logs(self) -> None:
|
||||
self.app.switch_screen(LogsScreen())
|
||||
|
||||
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("#accounts", DataTable).focus()
|
||||
|
||||
def refresh_data(self, refresh: bool = False) -> None:
|
||||
status = self.query_one("#status", Static)
|
||||
status.update("refreshing...")
|
||||
@@ -648,6 +855,119 @@ def run_textual(api_url: str, status_url: str, refresh_seconds: int, timeout: in
|
||||
detail += f" | {error}"
|
||||
self.query_one("#detail", Static).update(detail)
|
||||
|
||||
class LogsScreen(Screen[None]):
|
||||
AUTO_FOCUS = "#logs"
|
||||
BINDINGS = [
|
||||
("r", "refresh", "Refresh"),
|
||||
("/", "focus_filter", "Filter"),
|
||||
("a", "show_accounts", "Accounts"),
|
||||
]
|
||||
|
||||
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", id="filter")
|
||||
yield DataTable(id="logs")
|
||||
yield Static("", id="detail")
|
||||
yield Static("", id="status")
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
table = self.query_one("#logs", DataTable)
|
||||
table.cursor_type = "row"
|
||||
table.zebra_stripes = True
|
||||
table.add_columns("Key", "Account", "Model", "Type", "Tokens", "Cost", "Latency", "Time")
|
||||
self.refresh_data()
|
||||
self.set_interval(logs_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 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("#logs", DataTable).focus()
|
||||
|
||||
def refresh_data(self) -> None:
|
||||
status = self.query_one("#status", Static)
|
||||
if not str(logs_token or "").strip():
|
||||
status.update(
|
||||
"logs token not configured: set SHUSUB2_LOGS_TOKEN or write the Sub2API admin API key to "
|
||||
f"{logs_token_config_file_path()}"
|
||||
)
|
||||
return
|
||||
status.update("refreshing logs...")
|
||||
try:
|
||||
self.payload = fetch_logs_payload(logs_url, logs_token, timeout, logs_limit)
|
||||
self.render_payload()
|
||||
status_bits = [version_message, f"latest {logs_limit} requests | {logs_url}"]
|
||||
status.update(" | ".join(bit for bit in status_bits if bit))
|
||||
except Exception as exc:
|
||||
status.update(f"logs error: {exc}")
|
||||
|
||||
def render_payload(self) -> None:
|
||||
filter_text = self.query_one("#filter", Input).value
|
||||
self.rows = normalize_log_rows(self.payload, filter_text)
|
||||
table = self.query_one("#logs", DataTable)
|
||||
table.clear()
|
||||
self.row_by_key = {}
|
||||
for index, row in enumerate(self.rows):
|
||||
key = f"{row['id']}-{index}"
|
||||
self.row_by_key[key] = row
|
||||
table.add_row(
|
||||
row["key"],
|
||||
row["account"],
|
||||
row["model"],
|
||||
row["type"],
|
||||
format_count(row["tokens"]),
|
||||
format_cost(row["cost"]),
|
||||
format_latency(row["duration_ms"]),
|
||||
row["time"],
|
||||
key=key,
|
||||
)
|
||||
self.query_one("#summary", Static).update(logs_summary_line(self.payload, len(self.rows)))
|
||||
if self.rows:
|
||||
self.query_one("#detail", Static).update(log_detail_line(self.rows[0]))
|
||||
else:
|
||||
self.query_one("#detail", Static).update("no request logs")
|
||||
|
||||
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
||||
key = str(event.row_key.value)
|
||||
row = self.row_by_key.get(key)
|
||||
if row:
|
||||
self.query_one("#detail", Static).update(log_detail_line(row))
|
||||
|
||||
class Sub2APIQuotaApp(App[None]):
|
||||
CSS = """
|
||||
#summary { height: 1; padding: 0 1; color: $accent; }
|
||||
#filter { height: 3; }
|
||||
#accounts { height: 1fr; }
|
||||
#logs { height: 1fr; }
|
||||
#detail { height: 3; padding: 0 1; border-top: solid $panel; }
|
||||
#status { height: 1; padding: 0 1; color: $text-muted; }
|
||||
"""
|
||||
BINDINGS = [
|
||||
("q", "quit", "Quit"),
|
||||
]
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.push_screen(LogsScreen() if start_page == "logs" else AccountsScreen())
|
||||
|
||||
Sub2APIQuotaApp().run()
|
||||
return 0
|
||||
|
||||
@@ -656,8 +976,10 @@ 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("--save-config", action="store_true", help="persist --api-url to ~/.config/shusub2/api-url before running")
|
||||
parser.add_argument("--install", action="store_true", help="persist --api-url, install shusub2 as a uv tool, then exit")
|
||||
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 the logs page (prefer SHUSUB2_LOGS_TOKEN or ~/.config/shusub2/logs-token)")
|
||||
parser.add_argument("--save-config", action="store_true", help="persist --api-url/--logs-url/--logs-token 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(
|
||||
"--version-check-timeout",
|
||||
@@ -670,8 +992,19 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
type=int,
|
||||
default=env_int("SUB2API_QUOTA_TUI_REFRESH_SECONDS", DEFAULT_REFRESH_SECONDS),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--logs-refresh-seconds",
|
||||
type=int,
|
||||
default=env_int("SHUSUB2_LOGS_REFRESH_SECONDS", DEFAULT_LOGS_REFRESH_SECONDS),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--logs-limit",
|
||||
type=int,
|
||||
default=env_int("SHUSUB2_LOGS_LIMIT", DEFAULT_LOGS_LIMIT),
|
||||
)
|
||||
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("--logs", action="store_true", help="start on the request logs page (with --once: print a logs snapshot)")
|
||||
parser.add_argument("--filter", default="", help="initial filter for --once output")
|
||||
return parser
|
||||
|
||||
@@ -679,9 +1012,16 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
status_url = str(args.status_url or "").strip() or inferred_status_url(args.api_url)
|
||||
logs_limit = max(1, min(args.logs_limit, 1000))
|
||||
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.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}")
|
||||
if str(args.logs_token or "").strip():
|
||||
logs_token_path = write_config_value(logs_token_config_file_path(), args.logs_token)
|
||||
print(f"saved logs token to {logs_token_path}")
|
||||
if args.install:
|
||||
print("installing shusub2 with uv tool...")
|
||||
return run_install_command()
|
||||
@@ -693,10 +1033,24 @@ def main(argv: list[str] | None = None) -> int:
|
||||
if args.once:
|
||||
if version_message:
|
||||
print(version_message)
|
||||
if args.logs:
|
||||
print_logs_once(fetch_logs_payload(args.logs_url, args.logs_token, args.timeout, logs_limit), args.filter)
|
||||
return 0
|
||||
status_payload, status_error = fetch_optional_payload(status_url, args.timeout)
|
||||
print_once(fetch_payload(args.api_url, args.timeout, refresh=True), args.filter, status_payload, status_error)
|
||||
return 0
|
||||
return run_textual(args.api_url, status_url, max(1, args.refresh_seconds), max(1, args.timeout), version_message)
|
||||
return run_textual(
|
||||
args.api_url,
|
||||
status_url,
|
||||
args.logs_url,
|
||||
args.logs_token,
|
||||
max(1, args.refresh_seconds),
|
||||
max(1, args.logs_refresh_seconds),
|
||||
max(1, args.timeout),
|
||||
logs_limit,
|
||||
version_message,
|
||||
start_page="logs" if args.logs else "accounts",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user