docs: close out Plan 3 — React SPA live

This commit is contained in:
Claude
2026-08-28 16:38:55 +00:00
parent 294bd57317
commit 2a1b90c731

View File

@@ -7,36 +7,33 @@ VolcanicCore is a self-hosted N8N-style workflow engine (C# backend + React fron
- **Design spec** — `docs/superpowers/specs/2026-08-28-volcaniccore-workflow-engine-design.md`. The whole architecture (execution model, extension contracts, webhook mechanics, persistence, deploy shape). Read this before writing plans or code.
- **Plan 1** (backend foundation, executed) — `docs/superpowers/plans/2026-08-28-plan-1-backend-foundation.md`. Shows the working style and the file structure conventions.
- **Plan 2** (workflow engine, executed) — `docs/superpowers/plans/2026-08-29-plan-2-workflow-engine.md`. Executor, WebhookRouter, TriggerHost, five nodes, NATS provider, retention.
- **Plan 3** (React SPA, executed) — `docs/superpowers/plans/2026-08-30-plan-3-react-spa.md`. Vite/React/TS/Tailwind/React Flow SPA under `frontend/`, bundled into `wwwroot`.
- **Follow-ups** — `docs/superpowers/followups.md`. Cross-plan items surfaced during review that later plans must handle. Read before starting a plan; extend as new items surface.
## Current status
**Plan 2 (workflow engine) shipped.** Head commit at plan close: `6065f0c`. `dotnet build` clean, `dotnet test` 70/70.
**Plan 3 (React SPA) shipped.** Head commit at plan close: `294bd57`. `dotnet build` clean, `dotnet test` 74/74, `pnpm --dir frontend test` 20/20, `pnpm --dir frontend test:e2e` 1/1 (Playwright smoke).
What's done:
- ASP.NET Core 9 monolith. Four projects: `VolcanicCore.Core` (contracts + `WebhookRegistry`/`WebhookRouter`), `.Persistence` (SQLite, migrator, encryption, repos), `.Providers` (`NatsConnectionProvider`), `.Nodes` (five node implementations), `.Api` (endpoints, auth, registries, engine host), `VolcanicCore.Tests` (xUnit + WebApplicationFactory).
- Admin cookie auth (BCrypt, `ADMIN_PASSWORD` env), sessions endpoints.
- SQLite with hand-rolled versioned migrator (5 migrations: `admin_config`, `connections`, `workflows`, `executions`, `node_executions`).
- AES-GCM encryption for connection secrets and for the `admin_config` password-source row.
- Extension contracts: `INodeType` / `IActionNode` / `ITriggerNode` / `IConnectionProvider` in `.Core`.
- DI-collected `NodeRegistry` and `ConnectionProviderRegistry`; 5 nodes + NATS provider registered.
- `Executor` — in-degree scheduler, fan-out, failure propagation, per-node persistence per §4 of the design spec.
- `WebhookRouter` + `WebhookRegistry` in `VolcanicCore.Core`; `POST /wh/{workflow-id}/{*path}` endpoint bridging HTTP to the executor via `answerChannelId` TCS.
- `TriggerHost` `IHostedService` — activates triggers on workflow.active flips; re-hydrates from DB on boot; disposes on deactivate.
- `TriggerActivationSink.EmitAsync` — fire-and-forget `Task.Run` per event; does not block the trigger pump.
- Five nodes: NATS Hook (trigger), NATS Publish (terminal), Webhook (trigger), WebResponse (terminal), PythonTransform (IronPython 3).
- NATS `IConnectionProvider` (userpass + TLS URL).
- `SchemaSecretPolicy` — reads `secretFields[]` off each provider's `ConfigSchema`.
- REST endpoints: session, discovery (returns 5 nodes + `nats` provider), connections CRUD, workflows CRUD + activate/deactivate, executions list + detail, manual test-run.
- `RetentionService` `BackgroundService` — daily prune of executions older than `EXECUTION_RETENTION_DAYS` (default 30).
What's done (in addition to Plans 1 + 2):
- Vite + React 18 + TypeScript + Tailwind SPA under `frontend/` (pnpm).
- Same-origin cookie auth via `apiFetch` (`credentials: "include"`, typed `ApiError` / `UnauthorizedError`).
- TanStack Query for server state; `useSession` / `useLogin` / `useLogout` + query hooks for every endpoint.
- Router w/ `PrivateRoute` gate. Pages: Login / Workflows / Editor / Connections / Executions.
- React Flow (`@xyflow/react`) editor w/ draggable palette from `/api/node-types`, right-panel schema-driven config form (auto-generated from each `ConfigSchema`), connection picker, per-node position + connection_id persisted.
- Editor toolbar: Save (`PUT /api/workflows/{id}`), Test run (`POST /api/workflows/{id}/executions`), Activate/Deactivate, Executions link.
- Executions viewer: per-workflow run list, click → per-node status/timing/expandable IO JSON.
- MSW-driven Vitest suite (20 tests across apiFetch, useSession, SchemaForm, LoginPage, WorkflowsPage, ConnectionsPage, graphAdapter).
- `pnpm build` emits into `src/VolcanicCore.Api/wwwroot/`; `.Api` serves w/ `UseStaticFiles` + `MapFallbackToFile("index.html")` (API + `/wh` routes still win — 4 new xUnit tests cover the ordering).
- Vite dev proxy `/api` + `/wh` → `http://127.0.0.1:5099` so `pnpm dev` co-exists with `dotnet run`.
- Playwright happy-path smoke in `frontend/e2e/smoke.spec.ts` — login → create workflow → drop node → save → reload.
What's next: **Plan 3** — React SPA. Not yet written. When authored, it will cover:
- React 18 + Vite + TypeScript + Tailwind + React Flow + TanStack Query SPA under `frontend/`.
- Login screen; workflows list + editor; connections list + form; executions viewer.
- SPA build lands in `wwwroot/` and is served by the `.Api` project via `UseStaticFiles` + fallback-to-index.
- No auth-token work — cookie session already in place; the SPA runs same-origin.
Plan 4 (Coolify Dockerfile + deploy) follows.
What's next: **Plan 4** — Dockerfile + Coolify deploy. Not yet written. When authored, it will cover:
- Multi-stage Dockerfile: pnpm build the SPA into `wwwroot`, `dotnet publish` the backend, distroless runtime image.
- Coolify docker-compose using `preparing-dotnet-react-app-for-coolify` skill (named volume for `/data`, `expose: [8080]`, `SERVICE_FQDN_APP`).
- Wire `UseForwardedHeaders` + `KnownProxies` so Secure cookies work behind Traefik (Plan-2 follow-up).
- Boot-time re-seed of legacy plaintext `admin.password_hash_source` (Plan-2 follow-up).
- CI install of Playwright's Chromium (Plan-3 follow-up if needed).
- v4 API deploy trigger — fire-and-forget per global CLAUDE.md rule.
## Invariants (do NOT relearn from scratch)
@@ -52,6 +49,10 @@ Plan 4 (Coolify Dockerfile + deploy) follows.
- **`AdminBootstrap`** stores `admin.password_hash_source` in the `admin_config` table as the change-detector for the env-provided `ADMIN_PASSWORD` (see the comment in `AdminBootstrap.cs`). Do not "fix" this by hashing the source — it must be reversible so we can compare against the current env var on boot. The row is AES-GCM encrypted at rest as of Plan 2.
- **Fire-and-forget trigger sinks.** `TriggerActivationSink.EmitAsync` returns `Task.CompletedTask` after spinning the execution into a background `Task.Run`. This prevents NATS pumps from blocking per-event on execution completion. `WebhookRouter.Register` (not the sink) is what the `/wh` endpoint awaits for the reply.
- **Cross-project cycle-avoidance for `WebhookRegistry` and `WebhookRouter`.** Both types live in `VolcanicCore.Core`, NOT `VolcanicCore.Api.Engine`, because `WebhookNode` and `WebResponseNode` in `.Nodes` need them and `.Api` already references `.Nodes` (adding a back-reference would cycle). Do not move them back.
- **SPA bundles into `src/VolcanicCore.Api/wwwroot/`.** Vite's `build.outDir` is repo-relative to `frontend/` and points at the .Api project's wwwroot. The folder itself is `.gitignore`'d except for `.gitkeep`; the compiled JS/CSS is produced by `pnpm build` and consumed by `dotnet publish` (csproj has `<Content Update="wwwroot\**\*" CopyToPublishDirectory="PreserveNewest" />`). Do NOT commit `wwwroot/*.js`/`*.css` — CI produces them.
- **SPA fallback goes AFTER `MapXxxEndpoints`.** `MapFallbackToFile("index.html")` in `Program.cs` must remain the last `Map*` call so `/api/*` and `/wh/*` still win. Static-file middleware (`UseStaticFiles`) goes right after `UseAuthorization` and is intentionally unauthenticated — the SPA itself sends users to `/login` when it sees a 401.
- **Frontend tsconfig has both `references` and `paths`.** `tsconfig.node.json` is a composite project referenced from `tsconfig.json`, and needs `noEmit: false` + its own `outDir` (`dist-tsconfig-node/`, gitignored) to satisfy the composite constraint. `tsconfig.json` sets `"paths": { "@/*": ["src/*"] }` so `tsc -b` matches Vite's runtime `@` alias — do NOT remove either.
- **`vite.config.ts` needs `/// <reference types="vitest" />` on line 1.** The `test:` block on `defineConfig` from `"vite"` won't typecheck without Vitest's config augmentation.
## Working style
@@ -65,7 +66,7 @@ Plan 4 (Coolify Dockerfile + deploy) follows.
## How to run
```bash
# Build + test
# Build + test (backend)
dotnet build
dotnet test
@@ -79,6 +80,29 @@ DATA_DIRECTORY=$(pwd)/_smoke ADMIN_PASSWORD=hunter2 \
`dotnet run --project X` sets the content root to X's directory, so `DATA_DIRECTORY=./relative` lands under `src/VolcanicCore.Api/` — always pass an absolute path.
### SPA dev loop
```bash
# Terminal A — backend on :5099
DATA_DIRECTORY=$(pwd)/_smoke ADMIN_PASSWORD=hunter2 \
dotnet run --project src/VolcanicCore.Api --urls http://127.0.0.1:5099
# Terminal B — Vite dev server on :5173 (proxies /api and /wh to :5099)
pnpm --dir frontend dev
# Production-ish (SPA bundled + served by the API)
pnpm --dir frontend build
DATA_DIRECTORY=$(pwd)/_smoke ADMIN_PASSWORD=hunter2 \
dotnet run --project src/VolcanicCore.Api --urls http://127.0.0.1:5099
# → open http://127.0.0.1:5099
```
Frontend tests:
```bash
pnpm --dir frontend test # Vitest unit + integration (20 tests)
pnpm --dir frontend test:e2e # Playwright smoke; needs `pnpm exec playwright install chromium` once
```
## Deploy target
Coolify — env vars `COOLIFY_URL`, `COOLIFY_KEY`, `GIT_URL`, `GIT_USER`, `GIT_KEY` are available in the container. Actual container packaging + Coolify wiring is Plan 4. Global CLAUDE.md rule applies: fire-and-forget deploys via the v4 API, never block on build.