ui: move B2 panel and expose zero-byte backup sizes

This commit is contained in:
Brad Stein 2026-04-13 16:48:30 -03:00
parent 5d550faec9
commit af6de62ca2
4 changed files with 66 additions and 66 deletions

View File

@ -75,8 +75,8 @@ type PVCInventory struct {
LastJobState string `json:"last_job_state,omitempty"`
LastJobStartedAt string `json:"last_job_started_at,omitempty"`
LastJobProgressPct int `json:"last_job_progress_pct"`
LastBackupSizeBytes float64 `json:"last_backup_size_bytes,omitempty"`
TotalBackupSizeBytes float64 `json:"total_backup_size_bytes,omitempty"`
LastBackupSizeBytes float64 `json:"last_backup_size_bytes"`
TotalBackupSizeBytes float64 `json:"total_backup_size_bytes"`
Healthy bool `json:"healthy"`
HealthReason string `json:"health_reason,omitempty"`
Error string `json:"error,omitempty"`

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Soteria Backup Console</title>
<script type="module" crossorigin src="/assets/index-C9X7C4pD.js"></script>
<script type="module" crossorigin src="/assets/index-C8vHBL9g.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B24a4-XK.css">
</head>
<body>

View File

@ -694,6 +694,68 @@ function App() {
</section>
<section className="column">
<section className="panel">
<div className="panel-header">
<h2>B2 Consumption</h2>
<button type="button" className="secondary" onClick={() => void refreshB2Usage()} disabled={b2Refreshing}>
{b2Refreshing ? 'Refreshing...' : 'Refresh B2'}
</button>
</div>
{b2Error && <p className="error">{b2Error}</p>}
{!b2Error && !b2Usage.enabled && <p className="subtle">B2 monitoring is disabled in Soteria config.</p>}
{!b2Error && b2Usage.enabled && !b2Usage.available && <p className="error">{b2Usage.error || 'B2 usage currently unavailable.'}</p>}
{b2Usage.enabled && (
<div className="stack">
<p className="subtle tiny">Endpoint: {b2Usage.endpoint || 'n/a'} | Region: {b2Usage.region || 'n/a'}</p>
<p className="subtle tiny">Last scan: {formatTimestamp(b2Usage.scanned_at)} | Duration: {b2Usage.scan_duration_ms || 0}ms</p>
<div className="stat-grid">
<div className="stat">
<span className="label">Stored bytes</span>
<strong>{formatBytes(b2Usage.total_bytes)}</strong>
</div>
<div className="stat">
<span className="label">Objects</span>
<strong>{b2Usage.total_objects}</strong>
</div>
<div className="stat">
<span className="label">Recent bytes (24h)</span>
<strong>{formatBytes(b2Usage.recent_bytes_24h)}</strong>
</div>
<div className="stat">
<span className="label">Recent objects (24h)</span>
<strong>{b2Usage.recent_objects_24h}</strong>
</div>
</div>
<p className="subtle tiny">Recent 24h values are object-change bandwidth proxy from bucket scans. B2 egress billing totals are not exposed by S3 object listing.</p>
<div className="bucket-table-wrap">
<table>
<thead>
<tr>
<th>Bucket</th>
<th>Objects</th>
<th>Stored</th>
<th>Recent 24h</th>
</tr>
</thead>
<tbody>
{(b2Usage.buckets || []).map((bucket) => (
<tr key={bucket.name}>
<td>
<div>{bucket.name}</div>
<div className="subtle tiny">Last object: {formatTimestamp(bucket.last_modified_at)}</div>
</td>
<td>{bucket.object_count}</td>
<td>{formatBytes(bucket.total_bytes)}</td>
<td>{formatBytes(bucket.recent_bytes_24h)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</section>
<section className="panel">
<h2>Restore Planner</h2>
<p className="subtle tiny">Inventory restore buttons preload this panel. This is where restore dry-runs and execution happen.</p>
@ -770,68 +832,6 @@ function App() {
</section>
<section className="column">
<section className="panel">
<div className="panel-header">
<h2>B2 Consumption</h2>
<button type="button" className="secondary" onClick={() => void refreshB2Usage()} disabled={b2Refreshing}>
{b2Refreshing ? 'Refreshing...' : 'Refresh B2'}
</button>
</div>
{b2Error && <p className="error">{b2Error}</p>}
{!b2Error && !b2Usage.enabled && <p className="subtle">B2 monitoring is disabled in Soteria config.</p>}
{!b2Error && b2Usage.enabled && !b2Usage.available && <p className="error">{b2Usage.error || 'B2 usage currently unavailable.'}</p>}
{b2Usage.enabled && (
<div className="stack">
<p className="subtle tiny">Endpoint: {b2Usage.endpoint || 'n/a'} | Region: {b2Usage.region || 'n/a'}</p>
<p className="subtle tiny">Last scan: {formatTimestamp(b2Usage.scanned_at)} | Duration: {b2Usage.scan_duration_ms || 0}ms</p>
<div className="stat-grid">
<div className="stat">
<span className="label">Stored bytes</span>
<strong>{formatBytes(b2Usage.total_bytes)}</strong>
</div>
<div className="stat">
<span className="label">Objects</span>
<strong>{b2Usage.total_objects}</strong>
</div>
<div className="stat">
<span className="label">Recent bytes (24h)</span>
<strong>{formatBytes(b2Usage.recent_bytes_24h)}</strong>
</div>
<div className="stat">
<span className="label">Recent objects (24h)</span>
<strong>{b2Usage.recent_objects_24h}</strong>
</div>
</div>
<p className="subtle tiny">Recent 24h values are object-change bandwidth proxy from bucket scans. B2 egress billing totals are not exposed by S3 object listing.</p>
<div className="bucket-table-wrap">
<table>
<thead>
<tr>
<th>Bucket</th>
<th>Objects</th>
<th>Stored</th>
<th>Recent 24h</th>
</tr>
</thead>
<tbody>
{(b2Usage.buckets || []).map((bucket) => (
<tr key={bucket.name}>
<td>
<div>{bucket.name}</div>
<div className="subtle tiny">Last object: {formatTimestamp(bucket.last_modified_at)}</div>
</td>
<td>{bucket.object_count}</td>
<td>{formatBytes(bucket.total_bytes)}</td>
<td>{formatBytes(bucket.recent_bytes_24h)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</section>
<section className="panel scroll-panel">
<h2>Backup Policies</h2>
<p className="subtle tiny">Policy backups create new restic snapshots. `Keep last` controls version retention per PVC: 1 means only newest copy remains after each run. With dedupe on, unchanged blocks are reused in the shared repository. With dedupe off, Soteria isolates each PVC to its own repository path.</p>