# 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.ts` handlers
- `lib/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 wiring
- `components/ui/` — shadcn primitives; sibling dirs are feature components
- `migrations/NNN_*.sql` — numbered SQL, applied in alphabetical order on Postgres
  init. Use `IF NOT EXISTS` + `ON CONFLICT DO NOTHING` for 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 are **`camelCase`** — handlers
  transform manually (no ORM).
- Use the `postgresClient` singleton from `lib/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 via `scripts/apply-migrations` (check first; behavior varies).

## API routes
- Pattern: `app/api/<resource>/route.ts` exporting `GET`/`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`, return `NextResponse.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`:
  ```ts
  const { session, error } = await requireAuth();          // or requireAdmin() / requireSuperAdmin() / requirePermission(resource, action)
  if (error) return error;
  ```
  `middleware.ts` only verifies a session cookie exists — role checks happen here.
- No `'use server'` actions in this codebase. Everything is API routes called from
  client components via `fetch`.

## Frontend
- Most pages are `'use client'` with `useState`/`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-table` via `components/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) |
| AWS Route 53 | `AWS_*` (literal `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_REGION` — intentional exception to the per-service prefix convention; the AWS SDK's default credential chain hardcodes these names. Injected by BWS at the container entrypoint, never in `.env`) |
| 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
  via `lastTrackedModificationDateTime` when 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 per `middleware.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 migration `012`.
- Default admin bootstrapped from `DEFAULT_ADMIN_EMAIL` via `lib/bootstrap.ts`.
- `middleware.ts` redirects 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) — covers `lib/services/analyzer/**`,
  `lib/services/rmm/**`, `lib/services/b2/**`, and `lib/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 up` from repo root. Postgres applies `migrations/*.sql`
  on init only (existing volumes won't re-run them).

## Conventions to follow
- Files: kebab-case. Components: `PascalCase` exports 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_DISABLED` env var (legacy / bootstrap fallback).
    Comma- or space-separated keys with aliases (`sentinelone` → `s1`,
    `datto` → `datto_rmm`, `it-glue` → `itglue`, `ms-graph` → `msgraph`).
    Set in `.env` and restart.
  - **`/admin/integrations`** UI backed by the `integration_settings`
    table (migration 081). Toggle without a container restart; takes
    effect within the 5-minute health cache (PATCH clears the cache
    immediately). Audit columns capture `disabled_by` (session email),
    `disabled_at`, and an optional `disabled_reason`.
  In both cases live auth checks still run (logs surface the underlying
  state); the UI ignores the result for disabled integrations.
  - **PAX8 is the first exception**: disabling `key='pax8'` doesn't just
    suppress health-check display — the `pax8-daily` scheduler branch skips
    `fullSync()` and `POST /api/pax8/sync` returns 403. Every other
    integration's toggle today is display-only.

## Watch out for
- A `.env` file 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
  idempotency `content_hash` is 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 backlog
- `AUTOTASK_API_GUIDE.md`, `ADDIGY_API_GUIDE.md` — credential setup
- `POSTGRES_SYNC_SETUP.md`, `DOCKER_README.md`
- `PULSE_DATABASE_SKILL.md` — diagnostic queries
- `docs/` — sync behavior, webhook setup, workflow editor, analyzer runbook,
  RMM Overshell + LogLift specs, IT Glue audit spec, per-integration guides

<!-- GSD:project-start source:PROJECT.md -->
## 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 `master` cleanly.
- **No service worker / no offline**: don't introduce `next-pwa` or 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).
<!-- GSD:project-end -->

<!-- GSD:stack-start source:codebase/STACK.md -->
## 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 `postgresClient` singleton
- 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:3100` or `https://pulse.wulfconsulting.cloud`)
- `BETTER_AUTH_SECRET` - Secret key for session signing
- `DATABASE_URL` or `POSTGRES_*` - PostgreSQL connection (host, port, db, user, password)
- `REDIS_URL` - Redis connection (e.g., `redis://localhost:6380` in 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.local` file mounted read-only
<!-- GSD:stack-end -->

<!-- GSD:conventions-start source:CONVENTIONS.md -->
## 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: `_INTERNALS` objects 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 `InviteUserForm` from `invite-user-form.tsx`)
- Page components: `export default function ComponentName()` at end of file
- Form components: follow `[Resource]Form` naming (e.g., `InviteUserForm`, `SignInForm`, `UserForm`)
## Code Style
- TypeScript strict mode enabled (`"strict": true` in `tsconfig.json`)
- No explicit formatter config (ESLint handles style)
- Indentation: 2 spaces (inferred from existing code)
- ESLint: `eslint.config.mjs` with 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 `"@/*": ["./*"]` in `tsconfig.json`
- Use `@/lib/...`, `@/components/...`, `@/app/...` always
- Never use relative paths like `../../../` for imports
## Error Handling
- All async functions use `try/catch` blocks
- API routes: catch errors and return `NextResponse.json({ error, message }, { status })`
- Standard status codes: `500` for runtime errors, `503` for missing/bad config, `401`/`403` from 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 `null` over `undefined` for missing values
- Use discriminated unions for success/error returns in critical paths (see analyzer pipeline)
## Module Design
- Prefer `export` at declaration point rather than grouped re-exports
- One main export per file (exception: barrel files in `components/ui/`)
- Internal utilities prefixed with underscore: `_INTERNALS` object for test access
- `components/ui/index.ts` exports all shadcn primitives
- `lib/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-table` via `components/admin/DataTable.tsx` wrapper
- Example: `<DataTable columns={columns} data={data} />`
- Use `components/admin/DetailModal.tsx` for entity details
- Follows card + tabs pattern (formatted/raw)
- Use `components/navigation/app-navigation.tsx` (`NavigationMenu` from Radix)
- Dropdowns prefer `@radix-ui/react-dropdown-menu` over submenus
- Use `sonner` library: `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()` with `zodResolver()`, then `<Form>` wrapper from shadcn
- Use `recharts` for data visualization (e.g., `<BarChart>`, `<LineChart>`)
## What NOT to Introduce
- No ORMs (Prisma, TypeORM, etc.) — use `postgresClient` singleton and manual transforms
- No server actions (`'use server'`) — use API routes called via `fetch()` from clients
- No additional state libraries (SWR, react-query, TanStack Query) — match local `useState` + `fetch` pattern
- 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
<!-- GSD:conventions-end -->

<!-- GSD:architecture-start source:ARCHITECTURE.md -->
## Architecture

## Pattern Overview
- App Router pages (`'use client'`) fetch data via API routes using `fetch()`
- 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 via `lib/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()` from `lib/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 with `is<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 `lastTrackedModificationDateTime` from last_sync table; queries external API; batches upsert via `bulkUpsert()`
- 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.ts` fires HTTP POST at configured times
- Responsibilities: Call sync service, update `last_sync` timestamp, 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`, return `NextResponse.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_error` to `sync_schedules` table
- **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
<!-- GSD:architecture-end -->

<!-- GSD:skills-start source:skills/ -->
## 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:skills-end -->

<!-- GSD:workflow-start source:GSD defaults -->
## 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-quick` for small fixes, doc updates, and ad-hoc tasks
- `/gsd-debug` for investigation and bug fixing
- `/gsd-execute-phase` for planned phase work

Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it.
<!-- GSD:workflow-end -->

<!-- GSD:profile-start -->
## Developer Profile

> Profile not yet configured. Run `/gsd-profile-user` to generate your developer profile.
> This section is managed by `generate-claude-profile` -- do not edit manually.
<!-- GSD:profile-end -->
