# The console's wasm bundle, in bytes on disk, as last measured and accepted.
#
# WHAT THIS NUMBER IS
#
# A ratchet, not a budget. CI fails when the built bundle exceeds this by more
# than 5%, which detects a CHANGE — a dependency that arrived, a page that
# monomorphised badly — and nothing else. It has never expressed an opinion
# about how big the console is allowed to be, and if you read it as one you will
# be misled: it has been raised every time it bound, which is what a ratchet
# does and what a budget must not do.
#
# The budget that does mean something is the hard ceiling in `.github/
# workflows/ci.yml`, on the COMPRESSED size, because that is what a person
# waits for. `gunnar-ui-server` gzips and brotlies the static files
# (`serve::console_files`), so this on-disk number is not what crosses the wire.
#
# WHAT THE BUNDLE IS MADE OF (measured 2026-08-31, twiggy, name section excluded)
#
# 53% leptos tachys 27%, reactive_graph 18%, leptos_router 4%,
# either_of 3% — the framework, not the console
# 12% core fmt, float formatting, panic machinery
# 8% gunnar_ui_web the console's OWN code
# 7% data segments .rodata
# 6% serde_json
#
# 1 380 889 bytes on disk · 520 239 gzip · 383 428 brotli
#
# Single largest item: `EitherOf12::choose` from leptos_router, 48 092
# bytes — one monomorphisation per route count. Adding a thirteenth
# top-level route makes it an EitherOf13 and costs several more kilobytes.
#
# opt-level="z", lto, codegen-units=1 and `wasm-opt -Oz` are ALL already on
# (workspace `Cargo.toml`, `index.html`). wasm-opt buys about 7%. There is no
# oversized dependency left to remove — `url`/`idna` pull ICU in at compile time
# but every byte of it is dead-code-eliminated. Shrinking this meaningfully
# means fewer top-level routes, or not Leptos.
#
# WHEN IT BINDS
#
# Raising it is allowed. Raising it SILENTLY is not. Before you change the
# number:
#
# 1. Find out where the bytes went, don't guess:
# cd crates/gunnar-ui-web
# # temporarily: data-wasm-opt="0" data-keep-debug in index.html
# trunk build --release --dist dist-names
# twiggy top -n 30 dist-names/*_bg.wasm
# (wasm-opt strips the name section, so the shipped bundle cannot be
# attributed — that is why the flags come off for the measurement.)
# 2. Say in the commit message WHAT grew and why it is worth it. "the bundle
# grew" is not a reason; "the diff view brought syntect's parser in, and it
# is 40 KB for the feature the whole page exists for" is.
# 3. If the compressed size would cross the ceiling in ci.yml, the answer is
# not a bigger number. It is a code change: split a route, drop a
# dependency, or load a page lazily.
#
# RE-MEASURED 2026-09-08, and the number moved a long way. Two separate things
# happened and only one of them is the mirroring feature:
#
# 1 380 889 what this file said, measured 2026-08-31
# 1 771 262 the same commit's descendant BEFORE any mirroring code
# (gunnar-ui a37ac7e) — +28% from the work that landed between
# those dates: the usage/Room table, the accounts surface, the
# monetize panel and the chain view. **The ratchet was already
# bound and nobody moved the number**, which is precisely the
# silent-raise this file forbids, arriving as a silent NON-raise.
# 1 941 928 with mirroring: /mirrors, /mirrors/import, the per-repository
# panel, and the row they share.
#
# So mirroring is +170 666 bytes on disk (+9.6%) and +65 609 gzipped (+10.1%)
# of the 561 039 total drift. What it bought: six components — the picker's
# table, the connect form with two doors, the mirror row with its progress bar
# and three buttons, the import summary, and the panel on a repository page.
# Measured by stubbing them one at a time: the picker alone is 69 101 on disk
# and 26 881 gzipped; the panel, row and connect form are the rest.
#
# One thing was tried on this file's own advice and did NOT work, so it is
# written down rather than tried again: the note above says the single largest
# item is `EitherOf12::choose`, one monomorphisation per top-level route count,
# so /mirrors and /mirrors/import were merged into one route selected by
# `?import`. Measured 1 945 196 — **3 268 bytes LARGER**. On leptos 0.8 the
# `<Show>` that replaced the route costs at least what the route did. The two
# routes went back, because the address reads better and nothing was bought.
#
# ⚠ THE GZIP CEILING IN ci.yml IS NOW CROSSED, and raising this number does not
# address it. 716 684 gzipped against a 655 360 ceiling. Note where the console
# already was BEFORE mirroring: 651 075, which is 99.3% of the ceiling — the
# next feature of any size was always going to cross it. That is a decision
# about the product (split a route, drop leptos_router's fatter pieces, load a
# page lazily, or move the ceiling and say why), not a number to edit quietly,
# and it is left to whoever owns it.
#
# RE-MEASURED 2026-09-08 (paid signup, console half). +12 649 bytes on disk
# (+0.65%) and +5 137 gzipped (+0.72%): 1 954 577 on disk, 721 821 gzipped
# against the 819 200 ceiling ci.yml now carries — 11.9% of headroom left.
#
# What grew, and it is one component: `pages/account::PushKey`, the first-run
# nudge that asks a brand-new account for an SSH public key. It adds a `Show`,
# two `LocalResource`s over lists the console already fetches elsewhere, and
# renders `access::AddKeyForm` — the SAME component the Keys page uses, not a
# second one, which is why five kilobytes buys a whole panel. No route was
# added, so `EitherOf12` did not become an `EitherOf13`.
#
# The number is moved rather than left alone even though 0.65% does not bind
# the 5% tolerance: that is what a ratchet is, and this file has already been
# bitten once by a raise nobody made.
#
# RE-MEASURED again the same day, after the ruling that a paid user must be
# able to add their own SSH key: 1 958 565 on disk, 722 743 gzipped. +3 988 and
# +922 on top of the line above — `pages/account::OwnKeys` replaced `PushKey`
# and grew a table with a Remove button, and `access::AddKeyForm` went back to
# being the Access page's alone. Still no new top-level route, so `EitherOf12`
# is still an `EitherOf12`. 11.8% of headroom under the 819 200 ceiling.
#
# RE-MEASURED 2026-09-10 (the Activity panel: `Usage.Activity`, the FLOW
# series). Both numbers are from the SAME box and the same toolchain, built
# twice — once on the tree as it stood and once with the panel — because a
# figure from one machine compared with a figure in a file is not a delta:
#
# 1 962 798 on disk · 724 478 gzip the tree BEFORE the panel
# 1 999 586 on disk · 739 827 gzip with it
#
# +36 788 on disk (+1.87%) and +15 349 gzipped (+2.12%). Against the 819 200
# gzip ceiling in ci.yml that is 90.3% used and 79 373 bytes — 9.7% — of
# headroom left, down from 11.6%.
#
# Note the first line against the 1 958 565 this file said: **+4 233 bytes had
# already drifted in before this change**, from work that landed after
# 2026-09-08 without the number being moved. Well inside the 5% tolerance, so
# nothing failed and nobody noticed — which is the exact shape of the silent
# non-raise the paragraph above is about, arriving a second time. It is folded
# into the new number here rather than left to accumulate.
#
# WHAT GREW, and it is three things, none of them a dependency:
#
# * `pages/usage::Flow` and `pages/usage::DayChart` — the panel and the chart
# it draws twice (bytes in/out, then pushes/fetches). Thirty `<li>` slots
# per chart, each with two `<span>`s and a sentence, and one more
# `LocalResource`: `Usage.Activity` is its own RPC and is read on its own
# rather than folded into the meters read, so a gunnar too old to answer
# the series still serves the rest of the page.
# * `activity` — the pure module the panel's words and bar arithmetic live
# in, host-testable like `room`. Its three empty states are three
# sentences, and that is deliberate: an empty chart can mean *nothing
# moved*, *this box does not count activity* or *this could not be read*.
# * `fmt::date` — Hinnant's `civil_from_days`, about twenty lines. **This is
# what a date crate was NOT added for**: `chrono` and `time` are tens of
# kilobytes of wasm for a calendar the console uses in one place, and the
# ceiling is 9.7% away.
#
# No new top-level route, so `EitherOf12` is still an `EitherOf12` — the panel
# hangs off `/usage/:namespace`, which already existed.
# RE-MEASURED 2026-09-09 (the README panel). 1 979 992 on disk, 729 475
# gzipped: +21 427 and +6 732 over the line above — but only +15 685 and
# +4 179 of that is this change. The rest is drift measured on the same
# commit before any README code (1 964 307 / 725 296), which is what the
# gunnar-forge sibling moving from 1.3.0 to 1.4.0 costs; it is recorded here
# rather than folded in silently, because this file has been bitten once by a
# raise nobody made.
#
# What grew, and it is one component: `pages/readme::ReadmePanel`. One
# `LocalResource`, one four-way match on `kind`, two `inner_html` sinks and a
# `<pre>`. Fifteen kilobytes for a panel is the going rate here (compare
# `account::OwnKeys` at +3 988 for a table, and the mirror picker at +69 101
# for a page) and it buys the repository page the thing a person came to it
# for. No markdown parser and no SVG library crossed the wasm boundary — the
# server does all of it and the console sets a string. No new top-level
# route, so `EitherOf12` is still an `EitherOf12`.
#
# 729 475 gzipped against the 819 200 ceiling: 11.0% of headroom left, down
# from 11.8%.
# RE-MEASURED 2026-09-09, after the README panel moved onto edda's
# `nornir-docbook` and grew a Typeset toggle: 1 985 937 on disk, 730 910
# gzipped. +5 945 and +1 435 over the line above — one `RwSignal`, a `<Show>`
# and a button whose label swaps. The engine change cost the bundle NOTHING,
# because none of it is in the bundle: the console still parses no markdown,
# sanitises nothing and links no SVG library. It reads `kind` and sets a string.
#
# 730 910 gzipped against the 819 200 ceiling: 10.8% of headroom left.
# RE-MEASURED 2026-09-09, after the typeset page became the README panel's
# DEFAULT view: 1 986 403 on disk, 731 592 gzipped. +466 and +682 over the line
# above, and that is the whole of it — a `RwSignal` renamed, a query string that
# went from `?as=svg` to `?as=html`, and a button whose two labels are now
# "HTML" and "Typeset". Four hundred bytes.
#
# The feature this is the console half of is nowhere in this number, which is
# the point worth recording. `nornir-docbook` 0.1.2 gives the typeset SVG a
# selectable text layer — an invisible `<text>` per shaped run — and that costs
# about +23% of the SVG's bytes, on the SERVER, in a cached response. None of it
# crosses the wasm boundary: the console still parses no markdown, sanitises
# nothing, links no SVG library and renders no text layer. It reads `kind` and
# sets a string. The response a reader downloads got bigger; the program that
# downloads it did not.
#
# No new top-level route, so `EitherOf12` is still an `EitherOf12`.
#
# 731 592 gzipped against the 819 200 ceiling: 10.7% of headroom left, down
# from 10.8%.
# PORTED 2026-09-10: the three README-panel re-measures above were made on the
# private lineage and never reached the public tree, which had the panel carved
# out. The panel is back; this number is the 2026-09-10 Activity figure and has
# NOT been re-measured with the panel present. Measure and move it.
# MEASURED 2026-10-05 (encrypted repositories: open and diff in the tab), both
# builds on ONE box (oden, trunk 0.21.14, `trunk build --release`), the tree
# before (gunnar-ui eabb4e0) and after:
#
# 2 701 718 on disk · 1 001 401 gzip eabb4e0, BEFORE
# 2 804 912 on disk · 1 048 322 gzip with encrypted files and diffs
#
# +103 194 on disk (+3.82%) and +46 921 gzipped (+4.69%). What grew:
#
# * gunnar-crypt's `cipher` (AES-256-SIV: aes, cmac, ctr; base64) — the
# envelope format the CLI also runs. No age, no key generation, no SSH
# parsing in the bundle (until CRYPT-YUBI: see below).
# The cache key is the key file's text, so no hash function is linked.
# * gunnar-crypt's `diff` (imara-diff, the same histogram diff the console
# server uses): +28 174 on disk / +13 525 gzipped of the total, measured by
# building once without it. The server cannot diff ciphertext and keeps no
# pre-made diffs, so this is the only place an encrypted diff can happen.
# * Two components: `browse::SealedFile` and `diff::SealedDiff`. No route.
#
# The number below is NOT moved by this change, and that is deliberate: on
# this box the tree BEFORE it is already 2 701 718 (+35% over the number) and
# 1 001 401 gzipped (over the 819 200 ceiling). That drift predates this work
# and is a product decision for whoever owns the ceiling; folding it in here
# would be the silent raise this file forbids.
# MEASURED 2026-10-26 (CRYPT-YUBI: the local unlock helper removed; encrypted
# files open with the person's security key in the tab), both builds on ONE
# box (builder-64, trunk 0.21.14, `trunk build --release`), gunnar-ui main
# f7e6b52 BEFORE and crypt-yubi/security-key-2026-10-26 after:
#
# 3 666 956 on disk · 1 359 920 gzip f7e6b52, BEFORE
# 4 120 066 on disk · 1 558 431 gzip with the security key
#
# +453 110 on disk (+12.4%) and +198 511 gzipped (+14.6%). What grew: age
# 0.11.5 (no `ssh` feature, no PQ) for the person's encryption key — its
# X25519 and scrypt stanzas, its STREAM payload, and the fluent/i18n-embed
# message tables age always links — plus hkdf and bech32 for the PRF
# derivation. The console must open the age wrap itself: there is no helper
# to ask any more, and a hand-written age reader would be a home-made one.
# The number below is not moved, for the reason the 2026-10-05 note gives.
1999586