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.
This commit is contained in:
Sanaei
2026-07-29 20:17:37 +02:00
parent 87ebcc7a6f
commit c3fa73d5a0
30 changed files with 1467 additions and 765 deletions
+17 -9
View File
@@ -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) {
</span>
</div>
)}
{legendItems.length > 0 && (
{showLegend && legendItems.length > 0 && (
<div className="sparkline-legend" aria-hidden="true">
{legendItems.map((s) => (
<span key={s.name} className="extrema-item" style={{ color: s.color }}> {s.name}</span>