wulf-pulse/ARCHITECTURE.md
lorentz 9bfb57553d feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch.  Drops 2013-era
inline styles and consolidates patterns behind shared primitives.

Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
  the standards-guide blue (#0075AD) with utility classes for numerics
  (.num / .num-lg / .num-xl), metric labels, surface tints, and the
  wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
  Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
  "Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page

Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
  health table, worker pulse cards (analyzer / RMM / sync scheduler),
  token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
  to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
  integrations (e.g. SentinelOne) — no failure noise from broken-on-
  purpose entries.  Aliases supported (sentinelone → s1, etc.)

Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
  total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
  area chart, 30-day mean resolution time line chart, today's active
  engineers leaderboard

Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
  status, classification, source, company type, publish, active /
  yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)

Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
  PageHeader rule (consistent across flat links and submenu triggers);
  active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config

Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs

DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
  unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow

Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
  collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below

Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
  workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
  rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
  INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)

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

15 KiB
Raw Blame History

Pulse — Architecture

Pulse is a single Next.js 16 app (output: 'standalone') that backs Wulf Consulting's PSA workflows. It pulls data from Autotask, Datto RMM, IT Glue, MS Graph, Veeam, and ~10 other systems into Postgres, runs background workers for sync / AI analysis / RMM execution, and serves dashboards + admin tooling on port 3100.

This file is the load-bearing reference for how the system is wired. Per- feature deep dives live in docs/. UI/visual conventions live in DESIGN.md.

1. Runtime topology

One Node process, one Postgres, one Redis. Background work runs in-process inside the Next server — there is no external job queue.

┌─────────────────────────────────────────────────────────┐
│ Next.js 16 (port 3100, output: 'standalone')            │
│                                                         │
│  HTTP routes ──► API handlers ──► Postgres / Redis      │
│                                                         │
│  Side-effect imports auto-start three workers:          │
│   • SyncScheduler     (node-cron)                       │
│   • AnalyzerWorker    (poll analyzer_jobs every 2s)     │
│   • RmmOvershellWorker (poll rmm_executions every 5s)   │
└─────────────────────────────────────────────────────────┘
        │                  │                    │
        ▼                  ▼                    ▼
   Postgres 16         Redis 7            External APIs
   (state)             (cache only)       (Autotask, RMM, …)

Workers are side-effect imports. Touching one of these modules from a server-side import path starts the loop:

Worker File Trigger
Sync scheduler lib/services/sync-scheduler.ts Self-init at module bottom
Analyzer lib/services/analyzer/worker.ts Auto-starts when NODE_ENV=production or ANALYZER_WORKER_AUTOSTART=1
RMM Overshell lib/services/rmm/worker.ts Same gate as analyzer

Routes that need a worker running deliberately import its module — e.g. app/api/analyzer/tickets/[ticketNumber]/analyze/route.ts imports lib/services/analyzer/worker.ts purely so the loop starts on first request. Don't eager-import these from hot paths or shared utilities.

There is no out-of-process queue. Scaling out to multiple Next instances means each instance runs duplicate pollers. The analyzer worker uses SELECT … FOR UPDATE SKIP LOCKED so jobs run exactly once across instances, but the sync scheduler does not — run it on a single dedicated instance, or gate it behind an env flag on replicas.

2. Data flow

Autotask (primary system of record)

  • WebhookPOST /api/webhooks/autotask, public per middleware.ts, HMAC-verified inside the handler. Returns 200 even on processing errors so Autotask doesn't deactivate the subscription. Async handlers may enqueue analyzer jobs.
  • Periodic synclib/services/entity-sync.ts runs per scheduled task. Incremental via lastTrackedModificationDateTime when supported; full upsert otherwise. Targets tickets, companies, contacts, resources, tasks, time_entries, etc.

Datto RMM

  • Sync — devices, sites, alerts → datto_rmm_* tables.
  • Overshell executorlib/services/rmm/executor.ts validates a registered script ID, resolves the target device, enforces a per-user rate limit (50 / 24h), inserts a pending row in rmm_executions, and calls client.runQuickJob(). The worker polls Datto for the result and parses output via the script's parseOutput() method. Scripts are code- registered (lib/services/rmm/scripts/) — adding one is a TS change, not a DB change.
  • LogLiftPOST /api/rmm/loglift/upload (public, x-openclaw-key header). Receives a B2 object key, downloads + decompresses the gzipped JSON (capped at 100 MB), stores a slim summary in loglift_uploads and the full payload in B2 (lib/services/b2/client.ts). Resolves the device → Autotask company → IT Glue configuration; if a unique IT Glue match is found, fires an asset-first audit automatically.

