diff --git a/app/src/App.jsx b/app/src/App.jsx
index bf4522f..feda277 100644
--- a/app/src/App.jsx
+++ b/app/src/App.jsx
@@ -23,8 +23,23 @@ const CATEGORY_LABEL = {
// and the Rust command re-checks server-side anyway (defense in depth).
const ACTIONABLE_VERDICTS = new Set(["safe", "review"]);
-function bytesToGB(n) {
- return (n / 1e9).toFixed(2);
+// Same SI thresholds and 999.95 carry as crates/diskern-cli human_bytes,
+// so the desktop app and CLI describe a finding the same way.
+function humanBytes(n) {
+ const UNITS = ["B", "KB", "MB", "GB", "TB"];
+ let value = Number(n);
+ let unit = 0;
+ // 999.95, not 1000.0: at one decimal place anything at or above that
+ // rounds to "1000.0", which belongs in the next unit up. Choosing the
+ // unit before rounding printed 999_999 as "1000.0 KB".
+ while (value >= 999.95 && unit < UNITS.length - 1) {
+ value /= 1000;
+ unit += 1;
+ }
+ if (unit === 0) {
+ return `${Math.trunc(n)} B`;
+ }
+ return `${value.toFixed(1)} ${UNITS[unit]}`;
}
function groupFindings(findings) {
@@ -91,7 +106,7 @@ function FindingRow({ f, quarantineDir, onQuarantined }) {
return (
{f.entry.path}
- {(f.entry.size / 1e6).toFixed(1)} MB
+ {humanBytes(f.entry.size)}
{/* Every reason, not just the matched rule: "referenced by 3
projects" is what explains a risky row, and it is never the
first one. */}
@@ -141,7 +156,7 @@ function CategorySection({ title, items, defaultOpen, quarantineDir, onQuarantin
{isOpen ? "▾" : "▸"}
{title}
- {items.length} item{items.length === 1 ? "" : "s"} · {bytesToGB(total)} GB
+ {items.length} item{items.length === 1 ? "" : "s"} · {humanBytes(total)}
{isOpen && (
@@ -178,7 +193,7 @@ function DuplicatesSection({ sets }) {
{isOpen ? "▾" : "▸"}
Duplicate files
- {sets.length} set{sets.length === 1 ? "" : "s"} · {bytesToGB(total)} GB wasted
+ {sets.length} set{sets.length === 1 ? "" : "s"} · {humanBytes(total)} wasted
{isOpen && (
@@ -186,8 +201,8 @@ function DuplicatesSection({ sets }) {
{sets.map((set, i) => (
{filesSeen.toLocaleString()} files found
- {bytesSeen > 0 && <> · {(bytesSeen / 1e9).toFixed(2)} GB so far>}
+ {bytesSeen > 0 && <> · {humanBytes(bytesSeen)} so far>}
{/* The walk checks the cancel flag per entry, so stopping is quick but
not instant — say "Stopping…" rather than pretending it's done. */}
@@ -543,7 +558,7 @@ export default function App() {
{scannedFolder && {scannedFolder}}
{report.files_scanned.toLocaleString()} files scanned ·{" "}
- {bytesToGB(report.total_reclaimable - reclaimed)} GB reclaimable
+ {humanBytes(report.total_reclaimable - reclaimed)} reclaimable