This commit is contained in:
onysd 2026-07-28 02:21:10 +03:00
parent b39d35585e
commit 12b8b52ced

View file

@ -26,12 +26,13 @@ from dataclasses import dataclass
from pathlib import Path
import psutil
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, load_pem_private_key
from textual import work
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Horizontal, Vertical
from textual.screen import ModalScreen, Screen
from textual.widgets import Button, DataTable, Footer, Header, Label, OptionList, ProgressBar, RichLog, Static
from textual.screen import Screen
from textual.widgets import DataTable, Footer, Header, Label, OptionList, ProgressBar, RichLog, Static
from textual.widgets.option_list import Option
IS_WINDOWS = platform.system() == "Windows"
@ -326,6 +327,40 @@ def admin_ui_info() -> tuple[str, str | None] | None:
return url, (read_env_value("TELESRV_ADMIN_UI_PASSWORD") or None)
def server_address() -> str | None:
"""The MTProto address clients connect to (advertise IP + listen port).
Present as soon as .env is configured, even before the first Start."""
ip = read_env_value("TELESRV_ADVERTISE_IP")
listen = read_env_value("TELESRV_LISTEN")
if not ip or not listen:
return None
port = listen.rsplit(":", 1)[-1]
if not port.isdigit():
return None
return f"{ip}:{port}"
def server_public_key_pem() -> str | None:
"""The server's RSA public key (PKCS1 "RSA PUBLIC KEY" PEM, the format
patched into client builds), derived from the private key file. That
file is only generated the first time telesrv actually starts, so this
is legitimately absent on a never-started install -- and returns None
rather than raising on a missing/corrupt/unparseable file instead of
crashing the panel over it."""
key_path_value = read_env_value("TELESRV_RSA_KEY") or "data/server_rsa.pem"
key_path = Path(key_path_value)
if not key_path.is_absolute():
key_path = ROOT / key_path
if not key_path.exists():
return None
try:
private_key = load_pem_private_key(key_path.read_bytes(), password=None)
public_pem = private_key.public_key().public_bytes(Encoding.PEM, PublicFormat.PKCS1)
return public_pem.decode("ascii").strip()
except Exception: # noqa: BLE001 - any parse/format issue just means "unavailable"
return None
def copy_to_clipboard(text: str) -> bool:
if IS_WINDOWS:
candidates = [["clip"]]
@ -358,71 +393,32 @@ def system_stats() -> tuple[float, float, float]:
# --- UI -----------------------------------------------------------------
class PasswordSpoiler(Static):
"""Masks a password until clicked; the click handler (owned by whoever
creates this widget) decides whether/how to confirm before revealing."""
class CopyButton(Static):
"""A clickable label that copies `value` to the clipboard on click. The
actual value is never shown on screen -- just the label and a "click to
copy" hint (optionally masked with dots for secret-looking fields, purely
cosmetic since nothing is ever displayed either way). value=None renders
as "not available" and isn't clickable -- used before the first Start,
when e.g. the RSA public key file doesn't exist yet."""
def __init__(self, password: str, on_click_callback, **kwargs):
def __init__(self, label: str, value: str | None, on_copy_callback, *, masked: bool = False, **kwargs):
super().__init__(**kwargs)
self.password = password
self.revealed = False
self._on_click_callback = on_click_callback
self.label = label
self.value = value
self.masked = masked
self._on_copy_callback = on_copy_callback
def on_mount(self) -> None:
self._render_masked()
def _render_masked(self) -> None:
self.update("Password: [dim]" + "" * 10 + "[/] [i](click to reveal)[/]")
def reveal(self) -> None:
self.revealed = True
self.update(f"Password: [b green]{self.password}[/]")
if self.value is None:
self.update(f"{self.label}: [dim]not available[/]")
elif self.masked:
self.update(f"{self.label}: [dim]{'' * 10}[/] [i](click to copy)[/]")
else:
self.update(f"{self.label}: [dim](click to copy)[/]")
def on_click(self, event) -> None:
if not self.revealed:
self._on_click_callback(self)
class ConfirmRevealScreen(ModalScreen[bool]):
"""Yes/no confirmation before a spoiler is revealed."""
CSS = """
ConfirmRevealScreen {
align: center middle;
}
#confirm-dialog {
width: 56;
height: auto;
padding: 1 2;
border: round $warning;
background: $surface;
}
#confirm-dialog Label {
width: 100%;
content-align: center middle;
margin-bottom: 1;
}
#confirm-buttons {
align: center middle;
height: auto;
}
#confirm-buttons Button {
margin: 0 1;
}
"""
def compose(self) -> ComposeResult:
with Vertical(id="confirm-dialog"):
yield Label("Show the admin UI password?\nIt will also be copied to the clipboard.")
with Horizontal(id="confirm-buttons"):
yield Button("Yes, show it", id="yes", variant="warning")
yield Button("Cancel", id="no", variant="default")
def on_mount(self) -> None:
self.query_one("#no", Button).focus()
def on_button_pressed(self, event: Button.Pressed) -> None:
self.dismiss(event.button.id == "yes")
if self.value is not None:
self._on_copy_callback(self)
class LogTailScreen(Screen):
@ -544,6 +540,9 @@ class MainScreen(Screen):
yield ProgressBar(total=100, id="ram-bar", show_eta=False)
yield Label("Disk", id="disk-label", classes="stat-label")
yield ProgressBar(total=100, id="disk-bar", show_eta=False)
yield Label("Server", classes="panel-title")
yield CopyButton("Address", server_address(), self._handle_copy_click, id="server-address")
yield CopyButton("Public key", server_public_key_pem(), self._handle_copy_click, masked=True, id="server-public-key")
yield Label("Admin UI", classes="panel-title")
yield Static(id="admin-link")
yield self._admin_password_widget()
@ -560,20 +559,14 @@ class MainScreen(Screen):
info = admin_ui_info()
password = info[1] if info else None
if password:
return PasswordSpoiler(password, self._handle_password_click, id="admin-password")
return CopyButton("Password", password, self._handle_copy_click, masked=True, id="admin-password")
return Static("Password: [dim]not set (token auth)[/]", id="admin-password")
def _handle_password_click(self, spoiler: PasswordSpoiler) -> None:
def handle(confirmed: bool | None) -> None:
if not confirmed:
return
spoiler.reveal()
if copy_to_clipboard(spoiler.password):
self.notify("Password revealed and copied to clipboard")
else:
self.notify("Password revealed (clipboard copy failed)", severity="warning")
self.app.push_screen(ConfirmRevealScreen(), handle)
def _handle_copy_click(self, widget: CopyButton) -> None:
if copy_to_clipboard(widget.value):
self.notify(f"{widget.label} copied to clipboard")
else:
self.notify(f"{widget.label} copy failed", severity="warning")
def on_mount(self) -> None:
self.query_one("#services-table", DataTable).add_columns("Service", "Type", "Status")
@ -810,6 +803,7 @@ class ServerPanelApp(App):
height: 100%;
border: round $accent;
padding: 1 2;
overflow-y: auto;
}
.stat-label {
margin-top: 1;
@ -820,8 +814,9 @@ class ServerPanelApp(App):
#admin-link {
margin-top: 1;
}
#admin-password {
#admin-password, #server-address, #server-public-key {
margin-top: 1;
height: auto;
}
LogTailScreen Horizontal {
height: 1fr;