From ea11389752113a77e7613e7fb323e832fcb4deea Mon Sep 17 00:00:00 2001 From: BONNAURE Olivier Date: Sun, 23 Aug 2026 17:53:19 +0200 Subject: [PATCH 1/2] feat(tui): redesign shell, fix scrolling, auth and daemon polling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TUI gets a left sidebar, a mint-on-ink palette (src/tui/theme.rs), mouse support and a redrawn dashboard. Review follow-ups folded in: Correctness - Login: `q` was bound to quit under an `if password.is_empty()` guard that applied to the whole or-pattern, making every password starting with `q` unenterable. Quit is now Esc-on-empty (plus Ctrl+C/Ctrl+D). - `G` set scroll_offset to the last *index*; every screen renders `.skip(scroll_offset)`, so it collapsed the list to a single row. Added `max_offset(len, visible)` — the top of the last page — and a clamp pass on render so a resize or a shrinking list cannot strand the viewport. - Routes counted unfiltered rules for selection bounds while rendering the filtered list, and resolved the cursor back to a rule with a *different* predicate (matcher Debug vs format_matcher) than the one it displayed — a search could line a row up with a rule other than the one `d` deleted. Both now share `screens::routes::filter_indices`. - Config screen could not scroll at all: get_max_selection returned 0, so move_selection bailed. Added a text-viewport scroll path and cached the file contents instead of re-reading them every frame. - tail_file used read_to_string, so a seek landing mid-codepoint (or any non-UTF-8 byte) blanked the log viewer with no explanation. Reads bytes and converts lossily; open/read errors now name the path and cause. Responsiveness - Daemon metrics moved off the render thread into a background poller. The inline block_on froze the UI for the request timeout every tick, worst exactly when the daemon was down. The client is built once, not per tick. - Mouse reporting uses ?1000/?1006 rather than crossterm's EnableMouseCapture, which also enables ?1003 any-motion tracking: a redraw per pointer movement, and terminal text selection taken away from the user (the OSC-52 fallback). - The event loop only repaints when something changed. Reporting - daemon_ok: bool -> DaemonStatus. A proxy running with `admin.enabled = false` is healthy; it was reported as "daemon down". Metric panes now show why they are empty instead of a confident 0. - Dashboard regained listen/https/tls/admin/auth/scripts, in-flight, bytes in/out, TLS connections, half-open circuits and the apps overflow hint. Docs - docs/tui-screenshots depicted a UI that was never built (top tabs, cyan palette, panels since removed). Regenerated against the shipped layout, with a README stating they are mockups and where each element's source of truth lives. Co-Authored-By: Claude Opus 5 (1M context) --- docs/tui-screenshots/01-dashboard.png | Bin 0 -> 49142 bytes docs/tui-screenshots/02-routes.png | Bin 0 -> 33700 bytes docs/tui-screenshots/03-apps.png | Bin 0 -> 30687 bytes docs/tui-screenshots/04-circuits.png | Bin 0 -> 25756 bytes docs/tui-screenshots/05-errors.png | Bin 0 -> 31922 bytes docs/tui-screenshots/06-config.png | Bin 0 -> 37465 bytes docs/tui-screenshots/07-help.png | Bin 0 -> 47413 bytes docs/tui-screenshots/README.md | 23 + scripts/tui_screenshots.py | 448 +++++++++++++ src/metrics.rs | 1 + src/tui/app.rs | 897 +++++++++++++++++++------- src/tui/mod.rs | 178 +++-- src/tui/screens/apps.rs | 27 +- src/tui/screens/circuits.rs | 29 +- src/tui/screens/config_viewer.rs | 50 +- src/tui/screens/dashboard.rs | 620 +++++++++--------- src/tui/screens/errors.rs | 16 +- src/tui/screens/routes.rs | 96 +-- src/tui/theme.rs | 300 +++++++++ 19 files changed, 2005 insertions(+), 680 deletions(-) create mode 100644 docs/tui-screenshots/01-dashboard.png create mode 100644 docs/tui-screenshots/02-routes.png create mode 100644 docs/tui-screenshots/03-apps.png create mode 100644 docs/tui-screenshots/04-circuits.png create mode 100644 docs/tui-screenshots/05-errors.png create mode 100644 docs/tui-screenshots/06-config.png create mode 100644 docs/tui-screenshots/07-help.png create mode 100644 docs/tui-screenshots/README.md create mode 100644 scripts/tui_screenshots.py create mode 100644 src/tui/theme.rs diff --git a/docs/tui-screenshots/01-dashboard.png b/docs/tui-screenshots/01-dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..5e5f7f43f6eb0ff0072d16f79701d452ff1c8513 GIT binary patch literal 49142 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 literal 0 HcmV?d00001 diff --git a/docs/tui-screenshots/02-routes.png b/docs/tui-screenshots/02-routes.png new file mode 100644 index 0000000000000000000000000000000000000000..fa571fc0bea65bb13af44a3603fca2612ad8e8a8 GIT binary patch literal 33700 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 literal 0 HcmV?d00001 diff --git a/docs/tui-screenshots/03-apps.png b/docs/tui-screenshots/03-apps.png new file mode 100644 index 0000000000000000000000000000000000000000..a91d92a40c377864c3a9149089f652a7ab55d18b GIT binary patch literal 30687 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 literal 0 HcmV?d00001 diff --git a/docs/tui-screenshots/04-circuits.png b/docs/tui-screenshots/04-circuits.png new file mode 100644 index 0000000000000000000000000000000000000000..cd7ac76b4d3b4a4985448f75a016e0107e305fa9 GIT binary patch literal 25756 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 literal 0 HcmV?d00001 diff --git a/docs/tui-screenshots/07-help.png b/docs/tui-screenshots/07-help.png new file mode 100644 index 0000000000000000000000000000000000000000..f9126b557ead5cb9e1d10ce102dcd532b5d573e7 GIT binary patch literal 47413 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 literal 0 HcmV?d00001 diff --git a/docs/tui-screenshots/README.md b/docs/tui-screenshots/README.md new file mode 100644 index 0000000..4ad8616 --- /dev/null +++ b/docs/tui-screenshots/README.md @@ -0,0 +1,23 @@ +# TUI screenshots + +**These images are mockups, not captures.** They are drawn by +`scripts/tui_screenshots.py` with representative sample data, because the real +TUI needs a live daemon, a terminal and an admin password to render anything. + +The layout, palette and key hints are mirrored from the source by hand: + +| Image element | Source of truth | +| ------------------------------ | -------------------------- | +| palette, sidebar, panel chrome | `src/tui/theme.rs` | +| screen composition | `src/tui/screens/*.rs` | +| footer hints, help overlay | `src/tui/app.rs` | + +If you change any of those, re-run the script and compare the result against the +real TUI: + +```sh +python3 scripts/tui_screenshots.py # needs Pillow +``` + +A mockup that has drifted from the shipped UI is worse than no mockup — if you +cannot keep these current, delete them rather than leaving them stale. diff --git a/scripts/tui_screenshots.py b/scripts/tui_screenshots.py new file mode 100644 index 0000000..a13f494 --- /dev/null +++ b/scripts/tui_screenshots.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python3 +"""Render mockups of the Soli Proxy TUI for the docs. + +These are drawn, not captured: the TUI needs a live daemon, a terminal and a +password prompt, none of which a docs build has. The layout, palette and copy +below are therefore kept in lockstep with the real code by hand: + + palette + sidebar + list_block -> src/tui/theme.rs + screen composition -> src/tui/screens/*.rs + footer and help text -> src/tui/app.rs + +If you change any of those, re-run this script and eyeball the result against +the real thing. A mockup that has drifted from the UI is worse than no mockup. +""" + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + +# The TUI is laid out for a wide terminal; 118x34 matches a maximised window. +COLS, ROWS = 118, 34 +CW, CH = 8, 16 +W, H = COLS * CW, ROWS * CH + +# ── palette: mirrors src/tui/theme.rs ────────────────────────────────── +ACCENT = (0, 212, 170) +ACCENT_DIM = (0, 120, 100) +SUCCESS = (80, 250, 123) +WARN = (255, 184, 108) +DANGER = (255, 85, 85) +MUTED = (98, 114, 164) +FG = (248, 248, 242) +SELECT_BG = (15, 55, 52) +SIDEBAR_BG = (18, 22, 28) +INK = (10, 12, 16) +MAGENTA = (189, 147, 249) +CYAN_3XX = (139, 233, 253) + +# ── theme.rs constants ───────────────────────────────────────────────── +SIDEBAR_WIDTH = 16 +NAV_TOP_OFFSET = 3 +SCREEN_SHORT = ["dash", "routes", "apps", "circuits", "errors", "config"] +VERSION = "0.29.2" + +BODY_X = SIDEBAR_WIDTH +BODY_W = COLS - SIDEBAR_WIDTH +BODY_H = ROWS - 1 # last row is the footer +FOOTER_Y = ROWS - 1 + +OUT = Path("docs/tui-screenshots") + +FONT_CANDIDATES = [ + "/System/Library/Fonts/Menlo.ttc", + "/System/Library/Fonts/Monaco.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", + "/usr/share/fonts/TTF/DejaVuSansMono.ttf", + "/Library/Fonts/DejaVuSansMono.ttf", +] + + +def load_font(): + for path in FONT_CANDIDATES: + if Path(path).exists(): + try: + return ImageFont.truetype(path, 12) + except OSError: + continue + raise SystemExit( + "No monospace font found. Add one to FONT_CANDIDATES in this script." + ) + + +FONT = load_font() + + +# ── cell-grid primitives ─────────────────────────────────────────────── + + +def put(img, x, y, text, fg=FG, bg=None): + """Draw `text` starting at character cell (x, y), clipped to the grid.""" + if y < 0 or y >= ROWS or x >= COLS: + return + text = text[: max(0, COLS - x)] + if not text: + return + draw = ImageDraw.Draw(img) + px, py = x * CW, y * CH + if bg: + draw.rectangle([px, py, px + len(text) * CW - 1, py + CH - 1], fill=bg) + draw.text((px, py + 1), text, font=FONT, fill=fg) + + +def fill(img, x, y, w, h, color): + ImageDraw.Draw(img).rectangle( + [x * CW, y * CH, (x + w) * CW - 1, (y + h) * CH - 1], fill=color + ) + + +def list_block(img, x, y, w, h, title): + """theme::list_block — a left rule plus an inverted title chip. + + Returns the content rect (theme::body): one column in, one row down. + """ + for i in range(h): + put(img, x, y + i, "│", ACCENT_DIM) + put(img, x + 1, y, f" {title} ", INK, ACCENT) + return x + 1, y + 1, w - 1, h - 1 + + +def kpi(img, x, y, w, h, value, label, color): + """theme::kpi — left rule, value on the first row, label under it.""" + for i in range(h): + put(img, x, y + i, "│", color) + put(img, x + 2, y, value, color) + put(img, x + 2, y + 1, label, MUTED) + + +def bar(img, x, y, cells, color): + """A solid horizontal bar. Drawn as a rectangle rather than repeated "█" + so it fills the cell the way a terminal does, whatever the font.""" + if cells <= 0: + return + fill(img, x, y, cells, 1, color) + + +def sparkline(img, x, y, w, h, values, color): + """ratatui's Sparkline: one column per sample, bars grown from the bottom.""" + peak = max(values) or 1 + for i, value in enumerate(values[-w:]): + filled = value / peak * h + full = int(filled) + for row in range(full): + fill(img, x + i, y + h - 1 - row, 1, 1, color) + frac = filled - full + if frac > 0.125 and full < h: + # Partial top cell: an eighth-block, approximated by a part-height rect. + top = y + h - 1 - full + px, py = (x + i) * CW, top * CH + height = max(1, round(CH * frac)) + ImageDraw.Draw(img).rectangle( + [px, py + CH - height, px + CW - 1, py + CH - 1], fill=color + ) + + +def pct_split(x, w, *weights): + """Ratatui's Percentage constraints across `w` columns.""" + total = sum(weights) + out, cur = [], x + for i, weight in enumerate(weights): + cw = w - (cur - x) if i == len(weights) - 1 else round(w * weight / total) + out.append((cur, cw)) + cur += cw + return out + + +# ── chrome ───────────────────────────────────────────────────────────── + + +def sidebar(img, active): + fill(img, 0, 0, SIDEBAR_WIDTH, ROWS, SIDEBAR_BG) + put(img, 0, 0, " SOLI", INK, ACCENT) + put(img, 0, 1, " proxy", MUTED, SIDEBAR_BG) + for i, short in enumerate(SCREEN_SHORT): + marker = "▸" if i == active else " " + label = f" {marker} {i + 1} {short:<8}" + if i == active: + put(img, 0, NAV_TOP_OFFSET + i, label, ACCENT, SELECT_BG) + else: + put(img, 0, NAV_TOP_OFFSET + i, label, MUTED, SIDEBAR_BG) + put(img, 0, ROWS - 2, f" v{VERSION}", MUTED, SIDEBAR_BG) + put(img, 0, ROWS - 1, " 1-6 ?", ACCENT_DIM, SIDEBAR_BG) + + +def chrome(active, keys, daemon=("● ", SUCCESS, "daemon")): + img = Image.new("RGB", (W, H), INK) + sidebar(img, active) + put(img, BODY_X, FOOTER_Y, f" {keys} ", MUTED) + glyph, color, name = daemon + badge = f" {name} {glyph}" + put(img, COLS - len(badge) - 1, FOOTER_Y, badge, color) + return img + + +NAV_KEYS = "1-6 nav" + + +# ── screens ──────────────────────────────────────────────────────────── + + +def dashboard(): + img = chrome(0, f"{NAV_KEYS} Tab cycle r ? q") + + # Row 0: four KPI tiles (Length(4)). + tiles = pct_split(BODY_X, BODY_W, 25, 25, 25, 25) + for (x, w), (value, label, color) in zip( + tiles, + [ + ("12.4K", "requests", ACCENT), + ("42", "req / s", SUCCESS), + ("18.2ms", "avg latency", WARN), + ("0.10%", "error rate", DANGER), + ], + ): + kpi(img, x, 0, w, 4, value, label, color) + + # Row 1: live rps sparkline | http status bars (Length(6)). + (sx, sw), (hx, hw) = pct_split(BODY_X, BODY_W, 55, 45) + cx, cy, cw, ch = list_block(img, sx, 4, sw, 6, "live rps 42") + series = [ + 12, 18, 27, 19, 24, 36, 31, 22, 17, 29, 44, 33, 25, 13, 19, 26, 34, 41, + 28, 20, 14, 21, 30, 39, 47, 35, 27, 18, 12, 20, 28, 33, 40, 46, 38, 31, + 24, 17, 11, 19, 26, 32, 39, 34, 27, 21, 15, 42, + ] + sparkline(img, cx, cy, cw, ch, series, ACCENT) + + cx, cy, cw, _ = list_block(img, hx, 4, hw, 6, "http") + bar_w = cw - 16 + for i, (code, count, pct, color) in enumerate( + [ + ("2xx", "12.1K", 0.965, SUCCESS), + ("3xx", "180", 0.014, CYAN_3XX), + ("4xx", "142", 0.011, WARN), + ("5xx", "12", 0.010, DANGER), + ] + ): + put(img, cx, cy + i, code, color) + put(img, cx + 5, cy + i, count, FG) + bar(img, cx + 14, cy + i, round(bar_w * pct), color) + + # Row 2: four meta tiles (Length(6)). + tiles = pct_split(BODY_X, BODY_W, 25, 25, 25, 25) + for (x, w), (value, label, color) in zip( + tiles, + [ + ("2h 14m 8s", "up 0.0.0.0:80 :443", ACCENT), + ("3/4", "apps running", SUCCESS), + ("8", "routes", MAGENTA), + ("5", "circuits", SUCCESS), + ], + ): + kpi(img, x, 10, w, 6, value, label, color) + + # Row 3: apps overview | server detail (Min(6)). + (ax, aw), (vx, vw) = pct_split(BODY_X, BODY_W, 58, 42) + cx, cy, _, _ = list_block(img, ax, 16, aw, BODY_H - 16, "apps") + put(img, cx, cy, "name domain slot status port", MUTED) + apps = [ + ("api", "api.example.com", "blue", "● run", SUCCESS, ":8081"), + ("web", "www.example.com", "green", "● run", SUCCESS, ":8082"), + ("docs", "docs.example.com", "blue", "● run", SUCCESS, ":8083"), + ("legacy", "old.example.com", "blue", "○ stop", MUTED, "-"), + ] + for i, (name, domain, slot, status, color, port) in enumerate(apps): + row = cy + 1 + i + put(img, cx, row, name, FG) + put(img, cx + 12, row, domain, MUTED) + put(img, cx + 34, row, slot, MUTED) + put(img, cx + 43, row, status, color) + put(img, cx + 53, row, port, MUTED) + + cx, cy, _, _ = list_block(img, vx, 16, vw, BODY_H - 16, "server") + server = [ + ("listen", "0.0.0.0:80", FG), + ("https", ":443", FG), + ("tls", "acme", FG), + ("admin", "127.0.0.1:9090", FG), + ("auth", "enabled", FG), + ("scripts", "auth.lua", MAGENTA), + ("in flight", "3", FG), + ("bytes in", "4.10 MB", FG), + ("bytes out", "82.3 MB", FG), + ("tls conns", "1.3K", FG), + ("errors", "12", DANGER), + ] + for i, (key, value, color) in enumerate(server): + put(img, cx, cy + i, key, MUTED) + put(img, cx + 10, cy + i, value, color) + return img + + +def _table(img, x, y, w, h, title, header, rows, selected=0): + cx, cy, cw, _ = list_block(img, x, y, w, h, title) + put(img, cx, cy, header, ACCENT) + for i, cells in enumerate(rows): + row = cy + 1 + i + if i == selected: + fill(img, cx, row, cw, 1, SELECT_BG) + for col, text, color in cells: + put(img, cx + col, row, text, FG if i == selected else color) + return cx, cy, cw + + +def routes(): + img = chrome(1, f"{NAV_KEYS} j/k a add e edit d delete / r ? q") + rows = [ + [(0, "0", MUTED), (4, "api.example.com", FG), (26, "http://127.0.0.1:8081", FG), + (52, "-", MUTED), (60, "auth.lua", MAGENTA), (72, "round_robin", MUTED)], + [(0, "1", MUTED), (4, "www.example.com", FG), (26, "http://127.0.0.1:8082", FG), + (52, "2 users", SUCCESS), (60, "-", MUTED), (72, "least_conn", MUTED)], + [(0, "2", MUTED), (4, "docs.example.com", FG), (26, "http://127.0.0.1:8083", FG), + (52, "-", MUTED), (60, "-", MUTED), (72, "round_robin", MUTED)], + [(0, "3", MUTED), (4, "/api/v2/*", FG), (26, "http://10.0.0.4:9000 +2", FG), + (52, "1 user", SUCCESS), (60, "rate.lua", MAGENTA), (72, "ip_hash", MUTED)], + [(0, "4", MUTED), (4, "^/static/(.*)$", FG), (26, "http://127.0.0.1:8090", FG), + (52, "-", MUTED), (60, "-", MUTED), (72, "round_robin", MUTED)], + ] + _table( + img, BODY_X, 0, BODY_W, BODY_H, "routes", + "# Matcher Targets Auth Scripts LB", + rows, selected=1, + ) + return img + + +def apps(): + img = chrome(2, f"{NAV_KEYS} j/k Enter action / r ? q") + rows = [ + [(0, "api", FG), (12, "api.example.com", MUTED), (34, "Running", SUCCESS), + (45, "2.4%", FG), (53, "128.4 MB", FG), (65, "8.2K", FG), (73, "4", DANGER), (81, "12.1ms", FG)], + [(0, "web", FG), (12, "www.example.com", MUTED), (34, "Running", SUCCESS), + (45, "1.1%", FG), (53, "96.0 MB", FG), (65, "3.4K", FG), (73, "0", FG), (81, "8.4ms", FG)], + [(0, "docs", FG), (12, "docs.example.com", MUTED), (34, "Running", SUCCESS), + (45, "0.3%", FG), (53, "42.1 MB", FG), (65, "612", FG), (73, "0", FG), (81, "6.0ms", FG)], + [(0, "legacy", FG), (12, "old.example.com", MUTED), (34, "Stopped", MUTED), + (45, "-", MUTED), (53, "-", MUTED), (65, "0", FG), (73, "0", FG), (81, "-", MUTED)], + ] + _table( + img, BODY_X, 0, BODY_W, BODY_H, "apps", + "Name Domain Status CPU Memory Reqs Errors Avg RT", + rows, selected=0, + ) + return img + + +def circuits(): + img = chrome(3, f"{NAV_KEYS} j/k r ? q") + rows = [ + [(0, "http://127.0.0.1:8081", FG), (40, "closed", SUCCESS), (52, "0", FG), (66, "8241", FG)], + [(0, "http://127.0.0.1:8082", FG), (40, "closed", SUCCESS), (52, "0", FG), (66, "3402", FG)], + [(0, "http://10.0.0.4:9000", FG), (40, "open", DANGER), (52, "12", FG), (66, "0", FG)], + [(0, "http://10.0.0.5:9000", FG), (40, "half_open", WARN), (52, "5", FG), (66, "2", FG)], + [(0, "http://127.0.0.1:8090", FG), (40, "closed", SUCCESS), (52, "1", FG), (66, "612", FG)], + ] + _table( + img, BODY_X, 0, BODY_W, BODY_H, "circuits", + "Target State Failures Successes", + rows, selected=2, + ) + return img + + +def errors(): + img = chrome(4, f"{NAV_KEYS} j/k Enter detail r ? q") + rows = [ + [(0, "14:02:11", MUTED), (10, "502", DANGER), (16, "GET", FG), (22, "api.example.com", FG), (40, "/v1/orders", FG)], + [(0, "14:02:44", MUTED), (10, "504", DANGER), (16, "POST", FG), (22, "api.example.com", FG), (40, "/v1/checkout", FG)], + [(0, "14:05:02", MUTED), (10, "failed", DANGER), (16, "GET", FG), (22, "old.example.com", FG), (40, "/legacy/report", FG)], + [(0, "14:09:37", MUTED), (10, "500", DANGER), (16, "GET", FG), (22, "www.example.com", FG), (40, "/account/settings", FG)], + [(0, "14:11:20", MUTED), (10, "502", DANGER), (16, "GET", FG), (22, "api.example.com", FG), (40, "/v1/orders", FG)], + ] + _table( + img, BODY_X, 0, BODY_W, BODY_H, "errors 5", + "Time Status Method Host Path", + rows, selected=3, + ) + return img + + +def config(): + img = chrome(5, f"{NAV_KEYS} j/k r ? q") + cx, cy, cw, _ = list_block(img, BODY_X, 0, BODY_W, BODY_H, "proxy.conf") + text = [ + "[server]", + 'bind = "0.0.0.0:80"', + "https_port = 443", + "", + "[tls]", + 'mode = "acme"', + 'acme_email = "ops@example.com"', + "", + "[admin]", + 'bind = "127.0.0.1:9090"', + "enabled = true", + 'username = "admin"', + "", + "[[rules]]", + 'matcher = { domain = "api.example.com" }', + 'targets = [{ url = "http://127.0.0.1:8081" }]', + 'scripts = ["auth.lua"]', + "", + "[[rules]]", + 'matcher = { domain = "www.example.com" }', + 'targets = [{ url = "http://127.0.0.1:8082" }]', + 'load_balancing = "least_conn"', + ] + for i, line in enumerate(text): + put(img, cx, cy + i, line, FG) + hint = " 1-32 of 214 · j/k " + put(img, cx + cw - len(hint), 0, hint, MUTED) + return img + + +def help_overlay(): + img = dashboard() + # theme::centered_modal(body, 64, 22) over the body area. + w, h = 64, 22 + x = BODY_X + (BODY_W - w) // 2 + y = (BODY_H - h) // 2 + fill(img, x, y, w, h, INK) + cx, cy, _, _ = list_block(img, x, y, w, h, "help") + text = [ + " 1-6 Jump to screen Tab / S-Tab Cycle", + " j/k PgUp/Dn Move g / G First / last", + " / Search r Refresh now", + " Enter Select / open Esc Back", + " a/e/d Route add/edit/del", + " Mouse Click nav, click rows, wheel scrolls", + " q Quit", + "", + " Apps: Enter -> Deploy / Restart / Stop / Rollback / Logs", + " Errors: Enter detail · y copy (OSC 52)", + "", + " Any key closes this overlay", + ] + for i, line in enumerate(text): + put(img, cx, cy + i, line, FG) + return img + + +def main(): + OUT.mkdir(parents=True, exist_ok=True) + shots = { + "01-dashboard.png": dashboard, + "02-routes.png": routes, + "03-apps.png": apps, + "04-circuits.png": circuits, + "05-errors.png": errors, + "06-config.png": config, + "07-help.png": help_overlay, + } + for name, fn in shots.items(): + path = OUT / name + fn().save(path) + print(path) + + +if __name__ == "__main__": + main() diff --git a/src/metrics.rs b/src/metrics.rs index a857cc7..08deeea 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -9,6 +9,7 @@ struct CpuSnapshot { timestamp: Instant, } +#[derive(Debug, Clone, Copy)] pub struct MetricsSnapshot { pub requests_total: u64, pub requests_in_flight: usize, diff --git a/src/tui/app.rs b/src/tui/app.rs index dc6c78b..e6a92b2 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -2,15 +2,20 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, prelude::Stylize, style::{Color, Style}, + text::{Line, Span}, widgets::{Block, Borders, Clear, Paragraph}, Frame, }; use std::collections::{HashMap, VecDeque}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; +use crate::config::ConfigManager; use crate::metrics::{AppMetricsJson, MetricsSnapshot}; use super::errors::{load_request_errors, ErrorEntry}; +use super::theme; use super::{route_form::RouteForm, screens, TuiContext}; /// Per-app stats combining traffic (from admin API) and system (from /proc). @@ -31,7 +36,187 @@ pub struct AppHistory { pub mem: VecDeque, } -const HISTORY_LEN: usize = 60; // 60 samples × 2s = 2 minutes +const HISTORY_LEN: usize = 60; // 60 samples × 1s = 1 minute + +/// How often the background poller re-reads the daemon's admin API. +const DAEMON_POLL_INTERVAL: Duration = Duration::from_secs(1); + +/// State of the daemon's admin API. "Not reachable" and "not enabled" are +/// different things: a proxy running with `admin.enabled = false` is perfectly +/// healthy, it just has no metrics endpoint to offer. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum DaemonStatus { + /// No poll has completed yet. + #[default] + Connecting, + Ok, + /// Admin API is enabled but did not answer. + Unreachable, + /// `admin.enabled = false` — nothing to connect to, nothing wrong. + Disabled, +} + +impl DaemonStatus { + /// Short footer badge. + pub fn badge(self) -> &'static str { + match self { + DaemonStatus::Connecting => " daemon … ", + DaemonStatus::Ok => " daemon ● ", + DaemonStatus::Unreachable => " daemon ✕ ", + DaemonStatus::Disabled => " daemon ○ ", + } + } + + pub fn color(self) -> Color { + match self { + DaemonStatus::Connecting => theme::MUTED, + DaemonStatus::Ok => theme::SUCCESS, + DaemonStatus::Unreachable => theme::DANGER, + DaemonStatus::Disabled => theme::MUTED, + } + } + + /// Why the metric panes are empty, when they are. + pub fn explain(self) -> &'static str { + match self { + DaemonStatus::Connecting => "connecting", + DaemonStatus::Ok => "", + DaemonStatus::Unreachable => "daemon unreachable", + DaemonStatus::Disabled => "admin api off", + } + } + + pub fn is_ok(self) -> bool { + self == DaemonStatus::Ok + } +} + +/// One completed poll of the admin API. +#[derive(Default)] +struct DaemonSample { + apps: HashMap, + global: Option, + status: DaemonStatus, + /// Incremented per completed poll so the UI can tell a fresh sample from a + /// repeat read of the same one. + seq: u64, +} + +/// Handle to the background poller. The fetch used to run inline on the render +/// thread via `block_on`, which froze the UI for the whole request timeout on +/// every tick — worst exactly when the daemon was down and you most wanted to +/// look around. Now the UI only ever reads the last completed sample. +struct DaemonFeed { + latest: Arc>, + wake: Arc, +} + +impl DaemonFeed { + fn spawn(runtime: &tokio::runtime::Runtime, config_manager: Arc) -> Self { + let latest = Arc::new(Mutex::new(DaemonSample::default())); + let wake = Arc::new(tokio::sync::Notify::new()); + let cell = latest.clone(); + let signal = wake.clone(); + + runtime.spawn(async move { + // Off the render thread, so the timeout can be generous, and the + // client (with its connection pool) is built once rather than per tick. + let client = match reqwest::Client::builder() + .timeout(Duration::from_millis(1500)) + .build() + { + Ok(client) => client, + Err(_) => { + if let Ok(mut slot) = cell.lock() { + slot.status = DaemonStatus::Unreachable; + slot.seq = slot.seq.wrapping_add(1); + } + return; + } + }; + + loop { + let sample = poll_daemon(&client, &config_manager).await; + if let Ok(mut slot) = cell.lock() { + let seq = slot.seq.wrapping_add(1); + *slot = DaemonSample { seq, ..sample }; + } + tokio::select! { + _ = tokio::time::sleep(DAEMON_POLL_INTERVAL) => {} + _ = signal.notified() => {} + } + } + }); + + Self { latest, wake } + } + + /// Ask the poller to fetch again now instead of waiting out its interval. + fn request_refresh(&self) { + self.wake.notify_one(); + } +} + +/// Fetch per-app and global traffic metrics from the daemon's admin API. +/// Best effort: an unreachable daemon yields an `Unreachable` sample rather +/// than an error, so the UI keeps running with whatever it last knew. +async fn poll_daemon(client: &reqwest::Client, config_manager: &ConfigManager) -> DaemonSample { + let cfg = config_manager.get_config(); + if !cfg.admin.enabled.unwrap_or(true) { + return DaemonSample { + status: DaemonStatus::Disabled, + ..Default::default() + }; + } + + // Convert "0.0.0.0:9090" to "127.0.0.1:9090" for local connections + let admin_addr = cfg + .admin + .bind + .replace("0.0.0.0:", "127.0.0.1:") + .replace("[::]:", "127.0.0.1:"); + let apps_url = format!("http://{}/api/v1/app-metrics", admin_addr); + let global_url = format!("http://{}/api/v1/metrics", admin_addr); + let api_key = cfg.admin.api_key.clone(); + + let with_key = |mut req: reqwest::RequestBuilder| { + if let Some(ref key) = api_key { + req = req.header("X-Api-Key", key); + } + req + }; + + // Admin API wraps JSON responses in {"ok": true, "data": ...} + #[derive(serde::Deserialize)] + struct Envelope { + data: HashMap, + } + + let (apps, global) = tokio::join!( + async { + let resp = with_key(client.get(&apps_url)).send().await.ok()?; + resp.json::().await.ok().map(|e| e.data) + }, + async { + let resp = with_key(client.get(&global_url)).send().await.ok()?; + let text = resp.text().await.ok()?; + Some(parse_prometheus_snapshot(&text)) + } + ); + + let status = if apps.is_some() || global.is_some() { + DaemonStatus::Ok + } else { + DaemonStatus::Unreachable + }; + + DaemonSample { + apps: apps.unwrap_or_default(), + global, + status, + seq: 0, + } +} #[derive(Debug, Clone, Copy, PartialEq, Default)] pub enum Screen { @@ -59,6 +244,17 @@ pub enum Modal { const APP_ACTIONS: &[&str] = &["Deploy", "Restart", "Stop", "Rollback", "View Logs"]; +/// How long a transient status flash stays on screen. +const TOAST_TTL: Duration = Duration::from_secs(2); + +/// Largest scroll offset that still fills the viewport: the top of the last +/// page, not the index of the last row. Every screen renders its rows as +/// `.skip(scroll_offset).take(visible)`, so an offset of `len - 1` would leave +/// exactly one row on screen. +fn max_offset(len: usize, visible: usize) -> usize { + len.saturating_sub(visible.max(1)) +} + pub struct TuiApp { ctx: TuiContext, current_screen: Screen, @@ -85,10 +281,29 @@ pub struct TuiApp { errors: Vec, /// Transient "copied to clipboard" flash for the error detail modal. error_copied: bool, + toast: Option<(String, Instant)>, + /// Background poller for the daemon's admin API. + daemon: DaemonFeed, + daemon_status: DaemonStatus, + /// `seq` of the last sample folded into `rps_history`, so a tick that + /// re-reads an unchanged sample does not fabricate a rate of zero. + last_daemon_seq: u64, + rps_history: VecDeque, + last_requests_total: u64, + last_metrics_at: Option, + frame_ticks: u64, + /// Cached `proxy.conf` text and line count for the Config screen, so it is + /// not re-read from disk on every frame. + config_text: String, + config_line_count: usize, + last_sidebar: Rect, + last_body: Rect, + visible_height: usize, } impl TuiApp { pub fn new(ctx: TuiContext) -> Self { + let daemon = DaemonFeed::spawn(&ctx.runtime, ctx.config_manager.clone()); let mut app = Self { ctx, current_screen: Screen::Dashboard, @@ -110,16 +325,61 @@ impl TuiApp { log_auto_follow: true, errors: Vec::new(), error_copied: false, + toast: None, + daemon, + daemon_status: DaemonStatus::Connecting, + last_daemon_seq: 0, + rps_history: VecDeque::with_capacity(HISTORY_LEN), + last_requests_total: 0, + last_metrics_at: None, + frame_ticks: 0, + config_text: String::new(), + config_line_count: 0, + last_sidebar: Rect::default(), + last_body: Rect::default(), + visible_height: 20, }; app.collect_stats(); app } - /// Collect all per-app stats: traffic from admin API + system from /proc. + fn show_toast(&mut self, msg: impl Into) { + self.toast = Some((msg.into(), Instant::now())); + } + + /// Collect all per-app stats: traffic from the background daemon poller + + /// system stats from /proc. Never blocks on the network. fn collect_stats(&mut self) { - // Traffic counters live in the daemon process — fetch them via the - // admin API (best effort; zeros if the daemon is unreachable). - let (traffic, global) = self.fetch_daemon_metrics(); + let (traffic, global, status, seq) = self.read_daemon_sample(); + self.daemon_status = status; + + // Only fold a sample into the rate history once. Re-reading the same + // sample would show a delta of zero over a growing interval. + let fresh = seq != self.last_daemon_seq; + self.last_daemon_seq = seq; + if fresh { + if let Some(ref snap) = global { + let now = Instant::now(); + if let Some(prev_at) = self.last_metrics_at { + let elapsed = now.duration_since(prev_at).as_secs_f64(); + let rps = theme::rps_from_delta( + self.last_requests_total, + snap.requests_total, + elapsed, + ); + if self.rps_history.len() >= HISTORY_LEN { + self.rps_history.pop_front(); + } + self.rps_history.push_back(rps); + } + self.last_requests_total = snap.requests_total; + self.last_metrics_at = Some(now); + } else { + // Daemon went away: drop the stale anchor so the first sample + // after it returns is not averaged across the whole outage. + self.last_metrics_at = None; + } + } self.remote_snapshot = global; // Re-probe PIDs for apps that lost theirs, then collect /proc stats @@ -192,60 +452,38 @@ impl TuiApp { // Parse individual request failures from proxy.log for the Errors screen. self.errors = load_request_errors(); - } - /// Fetch per-app and global traffic metrics from the daemon's admin API. - /// Best effort: returns empty/None when the admin API is disabled or - /// unreachable (short timeout so the UI never stalls noticeably). - fn fetch_daemon_metrics(&self) -> (HashMap, Option) { - let cfg = self.ctx.config_manager.get_config(); - if !cfg.admin.enabled.unwrap_or(true) { - return (HashMap::new(), None); - } + // Cached for the Config screen, which used to re-read the file on every + // frame and had no way to know how far it could scroll. + self.config_text = std::fs::read_to_string(self.ctx.config_manager.config_path()) + .unwrap_or_else(|e| format!("Failed to read config file: {e}")); + self.config_line_count = self.config_text.lines().count(); - // Convert "0.0.0.0:9090" to "127.0.0.1:9090" for local connections - let admin_addr = cfg - .admin - .bind - .replace("0.0.0.0:", "127.0.0.1:") - .replace("[::]:", "127.0.0.1:"); - let apps_url = format!("http://{}/api/v1/app-metrics", admin_addr); - let global_url = format!("http://{}/api/v1/metrics", admin_addr); - let api_key = cfg.admin.api_key.clone(); - - self.ctx.runtime.block_on(async { - let Ok(client) = reqwest::Client::builder() - .timeout(std::time::Duration::from_millis(500)) - .build() - else { - return (HashMap::new(), None); - }; - let with_key = |mut req: reqwest::RequestBuilder| { - if let Some(ref key) = api_key { - req = req.header("X-Api-Key", key); - } - req - }; + self.clamp_scroll(); + } - // Admin API wraps JSON responses in {"ok": true, "data": ...} - #[derive(serde::Deserialize)] - struct Envelope { - data: HashMap, - } - - let (apps, global) = tokio::join!( - async { - let resp = with_key(client.get(&apps_url)).send().await.ok()?; - resp.json::().await.ok().map(|e| e.data) - }, - async { - let resp = with_key(client.get(&global_url)).send().await.ok()?; - let text = resp.text().await.ok()?; - Some(parse_prometheus_snapshot(&text)) - } - ); - (apps.unwrap_or_default(), global) - }) + /// Copy the newest sample produced by the background poller. Lock + /// contention is a few microseconds and there is no I/O on this path, so + /// this is safe to call from the render thread. + fn read_daemon_sample( + &self, + ) -> ( + HashMap, + Option, + DaemonStatus, + u64, + ) { + match self.daemon.latest.lock() { + Ok(slot) => (slot.apps.clone(), slot.global, slot.status, slot.seq), + // Poisoned only if the poller panicked mid-write; report it rather + // than propagating the panic into the render loop. + Err(_) => ( + HashMap::new(), + None, + DaemonStatus::Unreachable, + self.last_daemon_seq, + ), + } } /// Called on each tick (auto-refresh). @@ -266,6 +504,21 @@ impl TuiApp { self.check_pending_action(); } + /// Per-loop housekeeping. Returns true when the frame needs repainting, so + /// an idle TUI does not redraw ten times a second for an identical image. + pub fn on_frame(&mut self) -> bool { + self.frame_ticks += 1; + let mut dirty = false; + if let Some((_, at)) = self.toast { + if at.elapsed() >= TOAST_TTL { + self.toast = None; + dirty = true; + } + } + // The footer spinner animates only while an action is in flight. + dirty || self.pending_action.is_some() + } + pub fn has_pending_action(&self) -> bool { self.pending_action.is_some() } @@ -280,13 +533,12 @@ impl TuiApp { } let handle = self.pending_action.take().unwrap(); match self.ctx.runtime.block_on(handle) { - Ok(Ok(_msg)) => { - // Re-probe after action to update status + Ok(Ok(msg)) => { if let Some(ref mgr) = self.ctx.app_manager { mgr.probe_running_apps(); } - // Clear the progress modal silently on success self.modal = Modal::None; + self.show_toast(msg); } Ok(Err(e)) => { self.modal = Modal::AppActionResult(format!("Error: {}", e)); @@ -299,25 +551,46 @@ impl TuiApp { pub fn render(&mut self, f: &mut Frame) { let size = f.area(); + let (sidebar, rest) = theme::split_shell(size); + let has_toast = self.toast.is_some(); let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(3), Constraint::Min(0), + Constraint::Length(if has_toast { 1 } else { 0 }), Constraint::Length(1), ]) - .split(size); + .split(rest); - self.render_header(f, chunks[0]); + self.last_sidebar = sidebar; + self.last_body = chunks[0]; + // Panel rows minus the title chip and the column header. + self.visible_height = (chunks[0].height.saturating_sub(2) as usize).max(1); + // The terminal may have been resized since the last input event. + self.clamp_scroll(); - if self.help_show { - self.render_help(f, chunks[1]); - } else { - self.render_main(f, chunks[1]); + let nav_idx = match self.current_screen { + Screen::Dashboard => 0, + Screen::Routes => 1, + Screen::Apps => 2, + Screen::Circuits => 3, + Screen::Errors => 4, + Screen::Config => 5, + Screen::Help => 0, + }; + theme::render_sidebar(f, sidebar, nav_idx, env!("CARGO_PKG_VERSION")); + self.render_main(f, chunks[0]); + if has_toast { + if let Some((ref msg, _)) = self.toast { + theme::render_toast(f, chunks[1], msg); + } } - self.render_footer(f, chunks[2]); + if self.help_show { + self.render_help(f, chunks[0]); + } + match &self.modal { Modal::RouteForm => self.render_route_form_modal(f), Modal::DeleteConfirm(idx) => self.render_delete_confirm(f, *idx), @@ -391,6 +664,20 @@ impl TuiApp { KeyCode::Char('r') => { self.refresh_data(); } + KeyCode::Char('1') => self.jump_screen(Screen::Dashboard), + KeyCode::Char('2') => self.jump_screen(Screen::Routes), + KeyCode::Char('3') => self.jump_screen(Screen::Apps), + KeyCode::Char('4') => self.jump_screen(Screen::Circuits), + KeyCode::Char('5') => self.jump_screen(Screen::Errors), + KeyCode::Char('6') => self.jump_screen(Screen::Config), + KeyCode::PageDown => { + let page = self.get_visible_height().max(1) as i32; + self.move_selection(page); + } + KeyCode::PageUp => { + let page = self.get_visible_height().max(1) as i32; + self.move_selection(-page); + } KeyCode::Char('/') => { self.search_active = true; } @@ -874,6 +1161,10 @@ impl TuiApp { } fn move_selection(&mut self, dir: i32) { + if self.scrolls_text() { + self.scroll_text(dir); + return; + } let max = self.get_max_selection(); if max == 0 { return; @@ -894,7 +1185,13 @@ impl TuiApp { fn get_max_selection(&self) -> usize { match self.current_screen { Screen::Dashboard => 0, - Screen::Routes => self.ctx.config_manager.get_config().rules.len(), + // The filtered count, not the full one: with a search active the + // screen renders fewer rows than the config holds, and a cursor + // past the end scrolls the list into empty space. + Screen::Routes => { + let rules = self.ctx.config_manager.get_config().rules.clone(); + screens::routes::filter_indices(&rules, &self.search_query).len() + } Screen::Apps => self.filtered_apps_count, Screen::Circuits => self.ctx.circuit_breaker.get_states().len(), Screen::Errors => self.errors.len(), @@ -904,14 +1201,119 @@ impl TuiApp { } fn get_visible_height(&self) -> usize { - 20 + self.visible_height.max(1) + } + + /// Screens that scroll a block of text rather than moving a row cursor. + /// They have no selectable rows, so `get_max_selection` is 0 for them and + /// the cursor-based path would refuse to move at all. + fn scrolls_text(&self) -> bool { + matches!(self.current_screen, Screen::Config) + } + + fn text_line_count(&self) -> usize { + match self.current_screen { + Screen::Config => self.config_line_count, + _ => 0, + } + } + + fn max_text_offset(&self) -> usize { + max_offset(self.text_line_count(), self.get_visible_height()) + } + + fn scroll_text(&mut self, dir: i32) { + let max_offset = self.max_text_offset() as i64; + let next = (self.scroll_offset as i64 + dir as i64).clamp(0, max_offset); + self.scroll_offset = next as usize; + } + + /// Keep the viewport in range after the underlying content changes — a + /// search filter shrinking the list, errors rotating out of proxy.log, the + /// config file being edited, or the terminal being resized. + fn clamp_scroll(&mut self) { + if self.scrolls_text() { + self.scroll_offset = self.scroll_offset.min(self.max_text_offset()); + return; + } + let len = self.get_max_selection(); + if len == 0 { + self.selected_index = 0; + self.scroll_offset = 0; + return; + } + self.selected_index = self.selected_index.min(len - 1); + self.scroll_offset = self + .scroll_offset + .min(max_offset(len, self.get_visible_height())); + if self.selected_index < self.scroll_offset { + self.scroll_offset = self.selected_index; + } + } + + fn jump_screen(&mut self, screen: Screen) { + self.current_screen = screen; + self.selected_index = 0; + self.scroll_offset = 0; + } + + /// Returns true when the event changed something worth repainting. + pub fn handle_mouse(&mut self, mouse: crossterm::event::MouseEvent) -> bool { + use crossterm::event::MouseEventKind; + if self.help_show || self.modal != Modal::None { + return false; + } + match mouse.kind { + MouseEventKind::Down(_) => { + if let Some(idx) = theme::nav_at(self.last_sidebar, mouse.column, mouse.row) { + let screen = match idx { + 0 => Screen::Dashboard, + 1 => Screen::Routes, + 2 => Screen::Apps, + 3 => Screen::Circuits, + 4 => Screen::Errors, + 5 => Screen::Config, + _ => return false, + }; + self.jump_screen(screen); + return true; + } + // Row 0 of the panel is the title chip and row 1 the column + // header; data rows start at +2. Clamping instead of offsetting + // would make a click on the header select the first row. + let first_row = self.last_body.y.saturating_add(2); + let past_end = self.last_body.y.saturating_add(self.last_body.height); + if mouse.row >= first_row && mouse.row < past_end { + let rel = (mouse.row - first_row) as usize; + let idx = self.scroll_offset + rel; + if idx < self.get_max_selection() && idx != self.selected_index { + self.selected_index = idx; + return true; + } + } + false + } + MouseEventKind::ScrollDown => { + self.move_selection(1); + true + } + MouseEventKind::ScrollUp => { + self.move_selection(-1); + true + } + _ => false, + } } fn scroll_to_bottom(&mut self) { + if self.scrolls_text() { + self.scroll_offset = self.max_text_offset(); + return; + } let max = self.get_max_selection(); if max > 0 { - self.scroll_offset = max.saturating_sub(1); self.selected_index = max - 1; + self.scroll_offset = max_offset(max, self.get_visible_height()); } } @@ -954,90 +1356,42 @@ impl TuiApp { /// Map the current selected_index to the actual rule index in config, /// accounting for search filtering. + /// Map the on-screen cursor back to an index into `config.rules`. + /// Uses the same predicate the Routes screen renders with, so `e`/`d` + /// always act on the row the user is looking at. fn resolve_route_index(&self) -> Option { let rules = self.ctx.config_manager.get_config().rules.clone(); - if self.search_query.is_empty() { - if self.selected_index < rules.len() { - Some(self.selected_index) - } else { - None - } - } else { - let search_lower = self.search_query.to_lowercase(); - let filtered: Vec = rules - .iter() - .enumerate() - .filter(|(idx, rule)| { - let matcher_str = format!("{:?}", rule.matcher).to_lowercase(); - let targets_str: String = rule - .targets - .iter() - .map(|t| t.url.to_string()) - .collect::>() - .join(", "); - let auth_str: String = rule - .auth - .iter() - .map(|a| a.username.as_str()) - .collect::>() - .join(", "); - let scripts_str = rule.scripts.join(", "); - matcher_str.contains(&search_lower) - || targets_str.to_lowercase().contains(&search_lower) - || auth_str.to_lowercase().contains(&search_lower) - || scripts_str.to_lowercase().contains(&search_lower) - || idx.to_string() == search_lower - }) - .map(|(idx, _)| idx) - .collect(); - filtered.get(self.selected_index).copied() - } + screens::routes::filter_indices(&rules, &self.search_query) + .get(self.selected_index) + .copied() } - fn refresh_data(&mut self) {} - - fn render_header(&self, f: &mut Frame, area: Rect) { - let screen_names = [ - "Dashboard", - "Routes", - "Apps", - "Circuits", - "Errors", - "Config", - ]; - let current_idx = match self.current_screen { - Screen::Dashboard => 0, - Screen::Routes => 1, - Screen::Apps => 2, - Screen::Circuits => 3, - Screen::Errors => 4, - Screen::Config => 5, - Screen::Help => return, - }; - - let mut header_text = String::new(); - for (i, name) in screen_names.iter().enumerate() { - if i == current_idx { - header_text.push_str(&format!("[ {} ] ", name)); - } else { - header_text.push_str(&format!(" {} ", name)); - } - } - - let paragraph = Paragraph::new(header_text) - .style(Style::default().fg(Color::Yellow)) - .alignment(Alignment::Center); - f.render_widget(paragraph, area); + fn refresh_data(&mut self) { + // Kick the poller; its next sample lands on a later tick. The local + // (/proc, log, config) half refreshes synchronously right here. + self.daemon.request_refresh(); + self.collect_stats(); + self.show_toast("refreshed"); } fn render_main(&mut self, f: &mut Frame, area: Rect) { match self.current_screen { - Screen::Dashboard => { - screens::dashboard::render(f, area, &self.ctx, self.remote_snapshot.as_ref()) - } - Screen::Routes => { - screens::routes::render(f, area, &self.ctx, self.selected_index, &self.search_query) - } + Screen::Dashboard => screens::dashboard::render( + f, + area, + &self.ctx, + self.remote_snapshot.as_ref(), + self.daemon_status, + &self.rps_history, + ), + Screen::Routes => screens::routes::render( + f, + area, + &self.ctx, + self.selected_index, + self.scroll_offset, + &self.search_query, + ), Screen::Apps => { let all_apps = self .ctx @@ -1074,7 +1428,13 @@ impl TuiApp { }, ) } - Screen::Circuits => screens::circuits::render(f, area, &self.ctx, self.selected_index), + Screen::Circuits => screens::circuits::render( + f, + area, + &self.ctx, + self.selected_index, + self.scroll_offset, + ), Screen::Errors => screens::errors::render( f, area, @@ -1082,88 +1442,88 @@ impl TuiApp { self.selected_index, self.scroll_offset, ), - Screen::Config => screens::config_viewer::render(f, area, &self.ctx), + Screen::Config => screens::config_viewer::render( + f, + area, + &self.config_text, + self.scroll_offset, + self.config_line_count, + ), Screen::Help => {} } } fn render_footer(&self, f: &mut Frame, area: Rect) { - let mut footer_text = String::new(); - footer_text.push_str( - " q:quit | ?:help | /:search | r:refresh | j/k:move | Tab:cycle | Enter:select", - ); - if self.current_screen == Screen::Routes { - footer_text.push_str(" | a:add | e:edit | d:delete"); - } - if self.current_screen == Screen::Errors { - footer_text.push_str(" | Enter:detail"); - } - if self.search_active { - footer_text = format!("Search: {}_", self.search_query); + let paragraph = Paragraph::new(format!(" / {}_ Esc:cancel", self.search_query)) + .style(Style::default().fg(theme::WARN)); + f.render_widget(paragraph, area); + return; } - let paragraph = Paragraph::new(footer_text) - .style(Style::default().fg(Color::DarkGray)) - .alignment(Alignment::Left); - f.render_widget(paragraph, area); - } + let keys = match self.modal { + Modal::None => match self.current_screen { + Screen::Routes => "1-6 nav j/k a add e edit d delete / r ? q", + Screen::Apps => "1-6 nav j/k Enter action / r ? q", + Screen::Errors => "1-6 nav j/k Enter detail r ? q", + Screen::Circuits => "1-6 nav j/k r ? q", + Screen::Config => "1-6 nav j/k r ? q", + _ => "1-6 nav Tab cycle r ? q", + }, + Modal::RouteForm => "Tab fields Enter save Esc cancel", + Modal::DeleteConfirm(_) => "y confirm n/Esc cancel", + Modal::AppActionMenu(_, _) => "j/k Enter Esc", + Modal::AppActionProgress(_, _) => "Esc dismiss (action continues)", + Modal::AppActionResult(_) => "Esc/Enter close", + Modal::LogViewer(_, _) => "j/k scroll G follow Esc close", + Modal::ErrorDetail(_) => "j/k y copy Esc", + }; - fn render_help(&self, f: &mut Frame, area: Rect) { - let help_text = vec![ - "", - " SOLI PROXY TUI - HELP", - " =====================", - "", - " NAVIGATION", - " j / Down Move down", - " k / Up Move up", - " g Go to first item", - " G Go to last item", - " Tab Next screen", - " Shift+Tab Previous screen", - " Enter Select / Open", - " Esc Go back / Clear", - "", - " ACTIONS", - " q Quit", - " ? Toggle this help", - " / Search filter", - " r Refresh data", - "", - " ROUTES SCREEN", - " a Add new route", - " e Edit selected route", - " d Delete selected route", - "", - " ROUTE FORM", - " Tab/Shift+Tab Navigate fields", - " Left/Right Change select options", - " Enter Save route", - " Esc Cancel", - "", - " APPS SCREEN", - " Enter Open action menu", - " (Deploy/Restart/Stop/Rollback/Logs)", - "", - " ERRORS SCREEN", - " Enter Open error detail", - " j/k (in detail) Navigate between errors", - " y / c Copy error block to clipboard", - " (needs log_endpoints = true)", - "", - " Press any key to close this help", - ]; + let daemon = Span::styled( + self.daemon_status.badge(), + Style::default().fg(self.daemon_status.color()), + ); + let spinner = if self.pending_action.is_some() { + Span::styled( + format!(" {} ", theme::spinner_frame(self.frame_ticks)), + Style::default().fg(theme::WARN), + ) + } else { + Span::raw("") + }; - let block = Block::default() - .title(" Help ") - .borders(Borders::ALL) - .style(Style::default().fg(Color::Cyan)); + let line = Line::from(vec![ + Span::styled(format!(" {keys} "), Style::default().fg(theme::MUTED)), + Span::raw(" "), + spinner, + daemon, + ]); + f.render_widget(Paragraph::new(line).alignment(Alignment::Left), area); + } - let paragraph = Paragraph::new(help_text.join("\n")) - .block(block) - .style(Style::default().fg(Color::White)); - f.render_widget(paragraph, area); + fn render_help(&self, f: &mut Frame, area: Rect) { + let modal = theme::centered_modal(area, 64, 22); + f.render_widget(Clear, modal); + let help_text = "\ + 1-6 Jump to screen Tab / S-Tab Cycle + j/k PgUp/Dn Move g / G First / last + / Search r Refresh now + Enter Select / open Esc Back + a/e/d Route add/edit/del + Mouse Click nav, click rows, wheel scrolls + q Quit + + Apps: Enter → Deploy / Restart / Stop / Rollback / Logs + Errors: Enter detail · y copy (OSC 52) + + Any key closes this overlay"; + let block = theme::list_block("help"); + f.render_widget( + Paragraph::new(help_text) + .block(block) + .style(Style::default().fg(theme::FG)), + modal, + ); } fn render_route_form_modal(&self, f: &mut Frame) { @@ -1509,8 +1869,7 @@ impl TuiApp { fn render_log_viewer(&mut self, f: &mut Frame, app_name: &str, slot: &str) { let log_path = format!("./run/logs/{}/{}.log", app_name, slot); - let log_content = - std::fs::read_to_string(&log_path).unwrap_or_else(|_| "No log file found.".to_string()); + let log_content = tail_file(&log_path, 256 * 1024); let lines: Vec<&str> = log_content.lines().collect(); let total_lines = lines.len(); @@ -1559,12 +1918,62 @@ impl TuiApp { .borders(Borders::ALL) .style(Style::default().fg(Color::Cyan)); - let paragraph = Paragraph::new(display_text).block(block); + let paragraph = Paragraph::new(colorize_log(&display_text)).block(block); f.render_widget(paragraph, modal_area); } } +/// Last `max_bytes` of a log file, as text. +/// +/// Reads bytes rather than a `String`: seeking to a byte offset can land +/// mid-codepoint, and a single non-UTF-8 byte anywhere in the file would make +/// `read_to_string` fail — leaving the viewer blank with no explanation. +fn tail_file(path: &str, max_bytes: u64) -> String { + use std::io::{Read, Seek, SeekFrom}; + let mut file = match std::fs::File::open(path) { + Ok(f) => f, + Err(e) => return format!("Cannot read {path}: {e}"), + }; + let len = file.metadata().map(|m| m.len()).unwrap_or(0); + let truncated = len > max_bytes; + if truncated { + let _ = file.seek(SeekFrom::Start(len - max_bytes)); + } + let mut bytes = Vec::new(); + if let Err(e) = file.read_to_end(&mut bytes) { + return format!("Cannot read {path}: {e}"); + } + let mut text = String::from_utf8_lossy(&bytes).into_owned(); + if truncated { + // Drop the partial first line left by the seek. + match text.find('\n') { + Some(i) => text.replace_range(..=i, ""), + None => text.clear(), + } + } + text +} + +fn colorize_log(text: &str) -> ratatui::text::Text<'static> { + use ratatui::text::{Line, Span, Text}; + let lines: Vec = text + .lines() + .map(|line| { + let lower = line.to_ascii_lowercase(); + let style = if lower.contains("error") || lower.contains("fatal") { + Style::default().fg(theme::DANGER) + } else if lower.contains("warn") { + Style::default().fg(theme::WARN) + } else { + Style::default().fg(theme::FG) + }; + Line::from(Span::styled(line.to_string(), style)) + }) + .collect(); + Text::from(lines) +} + /// Copy `text` to the terminal clipboard via the OSC52 escape sequence. Works /// locally and over SSH without any display server or extra dependency. Note: /// tmux requires `set -g set-clipboard on` to forward the sequence. @@ -1693,4 +2102,50 @@ proxy_response_status_codes_total{code=\"502\"} 6 assert_eq!(snap.requests_total, 0); assert_eq!(snap.avg_response_time_ms, 0.0); } + + #[test] + fn max_offset_keeps_the_last_page_full() { + // 100 rows in a 20-row viewport: the last page starts at 80, not 99. + assert_eq!(max_offset(100, 20), 80); + // Everything fits: never scroll. + assert_eq!(max_offset(5, 20), 0); + assert_eq!(max_offset(0, 20), 0); + // A degenerate viewport must not divide by / subtract zero. + assert_eq!(max_offset(10, 0), 9); + } + + #[test] + fn tail_file_reports_missing_and_unreadable_files() { + let text = tail_file("/definitely/not/a/log/file.log", 1024); + assert!(text.starts_with("Cannot read"), "{text}"); + } + + #[test] + fn tail_file_survives_non_utf8_bytes() { + let dir = std::env::temp_dir().join("soli-tail-test"); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("bad.log"); + // An invalid UTF-8 byte used to make read_to_string fail, leaving the + // log viewer blank with no indication why. + std::fs::write(&path, b"first line\n\xffsecond line\n").unwrap(); + + let text = tail_file(path.to_str().unwrap(), 1024); + assert!(text.contains("first line"), "{text}"); + assert!(text.contains("second line"), "{text}"); + let _ = std::fs::remove_file(&path); + } + + #[test] + fn tail_file_drops_the_partial_first_line() { + let dir = std::env::temp_dir().join("soli-tail-test"); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("long.log"); + std::fs::write(&path, "aaaaaaaaaa\nbbbbbbbbbb\ncccccccccc\n").unwrap(); + + // 16 bytes back lands mid-way through the "bbb" line. + let text = tail_file(path.to_str().unwrap(), 16); + assert!(!text.contains("aaaa"), "{text}"); + assert!(text.starts_with("cccccccccc"), "{text}"); + let _ = std::fs::remove_file(&path); + } } diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 4db5ee2..57d6fc3 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -2,6 +2,7 @@ pub mod app; pub mod errors; pub mod route_form; pub mod screens; +pub mod theme; use anyhow::Result; use std::sync::Arc; @@ -65,63 +66,131 @@ impl TuiContext { } } -pub fn authenticate(ctx: &TuiContext) -> Result { - use std::io::{self, Write}; +pub fn authenticate(ctx: &TuiContext, terminal: &mut ratatui::DefaultTerminal) -> Result { + use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers}; + use ratatui::{ + layout::Alignment, + style::{Color, Style}, + widgets::{Block, Borders, Paragraph}, + }; + use std::time::Duration; if !ctx.auth_required { return Ok(true); } let cfg = ctx.config_manager.get_config(); - let username = cfg.admin.username.as_deref().unwrap_or("admin"); - - let mut attempts = 0; - let max_attempts = 3; - - while attempts < max_attempts { - print!("\x1b[2J\x1b[H"); - println!("Soli Proxy TUI - Authentication Required"); - println!("========================================"); - println!("Username: {}", username); - print!("Password: "); - io::stdout().flush()?; + let username = cfg.admin.username.as_deref().unwrap_or("admin").to_string(); + let mut password = String::new(); + let mut attempts = 0u32; + let max_attempts: u32 = 3; + let mut error: Option = None; - let password = rpassword::read_password()?; - - if ctx.verify_password(&password) { - println!("\nAuthentication successful!"); - std::thread::sleep(std::time::Duration::from_millis(500)); - return Ok(true); + loop { + terminal.draw(|f| { + let area = theme::centered_modal(f.area(), 52, 11); + let block = Block::default() + .title(" Soli Proxy ") + .borders(Borders::ALL) + .border_style(Style::default().fg(theme::ACCENT)); + f.render_widget(block, area); + let inner = theme::inner(area); + let masked: String = "•".repeat(password.len()); + let remaining = max_attempts.saturating_sub(attempts); + let body = format!( + "\n Sign in as {username}\n\n Password: {masked}_\n\n {}\n {} attempt(s) left · Esc to quit", + error.as_deref().unwrap_or(""), + remaining + ); + f.render_widget( + Paragraph::new(body) + .style(Style::default().fg(Color::White)) + .alignment(Alignment::Left), + inner, + ); + })?; + + if crossterm::event::poll(Duration::from_millis(100))? { + match crossterm::event::read()? { + Event::Key(key) if key.kind == KeyEventKind::Press => { + // Raw mode is on, so Ctrl+C is ours to honour. + let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); + if ctrl && matches!(key.code, KeyCode::Char('c') | KeyCode::Char('d')) { + return Ok(false); + } + match key.code { + // Esc quits on an empty field, clears it otherwise. Never + // bind a printable character here: it would make every + // password starting with that character unenterable. + KeyCode::Esc if password.is_empty() => return Ok(false), + KeyCode::Esc => { + password.clear(); + error = None; + } + KeyCode::Enter => { + if ctx.verify_password(&password) { + return Ok(true); + } + attempts += 1; + password.clear(); + if attempts >= max_attempts { + return Ok(false); + } + error = + Some(format!("Wrong password. {} left.", max_attempts - attempts)); + } + KeyCode::Char(c) if !ctrl => { + password.push(c); + error = None; + } + KeyCode::Backspace => { + password.pop(); + } + _ => {} + } + } + _ => {} + } } - - attempts += 1; - println!( - "\nAuthentication failed. {} attempt(s) remaining.", - max_attempts - attempts - ); - std::thread::sleep(std::time::Duration::from_secs(1)); } +} - println!("Maximum attempts reached. Exiting."); - Ok(false) +/// Button-press/release + wheel reporting in SGR encoding. +/// +/// Deliberately *not* crossterm's `EnableMouseCapture`, which also turns on +/// `?1002`/`?1003` (drag and any-motion tracking). Those flood the event loop +/// with a redraw for every pixel of pointer movement, and the wider capture +/// takes over the terminal's own text selection — which matters here because +/// the Errors screen's copy path is OSC 52 with drag-select as the fallback. +const MOUSE_ON: &str = "\x1b[?1000h\x1b[?1006h"; +const MOUSE_OFF: &str = "\x1b[?1006l\x1b[?1000l"; + +fn set_mouse_reporting(on: bool) { + use std::io::Write; + let mut out = std::io::stdout(); + let _ = out.write_all(if on { MOUSE_ON } else { MOUSE_OFF }.as_bytes()); + let _ = out.flush(); } pub fn run_tui(ctx: TuiContext) -> Result<()> { - if !authenticate(&ctx)? { - anyhow::bail!("Authentication failed"); - } - - // Install panic hook to restore terminal on panic let original_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |panic_info| { + set_mouse_reporting(false); ratatui::restore(); original_hook(panic_info); })); let mut terminal = ratatui::init(); - let result = run_tui_loop(&mut terminal, ctx); + set_mouse_reporting(true); + + let result = (|| { + if !authenticate(&ctx, &mut terminal)? { + anyhow::bail!("Authentication failed"); + } + run_tui_loop(&mut terminal, ctx) + })(); - // Always restore the terminal + set_mouse_reporting(false); ratatui::restore(); result @@ -133,35 +202,48 @@ fn run_tui_loop(terminal: &mut ratatui::DefaultTerminal, ctx: TuiContext) -> Res let mut app = app::TuiApp::new(ctx); terminal.draw(|f| app.render(f))?; - let tick_rate = Duration::from_secs(2); + let metrics_tick = Duration::from_secs(1); + let poll_timeout = Duration::from_millis(100); let mut last_tick = Instant::now(); loop { - // Poll more frequently when a background action is running - let poll_timeout = if app.has_pending_action() { - Duration::from_millis(200) - } else { - tick_rate.saturating_sub(last_tick.elapsed()) - }; + // Only repaint when something actually moved. The loop wakes ten times a + // second to animate the spinner and expire toasts; redrawing on every + // wake (and on every mouse event) burns CPU for an identical frame. + let mut dirty = false; if crossterm::event::poll(poll_timeout)? { - if let crossterm::event::Event::Key(key) = crossterm::event::read()? { - if app.handle_key(key) { - break; + match crossterm::event::read()? { + crossterm::event::Event::Key(key) => { + if app.handle_key(key) { + break; + } + dirty = true; + } + crossterm::event::Event::Mouse(mouse) => { + dirty |= app.handle_mouse(mouse); } + crossterm::event::Event::Resize(_, _) => dirty = true, + _ => {} } } if app.has_pending_action() { app.check_pending_action(); + dirty = true; } - if last_tick.elapsed() >= tick_rate { + dirty |= app.on_frame(); + + if last_tick.elapsed() >= metrics_tick { app.on_tick(); last_tick = Instant::now(); + dirty = true; } - terminal.draw(|f| app.render(f))?; + if dirty { + terminal.draw(|f| app.render(f))?; + } } Ok(()) diff --git a/src/tui/screens/apps.rs b/src/tui/screens/apps.rs index bad7cc4..4861fb5 100644 --- a/src/tui/screens/apps.rs +++ b/src/tui/screens/apps.rs @@ -27,11 +27,9 @@ pub fn render(f: &mut Frame, area: Rect, ctx: &TuiContext, view: &AppsView) { }; if all_apps.is_empty() { - let block = Block::default() - .title(" Applications ") - .borders(Borders::ALL); + let block = crate::tui::theme::list_block("apps"); f.render_widget(block, area); - let inner = Rect::new(area.x + 1, area.y + 1, area.width - 2, area.height - 2); + let inner = crate::tui::theme::body(area); let msg = if ctx.app_manager.is_none() { "App manager not available. Check sites/ directory." } else { @@ -69,7 +67,7 @@ pub fn render(f: &mut Frame, area: Rect, ctx: &TuiContext, view: &AppsView) { )) .borders(Borders::ALL); f.render_widget(block, area); - let inner = Rect::new(area.x + 1, area.y + 1, area.width - 2, area.height - 2); + let inner = crate::tui::theme::body(area); f.render_widget(Paragraph::new("No apps match your search."), inner); return; } @@ -109,17 +107,15 @@ fn render_app_table( scroll_offset: usize, app_stats: &HashMap, ) { - let block = Block::default() - .title(" Applications ") - .borders(Borders::ALL); + let block = crate::tui::theme::list_block("apps"); f.render_widget(block, area); - let inner = Rect::new(area.x + 1, area.y + 1, area.width - 2, area.height - 2); + let inner = crate::tui::theme::body(area); let header = Row::new(vec![ "Name", "Domain", "Status", "CPU", "Memory", "Reqs", "Errors", "Avg RT", ]) - .style(Style::default().fg(Color::Green).bold()); + .style(Style::default().fg(crate::tui::theme::ACCENT).bold()); let max_rows = inner.height.saturating_sub(1) as usize; @@ -145,11 +141,7 @@ fn render_app_table( crate::app::InstanceStatus::Failed => Color::Red, }; - let style = if is_selected { - Style::default().bg(Color::Blue).fg(Color::White) - } else { - Style::default().fg(Color::White) - }; + let style = crate::tui::theme::row_style(is_selected); let s = app_stats.get(&app.config.name); @@ -225,10 +217,7 @@ fn render_app_detail( stats: Option<&AppStats>, history: Option<&AppHistory>, ) { - let block = Block::default() - .title(format!(" {} ", app.config.name)) - .borders(Borders::ALL) - .style(Style::default().fg(Color::Cyan)); + let block = crate::tui::theme::list_block(&app.config.name); f.render_widget(block, area); let inner = Rect::new( diff --git a/src/tui/screens/circuits.rs b/src/tui/screens/circuits.rs index 5624256..45e96e7 100644 --- a/src/tui/screens/circuits.rs +++ b/src/tui/screens/circuits.rs @@ -2,19 +2,23 @@ use ratatui::{ layout::{Constraint, Rect}, prelude::Stylize, style::{Color, Style}, - widgets::{Block, Borders, Cell, Paragraph, Row, Table}, + widgets::{Cell, Paragraph, Row, Table}, Frame, }; use crate::tui::TuiContext; -pub fn render(f: &mut Frame, area: Rect, ctx: &TuiContext, selected_index: usize) { - let block = Block::default() - .title(" Circuit Breakers ") - .borders(Borders::ALL); +pub fn render( + f: &mut Frame, + area: Rect, + ctx: &TuiContext, + selected_index: usize, + scroll_offset: usize, +) { + let block = crate::tui::theme::list_block("circuits"); f.render_widget(block, area); - let inner = Rect::new(area.x + 1, area.y + 1, area.width - 2, area.height - 2); + let inner = crate::tui::theme::body(area); let states = ctx.circuit_breaker.get_states(); @@ -26,16 +30,19 @@ pub fn render(f: &mut Frame, area: Rect, ctx: &TuiContext, selected_index: usize } let header = Row::new(vec!["Target", "State", "Failures", "Successes"]) - .style(Style::default().fg(Color::Green).bold()); + .style(Style::default().fg(crate::tui::theme::ACCENT).bold()); let states_vec: Vec<(String, crate::circuit_breaker::CircuitBreakerInfo)> = states.into_iter().collect(); + let max_rows = inner.height.saturating_sub(1) as usize; let rows: Vec = states_vec .iter() + .skip(scroll_offset) + .take(max_rows) .enumerate() .map(|(idx, (url, info))| { - let is_selected = idx == selected_index; + let is_selected = scroll_offset + idx == selected_index; let state_color = match info.state.as_str() { "open" => Color::Red, @@ -43,11 +50,7 @@ pub fn render(f: &mut Frame, area: Rect, ctx: &TuiContext, selected_index: usize _ => Color::Green, }; - let style = if is_selected { - Style::default().bg(Color::Blue).fg(Color::White) - } else { - Style::default().fg(Color::White) - }; + let style = crate::tui::theme::row_style(is_selected); Row::new(vec![ Cell::from(url.as_str()).style(style), diff --git a/src/tui/screens/config_viewer.rs b/src/tui/screens/config_viewer.rs index 3f4942a..7ac7438 100644 --- a/src/tui/screens/config_viewer.rs +++ b/src/tui/screens/config_viewer.rs @@ -1,32 +1,42 @@ use ratatui::{ - layout::Rect, + layout::{Alignment, Rect}, style::Style, - widgets::{Block, Borders, Paragraph}, + widgets::Paragraph, Frame, }; -use crate::tui::TuiContext; +use crate::tui::theme; -pub fn render(f: &mut Frame, area: Rect, ctx: &TuiContext) { - let block = Block::default().title(" proxy.conf ").borders(Borders::ALL); - f.render_widget(block, area); +/// Renders `proxy.conf` from the text cached by `TuiApp` — reading the file +/// here would mean a disk hit on every frame, and the caller needs the line +/// count anyway to know how far `scroll_offset` may go. +pub fn render(f: &mut Frame, area: Rect, config_text: &str, scroll_offset: usize, lines: usize) { + f.render_widget(theme::list_block("proxy.conf"), area); - let inner = Rect::new(area.x + 1, area.y + 1, area.width - 2, area.height - 2); - - let config_path = ctx.config_manager.config_path(); - let config_content = std::fs::read_to_string(config_path) - .unwrap_or_else(|_| "Failed to read config file.".to_string()); - - let lines: Vec<&str> = config_content.lines().collect(); - let visible_lines: Vec = lines - .iter() + let inner = theme::body(area); + let visible: Vec<&str> = config_text + .lines() + .skip(scroll_offset) .take(inner.height as usize) - .map(|s| s.to_string()) .collect(); - let display_text = visible_lines.join("\n"); - let paragraph = - Paragraph::new(display_text).style(Style::default().fg(ratatui::style::Color::White)); + f.render_widget( + Paragraph::new(visible.join("\n")).style(Style::default().fg(theme::FG)), + inner, + ); - f.render_widget(paragraph, inner); + // Scroll affordance: without a cursor there is nothing else to say whether + // the view is at the top, in the middle, or at the end of the file. + if lines > inner.height as usize && inner.width > 8 { + let end = (scroll_offset + inner.height as usize).min(lines); + let hint = format!(" {}-{end} of {lines} · j/k ", scroll_offset + 1); + let w = (hint.chars().count() as u16).min(inner.width); + let hint_area = Rect::new(inner.x + inner.width.saturating_sub(w), area.y, w, 1); + f.render_widget( + Paragraph::new(hint) + .style(Style::default().fg(theme::MUTED)) + .alignment(Alignment::Right), + hint_area, + ); + } } diff --git a/src/tui/screens/dashboard.rs b/src/tui/screens/dashboard.rs index 0d93c1f..da87372 100644 --- a/src/tui/screens/dashboard.rs +++ b/src/tui/screens/dashboard.rs @@ -1,172 +1,196 @@ +use std::collections::VecDeque; + use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, prelude::Stylize, style::{Color, Style}, - widgets::{Block, Borders, Cell, Paragraph, Row, Table}, + widgets::{Cell, Paragraph, Row, Sparkline, Table}, Frame, }; use crate::metrics::MetricsSnapshot; +use crate::tui::app::DaemonStatus; +use crate::tui::theme; use crate::tui::TuiContext; -/// `remote_snap` carries traffic counters fetched from the daemon's admin -/// API; the TUI's own metrics registry is empty (separate process), so it -/// is only used as a fallback when the daemon is unreachable. -pub fn render(f: &mut Frame, area: Rect, ctx: &TuiContext, remote_snap: Option<&MetricsSnapshot>) { +/// `remote_snap` carries traffic counters fetched from the daemon's admin API. +/// The TUI runs in its own process, so its local metrics registry is always +/// empty — `status` is what decides whether the numbers mean anything. +pub fn render( + f: &mut Frame, + area: Rect, + ctx: &TuiContext, + remote_snap: Option<&MetricsSnapshot>, + status: DaemonStatus, + rps_history: &VecDeque, +) { let local_snap = ctx.metrics.snapshot(); let snap = remote_snap.unwrap_or(&local_snap); + let have_metrics = remote_snap.is_some(); + let rows = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(10), // Server + Traffic - Constraint::Length(8), // Resources + Status codes - Constraint::Min(4), // Apps / Circuits + Constraint::Length(4), + Constraint::Length(6), + Constraint::Length(6), + Constraint::Min(6), ]) .split(area); - // Row 1: Server Info | Traffic - let top_cols = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(rows[0]); + render_kpis(f, rows[0], snap, have_metrics, status, rps_history); + render_rps_and_status(f, rows[1], snap, have_metrics, rps_history); + render_meta(f, rows[2], ctx, status); - render_server_info(f, top_cols[0], ctx); - render_traffic(f, top_cols[1], snap); - - // Row 2: Resources | Status Codes - let mid_cols = Layout::default() + let bottom = Layout::default() .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(rows[1]); - - render_resources(f, mid_cols[0], ctx); - render_status_codes(f, mid_cols[1], snap); - - // Row 3: Apps quick view - render_apps_overview(f, rows[2], ctx); + .constraints([Constraint::Percentage(58), Constraint::Percentage(42)]) + .split(rows[3]); + render_apps_overview(f, bottom[0], ctx); + render_server(f, bottom[1], ctx, snap, have_metrics); } -fn render_server_info(f: &mut Frame, area: Rect, ctx: &TuiContext) { - let cfg = ctx.config_manager.get_config(); - let uptime = ctx.uptime(); - - let block = Block::default() - .title(" Server ") - .borders(Borders::ALL) - .style(Style::default().fg(Color::Cyan)); - f.render_widget(block, area); - - let inner = inner_area(area); +fn render_kpis( + f: &mut Frame, + area: Rect, + snap: &MetricsSnapshot, + have_metrics: bool, + status: DaemonStatus, + rps_history: &VecDeque, +) { + let cols = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + ]) + .split(area); - let uptime_str = format_uptime(uptime); - let tls_str = if cfg.tls.mode.is_empty() || cfg.tls.mode == "disabled" { - "disabled".to_string() - } else { - cfg.tls.mode.clone() - }; + // Without a sample the counters are not "zero", they are unknown — say + // which of the two it is rather than showing a confident 0. + if !have_metrics { + // `Ok` with no global sample means the metrics endpoint specifically + // did not answer, which `explain()` has nothing to say about. + let why = match status.explain() { + "" => "no metrics", + other => other, + }; + theme::kpi(f, cols[0], "—", why, status.color()); + theme::kpi(f, cols[1], "—", "req / s", theme::MUTED); + theme::kpi(f, cols[2], "—", "avg latency", theme::MUTED); + theme::kpi(f, cols[3], "—", "error rate", theme::MUTED); + return; + } - let admin_str = if cfg.admin.enabled.unwrap_or(true) { - cfg.admin.bind.clone() + let rps = rps_history.back().copied().unwrap_or(0); + let lat = if snap.avg_response_time_ms > 0.0 { + theme::fmt_ms(snap.avg_response_time_ms) } else { - "disabled".to_string() + "-".into() }; - - let rows = vec![ - kv_row( - "", - format!("Soli Proxy v{}", env!("CARGO_PKG_VERSION")), - Color::Green, - ), - kv_row("Listen", cfg.server.bind.clone(), Color::White), - kv_row("HTTPS", format!(":{}", cfg.server.https_port), Color::White), - kv_row("TLS", tls_str, Color::White), - kv_row("Admin", admin_str, Color::White), - kv_row( - "Auth", - if ctx.auth_required { "enabled" } else { "-" }.to_string(), - Color::White, - ), - kv_row("Uptime", uptime_str, Color::Yellow), - ]; - - let table = Table::new(rows, [Constraint::Length(10), Constraint::Min(10)]); - f.render_widget(table, inner); -} - -fn render_traffic(f: &mut Frame, area: Rect, snap: &MetricsSnapshot) { - let block = Block::default() - .title(" Traffic ") - .borders(Borders::ALL) - .style(Style::default().fg(Color::Cyan)); - f.render_widget(block, area); - - let inner = inner_area(area); - - let error_str = if snap.requests_total > 0 && snap.errors_total > 0 { + let err = if snap.requests_total > 0 && snap.errors_total > 0 { format!( - "{} ({:.2}%)", - format_number(snap.errors_total), + "{:.2}%", (snap.errors_total as f64 / snap.requests_total as f64) * 100.0 ) } else { - format_number(snap.errors_total) + "0%".into() }; - let resp_time_str = if snap.avg_response_time_ms > 0.0 { - if snap.avg_response_time_ms >= 1000.0 { - format!("{:.2} s", snap.avg_response_time_ms / 1000.0) + theme::kpi( + f, + cols[0], + &theme::fmt_num(snap.requests_total), + "requests", + theme::ACCENT, + ); + theme::kpi(f, cols[1], &rps.to_string(), "req / s", theme::SUCCESS); + theme::kpi(f, cols[2], &lat, "avg latency", theme::WARN); + theme::kpi( + f, + cols[3], + &err, + "error rate", + if snap.errors_total > 0 { + theme::DANGER } else { - format!("{:.1} ms", snap.avg_response_time_ms) - } - } else { - "-".to_string() - }; + theme::SUCCESS + }, + ); +} + +fn render_rps_and_status( + f: &mut Frame, + area: Rect, + snap: &MetricsSnapshot, + have_metrics: bool, + rps_history: &VecDeque, +) { + let cols = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(55), Constraint::Percentage(45)]) + .split(area); - let error_color = if snap.errors_total > 0 { - Color::Red + let rps_now = rps_history.back().copied().unwrap_or(0); + let spark_block = theme::list_block(&format!("live rps {rps_now}")); + if rps_history.is_empty() { + f.render_widget(spark_block, cols[0]); } else { - Color::Green - }; + let data: Vec = rps_history.iter().copied().collect(); + let max = data.iter().copied().max().unwrap_or(1).max(1); + let sparkline = Sparkline::default() + .block(spark_block) + .data(&data) + .max(max) + .style(Style::default().fg(theme::ACCENT)); + f.render_widget(sparkline, cols[0]); + } + let total = snap.status_2xx + snap.status_3xx + snap.status_4xx + snap.status_5xx; + f.render_widget(theme::list_block("http"), cols[1]); + let inner = theme::body(cols[1]); + if !have_metrics || total == 0 { + f.render_widget( + Paragraph::new("no traffic yet").style(Style::default().fg(theme::MUTED)), + inner, + ); + return; + } + let bar_width = inner.width.saturating_sub(16) as usize; let rows = vec![ - kv_row("Requests", format_number(snap.requests_total), Color::Cyan), - kv_row( - "In Flight", - format_number(snap.requests_in_flight as u64), - Color::Cyan, - ), - kv_row("Avg Resp", resp_time_str, Color::Cyan), - kv_row("Bytes In", format_bytes(snap.bytes_received), Color::Cyan), - kv_row("Bytes Out", format_bytes(snap.bytes_sent), Color::Cyan), - kv_row( - "TLS Conns", - format_number(snap.tls_connections), - Color::Cyan, + status_row("2xx", snap.status_2xx, total, bar_width, theme::SUCCESS), + status_row( + "3xx", + snap.status_3xx, + total, + bar_width, + Color::Rgb(139, 233, 253), ), - kv_row("Errors", error_str, error_color), + status_row("4xx", snap.status_4xx, total, bar_width, theme::WARN), + status_row("5xx", snap.status_5xx, total, bar_width, theme::DANGER), ]; - - let table = Table::new(rows, [Constraint::Length(12), Constraint::Min(10)]); - f.render_widget(table, inner); + f.render_widget( + Table::new( + rows, + [ + Constraint::Length(4), + Constraint::Length(8), + Constraint::Min(6), + ], + ), + inner, + ); } -fn render_resources(f: &mut Frame, area: Rect, ctx: &TuiContext) { +fn render_meta(f: &mut Frame, area: Rect, ctx: &TuiContext, status: DaemonStatus) { let cfg = ctx.config_manager.get_config(); - - let block = Block::default() - .title(" Resources ") - .borders(Borders::ALL) - .style(Style::default().fg(Color::Cyan)); - f.render_widget(block, area); - - let inner = inner_area(area); - let apps = ctx .app_manager .as_ref() .map(|m| m.list_apps_sync()) .unwrap_or_default(); - let running = apps .iter() .filter(|a| { @@ -178,203 +202,226 @@ fn render_resources(f: &mut Frame, area: Rect, ctx: &TuiContext) { matches!(inst.status, crate::app::InstanceStatus::Running) }) .count(); - - let apps_str = if apps.is_empty() { - "-".to_string() - } else { - format!("{} ({} running)", apps.len(), running) - }; - let circuits = ctx.circuit_breaker.get_states(); let open = circuits.values().filter(|s| s.state == "open").count(); let half = circuits.values().filter(|s| s.state == "half_open").count(); - let circuits_str = if circuits.is_empty() { - "none".to_string() - } else if open == 0 && half == 0 { - format!("{} (all healthy)", circuits.len()) - } else { - let mut parts = vec![format!("{} total", circuits.len())]; - if open > 0 { - parts.push(format!("{} open", open)); - } - if half > 0 { - parts.push(format!("{} half-open", half)); - } - parts.join(", ") - }; + let cols = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + ]) + .split(area); + + theme::kpi( + f, + cols[0], + &theme::fmt_uptime(ctx.uptime()), + &format!("up {} :{}", cfg.server.bind, cfg.server.https_port), + theme::ACCENT, + ); + theme::kpi( + f, + cols[1], + &format!("{running}/{}", apps.len()), + "apps running", + theme::SUCCESS, + ); + theme::kpi( + f, + cols[2], + &cfg.rules.len().to_string(), + "routes", + theme::MAGENTA, + ); - let circuits_color = if open > 0 { - Color::Red + let (cval, ccol) = if open > 0 { + (format!("{open} open"), theme::DANGER) } else if half > 0 { - Color::Yellow + (format!("{half} half-open"), theme::WARN) + } else { + (circuits.len().to_string(), theme::SUCCESS) + }; + // Circuit state is local to this process, so it stays meaningful whatever + // the admin API is doing; only annotate when that distinction matters. + let clabel = if status.is_ok() { + "circuits".to_string() } else { - Color::Green + format!("circuits · {}", status.explain()) }; + theme::kpi(f, cols[3], &cval, &clabel, ccol); +} + +/// Listener/TLS configuration plus the traffic counters that do not fit in the +/// KPI strip (bytes moved, in-flight requests, TLS connections). +fn render_server( + f: &mut Frame, + area: Rect, + ctx: &TuiContext, + snap: &MetricsSnapshot, + have_metrics: bool, +) { + let cfg = ctx.config_manager.get_config(); + f.render_widget(theme::list_block("server"), area); + let inner = theme::body(area); - let scripts_str = if cfg.global_scripts.is_empty() { + let tls = if cfg.tls.mode.is_empty() || cfg.tls.mode == "disabled" { + "disabled".to_string() + } else { + cfg.tls.mode.clone() + }; + let admin = if cfg.admin.enabled.unwrap_or(true) { + cfg.admin.bind.clone() + } else { + "disabled".to_string() + }; + let scripts = if cfg.global_scripts.is_empty() { "-".to_string() } else { cfg.global_scripts.join(", ") }; + let unknown = |s: String| if have_metrics { s } else { "—".to_string() }; let rows = vec![ - kv_row("Routes", cfg.rules.len().to_string(), Color::White), - kv_row("Apps", apps_str, Color::White), - kv_row("Scripts", scripts_str, Color::Magenta), - Row::new(vec![ - Cell::from("Circuits").style(Style::default().fg(Color::DarkGray)), - Cell::from(circuits_str).style(Style::default().fg(circuits_color)), - ]), + kv("listen", cfg.server.bind.clone(), theme::FG), + kv("https", format!(":{}", cfg.server.https_port), theme::FG), + kv("tls", tls, theme::FG), + kv("admin", admin, theme::FG), + kv( + "auth", + if ctx.auth_required { "enabled" } else { "-" }.to_string(), + theme::FG, + ), + kv("scripts", scripts, theme::MAGENTA), + kv( + "in flight", + unknown(theme::fmt_num(snap.requests_in_flight as u64)), + theme::FG, + ), + kv( + "bytes in", + unknown(theme::fmt_bytes(snap.bytes_received)), + theme::FG, + ), + kv( + "bytes out", + unknown(theme::fmt_bytes(snap.bytes_sent)), + theme::FG, + ), + kv( + "tls conns", + unknown(theme::fmt_num(snap.tls_connections)), + theme::FG, + ), + kv( + "errors", + unknown(theme::fmt_num(snap.errors_total)), + if snap.errors_total > 0 && have_metrics { + theme::DANGER + } else { + theme::FG + }, + ), ]; - let table = Table::new(rows, [Constraint::Length(10), Constraint::Min(10)]); - f.render_widget(table, inner); + f.render_widget( + Table::new(rows, [Constraint::Length(10), Constraint::Min(8)]), + inner, + ); } -fn render_status_codes(f: &mut Frame, area: Rect, snap: &MetricsSnapshot) { - let block = Block::default() - .title(" HTTP Status ") - .borders(Borders::ALL) - .style(Style::default().fg(Color::Cyan)); - f.render_widget(block, area); - - let inner = inner_area(area); - - let total = snap.status_2xx + snap.status_3xx + snap.status_4xx + snap.status_5xx; - - if total == 0 { - let text = Paragraph::new("No requests yet.").style(Style::default().fg(Color::DarkGray)); - f.render_widget(text, inner); - return; - } - - let bar_width = inner.width.saturating_sub(18) as usize; - - let rows = vec![ - status_row("2xx", snap.status_2xx, total, bar_width, Color::Green), - status_row("3xx", snap.status_3xx, total, bar_width, Color::Blue), - status_row("4xx", snap.status_4xx, total, bar_width, Color::Yellow), - status_row("5xx", snap.status_5xx, total, bar_width, Color::Red), - ]; - - let table = Table::new( - rows, - [ - Constraint::Length(5), - Constraint::Length(10), - Constraint::Min(8), - ], - ); - f.render_widget(table, inner); +fn kv(label: &str, value: String, color: Color) -> Row<'static> { + Row::new(vec![ + Cell::from(label.to_string()).style(Style::default().fg(theme::MUTED)), + Cell::from(value).style(Style::default().fg(color)), + ]) } fn render_apps_overview(f: &mut Frame, area: Rect, ctx: &TuiContext) { - let block = Block::default() - .title(" Applications ") - .borders(Borders::ALL) - .style(Style::default().fg(Color::Cyan)); - f.render_widget(block, area); - - let inner = inner_area(area); - let apps = ctx .app_manager .as_ref() .map(|m| m.list_apps_sync()) .unwrap_or_default(); + f.render_widget(theme::list_block("apps"), area); + let inner = theme::body(area); + if apps.is_empty() { - let text = - Paragraph::new("No apps discovered.").style(Style::default().fg(Color::DarkGray)); - f.render_widget(text, inner); + f.render_widget( + Paragraph::new("no apps in sites/").style(Style::default().fg(theme::MUTED)), + inner, + ); return; } - let header = Row::new(vec!["Name", "Domain", "Slot", "Status", "Port"]) - .style(Style::default().fg(Color::DarkGray).bold()); + let header = Row::new(vec!["name", "domain", "slot", "status", "port"]) + .style(Style::default().fg(theme::MUTED).bold()); let max_rows = inner.height.saturating_sub(1) as usize; + // Leave room for the overflow hint when the list does not fit. + let overflow = apps.len().saturating_sub(max_rows); + let shown = if overflow > 0 { + max_rows.saturating_sub(1) + } else { + max_rows + }; - let rows: Vec = apps + let mut rows: Vec = apps .iter() - .take(max_rows) + .take(shown) .map(|app| { let inst = if app.current_slot == "blue" { &app.blue } else { &app.green }; - let (status_str, status_color) = match inst.status { - crate::app::InstanceStatus::Running => ("Running", Color::Green), - crate::app::InstanceStatus::Starting => ("Starting", Color::Yellow), - crate::app::InstanceStatus::Stopped => ("Stopped", Color::DarkGray), - crate::app::InstanceStatus::Unhealthy => ("Unhealthy", Color::Red), - crate::app::InstanceStatus::Failed => ("Failed", Color::Red), + crate::app::InstanceStatus::Running => ("● run", theme::SUCCESS), + crate::app::InstanceStatus::Starting => ("● start", theme::WARN), + crate::app::InstanceStatus::Stopped => ("○ stop", theme::MUTED), + crate::app::InstanceStatus::Unhealthy => ("● sick", theme::DANGER), + crate::app::InstanceStatus::Failed => ("● fail", theme::DANGER), }; - let port_str = if inst.port > 0 { format!(":{}", inst.port) } else { - "-".to_string() + "-".into() }; - Row::new(vec![ - Cell::from(app.config.name.clone()).style(Style::default().fg(Color::White)), - Cell::from(app.config.domain.clone()).style(Style::default().fg(Color::White)), - Cell::from(app.current_slot.clone()).style(Style::default().fg(Color::DarkGray)), + Cell::from(app.config.name.clone()).style(Style::default().fg(theme::FG)), + Cell::from(app.config.domain.clone()).style(Style::default().fg(theme::MUTED)), + Cell::from(app.current_slot.clone()).style(Style::default().fg(theme::MUTED)), Cell::from(status_str).style(Style::default().fg(status_color)), - Cell::from(port_str).style(Style::default().fg(Color::DarkGray)), + Cell::from(port_str).style(Style::default().fg(theme::MUTED)), ]) }) .collect(); - let overflow = apps.len().saturating_sub(max_rows); - - let table = Table::new( - std::iter::once(header).chain(rows), - [ - Constraint::Percentage(20), - Constraint::Percentage(30), - Constraint::Length(8), - Constraint::Length(10), - Constraint::Length(8), - ], - ) - .column_spacing(1); - - f.render_widget(table, inner); - if overflow > 0 { - let hint = format!(" +{} more (see Apps tab) ", overflow); - let hint_len = hint.len() as u16; - if area.width > hint_len + 2 { - let hint_area = Rect::new(area.x + area.width - hint_len - 1, area.y, hint_len, 1); - f.render_widget( - Paragraph::new(hint).style(Style::default().fg(Color::DarkGray)), - hint_area, - ); - } + rows.push(Row::new(vec![Cell::from(format!( + "+{overflow} more — press 3" + )) + .style(Style::default().fg(theme::MUTED))])); } -} - -// ── helpers ──────────────────────────────────────────── -fn inner_area(area: Rect) -> Rect { - Rect::new( - area.x + 2, - area.y + 1, - area.width.saturating_sub(4), - area.height.saturating_sub(2), - ) -} - -fn kv_row(label: &str, value: String, value_color: Color) -> Row<'static> { - Row::new(vec![ - Cell::from(label.to_string()).style(Style::default().fg(Color::DarkGray)), - Cell::from(value).style(Style::default().fg(value_color)), - ]) + f.render_widget( + Table::new( + std::iter::once(header).chain(rows), + [ + Constraint::Percentage(20), + Constraint::Percentage(35), + Constraint::Length(8), + Constraint::Length(10), + Constraint::Length(8), + ], + ) + .column_spacing(1), + inner, + ); } fn status_row(label: &str, count: u64, total: u64, bar_width: usize, color: Color) -> Row<'static> { @@ -384,55 +431,10 @@ fn status_row(label: &str, count: u64, total: u64, bar_width: usize, color: Colo 0.0 }; let filled = ((pct / 100.0) * bar_width as f64).round() as usize; - let bar: String = "\u{2588}".repeat(filled); - + let bar: String = "█".repeat(filled); Row::new(vec![ Cell::from(label.to_string()).style(Style::default().fg(color).bold()), - Cell::from(format_number(count)).style(Style::default().fg(Color::White)), + Cell::from(theme::fmt_num(count)).style(Style::default().fg(theme::FG)), Cell::from(bar).style(Style::default().fg(color)), ]) } - -fn format_bytes(bytes: u64) -> String { - const KB: u64 = 1024; - const MB: u64 = KB * 1024; - const GB: u64 = MB * 1024; - - if bytes >= GB { - format!("{:.2} GB", bytes as f64 / GB as f64) - } else if bytes >= MB { - format!("{:.2} MB", bytes as f64 / MB as f64) - } else if bytes >= KB { - format!("{:.2} KB", bytes as f64 / KB as f64) - } else { - format!("{} B", bytes) - } -} - -fn format_number(n: u64) -> String { - if n >= 1_000_000 { - format!("{:.1}M", n as f64 / 1_000_000.0) - } else if n >= 10_000 { - format!("{:.1}K", n as f64 / 1_000.0) - } else { - n.to_string() - } -} - -fn format_uptime(uptime: std::time::Duration) -> String { - let secs = uptime.as_secs(); - let days = secs / 86400; - let hours = (secs % 86400) / 3600; - let mins = (secs % 3600) / 60; - let s = secs % 60; - - if days > 0 { - format!("{}d {}h {}m", days, hours, mins) - } else if hours > 0 { - format!("{}h {}m {}s", hours, mins, s) - } else if mins > 0 { - format!("{}m {}s", mins, s) - } else { - format!("{}s", s) - } -} diff --git a/src/tui/screens/errors.rs b/src/tui/screens/errors.rs index 44e1c19..18a5b07 100644 --- a/src/tui/screens/errors.rs +++ b/src/tui/screens/errors.rs @@ -2,7 +2,7 @@ use ratatui::{ layout::{Constraint, Rect}, prelude::Stylize, style::{Color, Style}, - widgets::{Block, Borders, Cell, Paragraph, Row, Table}, + widgets::{Cell, Paragraph, Row, Table}, Frame, }; @@ -15,12 +15,10 @@ pub fn render( selected_index: usize, scroll_offset: usize, ) { - let block = Block::default() - .title(format!(" Request Errors ({}) ", entries.len())) - .borders(Borders::ALL); + let block = crate::tui::theme::list_block(&format!("errors {}", entries.len())); f.render_widget(block, area); - let inner = Rect::new(area.x + 1, area.y + 1, area.width - 2, area.height - 2); + let inner = crate::tui::theme::body(area); if entries.is_empty() { let msg = Paragraph::new( @@ -33,7 +31,7 @@ pub fn render( } let header = Row::new(vec!["Time", "Status", "Method", "Host", "Path"]) - .style(Style::default().fg(Color::Green).bold()); + .style(Style::default().fg(crate::tui::theme::ACCENT).bold()); let max_rows = inner.height.saturating_sub(1) as usize; @@ -46,11 +44,7 @@ pub fn render( let visual_idx = scroll_offset + idx; let is_selected = visual_idx == selected_index; - let style = if is_selected { - Style::default().bg(Color::Blue).fg(Color::White) - } else { - Style::default().fg(Color::White) - }; + let style = crate::tui::theme::row_style(is_selected); // 5xx and connect failures are always red unless the row is selected. let status_style = if is_selected { style diff --git a/src/tui/screens/routes.rs b/src/tui/screens/routes.rs index 23b5663..f8dcde5 100644 --- a/src/tui/screens/routes.rs +++ b/src/tui/screens/routes.rs @@ -13,59 +13,40 @@ pub fn render( area: Rect, ctx: &TuiContext, selected_index: usize, + scroll_offset: usize, search_query: &str, ) { let rules = ctx.config_manager.get_config().rules.clone(); - let block = Block::default().title(" Routes ").borders(Borders::ALL); + let block = crate::tui::theme::list_block("routes"); f.render_widget(block, area); - let inner = Rect::new(area.x + 1, area.y + 1, area.width - 2, area.height - 2); + let inner = crate::tui::theme::body(area); if rules.is_empty() { - let text = Paragraph::new("No routes configured. Press 'a' to add a route."); + let text = Paragraph::new("No routes configured. Press a to add a route.") + .style(Style::default().fg(Color::DarkGray)); f.render_widget(text, inner); return; } let header = Row::new(vec!["#", "Matcher", "Targets", "Auth", "Scripts", "LB"]) - .style(Style::default().fg(Color::Green).bold()); + .style(Style::default().fg(crate::tui::theme::ACCENT).bold()); - let filtered_rules: Vec<(usize, &crate::config::ProxyRule)> = rules - .iter() - .enumerate() - .filter(|(idx, rule)| { - if search_query.is_empty() { - return true; - } - let search_lower = search_query.to_lowercase(); - let matcher_str = format_matcher(&rule.matcher).to_lowercase(); - let targets_str: String = rule - .targets - .iter() - .map(|t| t.url.to_string()) - .collect::>() - .join(", "); - let auth_str: String = rule - .auth - .iter() - .map(|a| a.username.as_str()) - .collect::>() - .join(", "); - let scripts_str = rule.scripts.join(", "); - matcher_str.contains(&search_lower) - || targets_str.to_lowercase().contains(&search_lower) - || auth_str.to_lowercase().contains(&search_lower) - || scripts_str.to_lowercase().contains(&search_lower) - || idx.to_string() == search_lower - }) - .collect(); + let filtered_rules: Vec<(usize, &crate::config::ProxyRule)> = + filter_indices(&rules, search_query) + .into_iter() + .map(|idx| (idx, &rules[idx])) + .collect(); + let max_rows = inner.height.saturating_sub(1) as usize; let rows: Vec = filtered_rules .iter() + .skip(scroll_offset) + .take(max_rows) .enumerate() .map(|(display_idx, (idx, rule))| { - let is_selected = display_idx == selected_index; + let is_selected = scroll_offset + display_idx == selected_index; let matcher_str = format_matcher(&rule.matcher); let targets_str: String = rule .targets @@ -94,11 +75,7 @@ pub fn render( format!("{} +{}", rule.scripts[0], rule.scripts.len() - 1) }; - let style = if is_selected { - Style::default().bg(Color::Blue).fg(Color::White) - } else { - Style::default().fg(Color::White) - }; + let style = crate::tui::theme::row_style(is_selected); let auth_style = if is_selected { style @@ -171,3 +148,44 @@ fn format_lb(lb: &crate::config::LoadBalancingStrategy) -> String { crate::config::LoadBalancingStrategy::Failover => "failover".to_string(), } } + +/// Indices of the rules matching `search_query`, in display order. +/// +/// Shared with `TuiApp::resolve_route_index` and `get_max_selection` so that a +/// row's position on screen always maps back to the same rule. These used to be +/// two separate predicates — this one matched on `format_matcher`, the other on +/// the matcher's `Debug` output — which meant a search could line up rows on +/// screen with a different rule than `d` would delete. +pub fn filter_indices(rules: &[crate::config::ProxyRule], search_query: &str) -> Vec { + if search_query.is_empty() { + return (0..rules.len()).collect(); + } + let needle = search_query.to_lowercase(); + rules + .iter() + .enumerate() + .filter(|(idx, rule)| { + let targets_str: String = rule + .targets + .iter() + .map(|t| t.url.to_string()) + .collect::>() + .join(", "); + let auth_str: String = rule + .auth + .iter() + .map(|a| a.username.as_str()) + .collect::>() + .join(", "); + let scripts_str = rule.scripts.join(", "); + format_matcher(&rule.matcher) + .to_lowercase() + .contains(&needle) + || targets_str.to_lowercase().contains(&needle) + || auth_str.to_lowercase().contains(&needle) + || scripts_str.to_lowercase().contains(&needle) + || idx.to_string() == needle + }) + .map(|(idx, _)| idx) + .collect() +} diff --git a/src/tui/theme.rs b/src/tui/theme.rs new file mode 100644 index 0000000..c2944be --- /dev/null +++ b/src/tui/theme.rs @@ -0,0 +1,300 @@ +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Clear, Paragraph}, + Frame, +}; + +/// Mint-on-ink palette — distinct from the old default-cyan boxes. +pub const ACCENT: Color = Color::Rgb(0, 212, 170); +pub const ACCENT_DIM: Color = Color::Rgb(0, 120, 100); +pub const SUCCESS: Color = Color::Rgb(80, 250, 123); +pub const WARN: Color = Color::Rgb(255, 184, 108); +pub const DANGER: Color = Color::Rgb(255, 85, 85); +pub const MUTED: Color = Color::Rgb(98, 114, 164); +pub const FG: Color = Color::Rgb(248, 248, 242); +pub const SELECT_BG: Color = Color::Rgb(15, 55, 52); +pub const SIDEBAR_BG: Color = Color::Rgb(18, 22, 28); +pub const INK: Color = Color::Rgb(10, 12, 16); +pub const MAGENTA: Color = Color::Rgb(189, 147, 249); + +pub const SIDEBAR_WIDTH: u16 = 16; + +pub const SCREEN_SHORT: [&str; 6] = ["dash", "routes", "apps", "circuits", "errors", "config"]; + +/// Rows between the top of the sidebar and the first nav entry (brand block). +const NAV_TOP_OFFSET: u16 = 3; + +pub fn selected_style() -> Style { + Style::default() + .bg(SELECT_BG) + .fg(FG) + .add_modifier(Modifier::BOLD) +} + +pub fn row_style(selected: bool) -> Style { + if selected { + selected_style() + } else { + Style::default().fg(FG) + } +} + +/// Title chip, no wrapping cyan box — the old UI was "everything in a cyan frame". +pub fn list_block(title: &str) -> Block<'static> { + Block::default() + .title(Span::styled( + format!(" {title} "), + Style::default() + .fg(INK) + .bg(ACCENT) + .add_modifier(Modifier::BOLD), + )) + .borders(Borders::LEFT) + .border_style(Style::default().fg(ACCENT_DIM)) +} + +/// Content area of a full `Borders::ALL` box. +pub fn inner(area: Rect) -> Rect { + Rect::new( + area.x.saturating_add(1), + area.y.saturating_add(1), + area.width.saturating_sub(2), + area.height.saturating_sub(2), + ) +} + +/// Content area of a [`list_block`]: one column for the left rule, one row for +/// the title chip. Nothing is drawn on the right or bottom edge, so unlike +/// [`inner`] this keeps those cells. +pub fn body(area: Rect) -> Rect { + Rect::new( + area.x.saturating_add(1), + area.y.saturating_add(1), + area.width.saturating_sub(1), + area.height.saturating_sub(1), + ) +} + +pub fn centered_modal(area: Rect, width: u16, height: u16) -> Rect { + let w = width.min(area.width.saturating_sub(2).max(1)); + let h = height.min(area.height.saturating_sub(2).max(1)); + let x = (area.width.saturating_sub(w)) / 2; + let y = (area.height.saturating_sub(h)) / 2; + Rect::new(area.x + x, area.y + y, w, h) +} + +pub fn split_shell(area: Rect) -> (Rect, Rect) { + let cols = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Length(SIDEBAR_WIDTH), Constraint::Min(10)]) + .split(area); + (cols[0], cols[1]) +} + +pub fn render_sidebar(f: &mut Frame, area: Rect, current_idx: usize, version: &str) { + f.render_widget( + Block::default().style(Style::default().bg(SIDEBAR_BG)), + area, + ); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(NAV_TOP_OFFSET), + Constraint::Length(SCREEN_SHORT.len() as u16 + 2), + Constraint::Min(0), + Constraint::Length(2), + ]) + .split(area); + + let brand = Paragraph::new(vec![ + Line::from(Span::styled( + " SOLI", + Style::default() + .fg(INK) + .bg(ACCENT) + .add_modifier(Modifier::BOLD), + )), + Line::from(Span::styled( + " proxy", + Style::default().fg(MUTED).bg(SIDEBAR_BG), + )), + ]); + f.render_widget(brand, chunks[0]); + + let mut lines = Vec::new(); + for (i, short) in SCREEN_SHORT.iter().enumerate() { + let active = i == current_idx; + let marker = if active { "▸" } else { " " }; + let label = format!(" {marker} {} {short:<8}", i + 1); + let style = if active { + Style::default() + .fg(ACCENT) + .bg(SELECT_BG) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(MUTED).bg(SIDEBAR_BG) + }; + lines.push(Line::from(Span::styled(label, style))); + } + f.render_widget(Paragraph::new(lines), chunks[1]); + + let foot = Paragraph::new(vec![ + Line::from(Span::styled( + format!(" v{version}"), + Style::default().fg(MUTED).bg(SIDEBAR_BG), + )), + Line::from(Span::styled( + " 1-6 ?", + Style::default().fg(ACCENT_DIM).bg(SIDEBAR_BG), + )), + ]); + f.render_widget(foot, chunks[3]); +} + +/// Nav item under the brand block, or `None` when the click misses the list. +pub fn nav_at(area: Rect, col: u16, row: u16) -> Option { + if col < area.x || col >= area.x.saturating_add(area.width) { + return None; + } + if row >= area.y.saturating_add(area.height) { + return None; + } + let first = area.y.saturating_add(NAV_TOP_OFFSET); + if row < first { + return None; + } + let idx = (row - first) as usize; + if idx < SCREEN_SHORT.len() { + Some(idx) + } else { + None + } +} + +pub fn spinner_frame(ticks: u64) -> char { + const FRAMES: [char; 4] = ['⠋', '⠙', '⠹', '⠸']; + FRAMES[(ticks as usize) % FRAMES.len()] +} + +pub fn fmt_bytes(bytes: u64) -> String { + const KB: u64 = 1024; + const MB: u64 = KB * 1024; + const GB: u64 = MB * 1024; + if bytes >= GB { + format!("{:.2} GB", bytes as f64 / GB as f64) + } else if bytes >= MB { + format!("{:.2} MB", bytes as f64 / MB as f64) + } else if bytes >= KB { + format!("{:.2} KB", bytes as f64 / KB as f64) + } else { + format!("{bytes} B") + } +} + +pub fn fmt_num(n: u64) -> String { + if n >= 1_000_000 { + format!("{:.1}M", n as f64 / 1_000_000.0) + } else if n >= 10_000 { + format!("{:.1}K", n as f64 / 1_000.0) + } else { + n.to_string() + } +} + +pub fn fmt_ms(ms: f64) -> String { + if ms >= 1000.0 { + format!("{:.2}s", ms / 1000.0) + } else if ms >= 1.0 { + format!("{:.1}ms", ms) + } else if ms > 0.0 { + format!("{:.0}us", ms * 1000.0) + } else { + "-".to_string() + } +} + +pub fn fmt_uptime(uptime: std::time::Duration) -> String { + let secs = uptime.as_secs(); + let days = secs / 86400; + let hours = (secs % 86400) / 3600; + let mins = (secs % 3600) / 60; + let s = secs % 60; + if days > 0 { + format!("{days}d {hours}h {mins}m") + } else if hours > 0 { + format!("{hours}h {mins}m {s}s") + } else if mins > 0 { + format!("{mins}m {s}s") + } else { + format!("{s}s") + } +} + +pub fn rps_from_delta(prev: u64, next: u64, elapsed_secs: f64) -> u64 { + if elapsed_secs <= 0.0 || next < prev { + return 0; + } + ((next - prev) as f64 / elapsed_secs).round() as u64 +} + +pub fn render_toast(f: &mut Frame, area: Rect, message: &str) { + if area.height == 0 || message.is_empty() { + return; + } + f.render_widget(Clear, area); + let para = Paragraph::new(format!(" {message} ")) + .style( + Style::default() + .fg(INK) + .bg(ACCENT) + .add_modifier(Modifier::BOLD), + ) + .alignment(Alignment::Center); + f.render_widget(para, area); +} + +pub fn kpi(f: &mut Frame, area: Rect, value: &str, label: &str, color: Color) { + let block = Block::default() + .borders(Borders::LEFT) + .border_style(Style::default().fg(color)); + let inner = Rect::new( + area.x.saturating_add(2), + area.y, + area.width.saturating_sub(2), + area.height, + ); + f.render_widget(block, area); + let lines = vec![ + Line::from(Span::styled( + value, + Style::default().fg(color).add_modifier(Modifier::BOLD), + )), + Line::from(Span::styled(label, Style::default().fg(MUTED))), + ]; + f.render_widget(Paragraph::new(lines), inner); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rps_from_delta_basic() { + assert_eq!(rps_from_delta(100, 200, 1.0), 100); + assert_eq!(rps_from_delta(100, 100, 1.0), 0); + assert_eq!(rps_from_delta(200, 100, 1.0), 0); + assert_eq!(rps_from_delta(0, 50, 2.0), 25); + } + + #[test] + fn nav_at_picks_item() { + let area = Rect::new(0, 0, 16, 24); + assert_eq!(nav_at(area, 1, 3), Some(0)); + assert_eq!(nav_at(area, 1, 5), Some(2)); + assert_eq!(nav_at(area, 1, 2), None); + assert_eq!(nav_at(area, 20, 3), None); + } +} From a6367bc8f584149e56d1aca541bc72f23a15f7cd Mon Sep 17 00:00:00 2001 From: BONNAURE Olivier Date: Sun, 23 Aug 2026 18:03:35 +0200 Subject: [PATCH 2/2] fix(admin): box the large Err variant in resolve_admin_port MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cargo clippy -- -D warnings` failed on `result_large_err`: the function returns a `u16` on success against a `Response` (128+ bytes) on failure, so every call paid for the error variant. Pre-existing — main fails the same lint. It surfaced now because CI pins `dtolnay/rust-toolchain@stable`, which has moved past the clippy release the last green run used, while mise.toml pins 1.93.0 locally. Every caller immediately returns the error as its own response, so boxing it costs an allocation only on the error path. Co-Authored-By: Claude Opus 5 (1M context) --- src/admin/mod.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/admin/mod.rs b/src/admin/mod.rs index 2590f3b..ef2f5f7 100644 --- a/src/admin/mod.rs +++ b/src/admin/mod.rs @@ -390,7 +390,7 @@ async fn proxy_to_admin_app( let port = match resolve_admin_port(state).await { Ok(p) => p, - Err(resp) => return resp, + Err(resp) => return *resp, }; let path = req.uri().path(); @@ -454,15 +454,23 @@ async fn proxy_to_admin_app( } /// Resolve the _admin app's backend port, or return an error response. -async fn resolve_admin_port(state: &Arc) -> Result> { +/// +/// The error is boxed because `Response` dwarfs the `u16` success +/// value, and every caller immediately returns it as its own response. +async fn resolve_admin_port(state: &Arc) -> Result>> { let app_manager = match &state.app_manager { Some(m) => m, - None => return Err(error_response(501, "App management not configured")), + None => { + return Err(Box::new(error_response( + 501, + "App management not configured", + ))) + } }; let app = match app_manager.get_app("_admin").await { Some(a) => a, - None => return Err(error_response(502, "_admin app not found")), + None => return Err(Box::new(error_response(502, "_admin app not found"))), }; let port = if app.current_slot == "blue" { @@ -472,7 +480,7 @@ async fn resolve_admin_port(state: &Arc) -> Result Response { let port = match resolve_admin_port(state).await { Ok(p) => p, - Err(resp) => return resp, + Err(resp) => return *resp, }; let path = req.uri().path().to_string();