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>
- 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
- Header always visible: ticket icon, number, title (truncated), ExternalLink icon
to Autotask, X close button — all in a fixed top bar
- showCloseButton={false} on DialogContent, replaced with custom header buttons
- Modal is now flex-col h-85vh: header shrinks-to-content, body scrolls
- Removed Close/Open in Autotask bottom action bar entirely
- LinkedText component splits on newlines and converts raw URLs to clickable
links showing just the hostname (e.g. 'ct.sendgrid.net' instead of full URL)
- [overflow-wrap:anywhere] on all text content (more aggressive than break-words)
- w-0 flex-1 on timeline item content divs to prevent flex children expanding
past container boundary
- Applied to description, resolution, note bodies, time entry notes
- Merge notes + time entries into single chronological timeline
- Time entries: blue clock icon, hours badge, resource name, summary
- Human notes: green user icon, creator name, title + body
- System notes (noteType 13/91/93/94/99/101): bot icon, dimmed, hidden by default
- 'Show/Hide system notes' toggle with count badge appears only when system notes exist
- Both data sources fetched in parallel on first expand, cached for session
- 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
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.
Serial numbers, hostnames, SNMP strings, RMM identifiers, backup type UDF
all exceed VARCHAR limits in real data. Convert all to TEXT to prevent
future truncation errors during sync.
IP addresses, MACs, and mobile numbers can exceed 50 chars (IPv6, UUIDs).
Widen datto_internal_ip, datto_remote_ip, rmm_device_audit_external_ip_address,
rmm_device_audit_ip_address to VARCHAR(255); mac_address and mobile_number to VARCHAR(100).
- 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
- Formatted tab: grouped sections (Overview, Parties, Classification, Dates, System)
- ID-to-label resolution for status, priority, source, queue, company type
- Elegant bordered table layout with muted label column
- Clickable phone/URL fields, monospace IDs, formatted dates
- Description rendered in its own block for tickets
- Raw tab: all fields with monospace keys, copy-on-hover
- Auto-detects ticket vs company data for appropriate grouping
- Shadcn Tabs, Separator, Badge used throughout
- Removed sm:max-w-lg default that was overriding max-w-7xl in DetailModal
- DialogContent now respects whatever max-w class is passed via className
- Fixes data browser detail modal not widening as expected