Commit Graph
32 Commits
Author SHA1 Message Date
mr.zeroandClaude Opus 5 7c9f2988be The build endpoints state their input rules once
`POST /build/publish` opened with three guard clauses and `POST /build/upload`
with eight, each one a `return render json: { error: ... }` — name present,
version present, name format, version format, file present, filename prefix,
size, digest. That is a validation layer written by hand, in a place where it
cannot be unit tested: exercising it needs a request.

`PublishInputDto` was already there and was a bare `Struct` with no rules at
all, so the controller carried them. It is an `ActiveModel::Model` now, with
the presence and platform-inclusion validations on it, and `UploadInputDto`
joins it with the name and version formats and the `<name>-<version>` filename
convention. Each controller reads:

    return render json: { error: input.error_message }, status: :bad_request unless input.valid?

Size and digest keep their own explicit checks, because they are not the same
answer: 413 tells a caller to stop, 422 tells it to retry a truncated upload,
and a single error bag cannot say which. Every status code the endpoints
answered before, they answer now — build_publish_controller_spec and
build_uploads_controller_spec pin all of them, unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 09:01:10 +02:00
mr.zeroandClaude Opus 5 f1ad80ed0c A platform is named once: WarpEngine::Platform
The list of platforms lived on `PlatformLink::SUPPORTED_PLATFORMS` — a model
about links to a platform's website — and was copied around it four times in
the admin (`%w[tic80 ebitengine love c64 godot bevy phaser]`), spelled out in
three apipie descriptions, and turned into a class name by string
interpolation in three services:

    "WarpEngine::Platforms::#{platform.camelize}::Service".constantize

Seven places that had to agree, and nothing that made them.

`WarpEngine::Platform` is now that one place. `Platform.names` is the list,
`Platform.find!("godot")` answers with a value object that knows its `label`,
its `expected_kinds` and its updater `service`, and the constantize is gone —
the registry holds the reference. `PublishService` and `BuildsService` ask it,
the admin selects read `Platform.names`, and
`PlatformLink::SUPPORTED_PLATFORMS` stays as an alias of `Platform::NAMES` so
a host pinned to 0.7 keeps working.

`Software` also defends its own value sets now. It validated neither `status`
nor `platform`, so a mistyped platform only surfaced later, at publish time,
as "Unsupported platform" — after the row existed. And `status` had three
different answers depending on where you looked: the admin offered
development/demo/released/archived, the API documentation claimed
"active, inactive", and the database holds the first four. `Software::STATUSES`
is the list, the inclusion validations enforce both, and the documentation
names the values the database actually has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 09:00:55 +02:00
mr.zeroandClaude Opus 5 19d142ef64 WarpEngine 0.7.0: a képtár és a CI is a hoszté, adapteren keresztül
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>
2026-08-23 00:56:01 +02:00
mr.zeroandClaude Opus 5 a0fbf1e2b4 A kódbázis kommentek nélkül marad
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>
2026-08-20 12:52:47 +02:00
mr.zeroandClaude Opus 5 76427bab91 WarpEngine: let the host say who a browser is
The access policy asks who the caller is, and until now only a bearer token
could answer. That is what a desktop client carries — but a person clicking a
download link on the site carries a session instead, and the engine has no idea
what a session is. So a host that gated its catalog found its own signed-in
visitors refused at /api/download, which is a regression the shadowed route used
to hide.

c.subject_resolver is a callable taking the Rack request and returning the
host's subject: `->(request) { request.env["warden"]&.user }` for a Devise app.
Unset — every deployment today — a non-bearer request stays anonymous, exactly
as before. A resolver that raises is logged and treated as anonymous, because a
broken one turning every read into a 500 is worse than an anonymous request.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 10:36:30 +02:00
mr.zeroandClaude Opus 5 6c8b026590 WarpEngine 0.5.0: a catalog that can say a title is not yours
A desktop client reading /api/software had no way to learn that a title costs
money. There was nothing in the response to say so, no way to sign in, and no
way to be told "you do not own this" — so a store with paid titles could only
hand the client a 403 at download time and let it guess why.

The fix belongs here rather than in the client. A client serves more than one
store, so anything it knows about a particular one has to arrive from that
store's own API; a rule compiled into the client is a rule that breaks every
other catalog it reads. Three seams, each following the storage adapter's
shape — documented contract, default that is byte for byte the old behaviour,
one config key to replace it:

