From 2976c08ef0a7d8b616c93476683f4a1e2bf4eca7 Mon Sep 17 00:00:00 2001 From: Bakhomious Basta Date: Fri, 30 Oct 2026 03:06:50 +0100 Subject: [PATCH 1/5] feat(gateway): forward /api/github to the github service The GitHub page and Home's "Needs you" need my lists through the gateway. The gateway now requires a github peer and forwards /api/github behind both locks, hiding the session cookie as for every service. mise run up starts the gateway once github is healthy, and the end-to-end gateway names the peer too. --- apps/web/playwright.config.ts | 4 +++- docs/plan.md | 6 +++--- docs/setup.md | 4 ++-- process-compose.yaml | 4 +++- services/gateway/headers_test.go | 2 +- services/gateway/lock_test.go | 7 +++++-- services/gateway/main.go | 8 +++++--- services/gateway/main_test.go | 10 ++++------ services/gateway/open_test.go | 5 ++--- services/gateway/peers_test.go | 9 ++++++--- services/gateway/router.go | 1 + 11 files changed, 35 insertions(+), 25 deletions(-) diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index 81c73f3..61a679f 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}`), + ...['github', '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 18af464..c7d296e 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -186,7 +186,7 @@ The v1.2 design in §4 to §6 stays as reference. A piece of it is built only wh | `github-client` | A client of GitHub's search, in the new `github` service's folder, finds my open PRs, the PRs that wait for my review or one of my teams', and the open issues assigned to me, across every owner, in repositories that aren't archived, the latest updated first, as the user whose classic PAT it holds. It reads up to ten pages of 100, lists each item once, and fails the whole search on a bad answer or one that GitHub marks incomplete. Nothing calls it yet. The second of `github-widgets`' seven steps (decisions 200, 202 and 203) | Done (#173) | | `github-service` | A new `github` service polls the three searches as it starts and every 5 minutes after, and answers them at `GET /github/mine`, `GET /github/reviews` and `GET /github/issues`; without GitHub's setting it answers 404. `mise run up` runs it on port 8089 and the Gitea workflow builds its image. The third of `github-widgets`' seven steps (decisions 200 and 204) | Done (#176) | | `homelab-github` | The homelab repo runs the `github` service on the laptop, with the token in a Secret, and names it among the gateway's and home's peers, before `github-gateway` makes the gateway require it. The fourth of `github-widgets`' seven steps (decision 200) | Pending | -| `github-gateway` | The gateway forwards `/api/github` to the `github` service, behind both of its locks. The fifth of `github-widgets`' seven steps (decision 200) | Pending | +| `github-gateway` | The gateway forwards `/api/github` to the `github` service, behind both of its locks, so the app reaches my three lists at `GET /api/github/mine`, `GET /api/github/reviews` and `GET /api/github/issues`. `github` is one of the gateway's peers, each of which `BAKHYARD_PEERS` must name, and `mise run up` starts the gateway once github is ready. The fifth of `github-widgets`' seven steps (decision 200) | In review | | `github-page` | The app shows the three lists, on a page of their own at `/github` or beside Bitbucket's on the PRs page, as the user chooses. The sixth of `github-widgets`' seven steps (decision 200) | Pending | | `github-attention` | The `github` service answers the attention contract, so the Home's "Needs you" lists what the user chooses of the review requests, the issues assigned to me and my PRs. The last of `github-widgets`' seven steps (decision 200) | Pending | | `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 | @@ -406,7 +406,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, github, 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 | @@ -582,7 +582,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` github, 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. diff --git a/docs/setup.md b/docs/setup.md index bf9047d..7ca66b3 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -27,7 +27,7 @@ cp mise.local.example.toml mise.local.toml | `BAKHYARD_GITHUB_API` | github | GitHub's REST API, `https://api.github.com`, where github searches for your open pull requests, review requests and issues ([below](#github)) | | `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, and github without GitHub when `BAKHYARD_GITHUB_API` 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 github, 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, and github without GitHub when `BAKHYARD_GITHUB_API` isn't set. On the Mac, the gateway, todos, pulls, time and github 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`) 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. @@ -177,7 +177,7 @@ The github service searches GitHub, across every owner that your token reaches, 3. In `mise.local.toml`, set `BAKHYARD_GITHUB_API` to `https://api.github.com`, then restart github. -The API's URL must be https, as Jira's must; only a loopback IP address may take http, for a stand-in GitHub in tests. With a URL that isn't https or no token in the Keychain, github exits 1 and says which: `BAKHYARD_GITHUB_API: "…" is not an https URL` or `read the GitHub token: no Keychain item with service bakhyard and account github-token`. `mise run up` runs it on port 8089. Without the setting, `GET /github/mine`, `GET /github/reviews` and `GET /github/issues` answer 404 `GitHub is not set up`. With it, github searches GitHub for each of the three lists as it starts and every 5 minutes after, the latest updated first, and each route answers its list, with when the latest search to succeed began and why the latest search failed, if it did. When GitHub refuses the token, for example once it has expired, each route keeps answering what the last search to succeed found and adds GitHub's answer as its `failure`, such as `search GitHub for my open PRs: 401 Unauthorized`, and github logs `poll GitHub`, `poll GitHub reviews` or `poll GitHub issues` with it. To replace the token, add `-U` to the command in step 2, then restart github. +The API's URL must be https, as Jira's must; only a loopback IP address may take http, for a stand-in GitHub in tests. With a URL that isn't https or no token in the Keychain, github exits 1 and says which: `BAKHYARD_GITHUB_API: "…" is not an https URL` or `read the GitHub token: no Keychain item with service bakhyard and account github-token`. `mise run up` runs it on port 8089, and the gateway forwards `/api/github` to it, so `GET /api/github/mine` reaches `GET /github/mine`. Without the setting, `GET /github/mine`, `GET /github/reviews` and `GET /github/issues` answer 404 `GitHub is not set up`. With it, github searches GitHub for each of the three lists as it starts and every 5 minutes after, the latest updated first, and each route answers its list, with when the latest search to succeed began and why the latest search failed, if it did. When GitHub refuses the token, for example once it has expired, each route keeps answering what the last search to succeed found and adds GitHub's answer as its `failure`, such as `search GitHub for my open PRs: 401 Unauthorized`, and github logs `poll GitHub`, `poll GitHub reviews` or `poll GitHub issues` with it. To replace the token, add `-U` to the command in step 2, then restart github. ## Run diff --git a/process-compose.yaml b/process-compose.yaml index c3d4a66..eee88fd 100644 --- a/process-compose.yaml +++ b/process-compose.yaml @@ -147,9 +147,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=github=tcp://127.0.0.1:8089,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: + github: + condition: process_healthy home: condition: process_healthy notes: diff --git a/services/gateway/headers_test.go b/services/gateway/headers_test.go index aa5f618..6bb3ecb 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"] = "github=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..da6d072 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/github/mine", 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") + github, timeService := service("github"), service("time") vars, items := gatewayEnv(t, web.URL) - vars["BAKHYARD_PEERS"] = "home=tcp://" + strings.TrimPrefix(home.URL, "http://") + + vars["BAKHYARD_PEERS"] = "github=tcp://" + strings.TrimPrefix(github.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/github/mine", "github", 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..b8a82f5 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 + github, 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 github, 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) { + github, githubErr := peer(getenv, "github") 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, + githubErr, homeErr, notesErr, notifyErr, nudgeErr, pullsErr, timeErr, todosErr, webErr, ) if err != nil { return upstreams{}, err } return upstreams{ + github: github, home: home, notes: notes, notify: notify, diff --git a/services/gateway/main_test.go b/services/gateway/main_test.go index 8bb8dcf..9e2d144 100644 --- a/services/gateway/main_test.go +++ b/services/gateway/main_test.go @@ -215,12 +215,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": "github=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..c688209 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/github/mine", "github got GET /github/mine"}, {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..33b2b47 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") + github, timeService := service("github"), 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"] = "github=tcp://" + strings.TrimPrefix(github.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,7 @@ 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/github/mine", "github got GET /github/mine"}, {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 +81,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{"github", "home", "notes", "notify", "nudge", "pulls", "time", "todos", "web"} } // withPeers returns BAKHYARD_PEERS with every peer but those in lacking. @@ -103,6 +105,7 @@ func TestTheGatewayNeedsItsPeers(t *testing.T) { withPeers("notify", "nudge"): {"notify", "nudge"}, withPeers("pulls", "todos"): {"pulls", "todos"}, withPeers("time"): {"time"}, + withPeers("github"): {"github"}, "": 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..8f875ec 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, "github", upstream.github, logger) proxy.Register(api, "home", upstream.home, logger) proxy.Register(api, "notes", upstream.notes, logger) proxy.Register(api, "notify", upstream.notify, logger) From 1a347d170e46a17ab490d8de81c946d167db3309 Mon Sep 17 00:00:00 2001 From: Bakhomious Basta Date: Fri, 30 Oct 2026 03:11:54 +0100 Subject: [PATCH 2/5] test(gateway): check both locks on /api/github A github route behind the session lock alone passed every test, since the tables of the Tailscale lock didn't name /api/github. --- services/gateway/main_test.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/services/gateway/main_test.go b/services/gateway/main_test.go index 9e2d144..c8a2115 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 github, 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/github/mine": 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/github/mine", "/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} { From 49fa3b5b8e9a34dc4d1195c607f2b0922e444d08 Mon Sep 17 00:00:00 2001 From: Bakhomious Basta Date: Fri, 30 Oct 2026 03:13:21 +0100 Subject: [PATCH 3/5] docs: mark github-gateway done (#179) --- docs/plan.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plan.md b/docs/plan.md index 918bc30..572d69c 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -186,7 +186,7 @@ The v1.2 design in §4 to §6 stays as reference. A piece of it is built only wh | `github-client` | A client of GitHub's search, in the new `github` service's folder, finds my open PRs, the PRs that wait for my review or one of my teams', and the open issues assigned to me, across every owner, in repositories that aren't archived, the latest updated first, as the user whose classic PAT it holds. It reads up to ten pages of 100, lists each item once, and fails the whole search on a bad answer or one that GitHub marks incomplete. Nothing calls it yet. The second of `github-widgets`' seven steps (decisions 200, 202 and 203) | Done (#173) | | `github-service` | A new `github` service polls the three searches as it starts and every 5 minutes after, and answers them at `GET /github/mine`, `GET /github/reviews` and `GET /github/issues`; without GitHub's setting it answers 404. `mise run up` runs it on port 8089 and the Gitea workflow builds its image. The third of `github-widgets`' seven steps (decisions 200 and 204) | Done (#176) | | `homelab-github` | The homelab repo runs the `github` service on the laptop, with the token in a Secret, and names it among the gateway's and home's peers, before `github-gateway` makes the gateway require it. The fourth of `github-widgets`' seven steps (decision 200) | Pending | -| `github-gateway` | The gateway forwards `/api/github` to the `github` service, behind both of its locks, so the app reaches my three lists at `GET /api/github/mine`, `GET /api/github/reviews` and `GET /api/github/issues`. `github` is one of the gateway's peers, each of which `BAKHYARD_PEERS` must name, and `mise run up` starts the gateway once github is ready. The fifth of `github-widgets`' seven steps (decision 200) | In review | +| `github-gateway` | The gateway forwards `/api/github` to the `github` service, behind both of its locks, so the app reaches my three lists at `GET /api/github/mine`, `GET /api/github/reviews` and `GET /api/github/issues`. `github` is one of the gateway's peers, each of which `BAKHYARD_PEERS` must name, and `mise run up` starts the gateway once github is ready. The fifth of `github-widgets`' seven steps (decision 200) | Done (#179) | | `github-page` | The app shows the three lists, on a page of their own at `/github` or beside Bitbucket's on the PRs page, as the user chooses. The sixth of `github-widgets`' seven steps (decision 200) | Pending | | `github-attention` | The `github` service answers the attention contract, so the Home's "Needs you" lists what the user chooses of the review requests, the issues assigned to me and my PRs. The last of `github-widgets`' seven steps (decision 200) | Pending | | `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 | @@ -1259,7 +1259,7 @@ Every item is pending unless it says done. The service an item lands in is in br - `github-client`: a client of GitHub's search finds my open PRs, the PRs that wait for my review or my teams', and the open issues assigned to me, across every owner, through a classic PAT with `repo` and `read:org` (decisions 202 and 203). Done (#173). - `github-service`: the `github` service polls the three searches every 5 minutes, within search's 30 requests a minute, and answers them at `GET /github/mine`, `/github/reviews` and `/github/issues`, or 404 without its setting (decision 204). Done (#176). - `homelab-github`: the laptop runs the service, with its token in a Secret, before the gateway requires it. - - `github-gateway`: the gateway forwards `/api/github` to the service. + - `github-gateway`: the gateway forwards `/api/github` to the service. Done (#179). - `github-page`: the lists in the app, where the user chooses: a page of their own at `/github`, or beside Bitbucket's on the PRs page. - `github-attention`: what the user chooses of the lists reaches the Home's "Needs you", over the attention contract. From 084db88ad34aae323a77867c85738c81b7ab21f1 Mon Sep 17 00:00:00 2001 From: Bakhomious Basta Date: Fri, 30 Oct 2026 11:34:26 +0100 Subject: [PATCH 4/5] docs(plan): count github-widgets' nine steps in each of its rows The rows written before decisions 205 and 206 added two steps still said seven or eight. --- docs/plan.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/plan.md b/docs/plan.md index 97e0a3c..6803228 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -182,12 +182,12 @@ The v1.2 design in §4 to §6 stays as reference. A piece of it is built only wh | `nudge-merge` | Nudge, the user's phone-questions tool for agents (`codedogapp/nudge`), moves into Bakhyard: one app, not two (decision 157). The nudge session does the work, one small PR per step, from its plan, [`bakhyard-merge.md`](https://github.com/codedogapp/nudge/blob/main/docs/bakhyard-merge.md); this session doesn't stop its own steps for it | In review | | `parallel-sessions` | Several sessions work at once, each on its own feature in a worktree of its own, with this session as the orchestrator: AGENTS.md says who works where, plan §0 and §9 give each session places of its own, the end-to-end tests take their ports from `BAKHYARD_E2E_PORT`, golangci-lint runs wait for each other, and the tests' time limits allow for the other sessions' load (decision 195) | Done (#166) | | `github-widgets` | The GitHub widgets (§8.2 Phase 4), by a session of their own in `../bakhyard-github/`, which adds its steps' rows under this one (decision 195) | In progress | -| `shared-web-link` | The Bitbucket client's check that a PR's page is an https URL moves to a package of its own, `internal/weblink`, for the GitHub client, its second user; nothing changes in behaviour. The first of `github-widgets`' seven steps (decisions 200 and 201) | Done (#171) | -| `github-client` | A client of GitHub's search, in the new `github` service's folder, finds my open PRs, the PRs that wait for my review or one of my teams', and the open issues assigned to me, across every owner, in repositories that aren't archived, the latest updated first, as the user whose classic PAT it holds. It reads up to ten pages of 100, lists each item once, and fails the whole search on a bad answer or one that GitHub marks incomplete. Nothing calls it yet. The second of `github-widgets`' seven steps (decisions 200, 202 and 203) | Done (#173) | -| `github-service` | A new `github` service polls the three searches as it starts and every 5 minutes after, and answers them at `GET /github/mine`, `GET /github/reviews` and `GET /github/issues`; without GitHub's setting it answers 404. `mise run up` runs it on port 8089 and the Gitea workflow builds its image. The third of `github-widgets`' seven steps (decisions 200 and 204) | Done (#176) | -| `homelab-github` | The homelab repo runs the `github` service on the laptop, with the token in a Secret, and names it among the gateway's and home's peers, before `github-gateway` makes the gateway require it. The fourth of `github-widgets`' seven steps (decision 200) | Pending | +| `shared-web-link` | The Bitbucket client's check that a PR's page is an https URL moves to a package of its own, `internal/weblink`, for the GitHub client, its second user; nothing changes in behaviour. The first of `github-widgets`' nine steps (decisions 200 and 201) | Done (#171) | +| `github-client` | A client of GitHub's search, in the new `github` service's folder, finds my open PRs, the PRs that wait for my review or one of my teams', and the open issues assigned to me, across every owner, in repositories that aren't archived, the latest updated first, as the user whose classic PAT it holds. It reads up to ten pages of 100, lists each item once, and fails the whole search on a bad answer or one that GitHub marks incomplete. Nothing calls it yet. The second of `github-widgets`' nine steps (decisions 200, 202 and 203) | Done (#173) | +| `github-service` | A new `github` service polls the three searches as it starts and every 5 minutes after, and answers them at `GET /github/mine`, `GET /github/reviews` and `GET /github/issues`; without GitHub's setting it answers 404. `mise run up` runs it on port 8089 and the Gitea workflow builds its image. The third of `github-widgets`' nine steps (decisions 200 and 204) | Done (#176) | +| `homelab-github` | The homelab repo runs the `github` service on the laptop, with the token in a Secret, and names it among the gateway's and home's peers, before `github-gateway` makes the gateway require it. The fourth of `github-widgets`' nine steps (decision 200) | Pending | | `github-gateway` | The gateway forwards `/api/github` to the `github` service, behind both of its locks, so the app reaches my three lists at `GET /api/github/mine`, `GET /api/github/reviews` and `GET /api/github/issues`. `github` is one of the gateway's peers, each of which `BAKHYARD_PEERS` must name, and `mise run up` starts the gateway once github is ready. The fifth of `github-widgets`' nine steps (decision 200) | Done (#179) | -| `shared-polled-section` | The PRs page's lists and the Todos page's Jira tickets show a poller's list through one component in the shell, `PolledSection`, for the GitHub page's lists, its third user; nothing changes on screen. The sixth of `github-widgets`' eight steps (decision 205) | Done (#184) | +| `shared-polled-section` | The PRs page's lists and the Todos page's Jira tickets show a poller's list through one component in the shell, `PolledSection`, for the GitHub page's lists, its third user; nothing changes on screen. The sixth of `github-widgets`' nine steps (decision 205) | Done (#184) | | `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) | From 1e81a174544bb5ab738ec8f929e61ca0f70e9a40 Mon Sep 17 00:00:00 2001 From: Bakhomious Basta Date: Fri, 30 Oct 2026 12:59:55 +0100 Subject: [PATCH 5/5] docs(plan): mark homelab-github and github-widgets done codedogapp/homelab#43 runs github on the laptop with the user's token, so the gateway can require it now, and this step finishes the feature. --- docs/plan.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/plan.md b/docs/plan.md index 2b83e43..bb6949d 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -181,11 +181,11 @@ The v1.2 design in §4 to §6 stays as reference. A piece of it is built only wh | `fill-day` | The Time page fills a day: a form under the head picks one of the report's 7 days, the earliest missing one first, one of my open Jira tickets and the hours, and its Log asks in a dialog before the worklog goes to Jira through `POST /api/time/worklogs`; the page then shows the report again. The last of `time-tracking`'s twenty steps (decision 194) | Done (#165) | | `nudge-merge` | Nudge, the user's phone-questions tool for agents (`codedogapp/nudge`), moves into Bakhyard: one app, not two (decision 157). The nudge session does the work, one small PR per step, from its plan, [`bakhyard-merge.md`](https://github.com/codedogapp/nudge/blob/main/docs/bakhyard-merge.md); this session doesn't stop its own steps for it | In review | | `parallel-sessions` | Several sessions work at once, each on its own feature in a worktree of its own, with this session as the orchestrator: AGENTS.md says who works where, plan §0 and §9 give each session places of its own, the end-to-end tests take their ports from `BAKHYARD_E2E_PORT`, golangci-lint runs wait for each other, and the tests' time limits allow for the other sessions' load (decision 195) | Done (#166) | -| `github-widgets` | The GitHub widgets (§8.2 Phase 4), by a session of their own in `../bakhyard-github/`, which adds its steps' rows under this one (decision 195) | In progress | +| `github-widgets` | The GitHub widgets (§8.2 Phase 4), by a session of their own in `../bakhyard-github/`, which adds its steps' rows under this one (decision 195) | Done (#179) | | `shared-web-link` | The Bitbucket client's check that a PR's page is an https URL moves to a package of its own, `internal/weblink`, for the GitHub client, its second user; nothing changes in behaviour. The first of `github-widgets`' nine steps (decisions 200 and 201) | Done (#171) | | `github-client` | A client of GitHub's search, in the new `github` service's folder, finds my open PRs, the PRs that wait for my review or one of my teams', and the open issues assigned to me, across every owner, in repositories that aren't archived, the latest updated first, as the user whose classic PAT it holds. It reads up to ten pages of 100, lists each item once, and fails the whole search on a bad answer or one that GitHub marks incomplete. Nothing calls it yet. The second of `github-widgets`' nine steps (decisions 200, 202 and 203) | Done (#173) | | `github-service` | A new `github` service polls the three searches as it starts and every 5 minutes after, and answers them at `GET /github/mine`, `GET /github/reviews` and `GET /github/issues`; without GitHub's setting it answers 404. `mise run up` runs it on port 8089 and the Gitea workflow builds its image. The third of `github-widgets`' nine steps (decisions 200 and 204) | Done (#176) | -| `homelab-github` | The homelab repo runs the `github` service on the laptop, with the token in a Secret, and names it among the gateway's and home's peers, before `github-gateway` makes the gateway require it. The fourth of `github-widgets`' nine steps (decision 200) | Pending | +| `homelab-github` | The homelab repo runs the `github` service on the laptop, with the token in a Secret, and names it among the gateway's and home's peers, before `github-gateway` makes the gateway require it. The fourth of `github-widgets`' nine steps (decision 200) | Done (codedogapp/homelab#43) | | `github-gateway` | The gateway forwards `/api/github` to the `github` service, behind both of its locks, so the app reaches my three lists at `GET /api/github/mine`, `GET /api/github/reviews` and `GET /api/github/issues`. `github` is one of the gateway's peers, each of which `BAKHYARD_PEERS` must name, and `mise run up` starts the gateway once github is ready. The fifth of `github-widgets`' nine steps (decision 200) | Done (#179) | | `shared-polled-section` | The PRs page's lists and the Todos page's Jira tickets show a poller's list through one component in the shell, `PolledSection`, for the GitHub page's lists, its third user; nothing changes on screen. The sixth of `github-widgets`' nine steps (decision 205) | Done (#184) | | `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) | @@ -1291,11 +1291,11 @@ Every item is pending unless it says done. The service an item lands in is in br **Phase 4: GitHub** -- `github-widgets` [github]. Needs `frontend-shell`. Nine steps (decisions 200 to 208): +- `github-widgets` [github]: done (#179). Needs `frontend-shell`. Nine steps (decisions 200 to 208): - `shared-web-link`: the Bitbucket client's check that a PR's page is an https URL moves to the root `internal/weblink`, for the GitHub client; nothing changes in behaviour (decision 201). Done (#171). - `github-client`: a client of GitHub's search finds my open PRs, the PRs that wait for my review or my teams', and the open issues assigned to me, across every owner, through a classic PAT with `repo` and `read:org` (decisions 202 and 203). Done (#173). - `github-service`: the `github` service polls the three searches every 5 minutes, within search's 30 requests a minute, and answers them at `GET /github/mine`, `/github/reviews` and `/github/issues`, or 404 without its setting (decision 204). Done (#176). - - `homelab-github`: the laptop runs the service, with its token in a Secret, before the gateway requires it. + - `homelab-github`: the laptop runs the service, with its token in a Secret, before the gateway requires it. Done (codedogapp/homelab#43). - `github-gateway`: the gateway forwards `/api/github` to the service. Done (#179). - `shared-polled-section`: the PRs page's lists and the Todos page's Jira tickets show a poller's list through one component in the shell, for the GitHub page's lists; nothing changes on screen (decision 205). Done (#184). - `shared-page-status`: the PRs, Time, Nudge and Todos pages draw the line that takes their sections' place from one global class, for the GitHub page's (decision 206). Done (#187).