Add easy & DBA mode

This commit is contained in:
2026-02-12 11:37:25 +01:00
parent d1af2bf4c6
commit 64b4c3dfa4
4 changed files with 238 additions and 4 deletions

View File

@@ -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 }) {
)}
</nav>
<div className="profile">
<div className="mode-switch-block">
<div className="mode-switch-label">View Mode</div>
<button
className={`mode-toggle ${uiMode === "easy" ? "easy" : "dba"}`}
onClick={() => setUiMode(uiMode === "easy" ? "dba" : "easy")}
type="button"
>
<span className="mode-pill">Easy</span>
<span className="mode-pill">DBA</span>
</button>
<small>{uiMode === "easy" ? "Simple health guidance" : "Advanced DBA metrics"}</small>
</div>
<div>{me?.email}</div>
<div className="role">{me?.role}</div>
<button className="logout-btn" onClick={logout}>Logout</button>

View File

@@ -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 <div className="card">Loading target detail...</div>;
if (error) return <div className="card error">{error}</div>;
@@ -148,7 +180,82 @@ export function TargetDetailPage() {
Target Detail {targetMeta?.name || `#${id}`}
{targetMeta?.dbname ? ` (${targetMeta.dbname})` : ""}
</h2>
{overview && (
{uiMode === "easy" && overview && easySummary && (
<>
<div className={`card easy-status ${easySummary.health}`}>
<h3>System Health</h3>
<p>{easySummary.message}</p>
<div className="easy-badge-row">
<span className={`easy-badge ${easySummary.health}`}>
{easySummary.health === "ok" ? "OK" : easySummary.health === "warning" ? "Warning" : "Problem"}
</span>
</div>
</div>
<div className="grid three">
<div className="card stat">
<strong>{formatNumber(easySummary.latest?.connections, 0)}</strong>
<span>Current Connections</span>
</div>
<div className="card stat">
<strong>{formatNumber(easySummary.latest?.tps, 2)}</strong>
<span>Transactions/sec (approx)</span>
</div>
<div className="card stat">
<strong>{formatNumber(easySummary.latest?.cache, 2)}%</strong>
<span>Cache Hit Ratio</span>
</div>
</div>
<div className="card">
<h3>Quick Explanation</h3>
{easySummary.issues.length === 0 && easySummary.warnings.length === 0 && (
<p>No immediate problems were detected. Keep monitoring over time.</p>
)}
{easySummary.issues.length > 0 && (
<>
<strong>Problems</strong>
<ul className="easy-list">
{easySummary.issues.map((item, idx) => <li key={`i-${idx}`}>{item}</li>)}
</ul>
</>
)}
{easySummary.warnings.length > 0 && (
<>
<strong>Things to watch</strong>
<ul className="easy-list">
{easySummary.warnings.map((item, idx) => <li key={`w-${idx}`}>{item}</li>)}
</ul>
</>
)}
</div>
<div className="grid two">
<div className="card">
<h3>Instance Summary</h3>
<div className="overview-metrics">
<div><span>Role</span><strong>{overview.instance.role}</strong></div>
<div><span>Uptime</span><strong>{formatSeconds(overview.instance.uptime_seconds)}</strong></div>
<div><span>Target Port</span><strong>{targetMeta?.port ?? "-"}</strong></div>
<div><span>Current DB Size</span><strong>{formatBytes(overview.storage.current_database_size_bytes)}</strong></div>
<div><span>Replication Clients</span><strong>{overview.replication.active_replication_clients ?? "-"}</strong></div>
<div><span>Autovacuum Workers</span><strong>{overview.performance.autovacuum_workers ?? "-"}</strong></div>
</div>
</div>
<div className="card">
<h3>Activity Snapshot</h3>
<div className="overview-metrics">
<div><span>Running Sessions</span><strong>{activity.filter((a) => a.state === "active").length}</strong></div>
<div><span>Total Sessions</span><strong>{activity.length}</strong></div>
<div><span>Current Locks</span><strong>{locks.length}</strong></div>
<div><span>Deadlocks</span><strong>{overview.performance.deadlocks ?? 0}</strong></div>
</div>
</div>
</div>
</>
)}
{uiMode === "dba" && overview && (
<div className="card">
<h3>Database Overview</h3>
<div className="grid three overview-kv">

View File

@@ -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 <AuthCtx.Provider value={value}>{children}</AuthCtx.Provider>;
}

View File

@@ -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;