- Replace page/offset pagination with opaque base64 cursor (last_activity_date, id) - Export MobileTicket and MobileTicketListResponse interfaces for Plan 02 import - Add requireAuth() gate (T-04-03: legacy route lacked auth) - Server-side limit cap at 25 rows (D-11, T-04-04) - Default status filter [1,8,7] when no status param supplied (matches legacy t.status != 5) - Preserve getMobileCompanyFilter() helper verbatim - Support status/priority arrays, queue, mine, and search filters - Cursor seek predicate: (last_activity_date, id) < (cursor) for stable keyset order
8.8 KiB
8.8 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>_* |
| 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