IT Glue

  • Synclib/services/itglue-sync-service.ts pulls org types, configs, contacts, locations, flexible assets, etc. → itg_* tables. Note: flexible assets must be listed per type (API 422 otherwise — see commit a0a6e7f).
  • Search (analyzer)lib/services/analyzer/itglue-search.ts returns redacted documents only. Credentials/PII pass through redact() before any LLM sees them. Callers must not bypass redaction; the raw client is for non- LLM use.
  • Audit + write-backlib/services/analyzer/asset-audit/ runs LLM-driven audits against IT Glue configurations or flexible assets, writes results to itglue_audit_logs, and links tickets via itglue_ticket_xrefs. Reverts go through …/revert/[writeId].

MS Graph (Engagement)

  • App-only auth, specific tenant ID (not common). Reports API returns CSV; parsed inline. Joins to Autotask hours via resources.email = graph_users.email. See lib/services/engagement-sync-service.ts.

Other integrations

Each has a factory + is<Name>Configured() helper in lib/services/. All credentials come from env; clients throw if missing.

System Role Files
Veeam VSPC Backup status, RPO, ticket analysis veeam-*-service.ts
Auvik Network monitoring; tenant mappings auvik-client.ts
Addigy Apple endpoints; org mappings addigy-factory.ts
Mimecast Mail security mimecast-sync-service.ts
SentinelOne EDR sentinelone-sync-service.ts
Duo MFA duo-sync-service.ts
Zoom Meetings zoom-sync-service.ts
QuickBooks Billing reconciliation qbo-sync-service.ts
Zabbix WAN monitoring; webhook at /api/zabbix/webhook
Salesbldr Sales pipeline

3. Analyzer pipeline

lib/services/analyzer/pipeline.ts orchestrates seven stages. Provider is chosen per request (anthropic default, openrouter opt-in); models are mapped per stage in lib/services/llm/models.ts.

Stage Model (Anthropic) Purpose
0 — Preprocess Filter workflow noise, tag entities, compute content_hash (idempotency key)
1 — Triage Haiku Categorize, extract entities, initial priority
2 — IT Glue retrieval Redacted doc lookup, skipped if IT Glue not configured
3 — Deep analysis Sonnet Summary, gaps, what-was-done, what-should-have-been-done
4 — Deep reasoning Opus (optional) Apply corrections, propose IT Glue updates, re-rank
5 — Persist Write analyzer_analyses row + per-stage execution rows
6 — Fingerprint Haiku Structured fingerprint for cross-ticket aggregation

Idempotency. If content_hash already exists for the ticket and force=false, the pipeline returns the existing analysis. The hash is provider-scoped — Claude and DeepSeek analyses of the same ticket are separate rows.

Cost ceiling. Stage 4 is skipped if estimated total cost exceeds $2.00; the analysis is flagged for human review. All LLM/RMM activity is logged to analyzer_cost_audit.

Link-aware bundles. lib/services/analyzer/link-discovery.ts resolves related tickets two ways: (1) explicit — regex T-numbers in descriptions/notes, "RELATED TICKETS:" blocks, the problem_ticket_id column; (2) suggested (opt-in) — Haiku ranks recent same-company tickets by semantic similarity. When a bundle is analyzed, members get pending_analyses rows; once all complete, an aggregate report fires.

Aggregate reports. stages/aggregate-reduce.ts pairs SQL distributions (category, client, resolution path, root cause) with a Sonnet pass that identifies documentation gaps, process gaps, client patterns, recurrence clusters. Persisted to analyzer_aggregate_reports.

Asset audit (Phase 4). lib/services/analyzer/asset-audit/runner.ts runs post-analysis. Two modes: all-time evidence across every analysis linked to the asset, or ticket-first (Phase 4.1) narrowed to a single analysis ID.

4. Background jobs

Worker Cadence Scope Concurrency model
AnalyzerWorker 2s poll Claims analyzer_jobs.status='queued' FOR UPDATE SKIP LOCKED, exactly-once across instances
RmmOvershellWorker 5s poll Polls in-flight rmm_executions; advances state by querying Datto Single in-process loop
SyncScheduler node-cron Per-schedule rows in DB (Autotask, IT Glue, Veeam, Engagement, Zoom, Duo, …) Not safe for multi-instance — overlaps possible
integration-health-alerts Cron-fired from sync scheduler Detects stale syncs, publishes alerts Single in-process

Stale in-flight analyzer jobs are reset on worker boot (commit 378e68a) so a crashed pod doesn't leave jobs orphaned.

5. Auth & permissions

Better Auth 1.4 with magic link + TOTP 2FA + Microsoft OAuth. Sessions live in Postgres (no Redis session store). Account-linking is enabled for Microsoft so admin-invited users join their MS account in one click.

Roles. user, admin, super-admin. Default admin bootstrapped from DEFAULT_ADMIN_EMAIL via lib/bootstrap.ts.

Resources (lib/permissions.ts) — tickets, configItems, admin, users, roles, auditLog, settings, itglue, rmm. The itglue and rmm resources were added with the Overshell + IT Glue write-back work; user role gets read-only on both.

API auth pattern. Every route handler calls one of:

