Commit Graph

17 Commits

Author SHA1 Message Date
Grant 11cf1808c6 v0.2.0:17 — Discount Codes form usability
Three improvements to the Discount Codes tab:

1. Scope pickers replace text inputs. The create form's
   'Restrict to product slug' free-text input is now a dropdown
   populated from /v1/products. A dependent 'Restrict to policy'
   dropdown loads policies for the selected product on the fly.
   Both default to 'Any' so the no-scope global-code behavior
   is preserved.

2. datetime-local picker on expires_at. Native calendar + time
   spinner on both create + edit forms. Submit converts back to
   RFC3339 UTC automatically. Empty = no expiry.

3. Edit form shows scope read-only. 'Applies to: [product]
   -> [policy]' (or 'all products on this instance' for global
   codes) renders as a muted info block at the top. Scope
   remains immutable (disable + create new to change).

routes.codes now pre-fetches /v1/products once at the top
(reused for both the create form scope pickers and the per-
product table grouping). No more duplicate fetch.

UI-only release.
2026-05-11 13:08:49 -05:00
Grant 4334a9f044 v0.2.0:16 — Launch-special discount codes + marketing bullets
Major feature release.

Featured (launch-special) discount codes:
  - New 'featured' flag on discount_codes (migration 0017). When true,
    the buy page renders a diagonal LAUNCH SPECIAL ribbon + slashed
    original price + new price for every applicable tier. Purchase
    endpoint auto-applies the discount for buyers who don't type a
    code. Operator-typed codes still win.
  - find_applicable_featured_discount repo helper: most-specific match
    (policy > product > global), tiebreak by created_at.
  - GET /v1/products/<slug>/policies now returns featured_discount per
    policy with the post-discount price computed server-side. SDK
    consumers + the dynamic pricing page get this for free.

Marketing bullets on policies:
  - metadata.marketing_bullets — operator-controlled copy that renders
    as additional checkmarks above the entitlement bullets on both the
    admin grid tier card and the buy page tier. For things like 'Up
    to 5 products' or 'BTCPay integration' that aren't real
    entitlement gates.
  - Authored via textarea on draft + edit policy forms.

UI:
  - 'Most popular' checkbox now on the draft tier card (was edit-only).
  - Discount codes tab grouped by product (matching Licenses /
    Subscriptions tabs). Each code row gets a 'featured' badge when
    flagged.

All 87 tests still pass. Migration is additive, no SDK changes,
backwards-compatible.
2026-05-11 12:47:45 -05:00
Grant 2789d1da1f v0.2.0:15 — Multi-draft tier authoring + custom durations on draft cards
Two papercut fixes for the policy create flow:

1. Multi-draft survival. Previously, committing one draft tier card
   triggered a full grid reload via onMutate(), wiping any sibling
   drafts the operator had open. Now the commit callback receives the
   saved policy and replaces ONLY that draft's grid slot with a
   finalized tier card — sibling drafts keep their input state intact.
   Author Creator / Pro / Patron in parallel and click Create on each
   as it's ready, in any order.

2. Custom duration on draft cards. The Duration dropdown gains a
   "Custom (days)" option at the bottom; selecting it reveals a number
   input. On submit, days * 86400 = seconds is what gets sent. Matches
   the Edit-policy modal's existing custom pattern (which is in raw
   seconds); the draft uses days because day-based input is friendlier
   for the cadences operators actually pick.

UI-only release. No daemon code changes, no schema.
2026-05-11 11:38:47 -05:00
Grant 519fa1a8e6 v0.2.0:14 — Entitlements catalog read fix + drag-and-drop tier ordering
Bug fix:
  Product entitlements catalog reads were silently dropping. Every
  SELECT against the products table was missing entitlements_catalog_json
  from the column list, so the PATCH handler wrote the catalog correctly
  but every subsequent read returned null. Admin UI edits appeared to
  vanish on save. Fix: added the column to all four product SELECTs
  in repo.rs (list_products, get_product_by_slug, get_product_by_id —
  one column list, replace_all). Added regression test
  product_entitlements_catalog_round_trips_through_list_endpoint that
  exercises the full PATCH → list round-trip the admin UI hits.

UX:
  Drag-and-drop reordering on the tier-card grid. Operator drags any
  tier card to a new position; on drop, parallel PATCH requests set
  tier_rank 1..N based on the new visual order. Archived tiers are
  excluded (their position in the ladder is moot). Edit-policy modal
  retains the tier_rank number field for the two cases drag-and-drop
  can't express (precise override + blank-to-remove-from-ladder).
  Cursor signals grab/grabbing on hover/drag; dragging card lifts +
  fades for visual feedback.

