Skip to content

fix(adopt): never let a same-millisecond backup overwrite an earlier one - #4

Merged
fylorn merged 3 commits into
mainfrom
dev
Sep 14, 2026
Merged

fylorn merged 3 commits into
mainfrom
dev

Conversation

@fylorn

@fylorn fylorn commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Brings dev to main. Two commits:

Both are already green on dev.

🤖 Generated with Claude Code

fylorn and others added 3 commits September 14, 2026 10:39
…one (#3)

backup_to named the backup directory after the current millisecond and
the file inside after the flattened real path. Two adoptions of the same
file within one millisecond therefore wrote to the exact same backup
path. The second backup already contains our gateway key, so it replaced
the first backup holding the user's original key. The sidecar still
pointed at that path, so restore recovered our key instead of the user's
and their original secret was gone. This is what made
re_adopting_does_not_overwrite_what_the_original_was flaky (~1 in 3
local runs), but the data loss is real, not just a test artefact.

Directories are now named `<ms>-<seq:04>` and claimed with create_dir
(not create_dir_all), bumping seq on AlreadyExists, so the filesystem
decides atomically who gets a name. The file inside is opened with
create_new so an existing backup is never overwritten. Zero-padding
keeps "sort by name = chronological order"; old plain `<ms>` directories
sort before any `<ms>-NNNN`, and nothing parses the names as integers.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
These show up on crates.io and docs.rs, and this is a public repository.

Co-authored-by: fylorn <130388298+SecAegis@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The overview had nothing to draw. `/summary` returns one total for one
window, and `/history` returns rows — there was no aggregate over time
and none by model or upstream, so the dashboard could only ever be
numbers and tables.

Two queries and two endpoints:

- `/summary/buckets?bucket_ms=` — spend and request count per time
  bucket. Width comes from the caller: "today by hour" and "30 days by
  day" want different buckets, and hard-coding one means adding an
  endpoint for the other. Floored at 1s, or a `bucket_ms=1` would scan
  out millions of groups.
- `/summary/by?dim=model|provider` — spend grouped, sorted by spend
  descending, because the first thing that chart has to answer is where
  the money went.

`dim` is an enum rather than a string. It picks the column name in the
SQL, so a string would be an injection point, and the few lines it saves
aren't worth it — serde rejects a bad value with a 400 before it reaches
the query. Verified against the real binary: `?dim=model);DROP` gets
400.

**The three cost states stay apart inside a bucket** (§4.3): measured,
estimated, and the *count* of rows with no price. Folding the third into
the bar as zero makes that bar quietly low, with nothing on screen to
say what wasn't counted.

Empty buckets are not filled in. GROUP BY only produces buckets that
have data, and how many slots to draw is something only the UI knows —
filling here would invent tens of thousands of zeros for a wide range
with a narrow bucket. The test says so explicitly, so the next reader
doesn't assume a dense series.

Four tests, including that local answers stay out of both aggregates
(§4.8 — cost 0, latency 0 would dilute every bar).

1066 tests, 42 smoke checks, clippy clean, verified against the real
binary with 25 recorded requests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@fylorn
fylorn merged commit e28d468 into main Sep 14, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant