refactor: reduce window size and remove transfer metrics display from main UI

Reduce default window dimensions from 1120x760 to 940x640 pixels and disable resizing. Remove TunnelMetrics and RawTunnelMetrics types, formatDataSize and normalizeTunnelMetrics helpers, and all transfer statistics tracking from App component. Replace refreshTunnelMetrics with simpler refreshTunnelStatus that only queries tunnel active state. Remove received/sent data display cards from status panel and eliminate metrics
This commit is contained in:
2026-03-18 10:34:12 +01:00
parent 799bc6550e
commit 56acc96229
3 changed files with 37 additions and 96 deletions

View File

@@ -13,8 +13,8 @@
"windows": [ "windows": [
{ {
"title": "NexaVPN", "title": "NexaVPN",
"width": 1120, "width": 940,
"height": 760, "height": 640,
"resizable": false, "resizable": false,
"maximizable": false "maximizable": false
} }

View File

@@ -11,20 +11,6 @@ type EnrollmentState = {
tunnelStrategy: string; tunnelStrategy: string;
}; };
type TunnelMetrics = {
active: boolean;
rxBytes: number;
txBytes: number;
};
type RawTunnelMetrics = {
active?: boolean;
rxBytes?: number;
txBytes?: number;
rx_bytes?: number;
tx_bytes?: number;
};
function formatInvokeError(err: unknown, fallback: string) { function formatInvokeError(err: unknown, fallback: string) {
if (typeof err === "string" && err.trim().length > 0) { if (typeof err === "string" && err.trim().length > 0) {
return err; return err;
@@ -58,31 +44,6 @@ function currentProfileLabel(state: EnrollmentState | null) {
return `Split tunnel (${state.resources.length} resources)`; return `Split tunnel (${state.resources.length} resources)`;
} }
function formatDataSize(bytes: number) {
if (!bytes) {
return "0 B";
}
const units = ["B", "KB", "MB", "GB", "TB"];
let value = bytes;
let unitIndex = 0;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex += 1;
}
return `${value >= 100 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unitIndex]}`;
}
function normalizeTunnelMetrics(value: RawTunnelMetrics | null | undefined): TunnelMetrics {
return {
active: Boolean(value?.active),
rxBytes: Number(value?.rxBytes ?? value?.rx_bytes ?? 0),
txBytes: Number(value?.txBytes ?? value?.tx_bytes ?? 0)
};
}
export function App() { export function App() {
const [serverUrl, setServerUrl] = useState("http://localhost"); const [serverUrl, setServerUrl] = useState("http://localhost");
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
@@ -92,35 +53,21 @@ export function App() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [connected, setConnected] = useState(false); const [connected, setConnected] = useState(false);
const [state, setState] = useState<EnrollmentState | null>(null); const [state, setState] = useState<EnrollmentState | null>(null);
const [metrics, setMetrics] = useState<TunnelMetrics>({ async function refreshTunnelStatus() {
active: false,
rxBytes: 0,
txBytes: 0
});
async function refreshTunnelMetrics() {
try {
const value = normalizeTunnelMetrics(await invoke<RawTunnelMetrics>("tunnel_metrics"));
setMetrics(value);
setConnected(value.active);
} catch {
try { try {
const active = await invoke<boolean>("tunnel_status"); const active = await invoke<boolean>("tunnel_status");
setConnected(active); setConnected(active);
setMetrics((current) => ({ ...current, active: Boolean(active) }));
} catch { } catch {
setMetrics({ active: false, rxBytes: 0, txBytes: 0 });
setConnected(false); setConnected(false);
} }
} }
}
useEffect(() => { useEffect(() => {
void invoke<EnrollmentState | null>("load_state") void invoke<EnrollmentState | null>("load_state")
.then(async (value) => { .then(async (value) => {
if (value) { if (value) {
setState(value); setState(value);
await refreshTunnelMetrics(); await refreshTunnelStatus();
} }
}) })
.catch(() => undefined); .catch(() => undefined);
@@ -128,13 +75,13 @@ export function App() {
useEffect(() => { useEffect(() => {
if (!state) { if (!state) {
setMetrics({ active: false, rxBytes: 0, txBytes: 0 }); setConnected(false);
return undefined; return undefined;
} }
void refreshTunnelMetrics(); void refreshTunnelStatus();
const timer = window.setInterval(() => { const timer = window.setInterval(() => {
void refreshTunnelMetrics(); void refreshTunnelStatus();
}, 5000); }, 5000);
return () => window.clearInterval(timer); return () => window.clearInterval(timer);
@@ -148,12 +95,10 @@ export function App() {
const active = await invoke<boolean>("tunnel_status"); const active = await invoke<boolean>("tunnel_status");
setConnected(active); setConnected(active);
if (active === expected) { if (active === expected) {
await refreshTunnelMetrics();
return active; return active;
} }
} catch { } catch {
if (!expected) { if (!expected) {
setMetrics({ active: false, rxBytes: 0, txBytes: 0 });
setConnected(false); setConnected(false);
return false; return false;
} }
@@ -165,7 +110,6 @@ export function App() {
return invoke<boolean>("tunnel_status") return invoke<boolean>("tunnel_status")
.then(async (active) => { .then(async (active) => {
setConnected(active); setConnected(active);
await refreshTunnelMetrics();
return active; return active;
}) })
.catch(() => false); .catch(() => false);
@@ -181,7 +125,6 @@ export function App() {
payload: { serverUrl, username, password } payload: { serverUrl, username, password }
}); });
setState(result); setState(result);
setMetrics({ active: false, rxBytes: 0, txBytes: 0 });
} catch (err) { } catch (err) {
setError(formatInvokeError(err, "Enrollment failed")); setError(formatInvokeError(err, "Enrollment failed"));
} finally { } finally {
@@ -196,7 +139,7 @@ export function App() {
try { try {
const result = await invoke<EnrollmentState>("sync_profile"); const result = await invoke<EnrollmentState>("sync_profile");
setState(result); setState(result);
await refreshTunnelMetrics(); await refreshTunnelStatus();
} catch (err) { } catch (err) {
setError(formatInvokeError(err, "Profile sync failed")); setError(formatInvokeError(err, "Profile sync failed"));
} finally { } finally {
@@ -240,7 +183,6 @@ export function App() {
await invoke("clear_session"); await invoke("clear_session");
setConnected(false); setConnected(false);
setState(null); setState(null);
setMetrics({ active: false, rxBytes: 0, txBytes: 0 });
setError(null); setError(null);
} catch (err) { } catch (err) {
setError(formatInvokeError(err, "Unable to clear local profile")); setError(formatInvokeError(err, "Unable to clear local profile"));
@@ -343,14 +285,6 @@ export function App() {
<span>Last sync</span> <span>Last sync</span>
<strong>{state.lastSyncTime}</strong> <strong>{state.lastSyncTime}</strong>
</div> </div>
<div className="detail-card">
<span>Received</span>
<strong>{formatDataSize(metrics.rxBytes)}</strong>
</div>
<div className="detail-card">
<span>Sent</span>
<strong>{formatDataSize(metrics.txBytes)}</strong>
</div>
</div> </div>
</div> </div>
@@ -365,7 +299,7 @@ export function App() {
<h4>Resources</h4> <h4>Resources</h4>
</div> </div>
</div> </div>
<p>Press <strong>Sync</strong> after policy changes.</p> <p className="access-hint">Press <strong>Sync</strong> after policy changes.</p>
<ul className="resource-stack"> <ul className="resource-stack">
{(state?.resources ?? ["No resources assigned yet"]).map((resource) => ( {(state?.resources ?? ["No resources assigned yet"]).map((resource) => (
<li key={resource}>{resource}</li> <li key={resource}>{resource}</li>

View File

@@ -28,14 +28,14 @@ input {
.client-shell { .client-shell {
min-height: 100vh; min-height: 100vh;
padding: 28px; padding: 18px;
} }
.app-frame { .app-frame {
width: min(1120px, 100%); width: min(920px, 100%);
margin: 0 auto; margin: 0 auto;
display: grid; display: grid;
gap: 18px; gap: 14px;
} }
.top-strip { .top-strip {
@@ -88,7 +88,7 @@ input {
.top-actions { .top-actions {
display: flex; display: flex;
gap: 10px; gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
} }
@@ -96,7 +96,7 @@ input {
.shell-button-secondary { .shell-button-secondary {
border: 0; border: 0;
border-radius: 999px; border-radius: 999px;
padding: 12px 18px; padding: 10px 16px;
font-weight: 700; font-weight: 700;
cursor: pointer; cursor: pointer;
transition: 160ms ease; transition: 160ms ease;
@@ -130,16 +130,16 @@ input {
.body-grid { .body-grid {
display: grid; display: grid;
grid-template-columns: 1.3fr 0.7fr; grid-template-columns: minmax(0, 1fr) 290px;
gap: 22px; gap: 16px;
} }
.login-panel, .login-panel,
.status-panel { .status-panel {
border-radius: 28px; border-radius: 28px;
padding: 24px; padding: 18px;
display: grid; display: grid;
gap: 20px; gap: 14px;
} }
.status-panel > p { .status-panel > p {
@@ -178,9 +178,9 @@ input {
.surface { .surface {
border-radius: 24px; border-radius: 24px;
padding: 18px; padding: 14px;
display: grid; display: grid;
gap: 14px; gap: 12px;
} }
.surface-header { .surface-header {
@@ -199,22 +199,22 @@ input {
.profile-grid { .profile-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px; gap: 10px;
} }
.detail-card, .detail-card,
.profile-card { .profile-card {
padding: 14px; padding: 12px;
border-radius: 18px; border-radius: 16px;
background: rgba(255, 255, 255, 0.03); background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(177, 197, 229, 0.1); border: 1px solid rgba(177, 197, 229, 0.1);
display: grid; display: grid;
gap: 6px; gap: 5px;
} }
.detail-card strong, .detail-card strong,
.profile-card strong { .profile-card strong {
font-size: 1rem; font-size: 0.98rem;
word-break: break-word; word-break: break-word;
} }
@@ -223,16 +223,23 @@ input {
padding: 0; padding: 0;
list-style: none; list-style: none;
display: grid; display: grid;
gap: 10px; gap: 8px;
} }
.resource-stack li { .resource-stack li {
padding: 14px 16px; padding: 12px 14px;
border-radius: 16px; border-radius: 16px;
background: rgba(255, 255, 255, 0.03); background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(177, 197, 229, 0.1); border: 1px solid rgba(177, 197, 229, 0.1);
color: #eef4ff; color: #eef4ff;
word-break: break-word; word-break: break-word;
min-height: 54px;
display: flex;
align-items: center;
}
.access-hint {
font-size: 0.95rem;
} }
.login-panel form { .login-panel form {