Skip to content

feat(platforms): platform catalogue from the API — desktop keeps only code - #1203

Open
JamieRuderman wants to merge 36 commits into
feature/agent-chat-interfacefrom
feat/platform-catalogue
Open

feat(platforms): platform catalogue from the API — desktop keeps only code#1203
JamieRuderman wants to merge 36 commits into
feature/agent-chat-interfacefrom
feat/platform-catalogue

Conversation

@JamieRuderman

@JamieRuderman JamieRuderman commented Sep 2, 2026

Copy link
Copy Markdown
Member

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's platformTypes + platformInstallations, produced by scripts/platforms-generate.mjs and committed. 69 types, 33 installations, keyed by slug (the API's id is 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 in catalogue.ts (hand-written, typed against the ambient IServiceRegistration).

Registry (platforms/index.ts) — the catalogue drives what exists; local files attach code. At construction every catalogue type gets its name, and platforms.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-matched altLink, a deliberate hot-fix), undefined ones fall through. A type that several pages onboard (1120 Debian: linux and ubuntu) renders as its default page — the API's sortOrder, carried in the snapshot's routes — not whichever registered last.

31 platform files reduced to code. docker/index.tsx is id, component, and its Docker Hub JSX note. −190 lines.

Drift detection is a local tool, not a CI gate. npm run platforms:check compares parsed JSON (formatting- and CRLF-immune), and platforms:generate refuses 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

  • Docker, Jumpbox and IDY had lost their commands. The registry mapped every command row to "show the API's command", but the API renders templates only once its flag is on. Now a row's commandTemplate is substituted client-side — identical to the API's output once a stage renders server-side — so the command is right regardless of deploy order.
  • Android had lost its Play Store button. The old download: true + command: '[CODE]' combination is now the rule "a code row with a link is a download page with the code as fallback". No schema change.
  • Docker routes had gained a default SSH service. The old files said 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).
  • The merge silently discarded local fields (only JSX instructions and altLink survived, and altLink: undefined on the wrong OS erased a catalogue value). Replaced with defined-wins.
  • A route with no catalogue row degraded silently to name: slug, types: {} — now a dev-console warning, and catalogue-seeded registration makes the case rare.
  • The old generator could brick the app: run against a flag-off stage it would have written an empty catalogue. It now refuses; fetch has a 15 s timeout; non-JSON edge responses are named; nulls are stripped recursively (the API returns port: null); page-only routes (this, remoteit) come from the new platformInstallations query; the unreproducible --from-seed bootstrap is gone.
  • Wording: "offline/startup fallback" was false and is gone everywhere.

altLink is gone

It carried two unrelated things, and neither was platform data — so it is out of the catalogue entirely and split into two named client capabilities:

was now where the destination lives
altLink on linux, raspberrypi (from the DB) oemGuide?: boolean OEM_GUIDE_LINK in constants.ts
altLink on windows, mac (browser-conditional local) addThisDevice?: boolean DEVICE_SETUP_PATH in constants.ts

The 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/setup is 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.

link is 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/**/*.tsx where i18next-parser could reach them. They were never actually translated (no t() in any platform file, no platform copy in any catalog), so nothing regressed, but the door needed reopening before the platforms namespace below.

i18n:check now 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 platforms namespace, keyed by route slug: platforms:docker.description. The generator writes English from the database and creates the matching empty ja/de/es entries, so translators pick these up exactly like every other key. 33 slugs, covering name, description and the four string instructions; 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.ts is 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:

  • Regenerating never discards a translation. Non-English locales keep their values, gain empty entries for new keys, and lose only keys that no longer exist. Tested by adding a Japanese description, regenerating, and confirming it survived.
  • A database wording change is caught. With the Japanese translation in place I reworded the English description as a catalogue edit would, and i18n:check failed with the before/after text naming ja/platforms.

platforms:check now also verifies the English catalog, not just the data snapshot, so the two cannot drift apart.

Codex review fixes

  • Locale-only plural forms were unwatched. Spanish and German carry _many where English has only _one/_other, so source[key] was undefined and the staleness check skipped them. It now falls back to the English _other form: 43 translations that were invisible are now covered, verified by rewording an English _other and watching the Spanish _many get flagged.
  • A translation added without i18n:accept was silently exempt forever, because check mode never writes the record. It is now an error naming the key and the command.
  • Catalogue-only platform types still showed "Unknown". Seeding nameLookup was not enough: TargetPlatform resolves through platforms.type(), which only consults lookup. type() now falls back to the catalogue name.

Not taken, by design: Codex also asked for /add to enumerate catalogue-backed platforms. I built it, saw it surface cachengo, docker-extension and unknown (putting "Unknown" in the device picker), and reverted.

The curated arrays in AddPage are 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-extension is 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 (), Japanese polite です/ます with Latin script unspaced (Linuxベースのシステム, matching Remote.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" in docker-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:

  • Platform type 0 is no longer "Windows". main has labelled it so since forever, and ~644k devices carry it — but 0 is not a code the agent can emit; it is a parse artifact of short or missing devicetype strings on legacy devices (graphql-api docs/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 literally 00: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.ts read nameLookup[platform] and string-concatenated it, so a shared type-0 device produced the OS notification body "undefined - owner@email"; it now resolves the name through platforms.type(), which carries the unknown-platform fallback. A second consequence too: seeding nameLookup from 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 (Unknown 1280/65535, Lorex DVR 1 407/1040, Lorex DVR 2 408/1042) where picking the wrong one silently matched nothing. The registry now exposes pageTypes for pickers and the filter uses it: 38 entries, no duplicates. Type 0 would then have had no bucket at all — devices labelled "Unknown" that the "Unknown" filter did not match — so the desktop's unknown page now claims both 65535 and 0, and the filter groups rows by label: one "Unknown" row selecting both ids. Claiming it client-side rather than in the catalogue is deliberate — 0 is the absence of platform information, not a platform, and naming it in r3_PlatformTypes would assert one that does not exist.
  • Debian devices render with the Linux icon. 1120 was mapped to the ubuntu page only because that picker entry is a desktop download; the API now gives it both routes with linux first.

Honest open items

  • Reaching the catalogue from CI. Auth is enforced at the gateway, not the resolver (POST /graphql carries authorizer: name: authorizer), so the request never reaches platformTypes and 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: put npm run platforms:generate in 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_LINK still exists in constants.ts for two components outside this diff; the catalogue's android.link is 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.
  • instructions is declared markdown on the API and rendered as plain text here; every current string is plain prose.

Verification

npm run typecheck clean at root (mirroring CI). Snapshot invariants checked: every type's page resolves, 1213 → android (download + code), no type 0, 1120 → linux by default and still listed on ubuntu, 1219 → "Docker Container" with its docker run … template, the three Docker rows carry services: [], the Jumpbox template's doubled spaces are gone.

@JamieRuderman
JamieRuderman force-pushed the feat/platform-catalogue branch from e897ad7 to 2559875 Compare September 2, 2026 23:31
…ined-wins merge, catalogue-seeded registry, hardened generator and CI after review
@JamieRuderman

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ 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".

@JamieRuderman

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ 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".

@JamieRuderman

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 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".

Comment thread frontend/src/platforms/index.ts
Comment thread frontend/src/platforms/index.ts Outdated
Comment thread frontend/scripts/i18n-check.mjs Outdated
Comment thread frontend/scripts/i18n-check.mjs Outdated
@JamieRuderman
JamieRuderman force-pushed the feat/platform-catalogue branch from 6965e52 to 005bfd1 Compare September 4, 2026 22:28
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