Commit graph

71 commits

Author SHA1 Message Date
b4f8ccac8b feat(09-01): add owner_user_id to notification_channels + event-key and subscription tables
- migrations/085: ALTER TABLE notification_channels ADD COLUMN owner_user_id TEXT REFERENCES user(id) ON DELETE CASCADE
- migrations/085: partial unique index notification_channels_owner_user_id_channel_type_uniq WHERE owner_user_id IS NOT NULL (UPSERT race defense)
- migrations/086: CREATE TABLE notify_event_keys (key PK, display_label, description, sort_order, is_active) with seed row
- migrations/086: CREATE TABLE user_event_subscriptions composite PK (user_id, event_key, channel_type) opt-out model
- lib/types/pipeline.ts: NotificationChannel gains owner_user_id: string | null
- lib/types/pipeline.ts: exports NotifyEventKey and UserEventSubscription interfaces
2026-05-10 07:23:24 -04:00
4fc4a3d3b9 feat(09-01): add theme column to user table + Better Auth additionalField
- migrations/084_add_user_theme.sql: ALTER TABLE user ADD COLUMN theme TEXT NOT NULL DEFAULT 'system'
- Defensive backfill UPDATE for in-flight NULL rows on managed Postgres
- COMMENT ON COLUMN documents allowed values (light|dark|system)
- lib/auth.ts: adds theme additionalField with defaultValue 'system' after timezone
- session.user.theme now exposed via Better Auth same as session.user.timezone
2026-05-10 07:22:26 -04:00
3f6b13572e feat(08-01): add getUserPhotoBytes() to MsGraphClient
- New public method fetches binary photo from Graph /users/{id}/photo/$value
- Returns { bytes, contentType } on 200, null on 404 (no photo)
- Throws on other non-2xx for upstream caller to map to 502/503
- Reuses getToken() OAuth2 cache; no retry (best-effort per D-26)
- Existing methods (getToken, fetchJson, getUsers, etc.) untouched
2026-05-07 20:39:42 -04:00
ea5532c5c3 feat(07.1-03): add lib/services/user-timezone.ts helper
- getUserTimezone(session) returns validated IANA tz string with safe fallback
- DEFAULT_TIMEZONE_FALLBACK reads process.env.DEFAULT_TIMEZONE || 'UTC'
- Validates against Intl.supportedValuesOf('timeZone'); 64-char length cap
- Pure / synchronous / no DB / no @/lib/auth-utils import (avoids circular)
2026-05-07 08:01:23 -04:00
2ac2db7a23 feat(07.1-04): add useUserTimezone client hook
- New lib/hooks/use-user-timezone.ts exporting useUserTimezone() and formatInUserTimezone()
- Reads user.timezone from Better Auth useSession() additionalField (Plan 01)
- Validates against Intl.supportedValuesOf('timeZone') with safe fallback to NEXT_PUBLIC_DEFAULT_TIMEZONE || 'UTC'
- Pure formatInUserTimezone helper safe to call inside loops (not a hook)
- Resolves TZ-04
2026-05-07 07:52:16 -04:00
061f266b18 feat(07.1-01): expose user timezone on Better Auth session
- Adds `timezone` to additionalFields on the auth `user` config
- Default value reads process.env.DEFAULT_TIMEZONE (falls back to "UTC")
- session.user.timezone now available on every authenticated request
- Inferred User type automatically picks up the new field — no type changes needed
2026-05-07 07:36:35 -04:00
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
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
1112a06afe feat: RMM Overshell, IT Glue audit/write-back, LogLift, link-aware bundles, dashboard overhaul
- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target
  resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift)
- LogLift evidence pipeline (migration 078): upload webhook, B2 storage client,
  receiver/matcher, EventLogCollector PowerShell script
- IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket
  xrefs, applications/configurations browse pages + apply/revert/audit endpoints
- Link-aware analyzer bundles (migration 073) + provider toggle (migration 074):
  link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion
  panels, analyze-bundle endpoint
- Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts
  admin page, reconciler service, resolve endpoints
- Dashboard overhaul: integration-health service + alerts, overview/health endpoints
- Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:13:18 -04:00
378e68ad8a fix(analyzer): reset stale in-flight jobs on worker boot
A container restart leaves analyzer_jobs rows stuck in
fetching/triaging/itglue/analyzing/deep_review forever — the worker's
claimQueuedJob only picks up status='queued', so a job mid-pipeline
when the process died gets orphaned.

