tenantpilot/specs/002-manual-policy-sync/plan.md
Ahmed Darrazi bd7758191e
All checks were successful
Trigger Cloudarix Deploy / call-webhook (push) Successful in 1s
Switch to development branch and update deployment workflow
2025-12-06 20:42:10 +01:00

13 KiB

Implementation Plan: Manual Policy Sync Button

Branch: 002-manual-policy-sync | Date: 2025-12-06 | Spec: spec.md
Status: Implementation Complete (Documenting for record)

Summary

Add a "Sync Policies" button to the global search page (/search) that allows admins to trigger immediate policy synchronization from Microsoft Intune via n8n webhook. This eliminates the need to wait for scheduled nightly syncs when policy changes need to be reflected immediately in TenantPilot.

Technical Approach: Implement as a client component with Server Action, following TenantPilot's server-first architecture. Use existing authentication system to extract tenant ID and send POST request to n8n webhook endpoint configured via environment variable.

Technical Context

Language/Version: TypeScript 5.x strict mode
Primary Dependencies: Next.js 16.0.3 App Router, Drizzle ORM 0.44.7, Shadcn UI, NextAuth.js 4.24.13
Storage: PostgreSQL (no schema changes required)
Testing: Manual testing (no E2E tests for this feature)
Target Platform: Docker containers (standalone build), web browsers
Project Type: Web application (Next.js App Router)
Performance Goals: Webhook request < 2s, UI feedback < 1s
Constraints: Server-first architecture (Server Actions only), Azure AD multi-tenant auth
Scale/Scope: Multi-tenant SaaS, each tenant triggers sync independently

Constitution Check

GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.

  • Uses Next.js App Router with Server Actions (no client-side fetches)
  • TypeScript strict mode enabled (all types properly defined)
  • Drizzle ORM for all database operations (no DB changes needed)
  • Shadcn UI for all new components (Button from shadcn/ui)
  • Azure AD multi-tenant authentication (tenant ID from session)
  • Docker deployment with standalone build (no special config needed)

Constitution Compliance: All requirements met

Project Structure

Documentation (this feature)

specs/002-manual-policy-sync/
├── spec.md              # Feature specification (completed)
├── plan.md              # This file (implementation plan)
├── checklists/
│   └── requirements.md  # Specification quality checklist (passed)
└── [No research.md]     # Not needed - straightforward implementation
└── [No data-model.md]   # Not needed - no schema changes
└── [No contracts/]      # Not needed - uses existing n8n webhook
└── [No quickstart.md]   # Not needed - single feature, no complex setup

Source Code (repository root)

lib/
├── env.mjs                          # [MODIFIED] Added N8N_SYNC_WEBHOOK_URL
├── actions/
│   └── policySettings.ts            # [MODIFIED] Added triggerPolicySync()
└── auth/
    └── utils.ts                     # [MODIFIED] Added tenant_id DB update in JWT callback

components/
└── search/
    ├── SearchInput.tsx              # [EXISTING] Search component
    ├── ResultsTable.tsx             # [EXISTING] Results display
    ├── EmptyState.tsx               # [EXISTING] No results state
    └── SyncButton.tsx               # [NEW] Sync button component

app/
└── (app)/
    └── search/
        └── page.tsx                 # [MODIFIED] Integrated SyncButton

specs/
└── 002-manual-policy-sync/         # [NEW] This feature spec directory

Structure Decision: Single Next.js project with App Router. Feature follows existing patterns from 001-global-policy-search. No new directories created - components added to existing components/search/ structure.

Phase 0: Research & Unknowns Resolution

Status: Complete (implementation straightforward, no research needed)

