From cc5b108b2f75bde09971e0410594c2367f128bb0 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 27 Feb 2026 01:48:38 +0000 Subject: [PATCH 01/23] fix(security): bump minimatch override to >=10.2.3 (#528) Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- .woodpecker/api.yml | 30 +++++++++++------------ .woodpecker/orchestrator.yml | 17 ++++++++++--- .woodpecker/web.yml | 34 +++++++++++++------------- package.json | 2 +- pnpm-lock.yaml | 47 ++++++++++++++---------------------- turbo.json | 1 + 6 files changed, 65 insertions(+), 66 deletions(-) diff --git a/.woodpecker/api.yml b/.woodpecker/api.yml index a2ab0f3..c283861 100644 --- a/.woodpecker/api.yml +++ b/.woodpecker/api.yml @@ -24,6 +24,13 @@ variables: pnpm install --frozen-lockfile - &use_deps | corepack enable + - &turbo_env + TURBO_API: + from_secret: turbo_api + TURBO_TOKEN: + from_secret: turbo_token + TURBO_TEAM: + from_secret: turbo_team - &kaniko_setup | mkdir -p /kaniko/.docker echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$GITEA_USER\",\"password\":\"$GITEA_TOKEN\"}}}" > /kaniko/.docker/config.json @@ -52,17 +59,6 @@ steps: depends_on: - install - lint: - image: *node_image - environment: - SKIP_ENV_VALIDATION: "true" - commands: - - *use_deps - - pnpm --filter "@mosaic/api" lint - depends_on: - - prisma-generate - - build-shared - prisma-generate: image: *node_image environment: @@ -73,26 +69,27 @@ steps: depends_on: - install - build-shared: + lint: image: *node_image environment: SKIP_ENV_VALIDATION: "true" + <<: *turbo_env commands: - *use_deps - - pnpm --filter "@mosaic/shared" build + - pnpm turbo lint --filter=@mosaic/api depends_on: - - install + - prisma-generate typecheck: image: *node_image environment: SKIP_ENV_VALIDATION: "true" + <<: *turbo_env commands: - *use_deps - - pnpm --filter "@mosaic/api" typecheck + - pnpm turbo typecheck --filter=@mosaic/api depends_on: - prisma-generate - - build-shared prisma-migrate: image: *node_image @@ -124,6 +121,7 @@ steps: environment: SKIP_ENV_VALIDATION: "true" NODE_ENV: "production" + <<: *turbo_env commands: - *use_deps - pnpm turbo build --filter=@mosaic/api diff --git a/.woodpecker/orchestrator.yml b/.woodpecker/orchestrator.yml index aca8b66..903f9c8 100644 --- a/.woodpecker/orchestrator.yml +++ b/.woodpecker/orchestrator.yml @@ -24,6 +24,13 @@ variables: pnpm install --frozen-lockfile - &use_deps | corepack enable + - &turbo_env + TURBO_API: + from_secret: turbo_api + TURBO_TOKEN: + from_secret: turbo_token + TURBO_TEAM: + from_secret: turbo_team - &kaniko_setup | mkdir -p /kaniko/.docker echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$GITEA_USER\",\"password\":\"$GITEA_TOKEN\"}}}" > /kaniko/.docker/config.json @@ -48,9 +55,10 @@ steps: image: *node_image environment: SKIP_ENV_VALIDATION: "true" + <<: *turbo_env commands: - *use_deps - - pnpm --filter "@mosaic/orchestrator" lint + - pnpm turbo lint --filter=@mosaic/orchestrator depends_on: - install @@ -58,9 +66,10 @@ steps: image: *node_image environment: SKIP_ENV_VALIDATION: "true" + <<: *turbo_env commands: - *use_deps - - pnpm --filter "@mosaic/orchestrator" typecheck + - pnpm turbo typecheck --filter=@mosaic/orchestrator depends_on: - install @@ -68,9 +77,10 @@ steps: image: *node_image environment: SKIP_ENV_VALIDATION: "true" + <<: *turbo_env commands: - *use_deps - - pnpm --filter "@mosaic/orchestrator" test + - pnpm turbo test --filter=@mosaic/orchestrator depends_on: - install @@ -81,6 +91,7 @@ steps: environment: SKIP_ENV_VALIDATION: "true" NODE_ENV: "production" + <<: *turbo_env commands: - *use_deps - pnpm turbo build --filter=@mosaic/orchestrator diff --git a/.woodpecker/web.yml b/.woodpecker/web.yml index c97c8ad..c43a22a 100644 --- a/.woodpecker/web.yml +++ b/.woodpecker/web.yml @@ -24,6 +24,13 @@ variables: pnpm install --frozen-lockfile - &use_deps | corepack enable + - &turbo_env + TURBO_API: + from_secret: turbo_api + TURBO_TOKEN: + from_secret: turbo_token + TURBO_TEAM: + from_secret: turbo_team - &kaniko_setup | mkdir -p /kaniko/.docker echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$GITEA_USER\",\"password\":\"$GITEA_TOKEN\"}}}" > /kaniko/.docker/config.json @@ -44,46 +51,38 @@ steps: depends_on: - install - build-shared: - image: *node_image - environment: - SKIP_ENV_VALIDATION: "true" - commands: - - *use_deps - - pnpm --filter "@mosaic/shared" build - - pnpm --filter "@mosaic/ui" build - depends_on: - - install - lint: image: *node_image environment: SKIP_ENV_VALIDATION: "true" + <<: *turbo_env commands: - *use_deps - - pnpm --filter "@mosaic/web" lint + - pnpm turbo lint --filter=@mosaic/web depends_on: - - build-shared + - install typecheck: image: *node_image environment: SKIP_ENV_VALIDATION: "true" + <<: *turbo_env commands: - *use_deps - - pnpm --filter "@mosaic/web" typecheck + - pnpm turbo typecheck --filter=@mosaic/web depends_on: - - build-shared + - install test: image: *node_image environment: SKIP_ENV_VALIDATION: "true" + <<: *turbo_env commands: - *use_deps - - pnpm --filter "@mosaic/web" test + - pnpm turbo test --filter=@mosaic/web depends_on: - - build-shared + - install # === Build === @@ -92,6 +91,7 @@ steps: environment: SKIP_ENV_VALIDATION: "true" NODE_ENV: "production" + <<: *turbo_env commands: - *use_deps - pnpm turbo build --filter=@mosaic/web diff --git a/package.json b/package.json index c33fffe..24e8900 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ ], "overrides": { "@isaacs/brace-expansion": ">=5.0.1", - "minimatch": ">=10.2.1", + "minimatch": ">=10.2.3", "tar": ">=7.5.8", "form-data": ">=2.5.4", "lodash": ">=4.17.23", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 177b0a4..9a47622 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: overrides: '@isaacs/brace-expansion': '>=5.0.1' - minimatch: '>=10.2.1' + minimatch: '>=10.2.3' tar: '>=7.5.8' form-data: '>=2.5.4' lodash: '>=4.17.23' @@ -1596,6 +1596,7 @@ packages: '@mosaicstack/telemetry-client@0.1.1': resolution: {integrity: sha512-1udg6p4cs8rhQgQ2pKCfi7EpRlJieRRhA5CIqthRQ6HQZLgQ0wH+632jEulov3rlHSM1iplIQ+AAe5DWrvSkEA==, tarball: https://git.mosaicstack.dev/api/packages/mosaic/npm/%40mosaicstack%2Ftelemetry-client/-/0.1.1/telemetry-client-0.1.1.tgz} + engines: {node: '>=18'} '@mrleebo/prisma-ast@0.13.1': resolution: {integrity: sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==} @@ -5776,9 +5777,9 @@ packages: minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - minimatch@10.2.1: - resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==} - engines: {node: 20 || >=22} + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -7965,7 +7966,7 @@ snapshots: chalk: 5.6.2 commander: 12.1.0 dotenv: 17.2.4 - drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)) + drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)) open: 10.2.0 pg: 8.17.2 prettier: 3.8.1 @@ -8303,7 +8304,7 @@ snapshots: dependencies: '@eslint/object-schema': 2.1.7 debug: 4.4.3 - minimatch: 10.2.1 + minimatch: 10.2.4 transitivePeerDependencies: - supports-color @@ -8324,7 +8325,7 @@ snapshots: ignore: 5.3.2 import-fresh: 3.3.1 js-yaml: 4.1.1 - minimatch: 10.2.1 + minimatch: 10.2.4 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color @@ -10780,7 +10781,7 @@ snapshots: '@typescript-eslint/types': 8.54.0 '@typescript-eslint/visitor-keys': 8.54.0 debug: 4.4.3 - minimatch: 10.2.1 + minimatch: 10.2.4 semver: 7.7.3 tinyglobby: 0.2.15 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -11291,7 +11292,7 @@ snapshots: optionalDependencies: '@prisma/client': 5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)) better-sqlite3: 12.6.2 - drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)) + drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)) next: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) pg: 8.17.2 prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3) @@ -11316,7 +11317,7 @@ snapshots: optionalDependencies: '@prisma/client': 6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3) better-sqlite3: 12.6.2 - drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)) + drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)) next: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) pg: 8.17.2 prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3) @@ -12135,17 +12136,6 @@ snapshots: dotenv@17.2.4: {} - drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)): - optionalDependencies: - '@opentelemetry/api': 1.9.0 - '@prisma/client': 5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)) - '@types/pg': 8.16.0 - better-sqlite3: 12.6.2 - kysely: 0.28.10 - pg: 8.17.2 - postgres: 3.4.8 - prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3) - drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)): optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -12156,7 +12146,6 @@ snapshots: pg: 8.17.2 postgres: 3.4.8 prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3) - optional: true dunder-proto@1.0.1: dependencies: @@ -12362,7 +12351,7 @@ snapshots: is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 - minimatch: 10.2.1 + minimatch: 10.2.4 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -12605,7 +12594,7 @@ snapshots: deepmerge: 4.3.1 fs-extra: 10.1.0 memfs: 3.5.3 - minimatch: 10.2.1 + minimatch: 10.2.4 node-abort-controller: 3.1.1 schema-utils: 3.3.0 semver: 7.7.3 @@ -12731,14 +12720,14 @@ snapshots: dependencies: foreground-child: 3.3.1 jackspeak: 3.4.3 - minimatch: 10.2.1 + minimatch: 10.2.4 minipass: 7.1.2 package-json-from-dist: 1.0.1 path-scurry: 1.11.1 glob@13.0.0: dependencies: - minimatch: 10.2.1 + minimatch: 10.2.4 minipass: 7.1.2 path-scurry: 2.0.1 @@ -13374,7 +13363,7 @@ snapshots: minimalistic-assert@1.0.1: {} - minimatch@10.2.1: + minimatch@10.2.4: dependencies: brace-expansion: 5.0.2 @@ -14110,7 +14099,7 @@ snapshots: readdir-glob@1.1.3: dependencies: - minimatch: 10.2.1 + minimatch: 10.2.4 readdirp@3.6.0: dependencies: @@ -14797,7 +14786,7 @@ snapshots: dependencies: '@istanbuljs/schema': 0.1.3 glob: 10.5.0 - minimatch: 10.2.1 + minimatch: 10.2.4 text-decoder@1.2.3: dependencies: diff --git a/turbo.json b/turbo.json index d70d42b..495574b 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,6 @@ { "$schema": "https://turbo.build/schema.json", + "remoteCache": {}, "tasks": { "prisma:generate": { "cache": false From 83d5aee53acd4aaad89846b1b4ec17649878852c Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 27 Feb 2026 02:44:11 +0000 Subject: [PATCH 02/23] fix(api): add debian-openssl-3.0.x to Prisma binaryTargets (#529) Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- .env.example | 18 ++++++++++-------- apps/api/prisma/schema.prisma | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index 1700b65..6c4e43e 100644 --- a/.env.example +++ b/.env.example @@ -314,17 +314,19 @@ COORDINATOR_ENABLED=true # TTL is in seconds, limits are per TTL window # Global rate limit (applies to all endpoints unless overridden) -RATE_LIMIT_TTL=60 # Time window in seconds -RATE_LIMIT_GLOBAL_LIMIT=100 # Requests per window +# Time window in seconds +RATE_LIMIT_TTL=60 +# Requests per window +RATE_LIMIT_GLOBAL_LIMIT=100 -# Webhook endpoints (/stitcher/webhook, /stitcher/dispatch) -RATE_LIMIT_WEBHOOK_LIMIT=60 # Requests per minute +# Webhook endpoints (/stitcher/webhook, /stitcher/dispatch) — requests per minute +RATE_LIMIT_WEBHOOK_LIMIT=60 -# Coordinator endpoints (/coordinator/*) -RATE_LIMIT_COORDINATOR_LIMIT=100 # Requests per minute +# Coordinator endpoints (/coordinator/*) — requests per minute +RATE_LIMIT_COORDINATOR_LIMIT=100 -# Health check endpoints (/coordinator/health) -RATE_LIMIT_HEALTH_LIMIT=300 # Requests per minute (higher for monitoring) +# Health check endpoints (/coordinator/health) — requests per minute (higher for monitoring) +RATE_LIMIT_HEALTH_LIMIT=300 # Storage backend for rate limiting (redis or memory) # redis: Uses Valkey for distributed rate limiting (recommended for production) diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 833e0c6..9641c47 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -3,6 +3,7 @@ generator client { provider = "prisma-client-js" + binaryTargets = ["native", "debian-openssl-3.0.x"] previewFeatures = ["postgresqlExtensions"] } From 21bf7e050f4b46fd68e149845366333885c563eb Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 27 Feb 2026 03:49:57 +0000 Subject: [PATCH 03/23] fix(web): resolve dashboard widget errors and deployment config (#530) Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- .env.example | 12 ++++++------ apps/web/src/app/(auth)/login/page.tsx | 2 +- .../widgets/ActiveProjectsWidget.tsx | 18 ++++++++++++++---- docker-compose.swarm.portainer.yml | 4 ++++ 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index 6c4e43e..36dbd73 100644 --- a/.env.example +++ b/.env.example @@ -79,7 +79,7 @@ OIDC_CLIENT_ID=your-client-id-here OIDC_CLIENT_SECRET=your-client-secret-here # Redirect URI must match what's configured in Authentik # Development: http://localhost:3001/auth/oauth2/callback/authentik -# Production: https://api.mosaicstack.dev/auth/oauth2/callback/authentik +# Production: https://mosaic-api.woltje.com/auth/oauth2/callback/authentik OIDC_REDIRECT_URI=http://localhost:3001/auth/oauth2/callback/authentik # Authentik PostgreSQL Database @@ -361,17 +361,17 @@ RATE_LIMIT_STORAGE=redis # a single workspace. MATRIX_HOMESERVER_URL=http://synapse:8008 MATRIX_ACCESS_TOKEN= -MATRIX_BOT_USER_ID=@mosaic-bot:matrix.example.com -MATRIX_SERVER_NAME=matrix.example.com -# MATRIX_CONTROL_ROOM_ID=!roomid:matrix.example.com +MATRIX_BOT_USER_ID=@mosaic-bot:matrix.woltje.com +MATRIX_SERVER_NAME=matrix.woltje.com +# MATRIX_CONTROL_ROOM_ID=!roomid:matrix.woltje.com # MATRIX_WORKSPACE_ID=your-workspace-uuid # ====================== # Matrix / Synapse Deployment # ====================== # Domains for Traefik routing to Matrix services -MATRIX_DOMAIN=matrix.example.com -ELEMENT_DOMAIN=chat.example.com +MATRIX_DOMAIN=matrix.woltje.com +ELEMENT_DOMAIN=chat.woltje.com # Synapse database (created automatically by synapse-db-init in the swarm compose) SYNAPSE_POSTGRES_DB=synapse diff --git a/apps/web/src/app/(auth)/login/page.tsx b/apps/web/src/app/(auth)/login/page.tsx index d5c09e5..467b647 100644 --- a/apps/web/src/app/(auth)/login/page.tsx +++ b/apps/web/src/app/(auth)/login/page.tsx @@ -326,7 +326,7 @@ function LoginPageContent(): ReactElement {
- +
diff --git a/apps/web/src/components/widgets/ActiveProjectsWidget.tsx b/apps/web/src/components/widgets/ActiveProjectsWidget.tsx index 1db97d5..068ed71 100644 --- a/apps/web/src/components/widgets/ActiveProjectsWidget.tsx +++ b/apps/web/src/components/widgets/ActiveProjectsWidget.tsx @@ -7,6 +7,7 @@ import { useState, useEffect } from "react"; import { FolderOpen, Bot, Activity, Clock, AlertCircle, CheckCircle2 } from "lucide-react"; import type { WidgetProps } from "@mosaic/shared"; import { apiPost } from "@/lib/api/client"; +import { useWorkspaceId } from "@/lib/hooks"; interface ActiveProject { id: string; @@ -34,6 +35,7 @@ interface AgentSession { } export function ActiveProjectsWidget({ id: _id, config: _config }: WidgetProps): React.JSX.Element { + const workspaceId = useWorkspaceId(); const [projects, setProjects] = useState([]); const [agentSessions, setAgentSessions] = useState([]); const [isLoadingProjects, setIsLoadingProjects] = useState(true); @@ -48,7 +50,11 @@ export function ActiveProjectsWidget({ id: _id, config: _config }: WidgetProps): try { setProjectsError(null); // Use API client to ensure CSRF token is included - const data = await apiPost("/api/widgets/data/active-projects"); + const data = await apiPost( + "/api/widgets/data/active-projects", + undefined, + workspaceId ?? undefined + ); setProjects(data); } catch (error) { console.error("Failed to fetch active projects:", error); @@ -67,7 +73,7 @@ export function ActiveProjectsWidget({ id: _id, config: _config }: WidgetProps): return (): void => { clearInterval(interval); }; - }, []); + }, [workspaceId]); // Fetch agent chains useEffect(() => { @@ -75,7 +81,11 @@ export function ActiveProjectsWidget({ id: _id, config: _config }: WidgetProps): try { setAgentsError(null); // Use API client to ensure CSRF token is included - const data = await apiPost("/api/widgets/data/agent-chains"); + const data = await apiPost( + "/api/widgets/data/agent-chains", + undefined, + workspaceId ?? undefined + ); setAgentSessions(data); } catch (error) { console.error("Failed to fetch agent sessions:", error); @@ -94,7 +104,7 @@ export function ActiveProjectsWidget({ id: _id, config: _config }: WidgetProps): return (): void => { clearInterval(interval); }; - }, []); + }, [workspaceId]); const getStatusIcon = (status: string): React.JSX.Element => { const statusUpper = status.toUpperCase(); diff --git a/docker-compose.swarm.portainer.yml b/docker-compose.swarm.portainer.yml index 3f90de0..0cd84aa 100644 --- a/docker-compose.swarm.portainer.yml +++ b/docker-compose.swarm.portainer.yml @@ -176,6 +176,9 @@ services: NODE_ENV: production PORT: ${WEB_PORT:-3000} NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL} + # Server-side orchestrator proxy (API routes forward to orchestrator service) + ORCHESTRATOR_URL: http://orchestrator:3001 + ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY} healthcheck: test: [ @@ -187,6 +190,7 @@ services: retries: 3 start_period: 40s networks: + - internal - traefik-public deploy: restart_policy: From e3cba37e8c83baa54495213691b0b9862423a6b4 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 27 Feb 2026 04:18:35 +0000 Subject: [PATCH 04/23] fix(api,web): resolve RLS context SQL error, workspace guard crash, and projects response unwrapping (#531) Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- apps/api/src/common/guards/workspace.guard.ts | 8 ++++---- apps/api/src/prisma/prisma.service.ts | 11 +++++++---- apps/web/src/lib/api/projects.ts | 3 ++- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/apps/api/src/common/guards/workspace.guard.ts b/apps/api/src/common/guards/workspace.guard.ts index 75d065f..058441a 100644 --- a/apps/api/src/common/guards/workspace.guard.ts +++ b/apps/api/src/common/guards/workspace.guard.ts @@ -110,10 +110,10 @@ export class WorkspaceGuard implements CanActivate { return paramWorkspaceId; } - // 3. Check request body - const bodyWorkspaceId = request.body.workspaceId; - if (typeof bodyWorkspaceId === "string") { - return bodyWorkspaceId; + // 3. Check request body (body may be undefined for GET requests despite Express typings) + const body = request.body as Record | undefined; + if (body && typeof body.workspaceId === "string") { + return body.workspaceId; } // 4. Check query string (backward compatibility for existing clients) diff --git a/apps/api/src/prisma/prisma.service.ts b/apps/api/src/prisma/prisma.service.ts index 66cfbfd..6721734 100644 --- a/apps/api/src/prisma/prisma.service.ts +++ b/apps/api/src/prisma/prisma.service.ts @@ -140,8 +140,11 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul workspaceId: string, client: PrismaClient = this ): Promise { - await client.$executeRaw`SET LOCAL app.current_user_id = ${userId}`; - await client.$executeRaw`SET LOCAL app.current_workspace_id = ${workspaceId}`; + // Use set_config() instead of SET LOCAL so values are safely parameterized. + // SET LOCAL with Prisma's tagged template produces invalid SQL (bind parameter $1 + // is not supported in SET statements by PostgreSQL). + await client.$executeRaw`SELECT set_config('app.current_user_id', ${userId}, true)`; + await client.$executeRaw`SELECT set_config('app.current_workspace_id', ${workspaceId}, true)`; } /** @@ -151,8 +154,8 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul * @param client - Optional Prisma client (uses 'this' if not provided) */ async clearWorkspaceContext(client: PrismaClient = this): Promise { - await client.$executeRaw`SET LOCAL app.current_user_id = NULL`; - await client.$executeRaw`SET LOCAL app.current_workspace_id = NULL`; + await client.$executeRaw`SELECT set_config('app.current_user_id', '', true)`; + await client.$executeRaw`SELECT set_config('app.current_workspace_id', '', true)`; } /** diff --git a/apps/web/src/lib/api/projects.ts b/apps/web/src/lib/api/projects.ts index 89a11ca..8d68448 100644 --- a/apps/web/src/lib/api/projects.ts +++ b/apps/web/src/lib/api/projects.ts @@ -65,7 +65,8 @@ export interface UpdateProjectDto { * Fetch all projects for a workspace */ export async function fetchProjects(workspaceId?: string): Promise { - return apiGet("/api/projects", workspaceId); + const response = await apiGet<{ data: Project[]; meta?: unknown }>("/api/projects", workspaceId); + return response.data; } /** From edcff6a0e02495ff0b2a1ca528ba95575f8fdef4 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 27 Feb 2026 04:53:07 +0000 Subject: [PATCH 05/23] fix(api,web): add workspace context to widgets and auto-detect workspace ID (#532) Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- apps/api/src/widgets/widgets.controller.ts | 70 ++++++------------- .../settings/credentials/audit/page.tsx | 13 ++-- .../settings/credentials/page.tsx | 12 ++-- apps/web/src/lib/api/client.ts | 10 ++- 4 files changed, 43 insertions(+), 62 deletions(-) diff --git a/apps/api/src/widgets/widgets.controller.ts b/apps/api/src/widgets/widgets.controller.ts index c90c33d..fb4bd5a 100644 --- a/apps/api/src/widgets/widgets.controller.ts +++ b/apps/api/src/widgets/widgets.controller.ts @@ -1,22 +1,14 @@ -import { - Controller, - Get, - Post, - Body, - Param, - UseGuards, - Request, - UnauthorizedException, -} from "@nestjs/common"; +import { Controller, Get, Post, Body, Param, UseGuards, Request } from "@nestjs/common"; import { WidgetsService } from "./widgets.service"; import { WidgetDataService } from "./widget-data.service"; import { AuthGuard } from "../auth/guards/auth.guard"; +import { WorkspaceGuard } from "../common/guards/workspace.guard"; import type { StatCardQueryDto, ChartQueryDto, ListQueryDto, CalendarPreviewQueryDto } from "./dto"; -import type { AuthenticatedRequest } from "../common/types/user.types"; +import type { RequestWithWorkspace } from "../common/types/user.types"; /** * Controller for widget definition and data endpoints - * All endpoints require authentication + * All endpoints require authentication; data endpoints also require workspace context */ @Controller("widgets") @UseGuards(AuthGuard) @@ -51,12 +43,9 @@ export class WidgetsController { * Get stat card widget data */ @Post("data/stat-card") - async getStatCardData(@Request() req: AuthenticatedRequest, @Body() query: StatCardQueryDto) { - const workspaceId = req.user?.currentWorkspaceId ?? req.user?.workspaceId; - if (!workspaceId) { - throw new UnauthorizedException("Workspace ID required"); - } - return this.widgetDataService.getStatCardData(workspaceId, query); + @UseGuards(WorkspaceGuard) + async getStatCardData(@Request() req: RequestWithWorkspace, @Body() query: StatCardQueryDto) { + return this.widgetDataService.getStatCardData(req.workspace.id, query); } /** @@ -64,12 +53,9 @@ export class WidgetsController { * Get chart widget data */ @Post("data/chart") - async getChartData(@Request() req: AuthenticatedRequest, @Body() query: ChartQueryDto) { - const workspaceId = req.user?.currentWorkspaceId ?? req.user?.workspaceId; - if (!workspaceId) { - throw new UnauthorizedException("Workspace ID required"); - } - return this.widgetDataService.getChartData(workspaceId, query); + @UseGuards(WorkspaceGuard) + async getChartData(@Request() req: RequestWithWorkspace, @Body() query: ChartQueryDto) { + return this.widgetDataService.getChartData(req.workspace.id, query); } /** @@ -77,12 +63,9 @@ export class WidgetsController { * Get list widget data */ @Post("data/list") - async getListData(@Request() req: AuthenticatedRequest, @Body() query: ListQueryDto) { - const workspaceId = req.user?.currentWorkspaceId ?? req.user?.workspaceId; - if (!workspaceId) { - throw new UnauthorizedException("Workspace ID required"); - } - return this.widgetDataService.getListData(workspaceId, query); + @UseGuards(WorkspaceGuard) + async getListData(@Request() req: RequestWithWorkspace, @Body() query: ListQueryDto) { + return this.widgetDataService.getListData(req.workspace.id, query); } /** @@ -90,15 +73,12 @@ export class WidgetsController { * Get calendar preview widget data */ @Post("data/calendar-preview") + @UseGuards(WorkspaceGuard) async getCalendarPreviewData( - @Request() req: AuthenticatedRequest, + @Request() req: RequestWithWorkspace, @Body() query: CalendarPreviewQueryDto ) { - const workspaceId = req.user?.currentWorkspaceId ?? req.user?.workspaceId; - if (!workspaceId) { - throw new UnauthorizedException("Workspace ID required"); - } - return this.widgetDataService.getCalendarPreviewData(workspaceId, query); + return this.widgetDataService.getCalendarPreviewData(req.workspace.id, query); } /** @@ -106,12 +86,9 @@ export class WidgetsController { * Get active projects widget data */ @Post("data/active-projects") - async getActiveProjectsData(@Request() req: AuthenticatedRequest) { - const workspaceId = req.user?.currentWorkspaceId ?? req.user?.workspaceId; - if (!workspaceId) { - throw new UnauthorizedException("Workspace ID required"); - } - return this.widgetDataService.getActiveProjectsData(workspaceId); + @UseGuards(WorkspaceGuard) + async getActiveProjectsData(@Request() req: RequestWithWorkspace) { + return this.widgetDataService.getActiveProjectsData(req.workspace.id); } /** @@ -119,11 +96,8 @@ export class WidgetsController { * Get agent chains widget data (active agent sessions) */ @Post("data/agent-chains") - async getAgentChainsData(@Request() req: AuthenticatedRequest) { - const workspaceId = req.user?.currentWorkspaceId ?? req.user?.workspaceId; - if (!workspaceId) { - throw new UnauthorizedException("Workspace ID required"); - } - return this.widgetDataService.getAgentChainsData(workspaceId); + @UseGuards(WorkspaceGuard) + async getAgentChainsData(@Request() req: RequestWithWorkspace) { + return this.widgetDataService.getAgentChainsData(req.workspace.id); } } diff --git a/apps/web/src/app/(authenticated)/settings/credentials/audit/page.tsx b/apps/web/src/app/(authenticated)/settings/credentials/audit/page.tsx index 4eb77d4..271df01 100644 --- a/apps/web/src/app/(authenticated)/settings/credentials/audit/page.tsx +++ b/apps/web/src/app/(authenticated)/settings/credentials/audit/page.tsx @@ -14,6 +14,7 @@ import { SelectValue, } from "@/components/ui/select"; import { fetchCredentialAuditLog, type AuditLogEntry } from "@/lib/api/credentials"; +import { useWorkspaceId } from "@/lib/hooks"; const ACTIVITY_ACTIONS = [ { value: "CREDENTIAL_CREATED", label: "Created" }, @@ -39,17 +40,17 @@ export default function CredentialAuditPage(): React.ReactElement { const [filters, setFilters] = useState({}); const [hasFilters, setHasFilters] = useState(false); - // TODO: Get workspace ID from context/auth - const workspaceId = "default-workspace-id"; // Placeholder + const workspaceId = useWorkspaceId(); useEffect(() => { - void loadLogs(); - }, [page, filters]); + if (!workspaceId) return; + void loadLogs(workspaceId); + }, [workspaceId, page, filters]); - async function loadLogs(): Promise { + async function loadLogs(wsId: string): Promise { try { setIsLoading(true); - const response = await fetchCredentialAuditLog(workspaceId, { + const response = await fetchCredentialAuditLog(wsId, { ...filters, page, limit, diff --git a/apps/web/src/app/(authenticated)/settings/credentials/page.tsx b/apps/web/src/app/(authenticated)/settings/credentials/page.tsx index 5df0c66..73adb5a 100644 --- a/apps/web/src/app/(authenticated)/settings/credentials/page.tsx +++ b/apps/web/src/app/(authenticated)/settings/credentials/page.tsx @@ -6,22 +6,24 @@ import Link from "next/link"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { fetchCredentials, type Credential } from "@/lib/api/credentials"; +import { useWorkspaceId } from "@/lib/hooks"; export default function CredentialsPage(): React.ReactElement { const [credentials, setCredentials] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const workspaceId = "default-workspace-id"; + const workspaceId = useWorkspaceId(); useEffect(() => { - void loadCredentials(); - }, []); + if (!workspaceId) return; + void loadCredentials(workspaceId); + }, [workspaceId]); - async function loadCredentials(): Promise { + async function loadCredentials(wsId: string): Promise { try { setIsLoading(true); - const response = await fetchCredentials(workspaceId); + const response = await fetchCredentials(wsId); setCredentials(response.data); setError(null); } catch (err) { diff --git a/apps/web/src/lib/api/client.ts b/apps/web/src/lib/api/client.ts index 7bdd8fc..e407fcd 100644 --- a/apps/web/src/lib/api/client.ts +++ b/apps/web/src/lib/api/client.ts @@ -202,9 +202,13 @@ export async function apiRequest(endpoint: string, options: ApiRequestOptions ...baseHeaders, }; - // Add workspace ID header if provided (recommended over query string) - if (workspaceId) { - headers["X-Workspace-Id"] = workspaceId; + // Add workspace ID header — use explicit value, or auto-detect from localStorage + const resolvedWorkspaceId = + workspaceId ?? + (typeof window !== "undefined" ? localStorage.getItem("mosaic-workspace-id") : null) ?? + undefined; + if (resolvedWorkspaceId) { + headers["X-Workspace-Id"] = resolvedWorkspaceId; } // Add CSRF token for state-changing requests (POST, PUT, PATCH, DELETE) From 11f22a7e968d03e9f8702227338943d0102eac76 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 27 Feb 2026 05:16:30 +0000 Subject: [PATCH 06/23] fix(api): add sort, search, visibility to knowledge entry query DTO (#533) Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- apps/api/src/knowledge/dto/entry-query.dto.ts | 22 +++++++++++++++++-- apps/api/src/knowledge/knowledge.service.ts | 22 ++++++++++++++++--- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/apps/api/src/knowledge/dto/entry-query.dto.ts b/apps/api/src/knowledge/dto/entry-query.dto.ts index 5a5f97b..c455838 100644 --- a/apps/api/src/knowledge/dto/entry-query.dto.ts +++ b/apps/api/src/knowledge/dto/entry-query.dto.ts @@ -1,6 +1,6 @@ -import { IsOptional, IsEnum, IsString, IsInt, Min, Max } from "class-validator"; +import { IsOptional, IsEnum, IsString, IsInt, IsIn, Min, Max } from "class-validator"; import { Type } from "class-transformer"; -import { EntryStatus } from "@prisma/client"; +import { EntryStatus, Visibility } from "@prisma/client"; /** * DTO for querying knowledge entries (list endpoint) @@ -10,10 +10,28 @@ export class EntryQueryDto { @IsEnum(EntryStatus, { message: "status must be a valid EntryStatus" }) status?: EntryStatus; + @IsOptional() + @IsEnum(Visibility, { message: "visibility must be a valid Visibility" }) + visibility?: Visibility; + @IsOptional() @IsString({ message: "tag must be a string" }) tag?: string; + @IsOptional() + @IsString({ message: "search must be a string" }) + search?: string; + + @IsOptional() + @IsIn(["updatedAt", "createdAt", "title"], { + message: "sortBy must be updatedAt, createdAt, or title", + }) + sortBy?: "updatedAt" | "createdAt" | "title"; + + @IsOptional() + @IsIn(["asc", "desc"], { message: "sortOrder must be asc or desc" }) + sortOrder?: "asc" | "desc"; + @IsOptional() @Type(() => Number) @IsInt({ message: "page must be an integer" }) diff --git a/apps/api/src/knowledge/knowledge.service.ts b/apps/api/src/knowledge/knowledge.service.ts index f004d91..e1ef04c 100644 --- a/apps/api/src/knowledge/knowledge.service.ts +++ b/apps/api/src/knowledge/knowledge.service.ts @@ -48,6 +48,10 @@ export class KnowledgeService { where.status = query.status; } + if (query.visibility) { + where.visibility = query.visibility; + } + if (query.tag) { where.tags = { some: { @@ -58,6 +62,20 @@ export class KnowledgeService { }; } + if (query.search) { + where.OR = [ + { title: { contains: query.search, mode: "insensitive" } }, + { content: { contains: query.search, mode: "insensitive" } }, + ]; + } + + // Build orderBy + const sortField = query.sortBy ?? "updatedAt"; + const sortDirection = query.sortOrder ?? "desc"; + const orderBy: Prisma.KnowledgeEntryOrderByWithRelationInput = { + [sortField]: sortDirection, + }; + // Get total count const total = await this.prisma.knowledgeEntry.count({ where }); @@ -71,9 +89,7 @@ export class KnowledgeService { }, }, }, - orderBy: { - updatedAt: "desc", - }, + orderBy, skip, take: limit, }); From 8964226163bf09c72ac2527ea1a41116b4f10915 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 27 Feb 2026 10:12:24 +0000 Subject: [PATCH 07/23] chore(orchestrator): bootstrap MS20 Site Stabilization mission (#535) Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- docs/MISSION-MANIFEST.md | 94 ++++++++----------- docs/PRD.md | 93 ++++++++++++++---- docs/TASKS.md | 81 ++++++++-------- .../ms20-site-stabilization-20260227.md | 67 +++++++++++++ 4 files changed, 223 insertions(+), 112 deletions(-) create mode 100644 docs/scratchpads/ms20-site-stabilization-20260227.md diff --git a/docs/MISSION-MANIFEST.md b/docs/MISSION-MANIFEST.md index 64b9f19..231a10f 100644 --- a/docs/MISSION-MANIFEST.md +++ b/docs/MISSION-MANIFEST.md @@ -1,51 +1,53 @@ -# Mission Manifest — MS19 Chat & Terminal System +# Mission Manifest — MS20 Site Stabilization > Persistent document tracking full mission scope, status, and session history. > Updated by the orchestrator at each phase transition and milestone completion. ## Mission -**ID:** ms19-chat-terminal-20260225 -**Statement:** Implement MS19 (Chat & Terminal System) — real terminal with PTY backend, chat streaming, master chat polish, project-level orchestrator chat, and agent output integration -**Phase:** Completion -**Current Milestone:** MS19-ChatTerminal -**Progress:** 1 / 1 milestones -**Status:** completed -**Last Updated:** 2026-02-26T04:20Z +**ID:** ms20-site-stabilization-20260227 +**Statement:** Fix runtime bugs, missing API endpoints, orchestrator connectivity, and feature gaps discovered during live site testing at mosaic.woltje.com +**Phase:** Planning +**Current Milestone:** MS20-SiteStabilization +**Progress:** 0 / 1 milestones +**Status:** active +**Last Updated:** 2026-02-27T05:30Z ## Success Criteria -1. Terminal panel has real xterm.js with PTY backend via WebSocket — **DONE** (PR #518) -2. Terminal supports multiple named sessions (create/close/rename tabs) — **DONE** (PR #520) -3. Terminal sessions persist in PostgreSQL and recover on reconnect — **DONE** (PR #517) -4. Chat streaming renders tokens in real-time via SSE — **DONE** (PR #516) -5. Master chat sidebar accessible from any page (Cmd+Shift+J / Cmd+K) — **DONE** (PR #519) -6. Master chat supports model selection, temperature, conversation management — **DONE** (PR #519) -7. Project-level chat can trigger orchestrator actions (/spawn, /status, /jobs) — **DONE** (PR #521) -8. Agent output from orchestrator viewable in terminal tabs — **DONE** (PR #522) -9. All features support all 5 themes (Dark, Light, Nord, Dracula, Solarized) — **DONE** (CSS variables) -10. Lint, typecheck, and tests pass — **DONE** (1441 web + 3303 API = 4744 tests) -11. Deployed and smoke-tested at mosaic.woltje.com — **DONE** (CI #635 green, web image sha:7165e7a deployed) +1. Domains page: can create and list domains without workspace errors +2. Projects page: can create new projects without workspace errors +3. Personalities page: full CRUD works with proper dark mode theming +4. User preferences endpoint (`/users/me/preferences`) returns data +5. Credentials page: can add, view credentials (not just disabled stub) +6. Orchestrator proxy endpoints return real data (no 502) +7. Orchestrator WebSocket connects successfully +8. Dashboard Agent Status, Task Progress, Orchestrator Events widgets work +9. Terminal has dedicated `/terminal` page route +10. favicon.ico serves correctly (no 404) +11. `useWorkspaceId` warning resolved — workspace ID persists in localStorage +12. All 5 themes render correctly on all affected pages +13. Lint, typecheck, and tests pass +14. Deployed and verified at mosaic.woltje.com ## Existing Infrastructure -Key components already built that MS19 builds upon: +Key components already built that MS20 builds upon: -| Component | Status | Location | -| --------------------------------- | ------------------- | ------------------------------------ | -| ChatOverlay + ConversationSidebar | ~95% complete | `apps/web/src/components/chat/` | -| LLM Controller with SSE | Working | `apps/api/src/llm/` | -| WebSocket Gateway | Production | `apps/api/src/websocket/` | -| TerminalPanel UI (mock) | UI-only, no backend | `apps/web/src/components/terminal/` | -| Orchestrator proxy routes | Working | `apps/web/src/app/api/orchestrator/` | -| Speech Gateway (pattern ref) | Production | `apps/api/src/speech/` | -| Ideas API (chat persistence) | Working | `apps/api/src/ideas/` | +| Component | Status | Location | +| ------------------------- | --------------- | ----------------------------------------------- | +| WorkspaceGuard | Working | `apps/api/src/common/guards/workspace.guard.ts` | +| Auto-detect workspace ID | Working (reads) | `apps/web/src/lib/api/client.ts` | +| Credentials API backend | Built (M7) | `apps/api/src/credentials/` | +| Orchestrator proxy routes | Built (MS19) | `apps/web/src/app/api/orchestrator/` | +| Terminal components | Built (MS19) | `apps/web/src/components/terminal/` | +| Theme system | Working (MS18) | `apps/web/src/lib/themes/` | ## Milestones -| # | ID | Name | Status | Branch | Issue | Started | Completed | -| --- | ---- | ---------------------- | --------- | ------------------------- | ------------------------ | ---------- | ---------- | -| 1 | MS19 | Chat & Terminal System | completed | per-task feature branches | #508,#509,#510,#511,#512 | 2026-02-25 | 2026-02-25 | +| # | ID | Name | Status | Branch | Issue | Started | Completed | +| --- | ---- | ------------------ | ----------- | ------------------------- | ----- | ---------- | --------- | +| 1 | MS20 | Site Stabilization | not-started | per-task feature branches | TBD | 2026-02-27 | — | ## Deployment @@ -57,32 +59,16 @@ Key components already built that MS19 builds upon: | Metric | Value | | ------ | ----------------- | -| Budget | ~300K (estimated) | -| Used | ~220K | +| Budget | ~400K (estimated) | +| Used | 0 | | Mode | normal | ## Session History -| Session | Runtime | Started | Duration | Ended Reason | Last Task | -| ------- | --------------- | ----------------- | -------- | ------------ | ------------------------------------------------- | -| S1 | Claude Opus 4.6 | 2026-02-25T20:00Z | ~1h | context | Planning (PLAN-001) | -| S2 | Claude Opus 4.6 | 2026-02-25T21:00Z | ~2h | context | Wave 1+2 (5 tasks, PRs #515-518) | -| S3 | Claude Opus 4.6 | 2026-02-25T23:00Z | ~1.5h | context | Wave 3+4 (TERM-004, CHAT-002, ORCH-001, ORCH-002) | -| S4 | Claude Opus 4.6 | 2026-02-26T04:00Z | ~30m | completed | VER-001, DOC-001, VER-002 — mission complete | - -## PRs Merged - -| PR | Commit | Task | Description | -| ---- | ------- | -------- | ---------------------------------------- | -| #515 | 6290fc3 | TERM-001 | Terminal WebSocket gateway & PTY service | -| #516 | 7de0e73 | CHAT-001 | SSE chat streaming | -| #517 | 8128eb7 | TERM-002 | Terminal session persistence | -| #518 | 417c6ab | TERM-003 | xterm.js integration | -| #519 | 13aa52a | CHAT-002 | Master chat polish | -| #520 | 859dcfc | TERM-004 | Terminal tab management | -| #521 | b110c46 | ORCH-001 | Orchestrator command system | -| #522 | 9b2520c | ORCH-002 | Agent output terminal tabs | +| Session | Runtime | Started | Duration | Ended Reason | Last Task | +| ------- | --------------- | ----------------- | -------- | ------------ | --------- | +| S1 | Claude Opus 4.6 | 2026-02-27T05:30Z | — | — | Planning | ## Scratchpad -Path: `docs/scratchpads/ms19-chat-terminal-20260225.md` +Path: `docs/scratchpads/ms20-site-stabilization-20260227.md` diff --git a/docs/PRD.md b/docs/PRD.md index 8c1937c..f24e333 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -134,21 +134,28 @@ This is the active mission scope. MS16 (Pages) and MS17 (Backend Integration) ar 13. Global terminal: project/orchestrator level, smart (MS19) 14. Project-level orchestrator chat (MS19) 15. Master chat session: collapsible sidebar/slideout, always available (MS19) -16. Settings page for ALL environment variables, dynamically configurable via webUI (MS20) -17. Multi-tenant configuration with admin user management (MS20) -18. Team management with shared data spaces and chat rooms (MS20) -19. RBAC for file access, resources, models (MS20) -20. Federation: master-master and master-slave with key exchange (MS21) -21. Federation testing: 3 instances on Portainer (woltje.com domain) (MS21) -22. Agent task mapping configuration: system-level defaults, user-level overrides (MS22) -23. Telemetry: opt-out, customizable endpoint, sanitized data (MS22) -24. File manager with WYSIWYG editing: system/user/project levels (MS18) -25. User-level and project-level Kanban with filtering (MS18) -26. Break-glass authentication user (MS20) -27. Playwright E2E tests for all pages (MS23) -28. API documentation via Swagger (MS23) -29. Backend endpoints for all dashboard data (MS17 — already complete for existing modules) -30. Profile page linked from user card (MS16) +16. Site stabilization: workspace context propagation for mutations (MS20) +17. Site stabilization: personalities API + UI (MS20) +18. Site stabilization: user preferences API endpoint (MS20) +19. Site stabilization: orchestrator 502 and WebSocket connectivity (MS20) +20. Site stabilization: credential management UI (MS20) +21. Site stabilization: terminal page route (MS20) +22. Site stabilization: favicon, dark mode dropdown fix (MS20) +23. Settings page for ALL environment variables, dynamically configurable via webUI (MS21) +24. Multi-tenant configuration with admin user management (MS21) +25. Team management with shared data spaces and chat rooms (MS21) +26. RBAC for file access, resources, models (MS21) +27. Federation: master-master and master-slave with key exchange (MS22) +28. Federation testing: 3 instances on Portainer (woltje.com domain) (MS22) +29. Agent task mapping configuration: system-level defaults, user-level overrides (MS23) +30. Telemetry: opt-out, customizable endpoint, sanitized data (MS23) +31. File manager with WYSIWYG editing: system/user/project levels (MS18) +32. User-level and project-level Kanban with filtering (MS18) +33. Break-glass authentication user (MS20) +34. Playwright E2E tests for all pages (MS23) +35. API documentation via Swagger (MS23) +36. Backend endpoints for all dashboard data (MS17 — already complete for existing modules) +37. Profile page linked from user card (MS16) ### Out of Scope @@ -334,7 +341,46 @@ This is the active mission scope. MS16 (Pages) and MS17 (Backend Integration) ar - ASSUMPTION: Orchestrator commands route through existing web proxy (/api/orchestrator/\*) to orchestrator service. Rationale: Proxy routes already exist and handle auth. - **Status: COMPLETE (MS19) — PRs #521 (commands), #522 (agent terminal). /status, /agents, /jobs, /pause, /resume, /help commands. Agent output streaming via SSE. 113 web tests.** -### FR-020: Settings Configuration (Future — MS20) +### FR-020: Site Stabilization & Feature Gaps (MS20) — IN PROGRESS + +Runtime bugs and feature gaps discovered during live testing of mosaic.woltje.com. + +**Workspace Context Propagation:** + +- Domains page: "Workspace ID is required" when creating domains +- Projects page: "Workspace ID is required" when creating projects +- Credentials page: unable to add credentials (button disabled, feature stub) +- ASSUMPTION: The `useWorkspaceId()` hook + auto-detect in `apiRequest` from PR #532 handles reads, but mutation endpoints on some pages don't pass workspace ID correctly. Rationale: GET requests work after PR #532 but POST/mutation requests still fail on domains and projects pages. + +**Missing API Endpoints:** + +- `/api/personalities` — no controller/service exists; frontend expects GET/POST/PATCH/DELETE +- `/users/me/preferences` — listed in PRD API table but returns 404; frontend profile page depends on it +- ASSUMPTION: Personalities API follows existing NestJS module patterns (controller + service + DTO + Prisma model). Rationale: Consistent with all other API modules in the codebase. +- ASSUMPTION: User preferences endpoint is part of the existing users module but route is not registered. Rationale: PRD lists it as an existing endpoint. + +**Orchestrator Connectivity:** + +- All orchestrator-proxied endpoints return HTTP 502 +- Orchestrator WebSocket connection fails ("Reconnecting to server...") +- Dashboard widgets: Agent Status, Task Progress, Orchestrator Events all error +- ASSUMPTION: The orchestrator service container runs but the Next.js API proxy cannot reach it. Root cause is likely environment variable or network configuration in Docker Swarm. Rationale: The orchestrator container exists in the compose file and has Traefik labels. + +**UI/UX Issues:** + +- Dark mode theming on Formality Level dropdown in Personalities page incorrect +- favicon.ico missing (404) +- Terminal sidebar link uses `#terminal` anchor instead of page route +- `useWorkspaceId` warning in console: no workspace ID in localStorage on fresh sessions +- ASSUMPTION: Terminal should have a dedicated page route `/terminal` that renders the terminal panel full-screen. Rationale: The sidebar has a Terminal link in the Operations section alongside Logs, implying it should be a navigable page. + +**Credential Management:** + +- "Add Credential" button is `disabled` in code — feature was stubbed as "coming soon" +- Need to implement credential creation UI and wire to existing `/api/credentials` CRUD endpoints +- ASSUMPTION: Credential CRUD frontend can use the existing `/api/credentials` API which was built during M7-CredentialSecurity. Rationale: Backend endpoints exist per audit. + +### FR-021: Settings Configuration (Future — MS21) - All environment variables configurable via UI - Minimal launch env vars, rest configurable dynamically @@ -496,10 +542,11 @@ These 19 NestJS modules are already implemented with Prisma and available for fr | MS16+MS17-PagesDataIntegration | 0.0.17 | All pages built + wired to real API data | COMPLETE | | MS18-ThemeWidgets | 0.0.18 | Theme package system, widget registry, WYSIWYG, Kanban filtering | COMPLETE | | MS19-ChatTerminal | 0.0.19 | Global terminal, project chat, master chat session | COMPLETE | -| MS20-MultiTenant | 0.0.20 | Multi-tenant, teams, RBAC, RLS enforcement, break-glass auth | NOT STARTED | -| MS21-Federation | 0.0.21 | Federation (M-M, M-S), 3 instances, key exchange, data separation | NOT STARTED | -| MS22-AgentTelemetry | 0.0.22 | Agent task mapping, telemetry, wide-event logging | NOT STARTED | -| MS23-Testing | 0.0.23 | Playwright E2E, federation tests, documentation finalization | NOT STARTED | +| MS20-SiteStabilization | 0.0.20 | Runtime bug fixes, missing endpoints, orchestrator connectivity | IN PROGRESS | +| MS21-MultiTenant | 0.0.21 | Multi-tenant, teams, RBAC, RLS enforcement, break-glass auth | NOT STARTED | +| MS22-Federation | 0.0.22 | Federation (M-M, M-S), 3 instances, key exchange, data separation | NOT STARTED | +| MS23-AgentTelemetry | 0.0.23 | Agent task mapping, telemetry, wide-event logging | NOT STARTED | +| MS24-Testing | 0.0.24 | Playwright E2E, federation tests, documentation finalization | NOT STARTED | ## Assumptions @@ -511,3 +558,9 @@ These 19 NestJS modules are already implemented with Prisma and available for fr 6. ASSUMPTION: Theme packages are code-level TypeScript files (not runtime-installable npm packages). Each theme exports CSS variable overrides. Rationale: Keeps the system simple for MS18; runtime package loading can be added in a future milestone. 7. ASSUMPTION: WYSIWYG editor uses Tiptap (ProseMirror-based, headless). Rationale: Headless approach integrates naturally with the CSS variable design system, excellent markdown import/export, TypeScript-first, battle-tested. 8. ASSUMPTION: MS18 includes WYSIWYG editing for knowledge entries and Kanban filtering enhancements in addition to themes and widgets. These were originally listed separately but are grouped into MS18 per PRD scope items 24-25. Rationale: All are frontend-focused enhancements that build on the existing page infrastructure. +9. ASSUMPTION: The `useWorkspaceId()` hook + auto-detect in `apiRequest` from PR #532 handles reads, but mutation endpoints on some pages don't pass workspace ID correctly. Rationale: GET requests work after PR #532 but POST/mutation requests still fail on domains and projects pages. +10. ASSUMPTION: Personalities API follows existing NestJS module patterns (controller + service + DTO + Prisma model). Rationale: Consistent with all other API modules in the codebase. +11. ASSUMPTION: User preferences endpoint is part of the existing users module but route is not registered. Rationale: PRD lists it as an existing endpoint. +12. ASSUMPTION: The orchestrator service container runs but the Next.js API proxy cannot reach it. Root cause is likely environment variable or network configuration in Docker Swarm. Rationale: The orchestrator container exists in the compose file and has Traefik labels. +13. ASSUMPTION: Terminal should have a dedicated page route `/terminal` that renders the terminal panel full-screen. Rationale: The sidebar has a Terminal link in the Operations section alongside Logs, implying it should be a navigable page. +14. ASSUMPTION: Credential CRUD frontend can use the existing `/api/credentials` API which was built during M7-CredentialSecurity. Rationale: Backend endpoints exist per audit. diff --git a/docs/TASKS.md b/docs/TASKS.md index 3bd3d6f..b62d3e9 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -1,54 +1,59 @@ -# Tasks — MS19 Chat & Terminal System +# Tasks — MS20 Site Stabilization > Single-writer: orchestrator only. Workers read but never modify. -| id | status | description | issue | repo | branch | depends_on | blocks | agent | started_at | completed_at | estimate | used | notes | -| ----------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------- | ------------------------------ | ----------------------------------------------- | ----------------------------------------------- | ------------ | ---------- | ------------ | -------- | ---- | ----------------------------------------------------------------- | -| CT-PLAN-001 | done | Plan MS19 task breakdown, create milestone + issues, populate TASKS.md | — | — | — | | CT-TERM-001,CT-TERM-002,CT-CHAT-001,CT-CHAT-002 | orchestrator | 2026-02-25 | 2026-02-25 | 15K | ~15K | Planning complete | -| CT-TERM-001 | done | Terminal WebSocket gateway & PTY session service — NestJS gateway (namespace: /terminal), node-pty spawn/kill/resize, workspace-scoped rooms, auth via token | #508 | api | feat/ms19-terminal-gateway | CT-PLAN-001 | CT-TERM-003,CT-TERM-004,CT-ORCH-002 | sonnet | 2026-02-25 | 2026-02-25 | 30K | ~30K | PR #515 merged (6290fc3), 48 tests | -| CT-TERM-002 | done | Terminal session persistence — Prisma model (TerminalSession: id, workspaceId, name, status, createdAt, closedAt), migration, CRUD service | #508 | api | feat/ms19-terminal-persistence | CT-PLAN-001 | CT-TERM-004 | sonnet | 2026-02-25 | 2026-02-25 | 15K | ~15K | PR #517 merged (8128eb7), 12 tests, #508 closed | -| CT-TERM-003 | done | xterm.js integration — Replace mock TerminalPanel with real xterm.js, WebSocket connection to /terminal namespace, resize handling, copy/paste, theme support | #509 | web | feat/ms19-xterm-integration | CT-TERM-001 | CT-TERM-004 | sonnet | 2026-02-25 | 2026-02-25 | 30K | ~30K | PR #518 merged (417c6ab), 40 tests | -| CT-TERM-004 | done | Terminal tab management — Multiple named sessions, create/close/rename tabs, tab switching, session list from API, reconnect on page reload | #509 | web | feat/ms19-terminal-tabs | CT-TERM-001,CT-TERM-002,CT-TERM-003 | CT-VER-001 | sonnet | 2026-02-25 | 2026-02-25 | 20K | ~20K | PR #520 merged (859dcfc), 76 tests, #509 closed | -| CT-CHAT-001 | done | Complete SSE chat streaming — Wire streamChatMessage() in frontend, token-by-token rendering in MessageList, streaming state indicators, abort/cancel support | #510 | web | feat/ms19-chat-streaming-v2 | CT-PLAN-001 | CT-CHAT-002,CT-ORCH-001 | sonnet | 2026-02-25 | 2026-02-25 | 25K | ~25K | PR #516 merged (7de0e73), streaming+fallback+abort | -| CT-CHAT-002 | done | Master chat polish — Model selector dropdown, temperature/params config, conversation search in sidebar, keyboard shortcut improvements, empty state design | #510 | web | feat/ms19-chat-polish | CT-CHAT-001 | CT-VER-001 | sonnet | 2026-02-25 | 2026-02-25 | 15K | ~15K | PR #519 merged (13aa52a), 46 tests, #510 closed | -| CT-ORCH-001 | done | Project-level orchestrator chat — Chat context scoped to project, command prefix parsing (/spawn, /status, /jobs, /kill), route commands through orchestrator proxy, display structured responses | #511 | web | feat/ms19-orchestrator-chat | CT-CHAT-001 | CT-ORCH-002,CT-VER-001 | sonnet | 2026-02-25 | 2026-02-25 | 30K | ~25K | PR #521 merged (b110c46), 34 tests | -| CT-ORCH-002 | done | Agent output in terminal — View orchestrator agent sessions as terminal tabs, stream agent stdout/stderr via SSE (/agents/events), agent lifecycle indicators (spawning/running/done) | #511 | web | feat/ms19-agent-terminal | CT-TERM-001,CT-ORCH-001 | CT-VER-001 | sonnet | 2026-02-25 | 2026-02-25 | 25K | ~25K | PR #522 merged (9b2520c), 79 tests, #511 closed | -| CT-VER-001 | done | Unit tests — Tests for terminal gateway, xterm component, chat streaming, orchestrator chat, agent terminal integration | #512 | web,api | — | CT-TERM-004,CT-CHAT-002,CT-ORCH-001,CT-ORCH-002 | CT-DOC-001 | orchestrator | 2026-02-25 | 2026-02-25 | 20K | ~5K | 328 MS19 tests (268 web + 60 API), all inline with tasks | -| CT-DOC-001 | done | Documentation updates — TASKS.md, manifest, scratchpad, PRD status updates | #512 | — | — | CT-VER-001 | CT-VER-002 | orchestrator | 2026-02-25 | 2026-02-25 | 10K | ~5K | Updated PRD, manifest, scratchpad, TASKS.md | -| CT-VER-002 | done | Deploy + smoke test — Deploy to Portainer, verify terminal, chat streaming, orchestrator chat, agent output all functional | #512 | — | — | CT-DOC-001 | | orchestrator | 2026-02-25 | 2026-02-25 | 15K | ~5K | CI #635 green, web deployed (sha:7165e7a), API crash pre-existing | +| id | status | description | issue | repo | branch | depends_on | blocks | agent | started_at | completed_at | estimate | used | notes | +| ----------- | ----------- | ---------------------------------------------------------------------------------------- | ----- | ------- | ----------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------ | ------------ | ---------- | ------------ | -------- | ---- | --------------------------------------------------------------------------------------- | +| SS-PLAN-001 | done | Plan MS20 task breakdown, create milestone + issues, populate TASKS.md | — | — | — | | SS-WS-001,SS-ORCH-001,SS-API-001,SS-UI-001 | orchestrator | 2026-02-27 | 2026-02-27 | 15K | ~15K | Planning complete | +| SS-WS-001 | not-started | Fix workspace context for domain creation — domains page POST sends workspace ID | #534 | web,api | fix/workspace-domain-project-create | SS-PLAN-001 | SS-WS-002 | | | | 15K | | Domains page "Workspace ID is required" on create | +| SS-WS-002 | not-started | Fix workspace context for project creation — projects page POST sends workspace ID | #534 | web,api | fix/workspace-domain-project-create | SS-WS-001 | SS-VER-001 | | | | 10K | | Projects page "Workspace ID is required" on create. May be same root cause as SS-WS-001 | +| SS-WS-003 | not-started | Fix useWorkspaceId localStorage initialization — ensure workspace ID persists from login | #534 | web | fix/workspace-id-persistence | SS-PLAN-001 | SS-VER-001 | | | | 15K | | Console warning: no workspace ID in localStorage | +| SS-ORCH-001 | not-started | Fix orchestrator 502 — diagnose and fix proxy connectivity to orchestrator service | #534 | web,api | fix/orchestrator-connectivity | SS-PLAN-001 | SS-ORCH-002 | | | | 25K | | All orchestrator endpoints return 502 | +| SS-ORCH-002 | not-started | Fix orchestrator WebSocket connection — "Reconnecting to server..." in chat panel | #534 | web | fix/orchestrator-websocket | SS-ORCH-001 | SS-VER-001 | | | | 15K | | Depends on orchestrator proxy fix | +| SS-API-001 | not-started | Implement personalities API — controller, service, DTOs, Prisma model for CRUD | #534 | api | feat/personalities-api | SS-PLAN-001 | SS-UI-002 | | | | 30K | | Cannot GET /api/personalities?isActive=true | +| SS-API-002 | not-started | Implement /users/me/preferences endpoint — wire to UserPreference model | #534 | api | feat/user-preferences-endpoint | SS-PLAN-001 | SS-VER-001 | | | | 15K | | Profile page: "Preferences unavailable" | +| SS-UI-001 | not-started | Credential management UI — enable Add Credential button, create/view forms, wire to API | #534 | web | feat/credential-management-ui | SS-PLAN-001 | SS-VER-001 | | | | 25K | | Button currently disabled, feature stubbed | +| SS-UI-002 | not-started | Fix personalities page — dark mode Formality dropdown, save functionality, wire to API | #534 | web | fix/personalities-page | SS-API-001 | SS-VER-001 | | | | 15K | | Dark mode theming broken, unable to save | +| SS-UI-003 | not-started | Terminal page route — create /terminal page with full-screen terminal panel | #534 | web | feat/terminal-page-route | SS-PLAN-001 | SS-VER-001 | | | | 10K | | Sidebar Terminal link goes to #terminal anchor | +| SS-UI-004 | not-started | Add favicon.ico and fix dark mode polish | #534 | web | fix/favicon-polish | SS-PLAN-001 | SS-VER-001 | | | | 5K | | favicon.ico 404 | +| SS-VER-001 | not-started | Verification — full site test, all pages load without errors, deploy + smoke test | #534 | web,api | — | SS-WS-002,SS-WS-003,SS-ORCH-002,SS-API-002,SS-UI-001,SS-UI-002,SS-UI-003,SS-UI-004 | SS-DOC-001 | | | | 15K | | Primary validation gate | +| SS-DOC-001 | not-started | Documentation — update PRD status, manifest, scratchpad, close mission | #534 | — | — | SS-VER-001 | | | | | 5K | | | ## Summary -| Metric | Value | -| --------------- | ----------------- | -| Total tasks | 12 | -| Completed | 12 | -| In Progress | 0 | -| Remaining | 0 | -| Estimated total | ~250K tokens | -| Used | ~215K tokens | -| Milestone | MS19-ChatTerminal | +| Metric | Value | +| --------------- | ---------------------- | +| Total tasks | 14 | +| Completed | 1 | +| In Progress | 0 | +| Remaining | 13 | +| Estimated total | ~215K tokens | +| Used | ~15K tokens | +| Milestone | MS20-SiteStabilization | ## Dependency Graph ``` -PLAN-001 ──┬──→ TERM-001 ──┬──→ TERM-003 ──→ TERM-004 ──→ VER-001 ──→ DOC-001 ──→ VER-002 - │ │ ↑ - │ └──→ ORCH-002 ───────┘ - │ ↑ - ├──→ TERM-002 ────────→ TERM-004 +PLAN-001 ──┬──→ WS-001 ──→ WS-002 ──→ VER-001 ──→ DOC-001 │ - ├──→ CHAT-001 ──┬──→ CHAT-002 ──→ VER-001 - │ │ - │ └──→ ORCH-001 ──→ ORCH-002 + ├──→ WS-003 ──→ VER-001 │ - └──→ CHAT-002 (also depends on CHAT-001) + ├──→ ORCH-001 ──→ ORCH-002 ──→ VER-001 + │ + ├──→ API-001 ──→ UI-002 ──→ VER-001 + │ + ├──→ API-002 ──→ VER-001 + │ + ├──→ UI-001 ──→ VER-001 + │ + ├──→ UI-003 ──→ VER-001 + │ + └──→ UI-004 ──→ VER-001 ``` ## Parallel Execution Opportunities -- **Wave 1** (after PLAN-001): TERM-001 + TERM-002 + CHAT-001 can run in parallel (3 independent tracks) -- **Wave 2**: TERM-003 (after TERM-001) + CHAT-002 (after CHAT-001) + ORCH-001 (after CHAT-001) can overlap -- **Wave 3**: TERM-004 (after TERM-001+002+003) + ORCH-002 (after TERM-001+ORCH-001) -- **Wave 4**: VER-001 (after all implementation) -- **Wave 5**: DOC-001 → VER-002 (sequential) +- **Wave 1** (after PLAN-001): WS-001 + WS-003 + ORCH-001 + API-001 + API-002 + UI-001 + UI-003 + UI-004 (all independent) +- **Wave 2**: WS-002 (after WS-001) + ORCH-002 (after ORCH-001) + UI-002 (after API-001) +- **Wave 3**: VER-001 (after all implementation) +- **Wave 4**: DOC-001 (after verification) diff --git a/docs/scratchpads/ms20-site-stabilization-20260227.md b/docs/scratchpads/ms20-site-stabilization-20260227.md new file mode 100644 index 0000000..a4b95be --- /dev/null +++ b/docs/scratchpads/ms20-site-stabilization-20260227.md @@ -0,0 +1,67 @@ +# Mission Scratchpad — MS20 Site Stabilization + +> Append-only log. NEVER delete entries. NEVER overwrite sections. +> This is the orchestrator's working memory across sessions. + +## Original Mission Prompt + +``` +User tested every aspect of mosaic.woltje.com and found: + +settings/personalities: +- Unable to save new personality +- Dark mode theming on Formality Level dropdown not correct +- Error: Cannot GET /api/personalities?isActive=true + +settings/credentials: +- "Loading credentials" displayed, none populated, unable to add +- favicon.ico 404 +- useWorkspaceId warning in console + +settings/domains: +- Workspace ID is required error + +projects: +- Unable to create new project +- Workspace ID is required error + +Additional: +- Fix Orchestrator 502 +- Fix Orchestrator WebSocket connection +- /users/me/preferences endpoint needs implemented +- #terminal anchor panel toggle needs page route +``` + +## Planning Decisions + +### S1 — 2026-02-27 + +1. **Mission scope**: Stabilization mission covering runtime bugs and feature gaps from live testing. NOT the originally planned MS20-MultiTenant. Bumped MultiTenant to MS21. + +2. **Task categorization**: + - P1 (Critical — blocking core functionality): Workspace context for mutations, orchestrator 502 + - P2 (High — important features): Personalities API, preferences endpoint, credentials UI, terminal route + - P3 (Medium — polish): Dark mode dropdown, favicon, workspace ID warning + +3. **PRD updated**: Added FR-020 (Site Stabilization) with 6 new assumptions. Shifted MS20-MultiTenant to MS21, renumbered subsequent milestones. + +4. **Prior fixes already merged**: + - PR #531: RLS context SQL, workspace guard crash, projects response unwrapping + - PR #532: Widget endpoints workspace context + auto-detect workspace ID + credentials pages + - PR #533: Knowledge entry query DTO — sortBy, sortOrder, search, visibility + +## Session Log + +| Session | Date | Milestone | Tasks Done | Outcome | +| ------- | ---------- | --------- | ---------- | ----------- | +| S1 | 2026-02-27 | MS20 | Planning | In progress | + +## Open Questions + +- Orchestrator 502: Is the orchestrator container actually running? Need to check Docker service status. +- Workspace ID lifecycle: When does the workspace ID first get set in localStorage? Is it during login/auth callback? +- Credentials backend: Do the M7 credential CRUD endpoints still work, or has something changed since? + +## Corrections + + From dd0568cf154009cf2b3ea66ee377a346c30efa00 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 27 Feb 2026 10:28:40 +0000 Subject: [PATCH 08/23] fix(web): add workspace context to domain and project creation (#536) Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- .../(authenticated)/settings/domains/page.tsx | 397 +++++++++++++++++- apps/web/src/lib/api/domains.ts | 26 +- 2 files changed, 406 insertions(+), 17 deletions(-) diff --git a/apps/web/src/app/(authenticated)/settings/domains/page.tsx b/apps/web/src/app/(authenticated)/settings/domains/page.tsx index a945f87..20ca4a7 100644 --- a/apps/web/src/app/(authenticated)/settings/domains/page.tsx +++ b/apps/web/src/app/(authenticated)/settings/domains/page.tsx @@ -1,23 +1,383 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, type SyntheticEvent } from "react"; +import type { ReactElement } from "react"; import type { Domain } from "@mosaic/shared"; import { DomainList } from "@/components/domains/DomainList"; -import { fetchDomains, deleteDomain } from "@/lib/api/domains"; +import { fetchDomains, createDomain, deleteDomain } from "@/lib/api/domains"; +import type { CreateDomainDto } from "@/lib/api/domains"; +import { useWorkspaceId } from "@/lib/hooks"; -export default function DomainsPage(): React.ReactElement { +/* --------------------------------------------------------------------------- + Slug generation helper + --------------------------------------------------------------------------- */ + +function generateSlug(name: string): string { + return name + .toLowerCase() + .trim() + .replace(/[^a-z0-9\s-]/g, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-") + .slice(0, 100); +} + +/* --------------------------------------------------------------------------- + Create Domain Dialog + --------------------------------------------------------------------------- */ + +interface CreateDomainDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onSubmit: (data: CreateDomainDto) => Promise; + isSubmitting: boolean; +} + +function CreateDomainDialog({ + open, + onOpenChange, + onSubmit, + isSubmitting, +}: CreateDomainDialogProps): ReactElement | null { + const [name, setName] = useState(""); + const [slug, setSlug] = useState(""); + const [slugTouched, setSlugTouched] = useState(false); + const [description, setDescription] = useState(""); + const [formError, setFormError] = useState(null); + + function resetForm(): void { + setName(""); + setSlug(""); + setSlugTouched(false); + setDescription(""); + setFormError(null); + } + + function handleNameChange(value: string): void { + setName(value); + if (!slugTouched) { + setSlug(generateSlug(value)); + } + } + + function handleSlugChange(value: string): void { + setSlugTouched(true); + setSlug(value.toLowerCase().replace(/[^a-z0-9-]/g, "")); + } + + async function handleSubmit(e: SyntheticEvent): Promise { + e.preventDefault(); + setFormError(null); + + const trimmedName = name.trim(); + if (!trimmedName) { + setFormError("Domain name is required."); + return; + } + + const trimmedSlug = slug.trim(); + if (!trimmedSlug) { + setFormError("Slug is required."); + return; + } + + if (!/^[a-z0-9-]+$/.test(trimmedSlug)) { + setFormError("Slug must contain only lowercase letters, numbers, and hyphens."); + return; + } + + try { + const payload: CreateDomainDto = { name: trimmedName, slug: trimmedSlug }; + const trimmedDesc = description.trim(); + if (trimmedDesc) { + payload.description = trimmedDesc; + } + await onSubmit(payload); + resetForm(); + } catch (err: unknown) { + setFormError(err instanceof Error ? err.message : "Failed to create domain."); + } + } + + if (!open) return null; + + return ( +
+ {/* Backdrop */} +
{ + if (!isSubmitting) { + resetForm(); + onOpenChange(false); + } + }} + /> + + {/* Dialog */} +
+

+ New Domain +

+

+ Domains help you organize tasks, projects, and events by life area. +

+ +
{ + void handleSubmit(e); + }} + > + {/* Name */} +
+ + { + handleNameChange(e.target.value); + }} + placeholder="e.g. Personal Finance" + maxLength={255} + autoFocus + style={{ + width: "100%", + padding: "8px 12px", + background: "var(--bg, #f9fafb)", + border: "1px solid var(--border, #d1d5db)", + borderRadius: "6px", + color: "var(--text, #111)", + fontSize: "0.9rem", + outline: "none", + boxSizing: "border-box", + }} + /> +
+ + {/* Slug */} +
+ + { + handleSlugChange(e.target.value); + }} + placeholder="e.g. personal-finance" + maxLength={100} + style={{ + width: "100%", + padding: "8px 12px", + background: "var(--bg, #f9fafb)", + border: "1px solid var(--border, #d1d5db)", + borderRadius: "6px", + color: "var(--text, #111)", + fontSize: "0.9rem", + outline: "none", + boxSizing: "border-box", + fontFamily: "var(--mono, monospace)", + }} + /> +

+ Lowercase letters, numbers, and hyphens only. +

+
+ + {/* Description */} +
+ +