Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default defineConfig({
env: {
BAKHYARD_LISTEN: `tcp://${gateway}`,
BAKHYARD_PEERS: [
...['games', 'home', 'notes', 'notify', 'nudge', 'pulls', 'time', 'todos'].map(
...['games', 'github', 'home', 'notes', 'notify', 'nudge', 'pulls', 'time', 'todos'].map(
(name) => `${name}=${nowhere}`
),
`web=tcp://${web}`,
Expand Down
24 changes: 12 additions & 12 deletions docs/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,13 @@ 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 |
| `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 |
| `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 |
| `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) |
| `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) | 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) |
| `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) |
Expand Down Expand Up @@ -439,7 +439,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, games, 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, 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 |
Expand Down Expand Up @@ -615,7 +615,7 @@ Streaming handlers watch an app-lifetime context, so open streams end promptly.

- Each service has a `description`, a `command` (`bin/<name>`, 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` games, 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, 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. 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).
Expand Down Expand Up @@ -1291,12 +1291,12 @@ 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.
- `github-gateway`: the gateway forwards `/api/github` to the service.
- `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).
- `github-page`: the lists in the app, on a page of their own at `/github`, as the user chose (decision 207). Done (#191).
Expand Down
4 changes: 2 additions & 2 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.
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, 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, 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.

Expand Down Expand Up @@ -181,7 +181,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.

## Exophase

Expand Down
4 changes: 3 additions & 1 deletion process-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,13 @@ processes:
command: bin/gateway
environment:
- BAKHYARD_LISTEN=tcp://127.0.0.1:8080
- 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_PEERS=games=tcp://127.0.0.1:8090,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:
games:
condition: process_healthy
github:
condition: process_healthy
home:
condition: process_healthy
notes:
Expand Down
3 changes: 2 additions & 1 deletion services/gateway/headers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ func TestEveryResponseCarriesTheSecurityHeaders(t *testing.T) {
t.Cleanup(web.Close)

vars, items := gatewayEnv(t, web.URL)
vars["BAKHYARD_PEERS"] = "games=tcp://127.0.0.1:1,home=tcp://127.0.0.1:1" +
vars["BAKHYARD_PEERS"] = "games=tcp://127.0.0.1:1,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" +
Expand Down
5 changes: 4 additions & 1 deletion services/gateway/lock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func TestTheGatewayNeedsASessionForTheAPIOnly(t *testing.T) {
{http.MethodHead, "/notes", http.StatusOK},
{http.MethodGet, "/auth/other", http.StatusOK},
{http.MethodGet, "/api/games/recent", http.StatusUnauthorized},
{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},
Expand Down Expand Up @@ -71,10 +72,11 @@ func TestTheGatewayKeepsTheSessionFromTheServices(t *testing.T) {
return srv
}
home, pulls, todos, web := service("home"), service("pulls"), service("todos"), service("web")
games, timeService := service("games"), service("time")
games, github, timeService := service("games"), service("github"), service("time")

vars, items := gatewayEnv(t, web.URL)
vars["BAKHYARD_PEERS"] = "games=tcp://" + strings.TrimPrefix(games.URL, "http://") +
",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://") +
Expand All @@ -90,6 +92,7 @@ func TestTheGatewayKeepsTheSessionFromTheServices(t *testing.T) {
renewed bool
}{
{"/api/games/recent", "games", true},
{"/api/github/mine", "github", true},
{"/api/home", "home", true},
{"/api/pulls/mine", "pulls", true},
{"/api/time/report", "time", true},
Expand Down
Loading
Loading