From c46dedf2f7bf1e808dac13072d8eb0737906f5ca Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sun, 22 Mar 2026 21:45:14 +0000 Subject: [PATCH 01/73] docs: map existing codebase --- .planning/codebase/ARCHITECTURE.md | 214 ++++++++++++++++ .planning/codebase/CONCERNS.md | 385 +++++++++++++++++++++++++++++ .planning/codebase/CONVENTIONS.md | 253 +++++++++++++++++++ .planning/codebase/INTEGRATIONS.md | 200 +++++++++++++++ .planning/codebase/STACK.md | 177 +++++++++++++ .planning/codebase/STRUCTURE.md | 254 +++++++++++++++++++ .planning/codebase/TESTING.md | 381 ++++++++++++++++++++++++++++ 7 files changed, 1864 insertions(+) create mode 100644 .planning/codebase/ARCHITECTURE.md create mode 100644 .planning/codebase/CONCERNS.md create mode 100644 .planning/codebase/CONVENTIONS.md create mode 100644 .planning/codebase/INTEGRATIONS.md create mode 100644 .planning/codebase/STACK.md create mode 100644 .planning/codebase/STRUCTURE.md create mode 100644 .planning/codebase/TESTING.md diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md new file mode 100644 index 00000000..c422fbea --- /dev/null +++ b/.planning/codebase/ARCHITECTURE.md @@ -0,0 +1,214 @@ +# Architecture + +**Analysis Date:** 2026-03-22 + +## Pattern Overview + +**Overall:** Full-stack Next.js application using tRPC for type-safe client-server communication. Follows a monolithic architecture with clear separation between: +- **Frontend:** Next.js App Router with React components +- **API Layer:** tRPC routers exposing procedures grouped by domain +- **Business Logic:** Server services handling cross-cutting concerns +- **Data Layer:** Prisma ORM with PostgreSQL + +**Key Characteristics:** +- Type-safe end-to-end communication via tRPC (TypeScript definitions shared between client and server) +- Team-scoped authorization middleware (`withTeamAccess`) resolving permissions from database state +- Client-side state management via Zustand for editor state and local UI state +- Server-side state management via Prisma for persistent data +- Node-based visual pipeline editor using @xyflow/react + +## Layers + +**Frontend (Client):** +- Purpose: Next.js pages, React components, forms, visual editors +- Location: `src/app/` (pages), `src/components/` (reusable components) +- Contains: Page components, feature-specific component folders, UI primitives +- Depends on: tRPC client, React Query, Zustand stores, hooks +- Used by: End users via browser + +**API Layer (tRPC Routers):** +- Purpose: Expose typed RPC procedures; handle authentication and authorization +- Location: `src/server/routers/` (one file per domain) +- Contains: `router()` definitions with `protectedProcedure`, `publicProcedure`, input validation (zod), middleware chaining +- Depends on: tRPC init setup, services, middleware, Prisma +- Used by: Frontend client via tRPC client + +**Business Logic (Services):** +- Purpose: Core logic isolated from API layer; reusable across procedures +- Location: `src/server/services/` (utilities, generators, orchestrators) +- Contains: Config generation, validation, encryption, deployment, metrics ingestion, AI processing, audit logging +- Depends on: Prisma, external APIs (Vector GraphQL, AI providers) +- Used by: Routers, other services + +**Middleware (Cross-Cutting):** +- Purpose: Apply concerns to procedures: authorization, audit logging, CSRF +- Location: `src/server/middleware/` +- Contains: Authorization (`withTeamAccess`, `requireRole`, `requireSuperAdmin`), audit logging (`withAudit`) +- Depends on: Prisma, context +- Used by: Routers + +**Data Layer (Prisma):** +- Purpose: Object-relational mapping for PostgreSQL +- Location: `prisma/schema.prisma` (defines models); `src/generated/prisma/` (generated client) +- Contains: Models (User, Team, Pipeline, Environment, VectorNode, etc.), migrations +- Depends on: PostgreSQL +- Used by: All layers + +**Client State (Zustand):** +- Purpose: Non-persistent UI state for editor, selections, UI preferences +- Location: `src/stores/` (flow-store.ts, environment-store.ts, team-store.ts) +- Contains: Node/edge history, undo/redo, clipboard, current selections +- Depends on: React, @xyflow/react +- Used by: Page components and feature components + +**Library/Utilities:** +- Purpose: Shared logic not tied to any domain +- Location: `src/lib/` (config generators, formatters, Vector component schemas) +- Contains: YAML/TOML generation, Vector catalog, VRL function registry, type formatters, logger +- Depends on: No Prisma, no domain routers (stateless) +- Used by: Services, components, any layer + +## Data Flow + +**Pipeline Deployment Flow:** + +1. **UI:** User edits pipeline in visual editor (flow-store manages nodes/edges) +2. **Client:** User clicks "Deploy" → calls `deploy.execute` tRPC procedure with pipelineId +3. **Router:** `deploy.ts:execute` procedure (`withTeamAccess("EDITOR")`) validates team access +4. **Service:** Calls `deployAgent(nodeId, configYaml)` → connects to Vector agent via push registry +5. **Service:** Calls `generateVectorYaml()` → converts node/edge graph to Vector TOML +6. **Service:** Calls `validateConfig(configYaml)` → validates against Vector schema via GraphQL +7. **Audit:** `withAudit` middleware logs deployment request before execution +8. **Response:** Returns status to client; client subscribes to `fleet.getStatusTimeline` for deployment progress +9. **Agent:** Vector agent receives config push via WebSocket (push registry), applies config + +**Data Fetch Flow (Example: Pipelines List):** + +1. **UI:** Page component uses `useQuery(["pipelines"])` from React Query +2. **tRPC Client:** Client intercepts and calls `pipeline.list` tRPC procedure +3. **HTTP:** Request sent to `/api/trpc/[trpc]` with batch link, includes x-trpc-source header (CSRF) +4. **Router:** `pipeline.ts:list` procedure (`withTeamAccess("VIEWER")`) resolves teamId from context +5. **Middleware:** `withTeamAccess` queries database to validate user is team member with VIEWER+ role +6. **Service:** Queries `prisma.pipeline.findMany()` with filters and includes +7. **Response:** serialized via superjson, returned as streamed JSON to client +8. **UI:** React Query caches result, re-renders component with pipelines + +**Authentication & Authorization Flow:** + +1. **Initial:** User submits login form → calls `/api/auth/callback/credentials` (NextAuth) +2. **Session:** NextAuth creates JWT session, stored in secure httpOnly cookie +3. **Per Request:** `createContext` in `trpc/init.ts` reads session via `auth()` (NextAuth) +4. **Per Procedure:** Middleware checks `ctx.session?.user` exists; throws UNAUTHORIZED if missing +5. **Team Check:** `withTeamAccess` middleware parses input to resolve teamId, queries database for membership and role +6. **Execution:** Only executes if user has required role in target team (or is superAdmin) + +**State Management:** + +- **Persistent State:** User, Team, Pipeline, Environment → Prisma → PostgreSQL +- **Session State:** Authentication token → NextAuth JWT in cookie +- **UI State (Transient):** Editor nodes/edges, selections, UI preferences → Zustand stores (memory) +- **Metrics State:** Event samples, pipeline metrics → MetricStore (in-memory cache) + Prisma for persistence +- **Configuration State:** Node configs stored encrypted in database (`config-crypto.ts` handles encryption) + +## Key Abstractions + +**Component Definition:** +- Purpose: Represents a Vector component (source, transform, sink) with schema and metadata +- Examples: `src/lib/vector/types.ts` (`VectorComponentDef`), `src/lib/vector/catalog.ts` +- Pattern: Immutable definitions fetched from Vector's official component catalog, indexed for UI autocomplete + +**Pipeline Graph:** +- Purpose: Represents a directed acyclic graph of Vector components and data flows +- Examples: `src/stores/flow-store.ts` (runtime), `prisma/schema.prisma` (Pipeline + Node + Edge models) +- Pattern: Nodes (components + config), Edges (data flows); supports disabled state, position metadata + +**Team Scope:** +- Purpose: Enforce multi-tenancy — all resources belong to a team; users have role per team +- Examples: `src/trpc/init.ts` (`withTeamAccess`), `prisma/schema.prisma` (Team model) +- Pattern: Middleware resolves teamId from input or entity lookups; queries database for membership; throws FORBIDDEN if not member + +**Config Encryption:** +- Purpose: Encrypt sensitive node configs (API keys, passwords) at rest in database +- Examples: `src/server/services/config-crypto.ts` +- Pattern: Symmetric encryption per component type; keys stored in environment variable; called during deployment + +**Deployment Request:** +- Purpose: Atomic unit of work for pushing config to agents, with versioning and approval workflow +- Examples: `prisma/schema.prisma` (DeployRequest model), `src/server/routers/deploy.ts` +- Pattern: Create DeployRequest, optionally require approval, execute to push agents, track status + +**AI Conversation:** +- Purpose: Chat interface for AI suggestions within pipeline editor or VRL editor +- Examples: `prisma/schema.prisma` (AiConversation, AiMessage), `src/server/routers/ai.ts` +- Pattern: Conversation per pipeline/component; messages contain suggestions; suggestions applied via applier service + +## Entry Points + +**Web UI:** +- Location: `src/app/layout.tsx` (root) → `src/app/(dashboard)/layout.tsx` → feature pages +- Triggers: User navigates in browser; Next.js App Router matches URL to page +- Responsibilities: Render layout, set up providers (theme, auth, tRPC), display dashboard or auth pages + +**API (tRPC):** +- Location: `src/app/api/trpc/[trpc]/route.ts` +- Triggers: POST/GET to `/api/trpc?batch=1&input=...` from tRPC client +- Responsibilities: Deserialize batch request, route to procedure, execute with context, serialize response + +**Agent Enrollment:** +- Location: `src/app/api/agent/enroll/route.ts` +- Triggers: POST from Vector agent with enrollment token +- Responsibilities: Create VectorNode record, generate agent auth token, return config URL + +**Agent Config Push:** +- Location: `src/app/api/agent/config/route.ts` +- Triggers: Vector agent long-polls for config updates +- Responsibilities: Return latest deployment config or null; block until config change + +**Agent Event Ingestion:** +- Location: `src/app/api/agent/samples/route.ts` +- Triggers: POST from agent with metric/event samples +- Responsibilities: Parse samples, store in MetricStore, update PipelineStatus records + +**Webhook Ingestion (External):** +- Location: `src/app/api/fleet/events/route.ts` (agent lifecycle), `src/app/api/webhooks/git/route.ts` (git sync) +- Triggers: External systems POST to these endpoints +- Responsibilities: Validate secret, ingest event, trigger side effects (alerts, deployments) + +## Error Handling + +**Strategy:** Typed error propagation via tRPC; explicit validation at procedure boundaries + +**Patterns:** +- **Input Validation:** Zod schemas in procedure definitions; validation failures throw INVALID_INPUT before execution +- **Authorization Errors:** Middleware throws UNAUTHORIZED (no session) or FORBIDDEN (insufficient role) +- **Not Found:** Service throws NOT_FOUND for missing entities +- **Business Logic Errors:** Service throws BAD_REQUEST with descriptive message +- **Database Errors:** Prisma errors propagate (constraint violations, etc.); optionally caught and converted to tRPC errors +- **External API Errors:** Caught by service, converted to tRPC errors (e.g., Vector validation errors) +- **Client Handling:** tRPC client receives error object; pages display toast or error UI via `react-hot-toast` or `sonner` + +## Cross-Cutting Concerns + +**Logging:** +- Approach: Console logging in development (`src/lib/logger.ts`), structured logs in production +- Audit logging: `withAudit` middleware captures procedure names, user ID, resource IDs; `writeAuditLog` stores to AuditLog table + +**Validation:** +- Procedure input validation via Zod schemas (declarative) +- Config validation against Vector schema via GraphQL query (`vector-graphql.ts`) +- Node config encryption/decryption via `config-crypto.ts` + +**Authentication:** +- NextAuth v5 with JWT session strategy +- Pages redirect unauthenticated users to `/login` +- API procedures require `protectedProcedure` (checks session exists) +- Service endpoints (agent, REST API v1) use Bearer token auth via middleware + +**Team Access Control:** +- `withTeamAccess` middleware resolves teamId and checks user has required role in team +- Super admins bypass team membership checks +- Supports fallback resolution: direct input, parent entity (environment → pipeline), association lookups + +--- + +*Architecture analysis: 2026-03-22* diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md new file mode 100644 index 00000000..65a8e07a --- /dev/null +++ b/.planning/codebase/CONCERNS.md @@ -0,0 +1,385 @@ +# Codebase Concerns + +**Analysis Date:** 2026-03-22 + +## Tech Debt + +### Silent Error Handlers + +**Issue:** 33+ locations silently swallow errors using `.catch(() => {})` pattern. + +Files with patterns: +- `src/auth.ts` - Lines 91, 98, 110, 144, 152, 229, 237, 308, 341 (9 instances in auth flow) +- `src/app/api/ai/vrl-chat/route.ts` - Line 215 +- `src/app/api/ai/pipeline/route.ts` - Line 229 +- `src/server/services/validator.ts` - Line 55 +- `src/server/services/backup.ts` - Lines 283-284 (file cleanup) +- `src/server/services/version-check.ts` - Line 147 +- `src/server/services/git-sync.ts` - Lines 113, 166 +- `src/app/api/v1/alerts/rules/route.ts` - Line 122 +- `src/app/api/v1/pipelines/[id]/rollback/route.ts` - Line 64 +- `src/app/api/v1/pipelines/[id]/undeploy/route.ts` - Line 43 +- `src/server/services/backup-scheduler.ts` - Line 61 +- `src/app/api/v1/secrets/route.ts` - Lines 83, 155 + +**Impact:** Failures in critical paths (auth logging, backup, git sync, AI requests) are completely hidden. Difficult to diagnose production issues. Audit log writes fail silently. + +**Fix approach:** Replace with proper logging and selective error isolation. Only use silent catch for cleanup operations (file deletion). For application logic, log errors with context even if they're non-blocking. + +### Type Safety Issues (Any Types) + +**Issue:** Auth module caches NextAuth instance using `any` types. + +Files: +- `src/auth.ts` - Lines 168-169 + - `type AuthInstance = { handlers: any; auth: any; signIn: any; signOut: any }` + - Lines 376, 382, 388 (additional eslint-disable comments) + +**Impact:** Type-checking disabled in critical auth infrastructure. Difficult to catch auth-related bugs at compile time. + +**Fix approach:** Create proper TypeScript types for NextAuth handlers instead of using `any`. Reference NextAuth's own types. + +### Unresolved TODOs + +**Certificate Expiration Monitoring (Certificate_Expiring Alert)** + +File: `src/server/services/event-alerts.ts` - Lines 117-121 + +**Issue:** Certificate expiry alert is defined in the alert system but never implemented. Certificates are stored as encrypted PEM blobs without parsed expiry metadata. No periodic job checks for certificate expiration. + +**Impact:** Users can set up certificate_expiring alert rules but they will never fire. Certificates can silently expire in production. No way to detect certificate approaching expiration until it fails. + +**Fix approach:** +1. Add `expiryDate` field to Certificate model (parsed from PEM) +2. Create periodic job (hourly) that queries certificates and fires event alert when within N days of expiration +3. Parse PEM notAfter date when certificate is created/updated +4. Update alert documentation to clarify this alert now works + +## Known Bugs + +### Concurrent Audit Log Write Suppression + +**Issue:** Audit logging operations are awaited with silent failure handling. + +File: `src/auth.ts` - Multiple locations +- Failed logins/successful logins write audit logs with `.catch(() => {})` +- User lock events, TOTP failures all silently fail to log + +**Trigger:** Write audit log during auth flow → logging service error (DB connection, permissions) → silently ignored + +**Workaround:** None. Audit trail is incomplete when logging fails, but user login still succeeds. + +**Fix:** Queue audit logs with retry logic instead of fire-and-forget. + +### VRL Chat Conversation Isolation Gap + +**Issue:** Conversation persistence creates a new conversation every time if conversationId not provided, but there's no validation that conversation permissions still match the pipeline. + +File: `src/app/api/ai/vrl-chat/route.ts` - Lines 86-100 + +**Trigger:** User A creates conversation on pipeline P → conversation persisted to DB with userId A. User B (same team) reuses old conversationId → can read User A's VRL drafts and chat history. + +**Risk:** Cross-user conversation history leakage if conversationId is predictable or leaked. + +**Fix:** When resuming conversation, re-validate user's access to the pipeline and add permission check on conversation load. + +## Security Considerations + +### Silent JSON.parse in Multiple Locations + +**Issue:** Untrusted JSON parsing without try-catch in user-facing code: + +Files: +- `src/app/(dashboard)/alerts/page.tsx` - Lines 693, 889, 1460 (parsing webhook headers from form) +- `src/stores/flow-store.ts` - Line 715 (parsing clipboard data) +- `src/server/routers/settings.ts` - Line 73 (parsing OIDC team mappings) +- `src/server/routers/vrl.ts` - Lines 31, 74 (parsing VRL input/output) +- `src/server/routers/user.ts` - Line 241 (parsing backup codes) +- `src/server/services/group-mappings.ts` - Line 24 + +**Current State:** Most have try-catch wrapping, but not all. Headers parsing in alerts page is particularly risky. + +**Risk:** Malformed JSON in webhook headers → unhandled parse error → 500 error exposing stack trace + +**Recommendation:** Wrap all JSON.parse in try-catch. Use Zod for schema validation after parsing. + +### Direct Environment Variable Access (30+ locations) + +**Issue:** Code directly reads `process.env.*` throughout codebase rather than using centralized config. + +**Impact:** Difficult to audit which env vars are required, difficult to validate at startup, impossible to mock in tests. + +**Recommendation:** Create `src/lib/config.ts` that validates all required env vars at startup using Zod, exports typed config object. + +### Command Execution in Backup and Git Services + +Files with potential injection risks: +- `src/server/services/backup.ts` - Uses `execFile` with parsed DATABASE_URL components (safer pattern) +- `src/server/services/git-sync.ts` - May spawn git commands + +**Current State:** Database backup uses safe `execFile` with separate args array (not shell-injectable). Git operations need review. + +**Recommendation:** Audit git-sync operations for injection. Use `execFile` (args array) never `exec` (shell string). + +### Webhook Secret Validation + +File: `src/app/api/webhooks/git/route.ts` - Line 51 + +**Issue:** Git webhook signature validation exists but may have timing attack vulnerability if using simple string comparison. + +**Recommendation:** Verify webhook signature uses timing-safe comparison. + +## Performance Bottlenecks + +### Large File Heartbeat Processing (Pipeline metrics ingest) + +File: `src/app/api/agent/heartbeat/route.ts` - ~600 lines + +**Issue:** Single large endpoint handles agent authentication, pipeline validation, metrics ingest, log ingest, alert evaluation, and webhook delivery. No pagination for metrics. + +**Current State:** Works in-process synchronously. + +**Scaling concern:** At scale, heartbeat endpoint becomes bottleneck. 1000 agents × 1000 metrics/agent = 1M metric writes per heartbeat interval. + +**Improvement path:** +1. Move metric ingest to background queue (Bull/RabbitMQ) +2. Move alert evaluation to separate service with caching +3. Batch metric writes into PipelineMetric inserts + +### Database Indices for Time-Series Queries + +File: `prisma/schema.prisma` + +**Indices present:** +- PipelineMetric: `@@index([pipelineId, timestamp])` +- PipelineLog: `@@index([pipelineId, timestamp])` +- NodeMetric: `@@index([nodeId, timestamp])` + +**Gap:** Missing backward time-range queries. When querying "last 24 hours of metrics for pipeline P" without filtering by componentId, needs to scan potentially millions of rows. + +**Recommendation:** Add `@@index([pipelineId, timestamp desc])` for reverse chronological queries, or ensure query planner uses existing indices efficiently. + +### Memory Usage of Flow Store + +File: `src/stores/flow-store.ts` - 951 lines, manages entire pipeline as Zustand state + +**Issue:** Large pipelines (100+ nodes, 500+ edges) held entirely in memory with full undo/redo history. MAX_HISTORY = 50 snapshots. + +**At scale:** Each snapshot can be 100KB+, 50 snapshots = 5MB per pipeline in browser memory. + +**Impact:** Slow UI for large pipelines, memory pressure on embedded devices. + +**Recommendation:** Implement pagination/virtualization in flow canvas. Consider server-side undo/redo instead of client-side. + +## Fragile Areas + +### Migration History (69 migrations) + +File: `prisma/migrations/` directory + +**Risk:** 69 migrations suggests evolving schema. Key concerns: +- User model evolved from single auth method → OIDC + local with complex fallback logic +- Alert system evolved multiple times (multiple migration timestamps suggest schema churn) +- GitOps features added piecemeal (git fields scattered across Environment model) + +**Safe modification:** When adding new fields, ensure they have defaults or are nullable. Test migration against production-like data volume. + +### Pipeline Version Snapshots (New Feature) + +File: `prisma/schema.prisma` mentions PipelineVersion with snapshots + +**Risk:** Version snapshots may have incomplete historical data if feature recently added. Old pipelines may lack version snapshots. + +**Safe modification:** Ensure queries handle null snapshots gracefully. Add migration to backfill snapshots for existing pipelines. + +### Event-Based Alerts System + +Files: +- `src/server/services/event-alerts.ts` +- `src/app/(dashboard)/alerts/page.tsx` (1910 lines, large component) + +**Risk:** Alerts page is a mega-component with form state management for 6+ alert channel types. Heavy reliance on local form state leads to fragility when integrating new channel types. + +**What breaks easily:** +- Adding new alert channel type requires changes in multiple places +- Form validation logic not extracted, spread across component +- Webhook header parsing in form component, not isolated + +**Safe modification:** Extract alert form logic into separate composition. Create AlertChannelForm abstraction for each channel type. Test each channel type's form independently. + +### Configuration Encryption/Decryption + +Files involved: +- `src/server/services/config-crypto.ts` - Node config encryption +- `src/server/services/crypto.ts` - System-wide encryption (OIDC secrets, git tokens, TOTP) + +**Risk:** Two separate crypto modules with potentially different implementations. No clear separation of concerns between config-level and secret-level encryption. + +**Safe modification:** Ensure all encrypted fields are tracked in schema. Test encryption key rotation scenario. Document which fields are encrypted and where. + +## Scaling Limits + +### PostgreSQL Query Complexity (Heartbeat Endpoint) + +**Current capacity:** ~100 agents, ~10 pipelines each, ~100 metrics per heartbeat + +**Limit:** Query validates all pipeline ownership in memory. At 10,000 pipelines, this becomes slow. + +```typescript +// src/app/api/agent/heartbeat/route.ts line 141-145 +const validPipelineIds = new Set( + (await prisma.pipeline.findMany({ + where: { environmentId: agent.environmentId }, + select: { id: true }, + })).map((p) => p.id), +); +``` + +**Scaling path:** Cache valid pipeline IDs for environment. Invalidate on pipeline create/delete. + +### Alert Rule Querying (Event Alerts) + +File: `src/server/services/event-alerts.ts` - Line 36 + +**Issue:** Every event fires a `fireEventAlert()` call that queries ALL matching alert rules for that environment + metric combination. + +**Current:** Works fine for <100 rules per environment + +**Limit:** At 1000+ alert rules per environment, each event causes expensive query + +**Scaling path:** +1. Denormalize active rules by (environmentId, metric) into cache +2. Or use PostgreSQL LISTEN/NOTIFY for alert subscriptions +3. Or pre-compute alert eligibility during metric ingest + +### Team-scoped Querying + +Multiple routers use `withTeamAccess()` middleware which requires loading team membership for every query. + +**Recommendation:** Add team context to session to avoid repeated lookups. + +## Dependencies at Risk + +### Next.js Crypto Utilities + +**Risk:** Using Node.js crypto for password hashing (bcrypt) and encryption (crypto.ts). These are stable but require careful key management. + +**Current state:** Uses `crypto.subtle` for AES-256-GCM encryption in `src/server/services/crypto.ts`. + +**Mitigation:** Key rotation not documented. If key is compromised, all encrypted secrets are at risk. + +**Migration plan:** Consider AWS KMS or HashiCorp Vault for key management in production. + +### Prisma Client Generation (69 MB generated code) + +File: `src/generated/prisma/` + +**Risk:** Large generated types can cause slow TypeScript compilation if schema changes frequently + +**Mitigation:** Already committed to repo, not ideal but works + +**Recommendation:** Consider using Prisma Accelerate for query optimization layer. + +## Missing Critical Features + +### Backup Restoration Incomplete + +File: `src/server/services/backup.ts` + +**Issue:** `createBackup()` is implemented but `restoreBackup()` is partially implemented. No test for restoration. + +**Impact:** Users can create backups but restoration path may fail in production. No recovery from catastrophic data loss. + +**Blocks:** Disaster recovery testing, backup strategy validation + +### Certificate Management Gaps + +- No automatic certificate renewal +- No expiry notifications (as noted in TODO) +- No certificate rotation orchestration +- No CRL/OCSP support + +**Blocks:** Long-term production deployment without manual intervention + +### Git Sync Bidirectional Not Fully Tested + +File: `src/server/services/git-sync.ts` + +**Issue:** gitOpsMode can be "off" | "push" | "bidirectional" but bidirectional sync logic may be incomplete + +**Recommendation:** Add integration tests for bidirectional sync conflicts. + +## Test Coverage Gaps + +### Auth Module + +File: `src/auth.ts` - Complex credential provider with TOTP, backup codes, OIDC fallback + +**Not tested:** +- TOTP verification edge cases (clock skew, code reuse) +- Backup code consumption atomicity +- OIDC group sync and role mapping +- Account locking logic +- Email verification (if applicable) + +**Risk:** Password resets, 2FA, SSO integration failures won't be caught until production + +**Priority:** High - affects all user access + +### Event Alerts and Webhook Delivery + +Files: +- `src/server/services/event-alerts.ts` +- `src/server/services/webhook-delivery.ts` +- `src/server/services/channels/index.ts` + +**Not tested:** +- Alert rule matching logic (scoping to pipeline, environment) +- Webhook delivery retry logic +- Notification channel delivery (Slack, email, PagerDuty) +- Certificate expiry detection (when implemented) + +**Risk:** Users configure alerts that never fire, or alerts fire for wrong pipelines + +**Priority:** High - core monitoring feature + +### Configuration Encryption + +File: `src/server/services/crypto.ts` + +**Not tested:** +- Key rotation scenario +- Decryption of old-format encrypted values +- IV (initialization vector) handling + +**Risk:** Encrypted configs become unreadable after key rotation or migration + +**Priority:** Medium - impacts secret management + +### Large Component Behavior + +Files: +- `src/app/(dashboard)/alerts/page.tsx` (1910 lines) +- `src/app/(dashboard)/settings/_components/team-settings.tsx` (865 lines) + +**Not tested:** Form submission edge cases, validation, concurrent updates + +**Priority:** Medium - causes UI bugs + +### Backup and Restore Flow + +File: `src/server/services/backup.ts` + +**Not tested:** +- Actual backup creation and compression +- Restore from backup +- Backup with large databases (>1GB) +- Concurrent backup attempts (handled with mutex but not tested) + +**Risk:** Backups created but unrecoverable, no warning before restore + +**Priority:** High - affects data safety + +--- + +*Concerns audit: 2026-03-22* diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md new file mode 100644 index 00000000..06b17fbc --- /dev/null +++ b/.planning/codebase/CONVENTIONS.md @@ -0,0 +1,253 @@ +# Coding Conventions + +**Analysis Date:** 2026-03-22 + +## Naming Patterns + +**Files:** +- Components: `kebab-case.tsx` (e.g., `status-timeline.tsx`, `field-renderer.tsx`) +- Services/utilities: `kebab-case.ts` (e.g., `agent-auth.ts`, `git-sync.ts`, `config-crypto.ts`) +- Hooks: `use-kebab-case.ts` (e.g., `use-fleet-events.ts`, `use-ai-conversation.ts`, `use-mobile.ts`) +- Stores: `kebab-case.ts` (e.g., `team-store.ts`, `environment-store.ts`) +- Routers: `kebab-case.ts` (e.g., `fleet.ts`, `user.ts`, `secret.ts`) +- Index files (barrel exports): `index.ts` for grouping related exports +- Schema/type definition files: `kebab-case.ts` (e.g., `node-types.ts`, `source-output-schemas.ts`) + +**Functions and Variables:** +- camelCase for all function declarations and variables (e.g., `generateId()`, `formatTime()`, `validateConfig()`) +- Single-word verbs preferred for action functions: `fetch`, `validate`, `sync`, `commit` +- Compound names use full context: `getStatusTimeline()`, `gitSyncCommitPipeline()`, `parseVectorErrors()` +- Helper functions prefixed with context when in shared modules: `toTitleCase()`, `toFilenameSlug()`, `isMultilineName()` +- Boolean predicates start with `is` or `has`: `isClean()`, `hasTeamAccess()`, `isMultilineName()` +- TRPC procedures named as verbs: `.query()`, `.mutation()`, `.list`, `.get`, `.getStatusTimeline` + +**Types and Interfaces:** +- PascalCase for all types, interfaces, and enums +- Component prop interfaces suffix with `Props` (e.g., `StatusTimelineProps`, `FieldRendererProps`) +- Use `interface` for object shapes, `type` for unions and type aliases +- Database model names match Prisma schema (e.g., `VectorNode`, `User`, `Team`) +- Derived interface names combine entity + purpose (e.g., `VectorComponentMetrics`, `VectorHealthResult`, `GitSyncConfig`) +- Exported types prefixed with `export` at declaration + +**Constants:** +- Local constants: camelCase (e.g., `rangeMs`, `tmpDir`, `tmpFile`) +- Maps and lookup tables: camelCase with semantic naming (e.g., `STATUS_COLORS`, `rangeMs`) +- Environment variables referenced as constants: SCREAMING_SNAKE_CASE (e.g., `NEXTAUTH_SECRET`) + +## Code Style + +**Formatting:** +- Tool: ESLint 9 with Next.js core web vitals config +- Config file: `eslint.config.mjs` +- Prettier is not explicitly configured in this codebase; ESLint handles linting +- Line length: No explicit limit enforced; files follow Next.js conventions + +**Linting:** +- Config: `eslint.config.mjs` extends `eslint-config-next/core-web-vitals` and `eslint-config-next/typescript` +- Global ignores: `.next/`, `out/`, `build/`, `next-env.d.ts`, `src/generated/**` +- Run linting: `pnpm lint` +- Next.js ESLint rules enforce core web vitals and React best practices + +**Indentation and Spacing:** +- 2 spaces (inferred from project style) +- No trailing commas in object literals (not enforced but observed) +- Empty lines between logical sections in files (see `field-renderer.tsx` pattern) + +## Import Organization + +**Order:** +1. External third-party packages (React, Next.js, libraries) +2. Internal server packages (`@trpc`, `@prisma`) +3. Internal utilities and types from `@/` alias +4. Local relative imports (rare; mostly avoided in favor of absolute paths) + +**Pattern:** +```typescript +import { useState, useRef, useCallback } from "react"; +import { useQuery, useMutation } from "@tanstack/react-query"; +import { TRPCError } from "@trpc/server"; +import { useTRPC } from "@/trpc/client"; +import { prisma } from "@/lib/prisma"; +import { validateConfig } from "@/server/services/validator"; +import { SecretPickerInput } from "./secret-picker-input"; +``` + +**Path Aliases:** +- `@/*` resolves to `src/*` (defined in `tsconfig.json`) +- Always use `@/` prefix for imports from `src/` directory +- No relative `../` paths in most codebases; use absolute `@/` paths + +**Grouped Imports:** +- Group by source (external, internal by layer) +- Destructure multiple items from same module +- Default imports on separate line from named imports + +## Error Handling + +**Patterns:** +- TRPC: Throw `TRPCError` with code and message (e.g., `NOT_FOUND`, `BAD_REQUEST`) + ```typescript + if (!node) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Node not found", + }); + } + ``` +- Services: Use try/catch with typed error handling + ```typescript + try { + // operation + } catch (err: unknown) { + const execErr = err as NodeJS.ErrnoException & { stdout?: string }; + // Handle specific error types + } finally { + // cleanup + } + ``` +- GraphQL queries: Catch-all with boolean return (e.g., `queryHealth()` returns `{ healthy: false }` on error) +- Client-side (React): Use state for error tracking, set via try/catch in async operations + ```typescript + const [error, setError] = useState(null); + try { + // async operation + } catch (e) { + setError(String(e)); + } + ``` +- Environment validation: Throw `Error` if critical env var missing + ```typescript + if (!secret) { + throw new Error("NEXTAUTH_SECRET environment variable is required but not set"); + } + ``` + +## Logging + +**Framework:** `console` (browser DevTools for client, Node console for server) + +**Patterns:** +- No explicit logging library detected; uses browser/Node native console +- Implicit logging through error handling and TRPC error messages +- Audit trail via `withAudit` middleware for sensitive operations (e.g., `user.password_changed`) + +**When to Log:** +- Server-side: Errors and exceptional conditions only (via TRPC or error response) +- Client-side: Debugging only; production code relies on error states +- Audit: Use `withAudit` middleware for security-sensitive mutations + +## Comments + +**When to Comment:** +- Describe *why*, not *what* the code does +- Use for non-obvious logic or business rules +- Mark workarounds and fallbacks explicitly + +**JSDoc/TSDoc:** +- Used for exported functions and public APIs +- Format: Single-line comments for simple explanations + ```typescript + /** Generate a UUID, with fallback for non-secure (HTTP) contexts. */ + export function generateId(): string { ... } + ``` +- Multiline comments for complex functions + ```typescript + /** + * Validate a Vector YAML config using the `vector validate` CLI. + * The Vector binary must be available (it is embedded in the server Docker image). + */ + export async function validateConfig(yamlContent: string): Promise { ... } + ``` +- Comments within functions use `//` for inline explanations + +**Section Markers:** +- Use ASCII comment headers to separate major sections + ```typescript + /* ------------------------------------------------------------------ */ + /* Types */ + /* ------------------------------------------------------------------ */ + ``` + +## Function Design + +**Size:** +- Prefer functions under 50 lines +- Helper functions extracted when logic repeats or when section separators emerge +- Large mutations split into setup, mutation, error handling, and cleanup phases + +**Parameters:** +- Destructure object parameters when multiple are needed +- Options objects for 3+ parameters + ```typescript + export interface UseAiConversationOptions { + pipelineId: string; + currentYaml?: string; + environmentName?: string; + } + export function useAiConversation(options: UseAiConversationOptions) { ... } + ``` + +**Return Values:** +- Explicit return types on all exported functions +- Void for operations with only side effects +- Objects with clear shape when returning multiple values +- TRPC procedures return serializable data (Prisma models, JSON objects) +- GraphQL query functions return typed interfaces defined at top of module + +**Async/Await:** +- Preferred over `.then()` chains +- Used consistently in services and hooks +- Server-side (TRPC): All async operations awaited before returning + +## Module Design + +**Exports:** +- Named exports preferred for utilities and hooks +- Default export for React components +- Barrel files (`index.ts`) group related exports + ```typescript + export { generateVectorYaml } from "./yaml-generator"; + export { generateVectorToml } from "./toml-generator"; + export { importVectorConfig, type ImportResult } from "./importer"; + ``` + +**Barrel Files:** +- Located in `src/lib/config-generator/index.ts` to re-export submodules +- Used for public APIs to reduce import depth +- Not used for `.../src/components/ui` (each component imported directly) + +**Layers:** +- `src/lib/` — Utilities, helpers, types, integrations (database, crypto, validation) +- `src/server/` — TRPC routers, middleware, services, database access +- `src/components/` — React components (UI, feature, domain-specific) +- `src/hooks/` — Custom React hooks with state and side effects +- `src/stores/` — Zustand stores for global state +- `src/trpc/` — TRPC client setup and type exports +- `src/app/` — Next.js pages and routes + +**Conventions by Directory:** +- Services (`src/server/services/`) export pure functions, no default exports +- Routers (`src/server/routers/`) define router objects with procedures +- Stores (Zustand) export store hooks via `export const useXStore = create(...)` +- React components export function components with `export function XYZ()` or default +- Utilities in `src/lib/` group by domain (config-generator, vector, vrl, ai) + +## Type Safety + +**TypeScript Configuration:** +- Target: ES2017 +- Strict mode: Enabled +- JSX: React 19 with JSX runtime +- Module resolution: Bundler (Next.js) +- Path alias: `@/*` → `src/*` + +**Patterns:** +- Use `unknown` for caught errors, then cast/guard to specific type +- Type guards for discriminated unions (e.g., status checks) +- Optional chaining (`?.`) and nullish coalescing (`??`) preferred over ternaries for null checks +- Explicit `null` vs `undefined` — use null for intentional absence, undefined for unset values +- Export type declarations alongside value exports where relevant + +--- + +*Convention analysis: 2026-03-22* diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md new file mode 100644 index 00000000..b73dddc3 --- /dev/null +++ b/.planning/codebase/INTEGRATIONS.md @@ -0,0 +1,200 @@ +# External Integrations + +**Analysis Date:** 2026-03-22 + +## APIs & External Services + +**AI Providers:** +- OpenAI (default) - VRL and pipeline suggestions + - SDK/Client: Native OpenAI-compatible HTTP client + - Configuration: Stored in `Team.aiProvider`, `Team.aiBaseUrl`, `Team.aiModel` + - API Key: Encrypted in `Team.aiApiKey` (via `crypto.ts`) + - Default models: `gpt-4o` (OpenAI), `claude-sonnet-4-20250514` (Anthropic) + - Base URLs: `https://api.openai.com/v1`, `https://api.anthropic.com/v1` + - Custom providers: OpenAI-compatible APIs supported via `Team.aiBaseUrl` + - Implementation: `src/server/services/ai.ts`, `src/lib/ai/rate-limiter.ts` + +**Notification Channels:** +- Slack - Alert delivery via webhooks + - Endpoint: Webhook URL (stored in `NotificationChannel.config`) + - Format: Block Kit format with status emoji, metrics, dashboard link + - Implementation: `src/server/services/channels/slack.ts` + +- PagerDuty - Incident management integration + - Endpoint: `https://events.pagerduty.com/v2/enqueue` + - Configuration: Integration key in `NotificationChannel.config` + - Dedup key: `vectorflow-{alertId}` for incident correlation + - Severity mapping: Configurable in channel config + - Implementation: `src/server/services/channels/pagerduty.ts` + +- Email - SMTP-based alert delivery + - SDK/Client: `nodemailer@8.0.1` + - Configuration: SMTP host, port, auth in `NotificationChannel.config` + - SMTP host validation: Via `validateSmtpHost()` to prevent internal network access + - HTML templates: Styled email notifications with alert details + - Implementation: `src/server/services/channels/email.ts` + +- Generic Webhooks - Custom HTTP endpoints + - Endpoint: Configurable URL (stored in config) + - Authentication: Optional HMAC-SHA256 signature in `X-VectorFlow-Signature` header + - Payload: JSON with alert details, metrics, and dashboard link + - URL validation: Prevents internal/private network access + - Timeout: 10 second per webhook + - Implementation: `src/server/services/channels/webhook.ts` + +**Vector Data Pipeline:** +- Vector - Local telemetry pipeline engine + - Binary: Spawned as child process (configurable path via `VF_VECTOR_BIN`) + - Configuration: YAML format written to disk at runtime + - Data directory: `.vectorflow/vector-data/` + - System pipeline: User-defined sources, transforms, sinks + - Audit log source: Automatically injected with runtime path + - Implementation: `src/server/services/system-vector.ts` + - Component schemas: `src/lib/vector/schemas/` (sources, sinks, transforms) + - Catalog: `src/lib/vector/catalog.ts` (component metadata) + +## Data Storage + +**Databases:** +- PostgreSQL 12+ (primary data store) + - Connection: Via `process.env.DATABASE_URL` + - Adapter: `@prisma/adapter-pg@7.4.2` (Prisma driver) + - ORM: Prisma 7.4.2 + - Schema: `prisma/schema.prisma` + - Models: User, Team, Pipeline, Environment, AiConversation, NotificationChannel, AlertRule, etc. + - Migrations: Version-controlled in `prisma/migrations/` + - Features: Multi-tenancy (Team-scoped), audit logging + +**File Storage:** +- Local filesystem only + - Backup directory: `process.env.VF_BACKUP_DIR` (default: `/backups`) + - System config: `process.env.VF_SYSTEM_CONFIG_PATH` (default: `.vectorflow/system-pipeline.yaml`) + - Audit logs: `process.env.VF_AUDIT_LOG_PATH` (default: `/var/lib/vectorflow/audit.log`) + - Vector data: `.vectorflow/vector-data/` + - No cloud storage (S3, GCS, etc.) integrated + +**Caching:** +- In-memory metric store - No external cache required + - Implementation: `src/server/services/metric-store.ts` + - TanStack React Query - Client-side data caching + - Global singleton pattern to prevent duplication + +## Authentication & Identity + +**Auth Provider:** +- NextAuth 5.0.0-beta.30 (custom implementation) + - Configuration: `src/auth.config.ts`, `src/auth.ts` + - Approach: Hybrid local + OIDC + - Session: JWT-based (`session.strategy: "jwt"`) + - Adapter: Prisma for user/account persistence + +**Local Authentication:** +- Credentials provider - Email + password + - Password hashing: bcryptjs with salt + - 2FA: TOTP/OTP with backup codes + - Account lockout: User lockout tracking + - Audit: Login attempts logged + +**SSO / OIDC:** +- OpenID Connect - Optional SSO configuration + - Settings: Stored in `SystemSettings` model + - Configuration keys: `oidcIssuer`, `oidcClientId`, `oidcClientSecret`, `oidcDisplayName` + - Token endpoint: Configurable auth method (default: `client_secret_post`) + - Group sync: Optional OIDC group mapping to VectorFlow teams + - Scopes: Configurable (`oidcGroupsScope`), defaults to "groups" + - Claims: Configurable claim name for groups (default: "groups") + - Implementation: `src/auth.ts` (lines 35-65) + +**Service Accounts:** +- Token-based API auth for external integrations + - Token storage: `ServiceAccount` model in database + - REST API v1 - Bearer token authentication at `src/app/api/v1/*` + - Agent API - Enrollment tokens at `src/app/api/agent/*` + +**SCIM API:** +- System for Cross-domain Identity Management + - Endpoint: `/api/scim/v2/*` + - Bearer token authentication + - Group provisioning: Group and User management + - Implementation: `src/app/api/scim/v2/` + - Sync: One-way SCIM-to-VectorFlow group provisioning + +## Monitoring & Observability + +**Error Tracking:** +- None detected - No external error tracking service (Sentry, DataDog, etc.) +- Custom implementation: Error logging to stdout/stderr + +**Logs:** +- Local file-based logging + - Level: `process.env.VF_LOG_LEVEL` (default: "info") + - Audit logs: File-based audit trail at `VF_AUDIT_LOG_PATH` + - Destination: Console + audit log file + - Implementation: `src/lib/logger.ts` + +**Metrics:** +- In-memory metric store with periodic cleanup + - Sources: Vector pipeline metrics via `/api/metrics` endpoints + - Storage: In-memory cache (not persisted) + - Cleanup: Scheduled via `metrics-cleanup.ts` + - Implementation: `src/server/services/metric-store.ts` + +## CI/CD & Deployment + +**Hosting:** +- Self-hosted / Docker + - Build output: Standalone Next.js (no Node.js dependency in output) + - Database: External PostgreSQL required + - Runtime: Node.js LTS + +**CI Pipeline:** +- Not detected in codebase - Deployment config external +- PR checks: Greptile code review (GitHub integration) + +## Environment Configuration + +**Required env vars:** +- `DATABASE_URL` - PostgreSQL connection string +- `NEXTAUTH_SECRET` - JWT signing secret (minimum 32 bytes) +- `NEXTAUTH_URL` - Public app URL (for callback URLs) + +**Secrets location:** +- `.env` file (not committed, in `.gitignore`) +- Encrypted in database: AI API keys, OIDC client secrets, Git PAT +- Encryption: Via `crypto.ts` using `NEXTAUTH_SECRET` + +## Webhooks & Callbacks + +**Incoming:** +- Health check endpoint: `GET /api/health` +- Agent heartbeat: `POST /api/agent/heartbeat` +- Webhooks for pipeline deployments: Stored in `Environment` model +- Git push webhooks: Can trigger GitSync on pipeline commits + +**Outgoing:** +- Slack webhooks - Alert notifications +- PagerDuty Events API - Incident management +- Email via SMTP - Alert delivery +- Generic webhooks - Custom alert destinations +- Git push - GitSync commits to repository + - Supports: GitHub, GitLab, Bitbucket + - Authentication: Personal access token (encrypted) + - Implementation: `src/server/services/git-sync.ts` + +**Agent Communication:** +- Agent-to-Dashboard: Heartbeat HTTP POST +- Dashboard-to-Agent: Config HTTP GET, metrics polling +- Enrollment tokens: Secure agent onboarding +- Implementation: `src/app/api/agent/config/route.ts`, `src/app/api/agent/heartbeat/route.ts` + +## External Data Sources + +**Vector Integration Points:** +- Sources: Kubernetes, Docker, files, syslog, HTTP, Datadog, CloudWatch, etc. +- Sinks: AWS (S3, CloudWatch, Kinesis), Azure, GCP, Datadog, New Relic, Splunk, etc. +- Authentication: Per-sink configuration (API keys, managed credentials) +- Schema validation: `src/lib/vector/source-output-schemas.ts`, component schemas + +--- + +*Integration audit: 2026-03-22* diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md new file mode 100644 index 00000000..42cd50fd --- /dev/null +++ b/.planning/codebase/STACK.md @@ -0,0 +1,177 @@ +# Technology Stack + +**Analysis Date:** 2026-03-22 + +## Languages + +**Primary:** +- TypeScript 5 - All source code (`src/**/*.ts`, `src/**/*.tsx`) +- JavaScript (React 19) - Frontend UI and components +- YAML - Vector pipeline configuration (`.vectorflow/system-pipeline.yaml`) +- SQL - Database schema managed by Prisma + +**Secondary:** +- Bash - Scripts and CLI tools +- JSON - Configuration and data files + +## Runtime + +**Environment:** +- Node.js (version not pinned, uses packageManager spec in package.json) +- Next.js 16.1.6 - Full-stack framework (SSR, API routes, standalone build) +- React 19.2.3 - UI component framework +- React DOM 19.2.3 - DOM rendering + +**Package Manager:** +- pnpm 10.13.1 - Dependency management +- Lockfile: `pnpm-lock.yaml` (tracked in repo, use `pnpm install`) +- Override rules: `hono>=4.11.10`, `lodash>=4.17.23` + +## Frameworks + +**Core:** +- Next.js 16.1.6 - Full-stack React framework with API routes + - Configuration: `next.config.ts` + - Standalone build output enabled + - Server actions: 2mb size limit + - Edge Runtime incompatibility: Uses Node.js-only modules in auth and database layers +- React 19.2.3 - UI component library +- TailwindCSS 4 - Utility-first CSS framework + - Configuration: Integrated in build, no separate tailwind.config (uses @tailwindcss/postcss) + - PostCSS: `postcss.config.mjs` + +**Backend & Data:** +- Prisma 7.4.2 - ORM for PostgreSQL + - Configuration: `prisma/schema.prisma` + - Client: `@prisma/client@7.4.2` with `@prisma/adapter-pg@7.4.2` for PostgreSQL + - Schema location: `prisma/schema.prisma` + - Migrations: `prisma/migrations/` + - Generated client: `src/generated/prisma/` + - Post-install: `prisma generate` (runs automatically) + +**API & RPC:** +- tRPC 11.8.0 - End-to-end typesafe API + - Client: `@trpc/client@11.8.0` (React hooks) + - Server: `@trpc/server@11.8.0` + - React integration: `@trpc/tanstack-react-query@11.8.0` + - Server routers: `src/server/routers/` + - Client setup: `src/trpc/client.tsx` +- REST API v1 - Bearer token authentication at `src/app/api/v1/` + +**Authentication & Authorization:** +- NextAuth 5.0.0-beta.30 - Authentication + - Configuration: `src/auth.config.ts`, `src/auth.ts` + - Adapter: `@auth/prisma-adapter@2.11.1` for database sessions + - Providers: Credentials (local), OIDC (configurable) + - Session strategy: JWT + - Features: 2FA/TOTP, password hashing (bcryptjs), account lockout +- bcryptjs 3.0.3 - Password hashing + +**Testing & Development:** +- ESLint 9 - Linting with Next.js config + - Config: `eslint.config.mjs` (flat config) + - Ignores generated code: `src/generated/` +- TypeScript 5 - Type checking + - Config: `tsconfig.json` + - Target: ES2017 + - Module resolution: bundler + - Path alias: `@/*` → `src/*` + +## Key Dependencies + +**Critical:** +- `@prisma/adapter-pg@7.4.2` - PostgreSQL connection adapter for Prisma +- `@prisma/client@7.4.2` - ORM runtime +- `next-auth@5.0.0-beta.30` - Session management and auth flows +- `@trpc/server@11.8.0` - Type-safe API endpoint definitions +- `react@19.2.3` - Component rendering engine + +**Infrastructure & Utilities:** +- `zustand@5.0.11` - State management (client) +- `@tanstack/react-query@5.90.21` - Server state and caching +- `zod@4.3.6` - Runtime validation and schemas +- `class-variance-authority@0.7.1` - Variant-based component styling +- `radix-ui@1.4.3` - Headless UI components +- `tailwind-merge@3.5.0` - Smart Tailwind class merging + +**Data Processing & UI:** +- `@xyflow/react@12.10.1` - Graph visualization for pipelines (DAG editor) +- `@dagrejs/dagre@2.0.4` - DAG layout algorithm +- `@monaco-editor/react@4.7.0` - Monaco editor for VRL syntax +- `monaco-editor@0.55.1` - Monaco editor distribution +- `recharts@2.15.4` - React charting library for metrics +- `react-grid-layout@2.2.2` - Grid layout system + +**Forms & Validation:** +- `react-hook-form@7.71.2` - Form state management +- `@hookform/resolvers@5.2.2` - Integration with validation libraries + +**Notifications & Channels:** +- `sonner@2.0.7` - Toast notifications +- `nodemailer@8.0.1` - SMTP email delivery + +**Utilities:** +- `js-yaml@4.1.1` - YAML parsing/serialization (Vector config) +- `nanoid@5.1.6` - Unique ID generation +- `qrcode@1.5.4` - QR code generation (2FA/TOTP) +- `otpauth@9.5.0` - TOTP/OTP generation and validation +- `simple-git@3.32.3` - Git operations (GitSync commits) +- `diff@8.0.3` - Diff generation +- `node-cron@4.2.1` - Scheduled tasks +- `superjson@2.2.6` - JSON serialization for complex types +- `dotenv@17.3.1` - Environment variable loading +- `clsx@2.1.1` - Conditional className helper +- `cmdk@1.1.1` - Command/search dialog +- `lucide-react@0.575.0` - Icon library +- `next-themes@0.4.6` - Dark mode theme management + +## Configuration + +**Environment:** +- `.env` file (not committed) - Runtime configuration +- `process.env.DATABASE_URL` - PostgreSQL connection string (required) +- `process.env.NEXTAUTH_SECRET` - NextAuth JWT secret (required) +- `process.env.NEXTAUTH_URL` - Public application URL (optional, defaults to localhost:3000) +- `process.env.NODE_ENV` - Environment detection ("development", "production") +- `process.env.NEXT_RUNTIME` - Runtime detection ("nodejs", "edge") + +**VectorFlow-Specific Env Vars:** +- `process.env.VF_VECTOR_BIN` - Path to Vector binary (default: "vector") +- `process.env.VF_SYSTEM_CONFIG_PATH` - Path to system pipeline config (default: `.vectorflow/system-pipeline.yaml`) +- `process.env.VF_AUDIT_LOG_PATH` - Path to audit log file (default: `/var/lib/vectorflow/audit.log`) +- `process.env.VF_BACKUP_DIR` - Directory for database backups (default: `/backups`) +- `process.env.VF_VERSION` - Application version (default: "dev") +- `process.env.VF_LOG_LEVEL` / `process.env.LOG_LEVEL` - Logging level (default: "info") +- `process.env.VF_DISABLE_LOCAL_AUTH` - Disable credential login ("true" to disable) +- `process.env.PORT` - Server port (default: 3000, used for agent config URLs) + +**Build:** +- `tsconfig.json` - TypeScript configuration +- `next.config.ts` - Next.js configuration +- `postcss.config.mjs` - PostCSS configuration for Tailwind +- `eslint.config.mjs` - ESLint configuration (flat config) + +## Platform Requirements + +**Development:** +- Node.js (LTS or later recommended) +- pnpm 10.13.1 or compatible version +- PostgreSQL 12+ database +- Git (for GitSync functionality) +- Vector binary (optional, for local pipeline testing) + +**Production:** +- Node.js LTS (server runtime) +- PostgreSQL 12+ (data persistence) +- Vector binary (for system pipeline execution) +- Optional: SMTP server (for email notifications) +- Optional: External AI provider (OpenAI, Anthropic, or compatible OpenAI API) + +**Build Output:** +- Standalone Docker-friendly build (`output: "standalone"` in next.config.ts) +- No Node.js modules bundled with output +- Requires `node_modules` and `.next` at runtime + +--- + +*Stack analysis: 2026-03-22* diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md new file mode 100644 index 00000000..ce83df58 --- /dev/null +++ b/.planning/codebase/STRUCTURE.md @@ -0,0 +1,254 @@ +# Codebase Structure + +**Analysis Date:** 2026-03-22 + +## Directory Layout + +``` +vectorflow/ +├── src/ # Main application source +│ ├── app/ # Next.js App Router pages and API routes +│ ├── components/ # React components organized by feature +│ ├── generated/ # Auto-generated files (Prisma client) +│ ├── hooks/ # Custom React hooks +│ ├── lib/ # Shared utilities and logic +│ ├── server/ # Backend services, routers, middleware +│ ├── stores/ # Zustand client state stores +│ ├── trpc/ # tRPC setup and configuration +│ ├── types/ # TypeScript type definitions +│ ├── auth.config.ts # NextAuth configuration +│ +├── prisma/ # Database schema and migrations +│ ├── schema.prisma # Prisma data model +│ └── migrations/ # Database migration history +│ +├── public/ # Static assets (images, icons) +├── docs/ # Documentation +│ ├── public/ # User-facing docs (GitBook synced) +│ ├── plans/ # Implementation plans (not committed) +│ └── superpowers/ # Internal design docs +│ +├── docker/ # Docker configurations for deployment +│ ├── agent/ # Agent Docker setup +│ └── server/ # Server Docker setup +│ +├── scripts/ # Utility scripts +├── agent/ # Go-based agent (separate service) +├── assets/ # Design assets +│ +├── package.json # pnpm dependencies +├── tsconfig.json # TypeScript configuration +├── next.config.ts # Next.js configuration +├── eslint.config.mjs # ESLint configuration +└── tailwind.config.ts # Tailwind CSS configuration +``` + +## Directory Purposes + +**`src/app/`:** +- Purpose: Next.js App Router pages and API route handlers +- Contains: Page components (`.tsx`), API route handlers (`route.ts`), layouts +- Key files: + - `src/app/layout.tsx`: Root layout (theme, auth, tRPC providers) + - `src/app/(auth)/`: Auth group (login, 2FA setup) + - `src/app/(dashboard)/`: Protected dashboard routes (pipelines, fleet, environments, etc.) + - `src/app/api/`: API routes (tRPC, agent, webhooks, health) + +**`src/components/`:** +- Purpose: Reusable React components organized by feature +- Contains: Feature-scoped folders + UI primitives +- Key subdirectories: + - `src/components/ui/`: shadcn-based UI primitives (button, dialog, table, etc.) + - `src/components/pipeline/`: Pipeline editor related components + - `src/components/flow/`: Visual flow/graph components + - `src/components/fleet/`: Vector fleet management UI + - `src/components/vrl-editor/`: VRL syntax editor with Monaco + - `src/components/deploy/`: Deployment workflow UI + - `src/components/dashboard/`: Dashboard overview UI + - `src/components/config-forms/`: Dynamic forms for component configuration + - `src/components/metrics/`: Metrics visualization + +**`src/server/`:** +- Purpose: Backend business logic +- Contains: tRPC routers, services, middleware, integrations +- Subdirectories: + - `src/server/routers/`: tRPC router definitions (one per domain: pipeline, fleet, environment, etc.) + - `src/server/services/`: Business logic modules (config generation, validation, encryption, deployment, metrics) + - `src/server/middleware/`: tRPC middleware (authorization, audit) + - `src/server/integrations/`: External API integrations (Vector GraphQL, AI providers) + +**`src/lib/`:** +- Purpose: Shared utilities and domain-specific logic +- Contains: Stateless functions, constants, type definitions +- Subdirectories: + - `src/lib/vector/`: Vector component definitions and catalog + - `src/lib/ai/`: AI suggestion logic (prompts, validators, appliers) + - `src/lib/config-generator/`: YAML/TOML generation for Vector configs + - `src/lib/vrl/`: VRL language utilities (function registry, snippets) +- Key files: + - `src/lib/prisma.ts`: Prisma client singleton + - `src/lib/utils.ts`: String utilities, ID generation + - `src/lib/format.ts`: Data formatting (bytes, rates, dates) + - `src/lib/logger.ts`: Logging utility + +**`src/stores/`:** +- Purpose: Zustand client-side state (non-persistent UI state) +- Contains: Store definitions with actions and selectors +- Key files: + - `src/stores/flow-store.ts`: Pipeline editor state (nodes, edges, history, clipboard) + - `src/stores/team-store.ts`: Current team selection + - `src/stores/environment-store.ts`: Current environment selection + +**`src/trpc/`:** +- Purpose: tRPC setup and configuration +- Key files: + - `src/trpc/init.ts`: tRPC initialization, context, middleware definitions + - `src/trpc/router.ts`: Main router aggregating all domain routers + - `src/trpc/client.tsx`: tRPC client provider for React + +**`src/hooks/`:** +- Purpose: Custom React hooks for shared stateful logic +- Key files: + - `src/hooks/use-ai-conversation.ts`: Pipeline AI chat + - `src/hooks/use-vrl-ai-conversation.ts`: VRL editor AI chat + - `src/hooks/use-fleet-events.ts`: Real-time fleet event streaming + +**`src/generated/`:** +- Purpose: Auto-generated code (DO NOT edit directly) +- Contains: Prisma client type definitions +- Notes: Regenerated via `pnpm postinstall` (prisma generate) + +**`prisma/`:** +- Purpose: Database schema and migrations +- Key files: + - `prisma/schema.prisma`: Data model definitions (User, Team, Pipeline, Environment, VectorNode, etc.) + - `prisma/migrations/`: SQL migration files (one per schema change) +- Notes: Migrations tracked in git; run via `prisma migrate deploy` in production + +**`public/`:** +- Purpose: Static assets served by Next.js +- Contains: Favicons, logos, default images + +**`docs/public/`:** +- Purpose: User-facing documentation synced to GitBook +- Key pages: + - `docs/public/user-guide/pipeline-editor.md`: Pipeline editor docs + - `docs/public/user-guide/fleet.md`: Fleet management docs + - `docs/public/operations/configuration.md`: Environment variables + - `docs/public/operations/authentication.md`: Auth setup + +## Key File Locations + +**Entry Points:** +- `src/app/layout.tsx`: Root layout with providers +- `src/app/(dashboard)/page.tsx`: Dashboard homepage +- `src/app/api/trpc/[trpc]/route.ts`: tRPC HTTP handler +- `src/app/api/health/route.ts`: Health check endpoint + +**Configuration:** +- `src/auth.config.ts`: NextAuth provider setup +- `tsconfig.json`: TypeScript compiler options with `@/*` path alias +- `tailwind.config.ts`: Tailwind CSS theming +- `prisma/schema.prisma`: Database schema + +**Core Logic:** +- `src/server/routers/`: Domain-specific API procedures (15+ routers) +- `src/server/services/`: Business logic modules (30+ services) +- `src/lib/config-generator/index.ts`: YAML generation pipeline +- `src/lib/vector/catalog.ts`: Vector component definitions + +**Testing:** +- No test files present in src/ (testing pattern not detected) +- Integration testing would use tRPC client calls + +## Naming Conventions + +**Files:** +- `.tsx`: React components (default) +- `.ts`: TypeScript utilities, services, routers +- `[brackets].tsx`: Dynamic routes (Next.js) +- `(parentheses)/`: Route groups (Next.js) — no URL path + +**Directories:** +- Plural names: `components/`, `stores/`, `services/`, `routers/`, `hooks/` +- Descriptive feature names: `pipeline/`, `fleet/`, `vrl-editor/`, `config-forms/` + +**Functions & Variables:** +- camelCase: `createPipelineVersion()`, `withTeamAccess`, `flowStore` +- PascalCase: React components (``), types (`User`, `VectorComponentDef`) +- UPPERCASE: Constants (`MAX_HISTORY`, `AUDIT_LOG_PATH`) + +**Database Models:** +- PascalCase: User, Team, Pipeline, Environment, VectorNode, AiConversation + +**Zod Schemas:** +- camelCase with "Schema" suffix: `pipelineNameSchema`, `nodeSchema`, `edgeSchema` + +## Where to Add New Code + +**New Feature (e.g., "User Preferences"):** +- **Router:** `src/server/routers/user-preference.ts` (add to `src/trpc/router.ts`) +- **Page:** `src/app/(dashboard)/settings/preferences/page.tsx` (new route) +- **Components:** `src/components/preferences/` (feature-scoped folder) +- **Services:** `src/server/services/preferences.ts` (if complex logic) +- **Hooks:** `src/hooks/use-user-preferences.ts` (if shared state logic) +- **Schema:** Add model to `prisma/schema.prisma`, run `prisma migrate dev` + +**New Component/Widget:** +- **Reusable UI:** `src/components/ui/my-component.tsx` (if primitive; use shadcn) +- **Feature Component:** `src/components/{feature}/MyComponent.tsx` (if scoped to feature) +- **Export:** Re-export from feature folder's index if multiple components + +**New Utility/Helper:** +- **Shared logic:** `src/lib/my-utility.ts` (if domain-agnostic) +- **Domain-specific:** `src/lib/{domain}/my-utility.ts` (if scoped, e.g., `src/lib/vector/`) +- **Service logic:** `src/server/services/my-service.ts` (if database/side effects) + +**New API Route (non-tRPC):** +- **Agent/Webhook:** `src/app/api/{service}/{endpoint}/route.ts` +- **Pattern:** Use middleware for auth, validate input, call service, return response + +**New Middleware:** +- **File:** `src/server/middleware/{concern}.ts` (e.g., `rate-limit.ts`) +- **Pattern:** Export middleware function compatible with tRPC; use in router procedures via `.use()` + +**New Page:** +- **Authenticated:** `src/app/(dashboard)/{feature}/page.tsx` (inside dashboard layout) +- **Public:** `src/app/{feature}/page.tsx` (top-level) +- **Auth:** `src/app/(auth)/{flow}/page.tsx` (e.g., login, setup) + +## Special Directories + +**`src/generated/`:** +- Purpose: Auto-generated Prisma client +- Generated: Yes (via `prisma generate`) +- Committed: No (excluded via `.gitignore` in typical setup; regenerated post-install) +- Notes: DO NOT edit; generated on `pnpm install` + +**`prisma/migrations/`:** +- Purpose: Database migration history +- Generated: Yes (via `prisma migrate dev`) +- Committed: Yes (track schema evolution) +- Notes: Each migration is timestamped SQL file + metadata JSON + +**`.next/`:** +- Purpose: Next.js build output +- Generated: Yes (via `pnpm build`) +- Committed: No (in `.gitignore`) +- Notes: Contains compiled pages, static assets, server bundles + +**`docs/plans/`:** +- Purpose: Implementation plans for features +- Generated: No (manually created) +- Committed: No (in `.gitignore`) +- Notes: Ephemeral; deleted after implementation + +**`docs/superpowers/`:** +- Purpose: Internal design specifications and brainstorms +- Generated: No (manually created) +- Committed: No (internal only) +- Notes: Research, architecture decisions, sketches + +--- + +*Structure analysis: 2026-03-22* diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md new file mode 100644 index 00000000..dc626163 --- /dev/null +++ b/.planning/codebase/TESTING.md @@ -0,0 +1,381 @@ +# Testing Patterns + +**Analysis Date:** 2026-03-22 + +## Test Framework + +**Status:** No automated test framework configured + +**Notable:** +- No test files found in codebase (`find` returned 0 results for `*.test.ts`, `*.spec.ts`) +- No jest.config.js, vitest.config.js, or test runner configuration +- No test dependencies in `package.json` (no jest, vitest, mocha, chai) +- No test scripts in package.json (no `test`, `test:watch`, `test:coverage`) + +**Implication:** Testing strategy is manual or external (e.g., E2E tests via Cypress/Playwright, QA team testing). Feature work does not include unit test requirements. + +## Testing Approach + +**Current State:** +- Manual testing by developers +- Potential external E2E testing (not integrated into codebase) +- Type safety via TypeScript as primary quality gate + +**Recommendations for Implementation:** +If automated testing is added, the following patterns should be adopted: + +### Unit Test Setup (if adopting Vitest) + +**Framework Choice:** Vitest recommended (faster, better TypeScript support for Next.js than Jest) + +**Config Location:** `vitest.config.ts` in project root + +**Basic Config:** +```typescript +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['src/**/*.test.ts', 'src/**/*.spec.ts'], + }, +}); +``` + +**Run Commands:** +```bash +pnpm test # Run all tests once +pnpm test:watch # Watch mode +pnpm test:coverage # Coverage report +``` + +## Test File Organization + +**Location Strategy (recommended):** +- Co-located with source code (same directory) +- Naming: `{component}.test.ts`, `{function}.spec.ts` + +**Pattern:** +``` +src/ + server/ + services/ + validator.ts + validator.test.ts + routers/ + fleet.ts + fleet.test.ts + components/ + fleet/ + status-timeline.tsx + status-timeline.test.tsx + lib/ + utils.ts + utils.test.ts +``` + +**Why co-located:** +- Easy to find tests for a given file +- Tests moved/deleted with source +- Simpler import paths in tests + +## Test Structure + +**Suite Organization (recommended pattern):** +```typescript +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { validateConfig } from './validator'; + +describe('validateConfig', () => { + describe('valid YAML', () => { + it('returns { valid: true, errors: [], warnings: [] }', async () => { + const result = await validateConfig('sources:\n in:\n type: stdin'); + expect(result.valid).toBe(true); + }); + }); + + describe('invalid YAML', () => { + it('parses error messages from Vector', async () => { + const result = await validateConfig('invalid: [yaml'); + expect(result.valid).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + }); + }); + + describe('when Vector binary missing', () => { + it('returns specific error', async () => { + vi.spyOn(child_process, 'execFile').mockImplementation(() => { + throw { code: 'ENOENT' }; + }); + const result = await validateConfig('sources: {}'); + expect(result.errors[0]?.message).toContain('Vector binary not found'); + }); + }); +}); +``` + +**Patterns:** +- Use `describe()` blocks to group related tests +- Nest `describe()` for different scenarios (valid input, invalid input, edge cases) +- One `it()` per assertion (each test verifies one behavior) +- Descriptive test names that read as sentences + +## Mocking + +**Framework:** Vitest built-in `vi` module + +**Mocking Pattern:** +```typescript +import { vi } from 'vitest'; + +// Mock a module +vi.mock('@/lib/prisma', () => ({ + prisma: { + vectorNode: { + findMany: vi.fn(), + findUnique: vi.fn(), + }, + }, +})); + +// Mock a function +vi.spyOn(crypto, 'randomUUID').mockReturnValue('mocked-uuid'); + +// Mock with implementation +vi.spyOn(fetch).mockImplementation(() => + Promise.resolve(new Response(JSON.stringify({ data: {} }))) +); +``` + +**What to Mock:** +- External APIs (GraphQL endpoints, HTTP calls) +- Database operations (Prisma queries) +- Crypto/random functions (for deterministic tests) +- File system operations (fs module) +- Async operations with side effects + +**What NOT to Mock:** +- Utility functions (`utils.ts`, `crypto.ts`, `git-sync.ts` helpers) +- Type definitions and constants +- Core business logic (validate with real data when possible) +- Internal function calls within same module + +**Pattern for Services with External Dependencies:** +```typescript +describe('authenticateAgent', () => { + beforeEach(() => { + vi.mock('@/lib/prisma'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('returns nodeId and environmentId when token matches', async () => { + const mockPrisma = vi.mocked(prisma); + mockPrisma.vectorNode.findMany.mockResolvedValue([ + { id: 'node-1', environmentId: 'env-1', nodeTokenHash: 'hash' }, + ]); + + const result = await authenticateAgent(mockRequest); + expect(result).toEqual({ nodeId: 'node-1', environmentId: 'env-1' }); + }); +}); +``` + +## Fixtures and Factories + +**Test Data Pattern (recommended):** + +Create factories in `src/__tests__/fixtures/` directory: + +```typescript +// src/__tests__/fixtures/vector-node.ts +export function createVectorNode(overrides = {}) { + return { + id: 'node-' + Math.random().toString(36).slice(2), + environmentId: 'env-1', + name: 'test-node', + status: 'HEALTHY', + lastSeen: new Date(), + nodeTokenHash: null, + ...overrides, + }; +} + +// Usage in tests +it('lists nodes by environment', async () => { + const nodes = [ + createVectorNode({ status: 'HEALTHY' }), + createVectorNode({ status: 'DEGRADED' }), + ]; + mockPrisma.vectorNode.findMany.mockResolvedValue(nodes); + // ... +}); +``` + +**Location:** +- `src/__tests__/fixtures/` for shared test data +- `src/__tests__/mocks/` for mock implementations +- Keep fixtures close to tests that use them + +## Coverage + +**Requirements:** Not enforced (no coverage threshold specified in codebase) + +**If Coverage is Added:** + +Add to `vitest.config.ts`: +```typescript +export default defineConfig({ + test: { + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + include: ['src/**/*.ts', 'src/**/*.tsx'], + exclude: ['src/**/*.d.ts', 'src/generated/**'], + lines: 70, + functions: 70, + branches: 65, + }, + }, +}); +``` + +**View Coverage:** +```bash +pnpm test:coverage +# Opens coverage/index.html +``` + +## Test Types + +**Unit Tests:** +- Scope: Single function or component in isolation +- Approach: Mock all external dependencies +- Examples: `validateConfig()`, `authenticateAgent()`, `encrypt()`/`decrypt()` +- Location: Co-located with source file + +**Integration Tests:** +- Scope: Multiple modules working together (e.g., TRPC procedure calling service) +- Approach: Mock database and external APIs, test flow through layers +- Examples: TRPC `fleet.list` calling `prisma.vectorNode.findMany()` and returning transformed data +- Pattern: + ```typescript + describe('fleetRouter.list integration', () => { + it('fetches nodes and adds pushConnected status', async () => { + mockPrisma.vectorNode.findMany.mockResolvedValue([...]); + mockPushRegistry.isConnected.mockReturnValue(true); + + const result = await fleetRouter.createCaller({}).list({ environmentId: 'env-1' }); + expect(result[0]).toHaveProperty('pushConnected', true); + }); + }); + ``` + +**E2E Tests:** +- Scope: Full user workflows via browser automation +- Framework: Playwright or Cypress (not currently in project) +- Approach: Run against running application with real database +- Would test: Login → Create Pipeline → Deploy → View Status + +## Common Patterns + +**Async Testing:** +```typescript +it('validates config asynchronously', async () => { + const result = await validateConfig('sources: {}'); + expect(result).toBeDefined(); +}); + +// With timeout for slow operations +it('handles long-running validation', async () => { + const result = await validateConfig(largeYaml, { timeout: 5000 }); + expect(result.valid).toBe(true); +}, 10000); // Test timeout in ms +``` + +**Error Testing:** +```typescript +it('throws TRPCError when user not found', async () => { + mockPrisma.user.findUnique.mockResolvedValue(null); + + await expect(() => + userRouter.changePassword({ currentPassword: '...', newPassword: '...' }) + ).rejects.toThrow(TRPCError); +}); + +it('returns specific error when Vector binary missing', async () => { + vi.spyOn(execFileAsync).mockRejectedValue({ code: 'ENOENT' }); + + const result = await validateConfig('test'); + expect(result.valid).toBe(false); + expect(result.errors[0]?.message).toContain('Vector binary not found'); +}); +``` + +**Hook Testing (with vitest-react-hooks or similar):** +```typescript +import { renderHook, act } from '@testing-library/react-hooks'; +import { useTeamStore } from '@/stores/team-store'; + +it('updates selected team', () => { + const { result } = renderHook(() => useTeamStore()); + + act(() => { + result.current.setSelectedTeamId('team-1'); + }); + + expect(result.current.selectedTeamId).toBe('team-1'); +}); +``` + +**Component Testing (with vitest + React Testing Library):** +```typescript +import { render, screen } from '@testing-library/react'; +import { StatusTimeline } from './status-timeline'; + +it('renders time range selector', () => { + render(); + expect(screen.getByText(/1h/i)).toBeInTheDocument(); +}); + +it('calls onRangeChange when selection changes', async () => { + const handleChange = vi.fn(); + const { user } = render( + + ); + + await user.selectOption(screen.getByRole('combobox'), '6h'); + expect(handleChange).toHaveBeenCalledWith('6h'); +}); +``` + +## Recommended Test Priority + +If implementing tests, prioritize in this order: + +1. **Services** (`src/server/services/`) — Pure functions with business logic + - `validator.ts` (validates YAML configs) + - `crypto.ts` (encryption/decryption) + - `git-sync.ts` (Git operations) + - `agent-auth.ts` (authentication logic) + +2. **TRPC Routers** (`src/server/routers/`) — Request/response handling + - Focus on error cases and authorization checks + - Mock Prisma queries + +3. **Utilities** (`src/lib/utils.ts`, config-generator) — Reusable helpers + - Pure utility functions with predictable inputs/outputs + +4. **Hooks** (`src/hooks/`) — State and side effects + - Use vitest + React testing library + - Mock TRPC calls + +5. **Components** (`src/components/`) — UI layers (lower priority) + - Focus on components with business logic + - UI-only components can rely on manual testing + +--- + +*Testing analysis: 2026-03-22* From 98d61aee8d9a2f71f6b637d2db60f558db57041a Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sun, 22 Mar 2026 22:12:25 +0000 Subject: [PATCH 02/73] chore(M001/S01): auto-commit after research-slice --- .bg-shell/manifest.json | 1 + .gsd/DECISIONS.md | 11 ++ .gsd/PROJECT.md | 30 ++++ .gsd/REQUIREMENTS.md | 169 ++++++++++++++++++ .gsd/journal/2026-03-22.jsonl | 4 + .gsd/milestones/M001/M001-CONTEXT.md | 115 ++++++++++++ .gsd/milestones/M001/M001-META.json | 3 + .gsd/milestones/M001/M001-ROADMAP.md | 126 +++++++++++++ .../M001/slices/S01/S01-RESEARCH.md | 72 ++++++++ 9 files changed, 531 insertions(+) create mode 100644 .bg-shell/manifest.json create mode 100644 .gsd/DECISIONS.md create mode 100644 .gsd/PROJECT.md create mode 100644 .gsd/REQUIREMENTS.md create mode 100644 .gsd/journal/2026-03-22.jsonl create mode 100644 .gsd/milestones/M001/M001-CONTEXT.md create mode 100644 .gsd/milestones/M001/M001-META.json create mode 100644 .gsd/milestones/M001/M001-ROADMAP.md create mode 100644 .gsd/milestones/M001/slices/S01/S01-RESEARCH.md diff --git a/.bg-shell/manifest.json b/.bg-shell/manifest.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/.bg-shell/manifest.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/.gsd/DECISIONS.md b/.gsd/DECISIONS.md new file mode 100644 index 00000000..749ac070 --- /dev/null +++ b/.gsd/DECISIONS.md @@ -0,0 +1,11 @@ +# Decisions Register + + + +| # | When | Scope | Decision | Choice | Rationale | Revisable? | Made By | +|---|------|-------|----------|--------|-----------|------------|---------| +| D001 | M001/S04 | arch | Test framework for Next.js + tRPC + Prisma codebase | Vitest — to be set up in S04 | Standard choice for Next.js projects, fast, good TypeScript support, compatible with tRPC testing patterns | Yes — if Vitest proves incompatible with the codebase | agent | +| D002 | M001 | arch | Refactoring depth for baseline quality milestone | Moderate — split files over ~800 lines, extract duplicates, move inline logic to services; don't restructure entire module tree | User confirmed moderate approach — split worst offenders without deep restructuring. Keeps scope bounded. | No | collaborative | +| D003 | M001/S02 | convention | Whether purely declarative data files (vrl/function-registry.ts) count against the file size target | Exempt from ~800-line target | function-registry.ts is 1775 lines of structured data definitions, not logic. Splitting it would add indirection without improving maintainability. | Yes — if the file gains logic beyond data definitions | agent | diff --git a/.gsd/PROJECT.md b/.gsd/PROJECT.md new file mode 100644 index 00000000..067d4cf8 --- /dev/null +++ b/.gsd/PROJECT.md @@ -0,0 +1,30 @@ +# Project + +## What This Is + +VectorFlow is a self-hosted control plane for Vector.dev data pipelines. It provides a visual drag-and-drop pipeline editor, fleet deployment with pull-based agents, real-time monitoring, version control with rollback, enterprise auth (OIDC SSO, RBAC, 2FA, SCIM), alerting with webhooks, and a Go agent that runs on fleet nodes. Built with Next.js 16, tRPC, Prisma (PostgreSQL), Zustand, React Flow, and shadcn/ui. + +## Core Value + +Visual pipeline management with fleet deployment — build Vector configs on a canvas and push them to your infrastructure without hand-editing YAML. + +## Current State + +Feature-rich and functional. ~316 source files, ~63K lines of TypeScript, plus a Go agent. The product has grown fast with many enterprise features (OIDC, SCIM, audit logging, RBAC, 2FA, alerting, git sync, backups, shared components, AI suggestions). However, the codebase has zero tests, several large monolithic files (1000+ lines), some TypeScript errors from schema drift, duplicated utility functions, and UI inconsistencies across the 35+ dashboard pages. + +## Architecture / Key Patterns + +- **Frontend:** Next.js 16 App Router, all pages `"use client"`, shadcn/ui components, Zustand stores, React Flow for pipeline canvas, Monaco editor for VRL +- **API:** tRPC with 22 routers, Zod validation, Prisma ORM +- **Auth:** NextAuth v5 beta with credentials + OIDC, PrismaAdapter +- **Agent:** Go binary, pull-based config delivery, heartbeat/metrics push +- **Deployment:** Docker (standalone Next.js output), PostgreSQL +- **Patterns:** Fire-and-forget audit logging, AES-256-GCM encryption for secrets, structured logger with log-injection prevention + +## Capability Contract + +See `.gsd/REQUIREMENTS.md` for the explicit capability contract, requirement status, and coverage mapping. + +## Milestone Sequence + +- [ ] M001: Baseline Quality — Fix TS errors, refactor large files, consistent UI, foundational tests, performance audit diff --git a/.gsd/REQUIREMENTS.md b/.gsd/REQUIREMENTS.md new file mode 100644 index 00000000..7e77a9ea --- /dev/null +++ b/.gsd/REQUIREMENTS.md @@ -0,0 +1,169 @@ +# Requirements + +This file is the explicit capability and coverage contract for the project. + +## Active + +### R001 — Zero TypeScript errors +- Class: quality-attribute +- Status: active +- Description: `tsc --noEmit` must pass with zero errors. Currently 8 errors: stale Prisma client fields in `event-log.tsx` destructuring, missing `monaco-editor` type resolution in `vrl-editor.tsx` and `vrl-language.ts`. +- Why it matters: Type errors indicate schema drift and broken contracts — they mask real bugs and make refactoring unsafe. +- Source: execution +- Primary owning slice: M001/S01 +- Supporting slices: none +- Validation: unmapped +- Notes: Prisma generate fixes most errors; remaining are event-log destructuring bug and monaco-editor module resolution. + +### R002 — Foundational test coverage for critical paths +- Class: quality-attribute +- Status: active +- Description: Test suite exists with coverage for auth flows (login, 2FA, OIDC), pipeline CRUD, deploy operations, and alert evaluation. Test runner configured and passing in CI. +- Why it matters: Zero tests on a product with enterprise security features is a liability. Critical paths need automated verification before further feature work. +- Source: user +- Primary owning slice: M001/S04 +- Supporting slices: none +- Validation: unmapped +- Notes: Need to set up test infrastructure from scratch — runner, Prisma mocking strategy, test utilities. + +### R003 — No source file over ~800 lines +- Class: quality-attribute +- Status: active +- Description: All `.ts`/`.tsx` source files (excluding generated code) should be under ~800 lines. Currently 10+ files over 600 lines, with the alerts page at 1910 lines. +- Why it matters: Large monolithic files are hard to navigate, review, and maintain. They signal mixed concerns that should be separated. +- Source: user +- Primary owning slice: M001/S02 +- Supporting slices: none +- Validation: unmapped +- Notes: Biggest offenders: alerts page (1910), vrl function-registry (1775), pipeline router (1318), dashboard router (1074), flow-store (951), team-settings (865), users-settings (813), vrl-editor (795). + +### R004 — Duplicated utilities extracted to shared modules +- Class: quality-attribute +- Status: active +- Description: Utility functions duplicated across files (e.g., `aggregateProcessStatus` in 3 files, `derivePipelineStatus` in dashboard page) are extracted to shared modules in `src/lib/`. +- Why it matters: Duplicated logic drifts over time and creates maintenance burden. +- Source: execution +- Primary owning slice: M001/S01 +- Supporting slices: M001/S02 +- Validation: unmapped +- Notes: Scout found `aggregateProcessStatus` in pipelines/page.tsx, pipelines/[id]/page.tsx, and dashboard page.tsx. + +### R005 — Consistent loading/empty/error states across all pages +- Class: primary-user-loop +- Status: active +- Description: All 35+ dashboard pages have consistent loading skeletons, empty state messaging with CTAs, and error handling. No page should show a blank white screen during loading or when data is empty. +- Why it matters: Inconsistent loading/empty states make the product feel unfinished and confuse users. +- Source: user +- Primary owning slice: M001/S03 +- Supporting slices: none +- Validation: unmapped +- Notes: Most pages already have Skeleton loading — need to audit for gaps and standardize the pattern. + +### R006 — UI consistency sweep — visual rough edges cleaned +- Class: primary-user-loop +- Status: active +- Description: General UI polish pass — consistent spacing, typography, icon usage, button patterns, table styles, dialog patterns, and visual consistency across all dashboard pages. +- Why it matters: Visual inconsistencies undermine trust in a product aimed at infrastructure teams. +- Source: user +- Primary owning slice: M001/S03 +- Supporting slices: none +- Validation: unmapped +- Notes: General sweep based on code audit — no specific user-reported pain points. + +### R007 — Inline router business logic extracted to services +- Class: quality-attribute +- Status: active +- Description: Complex business logic currently inline in tRPC router handlers is extracted to service modules in `src/server/services/`. Routers become thin orchestration layers. +- Why it matters: Inline logic in routers is harder to test, reuse, and reason about. Service extraction enables R002 (testability). +- Source: inferred +- Primary owning slice: M001/S02 +- Supporting slices: M001/S04 +- Validation: unmapped +- Notes: Pipeline router (1318 lines) and dashboard router (1074 lines) are the primary targets. Some routers already delegate to services well. + +### R008 — Clean lint pass +- Class: quality-attribute +- Status: active +- Description: `eslint` runs clean with no errors across the codebase. +- Why it matters: Lint errors signal code quality issues and should be addressed alongside TS errors. +- Source: inferred +- Primary owning slice: M001/S01 +- Supporting slices: none +- Validation: unmapped +- Notes: ESLint config uses next/core-web-vitals and next/typescript presets. + +### R010 — Performance audit — bundle size, runtime perf, query efficiency +- Class: quality-attribute +- Status: active +- Description: Analyze Next.js bundle size, identify large dependencies or unnecessary client-side imports, review Prisma query patterns for N+1 or missing indexes, and address measurable bottlenecks found. +- Why it matters: Performance issues compound as the product grows — catching them now prevents worse problems later. +- Source: user +- Primary owning slice: M001/S05 +- Supporting slices: none +- Validation: unmapped +- Notes: Includes bundle analysis, Prisma query review, and runtime profiling of heavy pages (dashboard, pipeline editor, fleet). + +## Validated + +(none yet) + +## Deferred + +### R009 — Build succeeds without ignoreBuildErrors workaround +- Class: quality-attribute +- Status: deferred +- Description: Remove `ignoreBuildErrors: true` from `next.config.ts` so `next build` type-checks without bypassing errors. +- Why it matters: The workaround exists because Next.js build checker diverges from `tsc` on complex intersection types. Removing it would catch errors earlier. +- Source: inferred +- Primary owning slice: none +- Supporting slices: none +- Validation: unmapped +- Notes: Lower priority — `tsc --noEmit` in CI catches real errors. The config comment explains the divergence is in contextual typing through complex intersection types, causing false positives. + +## Out of Scope + +### R011 — Accessibility audit (WCAG compliance) +- Class: quality-attribute +- Status: out-of-scope +- Description: Full accessibility audit and remediation +- Why it matters: Prevents scope creep — accessibility is a separate focused effort +- Source: inferred +- Primary owning slice: none +- Supporting slices: none +- Validation: n/a +- Notes: Future milestone candidate + +### R012 — New feature development +- Class: constraint +- Status: out-of-scope +- Description: No new features are added in this milestone — purely quality improvements to existing functionality +- Why it matters: Prevents scope creep during a quality-focused milestone +- Source: inferred +- Primary owning slice: none +- Supporting slices: none +- Validation: n/a +- Notes: New features belong in subsequent milestones + +## Traceability + +| ID | Class | Status | Primary owner | Supporting | Proof | +|---|---|---|---|---|---| +| R001 | quality-attribute | active | M001/S01 | none | unmapped | +| R002 | quality-attribute | active | M001/S04 | none | unmapped | +| R003 | quality-attribute | active | M001/S02 | none | unmapped | +| R004 | quality-attribute | active | M001/S01 | M001/S02 | unmapped | +| R005 | primary-user-loop | active | M001/S03 | none | unmapped | +| R006 | primary-user-loop | active | M001/S03 | none | unmapped | +| R007 | quality-attribute | active | M001/S02 | M001/S04 | unmapped | +| R008 | quality-attribute | active | M001/S01 | none | unmapped | +| R009 | quality-attribute | deferred | none | none | unmapped | +| R010 | quality-attribute | active | M001/S05 | none | unmapped | +| R011 | quality-attribute | out-of-scope | none | none | n/a | +| R012 | constraint | out-of-scope | none | none | n/a | + +## Coverage Summary + +- Active requirements: 9 +- Mapped to slices: 9 +- Validated: 0 +- Unmapped active requirements: 0 diff --git a/.gsd/journal/2026-03-22.jsonl b/.gsd/journal/2026-03-22.jsonl new file mode 100644 index 00000000..4a531d9b --- /dev/null +++ b/.gsd/journal/2026-03-22.jsonl @@ -0,0 +1,4 @@ +{"ts":"2026-03-22T22:05:17.789Z","flowId":"dfc4628d-0bfb-4a52-b119-cf64f821f058","seq":1,"eventType":"iteration-start","data":{"iteration":1}} +{"ts":"2026-03-22T22:05:18.028Z","flowId":"dfc4628d-0bfb-4a52-b119-cf64f821f058","seq":2,"eventType":"dispatch-match","rule":"planning (no research, not S01) → research-slice","data":{"unitType":"research-slice","unitId":"M001/S01"}} +{"ts":"2026-03-22T22:05:18.034Z","flowId":"dfc4628d-0bfb-4a52-b119-cf64f821f058","seq":3,"eventType":"unit-start","data":{"unitType":"research-slice","unitId":"M001/S01"}} +{"ts":"2026-03-22T22:12:24.384Z","flowId":"dfc4628d-0bfb-4a52-b119-cf64f821f058","seq":4,"eventType":"unit-end","data":{"unitType":"research-slice","unitId":"M001/S01","status":"completed","artifactVerified":true},"causedBy":{"flowId":"dfc4628d-0bfb-4a52-b119-cf64f821f058","seq":3}} diff --git a/.gsd/milestones/M001/M001-CONTEXT.md b/.gsd/milestones/M001/M001-CONTEXT.md new file mode 100644 index 00000000..28eddb69 --- /dev/null +++ b/.gsd/milestones/M001/M001-CONTEXT.md @@ -0,0 +1,115 @@ +# M001: Baseline Quality + +**Gathered:** 2026-03-22 +**Status:** Ready for planning + +## Project Description + +VectorFlow is a self-hosted control plane for Vector.dev data pipelines — visual editor, fleet deployment, monitoring, enterprise auth, alerting. Next.js 16 + tRPC + Prisma + React Flow. The codebase has grown fast with many features but needs a quality baseline pass before further development. + +## Why This Milestone + +The product works but has accumulated technical debt: 8 TypeScript errors from schema drift, zero tests, several 1000+ line monolithic files, duplicated utilities, and inconsistent UI patterns across 35+ pages. This milestone establishes a clean, maintainable baseline before building more on top. + +## User-Visible Outcome + +### When this milestone is complete, the user can: + +- See consistent loading, empty, and error states across every dashboard page +- Experience a visually polished, consistent interface without rough edges +- Trust that critical paths (auth, pipeline CRUD, deploy) are covered by automated tests + +### Entry point / environment + +- Entry point: `http://localhost:3000` (Next.js dev server) +- Environment: local dev +- Live dependencies involved: PostgreSQL (Prisma) + +## Completion Class + +- Contract complete means: zero TS errors, clean lint, all tests passing, no file over ~800 lines, bundle analysis report generated +- Integration complete means: refactored routers still serve the same API contracts, UI changes render correctly +- Operational complete means: none — no runtime behavior changes + +## Final Integrated Acceptance + +To call this milestone complete, we must prove: + +- `tsc --noEmit` exits 0 +- `eslint` exits 0 +- All tests pass +- `find src -name '*.ts' -o -name '*.tsx' | xargs wc -l | sort -rn | head -1` shows no file over ~800 lines (excluding generated) +- Bundle analysis shows no obvious oversized chunks or unnecessary client imports +- Visual spot-check of dashboard, pipelines, fleet, alerts, settings pages shows consistent patterns + +## Risks and Unknowns + +- Splitting large files may surface hidden coupling between components and state — moderate risk +- Setting up Prisma test mocking from scratch — some unknowns around the best approach with Prisma 7 +- Performance audit findings may open scope — discipline to note issues and only fix clear wins + +## Existing Codebase / Prior Art + +- `src/server/services/` — existing service layer pattern, some routers already delegate well +- `src/components/ui/` — shadcn/ui component library, well-structured +- `src/lib/` — shared utilities exist but some helpers are duplicated in page files +- `src/stores/flow-store.ts` — 951-line Zustand store, complex but cohesive +- `src/app/(dashboard)/alerts/page.tsx` — 1910 lines, the single largest file +- `src/server/routers/pipeline.ts` — 1318 lines, heaviest router +- `prisma/schema.prisma` — 806 lines, well-structured with proper indexes + +> See `.gsd/DECISIONS.md` for all architectural and pattern decisions — it is an append-only register; read it during planning, append to it during execution. + +## Relevant Requirements + +- R001 — Zero TypeScript errors (S01) +- R002 — Foundational test coverage (S04) +- R003 — No file over ~800 lines (S02) +- R004 — Duplicated utilities extracted (S01) +- R005 — Consistent loading/empty/error states (S03) +- R006 — UI consistency sweep (S03) +- R007 — Router logic extracted to services (S02) +- R008 — Clean lint pass (S01) +- R010 — Performance audit (S05) + +## Scope + +### In Scope + +- Fix all TypeScript errors +- Extract duplicated utility functions to shared modules +- Split files over ~800 lines into smaller, focused modules +- Extract inline router business logic to service layer +- Audit and standardize loading/empty/error states across all dashboard pages +- General UI consistency polish +- Set up test infrastructure and write foundational tests for critical paths +- Performance audit — bundle analysis, Prisma query review, runtime profiling +- Fix measurable performance issues found during audit + +### Out of Scope / Non-Goals + +- New feature development +- Full accessibility (WCAG) audit — future milestone +- Removing `ignoreBuildErrors: true` from next.config (deferred — see R009) +- Changing the Go agent code +- Database schema changes +- Auth flow changes + +## Technical Constraints + +- Must not change any API contracts — refactoring is internal only +- Prisma schema is stable — no migrations in this milestone +- Monaco editor is loaded dynamically via `@monaco-editor/react` — the type issue is about dev-time resolution, not runtime +- `vrl/function-registry.ts` at 1775 lines is a data file (function definitions) — may be acceptable to leave large if it's purely declarative + +## Integration Points + +- Prisma/PostgreSQL — needs mocking strategy for tests +- NextAuth v5 beta — auth flow testing +- tRPC — router contracts must remain unchanged after refactoring + +## Open Questions + +- Best Prisma mocking approach for Prisma 7 — likely `prisma-mock` or manual mocks with dependency injection +- Whether `flow-store.ts` (951 lines) should be split or left as-is given it's a cohesive Zustand store +- Whether `vrl/function-registry.ts` (1775 lines) counts against the 800-line target since it's declarative data diff --git a/.gsd/milestones/M001/M001-META.json b/.gsd/milestones/M001/M001-META.json new file mode 100644 index 00000000..b657e911 --- /dev/null +++ b/.gsd/milestones/M001/M001-META.json @@ -0,0 +1,3 @@ +{ + "integrationBranch": "main" +} diff --git a/.gsd/milestones/M001/M001-ROADMAP.md b/.gsd/milestones/M001/M001-ROADMAP.md new file mode 100644 index 00000000..97976d0f --- /dev/null +++ b/.gsd/milestones/M001/M001-ROADMAP.md @@ -0,0 +1,126 @@ +# M001: Baseline Quality + +**Vision:** Establish a clean, maintainable codebase baseline — zero TS errors, refactored large files, consistent UI, foundational tests, and performance audit — before building more features on top. + +## Success Criteria + +- `tsc --noEmit` exits with zero errors +- `eslint` exits with zero errors +- No source file exceeds ~800 lines (excluding generated code and purely declarative data files) +- Foundational tests pass for auth, pipeline CRUD, deploy, and alert evaluation +- All dashboard pages have consistent loading, empty, and error states +- Bundle analysis report generated with actionable findings addressed +- Duplicated utilities consolidated into shared modules + +## Key Risks / Unknowns + +- Large file splitting may surface hidden coupling — moderate risk, mitigated by S01 extracting shared utilities first +- Prisma 7 test mocking setup is uncharted for this codebase — some research needed in S04 +- Performance audit may open scope — discipline to fix clear wins and note the rest + +## Proof Strategy + +- Hidden coupling in large files → retire in S02 by splitting files and verifying `tsc --noEmit` still passes +- Prisma test mocking → retire in S04 by setting up test infrastructure and running first test suite +- Performance scope creep → retire in S05 by producing a report and only addressing measurable bottlenecks + +## Verification Classes + +- Contract verification: `tsc --noEmit`, `eslint`, test suite, `find` for file line counts +- Integration verification: spot-check that refactored pages still render, API contracts unchanged +- Operational verification: none — no runtime behavior changes +- UAT / human verification: visual spot-check of dashboard pages for UI consistency + +## Milestone Definition of Done + +This milestone is complete only when all are true: + +- `tsc --noEmit` exits 0 +- `eslint` exits 0 +- All foundational tests pass +- No source file over ~800 lines (excluding generated and declarative data) +- Duplicated utilities live in shared modules +- Every dashboard page has loading, empty, and error states +- Bundle analysis report exists with findings addressed +- Visual spot-check confirms consistent UI patterns + +## Requirement Coverage + +- Covers: R001, R002, R003, R004, R005, R006, R007, R008, R010 +- Partially covers: none +- Leaves for later: R009 +- Orphan risks: none + +## Slices + +- [ ] **S01: TypeScript fixes & shared utilities** `risk:low` `depends:[]` + > After this: `tsc --noEmit` passes with zero errors, `eslint` is clean, duplicated helpers are consolidated into `src/lib/` shared modules. + +- [ ] **S02: Router & component refactoring** `risk:medium` `depends:[S01]` + > After this: All source files are under ~800 lines, router business logic is extracted to service modules, `tsc --noEmit` still passes. + +- [ ] **S03: UI consistency sweep** `risk:low` `depends:[S01]` + > After this: Every dashboard page has consistent loading skeletons, empty states with CTAs, and error handling. Visual rough edges are cleaned up. + +- [ ] **S04: Foundational test suite** `risk:medium` `depends:[S01,S02]` + > After this: Test infrastructure is set up, foundational tests pass for auth flows, pipeline CRUD, deploy operations, and alert evaluation. + +- [ ] **S05: Performance audit & optimization** `risk:medium` `depends:[S01,S02]` + > After this: Bundle analysis report generated, Prisma query patterns reviewed, measurable bottlenecks addressed. + +## Boundary Map + +### S01 → S02 + +Produces: +- `src/lib/pipeline-status.ts` → `aggregateProcessStatus()`, `derivePipelineStatus()` (shared status derivation utilities) +- `src/lib/format.ts` → any additional formatting helpers extracted from page files +- Zero TS errors baseline — S02 refactoring can verify against this + +Consumes: +- nothing (first slice) + +### S01 → S03 + +Produces: +- Clean type baseline for UI components to build against +- Shared utilities that UI pages can import instead of inline definitions + +Consumes: +- nothing (first slice) + +### S01 → S04 + +Produces: +- Clean codebase that tests can import without type errors +- Shared utilities with stable APIs to test against + +Consumes: +- nothing (first slice) + +### S02 → S04 + +Produces: +- Service modules extracted from routers — testable units with clear inputs/outputs +- Smaller, focused router files that are easier to test in isolation + +Consumes from S01: +- Shared utilities from `src/lib/` +- Zero TS errors baseline + +### S02 → S05 + +Produces: +- Refactored modules with clearer boundaries for profiling +- Service layer separation that makes query patterns easier to audit + +Consumes from S01: +- Shared utilities, clean type baseline + +### S01 → S05 + +Produces: +- Clean codebase for accurate bundle analysis (no dead code from duplicates) + +Consumes: +- nothing (first slice) diff --git a/.gsd/milestones/M001/slices/S01/S01-RESEARCH.md b/.gsd/milestones/M001/slices/S01/S01-RESEARCH.md new file mode 100644 index 00000000..fa47b696 --- /dev/null +++ b/.gsd/milestones/M001/slices/S01/S01-RESEARCH.md @@ -0,0 +1,72 @@ +# S01 — Research: TypeScript Fixes & Shared Utilities + +**Date:** 2026-03-22 +**Depth:** Light — straightforward utility extraction following established codebase patterns + +## Summary + +R001 (`tsc --noEmit` zero errors) and R008 (clean `eslint`) are **already satisfied** — both commands exit 0 on the current codebase. The only remaining work for S01 is R004: extracting duplicated utility functions into shared modules in `src/lib/`. + +There are two clear duplication clusters: (1) **pipeline status derivation** — `aggregateProcessStatus` is copy-pasted identically in 2 files, and `derivePipelineStatus` is copy-pasted identically in 2 files; (2) **time/status formatting** — `formatTime` appears in 5 files (two variants: with-seconds and without-seconds), `STATUS_COLORS` + `statusColor` are duplicated identically in 2 files, and `formatTimestamp` in the audit page shadows the shared version in `src/lib/format.ts`. + +The codebase already has well-established shared modules (`src/lib/format.ts`, `src/lib/status.ts`, `src/lib/badge-variants.ts`) that are imported across 12+ consumers. The work is mechanical: add new exports to existing modules (or create one new module), update imports in consuming files, delete inline definitions, and verify `tsc --noEmit` still passes. + +## Recommendation + +Extract all duplicated utilities into two files: +1. **`src/lib/pipeline-status.ts`** (new) — `aggregateProcessStatus()` and `derivePipelineStatus()` since these are pipeline-specific logic that doesn't belong in the general `status.ts` +2. **`src/lib/format.ts`** (extend) — add `formatTime()` (HH:MM variant) and `formatTimeWithSeconds()` (HH:MM:SS variant) alongside existing formatting helpers +3. **`src/lib/status.ts`** (extend) — add `STATUS_COLORS` map and `statusColor()` function alongside existing status variant helpers + +This follows the existing codebase pattern exactly and keeps related concerns grouped. + +## Implementation Landscape + +### Key Files + +**New file to create:** +- `src/lib/pipeline-status.ts` — will export `aggregateProcessStatus()` and `derivePipelineStatus()` + +**Existing shared modules to extend:** +- `src/lib/format.ts` (83 lines) — add `formatTime()` and `formatTimeWithSeconds()` exports +- `src/lib/status.ts` (55 lines) — add `STATUS_COLORS` constant and `statusColor()` function + +**Consumer files to update (remove inline definitions, add imports):** + +| File | Functions to remove | Import from | +|------|-------------------|-------------| +| `src/app/(dashboard)/pipelines/page.tsx` | `aggregateProcessStatus` | `@/lib/pipeline-status` | +| `src/app/(dashboard)/pipelines/[id]/page.tsx` | `aggregateProcessStatus` | `@/lib/pipeline-status` | +| `src/app/(dashboard)/page.tsx` | `derivePipelineStatus` | `@/lib/pipeline-status` | +| `src/components/dashboard/custom-view.tsx` | `derivePipelineStatus` | `@/lib/pipeline-status` | +| `src/components/fleet/event-log.tsx` | `STATUS_COLORS`, `statusColor`, `formatTime` | `@/lib/status`, `@/lib/format` | +| `src/components/fleet/status-timeline.tsx` | `STATUS_COLORS`, `statusColor`, `formatTime` | `@/lib/status`, `@/lib/format` | +| `src/components/fleet/node-metrics-charts.tsx` | `formatTime` | `@/lib/format` | +| `src/components/fleet/node-logs.tsx` | `formatTime` (with-seconds variant) | `@/lib/format` | +| `src/components/pipeline/pipeline-logs.tsx` | `formatTime` (with-seconds variant) | `@/lib/format` | + +### Build Order + +1. **Create `src/lib/pipeline-status.ts`** — new shared module with `aggregateProcessStatus` and `derivePipelineStatus` +2. **Extend `src/lib/format.ts`** — add `formatTime` and `formatTimeWithSeconds` +3. **Extend `src/lib/status.ts`** — add `STATUS_COLORS` and `statusColor` +4. **Update all 9 consumer files** — replace inline definitions with imports +5. **Verify** — run `tsc --noEmit` and `eslint` to confirm zero regressions + +### Verification Approach + +```bash +pnpm exec tsc --noEmit # must exit 0 +pnpm exec eslint src/ # must exit 0 +# Verify no inline duplicates remain: +rg 'function aggregateProcessStatus' src/app src/components # should return nothing +rg 'function derivePipelineStatus' src/app src/components # should return nothing +rg '^function formatTime' src/app src/components # should return nothing +rg '^const STATUS_COLORS' src/components/fleet # should return nothing +``` + +## Constraints + +- `src/lib/pipeline-status.ts` is specified in the M001 Boundary Map as a deliverable of S01 — downstream slices S02-S05 depend on it existing with `aggregateProcessStatus()` and `derivePipelineStatus()` exports +- The `formatTime` name is used locally in 5 files — the shared version needs distinct names for the two variants (`formatTime` for HH:MM, `formatTimeWithSeconds` for HH:MM:SS) to avoid ambiguity +- All consumer files are `"use client"` components — the shared modules must not use server-only imports (they don't — they're pure functions) \ No newline at end of file From b327e6bb2a9964844ec6958142e3df3724dbf3ce Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sun, 22 Mar 2026 22:12:25 +0000 Subject: [PATCH 03/73] chore(M001/S01): auto-commit after state-rebuild --- .gitignore | 25 +++++++++++++++++++++++++ .gsd/doctor-history.jsonl | 1 + 2 files changed, 26 insertions(+) create mode 100644 .gsd/doctor-history.jsonl diff --git a/.gitignore b/.gitignore index 9a541d2b..2f788e2f 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,28 @@ scripts/ /scripts/* # worktrees .worktrees/ + +# ── GSD baseline (auto-generated) ── +Thumbs.db +*.swp +*.swo +*~ +.idea/ +.vscode/ +*.code-workspace +.env.* +!.env.example +node_modules/ +.next/ +dist/ +build/ +__pycache__/ +*.pyc +.venv/ +venv/ +target/ +vendor/ +*.log +coverage/ +.cache/ +tmp/ diff --git a/.gsd/doctor-history.jsonl b/.gsd/doctor-history.jsonl new file mode 100644 index 00000000..1effb288 --- /dev/null +++ b/.gsd/doctor-history.jsonl @@ -0,0 +1 @@ +{"ts":"2026-03-22T22:12:25.936Z","ok":true,"errors":0,"warnings":2,"fixes":2,"codes":["gitignore_missing_patterns","missing_slice_plan"],"issues":[{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"},{"severity":"warning","code":"missing_slice_plan","message":"Slice M001/S01 has no plan file","unitId":"M001/S01"}],"fixDescriptions":["added missing GSD runtime patterns to .gitignore","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"Clean · 2 fixed · 5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json"} From ee94dd87469b7d7b821aee249ef939e106d75e28 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sun, 22 Mar 2026 23:31:24 +0000 Subject: [PATCH 04/73] docs(S01): add slice plan --- .gsd/REQUIREMENTS.md | 40 ++++----- .gsd/milestones/M001/slices/S01/S01-PLAN.md | 55 ++++++++++++ .../M001/slices/S01/tasks/T01-PLAN.md | 65 ++++++++++++++ .../M001/slices/S01/tasks/T02-PLAN.md | 85 +++++++++++++++++++ 4 files changed, 223 insertions(+), 22 deletions(-) create mode 100644 .gsd/milestones/M001/slices/S01/S01-PLAN.md create mode 100644 .gsd/milestones/M001/slices/S01/tasks/T01-PLAN.md create mode 100644 .gsd/milestones/M001/slices/S01/tasks/T02-PLAN.md diff --git a/.gsd/REQUIREMENTS.md b/.gsd/REQUIREMENTS.md index 7e77a9ea..b05e083d 100644 --- a/.gsd/REQUIREMENTS.md +++ b/.gsd/REQUIREMENTS.md @@ -4,7 +4,7 @@ This file is the explicit capability and coverage contract for the project. ## Active -### R001 — Zero TypeScript errors +### R001 — `tsc --noEmit` must pass with zero errors. Currently 8 errors: stale Prisma client fields in `event-log.tsx` destructuring, missing `monaco-editor` type resolution in `vrl-editor.tsx` and `vrl-language.ts`. - Class: quality-attribute - Status: active - Description: `tsc --noEmit` must pass with zero errors. Currently 8 errors: stale Prisma client fields in `event-log.tsx` destructuring, missing `monaco-editor` type resolution in `vrl-editor.tsx` and `vrl-language.ts`. @@ -12,10 +12,10 @@ This file is the explicit capability and coverage contract for the project. - Source: execution - Primary owning slice: M001/S01 - Supporting slices: none -- Validation: unmapped +- Validation: `pnpm exec tsc --noEmit` exits 0 — already passing, S01 verifies no regression - Notes: Prisma generate fixes most errors; remaining are event-log destructuring bug and monaco-editor module resolution. -### R002 — Foundational test coverage for critical paths +### R002 — Test suite exists with coverage for auth flows (login, 2FA, OIDC), pipeline CRUD, deploy operations, and alert evaluation. Test runner configured and passing in CI. - Class: quality-attribute - Status: active - Description: Test suite exists with coverage for auth flows (login, 2FA, OIDC), pipeline CRUD, deploy operations, and alert evaluation. Test runner configured and passing in CI. @@ -26,7 +26,7 @@ This file is the explicit capability and coverage contract for the project. - Validation: unmapped - Notes: Need to set up test infrastructure from scratch — runner, Prisma mocking strategy, test utilities. -### R003 — No source file over ~800 lines +### R003 — All `.ts`/`.tsx` source files (excluding generated code) should be under ~800 lines. Currently 10+ files over 600 lines, with the alerts page at 1910 lines. - Class: quality-attribute - Status: active - Description: All `.ts`/`.tsx` source files (excluding generated code) should be under ~800 lines. Currently 10+ files over 600 lines, with the alerts page at 1910 lines. @@ -37,7 +37,7 @@ This file is the explicit capability and coverage contract for the project. - Validation: unmapped - Notes: Biggest offenders: alerts page (1910), vrl function-registry (1775), pipeline router (1318), dashboard router (1074), flow-store (951), team-settings (865), users-settings (813), vrl-editor (795). -### R004 — Duplicated utilities extracted to shared modules +### R004 — Utility functions duplicated across files (e.g., `aggregateProcessStatus` in 3 files, `derivePipelineStatus` in dashboard page) are extracted to shared modules in `src/lib/`. - Class: quality-attribute - Status: active - Description: Utility functions duplicated across files (e.g., `aggregateProcessStatus` in 3 files, `derivePipelineStatus` in dashboard page) are extracted to shared modules in `src/lib/`. @@ -45,10 +45,10 @@ This file is the explicit capability and coverage contract for the project. - Source: execution - Primary owning slice: M001/S01 - Supporting slices: M001/S02 -- Validation: unmapped +- Validation: S01/T01 creates shared modules, S01/T02 removes all inline duplicates; verified by grep checks returning no matches - Notes: Scout found `aggregateProcessStatus` in pipelines/page.tsx, pipelines/[id]/page.tsx, and dashboard page.tsx. -### R005 — Consistent loading/empty/error states across all pages +### R005 — All 35+ dashboard pages have consistent loading skeletons, empty state messaging with CTAs, and error handling. No page should show a blank white screen during loading or when data is empty. - Class: primary-user-loop - Status: active - Description: All 35+ dashboard pages have consistent loading skeletons, empty state messaging with CTAs, and error handling. No page should show a blank white screen during loading or when data is empty. @@ -59,7 +59,7 @@ This file is the explicit capability and coverage contract for the project. - Validation: unmapped - Notes: Most pages already have Skeleton loading — need to audit for gaps and standardize the pattern. -### R006 — UI consistency sweep — visual rough edges cleaned +### R006 — General UI polish pass — consistent spacing, typography, icon usage, button patterns, table styles, dialog patterns, and visual consistency across all dashboard pages. - Class: primary-user-loop - Status: active - Description: General UI polish pass — consistent spacing, typography, icon usage, button patterns, table styles, dialog patterns, and visual consistency across all dashboard pages. @@ -70,7 +70,7 @@ This file is the explicit capability and coverage contract for the project. - Validation: unmapped - Notes: General sweep based on code audit — no specific user-reported pain points. -### R007 — Inline router business logic extracted to services +### R007 — Complex business logic currently inline in tRPC router handlers is extracted to service modules in `src/server/services/`. Routers become thin orchestration layers. - Class: quality-attribute - Status: active - Description: Complex business logic currently inline in tRPC router handlers is extracted to service modules in `src/server/services/`. Routers become thin orchestration layers. @@ -81,7 +81,7 @@ This file is the explicit capability and coverage contract for the project. - Validation: unmapped - Notes: Pipeline router (1318 lines) and dashboard router (1074 lines) are the primary targets. Some routers already delegate to services well. -### R008 — Clean lint pass +### R008 — `eslint` runs clean with no errors across the codebase. - Class: quality-attribute - Status: active - Description: `eslint` runs clean with no errors across the codebase. @@ -89,10 +89,10 @@ This file is the explicit capability and coverage contract for the project. - Source: inferred - Primary owning slice: M001/S01 - Supporting slices: none -- Validation: unmapped +- Validation: `pnpm exec eslint src/` exits 0 — already passing, S01 verifies no regression - Notes: ESLint config uses next/core-web-vitals and next/typescript presets. -### R010 — Performance audit — bundle size, runtime perf, query efficiency +### R010 — Analyze Next.js bundle size, identify large dependencies or unnecessary client-side imports, review Prisma query patterns for N+1 or missing indexes, and address measurable bottlenecks found. - Class: quality-attribute - Status: active - Description: Analyze Next.js bundle size, identify large dependencies or unnecessary client-side imports, review Prisma query patterns for N+1 or missing indexes, and address measurable bottlenecks found. @@ -103,13 +103,9 @@ This file is the explicit capability and coverage contract for the project. - Validation: unmapped - Notes: Includes bundle analysis, Prisma query review, and runtime profiling of heavy pages (dashboard, pipeline editor, fleet). -## Validated - -(none yet) - ## Deferred -### R009 — Build succeeds without ignoreBuildErrors workaround +### R009 — Remove `ignoreBuildErrors: true` from `next.config.ts` so `next build` type-checks without bypassing errors. - Class: quality-attribute - Status: deferred - Description: Remove `ignoreBuildErrors: true` from `next.config.ts` so `next build` type-checks without bypassing errors. @@ -122,7 +118,7 @@ This file is the explicit capability and coverage contract for the project. ## Out of Scope -### R011 — Accessibility audit (WCAG compliance) +### R011 — Full accessibility audit and remediation - Class: quality-attribute - Status: out-of-scope - Description: Full accessibility audit and remediation @@ -133,7 +129,7 @@ This file is the explicit capability and coverage contract for the project. - Validation: n/a - Notes: Future milestone candidate -### R012 — New feature development +### R012 — No new features are added in this milestone — purely quality improvements to existing functionality - Class: constraint - Status: out-of-scope - Description: No new features are added in this milestone — purely quality improvements to existing functionality @@ -148,14 +144,14 @@ This file is the explicit capability and coverage contract for the project. | ID | Class | Status | Primary owner | Supporting | Proof | |---|---|---|---|---|---| -| R001 | quality-attribute | active | M001/S01 | none | unmapped | +| R001 | quality-attribute | active | M001/S01 | none | `pnpm exec tsc --noEmit` exits 0 — already passing, S01 verifies no regression | | R002 | quality-attribute | active | M001/S04 | none | unmapped | | R003 | quality-attribute | active | M001/S02 | none | unmapped | -| R004 | quality-attribute | active | M001/S01 | M001/S02 | unmapped | +| R004 | quality-attribute | active | M001/S01 | M001/S02 | S01/T01 creates shared modules, S01/T02 removes all inline duplicates; verified by grep checks returning no matches | | R005 | primary-user-loop | active | M001/S03 | none | unmapped | | R006 | primary-user-loop | active | M001/S03 | none | unmapped | | R007 | quality-attribute | active | M001/S02 | M001/S04 | unmapped | -| R008 | quality-attribute | active | M001/S01 | none | unmapped | +| R008 | quality-attribute | active | M001/S01 | none | `pnpm exec eslint src/` exits 0 — already passing, S01 verifies no regression | | R009 | quality-attribute | deferred | none | none | unmapped | | R010 | quality-attribute | active | M001/S05 | none | unmapped | | R011 | quality-attribute | out-of-scope | none | none | n/a | diff --git a/.gsd/milestones/M001/slices/S01/S01-PLAN.md b/.gsd/milestones/M001/slices/S01/S01-PLAN.md new file mode 100644 index 00000000..d32e5e59 --- /dev/null +++ b/.gsd/milestones/M001/slices/S01/S01-PLAN.md @@ -0,0 +1,55 @@ +# S01: TypeScript Fixes & Shared Utilities + +**Goal:** All duplicated utility functions are extracted into shared modules in `src/lib/`, with zero regressions to `tsc --noEmit` and `eslint`. +**Demo:** `tsc --noEmit` exits 0, `eslint src/` exits 0, and `rg 'function aggregateProcessStatus|function derivePipelineStatus' src/app src/components` returns no matches — all pipeline status logic lives in `src/lib/pipeline-status.ts`. + +## Must-Haves + +- `src/lib/pipeline-status.ts` exists with `aggregateProcessStatus()` and `derivePipelineStatus()` exports (boundary contract for S02–S05) +- `src/lib/format.ts` exports `formatTime()` (HH:MM) and `formatTimeWithSeconds()` (HH:MM:SS) alongside existing helpers +- `src/lib/status.ts` exports `STATUS_COLORS` and `statusColor()` alongside existing status variant helpers +- All 9 consumer files import from shared modules instead of defining inline duplicates +- `tsc --noEmit` exits 0 (R001 — already passing, must not regress) +- `eslint src/` exits 0 (R008 — already passing, must not regress) + +## Verification + +- `pnpm exec tsc --noEmit` exits 0 +- `pnpm exec eslint src/` exits 0 +- `rg 'function aggregateProcessStatus' src/app src/components` returns no matches +- `rg 'function derivePipelineStatus' src/app src/components` returns no matches +- `rg '^function formatTime' src/app src/components` returns no matches +- `rg '^const STATUS_COLORS' src/components/fleet` returns no matches +- `test -f src/lib/pipeline-status.ts` exits 0 + +## Tasks + +- [ ] **T01: Create shared utility modules for pipeline status, time formatting, and status colors** `est:20m` + - Why: Establishes the shared modules that all consumer files will import from. Creates `src/lib/pipeline-status.ts` (boundary contract for downstream slices) and extends `src/lib/format.ts` and `src/lib/status.ts` with extracted functions. + - Files: `src/lib/pipeline-status.ts`, `src/lib/format.ts`, `src/lib/status.ts` + - Do: (1) Create `src/lib/pipeline-status.ts` with `aggregateProcessStatus()` and `derivePipelineStatus()` copied from existing inline definitions. (2) Add `formatTime()` (HH:MM variant) and `formatTimeWithSeconds()` (HH:MM:SS variant) to `src/lib/format.ts`. (3) Add `STATUS_COLORS` constant and `statusColor()` function to `src/lib/status.ts`. (4) Update the shared `formatTimestamp` in `src/lib/format.ts` to use explicit locale options (year, month, day, hour, minute, second) matching the audit page's more detailed version. + - Verify: `pnpm exec tsc --noEmit` exits 0 (new exports compile cleanly) + - Done when: All three shared modules export the new functions and `tsc --noEmit` passes + +- [ ] **T02: Replace inline duplicate definitions with imports from shared modules** `est:25m` + - Why: Completes R004 by removing all inline duplicates and wiring consumers to the shared modules. This is the task that actually eliminates duplication. + - Files: `src/app/(dashboard)/pipelines/page.tsx`, `src/app/(dashboard)/pipelines/[id]/page.tsx`, `src/app/(dashboard)/page.tsx`, `src/components/dashboard/custom-view.tsx`, `src/components/fleet/event-log.tsx`, `src/components/fleet/status-timeline.tsx`, `src/components/fleet/node-metrics-charts.tsx`, `src/components/fleet/node-logs.tsx`, `src/components/pipeline/pipeline-logs.tsx` + - Do: In each consumer file: (1) Add import for the shared function(s) from `@/lib/pipeline-status`, `@/lib/format`, or `@/lib/status`. (2) Delete the inline function definition. (3) Verify the imported name matches usage — for the HH:MM:SS variant in `node-logs.tsx` and `pipeline-logs.tsx`, import `formatTimeWithSeconds` and alias or rename at call sites. Also update `src/app/(dashboard)/audit/page.tsx` to import `formatTimestamp` from `@/lib/format` and delete the local definition. + - Verify: `pnpm exec tsc --noEmit` exits 0 && `pnpm exec eslint src/` exits 0 && `rg 'function aggregateProcessStatus' src/app src/components` returns nothing + - Done when: No inline duplicate definitions remain in consumer files, all imports resolve, `tsc` and `eslint` both exit 0 + +## Files Likely Touched + +- `src/lib/pipeline-status.ts` (new) +- `src/lib/format.ts` (extend) +- `src/lib/status.ts` (extend) +- `src/app/(dashboard)/pipelines/page.tsx` +- `src/app/(dashboard)/pipelines/[id]/page.tsx` +- `src/app/(dashboard)/page.tsx` +- `src/components/dashboard/custom-view.tsx` +- `src/components/fleet/event-log.tsx` +- `src/components/fleet/status-timeline.tsx` +- `src/components/fleet/node-metrics-charts.tsx` +- `src/components/fleet/node-logs.tsx` +- `src/components/pipeline/pipeline-logs.tsx` +- `src/app/(dashboard)/audit/page.tsx` diff --git a/.gsd/milestones/M001/slices/S01/tasks/T01-PLAN.md b/.gsd/milestones/M001/slices/S01/tasks/T01-PLAN.md new file mode 100644 index 00000000..7e6ae2d7 --- /dev/null +++ b/.gsd/milestones/M001/slices/S01/tasks/T01-PLAN.md @@ -0,0 +1,65 @@ +--- +estimated_steps: 4 +estimated_files: 3 +skills_used: [] +--- + +# T01: Create shared utility modules for pipeline status, time formatting, and status colors + +**Slice:** S01 — TypeScript Fixes & Shared Utilities +**Milestone:** M001 + +## Description + +Create `src/lib/pipeline-status.ts` as a new shared module and extend `src/lib/format.ts` and `src/lib/status.ts` with functions currently duplicated across consumer files. This establishes the shared API surface that T02 will wire consumers into, and produces the `src/lib/pipeline-status.ts` boundary contract that downstream slices S02–S05 depend on. + +## Steps + +1. Create `src/lib/pipeline-status.ts` with two exported functions: + - `aggregateProcessStatus(statuses: Array<{ status: string }>): "RUNNING" | "STARTING" | "STOPPED" | "CRASHED" | "PENDING" | null` — returns the worst-case status across processes. Logic: empty → null, any CRASHED → CRASHED, any STOPPED → STOPPED, any STARTING → STARTING, any PENDING → PENDING, else RUNNING. + - `derivePipelineStatus(nodes: Array<{ pipelineStatus: string }>): string` — derives overall pipeline status from node statuses. Logic: empty → "PENDING", any CRASHED → "CRASHED", any RUNNING → "RUNNING", any STARTING → "STARTING", all STOPPED → "STOPPED", else first node's status. + +2. Add two new exports to `src/lib/format.ts` (append after existing functions): + - `formatTime(date: Date | string): string` — HH:MM format: `new Date(date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })` + - `formatTimeWithSeconds(date: Date | string): string` — HH:MM:SS format: `new Date(date).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false })` + - Also update the existing `formatTimestamp` function to use explicit locale options: `d.toLocaleString(undefined, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit" })` — this makes it match the audit page's more detailed version so the audit page can import it instead of defining its own. + +3. Add two new exports to `src/lib/status.ts` (append after existing functions): + - `const STATUS_COLORS: Record = { HEALTHY: "#22c55e", UNREACHABLE: "#ef4444", DEGRADED: "#f59e0b", UNKNOWN: "#6b7280" }` + - `function statusColor(status: string | null | undefined): string` — returns `STATUS_COLORS[status ?? "UNKNOWN"] ?? "#6b7280"` + +4. Run `pnpm exec tsc --noEmit` to verify the new exports compile cleanly. + +## Must-Haves + +- [ ] `src/lib/pipeline-status.ts` exports `aggregateProcessStatus` and `derivePipelineStatus` +- [ ] `src/lib/format.ts` exports `formatTime` and `formatTimeWithSeconds` +- [ ] `src/lib/status.ts` exports `STATUS_COLORS` and `statusColor` +- [ ] `tsc --noEmit` passes with zero errors + +## Verification + +- `pnpm exec tsc --noEmit` exits 0 +- `test -f src/lib/pipeline-status.ts` exits 0 +- `rg 'export function aggregateProcessStatus' src/lib/pipeline-status.ts` returns a match +- `rg 'export function derivePipelineStatus' src/lib/pipeline-status.ts` returns a match +- `rg 'export function formatTime' src/lib/format.ts` returns a match +- `rg 'export function formatTimeWithSeconds' src/lib/format.ts` returns a match +- `rg 'export const STATUS_COLORS' src/lib/status.ts` returns a match +- `rg 'export function statusColor' src/lib/status.ts` returns a match + +## Inputs + +- `src/lib/format.ts` — existing shared format module to extend +- `src/lib/status.ts` — existing shared status module to extend +- `src/app/(dashboard)/pipelines/page.tsx` — reference implementation for `aggregateProcessStatus` +- `src/app/(dashboard)/page.tsx` — reference implementation for `derivePipelineStatus` +- `src/components/fleet/event-log.tsx` — reference implementation for `STATUS_COLORS`, `statusColor`, and HH:MM `formatTime` +- `src/components/fleet/node-logs.tsx` — reference implementation for HH:MM:SS `formatTime` variant +- `src/app/(dashboard)/audit/page.tsx` — reference for explicit-options `formatTimestamp` + +## Expected Output + +- `src/lib/pipeline-status.ts` — new shared module with `aggregateProcessStatus` and `derivePipelineStatus` +- `src/lib/format.ts` — extended with `formatTime`, `formatTimeWithSeconds`, and updated `formatTimestamp` +- `src/lib/status.ts` — extended with `STATUS_COLORS` and `statusColor` diff --git a/.gsd/milestones/M001/slices/S01/tasks/T02-PLAN.md b/.gsd/milestones/M001/slices/S01/tasks/T02-PLAN.md new file mode 100644 index 00000000..6ae2c58c --- /dev/null +++ b/.gsd/milestones/M001/slices/S01/tasks/T02-PLAN.md @@ -0,0 +1,85 @@ +--- +estimated_steps: 4 +estimated_files: 10 +skills_used: [] +--- + +# T02: Replace inline duplicate definitions with imports from shared modules + +**Slice:** S01 — TypeScript Fixes & Shared Utilities +**Milestone:** M001 + +## Description + +Remove all inline duplicate utility functions from consumer files and replace them with imports from the shared modules created in T01. This completes R004 (duplicated utilities extracted to `src/lib/`) and ensures no inline copies remain. + +## Steps + +1. Update pipeline status consumers (4 files): + - `src/app/(dashboard)/pipelines/page.tsx` — add `import { aggregateProcessStatus } from "@/lib/pipeline-status"`, delete the inline `function aggregateProcessStatus(...)` definition (approximately lines near the top of the file, before the component). + - `src/app/(dashboard)/pipelines/[id]/page.tsx` — same: add import, delete inline definition. + - `src/app/(dashboard)/page.tsx` — add `import { derivePipelineStatus } from "@/lib/pipeline-status"`, delete the inline `function derivePipelineStatus(...)` definition. + - `src/components/dashboard/custom-view.tsx` — same: add import, delete inline definition. + +2. Update formatTime consumers (5 files): + - `src/components/fleet/event-log.tsx` — add `import { formatTime } from "@/lib/format"` and `import { STATUS_COLORS, statusColor } from "@/lib/status"`. Delete the inline `function formatTime(...)`, `const STATUS_COLORS = ...`, and `function statusColor(...)` definitions. + - `src/components/fleet/status-timeline.tsx` — add `import { formatTime } from "@/lib/format"` and `import { STATUS_COLORS, statusColor } from "@/lib/status"`. Delete the inline `function formatTime(...)`, `const STATUS_COLORS = ...`, and `function statusColor(...)` definitions. + - `src/components/fleet/node-metrics-charts.tsx` — add `import { formatTime } from "@/lib/format"`. Delete the inline `function formatTime(...)` definition. + - `src/components/fleet/node-logs.tsx` — add `import { formatTimeWithSeconds } from "@/lib/format"`. Delete the inline `function formatTime(...)` definition. Rename all call sites from `formatTime(...)` to `formatTimeWithSeconds(...)`. + - `src/components/pipeline/pipeline-logs.tsx` — add `import { formatTimeWithSeconds } from "@/lib/format"`. Delete the inline `function formatTime(...)` definition. Rename all call sites from `formatTime(...)` to `formatTimeWithSeconds(...)`. + +3. Update audit page formatTimestamp: + - `src/app/(dashboard)/audit/page.tsx` — add `import { formatTimestamp } from "@/lib/format"`. Delete the inline `function formatTimestamp(...)` definition. + +4. Run full verification: + - `pnpm exec tsc --noEmit` — must exit 0 + - `pnpm exec eslint src/` — must exit 0 + - Grep checks to confirm no inline duplicates remain + +## Must-Haves + +- [ ] No inline `aggregateProcessStatus` definitions in `src/app/` or `src/components/` +- [ ] No inline `derivePipelineStatus` definitions in `src/app/` or `src/components/` +- [ ] No inline `formatTime` definitions in `src/app/` or `src/components/` +- [ ] No inline `STATUS_COLORS` / `statusColor` definitions in `src/components/fleet/` +- [ ] `tsc --noEmit` exits 0 +- [ ] `eslint src/` exits 0 + +## Verification + +- `pnpm exec tsc --noEmit` exits 0 +- `pnpm exec eslint src/` exits 0 +- `rg 'function aggregateProcessStatus' src/app src/components` returns no matches (exit code 1) +- `rg 'function derivePipelineStatus' src/app src/components` returns no matches (exit code 1) +- `rg '^function formatTime' src/app src/components` returns no matches (exit code 1) +- `rg '^const STATUS_COLORS' src/components/fleet` returns no matches (exit code 1) +- `rg '^function formatTimestamp' src/app` returns no matches (exit code 1) + +## Inputs + +- `src/lib/pipeline-status.ts` — shared module created in T01 (exports `aggregateProcessStatus`, `derivePipelineStatus`) +- `src/lib/format.ts` — shared format module extended in T01 (exports `formatTime`, `formatTimeWithSeconds`, `formatTimestamp`) +- `src/lib/status.ts` — shared status module extended in T01 (exports `STATUS_COLORS`, `statusColor`) +- `src/app/(dashboard)/pipelines/page.tsx` — consumer to update +- `src/app/(dashboard)/pipelines/[id]/page.tsx` — consumer to update +- `src/app/(dashboard)/page.tsx` — consumer to update +- `src/components/dashboard/custom-view.tsx` — consumer to update +- `src/components/fleet/event-log.tsx` — consumer to update +- `src/components/fleet/status-timeline.tsx` — consumer to update +- `src/components/fleet/node-metrics-charts.tsx` — consumer to update +- `src/components/fleet/node-logs.tsx` — consumer to update +- `src/components/pipeline/pipeline-logs.tsx` — consumer to update +- `src/app/(dashboard)/audit/page.tsx` — consumer to update + +## Expected Output + +- `src/app/(dashboard)/pipelines/page.tsx` — imports `aggregateProcessStatus` from shared module, inline definition removed +- `src/app/(dashboard)/pipelines/[id]/page.tsx` — imports `aggregateProcessStatus` from shared module, inline definition removed +- `src/app/(dashboard)/page.tsx` — imports `derivePipelineStatus` from shared module, inline definition removed +- `src/components/dashboard/custom-view.tsx` — imports `derivePipelineStatus` from shared module, inline definition removed +- `src/components/fleet/event-log.tsx` — imports `formatTime`, `STATUS_COLORS`, `statusColor` from shared modules, inline definitions removed +- `src/components/fleet/status-timeline.tsx` — imports `formatTime`, `STATUS_COLORS`, `statusColor` from shared modules, inline definitions removed +- `src/components/fleet/node-metrics-charts.tsx` — imports `formatTime` from shared module, inline definition removed +- `src/components/fleet/node-logs.tsx` — imports `formatTimeWithSeconds` from shared module, inline definition removed, call sites renamed +- `src/components/pipeline/pipeline-logs.tsx` — imports `formatTimeWithSeconds` from shared module, inline definition removed, call sites renamed +- `src/app/(dashboard)/audit/page.tsx` — imports `formatTimestamp` from shared module, inline definition removed From ad3758f8c06d9e9d2f5b8f4fe1d6c09bf76efc30 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sun, 22 Mar 2026 23:31:32 +0000 Subject: [PATCH 05/73] chore(M001/S01): auto-commit after plan-slice --- .gsd/gsd.db-shm | Bin 0 -> 32768 bytes .gsd/gsd.db-wal | Bin 0 -> 201912 bytes .gsd/journal/2026-03-22.jsonl | 4 ++++ 3 files changed, 4 insertions(+) create mode 100644 .gsd/gsd.db-shm create mode 100644 .gsd/gsd.db-wal diff --git a/.gsd/gsd.db-shm b/.gsd/gsd.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..b0f4f01ca27bf1f9646bd5ee1a413e374bebbdd6 GIT binary patch literal 32768 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 HvF`u>>#L5z literal 0 HcmV?d00001 diff --git a/.gsd/journal/2026-03-22.jsonl b/.gsd/journal/2026-03-22.jsonl index 4a531d9b..7d68bec1 100644 --- a/.gsd/journal/2026-03-22.jsonl +++ b/.gsd/journal/2026-03-22.jsonl @@ -2,3 +2,7 @@ {"ts":"2026-03-22T22:05:18.028Z","flowId":"dfc4628d-0bfb-4a52-b119-cf64f821f058","seq":2,"eventType":"dispatch-match","rule":"planning (no research, not S01) → research-slice","data":{"unitType":"research-slice","unitId":"M001/S01"}} {"ts":"2026-03-22T22:05:18.034Z","flowId":"dfc4628d-0bfb-4a52-b119-cf64f821f058","seq":3,"eventType":"unit-start","data":{"unitType":"research-slice","unitId":"M001/S01"}} {"ts":"2026-03-22T22:12:24.384Z","flowId":"dfc4628d-0bfb-4a52-b119-cf64f821f058","seq":4,"eventType":"unit-end","data":{"unitType":"research-slice","unitId":"M001/S01","status":"completed","artifactVerified":true},"causedBy":{"flowId":"dfc4628d-0bfb-4a52-b119-cf64f821f058","seq":3}} +{"ts":"2026-03-22T23:28:13.562Z","flowId":"462cf86c-8675-4274-9ebe-6afa3cef9e5b","seq":1,"eventType":"iteration-start","data":{"iteration":1}} +{"ts":"2026-03-22T23:28:15.905Z","flowId":"462cf86c-8675-4274-9ebe-6afa3cef9e5b","seq":2,"eventType":"dispatch-match","rule":"planning → plan-slice","data":{"unitType":"plan-slice","unitId":"M001/S01"}} +{"ts":"2026-03-22T23:28:15.912Z","flowId":"462cf86c-8675-4274-9ebe-6afa3cef9e5b","seq":3,"eventType":"unit-start","data":{"unitType":"plan-slice","unitId":"M001/S01"}} +{"ts":"2026-03-22T23:31:31.995Z","flowId":"462cf86c-8675-4274-9ebe-6afa3cef9e5b","seq":4,"eventType":"unit-end","data":{"unitType":"plan-slice","unitId":"M001/S01","status":"completed","artifactVerified":true},"causedBy":{"flowId":"462cf86c-8675-4274-9ebe-6afa3cef9e5b","seq":3}} From eb900c87e6f5791c955ccb110222984eecc67b45 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sun, 22 Mar 2026 23:31:33 +0000 Subject: [PATCH 06/73] chore(M001/S01): auto-commit after state-rebuild --- .gsd/doctor-history.jsonl | 1 + .gsd/gsd.db-wal | Bin 201912 -> 210152 bytes 2 files changed, 1 insertion(+) diff --git a/.gsd/doctor-history.jsonl b/.gsd/doctor-history.jsonl index 1effb288..e2d79ee4 100644 --- a/.gsd/doctor-history.jsonl +++ b/.gsd/doctor-history.jsonl @@ -1 +1,2 @@ {"ts":"2026-03-22T22:12:25.936Z","ok":true,"errors":0,"warnings":2,"fixes":2,"codes":["gitignore_missing_patterns","missing_slice_plan"],"issues":[{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"},{"severity":"warning","code":"missing_slice_plan","message":"Slice M001/S01 has no plan file","unitId":"M001/S01"}],"fixDescriptions":["added missing GSD runtime patterns to .gitignore","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"Clean · 2 fixed · 5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json"} +{"ts":"2026-03-22T23:31:32.945Z","ok":true,"errors":0,"warnings":2,"fixes":3,"codes":["state_file_stale","gitignore_missing_patterns"],"issues":[{"severity":"warning","code":"state_file_stale","message":"STATE.md is stale — shows \"planning\" but derived state is \"executing\"","unitId":"project"},{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"}],"fixDescriptions":["rebuilt STATE.md from derived state","added missing GSD runtime patterns to .gitignore","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"Clean · 3 fixed · STATE.md is stale — shows \"planning\" but derived state is \"executing\""} diff --git a/.gsd/gsd.db-wal b/.gsd/gsd.db-wal index 92d9abbcb2b9baecc37dde7cf65f4dc3bc47e5f2..4dcddf06137fc6448c83cad6a6b847f8bdaf82dc 100644 GIT binary patch delta 60 zcmdn7lIO)so`x327N#xCJ47TH7#KK!nBo7cGoHt-0-L)00(zd+aVl(I@P$c0fTJ%h P;o`bKB^k>DgqZ{Yt=tyK delta 15 WcmaFyl4r+Co`x327N#xCJ466Gf(BUt From 5f5c6153fdc7646d6736c1b8677c47afb8a7bdb9 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sun, 22 Mar 2026 23:33:52 +0000 Subject: [PATCH 07/73] =?UTF-8?q?feat(S01/T01):=20Created=20src/lib/pipeli?= =?UTF-8?q?ne-status.ts=20and=20extended=20format.t=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/lib/pipeline-status.ts - src/lib/format.ts - src/lib/status.ts --- .gsd/gsd.db-wal | Bin 210152 -> 333752 bytes .gsd/journal/2026-03-22.jsonl | 5 + .gsd/milestones/M001/slices/S01/S01-PLAN.md | 8 +- .../M001/slices/S01/tasks/T01-PLAN.md | 6 ++ .../M001/slices/S01/tasks/T01-SUMMARY.md | 87 ++++++++++++++++++ src/lib/format.ts | 29 +++++- src/lib/pipeline-status.ts | 38 ++++++++ src/lib/status.ts | 13 +++ 8 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 .gsd/milestones/M001/slices/S01/tasks/T01-SUMMARY.md create mode 100644 src/lib/pipeline-status.ts diff --git a/.gsd/gsd.db-wal b/.gsd/gsd.db-wal index 4dcddf06137fc6448c83cad6a6b847f8bdaf82dc..bca33e4d131d554073a254b25d3eaf6b349bd568 100644 GIT binary patch delta 13427 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 JkCV=Z{{t%~!dd_T delta 15 Wcmdn7UgX6~o`x327N#xCFH8VCZU(mi diff --git a/.gsd/journal/2026-03-22.jsonl b/.gsd/journal/2026-03-22.jsonl index 7d68bec1..8eab30a1 100644 --- a/.gsd/journal/2026-03-22.jsonl +++ b/.gsd/journal/2026-03-22.jsonl @@ -6,3 +6,8 @@ {"ts":"2026-03-22T23:28:15.905Z","flowId":"462cf86c-8675-4274-9ebe-6afa3cef9e5b","seq":2,"eventType":"dispatch-match","rule":"planning → plan-slice","data":{"unitType":"plan-slice","unitId":"M001/S01"}} {"ts":"2026-03-22T23:28:15.912Z","flowId":"462cf86c-8675-4274-9ebe-6afa3cef9e5b","seq":3,"eventType":"unit-start","data":{"unitType":"plan-slice","unitId":"M001/S01"}} {"ts":"2026-03-22T23:31:31.995Z","flowId":"462cf86c-8675-4274-9ebe-6afa3cef9e5b","seq":4,"eventType":"unit-end","data":{"unitType":"plan-slice","unitId":"M001/S01","status":"completed","artifactVerified":true},"causedBy":{"flowId":"462cf86c-8675-4274-9ebe-6afa3cef9e5b","seq":3}} +{"ts":"2026-03-22T23:31:33.041Z","flowId":"462cf86c-8675-4274-9ebe-6afa3cef9e5b","seq":5,"eventType":"iteration-end","data":{"iteration":1}} +{"ts":"2026-03-22T23:31:33.042Z","flowId":"02dd6adc-7fce-4862-bb3b-3d3353ca8663","seq":1,"eventType":"iteration-start","data":{"iteration":2}} +{"ts":"2026-03-22T23:31:33.270Z","flowId":"02dd6adc-7fce-4862-bb3b-3d3353ca8663","seq":2,"eventType":"dispatch-match","rule":"executing → execute-task","data":{"unitType":"execute-task","unitId":"M001/S01/T01"}} +{"ts":"2026-03-22T23:31:33.284Z","flowId":"02dd6adc-7fce-4862-bb3b-3d3353ca8663","seq":3,"eventType":"unit-start","data":{"unitType":"execute-task","unitId":"M001/S01/T01"}} +{"ts":"2026-03-22T23:33:51.941Z","flowId":"02dd6adc-7fce-4862-bb3b-3d3353ca8663","seq":4,"eventType":"unit-end","data":{"unitType":"execute-task","unitId":"M001/S01/T01","status":"completed","artifactVerified":true},"causedBy":{"flowId":"02dd6adc-7fce-4862-bb3b-3d3353ca8663","seq":3}} diff --git a/.gsd/milestones/M001/slices/S01/S01-PLAN.md b/.gsd/milestones/M001/slices/S01/S01-PLAN.md index d32e5e59..0f6996af 100644 --- a/.gsd/milestones/M001/slices/S01/S01-PLAN.md +++ b/.gsd/milestones/M001/slices/S01/S01-PLAN.md @@ -22,9 +22,15 @@ - `rg '^const STATUS_COLORS' src/components/fleet` returns no matches - `test -f src/lib/pipeline-status.ts` exits 0 +## Observability / Diagnostics + +- **Inspection surface:** Shared modules are importable at `@/lib/pipeline-status`, `@/lib/format`, `@/lib/status`. TypeScript compilation (`tsc --noEmit`) validates API surface integrity. +- **Failure visibility:** If a consumer imports a function that doesn't exist or has wrong types, `tsc --noEmit` fails with clear missing-export or type-mismatch errors. +- **Redaction constraints:** None — these are pure formatting/status utilities with no sensitive data. + ## Tasks -- [ ] **T01: Create shared utility modules for pipeline status, time formatting, and status colors** `est:20m` +- [x] **T01: Create shared utility modules for pipeline status, time formatting, and status colors** `est:20m` - Why: Establishes the shared modules that all consumer files will import from. Creates `src/lib/pipeline-status.ts` (boundary contract for downstream slices) and extends `src/lib/format.ts` and `src/lib/status.ts` with extracted functions. - Files: `src/lib/pipeline-status.ts`, `src/lib/format.ts`, `src/lib/status.ts` - Do: (1) Create `src/lib/pipeline-status.ts` with `aggregateProcessStatus()` and `derivePipelineStatus()` copied from existing inline definitions. (2) Add `formatTime()` (HH:MM variant) and `formatTimeWithSeconds()` (HH:MM:SS variant) to `src/lib/format.ts`. (3) Add `STATUS_COLORS` constant and `statusColor()` function to `src/lib/status.ts`. (4) Update the shared `formatTimestamp` in `src/lib/format.ts` to use explicit locale options (year, month, day, hour, minute, second) matching the audit page's more detailed version. diff --git a/.gsd/milestones/M001/slices/S01/tasks/T01-PLAN.md b/.gsd/milestones/M001/slices/S01/tasks/T01-PLAN.md index 7e6ae2d7..a026f283 100644 --- a/.gsd/milestones/M001/slices/S01/tasks/T01-PLAN.md +++ b/.gsd/milestones/M001/slices/S01/tasks/T01-PLAN.md @@ -63,3 +63,9 @@ Create `src/lib/pipeline-status.ts` as a new shared module and extend `src/lib/f - `src/lib/pipeline-status.ts` — new shared module with `aggregateProcessStatus` and `derivePipelineStatus` - `src/lib/format.ts` — extended with `formatTime`, `formatTimeWithSeconds`, and updated `formatTimestamp` - `src/lib/status.ts` — extended with `STATUS_COLORS` and `statusColor` + +## Observability Impact + +- **Signals added:** No runtime signals — these are pure compile-time utility exports. The observable signal is `tsc --noEmit` exit code, which validates that all exported type signatures are correct. +- **Inspection:** Future agents can verify the shared API surface with `rg 'export function|export const' src/lib/pipeline-status.ts src/lib/format.ts src/lib/status.ts`. +- **Failure state:** If any exported function signature is wrong, downstream consumer imports (wired in T02) will produce TypeScript errors visible via `tsc --noEmit`. diff --git a/.gsd/milestones/M001/slices/S01/tasks/T01-SUMMARY.md b/.gsd/milestones/M001/slices/S01/tasks/T01-SUMMARY.md new file mode 100644 index 00000000..a63205e7 --- /dev/null +++ b/.gsd/milestones/M001/slices/S01/tasks/T01-SUMMARY.md @@ -0,0 +1,87 @@ +--- +id: T01 +parent: S01 +milestone: M001 +provides: + - shared pipeline-status module (aggregateProcessStatus, derivePipelineStatus) + - shared formatTime and formatTimeWithSeconds in format.ts + - shared STATUS_COLORS and statusColor in status.ts + - updated formatTimestamp with explicit locale options +key_files: + - src/lib/pipeline-status.ts + - src/lib/format.ts + - src/lib/status.ts +key_decisions: + - Matched reference implementations exactly (no logic changes) to ensure zero-risk extraction +patterns_established: + - Pipeline status derivation lives in src/lib/pipeline-status.ts — all consumers must import from there + - Node health color mapping lives in src/lib/status.ts alongside status variant helpers +observability_surfaces: + - tsc --noEmit validates exported type signatures; rg can verify no inline duplicates remain after T02 +duration: 5m +verification_result: passed +completed_at: 2026-03-22 +blocker_discovered: false +--- + +# T01: Create shared utility modules for pipeline status, time formatting, and status colors + +**Created src/lib/pipeline-status.ts and extended format.ts and status.ts with extracted shared utilities for T02 consumer wiring** + +## What Happened + +Created `src/lib/pipeline-status.ts` as a new shared module with `aggregateProcessStatus` and `derivePipelineStatus`, copied verbatim from their reference implementations in `pipelines/page.tsx` and `page.tsx`. Extended `src/lib/format.ts` with `formatTime` (HH:MM) and `formatTimeWithSeconds` (HH:MM:SS), and updated the existing `formatTimestamp` to use explicit locale options matching the audit page's version. Extended `src/lib/status.ts` with `STATUS_COLORS` constant and `statusColor` function from `event-log.tsx`. + +All implementations were verified against their reference sources to ensure behavioral equivalence. + +## Verification + +- `tsc --noEmit` exits 0 — all new exports compile cleanly +- `test -f src/lib/pipeline-status.ts` exits 0 — file exists +- All 8 rg checks for exported symbols match in the correct files + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `pnpm exec tsc --noEmit` | 0 | ✅ pass | 1.8s | +| 2 | `test -f src/lib/pipeline-status.ts` | 0 | ✅ pass | 0s | +| 3 | `rg 'export function aggregateProcessStatus' src/lib/pipeline-status.ts` | 0 | ✅ pass | 0s | +| 4 | `rg 'export function derivePipelineStatus' src/lib/pipeline-status.ts` | 0 | ✅ pass | 0s | +| 5 | `rg 'export function formatTime' src/lib/format.ts` | 0 | ✅ pass | 0s | +| 6 | `rg 'export function formatTimeWithSeconds' src/lib/format.ts` | 0 | ✅ pass | 0s | +| 7 | `rg 'export const STATUS_COLORS' src/lib/status.ts` | 0 | ✅ pass | 0s | +| 8 | `rg 'export function statusColor' src/lib/status.ts` | 0 | ✅ pass | 0s | + +### Slice-level checks (partial — T02 will complete these) + +| # | Command | Exit Code | Verdict | Notes | +|---|---------|-----------|---------|-------| +| 1 | `pnpm exec tsc --noEmit` | 0 | ✅ pass | | +| 2 | `rg 'function aggregateProcessStatus' src/app src/components` | 0 (matches) | ⏳ expected | Inline copies remain until T02 | +| 3 | `rg 'function derivePipelineStatus' src/app src/components` | 0 (matches) | ⏳ expected | Inline copies remain until T02 | +| 4 | `rg '^function formatTime' src/app src/components` | 0 (matches) | ⏳ expected | Inline copies remain until T02 | +| 5 | `rg '^const STATUS_COLORS' src/components/fleet` | 0 (matches) | ⏳ expected | Inline copies remain until T02 | +| 6 | `test -f src/lib/pipeline-status.ts` | 0 | ✅ pass | | + +## Diagnostics + +- Verify shared API surface: `rg 'export function|export const' src/lib/pipeline-status.ts src/lib/format.ts src/lib/status.ts` +- Compilation check: `pnpm exec tsc --noEmit` — any export signature mismatch will surface here once T02 wires consumers +- No runtime signals — these are pure utility functions with no side effects + +## Deviations + +None — all implementations matched the plan exactly. + +## Known Issues + +None. + +## Files Created/Modified + +- `src/lib/pipeline-status.ts` — new shared module with `aggregateProcessStatus` and `derivePipelineStatus` +- `src/lib/format.ts` — added `formatTime`, `formatTimeWithSeconds`; updated `formatTimestamp` with explicit locale options +- `src/lib/status.ts` — added `STATUS_COLORS` constant and `statusColor` function +- `.gsd/milestones/M001/slices/S01/S01-PLAN.md` — added Observability / Diagnostics section (pre-flight fix) +- `.gsd/milestones/M001/slices/S01/tasks/T01-PLAN.md` — added Observability Impact section (pre-flight fix) diff --git a/src/lib/format.ts b/src/lib/format.ts index 6b3e90fe..cad09c6a 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -53,7 +53,15 @@ export function formatLastSeen(date: Date | string | null): string { export function formatTimestamp(date: Date | string | null): string { if (!date) return "Never"; - return new Date(date).toLocaleString(); + const d = new Date(date); + return d.toLocaleString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); } /** SI-suffix formatter for chart Y-axes: 1000 → "1K", 1500000 → "1.5M" */ @@ -81,3 +89,22 @@ export function formatLatency(ms: number): string { if (ms >= 0.001) return `${(ms * 1000).toFixed(0)}us`; return `${ms.toFixed(3)}ms`; } + +/** Format a date/string to HH:MM (locale-aware, 2-digit hour and minute). */ +export function formatTime(date: Date | string): string { + return new Date(date).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }); +} + +/** Format a date/string to HH:MM:SS (24-hour, no AM/PM). */ +export function formatTimeWithSeconds(date: Date | string): string { + const d = new Date(date); + return d.toLocaleTimeString(undefined, { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }); +} diff --git a/src/lib/pipeline-status.ts b/src/lib/pipeline-status.ts new file mode 100644 index 00000000..8fd7b6b1 --- /dev/null +++ b/src/lib/pipeline-status.ts @@ -0,0 +1,38 @@ +/** + * Shared pipeline status derivation functions. + * + * These are the canonical implementations — all consumer files should + * import from here instead of defining inline copies. + */ + +/** + * Returns the worst-case status across an array of process statuses. + * Priority: CRASHED > STOPPED > STARTING > PENDING > RUNNING. + * Returns null for an empty array. + */ +export function aggregateProcessStatus( + statuses: Array<{ status: string }> +): "RUNNING" | "STARTING" | "STOPPED" | "CRASHED" | "PENDING" | null { + if (statuses.length === 0) return null; + if (statuses.some((s) => s.status === "CRASHED")) return "CRASHED"; + if (statuses.some((s) => s.status === "STOPPED")) return "STOPPED"; + if (statuses.some((s) => s.status === "STARTING")) return "STARTING"; + if (statuses.some((s) => s.status === "PENDING")) return "PENDING"; + return "RUNNING"; +} + +/** + * Derives an overall pipeline status from its node statuses. + * Logic: empty → PENDING, any CRASHED → CRASHED, any RUNNING → RUNNING, + * any STARTING → STARTING, all STOPPED → STOPPED, else first node's status. + */ +export function derivePipelineStatus( + nodes: Array<{ pipelineStatus: string }> +): string { + if (nodes.length === 0) return "PENDING"; + if (nodes.some((n) => n.pipelineStatus === "CRASHED")) return "CRASHED"; + if (nodes.some((n) => n.pipelineStatus === "RUNNING")) return "RUNNING"; + if (nodes.some((n) => n.pipelineStatus === "STARTING")) return "STARTING"; + if (nodes.every((n) => n.pipelineStatus === "STOPPED")) return "STOPPED"; + return nodes[0].pipelineStatus; +} diff --git a/src/lib/status.ts b/src/lib/status.ts index 13722ff2..5ba60794 100644 --- a/src/lib/status.ts +++ b/src/lib/status.ts @@ -53,3 +53,16 @@ export function nodeStatusLabel(status: string): string { default: return status; } } + +/** Hex color map for node health statuses (used in charts, badges, timelines). */ +export const STATUS_COLORS: Record = { + HEALTHY: "#22c55e", + UNREACHABLE: "#ef4444", + DEGRADED: "#f59e0b", + UNKNOWN: "#6b7280", +}; + +/** Returns the hex color for a given node status, defaulting to UNKNOWN gray. */ +export function statusColor(status: string | null | undefined): string { + return STATUS_COLORS[status ?? "UNKNOWN"] ?? "#6b7280"; +} From 79040580df5c99ddab4051e39cd45459665d4652 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sun, 22 Mar 2026 23:33:52 +0000 Subject: [PATCH 08/73] chore(M001/S01/T01): auto-commit after state-rebuild --- .gsd/doctor-history.jsonl | 1 + .gsd/gsd.db-wal | Bin 333752 -> 341992 bytes 2 files changed, 1 insertion(+) diff --git a/.gsd/doctor-history.jsonl b/.gsd/doctor-history.jsonl index e2d79ee4..aab10b1b 100644 --- a/.gsd/doctor-history.jsonl +++ b/.gsd/doctor-history.jsonl @@ -1,2 +1,3 @@ {"ts":"2026-03-22T22:12:25.936Z","ok":true,"errors":0,"warnings":2,"fixes":2,"codes":["gitignore_missing_patterns","missing_slice_plan"],"issues":[{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"},{"severity":"warning","code":"missing_slice_plan","message":"Slice M001/S01 has no plan file","unitId":"M001/S01"}],"fixDescriptions":["added missing GSD runtime patterns to .gitignore","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"Clean · 2 fixed · 5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json"} {"ts":"2026-03-22T23:31:32.945Z","ok":true,"errors":0,"warnings":2,"fixes":3,"codes":["state_file_stale","gitignore_missing_patterns"],"issues":[{"severity":"warning","code":"state_file_stale","message":"STATE.md is stale — shows \"planning\" but derived state is \"executing\"","unitId":"project"},{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"}],"fixDescriptions":["rebuilt STATE.md from derived state","added missing GSD runtime patterns to .gitignore","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"Clean · 3 fixed · STATE.md is stale — shows \"planning\" but derived state is \"executing\""} +{"ts":"2026-03-22T23:33:52.798Z","ok":true,"errors":0,"warnings":1,"fixes":2,"codes":["gitignore_missing_patterns"],"issues":[{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"}],"fixDescriptions":["added missing GSD runtime patterns to .gitignore","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"Clean · 2 fixed · 5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json"} diff --git a/.gsd/gsd.db-wal b/.gsd/gsd.db-wal index bca33e4d131d554073a254b25d3eaf6b349bd568..be62e621e00056594bd159e790438f8cabd4b46d 100644 GIT binary patch delta 53 zcmV-50LuTkuN3I-6o7;QgaU*Egaot&xEGhW6ax?lXtEn#9lDJFx1jd~5D*C03M?y@ Liu((vaMuJ7JXjRg delta 19 acmaFyUS!95k%kt=7N!>F7M3lnJH!E40tblz From 25d2b3bf6ea33d87ca09792ead8c5b398a94607b Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sun, 22 Mar 2026 23:39:26 +0000 Subject: [PATCH 09/73] =?UTF-8?q?feat(S01/T02):=20Replaced=20all=20inline?= =?UTF-8?q?=20duplicate=20utility=20functions=20across=201=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/app/(dashboard)/pipelines/page.tsx - src/app/(dashboard)/pipelines/[id]/page.tsx - src/app/(dashboard)/page.tsx - src/components/dashboard/custom-view.tsx - src/components/fleet/event-log.tsx - src/components/fleet/status-timeline.tsx - src/components/fleet/node-metrics-charts.tsx - src/components/fleet/node-logs.tsx --- .gsd/gsd.db-wal | Bin 341992 -> 465592 bytes .gsd/journal/2026-03-22.jsonl | 5 + .gsd/milestones/M001/slices/S01/S01-PLAN.md | 2 +- .../M001/slices/S01/tasks/T01-VERIFY.json | 16 +++ .../M001/slices/S01/tasks/T02-PLAN.md | 6 + .../M001/slices/S01/tasks/T02-SUMMARY.md | 105 ++++++++++++++++++ src/app/(dashboard)/audit/page.tsx | 13 +-- src/app/(dashboard)/page.tsx | 13 +-- src/app/(dashboard)/pipelines/[id]/page.tsx | 12 +- src/app/(dashboard)/pipelines/page.tsx | 12 +- src/components/dashboard/custom-view.tsx | 13 +-- src/components/fleet/event-log.tsx | 20 +--- src/components/fleet/node-logs.tsx | 13 +-- src/components/fleet/node-metrics-charts.tsx | 9 +- src/components/fleet/status-timeline.tsx | 20 +--- src/components/pipeline/pipeline-logs.tsx | 13 +-- 16 files changed, 147 insertions(+), 125 deletions(-) create mode 100644 .gsd/milestones/M001/slices/S01/tasks/T01-VERIFY.json create mode 100644 .gsd/milestones/M001/slices/S01/tasks/T02-SUMMARY.md diff --git a/.gsd/gsd.db-wal b/.gsd/gsd.db-wal index be62e621e00056594bd159e790438f8cabd4b46d..5af38b7f224c5decf3d8346445c4098891b91942 100644 GIT binary patch delta 5894 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 delta 19 acmdn7Rp!Nek%kt=7N!>F7M3lnFU$d0CI{yL diff --git a/.gsd/journal/2026-03-22.jsonl b/.gsd/journal/2026-03-22.jsonl index 8eab30a1..0bcdd8b0 100644 --- a/.gsd/journal/2026-03-22.jsonl +++ b/.gsd/journal/2026-03-22.jsonl @@ -11,3 +11,8 @@ {"ts":"2026-03-22T23:31:33.270Z","flowId":"02dd6adc-7fce-4862-bb3b-3d3353ca8663","seq":2,"eventType":"dispatch-match","rule":"executing → execute-task","data":{"unitType":"execute-task","unitId":"M001/S01/T01"}} {"ts":"2026-03-22T23:31:33.284Z","flowId":"02dd6adc-7fce-4862-bb3b-3d3353ca8663","seq":3,"eventType":"unit-start","data":{"unitType":"execute-task","unitId":"M001/S01/T01"}} {"ts":"2026-03-22T23:33:51.941Z","flowId":"02dd6adc-7fce-4862-bb3b-3d3353ca8663","seq":4,"eventType":"unit-end","data":{"unitType":"execute-task","unitId":"M001/S01/T01","status":"completed","artifactVerified":true},"causedBy":{"flowId":"02dd6adc-7fce-4862-bb3b-3d3353ca8663","seq":3}} +{"ts":"2026-03-22T23:34:02.589Z","flowId":"02dd6adc-7fce-4862-bb3b-3d3353ca8663","seq":5,"eventType":"iteration-end","data":{"iteration":2}} +{"ts":"2026-03-22T23:34:02.590Z","flowId":"e875b89c-02ee-4737-bfb5-61e8825b15df","seq":1,"eventType":"iteration-start","data":{"iteration":3}} +{"ts":"2026-03-22T23:34:02.857Z","flowId":"e875b89c-02ee-4737-bfb5-61e8825b15df","seq":2,"eventType":"dispatch-match","rule":"executing → execute-task","data":{"unitType":"execute-task","unitId":"M001/S01/T02"}} +{"ts":"2026-03-22T23:34:02.869Z","flowId":"e875b89c-02ee-4737-bfb5-61e8825b15df","seq":3,"eventType":"unit-start","data":{"unitType":"execute-task","unitId":"M001/S01/T02"}} +{"ts":"2026-03-22T23:39:25.849Z","flowId":"e875b89c-02ee-4737-bfb5-61e8825b15df","seq":4,"eventType":"unit-end","data":{"unitType":"execute-task","unitId":"M001/S01/T02","status":"completed","artifactVerified":true},"causedBy":{"flowId":"e875b89c-02ee-4737-bfb5-61e8825b15df","seq":3}} diff --git a/.gsd/milestones/M001/slices/S01/S01-PLAN.md b/.gsd/milestones/M001/slices/S01/S01-PLAN.md index 0f6996af..2eafcf3c 100644 --- a/.gsd/milestones/M001/slices/S01/S01-PLAN.md +++ b/.gsd/milestones/M001/slices/S01/S01-PLAN.md @@ -37,7 +37,7 @@ - Verify: `pnpm exec tsc --noEmit` exits 0 (new exports compile cleanly) - Done when: All three shared modules export the new functions and `tsc --noEmit` passes -- [ ] **T02: Replace inline duplicate definitions with imports from shared modules** `est:25m` +- [x] **T02: Replace inline duplicate definitions with imports from shared modules** `est:25m` - Why: Completes R004 by removing all inline duplicates and wiring consumers to the shared modules. This is the task that actually eliminates duplication. - Files: `src/app/(dashboard)/pipelines/page.tsx`, `src/app/(dashboard)/pipelines/[id]/page.tsx`, `src/app/(dashboard)/page.tsx`, `src/components/dashboard/custom-view.tsx`, `src/components/fleet/event-log.tsx`, `src/components/fleet/status-timeline.tsx`, `src/components/fleet/node-metrics-charts.tsx`, `src/components/fleet/node-logs.tsx`, `src/components/pipeline/pipeline-logs.tsx` - Do: In each consumer file: (1) Add import for the shared function(s) from `@/lib/pipeline-status`, `@/lib/format`, or `@/lib/status`. (2) Delete the inline function definition. (3) Verify the imported name matches usage — for the HH:MM:SS variant in `node-logs.tsx` and `pipeline-logs.tsx`, import `formatTimeWithSeconds` and alias or rename at call sites. Also update `src/app/(dashboard)/audit/page.tsx` to import `formatTimestamp` from `@/lib/format` and delete the local definition. diff --git a/.gsd/milestones/M001/slices/S01/tasks/T01-VERIFY.json b/.gsd/milestones/M001/slices/S01/tasks/T01-VERIFY.json new file mode 100644 index 00000000..1b8bf3c6 --- /dev/null +++ b/.gsd/milestones/M001/slices/S01/tasks/T01-VERIFY.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "taskId": "T01", + "unitId": "M001/S01/T01", + "timestamp": 1774222432903, + "passed": true, + "discoverySource": "package-json", + "checks": [ + { + "command": "npm run lint", + "exitCode": 0, + "durationMs": 9648, + "verdict": "pass" + } + ] +} diff --git a/.gsd/milestones/M001/slices/S01/tasks/T02-PLAN.md b/.gsd/milestones/M001/slices/S01/tasks/T02-PLAN.md index 6ae2c58c..a8f286b3 100644 --- a/.gsd/milestones/M001/slices/S01/tasks/T02-PLAN.md +++ b/.gsd/milestones/M001/slices/S01/tasks/T02-PLAN.md @@ -83,3 +83,9 @@ Remove all inline duplicate utility functions from consumer files and replace th - `src/components/fleet/node-logs.tsx` — imports `formatTimeWithSeconds` from shared module, inline definition removed, call sites renamed - `src/components/pipeline/pipeline-logs.tsx` — imports `formatTimeWithSeconds` from shared module, inline definition removed, call sites renamed - `src/app/(dashboard)/audit/page.tsx` — imports `formatTimestamp` from shared module, inline definition removed + +## Observability Impact + +- **Signals changed:** No new runtime signals. This is a pure refactoring task — import paths change but runtime behavior is identical. +- **Inspection:** `rg 'function aggregateProcessStatus|function derivePipelineStatus' src/app src/components` should return no matches (exit 1). `pnpm exec tsc --noEmit` validates all import paths resolve correctly. +- **Failure visibility:** If a shared module export is removed or renamed, `tsc --noEmit` will fail with a clear missing-export error in the consumer file. If an import is unused, `eslint` will flag it as an `@typescript-eslint/no-unused-vars` warning. diff --git a/.gsd/milestones/M001/slices/S01/tasks/T02-SUMMARY.md b/.gsd/milestones/M001/slices/S01/tasks/T02-SUMMARY.md new file mode 100644 index 00000000..bffb009b --- /dev/null +++ b/.gsd/milestones/M001/slices/S01/tasks/T02-SUMMARY.md @@ -0,0 +1,105 @@ +--- +id: T02 +parent: S01 +milestone: M001 +provides: + - all inline duplicate utility functions replaced with imports from shared modules + - zero inline copies of aggregateProcessStatus, derivePipelineStatus, formatTime, STATUS_COLORS, statusColor, formatTimestamp in consumer files +key_files: + - src/app/(dashboard)/pipelines/page.tsx + - src/app/(dashboard)/pipelines/[id]/page.tsx + - src/app/(dashboard)/page.tsx + - src/components/dashboard/custom-view.tsx + - src/components/fleet/event-log.tsx + - src/components/fleet/status-timeline.tsx + - src/components/fleet/node-metrics-charts.tsx + - src/components/fleet/node-logs.tsx + - src/components/pipeline/pipeline-logs.tsx + - src/app/(dashboard)/audit/page.tsx +key_decisions: + - Removed unused STATUS_COLORS import from event-log.tsx to keep eslint clean (only statusColor is needed there) +patterns_established: + - All pipeline status logic imports from @/lib/pipeline-status; all time formatting imports from @/lib/format; all status color helpers import from @/lib/status +observability_surfaces: + - tsc --noEmit validates all import paths and type signatures; rg confirms no inline duplicates remain; eslint catches unused imports +duration: 5m +verification_result: passed +completed_at: 2026-03-22 +blocker_discovered: false +--- + +# T02: Replace inline duplicate definitions with imports from shared modules + +**Replaced all inline duplicate utility functions across 10 consumer files with imports from shared modules in src/lib/, with zero regressions to tsc and eslint** + +## What Happened + +Removed inline definitions of `aggregateProcessStatus` (2 files), `derivePipelineStatus` (2 files), `formatTime` (4 files), `STATUS_COLORS`/`statusColor` (2 files), and `formatTimestamp` (1 file) from consumer files. Replaced each with imports from the shared modules created in T01 (`@/lib/pipeline-status`, `@/lib/format`, `@/lib/status`). + +For `node-logs.tsx` and `pipeline-logs.tsx`, the inline `formatTime` used seconds-precision formatting (HH:MM:SS), so these were wired to `formatTimeWithSeconds` from `@/lib/format` and all call sites renamed accordingly. + +After initial eslint pass revealed an unused `STATUS_COLORS` import in `event-log.tsx` (only `statusColor` was actually referenced in the component), removed the unnecessary import to achieve a clean lint. + +## Verification + +- `pnpm exec tsc --noEmit` — exits 0, all import paths and type signatures valid +- `pnpm exec eslint src/` — exits 0, no errors or warnings +- `rg 'function aggregateProcessStatus' src/app src/components` — exit 1, no matches +- `rg 'function derivePipelineStatus' src/app src/components` — exit 1, no matches +- `rg '^function formatTime' src/app src/components` — exit 1, no matches +- `rg '^const STATUS_COLORS' src/components/fleet` — exit 1, no matches +- `rg '^function formatTimestamp' src/app` — exit 1, no matches +- `test -f src/lib/pipeline-status.ts` — exits 0 + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `pnpm exec tsc --noEmit` | 0 | ✅ pass | 3.9s | +| 2 | `pnpm exec eslint src/` | 0 | ✅ pass | 8.4s | +| 3 | `rg 'function aggregateProcessStatus' src/app src/components` | 1 | ✅ pass | 0s | +| 4 | `rg 'function derivePipelineStatus' src/app src/components` | 1 | ✅ pass | 0s | +| 5 | `rg '^function formatTime' src/app src/components` | 1 | ✅ pass | 0s | +| 6 | `rg '^const STATUS_COLORS' src/components/fleet` | 1 | ✅ pass | 0s | +| 7 | `rg '^function formatTimestamp' src/app` | 1 | ✅ pass | 0s | +| 8 | `test -f src/lib/pipeline-status.ts` | 0 | ✅ pass | 0s | + +### Slice-level checks (all passing — final task) + +| # | Command | Exit Code | Verdict | +|---|---------|-----------|---------| +| 1 | `pnpm exec tsc --noEmit` | 0 | ✅ pass | +| 2 | `pnpm exec eslint src/` | 0 | ✅ pass | +| 3 | `rg 'function aggregateProcessStatus' src/app src/components` | 1 | ✅ pass | +| 4 | `rg 'function derivePipelineStatus' src/app src/components` | 1 | ✅ pass | +| 5 | `rg '^function formatTime' src/app src/components` | 1 | ✅ pass | +| 6 | `rg '^const STATUS_COLORS' src/components/fleet` | 1 | ✅ pass | +| 7 | `test -f src/lib/pipeline-status.ts` | 0 | ✅ pass | + +## Diagnostics + +- Verify no inline duplicates remain: `rg 'function aggregateProcessStatus|function derivePipelineStatus' src/app src/components` +- Verify shared module imports: `rg "from.*@/lib/pipeline-status" src/` and `rg "from.*@/lib/format" src/` and `rg "from.*@/lib/status" src/` +- Compilation check: `pnpm exec tsc --noEmit` — any broken import will surface immediately +- No runtime signals — pure refactoring of import paths with no behavior changes + +## Deviations + +- Removed unused `STATUS_COLORS` import from `event-log.tsx` — the original code defined it locally alongside `statusColor`, but the component only calls `statusColor()` directly. Importing the unused constant triggered an eslint warning. + +## Known Issues + +None. + +## Files Created/Modified + +- `src/app/(dashboard)/pipelines/page.tsx` — removed inline `aggregateProcessStatus`, added import from `@/lib/pipeline-status` +- `src/app/(dashboard)/pipelines/[id]/page.tsx` — removed inline `aggregateProcessStatus`, added import from `@/lib/pipeline-status` +- `src/app/(dashboard)/page.tsx` — removed inline `derivePipelineStatus`, added import from `@/lib/pipeline-status` +- `src/components/dashboard/custom-view.tsx` — removed inline `derivePipelineStatus`, added import from `@/lib/pipeline-status` +- `src/components/fleet/event-log.tsx` — removed inline `STATUS_COLORS`, `statusColor`, `formatTime`; added imports from `@/lib/format` and `@/lib/status` +- `src/components/fleet/status-timeline.tsx` — removed inline `STATUS_COLORS`, `statusColor`, `formatTime`; added imports from `@/lib/format` and `@/lib/status` +- `src/components/fleet/node-metrics-charts.tsx` — removed inline `formatTime`, added to existing `@/lib/format` import +- `src/components/fleet/node-logs.tsx` — removed inline `formatTime`, added `formatTimeWithSeconds` import, renamed call site +- `src/components/pipeline/pipeline-logs.tsx` — removed inline `formatTime`, added `formatTimeWithSeconds` import, renamed call site +- `src/app/(dashboard)/audit/page.tsx` — removed inline `formatTimestamp`, added import from `@/lib/format` diff --git a/src/app/(dashboard)/audit/page.tsx b/src/app/(dashboard)/audit/page.tsx index 21c1a86a..f621081e 100644 --- a/src/app/(dashboard)/audit/page.tsx +++ b/src/app/(dashboard)/audit/page.tsx @@ -26,22 +26,11 @@ import { } from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; import { useTeamStore } from "@/stores/team-store"; +import { formatTimestamp } from "@/lib/format"; const ALL_VALUE = "__all__"; const SCIM_VALUE = "__SCIM__"; -function formatTimestamp(date: Date | string): string { - const d = new Date(date); - return d.toLocaleString(undefined, { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); -} - function truncate(value: unknown, maxLength = 80): string { if (value === null || value === undefined) return "-"; const str = diff --git a/src/app/(dashboard)/page.tsx b/src/app/(dashboard)/page.tsx index b96a6fa5..c34f34bb 100644 --- a/src/app/(dashboard)/page.tsx +++ b/src/app/(dashboard)/page.tsx @@ -31,20 +31,9 @@ import { MetricChart } from "@/components/dashboard/metric-chart"; import { ViewBuilderDialog } from "@/components/dashboard/view-builder-dialog"; import { CustomView } from "@/components/dashboard/custom-view"; import { formatSI, formatBytesRate, formatEventsRate, formatLatency } from "@/lib/format"; +import { derivePipelineStatus } from "@/lib/pipeline-status"; import { cn } from "@/lib/utils"; -/** Derive an overall status for a pipeline from its node statuses */ -function derivePipelineStatus( - nodes: Array<{ pipelineStatus: string }> -): string { - if (nodes.length === 0) return "PENDING"; - if (nodes.some((n) => n.pipelineStatus === "CRASHED")) return "CRASHED"; - if (nodes.some((n) => n.pipelineStatus === "RUNNING")) return "RUNNING"; - if (nodes.some((n) => n.pipelineStatus === "STARTING")) return "STARTING"; - if (nodes.every((n) => n.pipelineStatus === "STOPPED")) return "STOPPED"; - return nodes[0].pipelineStatus; -} - export default function DashboardPage() { const trpc = useTRPC(); const queryClient = useQueryClient(); diff --git a/src/app/(dashboard)/pipelines/[id]/page.tsx b/src/app/(dashboard)/pipelines/[id]/page.tsx index eefb9fa4..ff63f8f4 100644 --- a/src/app/(dashboard)/pipelines/[id]/page.tsx +++ b/src/app/(dashboard)/pipelines/[id]/page.tsx @@ -13,6 +13,7 @@ import { Trash2, Pencil, Check, X } from "lucide-react"; import { useTRPC } from "@/trpc/client"; import { useFlowStore } from "@/stores/flow-store"; import { findComponentDef } from "@/lib/vector/catalog"; +import { aggregateProcessStatus } from "@/lib/pipeline-status"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -39,17 +40,6 @@ import { PipelineMetricsChart } from "@/components/pipeline/metrics-chart"; import { PipelineLogs } from "@/components/pipeline/pipeline-logs"; import { useTeamStore } from "@/stores/team-store"; -function aggregateProcessStatus( - statuses: Array<{ status: string }> -): "RUNNING" | "STARTING" | "STOPPED" | "CRASHED" | "PENDING" | null { - if (statuses.length === 0) return null; - if (statuses.some((s) => s.status === "CRASHED")) return "CRASHED"; - if (statuses.some((s) => s.status === "STOPPED")) return "STOPPED"; - if (statuses.some((s) => s.status === "STARTING")) return "STARTING"; - if (statuses.some((s) => s.status === "PENDING")) return "PENDING"; - return "RUNNING"; -} - /** * Convert database PipelineNode rows into React Flow nodes. * Each node's data includes the resolved VectorComponentDef from the catalog. diff --git a/src/app/(dashboard)/pipelines/page.tsx b/src/app/(dashboard)/pipelines/page.tsx index 9148deb2..7f25ea84 100644 --- a/src/app/(dashboard)/pipelines/page.tsx +++ b/src/app/(dashboard)/pipelines/page.tsx @@ -37,19 +37,9 @@ import { PromotePipelineDialog } from "@/components/promote-pipeline-dialog"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import { formatEventsRate, formatBytesRate } from "@/lib/format"; +import { aggregateProcessStatus } from "@/lib/pipeline-status"; import { tagBadgeClass, reductionBadgeClass } from "@/lib/badge-variants"; -function aggregateProcessStatus( - statuses: Array<{ status: string }> -): "RUNNING" | "STARTING" | "STOPPED" | "CRASHED" | "PENDING" | null { - if (statuses.length === 0) return null; - if (statuses.some((s) => s.status === "CRASHED")) return "CRASHED"; - if (statuses.some((s) => s.status === "STOPPED")) return "STOPPED"; - if (statuses.some((s) => s.status === "STARTING")) return "STARTING"; - if (statuses.some((s) => s.status === "PENDING")) return "PENDING"; - return "RUNNING"; -} - function sumNodeStatuses(statuses: Array<{ eventsIn: bigint; eventsOut: bigint; errorsTotal: bigint; eventsDiscarded: bigint; bytesIn: bigint; bytesOut: bigint }>) { let eventsIn = BigInt(0), eventsOut = BigInt(0), errorsTotal = BigInt(0), eventsDiscarded = BigInt(0), bytesIn = BigInt(0), bytesOut = BigInt(0); for (const s of statuses) { diff --git a/src/components/dashboard/custom-view.tsx b/src/components/dashboard/custom-view.tsx index d55008de..afd343c5 100644 --- a/src/components/dashboard/custom-view.tsx +++ b/src/components/dashboard/custom-view.tsx @@ -30,6 +30,7 @@ import { } from "@/components/dashboard/metrics-filter-bar"; import { MetricChart } from "@/components/dashboard/metric-chart"; import { formatSI, formatBytesRate, formatEventsRate } from "@/lib/format"; +import { derivePipelineStatus } from "@/lib/pipeline-status"; import { cn } from "@/lib/utils"; import type { PanelId } from "@/components/dashboard/view-builder-dialog"; @@ -40,18 +41,6 @@ import { } from "react-grid-layout"; import type { LayoutItem, Layout } from "react-grid-layout"; -/** Derive an overall status for a pipeline from its node statuses */ -function derivePipelineStatus( - nodes: Array<{ pipelineStatus: string }> -): string { - if (nodes.length === 0) return "PENDING"; - if (nodes.some((n) => n.pipelineStatus === "CRASHED")) return "CRASHED"; - if (nodes.some((n) => n.pipelineStatus === "RUNNING")) return "RUNNING"; - if (nodes.some((n) => n.pipelineStatus === "STARTING")) return "STARTING"; - if (nodes.every((n) => n.pipelineStatus === "STOPPED")) return "STOPPED"; - return nodes[0].pipelineStatus; -} - const SUMMARY_PANELS: PanelId[] = [ "node-health-summary", "pipeline-health-summary", diff --git a/src/components/fleet/event-log.tsx b/src/components/fleet/event-log.tsx index b225e065..660b58a8 100644 --- a/src/components/fleet/event-log.tsx +++ b/src/components/fleet/event-log.tsx @@ -3,6 +3,8 @@ import { useQuery } from "@tanstack/react-query"; import { useTRPC } from "@/trpc/client"; import { Skeleton } from "@/components/ui/skeleton"; +import { formatTime } from "@/lib/format"; +import { statusColor } from "@/lib/status"; type Range = "1h" | "6h" | "1d" | "7d" | "30d"; @@ -11,24 +13,6 @@ interface EventLogProps { range: Range; } -const STATUS_COLORS: Record = { - HEALTHY: "#22c55e", - UNREACHABLE: "#ef4444", - DEGRADED: "#f59e0b", - UNKNOWN: "#6b7280", -}; - -function statusColor(status: string | null | undefined): string { - return STATUS_COLORS[status ?? "UNKNOWN"] ?? "#6b7280"; -} - -function formatTime(date: Date | string): string { - return new Date(date).toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - }); -} - export function EventLog({ nodeId, range }: EventLogProps) { const trpc = useTRPC(); diff --git a/src/components/fleet/node-logs.tsx b/src/components/fleet/node-logs.tsx index e617de3a..3f217c1d 100644 --- a/src/components/fleet/node-logs.tsx +++ b/src/components/fleet/node-logs.tsx @@ -14,6 +14,7 @@ import { SelectValue, } from "@/components/ui/select"; import { highlightMatch } from "@/components/log-search-utils"; +import { formatTimeWithSeconds } from "@/lib/format"; import type { LogLevel } from "@/generated/prisma"; const ALL_LEVELS: LogLevel[] = ["ERROR", "WARN", "INFO", "DEBUG", "TRACE"]; @@ -34,16 +35,6 @@ const LEVEL_BADGE_COLORS: Record = { TRACE: "bg-gray-700/20 text-gray-600 transition-colors hover:bg-gray-700/30", }; -function formatTime(date: Date | string): string { - const d = new Date(date); - return d.toLocaleTimeString(undefined, { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false, - }); -} - interface PipelineOption { id: string; name: string; @@ -212,7 +203,7 @@ export function NodeLogs({ nodeId, pipelines }: NodeLogsProps) { )} {filteredItems.map((log) => (
- {formatTime(log.timestamp)} + {formatTimeWithSeconds(log.timestamp)} {" "} {log.level} diff --git a/src/components/fleet/node-metrics-charts.tsx b/src/components/fleet/node-metrics-charts.tsx index 505c149d..38943ed8 100644 --- a/src/components/fleet/node-metrics-charts.tsx +++ b/src/components/fleet/node-metrics-charts.tsx @@ -21,19 +21,12 @@ import { import { Skeleton } from "@/components/ui/skeleton"; import { Cpu, MemoryStick, HardDrive, Network } from "lucide-react"; import { useState } from "react"; -import { formatBytes, formatBytesRate, formatPercent } from "@/lib/format"; +import { formatBytes, formatBytesRate, formatPercent, formatTime } from "@/lib/format"; interface NodeMetricsChartsProps { nodeId: string; } -function formatTime(date: Date | string): string { - return new Date(date).toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - }); -} - const CHART_HEIGHT = 180; const cpuChartConfig = { diff --git a/src/components/fleet/status-timeline.tsx b/src/components/fleet/status-timeline.tsx index 565b823b..a410736e 100644 --- a/src/components/fleet/status-timeline.tsx +++ b/src/components/fleet/status-timeline.tsx @@ -17,6 +17,8 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { formatTime } from "@/lib/format"; +import { statusColor } from "@/lib/status"; type Range = "1h" | "6h" | "1d" | "7d" | "30d"; @@ -26,24 +28,6 @@ interface StatusTimelineProps { onRangeChange: (range: Range) => void; } -const STATUS_COLORS: Record = { - HEALTHY: "#22c55e", - UNREACHABLE: "#ef4444", - DEGRADED: "#f59e0b", - UNKNOWN: "#6b7280", -}; - -function statusColor(status: string | null | undefined): string { - return STATUS_COLORS[status ?? "UNKNOWN"] ?? "#6b7280"; -} - -function formatTime(date: Date | string): string { - return new Date(date).toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - }); -} - function formatDuration(ms: number): string { const minutes = Math.round(ms / 60_000); if (minutes < 60) return `${minutes}min`; diff --git a/src/components/pipeline/pipeline-logs.tsx b/src/components/pipeline/pipeline-logs.tsx index c9ea2499..ae215715 100644 --- a/src/components/pipeline/pipeline-logs.tsx +++ b/src/components/pipeline/pipeline-logs.tsx @@ -7,6 +7,7 @@ import { useTRPC } from "@/trpc/client"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { highlightMatch } from "@/components/log-search-utils"; +import { formatTimeWithSeconds } from "@/lib/format"; import type { LogLevel } from "@/generated/prisma"; const ALL_LEVELS: LogLevel[] = ["ERROR", "WARN", "INFO", "DEBUG", "TRACE"]; @@ -27,16 +28,6 @@ const LEVEL_BADGE_COLORS: Record = { TRACE: "bg-gray-700/20 text-gray-600 transition-colors hover:bg-gray-700/30", }; -function formatTime(date: Date | string): string { - const d = new Date(date); - return d.toLocaleTimeString(undefined, { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false, - }); -} - interface PipelineLogsProps { pipelineId: string; nodeId?: string; @@ -182,7 +173,7 @@ export function PipelineLogs({ pipelineId, nodeId }: PipelineLogsProps) { )} {filteredItems.map((log) => (
- {formatTime(log.timestamp)} + {formatTimeWithSeconds(log.timestamp)} {" "} {log.level} From 48ba7b288d549214e92db3e09bb62e3b1a55f47c Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sun, 22 Mar 2026 23:39:26 +0000 Subject: [PATCH 10/73] chore(M001/S01/T02): auto-commit after state-rebuild --- .gsd/doctor-history.jsonl | 1 + .gsd/gsd.db-wal | Bin 465592 -> 473832 bytes .gsd/milestones/M001/slices/S01/S01-UAT.md | 27 +++++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 .gsd/milestones/M001/slices/S01/S01-UAT.md diff --git a/.gsd/doctor-history.jsonl b/.gsd/doctor-history.jsonl index aab10b1b..5c5276d9 100644 --- a/.gsd/doctor-history.jsonl +++ b/.gsd/doctor-history.jsonl @@ -1,3 +1,4 @@ {"ts":"2026-03-22T22:12:25.936Z","ok":true,"errors":0,"warnings":2,"fixes":2,"codes":["gitignore_missing_patterns","missing_slice_plan"],"issues":[{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"},{"severity":"warning","code":"missing_slice_plan","message":"Slice M001/S01 has no plan file","unitId":"M001/S01"}],"fixDescriptions":["added missing GSD runtime patterns to .gitignore","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"Clean · 2 fixed · 5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json"} {"ts":"2026-03-22T23:31:32.945Z","ok":true,"errors":0,"warnings":2,"fixes":3,"codes":["state_file_stale","gitignore_missing_patterns"],"issues":[{"severity":"warning","code":"state_file_stale","message":"STATE.md is stale — shows \"planning\" but derived state is \"executing\"","unitId":"project"},{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"}],"fixDescriptions":["rebuilt STATE.md from derived state","added missing GSD runtime patterns to .gitignore","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"Clean · 3 fixed · STATE.md is stale — shows \"planning\" but derived state is \"executing\""} {"ts":"2026-03-22T23:33:52.798Z","ok":true,"errors":0,"warnings":1,"fixes":2,"codes":["gitignore_missing_patterns"],"issues":[{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"}],"fixDescriptions":["added missing GSD runtime patterns to .gitignore","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"Clean · 2 fixed · 5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json"} +{"ts":"2026-03-22T23:39:26.714Z","ok":false,"errors":2,"warnings":3,"fixes":4,"codes":["state_file_stale","gitignore_missing_patterns","all_tasks_done_missing_slice_summary","all_tasks_done_missing_slice_uat","all_tasks_done_roadmap_not_checked"],"issues":[{"severity":"warning","code":"state_file_stale","message":"STATE.md is stale — shows \"executing\" but derived state is \"summarizing\"","unitId":"project"},{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"},{"severity":"error","code":"all_tasks_done_missing_slice_summary","message":"All tasks are done but S01-SUMMARY.md is missing","unitId":"M001/S01"},{"severity":"warning","code":"all_tasks_done_missing_slice_uat","message":"All tasks are done but S01-UAT.md is missing","unitId":"M001/S01"},{"severity":"error","code":"all_tasks_done_roadmap_not_checked","message":"All tasks are done but roadmap still shows S01 as incomplete","unitId":"M001/S01"}],"fixDescriptions":["rebuilt STATE.md from derived state","added missing GSD runtime patterns to .gitignore","created placeholder /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/milestones/M001/slices/S01/S01-UAT.md","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"2 errors, 3 warnings · 4 fixed · All tasks are done but S01-SUMMARY.md is missing"} diff --git a/.gsd/gsd.db-wal b/.gsd/gsd.db-wal index 5af38b7f224c5decf3d8346445c4098891b91942..3f6085809e99e0befa8527690390f293e78997da 100644 GIT binary patch delta 57 zcmV-90LK5gwH)a69DsxYgaU*Egam{Iga)(+xEhzZ6ax?lcGJLT6vP5@x1jn45D*AH P)H^i8@mEr(aM}hCi%k|= delta 23 ecmaFyRc6OlnT8g|7N!>F7M2#)7Pc+yJEQ=4wg|ld diff --git a/.gsd/milestones/M001/slices/S01/S01-UAT.md b/.gsd/milestones/M001/slices/S01/S01-UAT.md new file mode 100644 index 00000000..c7584573 --- /dev/null +++ b/.gsd/milestones/M001/slices/S01/S01-UAT.md @@ -0,0 +1,27 @@ +# S01: Recovery placeholder UAT + +**Milestone:** M001 +**Written:** 2026-03-22T23:39:26.713Z + +## Preconditions +- Doctor created this placeholder because the expected UAT file was missing. + +## Smoke Test +- Re-run the slice verification from the slice plan before shipping. + +## Test Cases +### 1. Replace this placeholder +1. Read the slice plan and task summaries. +2. Write a real UAT script. +3. **Expected:** This placeholder is replaced with meaningful human checks. + +## Edge Cases +### Missing completion artifacts +1. Confirm the summary, roadmap checkbox, and state file are coherent. +2. **Expected:** GSD doctor reports no remaining completion drift for this slice. + +## Failure Signals +- Placeholder content still present when treating the slice as done + +## Notes for Tester +Doctor created this file only to restore the required artifact shape. Replace it with a real UAT script. From a26d9c1eff2d0d3bc4fb1fb3e5f69ce490e959e2 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sun, 22 Mar 2026 23:42:14 +0000 Subject: [PATCH 11/73] chore(M001/S01): auto-commit after complete-slice --- .gsd/KNOWLEDGE.md | 14 ++ .gsd/PROJECT.md | 5 + .gsd/REQUIREMENTS.md | 18 +-- .gsd/gsd.db-wal | Bin 473832 -> 679832 bytes .gsd/journal/2026-03-22.jsonl | 5 + .gsd/milestones/M001/M001-ROADMAP.md | 2 +- .../milestones/M001/slices/S01/S01-SUMMARY.md | 131 ++++++++++++++++++ .gsd/milestones/M001/slices/S01/S01-UAT.md | 94 +++++++++++-- .../M001/slices/S01/tasks/T02-VERIFY.json | 16 +++ 9 files changed, 262 insertions(+), 23 deletions(-) create mode 100644 .gsd/KNOWLEDGE.md create mode 100644 .gsd/milestones/M001/slices/S01/S01-SUMMARY.md create mode 100644 .gsd/milestones/M001/slices/S01/tasks/T02-VERIFY.json diff --git a/.gsd/KNOWLEDGE.md b/.gsd/KNOWLEDGE.md new file mode 100644 index 00000000..a597ace1 --- /dev/null +++ b/.gsd/KNOWLEDGE.md @@ -0,0 +1,14 @@ +# Knowledge Base + + + +## Shared Utility Module Convention (from M001/S01) + +**Pattern:** All pipeline status derivation imports from `@/lib/pipeline-status`, time formatting from `@/lib/format`, status color helpers from `@/lib/status`. No inline utility definitions in consumer files. + +**Key distinction:** `formatTime` returns HH:MM (used in dashboard cards, charts), `formatTimeWithSeconds` returns HH:MM:SS (used in log viewers like `node-logs.tsx` and `pipeline-logs.tsx`). Don't mix them up — logs need seconds precision. + +**Gotcha:** `event-log.tsx` defines `STATUS_COLORS` locally but only uses `statusColor()` — importing the unused constant triggers an eslint warning. Only import what's actually referenced. + +**Diagnostic shortcut:** `rg 'export function|export const' src/lib/pipeline-status.ts src/lib/format.ts src/lib/status.ts` shows the full shared API surface at a glance. diff --git a/.gsd/PROJECT.md b/.gsd/PROJECT.md index 067d4cf8..bd21d5f8 100644 --- a/.gsd/PROJECT.md +++ b/.gsd/PROJECT.md @@ -28,3 +28,8 @@ See `.gsd/REQUIREMENTS.md` for the explicit capability contract, requirement sta ## Milestone Sequence - [ ] M001: Baseline Quality — Fix TS errors, refactor large files, consistent UI, foundational tests, performance audit + - [x] S01: TypeScript fixes & shared utilities — completed 2026-03-22 + - [ ] S02: Router & component refactoring + - [ ] S03: UI consistency sweep + - [ ] S04: Foundational test suite + - [ ] S05: Performance audit & optimization diff --git a/.gsd/REQUIREMENTS.md b/.gsd/REQUIREMENTS.md index b05e083d..859eb8bc 100644 --- a/.gsd/REQUIREMENTS.md +++ b/.gsd/REQUIREMENTS.md @@ -12,8 +12,8 @@ This file is the explicit capability and coverage contract for the project. - Source: execution - Primary owning slice: M001/S01 - Supporting slices: none -- Validation: `pnpm exec tsc --noEmit` exits 0 — already passing, S01 verifies no regression -- Notes: Prisma generate fixes most errors; remaining are event-log destructuring bug and monaco-editor module resolution. +- Validation: `pnpm exec tsc --noEmit` exits 0 — S01 verified no regression after extracting shared utilities and rewiring 10 consumer files +- Notes: Prisma generate fixes most errors; remaining are event-log destructuring bug and monaco-editor module resolution. S01 verified no regression — tsc --noEmit exits 0 after shared utility extraction and consumer rewiring. ### R002 — Test suite exists with coverage for auth flows (login, 2FA, OIDC), pipeline CRUD, deploy operations, and alert evaluation. Test runner configured and passing in CI. - Class: quality-attribute @@ -45,8 +45,8 @@ This file is the explicit capability and coverage contract for the project. - Source: execution - Primary owning slice: M001/S01 - Supporting slices: M001/S02 -- Validation: S01/T01 creates shared modules, S01/T02 removes all inline duplicates; verified by grep checks returning no matches -- Notes: Scout found `aggregateProcessStatus` in pipelines/page.tsx, pipelines/[id]/page.tsx, and dashboard page.tsx. +- Validation: S01/T01 creates shared modules, S01/T02 removes all inline duplicates; verified by grep checks returning no matches in src/app and src/components +- Notes: S01/T01 created shared modules, S01/T02 replaced all inline duplicates in 10 consumer files. grep confirms zero inline copies remain. S02 may discover additional duplicates during file splitting. ### R005 — All 35+ dashboard pages have consistent loading skeletons, empty state messaging with CTAs, and error handling. No page should show a blank white screen during loading or when data is empty. - Class: primary-user-loop @@ -89,8 +89,8 @@ This file is the explicit capability and coverage contract for the project. - Source: inferred - Primary owning slice: M001/S01 - Supporting slices: none -- Validation: `pnpm exec eslint src/` exits 0 — already passing, S01 verifies no regression -- Notes: ESLint config uses next/core-web-vitals and next/typescript presets. +- Validation: `pnpm exec eslint src/` exits 0 — S01 verified no regression after extracting shared utilities and rewiring 10 consumer files +- Notes: ESLint config uses next/core-web-vitals and next/typescript presets. S01 verified no regression — eslint src/ exits 0 after shared utility extraction and consumer rewiring. ### R010 — Analyze Next.js bundle size, identify large dependencies or unnecessary client-side imports, review Prisma query patterns for N+1 or missing indexes, and address measurable bottlenecks found. - Class: quality-attribute @@ -144,14 +144,14 @@ This file is the explicit capability and coverage contract for the project. | ID | Class | Status | Primary owner | Supporting | Proof | |---|---|---|---|---|---| -| R001 | quality-attribute | active | M001/S01 | none | `pnpm exec tsc --noEmit` exits 0 — already passing, S01 verifies no regression | +| R001 | quality-attribute | active | M001/S01 | none | `pnpm exec tsc --noEmit` exits 0 — S01 verified no regression after extracting shared utilities and rewiring 10 consumer files | | R002 | quality-attribute | active | M001/S04 | none | unmapped | | R003 | quality-attribute | active | M001/S02 | none | unmapped | -| R004 | quality-attribute | active | M001/S01 | M001/S02 | S01/T01 creates shared modules, S01/T02 removes all inline duplicates; verified by grep checks returning no matches | +| R004 | quality-attribute | active | M001/S01 | M001/S02 | S01/T01 creates shared modules, S01/T02 removes all inline duplicates; verified by grep checks returning no matches in src/app and src/components | | R005 | primary-user-loop | active | M001/S03 | none | unmapped | | R006 | primary-user-loop | active | M001/S03 | none | unmapped | | R007 | quality-attribute | active | M001/S02 | M001/S04 | unmapped | -| R008 | quality-attribute | active | M001/S01 | none | `pnpm exec eslint src/` exits 0 — already passing, S01 verifies no regression | +| R008 | quality-attribute | active | M001/S01 | none | `pnpm exec eslint src/` exits 0 — S01 verified no regression after extracting shared utilities and rewiring 10 consumer files | | R009 | quality-attribute | deferred | none | none | unmapped | | R010 | quality-attribute | active | M001/S05 | none | unmapped | | R011 | quality-attribute | out-of-scope | none | none | n/a | diff --git a/.gsd/gsd.db-wal b/.gsd/gsd.db-wal index 3f6085809e99e0befa8527690390f293e78997da..3f2811db4d1603ade08a96b4f15fdf37721d84b4 100644 GIT binary patch delta 8823 zcmds6dw3L8vY(zl(@7?e8Aw82Bsl?*yk;_)gognZvLr51LV$$licDvwleC$c9=d0M zu*mQf)C+>{_4vXIyP~M<2Z(I@TG{>dey*U1u-@g974a=AC|NJ7uHNgd)7>-7gop0u z?*4IsX}V8W*Qq*H^{YB{K0ZJ6o&yyt*D`C(YuUAEtz|9uWXi`4PzM+Pz9+G;Z36_@yavZKrF ztNC!J#rCy{wVk1_2HW~=f3=-~zaQ=6?PX@Q^eSdgW-Yw2SHLB&1}-*wXRn5fWjI_? z;1{2A=^qb?6J3waRd?>RJYb8KQ`u&^YTb3lO|4tMaa~hujJRCJ$k-a=W{lIg8SOA` zMpYX(^;O1AU8Qm3royBqitvZM-J1$KJsV*uGC{3=`mV(Gwe7ED?LYLm^vg+9gAdgqJiwMPctQWIxfc2V`MJi8oBkgWS`>fX1#+mb+0iU&w?Irw&=qmE`A1xV78oA>` zG?JYC1DnRA(t8(5sbJ?&ilHg(u3E>e#t!n_he#zS-T;4Dd1BKP&%B3rl5@u`B}|3t z`7ic8#FUe=)0Wl5?qd4XiGN~WL+WqdvrM+64#iVelpH$VPf|}?wldY~A_nb7>}_+a z)VZIq^DRuJy6+q-STgJ#FW-ISjdxz9Dykc&phJxM#pB#foMq_(TvvyrqYsZg`+9M>Wt}Jbh@0x>AtLaKjZML zrxvPv=5b%xVq(=%y4*y%(hdK)#J=j;LUqq<^o6Z9k>8L+I_8;X>w<57KRgo#k?BEG z`ZK94YBW7~7jNH`@koA3nvk2Do&9Rc1hSiFibzP}tOUQorl>m~qeNPeFnW1zBHD=S+?zo3Mr zpwLiRiRouBHb)l2VIfFI+%ETMhpW!vc6Yez8enbQqn%@F>*ian!-wPPGB6JM#IPs@ z6{`bBnhA_lvi z8vpiy)GOc)kPn#86dZCSsC}yVMX!KCBe6^L@^p6GB})MurF`|5plJ|y3SE*6BRyg$ z6lY4=;mN!rD4@b{Tzv@x&4#{oRXYYQ46Ku^OX0&y#BdJ|2z(Gk?uz(vPb2`kczXnI zFHA(&z1i0-B&g7;uNn46r9j9pXzRhtVX=$%h83#BWGUDs$^l&uP3FF8EK9sEz=w3k zozl{3O!ZR2U^(3!JS?48>Jeli2s(g*$vXU`y$SntdxPkb99D2ND1jOQUZkSKw{dBF zV5u+$W9VzFnMhaVT3+-=V4`-hJIMQ~6HQO>QSdJC!!SrC`u#X03krQ&(j$N$P#>ey zTrdtFRY0Pkpsmp~DJ&@3l+@sYY`v6kYHlY5{e3u0C5JcwQ7A)MSQHUuaL(8|f)nu} z;exM-AO^KllC?{8b6*rQ8hp9utIvgy%Blx$#Pz0dQi#q~T>j zBHhj#Tb@4C)6}A#TAV=VrT|u$NO$O|jW=$7xb8RVsXhQYhXG(3T~Q+4v7e8~4m~qU zrT}JUQ%#n+Y${}+Z_MmN&1vdXiJNN9%-LU@ssq{SK{}ABcsD|+B;y|R@9Lujz0Y9y ze5mn@WBUp6$<04`_vAZrGe4|WSa8cNe792AydfQGOb zfH2kl$f<`IgcXlN)W2kbMuQ}49z$dI-IYJbVu1QB4)Nh01>%76#GR3FoLQ&P!!H)W zYk`mG1J?tz@jI;!JA^_@P@(t**aK1!<`HheFr`O|_~Tv!VJ zQd{99gQjwYRf~DQ=z}l=;lQhtga99u{EKPa$$+h4KRLXM8x{rMqrMoBd=Y?208T2k z1{ndsbsq)^AxQav;H$=-u;RrIM^I`Gh+z*nDNNA?K^AjYuV3Kh7|c2hUVE- zpaWoo3_+vAiCem&fDAD)Vb(|xAe_!5KpY3-bNKmg3|az)1GQ&TxGyB=_~+2+YJ!qO z|J1RVmlY6=mnlvFv<%EKM!bOKi>dkBWBLB+4J-RUJu~MmItXzd3|9b*3Hln)sEq`J zf>%%!9#WmLtPt0{w?`CUO?gap4`M2`lkB}8Imb{Zpte!43*c{{F*KY9HFT!}qk_dW z+mRzvP!TzDFG3~pufg2fDk{_(hH47g@)*~c9-9T%R+EMnbY0TG?JX#o;#dosIFMo_ z*|ZhelST`!Lv<{0CbiSJ0`mAmZfi!J^BM&&w=TzSyqq+CYbhvRj$L>;e)oZO;NuFs z!(*NGI!PUnfYXj3H4mUdb~&ykTROSyl;yY{s61rFYs@tk58UeGKro6d^KoMt{n@e{ zj|K*(T97;bEsTwUv2WVA%EYm;F!sAu+|}f54>yE7)X5bT(uJn-&{(8v(g39a-3#P-9?r!j=rUxWF7Pf%iF^Ea5b7h$E;6^kStCAFjnz8;509X3I4s7soJ#lTD}2DO4sk4!!2v8TPG zv13;I!pUt@+h(?VVr;d-%>(fn3_JW}!Hrx>RC5X%&j8omns0a|pMZS=I0~R0fQ(-P zAcm~X`=wyF0?>>-y55r|za)FAH58{DXbqwv1=g}W{YpuqEwAukA7pyK+CIQKnh@&a zWrZO*1;j89g-00V>4tg;%7P%)Q7sngq`8}$Kq~G+l@?8F;<^vvYycIXkBY1(8mcR) zJ;>yfTkb*yB=iJqw+#IHZu)YAvAvZmxjf^XbzC;N@gP&Afklk@cYfo{&}}6K0UZTm z`wvt~{$~Zsa*a#U017HvXL}|kc%$K}`jA4h)^Vj|;vpuNY}YG`F0vAE3{(lz{><#f+~Z$7(`|WHcnXTBHJJ1^6n0C zLo%yrxeoY7Em=(#Jc)|x5{ERMX_1mtpeWW}p>H8ARg?y862hziD;$}@4YDLWL#z$C z@I2JGZ9B-6BT7XYkATPCF&$+kJpSkqlauf`>DMSf;nDsIlVz}(v>jDkV4g!lvr$%5 zlglS~MTG*QF566_STTSl2@w-P0vmWJbvwh#Qo1?m6@*X>a`ZB$Ttl=3*rOF;P!K5I z0Q8m0LWLDD8@4_{Wg%>B_)fp5^gz+9tx6*)U5pB{bk8xuTF*dH2zhX4t(w;Dp+!+L zWkUSxFojovULZSUsYbK zeOIA_b8>XVcdQ^d6vE_Zdr-X@my@<#+?bRK*n?jy$)t5As+>pDr4gHnh>B^kj}@)a zqc~s-t~<05s+wz*Pnput+S<_G4&}HaQRjme&l1R!u;+keNuu|lLh`E}C}qe1D5lE^ zh9-+1M%go=PKRBO4-gwNvDMnx1)d86L30U;LnGO0Rjdrb!Ta?je8t*=oIAefS~^hM+AfGfZpzai>(SSNk;_L zpJhCA{fq@;jU$43y^;UVJ|eJz7;$Ghzq)i**+<{rpJ{>hV(FR5UP%4_@34NXjigyW zsq?jKf<0)tZQS-u5Y)_0F>wdjDbPk5&kpQ=eMOf6HLGtUt_hV=?~8lqOFhoJAFa9f z4y}GkyU&z%A8^>5Hu&GX^@DS33f5Gt@|G6ouFhQ3OkAfeigs*4;MigZ?T{O-WB>rz z*~mgSZ1ic;#yrI!Qus@zxP(HLEC8O-igIAeP)L6zT6EKuSkd}2u`?5EJLq%7wuceu zb3#Uhmth(3Ns|F4jzfA(0+p{q8(F#lNnTxBXwO~wqff~{%d1CS^584UtIHxgKHu>C zS-aryJW^Oq?W_F}tKDDozJqY$?4}yiCXLD|E9Yrm?T3lr-EiPVN zvnG3Et{PauT*Z)S{bt1x%jA%xwfs%9dVE``fc0xSzypjU3$mc!T*PU|0a~e$Hxg39 zMdo&5a1rR78?^|nYYDR##mN_!p+EZ|nT}E$e-!vf zo(B9*xVsqm1Nh~?`Hl{UGc@gN`0oP#(T4k9>1_DYA(8~p&lOmo{YCSyLIBX&0Q@>a za|b|N?ax4jW@L)cL597xZ@?iyGD0(dy1Ty0^`_P^2Sya3>0#;jLFmDk$j?a#y}!)U zSNTHeF?!HC|E`dIHCiWs_i^hmm_BKfIo$i`_~W|=thaU*dI0qG(lgq*1pZm+5 f{pP0-)%%B)gYkSfzkbVz+y0#T-~YOD@X7xJ=cVkB delta 23 ecmbQSU-QLRnT8g|7N!>F7M2#)7Pc+yFRTD}^9ck1 diff --git a/.gsd/journal/2026-03-22.jsonl b/.gsd/journal/2026-03-22.jsonl index 0bcdd8b0..2622f7ba 100644 --- a/.gsd/journal/2026-03-22.jsonl +++ b/.gsd/journal/2026-03-22.jsonl @@ -16,3 +16,8 @@ {"ts":"2026-03-22T23:34:02.857Z","flowId":"e875b89c-02ee-4737-bfb5-61e8825b15df","seq":2,"eventType":"dispatch-match","rule":"executing → execute-task","data":{"unitType":"execute-task","unitId":"M001/S01/T02"}} {"ts":"2026-03-22T23:34:02.869Z","flowId":"e875b89c-02ee-4737-bfb5-61e8825b15df","seq":3,"eventType":"unit-start","data":{"unitType":"execute-task","unitId":"M001/S01/T02"}} {"ts":"2026-03-22T23:39:25.849Z","flowId":"e875b89c-02ee-4737-bfb5-61e8825b15df","seq":4,"eventType":"unit-end","data":{"unitType":"execute-task","unitId":"M001/S01/T02","status":"completed","artifactVerified":true},"causedBy":{"flowId":"e875b89c-02ee-4737-bfb5-61e8825b15df","seq":3}} +{"ts":"2026-03-22T23:39:35.074Z","flowId":"e875b89c-02ee-4737-bfb5-61e8825b15df","seq":5,"eventType":"iteration-end","data":{"iteration":3}} +{"ts":"2026-03-22T23:39:35.075Z","flowId":"219d473c-29b2-4748-8541-c3c6bcb420e0","seq":1,"eventType":"iteration-start","data":{"iteration":4}} +{"ts":"2026-03-22T23:39:35.317Z","flowId":"219d473c-29b2-4748-8541-c3c6bcb420e0","seq":2,"eventType":"dispatch-match","rule":"summarizing → complete-slice","data":{"unitType":"complete-slice","unitId":"M001/S01"}} +{"ts":"2026-03-22T23:39:35.323Z","flowId":"219d473c-29b2-4748-8541-c3c6bcb420e0","seq":3,"eventType":"unit-start","data":{"unitType":"complete-slice","unitId":"M001/S01"}} +{"ts":"2026-03-22T23:42:14.075Z","flowId":"219d473c-29b2-4748-8541-c3c6bcb420e0","seq":4,"eventType":"unit-end","data":{"unitType":"complete-slice","unitId":"M001/S01","status":"completed","artifactVerified":true},"causedBy":{"flowId":"219d473c-29b2-4748-8541-c3c6bcb420e0","seq":3}} diff --git a/.gsd/milestones/M001/M001-ROADMAP.md b/.gsd/milestones/M001/M001-ROADMAP.md index 97976d0f..8b76e3bf 100644 --- a/.gsd/milestones/M001/M001-ROADMAP.md +++ b/.gsd/milestones/M001/M001-ROADMAP.md @@ -53,7 +53,7 @@ This milestone is complete only when all are true: ## Slices -- [ ] **S01: TypeScript fixes & shared utilities** `risk:low` `depends:[]` +- [x] **S01: TypeScript fixes & shared utilities** `risk:low` `depends:[]` > After this: `tsc --noEmit` passes with zero errors, `eslint` is clean, duplicated helpers are consolidated into `src/lib/` shared modules. - [ ] **S02: Router & component refactoring** `risk:medium` `depends:[S01]` diff --git a/.gsd/milestones/M001/slices/S01/S01-SUMMARY.md b/.gsd/milestones/M001/slices/S01/S01-SUMMARY.md new file mode 100644 index 00000000..54df9b40 --- /dev/null +++ b/.gsd/milestones/M001/slices/S01/S01-SUMMARY.md @@ -0,0 +1,131 @@ +--- +id: S01 +parent: M001 +milestone: M001 +provides: + - src/lib/pipeline-status.ts with aggregateProcessStatus() and derivePipelineStatus() (boundary contract for S02–S05) + - src/lib/format.ts extended with formatTime() and formatTimeWithSeconds() + - src/lib/status.ts extended with STATUS_COLORS and statusColor() + - formatTimestamp() updated with explicit locale options matching audit page + - zero inline duplicate utility definitions across 10 consumer files + - tsc --noEmit exits 0 baseline preserved + - eslint src/ exits 0 baseline preserved +requires: + - slice: none + provides: first slice — no dependencies +affects: + - S02 + - S03 + - S04 + - S05 +key_files: + - src/lib/pipeline-status.ts + - src/lib/format.ts + - src/lib/status.ts +key_decisions: + - Matched reference implementations exactly (no logic changes) to ensure zero-risk extraction + - Removed unused STATUS_COLORS import from event-log.tsx to keep eslint clean (only statusColor is needed there) +patterns_established: + - All pipeline status derivation logic imports from @/lib/pipeline-status — no inline definitions + - All time formatting (HH:MM via formatTime, HH:MM:SS via formatTimeWithSeconds, timestamp via formatTimestamp) imports from @/lib/format + - Node health color mapping (STATUS_COLORS, statusColor) lives in @/lib/status alongside status variant helpers +observability_surfaces: + - tsc --noEmit validates all export type signatures and import paths + - rg 'function aggregateProcessStatus' src/app src/components confirms no inline duplicates + - rg 'export function|export const' src/lib/pipeline-status.ts src/lib/format.ts src/lib/status.ts shows full shared API surface +drill_down_paths: + - .gsd/milestones/M001/slices/S01/tasks/T01-SUMMARY.md + - .gsd/milestones/M001/slices/S01/tasks/T02-SUMMARY.md +duration: 10m +verification_result: passed +completed_at: 2026-03-22 +--- + +# S01: TypeScript Fixes & Shared Utilities + +**Extracted 7 duplicated utility functions from 10 consumer files into 3 shared modules in `src/lib/`, maintaining zero TS and eslint errors** + +## What Happened + +T01 created the shared modules: `src/lib/pipeline-status.ts` (new, with `aggregateProcessStatus` and `derivePipelineStatus`), and extended `src/lib/format.ts` (with `formatTime` and `formatTimeWithSeconds`) and `src/lib/status.ts` (with `STATUS_COLORS` and `statusColor`). All implementations were copied verbatim from their reference sources to ensure behavioral equivalence — no logic changes. + +T02 wired all 10 consumer files to import from the shared modules and deleted every inline duplicate. Files importing the HH:MM:SS variant (`node-logs.tsx`, `pipeline-logs.tsx`) were switched to `formatTimeWithSeconds` with call sites renamed. An unused `STATUS_COLORS` import in `event-log.tsx` was removed to satisfy eslint — the component only uses `statusColor()` directly. + +The `formatTimestamp` function in `format.ts` was also updated to use explicit locale options (year, month, day, hour, minute, second), matching the audit page's more detailed formatting. The audit page's local definition was then replaced with an import. + +## Verification + +| # | Check | Result | +|---|-------|--------| +| 1 | `pnpm exec tsc --noEmit` exits 0 | ✅ pass | +| 2 | `pnpm exec eslint src/` exits 0 | ✅ pass | +| 3 | `rg 'function aggregateProcessStatus' src/app src/components` — no matches | ✅ pass | +| 4 | `rg 'function derivePipelineStatus' src/app src/components` — no matches | ✅ pass | +| 5 | `rg '^function formatTime' src/app src/components` — no matches | ✅ pass | +| 6 | `rg '^const STATUS_COLORS' src/components/fleet` — no matches | ✅ pass | +| 7 | `rg '^function formatTimestamp' src/app` — no matches | ✅ pass | +| 8 | `test -f src/lib/pipeline-status.ts` — exists | ✅ pass | + +## Requirements Advanced + +- R001 — `tsc --noEmit` exits 0, no regression introduced by refactoring +- R004 — All duplicated utility functions extracted to shared modules; grep confirms zero inline copies remain +- R008 — `eslint src/` exits 0, no regression introduced by refactoring + +## Requirements Validated + +- None — R001, R004, and R008 are advanced but not yet validated (R001/R008 need to hold across all slices; R004 has supporting work in S02) + +## Requirements Invalidated or Re-scoped + +- None + +## New Requirements Surfaced + +- None + +## Deviations + +None — both tasks matched the plan exactly. The only minor adjustment was removing an unused `STATUS_COLORS` import in `event-log.tsx` that the plan didn't anticipate, because the original code defined it locally but only used `statusColor()`. + +## Known Limitations + +- `formatTimestamp` locale options change is a minor formatting difference on the audit page (now includes explicit year/month/day/hour/minute/second). This matches the existing behavior but the explicit options may render slightly differently on edge-case locales. +- S02 may discover additional duplicates during file splitting that weren't visible in the duplication audit. + +## Follow-ups + +- None — all planned work completed. + +## Files Created/Modified + +- `src/lib/pipeline-status.ts` — new shared module with `aggregateProcessStatus` and `derivePipelineStatus` +- `src/lib/format.ts` — added `formatTime`, `formatTimeWithSeconds`; updated `formatTimestamp` with explicit locale options +- `src/lib/status.ts` — added `STATUS_COLORS` constant and `statusColor` function +- `src/app/(dashboard)/pipelines/page.tsx` — replaced inline `aggregateProcessStatus` with import +- `src/app/(dashboard)/pipelines/[id]/page.tsx` — replaced inline `aggregateProcessStatus` with import +- `src/app/(dashboard)/page.tsx` — replaced inline `derivePipelineStatus` with import +- `src/components/dashboard/custom-view.tsx` — replaced inline `derivePipelineStatus` with import +- `src/components/fleet/event-log.tsx` — replaced inline `STATUS_COLORS`, `statusColor`, `formatTime` with imports +- `src/components/fleet/status-timeline.tsx` — replaced inline `STATUS_COLORS`, `statusColor`, `formatTime` with imports +- `src/components/fleet/node-metrics-charts.tsx` — replaced inline `formatTime` with import +- `src/components/fleet/node-logs.tsx` — replaced inline `formatTime` with `formatTimeWithSeconds` import +- `src/components/pipeline/pipeline-logs.tsx` — replaced inline `formatTime` with `formatTimeWithSeconds` import +- `src/app/(dashboard)/audit/page.tsx` — replaced inline `formatTimestamp` with import + +## Forward Intelligence + +### What the next slice should know +- All shared utilities live in `src/lib/pipeline-status.ts`, `src/lib/format.ts`, and `src/lib/status.ts`. Import from `@/lib/pipeline-status`, `@/lib/format`, `@/lib/status` respectively. +- `aggregateProcessStatus` takes a `processes` array with `status` fields and returns an aggregate status string. `derivePipelineStatus` takes pipeline data and returns derived status. See `src/lib/pipeline-status.ts` for exact signatures. +- The `formatTime` (HH:MM) vs `formatTimeWithSeconds` (HH:MM:SS) distinction matters — log viewers use seconds precision, dashboard cards use minutes. + +### What's fragile +- `formatTimestamp` locale options are now explicit — if the Intl API behavior changes across Node versions or the audit page's formatting expectations shift, this could produce subtle differences. Low risk but worth knowing. + +### Authoritative diagnostics +- `pnpm exec tsc --noEmit` — the single most reliable check; catches any import/export mismatch across the entire codebase +- `rg 'export function|export const' src/lib/pipeline-status.ts src/lib/format.ts src/lib/status.ts` — shows the complete shared API surface at a glance + +### What assumptions changed +- No assumptions changed. The codebase was in the expected state and all reference implementations matched their audit descriptions exactly. diff --git a/.gsd/milestones/M001/slices/S01/S01-UAT.md b/.gsd/milestones/M001/slices/S01/S01-UAT.md index c7584573..ec51ea77 100644 --- a/.gsd/milestones/M001/slices/S01/S01-UAT.md +++ b/.gsd/milestones/M001/slices/S01/S01-UAT.md @@ -1,27 +1,95 @@ -# S01: Recovery placeholder UAT +# S01: TypeScript Fixes & Shared Utilities — UAT **Milestone:** M001 -**Written:** 2026-03-22T23:39:26.713Z +**Written:** 2026-03-22 + +## UAT Type + +- UAT mode: artifact-driven +- Why this mode is sufficient: This slice is a pure refactoring — no runtime behavior changes, no new UI, no API changes. All verification is through static analysis (tsc, eslint) and code structure checks (grep). The shared modules export pure functions with no side effects. ## Preconditions -- Doctor created this placeholder because the expected UAT file was missing. + +- Working directory is the project root with `node_modules` installed (`pnpm install` has been run) +- `pnpm exec tsc --noEmit` must be functional (TypeScript configured) +- `pnpm exec eslint` must be functional (ESLint configured) ## Smoke Test -- Re-run the slice verification from the slice plan before shipping. + +Run `pnpm exec tsc --noEmit && pnpm exec eslint src/` — both must exit 0. This confirms the refactoring didn't break anything. ## Test Cases -### 1. Replace this placeholder -1. Read the slice plan and task summaries. -2. Write a real UAT script. -3. **Expected:** This placeholder is replaced with meaningful human checks. + +### 1. Shared modules exist with correct exports + +1. Run `test -f src/lib/pipeline-status.ts && echo OK` +2. Run `rg 'export function aggregateProcessStatus' src/lib/pipeline-status.ts` +3. Run `rg 'export function derivePipelineStatus' src/lib/pipeline-status.ts` +4. Run `rg 'export function formatTime\b' src/lib/format.ts` +5. Run `rg 'export function formatTimeWithSeconds' src/lib/format.ts` +6. Run `rg 'export const STATUS_COLORS' src/lib/status.ts` +7. Run `rg 'export function statusColor' src/lib/status.ts` +8. **Expected:** All commands return matches. The shared modules exist and export the correct functions. + +### 2. No inline duplicates remain in consumer files + +1. Run `rg 'function aggregateProcessStatus' src/app src/components` +2. Run `rg 'function derivePipelineStatus' src/app src/components` +3. Run `rg '^function formatTime' src/app src/components` +4. Run `rg '^const STATUS_COLORS' src/components/fleet` +5. Run `rg '^function formatTimestamp' src/app` +6. **Expected:** All commands return exit code 1 (no matches). Zero inline duplicate definitions remain. + +### 3. TypeScript compilation passes + +1. Run `pnpm exec tsc --noEmit` +2. **Expected:** Exits 0 with no output. All import paths resolve, all type signatures match. + +### 4. ESLint passes + +1. Run `pnpm exec eslint src/` +2. **Expected:** Exits 0. No unused imports, no lint errors from the refactoring. + +### 5. Consumer files import from shared modules + +1. Run `rg "from.*@/lib/pipeline-status" src/app src/components` +2. **Expected:** Matches in `pipelines/page.tsx`, `pipelines/[id]/page.tsx`, `page.tsx` (dashboard), and `custom-view.tsx` — 4 files importing pipeline status utilities. +3. Run `rg "from.*@/lib/format" src/app src/components` +4. **Expected:** Matches in `event-log.tsx`, `status-timeline.tsx`, `node-metrics-charts.tsx`, `node-logs.tsx`, `pipeline-logs.tsx`, and `audit/page.tsx` — at least 6 files importing format utilities. +5. Run `rg "from.*@/lib/status" src/components/fleet` +6. **Expected:** Matches in `event-log.tsx` and `status-timeline.tsx` — 2 files importing status color utilities. + +### 6. formatTimeWithSeconds used in log viewers + +1. Run `rg 'formatTimeWithSeconds' src/components/fleet/node-logs.tsx src/components/pipeline/pipeline-logs.tsx` +2. **Expected:** Both files import and call `formatTimeWithSeconds` (not `formatTime`), confirming the HH:MM:SS variant is used for log timestamps. ## Edge Cases -### Missing completion artifacts -1. Confirm the summary, roadmap checkbox, and state file are coherent. -2. **Expected:** GSD doctor reports no remaining completion drift for this slice. + +### Unused import cleanup + +1. Run `rg 'STATUS_COLORS' src/components/fleet/event-log.tsx` +2. **Expected:** No match for `STATUS_COLORS` — only `statusColor` should be imported, since the component never references the constant directly. + +### formatTimestamp explicit locale options + +1. Run `rg 'year.*month.*day.*hour.*minute.*second' src/lib/format.ts` +2. **Expected:** Match in `formatTimestamp` function — confirms explicit locale options are present (not relying on Intl defaults). ## Failure Signals -- Placeholder content still present when treating the slice as done + +- `tsc --noEmit` exits non-zero → an import path is broken or a type signature doesn't match +- `eslint src/` exits non-zero → an unused import was left behind or a new lint issue introduced +- Any grep for inline definitions returns matches → a consumer file wasn't updated +- A consumer file has no import from `@/lib/*` → the wiring was missed + +## Not Proven By This UAT + +- Runtime behavior equivalence — we verify type signatures match but don't execute the functions with test data +- Visual rendering — no browser checks that formatted timestamps or status colors display correctly +- Performance — no measurement that shared module imports affect bundle size or load time (expected neutral) ## Notes for Tester -Doctor created this file only to restore the required artifact shape. Replace it with a real UAT script. + +- This is a pure refactoring slice. If all static checks pass, the risk of runtime regression is extremely low — the functions were copied verbatim from their original locations. +- The `formatTimestamp` locale options change is the only behavioral delta — it now uses explicit options instead of Intl defaults. On most locales this produces identical output, but edge-case locales could show minor formatting differences on the audit page. diff --git a/.gsd/milestones/M001/slices/S01/tasks/T02-VERIFY.json b/.gsd/milestones/M001/slices/S01/tasks/T02-VERIFY.json new file mode 100644 index 00000000..b9c7c9cb --- /dev/null +++ b/.gsd/milestones/M001/slices/S01/tasks/T02-VERIFY.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "taskId": "T02", + "unitId": "M001/S01/T02", + "timestamp": 1774222766821, + "passed": true, + "discoverySource": "package-json", + "checks": [ + { + "command": "npm run lint", + "exitCode": 0, + "durationMs": 8218, + "verdict": "pass" + } + ] +} From 87957682c0e54837508cb9a2eb45ec2571755694 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sun, 22 Mar 2026 23:42:14 +0000 Subject: [PATCH 12/73] chore(M001/S01): auto-commit after state-rebuild --- .gsd/doctor-history.jsonl | 1 + .gsd/gsd.db-wal | Bin 679832 -> 688072 bytes 2 files changed, 1 insertion(+) diff --git a/.gsd/doctor-history.jsonl b/.gsd/doctor-history.jsonl index 5c5276d9..2754bb6f 100644 --- a/.gsd/doctor-history.jsonl +++ b/.gsd/doctor-history.jsonl @@ -2,3 +2,4 @@ {"ts":"2026-03-22T23:31:32.945Z","ok":true,"errors":0,"warnings":2,"fixes":3,"codes":["state_file_stale","gitignore_missing_patterns"],"issues":[{"severity":"warning","code":"state_file_stale","message":"STATE.md is stale — shows \"planning\" but derived state is \"executing\"","unitId":"project"},{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"}],"fixDescriptions":["rebuilt STATE.md from derived state","added missing GSD runtime patterns to .gitignore","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"Clean · 3 fixed · STATE.md is stale — shows \"planning\" but derived state is \"executing\""} {"ts":"2026-03-22T23:33:52.798Z","ok":true,"errors":0,"warnings":1,"fixes":2,"codes":["gitignore_missing_patterns"],"issues":[{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"}],"fixDescriptions":["added missing GSD runtime patterns to .gitignore","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"Clean · 2 fixed · 5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json"} {"ts":"2026-03-22T23:39:26.714Z","ok":false,"errors":2,"warnings":3,"fixes":4,"codes":["state_file_stale","gitignore_missing_patterns","all_tasks_done_missing_slice_summary","all_tasks_done_missing_slice_uat","all_tasks_done_roadmap_not_checked"],"issues":[{"severity":"warning","code":"state_file_stale","message":"STATE.md is stale — shows \"executing\" but derived state is \"summarizing\"","unitId":"project"},{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"},{"severity":"error","code":"all_tasks_done_missing_slice_summary","message":"All tasks are done but S01-SUMMARY.md is missing","unitId":"M001/S01"},{"severity":"warning","code":"all_tasks_done_missing_slice_uat","message":"All tasks are done but S01-UAT.md is missing","unitId":"M001/S01"},{"severity":"error","code":"all_tasks_done_roadmap_not_checked","message":"All tasks are done but roadmap still shows S01 as incomplete","unitId":"M001/S01"}],"fixDescriptions":["rebuilt STATE.md from derived state","added missing GSD runtime patterns to .gitignore","created placeholder /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/milestones/M001/slices/S01/S01-UAT.md","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"2 errors, 3 warnings · 4 fixed · All tasks are done but S01-SUMMARY.md is missing"} +{"ts":"2026-03-22T23:42:14.906Z","ok":true,"errors":0,"warnings":2,"fixes":3,"codes":["state_file_stale","gitignore_missing_patterns"],"issues":[{"severity":"warning","code":"state_file_stale","message":"STATE.md is stale — shows \"summarizing\" but derived state is \"planning\"","unitId":"project"},{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"}],"fixDescriptions":["rebuilt STATE.md from derived state","added missing GSD runtime patterns to .gitignore","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"Clean · 3 fixed · STATE.md is stale — shows \"summarizing\" but derived state is \"planning\""} diff --git a/.gsd/gsd.db-wal b/.gsd/gsd.db-wal index 3f2811db4d1603ade08a96b4f15fdf37721d84b4..11a3fcbb8a4ed63121f46aa0fa7f382daf63d39e 100644 GIT binary patch delta 63 zcmV-F0Kos4zbVN7DS(6lgaU*Egam{Iga(8Mgb0KQvNJ? delta 29 kcmX@{UvtKO&4w1n7N!>F7M2#)7Pc1l7LFFqEnG9=0k5$NcK`qY From 9e63e75f90f0b0bf5ac884a549fd2bd1d18f7aa1 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sun, 22 Mar 2026 23:45:59 +0000 Subject: [PATCH 13/73] chore(M001/S02): auto-commit after research-slice --- .gsd/gsd.db-wal | Bin 688072 -> 840512 bytes .gsd/journal/2026-03-22.jsonl | 5 + .../M001/slices/S02/S02-RESEARCH.md | 109 ++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 .gsd/milestones/M001/slices/S02/S02-RESEARCH.md diff --git a/.gsd/gsd.db-wal b/.gsd/gsd.db-wal index 11a3fcbb8a4ed63121f46aa0fa7f382daf63d39e..5bea66d913c24eae7d97ddb4bc5a84e22dcfc943 100644 GIT binary patch delta 10451 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 M^v1gXGs|87KkFxBn*aa+ delta 29 kcmX@G%=pBA&4w1n7N!>F7M2#)7Pc1l7LFFqEnFw+0kOLZ-~a#s diff --git a/.gsd/journal/2026-03-22.jsonl b/.gsd/journal/2026-03-22.jsonl index 2622f7ba..34509f97 100644 --- a/.gsd/journal/2026-03-22.jsonl +++ b/.gsd/journal/2026-03-22.jsonl @@ -21,3 +21,8 @@ {"ts":"2026-03-22T23:39:35.317Z","flowId":"219d473c-29b2-4748-8541-c3c6bcb420e0","seq":2,"eventType":"dispatch-match","rule":"summarizing → complete-slice","data":{"unitType":"complete-slice","unitId":"M001/S01"}} {"ts":"2026-03-22T23:39:35.323Z","flowId":"219d473c-29b2-4748-8541-c3c6bcb420e0","seq":3,"eventType":"unit-start","data":{"unitType":"complete-slice","unitId":"M001/S01"}} {"ts":"2026-03-22T23:42:14.075Z","flowId":"219d473c-29b2-4748-8541-c3c6bcb420e0","seq":4,"eventType":"unit-end","data":{"unitType":"complete-slice","unitId":"M001/S01","status":"completed","artifactVerified":true},"causedBy":{"flowId":"219d473c-29b2-4748-8541-c3c6bcb420e0","seq":3}} +{"ts":"2026-03-22T23:42:15.019Z","flowId":"219d473c-29b2-4748-8541-c3c6bcb420e0","seq":5,"eventType":"iteration-end","data":{"iteration":4}} +{"ts":"2026-03-22T23:42:15.020Z","flowId":"80816278-1662-42d6-a8ec-5e3ecead6851","seq":1,"eventType":"iteration-start","data":{"iteration":5}} +{"ts":"2026-03-22T23:42:15.252Z","flowId":"80816278-1662-42d6-a8ec-5e3ecead6851","seq":2,"eventType":"dispatch-match","rule":"planning (no research, not S01) → research-slice","data":{"unitType":"research-slice","unitId":"M001/S02"}} +{"ts":"2026-03-22T23:42:15.267Z","flowId":"80816278-1662-42d6-a8ec-5e3ecead6851","seq":3,"eventType":"unit-start","data":{"unitType":"research-slice","unitId":"M001/S02"}} +{"ts":"2026-03-22T23:45:59.274Z","flowId":"80816278-1662-42d6-a8ec-5e3ecead6851","seq":4,"eventType":"unit-end","data":{"unitType":"research-slice","unitId":"M001/S02","status":"completed","artifactVerified":true},"causedBy":{"flowId":"80816278-1662-42d6-a8ec-5e3ecead6851","seq":3}} diff --git a/.gsd/milestones/M001/slices/S02/S02-RESEARCH.md b/.gsd/milestones/M001/slices/S02/S02-RESEARCH.md new file mode 100644 index 00000000..e4d13fb7 --- /dev/null +++ b/.gsd/milestones/M001/slices/S02/S02-RESEARCH.md @@ -0,0 +1,109 @@ +# S02: Router & Component Refactoring — Research + +**Date:** 2026-03-22 +**Status:** Ready for planning +**Depth:** Targeted — known patterns applied to known code, moderate complexity from volume + +## Summary + +S02 targets R003 (no file over ~800 lines), R007 (extract router logic to services), and must maintain R001 (`tsc --noEmit` exits 0) and R008 (`eslint src/` exits 0). Both `tsc` and `eslint` pass clean in the worktree as of research time. + +There are **5 files that must be split** (over 800 lines, non-exempt): `alerts/page.tsx` (1910), `pipeline.ts` router (1318), `dashboard.ts` router (1074), `team-settings.tsx` (865), and `users-settings.tsx` (813). Two files are borderline (710-795) and can be addressed if bandwidth allows. Two files are **exempt**: `flow-store.ts` (951 lines, cohesive Zustand store — splitting would create artificial boundaries) and `function-registry.ts` (1775 lines, declarative data — already exempt per D003). + +The codebase already has a well-established service extraction pattern in `src/server/services/` (35+ service modules, all using direct function exports with `prisma` imports). The alerts page already has 4 clearly-separated sections with comment headers. The settings `_components/` directory pattern exists and can be extended to alerts. This is straightforward refactoring with known patterns — the main risk is volume, not complexity. + +## Recommendation + +Split the work into 4 tasks by dependency order: + +1. **Alerts page split** — Highest ROI: 1910→~100 lines in the page file, 4 independent components extracted. Zero risk of hidden coupling since each section is already self-contained with its own hooks, mutations, and state. + +2. **Pipeline router service extraction** — Extract the 3 heaviest inline handlers (`saveGraph`, `promote`, `discardChanges`) into `src/server/services/pipeline-graph.ts`. This brings the router from 1318 to ~800 lines. The router already delegates to existing services for versions/rollback — extend that pattern. + +3. **Dashboard router service extraction** — Extract `chartMetrics` (~360 lines of time-series computation), `nodeCards` data assembly, and `pipelineCards` data assembly into `src/server/services/dashboard-data.ts`. This brings the router from 1074 to ~500 lines. + +4. **Settings components split** — Extract dialog sub-components from `team-settings.tsx` and `users-settings.tsx` into sibling files. These are only slightly over 800 lines, so extracting 2-3 dialogs each will bring them under target. + +## Implementation Landscape + +### Key Files + +**Must split (over 800 lines):** + +- `src/app/(dashboard)/alerts/page.tsx` (1910 lines) — 4 independent sections: `AlertRulesSection` (L144-630, ~486 lines), `NotificationChannelsSection` (L742-1324, ~582 lines + helpers at L671-741), `WebhooksSection` (L1339-1721, ~382 lines), `AlertHistorySection` (L1724-1875, ~151 lines). Constants/types at L65-142 are shared across sections. The main `AlertsPage` export (L1878-1910) is a thin wrapper composing the 4 sections with ``. + +- `src/server/routers/pipeline.ts` (1318 lines) — 25 tRPC procedures. Already delegates to `pipeline-version.ts`, `copy-pipeline-graph.ts`, `config-crypto.ts`, `strip-env-refs.ts`, `git-sync.ts`, `sli-evaluator.ts`, `push-registry.ts`. The heaviest inline logic: `saveGraph` (L683-825, 142 lines — shared component validation + node/edge transaction), `promote` (L559-682, 123 lines — cross-environment pipeline copy with secret stripping), `discardChanges` (L826-911, 85 lines — version restore). Zod schemas at top (L25-52) are local to this router. + +- `src/server/routers/dashboard.ts` (1074 lines) — 12 tRPC procedures. The `chartMetrics` endpoint (L604-964, ~360 lines) contains the largest block of inline logic: time-series bucketing, downsampling, CPU/memory delta computation, groupBy aggregation (pipeline/node/aggregate modes). `nodeCards` (L106-251, ~145 lines) and `pipelineCards` (L252-422, ~170 lines) also have substantial inline data assembly. `volumeAnalytics` (L492-603, ~111 lines) has bucketing logic. Custom dashboard views CRUD (L965-1074, ~109 lines) is thin. + +- `src/app/(dashboard)/settings/_components/team-settings.tsx` (865 lines) — Single exported `TeamSettings` component with ~12 mutations and multiple inline dialogs (reset password, lock/unlock, remove member, link to OIDC). The members table + dialogs form a natural sub-component. + +- `src/app/(dashboard)/settings/_components/users-settings.tsx` (813 lines) — Single exported `UsersSettings` component with ~8 mutations and multiple inline dialogs (assign to team, lock/unlock, reset password, delete user, create user, toggle super admin). Similar structure to team-settings. + +**Exempt (do not split):** + +- `src/stores/flow-store.ts` (951 lines) — Single Zustand store managing React Flow editor state. All methods operate on shared `nodes`, `edges`, history, and selection state. Splitting would require cross-store synchronization, adding complexity without reducing coupling. Decision D002 confirmed "moderate" refactoring depth. + +- `src/lib/vrl/function-registry.ts` (1775 lines) — Purely declarative data definitions. Already exempt per D003. + +**Borderline (address if bandwidth allows):** + +- `src/components/vrl-editor/vrl-editor.tsx` (795 lines) — Under 800, leave as-is. +- `src/server/routers/alert.ts` (710 lines) — Under 800, leave as-is. + +### Existing Service Pattern + +The project has 35+ service modules in `src/server/services/`. Pattern: +- Pure function exports (no classes, no DI) +- Import `prisma` from `@/lib/prisma` directly +- Import types from `@/generated/prisma` +- Throw `TRPCError` for error cases (coupling to tRPC is established convention) +- Example: `pipeline-version.ts` (170 lines) exports `createVersion`, `listVersions`, `getVersion`, `rollback` + +### Alerts Page Split Pattern + +The settings directory already uses a `_components/` pattern. For alerts: +- Create `src/app/(dashboard)/alerts/_components/` directory +- Extract each section into its own file: `alert-rules-section.tsx`, `notification-channels-section.tsx`, `webhooks-section.tsx`, `alert-history-section.tsx` +- Shared constants/types go in `_components/constants.ts` +- The main `page.tsx` becomes a thin composition (~50 lines) + +### Build Order + +1. **Alerts page split (T01)** — Independent, no downstream coupling. The 4 sections are completely self-contained (each has its own tRPC queries, mutations, state). Extract constants to shared file, then move each section. Verify with `tsc --noEmit`. + +2. **Pipeline router → service extraction (T02)** — Extract `saveGraph` validation+transaction, `promote` cross-env logic, and `discardChanges` restore logic into `src/server/services/pipeline-graph.ts`. These handlers don't share state with other endpoints. Verify with `tsc --noEmit`. + +3. **Dashboard router → service extraction (T03)** — Extract `chartMetrics` computation (the biggest win — 360 lines of pure data transformation), `nodeCards` assembly, and `pipelineCards` assembly into `src/server/services/dashboard-data.ts`. The `chartMetrics` handler contains utility functions (`addPoint`, `downsample`, `avgSeries`, `sumSeries`) that belong in the service. Verify with `tsc --noEmit`. + +4. **Settings components split (T04)** — Extract member management dialogs from `team-settings.tsx` and user management dialogs from `users-settings.tsx`. These share mutation hooks with the parent, so the extracted components will receive callbacks as props. This task has the most UI coupling — do it last so earlier tasks are verified clean. + +Tasks 1-3 are fully independent of each other and can run in parallel if desired. Task 4 is also independent but lower priority. + +### Verification Approach + +After each task: +1. `pnpm exec tsc --noEmit` exits 0 +2. `pnpm exec eslint src/` exits 0 + +After all tasks: +3. `find src -name '*.ts' -o -name '*.tsx' | xargs wc -l | sort -rn | head -20` — confirm no non-exempt file exceeds ~800 lines +4. `wc -l src/server/routers/pipeline.ts src/server/routers/dashboard.ts` — both under ~800 +5. `wc -l src/app/(dashboard)/alerts/page.tsx` — under ~200 + +## Constraints + +- **No API contract changes** — router refactoring is internal only. Service functions receive the same inputs and return the same outputs as the inline handlers. +- **tRPC middleware stays in routers** — `.use(withTeamAccess())`, `.use(withAudit())`, and `.use(requireSuperAdmin())` remain on the router procedure definitions. Only the handler body (`async ({ input, ctx }) => { ... }`) moves to service functions. +- **`ctx` access** — Some handlers read `ctx.session.user?.id`. Service functions should accept `userId` as a parameter rather than receiving the full `ctx` object, keeping them context-agnostic. +- **Audit metadata** — `saveGraph` sets `ctx.auditMetadata` directly. This side-effect must remain in the router handler; the service function returns the data, the router sets the metadata. + +## Common Pitfalls + +- **Circular imports from constants** — When extracting alert constants to `_components/constants.ts`, ensure types like `RuleFormState` and `ChannelFormState` are co-located with their section components (not in the shared constants file) to avoid pulling in section-specific dependencies. +- **Dashboard `metricStore` import** — `dashboard.ts` imports `metricStore` (a singleton in-memory metric store). The service extraction must pass `metricStore.getLatestAll()` results as a parameter to the extracted function, not import `metricStore` in the service — the service layer should remain stateless. +- **Prisma transaction context (`tx`)** — `saveGraph` and `discardChanges` use `prisma.$transaction(async (tx) => { ... })`. The extracted service function should accept the transaction client as a parameter so the transaction boundary stays in the service but the `tx` vs `prisma` choice is explicit. Follow the pattern in `copy-pipeline-graph.ts` which takes a `tx` parameter. + +## Open Risks + +- **`team-settings.tsx` and `users-settings.tsx` dialog extraction complexity** — These components share many mutation hooks and state variables between the table and its dialogs. Extracting dialogs means threading 3-5 callbacks per dialog as props. If the prop drilling makes the code harder to read than the current monolith, consider keeping them slightly over 800 lines. The ~800 target is a guideline, not a hard line. From 5f9b12ea6520d9b6bfb0351671a5693a6765aecf Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sun, 22 Mar 2026 23:46:00 +0000 Subject: [PATCH 14/73] chore(M001/S02): auto-commit after state-rebuild --- .gsd/doctor-history.jsonl | 1 + .gsd/gsd.db-wal | Bin 840512 -> 848752 bytes 2 files changed, 1 insertion(+) diff --git a/.gsd/doctor-history.jsonl b/.gsd/doctor-history.jsonl index 2754bb6f..5e4b1d20 100644 --- a/.gsd/doctor-history.jsonl +++ b/.gsd/doctor-history.jsonl @@ -3,3 +3,4 @@ {"ts":"2026-03-22T23:33:52.798Z","ok":true,"errors":0,"warnings":1,"fixes":2,"codes":["gitignore_missing_patterns"],"issues":[{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"}],"fixDescriptions":["added missing GSD runtime patterns to .gitignore","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"Clean · 2 fixed · 5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json"} {"ts":"2026-03-22T23:39:26.714Z","ok":false,"errors":2,"warnings":3,"fixes":4,"codes":["state_file_stale","gitignore_missing_patterns","all_tasks_done_missing_slice_summary","all_tasks_done_missing_slice_uat","all_tasks_done_roadmap_not_checked"],"issues":[{"severity":"warning","code":"state_file_stale","message":"STATE.md is stale — shows \"executing\" but derived state is \"summarizing\"","unitId":"project"},{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"},{"severity":"error","code":"all_tasks_done_missing_slice_summary","message":"All tasks are done but S01-SUMMARY.md is missing","unitId":"M001/S01"},{"severity":"warning","code":"all_tasks_done_missing_slice_uat","message":"All tasks are done but S01-UAT.md is missing","unitId":"M001/S01"},{"severity":"error","code":"all_tasks_done_roadmap_not_checked","message":"All tasks are done but roadmap still shows S01 as incomplete","unitId":"M001/S01"}],"fixDescriptions":["rebuilt STATE.md from derived state","added missing GSD runtime patterns to .gitignore","created placeholder /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/milestones/M001/slices/S01/S01-UAT.md","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"2 errors, 3 warnings · 4 fixed · All tasks are done but S01-SUMMARY.md is missing"} {"ts":"2026-03-22T23:42:14.906Z","ok":true,"errors":0,"warnings":2,"fixes":3,"codes":["state_file_stale","gitignore_missing_patterns"],"issues":[{"severity":"warning","code":"state_file_stale","message":"STATE.md is stale — shows \"summarizing\" but derived state is \"planning\"","unitId":"project"},{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"}],"fixDescriptions":["rebuilt STATE.md from derived state","added missing GSD runtime patterns to .gitignore","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"Clean · 3 fixed · STATE.md is stale — shows \"summarizing\" but derived state is \"planning\""} +{"ts":"2026-03-22T23:46:00.028Z","ok":true,"errors":0,"warnings":2,"fixes":2,"codes":["gitignore_missing_patterns","missing_slice_plan"],"issues":[{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"},{"severity":"warning","code":"missing_slice_plan","message":"Slice M001/S02 has no plan file","unitId":"M001/S02"}],"fixDescriptions":["added missing GSD runtime patterns to .gitignore","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"Clean · 2 fixed · 5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json"} diff --git a/.gsd/gsd.db-wal b/.gsd/gsd.db-wal index 5bea66d913c24eae7d97ddb4bc5a84e22dcfc943..47b8a6cfdef9a4699518a2e856fb18e82966e0fe 100644 GIT binary patch delta 72 zcmV-O0Js0ZrZe#7Gk}BvgaU*Egam{Iga(8Mgb0KQgbIWUvDkUY e-!r%1f(!=`moLK%2?(syJ3UkWRtl#;!we7;r5thq delta 33 ocmezH%=o}EF7M2#)7Pc1l7LFFq7OocVEj$jF0pMv1 Date: Sun, 22 Mar 2026 23:49:16 +0000 Subject: [PATCH 15/73] docs(S02): add slice plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split 5 over-limit files across 4 tasks: - T01: alerts page (1910 lines) → 4 section components - T02: pipeline router (1318 lines) → pipeline-graph service - T03: dashboard router (1074 lines) → dashboard-data service - T04: settings components (865/813 lines) → dialog extractions Covers R003 (file size), R007 (router→service), maintains R001/R008. --- .gsd/milestones/M001/slices/S02/S02-PLAN.md | 75 +++++++++++++++++++ .../M001/slices/S02/tasks/T01-PLAN.md | 67 +++++++++++++++++ .../M001/slices/S02/tasks/T02-PLAN.md | 68 +++++++++++++++++ .../M001/slices/S02/tasks/T03-PLAN.md | 64 ++++++++++++++++ .../M001/slices/S02/tasks/T04-PLAN.md | 65 ++++++++++++++++ 5 files changed, 339 insertions(+) create mode 100644 .gsd/milestones/M001/slices/S02/S02-PLAN.md create mode 100644 .gsd/milestones/M001/slices/S02/tasks/T01-PLAN.md create mode 100644 .gsd/milestones/M001/slices/S02/tasks/T02-PLAN.md create mode 100644 .gsd/milestones/M001/slices/S02/tasks/T03-PLAN.md create mode 100644 .gsd/milestones/M001/slices/S02/tasks/T04-PLAN.md diff --git a/.gsd/milestones/M001/slices/S02/S02-PLAN.md b/.gsd/milestones/M001/slices/S02/S02-PLAN.md new file mode 100644 index 00000000..dac7af70 --- /dev/null +++ b/.gsd/milestones/M001/slices/S02/S02-PLAN.md @@ -0,0 +1,75 @@ +# S02: Router & Component Refactoring + +**Goal:** All source files are under ~800 lines (excluding exempt files), router business logic is extracted to service modules, `tsc --noEmit` and `eslint src/` still pass clean. +**Demo:** `find src -name '*.ts' -o -name '*.tsx' | xargs wc -l | sort -rn | head -20` shows no non-exempt file over ~800 lines; `pnpm exec tsc --noEmit` exits 0; `pnpm exec eslint src/` exits 0. + +## Must-Haves + +- Alerts page split into 4 section components under `src/app/(dashboard)/alerts/_components/`, main `page.tsx` under ~200 lines +- Pipeline router `saveGraph`, `promote`, and `discardChanges` handler logic extracted to `src/server/services/pipeline-graph.ts`; router file under ~800 lines +- Dashboard router `chartMetrics`, `nodeCards`, and `pipelineCards` computation extracted to `src/server/services/dashboard-data.ts`; router file under ~800 lines +- Settings components (`team-settings.tsx`, `users-settings.tsx`) split by extracting dialog sub-components; both under ~800 lines +- `tsc --noEmit` exits 0 after all changes (R001) +- `eslint src/` exits 0 after all changes (R008) +- No API contract changes — all refactoring is internal + +## Verification + +- `pnpm exec tsc --noEmit` exits 0 +- `pnpm exec eslint src/` exits 0 +- `wc -l src/app/(dashboard)/alerts/page.tsx` — under 200 lines +- `wc -l src/server/routers/pipeline.ts` — under 850 lines +- `wc -l src/server/routers/dashboard.ts` — under 850 lines +- `wc -l src/app/(dashboard)/settings/_components/team-settings.tsx` — under 800 lines +- `wc -l src/app/(dashboard)/settings/_components/users-settings.tsx` — under 800 lines +- `test -f src/server/services/pipeline-graph.ts` — service file exists +- `test -f src/server/services/dashboard-data.ts` — service file exists +- `test -d src/app/(dashboard)/alerts/_components` — alerts components directory exists +- `find src -name '*.ts' -o -name '*.tsx' | xargs wc -l | sort -rn | head -10` — no non-exempt file over ~800 lines (exempt: `flow-store.ts` per D002, `function-registry.ts` per D003) + +## Tasks + +- [ ] **T01: Split alerts page into section components** `est:45m` + - Why: Alerts page is 1910 lines — the largest non-exempt file. It has 4 clearly separated sections (`AlertRulesSection`, `NotificationChannelsSection`, `WebhooksSection`, `AlertHistorySection`) that are already self-contained with their own hooks, mutations, and state. Extracting them is the highest-ROI split in S02. + - Files: `src/app/(dashboard)/alerts/page.tsx`, `src/app/(dashboard)/alerts/_components/alert-rules-section.tsx`, `src/app/(dashboard)/alerts/_components/notification-channels-section.tsx`, `src/app/(dashboard)/alerts/_components/webhooks-section.tsx`, `src/app/(dashboard)/alerts/_components/alert-history-section.tsx`, `src/app/(dashboard)/alerts/_components/constants.ts` + - Do: Create `_components/` directory. Extract shared constants/types (L65-142) to `constants.ts` — but keep form-state types (like `RuleFormState`, `ChannelFormState`) co-located with their section components to avoid pulling in section-specific dependencies. Move each section function into its own file with all its local helpers. Thin `page.tsx` to a composition wrapper (~50-100 lines) that imports and renders the 4 sections. Preserve all existing imports in each section file. + - Verify: `pnpm exec tsc --noEmit` exits 0 && `wc -l src/app/(dashboard)/alerts/page.tsx` under 200 + - Done when: Alerts page under 200 lines, 4 section component files exist, `tsc` and `eslint` pass clean + +- [ ] **T02: Extract pipeline router business logic to service module** `est:45m` + - Why: Pipeline router is 1318 lines with 3 heavy inline handlers. The codebase already delegates to services like `pipeline-version.ts` — extending this pattern to `saveGraph`, `promote`, and `discardChanges` brings the router under ~800 lines and advances R007 (thin routers). + - Files: `src/server/routers/pipeline.ts`, `src/server/services/pipeline-graph.ts` + - Do: Create `pipeline-graph.ts` following the existing service pattern (direct function exports, import `prisma` from `@/lib/prisma`, throw `TRPCError`). Extract: (1) `saveGraph` validation + transaction logic — accept `tx: Prisma.TransactionClient` parameter, return the saved data, leave `ctx.auditMetadata` assignment in the router; (2) `promote` cross-environment copy logic — accept `userId` as parameter instead of full `ctx`; (3) `discardChanges` version restore logic — accept `tx` parameter. Keep all `.use(withAudit())` and `.use(withTeamAccess())` middleware in the router. Keep Zod schemas in the router file (they're local to the router). + - Verify: `pnpm exec tsc --noEmit` exits 0 && `wc -l src/server/routers/pipeline.ts` under 850 + - Done when: `pipeline-graph.ts` service exists with 3 exported functions, pipeline router under ~800 lines, `tsc` and `eslint` pass clean + +- [ ] **T03: Extract dashboard router computation to service module** `est:45m` + - Why: Dashboard router is 1074 lines. The `chartMetrics` endpoint alone is 360 lines of pure data transformation (time-series bucketing, downsampling, aggregation). Extracting this plus `nodeCards` and `pipelineCards` assembly brings the router well under 800 lines and produces a testable service module for S04. + - Files: `src/server/routers/dashboard.ts`, `src/server/services/dashboard-data.ts` + - Do: Create `dashboard-data.ts` service. Extract: (1) `chartMetrics` computation including `addPoint`, `downsample`, `avgSeries`, `sumSeries` utility functions — accept the DB query results and metric samples as parameters (do NOT import `metricStore` in the service — the router passes `metricStore.getLatestAll()` results in); (2) `nodeCards` data assembly — accept raw DB query results, return assembled card data; (3) `pipelineCards` data assembly — same pattern. The service must remain stateless — all singleton/side-effect access stays in the router. + - Verify: `pnpm exec tsc --noEmit` exits 0 && `wc -l src/server/routers/dashboard.ts` under 850 + - Done when: `dashboard-data.ts` service exists, dashboard router under ~800 lines, `tsc` and `eslint` pass clean + +- [ ] **T04: Extract settings dialog sub-components** `est:30m` + - Why: `team-settings.tsx` (865 lines) and `users-settings.tsx` (813 lines) are just over the ~800-line target. Each contains multiple inline dialogs that can be extracted to sibling files. This task brings both under target and completes R003 coverage. + - Files: `src/app/(dashboard)/settings/_components/team-settings.tsx`, `src/app/(dashboard)/settings/_components/users-settings.tsx`, `src/app/(dashboard)/settings/_components/team-member-dialogs.tsx`, `src/app/(dashboard)/settings/_components/user-management-dialogs.tsx` + - Do: For `team-settings.tsx`: extract dialog components (reset password, lock/unlock, remove member, link to OIDC) into `team-member-dialogs.tsx`. The parent passes mutation callbacks and state as props. For `users-settings.tsx`: extract dialog components (assign to team, lock/unlock, reset password, delete user, create user, toggle super admin) into `user-management-dialogs.tsx`. Same pattern. If prop drilling makes a component harder to read than the monolith, keep that dialog inline — the ~800 target is a guideline (per research risk note). Aim for each parent file under 800 lines but accept ~650-700 as a realistic target. + - Verify: `pnpm exec tsc --noEmit` exits 0 && `wc -l src/app/(dashboard)/settings/_components/team-settings.tsx src/app/(dashboard)/settings/_components/users-settings.tsx` both under 800 + - Done when: Both settings files under 800 lines, dialog components extracted to sibling files, `tsc` and `eslint` pass clean + +## Files Likely Touched + +- `src/app/(dashboard)/alerts/page.tsx` +- `src/app/(dashboard)/alerts/_components/alert-rules-section.tsx` +- `src/app/(dashboard)/alerts/_components/notification-channels-section.tsx` +- `src/app/(dashboard)/alerts/_components/webhooks-section.tsx` +- `src/app/(dashboard)/alerts/_components/alert-history-section.tsx` +- `src/app/(dashboard)/alerts/_components/constants.ts` +- `src/server/routers/pipeline.ts` +- `src/server/services/pipeline-graph.ts` +- `src/server/routers/dashboard.ts` +- `src/server/services/dashboard-data.ts` +- `src/app/(dashboard)/settings/_components/team-settings.tsx` +- `src/app/(dashboard)/settings/_components/users-settings.tsx` +- `src/app/(dashboard)/settings/_components/team-member-dialogs.tsx` +- `src/app/(dashboard)/settings/_components/user-management-dialogs.tsx` diff --git a/.gsd/milestones/M001/slices/S02/tasks/T01-PLAN.md b/.gsd/milestones/M001/slices/S02/tasks/T01-PLAN.md new file mode 100644 index 00000000..2dab9766 --- /dev/null +++ b/.gsd/milestones/M001/slices/S02/tasks/T01-PLAN.md @@ -0,0 +1,67 @@ +--- +estimated_steps: 5 +estimated_files: 7 +skills_used: + - lint + - review +--- + +# T01: Split alerts page into section components + +**Slice:** S02 — Router & Component Refactoring +**Milestone:** M001 + +## Description + +The alerts page (`src/app/(dashboard)/alerts/page.tsx`) is 1910 lines — the single largest non-exempt file in the codebase. It contains 4 clearly separated sections (`AlertRulesSection` at L144-630, `NotificationChannelsSection` at L742-1324, `WebhooksSection` at L1339-1721, `AlertHistorySection` at L1724-1875) that are already self-contained with their own tRPC queries, mutations, and local state. The main export (L1878-1910) is a thin wrapper composing them with ``. + +This task extracts each section into its own file under `src/app/(dashboard)/alerts/_components/`, with shared constants in a `constants.ts` file. The main `page.tsx` becomes a ~50-100 line composition wrapper. + +## Steps + +1. **Read the full alerts page** to identify all shared constants, types, and imports at the top of the file (L1-142). Identify which types/constants are truly shared across sections vs. section-specific. + +2. **Create `_components/constants.ts`** with shared constants and types that are used by multiple sections (e.g., environment-related constants, shared enums). Do NOT put form-state types (`RuleFormState`, `ChannelFormState`, etc.) here — keep them co-located with their section to avoid pulling in section-specific deps. + +3. **Extract each section into its own file:** + - `_components/alert-rules-section.tsx` — `AlertRulesSection` function (L144-630) plus any helpers used exclusively by it (check L65-142 for section-specific helpers/types). Include all necessary imports (React, tRPC hooks, UI components, types). + - `_components/notification-channels-section.tsx` — `NotificationChannelsSection` function (L742-1324) plus helper functions at L671-741 that are exclusive to this section. + - `_components/webhooks-section.tsx` — `WebhooksSection` function (L1339-1721) with its local helpers. + - `_components/alert-history-section.tsx` — `AlertHistorySection` function (L1724-1875) with its local helpers. + +4. **Rewrite `page.tsx`** as a thin composition wrapper: imports from `_components/`, renders `AlertsPage` component that composes the 4 sections with environment selector and `` elements. Should be ~50-100 lines. + +5. **Verify** `tsc --noEmit` and `eslint src/` both pass clean. Check `wc -l` on `page.tsx` to confirm under 200 lines. + +## Must-Haves + +- [ ] `page.tsx` under 200 lines after extraction +- [ ] All 4 section component files exist and are importable +- [ ] Shared constants in `_components/constants.ts` — form-state types stay co-located with their section +- [ ] `pnpm exec tsc --noEmit` exits 0 +- [ ] `pnpm exec eslint src/` exits 0 +- [ ] No changes to the alert page's runtime behavior — same components, same props, same rendering + +## Verification + +- `pnpm exec tsc --noEmit` exits 0 +- `pnpm exec eslint src/` exits 0 +- `wc -l src/app/(dashboard)/alerts/page.tsx` — under 200 lines +- `test -f src/app/(dashboard)/alerts/_components/alert-rules-section.tsx` +- `test -f src/app/(dashboard)/alerts/_components/notification-channels-section.tsx` +- `test -f src/app/(dashboard)/alerts/_components/webhooks-section.tsx` +- `test -f src/app/(dashboard)/alerts/_components/alert-history-section.tsx` +- `test -f src/app/(dashboard)/alerts/_components/constants.ts` + +## Inputs + +- `src/app/(dashboard)/alerts/page.tsx` — the 1910-line source file to split + +## Expected Output + +- `src/app/(dashboard)/alerts/page.tsx` — rewritten as thin composition wrapper (~50-100 lines) +- `src/app/(dashboard)/alerts/_components/alert-rules-section.tsx` — AlertRulesSection component (~500 lines) +- `src/app/(dashboard)/alerts/_components/notification-channels-section.tsx` — NotificationChannelsSection component (~600 lines) +- `src/app/(dashboard)/alerts/_components/webhooks-section.tsx` — WebhooksSection component (~400 lines) +- `src/app/(dashboard)/alerts/_components/alert-history-section.tsx` — AlertHistorySection component (~160 lines) +- `src/app/(dashboard)/alerts/_components/constants.ts` — shared constants and types diff --git a/.gsd/milestones/M001/slices/S02/tasks/T02-PLAN.md b/.gsd/milestones/M001/slices/S02/tasks/T02-PLAN.md new file mode 100644 index 00000000..6b75506c --- /dev/null +++ b/.gsd/milestones/M001/slices/S02/tasks/T02-PLAN.md @@ -0,0 +1,68 @@ +--- +estimated_steps: 4 +estimated_files: 2 +skills_used: + - lint + - review +--- + +# T02: Extract pipeline router business logic to service module + +**Slice:** S02 — Router & Component Refactoring +**Milestone:** M001 + +## Description + +The pipeline router (`src/server/routers/pipeline.ts`, 1318 lines) contains 25 tRPC procedures. It already delegates to services like `pipeline-version.ts` and `copy-pipeline-graph.ts`, but 3 handlers have substantial inline logic: `saveGraph` (L683-825, 142 lines — component validation + node/edge transaction), `promote` (L559-682, 123 lines — cross-environment pipeline copy with secret stripping), and `discardChanges` (L826-911, 85 lines — version restore). Extracting these to `src/server/services/pipeline-graph.ts` brings the router under ~800 lines and makes the business logic independently testable. + +**Key constraints from research:** +- Follow the existing service pattern: pure function exports, import `prisma` from `@/lib/prisma`, throw `TRPCError` for errors +- `saveGraph` sets `ctx.auditMetadata` directly — this side-effect MUST remain in the router. The service function returns the data, the router sets the metadata. +- Service functions accept `userId: string` instead of the full `ctx` object +- For `saveGraph` and `discardChanges` which use `prisma.$transaction(async (tx) => { ... })`, follow the `copy-pipeline-graph.ts` pattern: define `type Tx = Prisma.TransactionClient` and accept `tx` as a parameter +- Keep all `.use(withAudit())`, `.use(withTeamAccess())`, `.use(requireSuperAdmin())` middleware in the router +- Keep Zod schemas (L25-52) in the router file — they're local to the router + +## Steps + +1. **Read the pipeline router** to understand the full handler bodies for `saveGraph` (L683-825), `promote` (L559-682), and `discardChanges` (L826-911). Identify all dependencies: prisma calls, imported services, types, and `ctx` accesses. + +2. **Create `src/server/services/pipeline-graph.ts`** with 3 exported functions: + - `saveGraphComponents(tx: Tx, pipelineId: string, nodes: ..., edges: ..., userId: string)` — the validation + transaction body from `saveGraph`. Returns the saved pipeline data. Does NOT set `auditMetadata`. + - `promotePipeline(sourcePipelineId: string, targetEnvironmentId: string, userId: string, ...)` — the cross-environment copy logic from `promote`. Returns the promoted pipeline. + - `discardPipelineChanges(tx: Tx, pipelineId: string, versionId: string)` — the version restore logic from `discardChanges`. Returns the restored pipeline. + - Use `type Tx = Prisma.TransactionClient` at the top, matching `copy-pipeline-graph.ts`. + +3. **Update the pipeline router** to import and call the service functions. Each handler becomes a thin wrapper: parse input, call service function, set `auditMetadata` if needed, return result. The `$transaction` boundary can stay in either the router or the service depending on what reads more naturally — but `auditMetadata` assignment MUST stay in the router. + +4. **Verify** `tsc --noEmit` and `eslint src/` both pass. Check `wc -l` on the router to confirm under ~800 lines. + +## Must-Haves + +- [ ] `pipeline-graph.ts` service module exists with exported functions for saveGraph, promote, and discardChanges logic +- [ ] Pipeline router under ~800 lines +- [ ] `ctx.auditMetadata` assignment remains in the router, NOT in the service +- [ ] Service functions accept `userId` parameter, not full `ctx` +- [ ] `pnpm exec tsc --noEmit` exits 0 +- [ ] `pnpm exec eslint src/` exits 0 +- [ ] No API contract changes — router endpoints accept same inputs and return same outputs + +## Verification + +- `pnpm exec tsc --noEmit` exits 0 +- `pnpm exec eslint src/` exits 0 +- `wc -l src/server/routers/pipeline.ts` — under 850 lines +- `test -f src/server/services/pipeline-graph.ts` +- `grep -q 'auditMetadata' src/server/routers/pipeline.ts` — still in router +- `! grep -q 'auditMetadata' src/server/services/pipeline-graph.ts` — NOT in service + +## Inputs + +- `src/server/routers/pipeline.ts` — the 1318-line router to refactor +- `src/server/services/copy-pipeline-graph.ts` — reference for the `Tx` type pattern +- `src/server/services/pipeline-version.ts` — reference for the service module pattern + +## Expected Output + +- `src/server/services/pipeline-graph.ts` — new service module with 3 exported functions (~300 lines) +- `src/server/routers/pipeline.ts` — slimmed router with handlers delegating to service (~800 lines) diff --git a/.gsd/milestones/M001/slices/S02/tasks/T03-PLAN.md b/.gsd/milestones/M001/slices/S02/tasks/T03-PLAN.md new file mode 100644 index 00000000..d8ab9244 --- /dev/null +++ b/.gsd/milestones/M001/slices/S02/tasks/T03-PLAN.md @@ -0,0 +1,64 @@ +--- +estimated_steps: 4 +estimated_files: 2 +skills_used: + - lint + - review +--- + +# T03: Extract dashboard router computation to service module + +**Slice:** S02 — Router & Component Refactoring +**Milestone:** M001 + +## Description + +The dashboard router (`src/server/routers/dashboard.ts`, 1074 lines) contains 12 tRPC procedures. The `chartMetrics` endpoint (L604-964, ~360 lines) has the largest block of inline logic: time-series bucketing, downsampling, CPU/memory delta computation, and groupBy aggregation across pipeline/node/aggregate modes. It also contains local utility functions (`addPoint`, `downsample`, `avgSeries`, `sumSeries`) that belong in a service module. `nodeCards` (L106-251, ~145 lines) and `pipelineCards` (L252-422, ~170 lines) have substantial inline data assembly. + +Extracting these to `src/server/services/dashboard-data.ts` brings the router well under 800 lines and produces a testable service for S04. + +**Key constraint from research:** The dashboard router imports `metricStore` (a singleton in-memory metric store at `@/server/services/metric-store`). The service extraction MUST NOT import `metricStore` in the service — the router passes `metricStore.getLatestAll()` results as a parameter to extracted functions, keeping the service layer stateless. + +## Steps + +1. **Read the dashboard router** to understand the full handler bodies for `chartMetrics` (L604-964), `nodeCards` (L106-251), and `pipelineCards` (L252-422). Identify local utility functions (`addPoint`, `downsample`, `avgSeries`, `sumSeries`) and their parameter types. Map all `metricStore` access points (L167, L306). + +2. **Create `src/server/services/dashboard-data.ts`** with exported functions: + - `computeChartMetrics(dbResults: ..., latestSamples: ..., options: ...)` — the full time-series computation from `chartMetrics`. Include the `addPoint`, `downsample`, `avgSeries`, `sumSeries` utility functions as module-private helpers. Accept DB query results and metric samples as parameters (NOT `metricStore` directly). + - `assembleNodeCards(nodes: ..., latestSamples: ...)` — the data assembly from `nodeCards`. Accept raw DB results and metric samples. + - `assemblePipelineCards(pipelines: ..., latestSamples: ...)` — the data assembly from `pipelineCards`. Same pattern. + - Follow the existing service pattern: direct function exports, import types from `@/generated/prisma`. + - Do NOT import `metricStore` — the service must remain stateless. + +3. **Update the dashboard router** to import and call the service functions. Each handler: run DB query, get metric samples from `metricStore`, pass both to service function, return result. The router stays responsible for DB queries and `metricStore` access — the service does pure computation. + +4. **Verify** `tsc --noEmit` and `eslint src/` both pass. Check `wc -l` on the router to confirm under ~800 lines. + +## Must-Haves + +- [ ] `dashboard-data.ts` service module exists with exported functions for chartMetrics, nodeCards, and pipelineCards logic +- [ ] Dashboard router under ~800 lines +- [ ] `metricStore` import stays in the router only — NOT imported in the service +- [ ] Service functions are stateless — accept data as parameters, return computed results +- [ ] `pnpm exec tsc --noEmit` exits 0 +- [ ] `pnpm exec eslint src/` exits 0 +- [ ] No API contract changes + +## Verification + +- `pnpm exec tsc --noEmit` exits 0 +- `pnpm exec eslint src/` exits 0 +- `wc -l src/server/routers/dashboard.ts` — under 850 lines +- `test -f src/server/services/dashboard-data.ts` +- `! grep -q 'metricStore' src/server/services/dashboard-data.ts` — NOT imported in service +- `grep -q 'metricStore' src/server/routers/dashboard.ts` — still in router + +## Inputs + +- `src/server/routers/dashboard.ts` — the 1074-line router to refactor +- `src/server/services/pipeline-version.ts` — reference for the service module pattern + +## Expected Output + +- `src/server/services/dashboard-data.ts` — new service module with computation functions (~500 lines) +- `src/server/routers/dashboard.ts` — slimmed router delegating to service (~550 lines) diff --git a/.gsd/milestones/M001/slices/S02/tasks/T04-PLAN.md b/.gsd/milestones/M001/slices/S02/tasks/T04-PLAN.md new file mode 100644 index 00000000..10e33529 --- /dev/null +++ b/.gsd/milestones/M001/slices/S02/tasks/T04-PLAN.md @@ -0,0 +1,65 @@ +--- +estimated_steps: 4 +estimated_files: 4 +skills_used: + - lint + - review +--- + +# T04: Extract settings dialog sub-components + +**Slice:** S02 — Router & Component Refactoring +**Milestone:** M001 + +## Description + +`team-settings.tsx` (865 lines) and `users-settings.tsx` (813 lines) are slightly over the ~800-line target. Each contains a main component with a data table plus multiple inline dialog components (reset password, lock/unlock, remove member, etc.). Extracting the dialogs into sibling files brings both under target and completes R003 coverage for all non-exempt files. + +**Risk note from research:** These components share many mutation hooks and state variables between the table and its dialogs. Extracting dialogs means threading 3-5 callbacks per dialog as props. If prop drilling makes a component harder to read than the current monolith, keep that dialog inline — the ~800 target is a guideline, not a hard line. Aim for each parent file in the 550-700 line range but prioritize readability. + +## Steps + +1. **Read both settings files** to identify which dialogs are most independent (least shared state) and which are tightly coupled to the parent. Prioritize extracting the most independent dialogs first. Check what mutation hooks, state variables, and callbacks each dialog uses. + +2. **Extract team member dialogs** from `team-settings.tsx` into `team-member-dialogs.tsx`: + - Identify dialogs: reset password dialog, lock/unlock confirmation, remove member confirmation, link to OIDC dialog. + - Create a new file with these dialog components. Each accepts its needed data and callbacks as props (open state, onConfirm callback, member data, loading state). + - Update `team-settings.tsx` to import and use the extracted dialogs. + - Target: `team-settings.tsx` under 800 lines. + +3. **Extract user management dialogs** from `users-settings.tsx` into `user-management-dialogs.tsx`: + - Identify dialogs: assign to team, lock/unlock, reset password, delete user, create user, toggle super admin. + - Same extraction pattern as step 2. + - Target: `users-settings.tsx` under 800 lines. + +4. **Verify** `tsc --noEmit` and `eslint src/` both pass. Check `wc -l` on both files. + +## Must-Haves + +- [ ] `team-settings.tsx` under 800 lines +- [ ] `users-settings.tsx` under 800 lines +- [ ] Extracted dialog components are properly typed with explicit prop interfaces +- [ ] `pnpm exec tsc --noEmit` exits 0 +- [ ] `pnpm exec eslint src/` exits 0 +- [ ] No changes to runtime behavior — same dialogs, same functionality + +## Verification + +- `pnpm exec tsc --noEmit` exits 0 +- `pnpm exec eslint src/` exits 0 +- `wc -l src/app/(dashboard)/settings/_components/team-settings.tsx` — under 800 lines +- `wc -l src/app/(dashboard)/settings/_components/users-settings.tsx` — under 800 lines +- `test -f src/app/(dashboard)/settings/_components/team-member-dialogs.tsx` +- `test -f src/app/(dashboard)/settings/_components/user-management-dialogs.tsx` + +## Inputs + +- `src/app/(dashboard)/settings/_components/team-settings.tsx` — 865-line source to split +- `src/app/(dashboard)/settings/_components/users-settings.tsx` — 813-line source to split + +## Expected Output + +- `src/app/(dashboard)/settings/_components/team-settings.tsx` — slimmed to ~550-700 lines +- `src/app/(dashboard)/settings/_components/users-settings.tsx` — slimmed to ~550-700 lines +- `src/app/(dashboard)/settings/_components/team-member-dialogs.tsx` — extracted dialog components (~200-300 lines) +- `src/app/(dashboard)/settings/_components/user-management-dialogs.tsx` — extracted dialog components (~200-300 lines) From 346c5126df15733e722c65f7b124acb361357dab Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sun, 22 Mar 2026 23:49:24 +0000 Subject: [PATCH 16/73] chore(M001/S02): auto-commit after plan-slice --- .gsd/gsd.db-wal | Bin 848752 -> 1017672 bytes .gsd/journal/2026-03-22.jsonl | 5 +++++ 2 files changed, 5 insertions(+) diff --git a/.gsd/gsd.db-wal b/.gsd/gsd.db-wal index 47b8a6cfdef9a4699518a2e856fb18e82966e0fe..0e1e2db188be0d44d4945a9719a13dc3978f5173 100644 GIT binary patch delta 11883 zcmb_i3v?URnf6$Uqe$}FU`!rhav`Cy9ZQzvXIuy*PSO|>Fm@8U^nu3GSejbWC^I8F zBHIjbOj%eS1@IykUu;{p4=CC8q&K08fbUV*dJ&O zEp9yZxfhpZ(7IW3{iJq8x9ForaW|l;iF%Uc3k!Hqgz+q)#i-T zvZfNr)Ftvua6#osio~jYTLwjbd>hg_Tn7}*~ZVk5P7`?!c{*XeIRr`dTfl~RXUcKAEq@@IjsjZIxN^ys$8=t?2theGa4lacmw{ga>O zAM@b9eDd_)pE>_O^vu0-vSqc~zu2;(_E)z>zSZU}iNC4SKk7G+{o&n%-@f;}+PA+E zIo`IGW<1^Be*aHD^t0Q3_w!0EeNE)IZAt%%SNr>Y^5W-S|JB_4`&;oapv*Q!_P0#p zpBG>#&$@jqd}H8-+@9K=ww9j-m5Xb)c0_|Kl#6N~SrffKxCTS$uP&xvSc8uod+Nzh z?dt8(-!`s&t-s%0H@x(#Z|=Wy5UKmJ5*@|@-GKhxi@?;rpAEV5SNGKi%{+>AAI!gE8<<%FMPMgUSGM~`k23C=T}!<{1-{2Y76;e;vZ~jfctknY z7Wd4a`^N@<$I&Z)aL=dT({z-Ks5$)Arq(CIUj-yQ9u8b*UQ+94;omeXdu!3wmh+oQ zG#mY6A6)$F|9p0F{D-xNXQId39P+z!{p}atbkEo;-~IVfJbOoYFwS~=r#0KwOF4^W zjAGd+X(iiYR=J?ty}j(kt3Sea9Ruw^ReJs_o2s>cBebJB`IIr!zD4ubWB&)@KM4XTE^Cmk~PNC+X|X# zPgSs!Y4I@4y7wdp2V=tni9U8@aCBI#zo(C-FEMO=R?nz3c08|^N?O4`YGg1KOAe$4 z`l{f9#*(8WL+GCzO3IbSnKO>f*(%JT->#N5`RbuqaxldeW#yr+EK@V4 zVZmagG`K2RQPoTGh1R5+)6&pd*)VMq%jn2xj3nE`bj%4oq3eRFma#oyf|gL<*%}+H zPYcc3d6qtJS}UujiXCiC$HVT+W@sbSG>{yXtLAD-4KaIaa-5Y-Bco+2CfV1vf~l$5 zD$8mGEvMSp4Qw!7*2@|hRt#2VK_9T(Gz66~%GFrCEr+$`>n){OJr~QE)v|4fXHMF( zO}!k`O7k&Oo3&)moNmXgYAGXs!k}Z?yjrNx+U2uy#mZwy4jM75^i0Suuc`C81|!u= zv^EB((=#wKOP$x=MQb7B8Xg^pji#Woqyu$u=iqynN~t?2!#njXH#Qhi$;fK`S~jOK zxOB;aS7Xy;8W2^{u%V{zJwroVV#A{;%$yqVP0Y1So77FCR3!VC0&+BHty~off)$t= zT!SQ1h8k(^tZrpgGfTFf(=2+$XlgjNB?VQFCkdf9CXIB_mW@X^VDmOmc zC}VJHC^nSp;TBjhGHT%z(j<(?Vy02CHB*?yYA35&`56N*CdsnyWMX8{ZzRdoiADkt zg7RV3_FfG;$Y30Ny=(xmNaTsZ*p^{9=8=KnIxJ+2k_}sjIbm#pUA zp{*A+V4(@^vYAR|P9xmyV_9PXx+ns2>DTz={{Es?gpZQ3+A8;nip^En$8y+Yd3%+q zxg0D<=(XE3)qbvD(d59$SrPiSAjmW|j6)V&{K>>{EHy;%0CMKZgu}HrQN3$wC>0w_ z!5EGtN9uc43qavKtdU{?M+ig)Z>z91m8q8HXvLXA6|bH*3YDU^t)v#JHWz94p201t z*l=QiW=(o{g9&{?5k?Km%1()JY&IP<5T;0KqFg*fkx0PO_bQ;?F5zpa|o1R z2xs3BT;q6QssGBlR{+sY3|QTx!*yv;+?$3v3Yr8i09|??#(2RpEue~`rLg=XFsPzb z6RZ`%uUIMAdbt4qkd7<@sKvTr;c${Nq_kj|**kp~!JPz1i$ziJ$pfqPt0Z)& z*({A9Cn#u#bX!0+FcZVGm5gYsa1@HO9a=`Gh{#Y8NMQ`ZUcXs{7o0 zH!472n%>^+@C?o50k9h@hs#(@pJg+U`huPX7f=fjvn6zN+5TY1_Ia}q6H@PA`U>y| z7wsO|(%{RSe;dg%!!~e(#Zc#GAtb4uweseGIT~loYu^F zJ)^Nn)kZifg~Q~t2(TZ35;@8Z3~j#ieJy>C>@WtQO@gJc*-D8AWC=L{iju)H1-J(> zUwq#&(SyDdVCi5|h}pVTRMSi_@C&)jy=PfiLk9eLsk@+XUf-Nn(j3xawqFiG&wVhL z1;8A*(@xVgOiT?6*61tCs8|H+&LH|wnBq=E91CEJhzB6jtvsv)hB6OGBvye5cOIgs zrh&V7fyGa>90cX1pd$F8h4{;T`hXy0#KthSEq`(!0h9N&nns~8qh{vPLJOSZT97?B z4)>BqH2_OQt^)yG00~i)RV6h9iVz=7`@=*UD9yDPXS*N;ZW*RATUTrnuWm66Cg;4PaQKUhX6(3cED{H0stV@Glm%G2~QdP zog5O=g3JW`4fI@0ECHpV5zCtv93E;TJgYPP9F_U=^+s*Yu6vR^t&may~ zI9cmk3T4PZ34#$>ytKG381e}T=#uRiNY#+Gl|II$a52GlX7f8Zi=k8WEd2`5&2qHuuF#7ju@ zQ@RlkTJ^dYuOA_FYC60Y$t4dhhzb@3@G`GcQF@_Ta}E#XG*Qt&%C~=_j!WErz$0}V z2_e;*P_k0NWUwn5h}mFG^MnC9y&MDtsY-YsZ%U$|Ma{F70#M3dZGstc6cLW2mL@S& zEitoFB1KYBq)^b1h0P*sv=IHX5DKP52?Y!08JmB$8L&RkCoBo!9}Lc}#@$=54R&>G zn+0bPZ=%JHg_Cjjlszr$-1O@!L(QqUd+@=Qvrh|)Zg{2G(pY#P?tbdC%0*qNM4Im( zwmPdL9RX`88KszDcX=>t=oOfI6y#dr|8#7wdjmNFpJWD`Wuk(21Y|8d zhKpSy-yq-+I90TF`jqqI>mrPojsxU*;z>A%ctJm`xfO7`~hI8xNJK*I%z zfmK8qXwGn`T;Z)N9fxIiQ@qXWm^SFg?LEI5e<_2PgB~njj|r< zfTi*h9AX4*L~NRDSQNxyBcS1q$HOnBk}LJ|kY>g}sSE%^IRxuIEM$Om%Ra z$_}%5JkCCt?&0wb+eheIE=sZgGzOs<%=aTzFb$4CuX03$Kv2hC&@B!E#;b7T;}^vB zEa31&1zCBX^3aS(*wWN@B8xXbMcfULX2F8NZ7IM2?;vTeQqEejM{+Ixy0pWL86xF< zY);dtT17G-eo?Xk4ly-Xf@kX)u@5eL%HdSPYPl1CtOd0s_KIz&q$X65c+WJ_bHT+4 zrQnN}p4DR7?5vh?Z14&Igw&P9W2q~tig$9oESIgcL}7Nc9!@qqHS?8_I=$5bUXY-S z8^t2*VN$ne;kZ^f?2vtq>t?1x8M4mP3QMFDkbxkCa-)|MmGOuN1jR`uNlNr0{hypI z_94z&6hcJ~Qqbp6W<#~y(ZH#T)5NFbaltrtje5BMF@|M~7*+a^tkl6=M`kTZGFt2C zjeLo!rb1-Pm@Vho2!IAt=M6oJm|k#rCWHQRs+d(q){ELa+(Q)kGYFwV{Y&H{4XL6w zIE~x^%NA1e%%Ks&Jc%}r=vH9@R9Uhqq1Q_>S?m+jf+LCA=RWAqQ@ba4^ydacl_Tb# z6$ISg0|8m~wnCwsyT66%6I`OvE3kXA3pt+yfqKA4Bpb4msb%998+ZZ-W}3uXTtThE z(WF;OQ|25QnT!_dKd z>XCd(?ibtj7R|TqP<^8jPH1DkX%U@us_-onBO?0pjxz=Vk&0{*TNn85ml1_q`b~Rf z^Uxhnx*a6%nY=-@0!pI~A+yftHf5Sr+Ta6Z_DM#aKFO&BTgdA;`LfkHk-pL^hz>x9 zR17E2zZBNYrgU%*D$w-y%Hulc#Pd2r)8)r?^4!m7gd&lm5|T9hef;#!=ZTVusikT? zb&_<9)XL3+%{!{8QwbAwU(H_7utqLbUe%;-l!{5lSWmYIr9Bo|1al$?qyR$C2Vz4$ z&PLTFx%4IaHZd#23NpvCj)~Jk8d&7AqnBq;+(Kxg(i1@i4^2FV@WzTs3=$!)WT5_* zM?eGs0?y6G0CJMUOU_am5g90p&LiI|4#091OhJV%;e#rFKQCs-!>I(0N)iLHRBB&p zV9Yx)9-)g8o9NHGnKQPq_j^abRK1Q)jFpyWnp&R0dGQxpqTlR^hSjdlj;3qw?Qys5 z4XsQT73dC^%H{0n- zu?Uw6hp|d2GK=A2$^aF{-1L1)=T;@)=*iE_=$+ExEp#N4;_yk#h!QF0 zUiPZe>HgqvgDayrP(m~28fD$?hbzI(l}PcogMYeDt_q#zaKzTHg)FC^$|R!3 z!cV0s@$}M5PM77r`B^A6Hb$a)D-KJjwXyy!N~AdbkV4{|ssRNYs-faMfDRxQ47nX* zvopRbEsCWv)LDfjDL26`W#tz7M`E(PcDsDC-d>R=^!8%ovw*-D4~E#E`>VsDsM6=I z|B|wLlv^1WpRWQgPXcgsM**D9qa-3Df{PWU?_h77=3aq%bU%*VG}KkW5h=~%i9K1a zR|s5Ij_z^)ld43sKm-WsV?HOP}kvrt7k%umHNhaw!W9m*NuT|Du| znKEFC=dTpGWf8Q2vUx<6N}^ua!7+hFECQ|}6$(G2>kr(Y#ky#X2w%i(?LReDy0?J> z3346EmzE9)`1$=ZY#^Z%u<}#|9AS=T5da{ta0wjTBZq>gJEEdXL&*K$itzA3k%F=r zIxm&94fjR}!pFi$UlH$4C^gEkwHu42^V+FPC&n0ENkEwmCyBC7>3KSrJFnuUgAaTU!*EgU@9vLYPN;8#xJd1C*xxo#SLbr$S{HOv&6^o9^ ztbXrQfMbQQSpmP&vLUcHuPMH*#Fo|Ox|c2kkh+2X@2(&5ll96XBZ023P{<+2s|UV$ zwXmT}UARlQ-cT}p3*pkW2NLd(XV!7LYf4wb6#Uiirrq}1nxEWp?RY=GN4$!-1>OGb zsJ&`N|GSNS;Myk+KODIAD?~4vczykCuE7qY-yd-ew)epNQ=k9x*_|$KHlOu>zW@4= zqR}GjB)YFDk@%0UzR_(`x`Rg-02%)1UMJoA^zKB`9Z$MTNw*Z~DjnTx zq;Yhmj$}mV3!7@`d!zTYZSmhYZMY@;?23yYxUMqtum8`|sqgE+ORXPU8`xU@ Date: Sun, 22 Mar 2026 23:49:25 +0000 Subject: [PATCH 17/73] chore(M001/S02): auto-commit after state-rebuild --- .gsd/doctor-history.jsonl | 1 + .gsd/gsd.db-wal | Bin 1017672 -> 1025912 bytes 2 files changed, 1 insertion(+) diff --git a/.gsd/doctor-history.jsonl b/.gsd/doctor-history.jsonl index 5e4b1d20..0ceac446 100644 --- a/.gsd/doctor-history.jsonl +++ b/.gsd/doctor-history.jsonl @@ -4,3 +4,4 @@ {"ts":"2026-03-22T23:39:26.714Z","ok":false,"errors":2,"warnings":3,"fixes":4,"codes":["state_file_stale","gitignore_missing_patterns","all_tasks_done_missing_slice_summary","all_tasks_done_missing_slice_uat","all_tasks_done_roadmap_not_checked"],"issues":[{"severity":"warning","code":"state_file_stale","message":"STATE.md is stale — shows \"executing\" but derived state is \"summarizing\"","unitId":"project"},{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"},{"severity":"error","code":"all_tasks_done_missing_slice_summary","message":"All tasks are done but S01-SUMMARY.md is missing","unitId":"M001/S01"},{"severity":"warning","code":"all_tasks_done_missing_slice_uat","message":"All tasks are done but S01-UAT.md is missing","unitId":"M001/S01"},{"severity":"error","code":"all_tasks_done_roadmap_not_checked","message":"All tasks are done but roadmap still shows S01 as incomplete","unitId":"M001/S01"}],"fixDescriptions":["rebuilt STATE.md from derived state","added missing GSD runtime patterns to .gitignore","created placeholder /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/milestones/M001/slices/S01/S01-UAT.md","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"2 errors, 3 warnings · 4 fixed · All tasks are done but S01-SUMMARY.md is missing"} {"ts":"2026-03-22T23:42:14.906Z","ok":true,"errors":0,"warnings":2,"fixes":3,"codes":["state_file_stale","gitignore_missing_patterns"],"issues":[{"severity":"warning","code":"state_file_stale","message":"STATE.md is stale — shows \"summarizing\" but derived state is \"planning\"","unitId":"project"},{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"}],"fixDescriptions":["rebuilt STATE.md from derived state","added missing GSD runtime patterns to .gitignore","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"Clean · 3 fixed · STATE.md is stale — shows \"summarizing\" but derived state is \"planning\""} {"ts":"2026-03-22T23:46:00.028Z","ok":true,"errors":0,"warnings":2,"fixes":2,"codes":["gitignore_missing_patterns","missing_slice_plan"],"issues":[{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"},{"severity":"warning","code":"missing_slice_plan","message":"Slice M001/S02 has no plan file","unitId":"M001/S02"}],"fixDescriptions":["added missing GSD runtime patterns to .gitignore","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"Clean · 2 fixed · 5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json"} +{"ts":"2026-03-22T23:49:25.385Z","ok":true,"errors":0,"warnings":2,"fixes":3,"codes":["state_file_stale","gitignore_missing_patterns"],"issues":[{"severity":"warning","code":"state_file_stale","message":"STATE.md is stale — shows \"planning\" but derived state is \"executing\"","unitId":"project"},{"severity":"warning","code":"gitignore_missing_patterns","message":"5 critical GSD runtime pattern(s) missing from .gitignore: .gsd/activity/, .gsd/runtime/, .gsd/auto.lock, .gsd/gsd.db, .gsd/completed-units.json","unitId":"project"}],"fixDescriptions":["rebuilt STATE.md from derived state","added missing GSD runtime patterns to .gitignore","updated /Users/danny/VSCode/workspace/github/vectorflow/.gsd/worktrees/M001/.gsd/STATE.md"],"summary":"Clean · 3 fixed · STATE.md is stale — shows \"planning\" but derived state is \"executing\""} diff --git a/.gsd/gsd.db-wal b/.gsd/gsd.db-wal index 0e1e2db188be0d44d4945a9719a13dc3978f5173..c82ca2a0a77532e9726a4b0506257aba216cd724 100644 GIT binary patch delta 72 zcmX@nXZPcrT|*0F3sVbo3rh=Y3tJ0&3r7oQ3s(zw3r`Dg3*Q!gkM`*fmw5y@G=<;G cf6Lq|zuh36UqFDP=J@)4xg0^iW$yd}0H)a(k^lez delta 39 ucmexy&hA8?T|*0F3sVbo3rh=Y3tJ0&3r7oQ3s(zw3r`Dg3*Q!gk9Ghkr438~ From 8fb113b45c378b75832a1b580a55590f30686077 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Mon, 23 Mar 2026 09:27:10 +0000 Subject: [PATCH 18/73] =?UTF-8?q?feat(S02/T02):=20Extracted=20saveGraph,?= =?UTF-8?q?=20promote,=20discardChanges,=20list=20logic=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/server/services/pipeline-graph.ts - src/server/routers/pipeline.ts --- .gsd/gsd.db-shm | Bin 32768 -> 32768 bytes .gsd/gsd.db-wal | Bin 1025912 -> 247232 bytes .gsd/journal/2026-03-22.jsonl | 4 + .gsd/journal/2026-03-23.jsonl | 4 + .gsd/milestones/M001/slices/S02/S02-PLAN.md | 19 +- .../M001/slices/S02/tasks/T01-SUMMARY.md | 96 + .../M001/slices/S02/tasks/T02-SUMMARY.md | 90 + .../_components/alert-history-section.tsx | 175 ++ .../_components/alert-rules-section.tsx | 569 +++++ .../alerts/_components/constants.ts | 63 + .../notification-channels-section.tsx | 750 +++++++ .../alerts/_components/webhooks-section.tsx | 439 ++++ src/app/(dashboard)/alerts/page.tsx | 1873 +---------------- src/server/routers/pipeline.ts | 521 +---- src/server/services/pipeline-graph.ts | 621 ++++++ 15 files changed, 2858 insertions(+), 2366 deletions(-) create mode 100644 .gsd/journal/2026-03-23.jsonl create mode 100644 .gsd/milestones/M001/slices/S02/tasks/T01-SUMMARY.md create mode 100644 .gsd/milestones/M001/slices/S02/tasks/T02-SUMMARY.md create mode 100644 src/app/(dashboard)/alerts/_components/alert-history-section.tsx create mode 100644 src/app/(dashboard)/alerts/_components/alert-rules-section.tsx create mode 100644 src/app/(dashboard)/alerts/_components/constants.ts create mode 100644 src/app/(dashboard)/alerts/_components/notification-channels-section.tsx create mode 100644 src/app/(dashboard)/alerts/_components/webhooks-section.tsx create mode 100644 src/server/services/pipeline-graph.ts diff --git a/.gsd/gsd.db-shm b/.gsd/gsd.db-shm index b0f4f01ca27bf1f9646bd5ee1a413e374bebbdd6..deac51d76ac885acc69c8ee0bc60ba9ff3b732fa 100644 GIT binary patch delta 551 zcmb7RV>_L% zAQtN40|W^o7O@er(SPSy*gEjd{NK-E;Mlfi+gjV`l!bSEfP@&V;C>0Q3gnI`t2%!`jEZGZ`L>Ne z^PFFIkPDn&c3qO4lOoCPK3bF8=Og5j$~1=Ok*LgOuvu&&wn?@y+Z0=bZCaTg6%_>R LF{4~Qofhc_GtYsJ delta 428 zcmZo@U}|V!s+V}A%K!rGK+MR%AYcfj#esPG%9Hnt{nj4&|LTnAajU?l+^MP|CZQY8 zk*XeO6d33O$^S?IDm<~i)e>ke0|yXu0z zi_Br>7&IAlf!wc5lRt#BFe@?`06E{8*f|-*7?cu)Az2?i|)Ly|!c!jNK612b5q8I&2cfy|#w%rXo* Sn>UtdgWP|Car3o0Ek*!71$~16 diff --git a/.gsd/gsd.db-wal b/.gsd/gsd.db-wal index c82ca2a0a77532e9726a4b0506257aba216cd724..b7309e816f3025442e10c1b9373d666d0aacf1ca 100644 GIT binary patch delta 24336 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 Kr)lOl=>GsSw(Tka diff --git a/.gsd/journal/2026-03-22.jsonl b/.gsd/journal/2026-03-22.jsonl index 104879dd..3fa0a51d 100644 --- a/.gsd/journal/2026-03-22.jsonl +++ b/.gsd/journal/2026-03-22.jsonl @@ -31,3 +31,7 @@ {"ts":"2026-03-22T23:46:00.393Z","flowId":"c61a761e-9985-4245-ae54-25d2fb0ed1d9","seq":2,"eventType":"dispatch-match","rule":"planning → plan-slice","data":{"unitType":"plan-slice","unitId":"M001/S02"}} {"ts":"2026-03-22T23:46:00.399Z","flowId":"c61a761e-9985-4245-ae54-25d2fb0ed1d9","seq":3,"eventType":"unit-start","data":{"unitType":"plan-slice","unitId":"M001/S02"}} {"ts":"2026-03-22T23:49:24.691Z","flowId":"c61a761e-9985-4245-ae54-25d2fb0ed1d9","seq":4,"eventType":"unit-end","data":{"unitType":"plan-slice","unitId":"M001/S02","status":"completed","artifactVerified":true},"causedBy":{"flowId":"c61a761e-9985-4245-ae54-25d2fb0ed1d9","seq":3}} +{"ts":"2026-03-22T23:49:25.485Z","flowId":"c61a761e-9985-4245-ae54-25d2fb0ed1d9","seq":5,"eventType":"iteration-end","data":{"iteration":6}} +{"ts":"2026-03-22T23:49:25.486Z","flowId":"4298477c-cf10-4734-89fe-3f5289694a6b","seq":1,"eventType":"iteration-start","data":{"iteration":7}} +{"ts":"2026-03-22T23:49:25.716Z","flowId":"4298477c-cf10-4734-89fe-3f5289694a6b","seq":2,"eventType":"dispatch-match","rule":"executing → execute-task","data":{"unitType":"execute-task","unitId":"M001/S02/T01"}} +{"ts":"2026-03-22T23:49:25.727Z","flowId":"4298477c-cf10-4734-89fe-3f5289694a6b","seq":3,"eventType":"unit-start","data":{"unitType":"execute-task","unitId":"M001/S02/T01"}} diff --git a/.gsd/journal/2026-03-23.jsonl b/.gsd/journal/2026-03-23.jsonl new file mode 100644 index 00000000..0f0a0502 --- /dev/null +++ b/.gsd/journal/2026-03-23.jsonl @@ -0,0 +1,4 @@ +{"ts":"2026-03-23T09:17:27.098Z","flowId":"8b8a1ff0-d09c-4cab-843e-a151ec459f34","seq":1,"eventType":"iteration-start","data":{"iteration":1}} +{"ts":"2026-03-23T09:17:27.406Z","flowId":"8b8a1ff0-d09c-4cab-843e-a151ec459f34","seq":2,"eventType":"dispatch-match","rule":"executing → execute-task","data":{"unitType":"execute-task","unitId":"M001/S02/T02"}} +{"ts":"2026-03-23T09:17:27.424Z","flowId":"8b8a1ff0-d09c-4cab-843e-a151ec459f34","seq":3,"eventType":"unit-start","data":{"unitType":"execute-task","unitId":"M001/S02/T02"}} +{"ts":"2026-03-23T09:27:10.234Z","flowId":"8b8a1ff0-d09c-4cab-843e-a151ec459f34","seq":4,"eventType":"unit-end","data":{"unitType":"execute-task","unitId":"M001/S02/T02","status":"completed","artifactVerified":true},"causedBy":{"flowId":"8b8a1ff0-d09c-4cab-843e-a151ec459f34","seq":3}} diff --git a/.gsd/milestones/M001/slices/S02/S02-PLAN.md b/.gsd/milestones/M001/slices/S02/S02-PLAN.md index dac7af70..322d4993 100644 --- a/.gsd/milestones/M001/slices/S02/S02-PLAN.md +++ b/.gsd/milestones/M001/slices/S02/S02-PLAN.md @@ -36,7 +36,7 @@ - Verify: `pnpm exec tsc --noEmit` exits 0 && `wc -l src/app/(dashboard)/alerts/page.tsx` under 200 - Done when: Alerts page under 200 lines, 4 section component files exist, `tsc` and `eslint` pass clean -- [ ] **T02: Extract pipeline router business logic to service module** `est:45m` +- [x] **T02: Extract pipeline router business logic to service module** `est:45m` - Why: Pipeline router is 1318 lines with 3 heavy inline handlers. The codebase already delegates to services like `pipeline-version.ts` — extending this pattern to `saveGraph`, `promote`, and `discardChanges` brings the router under ~800 lines and advances R007 (thin routers). - Files: `src/server/routers/pipeline.ts`, `src/server/services/pipeline-graph.ts` - Do: Create `pipeline-graph.ts` following the existing service pattern (direct function exports, import `prisma` from `@/lib/prisma`, throw `TRPCError`). Extract: (1) `saveGraph` validation + transaction logic — accept `tx: Prisma.TransactionClient` parameter, return the saved data, leave `ctx.auditMetadata` assignment in the router; (2) `promote` cross-environment copy logic — accept `userId` as parameter instead of full `ctx`; (3) `discardChanges` version restore logic — accept `tx` parameter. Keep all `.use(withAudit())` and `.use(withTeamAccess())` middleware in the router. Keep Zod schemas in the router file (they're local to the router). @@ -57,6 +57,23 @@ - Verify: `pnpm exec tsc --noEmit` exits 0 && `wc -l src/app/(dashboard)/settings/_components/team-settings.tsx src/app/(dashboard)/settings/_components/users-settings.tsx` both under 800 - Done when: Both settings files under 800 lines, dialog components extracted to sibling files, `tsc` and `eslint` pass clean +## Observability / Diagnostics + +This slice is a pure structural refactor — no new runtime signals are introduced and no existing signals are removed. The key observability invariant is **behavioral equivalence**: every tRPC endpoint and UI page must produce the same inputs/outputs, audit log entries, and error responses before and after refactoring. + +- **Runtime signals preserved:** All `withAudit()` middleware, `ctx.auditMetadata` assignments, `TRPCError` throws, and `console.error` calls remain in their original call paths. Extracted service functions throw `TRPCError` directly (matching the existing pattern in `pipeline-version.ts`). +- **Inspection surface:** `tsc --noEmit` and `eslint src/` are the primary verification surfaces. Any signature mismatch or missing import is caught at compile time. Line-count checks enforce the file-size constraint. +- **Failure visibility:** If a service extraction breaks an endpoint, the tRPC error boundary will surface it as a 500 with the original error message. The audit middleware logs the procedure name + error, making broken endpoints traceable in the audit log. +- **Redaction:** No new secrets or PII handling is introduced. The `encryptNodeConfig`/`decryptNodeConfig` and `stripEnvRefs` call paths are preserved unchanged. + +## Verification + +(continued below with failure-path check) + +- `pnpm exec tsc --noEmit` exits 0 — verifies no type errors after refactoring +- `pnpm exec eslint src/` exits 0 — verifies no lint violations +- `grep -r 'TRPCError' src/server/services/pipeline-graph.ts` — service module uses TRPCError for error paths (failure visibility) + ## Files Likely Touched - `src/app/(dashboard)/alerts/page.tsx` diff --git a/.gsd/milestones/M001/slices/S02/tasks/T01-SUMMARY.md b/.gsd/milestones/M001/slices/S02/tasks/T01-SUMMARY.md new file mode 100644 index 00000000..6189423f --- /dev/null +++ b/.gsd/milestones/M001/slices/S02/tasks/T01-SUMMARY.md @@ -0,0 +1,96 @@ +--- +id: T01 +parent: S02 +milestone: M001 +provides: + - alerts page split into 4 section components + constants module + - thin page.tsx wrapper (45 lines) +key_files: + - src/app/(dashboard)/alerts/page.tsx + - src/app/(dashboard)/alerts/_components/alert-rules-section.tsx + - src/app/(dashboard)/alerts/_components/notification-channels-section.tsx + - src/app/(dashboard)/alerts/_components/webhooks-section.tsx + - src/app/(dashboard)/alerts/_components/alert-history-section.tsx + - src/app/(dashboard)/alerts/_components/constants.ts +key_decisions: + - Form-state types (RuleFormState, ChannelFormState, WebhookFormState) kept co-located with their section components, not in shared constants + - Helper functions (buildConfigFromForm, formFromConfig, parseHeaders) kept in the section that uses them exclusively +patterns_established: + - Alert section components follow pattern: "use client" directive, own imports, own form-state types, exported named function + - Shared constants (METRIC_LABELS, BINARY_METRICS, etc.) live in _components/constants.ts and are imported by sections that need them +observability_surfaces: + - none +duration: 12m +verification_result: passed +completed_at: 2026-03-22T22:17:00Z +blocker_discovered: false +--- + +# T01: Split alerts page into section components + +**Extracted 1910-line alerts page into 4 section components + constants module; page.tsx is now 45 lines** + +## What Happened + +Split `src/app/(dashboard)/alerts/page.tsx` (1910 lines) into 6 files: + +1. **`_components/constants.ts`** (63 lines) — shared constants used across multiple sections: `METRIC_LABELS`, `CONDITION_LABELS`, `BINARY_METRICS`, `GLOBAL_METRICS`, `CHANNEL_TYPE_LABELS`, `CHANNEL_TYPE_ICONS`. + +2. **`_components/alert-rules-section.tsx`** (569 lines) — `AlertRulesSection` with its `RuleFormState` type, `EMPTY_RULE_FORM`, and all rule CRUD/toggle mutations. + +3. **`_components/notification-channels-section.tsx`** (750 lines) — `NotificationChannelsSection` with `ChannelFormState`, `buildConfigFromForm`, `formFromConfig` helpers, and all channel CRUD/test mutations. + +4. **`_components/webhooks-section.tsx`** (439 lines) — `WebhooksSection` with `WebhookFormState`, `parseHeaders` helper, and legacy webhook CRUD/test mutations. + +5. **`_components/alert-history-section.tsx`** (175 lines) — `AlertHistorySection` with cursor-based pagination and event display. + +6. **`page.tsx`** (45 lines) — thin composition wrapper importing all 4 sections with `Separator` elements and environment gate. + +Each section is self-contained with its own `"use client"` directive, imports, form-state types, and tRPC hooks. The initial tsc run revealed `AlertRulesSection` references `CHANNEL_TYPE_LABELS` in its channel badge display — fixed by adding that import from constants. + +## Verification + +All 8 task verification checks pass: +- `tsc --noEmit` exits 0 +- `eslint src/` exits 0 +- `page.tsx` is 45 lines (under 200) +- All 4 section component files and constants.ts exist +- `_components` directory exists + +Slice-level checks relevant to T01 also pass: +- `wc -l src/app/(dashboard)/alerts/page.tsx` → 45 (under 200) +- `test -d src/app/(dashboard)/alerts/_components` → exists + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `pnpm exec tsc --noEmit` | 0 | ✅ pass | 2.8s | +| 2 | `pnpm exec eslint src/` | 0 | ✅ pass | 11.7s | +| 3 | `wc -l src/app/(dashboard)/alerts/page.tsx` | 0 (45 lines) | ✅ pass | <1s | +| 4 | `test -f src/app/(dashboard)/alerts/_components/alert-rules-section.tsx` | 0 | ✅ pass | <1s | +| 5 | `test -f src/app/(dashboard)/alerts/_components/notification-channels-section.tsx` | 0 | ✅ pass | <1s | +| 6 | `test -f src/app/(dashboard)/alerts/_components/webhooks-section.tsx` | 0 | ✅ pass | <1s | +| 7 | `test -f src/app/(dashboard)/alerts/_components/alert-history-section.tsx` | 0 | ✅ pass | <1s | +| 8 | `test -f src/app/(dashboard)/alerts/_components/constants.ts` | 0 | ✅ pass | <1s | + +## Diagnostics + +No runtime observability changes — this is a pure structural refactor. The same tRPC queries, mutations, and toast notifications exist in the extracted components. To verify correct rendering, load the alerts page in a browser and confirm all 4 sections display. + +## Deviations + +- `CHANNEL_TYPE_LABELS` import was missing from `alert-rules-section.tsx` — the channel badge in the rule dialog's channel selector uses this constant. Added the import after the first tsc check caught it. + +## Known Issues + +None. + +## Files Created/Modified + +- `src/app/(dashboard)/alerts/page.tsx` — rewritten as 45-line thin composition wrapper +- `src/app/(dashboard)/alerts/_components/constants.ts` — shared constants (METRIC_LABELS, BINARY_METRICS, etc.) +- `src/app/(dashboard)/alerts/_components/alert-rules-section.tsx` — AlertRulesSection component (569 lines) +- `src/app/(dashboard)/alerts/_components/notification-channels-section.tsx` — NotificationChannelsSection component (750 lines) +- `src/app/(dashboard)/alerts/_components/webhooks-section.tsx` — WebhooksSection component (439 lines) +- `src/app/(dashboard)/alerts/_components/alert-history-section.tsx` — AlertHistorySection component (175 lines) diff --git a/.gsd/milestones/M001/slices/S02/tasks/T02-SUMMARY.md b/.gsd/milestones/M001/slices/S02/tasks/T02-SUMMARY.md new file mode 100644 index 00000000..e0f96079 --- /dev/null +++ b/.gsd/milestones/M001/slices/S02/tasks/T02-SUMMARY.md @@ -0,0 +1,90 @@ +--- +id: T02 +parent: S02 +milestone: M001 +provides: + - pipeline-graph.ts service module with saveGraphComponents, promotePipeline, discardPipelineChanges, detectConfigChanges, listPipelinesForEnvironment + - pipeline router slimmed from 1318 to 847 lines +key_files: + - src/server/services/pipeline-graph.ts + - src/server/routers/pipeline.ts +key_decisions: + - Extracted listPipelinesForEnvironment and detectConfigChanges in addition to the 3 planned handlers to meet the <850 line target; the duplicated YAML-diff logic in list+get was the natural candidate + - Service functions accept plain parameters (userId, pipelineId) not full tRPC ctx; audit metadata assignment stays in router + - discardPipelineChanges owns its own prisma.$transaction since it doesn't need external tx coordination; saveGraphComponents accepts tx parameter since the router wraps it +patterns_established: + - Service extraction pattern: pure function exports, import prisma from @/lib/prisma, throw TRPCError for errors, accept Tx parameter for transaction-scoped work + - detectConfigChanges shared utility avoids YAML-diff duplication between list and get handlers +observability_surfaces: + - none — pure structural refactor, all existing TRPCError throws, audit middleware, and console.error calls preserved in their original call paths +duration: 15m +verification_result: passed +completed_at: 2026-03-23T09:32:00Z +blocker_discovered: false +--- + +# T02: Extract pipeline router business logic to service module + +**Extracted saveGraph, promote, discardChanges, list logic, and YAML-diff utility from 1318-line pipeline router to pipeline-graph.ts service; router is now 847 lines** + +## What Happened + +Created `src/server/services/pipeline-graph.ts` (621 lines) with 5 exported functions: + +1. **`saveGraphComponents(tx, params)`** — shared component validation + node/edge persistence inside an existing transaction. Returns decrypted pipeline. The router keeps the `ctx.auditMetadata` assignment. + +2. **`promotePipeline(params)`** — cross-environment pipeline copy with secret stripping. Owns its own `prisma.$transaction` since it coordinates the name-collision check + create + copyPipelineGraph. + +3. **`discardPipelineChanges(pipelineId)`** — validates preconditions (deployed, has snapshot) and restores the pipeline graph from the latest version snapshot. + +4. **`detectConfigChanges(params)`** — pure function that generates YAML from nodes/edges and compares against a deployed version snapshot to detect undeployed changes. Replaces ~45 lines of duplicated logic that appeared in both `list` and `get` handlers. + +5. **`listPipelinesForEnvironment(environmentId)`** — the full pipeline list query + mapping logic extracted from the `list` handler, including the `hasUndeployedChanges` and `hasStaleComponents` computations. + +The pipeline router went from 1318 → 847 lines. Each extracted handler is now a thin wrapper: parse input, call service, set audit metadata if needed, return result. + +Removed unused imports from the router: `encryptNodeConfig`, `Prisma`, `generateVectorYaml`, `stripEnvRefs`, `StrippedRef`. Kept `copyPipelineGraph` import (still used by `clone` handler) and `decryptNodeConfig` (still used by `get` handler). + +## Verification + +All 6 task-level verification checks pass: +- `pnpm exec tsc --noEmit` exits 0 +- `pnpm exec eslint src/` exits 0 +- Router is 847 lines (under 850) +- `pipeline-graph.ts` service file exists +- `auditMetadata` appears in router code +- `auditMetadata` does NOT appear in service code + +Slice-level checks relevant to T02: +- `wc -l src/server/routers/pipeline.ts` → 847 (under 850) ✅ +- `test -f src/server/services/pipeline-graph.ts` → exists ✅ +- `grep -r 'TRPCError' src/server/services/pipeline-graph.ts` → 15 occurrences (failure visibility) ✅ + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `pnpm exec tsc --noEmit` | 0 | ✅ pass | 4.9s | +| 2 | `pnpm exec eslint src/` | 0 | ✅ pass | 22.3s | +| 3 | `wc -l src/server/routers/pipeline.ts` | 0 (847 lines) | ✅ pass | <1s | +| 4 | `test -f src/server/services/pipeline-graph.ts` | 0 | ✅ pass | <1s | +| 5 | `grep -q 'auditMetadata' src/server/routers/pipeline.ts` | 0 | ✅ pass | <1s | +| 6 | `! grep -q 'auditMetadata' src/server/services/pipeline-graph.ts` | 0 | ✅ pass | <1s | + +## Diagnostics + +No runtime observability changes — this is a pure structural refactor. All TRPCError throws, audit middleware chains, and console.error calls are preserved in their original call paths. The service module uses `TRPCError` directly for error paths (15 throw sites), matching the existing `pipeline-version.ts` pattern. + +## Deviations + +- Extracted `listPipelinesForEnvironment` and `detectConfigChanges` in addition to the 3 planned handlers. The plan estimated the router would reach ~800 lines after extracting the 3 handlers, but the actual reduction was only 1318→1024 (294 lines removed). The duplicated YAML-diff logic in `list` and `get` handlers was a natural extraction target that both eliminated code duplication and brought the router under the 850-line verification bar. +- Fixed `Prisma.InputJsonValue` type cast in service for `encryptNodeConfig` return — the original router code used `as unknown as typeof node.config` which resolved to `Record`, not a Prisma-compatible type. + +## Known Issues + +None. + +## Files Created/Modified + +- `src/server/services/pipeline-graph.ts` — new service module with 5 exported functions (621 lines) +- `src/server/routers/pipeline.ts` — slimmed router delegating to service (847 lines, down from 1318) diff --git a/src/app/(dashboard)/alerts/_components/alert-history-section.tsx b/src/app/(dashboard)/alerts/_components/alert-history-section.tsx new file mode 100644 index 00000000..da640e54 --- /dev/null +++ b/src/app/(dashboard)/alerts/_components/alert-history-section.tsx @@ -0,0 +1,175 @@ +"use client"; + +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { useTRPC } from "@/trpc/client"; +import { + Loader2, + History, +} from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { StatusBadge } from "@/components/ui/status-badge"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +// ─── Alert History Section ────────────────────────────────────────────────────── + +export function AlertHistorySection({ environmentId }: { environmentId: string }) { + const trpc = useTRPC(); + const [cursor, setCursor] = useState(undefined); + const [allItems, setAllItems] = useState< + Array<{ + id: string; + status: string; + value: number; + message: string | null; + firedAt: Date; + resolvedAt: Date | null; + node: { id: string; host: string } | null; + alertRule: { + id: string; + name: string; + metric: string; + condition: string | null; + threshold: number | null; + pipeline: { id: string; name: string } | null; + }; + }> + >([]); + + const eventsQuery = useQuery( + trpc.alert.listEvents.queryOptions( + { environmentId, limit: 50, cursor }, + { enabled: !!environmentId }, + ), + ); + + // Merge newly fetched items when data changes + const items = eventsQuery.data?.items ?? []; + const nextCursor = eventsQuery.data?.nextCursor; + + // Build display list: first page directly from query, subsequent pages accumulated + const displayItems = cursor ? allItems : items; + + const loadMore = () => { + if (nextCursor) { + setAllItems((prev) => { + // Combine previous items with current items, dedup by id + const existing = new Set(prev.map((i) => i.id)); + const newItems = items.filter((i) => !existing.has(i.id)); + return [...prev, ...newItems]; + }); + setCursor(nextCursor); + } + }; + + const formatTimestamp = (date: Date | string) => { + const d = typeof date === "string" ? new Date(date) : date; + return d.toLocaleString(); + }; + + const isLoading = eventsQuery.isLoading; + const isFetchingMore = eventsQuery.isFetching && !!cursor; + + return ( +
+
+ +

Alert History

+
+ + {isLoading && !cursor ? ( +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ ) : displayItems.length === 0 && items.length === 0 ? ( +
+

No alert events yet

+

+ Alert events will appear here when rules are triggered. +

+
+ ) : ( + <> + + + + Timestamp + Rule Name + Node + Pipeline + Status + Value + Message + + + + {(cursor ? displayItems : items).map((event) => ( + + + {formatTimestamp(event.firedAt)} + + + {event.alertRule.name} + + + {event.node?.host ?? "-"} + + + {event.alertRule.pipeline?.name ?? "-"} + + + + {event.status === "firing" ? "Firing" : "Resolved"} + + + + {typeof event.value === "number" + ? event.value.toFixed(2) + : event.value} + + + {event.message || "-"} + + + ))} + +
+ + {nextCursor && ( +
+ +
+ )} + + )} +
+ ); +} diff --git a/src/app/(dashboard)/alerts/_components/alert-rules-section.tsx b/src/app/(dashboard)/alerts/_components/alert-rules-section.tsx new file mode 100644 index 00000000..d2d9f535 --- /dev/null +++ b/src/app/(dashboard)/alerts/_components/alert-rules-section.tsx @@ -0,0 +1,569 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { + useQuery, + useMutation, + useQueryClient, +} from "@tanstack/react-query"; +import { useTRPC } from "@/trpc/client"; +import { useTeamStore } from "@/stores/team-store"; +import { toast } from "sonner"; +import { + Plus, + Pencil, + Trash2, + Loader2, +} from "lucide-react"; +import { AlertMetric, AlertCondition } from "@/generated/prisma"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { Switch } from "@/components/ui/switch"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { ConfirmDialog } from "@/components/confirm-dialog"; +import { isEventMetric } from "@/lib/alert-metrics"; + +import { + METRIC_LABELS, + CONDITION_LABELS, + BINARY_METRICS, + GLOBAL_METRICS, + CHANNEL_TYPE_LABELS, +} from "./constants"; + +// ─── Alert Rules Section ──────────────────────────────────────────────────────── + +interface RuleFormState { + name: string; + pipelineId: string; + metric: string; + condition: string; + threshold: string; + durationSeconds: string; + channelIds: string[]; +} + +const EMPTY_RULE_FORM: RuleFormState = { + name: "", + pipelineId: "", + metric: "", + condition: "", + threshold: "", + durationSeconds: "60", + channelIds: [], +}; + +export function AlertRulesSection({ environmentId }: { environmentId: string }) { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const selectedTeamId = useTeamStore((s) => s.selectedTeamId); + + const [dialogOpen, setDialogOpen] = useState(false); + const [editingRuleId, setEditingRuleId] = useState(null); + const [form, setForm] = useState(EMPTY_RULE_FORM); + const [deleteTarget, setDeleteTarget] = useState<{ + id: string; + name: string; + } | null>(null); + + const rulesQuery = useQuery( + trpc.alert.listRules.queryOptions( + { environmentId }, + { enabled: !!environmentId }, + ), + ); + + const pipelinesQuery = useQuery( + trpc.pipeline.list.queryOptions( + { environmentId }, + { enabled: !!environmentId }, + ), + ); + + const channelsQuery = useQuery( + trpc.alert.listChannels.queryOptions( + { environmentId }, + { enabled: !!environmentId }, + ), + ); + + const invalidateRules = useCallback(() => { + queryClient.invalidateQueries({ + queryKey: trpc.alert.listRules.queryKey({ environmentId }), + }); + }, [queryClient, trpc, environmentId]); + + const createMutation = useMutation( + trpc.alert.createRule.mutationOptions({ + onSuccess: () => { + toast.success("Alert rule created"); + invalidateRules(); + setDialogOpen(false); + }, + onError: (error) => { + toast.error(error.message || "Failed to create alert rule"); + }, + }), + ); + + const updateMutation = useMutation( + trpc.alert.updateRule.mutationOptions({ + onSuccess: () => { + toast.success("Alert rule updated"); + invalidateRules(); + setDialogOpen(false); + setEditingRuleId(null); + }, + onError: (error) => { + toast.error(error.message || "Failed to update alert rule"); + }, + }), + ); + + const toggleMutation = useMutation( + trpc.alert.updateRule.mutationOptions({ + onSuccess: () => { + invalidateRules(); + }, + onError: (error) => { + toast.error(error.message || "Failed to toggle alert rule"); + }, + }), + ); + + const deleteMutation = useMutation( + trpc.alert.deleteRule.mutationOptions({ + onSuccess: () => { + toast.success("Alert rule deleted"); + invalidateRules(); + setDeleteTarget(null); + }, + onError: (error) => { + toast.error(error.message || "Failed to delete alert rule"); + }, + }), + ); + + const rules = rulesQuery.data ?? []; + const pipelines = pipelinesQuery.data ?? []; + const channels = channelsQuery.data ?? []; + + const openCreate = () => { + setEditingRuleId(null); + setForm(EMPTY_RULE_FORM); + setDialogOpen(true); + }; + + const openEdit = (rule: (typeof rules)[0]) => { + setEditingRuleId(rule.id); + const skipThreshold = isEventMetric(rule.metric) || BINARY_METRICS.has(rule.metric); + setForm({ + name: rule.name, + pipelineId: rule.pipelineId ?? "", + metric: rule.metric, + condition: skipThreshold ? "" : (rule.condition ?? "gt"), + threshold: skipThreshold ? "" : String(rule.threshold ?? ""), + durationSeconds: skipThreshold ? "" : String(rule.durationSeconds ?? ""), + channelIds: rule.channels?.map((c) => c.channelId) ?? [], + }); + setDialogOpen(true); + }; + + const toggleChannel = (channelId: string) => { + setForm((f) => ({ + ...f, + channelIds: f.channelIds.includes(channelId) + ? f.channelIds.filter((id) => id !== channelId) + : [...f.channelIds, channelId], + })); + }; + + const handleSubmit = () => { + const isBinary = BINARY_METRICS.has(form.metric); + const isEvent = isEventMetric(form.metric); + if (!form.name || !form.metric || (!isBinary && !isEvent && !form.threshold)) { + toast.error("Please fill in all required fields"); + return; + } + + const skipThreshold = isEvent || isBinary; + + if (editingRuleId) { + updateMutation.mutate({ + id: editingRuleId, + name: form.name, + ...(skipThreshold + ? {} + : { + threshold: parseFloat(form.threshold), + durationSeconds: parseInt(form.durationSeconds, 10) || 60, + }), + channelIds: form.channelIds, + }); + } else { + createMutation.mutate({ + name: form.name, + environmentId, + pipelineId: form.pipelineId || undefined, + metric: form.metric as AlertMetric, + condition: skipThreshold ? null : (form.condition as AlertCondition), + threshold: skipThreshold ? null : parseFloat(form.threshold), + durationSeconds: skipThreshold ? null : (parseInt(form.durationSeconds, 10) || 60), + teamId: selectedTeamId!, + channelIds: form.channelIds.length > 0 ? form.channelIds : undefined, + }); + } + }; + + const isSaving = createMutation.isPending || updateMutation.isPending; + + return ( +
+
+ +
+ + {rulesQuery.isLoading ? ( +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ ) : rules.length === 0 ? ( +
+

No alert rules configured

+

+ Create an alert rule to monitor metrics and receive notifications. +

+
+ ) : ( + + + + Name + Metric + Condition + Threshold + Duration + Pipeline + Enabled + Actions + + + + {rules.map((rule) => ( + + {rule.name} + + + {METRIC_LABELS[rule.metric] ?? rule.metric} + + + + {BINARY_METRICS.has(rule.metric) || !rule.condition ? "—" : (CONDITION_LABELS[rule.condition] ?? rule.condition)} + + + {BINARY_METRICS.has(rule.metric) ? "—" : (rule.threshold ?? "—")} + + + {BINARY_METRICS.has(rule.metric) || rule.durationSeconds == null ? "—" : `${rule.durationSeconds}s`} + + + {GLOBAL_METRICS.has(rule.metric) ? ( + + ) : rule.pipeline ? ( + {rule.pipeline.name} + ) : ( + All + )} + + + + toggleMutation.mutate({ id: rule.id, enabled: checked }) + } + /> + + +
+ + +
+
+
+ ))} +
+
+ )} + + {/* Create / Edit Dialog */} + { + setDialogOpen(open); + if (!open) setEditingRuleId(null); + }} + > + + + + {editingRuleId ? "Edit Alert Rule" : "Create Alert Rule"} + + + {editingRuleId + ? "Update the alert rule configuration." + : "Define a new alert rule for this environment."} + + + +
+
+ + + setForm((f) => ({ ...f, name: e.target.value })) + } + /> +
+ + {!editingRuleId && ( + <> +
+ + +
+ + {!GLOBAL_METRICS.has(form.metric) && ( +
+ + +
+ )} + + )} + + {isEventMetric(form.metric) || BINARY_METRICS.has(form.metric) ? ( +

+ Notifications will be sent when this event occurs. +

+ ) : ( + <> +
+ + + setForm((f) => ({ ...f, threshold: e.target.value })) + } + /> +
+ +
+ + + setForm((f) => ({ ...f, durationSeconds: e.target.value })) + } + /> +
+ + )} + + {channels.length > 0 && ( +
+ +

+ Select channels for this rule. If none are selected, all + enabled channels will be used. +

+
+ {channels.map((ch) => { + const selected = form.channelIds.includes(ch.id); + return ( + toggleChannel(ch.id)} + > + {CHANNEL_TYPE_LABELS[ch.type] ?? ch.type}: {ch.name} + + ); + })} +
+
+ )} +
+ + + + + +
+
+ + {/* Delete Confirmation */} + { + if (!open) setDeleteTarget(null); + }} + title="Delete Alert Rule" + description={ + <> + Are you sure you want to delete{" "} + {deleteTarget?.name}? This action cannot be undone. + + } + confirmLabel="Delete" + isPending={deleteMutation.isPending} + pendingLabel="Deleting..." + onConfirm={() => { + if (deleteTarget) deleteMutation.mutate({ id: deleteTarget.id }); + }} + /> +
+ ); +} diff --git a/src/app/(dashboard)/alerts/_components/constants.ts b/src/app/(dashboard)/alerts/_components/constants.ts new file mode 100644 index 00000000..a99be549 --- /dev/null +++ b/src/app/(dashboard)/alerts/_components/constants.ts @@ -0,0 +1,63 @@ +import { + MessageSquare, + Mail, + AlertTriangle, + Webhook, +} from "lucide-react"; + +// ─── Constants shared across alert sections ───────────────────────────────────── + +export const METRIC_LABELS: Record = { + // Infrastructure (threshold-based) + node_unreachable: "Node Unreachable", + cpu_usage: "CPU Usage", + memory_usage: "Memory Usage", + disk_usage: "Disk Usage", + error_rate: "Error Rate", + discarded_rate: "Discarded Rate", + pipeline_crashed: "Pipeline Crashed", + // Events (fire on occurrence) + deploy_requested: "Deploy Requested", + deploy_completed: "Deploy Completed", + deploy_rejected: "Deploy Rejected", + deploy_cancelled: "Deploy Cancelled", + new_version_available: "New Version Available", + scim_sync_failed: "SCIM Sync Failed", + backup_failed: "Backup Failed", + certificate_expiring: "Certificate Expiring", + node_joined: "Node Joined", + node_left: "Node Left", +}; + +export const CONDITION_LABELS: Record = { + gt: ">", + lt: "<", + eq: "=", +}; + +export const BINARY_METRICS = new Set(["node_unreachable", "pipeline_crashed"]); + +/** Metrics that cannot be scoped to a specific pipeline. */ +export const GLOBAL_METRICS = new Set([ + "node_unreachable", + "new_version_available", + "scim_sync_failed", + "backup_failed", + "certificate_expiring", + "node_joined", + "node_left", +]); + +export const CHANNEL_TYPE_LABELS: Record = { + slack: "Slack", + email: "Email", + pagerduty: "PagerDuty", + webhook: "Webhook", +}; + +export const CHANNEL_TYPE_ICONS: Record = { + slack: MessageSquare, + email: Mail, + pagerduty: AlertTriangle, + webhook: Webhook, +}; diff --git a/src/app/(dashboard)/alerts/_components/notification-channels-section.tsx b/src/app/(dashboard)/alerts/_components/notification-channels-section.tsx new file mode 100644 index 00000000..f6420eee --- /dev/null +++ b/src/app/(dashboard)/alerts/_components/notification-channels-section.tsx @@ -0,0 +1,750 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { + useQuery, + useMutation, + useQueryClient, +} from "@tanstack/react-query"; +import { useTRPC } from "@/trpc/client"; +import { toast } from "sonner"; +import { + Plus, + Pencil, + Trash2, + Loader2, + Send, + Webhook, + BellRing, +} from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { Switch } from "@/components/ui/switch"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Textarea } from "@/components/ui/textarea"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { ConfirmDialog } from "@/components/confirm-dialog"; + +import { + CHANNEL_TYPE_LABELS, + CHANNEL_TYPE_ICONS, +} from "./constants"; + +// ─── Notification Channels Section ─────────────────────────────────────────────── + +type ChannelType = "slack" | "email" | "pagerduty" | "webhook"; + +interface ChannelFormState { + name: string; + type: ChannelType; + // Slack + webhookUrl: string; + // Email + smtpHost: string; + smtpPort: string; + smtpUser: string; + smtpPass: string; + emailFrom: string; + recipients: string; + // PagerDuty + integrationKey: string; + // Webhook + url: string; + headers: string; + hmacSecret: string; +} + +const EMPTY_CHANNEL_FORM: ChannelFormState = { + name: "", + type: "slack", + webhookUrl: "", + smtpHost: "", + smtpPort: "587", + smtpUser: "", + smtpPass: "", + emailFrom: "", + recipients: "", + integrationKey: "", + url: "", + headers: "", + hmacSecret: "", +}; + +function buildConfigFromForm(form: ChannelFormState): Record { + switch (form.type) { + case "slack": + return { webhookUrl: form.webhookUrl }; + case "email": + return { + smtpHost: form.smtpHost, + smtpPort: parseInt(form.smtpPort, 10) || 587, + smtpUser: form.smtpUser || undefined, + smtpPass: form.smtpPass || undefined, + from: form.emailFrom, + recipients: form.recipients + .split(",") + .map((e) => e.trim()) + .filter(Boolean), + }; + case "pagerduty": + return { integrationKey: form.integrationKey }; + case "webhook": { + const config: Record = { url: form.url }; + if (form.headers.trim()) { + try { + config.headers = JSON.parse(form.headers); + } catch { + // Will be caught by validation + } + } + if (form.hmacSecret) config.hmacSecret = form.hmacSecret; + return config; + } + } +} + +function formFromConfig( + type: string, + name: string, + config: Record, +): ChannelFormState { + const base = { ...EMPTY_CHANNEL_FORM, name, type: type as ChannelType }; + + switch (type) { + case "slack": + return { ...base, webhookUrl: (config.webhookUrl as string) ?? "" }; + case "email": + return { + ...base, + smtpHost: (config.smtpHost as string) ?? "", + smtpPort: String(config.smtpPort ?? 587), + smtpUser: (config.smtpUser as string) ?? "", + smtpPass: "", + emailFrom: (config.from as string) ?? "", + recipients: Array.isArray(config.recipients) + ? (config.recipients as string[]).join(", ") + : "", + }; + case "pagerduty": + return { ...base, integrationKey: "" }; + case "webhook": + return { + ...base, + url: (config.url as string) ?? "", + headers: config.headers + ? JSON.stringify(config.headers, null, 2) + : "", + hmacSecret: "", + }; + default: + return base; + } +} + +export function NotificationChannelsSection({ + environmentId, +}: { + environmentId: string; +}) { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + + const [dialogOpen, setDialogOpen] = useState(false); + const [editingChannelId, setEditingChannelId] = useState(null); + const [form, setForm] = useState(EMPTY_CHANNEL_FORM); + const [deleteTarget, setDeleteTarget] = useState<{ + id: string; + name: string; + } | null>(null); + + const channelsQuery = useQuery( + trpc.alert.listChannels.queryOptions( + { environmentId }, + { enabled: !!environmentId }, + ), + ); + + const invalidateChannels = useCallback(() => { + queryClient.invalidateQueries({ + queryKey: trpc.alert.listChannels.queryKey({ environmentId }), + }); + }, [queryClient, trpc, environmentId]); + + const createMutation = useMutation( + trpc.alert.createChannel.mutationOptions({ + onSuccess: () => { + toast.success("Notification channel created"); + invalidateChannels(); + setDialogOpen(false); + }, + onError: (error) => { + toast.error(error.message || "Failed to create channel"); + }, + }), + ); + + const updateMutation = useMutation( + trpc.alert.updateChannel.mutationOptions({ + onSuccess: () => { + toast.success("Notification channel updated"); + invalidateChannels(); + setDialogOpen(false); + setEditingChannelId(null); + }, + onError: (error) => { + toast.error(error.message || "Failed to update channel"); + }, + }), + ); + + const toggleMutation = useMutation( + trpc.alert.updateChannel.mutationOptions({ + onSuccess: () => { + invalidateChannels(); + }, + onError: (error) => { + toast.error(error.message || "Failed to toggle channel"); + }, + }), + ); + + const deleteMutation = useMutation( + trpc.alert.deleteChannel.mutationOptions({ + onSuccess: () => { + toast.success("Notification channel deleted"); + invalidateChannels(); + setDeleteTarget(null); + }, + onError: (error) => { + toast.error(error.message || "Failed to delete channel"); + }, + }), + ); + + const testMutation = useMutation( + trpc.alert.testChannel.mutationOptions({ + onSuccess: (result) => { + if (result.success) { + toast.success("Channel test successful"); + } else { + toast.error(`Channel test failed: ${result.error ?? "Unknown error"}`); + } + }, + onError: (error) => { + toast.error(error.message || "Failed to test channel"); + }, + }), + ); + + const channels = channelsQuery.data ?? []; + + const openCreate = () => { + setEditingChannelId(null); + setForm(EMPTY_CHANNEL_FORM); + setDialogOpen(true); + }; + + const openEdit = (channel: (typeof channels)[0]) => { + setEditingChannelId(channel.id); + setForm( + formFromConfig( + channel.type, + channel.name, + channel.config as Record, + ), + ); + setDialogOpen(true); + }; + + const validateForm = (): boolean => { + if (!form.name.trim()) { + toast.error("Name is required"); + return false; + } + + switch (form.type) { + case "slack": + if (!form.webhookUrl.trim()) { + toast.error("Webhook URL is required"); + return false; + } + break; + case "email": + if (!form.smtpHost.trim() || !form.emailFrom.trim() || !form.recipients.trim()) { + toast.error("SMTP host, from address, and recipients are required"); + return false; + } + break; + case "pagerduty": + if (!editingChannelId && !form.integrationKey.trim()) { + toast.error("Integration key is required"); + return false; + } + break; + case "webhook": + if (!form.url.trim()) { + toast.error("URL is required"); + return false; + } + if (form.headers.trim()) { + try { + const parsed = JSON.parse(form.headers); + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + toast.error("Headers must be a JSON object"); + return false; + } + } catch { + toast.error("Invalid JSON in headers field"); + return false; + } + } + break; + } + + return true; + }; + + const handleSubmit = () => { + if (!validateForm()) return; + + const config = buildConfigFromForm(form); + + if (editingChannelId) { + updateMutation.mutate({ + id: editingChannelId, + name: form.name, + config, + }); + } else { + createMutation.mutate({ + environmentId, + name: form.name, + type: form.type, + config, + }); + } + }; + + const isSaving = createMutation.isPending || updateMutation.isPending; + + return ( +
+
+
+ +

Notification Channels

+
+ +
+ + {channelsQuery.isLoading ? ( +
+ {Array.from({ length: 2 }).map((_, i) => ( + + ))} +
+ ) : channels.length === 0 ? ( +
+

No notification channels configured

+

+ Add a notification channel to receive alerts via Slack, Email, + PagerDuty, or Webhook. +

+
+ ) : ( + + + + Name + Type + Enabled + Actions + + + + {channels.map((channel) => { + const Icon = + CHANNEL_TYPE_ICONS[channel.type] ?? Webhook; + return ( + + {channel.name} + + + + {CHANNEL_TYPE_LABELS[channel.type] ?? channel.type} + + + + + toggleMutation.mutate({ + id: channel.id, + enabled: checked, + }) + } + /> + + +
+ + + +
+
+
+ ); + })} +
+
+ )} + + {/* Create / Edit Dialog */} + { + setDialogOpen(open); + if (!open) setEditingChannelId(null); + }} + > + + + + {editingChannelId + ? "Edit Notification Channel" + : "Add Notification Channel"} + + + {editingChannelId + ? "Update the channel configuration." + : "Configure a new notification channel for alert delivery."} + + + +
+
+ + + setForm((f) => ({ ...f, name: e.target.value })) + } + /> +
+ + {!editingChannelId && ( +
+ + +
+ )} + + {/* Type-specific config forms */} + {form.type === "slack" && ( +
+ + + setForm((f) => ({ ...f, webhookUrl: e.target.value })) + } + /> +

+ Create an Incoming Webhook in your Slack workspace settings. +

+
+ )} + + {form.type === "email" && ( + <> +
+
+ + + setForm((f) => ({ ...f, smtpHost: e.target.value })) + } + /> +
+
+ + + setForm((f) => ({ ...f, smtpPort: e.target.value })) + } + /> +
+
+
+
+ + + setForm((f) => ({ ...f, smtpUser: e.target.value })) + } + /> +
+
+ + + setForm((f) => ({ ...f, smtpPass: e.target.value })) + } + /> +
+
+
+ + + setForm((f) => ({ ...f, emailFrom: e.target.value })) + } + /> +
+
+ + + setForm((f) => ({ ...f, recipients: e.target.value })) + } + /> +

+ Comma-separated list of email addresses. +

+
+ + )} + + {form.type === "pagerduty" && ( +
+ + + setForm((f) => ({ + ...f, + integrationKey: e.target.value, + })) + } + /> +

+ {editingChannelId + ? "Leave blank to keep the existing key, or enter a new one to replace it." + : "Found in PagerDuty under Service > Integrations > Events API v2."} +

+
+ )} + + {form.type === "webhook" && ( + <> +
+ + + setForm((f) => ({ ...f, url: e.target.value })) + } + /> +
+
+ +