The API read its environment wherever it happened to need it: `Image` in the
model, `RssService` and `WikiService` in class-level constants, the softwares
admin page in a sidebar. Two of those wanted the same wiki and disagreed about
its name — `RssService::WIKI_URL` against `WikiService::GRAV_URL` — and only
the second one is set in docker-compose, so the howtos feed had been linking to
the hard-coded default all along.
Every `ENV` read is in `config/application.rb` now, as `config.x.site_url`,
`config.x.wiki_url` and `config.x.images.container_path`, and the code asks
`Rails.configuration.x`. One place says what this app needs from its
environment, and a test can override it.
`RssService` was three copies of the same twenty-line `RSS::Maker` block. It is
`Rss::Feed` plus `Rss::BlogFeed`, `Rss::ReleasesFeed` and `Rss::HowtosFeed`,
each of which now only answers what its title, its link and its items are — and
the `Time.parse(...) rescue Time.current` modifier, which swallowed everything,
is a rescue of ArgumentError and TypeError. `WikiService` becomes `Wiki::Pages`
and loses `alias_method :pages, :index`: two names for one method meant the
controller and the feeds each called it something different.
The admin cookie was a monkey patch — `ApplicationController.class_eval` in an
initializer, adding an `after_action` that the three-line controller file gave
no hint of. It is a `SyncsAdminCookie` concern the controller includes.
And the engine stops reading the host's config keys: the "View on site" link in
the softwares admin asked for `Rails.configuration.x.site_url`, which is ours,
not its. `c.site_url` is an engine setting now, nil by default, and the link is
left out when the host does not set it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Image#process_upload` ran in a `before_save`: it assigned the generated file
name and copied the bytes to disk, in the same breath, before the row existed.
If the insert failed afterwards — a validation on the owning record, a
uniqueness clash, a rollback from the surrounding transaction — the file stayed
behind with nothing pointing at it. The admin's own "Orphan" scope exists to
find rows in that family; this was the other half of it, the files with no row
at all.
Split in two: `before_validation` assigns the attributes (so validations and the
generated name still see them), `after_commit` copies the bytes. A rollback now
takes the file with it, because the copy never happens.
The same model is generated into every host, so the fix goes into the
generator template as well as ours. While there: `Image#url` comes back from
Teletype Orbit, where it was added and never flowed back, and the two admin
previews use it instead of interpolating `/api/image/#{id}` by hand.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four small duplications, each of which had already drifted or was one edit away
from it.
**The JSON error body.** The host's `ApiController` was a line-by-line copy of
the engine's four `rescue_from` blocks plus `resolve_mime`. It is a
`WarpEngine::ApiErrorRendering` concern now, included by both, so "what a
failure looks like on the wire" is decided once.
**Mime resolution.** The same three lines lived in the engine's API controller
and the host's. `WarpEngine::Storage.mime_for` owns it — next to the adapter
that hands out the files.
**Finding the asset behind a path.** `DownloadService` and `FileService` each
escaped `%` and `_` by hand and ran their own `LIKE` — the second one without
the exact-match attempt the first one had. `ReleaseAsset.for_relative_path` is
the one lookup: the absolute path first, then a suffix match anchored at `/`
rather than the old `%path%`, which could match a different file whose name
merely contained this one. Two now-unreachable helpers
(`DownloadService.base_path`, `FileService.base_path`) go with it.
**Ordering.** `PlatformLink` and `SoftwareImage` ordered inside their
`default_scope`, which leaks into every association and aggregate — the proof
was already in the tree: `admin/images.rb` had to write
`SoftwareImage.unscope(:order).distinct.pluck(:image_id)`, because MySQL will
not order a DISTINCT by a column it does not select. Ordering is a scope you
ask for now (`ordered`), the two places that need it ask (`for_platform`, the
`software_images` association, so the API's image order is unchanged), and the
`unscope` is gone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Két dolog volt beépítve az engine-be, ami nem az övé.
A **képtár** eddig `WarpEngine::Image` volt, pedig a modell teljesen általános:
a teletypegames-ben a tagok arcképét is ez hordozza, nem csak a katalógus
borítóit. Az `Image` modell, a feltöltött fájlok, az `/api/image/:id` végpont
és az admin oldal ezért átkerült a hosztba, az engine pedig adapteren szól
hozzá (`WarpEngine::Images`): `url_for` adja a katalógus JSON `imageUrl`-jét,
`select_options` a software-form képválasztóját, `build_from_upload` a
"tölts fel új képet" ágat. Az alapértelmezés az `Image` osztály, tehát a
default útvonal bitre a régi. A `SoftwareImage` (a katalógus-kapcsolat)
maradt az engine-ben, és **az `images` tábla nem mozdult**: az engine csak
abbahagyta a létrehozását, a generátor írja meg hoszt-kódként.
A **CI** eddig végig Woodpecker volt: kliens, aláírás-ellenőrzés,
pipeline-receptek, repo-szinkron, secret-kiosztás. Mindez egy adapter mögé
került (`WarpEngine.ci`), a Woodpecker-implementáció pedig az engine-ben
maradt `WarpEngine::CI::Woodpecker` néven — kliens, adapter, httpsig-ellenőrző
és a platformonkénti pipeline-receptek, mert a YAML-dialektus a szolgáltatóé.
Az engine saját kódja már nem nevez szolgáltatót: `CI::Repo` és `CI::Run`
értékeket kap, `CI::ConnectionError`/`ApiError`/`NotConfigured` hibákat dob, a
`Pipeline` pedig `remote_repo_id`-t ad a történelmi `woodpecker_repo_id`
kolumna fölött (a tábla itt sem mozdult). `c.ci_adapter = :none` azt jelenti,
hogy ez a hoszt nem buildel: az `/api/ci/*` 503, a `/build/config` elutasít,
az admin akciók elbújnak.
Mindkét seam a hoszt initializerében van kimondva, nem alapértelmezésre
hagyva — a hoszt megnevezi, mi a képtára és mi a CI-ja.
Törés a 0.6-hoz képest: `image_container_path`, `image_owners`,
`ci_platforms`, `ci_update_server`, `ci_extension_public_key(_url)`,
`woodpecker_url`, `woodpecker_api_token`, `woodpecker_repo_owner` és a
`WarpEngine.woodpecker_configured?` megszűnt; a helyük `c.image_class_name` /
`c.image_adapter` és `c.ci_adapter`. Az `/api/ci/*` `latest_run`/`trigger`
válasza a normalizált `CI::Run` alakot adja (number, status, branch, message,
createdAt, url), a `pipelines` lista pedig `repo_id`-t is közöl a megtartott
`woodpecker_repo_id` mellett.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Kérésre: minden magyarázó komment kikerült a forrásfájlokból — 89 Ruby, 16
TypeScript, 14 Vue, plusz a CSS/JS/CJS. Nem soralapú kereséssel: a Ruby-t a
Ripper tokenizálta, a JS/TS/CSS-t állapotgép járta végig, hogy az URL-ekben,
reguláris kifejezésekben és heredocokban álló // és # jelek helyükön
maradjanak.
Három komment maradt, mert nélkülük nem indul a kód: az entrypoint.sh
shebangja, a vite-env.d.ts hármas perjeles referenciája, és a sanitize
teszt @vitest-environment direktívája (ez utóbbi a magyarázó része nélkül).
Egy helyen kódot is kellett írni: a CommandBlock másolás-hibaágán a komment
volt a catch egyetlen tartalma, és üres blokkot az eslint nem enged — a
copied jelző visszaállítása került a helyére.
A yaml, Dockerfile, Makefile, erb és markdown fájlokat nem érintettem.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Soft deletion already said "this store is gone". What was missing is "not yet" —
a catalog still being set up, or one pulled from the picker for a while without
losing the row and its history. `GET /api/stores` now answers with the active
ones only.
The client is deliberately told nothing about the flag. It has no state for
"there but switched off", and giving it one would mean every client release
having an opinion about it; an inactive store is simply absent, which is a case
the client already handles because it is the same as never having existed. The
payload stays two fields, and a spec holds it there.
Default true, so the migration lists every store that exists today. One that
silently emptied the registry would be a client with nothing to install from.
No index: a handful of rows, read once per client on first run.
In the admin the flag is what the page is *for*, so it is not just a checkbox on
the form: Active is the default scope, the index shows listed/hidden as a status
tag with a one-click toggle beside Edit, and the two batch actions do it in bulk.
A request spec covers all of it, because none of it is reachable from a model
spec — the pipelines resource shipped without `permit_params` and every edit
raised, which is the same layer and the same lesson. The last example toggles in
the admin and then reads /api/stores, since a change here that the registry does
not reflect is the only failure that actually matters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both extras go. `config` was added this morning on the idea that the registry should
say how each store behaves; that was wrong. The configuration is fixed per installed
client — the client carries its own store engine and knows its own machine — so a copy
here was a second authority over decisions the client had already made correctly,
including which directories it may delete from. Keeping two stores on one machine apart
is a subfolder, and the client derives that itself.
`store_repository_url` goes with it. The store engines it pointed at no longer exist, and
a URL nobody follows is a URL that goes stale.
The public stores page loses its desktop card for the same reason: it advertised a
`curl … | sh` for a repository that is gone, and an ordinary computer is served by the
app in the section above it. `/desktop` now lands on that app rather than on a device
tab, so the old URL still means what someone typing it wants.
Unrelated but in the way: the dead `engines` list in that page has been failing
`vue-tsc` on master, so the frontend could not be built to check any of this. Removed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A store's `config.json` lived in a repository the desktop client fetched over HTTP,
which made a store's behaviour depend on a second thing existing and staying
reachable. The registry already answers what a store *is*; it now answers how it
behaves too, in the same shape that file had, so this record is the one source of
truth and a store can be configured from the admin alone.
`storeRepositoryUrl` stays, demoted to a pointer for a person — where the store's
own repository is, when it has one. Clients released before this field still fetch a
`config.json` from it, so nothing has to move at once.
The column is nullable because a store that configures nothing is still a store: the
client falls back to the engine's built-in defaults, which need only a name and a
catalog. The admin edits it as JSON text through a pair of accessors, so the column
holds real JSON and invalid input comes back with the text kept and a message rather
than a 500.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The client only ever took identity from a store repository — a slug, a name, a catalog —
and the store engine's own defaults cover everything else: the host-to-asset mapping, the
install modes, the platforms, the behaviour. So `store_repository_url` is now optional:
nullable in the schema, no presence validation, the format check only when a value is
given, and the serializer answers null rather than an empty string, because the client
branches on its absence.
Adding a store is therefore a row with two fields filled in. Given a repository the
client still reads its config.json, and that file remains the authority on how the store
behaves — the admin form and the endpoint's documentation say so.
The frontend's /stores page gains a section of its own for the graphical client on the
desktop tab: what it does, that it sets the store up itself, that it is the way in on
Windows where `curl … | sh` does not exist, and links to the releases, the repository and
the documentation — now under stores/warp-engine-client, which is where that repository
lives after the rename.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The engines listing is a handful of curated pages, not a stream — no feed
needed. Removes the route, controller action, RssService#engines_feed and
the footer link. The WikiService#pages alias stays (blog/howtos feeds use
it).
The engine pages' repo metadata (new in the wiki pages API) drives the
Explore button and card title links, with the wiki page as fallback. The
never-deployed /engines/:slug detail page and its store/api plumbing are
gone, and the engines RSS feed links to the repos too.
Frontend: /engines index + /engines/:slug detail routes, nav menu item and
en/hu translations. Engine pages are few, so the index uses an emphasized
poster-style design (dark slate, emerald accents, numbered full-width cards
with content preview) instead of the blog/howtos layouts. Slugs are the last
wiki path segment, since engine pages live scattered in the wiki tree.
API: /api/rss/engines feed linking to the site's engine pages, and a
WikiService#pages alias for #index — RssService called the alias-less name,
so the blog and howtos feeds were raising NoMethodError.
- the 7 catalog admin files (softwares, releases, external_links,
platform_links, images, files page, downloads) move to the engine's
app/admin; the host ActiveAdmin instance loads them via
ActiveAdmin.application.load_paths (single admin, URLs unchanged)
- engine initializers: Zeitwerk ignore for app/admin (production eager load)
and load_paths + watchable_dirs registration, guarded by defined?(ActiveAdmin)
so the admin-less dummy app boots
- images admin reads image owners from WarpEngine.config.image_owners; the
host registers the Member owner in config/initializers/warp_engine.rb;
the interim ImageUsage registry is gone
- fix: SoftwareImage.distinct.pluck clashed with its order(:position)
default scope on MySQL (unscope(:order)) — introduced in phase 0, caught
by the first authenticated /admin/images smoke test
- files page: download links use the public /file/ URL instead of the raw
container path; the picker's stored path comes from config
Verified: both suites green, zeitwerk:check (production) clean, JSON
baselines intact, authenticated admin smoke test on every page incl.
the 3-level nested software form and Files picker mode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- update, files and the 6 /api catalog controllers now live in the engine on
a new WarpEngine::ApiController base (same rescue/mime behavior as host)
- engine routes serve /update, /file/*path, /api/software*, /api/builds*,
/api/image/:id, /api/download at unchanged public paths via the root mount;
host routes keep only TTG endpoints (events, members, wiki, rss, swagger)
- /update secret comes from WarpEngine.config.update_secret and an
unconfigured secret now rejects every request (previously an empty
UPDATE_SECRET env accepted empty secrets)
- apipie-rails is an engine dependency (DSL in engine controllers); dummy app
configures apipie with validation off, mirroring the host
- engine request specs: catalog controller specs moved from host plus new
/update auth contract spec
Verified: engine suite 53 green, host suite 6 green, /api/software and
/api/builds byte-identical to baselines, /update 401/400 behavior intact,
admin and TTG endpoints OK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- all catalog services (update/software/highlighted/builds/file/file-manager/
download/image + SoftwareResponseBuilder), the SoftwareUpdater platform
services and their concerns, Blueprinter serializers (incl. TimestampFields)
and the 4 DTOs now live in the engine under WarpEngine::
- constantize dispatch strings use absolute names
(WarpEngine::SoftwareUpdater::<Platform>Service)
- container paths read from WarpEngine.config everywhere (FileService,
DownloadService, FileManagerService, ArchiveExtraction, ReleaseSerializer
path rewriting); FileManagerService base path is now lazy
- engine requires blueprinter itself; gemspec declares blueprinter + rubyzip
- engine test suite: spec/dummy app (mysql warp_engine_test, catalog-only
schema), rails_helper with engine-local factories; catalog model/service
specs and factories moved from the host
- host suite keeps TTG specs and loads catalog factories from the engine
Verified: engine suite 40 green, host suite 14 green, /api/software and
/api/builds byte-identical to baselines, admin OK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Software, Release, ReleaseAsset, ExternalLink, PlatformLink, Image,
SoftwareImage, Download now live in the engine under WarpEngine::, on top
of WarpEngine::ApplicationRecord; table names unchanged (empty prefix)
- each model runs an ActiveSupport load hook (:warp_engine_<model>) as a
host extension point
- WarpEngine::Image reads its upload path from WarpEngine.config
- host references fully qualified (services, serializers, admin, specs);
admin registrations renamed with as: so /admin URLs and route helpers are
byte-identical; factories pinned to the namespaced classes
- Member#image now class_name: "WarpEngine::Image"
Verified: full suite green, /api/software and /api/builds byte-identical to
the phase-0 baselines, admin routes unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- FileService/DownloadService: lazy container-path resolution instead of
class-load-time realpath (boot no longer requires /softwares to exist)
- downloadCount/totalDownloads: single grouped query passed as Blueprinter
option instead of per-release association counts (fixes N+1)
- admin Images: Member coupling replaced with ImageUsage owner registry
(precursor of the WarpEngine.config.image_owners hook)
- Image: UPLOAD_PATH constant replaced with call-time upload_path accessor
- proper test environment (config/environments/test.rb, softwares_test DB,
hosts.clear) — suite previously ran against the development DB
- spec fixes: case-insensitive uniqueness matchers (MySQL ai_ci collation),
DB-cascade has_many expectations, PlatformLink::SUPPORTED_PLATFORMS
- JSON baselines of /api/software and /api/builds for post-extraction diffing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Older-style tic80 pipelines (bombexpert, mranderson) do not upload a
docs zip; the unconditional extraction raised and aborted the whole
update, so the new binary assets never got registered. Extract and
register docs only when the zip/dir exists. Covered by new
Tic80Service specs (optional docs + binary slug pickup).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All software updater services now read an optional repo field from the
game metadata and upsert it as a "Repository" external link, keyed by
(software_id, label) so re-deploys update the existing link instead of
creating duplicates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>