system-status: show storage usage (DB, attachments, backups, disk free) — v0.1.0:63
/api/system/status now returns a best-effort storage block: database file size (crm.db + WAL + SHM), the email_attachments dir, the backups dir, and disk total/used/free via shutil.disk_usage(DATA_DIR). System Status renders a Storage section with human-readable sizes so growth can be watched over time. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3601,6 +3601,38 @@ class CRMHandler(BaseHTTPRequestHandler):
|
|||||||
}
|
}
|
||||||
except Exception:
|
except Exception:
|
||||||
out['source_counts'] = None
|
out['source_counts'] = None
|
||||||
|
# Storage usage — DB file(s), email attachments, backups, and disk headroom,
|
||||||
|
# so growth can be watched over time. Best-effort; never fails the status call.
|
||||||
|
try:
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
def _fsize(p):
|
||||||
|
try:
|
||||||
|
return os.path.getsize(p)
|
||||||
|
except OSError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _dirsize(d):
|
||||||
|
total = 0
|
||||||
|
for root, _dirs, files in os.walk(d):
|
||||||
|
for f in files:
|
||||||
|
try:
|
||||||
|
total += os.path.getsize(os.path.join(root, f))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return total
|
||||||
|
|
||||||
|
du = shutil.disk_usage(DATA_DIR)
|
||||||
|
out['storage'] = {
|
||||||
|
'database_bytes': sum(_fsize(DB_PATH + s) for s in ("", "-wal", "-shm")),
|
||||||
|
'attachments_bytes': _dirsize(os.path.join(DATA_DIR, "email_attachments")),
|
||||||
|
'backups_bytes': _dirsize(os.path.join(DATA_DIR, "backups")),
|
||||||
|
'disk_total_bytes': du.total,
|
||||||
|
'disk_used_bytes': du.used,
|
||||||
|
'disk_free_bytes': du.free,
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
out['storage'] = None
|
||||||
conn.close()
|
conn.close()
|
||||||
self.send_json({"data": out})
|
self.send_json({"data": out})
|
||||||
|
|
||||||
|
|||||||
@@ -10258,6 +10258,15 @@
|
|||||||
if (error) return <div className="toast error" style={{ position: 'static' }}>{error}</div>;
|
if (error) return <div className="toast error" style={{ position: 'static' }}>{error}</div>;
|
||||||
if (!data) return <div className="empty-state">No data</div>;
|
if (!data) return <div className="empty-state">No data</div>;
|
||||||
|
|
||||||
|
const fmtBytes = (n) => {
|
||||||
|
if (n == null) return '—';
|
||||||
|
if (n < 1024) return n + ' B';
|
||||||
|
const u = ['KB', 'MB', 'GB', 'TB']; let i = -1; let v = n;
|
||||||
|
do { v /= 1024; i++; } while (v >= 1024 && i < u.length - 1);
|
||||||
|
return v.toFixed(v < 10 ? 1 : 0) + ' ' + u[i];
|
||||||
|
};
|
||||||
|
const storage = data.storage;
|
||||||
|
|
||||||
const entities = data.canonical_entities || {};
|
const entities = data.canonical_entities || {};
|
||||||
const sync = data.last_index_sync;
|
const sync = data.last_index_sync;
|
||||||
const thesis = data.thesis || {};
|
const thesis = data.thesis || {};
|
||||||
@@ -10317,6 +10326,31 @@
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{storage && (
|
||||||
|
<div className="section">
|
||||||
|
<div className="section-title">Storage</div>
|
||||||
|
<div className="kpi-grid" style={{ marginBottom: 0 }}>
|
||||||
|
<div className="kpi-card">
|
||||||
|
<div className="kpi-label">Database</div>
|
||||||
|
<div className="kpi-value" style={{ fontSize: '18px' }}>{fmtBytes(storage.database_bytes)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="kpi-card">
|
||||||
|
<div className="kpi-label">Email attachments</div>
|
||||||
|
<div className="kpi-value" style={{ fontSize: '18px' }}>{fmtBytes(storage.attachments_bytes)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="kpi-card">
|
||||||
|
<div className="kpi-label">Backups</div>
|
||||||
|
<div className="kpi-value" style={{ fontSize: '18px' }}>{fmtBytes(storage.backups_bytes)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="kpi-card">
|
||||||
|
<div className="kpi-label">Disk free</div>
|
||||||
|
<div className="kpi-value" style={{ fontSize: '18px' }}>{fmtBytes(storage.disk_free_bytes)}</div>
|
||||||
|
<div className="kpi-subtitle">of {fmtBytes(storage.disk_total_bytes)} total</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<div className="section-title">Index Actions</div>
|
<div className="section-title">Index Actions</div>
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ export const PACKAGE_TITLE = 'Ten31 Database'
|
|||||||
// * 0.1.0:59 (Email Capture admin panel + matched email into the grounding corpus)
|
// * 0.1.0:59 (Email Capture admin panel + matched email into the grounding corpus)
|
||||||
// * 0.1.0:60 (Email Capture: single-mailbox enroll field for testing)
|
// * 0.1.0:60 (Email Capture: single-mailbox enroll field for testing)
|
||||||
// * 0.1.0:61 (Email Capture: live backfill progress + auto-refresh)
|
// * 0.1.0:61 (Email Capture: live backfill progress + auto-refresh)
|
||||||
// * Current: 0.1.0:62 (fix backfill crash on no-Reply-To emails; Sync now retries errored mailboxes)
|
// * 0.1.0:62 (fix backfill crash on no-Reply-To emails; Sync now retries errored mailboxes)
|
||||||
export const PACKAGE_VERSION = '0.1.0:62'
|
// * Current: 0.1.0:63 (System Status: storage usage — DB, attachments, backups, disk free)
|
||||||
|
export const PACKAGE_VERSION = '0.1.0:63'
|
||||||
|
|
||||||
export const DATA_MOUNT_PATH = '/data'
|
export const DATA_MOUNT_PATH = '/data'
|
||||||
export const WEB_PORT = 8080
|
export const WEB_PORT = 8080
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ import { v_0_1_0_59 } from './v0.1.0.59'
|
|||||||
import { v_0_1_0_60 } from './v0.1.0.60'
|
import { v_0_1_0_60 } from './v0.1.0.60'
|
||||||
import { v_0_1_0_61 } from './v0.1.0.61'
|
import { v_0_1_0_61 } from './v0.1.0.61'
|
||||||
import { v_0_1_0_62 } from './v0.1.0.62'
|
import { v_0_1_0_62 } from './v0.1.0.62'
|
||||||
|
import { v_0_1_0_63 } from './v0.1.0.63'
|
||||||
|
|
||||||
export const versionGraph = VersionGraph.of({
|
export const versionGraph = VersionGraph.of({
|
||||||
current: v_0_1_0_62,
|
current: v_0_1_0_63,
|
||||||
other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51, v_0_1_0_52, v_0_1_0_53, v_0_1_0_54, v_0_1_0_55, v_0_1_0_56, v_0_1_0_57, v_0_1_0_58, v_0_1_0_59, v_0_1_0_60, v_0_1_0_61],
|
other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51, v_0_1_0_52, v_0_1_0_53, v_0_1_0_54, v_0_1_0_55, v_0_1_0_56, v_0_1_0_57, v_0_1_0_58, v_0_1_0_59, v_0_1_0_60, v_0_1_0_61, v_0_1_0_62],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { VersionInfo } from '@start9labs/start-sdk'
|
||||||
|
|
||||||
|
// System Status now shows a Storage section: database file size, email attachments,
|
||||||
|
// backups, and disk free/total — so growth can be watched over time. Read-only,
|
||||||
|
// best-effort (never fails the status call). No schema migration.
|
||||||
|
export const v_0_1_0_63 = VersionInfo.of({
|
||||||
|
version: '0.1.0:63',
|
||||||
|
releaseNotes: {
|
||||||
|
en_US: [
|
||||||
|
'System Status now shows storage usage: how much space the database, email attachments,',
|
||||||
|
'and backups are using, plus free disk on the server, so you can watch it grow over time.',
|
||||||
|
].join(' '),
|
||||||
|
},
|
||||||
|
migrations: { up: async () => {}, down: async () => {} },
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user