Commit Graph

2 Commits

Author SHA1 Message Date
Grant 938eedc99f Mobile responsiveness pass — buy / recover / thank-you
The recurring-subs work just added new tier-card content (cadence
line + trial banner + /mo suffix), so a quick pass on the three
buyer-facing pages was timely. Targeted, CSS-only changes.

Buy page (`/buy/<slug>`):
- h1 uses clamp(28px, 7vw, 42px) so it scales smoothly from phones
  to desktop without cliff-edge breakpoints. The fixed 42px was
  cramping 360-380px viewports.
- New @media (max-width:480px) breakpoint tightens the outer rhythm:
  topbar padding, wrap margin, cert padding, price size, tier-card
  padding, etc. The desktop 48px outer + 32px cert padding ate too
  much of a small viewport.
- Form input font-size pinned to 16px on mobile so iOS Safari
  doesn't auto-zoom when the buyer taps the email or discount field.
  (iOS zooms on any <16px input, which interrupts the buy flow.)
- Tier picker already had a 560px breakpoint dropping to 1-column;
  unchanged.

Recovery page (`/recover`):
- Default input/button font-size raised to 16px (iOS zoom fix).
- New @media (max-width:480px) breakpoint reduces outer body
  padding (48px → 24px) and main padding (32px → 22px), tightens
  h1 + label, and bumps button padding for thumb-friendly tap
  targets.

Thank-you page (`/thank-you`):
- Adds a @media (max-width:480px) block — previously it had zero
  breakpoints. Mirrors the buy-page pattern: tighter topbar, wrap
  margin, card padding, h1 fluid scaling, lede + footer sizing.

Admin UI is operator-side and not addressed in this pass. Could be
revisited if operators report mobile pain points; for now the
buyer-facing surface is the priority because that's where buyers
actually arrive on phones.
2026-05-08 18:07:06 -05:00
Grant f6ba1c160e Buyer self-service recovery + db-info admin endpoint
Two operator-facing additions, both addressing risks we'd flagged
earlier in the v0.2 plan but hadn't shipped.

**POST /v1/recover (+ GET /recover HTML form).** Lets a buyer who
lost their license key re-derive it themselves by presenting their
invoice id + the email they paid with. Until now, the recovery
flow was "DM the operator with your invoice id and they re-send" —
operator-time scaling badly. With this, the buyer self-serves and
the operator never has to know.

The endpoint takes (invoice_id, email), case-insensitive on email.
Returns a generic 404 on any mismatch — does NOT distinguish
"invoice not found" from "wrong email" so an attacker can't
brute-force email addresses against a known invoice id. Per-IP
rate limited at 10 requests / minute. Audit-logged as
license.recovered with the email's SHA-256 hash so PII isn't
written to the log.

The HTML form at GET /recover is server-rendered, no JS framework,
no cookies — designed for a customer who's just had a catastrophic
failure of their primary computer and reached us from whatever
device they could find.

Test in tests/api.rs:recover_returns_license_key_for_matching_pair
exercises the happy path (case-insensitive email match), the
generic-404 paths (wrong email, missing invoice), the round-trip
(recovered key validates via /v1/validate), and the audit-log
write.

**GET /v1/admin/db-info.** Cheap insurance against the
catastrophic-loss risk: /data/keysat.db is a single SQLite file,
losing it invalidates every license ever issued. StartOS's backup
machinery handles snapshotting; this endpoint gives operators a
sanity-check surface they didn't have before:
  - DB file path + on-disk size
  - last-write timestamp (max across audit_log, invoices, licenses)
  - row counts for products, policies, licenses (total + active),
    invoices (total + settled), machines (active), discount codes,
    audit log entries

Doesn't report when StartOS last backed it up — the daemon has no
visibility into the host's snapshot subsystem. What it gives the
operator is a "I expected ~50 licenses and I see ~50 licenses; the
file is N MB; the last write was 6 hours ago" check.

Test count: 31 (was 30; +1 for the recover test).
2026-05-08 11:05:10 -05:00