TITLE=Public site ROUTE=/services/public-site LOCALE=en MODIFIED=1792417593 URL=https://wiki.teletypegames.org/services/public-site L=1 https://git.teletypegames.org/services/teletypegames L=2 #22 q│ The Teletype Games public platform: one monorepo (1cservices/teletypegamesq) q│ with the catalog backend (Catalog API) and the retro-styled frontend, run by q│ Docker Compose. 1Monorepo layout 1═══════════════ p apps/ p frontend/ # Vue 3 + Vite + TypeScript + Tailwind p api/ # Rails 8 API + ActiveAdmin cdocker compose up -d0 starts the dev environment. Both apps sit under one domain: t┌──────────────┬─────────────┐ t│h Path t│h Serves t│ t├──────────────┼─────────────┤ t│0 c/0 t│0 frontend t│ t│0 c/api0 t│0 Catalog API t│ t│0 c/admin0 t│0 ActiveAdmin t│ t│0 c/api/swagger0 t│0 API docs t│ t└──────────────┴─────────────┘ Linting: bRuboCop0 (rubocop-rails-omakase) for the backend, bESLint 90 (flat config, Vue + TypeScript) for the frontend. 1Catalog API 1═══════════ Rails 8 + MySQL. ActiveAdmin panel (blaze theme, Devise auth), Blueprinter serializers, apipie-rails docs with a browsable Swagger UI. It is the single source of truth for software metadata, releases, members, events and download logs, and it serves images and binaries. 2Domain model 2──────────── cSoftware0 (name, title, author, desc, story, license, platform, status, highlighted; soft-deleted) has many cRelease0s and cExternalLink0s, and images through cSoftwareImage0 (default flag + ordering). A cRelease0 holds the version and the cartridge / source / HTML folder / docs folder paths. Every download is a cDownload0 row (IP, user agent, referer, release), surfacing as cdownloadCount0 / ctotalDownloads0 in API responses. 2Endpoints 2───────── t┌─────────────────────────────────────┬──────────────────────────────────────┐ t│h Endpoint t│h Purpose t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cGET /api/software0 t│0 Catalog listing: software + sorted t│ t│0 t│0 releases + clatestRelease0 (skipping t│ t│0 t│0 cdev-0) + cwebPlayableRelease0 + t│ t│0 t│0 ctotalDownloads0 t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cGET /api/software/highlighted0 t│0 Featured software t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cGET /api/events0 t│0 Community events t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cGET /api/members0 t│0 Team members t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cGET /api/stores0 t│0 Store registry (see below) t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cGET /api/image/:id0 t│0 Image serving t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cGET0 t│0 Wiki proxy: fetches Grav's t│ t│0 c/api/wiki/0 t│0 ccustom/pages.json0 via cWIKI_GRAV_URL0, t│ t│0 cpages?tag=blog\|howto[&limit][&body0 t│0 so the frontend never talks to the t│ t│0 c=1][&lang=en\|hu]0 t│0 wiki. clang0 picks the wiki language, t│ t│0 t│0 defaulting to cen0 t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cGET /api/rss/blog0 t│0 RSS of blog-tagged wiki pages t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cGET /api/rss/releases0 t│0 RSS of the latest releases t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cGET /api/download?path=…0 t│0 Logged download (records IP / UA / t│ t│0 t│0 referer, then serves) t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cGET /api/swagger0 t│0 apipie docs t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cGET /file/*path0 t│0 Direct file download; release paths t│ t│0 t│0 in API responses point here t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cPOST /build/upload0 t│0 Artifact upload, one file per t│ t│0 t│0 request; application token with the t│ t│0 t│0 cupload0 scope t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cPOST /build/publish0 t│0 Release publishing; application t│ t│0 t│0 token with the cupdate0 scope, called t│ t│0 t│0 by CI after upload t│ t└─────────────────────────────────────┴──────────────────────────────────────┘ Tokens are described in the 2WarpEngine article. bThe store registry0 (cGET /api/stores0) says which catalogs a client can install from and where each store's config lives. It is public, because the desktop client reads it on first run and has nobody to log in as. A row is three fields (cname0, ccatalog_url0, cstore_repository_url0), edited in ActiveAdmin ▸ 🛒 Stores. It lives in the host app, not in WarpEngine: the engine serves one catalog and does not know who ships stores for it. 2Publish pipeline 2──────────────── c/build/publish0 dispatches by platform to a cPlatforms0 service (ctic800, clove0, cebitengine0, cc640, cplus40, cgodot0, cbevy0, cphaser0). Each one picks up the uploaded artifacts (C64 and Plus/4: c-.prg0 + c-.metadata.json0, the EmulatorJS c.html.zip0 and the yapeSDL c-linux-x640, c-win-x640 and c-mac-universal0 zips), upserts the cSoftware0 from the metadata, adds a iSource Code0 external link from its csite0 field, and creates the cRelease0. The Gitea + Woodpecker side is in the Deployment article. 1Frontend 1════════ A Vue 3 SPA built with Vite in TypeScript: vue-router (one module per page), Pinia stores per domain (software, events, members, git, blog, howtos, youtube, ui), vue-i18n, and Tailwind CSS + TuiCSS for the retro terminal look. Traefik proxies it on the same base domain as the API. Pages: chome0, ccatalog0 (index + show), cblog0, ccode0, chowtos0, cteam0, ccontact0. The catalog show page has an image lightbox, the release list, download counts and web-playable releases. One client module per source under csrc/api/0: t┌─────────────────────────────────────┬──────────────────────────────────────┐ t│h Module t│h Source t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 csoftware.api.ts0, cevent.api.ts0, t│0 Catalog API t│ t│0 cmember.api.ts0 t│0 t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cwiki.api.ts0 t│0 Catalog API wiki proxy (cblog0 / chowto0 t│ t│0 t│0 tags), passing the interface t│ t│0 t│0 language as clang0. cVITE_WIKI_BASE0 is t│ t│0 t│0 only used to build external links to t│ t│0 t│0 Grav t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cgit.api.ts0 t│0 Gitea REST API v1 (token t│ t│0 t│0 cWEBAPP_GITEA_TOKEN0) for the iCode0 t│ t│0 t│0 page t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cyoutube.api.ts0 t│0 YouTube Data API: latest channel t│ t│0 t│0 video and view count t│ t└─────────────────────────────────────┴──────────────────────────────────────┘ bAnalytics.0 cuseMatomo0 sets the Matomo tracker up once and counts a page view only in the router's cafterEach0, so a visit is one page view (it used to be two). The first navigation keeps the external referrer instead of overwriting it with the site's own c/0. Tracking is cookieless (cdisableCookies0, no c_pk_id0, no consent needed), and a heartbeat timer records the time spent on single-page visits too. The Gitea header carries the same tracker. 1CI 1══ t┌────────────────────────┬─────────────────────────┬─────────────────────────┐ t│h Workflow t│h Runs on t│h What it does t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 c.woodpecker/deploy.0 t│0 push to cmaster0, manual t│0 crubocop0 and ctest-host0 t│ t│0 cyaml0 t│0 t│0 in parallel, then t│ t│0 t│0 t│0 cdeploy0 waits for both; t│ t│0 t│0 t│0 deploy is an SSH call t│ t│0 t│0 t│0 to cci-deploy0 t│ t│0 t│0 t│0 cteletypegames0 on the t│ t│0 t│0 t│0 server t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 c.woodpecker/warp_0 t│0 changes under t│0 version checks, crubocop0 t│ t│0 cengine.yaml0 t│0 clibs/ruby/warp_engine0 t│0 + ctest-engine0, then the t│ t│0 t│0 t│0 split mirror to t│ t│0 t│0 t│0 cengines/warp_engine0 and t│ t│0 t│0 t│0 the gem publish t│ t├────────────────────────┼─────────────────────────┼─────────────────────────┤ t│0 c.woodpecker/ci-images.0 t│0 cci/**0 or gem-defining t│0 builds the CI images t│ t│0 cyaml0 t│0 files on cmaster0, t│0 below t│ t│0 t│0 manual, nightly cron t│0 t│ t└────────────────────────┴─────────────────────────┴─────────────────────────┘ No step gets a host folder or the docker socket, so nothing needs ctrusted: cvolumes0 (the old c/srv/ci-cache0 volume is gone): the tools and gems live in bour bown CI images0, built from cci/*.Dockerfile0 with bkaniko0 (no privileged mode, no docker socket) and pushed to the Gitea registry under cgit.teletypegames.org/build/…0: t┌─────────────────────────────────────┬──────────────────────────────────────┐ t│h Image t│h Used by t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cteletypegames-ci:api-ruby3.30 t│0 cdeploy.yaml0 rubocop + test-host t│ t│0 t│0 (ruby 3.3, mysql2 build deps, the t│ t│0 t│0 capps/api0 gems) t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cteletypegames-ci:warp_engine-ruby3.0 t│0 cwarp_engine.yaml0 rubocop + t│ t│0 c20 t│0 test-engine t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cdeploy-ssh:alpine3.220 t│0 every cssh deploy@0 deploy/store step t│ t│0 t│0 of every repo, referenced by digest t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cdos-ci:bookworm0 t│0 the test step of the cdos/*0 repos t│ t│0 t│0 (Open Watcom, DOSBox, DOSBox t│ t│0 t│0 Staging, Wine, clang-format) t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cci-tools:alpine3.220 t│0 helper steps: git, git-subtree, jq, t│ t│0 t│0 curl, zip, lua, luacheck, ldoc — t│ t│0 t│0 WarpEngine's ctools0 image for every t│ t│0 t│0 game pipeline (by digest), the t│ t│0 t│0 release steps, the split mirror t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cpyftpdlib:3.130 t│0 the test step of cinfra/ftp-server0 t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cwarp-engine-client-ci:node220 t│0 the check step of t│ t│0 t│0 cstores/warp-engine-client0, with its t│ t│0 t│0 cnode_modules0 preinstalled t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cebitengine-ttg:latest0 t│0 WarpEngine's ebitengine builder: t│ t│0 t│0 cebitengine-builder0 plus the Go t│ t│0 t│0 modules of every TTG Go project t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cbevy-ttg:latest0 t│0 WarpEngine's bevy builder: t│ t│0 t│0 cbevy-builder0 plus the crates of t│ t│0 t│0 every TTG Rust project t│ t└─────────────────────────────────────┴──────────────────────────────────────┘ Every image gets a moving tag and a commit-bound one (c…-0), and is rebuilt only when its Dockerfile or the files that decide its gems change on cmaster0, or by hand. The exceptions are cwarp-engine-client-ci0, cebitengine-ttg and cbevy-ttg0, whose dependencies live in other repos: the nightly cci-images-nightly0 cron looks at them too, and builds only when there is no image yet for the current dependencies (clock-0 for the client's cpackage-lock.json0; cdeps-0 for the cgo.mod0/cgo.sum0 and cCargo.toml0/cCargo.lock files that cci/collect-deps.sh0 gathers from every branch of the cgames0, cdemos and cengines0 orgs, plus the builder's digest). A new dependency still downloads in the build as before. The workflow needs one secret, cforge_token (cpackage:write0).