Files
Triple-C/app/src/lib/formatBytes.ts
T
shadow-testandClaude Opus 5 ed91423666 Hold back the Disk panel and OS drag-out from the ship branch
This is a scope reduction, not an abandonment. Both subsystems are
preserved in full on `hold/disk-and-dragout` and are intended to come
back once they have been hardened separately. Nothing here is a
judgement that the features are unwanted — three successive
audit-and-fix cycles each closed a critical defect in these two areas
and each opened a new one, so the rest of the round ships now and these
two get their own cycle rather than holding it up.

Removed: the Disk settings panel and its whole reclaim / destroy /
compaction surface — `DiskSettings`, `DiskProjectTable`, `useDiskUsage`,
`docker/disk.rs`, `disk_tests.rs`, the disk commands in
`docker_commands.rs`, and their `generate_handler!` entries. Dropping
the IPC entries is the point: a UI-only removal would have left five
commands callable by a compromised webview, one of them a verified
arbitrary-DELETE primitive. `sweep_orphaned_snapshots`'s *command* goes
with them (the panel was its only caller); the sweep itself stays.

Removed: OS drag-out from the Files tab — `stage_container_file_for_drag`
and its host staging lifecycle, the pointer gesture and `dragPreview`,
`stageForDrag` / `isStagedHostPath`, the `tauri-plugin-drag` and
`@crabnebula/tauri-plugin-drag` dependencies, and the
`drag:allow-start-drag` capability grant, which could not be scoped.
The capability test's expected list is updated; its `*:default` and
`store:*` assertions are untouched.

Kept, deliberately: drag-and-drop *into* the app (Files pane and
terminal) and "Save to host…", which is now the only route out of a
container. The prevention work is untouched — the pre-commit scrub and
`SNAPSHOT_SCRUB_PATHS`, capped container logs, the `triple-c.base` /
`triple-c.managed` labels, `sweep_orphaned_snapshots` and the startup
housekeeping, the migration pin/probe reapers, scheduler log pruning,
`formatBytes.ts`, and `project_lock.rs` in full with every acquisition
site outside `disk.rs`.

Entanglements, resolved rather than deleted blind:
* `container.rs`'s `a_compaction_runs_this_module_s_scrub_script_byte_for_byte`
  pinned the compaction Dockerfile against `snapshot_scrub_script()`.
  Dropped — it existed only for compaction. `snapshot_scrub_script` and
  its containment tests are untouched.
* `lib.rs`'s startup reap of `:compacting` tags and `triple-c-compact-*`
  containers is dropped: nothing on this branch creates them.
* `project_lock`'s `Compaction` / `CacheClear` variants and
  `any_held_excluding`, `migration_commands::is_migrating`, and
  `formatBytes{Delta,Ceiling}` lose their last production caller but are
  kept and still tested, annotated with why.
* `projects_store::corrupt_since` and `migration_store::peek_ownerless_since`
  were read only by the disk survey and are removed. The corrupt-load
  marker and `.bak` are still written.

Verified: `npm run test` 611 passing, `npx tsc --noEmit` clean,
`npm run build` green; `cargo test` 419 passed / 2 ignored,
`cargo build` 0 warnings. Every test removed belongs to a removed
feature — no kept-behaviour test was weakened or deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc
2026-08-23 15:20:22 -07:00

118 lines
5.1 KiB
TypeScript

