ci/woodpecker/push/woodpecker Pipeline was successful
A store with paid titles had nothing to tell this client and no way for it to listen: the catalog carried no price, no entitlement and no sign-in, so a gated download could only come back 403 and leave the window guessing why. The knowledge belongs on the server, not here. This client serves whichever catalog a registry names, so anything it knew about a particular shop would be a rule that breaks every other one. WarpEngine 0.5 answers GET /api/service with what it offers and puts an `access` block on every entry; this reads both. There is no store name anywhere in the diff. - **0.5 is a dialect of its own**, the older shape with `access` added. The version list is exhaustive over the selector, so adding it was a compile error until somebody said what it reads like — which is what that switch is for. - **A card shows a price and a Buy button** when a title is not yours, opening the store's own page. Buying stays in a browser: a checkout rebuilt here would be a second place to get card handling wrong. - **Signing in is the device grant**: a short code, the person's own browser, and no password crossing this window. The token goes in the OS keychain through safeStorage — one per store — and where no keychain exists it is not stored at all rather than written out in the clear. - **Owned / To buy** join the categories, since owning something is not the same as having installed it. Three things worth stating about the shape: The bearer token stops at the origin that issued it. A gated download redirects to signed storage — often somebody else's host — and some object stores refuse a request outright when an Authorization header arrives alongside the signature. An absent access block is not "free". It is an engine too old to have an opinion, and only one of those two is a reason to offer somebody a sign-in, so the three states are kept apart all the way to the card. state.json does not carry entitlement. Whether somebody may download a title is the server's answer to a question asked now; a copy on disk would go stale on the next purchase or refund, and a stale yes is the dangerous direction. A store with no sign-in shows none, and every WarpEngine before 0.5 is such a store: no Account block, no prices, no new categories. The smoke test against the live catalog reports exactly that — `sign-in: not offered`, `access: open:13`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
133 lines
5.4 KiB
HTML
133 lines
5.4 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<!-- Nothing is loaded from the network except box art, and no inline code
|
|
runs: the app ships its own script and stylesheet. -->
|
|
<meta http-equiv="Content-Security-Policy"
|
|
content="default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' https: data:; font-src 'self'; connect-src 'none'">
|
|
<title>WarpEngine Client</title>
|
|
<link rel="stylesheet" href="style.css">
|
|
</head>
|
|
<body>
|
|
<header class="bar">
|
|
<button id="nav-toggle" class="nav-toggle" aria-controls="side" aria-expanded="true">
|
|
<span aria-hidden="true">☰</span>
|
|
</button>
|
|
<div class="bar-title">
|
|
<span class="logo" aria-hidden="true">▚</span>
|
|
<span id="app-name">WarpEngine Client</span>
|
|
<!-- Kept for the window check, which reads it to see which store is open; the
|
|
side menu is where a person reads that. -->
|
|
<span class="store-id" id="store-id" hidden></span>
|
|
</div>
|
|
<div class="bar-actions">
|
|
<span class="progress" id="progress" hidden></span>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="shell">
|
|
<!-- The side menu: which store, what to do with it, and what to look at.
|
|
Collapsed with the button in the bar; the choice is remembered. -->
|
|
<aside class="side" id="side">
|
|
<section class="side-block">
|
|
<h2 class="side-head" id="head-stores"></h2>
|
|
<div class="store-list" id="store-list"></div>
|
|
<button id="add-store" class="btn btn-ghost btn-wide"></button>
|
|
</section>
|
|
|
|
<!--
|
|
Signing in, and who is signed in. The whole block is hidden where the catalog
|
|
offers no sign-in at all, which is most of them: a greyed-out button is telling
|
|
somebody about a door that does not exist.
|
|
-->
|
|
<section class="side-block" id="account-block" hidden>
|
|
<h2 class="side-head" id="head-account"></h2>
|
|
<p class="side-quiet-text" id="account-state"></p>
|
|
<button id="account-action" class="btn btn-ghost btn-wide"></button>
|
|
</section>
|
|
|
|
<section class="side-block side-cats">
|
|
<h2 class="side-head" id="head-cats"></h2>
|
|
<nav class="cats" id="cats"></nav>
|
|
</section>
|
|
|
|
<section class="side-block side-foot">
|
|
<button id="log-toggle" class="side-quiet" aria-expanded="false" aria-controls="log-panel"></button>
|
|
<!--
|
|
Two icons and nothing else. Both carry their name in `title` and `aria-label`,
|
|
set from the message bundle, so the tooltip and the screen reader stay
|
|
translated while the button stays the size of its glyph.
|
|
-->
|
|
<div class="side-tools">
|
|
<button id="refresh" class="icon-btn" disabled>
|
|
<svg class="icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
|
|
<path d="M13.5 8a5.5 5.5 0 1 1-1.61-3.89" />
|
|
<path d="M13.5 2v3h-3" />
|
|
</svg>
|
|
</button>
|
|
<!--
|
|
The select is still a real `<select>`, stretched over the icon and invisible:
|
|
the native dropdown knows how to open upward in a cramped window and is
|
|
already keyboard- and screen-reader-navigable, which a hand-rolled menu would
|
|
have to earn back.
|
|
-->
|
|
<label class="icon-btn side-lang" id="locale-control">
|
|
<svg class="icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
|
|
<circle cx="8" cy="8" r="6" />
|
|
<path d="M2 8h12" />
|
|
<path d="M8 2c1.8 1.6 2.8 3.8 2.8 6S9.8 12.4 8 14C6.2 12.4 5.2 10.2 5.2 8S6.2 3.6 8 2Z" />
|
|
</svg>
|
|
<select id="locale" class="locale-select"></select>
|
|
</label>
|
|
</div>
|
|
</section>
|
|
</aside>
|
|
|
|
<div class="content">
|
|
<!--
|
|
The code to type into a browser. A panel over the grid rather than a screen of
|
|
its own: the catalog is still there and still readable, and the sign-in is
|
|
something happening elsewhere that this window is only reporting on.
|
|
-->
|
|
<section id="signin" class="signin" hidden>
|
|
<h2 id="signin-title"></h2>
|
|
<p id="signin-body"></p>
|
|
<p class="signin-code" id="signin-code"></p>
|
|
<p class="signin-waiting" id="signin-waiting"></p>
|
|
<div class="signin-actions">
|
|
<button id="signin-open" class="btn btn-secondary"></button>
|
|
<button id="signin-cancel" class="btn btn-ghost"></button>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Shown instead of the grid when there is nothing to drive yet. -->
|
|
<section id="gate" class="gate" hidden>
|
|
<h1 id="gate-title"></h1>
|
|
<p id="gate-body"></p>
|
|
<div class="gate-actions">
|
|
<label id="gate-choice" class="gate-choice" hidden>
|
|
<span id="gate-choice-label"></span>
|
|
<select id="gate-select" class="select"></select>
|
|
</label>
|
|
<button id="gate-action" class="btn btn-primary" hidden></button>
|
|
<a id="gate-link" class="link" href="#" hidden></a>
|
|
</div>
|
|
</section>
|
|
|
|
<main id="grid" class="grid" hidden></main>
|
|
|
|
<section id="empty" class="empty" hidden></section>
|
|
|
|
<!-- Only on screen while the log is open; the window has no permanent footer. -->
|
|
<section class="log-panel" id="log-panel" hidden>
|
|
<div class="log-lines" id="log-lines"></div>
|
|
<div class="log-paths" id="log-paths"></div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="app.js"></script>
|
|
</body>
|
|
</html>
|