feat(ui): theme-aware logo, dashboard bar graph, server-side client sorting (v0.14.1-beta)

UI/UX refinements on top of v0.14.0-beta, all validated on the test server:

- Logo: new theme-aware compass iconmark (light/dark SVGs swapped via the
  dark class), sized to 45px; favicon updated to match. Wordmark casing
  corrected to "MikroTik Manager" (font size unchanged).
- Navigation icons enlarged 15 -> 17px.
- Dashboard: "Connected clients" line chart converted to a bar chart with a
  category axis + light downsampling so bars stay readably thick across ranges.
- Clients: sorting is now server-side across the whole dataset (was sorting
  only the current page, so Type/others appeared to do nothing on a uniform
  page); table uses a fixed column layout + keepPreviousData so columns no
  longer jump or blank out when sorting/paging. Backend clients route gains
  whitelisted sort/dir params (safe numeric IP sort, deterministic tiebreaker).
- Connections tab: surfaces the live /ip/firewall/connection/tracking state
  (on/auto/off pill) and clearer empty-state copy explaining auto-mode.

Lint/typecheck/build/jest green in both workspaces; npm audit --audit-level=high
clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2GT-Rich
2026-06-18 13:37:11 -07:00
co-authored by Claude Opus 4.8
parent ee9c9ff9f7
commit d79eb13119
18 changed files with 402 additions and 93 deletions
+28 -19
View File
@@ -2,7 +2,7 @@ import { useState, useEffect, useRef, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import {
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
} from 'recharts';
import {
Users, AlertTriangle, Wifi, MapPin, ArrowUpCircle, Cpu, X,
@@ -256,6 +256,19 @@ function HealthBar({
// ─── Summary View ─────────────────────────────────────────────────────────────
// Cap the number of bars so they render as readable columns instead of hairline
// slivers on dense ranges. Strides through the series (a point-in-time client
// gauge, so sampling preserves the trend) and always keeps the latest point.
function downsample<T>(arr: T[], max: number): T[] {
if (arr.length <= max) return arr;
const step = Math.ceil(arr.length / max);
const out: T[] = [];
for (let i = 0; i < arr.length; i += step) out.push(arr[i]);
const last = arr[arr.length - 1];
if (out[out.length - 1] !== last) out.push(last);
return out;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function SummaryView(props: Record<string, any>) {
const { summary, devices, wirelessCount, clientSparkline, clientsOverTime,
@@ -282,13 +295,11 @@ function SummaryView(props: Record<string, any>) {
router: { label: 'Routers', color: 'var(--violet)' },
};
const chartDomain: [number, number] = (() => {
const ranges: Record<string, number> = { '1h': 1, '6h': 6, '24h': 24, '7d': 168 };
const hrs = ranges[chartRange] ?? 24;
// eslint-disable-next-line react-hooks/purity
const now = Date.now();
return [now - hrs * 60 * 60 * 1000, now];
})();
// Bars over a category axis fill their slots (thick, minimal gaps) rather than
// sitting as thin fixed-width marks on a continuous time axis. Downsample to
// keep them chunky even on long ranges.
const chartData = downsample(clientsOverTime as { ts: number; value: number }[], 60);
const tickEvery = Math.max(0, Math.ceil(chartData.length / 6) - 1);
return (
<div className="space-y-4">
@@ -335,16 +346,18 @@ function SummaryView(props: Record<string, any>) {
</div>
{clientsOverTime.length > 0 ? (
<ResponsiveContainer width="100%" height={180}>
<LineChart data={clientsOverTime} margin={{ top: 4, right: 4, left: -24, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--line-soft)" />
<BarChart data={chartData} margin={{ top: 4, right: 4, left: -24, bottom: 0 }} barCategoryGap="12%">
<CartesianGrid strokeDasharray="3 3" stroke="var(--line-soft)" vertical={false} />
<XAxis
dataKey="ts" type="number" scale="time" domain={chartDomain}
dataKey="ts"
tick={{ fontSize: 10, fill: 'var(--ink-4)', fontFamily: 'Geist Mono' }}
tickCount={7}
tickFormatter={(t) => format(new Date(t), 'HH:mm')}
interval={tickEvery}
minTickGap={20}
tickFormatter={(t) => format(new Date(t as number), chartRange === '7d' ? 'MMM d' : 'HH:mm')}
/>
<YAxis tick={{ fontSize: 10, fill: 'var(--ink-4)' }} allowDecimals={false} />
<Tooltip
cursor={{ fill: 'var(--surface-3)', opacity: 0.4 }}
formatter={(v: number) => [v, 'Clients']}
labelFormatter={(t) => format(new Date(t as number), 'MMM d, HH:mm')}
contentStyle={{
@@ -355,12 +368,8 @@ function SummaryView(props: Record<string, any>) {
color: 'var(--ink)',
}}
/>
<Line
type="monotone" dataKey="value"
stroke="var(--accent)" strokeWidth={1.8}
dot={false} activeDot={{ r: 4, fill: 'var(--accent)' }}
/>
</LineChart>
<Bar dataKey="value" fill="var(--accent)" radius={[3, 3, 0, 0]} />
</BarChart>
</ResponsiveContainer>
) : (
<div className="flex items-center justify-center text-[13px]" style={{ height: 180, color: 'var(--ink-4)' }}>