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)
-
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. -
Q: Where to store webhook URL?
A: Environment variableN8N_SYNC_WEBHOOK_URLinlib/env.mjs, validated with Zod (optional). -
Q: How to get tenant ID?
A: From NextAuth session (session.user.tenantId) viagetUserAuth()in Server Action. -
Q: How to handle button loading state?
A: ReactuseTransition()hook for pending state, disable button during transition. -
Q: Toast notifications for feedback?
A: Existingsonnerlibrary already integrated, usetoast.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:
userstable (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:
- Check authentication → return error if not authenticated
- Extract
tenantIdfrom session → return error if missing - Validate
N8N_SYNC_WEBHOOK_URLexists → return error if not configured - Send POST request to webhook
- 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(fromuseTransition()) - tracks request in progress
UI States:
- Idle: "Sync Policies" with RefreshCw icon
- Loading: "Syncing..." with spinning RefreshCw icon, button disabled
- Success: Returns to idle, shows green toast
- 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()tolib/env.mjsserver schema - Document in
.env.example(if exists) or README
T002: Server Action Implementation
- Import
envfromlib/env.mjsinlib/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.tsxas 'use client' component - Import Button from
@/components/ui/button - Import RefreshCw icon from
lucide-react - Import
triggerPolicySyncaction - Import
toastfromsonner - Implement
useTransition()for pending state - Create
handleSyncfunction calling Server Action - Add toast notifications for success/error
- Render button with conditional icon spin animation
- Disable button when
isPendingis true
T004: Page Integration
- Import
SyncButtoninapp/(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-syncbranch - Feature spec and plan documented
- TODO: Set
N8N_SYNC_WEBHOOK_URLin Dokploy environment variables - TODO: Merge to
mainbranch - TODO: Deploy to production
- TODO: Verify button appears on
/searchpage - TODO: Test manual sync trigger in production
Implementation Notes
Key Decisions
-
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
-
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)
-
Environment variable optional: Made
N8N_SYNC_WEBHOOK_URLoptional (not required) because:- Allows Docker build without secret
- Runtime check provides clear error message
- Follows pattern of other optional env vars
-
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
- Need
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)
-
Happy Path:
- Login → Navigate to /search → Click "Sync Policies" → See "Syncing..." → See success toast
- Verify n8n receives webhook with correct tenant ID
-
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
-
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
- No sync progress: User only gets confirmation sync started, not when it completes
- No last sync timestamp: UI doesn't show when data was last synced
- No rate limiting: User could theoretically spam the button (mitigated by loading state)
- No tenant-specific sync: Always syncs all policies, can't select specific ones
- No webhook retry: If webhook fails, user must click again (no automatic retry)
Next Steps (Post-MVP)
- Add sync history: Track when syncs were triggered in database
- Show last sync time: Display on search page "Last synced: 5 minutes ago"
- Add rate limiting: Prevent syncs more frequently than once per 5 minutes
- Add sync status polling: Show progress while Intune data is being fetched
- Add sync notifications: Email/toast when sync completes (async)
References
- Feature Spec: spec.md
- Requirements Checklist: checklists/requirements.md
- Similar Feature: specs/001-global-policy-search/
- Constitution: .specify/memory/constitution.md