feat(screens): phase 9 — real screenshots, from a real shard
All checks were successful
PR checks / checks (pull_request) Successful in 1m25s
All checks were successful
PR checks / checks (pull_request) Successful in 1m25s
D4 asked for screenshots of the review stack rather than placeholders. Seventeen
of them: eleven of the site in a browser, six of the app on a phone, all from one
demo deployment wired to a running ServUO shard over a real sidecar, captured on
one day (D42).
The deployment is branded "Runic Gateway Demo" rather than a real community (D43),
and the captures sit beside the claims they support — the homepage, /features/, and
five of the administration pages phase 7 could describe but not show (D44).
The rig is committed rather than remembered (D45):
scripts/seedDemo.mjs content, by driving the site's own API — never SQL,
because a row the product could not have produced is
a screenshot of a product that does not exist
src/data/screens.mjs every capture: route, viewport, scroll, alt, caption
scripts/captureScreens.mjs npm run screens:capture
scripts/checkScreens.mjs the ninth check script, in CI
Shard-side dressing is servuo-plugins' scaffolding (D46), never deployed.
The rig found five things nothing else had. One is fixed upstream — a fresh
module-uo install pinned wire protocol 3 against a sidecar speaking 4, released as
v1.0.2, which this repo's own facts check then caught in platform.json. Four are
raised as product observations and worked around in the rig: a renamed guild
member never reaches the site, a guild deleted while the shard is down is a ghost
row forever, "Houses in danger" cannot show a house that was already collapsing,
and the app's news list prints raw ISO timestamps.
Players online reads 0. Logging a character in needs a UO client driven by hand,
and that is where this stopped — PLAN.md §10 says exactly why, and how to retake
the two frames that would change.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
81
src/components/Screenshot.astro
Normal file
81
src/components/Screenshot.astro
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
import { screenById, WEB, PHONE } from '../data/screens.mjs';
|
||||
|
||||
/**
|
||||
* One screenshot, as a figure with its caption. PLAN.md §13 phase 9, D4 / D44.
|
||||
*
|
||||
* -----------------------------------------------------------------------------------------
|
||||
* WHY THE PAGE PASSES AN ID AND NOTHING ELSE
|
||||
* -----------------------------------------------------------------------------------------
|
||||
* A marketing page and a documentation page show the same administration screen for
|
||||
* different reasons, and the thing they must not do is describe it differently. The alt
|
||||
* text and the caption therefore live with the capture in `screens.mjs`, next to the route
|
||||
* they came from, and a page asks for `admin-shard` rather than restating what is in it.
|
||||
*
|
||||
* It also means a re-capture cannot silently invalidate a caption: the sentence and the
|
||||
* frame it describes are edited in the same file.
|
||||
*
|
||||
* -----------------------------------------------------------------------------------------
|
||||
* WHY IT FAILS THE BUILD ON AN UNKNOWN ID
|
||||
* -----------------------------------------------------------------------------------------
|
||||
* The alternative is a page that renders a broken image, which looks like a deployment
|
||||
* problem rather than a typo and survives review. `checkScreens.mjs` covers the other
|
||||
* direction — a declared screen whose file is missing — so between them a screenshot is
|
||||
* either complete or the build stops.
|
||||
*/
|
||||
interface Props {
|
||||
/** An `id` from `src/data/screens.mjs`. */
|
||||
id: string;
|
||||
/** Suppress the caption where the surrounding prose already says it. */
|
||||
bare?: boolean;
|
||||
}
|
||||
|
||||
const { id, bare = false } = Astro.props;
|
||||
|
||||
const shot = screenById(id);
|
||||
|
||||
if (!shot) {
|
||||
throw new Error(`Screenshot "${id}" is not declared in src/data/screens.mjs`);
|
||||
}
|
||||
|
||||
const src = `/screens/${shot.id}.webp`;
|
||||
|
||||
// Intrinsic size comes from the family rather than the entry: every capture in a family is
|
||||
// taken at one geometry (see screens.mjs), and `checkScreens.mjs` asserts the files really
|
||||
// are that size, so these attributes cannot drift from the pixels.
|
||||
const { width, height } = shot.family === 'web' ? WEB : PHONE;
|
||||
---
|
||||
|
||||
<figure class="shot">
|
||||
<img
|
||||
src={src}
|
||||
alt={shot.alt}
|
||||
width={width}
|
||||
height={height}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{!bare && <figcaption>{shot.caption}</figcaption>}
|
||||
</figure>
|
||||
|
||||
<style>
|
||||
.shot {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.shot img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-card);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.shot figcaption {
|
||||
margin: 0.85rem 0 0;
|
||||
color: var(--dim);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -1,61 +1,36 @@
|
||||
---
|
||||
import { screensOf, PHONE } from '../../data/screens.mjs';
|
||||
|
||||
/**
|
||||
* The app's screenshot strip — defined now, empty until phase 9 (D26).
|
||||
* The app's screenshot strip. Reserved in phase 5 (D26), filled in phase 9.
|
||||
*
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* WHY A COMPONENT THAT RENDERS NOTHING IS WORTH COMMITTING
|
||||
* WHY THIS COMPONENT EXISTED FOR A PHASE WITH NOTHING IN IT
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* PLAN.md §10 says `/app/` shows "the 14 existing screenshots". They exist —
|
||||
* `docs/android/screenshots/` on the `docs` repository — and they are the wrong fourteen:
|
||||
* a trusted-device and recovery-code smoke test from 2026-07-22, captured against a
|
||||
* PLAN.md §10 said `/app/` shows "the 14 existing screenshots". They exist —
|
||||
* `docs/android/screenshots/` on the `docs` repository — and they are the wrong fourteen: a
|
||||
* trusted-device and recovery-code smoke test from 2026-07-22, captured against a
|
||||
* development instance with no seeded content, before the theming work that changed how
|
||||
* every screen looks. Five of them are two-factor prompts. The home shot is an empty page.
|
||||
*
|
||||
* Shipping them would break two things at once: D4, which says real screenshots from the
|
||||
* review stack rather than placeholders, and §1, because they would show an app that no
|
||||
* longer looks like that. D26 records the decision — the slot is reserved, phase 9 fills
|
||||
* it, and phase 9 is already the phase that stands up the review stack and seeds the
|
||||
* content the web screenshots need. Adding an emulator pass to a rig that is being built
|
||||
* anyway is most of the work already done, and it has the property that the phone shots
|
||||
* and the browser shots then show the same deployment on the same day.
|
||||
*
|
||||
* The component exists rather than the page carrying a `TODO` because a defined shape is
|
||||
* what makes phase 9 a data change instead of a design task: fill `shots`, and the section
|
||||
* appears with a heading, a caption line and a grid. Nothing else has to be decided then.
|
||||
* Shipping them would have broken D4 and §1 at once, so D26 reserved the slot for the phase
|
||||
* that stands up the review stack anyway. The shape was defined then and the data arrived
|
||||
* now, which is exactly what it was for: filling it was a data change.
|
||||
*
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* WHAT PHASE 9 SHOULD PUT HERE
|
||||
* WHY IT READS screens.mjs RATHER THAN HOLDING ITS OWN LIST
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* Portrait captures at the device's own pixel size, from an API 36 emulator pointed at the
|
||||
* seeded review stack, one per idea rather than one per screen: the shard hub with live
|
||||
* data, the marketplace, a character sheet, the news list, the notification settings, and
|
||||
* the drawer showing a deployment's own navigation. Six is plenty. Fourteen was never a
|
||||
* target — it was the number that happened to exist.
|
||||
* The draft carried its own `shots` array, written before there was anywhere else to put
|
||||
* one. There is now: `src/data/screens.mjs` holds every capture the site ships, web and
|
||||
* phone alike, and `scripts/checkScreens.mjs` proves each one exists at the size the markup
|
||||
* claims. A second list here would be the one nothing checks.
|
||||
*
|
||||
* They belong in `public/`, not `brand-default/`: these are editorial content shipped with
|
||||
* the image, not branding an operator overrides (§7).
|
||||
* The phone captures come from an emulator pointed at the same seeded deployment the web
|
||||
* screenshots were taken from, on the same day — which is the property D26 was really
|
||||
* after, since the app takes its colours, type and navigation from the site it connects to.
|
||||
*/
|
||||
|
||||
/**
|
||||
* One capture. `width` and `height` are the real pixel dimensions and are required rather
|
||||
* than optional: without them the page reflows as each image decodes, and a strip of six
|
||||
* phone screenshots is the worst possible place for that.
|
||||
*
|
||||
* Frontmatter is TypeScript, so this is an interface rather than the JSDoc typedef the
|
||||
* `.mjs` data files use — and it has to be typed explicitly, because an empty array
|
||||
* annotated by inference is `any[]` and `astro check` is right to refuse it.
|
||||
*/
|
||||
interface Shot {
|
||||
/** Site-absolute path under `/screens/`. */
|
||||
src: string;
|
||||
/** What the screen shows, for somebody who cannot see it. */
|
||||
alt: string;
|
||||
caption: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const shots: Shot[] = [];
|
||||
const shots = screensOf('phone');
|
||||
---
|
||||
|
||||
{
|
||||
@@ -63,8 +38,8 @@ const shots: Shot[] = [];
|
||||
<section class="page section shots">
|
||||
<h2>What it looks like</h2>
|
||||
<p class="prose shots__lede">
|
||||
Captured against a real deployment with real content, not mocked up. The app takes
|
||||
its colours, type and navigation from the site it is connected to, so these show one
|
||||
Captured against a real deployment with real content, not mocked up. The app takes its
|
||||
colours, type and navigation from the site it is connected to, so these show one
|
||||
community's app rather than a neutral one.
|
||||
</p>
|
||||
|
||||
@@ -72,10 +47,10 @@ const shots: Shot[] = [];
|
||||
{shots.map((shot) => (
|
||||
<li class="shots__item">
|
||||
<img
|
||||
src={shot.src}
|
||||
src={`/screens/${shot.id}.webp`}
|
||||
alt={shot.alt}
|
||||
width={shot.width}
|
||||
height={shot.height}
|
||||
width={PHONE.width}
|
||||
height={PHONE.height}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
|
||||
44
src/components/home/WhatItLooksLike.astro
Normal file
44
src/components/home/WhatItLooksLike.astro
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
import Screenshot from '../Screenshot.astro';
|
||||
|
||||
/**
|
||||
* The homepage's one screenshot. PLAN.md §13 phase 9, D4 / D44.
|
||||
*
|
||||
* -----------------------------------------------------------------------------------------
|
||||
* WHY ONE, AND WHY THIS ONE
|
||||
* -----------------------------------------------------------------------------------------
|
||||
* `DataPath` above it draws the claim — a private game server, a sidecar, a public site —
|
||||
* and a diagram of a data path is a promise that the data arrives. This is the page where
|
||||
* it arrives, captured from a deployment wired to a running shard, so the section directly
|
||||
* under the diagram is the diagram's evidence.
|
||||
*
|
||||
* A gallery here would compete with `Capabilities` further down, which is the part of the
|
||||
* homepage that enumerates. So: one figure, the signature screen, and the rest of the set
|
||||
* on `/features/` where each one sits beside the claim it supports.
|
||||
*/
|
||||
---
|
||||
|
||||
<section class="page section looks">
|
||||
<p class="eyebrow">What it looks like</p>
|
||||
<h2>The other end of that diagram</h2>
|
||||
<p class="prose looks__lede">
|
||||
A demo deployment with a real shard behind it. The gold supply, the uptime and the online
|
||||
count are read from the game server over the bridge — none of it is typed in, and the page
|
||||
is honest about a quiet realm.
|
||||
</p>
|
||||
|
||||
<Screenshot id="shard-status" />
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.looks h2 {
|
||||
margin: 0.35rem 0 0.75rem;
|
||||
font-size: clamp(1.6rem, 3.2vw, 2.1rem);
|
||||
}
|
||||
|
||||
.looks__lede {
|
||||
margin: 0;
|
||||
max-width: 46rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user