From 64b4c3dfa4f501bb80020a657c4fd255a5d60222 Mon Sep 17 00:00:00 2001 From: nessi Date: Thu, 12 Feb 2026 11:37:25 +0100 Subject: [PATCH] Add easy & DBA mode --- frontend/src/App.jsx | 14 ++- frontend/src/pages/TargetDetailPage.jsx | 111 +++++++++++++++++++++++- frontend/src/state.jsx | 20 ++++- frontend/src/styles.css | 97 +++++++++++++++++++++ 4 files changed, 238 insertions(+), 4 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7dfc5df..c3048ea 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -16,7 +16,7 @@ function Protected({ children }) { } function Layout({ children }) { - const { me, logout } = useAuth(); + const { me, logout, uiMode, setUiMode } = useAuth(); const navClass = ({ isActive }) => `nav-btn${isActive ? " active" : ""}`; return ( @@ -63,6 +63,18 @@ function Layout({ children }) { )}
+
+
View Mode
+ + {uiMode === "easy" ? "Simple health guidance" : "Advanced DBA metrics"} +
{me?.email}
{me?.role}
diff --git a/frontend/src/pages/TargetDetailPage.jsx b/frontend/src/pages/TargetDetailPage.jsx index 49dc8bc..2a52ef5 100644 --- a/frontend/src/pages/TargetDetailPage.jsx +++ b/frontend/src/pages/TargetDetailPage.jsx @@ -66,7 +66,7 @@ async function loadMetric(targetId, metric, range, tokens, refresh) { export function TargetDetailPage() { const { id } = useParams(); - const { tokens, refresh } = useAuth(); + const { tokens, refresh, uiMode } = useAuth(); const [range, setRange] = useState("1h"); const [series, setSeries] = useState({}); const [locks, setLocks] = useState([]); @@ -135,6 +135,38 @@ export function TargetDetailPage() { [series] ); + const easySummary = useMemo(() => { + if (!overview) return null; + const latest = chartData[chartData.length - 1]; + const issues = []; + const warnings = []; + + if (overview.partial_failures?.length > 0) warnings.push("Some advanced metrics are currently unavailable."); + if ((overview.performance?.deadlocks || 0) > 0) issues.push("Deadlocks were detected."); + if ((overview.replication?.mode === "standby") && (overview.replication?.replay_lag_seconds || 0) > 5) { + issues.push("Replication lag is above 5 seconds."); + } + if ((latest?.cache || 0) > 0 && latest.cache < 90) warnings.push("Cache hit ratio is below 90%."); + if ((latest?.connections || 0) > 120) warnings.push("Connection count is relatively high."); + if ((locks?.length || 0) > 150) warnings.push("High number of active locks."); + + const health = issues.length > 0 ? "problem" : warnings.length > 0 ? "warning" : "ok"; + const message = + health === "ok" + ? "Everything looks healthy. No major risks detected right now." + : health === "warning" + ? "System is operational, but there are signals you should watch." + : "Attention required. Critical signals need investigation."; + + return { + health, + message, + issues, + warnings, + latest, + }; + }, [overview, chartData, locks]); + if (loading) return
Loading target detail...
; if (error) return
{error}
; @@ -148,7 +180,82 @@ export function TargetDetailPage() { Target Detail {targetMeta?.name || `#${id}`} {targetMeta?.dbname ? ` (${targetMeta.dbname})` : ""} - {overview && ( + {uiMode === "easy" && overview && easySummary && ( + <> +
+

System Health

+

{easySummary.message}

+
+ + {easySummary.health === "ok" ? "OK" : easySummary.health === "warning" ? "Warning" : "Problem"} + +
+
+ +
+
+ {formatNumber(easySummary.latest?.connections, 0)} + Current Connections +
+
+ {formatNumber(easySummary.latest?.tps, 2)} + Transactions/sec (approx) +
+
+ {formatNumber(easySummary.latest?.cache, 2)}% + Cache Hit Ratio +
+
+ +
+

Quick Explanation

+ {easySummary.issues.length === 0 && easySummary.warnings.length === 0 && ( +

No immediate problems were detected. Keep monitoring over time.

+ )} + {easySummary.issues.length > 0 && ( + <> + Problems +
    + {easySummary.issues.map((item, idx) =>
  • {item}
  • )} +
+ + )} + {easySummary.warnings.length > 0 && ( + <> + Things to watch +
    + {easySummary.warnings.map((item, idx) =>
  • {item}
  • )} +
+ + )} +
+ +
+
+

Instance Summary

+
+
Role{overview.instance.role}
+
Uptime{formatSeconds(overview.instance.uptime_seconds)}
+
Target Port{targetMeta?.port ?? "-"}
+
Current DB Size{formatBytes(overview.storage.current_database_size_bytes)}
+
Replication Clients{overview.replication.active_replication_clients ?? "-"}
+
Autovacuum Workers{overview.performance.autovacuum_workers ?? "-"}
+
+
+
+

Activity Snapshot

+
+
Running Sessions{activity.filter((a) => a.state === "active").length}
+
Total Sessions{activity.length}
+
Current Locks{locks.length}
+
Deadlocks{overview.performance.deadlocks ?? 0}
+
+
+
+ + )} + + {uiMode === "dba" && overview && (

Database Overview

diff --git a/frontend/src/state.jsx b/frontend/src/state.jsx index b2b215f..3af912c 100644 --- a/frontend/src/state.jsx +++ b/frontend/src/state.jsx @@ -2,6 +2,7 @@ import React, { createContext, useContext, useMemo, useState } from "react"; import { API_URL } from "./api"; const AuthCtx = createContext(null); +const UI_MODE_KEY = "nexapg_ui_mode"; function loadStorage() { try { @@ -11,10 +12,21 @@ function loadStorage() { } } +function loadUiMode() { + try { + const value = localStorage.getItem(UI_MODE_KEY); + if (value === "easy" || value === "dba") return value; + } catch { + // ignore storage errors + } + return "dba"; +} + export function AuthProvider({ children }) { const initial = loadStorage(); const [tokens, setTokens] = useState(initial?.tokens || null); const [me, setMe] = useState(initial?.me || null); + const [uiMode, setUiModeState] = useState(loadUiMode); const persist = (nextTokens, nextMe) => { if (nextTokens && nextMe) { @@ -78,7 +90,13 @@ export function AuthProvider({ children }) { } }; - const value = useMemo(() => ({ tokens, me, login, logout, refresh }), [tokens, me]); + const setUiMode = (nextMode) => { + const mode = nextMode === "easy" ? "easy" : "dba"; + setUiModeState(mode); + localStorage.setItem(UI_MODE_KEY, mode); + }; + + const value = useMemo(() => ({ tokens, me, login, logout, refresh, uiMode, setUiMode }), [tokens, me, uiMode]); return {children}; } diff --git a/frontend/src/styles.css b/frontend/src/styles.css index ed1a241..687fdd3 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -140,6 +140,52 @@ a { color: #d7e4fa; } +.mode-switch-block { + margin-bottom: 12px; + padding: 10px; + border: 1px solid #2a588d; + border-radius: 10px; + background: #0d2244; +} + +.mode-switch-label { + font-size: 12px; + color: #9eb8d6; + margin-bottom: 6px; +} + +.mode-toggle { + width: 100%; + display: grid; + grid-template-columns: 1fr 1fr; + padding: 4px; + border-radius: 10px; + border: 1px solid #3170ae; + background: #0b1a36; +} + +.mode-pill { + padding: 7px 6px; + border-radius: 8px; + text-align: center; + font-size: 12px; + color: #9db4d1; +} + +.mode-toggle.easy .mode-pill:first-child, +.mode-toggle.dba .mode-pill:last-child { + background: linear-gradient(180deg, #165a96, #124978); + color: #eff8ff; + font-weight: 700; +} + +.mode-switch-block small { + display: block; + margin-top: 6px; + color: #9bb5d4; + font-size: 11px; +} + .role { color: var(--muted); margin-bottom: 10px; @@ -495,6 +541,57 @@ select:-webkit-autofill { margin: 8px 0 0 18px; } +.easy-status { + border-width: 1px; +} + +.easy-status.ok { + border-color: #1d8d5c; + background: linear-gradient(180deg, #123b39, #112a31); +} + +.easy-status.warning { + border-color: #ad7f25; + background: linear-gradient(180deg, #3a3214, #2d2610); +} + +.easy-status.problem { + border-color: #b54242; + background: linear-gradient(180deg, #3f1d22, #2f1519); +} + +.easy-badge-row { + margin-top: 8px; +} + +.easy-badge { + display: inline-block; + padding: 5px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + border: 1px solid transparent; +} + +.easy-badge.ok { + color: #86efac; + border-color: #2f8d5d; +} + +.easy-badge.warning { + color: #fde68a; + border-color: #9a7a2e; +} + +.easy-badge.problem { + color: #fecaca; + border-color: #b64a4a; +} + +.easy-list { + margin: 8px 0 10px 16px; +} + .chart-tooltip { background: #0f1934ee; border: 1px solid #2f4a8b;