Skip to content

Admin panel store: author and preview, never buy (#102) - #109

Merged
CaYatur merged 2 commits into
mainfrom
feat/panel-store-authoring
Jul 28, 2026
Merged

Admin panel store: author and preview, never buy (#102)#109
CaYatur merged 2 commits into
mainfrom
feat/panel-store-authoring

Conversation

@CaYatur

@CaYatur CaYatur commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Admin panel store: author and preview, never buy (#102)

The panel's Store tab rendered the buyer's storefront, Buy button and all, and
POST /api/servers/:id/store/buy was reachable with the view scope. An
operator opening their own store to check how a crate looks could spend
currency by clicking the thing that looks like a preview — and view is the
scope you hand someone who should be able to look and nothing else.

The panel is where a store is authored. The buyer is a player on the public
site, signed in with their own Minecraft name and their own balance.

SF.mode now tells the shared storefront what it is for. It defaults to
'preview', which is the fail-safe direction: a host that forgets to declare
itself gets a storefront that cannot spend anything. The public site sets
'buy'; the panel sets 'preview' and, when the viewer has the store scope,
SF.canEdit.

In preview the card action plays the product instead of buying it — a crate
rolls its own pool with its own configured animation, so the operator sees what
a player sees — and a dashed frame above the grid says so. Sold-out products
stay clickable there: with nothing to spend, the one that ran out is the one
most worth opening. Each card and the detail view also carry an Edit button
that opens the real product editor, resolving the id against the admin
catalogue first, because the storefront only holds the public shape and the
editor needs the commands that shape strips. New Item / New Crate sit in the
tab header.

The balance badge went with the Buy button; an operator's own balance is not
information about the store.

Breaking API change: POST /api/v1/servers/{id}/store/buy is removed, not
re-gated. A store-scoped key can already grant balance and deliver rewards, so
moving the same capability behind a different scope name would only hide it.

Asserting this needed running the pages, not reading them: both paste the same
STORE_JS, so every card in both carries the same sfAction(...) attribute and
a source grep cannot tell them apart. The smoke runs each page's own bootstrap,
checks the mode each one declares for itself, then presses a card's action and
watches for a request that spends money — required on the site, forbidden in the
panel. The route removal is checked with an owner token, so a refusal cannot
pass for a missing scope.

The double-spend and purchase-audit tests moved to the public site, which is now
the only surface that buys.

The panel's Store tab rendered the buyer's storefront, Buy button and all, and
`POST /api/servers/:id/store/buy` was reachable with the `view` scope. An
operator opening their own store to check how a crate looks could spend
currency by clicking the thing that looks like a preview — and `view` is the
scope you hand someone who should be able to look and nothing else.

The panel is where a store is authored. The buyer is a player on the public
site, signed in with their own Minecraft name and their own balance.

`SF.mode` now tells the shared storefront what it is for. It defaults to
`'preview'`, which is the fail-safe direction: a host that forgets to declare
itself gets a storefront that cannot spend anything. The public site sets
`'buy'`; the panel sets `'preview'` and, when the viewer has the `store` scope,
`SF.canEdit`.

In preview the card action plays the product instead of buying it — a crate
rolls its own pool with its own configured animation, so the operator sees what
a player sees — and a dashed frame above the grid says so. Sold-out products
stay clickable there: with nothing to spend, the one that ran out is the one
most worth opening. Each card and the detail view also carry an Edit button
that opens the real product editor, resolving the id against the admin
catalogue first, because the storefront only holds the public shape and the
editor needs the commands that shape strips. New Item / New Crate sit in the
tab header.

The balance badge went with the Buy button; an operator's own balance is not
information about the store.

**Breaking API change**: `POST /api/v1/servers/{id}/store/buy` is removed, not
re-gated. A store-scoped key can already grant balance and deliver rewards, so
moving the same capability behind a different scope name would only hide it.

Asserting this needed running the pages, not reading them: both paste the same
`STORE_JS`, so every card in both carries the same `sfAction(...)` attribute and
a source grep cannot tell them apart. The smoke runs each page's own bootstrap,
checks the mode each one declares for itself, then presses a card's action and
watches for a request that spends money — required on the site, forbidden in the
panel. The route removal is checked with an owner token, so a refusal cannot
pass for a missing scope.

The double-spend and purchase-audit tests moved to the public site, which is now
the only surface that buys.
Copilot AI review requested due to automatic review settings July 28, 2026 17:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…heck

.sf-modal is z-index 75 and .pm-modal is 50, both fixed and full-screen, so the
product editor opened from an open detail rendered underneath it behind a dimmed
backdrop. The same bug as the site login modal fixed in #106, in the affordance
this PR adds. sfEdit closes the detail first.

The Edit button is gated on SF.canEdit, set from the viewer scopes — but the
smoke seeded a store-scoped session and only asserted the button was there, so
deleting the check left the test green. The render is now re-run with canEdit
false and the button must be gone.

sfEdit's failure path used alert(), in the change that removes the panel store's
last one. It uses sfNotice.
@CaYatur

CaYatur commented Jul 28, 2026

Copy link
Copy Markdown
Owner Author

Self-review

Two defects, both in the Edit affordance this PR adds.

1. The product editor opened underneath the detail view it was opened from

.sf-modal (the product detail) is z-index: 75; .pm-modal (the editor) is
50. Both are position: fixed; inset: 0. So clicking Edit inside an open
product detail opened the editor behind it, under a dimmed backdrop — the
operator sees the page go dark and nothing else.

Exactly the bug reported on the public site's login modal and fixed in #106, in
a new surface, written by me while the fix for the old one was three commits
back. A modal opened from a modal has to say which one wins; sfEdit now closes
the detail first, the same way buy() does.

Asserted, and the assertion was proved able to fail — removing the
sfCloseDetail() gives
FAIL - opening the product editor left the detail on top of it.

2. Nothing proved the store-scope check on Edit did anything

sfEditable() gates the Edit button on SF.canEdit, which the panel sets from
the viewer's scopes. The storefront is visible with view and editing needs
store, so those are not the same audience.

But the smoke seeds current.scopes as ['view','store'] and then asserts the
Edit button is present. That assertion passes whether or not canEdit is
consulted — deleting the check entirely leaves it green. A permission test that
only ever tests the permitted case is not a permission test.

Now the render is re-run with canEdit false and the button must be gone.
Proved failable: dropping !!SF.canEdit from sfEditable() gives
FAIL - the panel offers to edit products to a viewer who cannot.

Also changed

sfEdit's failure path used alert(). This is the PR that removes the panel's
last alert() on the store path; adding a new one back in the same change was
not defensible. It uses sfNotice, which the panel now has.

Considered, not changed

The preview shows what publicStore() publishes, so hidden products are absent
from it — correct for a preview and awkward for authoring, since a hidden
product cannot be reached from the tab that previews it. The Manage tab still
lists them, and making the preview show what players cannot see would defeat
the point of it being a preview.

Verified

Twelve gates green.

@CaYatur
CaYatur merged commit ad44649 into main Jul 28, 2026
1 check passed
@CaYatur
CaYatur deleted the feat/panel-store-authoring branch July 28, 2026 17:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants