Skip to content

Commit 6a64dd6

Browse files
committed
feat(web): add a read-only SubmitQueue web UI as a mountable module
## Summary ### Why? SubmitQueue has no browser UI, and deployers need one they can mount in their own web stack without forking it. ### What? - `web/platform`: shared, domain-free contracts and React building blocks (host pipeline, theme, polling, cursor signing). - `web/submitqueue`: the UI as a mountable module (routes, controllers, pages, styles), free of generated protos and transports. - `web/service/submitqueue`: the Next.js reference host and its wiring, with a Bazel-built image in the local stack. - `web/api`: private generated gateway bindings for the host and the e2e test. - `doc/rfc/web-ui.md`: URL, UX, and boundary decisions. ## Test Plan ✅ `make web-check` ✅ `make web-e2e-test` ✅ `make lint` and `make check-gazelle` ### Try it locally Requires Docker; Bazel brings its own Node. ```bash make local-submitqueue-start # builds and starts the full stack, then prints the web URL make demo-requests # submits a few requests to browse make local-stop # tears the stack down ``` Open the printed URL and sign in as `test` / `test`. Start at the queue directory, open a queue, then a request's Summary and History tabs, and follow a change link to its submission history. `make web-e2e-test` runs the same flows headlessly.
1 parent 561518b commit 6a64dd6

188 files changed

Lines changed: 15057 additions & 22 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.dockerignore‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.git
2+
bazel-*
3+
bin
4+
web/**/.next
5+
web/**/coverage
6+
web/**/dist
7+
web/**/node_modules
8+
web/**/playwright-report
9+
web/**/test-results
10+
web/**/*.tsbuildinfo

‎.github/workflows/ci.yml‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,22 @@ jobs:
104104
if: ${{ failure() }}
105105
uses: ./.github/actions/upload-testlogs
106106

107+
web:
108+
name: Web
109+
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
110+
runs-on: ubuntu-latest
111+
steps:
112+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
113+
with:
114+
persist-credentials: false
115+
- uses: ./.github/actions/setup
116+
- name: Run web checks
117+
run: make web-check
118+
119+
- name: Upload Bazel failure logs
120+
if: ${{ failure() }}
121+
uses: ./.github/actions/upload-testlogs
122+
107123
# ---------------------------------------------------------------------------
108124
# INTEGRATION TESTS (e2e, gateway, orchestrator)
109125
# ---------------------------------------------------------------------------
@@ -126,6 +142,22 @@ jobs:
126142
if: ${{ failure() }}
127143
uses: ./.github/actions/upload-testlogs
128144

145+
web-e2e:
146+
name: Web E2E
147+
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
148+
runs-on: ubuntu-latest
149+
steps:
150+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
151+
with:
152+
persist-credentials: false
153+
- uses: ./.github/actions/setup
154+
- name: Run real-stack web E2E
155+
run: make web-e2e-test
156+
157+
- name: Upload Bazel failure logs
158+
if: ${{ failure() }}
159+
uses: ./.github/actions/upload-testlogs
160+
129161
gateway-integration-test:
130162
name: Gateway Integration Test
131163
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
@@ -310,6 +342,8 @@ jobs:
310342
- queue-integration-test
311343
- storage-integration-test
312344
- consumer-integration-test
345+
- web
346+
- web-e2e
313347
- workflow-security
314348
steps:
315349
- name: Fail if any required check did not succeed

‎.gitignore‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,11 @@ bin/
2323

2424
# Make completion cache
2525
.make_targets_cache
26+
/web/node_modules/
27+
/web/**/node_modules/
28+
/web/**/dist/
29+
/web/**/.next/
30+
/web/**/coverage/
31+
/web/**/playwright-report/
32+
/web/**/test-results/
33+
/web/**/*.tsbuildinfo

‎BUILD.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ load("@gazelle//:def.bzl", "gazelle")
1010
# Exclude nested worktrees (created under .claude/worktrees) so gazelle does not
1111
# index them as duplicate rule definitions and corrupt the canonical BUILD files.
1212
# gazelle:exclude .claude
13+
# gazelle:exclude web
1314

1415
# Disable Gazelle proto rule generation globally. We rely on the checked-in
1516
# .pb.go files in protopb/ dirs (plain go_library) rather than Bazel proto

