feat(platforms): platform catalogue from the API — desktop keeps only code - #1203
feat(platforms): platform catalogue from the API — desktop keeps only code#1203JamieRuderman wants to merge 36 commits into
Conversation
… CI staleness check
…nly platform files
…mponents, overrides and JSX stay
e897ad7 to
2559875
Compare
…ined-wins merge, catalogue-seeded registry, hardened generator and CI after review
|
@codex review |
|
Codex Review: Something went wrong. Try again later by commenting “@codex review”. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@codex review |
|
Codex Review: Something went wrong. Try again later by commenting “@codex review”. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…oemGuide, addThisDevice) and constants
… name plus override
…s composed in the client
…to feat/platform-catalogue # Conflicts: # frontend/src/components/AddDevice.tsx
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8ed6dc7b7b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… plurals and unrecorded translations
…ies are reachable
…ing is acceptable
…a token comes from
…a failed CLI call
…o-many routes, no type 0
…on leaves a clean tree
6965e52 to
005bfd1
Compare
…to feat/platform-catalogue
…known id is not "undefined"
…cal-override contract
… DEVICE_SETUP_PATH
…to feat/platform-catalogue # Conflicts: # frontend/src/i18n/locales/de/app.json # frontend/src/i18n/locales/es/app.json # frontend/src/i18n/locales/ja/app.json
Companion to remoteit/graphql-api#209 (phases 3–4 of its
docs/PLATFORM-CATALOGUE.md). The platform catalogue — names, onboarding routes, install commands — has one source, the API; the desktop stops carrying its own copy.Reviewed (ten-angle code review, 2026-09-02) and fixed in the last commit — see Review fixes. Commit messages are one-line, no trailers.
What changes
frontend/src/platforms/catalogue.generated.json— a build-time snapshot of the API'splatformTypes+platformInstallations, produced byscripts/platforms-generate.mjsand committed. 69 types, 33 installations, keyed by slug (the API'sidis a surrogate uuid a slug can be renamed under). The app reads only this file — there is no runtime fetch — so a catalogue change reaches clients when the snapshot is regenerated and shipped. Types live incatalogue.ts(hand-written, typed against the ambientIServiceRegistration).Registry (
platforms/index.ts) — the catalogue drives what exists; local files attach code. At construction every catalogue type gets its name, andplatforms.type()falls back to it, so the 31 legacy types render as "x86 Generic Linux" rather than "Unknown" in device lists and tooltips. Every page without a local file is registered logo-less, so it works if reached by URL.register()merges catalogue data over a code-only local file; any defined local value wins (JSX instructions, an OS-matchedaltLink, a deliberate hot-fix), undefined ones fall through. A type that several pages onboard (1120Debian:linuxandubuntu) renders as its default page — the API'ssortOrder, carried in the snapshot'sroutes— not whichever registered last.31 platform files reduced to code.
docker/index.tsxisid,component, and its Docker Hub JSX note. −190 lines.Drift detection is a local tool, not a CI gate.
npm run platforms:checkcompares parsed JSON (formatting- and CRLF-immune), andplatforms:generaterefuses to overwrite the snapshot while the API serves no catalogue. It is deliberately not wired into CI: the snapshot is meant to lag the database until someone regenerates and ships, so "differs from the API" is the normal state after a row edit rather than a fault, and as a per-PR gate it would redden every open pull request for a change none of them made. The right trigger is a scheduled job that opens a PR with the diff, which is worth setting up once the credential question is settled.Review fixes
commandrow to "show the API's command", but the API renders templates only once its flag is on. Now a row'scommandTemplateis substituted client-side — identical to the API's output once a stage renders server-side — so the command is right regardless of deploy order.download: true+command: '[CODE]'combination is now the rule "acoderow with alinkis a download page with the code as fallback". No schema change.services: [](none); the extraction had dropped it and the UI falls back to SSH when unset.[]is preserved end-to-end (snapshot, generator, and the migration in [DESK-180, DESK-699, DESK-464, DESK-176, DESK-336, DESK-639] Edit Services #209).altLinksurvived, andaltLink: undefinedon the wrong OS erased a catalogue value). Replaced with defined-wins.name: slug, types: {}— now a dev-console warning, and catalogue-seeded registration makes the case rare.port: null); page-only routes (this,remoteit) come from the newplatformInstallationsquery; the unreproducible--from-seedbootstrap is gone.altLinkis goneIt carried two unrelated things, and neither was platform data — so it is out of the catalogue entirely and split into two named client capabilities:
altLinkonlinux,raspberrypi(from the DB)oemGuide?: booleanOEM_GUIDE_LINKinconstants.tsaltLinkonwindows,mac(browser-conditional local)addThisDevice?: booleanDEVICE_SETUP_PATHinconstants.tsThe OEM warning sentence was already hard-coded in
AddDevice, so keeping only its href in the database split one piece of UI copy across two sources. Both values were also single constants — the OEM guide is the same URL for every platform that shows it, and/devices/setupis an in-app route, a capability of the running client rather than a property of the platform. Platform files now hold no URLs at all: only code and capability flags.linkis unchanged and stays catalogue data — Docker Hub, the Play Store, the Synology guide all genuinely differ per row.Translation catalogs
Moving platform copy into the database narrowed the path to translating it: those strings used to sit in
src/**/*.tsxwhere i18next-parser could reach them. They were never actually translated (not()in any platform file, no platform copy in any catalog), so nothing regressed, but the door needed reopening before theplatformsnamespace below.i18n:checknow runs in CI, which it never did despite its header claiming so. It enforces the three things it always has: a locale missing a key English has, a dead key English no longer has, and an empty English value. No network and no credential, so it runs unconditionally.Stale-translation detection was built and then removed. It worked — it caught 43 previously-invisible plural forms — but keeping its record current meant a step on every translation delivery, and slightly outdated wording is an acceptable trade. The reasoning is recorded in the script so it is not rebuilt by accident.
Platform copy is translatable again
A
platformsnamespace, keyed by route slug:platforms:docker.description. The generator writes English from the database and creates the matching emptyja/de/esentries, so translators pick these up exactly like every other key. 33 slugs, coveringname,descriptionand the four stringinstructions; the six JSX instruction blocks stay in code and pass through untranslated.The keys are built at render time, so i18next-parser cannot extract them — the generator maintains the catalogs instead, which is the arrangement the parser config already documents for the
columns.<id>labels.platforms/text.tsis the only place that knows the key shape, and the catalogue string is always the inline default, so a row that has never been through the generator still renders its English rather than a key.Two properties I proved rather than assumed:
i18n:checkfailed with the before/after text namingja/platforms.platforms:checknow also verifies the English catalog, not just the data snapshot, so the two cannot drift apart.Codex review fixes
_manywhere English has only_one/_other, sosource[key]was undefined and the staleness check skipped them. It now falls back to the English_otherform: 43 translations that were invisible are now covered, verified by rewording an English_otherand watching the Spanish_manyget flagged.i18n:acceptwas silently exempt forever, because check mode never writes the record. It is now an error naming the key and the command.nameLookupwas not enough:TargetPlatformresolves throughplatforms.type(), which only consultslookup.type()now falls back to the catalogue name.Not taken, by design: Codex also asked for
/addto enumerate catalogue-backed platforms. I built it, saw it surfacecachengo,docker-extensionandunknown(putting "Unknown" in the device picker), and reverted.The curated arrays in
AddPageare the right home for this. Driving the picker from the catalogue would need three columns — hidden, section, order — and two of them describe this app's layout rather than the platform:docker-extensionis omitted because you reach it from inside Docker Desktop, and Raspberry Pi leads the list because it is the common case. Those are client facts, and the catalogue holds platform facts. It would also buy nothing: a new platform needs a logo, which is a React component, so it needs a desktop release regardless — adding one line to the curated array happens in that same commit. What the database row saves is the part that actually changes without a release: the command, copy, labels and default services.Platform copy is translated
All 62 strings are filled in Japanese, German and Spanish — no empty entries. Style matched to the existing catalogs: German formal (Sie) with hyphenated compounds, Spanish informal (tú), Japanese polite です/ます with Latin script unspaced (
Linuxベースのシステム, matchingRemote.Itエージェント). Terminology was taken from strings already in the catalogs rather than invented —リモートアクセス/Remote-Zugriff/acceso remoto.Product and brand names are filled with the identical Latin text rather than left blank, so nobody later transliterates "Docker". Three names genuinely translate:
this("This system"),unknown("Unknown"), and the "Extension" indocker-extension.A long-standing English typo is fixed while translating it: "Install the Desktop or CLI on the Mac to you want to enable remote access to." The stray "to" predates this work, in both the Mac and Windows rows. Corrected at the source — the API seed and migration — and propagated to the snapshot and the English catalog, so a regeneration will not bring it back.
Worth a native reviewer's eye before release, particularly the Japanese
〜向けですphrasing used consistently across the descriptions.Follows the API on two visible changes (2026-09-04)
After Evan's revision of #209 the desktop tracks the API rather than its own history:
0is no longer "Windows".mainhas labelled it so since forever, and ~644k devices carry it — but0is not a code the agent can emit; it is a parse artifact of short or missingdevicetypestrings on legacy devices (graphql-apidocs/PLATFORM-CATALOGUE.md, Platform type 0 is not known to be Windows). Those devices now render as Unknown with the generic icon. Decided with Jamie. Measured against production 2026-09-10 rather than left to after release: 646,827 devices carry it, none soft-deleted, 62.7% have never reported device state, and ~1.6% are online at any moment (~10,300) against 4.2% for Debian and 6.8% for Windows Desktop. It is not diffuse legacy debt — 99.4% of those created in 2025 or later belong to one account,civil-remot3it-prd-ug@trimble.com, and they are still arriving (newest seen minutes before the query). Their devicetype is full length with the platform bytes literally00:00, so the registration path is not sending a platform code. The durable fix is there, not here; this PR only stops the desktop lying about it. One consequence was caught and fixed:Notifications.tsreadnameLookup[platform]and string-concatenated it, so a shared type-0 device produced the OS notification body"undefined - owner@email"; it now resolves the name throughplatforms.type(), which carries the unknown-platform fallback. A second consequence too: seedingnameLookupfrom the catalogue widened it from "types with an /add page" to "every type the catalogue knows", and the device Platform filter reads that map — it grew from 39 entries to 69, three of them duplicate labels (Unknown1280/65535,Lorex DVR 1407/1040,Lorex DVR 2408/1042) where picking the wrong one silently matched nothing. The registry now exposespageTypesfor pickers and the filter uses it: 38 entries, no duplicates. Type0would then have had no bucket at all — devices labelled "Unknown" that the "Unknown" filter did not match — so the desktop'sunknownpage now claims both65535and0, and the filter groups rows by label: one "Unknown" row selecting both ids. Claiming it client-side rather than in the catalogue is deliberate —0is the absence of platform information, not a platform, and naming it inr3_PlatformTypeswould assert one that does not exist.1120was mapped to theubuntupage only because that picker entry is a desktop download; the API now gives it both routes withlinuxfirst.Honest open items
POST /graphqlcarriesauthorizer: name: authorizer), so the request never reachesplatformTypesand dropping@Authorized()would change nothing. A public catalogue means a new unauthenticated route, which is a real surface to build and maintain for something that exists only to diff a file — so probably not worth it on CI's behalf. Proportionate alternatives: putnpm run platforms:generatein the release checklist (no infrastructure), or sign with an access key (Authorization: Signature, which the API already accepts for long-lived machine credentials) if a scheduled job is wanted. A public route is worth building only if the docs site or marketing pages want the catalogue for their own sake.SCREEN_VIEW_APP_LINKstill exists inconstants.tsfor two components outside this diff; the catalogue'sandroid.linkis the same URL today. Unlike the OEM guide, this one is genuinely per-platform, so it belongs in the catalogue — the constant is what should eventually go.instructionsis declared markdown on the API and rendered as plain text here; every current string is plain prose.Verification
npm run typecheckclean at root (mirroring CI). Snapshot invariants checked: every type's page resolves,1213 → android(download + code), no type0,1120 → linuxby default and still listed onubuntu,1219 → "Docker Container"with itsdocker run …template, the three Docker rows carryservices: [], the Jumpbox template's doubled spaces are gone.