Skip to content

refactor(app-store): read app data from the metadata API, not local JSON - #221

Merged
Alexgodoroja merged 4 commits into
mainfrom
feat/appstore-meta-api
Aug 25, 2026
Merged

refactor(app-store): read app data from the metadata API, not local JSON#221
Alexgodoroja merged 4 commits into
mainfrom
feat/appstore-meta-api

Conversation

@Alexgodoroja

Copy link
Copy Markdown
Collaborator

The site and the Alpha management console both render an app store, and each
built it from its own copy of the data. scripts/gen-apps.mjs assembled
records from app-overrides.json + app-methods.json + app-demos.json;
the console embedded a snapshot pasted across from here by hand.

They drifted — 25 of 27 apps carried different summary copy, and one
app's console blurb had been taken from a different paragraph of the
description entirely.

What changes

Both surfaces now read one served document
(app-template#101).
Editing an app means editing one file in pilot-protocol/app-template under
appstore-meta/data/apps/ and redeploying the API. Nothing about an app's
copy is decided in this repo any more, so the three local JSON sources are
removed rather than left to rot as a second truth.

Which glyph a mask icon is cut from now travels in the document as
icon.mark, so the id-to-glyph table this script used to hold is gone too.
That table was the last piece of app data still hardcoded here.

The build must not depend on a live service

The fetched document is written to src/data/app-metadata.json and
committed. If the API is unreachable the build uses that snapshot and carries
on, one release behind at worst. An empty or malformed response counts as
unreachable rather than as an empty store. Only a missing API and a
missing snapshot is fatal, and --offline skips the fetch outright.

npm run gen:apps                                   # fetch, snapshot, generate
APPSTORE_META_URL=http://host/v1/appstore/metadata npm run gen:apps
node scripts/gen-apps.mjs src/data/apps.ts --offline

Verification

  • apps.ts regenerated from the API is semantically identical to the
    committed one: same 27 apps in the same order, same categories, same
    featured order, and no field differing on any app. The textual diff is
    explicit nulls where optional keys used to be omitted.
  • The exported TypeScript shape is unchanged, so every page reading apps.ts
    is untouched.
  • npm run build: 407 pages, clean. /app-store and /apps/<id> render
    their shelves, methods and demos as before.

Note for reviewers

The src/data/apps.ts diff is large but mechanical — it is the explicit-null
serialisation above. The semantic comparison is the thing to trust; the
generator is the file worth reading.

The site and the Alpha management console both render an app store, and
each built it from its own copy of the data. This script assembled records
from app-overrides.json + app-methods.json + app-demos.json; the console
embedded a snapshot pasted across from here by hand. They drifted — 25 of
27 apps carried different summary copy, and one app's console blurb had been
taken from a different paragraph of the description entirely.

Both now read one served document. Editing an app means editing one file in
pilot-protocol/app-template under appstore-meta/data/apps/ and redeploying
the API; nothing about an app's copy is decided in this repo any more, so
the three local JSON sources are removed rather than left to rot as a second
truth.

Which glyph a mask icon is cut from now travels in the document as
icon.mark, so the id-to-glyph table this script used to hold is gone too.
That table was the last piece of app data still hardcoded here.

A static build must not depend on a live service. The fetched document is
written to src/data/app-metadata.json and committed; if the API is
unreachable the build uses that snapshot and carries on, one release behind
at worst. An empty or malformed response counts as unreachable rather than
as an empty store. Only a missing API *and* a missing snapshot is fatal, and
--offline skips the fetch outright.

Verification
------------

- apps.ts regenerated from the API is semantically identical to the
  committed one: same 27 apps in the same order, same categories, same
  featured order, and no field differing on any app. The textual diff is
  explicit nulls where optional keys used to be omitted.
- The exported TypeScript shape is unchanged, so every page reading apps.ts
  is untouched.
- npm run build: 407 pages, clean. /app-store and /apps/<id> render their
  shelves, methods and demos as before.
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

🚀 Preview deployed to Cloudflare Pages

  • Commit deploy URL: https://c642afa8.pilotprotocol.pages.dev
  • Branch alias: https://feat/appstore-meta-api.pilotprotocol.pages.dev (may take ~30s to propagate)
  • Commit: 1a20683df3b88ec7391a6d1fbc1a6e744051e48c

Alexgodoroja added 3 commits August 25, 2026 15:26
appstore-meta.pilotprotocol.network now resolves and serves over TLS, so the
snapshot and the generated banner record that rather than the VM's IP. Every
app record is byte-identical to the copy generated from the IP — only the
recorded source URL changes.
Auditing the app-store surface against the served schema turned up four
data-model faults. All predate the API switch, but the switch is what makes
them worth fixing now: the app list is no longer local, so a reference to a
field or an id that does not exist can no longer be checked by reading this
repo.

`astro check` reported all four; none is caught by `astro build`, which does
not type-check.

- **`app.pricing` was dead.** The app page carried a whole Pricing section
  guarded on a field that is not on the `App` type and that no generator has
  ever produced, so the guard was always false and the section never
  rendered. Removed, with its now-unreferenced markup. Cost is already shown
  from `productDemo.cost`, which is real data.

- **`app.isNew` was dead.** The card rendered a "New" badge on a field that
  does not exist, so the badge never appeared. Removed. `publishedAt` is now
  served for 13 apps, so the badge could be revived off real data — that is a
  visible change and left as a deliberate decision rather than smuggled in
  here.

- **The "New & Updated" shelf leaned on `filter(Boolean)`**, which strips
  undefined at runtime but leaves the type `(App | undefined)[]`. Its pinned
  ids are curated in this file while the app list is served, so an id can now
  disappear from under it and the shelf would quietly shrink. Narrowed with a
  real type guard, and a missing id is now reported at build time.

- **The description blocks were not a discriminated union**, so
  `p.type === 'ul'` did not narrow and `p.items.map` type-checked as
  possibly-undefined. Typed properly.

Also hardened the generator against demo data that would crash a build rather
than degrade: `quickstart` is always a whole step, because the app page
dereferences `demo.quickstart.command` directly and the TypeScript shape says
it is non-nullable; a demo with no quickstart command is dropped with a
warning instead of rendering an empty code block; and example steps with no
command are filtered out.

Verification
------------

- The eight app-store data-model errors are gone (`astro check`: 254 -> 244).
  The remainder are pre-existing implicit-any and possibly-null findings in
  inline `<script>` blocks across the whole site, plus `worker/test` needing
  dev deps that are not installed — all untouched here.
- **Zero rendered output changed.** All 31 app-store pages — `/app-store`,
  its plain twin, `/docs/app-store`, `/publish` and all 27 `/apps/<id>` —
  are byte-identical to the dev mirror at 7b88a265. That is what confirms the
  removed code really was dead.
- `npm run build` clean at 407 pages; `check:plain` and `check:site` pass.
- Regenerating `apps.ts` after the generator hardening produces a zero-line
  diff: every shipped demo already has a quickstart command, so the guards are
  for future data.

The plain twin of `app-store.astro` carries a provenance hash of its source.
The edit here is frontmatter only — the twin does not reference the shelf that
changed, and its rendered output is byte-identical to the mirror — so the
stamp is updated in place rather than regenerating a page whose content did
not change (regen needs a GEMINI_API_KEY that is not available here).
The app was renamed to io.pilot.smol; "smolmachines" is the retired id, and
the app's name is Smol Machines. Assets and copy still carried the old id.

Removed (orphaned — nothing referenced them, and io.pilot.smol ships its own
complete icon set):

- public/appicons/io.pilot.smolmachines.png
- public/appicons/optimized/io.pilot.smolmachines-{240,96}.webp

Renamed, because the homepage tile still pointed at it, and the brand
directory is keyed by short app name (aegis, duckdb, miren, …):

- public/brand/apps/smolmachines.png -> smol.png
- public/brand/apps/optimized/smolmachines.webp -> smol.webp

Prose in two learn pages and three blog posts introduced the app as
"smolmachines", which is now a name no app has. They read "Smol Machines".

Kept deliberately
-----------------

public/_redirects still maps /apps/io.pilot.smolmachines -> /apps/io.pilot.smol
(301). That rule exists *because* of this rename: it is what stops old
bookmarks and inbound links 404ing, and removing it would turn a working
redirect into a dead URL. The retired id has to survive in exactly this one
place for the rename to stay invisible to anyone who followed an old link.

Untouched: https://smolmachines.com is the vendor's live domain, not the app
id, and remains the app's vendor_url and homepage.

Verification
------------

- npm run build clean at 407 pages; check:plain and check:site pass.
- No smolmachines-named asset ships: `find dist -iname '*smolmachines*'` is
  empty. The only occurrences left in the built site are the vendor domain on
  the app page and the redirect rule itself.
- The homepage tile resolves: dist/brand/apps/optimized/smol.webp is present
  and referenced once.

The plain twin of index.astro carries a provenance hash of its source. The
edit here is a single image src; the twin has no image tiles at all, so the
stamp is updated in place rather than regenerating a page whose content did
not change (regen needs a GEMINI_API_KEY that is not available here).
@Alexgodoroja
Alexgodoroja merged commit 361feab into main Aug 25, 2026
3 checks passed
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.

1 participant