A11: Holger's buy and monetize flow, against Gunnar's (2026-10-23)
The owner's order: "no signup button at the front page. Let's redo the GAP analysis, read from Gunnar, and make a stinky list of how to get the same buy flow and monetize flow."
This is a read of the LIVE sites first, and of the code second. Live: gunnar.rs, test2.gunnar.rs, holger.rs and test3.holger.rs (Holger release caaf16f5), and the ONE box monetize.ignalina.dk (monetize 51e774b5). All reads were GETs with curl. Nothing was bought and no form was posted.
Code read: gunnar-ui main a80d73e, holger main cbf89f0, holger-ui main 10d3eec, monetize main 51e774b, private-monetize-ops (pmo) main 0a723e1. The live Holger assets are byte-identical to main: front.html, login.html, login.js, signup.html and signup.js all diff clean against the live pages.
Short names: GU = gunnar-ui crates/gunnar-ui-server/src, HF = holger holger-front, HS = holger server/lib/src, HU = holger-ui, HUS = holger-ui crates/holger-ui-server/src, M = monetize crates/monetize-server/src, PMO = private-monetize-ops. FLOW = ~/FLOW-gunnar-signup-buy-2026-10-22.md.
1. The landing: why the owner sees no sign-up button
Gunnar (gunnar.rs and test2, live). GET / with Accept: text/html and no session answers 303 /login?next=/ (GU auth/middleware.rs:698-700). Without that Accept header it answers 401 "log in to use the console". So the first page a visitor sees is the 56 KB login page, and it holds the split control:
-
The markup is one row,
<div class="entry-buttons">, with two equal-width children:<!--@entry-offer-->, then<button id="login" class="primary">Log in</button>(GUauth/login.html:331-345). The row isdisplay:flex; gap:.6rem, and each child isflex:1 1 0(login.html:222-223). -
In a
meteredbuild,routes::login_htmlsplices three METERED-only pieces into the page (GUauth/routes.rs:45-57):login-offer.cssgoes in at/*@entry-style*/(login.html:224). It stylesa.entry-buttonas a filled button (raised ground, ink text, 6 px radius) and keeps[hidden]asdisplay:none.login-offer.htmlgoes in at<!--@entry-offer-->. It is<a class="entry-button" id="paid-link" href="/signup" hidden>Sign up account</a>, a link drawn as a button that ships hidden (login-offer.html:7).login-offer.jsgoes in at/*@entry-script*/(login.html:644), inside thefetch('/auth/status')handler (login.html:621).
-
What the JS reads:
/auth/statussignup_url. When it is anhttps://URL or a rooted local path (not//and not/\), the script setslink.href = s.signup_urlandlink.hidden = false(login-offer.js). The server buildssignup_urlinShop{…}(GUauth/routes.rs:321-341) and sends it at:432-437. Live, it is"/signup", withmay_buy:true,buy_url:"/signup?plan=10gb"andprivate_url:"/signup?plan=private". -
What each half does:
- "Sign up account" goes to
/signup, the ONE sign-up page. There the visitor chooses "Account on " (preselected) or "Private Gunnar instance" (FLOW 0.4). - "Log in" runs the passkey ceremony in place. When no passkey answers, the page says "New here? Sign up first." if the sign-up half is visible, and otherwise asks for an invite (
login.html:711-718).
- "Sign up account" goes to
-
An open (not metered) build has only the one "Log in" button, the full row wide.
Holger (holger.rs and test3, live). GET / answers 200 with holger-front's 46 KB marketing page, "holger — artifacts at rest" (HF assets/front.html, served by HUS lib.rs:103, route_of("/") = Door).
- It has one button,
#enter. Its label comes from/-/front, and live it answers{"login":{"label":"Open the console","start":"/login","door":"link"}}(HFsrc/lib.rs:395CONSOLE_LABEL). - The only sign-up on the page is a dim, small text paragraph:
<p class="offer" id="signup-offer" hidden>Host your packages here: <a href="/signup">sign up</a> for an account …</p>(HFfront.html:325). It is shown when/-/signupanswersaccount_urlorprivate_offered(front.html:513-519). Live it is shown, as.offertext atfont-size:.86remin--ink-dim(front.html:151). It is a text link, not a button. That is the owner's "no signup button". - The Holger
/loginpage has no sign-up at all. Live it shows "Log in with passkey" and the line "No account here? An administrator mints you a single-use invite link." (HUassets/login.html:198, 201). On a site the shop sells for, that line is wrong. - Holger has no
/auth/status. Live,/auth/statusreturns the SPA'sindex.html(22 KB). Holger's equivalent isGET /-/signup(HSexposed/http.rs:1091,room.rs:140-172). Live it answersaccount_url = https://monetize.ignalina.dk/shop/signup?product=holger&site=holger.rs&from=account,private_offered:trueandprivate_url = …/shop/private?product=holger&site=holger.rs.
Root idea. Gunnar's first page is the sign-in page, and it carries a split "Sign up account | Log in": two equal buttons, and the sign-up half exists only where the server says the site sells. Holger's first page is a marketing page with one "Open the console" button and a text link. Holger's /login has no sign-up half.
2. The table
Status: SAME; PARTIAL; MISSING; BROKEN (the step is there but does not work live); DATA (pmo); SHARED (the same gap in Gunnar). "Live" means what curl saw on 2026-10-23.
| # | Step | Gunnar live | Holger live | Code (Gunnar / Holger) | Status | Fix (repo, size) |
| 1 | Visitor landing → the sign-up entry (the button) | / → 303 /login?next=/; split "Sign up account | Log in", equal width; the sign-up half is revealed by /auth/status.signup_url |
/ = marketing page; one button "Open the console" → /login; sign-up is a small dim text link |
GU login.html:222-224, :331-345, :621, :644; routes.rs:45-57, :321-341; login-offer.{html,css,js}; middleware.rs:698-700 / HF front.html:315, :325, :513-519; src/lib.rs:395 | MISSING | holger-front: the same split row on /: "Sign up account" (an a.entry-button, hidden, revealed by /-/signup) beside the login button, which says "Log in". UI only (front.html + one const). Small. Holger release |
| 2 | Sign-in page's sign-up half | /login is the split page |
/login: "Log in with passkey" only, plus "An administrator mints you a single-use invite link." |
GU as row 1 / HU assets/login.html:198, :201; login.js | MISSING | holger-ui: the same split on /login, revealed by /-/signup. The invite line is said only where nothing is sold, and "New here? Sign up first." where it is. UI only. Small. Holger release |
| 3 | The sign-up page's two choices | /signup: "Account on " (preselected) | "Private Gunnar instance" |
/signup (HU): the same two choices, drawn from /-/signup; "Choose your plan" → the shop |
GU signup.html:635-655 / HU assets/signup.js:37-62; HS room.rs:140-172 | SAME (A10 row 1 holds live) | — |
| 4 | Free account, 0 kr card check | in-console plan list, the free row first, the card check at Stripe, /welcome polls |
shop page /shop/signup?product=holger&site=holger.rs: free row "Create your account at holger.rs", the card check note and the BETA line; return /shop/signup/welcome?site=holger-rs (live answers "That link is not one of ours." with no session, as it should) |
GU signup.html:364-365; M services/signup.rs:397-452, :539-622 / M shop_signup.rs:150-166, :344, :437-439 | SAME (A10 rows 4, 7 hold live) | — |
| 5 | Paid plan | 1 m and 6 m plans in SE/FI/DK, the market from Accept-Language, "Create your account" → Stripe |
one row "5 GB … 100.00 SEK a month — Pay 100.00 SEK"; always SEK: /-/signup names no market and the shop has no fallback |
GU auth/signup.rs:651-700 (market_of_languages) / M shop_signup.rs:173-180, :218 (only &market=); HS room.rs:155 |
PARTIAL | monetize: the shop reads Accept-Language when the link names no market (as Gunnar's console does). Small. ONE box |
| 6 | Plan menu | 10 GB 1 m and 6 m (10 % off) per market | 5 GiB 1 m per market | PMO solo.toml:414-452 / :529-545 | DATA, owner's call | pmo rows, if the owner wants them; not built here (no unasked additions) |
| 7 | Private instance, visitor | /signup?plan=private (today: the account first via the card check, then the order) |
shop /shop/private: "New private Holger", account, e-mail, company, own domain, five sizes, "Pay 385.20 EUR" (6 months) on holger.rs, "Pay 3.78 EUR" (1 day) on test3; pay once |
GU signup.html:747-752, :883-906 / M shop_private.rs | SAME, and ahead of Gunnar (pay once, the BEING BUILT target) | — |
| 8 | Signed in: New instance / Buy a plan / Grow — the entry | Account page button "Buy a plan" / "Grow your account"; "Your purchases" → grow/renew links | /signup draws the signed-in choices, but nothing in the console links /signup. The console only has "More room in the shop" per repository on /mine |
GU WEB purchase.rs:451, :848-869; api/subscription.rs:1708-1709 / HU src/app.rs:134-162 (no route or link to /signup); src/pages/mine.rs:238-255 |
PARTIAL (A10 row 2 is fixed on the page, not reachable) | holger-ui: a link to /signup in the signed-in console ("Buy a plan" / "New instance"), plain href. UI only. Small. Holger release |
| 9 | Signed in on /signup from a new tab or a mail link |
cookie session (SameSite=Strict): the signed-in view | the session is per tab (sessionStorage holger-bearer-token): a new tab sees the visitor choices |
GU middleware / HU assets/signup.js:20-23; login.js:173, :227 | PARTIAL, by design | owner's call (a session cookie is a design change) |
| 10 | Buy a plan / Grow your account: the page | in-console list in English: the current plan "your plan now", rows with price_words "for one ", "Upgrade" |
ONE shop /shop?product=holger&repository=<r>: Swedish ("Köp — holger", "Mer utrymme för x", "Växer"), rows "5 GiB — disk_bytes 5368709120", no price, then /kop |
GU signup.html:785-798; api/subscription.rs:1367-1590 / HS room.rs:268-285; M kop.rs (shop list and /kop) |
BROKEN for a buyer (no price, raw bytes, another language) | monetize: the shop list and /kop in English, with the price and period from Purchase.Quote/the plan, and the button "Upgrade". Medium. ONE box |
| 11 | Buy a plan / Grow: back from Stripe | /paid/{reference} polls /api/account/subscription/status; /cancel/{reference} "Payment cancelled … Back to your account" |
Stripe returns to the vendor's success_url = https://holger.rs/paid/{reference} (pmo feed), which Holger does not serve: live it is the SPA fallback (22 KB, no route). On test3 the vendor is stripe/gunnar-test2, so a test3 buyer is sent back to test2.gunnar.rs/paid/… The shop's own /kop/klar is never reached |
GU WEB purchase.rs:918-958 / PMO xtask/src/solo.rs:875-876; solo.toml:115-118, :582; M kop.rs:12, :486-545; HUS lib.rs:100-152 | BROKEN | holger-ui-server: /paid/{reference} and /cancel/{reference} are sent to holger, and holger answers 303 to its shop's /kop/klar?reference=<ref> (the kop cookie is the shop's, so Lax carries it), which goes on to return= (Holger's /mine). Small. Holger release. test3's return host is DATA: a holger-test3 Stripe instance with site = "test3.holger.rs" (pmo, small, owner's webhook) |
| 12 | Grow 's instance | "Grow 's Gunnar", ladder, "Upgrade", hold for the days left | &change=<co> on the shop's private page (A10 row 17) |
GU auth/private.rs:364-394 / HS room.rs:164-167; M estate_order/change.rs | SAME in code; not reachable from the console (row 8) | row 8 |
| 13 | Cancel / back: account and plan | Stripe back → /signup?plan=10gb or ?plan=private |
back → https://<site>/signup?plan=account (code) |
GU auth/signup.rs:911-914 / M shop_signup.rs (A10 row 6) | SAME (code; not clicked live) | — |
| 14 | Cancel / back: private order | the vendor cancel URL (Gunnar has no private cancel page) | https://<site>/signup?plan=private via reserve_returning (in 51e774b) |
M estate_order.rs:3212 | SAME, and ahead of Gunnar | Gunnar follow (A10 "still open") |
| 15 | Cancel / back: grow | /cancel/{reference} pane |
https://holger.rs/cancel/{reference} → the SPA fallback |
as row 11 | BROKEN | row 11 |
| 16 | Welcome mail and sign-in link (free account) | account.sign_in → /welcome?session= |
account.sign_in → /shop/signup/welcome?…&session= → https://<site>/login?signup=…&user=… |
M services/signup.rs:350, :362, :798 / M shop_signup.rs:169-171 | SAME | — |
| 17 | Sign-in mail on a paid plan | none sent (only make_account mails), yet the welcome says "We've sent a sign-in link" |
the same | M services/signup.rs:350, :362 (the only callers) | SHARED gap | monetize: finish_paid_signup/settle mails account.sign_in once when CREATED. Small. ONE box (fixes Gunnar too) |
| 18 | Passkey and signed in at once | "Your security key is registered. Signing you in …" | the same sentence; the session is stored (HU login.js:223-228) | GU login.html:868-875 / HU login.js:223-230 | SAME (A10 row 10 holds live) | — |
| 19 | Read only until paid | free fact: 1-byte cap, no paid_until; "read and clone, not push" |
holger/account cap_bytes = 0; "read and fetch, but not upload" |
M signup.rs:397 / PMO solo.toml:551-555 | SAME | — |
| 20 | Stripe checkout once | one Checkout per sign-up / order | the same (the shop is product-generic) | M services/signup.rs:838-1127 | SAME | — |
| 21 | Prices and currency from monetize | Signup.Plans / ladder |
the same, on the shop pages | — | SAME (but row 5: always SEK) | row 5 |
| 22 | Receipts | Stripe: tax_code, managed_payments = false, the same block |
the same block for stripe/holger |
PMO solo.toml vendors (gunnar, holger) |
SAME | — |
| 23 | Nonpay at 10 days | purge_after_days = 10, grace_days = 3 |
purge_after_days = 10 (A10 row 19 DATA is now done) |
PMO solo.toml:358-366 / :471-475 | SAME | — |
| 24 | Refusal and pay links | entitlement refusal names the pay page | refusal_link fed by the ONE box (account_url_of → https://holger.rs/): the front page, which offers a signed-in person no buy button |
GU gunnar-server entitlement.rs:1360-1381 / HS late.rs:103; M registry.rs:690-700 | PARTIAL | rows 1 and 8 (the front page and the console then carry the way to buy) |
| 25 | Shop pages' look | Gunnar's own console style | monetize's plain shell (system-ui, blue buttons); the title says "— Holger" | M shop_signup.rs shell_in |
PARTIAL, cosmetic | later |
3. A10's 16 "Fixed" claims, checked live
| A10 row | Live check | Verdict |
| 1 two choices | /signup draws both from /-/signup (visitor) |
holds |
| 2 signed-in choices | drawn on /signup, but no console link reaches it |
holds on the page only → row 8 |
| 3 heading / "Pay " | "New private Holger", "Pay 385.20 EUR", "Pay 100.00 SEK" | holds |
| 4 free row | the card check and BETA lines are live | holds |
| 5 links | /-/signup answers account_url, private_url, private_offered |
holds (no market, row 5) |
| 6 cancel | in 51e774b (code); not clicked | holds in code |
| 7, 8 welcome | /shop/signup/welcome live, validates its site and session |
holds (no session tried) |
| 9 mail link | code | holds; the paid plan sends none (row 17, shared) |
| 10 signed in after the passkey | live login.js:223-228 | holds |
| 11 visitor pays once | live shop private form | holds |
| 12 no e-mail field when signed in | needs a ticket; code only | not checkable live |
| 16 grow account | the link is there; the page is Swedish with no prices; the return lands on a page Holger does not serve | does not hold live → rows 10, 11, 15 |
| 17 grow | code; not reachable from the console | partial → row 8 |
| 18 no cap shown | code | holds |
| 23 rename | code | holds |
4. Ranked stinky list: what blocks a buyer first
| Rank | Fix | Kind | Repo | Deploys where |
| 1 | holger.rs / gets the same split "Sign up account | Log in": an a.entry-button beside #enter, equal width, hidden until /-/signup answers account_url (https) or private_offered; the button says "Log in"; the small text link goes |
UI only (holger-front reads Holger's own /-/signup) |
holger (holger-front/assets/front.html, src/lib.rs CONSOLE_LABEL) |
Holger release |
| 2 | /login gets the same split; "New here? Sign up first." where the shop sells, the invite line only where it does not |
UI only | holger-ui (assets/login.html, login.js) |
Holger release |
| 3 | Grow return: /paid/{reference} and /cancel/{reference} on a Holger site go on to the shop's /kop/klar?reference= (today the SPA fallback) |
UI routing + one holger door | holger-ui-server (route to holger), holger (/paid/… → 303 to the shop) |
Holger release |
| 4 | The grow page in English, with prices: the shop's list and /kop show " — for " and "Upgrade", not "disk_bytes 5368709120" in Swedish |
monetize | monetize (kop.rs) |
ONE box |
| 5 | A way to buy from the signed-in console: "Buy a plan" / "New instance" → /signup |
UI only | holger-ui (src/app.rs nav, or pages/mine.rs) |
Holger release |
| 6 | The market from the browser: the shop reads Accept-Language when the link names no market (FI → EUR, DK → DKK) |
monetize | monetize (shop_signup.rs, shop_private.rs) |
ONE box |
| 7 | The sign-in mail on a paid plan (shared with Gunnar): mail account.sign_in once when a paid sign-up is CREATED |
monetize | monetize (services/signup.rs) |
ONE box |
| 8 | test3's grow return host: a holger-test3 Stripe instance (site = "test3.holger.rs", the same test account) so that test3 never returns to test2.gunnar.rs |
ops data | pmo | ONE box feed (the owner adds the webhook) |
| 9 | The session per tab: /signup from a new tab shows visitor choices |
design, owner's call | holger-ui / holger | — |
| 10 | The plan menu (6 m plans), the shop pages' look | data / cosmetic, owner's call | pmo / monetize | ONE box |
Fixes 1, 2, 3 and 5 are Holger release only (holger + holger-ui). Fixes 4, 6 and 7 are monetize, deployed on the ONE box. Fix 8 is pmo data, fed to the ONE box.
5. Implementation (2026-10-23)
Each fix was built red test first, then green, merged --no-ff to main and pushed. Nothing is deployed. ~/script-a11-holger-release-and-one-box-2026-10-23.sh ships it all: the ONE box live update first, then the Holger KVM gate, media, release and feed, then estate update of test3-holger-rs and then holger-rs, each checked live.
| Fix | Repo | Commits (RED, GREEN, merge) | Tests |
1 the split on / |
holger | 212f863, its GREEN, merge 43e3783 | holger-front 51/0 |
2 the split on /login |
holger-ui | RED, GREEN, merge ae62cb5 | holger-ui-server green |
| — the vendored proto follows holger main (proto_is_holgers was red on main) | holger-ui | merge 287dd31 | workspace 128/0 |
3 /paid/ and /cancel/ go on to /kop/klar |
holger, holger-ui | holger merge 3e69e22; holger-ui merge 41c38b9 | server-lib 534/0 (one timing flake in grow under load, green alone 3/3), +metered 546/0; holger-ui 129/0 |
| 4 the grow pages in English, room in people's units | monetize | 8d2a205, GREEN, merge 3f0b942 | lib 747/0, kop 6/0, shop_intake 3/0 |
| 5 the console's way to buy | holger-ui | RED, GREEN, merge bdbc768 | workspace 130/0, the wasm build is clean |
6 the market from Accept-Language |
monetize | RED, GREEN, merge d65018b | lib 748/0 |
| 7 the sign-in mail on a paid plan | monetize | RED, GREEN, merge 53d60e9 | paid_signup 29/0, lib 748/0 |
| 8 test3's return host | pmo | not built: it needs a holger-test3 Stripe instance with its own webhook, which only the owner can add |
— |
| 9, 10 | — | the owner's call | — |
What is still not like Gunnar after this: the grow list shows the price only on the next page, because the product's ticket is spent by one quote. test3's grow purchases return to test2.gunnar.rs until fix 8 is done.