- scripts/verify-pax8-auth.ts loads .env.local, calls getPax8Client().listCompanies(0,1), logs only counts/status (never token/secret) - CLAUDE.md External integrations table gains a PAX8 row (PAX8_* prefix) - INTEGRATIONS.md does not exist at repo root; edit skipped per plan instructions (noted in SUMMARY)
30 KiB
Pulse — Repo Guide for Claude
Pulse is an internal PSA management dashboard for Wulf Consulting. It syncs Autotask data into Postgres and adds dashboards, workflows, and analytics around it. Single Next.js 16 app — not a monorepo.
README.md covers the human-facing overview. Trust this file for the details
that matter to coding decisions. For deeper context:
ARCHITECTURE.md— runtime topology, data flow, workers, analyzer pipeline, invariants. Read before touching workers, sync, or the analyzer.DESIGN.md— design tokens, navigation IA, component vocabulary, layout rules, and the working backlog for nav/visual cleanup. Read before touching pages or shared UI components.
Stack
- Next.js 16 + React 19 (App Router,
reactCompiler: true,output: 'standalone') - TypeScript strict, path alias
@/* - Postgres 16 via
pg(no ORM), Redis (caching), Better Auth 1.4 - Tailwind 4, shadcn/ui (
components/ui/), recharts, sonner, lucide - Forms: react-hook-form + Zod resolver — admin/auth forms only
- Runs on port 3100 (Docker exposes 3100;
BETTER_AUTH_URL=http://localhost:3100)
Layout
app/— App Router pages +app/api/**/route.tshandlerslib/services/— integration clients, sync services, scheduler (~50 files)lib/types/<domain>.ts— shared types (autotask, sync, veeam, workflow, …)lib/auth.ts,lib/auth-utils.ts,lib/permissions.ts— auth wiringcomponents/ui/— shadcn primitives; sibling dirs are feature componentsmigrations/NNN_*.sql— numbered SQL, applied in alphabetical order on Postgres init. UseIF NOT EXISTS+ON CONFLICT DO NOTHINGfor seed data.docs/— long-form integration/sync guides; reference these, don't duplicate.scripts/— one-off ops scripts, not tests.
Database
- All columns are
snake_case. API responses arecamelCase— handlers transform manually (no ORM). - Use the
postgresClientsingleton fromlib/services/postgres-client.ts:postgresClient.query(sql, params),.transaction(),.upsert(),.bulkUpsert(). - Audit columns convention:
created_at,updated_at,synced_at,is_deleted,deleted_at. - Adding a migration: next number,
IF NOT EXISTS, no destructive ops on existing data without a guard. Postgres init applies them on first boot only — for an existing DB, run viascripts/apply-migrations(check first; behavior varies).
API routes
- Pattern:
app/api/<resource>/route.tsexportingGET/POST/etc. - No Zod validation in route handlers today. Validate inputs explicitly when it matters; don't add a framework just to validate one field.
- Errors:
try/catch, returnNextResponse.json({ error, message }, { status }). Convention: 503 for missing/bad config, 401/403 from auth helpers, 500 for runtime. - Auth in API routes: import from
lib/auth-utils.ts:const { session, error } = await requireAuth(); // or requireAdmin() / requireSuperAdmin() / requirePermission(resource, action) if (error) return error;middleware.tsonly verifies a session cookie exists — role checks happen here. - No
'use server'actions in this codebase. Everything is API routes called from client components viafetch.
Frontend
- Most pages are
'use client'withuseState/useEffect/fetch('/api/...'). No SWR/react-query — don't introduce one for one-off fetches; match the surrounding code. - Server components are fine for static shells; data calls live on the client today.
- Toasts:
sonner. Tables:@tanstack/react-tableviacomponents/admin/DataTable.tsx. Modals:components/admin/DetailModal.tsx. Navigation:components/navigation/app-navigation.tsx.
External integrations
All clients live in lib/services/ with a factory + is<Name>Configured() helper.
Examples: getAutotaskClient(), getMsgraphClient(), getDattoRmmClient(),
getVeeamClient(). Credentials always come from env vars; clients throw if missing.
| Service | Env prefix |
|---|---|
| Autotask | AUTOTASK_* (incl. AUTOTASK_WEBHOOK_SECRET) |
| MS Graph (app) | MSGRAPH_* (specific tenant, not common) |
| Microsoft OAuth (login) | MICROSOFT_* |
| Datto RMM | DATTO_RMM_* |
| Veeam VSPC | VEEAM_VSPC_* |
| Auvik / Addigy / IT Glue / Mimecast / S1 / Duo / Zoom / QBO / Zabbix / Salesbldr | <NAME>_* |
| PAX8 | PAX8_* (OAuth2 client-credentials, read-only partner/reseller API) |
| Anthropic | ANTHROPIC_API_KEY (analyzer pipeline + ai-triage-service.ts) |
| OpenRouter | OPENROUTER_API_KEY (alternate analyzer provider, opt-in per request) |
| Backblaze B2 | B2_* (LogLift evidence storage) |
| Postgres / Redis | POSTGRES_* or DATABASE_URL, REDIS_URL |
Sync & scheduling
lib/services/entity-sync.ts— per-entity Autotask → Postgres sync (incremental vialastTrackedModificationDateTimewhen supported, else full upsert).lib/services/sync-scheduler.ts— node-cron singleton. Self-initializes on first server-side import (side effect at the bottom of the file). Schedules live in DB, admin-editable at/admin.- Webhooks (
/api/webhooks/...,/api/zabbix/webhook,/api/rmm/loglift) are public permiddleware.ts; they verify HMAC or a shared header themselves. - Analyzer worker (
lib/services/analyzer/worker.ts) and RMM Overshell worker (lib/services/rmm/worker.ts) auto-start on import in production. Same side-effect-import caveat as the sync scheduler.
Auth
- Better Auth with magic link + TOTP 2FA + Microsoft OAuth. Roles:
user,admin,super-admin. Tables created in migration012. - Default admin bootstrapped from
DEFAULT_ADMIN_EMAILvialib/bootstrap.ts. middleware.tsredirects unauth'd page requests to/auth/sign-in. Public routes (webhooks, sync, health, mobile, openclaw, kiosk, legal, qbo callbacks) are hardcoded there — add to that list when introducing a new public endpoint.
Build / run / verify
- Dev:
npm run dev→ http://localhost:3100 - Build:
npm run build(turbopack via Next 16) - Type check:
npx tsc --noEmit --pretty - Tests:
npm test(vitest) — coverslib/services/analyzer/**,lib/services/rmm/**,lib/services/b2/**, andlib/services/analyzer/ link-discovery.test.ts. Other parts of the codebase have no tests — if you touch them, type-check is the only safety net. No CI yet; tests are local-only. - Docker:
docker compose upfrom repo root. Postgres appliesmigrations/*.sqlon init only (existing volumes won't re-run them).
Conventions to follow
- Files: kebab-case. Components:
PascalCaseexports from kebab-case files. - Don't introduce ORMs, server actions, or alternative state libraries unless asked — match the existing pattern.
- New SQL: numbered migration; never edit a committed one.
- Long-form per-feature documentation belongs in
docs/. Don't duplicate it here.
Operator config
- Integration disable — two sources, merged:
INTEGRATIONS_DISABLEDenv var (legacy / bootstrap fallback). Comma- or space-separated keys with aliases (sentinelone→s1,datto→datto_rmm,it-glue→itglue,ms-graph→msgraph). Set in.envand restart./admin/integrationsUI backed by theintegration_settingstable (migration 081). Toggle without a container restart; takes effect within the 5-minute health cache (PATCH clears the cache immediately). Audit columns capturedisabled_by(session email),disabled_at, and an optionaldisabled_reason. In both cases live auth checks still run (logs surface the underlying state); the UI ignores the result for disabled integrations.
Watch out for
- A
.envfile is committed to the repo. Treat secrets as potentially real; don't log/echo them, and flag this if it comes up. - Duplicate migration numbers exist (002, 004, 009) — alphabetical apply order.
- Sync scheduler, analyzer worker, and RMM worker all auto-start as side effects of being imported on the server. Don't eager-import them from hot paths or shared utilities.
- Analyzer LLM provider is per-request (
anthropic|openrouter). The idempotencycontent_hashis provider-scoped — the same ticket can have one Claude row and one OpenRouter row. - Analyzer cost ceiling: Stage 4 (Opus) skipped above $2.00 estimated cost; the analysis is flagged for human review.
- IT Glue results destined for an LLM must go through
lib/services/analyzer/itglue-search.ts(redacted). Don't pipe raw client output into a prompt.
Useful existing docs
ARCHITECTURE.md— runtime, data flow, workers, analyzer pipeline (read first)DESIGN.md— UI tokens, nav IA, component conventions, current cleanup backlogAUTOTASK_API_GUIDE.md,ADDIGY_API_GUIDE.md— credential setupPOSTGRES_SYNC_SETUP.md,DOCKER_README.mdPULSE_DATABASE_SKILL.md— diagnostic queriesdocs/— sync behavior, webhook setup, workflow editor, analyzer runbook, RMM Overshell + LogLift specs, IT Glue audit spec, per-integration guides
Project
Pulse Mobile Shell Redesign
A ground-up redesign of Pulse's /mobile/* shell — the manager-on-the-go view of
the existing Pulse PSA dashboard. It replaces the current mobile layout, swaps
the standalone /mobile/nav page for a Sheet drawer, restyles Dashboard /
Tickets / Finance, and adds two new mobile surfaces (Analyzer feed,
Engagement). Built on the existing Pulse codebase — same routes, same data,
phone-first layouts.
Core Value: A manager can open Pulse on their phone and, in under 30 seconds, see the state of the business and triage tickets — without ever needing to switch to desktop for read-only awareness.
Constraints
- Tech stack: Next.js 16 App Router, React 19, Tailwind 4, shadcn/ui — match existing Pulse conventions (no new state libraries, no SWR/react-query, no ORM, no Zod in API routes unless required)
- Routes: Keep all existing
/mobile/*paths. Replace files in place. No new top-level routes outside/mobile/. - Build order: Each spec step ships independently — no big-bang merge.
Phase boundaries should let each step land on
mastercleanly. - No service worker / no offline: don't introduce
next-pwaor a custom SW in this iteration. - Auth: existing Better Auth + middleware handles
/mobile/*. No new auth surface. - Data sources: reuse existing endpoints where possible; add
/api/mobile/*only when an existing list endpoint doesn't return the needed shape (e.g., analyzer feed).
Technology Stack
Languages
- TypeScript 5 - Entire codebase, strict mode enabled
- JavaScript/JSX - React components via TypeScript with JSX support
- SQL - PostgreSQL migrations and queries
- Bash - Build and deployment scripts
Runtime
- Node.js (version inferred from package.json compatibility)
- Next.js 16.1.1 running on port 3100
- npm (lockfile: package-lock.json)
Frameworks
- Next.js 16.1.1 - App Router with
output: 'standalone'for Docker, React Compiler enabled - React 19.2.3 - Server and client components, React Compiler active
- Better Auth 1.4.10 - Authentication with magic link, TOTP 2FA, Microsoft OAuth
- Tailwind CSS 4.1.18 - Utility-first styling
- shadcn/ui (via Radix UI primitives) - Component library:
components/ui/ - Recharts 3.7.0 - Charts and graphs (analytics/dashboards)
- Lucide React 0.562.0 - Icon library
- Sonner 2.0.7 - Toast notifications
- cmdk 1.1.1 - Command palette component
- react-hook-form 7.70.0 - Form state management (admin/auth only)
- Zod 4.3.5 - Type-safe schema validation
- @hookform/resolvers 5.2.2 - Form resolver for Zod
- @tanstack/react-table 8.21.3 - Headless table library with sorting, pagination, search
- react-markdown 10.1.0 - Markdown rendering
- remark-gfm 4.0.1 - GitHub-flavored markdown support
- date-fns 4.1.0 - Date manipulation and formatting
- react-day-picker 9.13.0 - Calendar date picker
- clsx 2.1.1 - Conditional className utility
- tailwind-merge 3.4.0 - Merge Tailwind class conflicts
- class-variance-authority 0.7.1 - CSS-in-JS variant management
- next-themes 0.4.6 - Dark mode theme switching
Testing & Build
- vitest 4.1.5 - Unit and integration testing runner
- Run:
npm test(run once),npm run test:watch(watch mode) - Test coverage for:
lib/services/analyzer/**,lib/services/rmm/**,lib/services/b2/**,lib/services/analyzer/link-discovery.test.ts - Turbopack (via Next.js 16) - Fast bundler for development and production
- ESLint 9.39.2 - Linting (with eslint-config-next 16.1.1)
- TypeScript - Type checking via
npx tsc --noEmit --pretty
Key Dependencies
- pg 8.11.0 - PostgreSQL client (no ORM); queries via
postgresClientsingleton - ioredis 5.9.0 - Redis client for caching; optional (graceful fallback if REDIS_URL unset)
- node-cron 4.2.1 - Job scheduler for sync tasks and workflows
- @anthropic-ai/sdk 0.91.1 - Anthropic Claude API client for AI Ticket Analyzer pipeline
- Better Auth ecosystem packages - OAuth, 2FA, session management
- nodemailer 7.0.12 - Email delivery for magic link auth
- babel-plugin-react-compiler 1.0.0 - React Compiler for optimized renders
- shadcn 4.6.0 - CLI tool for adding shadcn/ui components
- baseline-browser-mapping 2.10.8 - Browser compatibility mapping
- tw-animate-css 1.4.0 - Tailwind animation utilities
Configuration
BETTER_AUTH_URL- Base URL for auth (e.g.,http://localhost:3100orhttps://pulse.wulfconsulting.cloud)BETTER_AUTH_SECRET- Secret key for session signingDATABASE_URLorPOSTGRES_*- PostgreSQL connection (host, port, db, user, password)REDIS_URL- Redis connection (e.g.,redis://localhost:6380in Docker compose)SESSION_TIMEOUT_SECONDS- Session TTL (default: 86400 / 24 hours)MICROSOFT_CLIENT_ID,MICROSOFT_CLIENT_SECRET,MICROSOFT_TENANT_ID- Microsoft OAuth for login (tenant is specific, not 'common')DEFAULT_ADMIN_EMAIL- Bootstrap admin account email- Integration env vars: prefixed by service (e.g.,
AUTOTASK_*,DATTO_RMM_*,MSGRAPH_*, etc.) — see INTEGRATIONS.md - Path alias:
@/*maps to project root for cleaner imports - Target: ES2017
- Strict mode enabled
- Config:
tsconfig.json - File:
next.config.ts - Standalone output for Docker deployment
- React Compiler enabled
- Image domains: configurable (currently empty)
npm run build→ Next.js standalone app in.next/npm run start→ Starts production server on port 3100
Platform Requirements
- Node.js 18+ (inferred from Next.js 16 compatibility)
- npm 8+
- PostgreSQL 16 (local or Docker)
- Redis 7 (optional, enables caching)
- Docker & Docker Compose (for full stack)
- Docker with docker-compose.yml provided
- Postgres 16 container (applies migrations on init)
- Redis 7 container on port 6380 (custom, not 6379)
- Next.js app container on port 3100
- Traefik integration available (via labels in docker-compose.yml) for routing at
pulse.wulfconsulting.cloud - Container-based: standalone Next.js image with migrations applied via Postgres init
- Volume mounts for data persistence:
redis_data,postgres_data - Environment configuration via
.env.localfile mounted read-only
Conventions
Naming Patterns
- kebab-case for all files and directories (e.g.,
postgres-client.ts,invite-user-form.tsx,entity-sync.ts) - Nested directories use kebab-case (e.g.,
lib/services/analyzer/,components/admin/users/) - camelCase for all functions (e.g.,
getAutotaskClient(),transformCompany(),extractExplicitFromText()) - Factory functions prefixed with
get(e.g.,getAutotaskClient(),getDattoRmmClient()) - Helper functions suffixed with descriptive intent (e.g.,
relTime(),deriveSigningKey()) - Private/internal functions prefixed with underscore:
_INTERNALSobjects expose internals for test access - camelCase for all variables (e.g.,
isLoading,setData,ticketNumber) - Constants in UPPER_SNAKE_CASE (e.g.,
MAX_EXPLICIT_LINKS,TICKET_NUMBER_REGEX) - Database column names are always snake_case (e.g.,
company_name,is_active,created_at) - PascalCase for all type names (e.g.,
ClassificationRule,WorkflowExecution,TicketData) - Single-letter generics are acceptable (e.g.,
queryEntity<T>()) - Union types as literal strings (e.g.,
type RuleType = 'branch_routing' | 'ticket_type') - PascalCase exported from kebab-case files (e.g., export
InviteUserFormfrominvite-user-form.tsx) - Page components:
export default function ComponentName()at end of file - Form components: follow
[Resource]Formnaming (e.g.,InviteUserForm,SignInForm,UserForm)
Code Style
- TypeScript strict mode enabled (
"strict": trueintsconfig.json) - No explicit formatter config (ESLint handles style)
- Indentation: 2 spaces (inferred from existing code)
- ESLint:
eslint.config.mjswith Next.js config (eslint-config-next/core-web-vitals,eslint-config-next/typescript) - No additional custom rules beyond Next.js defaults
- Type checking:
npx tsc --noEmit --pretty(must pass before commit)
Import Organization
- Configured as
"@/*": ["./*"]intsconfig.json - Use
@/lib/...,@/components/...,@/app/...always - Never use relative paths like
../../../for imports
Error Handling
- All async functions use
try/catchblocks - API routes: catch errors and return
NextResponse.json({ error, message }, { status }) - Standard status codes:
500for runtime errors,503for missing/bad config,401/403from auth helpers - Error messages: include
error instanceof Error ? error.message : 'fallback message'
Logging
console.error()for exceptions caught in try/catch (usually in API routes and services)- Include context:
console.error('Failed to fetch companies:', error) - No
console.log()for debugging (remove before commit per linter checks)
Comments
- Explain why, not what (code shows the what)
- Non-obvious logic or business rules
- Performance-critical sections
- Workarounds or hacks (mark with
// HACK:or// NOTE:) - Used sparingly on complex functions
- Example from
lib/services/analyzer/link-discovery.ts: - Not required for simple getters/setters or obvious functions
Function Design
- Keep functions focused: one responsibility per function
- Aim for <50 lines for page components, <30 for utilities
- Complex operations broken into smaller helpers
- Prefer object parameters for >3 arguments
- Don't use
any— use specific types - Use
Partial<T>for optional object shapes - Async functions always return
Promise<T>explicitly - Prefer
nulloverundefinedfor missing values - Use discriminated unions for success/error returns in critical paths (see analyzer pipeline)
Module Design
- Prefer
exportat declaration point rather than grouped re-exports - One main export per file (exception: barrel files in
components/ui/) - Internal utilities prefixed with underscore:
_INTERNALSobject for test access components/ui/index.tsexports all shadcn primitiveslib/types/has domain-specific barrel files (e.g.,lib/types/workflow.ts,lib/types/autotask.ts)- Avoid deep nesting — import from files, not directories unless barrel exists
Database Transformations
Shared Components & Libraries
- shadcn primitives live in
components/ui/ - Feature-specific components in sibling directories (e.g.,
components/dashboard/,components/admin/) - Icons: Always use
lucide-react(e.g.,import { Loader2, Send } from 'lucide-react') - Use
@tanstack/react-tableviacomponents/admin/DataTable.tsxwrapper - Example:
<DataTable columns={columns} data={data} /> - Use
components/admin/DetailModal.tsxfor entity details - Follows card + tabs pattern (formatted/raw)
- Use
components/navigation/app-navigation.tsx(NavigationMenufrom Radix) - Dropdowns prefer
@radix-ui/react-dropdown-menuover submenus - Use
sonnerlibrary:import { toast } from 'sonner' - Patterns:
toast.success(),toast.error(),toast.info() - Use
react-hook-form+ Zod for validation - Only in admin/auth forms — NOT in every page
- Pattern:
useForm()withzodResolver(), then<Form>wrapper from shadcn - Use
rechartsfor data visualization (e.g.,<BarChart>,<LineChart>)
What NOT to Introduce
- No ORMs (Prisma, TypeORM, etc.) — use
postgresClientsingleton and manual transforms - No server actions (
'use server') — use API routes called viafetch()from clients - No additional state libraries (SWR, react-query, TanStack Query) — match local
useState+fetchpattern - No change to authentication (Better Auth is final)
- No editing of committed migrations — always create new numbered ones
- Keeps codebase lean and explicit
- Reduces abstraction overhead
- Makes data flow (DB → API → Client) visible
Migrations
Architecture
Pattern Overview
- App Router pages (
'use client') fetch data via API routes usingfetch() - Three auto-starting background workers via side-effect imports (sync-scheduler, analyzer, RMM overshell)
- Service layer in
lib/services/with factories + configuration helpers - Postgres 16 as system of record (snake_case columns), Redis for caching only
- Manual transformation from snake_case DB columns to camelCase API responses
- Better Auth 1.4 for sessions + magic link + TOTP 2FA + Microsoft OAuth
Layers
- Purpose: Accept HTTP requests, validate auth, delegate to services, return JSON responses
- Location:
app/api/*/route.ts,app/*/page.tsx - Contains: Next.js route handlers (GET/POST/PATCH/DELETE), page components
- Depends on: Auth via
lib/auth-utils.ts, services vialib/services/ - Used by: Client-side fetch calls from UI components
- Purpose: Sync data from external APIs, run background jobs, execute workflows, store results in Postgres
- Location:
lib/services/*.ts(~50 files) plus subdirectories (analyzer/,rmm/,llm/,b2/) - Contains: Integration clients (autotask, datto-rmm, itglue, veeam, msgraph, etc.), sync logic, job executors, pipeline orchestration
- Depends on: Postgres client, external API clients, environment configuration
- Used by: API routes (sync endpoints, webhook handlers) and background workers
- Purpose: Query, upsert, and manage state in Postgres
- Location:
lib/services/postgres-client.ts(singleton) +migrations/NNN_*.sql - Contains: Connection pool, query builder methods (
query(),transaction(),upsert(),bulkUpsert()), migration definitions - Depends on: PostgreSQL 16 connection string from env
- Used by: All services that read/write data
- Purpose: Poll for work and execute sync/analysis/RMM tasks without blocking HTTP requests
- Location:
lib/services/sync-scheduler.ts,lib/services/analyzer/worker.ts,lib/services/rmm/worker.ts - Contains: node-cron scheduler, polling loops with exponential backoff, state machine handlers
- Depends on: Postgres client, service layer integrations
- Used by: Auto-start on module import (side effects); HTTP routes that need them running will import the module to start them
- Purpose: Render pages, dialogs, tables, charts, and handle client-side state
- Location:
components/ui/(shadcn primitives),components/*/(feature-specific),app/*/page.tsx - Contains: React components, hooks for fetch + state, sonner toasts, recharts visualizations
- Depends on: API routes via
fetch(), client auth via Better Auth SDK - Used by: Next.js pages and other components
- Purpose: Define TypeScript interfaces and database schema
- Location:
lib/types/<domain>.ts,migrations/NNN_*.sql - Contains: Entity types (ticket, company, analysis, rmm_execution, etc.), sync request shapes, API response envelopes
- Depends on: (none — they define contracts)
- Used by: Services, API routes, components
Data Flow
- HTTP requests: Stateless; session from Better Auth cookie
- Background jobs: State in Postgres (status columns:
queued→in_flight→complete/failed) - Caching: Redis (optional, used for integration health checks, cache duration varies)
- Authorization: Checked in route handlers via
requireAuth()/requireAdmin()/requirePermission()fromlib/auth-utils.ts
Key Abstractions
- Purpose: Lazy-load integration clients with configured credentials; provide
is<Name>Configured()helper to check env vars - Examples:
lib/services/autotask-factory.ts,lib/services/datto-rmm-factory.ts,lib/services/msgraph-factory.ts - Pattern: Export
getAutotaskClient(),getDattoRmmClient(),getMsgraphClient()with caching; throw if credentials missing; bundled withis<Name>Configured()for upstream checks - Why: Decouples client initialization from route handlers; allows conditional feature gates per env
- Purpose: Single connection pool for all Postgres queries; auto-lazy-initializes; provides ORM-like query builder
- Examples:
postgresClient.query(),postgresClient.transaction(),postgresClient.upsert(),postgresClient.bulkUpsert() - Pattern: Private constructor, static
getInstance(), pool initialized on first use - Why: Prevents connection leaks; provides consistent interface across ~50 services
- Purpose: Incremental/full sync of any entity type from external API → Postgres
- Examples:
lib/services/entity-sync.ts(Autotask),lib/services/itglue-sync-service.ts,lib/services/veeam-sync-service.ts - Pattern: Reads
lastTrackedModificationDateTimefrom last_sync table; queries external API; batches upsert viabulkUpsert() - Why: Codifies the "last sync timestamp + incremental pull + batch insert" pattern across integrations
- Purpose: Orchestrate multi-stage LLM analysis with fallbacks, cost guards, and persistence
- Examples:
lib/services/analyzer/pipeline.ts,lib/services/analyzer/stages/*.ts - Pattern: Each stage returns structured output (gap analysis, category, cost estimate); cost guard checks ceiling before Stage 4; all stages persisted to
analyzer_stage_executions - Why: Allows cost control (skip expensive Opus above $2.00), idempotency (content_hash), and debugging (inspect each stage's output)
- Purpose: Auto-start in production, poll for work, claim rows with
FOR UPDATE SKIP LOCKED, execute, persist result - Examples:
lib/services/analyzer/worker.ts,lib/services/rmm/worker.ts,lib/services/sync-scheduler.ts - Pattern: Side-effect import auto-starts on module load; exponential backoff if no work; row locking for multi-instance safety (analyzer), or single-instance gate (sync scheduler)
- Why: Keeps background work out of HTTP request path; analyzer is safe to scale (row locking); sync scheduler should run on one instance only
- Purpose: Define three roles (
user,admin,super-admin) with per-resource permissions - Examples:
lib/auth.ts(auth config),lib/permissions.ts(permission matrix),lib/auth-utils.ts(runtime checks) - Pattern: Route handler calls
requireAdmin()/requirePermission(), which decode session and check role; middleware only verifies session cookie exists - Why: Separates auth (middleware) from authorization (route handler); role-based gates are checked at the point of use
Entry Points
- Location:
app/*/page.tsx - Triggers: Browser navigation to any route except public paths
- Responsibilities: Render page shell with
AppNavigation+PageHeader, fetch data from API, render client-side components, handle toast/dialog interactions - Location:
app/api/*/route.ts - Triggers:
fetch()from client, external webhooks (Autotask, Zabbix, RMM), scheduler HTTP calls - Responsibilities: Validate auth/webhook signature, delegate to service layer, return JSON response with appropriate status (200, 401, 403, 503, 500)
- Location:
app/api/webhooks/autotask,app/api/zabbix/webhook,app/api/rmm/loglift - Triggers: External systems (Autotask, Zabbix, OpenClaw) POST events
- Responsibilities: Verify HMAC or custom header, parse event, enqueue jobs or upsert data, return 200 (even on error so Autotask doesn't deactivate)
- Location:
app/api/sync/*,app/api/datto-rmm/sync,app/api/itglue/sync,app/api/veeam/sync, etc. - Triggers:
lib/services/sync-scheduler.tsfires HTTP POST at configured times - Responsibilities: Call sync service, update
last_synctimestamp, return 200 on success or error message - Location:
lib/services/sync-scheduler.ts(cron),lib/services/analyzer/worker.ts(2s poll),lib/services/rmm/worker.ts(5s poll) - Triggers: Auto-starts as side effect of module import; runs indefinitely in production
- Responsibilities: Poll for work from DB, claim row, execute, persist result, handle errors + logging
Error Handling
- Webhook handlers — Return 200 even if processing fails; log error so ops can investigate via audit log
- API route handlers —
try/catch, returnNextResponse.json({ error, message }, { status })with conventions: - Analyzer pipeline — Cost ceiling guard at $2.00; above that, Stage 4 (Opus) skipped, analysis flagged with
needs_review=true - Sync services — Incremental sync errors log + re-trigger on next schedule; full sync errors persist
last_errortosync_schedulestable - Background workers — Stale jobs reset on worker boot (no recovery); exponential backoff on empty polls; errors logged with job ID for manual inspection
Cross-Cutting Concerns
Project Skills
No project skills found. Add skills to any of: .claude/skills/, .agents/skills/, .cursor/skills/, or .github/skills/ with a SKILL.md index file.
GSD Workflow Enforcement
Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync.
Use these entry points:
/gsd-quickfor small fixes, doc updates, and ad-hoc tasks/gsd-debugfor investigation and bug fixing/gsd-execute-phasefor planned phase work
Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it.
Developer Profile
Profile not yet configured. Run
/gsd-profile-userto generate your developer profile. This section is managed bygenerate-claude-profile-- do not edit manually.