mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
fix(server): count whole storage tree in Fabro-managed bytes
build_disk_usage_response only summed scratch/ run dirs and logs/*.log, omitting objects/ (SlateDB + artifacts), sessions/, and vaults/ — a ~30x undercount of "Fabro managed" storage on the resources page. Measure the whole storage_dir tree for total_size_bytes so it can't drift as new subdirectories are added. Reclaimable stays a curated estimate that matches what `fabro system prune` actually frees. A residual "other" summary row keeps `fabro system df` totals consistent and surfaces as a "Database & artifacts" table row. Also add a KiB tier to formatBytesAsMemory so small storage values render human-readably instead of raw byte counts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
65eac48d11
commit
37d6b3dbcd
7 changed files with 83 additions and 5 deletions
|
|
@ -106,9 +106,10 @@ export function formatTokenCount(
|
|||
|
||||
const BYTES_PER_GIB = 1024 * 1024 * 1024;
|
||||
const BYTES_PER_MIB = 1024 * 1024;
|
||||
const BYTES_PER_KIB = 1024;
|
||||
|
||||
/**
|
||||
* Format a byte count as a memory/disk size (e.g. "8 GiB", "512 MiB", "1024 B").
|
||||
* Format a byte count as a memory/disk size (e.g. "8 GiB", "512 MiB", "4 KiB", "742 B").
|
||||
*/
|
||||
export function formatBytesAsMemory(bytes: number): string {
|
||||
if (bytes >= BYTES_PER_GIB) {
|
||||
|
|
@ -119,6 +120,10 @@ export function formatBytesAsMemory(bytes: number): string {
|
|||
const mib = bytes / BYTES_PER_MIB;
|
||||
return `${Number.isInteger(mib) ? mib : mib.toFixed(1)} MiB`;
|
||||
}
|
||||
if (bytes >= BYTES_PER_KIB) {
|
||||
const kib = bytes / BYTES_PER_KIB;
|
||||
return `${Number.isInteger(kib) ? kib : kib.toFixed(1)} KiB`;
|
||||
}
|
||||
return `${bytes} B`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -182,6 +182,15 @@ describe("formatBytesAsMemory", () => {
|
|||
test("falls back to mebibytes when below a gibibyte", () => {
|
||||
expect(formatBytesAsMemory(512 * 1024 * 1024)).toBe("512 MiB");
|
||||
});
|
||||
|
||||
test("falls back to kibibytes when below a mebibyte", () => {
|
||||
expect(formatBytesAsMemory(4 * 1024)).toBe("4 KiB");
|
||||
expect(formatBytesAsMemory(1536)).toBe("1.5 KiB");
|
||||
});
|
||||
|
||||
test("renders raw bytes when below a kibibyte", () => {
|
||||
expect(formatBytesAsMemory(742)).toBe("742 B");
|
||||
});
|
||||
});
|
||||
|
||||
describe("RunSandbox route", () => {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ fn df_from(
|
|||
.summary
|
||||
.iter()
|
||||
.find(|row| row.type_.as_deref() == Some("logs"));
|
||||
let other_summary = output
|
||||
.summary
|
||||
.iter()
|
||||
.find(|row| row.type_.as_deref() == Some("other"));
|
||||
|
||||
let run_count = runs_summary.and_then(|row| row.count).map_or(0, as_u64);
|
||||
let active_count = runs_summary.and_then(|row| row.active).map_or(0, as_u64);
|
||||
|
|
@ -58,6 +62,10 @@ fn df_from(
|
|||
.and_then(|row| row.size_bytes)
|
||||
.map_or(0, as_u64);
|
||||
|
||||
let total_other_size = other_summary
|
||||
.and_then(|row| row.size_bytes)
|
||||
.map_or(0, as_u64);
|
||||
|
||||
let run_reclaim_pct = if total_run_size > 0 {
|
||||
#[allow(
|
||||
clippy::cast_possible_truncation,
|
||||
|
|
@ -111,6 +119,15 @@ fn df_from(
|
|||
.cell()
|
||||
.justify(Justify::Right),
|
||||
],
|
||||
vec![
|
||||
"Database & artifacts".cell(),
|
||||
"-".cell().justify(Justify::Right),
|
||||
"-".cell().justify(Justify::Right),
|
||||
format_size(total_other_size).cell().justify(Justify::Right),
|
||||
format!("{} (0%)", format_size(0))
|
||||
.cell()
|
||||
.justify(Justify::Right),
|
||||
],
|
||||
];
|
||||
let summary_table = summary_rows
|
||||
.table()
|
||||
|
|
|
|||
|
|
@ -57,6 +57,10 @@ fn system_df_summarizes_runs_and_logs() {
|
|||
stdout.contains("Logs"),
|
||||
"system df should summarize logs: {stdout}"
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("Database & artifacts"),
|
||||
"system df should summarize database and artifact storage: {stdout}"
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("Data directory:"),
|
||||
"system df should print the storage directory: {stdout}"
|
||||
|
|
|
|||
|
|
@ -962,9 +962,16 @@ pub(crate) async fn get_system_disk_usage(
|
|||
"active": null,
|
||||
"size_bytes": 256,
|
||||
"reclaimable_bytes": 256
|
||||
},
|
||||
{
|
||||
"type": "other",
|
||||
"count": null,
|
||||
"active": null,
|
||||
"size_bytes": 16_777_216,
|
||||
"reclaimable_bytes": 0
|
||||
}
|
||||
],
|
||||
"total_size_bytes": 1280,
|
||||
"total_size_bytes": 16_778_496,
|
||||
"total_reclaimable_bytes": 1280,
|
||||
"runs": runs
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -1468,6 +1468,12 @@ fn build_disk_usage_response(
|
|||
}
|
||||
}
|
||||
|
||||
// Measure the whole storage tree so the managed total can't drift as new
|
||||
// subdirectories are added. "other" is the residual (database, artifacts,
|
||||
// sessions, vaults) — everything that isn't an enumerated run or log file.
|
||||
let managed_size = dir_size(storage_dir);
|
||||
let other_size = managed_size.saturating_sub(total_run_size + total_log_size);
|
||||
|
||||
Ok(DiskUsageResponse {
|
||||
summary: vec![
|
||||
DiskUsageSummaryRow {
|
||||
|
|
@ -1484,8 +1490,15 @@ fn build_disk_usage_response(
|
|||
size_bytes: Some(to_i64(total_log_size)),
|
||||
reclaimable_bytes: Some(to_i64(total_log_size)),
|
||||
},
|
||||
DiskUsageSummaryRow {
|
||||
type_: Some("other".to_string()),
|
||||
count: None,
|
||||
active: None,
|
||||
size_bytes: Some(to_i64(other_size)),
|
||||
reclaimable_bytes: Some(0),
|
||||
},
|
||||
],
|
||||
total_size_bytes: Some(to_i64(total_run_size + total_log_size)),
|
||||
total_size_bytes: Some(to_i64(managed_size)),
|
||||
total_reclaimable_bytes: Some(to_i64(reclaimable_run_size + total_log_size)),
|
||||
runs: verbose.then_some(run_rows),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -207,6 +207,15 @@ async fn get_system_resources_returns_server_visible_metrics() {
|
|||
&body["disk"]["fabro_reclaimable_bytes"],
|
||||
"disk.fabro_reclaimable_bytes",
|
||||
);
|
||||
assert!(
|
||||
body["disk"]["fabro_managed_bytes"]
|
||||
.as_i64()
|
||||
.unwrap_or_default()
|
||||
>= body["disk"]["fabro_reclaimable_bytes"]
|
||||
.as_i64()
|
||||
.unwrap_or_default(),
|
||||
"managed bytes must include everything reclaimable: {body}"
|
||||
);
|
||||
assert!(
|
||||
body["notes"]
|
||||
.as_array()
|
||||
|
|
@ -286,8 +295,22 @@ async fn get_system_disk_usage_returns_summary_and_verbose_rows() {
|
|||
"GET /api/v1/system/df?verbose=true",
|
||||
)
|
||||
.await;
|
||||
assert!(body["summary"].is_array());
|
||||
assert!(body["total_size_bytes"].as_i64().unwrap_or_default() > 0);
|
||||
let summary = body["summary"].as_array().expect("summary array");
|
||||
assert!(summary.iter().any(|row| row["type"] == "other"));
|
||||
|
||||
let row_size = |type_: &str| {
|
||||
summary
|
||||
.iter()
|
||||
.find(|row| row["type"] == type_)
|
||||
.and_then(|row| row["size_bytes"].as_i64())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let total_size = body["total_size_bytes"].as_i64().unwrap_or_default();
|
||||
assert!(total_size > 0);
|
||||
assert_eq!(
|
||||
row_size("runs") + row_size("logs") + row_size("other"),
|
||||
total_size,
|
||||
);
|
||||
assert!(
|
||||
body["runs"]
|
||||
.as_array()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue