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>
- 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
The hardcoded UDF filter (MSP Service Model = 'Wulf Managed') matched
only 1 company, making all mobile ticket views empty. Now reads
included_classifications from kiosk_settings (same as kiosk) which
correctly identifies all managed clients by classification ID.
- 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
- AppNavigation returns null on /mobile/* (no more horizontal scroll)
- Mobile header: 'Pulse' title (links home) + Menu icon (links to /mobile/nav)
- /mobile/nav: full-screen nav page with touch-friendly cards
- Mobile Views: Dashboard, Tickets, Finance (large icon cards)
- Full Site: Quotes, Config Items, Backup, Engagement, Ticket Digest, Admin
- Sign out button
- Bottom tab bar unchanged (Dashboard / Tickets / Finance)
- DB: inserted qbo-sync-2am (0 2 * * *) and qbo-sync-4pm (0 16 * * *) schedules
- Mobile finance: 'Sync QBO' button triggers POST /api/qbo/sync incremental,
polls /api/qbo/sync GET until lastSync timestamp changes (max 90s),
then reloads finance data
- Shows last sync timestamp below page title
- Separate refresh-only button (↻) for quick display refresh without re-syncing
- Sync status message shown during polling
- New GET /api/tickets/[id]/notes: fetches TicketNotes from Autotask,
enriches with creator resource names, sorted newest first
- Modal: collapsible Notes section (lazy fetch on expand, shows count badge)
- Modal: collapsible Time Entries section (lazy fetch, count + total hours in header)
- Both sections fetch once and cache for the modal session
- Ticket [id] route: resolve statusLabel/priorityLabel via getTicketStatusPicklist()
and getTicketPriorityPicklist() with module-level cache (fetched once per deploy)
- Modal: remove subtitle, move ticket# and title into header, compact single-row
metadata (status badge + priority badge + assigned resource), dates in 2-col grid,
smaller buttons
- Replace non-existent 'quotes' DB table lookup with live SalesBldr API call
- Fix RMM total: was checking 'rmm_sites' (wrong), now uses 'datto_rmm_sites'
- Add SALESBLDR_API_URL/KEY to .env.local (were only in .env, not loaded by container)
GET /api/openclaw/datto-rmm/sites
GET /api/openclaw/datto-rmm/devices (filters: siteUid, online, deleted, page, limit)
GET /api/openclaw/datto-rmm/devices/[uid]
GET /api/openclaw/datto-rmm/devices/[uid]/audit (always live)
GET /api/openclaw/datto-rmm/alerts (filters: resolved, siteUid, deviceUid, limit)
GET /api/openclaw/datto-rmm/alerts/open
- All protected by x-openclaw-key header
- Default: queries Pulse DB (datto_rmm_devices/alerts/sites tables)
- ?live=true: proxies to Datto RMM API via DattoRMMClient
- Responses include source:'db'|'live' for data freshness awareness
All mobile dashboard and ticket list queries now INNER JOIN companies
on user_defined_fields->>'MSP Service Model' = 'Wulf Managed', scoping
all stats (open total, by priority, by queue, SLA, recent activity)
and ticket list to managed clients only. 349 open tickets in scope.
- Replace stripHtml() with renderContent() that:
1. Extracts <a href="...">...</a> anchors before stripping HTML
2. Strips remaining HTML tags cleanly
3. Detects bare https?:// URLs in plain text
4. Renders each as a tappable <a>[link]</a> with text-primary underline
- Collapsed note preview still shows plain text (no link clutter)
- Expanded note/time-entry body shows inline [link] elements
- API: marks note_type 13,91,93,94,99,101 as is_system=true
(13=workflow rule fired, 91=workflow templates, 99=RMM system notes,
93/94=merge/absorb, 101=other system)
- Frontend: system notes filtered out by default; 'Show system' toggle
in timeline header reveals them
- Time entry expanded card uses bg-muted/border-border (dark-mode safe)
instead of hard-coded bg-blue-50 which was unreadable in dark theme
- Priority labels and colors corrected to match actual Autotask values
- Status labels expanded to full set
- API: replaced hardcoded wrong map (1=Critical etc) with JOIN on priorities table
- API: sorted by count DESC instead of priority value
- Frontend: PRIORITY_COLOR/TEXT maps updated to cover all 10 actual Autotask values (1-11)
- Middleware: added /api/mobile to publicRoutes (was getting auth-redirected)
- 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
- Enable Better Auth middleware (was bypassed with return NextResponse.next())
- Re-enable AuthProvider in root layout
- Add public routes for webhooks, sync, kiosk, QBO, legal, health endpoints
- Set Microsoft Entra ID credentials and production Better Auth URLs
- Hide Engagement and Admin nav sections from non-super-admins
- Fix auth DB columns: snake_case → camelCase for Better Auth compatibility
- Add twoFactorEnabled column to user table