* fix: correct reprint threshold display and de-duplicate the formula (F5)
Print.vue recomputed the recovery threshold as floor(total/2)+2 and stored
the *total* in a field misleadingly named `requiredShards`. The reprint
sheets therefore overstated how many more QR codes are needed (e.g. total=5
showed "need 4" instead of the correct 3) — misleading during recovery,
though the QR payloads themselves were always untouched.
Root cause: the threshold policy was duplicated. Share.vue computes
floor(total/2)+1; Print.vue re-implemented it and drifted. Extract a single
`defaultThreshold()` helper (src/util/shards.ts) and use it in both, so they
can never disagree again. Rename Print.vue's field to `totalShards` to match
what it actually holds.
Add unit tests for the helper, including the exact F5 case (total=5 -> 3) and
a strict-majority invariant across the whole 3..255 UI range.
* fix: reject fractional shard counts, clamp the remaining-code count
Review follow-ups on the reprint view.
`<input type="number">` only constrains the spinner, not the value: typing
"3.5" (or clearing the box, which `v-model.number` leaves as "") sailed
through the old `>= 3 && <= 255` check and reached defaultThreshold(),
producing fractional totals and remaining counts. Add `step="1"` for the
spinner and gate on a shared `isValidShardCount()` predicate that also
requires a whole number, so the input and the threshold policy keep agreeing
about what a shard count is.
`needMoreShards` compared with `!==`, so scanning more codes than announced
kept the scanner open and drove `remainingCodes` negative. Compare with `<`
and clamp the remainder at 0.
`threshold` returned 0 for "nothing entered yet", a valid-looking value that
could reach ShardInfo's required-shards prop. Return `undefined` instead and
guard the consuming block on it, making the not-yet-entered state explicit.
The `threshold` and validation paths are covered by shards.spec.ts; the
scanner-overshoot clamp is not, as the repo has no component-test harness yet.
* fix: apply the same shard-count validation to the generator
Share.vue's shard-count input had the defect Copilot flagged on Print.vue's:
no `step`, no integer check, and `totalShards` fed straight into
crypto.share() — a fractional count surfaced as an opaque secrets.js error
routed through the generic error hub, and an emptied box passed "" through.
Reuse `isValidShardCount()` and gate the generate button on it, matching the
existing `secretTooLong` pattern (disabled button plus an inline error span),
so both shard-count inputs now agree on what a shard count is.
* fix: stop rendering a threshold derived from an invalid shard count
Gating the generate button left the sentence above it still interpolating
defaultThreshold(totalShards) for values that had just been rejected: an
emptied field coerces to 0 and renders "Will require any 1 shards", and 3.5
renders 2. Give Share.vue's `requiredShards` the same contract as Print.vue's
`threshold` — `undefined` unless the count is usable — show an em dash in the
sentence, and guard both the generated-shards block and crypto.share() on it.
Also strengthen the threshold invariant test. Asserting only
`defaultThreshold(n) > n / 2` does not pin the policy: floor(n/2)+2, the very
formula this branch removed, satisfies it for every even n. Assert the
smallest strict majority instead, which floor(n/2)+1 alone satisfies.
Now that the rules are much more pedantic there are quite some places
where we are inconsistent with codestyle; all security issues detected
were false positives, though.