Copy:
  Policies-tab section headers now show just the product name
  ("Keysat") instead of redundant "Keysat — keysat". Entitlements-
  catalog row editor description placeholder shortened from
  "Description (shown on buy page tooltip)" to "Description (buyer
  tooltip)" so it fits the column; full hover hint kept on the
  input's title attribute.

Test count: 87.
2026-05-11 11:14:20 -05:00
Grant 76fe7fe6b9 v0.2.0:13 — CORS on public endpoints
Adds tower-http CorsLayer at the outermost router position so:

- Browsers can fetch /v1/products/<slug>/policies, /v1/openapi.json,
  /v1/issuer/public-key, /v1/validate from any origin. Unblocks the
  dynamic pricing page on docs.keysat.xyz reading live tier config
  from licensing.keysat.xyz.
- Preflight OPTIONS is handled by the CorsLayer directly, never
  reaches the session-bridge or any handler — so admin endpoints
  don't 401 on preflight.

Security posture unchanged. Access-Control-Allow-Credentials is OFF.
The combination of ACAO=* and no-credentials means a cross-origin
page can read public responses but can't ride a logged-in admin
session cookie to hit /v1/admin/*. Admin endpoints still require
an explicit Bearer token, which browsers don't auto-attach
cross-origin.

Tests: +2 CORS regression tests (cors_allows_cross_origin_on_public_
endpoints, cors_preflight_returns_2xx_without_auth). Full suite:
85 passing.
2026-05-11 10:17:15 -05:00
Grant 257669092b v0.2.0:11 + v0.2.0:12 — Archive, Settings, agent surface, machines redesign
Two release cycles prepared together: v0.2.0:11 (policy archive + safe-
delete cleanup + brand-consistent confirm modals) and v0.2.0:12 (Settings
tab + agent-friendly operator API + machines tab redesign + buyer-facing
copy alignment).

Highlights:

- Migration 0015: policies.archived_at column. Archive button on tier
  cards; safe-delete relaxed to ignore revoked-license tombstones;
  renewal worker refuses archived policies.
- Migration 0016: scoped_api_keys table. Four roles (read-only,
  license-issuer, support, full-admin) with bounded scopes. Master
  admin_api_key still works on every endpoint; scoped keys gated on
  endpoints wired through require_scope().
- New /v1/openapi.json — public, no auth. Curated OpenAPI 3.1 spec
  for agent / SDK discovery.
- New Settings tab: Operator name + Payment providers panel + API
  keys management. Replaces 8 StartOS Actions (Zaprite all, BTCPay
  all, operator name, switch-provider). StartOS Actions pruned to 4
  install-time essentials.
- Machines tab rewritten: global default view grouped by product,
  filter pills with counts, quick-stats row, drill-down via new
  "Machines" button on each Licenses-tab row. New repo helper
  list_machines_admin joins machines x licenses x products
  server-side.
- Branded confirmModal replaces every native window.confirm() call
  in the admin UI (7 callsites).
- Enforce mode killed: KEYSAT_LICENSE_ENFORCE compile-time flag
  retired; daemon always boots; missing self-license -> Creator
  (free) tier. "Unlicensed" label gone from admin UI.
- Zaprite gated on the new zaprite_payments entitlement (renamed
  from card_payments to reflect the broader gateway).
- Creator code cap 5 -> 10.
- KEYSAT_AGENT_GUIDE.md: auth, role-to-scope mapping, error envelope,
  webhook events, worked recipes.
- Buyer-facing copy aligned with new positioning: "Bitcoin-native
  self-hosted software licensing" everywhere on production surfaces.
- Cross-product safety section (Section 9a) added to KEYSAT_INTEGRATION.md.
- 5 new API integration smoke tests covering OpenAPI, scoped API
  keys CRUD, role-elevation guard, and Zaprite-tier gating.

Test count: 83 passing (was 78). All migration tests pass against
0015 and 0016 applied to populated DBs.
2026-05-11 08:45:25 -05:00
Grant 20b5293c81 v0.2.0:10 — Licenses + Subscriptions tabs reorganized by product
Both tabs now group by product (matching the per-product card
sections in Products + Policies), with product-filter pills + per-
product counts at the top. Multi-product instances see one section
per product with a status breakdown subtitle ("3 active · 1
revoked · 2 expired"); single-product instances continue to see a
flat table with no chrome overhead. Search results bypass grouping
(search is global across all products).

Three new shared helpers added at the top of the script:

- clickToCopy(fullValue, displayLabel) — clickable code element
  that copies the full ID to clipboard with a "✓ copied"
  indicator. Replaces the older hover-to-see-full-id UX for
  license / subscription IDs.

- relativeDate(rfc3339, opts) — renders an RFC3339 timestamp as
  a human-relative string ("in 3 days" / "12 hours ago") with
  the absolute timestamp in a hover tooltip. Applied to license
  issued/expires + subscription next_renewal.

- reasonModal({title, message, warning, confirmLabel,
  confirmVariant}) — inline overlay-card replacement for the
  native prompt() / confirm() dialogs. Used by:
  * Subscription cancellation flow
  * License suspend / unsuspend / revoke flows
  Same UX language as the Change Tier modal.

Subscriptions tab specifics:
- Product filter pills with per-product counts (filtered by
  active status filter so the counts reflect what the operator
  is currently viewing).
- Status filter pills gain counts (Active (3), Past due (0), etc.)
- New Product column shows display name + slug.
- Status badges have hover tooltips explaining each state's meaning.
- Cancel button uses reasonModal instead of prompt().

Licenses tab specifics:
- Quick-stats row: Licenses / Active / Revoked / Expiring < 30d.
  Scope follows the active product filter; hover "?" icons
  define each stat. Mirrors the Overview dashboard style.
- Search affordance preserved; search results render as a single
  flat table titled "Search results" (not grouped by product).
- Manual-issue form's hint blocks replaced with help icons on
  every field. Compact-form treatment to match Products + Policies.
- Suspend / unsuspend / revoke buttons use reasonModal with
  per-action context (irreversible warning on revoke, etc.)
  instead of confirm() + prompt() double-dialog.
- Entitlements rendered with display name primary + description
  tooltip (resolves against the product's catalog from
  /v1/products's response).

Pure UI release. 78/78 tests still pass. No schema, SDK, or
behavior change.
2026-05-10 12:07:06 -05:00
Grant 0ea3469899 v0.2.0:9 — side-by-side tier-card policy authoring + form polish
The Policies tab gets the redesign Grant asked for: replace the
table view + verbose disclosure form with a card grid where each
existing policy renders as a buy-page-style tier card sitting next
to a dashed "+ Add tier" placeholder. Click the placeholder, it
morphs into an editable draft tier card with inline form fields;
submit Create on the card and it flips into a read-only preview.
Multiple drafts can coexist for parallel multi-tier authoring with
side-by-side comparison.

New JS helpers:
- helpIcon(text) — small "?" hover tooltip for compact form labels
- slugify(s) — URL-safe slug derivation from display name
- renderTierCard(pol, product, onMutate) — read-only buy-page-style
  preview card with Edit / Hide-Show / Delete actions
- renderAddTierCard(onClick) — dashed placeholder with "+" affordance
- renderDraftTierCard(product, onCommit, onCancel) — inline editable
  card with name + slug + price + duration + entitlement bubble
  picker + recurring/trial toggles
- renderPolicyCardGrid(product, policies, byPolicyCounts, onMutate) —
  ties them together. Submitting "+ Add tier" appends a fresh
  placeholder, so operators can keep clicking to author multiple
  tiers in one session.

formInput() upgraded:
- New `help:` option renders a helpIcon next to the label (replaces
  verbose hint text under the input)
- New `placeholder:` option for cleaner empty-state cues

Auto-slug:
- Product create form's Display name field mirrors a slugified
  version into the Slug field as the operator types — until they
  manually edit the slug, which arms a "userOverridden" guard so
  manual edits stick. Re-arms when the slug field is cleared.

Legacy "Create a new policy" disclosure form unsurfaced from
the Policies route — the card grid replaces it. Advanced fields
(custom grace seconds, tip recipient, tier rank) still live on the
existing Edit modal of an already-committed tier card. Power-user
flow: card grid creates the basics, Edit modal refines.

Test count unchanged (78). UI-only release.
2026-05-10 10:23:07 -05:00
Grant 4b9ef0ea8c v0.2.0:8 release notes + integration doc section 8 update
Notes cover the entitlements catalog feature shipped in 68dfe7f
plus the four SDK 0.3.0 cuts (TS / Rust / Python / Go) that
surface the catalog on listPublicPolicies. Phase 2 (side-by-side
card-grid policy authoring UI) is queued for v0.2.0:9.

KEYSAT_INTEGRATION.md section 8 grows a subsection explaining the
catalog mechanics: bubble picker, buy page rendering, SDK surface,
catalog-stability rule.

Test count: 78 (unchanged from :7 except for migration_0014 already
counted in the prior commit).
2026-05-10 08:01:43 -05:00
Grant b95b47e0d5 v0.2.0:7 — align package copy with website positioning
The Start9 registry card was still showing "Keysat — self-hosted
Bitcoin-paid software license server" while keysat.xyz now leads
with "Bitcoin-native self-hosted licensing service for software
creators." Operators landing on the registry from the marketing
site got a jarring tagline mismatch.

Aligned everywhere the old copy was hardcoded:

- startos/manifest/i18n.ts (short + long descriptions — these
  drive the registry card)
- assets/ABOUT.md (in-StartOS About panel)
- README.md (root + licensing-service/)
- licensing-service/Cargo.toml description

Long description also picked up two updates that should have
landed when the features did but never made it into the marketing
copy:
- Zaprite mention (Bitcoin + cards) alongside BTCPay
- Recurring subscriptions + in-place tier upgrades

Pure copy change. No code, no behavior, no schema. Republishing as
:7 because the registry card text lives inside the .s9pk and
won't refresh on operators' boxes without a version bump.
2026-05-09 19:10:26 -05:00
Grant ff92ed0463 v0.2.0:6 release notes — recurring + trials + self-tier live refresh
Big release notes pass covering the testing-driven fixes from
2fbd36f (P0 batch), 54f7ea0 (P1 bundle), 927ac2b (UX polish),
89d505b (integration doc).

Test count callout: 77 (unchanged from :5).
2026-05-09 14:04:36 -05:00
Grant 58939d1dc6 v0.2.0:5 release notes — tier upgrades functional end-to-end
Bumps the milestone version + writes the operator-facing release
notes covering the complete tier-upgrades feature delivered across
8ce78ab (Phase 1 schema), f8affdb (Phase 2 quote/apply), b7fa6c7
(Phase 3 buyer endpoints + webhook), c5d716a (Phase 4 admin
endpoint + renewal-worker hook), and fb062d5 (Phase 5 admin UI).

Test count callout: 77 (was 57 at v0.2.0:4).
2026-05-08 20:16:14 -05:00
Grant 6112618c1b v0.2.0:4 release notes — recurring subscriptions functional end-to-end
Bumps the milestone version + writes the operator-facing release
notes covering the complete recurring-subs feature delivered across
7007bf8 (Phase 2 worker), c301eac (Phase 4 admin UI + buy page),
5d7f68f (Phase 6 cancellation backend), and 4bdc506 (Phase 6 cancel
UI).

Test count callout: 57 (was 42).
2026-05-08 18:04:51 -05:00
Grant 667db6ffd4 v0.2.0:3 release notes — durable provider switching
Bump with notes covering the active_payment_provider preference,
the new Activate <provider> actions, and the symmetric Disconnect
handling.

Test count: 42.
2026-05-08 16:51:47 -05:00
Grant 0a76c9d121 v0.2.0:2 release notes — Zaprite + recurring subs schema
Bump to v0.2.0:2 with notes covering Zaprite as second payment
provider, migration 0011 (recurring subs schema dormant), 0012
(zaprite_config). Test count 41.
2026-05-08 16:35:40 -05:00
Grant 622fa77e29 v0.2.0:1 — drop FOUNDERS50 placeholder from buy-page discount input
Per operator feedback: the discount-code field on /buy/<slug> was
showing 'FOUNDERS50' as a placeholder, which confused buyers (some
tried it as a real code, some assumed Keysat shipped a default
discount). Empty placeholder now; buyers paste their actual code.

No semantic change. Wrapper-only revision; daemon binary unchanged
beyond the embedded HTML template.
2026-05-08 13:41:17 -05:00
Grant 02f80b04eb v0.2.0:0 plumbing prep — draft version file + cutover doc
Adds startos/versions/v0.2.0.ts as a draft milestone version entry,
ready to swap in as `current` when we're ready to cut. NOT yet wired
into the version graph at versions/index.ts — flipping that switch
is a release decision (one-line change there, then make x86 +
publish), and the draft sits parked so we can iterate on the
release-notes content without committing to the cut.

Format note: the SDK's VersionInfo.of() expects releaseNotes as a
LocaleString (Record<string, string>), not the string[] form
v0.1.0.ts uses. The new file uses the modern shape; v0.1.0.ts keeps
its existing form to avoid churn on the alpha line.

CUTTING_V0.2.0.md walks the operator (or future me) through the
4-step cutover: edit versions/index.ts to swap in v0_2_0, npm run
check, make x86, publish. Plus rollback notes if anything goes
sideways post-cut.

Why park rather than cut now:
1. The user said "prepare for the version 0.2 plumbing" — that's
   "prepare" not "do". The cutover is intentional in the user's
   workflow, not bundled into a routine push.
2. Cutover changes how the StartOS marketplace renders the upgrade
   dialog to existing :N installs; best to QA the release-notes
   content first.
3. SDK migration-API behavior on the upstream version bump is
   worth verifying on a test install before flipping for everyone.

The v0.2.0 release notes themselves are written conservatively —
they describe what's already shipped and stable in the alpha line
through :47, not aspirational v0.3 features.
2026-05-08 11:41:55 -05:00