From 1f4813c786d90d96c02d6ad86a6105c567fff1b4 Mon Sep 17 00:00:00 2001 From: "MUH. IQRAM BAHRING" Date: Fri, 7 Aug 2026 08:11:44 +0800 Subject: [PATCH] feat(profile): Check Size Memory button + English geo-probe hint New /api/system/memory reports db + data-dir sizes and in-memory state (fitness/geo registries, freebuff session/cooldown counts); profile page 'Local Mode' card gets a Check Size Memory button rendering the summary. Geo-probe toggle hint localized to English. --- open-sse/executors/freebuff.js | 10 +++ src/app/(dashboard)/dashboard/profile/page.js | 43 +++++++++++++ .../dashboard/proxy-fitness/page.js | 2 +- src/app/api/system/memory/route.js | 62 +++++++++++++++++++ 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src/app/api/system/memory/route.js diff --git a/open-sse/executors/freebuff.js b/open-sse/executors/freebuff.js index 891a44a0..8bbf8d7b 100644 --- a/open-sse/executors/freebuff.js +++ b/open-sse/executors/freebuff.js @@ -369,6 +369,16 @@ export function resetSessionCache() { inflight.clear(); } +// Snapshot sizes of in-memory freebuff state (for the dashboard memory panel). +export function sessionStateSize() { + return { + sessions: sessionCache.size, + inflight: inflight.size, + modelLocks: modelLockCooldowns.size, + poolLimits: poolLimitCooldowns.size, + }; +} + // Periodic sweeper: drop stale session rows + expired cooldowns so long-running // servers never accumulate state for accounts/models no longer in use. // Returns how many entries were removed. diff --git a/src/app/(dashboard)/dashboard/profile/page.js b/src/app/(dashboard)/dashboard/profile/page.js index 4df8cbef..b28d547e 100644 --- a/src/app/(dashboard)/dashboard/profile/page.js +++ b/src/app/(dashboard)/dashboard/profile/page.js @@ -10,6 +10,16 @@ import { APP_CONFIG } from "@/shared/constants/config"; import { LOCALE_COOKIE, normalizeLocale } from "@/i18n/config"; import { LOCALE_FLAGS } from "@/shared/constants/locales"; +function memoryLine(m) { + if (!m) return ""; + const fb = m.inMemory?.freebuff || {}; + return [ + `DB: ${m.dbPath || "-"} (${m.dbSizeMB || "-"})`, + `Data dir: ${m.dataDirSizeMB || "-"}`, + `In-memory: fitness=${m.inMemory?.fitnessPools ?? 0} pool(s) · geo=${m.inMemory?.geoPools ?? 0} pool(s) · freebuff sessions=${fb.sessions ?? 0}, locks=${fb.modelLocks ?? 0}, pool-limits=${fb.poolLimits ?? 0}`, + ].join("\n"); +} + function getLocaleFromCookie() { if (typeof document === "undefined") return "en"; const cookie = document.cookie @@ -49,6 +59,8 @@ export default function ProfilePage() { const [oidcRedirectUri, setOidcRedirectUri] = useState("/api/auth/oidc/callback"); const [oidcExpanded, setOidcExpanded] = useState(false); const importFileRef = useRef(null); + const [memoryInfo, setMemoryInfo] = useState(null); + const [memoryLoading, setMemoryLoading] = useState(false); const [proxyForm, setProxyForm] = useState({ outboundProxyEnabled: false, outboundProxyUrl: "", @@ -563,6 +575,19 @@ export default function ProfilePage() { setShutdownOpen(false); }; + const handleMemoryCheck = async () => { + setMemoryLoading(true); + try { + const res = await fetch("/api/system/memory", { cache: "no-store" }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + setMemoryInfo(await res.json()); + } catch (err) { + setMemoryInfo({ error: err.message }); + } finally { + setMemoryLoading(false); + } + }; + const handleLogout = async () => { try { const res = await fetch("/api/auth/logout", { method: "POST" }); @@ -649,6 +674,24 @@ export default function ProfilePage() { {dbStatus.message}

)} +
+ + {memoryInfo && ( +
+                  {memoryInfo.error
+                    ? `Error: ${memoryInfo.error}`
+                    : memoryLine(memoryInfo)}
+                
+ )} +
diff --git a/src/app/(dashboard)/dashboard/proxy-fitness/page.js b/src/app/(dashboard)/dashboard/proxy-fitness/page.js index 36a71044..cfe4a6e8 100644 --- a/src/app/(dashboard)/dashboard/proxy-fitness/page.js +++ b/src/app/(dashboard)/dashboard/proxy-fitness/page.js @@ -253,7 +253,7 @@ export default function ProxyFitnessPage() { onChange={handleGeoToggle} disabled={geoUpdating} label="Geo probe" - description="Probe egress IP/country per pool ±30 min (pemakaian kuota geo-API)" + description="Probe egress IP/country per pool every ~30 min (uses geo-API quota)" /> diff --git a/src/app/api/system/memory/route.js b/src/app/api/system/memory/route.js new file mode 100644 index 00000000..41101d7c --- /dev/null +++ b/src/app/api/system/memory/route.js @@ -0,0 +1,62 @@ +import { NextResponse } from "next/server"; +import fs from "node:fs"; +import path from "node:path"; +import { getDataDir } from "@/lib/dataDir"; +import { DATA_DIR, DATA_FILE } from "@/lib/db/paths"; +import { poolFitnessSnapshot } from "open-sse/services/proxyPoolFitness.js"; +import { poolGeoSnapshot } from "open-sse/services/poolGeo.js"; + +function formatMB(bytes) { + return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; +} + +function dirSizeMB(dir, maxDepth = 4) { + let total = 0; + const walk = (p, depth) => { + if (depth > maxDepth) return; + let entries = []; + try { + entries = fs.readdirSync(p, { withFileTypes: true }); + } catch { + return; + } + for (const e of entries) { + const full = path.join(p, e.name); + try { + if (e.isDirectory()) walk(full, depth + 1); + else if (e.isFile()) total += fs.statSync(full).size; + } catch { /* skip */ } + } + }; + walk(dir, 0); + return total; +} + +// GET /api/system/memory — data + in-memory state sizes. +export async function GET() { + try { + const dataDir = getDataDir(); + const dbPath = DATA_FILE; + let dbFile = 0; + try { dbFile = fs.statSync(dbPath).size; } catch { dbFile = 0; } + + const fitness = poolFitnessSnapshot(); + const geo = poolGeoSnapshot(); + const { sessionStateSize } = await import("open-sse/executors/freebuff.js"); + + return NextResponse.json({ + dataDir, + dbPath, + dbSizeMB: formatMB(dbFile), + dataDirSizeMB: formatMB(dirSizeMB(dataDir)), + inMemory: { + fitnessPools: Object.keys(fitness).length, + geoPools: Object.keys(geo).length, + freebuff: sessionStateSize(), + }, + }); + } catch (error) { + console.log("Error reading memory sizes:", error); + return NextResponse.json({ error: "Failed to read memory sizes" }, { status: 500 }); + } +} \ No newline at end of file