‎MODULE.bazel‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ bazel_dep(name = "git", version = "2.55.0")
55
bazel_dep(name = "rules_go", version = "0.57.0")
66
bazel_dep(name = "gazelle", version = "0.45.0")
77
bazel_dep(name = "rules_proto", version = "7.1.0")
8+
bazel_dep(name = "aspect_rules_js", version = "3.5.0")
9+
bazel_dep(name = "aspect_rules_ts", version = "3.10.1")
10+
bazel_dep(name = "rules_nodejs", version = "6.7.3")
11+
bazel_dep(name = "rules_oci", version = "2.3.0")
12+
bazel_dep(name = "rules_pkg", version = "1.0.1")
13+
bazel_dep(name = "rules_playwright", version = "0.5.3")
814
bazel_dep(name = "rules_python", version = "2.2.0")
15+
bazel_dep(name = "bazel_lib", version = "3.7.2")
16+
bazel_dep(name = "platforms", version = "1.1.0")
917

1018
single_version_override(
1119
module_name = "git",
@@ -29,6 +37,52 @@ use_repo(protoc, "toolchains_protoc_hub")
2937

3038
register_toolchains("@toolchains_protoc_hub//:all")
3139

40+
node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node")
41+
node.toolchain(node_version = "22.20.0")
42+
43+
typescript = use_extension("@aspect_rules_ts//ts:extensions.bzl", "typescript")
44+
typescript.deps(version_from = "//web:package.json")
45+
use_repo(typescript, "npm_typescript")
46+
47+
npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm")
48+
npm.npm_translate_lock(
49+
name = "npm",
50+
pnpm_lock = "//web:pnpm-lock.yaml",
51+
)
52+
use_repo(npm, "npm")
53+
54+
playwright = use_extension("@rules_playwright//playwright:extensions.bzl", "playwright")
55+
playwright.repo(
56+
name = "playwright",
57+
integrity_path_map = {
58+
"builds/chromium/1193/chromium-linux.zip": "sha256-AzVW84unLl+wfBqcmm4P9WtYI8LMQF4DS6V2OmLirRI=",
59+
"builds/chromium/1193/chromium-mac-arm64.zip": "sha256-v7p0571A21zCoO9gPsm6kXNs9ragg2wnpZabV7gES2E=",
60+
},
61+
playwright_version = "1.55.1",
62+
)
63+
use_repo(playwright, "playwright")
64+
65+
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
66+
oci.pull(
67+
name = "node_22_20_0_bookworm_slim",
68+
digest = "sha256:b21fe589dfbe5cc39365d0544b9be3f1f33f55f3c86c87a76ff65a02f8f5848e",
69+
image = "node",
70+
platforms = ["linux/amd64"],
71+
)
72+
oci.pull(
73+
name = "mysql_8_0",
74+
digest = "sha256:7dcddc01f13bab2f15cde676d44d01f61fc9f99fe7785e86196dfc07d358ae2b",
75+
image = "mysql",
76+
platforms = ["linux/amd64"],
77+
)
78+
use_repo(
79+
oci,
80+
"mysql_8_0",
81+
"mysql_8_0_linux_amd64",
82+
"node_22_20_0_bookworm_slim",
83+
"node_22_20_0_bookworm_slim_linux_amd64",
84+
)
85+
3286
GO_VERSION = "1.25.0"
3387

3488
go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")

‎Makefile‎

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ COMPOSE = docker-compose
66

77
# SubmitQueue compose files
88
COMPOSE_FILE = service/submitqueue/docker-compose.yml
9+
WEB_COMPOSE_FILE = service/submitqueue/docker-compose.web.yml
910
GATEWAY_COMPOSE_FILE = service/submitqueue/gateway/server/docker-compose.yml
1011
ORCHESTRATOR_COMPOSE_FILE = service/submitqueue/orchestrator/server/docker-compose.yml
1112

@@ -41,6 +42,11 @@ PROTO_PACKAGES = api/base/change api/base/hook api/base/mergestrategy api/base/m
4142
# Set REPO_ROOT for docker-compose
4243
export REPO_ROOT := $(shell pwd)
4344

45+
# Local-only credential for the reference web host. Override it for manual
46+
# testing; production authentication is deliberately outside this demo.
47+
export SUBMITQUEUE_WEB_TOKEN ?= test
48+
export SUBMITQUEUE_WEB_CURSOR_SECRET ?= local-demo-cursor-secret
49+
4450
# Which provider the demo stack targets, and the only difference between a free
4551
# local run and a live one. Selects a configuration directory rather than a code
4652
# path, so adding a provider is mostly adding a directory — see
@@ -148,7 +154,7 @@ define assert_clean
148154
fi
149155
endef
150156

151-
.PHONY: build build-all-linux build-runway-linux build-submitqueue-gateway-client build-submitqueue-gateway-linux build-submitqueue-gateway-server build-submitqueue-orchestrator-linux build-stovepipe-linux build-stovepipe-linux-debug check-gazelle check-mocks check-tidy clean clean-proto demo-requests deps e2e-test fmt gazelle integration-test integration-test-submitqueue-consumer integration-test-extensions integration-test-submitqueue-gateway integration-test-submitqueue-orchestrator license-fix lint lint-binary lint-fmt lint-license local-init-runway-queue-schema local-init-stovepipe-schemas local-runway-start local-runway-stop local-submitqueue-stop local-submitqueue-clean local-submitqueue-gateway-start local-submitqueue-gateway-stop local-init-submitqueue-schemas local-submitqueue-logs local-submitqueue-orchestrator-start local-submitqueue-orchestrator-stop local-submitqueue-ps local-submitqueue-restart local-submitqueue-start local-stop local-stovepipe-debug-start local-stovepipe-logs local-stovepipe-start local-stovepipe-stop mocks proto query-deps query-targets run-client-runway run-client-submitqueue-gateway run-client-submitqueue-orchestrator run-client-stovepipe run-queue-admin test test-no-cache test-race tidy tidy-bazel tidy-go help
157+
.PHONY: build build-all-linux build-runway-linux build-submitqueue-gateway-client build-submitqueue-gateway-linux build-submitqueue-gateway-server build-submitqueue-orchestrator-linux build-stovepipe-linux build-stovepipe-linux-debug check-gazelle check-mocks check-tidy clean clean-proto demo-requests deps e2e-test fmt gazelle integration-test integration-test-submitqueue-consumer integration-test-extensions integration-test-submitqueue-gateway integration-test-submitqueue-orchestrator license-fix lint lint-binary lint-fmt lint-license local-init-runway-queue-schema local-init-stovepipe-schemas local-runway-start local-runway-stop local-submitqueue-stop local-submitqueue-clean local-submitqueue-gateway-start local-submitqueue-gateway-stop local-init-submitqueue-schemas local-submitqueue-logs local-submitqueue-orchestrator-start local-submitqueue-orchestrator-stop local-submitqueue-ps local-submitqueue-restart local-submitqueue-start local-stop local-stovepipe-debug-start local-stovepipe-logs local-stovepipe-start local-stovepipe-stop mocks proto query-deps query-targets run-client-runway run-client-submitqueue-gateway run-client-submitqueue-orchestrator run-client-stovepipe run-queue-admin test test-no-cache test-race tidy tidy-bazel tidy-go web-build web-check web-e2e-test web-image-load web-install web-proto help
152158

153159

154160
build: ## Build all services and examples
@@ -361,7 +367,7 @@ local-submitqueue-clean: ## Stop the stack and remove its volumes, images, and P
361367
@echo "Cleaning all services and data..."
362368
@# The overlay is named so that volumes it declares — Runway's checkouts —
363369
@# are removed too, rather than surviving as an orphan.
364-
@$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down -v --rmi local
370+
@$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down -v --rmi local
365371
@rm -rf "$(SQ_GIT_SANDBOX_DIR)"
366372
@echo "All services, volumes, images, and $(SQ_GIT_SANDBOX_DIR) removed."
367373

@@ -450,7 +456,7 @@ local-runway-stop: ## Stop Runway service
450456
@echo "Runway services stopped."
451457

452458
local-submitqueue-logs: ## View logs from all running services
453-
@$(COMPOSE) -f $(COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) logs -f
459+
@$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) logs -f
454460