- **access policy** — visible_software_scope / access_for / authorize_download.
  Every catalog entry now carries an `access` block (gated, entitled, price,
  purchaseUrl, webUrl) and both /api/download and /file/* ask before serving.
  The vocabulary is deliberately generic: a word from one host's domain would
  make every client that reads it specific to that host.
- **client sign-in** — the device authorization grant (RFC 8628), over the
  host's own user model. The approval page stays the host's, because approving
  needs a session and HTML. Tokens are ApplicationTokens with a `catalog`
  scope, so publishing and reading stay separable.
- **service descriptor** — GET /api/service says what this deployment is and
  whether it has a sign-in at all, which is how a client stops guessing.

With no policy and no subject class configured — every deployment today — the
API is unchanged: /api/auth/* answers 404, /api/service reports auth: null, and
the 187 pre-existing examples pass untouched.

A policy that raises is treated as a refusal, not permission. An artifact
served because the gatekeeper crashed is the one failure mode this must not
have, so a broken policy empties the catalog and denies the download.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 10:26:28 +02:00
mr.zeroandClaude Opus 5 f4983c6329 The pipeline admin form could never save
`ActiveAdmin.register WarpEngine::Pipeline` declared no `permit_params`, so every edit
handed unpermitted attributes to the model and Rails raised ForbiddenAttributesError. That
is not new: the form has been unable to save for as long as it has existed. My flash
message on `update` sat at the top of the traceback and made it look like the cause, which
it was not — and it is gone anyway, because overriding an ActiveAdmin action to say
something is a poor trade for what it can break. The move is written to the log instead.

Adding a spec that would have caught it, in the host app, because that is where the
ActiveAdmin instance lives: it signs in, PUTs the form, and checks both that the record
saves and that the software link moves off the pipeline that had it. Driven the same way
by hand against the development database first — 302, the link moved, the previous holder
left without one.

The engine's other admin resources were checked for the same omission: downloads and
releases are read-only and the file manager posts to its own routes, so pipelines was the
only one affected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 20:41:16 +02:00
mr.zeroandClaude Opus 5 ccf38bd9fd Do not let a response header take the API down
The version header was set from WarpEngine::VERSION_HEADER, a constant introduced in the
same commit. The deploy that followed ran these controllers with an older `lib/`, so the
before_action raised NameError on every request and every engine endpoint answered 500 —
the catalog, the images, the file server and the config extension Woodpecker calls, which
is how it surfaced: a pipeline could no longer fetch its own configuration.

The controller now spells the header name out. A response header is not worth a dependency
that can take the API down when one half of a deploy is older than the other, and the
constant remains the documented name with a spec holding the two in step.

Confirmed in production mode against the same code that failed: 200 with the header.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 20:20:39 +02:00
mr.zeroandClaude Opus 5 b652615e32 A version header on every WarpEngine response, and a movable pipeline link
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>
2026-08-18 19:46:29 +02:00
mr.zeroandClaude Opus 5 9b5c05e647 Build bundled Phaser projects with their own toolchain
The step assumed one project shape: plain sources concatenated with cat
and Phaser pulled from a CDN. trickster-tiles is the other shape — Vite
plus TypeScript, built with `tsc && vite build` — so `cat src/*.js`
found nothing to bundle and the build failed. The earlier guard on the
syntax check fixed only the first symptom of that mismatch.

A project with a build script now runs npm ci && npm run build and is
packaged from its own dist/, which already contains index.html and the
hashed assets. Plain projects keep the existing path untouched.

The bundler's base has to be relative, since games are served out of
/file/<name>-<version>/; the step fails loudly if the build leaves no
dist/index.html rather than shipping an empty zip.

Verified in the phaser-builder image against trickster-tiles: a 344 kB
package with index.html at the root and ./assets/ references.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:12:12 +02:00
mr.zeroandClaude Opus 5 64b5c16dc3 Add linux_arm64 builds for Ebitengine and Bevy
Batocera and the ES-family distributions run on ARM as much as on
x86_64 — Raspberry Pi, Odroid, the retro handhelds — and a linux_x64
binary installs there but will not start. There was no Linux ARM asset
kind at all: KINDS had linux_x86 and linux_x64 and mac_arm64, but
nothing for 64-bit ARM Linux.

Registering the kind is deliberately separate from producing it: a
platform service only includes BuildLinuxArm64 once its pipeline builds
the artifact, otherwise /api/builds would report it missing for every
release. Hence Ebitengine and Bevy only. Godot needs a Linux arm64
export preset in each game repo first; LÖVE fuses an upstream AppImage
that ships x86_64 only; TIC-80's export command has no ARM target.

Ebitengine needs cgo on Linux, so binary_build gained a cross-compiler
argument — and unsets CC for native targets, otherwise a build after
the ARM one silently picks up the cross gcc.

Verified by cross-compiling both demos in the rebuilt images: each
produced a genuine AArch64 ELF (e_machine 183), not a silent fallback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 20:57:23 +02:00
mr.zero f805555cc6 phaser pipeline fix 2026-08-16 16:45:16 +02:00
mr.zero 03a51a5b30 warp_engine 0.2.0: pluggable storage adapter and publish notifications
Two seams the hosts needed, both backward compatible.

Storage: artifacts are served through WarpEngine::Storage.adapter instead of
raw filesystem calls. The default :local adapter keeps the previous behaviour
byte for byte, including the path traversal guard. A host can now set
config.storage_adapter to any object answering file?/directory?/locate and
serve builds from an object store - FileService and /api/download both honour
a Location.redirect, so a signing adapter turns them into redirects.
DownloadService#create still returns an absolute path (nil when missing) for
existing callers; #locate is the new entry point that can also return a
redirect. Ingestion (upload, extraction, file manager) stays local for now.

Publish: PublishService emits ActiveSupport::Notifications
("warp_engine.publish") with platform/name/version/software/release, so hosts
can react to a new build without hanging callbacks on the models.
WarpEngine.instruments_publish? lets a host feature-detect and keep its
fallback for older engine versions.
2026-08-10 11:16:17 +02:00
mr.zero 2a94fc785f warp_engine: CiRepository -> Pipeline rename everywhere, ci_ service prefixes dropped, unknown platform allowed 2026-08-06 19:46:06 +02:00
mr.zero 05beb6230b warp_engine admin: CI Dashboard removed, Pipelines page with details sidebar, badge and Created fixes 2026-08-06 19:32:50 +02:00
mr.zero 161f923dc8 fixes 2026-08-06 19:23:04 +02:00
mr.zero 98c38f775d WarpEngine dropdown emoji 2026-08-06 16:23:52 +02:00
mr.zero 0cba2e3e3d WarpEngine dropdown 2026-08-06 16:22:17 +02:00
mr.zero 91c31e05bc pipeline fix 2026-08-06 16:04:50 +02:00
mr.zero 867f71f7c0 woodpecker integration 2026-08-06 13:58:58 +02:00
mr.zero 265e1092a1 platform files refact 2026-08-06 10:00:34 +02:00
mr.zero 1781b62f07 Verify RFC 9421 signatures on the Woodpecker config endpoint 2026-08-06 07:24:47 +02:00
mr.zero 57e47a15a0 Translate code comments and admin strings to English 2026-08-06 01:25:53 +02:00
mr.zero 1c7498ca4a Serve Woodpecker pipeline configs from the engine (/build/config) 2026-08-06 01:14:22 +02:00
mr.zero b64c82aa13 Remove the droparea: artifacts arrive over /build/upload 2026-08-05 22:16:34 +02:00
mr.zero 4e2c45dc45 build endpoints 2026-08-05 20:12:35 +02:00
mr.zero 1e974bdcc9 Rename the update auth switch to application_token_source and drop its ENV default 2026-08-05 18:53:04 +02:00
mr.zero 710594efda Add DB-backed application tokens for the update endpoint 2026-08-05 18:30:20 +02:00
mr.zeroandClaude Fable 5 c72279a470 Phase 5: ship the catalog ActiveAdmin resources from the engine
- 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>
2026-08-04 19:18:28 +02:00
mr.zeroandClaude Fable 5 2b2a9df136 Phase 4: move catalog controllers and routes into WarpEngine
- 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>
2026-08-04 19:09:11 +02:00
mr.zeroandClaude Fable 5 ca25f72c76 Phase 3: move service layer, serializers and DTOs into WarpEngine + dummy-app test suite
- 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>
2026-08-04 19:03:58 +02:00
mr.zeroandClaude Fable 5 7abfb89bca Phase 2: move the 8 catalog models into the WarpEngine engine
- 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>
2026-08-04 18:51:12 +02:00