wulf-pulse/CLAUDE.md
lorentz e1427b62d7 feat(admin): DB-backed integration toggles + sticky cols + dark contrast
Builds on the env-var INTEGRATIONS_DISABLED shipped with the nav-design
overhaul.  Adds a DB-backed admin UI so operators can flip integrations
without editing .env and restarting the container, plus the remaining
visual cleanup items from the design backlog.

Integration toggles
- Migration 081 — integration_settings table (key PK, disabled flag,
  reason, disabled_by audit, disabled_at).  Seeded with all 13 known
  integrations as enabled.
- GET / PATCH /api/admin/integrations — gated by requirePermission
  (admin, access).  PATCH clears the in-process integration-health
  cache so toggles take effect within seconds.
- /admin/integrations admin page with a Switch per integration, optional
  reason input, audit-info subtitle (disabled by, when, why), live
  status light from /api/dashboard/integration-health.
- integration-health service merges env-var disable list with DB rows;
  degrades gracefully if migration unapplied / DB unreachable.
- Wired into the Admin nav dropdown (eight items now).
- CLAUDE.md describes both env + DB sources.

Sticky first column on tables
- Table primitive accepts stickyFirstColumn?: boolean.  When true, TH
  and TD :first-child stay pinned during horizontal scroll, with
  background inheritance preserving hover and selected row tints.
- DataTable exposes the prop too — on by default for paginated tables.
- /addigy-devices opts in.

Dark-mode contrast
- --border lifted from 10% to 14% in .dark; --input from 15% to 18%;
  --sidebar-border to 14%.
- StatusLight outline ring lifted from /10 to /15 (light) and /20 (dark).
- DetailModal empty-cell em-dash lifted from /40 to /70 so missing
  values are legible on dark surfaces.

DESIGN.md
- Closed sticky-first-column, dark-mode contrast, and palette-audit
  items (palette deprioritized — most uses are semantic).
- Skeleton helpers documented as preferred for new code; existing
  ad-hoc patterns left in place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:55:22 -04:00

163 lines
8.8 KiB
Markdown

# 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>_*` |
| 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.
## 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