It set container: gitea/gitea:latest, which has no node - actions/checkout fails immediately with "node: executable file not found" (confirmed via tea actions runs logs 102 on the live instance: conclusion failure, 15s). ci.yml replaces it - same go test coverage plus sqlc/web/docker, and no container override, so it uses the runner's normal image instead of the Gitea server's own image. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Completed
A tracker for a video game collection: what you own, which platform it's on, your score for it, and whether (and when) you finished it.
Status
Each account has its own private game list, but all accounts share one platform list (only an admin can add/rename/delete a platform - the first account ever registered becomes an admin automatically). The API is usable, and serves its own web UI (see below). There are three small-base clients: a terminal UI, a desktop GUI (Fyne), and a web app (Angular) — each can log in and show your games, with more to come.
Running it
just docker-up # starts Postgres and the API on :8080
just docker-down # stops them
Or against a Postgres you already have running locally:
export DATABASE_URL="postgres://postgres:password@localhost:5432/gamedb?sslmode=disable"
just run-api
The API creates its database and runs migrations on startup if needed.
Browse http://localhost:8080/docs for the generated API docs, or
http://localhost:8080/ for the web UI - the Docker image bakes in a
production build of web/ and the API serves it itself (see
registerWebUI in cmd/api/main.go); unknown paths fall back to its
index.html so Angular's router handles deep links like /games. Running
the API outside Docker (just run-api) skips this and serves the API
only, unless WEB_DIST_DIR points at a build of web/ (just web-build
outputs one to web/dist/web/browser).
Using the API
# Register (also logs you in; the first account ever registered becomes
# an admin, which is required to create platforms - see Status above)
curl -X POST localhost:8080/api/auth/register -H 'Content-Type: application/json' \
-d '{"email":"you@example.com","password":"a long password","name":"You"}'
# -> {"token": "...", "user": {...}}
TOKEN=... # the token from above
curl -X POST localhost:8080/api/platforms -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"name":"PC"}'
curl localhost:8080/api/games -H "Authorization: Bearer $TOKEN"
A browser-based client can skip the Authorization header: register/login
also set an httpOnly session cookie.
Clients
just run-tui— a terminal UI (Bubble Tea). Log in, see your games in a scrollable table,rto refresh.just run-gui— a desktop GUI (Fyne). Log in, see your games in a table, refresh.just web-dev— a web app (Angular + Material) athttp://localhost:4200, proxying nothing itself; it talks directly to the API atlocalhost:8080(CORS is permissive enough for local dev as-is). Log in, see your games. This is for frontend development with hot reload; the API's own/(see above) serves a production build of the same app with nothing extra to run.
All three point at http://localhost:8080 by default. The TUI and GUI read
COMPLETED_API_URL to use a different API; the web app's URL is a build-time
constant in web/src/app/app.config.ts.
Each client is a small base - listing is there, but adding/editing a game or managing platforms isn't yet.
Development
just test # go test ./...
just sqlc # regenerate internal/db after changing db/query.sql or a migration
just openapi # regenerate api/openapi.json after changing a route
just web-test # web app unit tests (Vitest)
just web-build # production build of the web app
Schema changes go in db/migrations as a new <timestamp>_<name>.up.sql /
.down.sql pair; sqlc.yaml reads the schema from that directory directly,
so there's no separate schema file to keep in sync.
The TUI and GUI share internal/client, a typed Go HTTP client for the API.
The web app's API client (web/src/app/api/) is generated from
api/openapi.json by ng-openapi-gen (cd web && npx ng-openapi-gen) and
shouldn't be hand-edited.
CI/CD
.gitea/workflows/ci.yml builds and tests everything on every push to
main or develop: Go (gofmt/vet/build/test/tidy), sqlc, the web app, and
a Docker build-only check (the same image release.yml later pushes).
.gitea/workflows/release.yml runs on a vX.Y.Z tag:
- cross-compiles
completed-api/completed-tuifor linux, darwin and windows (amd64 + arm64 where it makes sense - both are pure Go, no cgo) - cross-compiles the Fyne GUI for linux/amd64 (native) and windows/amd64
(via mingw-w64; verified locally - a real GUI-subsystem
.exe, no console window). macOS isn't built, since cgo has no equivalent cross-toolchain and this repo has no registered macOS runner - builds the web app
- builds and pushes the API's Docker image (which embeds that same web
build) to
gitea.sanplex.xyz/sansan/completed, tagged with the version andlatest - uploads every build artifact to the Gitea generic package registry and attaches them to a Gitea release
Both workflows were run for real against this repo - the Go test suite,
sqlc diff, go mod tidy -diff, the Angular build and test suite, the
Fyne-for-Windows cross-compile, and the actual multi-stage Docker build
(built, run against a real Postgres container, and hit with curl) all
passed. The workflow YAML itself (trigger/job wiring, the Gitea API calls
for uploading packages and creating the release) is written to match this
Gitea instance's other projects, but wasn't run through Gitea Actions
itself from this environment - worth watching the first real run of each.