const { session, error } = await requireAuth();
const { session, error } = await requireAdmin();
const { session, error } = await requireSuperAdmin();
const { session, error } = await requirePermission('itglue', 'write');
if (error) return error;

middleware.ts only checks for a session cookie — role/permission checks live in the route handler.

Public routes (hardcoded in middleware.ts): /api/auth/*, /api/webhooks/*, /api/sync/*, /api/health, /api/zabbix/webhook, /api/rmm/loglift, /api/mobile/*, /api/openclaw/*, /legal, /api/kiosk, /api/qbo/*. Add to that list whenever you introduce a new public endpoint.

6. Database

89 numbered migrations (migrations/NNN_*.sql), applied in alphabetical order on Postgres init only. Existing volumes do not re-run them — for schema changes against an existing DB, use scripts/apply-migrations (verify behavior first; varies by age of script).

Topical groupings:

Range Topic
001014 Core schema, auth tables, admin settings
015026 Ticket fields, queues, RMM site mappings, integration health
027032 Datto RMM, Veeam agents/alarms, priorities, ticket categories, RMM webhooks
037044 IT Glue (large), Veeam RPO, contract services, engagement
045055 Zoom, Teams, morning summary, ping suppression, ticket digest, Zabbix WAN, QBO, Mimecast
056068 UDFs, Autotask tags, Duo, project phases, recurring revenue, Veeam ticket analysis
069074 Analyzer (jobs, analyses, stage executions, aggregate reports, cost audit, link-aware bundles, provider)
075076 IT Glue audit + ticket xrefs
077078 RMM Overshell, LogLift uploads
079080 Endpoint data model, device-xref company_id

Watch out for:

  • Duplicate numbers exist (002, 004, 009). Apply order is filesystem-sort alphabetical, not numeric. Don't introduce more.
  • Conventions: IF NOT EXISTS for tables/indexes, ON CONFLICT DO NOTHING for seed data, audit columns created_at / updated_at / synced_at / is_deleted / deleted_at.
  • priorities has no is_deleted column — caught the hard way (9acf48e).
  • Columns are snake_case; API responses are camelCase. Handlers transform manually. No ORM.

DB access is the singleton at lib/services/postgres-client.tspostgresClient.query(), .transaction(), .upsert(), .bulkUpsert().

7. Deployment

Docker Compose at the repo root.

  • postgres — Postgres 16, port 5432. Migrations volume mounted at /docker-entrypoint-initdb.d/. Init runs once per volume.
  • redis — Redis 7, port 6380 (host) / 6379 (container). Cache only.
  • app — built from Dockerfile (turbopack, output: 'standalone'); runs node server.js. Port 3100. .env.local mounted read-only. Traefik labels for pulse.wulfconsulting.cloud (HTTPS via Cloudflare cert).

npm run build uses turbopack (Next 16 default). npm run dev for local; npx tsc --noEmit --pretty for type check; npm test (vitest) for the analyzer / RMM / B2 / link-discovery unit tests. No CI; type-check is the only safety net for code that doesn't have unit tests.

8. Invariants & gotchas

  1. Worker side-effect imports. Importing sync-scheduler.ts, analyzer/worker.ts, or rmm/worker.ts from a hot path starts the loop.
  2. No external queue. Multiple instances duplicate pollers. Analyzer is safe via row locking; sync scheduler is not — pin to one instance.
  3. IT Glue redaction is mandatory for any LLM-bound query. Use itglue-search.ts, never the raw client.
  4. Provider-scoped idempotency. force=false only short-circuits if the same provider produced the existing analysis.
  5. Cost ceiling at $2.00 before Stage 4. Above that, Opus is skipped and the analysis is flagged.
  6. Webhook handlers return 200 on failure (Autotask) to avoid deactivation. Errors are logged, not surfaced.
  7. Postgres init runs migrations once. Existing volumes won't re-run them.
  8. Duplicate migration numbers. Apply order is alphabetic.
  9. .env is committed. Treat the values as potentially real production secrets; don't log or echo them.
  10. RMM script registry is in code. lib/services/rmm/scripts/ — unregistered scripts can't execute.
  11. LogLift zip-bomb guard caps inflated payloads at 100 MB.
  12. Stale analyzer jobs reset on worker boot. Don't rely on in_flight state surviving restarts.

9. Where to look

Concern Start here
HTTP routes app/api/**/route.ts
Pages app/**/page.tsx
Worker boot lib/services/{sync-scheduler,analyzer/worker,rmm/worker}.ts
Postgres access lib/services/postgres-client.ts
Auth wiring lib/auth.ts, lib/auth-utils.ts, lib/permissions.ts, middleware.ts
Analyzer pipeline lib/services/analyzer/pipeline.ts + stages/
LLM dispatch lib/services/llm/{call,models,pricing}.ts
RMM executor lib/services/rmm/{executor,worker,target-resolver}.ts
IT Glue write-back lib/services/analyzer/asset-audit/
Per-feature notes docs/ (one file per system)