diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index 81c73f3..589f178 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -50,7 +50,9 @@ export default defineConfig({ env: { BAKHYARD_LISTEN: `tcp://${gateway}`, BAKHYARD_PEERS: [ - ...['home', 'notes', 'notify', 'nudge', 'pulls', 'time', 'todos'].map((name) => `${name}=${nowhere}`), + ...['games', 'home', 'notes', 'notify', 'nudge', 'pulls', 'time', 'todos'].map( + (name) => `${name}=${nowhere}` + ), `web=tcp://${web}`, ].join(','), }, diff --git a/docs/plan.md b/docs/plan.md index 4e2d08d..ef851ca 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -191,7 +191,7 @@ The v1.2 design in §4 to §6 stays as reference. A piece of it is built only wh | `shared-page-status` | The PRs, Time, Nudge and Todos pages draw the line that takes their sections' place, such as "Bitbucket isn't set up.", from one global class, for the GitHub page's, its fifth user; on phones, the Nudge page's line now leaves a section's 48 px below it. The seventh of `github-widgets`' nine steps (decision 206) | Done (#187) | | `github-page` | The app shows the three lists on a page of their own at `/github`, as the user chose. The eighth of `github-widgets`' nine steps (decision 207) | Done (#191) | | `github-attention` | The `github` service answers the attention contract, so the Home's "Needs you" lists my open PRs, the PRs that wait for my review and the issues assigned to me, as the user chose; its new source, the first change to a contract in use, brings `proto:breaking` into `check`. The last of `github-widgets`' nine steps (decision 208) | Done (#194) | -| `games-exophase` | The games widgets from Exophase (§8.2 Phase 5), by a session of their own in `../bakhyard-games/`, which adds its steps' rows under this one (decision 195) | In progress | +| `games-exophase` | The games widgets from Exophase (§8.2 Phase 5), by a session of their own in `../bakhyard-games/`, which adds its steps' rows under this one (decision 195) | Done (#188); the laptop's covers wait for a Twitch app (decision 309) | | `games-plan` | `games-exophase` splits in eleven steps: the Exophase client, the cache, the service, the Twitch token, the IGDB search, the covers, the laptop, the gateway, the page and its covers; the games list, the covers' matches and the Twitch token stay in memory. The first of `games-exophase`'s eleven steps (decision 300) | Done (#168) | | `exophase-client` | The `games` service's Exophase client: the games I last played since a given time, the last played first, each with its platforms, playtime and trophies, from the games list's pages until one ends before that time, four at most; a 403 from Cloudflare's challenge says so. The second of `games-exophase`'s eleven steps (decision 301) | Done (#172) | | `games-cache` | The `games` service polls Exophase only on request: the first request waits for the first poll, later ones get the latest list at once and start a poll in the background once the list is an hour old; after a failed poll the last good list stays, and the next poll waits 5 minutes, twice as long after each failure in a row, up to 6 hours. The third of `games-exophase`'s eleven steps (decision 302) | Done (#175) | @@ -199,6 +199,7 @@ The v1.2 design in §4 to §6 stays as reference. A piece of it is built only wh | `twitch-token` | The `games` service gets a Twitch app token for IGDB with the client credentials grant, keeps it in memory until 9 tenths of its life, asks Twitch one request at a time, and forgets a token that IGDB refuses. The fifth of `games-exophase`'s eleven steps (decision 304) | Done (#181) | | `igdb-search` | The `games` service's IGDB client finds a title's cover: the top result of a search filtered to main games, expansions, remakes, remasters, expanded games and ports, or a game named exactly the title when the top result has another name; it asks one request at a time, at least 250 ms apart, and with a new token once when IGDB refuses one. The sixth of `games-exophase`'s eleven steps (decision 305) | Done (#183) | | `games-covers` | Each game in the `games` service's answer carries its IGDB cover when `BAKHYARD_TWITCH_CLIENT_ID` and the Keychain's `twitch-client-secret` are set: each title's cover, or that IGDB has none, is kept a week, the first failure stops the asking until the next fetch, and a missing cover never fails the list. The seventh of `games-exophase`'s eleven steps (decision 306) | Done (#186) | +| `games-gateway` | The gateway forwards `/api/games` to the `games` service, behind both of its locks, so the app reaches the games I played lately at `GET /api/games/recent`. `games` is one of the gateway's peers, each of which `BAKHYARD_PEERS` must name, and `mise run up` starts the gateway once games is ready. The ninth of `games-exophase`'s eleven steps (decision 300) | Done (#188) | | `games-page` | The Games page lists the games I played in the last 14 days from `GET /api/games/recent`, each time it shows: for each, the day I last played it, its title, its platforms, its trophies and its playtime. A list kept from an earlier fetch, after the latest failed, gets a Stale chip after its count, and the page says that Exophase isn't set up until the gateway forwards to `games`. The Games card says "Recent games and stats from Exophase". The tenth of `games-exophase`'s eleven steps (decision 307) | Done (#190) | | `games-page-covers` | The Games page shows each game's cover from IGDB, 48 by 68 px, between the day and the title, and on phones at the start of the row's second line. A game without a cover, a cover still loading and one that fails to load all leave the same blank, framed as a cover is, and screen readers skip the covers. The eleventh of `games-exophase`'s eleven steps (decision 308) | Done (#193) | | `vault-edit-sync` | Editing the vault from Bakhyard (§8.2 Phase 6), by the vault session in `../bakhyard-vault/`, which adds its steps' rows under this one and then does `copilot-planner` (decision 195) | In progress | @@ -435,7 +436,7 @@ flowchart LR | Service | Role | Listens on | Peers | Grows into (§8.2) | | --- | --- | --- | --- | --- | | nats | Event bus; JetStream on for later durable flows | `tcp://127.0.0.1:4222`, monitoring `:8222` | | reminders → notify | -| gateway | The only way in: `/api` to the services, everything else to web | `tcp://127.0.0.1:8080`; ops on an admin unix socket | web, home, notes, notify, nudge, pulls, time, todos | auth-signin | +| gateway | The only way in: `/api` to the services, everything else to web | `tcp://127.0.0.1:8080`; ops on an admin unix socket | web, games, home, notes, notify, nudge, pulls, time, todos | auth-signin | | web | Serves the embedded Svelte build | unix socket | | frontend-shell (PWA parts) | | home | Builds the Home screen from every app | unix socket | nats, time, notes, todos, pulls, github, games, planner | home-feed | | notes | Obsidian vault | unix socket | nats | vault-index, vault-render, vault-edit-sync | @@ -611,7 +612,7 @@ Streaming handlers watch an app-lifetime context, so open streams end promptly. - Each service has a `description`, a `command` (`bin/`, run with `bash -c` from the repo root) and an `environment` with its `BAKHYARD_LISTEN` and, for the gateway, `BAKHYARD_PEERS` (§4.7). - A readiness probe checks each service's `GET /healthz` (§4.7), first 1 s after the start, then every 2 s. A check gets 1 s to answer, and 3 failures in a row fail the probe. -- The gateway `depends_on` home, notes, notify, nudge, pulls, time, todos and web with `condition: process_healthy`, so it starts once their probes pass. +- The gateway `depends_on` games, home, notes, notify, nudge, pulls, time, todos and web with `condition: process_healthy`, so it starts once their probes pass. - What every service shares sits in an `x-service` YAML anchor, merged into each with `<<: *service`: - `availability.restart: always`: a service that exits, or fails its probe, restarts after 1 s, however often it needs to. A service stopped by hand stays stopped. - `shutdown.timeout_seconds: 10`: a stop sends SIGTERM to the service's process group, then SIGKILL if it's still running 10 s later. Without the timeout, process-compose never sends SIGKILL, and a hung service never stops. The group holds what a service starts too, such as the planner's Copilot CLI, so a hung CLI never outlives its service (decision 406). @@ -1308,10 +1309,11 @@ Every item is pending unless it says done. The service an item lands in is in br - `twitch-token`: the service gets a Twitch app token for IGDB, keeps it and renews it before it expires (decision 304). Done (#181). - `igdb-search`: the service's IGDB client finds a title's cover with a filtered search and an exact-name fallback (decisions 68 and 305), within IGDB's 4 requests a second. Done (#183). - `games-covers`: each game in the answer carries its IGDB cover; each title is matched once and the match kept for a week (decision 306). Done (#186). - - `homelab-games`: in the homelab repo, `games` runs on the laptop with its settings and secret, and the gateway's peers name it. - - `games-gateway`: the gateway forwards `/api/games` to `games`. + - `homelab-games`: done (codedogapp/homelab#44). In the homelab repo, `games` runs on the laptop with its setting, and the gateway's peers name it. Its games come without covers until there's a Twitch app (decision 309). + - `games-gateway`: the gateway forwards `/api/games` to `games`. Done (#188). - `games-page`: the Games page lists the games, and marks them stale when the latest fetch failed (decisions 67, 307); the Games card's description in the app becomes §7's. Done (#190). - `games-page-covers`: the Games page shows each game's cover (decision 308). Done (#193). + - `homelab-games-covers`: in the homelab repo, `games` gets a Twitch app's client ID and secret, so the laptop's games come with their covers. Waits for the user's Twitch app (decision 309). **Phase 6: vault edits and Copilot** @@ -1898,6 +1900,8 @@ Decisions 300 to 399 are the `games-exophase` session's. - **IGDB**: the browser loads the covers from IGDB's image host, which the gateway's Content Security Policy allows under `img-src https:`; the gateway's `Referrer-Policy: same-origin` keeps Bakhyard's address from IGDB. - **The demo**: the end-to-end tests and the screenshots show covers drawn as SVG, since IGDB's are third-party material and the repo is public. With the demo games, they live in `e2e/games.ts`, since `e2e/demo.ts` had reached its 200 lines. - **Rejected**: the cover before the day, where every other list starts its rows with the label; a bigger cover, such as 64 by 91, which would make every row taller than its text; leaving the column out when no game has a cover, as when IGDB isn't set up, which would change the rows' shape with the service's settings; greyscale covers, since §7's monochrome is for Bakhyard's own colours; an ``, with its broken-image icon, or with an error handler that swaps in the blank, which adds state for what a background does alone; the title as the cover's `alt`, which a screen reader would read twice; IGDB's `cover_small`, 90 by 128, which is blurry at 2x and would have the page rewrite the service's address; `loading="lazy"`, which a background doesn't have and a list of 14 days doesn't need. +309. **The laptop without covers for now** (`homelab-games`): the user's choices (2026-10-30). The user has no Twitch app yet, so codedogapp/homelab#44 runs `games` with the Exophase profile alone, and the games come without covers, on the laptop as on the Mac. Once the app exists, `homelab-games-covers` adds its client ID to the Secret `games-settings`, beside the profile, rather than to `games.yaml`, and its secret to a Secret `games-secrets` mounted at `/secrets`. + - **Rejected**: holding codedogapp/homelab#44 and #188 until the app exists, which would keep the games off the laptop for the sake of their covers. Decisions 400 to 499 are the vault session's: `vault-edit-sync`, then `copilot-planner`. diff --git a/docs/setup.md b/docs/setup.md index d6a7d0c..6633939 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -31,7 +31,7 @@ cp mise.local.example.toml mise.local.toml | `BAKHYARD_COPILOT_HOME` | planner | The Copilot CLI's own folder, as an absolute path | | `BAKHYARD_TEST_DATABASE` | Go tests | The test database's URL, password included ([below](#the-test-database)) | -The gateway doesn't start without its four settings, nor notes without its vault: each exits 1 and says what's missing. The gateway also waits for notes, notify, nudge, pulls, time and todos to start. Todos and time run without Jira when none of the three Jira settings is set, pulls without Bitbucket when none of the three Bitbucket settings is set, github without GitHub when `BAKHYARD_GITHUB_API` isn't set, and games without Exophase when `BAKHYARD_EXOPHASE_PLAYER` isn't set and without covers when `BAKHYARD_TWITCH_CLIENT_ID` isn't set, and planner without Copilot when `BAKHYARD_COPILOT_HOST` isn't set. +The gateway doesn't start without its four settings, nor notes without its vault: each exits 1 and says what's missing. The gateway also waits for games, notes, notify, nudge, pulls, time and todos to start. Todos and time run without Jira when none of the three Jira settings is set, pulls without Bitbucket when none of the three Bitbucket settings is set, github without GitHub when `BAKHYARD_GITHUB_API` isn't set, and games without Exophase when `BAKHYARD_EXOPHASE_PLAYER` isn't set and without covers when `BAKHYARD_TWITCH_CLIENT_ID` isn't set, and planner without Copilot when `BAKHYARD_COPILOT_HOST` isn't set. On the Mac, the gateway, todos, pulls, time, github, games and planner read their secrets from the login Keychain, as the sections below show. On the laptop, which has no Keychain, `BAKHYARD_SECRETS` names a folder of files instead, each named as its secret's Keychain account (`google-client-secret`, `postgres-gateway`, `postgres-todos`, `postgres-notify`, `postgres-nudge`, `vapid-key`, `jira-token`, `bitbucket-token`, `github-token`, `twitch-client-secret`, `copilot-token`) and holding the secret exactly, with no newline at the end: the files of the Kubernetes Secrets mounted there (decision 154). Leave it unset on the Mac. @@ -190,7 +190,7 @@ Games lists the games you played in the last 14 days, the last played first, wit 1. Open your profile page on Exophase, show the page's source, and find `window.playerProfileId`: the number it's set to is your profile's id. 2. In `mise.local.toml`, set `BAKHYARD_EXOPHASE_PLAYER` to that number, such as `1234567`, then restart games. -The id must be digits only; with anything else, games exits 1 and says so: `BAKHYARD_EXOPHASE_PLAYER: "…" is not an Exophase profile id, which is digits only`. Without it, `GET /games/recent` answers 404 `Exophase is not set up`. `mise run up` runs games on port 8090, where `GET /games/recent` answers the games, with when the latest fetch to succeed began and why the latest fetch failed, if it did. Games fetches the list only when asked: the first request waits for the first fetch, and later ones get the list at once, while one that finds it an hour old or more starts a fetch in the background. When a fetch fails, for example while Exophase is down, games keeps answering the last list it got, adds the failure, such as `list my Exophase games: 500 Internal Server Error`, which is also what Exophase answers for an id it doesn't know, `list my Exophase games: 403 Forbidden, a Cloudflare challenge` or, for a profile that isn't public, `list my Exophase games: Exophase has no public profile with this id`, and logs `poll Exophase` with it. The next fetch then waits 5 minutes, and twice as long after each failure in a row, up to 6 hours. The list lives in games' memory, so a restart fetches it again. +The id must be digits only; with anything else, games exits 1 and says so: `BAKHYARD_EXOPHASE_PLAYER: "…" is not an Exophase profile id, which is digits only`. Without it, `GET /games/recent` answers 404 `Exophase is not set up`. `mise run up` runs games on port 8090, and the gateway forwards `/api/games` to it, so `GET /api/games/recent` reaches `GET /games/recent`, which answers the games, with when the latest fetch to succeed began and why the latest fetch failed, if it did. Games fetches the list only when asked: the first request waits for the first fetch, and later ones get the list at once, while one that finds it an hour old or more starts a fetch in the background. When a fetch fails, for example while Exophase is down, games keeps answering the last list it got, adds the failure, such as `list my Exophase games: 500 Internal Server Error`, which is also what Exophase answers for an id it doesn't know, `list my Exophase games: 403 Forbidden, a Cloudflare challenge` or, for a profile that isn't public, `list my Exophase games: Exophase has no public profile with this id`, and logs `poll Exophase` with it. The next fetch then waits 5 minutes, and twice as long after each failure in a row, up to 6 hours. The list lives in games' memory, so a restart fetches it again. ## IGDB diff --git a/process-compose.yaml b/process-compose.yaml index 295981a..78974d7 100644 --- a/process-compose.yaml +++ b/process-compose.yaml @@ -173,9 +173,11 @@ processes: command: bin/gateway environment: - BAKHYARD_LISTEN=tcp://127.0.0.1:8080 - - BAKHYARD_PEERS=home=tcp://127.0.0.1:8081,notes=tcp://127.0.0.1:8083,notify=tcp://127.0.0.1:8086,nudge=tcp://127.0.0.1:8087,pulls=tcp://127.0.0.1:8085,time=tcp://127.0.0.1:8088,todos=tcp://127.0.0.1:8084,web=tcp://127.0.0.1:8082 + - BAKHYARD_PEERS=games=tcp://127.0.0.1:8090,home=tcp://127.0.0.1:8081,notes=tcp://127.0.0.1:8083,notify=tcp://127.0.0.1:8086,nudge=tcp://127.0.0.1:8087,pulls=tcp://127.0.0.1:8085,time=tcp://127.0.0.1:8088,todos=tcp://127.0.0.1:8084,web=tcp://127.0.0.1:8082 - BAKHYARD_DATABASE=postgres://bakhyard_gateway@127.0.0.1:5433/bakhyard_gateway depends_on: + games: + condition: process_healthy home: condition: process_healthy notes: diff --git a/services/gateway/headers_test.go b/services/gateway/headers_test.go index aa5f618..3da3eb6 100644 --- a/services/gateway/headers_test.go +++ b/services/gateway/headers_test.go @@ -30,7 +30,7 @@ func TestEveryResponseCarriesTheSecurityHeaders(t *testing.T) { t.Cleanup(web.Close) vars, items := gatewayEnv(t, web.URL) - vars["BAKHYARD_PEERS"] = "home=tcp://127.0.0.1:1" + + vars["BAKHYARD_PEERS"] = "games=tcp://127.0.0.1:1,home=tcp://127.0.0.1:1" + ",notes=tcp://" + strings.TrimPrefix(notes.URL, "http://") + ",notify=tcp://127.0.0.1:1,nudge=tcp://127.0.0.1:1" + ",pulls=tcp://127.0.0.1:1,time=tcp://127.0.0.1:1,todos=tcp://127.0.0.1:1" + diff --git a/services/gateway/lock_test.go b/services/gateway/lock_test.go index af821cb..d2a71da 100644 --- a/services/gateway/lock_test.go +++ b/services/gateway/lock_test.go @@ -26,6 +26,7 @@ func TestTheGatewayNeedsASessionForTheAPIOnly(t *testing.T) { {http.MethodGet, "/", http.StatusOK}, {http.MethodHead, "/notes", http.StatusOK}, {http.MethodGet, "/auth/other", http.StatusOK}, + {http.MethodGet, "/api/games/recent", http.StatusUnauthorized}, {http.MethodGet, "/api/home", http.StatusUnauthorized}, {http.MethodPost, "/api/home", http.StatusUnauthorized}, {http.MethodGet, "/api/notes", http.StatusUnauthorized}, @@ -70,10 +71,11 @@ func TestTheGatewayKeepsTheSessionFromTheServices(t *testing.T) { return srv } home, pulls, todos, web := service("home"), service("pulls"), service("todos"), service("web") - timeService := service("time") + games, timeService := service("games"), service("time") vars, items := gatewayEnv(t, web.URL) - vars["BAKHYARD_PEERS"] = "home=tcp://" + strings.TrimPrefix(home.URL, "http://") + + vars["BAKHYARD_PEERS"] = "games=tcp://" + strings.TrimPrefix(games.URL, "http://") + + ",home=tcp://" + strings.TrimPrefix(home.URL, "http://") + ",notes=tcp://127.0.0.1:1,notify=tcp://127.0.0.1:1,nudge=tcp://127.0.0.1:1" + ",pulls=tcp://" + strings.TrimPrefix(pulls.URL, "http://") + ",time=tcp://" + strings.TrimPrefix(timeService.URL, "http://") + @@ -87,6 +89,7 @@ func TestTheGatewayKeepsTheSessionFromTheServices(t *testing.T) { target, service string renewed bool }{ + {"/api/games/recent", "games", true}, {"/api/home", "home", true}, {"/api/pulls/mine", "pulls", true}, {"/api/time/report", "time", true}, diff --git a/services/gateway/main.go b/services/gateway/main.go index a48efb1..4e4b365 100644 --- a/services/gateway/main.go +++ b/services/gateway/main.go @@ -111,12 +111,13 @@ func routes(getenv func(string) string, readSecret secrets.Reader) (http.Handler // upstreams are the URLs of the services that the gateway forwards to. type upstreams struct { - home, notes, notify, nudge, pulls, time, todos, web *url.URL + games, home, notes, notify, nudge, pulls, time, todos, web *url.URL } -// peers returns the URLs of home, notes, notify, nudge, pulls, time, todos and web from +// peers returns the URLs of games, home, notes, notify, nudge, pulls, time, todos and web from // BAKHYARD_PEERS, or an error that names every one of them it lacks. func peers(getenv func(string) string) (upstreams, error) { + games, gamesErr := peer(getenv, "games") home, homeErr := peer(getenv, "home") notes, notesErr := peer(getenv, "notes") notify, notifyErr := peer(getenv, "notify") @@ -127,13 +128,14 @@ func peers(getenv func(string) string) (upstreams, error) { web, webErr := peer(getenv, "web") err := errors.Join( - homeErr, notesErr, notifyErr, nudgeErr, pullsErr, timeErr, todosErr, webErr, + gamesErr, homeErr, notesErr, notifyErr, nudgeErr, pullsErr, timeErr, todosErr, webErr, ) if err != nil { return upstreams{}, err } return upstreams{ + games: games, home: home, notes: notes, notify: notify, diff --git a/services/gateway/main_test.go b/services/gateway/main_test.go index 8bb8dcf..1ab157d 100644 --- a/services/gateway/main_test.go +++ b/services/gateway/main_test.go @@ -76,10 +76,11 @@ func TestTheGatewayLetsTheUserIn(t *testing.T) { router, token := gateway(t, web.URL) - // Nothing listens where the test gateway's home, notes, pulls and todos are, so a 503 means the - // request reached their forwarder. An /api path that no service claims keeps its 404 behind - // the lock. + // Nothing listens where the test gateway's games, home, notes, pulls and todos are, so a 503 + // means the request reached their forwarder. An /api path that no service claims keeps its 404 + // behind the lock. for target, want := range map[string]int{ + "/api/games/recent": http.StatusServiceUnavailable, "/api/home": http.StatusServiceUnavailable, "/api/notes": http.StatusServiceUnavailable, "/api/notes/files/Inbox.md": http.StatusServiceUnavailable, @@ -108,7 +109,8 @@ func TestTheGatewayRefusesEveryoneElse(t *testing.T) { // The Tailscale lock comes first: a session doesn't open it. for _, target := range []string{ "/", "/notes", "/api/home", "/api/notes", "/api/notes/files/Inbox.md", "/api/pulls/mine", - "/api/todos", "/api/nothing", "/auth/google/login", "/auth/google/callback", + "/api/todos", "/api/games/recent", "/api/nothing", + "/auth/google/login", "/auth/google/callback", "/agent/questions", "/agent/questions/x", "/agent/sessions", "/agent/sessions/x/note", } { for _, method := range []string{http.MethodGet, http.MethodHead, http.MethodPost} { @@ -207,7 +209,7 @@ func withSession(req *http.Request, token string) *http.Request { return req } -// gatewayEnv returns the test gateway's environment and secrets, with home, notes, pulls +// gatewayEnv returns the test gateway's environment and secrets, with games, home, notes, pulls // and todos on a port where nothing listens, web at webURL and the database in a schema of its // own. func gatewayEnv(t *testing.T, webURL string) (vars, items map[string]string) { @@ -215,12 +217,10 @@ func gatewayEnv(t *testing.T, webURL string) (vars, items map[string]string) { schema := databasetest.NewSchema(t) vars = map[string]string{ - "BAKHYARD_PEERS": "home=tcp://127.0.0.1:1,notes=tcp://127.0.0.1:1,notify=tcp://127.0.0.1:1" + - ",nudge=tcp://127.0.0.1:1,pulls=tcp://127.0.0.1:1" + - ",time=tcp://127.0.0.1:1,todos=tcp://127.0.0.1:1,web=tcp://" + strings.TrimPrefix( - webURL, - "http://", - ), + "BAKHYARD_PEERS": "games=tcp://127.0.0.1:1,home=tcp://127.0.0.1:1" + + ",notes=tcp://127.0.0.1:1,notify=tcp://127.0.0.1:1,nudge=tcp://127.0.0.1:1" + + ",pulls=tcp://127.0.0.1:1,time=tcp://127.0.0.1:1,todos=tcp://127.0.0.1:1" + + ",web=tcp://" + strings.TrimPrefix(webURL, "http://"), "BAKHYARD_TAILSCALE_LOGIN": testLogin, "BAKHYARD_HOST": testHost, "BAKHYARD_GOOGLE_CLIENT_ID": "client", diff --git a/services/gateway/open_test.go b/services/gateway/open_test.go index 4d35fb9..125ef2e 100644 --- a/services/gateway/open_test.go +++ b/services/gateway/open_test.go @@ -11,6 +11,7 @@ func TestTheOpenGatewayLetsEveryRequestThrough(t *testing.T) { router := openGateway(t) for _, test := range []struct{ method, target, want string }{ + {http.MethodGet, "/api/games/recent", "games got GET /games/recent"}, {http.MethodGet, "/api/home", "home got GET /home"}, {http.MethodGet, "/api/notes/files/Inbox.md", "notes got GET /notes/files/Inbox.md"}, {http.MethodGet, "/api/pulls/mine", "pulls got GET /pulls/mine"}, @@ -101,9 +102,7 @@ func TestOnlyTheEndToEndBuildIsOpen(t *testing.T) { // The open gateway needs nothing but its peers; the real one needs its sign-in settings too. _, err := served(secretsWith(nil))(t.Context(), func(key string) string { if key == "BAKHYARD_PEERS" { - return "home=tcp://127.0.0.1:1,notes=tcp://127.0.0.1:1,notify=tcp://127.0.0.1:1" + - ",nudge=tcp://127.0.0.1:1,pulls=tcp://127.0.0.1:1" + - ",time=tcp://127.0.0.1:1,todos=tcp://127.0.0.1:1,web=tcp://127.0.0.1:1" + return withPeers() } return "" diff --git a/services/gateway/peers_test.go b/services/gateway/peers_test.go index 7277174..2777c62 100644 --- a/services/gateway/peers_test.go +++ b/services/gateway/peers_test.go @@ -23,7 +23,7 @@ func TestTheGatewayForwardsEachAPIToItsService(t *testing.T) { home, notes := service("home"), service("notes") pulls, todos := service("pulls"), service("todos") notify, nudge := service("notify"), service("nudge") - timeService := service("time") + games, timeService := service("games"), service("time") web := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { t.Errorf("web got %s %s, want nothing", r.Method, r.URL.Path) @@ -31,7 +31,8 @@ func TestTheGatewayForwardsEachAPIToItsService(t *testing.T) { t.Cleanup(web.Close) vars, items := gatewayEnv(t, web.URL) - vars["BAKHYARD_PEERS"] = "home=tcp://" + strings.TrimPrefix(home.URL, "http://") + + vars["BAKHYARD_PEERS"] = "games=tcp://" + strings.TrimPrefix(games.URL, "http://") + + ",home=tcp://" + strings.TrimPrefix(home.URL, "http://") + ",notes=tcp://" + strings.TrimPrefix(notes.URL, "http://") + ",notify=tcp://" + strings.TrimPrefix(notify.URL, "http://") + ",nudge=tcp://" + strings.TrimPrefix(nudge.URL, "http://") + @@ -44,6 +45,8 @@ func TestTheGatewayForwardsEachAPIToItsService(t *testing.T) { // Whatever the method or the path under /api/, the service answers for itself // (decisions 41 and 92). for _, test := range []struct{ method, target, want string }{ + {http.MethodGet, "/api/games/recent", "games got GET /games/recent"}, + {http.MethodPost, "/api/games", "games got POST /games"}, {http.MethodGet, "/api/home", "home got GET /home"}, {http.MethodGet, "/api/notes", "notes got GET /notes"}, {http.MethodPost, "/api/notes", "notes got POST /notes"}, @@ -79,7 +82,7 @@ func TestTheGatewayForwardsEachAPIToItsService(t *testing.T) { // peerNames returns the names of the gateway's peers, each of which BAKHYARD_PEERS must name. func peerNames() []string { - return []string{"home", "notes", "notify", "nudge", "pulls", "time", "todos", "web"} + return []string{"games", "home", "notes", "notify", "nudge", "pulls", "time", "todos", "web"} } // withPeers returns BAKHYARD_PEERS with every peer but those in lacking. @@ -103,6 +106,8 @@ func TestTheGatewayNeedsItsPeers(t *testing.T) { withPeers("notify", "nudge"): {"notify", "nudge"}, withPeers("pulls", "todos"): {"pulls", "todos"}, withPeers("time"): {"time"}, + withPeers("games"): {"games"}, + withPeers("games", "web"): {"games", "web"}, "": peerNames(), } { vars, items := gatewayEnv(t, "http://127.0.0.1:2") diff --git a/services/gateway/router.go b/services/gateway/router.go index 1bc516e..b8f9227 100644 --- a/services/gateway/router.go +++ b/services/gateway/router.go @@ -39,6 +39,7 @@ func wire(upstream upstreams, guard locks, logger *slog.Logger) http.Handler { // The API needs a session as well (decision 86). router.Group(func(api chi.Router) { api.Use(guard.identified, guard.signedIn, session.Hide) + proxy.Register(api, "games", upstream.games, logger) proxy.Register(api, "home", upstream.home, logger) proxy.Register(api, "notes", upstream.notes, logger) proxy.Register(api, "notify", upstream.notify, logger)