Research Questions (All Resolved)

  1. Q: How to trigger n8n workflow from Next.js?
    A: POST request to webhook URL with JSON payload. n8n webhook accepts any POST with tenant ID.

  2. Q: Where to store webhook URL?
    A: Environment variable N8N_SYNC_WEBHOOK_URL in lib/env.mjs, validated with Zod (optional).

  3. Q: How to get tenant ID?
    A: From NextAuth session (session.user.tenantId) via getUserAuth() in Server Action.

  4. Q: How to handle button loading state?
    A: React useTransition() hook for pending state, disable button during transition.

  5. Q: Toast notifications for feedback?
    A: Existing sonner library already integrated, use toast.success() / toast.error().

No external research needed - all patterns exist in codebase (Server Actions, auth, env vars, Shadcn UI).

Phase 1: Design & Contracts

Status: Complete (no schema changes, webhook contract is simple)

Data Model

No database changes required. Feature uses existing:

  • users table (tenant_id column already added in previous feature)
  • NextAuth session management (tenant ID already in JWT token)

API Contracts

Server Action: triggerPolicySync()

File: lib/actions/policySettings.ts

export async function triggerPolicySync(): Promise<{
  success: boolean;
  message?: string;
  error?: string;
}>;

Security:

  • Validates user session via getUserAuth()
  • Extracts tenant ID from session (no user input)
  • Server-side only (Server Action)

Flow:

  1. Check authentication → return error if not authenticated
  2. Extract tenantId from session → return error if missing
  3. Validate N8N_SYNC_WEBHOOK_URL exists → return error if not configured
  4. Send POST request to webhook
  5. Return success/error based on response

n8n Webhook Contract (External)

Endpoint: process.env.N8N_SYNC_WEBHOOK_URL (configured in Dokploy)
Method: POST
Headers: Content-Type: application/json
Body:

{
  "tenantId": "00000000-0000-0000-0000-000000000000",
  "source": "manual_trigger",
  "triggeredAt": "2025-12-06T12:00:00.000Z"
}

Response:

  • Success: HTTP 200-299 (body irrelevant)
  • Failure: HTTP 400+ (triggers error toast)

Component Design

SyncButton Component

File: components/search/SyncButton.tsx

Props: None (uses Server Action directly)

State:

  • isPending (from useTransition()) - tracks request in progress

UI States:

  1. Idle: "Sync Policies" with RefreshCw icon
  2. Loading: "Syncing..." with spinning RefreshCw icon, button disabled
  3. Success: Returns to idle, shows green toast
  4. Error: Returns to idle, shows red toast

Styling: Shadcn Button component with default variant

Page Integration

File: app/(app)/search/page.tsx

Change: Add <SyncButton /> in CardHeader, positioned right of title/description using flexbox.

<CardHeader>
  <div className="flex items-center justify-between">
    <div>
      <CardTitle>Global Policy Search</CardTitle>
      <CardDescription>...</CardDescription>
    </div>
    <SyncButton />
  </div>
</CardHeader>

Phase 2: Implementation Tasks

Status: All tasks completed

Task Breakdown (Already Implemented)

T001: Environment Variable Setup

  • Add N8N_SYNC_WEBHOOK_URL: z.string().optional() to lib/env.mjs server schema
  • Document in .env.example (if exists) or README

T002: Server Action Implementation

  • Import env from lib/env.mjs in lib/actions/policySettings.ts
  • Create triggerPolicySync() async function with 'use server'
  • Add authentication check with getUserAuth()
  • Extract tenant ID from session with error handling
  • Validate webhook URL exists
  • Implement fetch POST request with try/catch
  • Return typed success/error response

T003: SyncButton Component

  • Create components/search/SyncButton.tsx as 'use client' component
  • Import Button from @/components/ui/button
  • Import RefreshCw icon from lucide-react
  • Import triggerPolicySync action
  • Import toast from sonner
  • Implement useTransition() for pending state
  • Create handleSync function calling Server Action
  • Add toast notifications for success/error
  • Render button with conditional icon spin animation
  • Disable button when isPending is true

T004: Page Integration

  • Import SyncButton in app/(app)/search/page.tsx
  • Modify CardHeader to use flexbox layout
  • Position button right of title/description
  • Verify responsive layout on mobile

