Files
Completed/README.md
T
SansanandClaude Sonnet 5 9972537b63 Add a small, tested base for all three clients, and Gitea CI/release workflows
internal/client (new): a typed Go HTTP client for the API - login,
register, logout, me, and full CRUD for games/platforms - shared by the
TUI and the Fyne GUI so request-building, auth and error handling (an
APIError with the HTTP status) live in one tested place instead of being
duplicated. 8 tests against an httptest server covering success, 401, 403
and that the token is stored/cleared at the right times.

cmd/tui (rewrite): drops the old unauthenticated read-only viewer. Now a
login screen (bubbles/textinput) -> a scrollable games table
(bubbles/table) with 'r' to refresh, built as an explicit state machine
(screenLogin/screenLoading/screenGames) so Update's transitions are
testable without a real terminal. 9 tests driving Update() directly.

cmd/gui (rewrite): drops raylib for Fyne. Same shape as the TUI - a login
form, then a games table, with networking on a goroutine and fyne.Do for
the UI updates. The table-cell and login-validation logic are plain
functions with no Fyne/display dependency, so they're tested directly (7
tests) rather than needing Fyne's headless test driver.

internal/api: tagged every operation (Auth/Platforms/Games/Misc) so the
generated OpenAPI spec groups into named services instead of one bucket;
api/openapi.json regenerated. Added CORS middleware (wildcard origin, no
credentials needed since clients use a bearer token, not the cookie) -
needed for the new web client's browser requests to the API's a different
origin.

web/ (new): an Angular 22 + Material app, API client generated from
api/openapi.json via ng-openapi-gen (services: true for per-resource
classes - AuthService/GamesService/PlatformsService - instead of one
generic Api facade). A Session service holds the token/user as signals,
persists the token to localStorage, and is the only thing that calls the
generated AuthService; an HttpInterceptorFn attaches the bearer token to
every request; a CanActivateFn guard protects /games. Login and Games are
standalone, lazy-loaded route components. 21 tests (Vitest, Angular's new
default - no browser/Karma needed) covering the service, interceptor,
guard and both components, run via `npm test` exactly as CI will call it.

Justfile: fixed run-tui/run-gui, which used `go run .../main.go` - broken
now that both packages span multiple files, since that only compiles the
one named file. Added web-install/web-dev/web-build/web-test.

.gitea/workflows/ci.yml (moved from .github, which Gitea doesn't read):
on push to main/develop - gofmt, go vet, go build, go test, go mod tidy
diff (with the Fyne build's system deps installed), sqlc diff, and the
web app's build+test.

.gitea/workflows/release.yml (new): on a vX.Y.Z tag, cross-compiles
api+tui for linux/darwin/windows (amd64+arm64, pure Go, no cgo), builds
the Fyne GUI for linux/amd64 only (cgo + its OpenGL/X11 link means it
can't cross-compile from one Linux runner the way api/tui do, and this
repo has no registered macOS/Windows runner to build it on natively -
documented in the workflow's comments), builds the web app, and creates a
Gitea release with all of it attached via the Gitea API. Written to
Gitea's documented GitHub-Actions-compatible syntax but not executable
from here - there's no Gitea server in this environment to run it
against, unlike everything else in this commit, which was.

Verified: every Go package builds/vets/tests clean; sqlc diff and go mod
tidy -diff are clean; the Angular app builds for production and its test
suite passes via `npm test`/`npm run build` (the exact commands CI runs),
including after a clean `npm ci` from the committed lockfile; the new CORS
config was checked with real preflight and GET requests against a running
server, not just read.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-03 12:16:59 +02:00

3.5 KiB

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. 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.

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, r to 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) at http://localhost:4200, proxying nothing itself; it talks directly to the API at localhost:8080 (CORS is permissive enough for local dev as-is). Log in, see your games.

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 (Go + web) on every push to main or develop. .gitea/workflows/release.yml builds all three clients and attaches them to a Gitea release when a vX.Y.Z tag is pushed; see that file's comments for its one real limitation (the desktop GUI is only built for linux/amd64, since it needs cgo and this repo has no registered macOS/Windows runner to cross-build it on).