mr.zeroandClaude Opus 5 066451f31c
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/tag/woodpecker Pipeline failed
warp_engine 0.5.1: the device_grants migration collided with the host's
`db:migrate` on the catalog API stopped before running anything:

    Multiple migrations have the version number 20260819000001.

The engine appends its `db/migrate` to the host's migration paths instead of
copying migrations in, so engine and host share one version namespace. Both had
picked 20260819000001 on the same day by the same habit — the engine for
device_grants, apps/api for carry_the_store_config_in_the_registry — and neither
repository could see the other's number.

Worse than a clash of our own making: it takes the *host's* migrations down with
it, for the whole application, before anything runs.

device_grants is renumbered to 20260819093412 — a real second-resolution
timestamp, which is the actual defence. A round hand-written number is precisely
what another repository lands on. Nothing had run it in production, so this is a
rename rather than a data migration; a host that already applied the old version
renumbers its schema_migrations row.

The older engine migrations keep their round numbers: renumbering one that has
been deployed everywhere is worse than the risk it carries. They are named in the
new spec's grandfather list rather than excused by a rule that would also let the
next one through.

That spec found a second thing, older than this change: the install template
creates `application_tokens`, and so does one of our own migrations — so a fresh
host runs CREATE TABLE twice and has to delete the block from its generated copy
by hand, which is exactly what teletype-orbit's migration header describes. I had
just made it worse by putting device_grants in the template too; that is out
again, and the template says why. `application_tokens` is grandfathered and left
for a change that is not a hotfix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 11:29:53 +02:00
2026-05-06 20:15:52 +02:00
2026-08-17 08:08:17 +02:00
2026-02-19 12:35:43 +01:00
2026-01-27 20:01:25 +01:00

Teletype Games

Monorepo for the Teletype Games portal: a Vue 3 frontend, a Rails 8 API host app, and the reusable WarpEngine software-catalog engine.

Project structure

apps/
  frontend/            # Vue 3 + Vite + TypeScript + Tailwind SPA
  api/                 # Rails 8 host app: TTG-specific API + ActiveAdmin shell
libs/
  ruby/warp_engine/    # WarpEngine: mountable Rails engine (catalog, updater, admin resources)

The API is split in two layers:

  • WarpEngine (libs/ruby/warp_engine) owns the software catalog: models (softwares, releases, release assets, images, platform links, download stats), the CI-callable /build/* publishing endpoints, the public read-only JSON API (/api/software*, /api/builds*, /api/image, /api/download, /file/*) and the catalog ActiveAdmin resources. See its README.
  • The host app (apps/api) owns everything TTG-specific: members, events, the store registry, wiki proxy, RSS feeds, Devise/ActiveAdmin authentication, theming and assets. It consumes WarpEngine as a path gem and mounts it at /.

The store registry

GET /api/stores lists the stores a client can install from. The desktop graphical client reads it on first run, which is why it is public: a client has nobody to log in as.

[
  { "name": "Teletype Games", "catalogUrl": "https://teletypegames.org", "storeRepositoryUrl": null }
]

A Store row has two required fields — name and catalog_url — plus an optional store_repository_url, maintained from ActiveAdmin ▸ 🛒 Stores; db/seeds.rb creates our own.

A store does not need a repository. The store engine's own defaults already cover the host-to-asset mapping, the install modes, the platforms and the behaviour; what they cannot know is identity — a slug, a name and a catalog — and that is what this row carries. With no repository the client derives the slug from the catalog host, writes a small config and installs. Given one, it reads that repository's config.json and points it at catalog_url, and that file remains the authority on how the store behaves; a repository without a config file is treated as no repository at all.

Every WarpEngine API response also carries a WarpEngine-Version header — the registry above is the host's own endpoint and does not, because it is not part of the engine.

This lives in the host app on purpose, not in WarpEngine. The engine serves one catalog and has no business knowing which stores exist for it; who ships a store for a catalog is a property of the site.

Model apps/api/app/models/store.rb
Endpoint apps/api/app/controllers/api/stores_controller.rb
Admin apps/api/app/admin/stores.rb
Client warp-engine-client

Development environment

docker compose up -d
Service URL
Frontend http://${WEBAPP_DOMAIN}
API http://${WEBAPP_DOMAIN}/api
Admin http://${WEBAPP_DOMAIN}/admin
API docs http://${WEBAPP_DOMAIN}/api/swagger

The api image is built from the repo root (so the libs/ path gems are visible during bundle install) and mounts ./libs at runtime. After changing the compose file or the Gemfile, rebuild with docker compose up -d --build api.

Testing

make api-test

Runs the host suite (apps/api, softwares_test DB), the WarpEngine suite (dummy app, warp_engine_test DB) and a production-mode zeitwerk:check. Individually:

docker exec -e RAILS_ENV=test api bundle exec rspec                                # host
docker exec -w /libs/ruby/warp_engine -e RAILS_ENV=test api bundle exec rspec     # engine

JSON contract baselines for /api/software and /api/builds live in apps/api/spec/snapshots/ — diff against them after refactors.

Linting

Backend (RuboCop)

docker compose run --rm --no-deps api bundle exec rubocop        # check
docker compose run --rm --no-deps api bundle exec rubocop -A     # autofix

Config: apps/api/.rubocop.yml (rubocop-rails-omakase preset)

Frontend (ESLint)

docker compose run --rm --no-deps frontend npm run lint          # check
docker compose run --rm --no-deps frontend npm run lint:fix      # autofix

Config: apps/frontend/eslint.config.js (ESLint 9 flat config, Vue + TypeScript)

S
Description
No description provided
Readme
9.6 MiB
Languages
Ruby 59.4%
Vue 12.6%
TypeScript 12.5%
HTML 10.6%
CSS 2.1%
Other 2.6%