T005: Testing & Validation

  • Test button click without webhook URL configured → error toast
  • Test button click when not authenticated → error toast
  • Test button click with valid webhook → success toast
  • Test rapid button clicks → only one request sent
  • Test network error simulation → error toast
  • Verify tenant ID extracted correctly from session

T006: Documentation

  • Update feature spec with actual implementation
  • Document N8N_SYNC_WEBHOOK_URL in environment variables
  • Add deployment notes for Dokploy configuration
  • Create this implementation plan

Deployment Checklist

  • Code committed to 002-manual-policy-sync branch
  • Feature spec and plan documented
  • TODO: Set N8N_SYNC_WEBHOOK_URL in Dokploy environment variables
  • TODO: Merge to main branch
  • TODO: Deploy to production
  • TODO: Verify button appears on /search page
  • TODO: Test manual sync trigger in production

Implementation Notes

Key Decisions

  1. No rate limiting: Decided against implementing rate limiting (e.g., 1 sync per 5 min) because:

    • n8n can handle duplicate requests
    • User feedback (loading state) discourages rapid clicks
    • Can add later if abuse occurs
  2. No sync status tracking: Feature only confirms sync was triggered, doesn't show progress because:

    • Intune API sync is async and can take minutes
    • Would require polling or WebSocket complexity
    • Out of scope for MVP (marked in spec)
  3. Environment variable optional: Made N8N_SYNC_WEBHOOK_URL optional (not required) because:

    • Allows Docker build without secret
    • Runtime check provides clear error message
    • Follows pattern of other optional env vars
  4. Client component for button: Had to use 'use client' despite server-first principle because:

    • Need useTransition() for loading state
    • Toast notifications run client-side
    • Server Action called from client follows Next.js patterns

Security Considerations

  • Tenant ID extracted from authenticated session (not user input)
  • Server Action validates auth before any operations
  • Webhook URL not exposed to client (server-side env var)
  • No CSRF risk (Server Action has built-in protection)
  • No SQL injection risk (no database writes)

Performance Considerations

  • Webhook request non-blocking (doesn't wait for Intune sync)
  • UI feedback immediate (< 1s to show loading state)
  • No database queries (reads session from NextAuth)
  • Button disabled prevents duplicate requests
  • Minimal bundle size (lucide-react tree-shaken, sonner already loaded)

Testing Strategy

Manual Testing (Completed)

  1. Happy Path:

    • Login → Navigate to /search → Click "Sync Policies" → See "Syncing..." → See success toast
    • Verify n8n receives webhook with correct tenant ID
  2. Error Cases:

    • Not authenticated → Error toast
    • No tenant ID in session → Error toast
    • Webhook URL not configured → Error toast
    • Network error → Error toast
    • Webhook returns 500 → Error toast
  3. UI/UX:

    • Button shows loading spinner
    • Button disabled during sync
    • Rapid clicks don't send multiple requests
    • Mobile responsive layout

Future Testing (Out of Scope for MVP)

  • E2E test with Playwright
  • Unit test for Server Action
  • Integration test with mock n8n webhook
  • Load test for concurrent sync requests

Known Limitations

  1. No sync progress: User only gets confirmation sync started, not when it completes
  2. No last sync timestamp: UI doesn't show when data was last synced
  3. No rate limiting: User could theoretically spam the button (mitigated by loading state)
  4. No tenant-specific sync: Always syncs all policies, can't select specific ones
  5. No webhook retry: If webhook fails, user must click again (no automatic retry)

Next Steps (Post-MVP)

  1. Add sync history: Track when syncs were triggered in database
  2. Show last sync time: Display on search page "Last synced: 5 minutes ago"
  3. Add rate limiting: Prevent syncs more frequently than once per 5 minutes
  4. Add sync status polling: Show progress while Intune data is being fetched
  5. Add sync notifications: Email/toast when sync completes (async)

References