- Four-tab detail page (Zones / Records / History / Schedule) following
the veeam/pax8 sync-detail-page shape
- Sync Now trigger with bounded polling and 409/503 handling
- Records tab: zone selector, type/search filters, actions cell gated on
the D-01 writable-type allowlist (NS/SOA render read-only)
- History tab: per-record change ledger with pulse_crud vs
sync_detected_drift source badges (D-06)
- Zone/history detail drill-downs via the existing DetailModal component
- Hand-authored public/logos/route53.svg (globe/DNS glyph, primitive
shapes only, no script/external refs/raster data)
- New INTEGRATIONS entry linking to /admin/sync/route53 (built in
plan 24-07), color: orange (already used by datto-rmm)
- Company table with search + type filter (cloned from client-scope pattern)
- Three independent Switch toggles per row: auto-parse, auto-classify, auto-report
- toggle() PATCHes /api/admin/phishing-automation/{companyId} with all three current flags
- Helper caption clarifies stage dependency (informational, not enforced)
- Append PAX8 entry to INTEGRATIONS with logo, description, and detail link
- Fetch /api/pax8/sync in fetchAll, wire pax8 branches in getSummary/getStatusIcon
- Add PAX8 stats block (last sync, companies, subscriptions) to card render
- Add public/logos/pax8.ico as placeholder (copied from itglue.ico — no network
access available to fetch the real PAX8 favicon; replace with the real logo
when convenient)
- New app/admin/sync/pax8/page.tsx mirroring sentinelone/duo pattern
- Polls GET /api/pax8/sync every 10s, shows companies/subscriptions/products stats
- Sync Now button POSTs with triggeredBy, handles 403/409 via sonner toast, polls until complete
- GET/POST /api/admin/notify-event-keys: list ordered by sort_order/key, create with key regex validation (^[a-z][a-z0-9_]*$/i), 409 on conflict
- PUT/DELETE /api/admin/notify-event-keys/[key]: update via COALESCE, hard delete with 404 guard
- app/admin/workflow/event-keys/page.tsx: list with inline edit, Switch for is_active toggle, + New event key form, sonner toasts
- All routes gated by requireAdmin()
- GET /api/notification-channels: requireAuth(), admin sees all rows with owner_email JOIN, non-admin sees global-only
- GET accepts ?owner=global|personal|all filter parameter
- POST /api/notification-channels: requireAdmin(); preserves all four channel_type values (teams/telegram/ntfy/webhook); adds owner_user_id column
- [id] routes: requireAuth() + per-row authorization (isAdmin || isOwner); global rows require admin
- Admin channels page: Owner badge (Global vs Personal: email), Show filter select, disclaimer text for personal channels
- zabbix-wan, rmm-overshell, itglue-writes, ticket-digest,
device-link-conflicts, workflow/history, workflow/pipelines/[id]:
each gets useUserTimezone() at the component entry; threads tz into
every inline toLocaleString call.
Migrates 11 of 81 audit leak callsites.
Adds company-level opt-out scoping so white-label / subcontract clients
(TTG, LEC, PER, VCF, Trivium Packaging, TNT Pizza, etc.) can be excluded
from Wulf's own dashboard KPIs and ticket analytics without affecting
per-company drill-down views.
- migration 082: company_scope table (opt-out; absent row = in scope)
- GET/PATCH /api/admin/company-scope[/companyId] — list + upsert
- /admin/client-scope — searchable company list with Switch per row,
type filter, and in/out scope filter; excluded rows are dimmed
- dashboard overview KPIs now exclude out-of-scope company tickets
- analyzer /tickets query excludes out-of-scope when no specific
client is selected (explicit per-company selection still works)
- "Client Scope" tile added to admin Tools & Data section
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bucket the integration list under category headers (PSA, RMM,
Documentation, Security, …) following a canonical order, with
unrecognized categories appended after. Rows within a bucket sort
alphabetically by name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The reconciler can emit the same configuration_item id multiple times
when more than one match rule fires (e.g. exact_serial AND
hostname_in_company). The page rendered each occurrence as a separate
row keyed on ciId, producing React duplicate-key errors.
Dedupe client-side keeping the strongest confidence (exact_serial > mac
> hostname_in_company > other) and use the deduped count in the badge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ons after the ⌘K palette:
StatusIndicator → Popover
- The top-bar status light is no longer a direct link to /status.
Clicking it opens a popover with grouped issues (failing
integrations, expired tokens, expiring tokens) so a quick glance
answers "what's broken" without leaving the current page. A "View
full status" link at the bottom routes to /status when needed.
- The trigger keeps the same color rollup so the visual hint is
visible without opening the popover.
DataTable → CSV export
- Optional `exportable` + `exportFilename` props add an "Export CSV"
button next to the search bar. Default behavior exports the current
page; pass `onExportAll` for server-side full-result downloads.
- Built client-side from column defs (label → header, raw value →
cell). BOM-prefixed UTF-8 so Excel decodes correctly. Quoting +
escape handled.
- Enabled on /admin/data-browser/{companies,tickets} as initial demos.
Other data-browser pages opt in by adding two props.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
- 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
- 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
- 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
- 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
- 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
- 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
- data-browser/companies: resolve classification picklist IDs to labels in
table column and detail modal; also added to DetailModal COMPANY_GROUPS
- DetailModal: add 'classification' FieldType with color-coded badge map
- kiosk stats + activity: switch from label-based exclusion to ID-based
allowlist (included_classifications). Only shows companies with
classification IN (15,16,17,18,203,205,206,207,202,5,12)
= managed service / recurring revenue tiers only
Bypass = security risk (MFA not enforced) — shown in red, expandable panel
Disabled = locked out, no threat — shown in muted gray, separate expandable panel
- Split /api/duo/status counts into bypass and disabled separately
- /api/duo/users/flagged returns { bypass: [], disabled: [] } instead of flat list
- Overview card: only bypass triggers red warning icon (disabled does not)
- Detail page: two separate expandable sections with distinct severity styling
- Both sections include user, email, account name, enrolled status, last login, notes
- Covers all accounts (parent + children)
- Created GET /api/duo/users/flagged — returns users with status bypass or disabled, joined with account name
- Clickable warning banner expands to show full user table
- Table shows: user, email, account, status badge (yellow=bypass, red=disabled), enrolled, last login, notes
- Fixed JOIN: duo_users.duo_account_id is varchar account_id, not integer id
Updated Resources data browser page to display all 40+ fields in the
detail modal including:
- Name fields (prefix, middle initial, suffix)
- All contact information (3 email addresses, home phone)
- Employment details (payroll, accounting, internal cost)
- Location and availability
- System preferences (date/time/number formats)
- Demographics and security (gender, license, security level)
- Survey ratings
Fields are organized into logical groups with comments for clarity.