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.
This commit is contained in:
1 parent
37d03aa0d5
commit
1f4813c786
4 files changed
+116
-1
No files matched your search
@@ -369,6 +369,16 @@ export function resetSessionCache() {
|
|||||||
inflight.clear();
|
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
|
// Periodic sweeper: drop stale session rows + expired cooldowns so long-running
|
||||||
// servers never accumulate state for accounts/models no longer in use.
|
// servers never accumulate state for accounts/models no longer in use.
|
||||||
// Returns how many entries were removed.
|
// Returns how many entries were removed.
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ import { APP_CONFIG } from "@/shared/constants/config";
|
|||||||
import { LOCALE_COOKIE, normalizeLocale } from "@/i18n/config";
|
import { LOCALE_COOKIE, normalizeLocale } from "@/i18n/config";
|
||||||
import { LOCALE_FLAGS } from "@/shared/constants/locales";
|
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() {
|
function getLocaleFromCookie() {
|
||||||
if (typeof document === "undefined") return "en";
|
if (typeof document === "undefined") return "en";
|
||||||
const cookie = document.cookie
|
const cookie = document.cookie
|
||||||
@@ -49,6 +59,8 @@ export default function ProfilePage() {
|
|||||||
const [oidcRedirectUri, setOidcRedirectUri] = useState("/api/auth/oidc/callback");
|
const [oidcRedirectUri, setOidcRedirectUri] = useState("/api/auth/oidc/callback");
|
||||||
const [oidcExpanded, setOidcExpanded] = useState(false);
|
const [oidcExpanded, setOidcExpanded] = useState(false);
|
||||||
const importFileRef = useRef(null);
|
const importFileRef = useRef(null);
|
||||||
|
const [memoryInfo, setMemoryInfo] = useState(null);
|
||||||
|
const [memoryLoading, setMemoryLoading] = useState(false);
|
||||||
const [proxyForm, setProxyForm] = useState({
|
const [proxyForm, setProxyForm] = useState({
|
||||||
outboundProxyEnabled: false,
|
outboundProxyEnabled: false,
|
||||||
outboundProxyUrl: "",
|
outboundProxyUrl: "",
|
||||||
@@ -563,6 +575,19 @@ export default function ProfilePage() {
|
|||||||
setShutdownOpen(false);
|
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 () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/auth/logout", { method: "POST" });
|
const res = await fetch("/api/auth/logout", { method: "POST" });
|
||||||
@@ -649,6 +674,24 @@ export default function ProfilePage() {
|
|||||||
{dbStatus.message}
|
{dbStatus.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
<div className="flex flex-col gap-1.5 mt-1 pt-3 border-t border-border">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
icon="memory"
|
||||||
|
onClick={handleMemoryCheck}
|
||||||
|
loading={memoryLoading}
|
||||||
|
className="w-full sm:w-auto self-start"
|
||||||
|
>
|
||||||
|
Check Size Memory
|
||||||
|
</Button>
|
||||||
|
{memoryInfo && (
|
||||||
|
<pre className="text-[11px] font-mono text-text-muted whitespace-pre-wrap break-all">
|
||||||
|
{memoryInfo.error
|
||||||
|
? `Error: ${memoryInfo.error}`
|
||||||
|
: memoryLine(memoryInfo)}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ export default function ProxyFitnessPage() {
|
|||||||
onChange={handleGeoToggle}
|
onChange={handleGeoToggle}
|
||||||
disabled={geoUpdating}
|
disabled={geoUpdating}
|
||||||
label="Geo probe"
|
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)"
|
||||||
/>
|
/>
|
||||||
<Button variant="secondary" size="sm" icon="refresh" onClick={fetchAll}>Refresh</Button>
|
<Button variant="secondary" size="sm" icon="refresh" onClick={fetchAll}>Refresh</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user