resetStaleJobsToQueued() reverts any active-state row whose started_at
is older than 10 min back to 'queued' with started_at=NULL. The worker
calls it once on start() before scheduling the first poll. 10 min is
3x the realistic pipeline ceiling — well past Sonnet+Opus combined.

Logs the count when nonzero so restarts that recover work are visible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:56:00 -04:00
a0a6e7f192 fix(itglue): list flexible assets per type to satisfy API 422 requirement
IT Glue's /flexible_assets endpoint refuses requests without a
filter[flexible-asset-type-id] (returns 422 "Cannot index flexible
assets without providing a flexible asset type ID filter"). The
analyzer's Stage 2 search was caught and tolerated, but never returned
docs.

Added getFlexibleAssetsForOrganization(orgId) on ITGlueClient. It
fetches the type list once per process (memoized), then fans out
per-type fetches with Promise.allSettled so a permission-restricted
type doesn't poison the whole org. Wired into itglue-search and
aggregate-persistence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:48:53 -04:00
bd3401df1c feat(analyzer): Phase 2 — full stage persistence, fingerprints, aggregate reports, cost guards
Eight sub-phases per docs/ticket-analyzer-phase2-spec.md:

2.1 Schema (migration 070): analyzer_stage_executions table; source_snapshot,
    aggregate_fingerprint, fingerprint_generated_at columns on analyzer_analyses.
    model_traces marked LEGACY (kept for back-compat).
2.2 Every pipeline stage records a row to analyzer_stage_executions, success
    or failure. Worker persists a status='failed' analyzer_analyses row when
    the pipeline throws so partial stage records have a parent. Pipeline
    exposes raw triage/sonnet/opus responses for downstream stages.
2.3 Stage 3 prompt updated with markdown formatting rules + banned filler
    phrases. Added react-markdown + remark-gfm + @tailwindcss/typography.
    New <AnalysisMarkdown> component replaces <ProseText>; coerces stray
    headers to bold paragraphs.
2.4 Stage 6 fingerprint (Haiku) runs after persistence, failure-tolerant.
    scripts/backfill-fingerprints.ts reconstructs Stage 6 input from the
    legacy model_traces blob.
2.5 Browse UI rebuild at /analyzer/tickets: multi-select for client/issue/
    queue/status/priority/assignee, sticky filter bar, active-filter chips,
    bulk selection persisted via localStorage, "Analyze N selected" +
    "Generate aggregate report" actions. New <MultiSelect> primitive.
    Staleness uses last_activity_date > completed_at heuristic per spec C.1.
2.6 Aggregate reports (migration 071): runner is fire-and-forget, persists
    SQL distributions immediately so UI shows partial state during the
    Sonnet reduce call. Three endpoints, three pages (/analyzer/reports[/new
    /:id]). IT Glue context fetcher capped at 200 doc titles.
2.7 Cost guards (migration 072): per-request $5 confirmation, soft-warn at
    $20/day, hard-block at $50/day with ANALYZER_DAILY_COST_OVERRIDE_USERS
    override. Every gating decision audited.
2.8 Runbook + build notes updated.

128 vitest tests passing, tsc clean. Migrations 070/071/072 idempotent
(IF NOT EXISTS). model_traces double-write retained — drop in a future
migration once aggregate reports have soaked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:00:22 -04:00
ed3b363d02 feat(analyzer): phase 7 — share-via-email
sendAnalysisShareEmail() reuses the existing nodemailer SMTP transport
(same path as magic-link/invitation mail). Share route persists the
audit row first, then attempts send; on failure returns
{share, emailSent:false, emailError} at HTTP 200 so the audit log
stays intact. Modal surfaces send failures as a warning toast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 11:03:11 -04:00
8f8b5ab7be feat: AI ticket analyzer (phases 1-6)
Multi-stage LLM pipeline that produces structured analyses of Autotask
tickets from local Postgres. Migration 069 + Zod schemas, Stage 0
preprocessor, IT Glue redaction + search, Anthropic SDK wrapper, Stages
1/3/4 (Haiku/Sonnet/Opus), pipeline + cost circuit breaker, job worker
(opt-in autostart), 6 API routes, 3 frontend pages, share-row
persistence (email send deferred to phase 7). 128 vitest tests, tsc
clean. Build journal in docs/wulf-pulse-ticket-analyzer-build-notes.md.

Sync: adds syncTicketNotes() + ticket_notes to ordered/date-filtered
entities so the analyzer's local mirror stays current via scheduler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 10:59:40 -04:00
ea3471d38d feat: Veeam RPO analysis, comparison, ticket analysis + company teams table
- Add Veeam RPO analysis page (/veeam-analysis) and comparison page (/veeam-comparison)
- Add API routes: /api/veeam/rpo-analyze, rpo-comparison, rpo-offline-log, ticket-analysis
- Add veeam-rpo-service.ts enhancements (RPO logic, offline detection, comparison)
- Add veeam-analysis-state.ts and rmm-device-resolver.ts services
- Add migrations 065-068: company_teams, veeam_rpo_offline_log, rpo_comparison_tables, veeam_ticket_analysis
- Add backup-status page updates and nav links for new Veeam pages
- Add scripts: deactivate-cis-for-inactive-companies, workstation category updates
- Add docs: mimecast-api-guide, veeam-backup-alerting-recommendation, workstation-backup-overview, ticket-analyzer-prompt
- Minor: webhook-service, entity-sync, entity-mapper, sync-helpers, sync.ts, middleware.ts updates
2026-04-29 09:16:46 -04:00
07067bef19 feat: Display Settings UI + Company Category/Type sync
- Add /admin/display-settings page with Kiosk and Mobile sections
- Company category checkbox filter + excluded companies searchable multi-select
- New DB tables: company_categories, company_types (migration 064)
- Sync COMPANY_CATEGORIES via CompanyCategories entity (id/name/isActive)
- Sync COMPANY_TYPES via Companies.companyType picklist
- Add to EntityType, ENTITY_DEPENDENCIES, sync-helpers, entity-mapper, entity-sync
- New API routes: /api/admin/display-settings (GET/POST), /api/data/company-categories, /api/data/companies-list
- Update all 4 routes (kiosk/stats, kiosk/activity, mobile/tickets, mobile/dashboard)
  to filter by kiosk_settings company_category_ids + excluded_company_ids
- Add Display Settings nav link (SlidersHorizontal icon) to Admin menu
- Seed kiosk_settings: kiosk_company_category_ids=1, mobile_company_category_ids=1
2026-04-06 09:03:19 -04:00
0e8eb4871e fix: prevent sync from nullifying assigned_resource_id on tickets
- bulkUpsert now accepts preserveExistingOnNull column list, using
  COALESCE(EXCLUDED.col, table.col) so null incoming values never
  overwrite existing non-null DB values
- bulkUpsertRecords passes resource ID columns as preserve-on-null
  for TICKETS and TASKS entities
- getValidResourceIds now throws on DB error instead of returning
  empty set (which would nullify every resource reference)
- Fix mimecast mailbox-remediate fetch handlers to check res.ok and
  content-type before calling res.json(), preventing JSON parse crash
  on 502 Bad Gateway responses
2026-04-05 08:55:41 -04:00
04a720f0d0 fix: use KQL $search with ConsistencyLevel:eventual for mailbox search (Graph $filter unsupported for nested from/emailAddress/address) 2026-04-01 10:14:16 -04:00
bc3904de4e feat: mailbox remediation via Graph Mail.ReadWrite — search + move to Deleted Items from analysis dialog
- Add searchMailboxMessages, deleteMailboxMessage, moveToDeletedItems to MsGraphClient
- POST /api/mimecast/mailbox-remediate: search, move, delete actions with permission error handling
- DeliveredAnalysisDialog: Remove from mailbox panel with search → confirm → delete flow
  - Shows matching messages in mailbox with checkboxes, received time, read/unread status
  - Moves selected to Deleted Items (recoverable) via Graph API
  - Surfaces clear permission guidance if Mail.ReadWrite not yet granted
2026-04-01 10:09:38 -04:00
8e28062d85 feat: add Delivered Mail tab with message-finder search, spam scoring, analysis dialog
- New POST /api/mimecast/delivered route using message-finder/search API
- DeliveredMailTab: search by recipient, sender, subject, time range (6h–7d)
- Results table with status badge, spam score, row tinting for high/moderate risk
- Summary stats bar: total / high spam (≥10) / moderate (5-9) / clean counts
- Sort by date or spam score; filter by status (accepted/held/rejected/bounced)
- DeliveredAnalysisDialog: explains why high-score mail got through, envelope mismatch detection, actionable remediation steps (block domain, adjust policy threshold, report)
- MimecastDeliveredMessage interface + searchDeliveredMessages() method in client
2026-04-01 08:17:18 -04:00
9952365df1 feat: held mail analysis dialog + fix release check
- Add analyzeMessage() — context-aware explanations for DMARC/impersonation/spam/auth-code/malware holds
- Add MessageAnalysisDialog with severity icon, message details, explanation, and resolution options
- Analyze button per row opens the dialog; Release button inside dialog triggers release + closes
- Fix releaseHeldMessage(): treat HTTP 200 + empty fail[] as success (not release===true check)
- Remove action:'release' from payload (API doesn't need it)
2026-04-01 07:18:52 -04:00
a15946daf8 feat: held mail release button + per-tenant fetch
- Add releaseHeldMessage() to MimecastClient (POST /api/gateway/hold-release)
- Add POST /api/mimecast/held/release route
- HeldMailTab: tenant selector before load (defaults to Wulf), only fetches selected tenant
- Release button per row with spinner + optimistic removal on success
- Error shown inline under Release button if release fails
2026-04-01 07:09:42 -04:00
a18d5b66bf fix: don't send x-mc-account header for tenant-specific credentials
getMimecastClientForTenant now always uses accountCode='' so the
x-mc-account header is never sent. Sending your own account code
with tenant-specific OAuth credentials causes Mimecast to 403
(it interprets it as an invalid impersonation attempt).
2026-04-01 06:57:40 -04:00
2f88be9ab3 fix: held mail route force-dynamic, 403 fallback, favicon 404s, error display
- export const dynamic = 'force-dynamic' on /api/mimecast/held to prevent Next.js caching
- Add AbortController timeout (20s) per request in MimecastClient.request()
- getHeldMessages: 403 fallback without admin:true flag for tenants lacking permission
- Reduce maxMessages default to 100 (10 pages) to stay within route timeout
- Show 'permission denied' tooltip in tenant badge for 403 errors
- Surface HTTP errors in HeldMailTab instead of silently failing
- Add missing favicons: sentinelone.ico, itglue.ico, mimecast.ico
2026-04-01 06:52:58 -04:00
fcdec8e38b feat: Mimecast multi-tenant held mail viewer
- migration 062: mimecast_tenants table (company_id, client_id/secret, account_code)
- Seed Wulf (CUSA13A95) + Seubert (CUSA96A181) tenants
- MimecastClient.getHeldMessages(): full pagination via meta.pagination.next cursor
  (API always returns 10/page regardless of pageSize param, totalCount in meta)
- getMimecastClientForTenant() factory for per-tenant instantiation
- GET /api/mimecast/held?tenantId=&recipient= — fetches all tenants in parallel,
  merges + sorts by date, returns per-tenant counts + combined messages[]
- Held Mail tab on /admin/sync/mimecast (on-demand load, recipient filter,
  tenant badges, policy filter dropdown, DMARC/impersonation highlighted red)
2026-03-31 22:38:22 -04:00
e3aba93857 feat: Duo Security integration — full data sync from Accounts + Admin API
Duo API Client (lib/services/duo-client.ts):
- HMAC-SHA1 request signing, GET/POST, automatic pagination
- Rate-limit handling (429 + Retry-After), configurable timeout
- Accounts API: listAccounts() via POST /accounts/v1/account/list
- Admin API: getUsers, getPhones, getGroups, getIntegrations, getAuthLogs
- Child account access: parent creds signed against child api_hostname + account_id
- Factory helpers: getDuoAccountsClient(), getDuoAdminClient()

Database (migration 058):
- 6 tables: duo_accounts, duo_users, duo_phones, duo_auth_logs, duo_groups, duo_integrations
- All with proper FKs, indexes, JSONB fields for capabilities/location/groups

Sync Service (lib/services/duo-sync-service.ts):
- syncAll() orchestration, per-child sequential sync, incremental auth logs
- Company matching: exact then case-insensitive containment (30/32 = 94% matched)
- Non-blocking with sync ID tracking

API Routes:
- POST/GET /api/duo/sync — trigger sync / check status
- GET /api/duo/accounts — list all accounts with stats + matched company
- GET /api/duo/accounts/[id]/users — users for a specific account
- POST /api/openclaw/sync/duo — OpenClaw trigger with API key auth

Verified data: 33 accounts, 832 users, 925 phones, 5927 auth logs, 46 groups, 78 integrations

Also: entity-mapper company fields update, task list marked complete
2026-03-27 11:10:30 -04:00
a4242b81be feat: Duo Security integration — full data sync from Accounts + Admin API
Duo API Client (lib/services/duo-client.ts):
- HMAC-SHA1 request signing, GET/POST, automatic pagination
- Rate-limit handling (429 + Retry-After), configurable timeout
- Accounts API: listAccounts() via POST /accounts/v1/account/list
- Admin API: getUsers, getPhones, getGroups, getIntegrations, getAuthLogs
- Child account access: parent creds signed against child api_hostname + account_id
- Factory helpers: getDuoAccountsClient(), getDuoAdminClient()

Database (migration 058):
- 6 tables: duo_accounts, duo_users, duo_phones, duo_auth_logs, duo_groups, duo_integrations
- All with proper FKs, indexes, JSONB fields for capabilities/location/groups

Sync Service (lib/services/duo-sync-service.ts):
- syncAll(): accounts → per-child data + auth logs → parent account → company matching
- Sequential child processing to respect rate limits
- Incremental auth logs (mintime = last synced timestamp, default 30 days)
- Company matching: exact → case-insensitive containment (30/32 = 94% matched)
- Non-blocking with sync ID tracking

API Routes:
- POST/GET /api/duo/sync — trigger sync / check status
- GET /api/duo/accounts — list all accounts with stats + matched company
- GET /api/duo/accounts/[id]/users — users for a specific account
- POST /api/openclaw/sync/duo — OpenClaw trigger with API key auth

Results: 33 accounts, 832 users, 925 phones, 5927 auth logs, 46 groups, 78 integrations
2026-03-27 09:18:04 -04:00
9a448d111c feat: contacts sync — add all missing fields + UDFs
Migration 057:
- ADD user_defined_fields JSONB (GIN indexed)
- ADD middle_initial, note, external_id, country_id, company_location_id
- ADD create_date, impersonator_creator_resource_id
- ADD is_opted_out_from_bulk_email, bulk_email_opt_out_time
- ADD solicitation_opt_out_time, survey_opt_out
- ADD receives_email_notifications, billing_contact

entity-mapper.ts mapContact():
- Fix broken snake_case field refs → correct camelCase API names:
  alternate_phone → alternatePhone, mobile_phone → mobilePhone
  name_prefix/suffix → namePrefix/nameSuffix
  facebook/twitter/linkedin_url → facebookUrl/twitterUrl/linkedInUrl
  primary_contact → primaryContact, solicitation_opt_out → solicitationOptOut
  last_activity/modified_date → lastActivityDate/lastModifiedDate
  api_vendor_id → apiVendorID, is_active → isActive, is_deleted → isDeleted
- Add UDF conversion: userDefinedFields[] → JSONB {name: value}
- 17 UDFs stored: UserID, Birthday, O365License, VIP User, Department,
  Password, Email Password, Archive Email, User System Profile, etc.

Results: 4234 contacts synced, 3318 with UDFs, 4140 with create_date
2026-03-26 13:16:13 -04:00
dd4cf68def feat: add project_phases entity sync with task project_id backfill
The Autotask Tasks bulk API does not return projectID in its response,
causing all tasks.project_id to be NULL. This fixes it by:

- Adding project_phases as a synced entity (Autotask endpoint: /Phases)
- Migration 059: project_phases table with project_id, phase_number,
  estimated_hours, start/due dates, parent_phase_id, is_scheduled
- EntityType.PROJECT_PHASES added to all sync maps and dependency graph
  (depends on PROJECTS, runs before TASKS in sync order)
- buildProjectPhasesFilter: Phases endpoint requires a filter (id > 0)
- mapProjectPhase: maps Autotask field names to DB columns
- Post-sync backfill in syncEntity: after each project_phases sync,
  UPDATE tasks SET project_id = pp.project_id FROM project_phases pp
  JOIN projects p WHERE tasks.phase_id = pp.id
  Only backfills where the project exists in our DB (FK constraint on
  tasks.project_id; archived projects are skipped gracefully)

Result: 2,455 of 4,966 tasks now have project_id populated. Tasks
belonging to archived/completed projects have phase_id resolvable via
project_phases even when project_id remains NULL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 13:57:33 -04:00
ff9e34cafe feat: add Autotask tags sync (tag groups, tags, ticket tag associations)
- Migration 057: autotask_tag_groups, autotask_tags, and junction tables
  (ticket_tags, company_tags, configuration_item_tags, contact_tags)
- Add TAG_GROUPS and TAGS to EntityType enum and dependency map
- Add mapTagGroup() and mapTag() entity mapper functions
- Add syncTagGroups(), syncTags(), syncTicketTagAssociations() methods
- Wire TicketTagAssociations bulk sync into full/incremental sync flow
- Add 'exist' operator to QueryFilter type
- No FK on ticket_id (tagged tickets may be outside 2yr sync window)

Synced: 26 tag groups, 7299 tags, 10141 ticket-tag associations
2026-03-20 09:22:40 -04:00
c8be128c5c feat: companies sync — add UDFs and fix all field mappings
Migration 056:
- ADD user_defined_fields JSONB (GIN indexed)
- ADD parent_company_id, web_address, create_date, company_category_id
- ADD is_client_portal_active, is_task_fire_active, is_comanaged
- ADD country_id, bill_to_country_id, bill_to_address_to_use
- ADD invoice_email_message_id, purchase_order_template_id
- ADD additional_address_information, sic_code, stock_market, etc.

entity-mapper.ts mapCompany():
- Fix broken snake_case field refs (was data.invoice_method, data.last_activity_date etc.)
- All corrected to actual Autotask camelCase API names (invoiceMethod, lastActivityDate, etc.)
- was data.webSiteURL → data.webAddress
- was data.taxExempt → data.isTaxExempt
- was data.lastTrackedModifiedDateTime (various) → data.lastTrackedModifiedDateTime
- Add UDF conversion: userDefinedFields[] array → JSONB {name: value} object
- 38 UDFs now stored: PassportalID, MimecastID, MSP Service Model, Short Name,
  Workstation Lifecycle Plan, Seats (End Users), CSProfileUID, UniFiID, etc.
2026-03-17 23:56:40 -04:00
5be7b22953 feat: OpenClaw external agent sync API at /api/openclaw/*
- API key auth via x-openclaw-key header (OPENCLAW_API_KEY env var)
- GET  /api/openclaw/sync/status
- POST /api/openclaw/sync/autotask/incremental
- POST /api/openclaw/sync/autotask/full
- POST /api/openclaw/sync/autotask/entity   { entities: [...] }
- POST /api/openclaw/sync/datto-rmm          { syncType: full|incremental }
- POST /api/openclaw/sync/sentinelone
- POST /api/openclaw/sync/veeam              { syncType: full|incremental }
- POST /api/openclaw/sync/zoom
- POST /api/openclaw/sync/engagement
- POST /api/openclaw/sync/qbo                { syncType: full|incremental }
- POST /api/openclaw/sync/zabbix
- POST /api/openclaw/sync/itglue
All routes bypass Better Auth middleware, delegate to existing sync services
2026-03-17 23:26:44 -04:00
7ce06803dc fix: time entries incremental sync uses lastModifiedDateTime not dateWorked
Previously the incremental filter compared dateWorked >= lastSyncTime,
meaning edits to existing entries (hours adjusted, notes changed, PTO
blocks entered in advance) were never picked up after initial ingestion.

Switch both the lastModifiedField mapping and buildTimeEntriesFilter to
use lastModifiedDateTime so any created or modified entry is captured
on the next scheduled sync.
2026-03-17 23:01:18 -04:00
b6c2681a69 fix: Mimecast 7-day chunk windows, 30-day retention limit, two-pass from+to domain filter 2026-03-17 16:53:46 -04:00
22bea85648 fix: Mimecast two-pass sync (from+to wulfconsulting.com), correct filter handling 2026-03-17 16:44:01 -04:00
c1479511bf fix: Mimecast correct credentials, x-mc-account header, no-ms date format, accountCode in body 2026-03-17 16:38:13 -04:00
25bb70cfa6 feat: Mimecast email integration — message logs, threat events, 120d retention, admin UI 2026-03-17 16:23:47 -04:00
cb293ea843 fix: configuration_items sync — correct camelCase field mapping, store all UDFs in udfs JSONB
- Fix mapConfigurationItem() to use Autotask camelCase field names (was using
  snake_case keys that never matched, leaving costs/Datto/RMM fields empty)
- Map all 60+ fields explicitly: costs, Datto device fields, RMM audit fields,
  location, vendor, SNMP, backup dates, source cost, etc.
- Store entire userDefinedFields array as udfs JSONB (keyed by UDF name)
- Retain backup_type_udf VARCHAR column for backward compat
- Migration 052: add udfs JSONB column + GIN index to configuration_items
2026-03-17 15:39:42 -04:00
b98c67482a feat: QuickBooks Online integration
- Add QBO OAuth2 client with token refresh (lib/services/qbo-client.ts)
- Add QBO sync service for invoices, payments, deposits, purchases, journal entries, reports (lib/services/qbo-sync-service.ts)
- Add QBO types (lib/types/qbo.ts)
- Add API routes: /api/qbo/auth, /api/qbo/sync, /api/qbo/disconnect
- Add /admin/qbo status and sync management page
- Add legal pages: /legal/eula, /legal/privacy (Intuit app assessment)
- Add QBO nav link under Admin
- Fix reports: remove invalid summarize_column_by, add accounting_method from Preferences API, add showrows=all&showcols=all
- Add CashFlow report type alongside P&L and BalanceSheet
- Add NoReportData check to skip empty report months
- Add intuit_tid capture in error messages
- Add redirect: follow for cluster routing
- Migration 051: qbo_tokens, qbo_invoices, qbo_payments, qbo_deposits, qbo_transactions, qbo_reports tables

Also includes earlier work:
- Ping flap suppression pipeline step
- Ticket digest reports with LLM analysis
- Zabbix WAN monitor and gap analysis
- Kiosk is_deleted filter fixes
- Datto RMM ping target enrichment
- Entity sync soft-delete detection
2026-03-17 07:39:55 -04:00
c518eefdb2 feat: Morning NOC Summary adaptive card for Teams
- Add MorningSummaryService with Zabbix aggregation and adaptive card builder
- Add webhook delivery system with Teams incoming webhooks
- Add admin UI at /admin/morning-summary for webhook/config management
- Add API routes: /send, /test, /webhooks, /webhooks/[id], /config, /history
- Register morning-summary cron job in SyncScheduler (Mon-Fri 6:30 AM)
- Add outages_only filter (Unavailable triggers only)
- Fix host resolution: use getTriggerEnabledHosts to exclude disabled hosts
- Fix resolved events: event.get value:1 scoped to window with r_eventid filter
- Remove emojis from fact rows and section headers in card
- Remove Open Zabbix button (duplicate of View Problems)
- Add migrations: morning_summary_config + morning_summaries tables
- Add outages_only column to morning_summary_config
2026-03-11 09:34:51 -04:00
19605f82aa feat: IT Glue integration, workflow engine, pipelines, Zabbix WAN, notification channels, backup status UI improvements, nav alignment fixes 2026-02-27 14:52:14 -05:00
ed6c4a8b65 feat: Add SentinelOne integration
- Add SentinelOne API client (lib/services/sentinelone-client.ts)
  - Paginated fetching for sites, agents, threats
  - JWT token auth via S1_API_URL / S1_API_TOKEN env vars

- Add SentinelOne sync service (lib/services/sentinelone-sync-service.ts)
  - Full sync: sites, agents, threats into s1_* tables
  - Sync history tracking with per-entity results

- Add DB migration 038: s1_sites, s1_agents, s1_threats,
  s1_company_mappings, s1_sync_history tables

- Add API routes:
  - POST/GET /api/sentinelone/sync
  - GET/POST/DELETE /api/sentinelone/company-mappings
  - GET /api/sentinelone/coverage (fixed Cartesian product bug)

- Add UI pages:
  - /admin/sync/sentinelone — sync admin with history + stats
  - /sentinelone/coverage — AV coverage report per site
  - /sentinelone/mappings — map S1 sites to Autotask companies

- Wire SentinelOne into admin sync overview card grid
- Add SentinelOne Sync to app navigation
- Fix docker-compose: remove explicit S1 env var entries that
  were overwriting env_file values with empty strings
2026-02-27 05:31:31 -05:00
d7c3dc7168 feat: Autotask webhook integration, TicketNotes, Datto RMM, workflow engine, Veeam agents/alarms, AI triage, misc improvements 2026-02-20 10:28:15 -05:00
5dc7a7e66b feat: Veeam VSPC backup integration - sync, compliance, UI
- Database: 7 Veeam tables + backup_type_udf column on configuration_items
- API Client: VSPC REST API v3 client with pagination, rate limiting, Bearer auth
- Sync Service: full/incremental sync for orgs, servers, repos, jobs, agent jobs, workloads
- Scheduler: veeam-incremental (30min) and veeam-full (daily 2AM) schedules
- Compliance Engine: cross-references Autotask config items vs Veeam workloads
- API Endpoints: backup-status, companies, workloads, jobs, repos, compliance, sync
- UI: Backup Status page with Overview + Contract Compliance tabs
- Navigation: added Backup Status link with HardDrive icon
- Docker: added VEEAM_VSPC_URL and VEEAM_VSPC_API_KEY env vars to compose
2026-02-11 21:04:28 -05:00
f27b23bec8 fix: pre-populate classifications instead of syncing from Autotask API
- Autotask REST API doesn't expose ClassificationIcons endpoint
- Created migration to populate 17 standard classification icons
- Removed sync button from UI (classifications are pre-loaded)
- Includes: Tools Only, Co-Managed, Wulf 365 Essentials, Partner, etc.
- Users can now select any classification to exclude from kiosk
2026-02-03 20:54:35 -05:00
b222d97225 feat: add classification icons sync from Autotask
- Created company_classifications table to store Autotask classification icons
- Added getClassificationIcons() method to AutotaskClient
- Created /api/sync/classifications endpoint (GET/POST)
- Updated kiosk settings UI to dynamically load classifications
- Added 'Sync from Autotask' button to pull latest classifications
- Removed hardcoded classification list
- Display classification name and description in checkboxes
- Allow excluding any classification synced from Autotask
2026-02-03 20:48:08 -05:00
root
1f9ac476b4 fix: correct environment variable name in sync scheduler
The scheduled sync was using AUTOTASK_INTEGRATION_CODE but the actual
environment variable is AUTOTASK_API_INTEGRATION_CODE. This caused
scheduled syncs to fail with 'IntegrationCode is invalid' while
manual syncs worked fine.

Changed:
- AUTOTASK_INTEGRATION_CODE -> AUTOTASK_API_INTEGRATION_CODE

This aligns the scheduler with autotask-factory.ts which was already
using the correct variable name.
2026-02-02 21:13:35 -05:00
root
9f912aed24 feat: add authentication, user management, and admin features
Added comprehensive authentication and authorization system:

Authentication System:
- Better Auth integration with session management
- Login/logout pages and API routes
- Middleware for route protection
- Auth utilities and client libraries

User Management:
- User list, detail, and invite pages
- User API endpoints (CRUD operations)
- Session management for users
- Profile settings page

Role-Based Access Control:
- Role management pages (list, create, edit)
- Permission system with granular controls
- Role assignment to users
- Role API endpoints

Admin Features:
- Audit log page for tracking system events
- Admin settings page
- Audit service for logging user actions

Additional Features:
- Quotes management pages and components
- SalesBldr API integration
- Email service for notifications

Configuration & Documentation:
- Updated docker-compose.yml
- MCP server configuration (mcp.json)
- CVE-2025-55182 security review documentation
- Standards guide and PRD documents
- Re-enabling authentication documentation

Database Migrations:
- 012: Auth tables (users, sessions, accounts, verifications)
- 013: Role tables (roles, permissions, role_permissions, user_roles)
- 014: Admin settings table

UI Updates:
- Updated dashboard layout
- Enhanced app layout with auth integration
2026-01-31 12:43:14 -05:00
root
d8e6931b85 fix: ensure Companies and Resources get filters during incremental sync fallback
Fixed critical logic bug where Companies and Resources were not getting
proper filters applied when falling back from incremental to full sync.

Root Cause:
- During scheduled incremental sync, Companies and Resources correctly
  identify they don't support incremental sync
- However, the filter-building logic was in an else block that only
  executed for non-incremental syncs
- This caused Companies and Resources to sync with NO filters at all
- Autotask API now rejects queries without filters, returning:
  'Value cannot be null. Parameter name: filters'

Fix:
- Restructured logic so filter building happens for BOTH:
  1. Non-incremental (full) syncs
  2. Incremental syncs that fall back to full sync
- Companies and Resources now get active filters applied even during
  scheduled incremental syncs

This resolves the scheduled sync failures for Companies, Resources,
and all other entities that were failing due to the cascading effect
of early failures.
2026-01-30 22:50:07 -05:00
root
8405c813a3 feat: add missing Tickets entity fields from Autotask API
Added 10 critical missing fields to Tickets entity:

Database Migration (016):
- billing_code_id: Billing code assignment
- configuration_item_id: Primary asset/CI
- creator_resource_id: Ticket creator
- creator_type: Creator type (resource/contact)
- problem_ticket_id: Link to problem ticket
- rma_status: RMA status tracking
- rma_type: RMA type classification
- service_level_agreement_paused_next_event_hours: SLA pause tracking
- is_assigned_to_comanaged: Co-managed assignment flag
- is_visible_to_comanaged: Co-managed visibility flag

Entity Mapper Updates:
- Added all new fields with correct camelCase mapping
- Ensures all Autotask Tickets API fields are captured

This completes the Tickets entity to match the full Autotask API
specification and should resolve issues with missing ticket data.
2026-01-26 15:26:34 -05:00