/**
* The one byte formatter.
*
* The app had four of them — `projects/home/format.ts`,
* `projects/migrationCopy.ts`, `settings/UpdateDialog.tsx` and an inline
* `toFixed(1)` in `useProjectActions.ts` — disagreeing about the divisor, the
* unit labels and the precision. All four now delegate here, and there are no
* remaining copies.
*
* The last two were held back because re-pointing them changes what they
* render, and that turned out to be the argument for doing it rather than
* against. `UpdateDialog` rendered KB at `toFixed(0)` (`512 KB` is now
* `512.0 KB`, consistent with every other size in the app) and both stopped
* the ladder at MB, so a 2 GB asset or backup read as a five-digit number of
* megabytes. Both are `{ binary: true }`: they describe files, and a host file
* browser shows the ÷1024 figure for the same bytes.
*
* ## Why the default is base 1000
*
* Anything explaining what Docker reports has to match it, and Docker formats
* every size it prints with `units.HumanSize`, which is **base 1000**. Showing
* 26.1 GB where the user's terminal said 28.0 GB for the same object would read
* as a bug in the app. So decimal is the default and binary is opt-in, rather
* than the other way round.
*
* Both existing conventions are preserved for every size either call site can
* realistically produce — a file size or a payload size, i.e. a non-negative
* finite number below a terabyte. Outside that range this deliberately differs
* from what it replaced: a negative or `NaN` input now renders `—` rather than
* `-1 B` or `NaN GB`, and the unit ladder continues past GB instead of
* stopping there.
*
* - `{ }` → `41.0 MB` (decimal, what migration used)
* - `{ binary: true }` → `1.5 GB` (÷1024 with decimal-style
* labels, what Project Home used
* — technically a misnomer, but
* it is the app's convention and
* changing it is not this
* feature's business)
* - `{ binary: true, iec: true }` → `1.5 GiB` (÷1024 labelled honestly)
*/
const DECIMAL_UNITS = ["B", "KB", "MB", "GB", "TB", "PB"];
const IEC_UNITS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
export interface FormatBytesOptions {
/** Divide by 1024 instead of 1000. */
binary?: boolean;
/** Label binary units as `KiB`/`MiB`/`GiB` rather than `KB`/`MB`/`GB`. */
iec?: boolean;
/** Decimal places above `B`. Bytes are always whole. */
precision?: number;
}
export function formatBytes(bytes: number, options: FormatBytesOptions = {}): string {
const { binary = false, iec = false, precision = 1 } = options;
// A negative or non-finite size is a bug upstream, not something to render as
// `NaN GB` in the middle of a table. Docker reports -1 for "not computed",
// and that is the case this actually catches.
if (!Number.isFinite(bytes) || bytes < 0) return "—";
const step = binary ? 1024 : 1000;
const units = binary && iec ? IEC_UNITS : DECIMAL_UNITS;
let value = bytes;
let unit = 0;
while (value >= step && unit < units.length - 1) {
value /= step;
unit += 1;
}
// **Promote again if rounding pushed the value back up to a whole step.**
// `toFixed` runs after the loop, so 999,999 B divides to 999.999 KB and then
// renders as "1000.0 KB" — a unit the loop had already decided against. The
// same happens at every boundary (999,999,999 → "1000.0 MB", and 1,048,575
// → "1024.0 KB" in binary).
if (unit < units.length - 1 && Number(value.toFixed(precision)) >= step) {
value /= step;
unit += 1;
}
// Whole bytes never get a decimal point: `512 B`, not `512.0 B`.
return unit === 0
? `${Math.round(bytes)} ${units[0]}`
: `${value.toFixed(precision)} ${units[unit]}`;
}
/**
* `12.3 GB` → `+12.3 GB`, for a figure that is being *added* rather than
* measured. Used for "next commit adds …", which is the number that explains
* why a snapshot grows.
*
* **No caller on this branch**, for the same reason as [`formatBytesCeiling`]:
* the Disk panel's per-project table was the last one, and it went to
* `hold/disk-and-dragout`.
*/
export function formatBytesDelta(bytes: number, options?: FormatBytesOptions): string {
const formatted = formatBytes(bytes, options);
return formatted === "—" ? formatted : `+${formatted}`;
}
/**
* `up to 12.3 GB` — for a bound rather than a measurement.
*
* A figure that cannot be known until an operation runs must not render like
* one that was measured; going through a different function is what stops it
* being read as a promise.
*
* **No caller on this branch.** Its last one was the Disk panel's projected
* compaction yield, which went to `hold/disk-and-dragout`. Kept with its tests
* because the distinction it encodes is the reusable part.
*/
export function formatBytesCeiling(bytes: number, options?: FormatBytesOptions): string {
if (!Number.isFinite(bytes) || bytes <= 0) return "an unknown amount";
return `up to ${formatBytes(bytes, options)}`;
}