Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

.nornir/stinky/17-buy-and-monetize-flow-vs-gunnar.md

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:

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).

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.