455461
local-submitqueue-orchestrator-start: build-submitqueue-orchestrator-linux ## Start Orchestrator service locally (Orchestrator + 2 MySQL databases)
456462
@echo "Starting Orchestrator with docker-compose..."
@@ -474,11 +480,12 @@ local-submitqueue-orchestrator-stop: ## Stop Orchestrator service
474480
local-submitqueue-ps: ## Show running containers and their ports
475481
@echo "Running containers and ports:"
476482
@echo ""
477-
@$(COMPOSE) -f $(COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) ps
483+
@$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) ps
478484
@echo ""
479485
@echo "📡 Service Endpoints:"
480486
@echo " Gateway gRPC: localhost:$$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-gateway-service-1 8080 2>/dev/null | cut -d: -f2 || echo 'not running')"
481487
@echo " Orchestrator gRPC: localhost:$$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-orchestrator-service-1 8080 2>/dev/null | cut -d: -f2 || echo 'not running')"
488+
@echo " Web UX: http://localhost:$$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-web-service-1 3000 2>/dev/null | cut -d: -f2 || echo 'not running')"
482489
@echo ""
483490
@echo "🗄️ Database Endpoints:"
484491
@echo " MySQL App: localhost:$$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-mysql-app-1 3306 2>/dev/null | cut -d: -f2 || echo 'not running')"
@@ -496,11 +503,11 @@ local-submitqueue-ps: ## Show running containers and their ports
496503

