From c3fa73d5a06f88e908cae85620215fa2c4a69e5e Mon Sep 17 00:00:00 2001 From: Sanaei Date: Wed, 29 Jul 2026 20:17:37 +0200 Subject: [PATCH] feat(ui): redesign the overview page as a trend-first command deck Replace the ten-small-cards overview with an action bar, four vitals tiles carrying 72-sample sparklines seeded from /server/history, a two-series throughput chart, a TCP/UDP connections chart, and a grouped system strip (uptime xray|os, panel ram|threads, ip addresses). StatusCard and XrayStatusCard are deleted; every modal stays reachable from the action bar, the Xray error message moves into a tooltip on the state pill, and the panel version text keeps opening the update modal (the dev-channel switch lives there) even when no update is available. Live values sit beside the upload/download and tcp/udp legends, a health sentence appears only when a vital crosses the shared warn/crit thresholds now exported from models/status, and load average is left to System History. The sidebar becomes an auto-collapsed 72px icon rail that expands as an overlay on hover: rail width, brand-row height and menu paddings are pinned so nothing shifts during the transition, the collapsed-menu tooltips are disabled, hover state survives the per-page sidebar remounts (with a matches(':hover') resync), and the manual collapse trigger is gone. Sparkline gains rgb()/rgba() support in its fill gradient, a showLegend prop so pages stop reaching into its internals, and loses a dependency-less repaint effect that doubled canvas paints. Chart tooltips show clock time via the new TimeFormatter.formatClock; accents come from theme tokens instead of status.cpu.color. Verified by screenshot at 390/800/1150/1280/1400/1600px in light and dark, en and fa-IR, plus programmatic geometry checks on the sidebar. Locale files gain 8 keys and lose 9 dead ones across all 13 languages. --- frontend/src/components/viz/Sparkline.tsx | 26 +- frontend/src/layouts/AppSidebar.css | 46 +- frontend/src/layouts/AppSidebar.tsx | 69 +-- frontend/src/models/status.ts | 11 +- frontend/src/pages/index/ConnectionsCard.tsx | 74 +++ frontend/src/pages/index/IndexPage.css | 477 ++++++++++++++++-- frontend/src/pages/index/IndexPage.tsx | 433 +++++----------- .../src/pages/index/OverviewActionBar.tsx | 163 ++++++ frontend/src/pages/index/StatusCard.css | 9 - frontend/src/pages/index/StatusCard.tsx | 115 ----- frontend/src/pages/index/SystemStrip.tsx | 97 ++++ frontend/src/pages/index/ThroughputCard.tsx | 97 ++++ frontend/src/pages/index/VitalTile.tsx | 75 +++ frontend/src/pages/index/XrayStatusCard.css | 14 - frontend/src/pages/index/XrayStatusCard.tsx | 123 ----- .../src/pages/index/useOverviewHistory.ts | 135 +++++ frontend/src/utils/index.ts | 8 + internal/web/translation/ar-EG.json | 20 +- internal/web/translation/en-US.json | 20 +- internal/web/translation/es-ES.json | 20 +- internal/web/translation/fa-IR.json | 20 +- internal/web/translation/id-ID.json | 20 +- internal/web/translation/ja-JP.json | 20 +- internal/web/translation/pt-BR.json | 20 +- internal/web/translation/ru-RU.json | 20 +- internal/web/translation/tr-TR.json | 20 +- internal/web/translation/uk-UA.json | 20 +- internal/web/translation/vi-VN.json | 20 +- internal/web/translation/zh-CN.json | 20 +- internal/web/translation/zh-TW.json | 20 +- 30 files changed, 1467 insertions(+), 765 deletions(-) create mode 100644 frontend/src/pages/index/ConnectionsCard.tsx create mode 100644 frontend/src/pages/index/OverviewActionBar.tsx delete mode 100644 frontend/src/pages/index/StatusCard.css delete mode 100644 frontend/src/pages/index/StatusCard.tsx create mode 100644 frontend/src/pages/index/SystemStrip.tsx create mode 100644 frontend/src/pages/index/ThroughputCard.tsx create mode 100644 frontend/src/pages/index/VitalTile.tsx delete mode 100644 frontend/src/pages/index/XrayStatusCard.css delete mode 100644 frontend/src/pages/index/XrayStatusCard.tsx create mode 100644 frontend/src/pages/index/useOverviewHistory.ts diff --git a/frontend/src/components/viz/Sparkline.tsx b/frontend/src/components/viz/Sparkline.tsx index 210025cb8..9be6765b2 100644 --- a/frontend/src/components/viz/Sparkline.tsx +++ b/frontend/src/components/viz/Sparkline.tsx @@ -48,6 +48,7 @@ interface SparklineProps { yTickStep?: number; tickCountX?: number; showTooltip?: boolean; + showLegend?: boolean; valueMin?: number; valueMax?: number | null; yFormatter?: (v: number) => string; @@ -80,13 +81,23 @@ interface SparklineView { extremaPoints: ExtremaResult | null; } -function hexToRgba(hex: string, alpha: number): string { - let h = hex.trim(); +function hexToRgba(color: string, alpha: number): string { + const trimmed = color.trim(); + const fn = trimmed.match(/^rgba?\(([^)]+)\)$/i); + if (fn) { + const parts = fn[1].split(/[,/]\s*|\s+/).filter(Boolean).map(Number); + if (parts.length >= 3 && parts.slice(0, 3).every((n) => Number.isFinite(n))) { + const baseAlpha = parts.length > 3 && Number.isFinite(parts[3]) ? parts[3] : 1; + return `rgba(${parts[0]}, ${parts[1]}, ${parts[2]}, ${baseAlpha * alpha})`; + } + return trimmed; + } + let h = trimmed; if (h.startsWith('#')) h = h.slice(1); if (h.length === 3) h = h.split('').map((c) => c + c).join(''); - if (h.length !== 6) return hex; + if (h.length !== 6) return trimmed; const int = Number.parseInt(h, 16); - if (Number.isNaN(int)) return hex; + if (Number.isNaN(int)) return trimmed; const r = (int >> 16) & 255; const g = (int >> 8) & 255; const b = int & 255; @@ -129,6 +140,7 @@ export default function Sparkline(props: SparklineProps) { yTickStep = 25, tickCountX = 4, showTooltip = false, + showLegend = true, valueMin = 0, valueMax = 100, yFormatter = (v: number) => `${Math.round(v)}%`, @@ -542,10 +554,6 @@ export default function Sparkline(props: SparklineProps) { ); }, [points, hasSeries2, hasSeries3, valueMin, valueMax]); - useEffect(() => { - plotRef.current?.redraw(false); - }); - useEffect(() => { const redraw = () => plotRef.current?.redraw(false); const moBody = new MutationObserver(redraw); @@ -570,7 +578,7 @@ export default function Sparkline(props: SparklineProps) { )} - {legendItems.length > 0 && ( + {showLegend && legendItems.length > 0 && (