ci/woodpecker/push/woodpecker Pipeline was successful
Every WarpEngine API response now carries `WarpEngine-Version`, so a client can branch on the engine's age without a round trip to ask. Set in a before_action rather than after: `rescue_from` never reaches an after_action, and a client needs the version most when something came back wrong. The name lives in `WarpEngine::VERSION_HEADER`. The host's own endpoints — the store registry — do not carry it, because they are not the engine. A software has one pipeline, and the newest assignment now wins. Two pipelines pointing at the same software was not an error the database caught; it was a link that silently did nothing, with the software still showing whichever row came first. Assigning a software another pipeline holds therefore moves it, the admin says which pipeline it was taken from, and `Pipeline#software_taken_from` carries that for anything else that cares. Deliberately a callback and not a unique index: rows here are soft-deleted, and a unique index counts deleted rows, so a pipeline removed last year would block its software from ever being linked again. The engine is 0.4.0. The site's /stores page and its screenshot follow the client's new name, and the shot is a fresh one showing the greyed-out titles the client now lists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
120 lines
4.3 KiB
Markdown
120 lines
4.3 KiB
Markdown
# 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](libs/ruby/warp_engine/README.md).
|
|
- **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.
|
|
|
|
```json
|
|
[
|
|
{ "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`](https://git.teletypegames.org/stores/warp-engine-client) |
|
|
|
|
## Development environment
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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)
|
|
|
|
```bash
|
|
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)
|
|
|
|
```bash
|
|
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)
|