497504
local-submitqueue-restart: build-all-linux ## Restart all services (rebuild and restart)
498505
@echo "Restarting all services..."
499-
@$(COMPOSE) -f $(COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) restart
506+
@$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) restart
500507
@echo "Services restarted!"
501508
@make local-submitqueue-ps
502509

503-
local-submitqueue-start: build-all-linux ## Start full stack (PROVIDER=fake|git|github; github needs GITHUB_TOKEN)
510+
local-submitqueue-start: build-all-linux web-image-load ## Start full stack (PROVIDER=fake|git|github; github needs GITHUB_TOKEN)
504511
@echo "Starting full stack against provider '$(PROVIDER)' ($(SQ_PROVIDER_CONFIG_DIR))..."
505512
@test -f "$(SQ_PROVIDER_CONFIG_DIR)/merge.yaml" \
506513
|| { echo "No such provider '$(PROVIDER)': $(SQ_PROVIDER_CONFIG_DIR)/merge.yaml not found"; exit 2; }
@@ -519,13 +526,16 @@ local-submitqueue-start: build-all-linux ## Start full stack (PROVIDER=fake|git|
519526
else \
520527
export SQ_CONTAINER_USER=$$(id -u):$$(id -g); \
521528
fi; \
522-
$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) up -d --build --wait
529+
$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) up -d --build --wait
523530
@echo "Applying database schemas..."
524531
@$(MAKE) -s local-init-submitqueue-schemas
525532
@echo ""
526533
@echo "✅ Stack is running against provider '$(PROVIDER)'."
527534
@echo ""
528535
@echo "Gateway gRPC port: $$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-gateway-service-1 8080 2>/dev/null | cut -d: -f2 || echo 'unknown')"
536+
@echo "Web UX: http://localhost:$$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-web-service-1 3000 2>/dev/null | cut -d: -f2 || echo 'unknown')"
537+
@echo "Web username: test"
538+
@echo "Web token: $(SUBMITQUEUE_WEB_TOKEN)"
529539
@if [ "$(PROVIDER)" = "git" ]; then \
530540
echo "Land target: $(SQ_GIT_SANDBOX_DIR)/sandbox.git"; \
531541
fi
@@ -535,7 +545,7 @@ local-submitqueue-start: build-all-linux ## Start full stack (PROVIDER=fake|git|
535545

536546
local-submitqueue-stop: ## Stop the SubmitQueue stack (keeps PROVIDER=git's sandbox; the databases do not survive)
537547
@echo "Stopping SubmitQueue services..."
538-
@$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down
548+
@$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down
539549
@# Both MySQL services mount anonymous volumes, so `down` detaches them and
540550
@# the next `up` creates fresh ones. Saying "data preserved" here would be
541551
@# read as "your requests are still there", which they are not.
@@ -547,7 +557,7 @@ local-submitqueue-stop: ## Stop the SubmitQueue stack (keeps PROVIDER=git's sand
547557

548558
local-stop: ## Stop every local stack — SubmitQueue, Stovepipe, and Runway
549559
@echo "Stopping all services..."
550-
@$(COMPOSE) -f $(COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down
560+
@$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down
551561
@$(COMPOSE) -f $(STOVEPIPE_COMPOSE_FILE) -p $(STOVEPIPE_LOCAL_PROJECT) down
552562
@$(COMPOSE) -f $(RUNWAY_COMPOSE_FILE) -p $(RUNWAY_LOCAL_PROJECT) down
553563
@echo "Services stopped. Anonymous database volumes are not reused on the next start."
@@ -677,6 +687,25 @@ tidy-go: ## Run go mod tidy
677687
@echo "Running go mod tidy..."
678688
@$(BAZEL) run @rules_go//go -- mod tidy -e
679689

690+
web-build: ## Build the web packages and deployable Next.js image with Bazel
691+
@$(BAZEL) build //web/platform:pkg //web/submitqueue:pkg //web/service/submitqueue:image
692+
693+
web-check: ## Run web generation, packaging, lint, type, unit, and image checks with Bazel
694+
@$(BAZEL) test //web:check --test_output=errors
695+
@$(BAZEL) build //web/service/submitqueue:image
696+
697+
web-e2e-test: ## Run the Bazel-managed real-stack Playwright and axe web test
698+
@$(BAZEL) test //web/test/e2e:web_test --test_output=errors
699+
700+
web-image-load: ## Build and load the deployable web image into Docker with Bazel
701+
@$(BAZEL) run //web/service/submitqueue:image_load
702+
703+
web-install: ## Install the pinned web workspace dependencies
704+
@corepack pnpm@10.17.1 --dir web install --frozen-lockfile
705+
706+
web-proto: ## Regenerate the committed TypeScript protobuf API with Bazel
707+
@$(BAZEL) run //web/api:generate
708+
680709
help: ## Show this help message
681710
@echo "Available targets:"
682711
@echo ""

‎REPO.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ignore_directories(["**/node_modules"])

‎api/base/change/proto/BUILD.bazel‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
exports_files(
22
["change.proto"],
3-
visibility = ["//tool/proto:__pkg__"],
3+
visibility = [
4+
"//tool/proto:__pkg__",
5+
"//web/api:__pkg__",
6+
],
47
)
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
exports_files(
22
["mergestrategy.proto"],
3-
visibility = ["//tool/proto:__pkg__"],
3+
visibility = [
4+
"//tool/proto:__pkg__",
5+
"//web/api:__pkg__",
6+
],
47
)

‎doc/howto/DEVELOPMENT.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ CI runs `make lint`, `make check-tidy`, and `make check-gazelle`. `make lint` in
108108
| `make gazelle` | Update `BUILD.bazel` files |
109109
| `make mocks` | Regenerate mockgen files |
110110
| `make check-mocks` | Fail if generated mocks are stale |
111+
| `make web-build` | Build the web packages and deployable OCI image with Bazel |
112+
| `make web-check` | Run web generation drift, package, lint, type, unit, and production image checks with Bazel |
113+
| `make web-e2e-test` | Run the Bazel-managed real-stack Playwright and axe test |
114+
| `make web-proto` | Regenerate the committed TypeScript protobuf API with Bazel |
111115
| `make proto` | Regenerate protobuf files |
112116
| `make clean` | Remove the Bazel cache and `bin/` |
113117
| `make clean-proto` | Remove generated protobuf Go files |
@@ -118,6 +122,8 @@ CI runs `make lint`, `make check-tidy`, and `make check-gazelle`. `make lint` in
118122
| `make local-stop` | Stop SubmitQueue, Stovepipe, and Runway |
119123
| `make help` | List every target |
120124

125+
The web build does not require a host Node.js or pnpm installation: Bazel supplies Node, npm packages, Buf, TypeScript, Next.js, Playwright, Chromium, and the container image toolchain. `make web-install` is an optional convenience for editors and direct pnpm development only.
126+
121127
## Running Specific Tests
122128

123129
```bash

0 commit comments

Comments
 (0)