From bd7758191e6372e2bc57db7e3f8bfba18593870c Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Sat, 6 Dec 2025 20:42:10 +0100 Subject: [PATCH] Switch to development branch and update deployment workflow --- .github/workflows/deploy.yml | 4 +- specs/002-manual-policy-sync/plan.md | 346 ++++++++++++++++++++++++++ specs/002-manual-policy-sync/tasks.md | 318 +++++++++++++++++++++++ 3 files changed, 666 insertions(+), 2 deletions(-) create mode 100644 specs/002-manual-policy-sync/plan.md create mode 100644 specs/002-manual-policy-sync/tasks.md diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 55eb546..f55e47f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,7 +3,7 @@ name: Trigger Cloudarix Deploy on: push: branches: - - main + - development workflow_dispatch: jobs: @@ -17,5 +17,5 @@ jobs: curl -X POST \ -H "X-Gitea-Event: Push Hook" \ -H "Content-Type: application/json" \ - -d '{"ref": "refs/heads/main"}' \ + -d '{"ref": "refs/heads/development"}' \ https://system.cloudarix.de/api/deploy/ph8pjvF1mWZUrjBDql-eE diff --git a/specs/002-manual-policy-sync/plan.md b/specs/002-manual-policy-sync/plan.md new file mode 100644 index 0000000..bc9cc86 --- /dev/null +++ b/specs/002-manual-policy-sync/plan.md @@ -0,0 +1,346 @@ +# Implementation Plan: Manual Policy Sync Button + +**Branch**: `002-manual-policy-sync` | **Date**: 2025-12-06 | **Spec**: [spec.md](./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.* + +- [x] Uses Next.js App Router with Server Actions (no client-side fetches) +- [x] TypeScript strict mode enabled (all types properly defined) +- [x] Drizzle ORM for all database operations (no DB changes needed) +- [x] Shadcn UI for all new components (Button from shadcn/ui) +- [x] Azure AD multi-tenant authentication (tenant ID from session) +- [x] Docker deployment with standalone build (no special config needed) + +**Constitution Compliance**: ✅ All requirements met + +## Project Structure + +### Documentation (this feature) + +```text +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) + +```text +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` + +```typescript +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**: + +```json +{ + "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 `` in CardHeader, positioned right of title/description using flexbox. + +```tsx + +
+
+ Global Policy Search + ... +
+ +
+
+``` + +## Phase 2: Implementation Tasks + +**Status**: ✅ All tasks completed + +### Task Breakdown (Already Implemented) + +#### T001: Environment Variable Setup +- [x] Add `N8N_SYNC_WEBHOOK_URL: z.string().optional()` to `lib/env.mjs` server schema +- [x] Document in `.env.example` (if exists) or README + +#### T002: Server Action Implementation +- [x] Import `env` from `lib/env.mjs` in `lib/actions/policySettings.ts` +- [x] Create `triggerPolicySync()` async function with 'use server' +- [x] Add authentication check with `getUserAuth()` +- [x] Extract tenant ID from session with error handling +- [x] Validate webhook URL exists +- [x] Implement fetch POST request with try/catch +- [x] Return typed success/error response + +#### T003: SyncButton Component +- [x] Create `components/search/SyncButton.tsx` as 'use client' component +- [x] Import Button from `@/components/ui/button` +- [x] Import RefreshCw icon from `lucide-react` +- [x] Import `triggerPolicySync` action +- [x] Import `toast` from `sonner` +- [x] Implement `useTransition()` for pending state +- [x] Create `handleSync` function calling Server Action +- [x] Add toast notifications for success/error +- [x] Render button with conditional icon spin animation +- [x] Disable button when `isPending` is true + +#### T004: Page Integration +- [x] Import `SyncButton` in `app/(app)/search/page.tsx` +- [x] Modify CardHeader to use flexbox layout +- [x] Position button right of title/description +- [x] Verify responsive layout on mobile + +#### T005: Testing & Validation +- [x] Test button click without webhook URL configured → error toast ✅ +- [x] Test button click when not authenticated → error toast ✅ +- [x] Test button click with valid webhook → success toast ✅ +- [x] Test rapid button clicks → only one request sent ✅ +- [x] Test network error simulation → error toast ✅ +- [x] Verify tenant ID extracted correctly from session ✅ + +#### T006: Documentation +- [x] Update feature spec with actual implementation +- [x] Document N8N_SYNC_WEBHOOK_URL in environment variables +- [x] Add deployment notes for Dokploy configuration +- [x] Create this implementation plan + +## Deployment Checklist + +- [x] Code committed to `002-manual-policy-sync` branch +- [x] 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 + +- Feature Spec: [spec.md](./spec.md) +- Requirements Checklist: [checklists/requirements.md](./checklists/requirements.md) +- Similar Feature: [specs/001-global-policy-search/](../001-global-policy-search/) +- Constitution: [.specify/memory/constitution.md](../../.specify/memory/constitution.md) diff --git a/specs/002-manual-policy-sync/tasks.md b/specs/002-manual-policy-sync/tasks.md new file mode 100644 index 0000000..f1e6bfd --- /dev/null +++ b/specs/002-manual-policy-sync/tasks.md @@ -0,0 +1,318 @@ +--- +description: "Task list for Manual Policy Sync Button feature" +--- + +# Tasks: Manual Policy Sync Button + +**Branch**: `002-manual-policy-sync` +**Input**: [plan.md](./plan.md), [spec.md](./spec.md) +**Status**: ✅ **COMPLETED** (Documenting for record) + +**Tests**: No test tasks included - manual testing only for this feature (as specified in plan.md) + +**Organization**: Tasks organized by user story to enable independent implementation and testing + +## Format: `- [ ] [ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to (US1, US2, US3) +- Paths are absolute from repository root + +--- + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: Configure environment and prepare existing codebase + +- [x] T001 Add N8N_SYNC_WEBHOOK_URL to lib/env.mjs server schema with z.string().optional() +- [x] T002 [P] Verify existing authentication extracts tenantId in lib/auth/utils.ts +- [x] T003 [P] Confirm Shadcn Button and Toast (sonner) are available + +**Status**: ✅ Complete - No new project setup needed, leveraging existing infrastructure + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented + +**⚠️ CRITICAL**: No user story work can begin until this phase is complete + +- [x] T004 Verify NextAuth session includes tenantId field (already implemented in previous feature) +- [x] T005 Verify getUserAuth() server action exists in lib/auth/utils.ts +- [x] T006 Confirm fetch() is available for webhook calls (Node.js built-in) + +**Status**: ✅ Complete - All foundational pieces already exist from 001-global-policy-search feature + +**Checkpoint**: Foundation ready - user story implementation can now begin in parallel + +--- + +## Phase 3: User Story 1 - Trigger Immediate Sync (Priority: P1) 🎯 MVP + +**Goal**: Admin can click button to trigger n8n webhook for immediate policy sync + +**Independent Test**: Login → /search page → Click "Sync Policies" → Success toast appears → n8n receives POST with tenantId + +### Implementation for User Story 1 + +- [x] T007 [P] [US1] Create SyncButton component in components/search/SyncButton.tsx +- [x] T008 [P] [US1] Implement triggerPolicySync() Server Action in lib/actions/policySettings.ts +- [x] T009 [US1] Add authentication check with getUserAuth() in triggerPolicySync() +- [x] T010 [US1] Extract tenantId from session with null check +- [x] T011 [US1] Validate N8N_SYNC_WEBHOOK_URL environment variable exists +- [x] T012 [US1] Implement fetch POST request to webhook with JSON body +- [x] T013 [US1] Add useTransition() hook in SyncButton for loading state +- [x] T014 [US1] Implement handleSync function calling Server Action +- [x] T015 [US1] Add success toast notification "Policy sync triggered successfully" +- [x] T016 [US1] Render Button with RefreshCw icon from lucide-react +- [x] T017 [US1] Integrate SyncButton into app/(app)/search/page.tsx CardHeader +- [x] T018 [US1] Position button right of title using flexbox (justify-between) + +**Files Modified**: +- ✅ `lib/env.mjs` - Added N8N_SYNC_WEBHOOK_URL +- ✅ `lib/actions/policySettings.ts` - Added triggerPolicySync() function +- ✅ `components/search/SyncButton.tsx` - Created new component +- ✅ `app/(app)/search/page.tsx` - Integrated SyncButton + +**Checkpoint**: ✅ User Story 1 is fully functional and testable independently + +--- + +## Phase 4: User Story 2 - Handle Sync Errors Gracefully (Priority: P2) + +**Goal**: Admin receives clear error feedback when sync fails + +**Independent Test**: Simulate webhook failure (wrong URL, network error) → Error toast appears with user-friendly message + +### Implementation for User Story 2 + +- [x] T019 [US2] Add try-catch block in triggerPolicySync() to handle network errors +- [x] T020 [US2] Return error response when session.user is null ("Not authenticated") +- [x] T021 [US2] Return error response when tenantId is null ("No tenant ID found in session") +- [x] T022 [US2] Return error response when N8N_SYNC_WEBHOOK_URL is not configured ("Sync webhook not configured") +- [x] T023 [US2] Check webhook response status and throw error if not ok +- [x] T024 [US2] Add error toast notification in SyncButton handleSync catch block +- [x] T025 [US2] Add console.error logging in triggerPolicySync() for debugging +- [x] T026 [US2] Ensure button returns to normal state after error (useTransition handles this) + +**Files Modified**: +- ✅ `lib/actions/policySettings.ts` - Added comprehensive error handling +- ✅ `components/search/SyncButton.tsx` - Added error toast display + +**Checkpoint**: ✅ User Stories 1 AND 2 both work independently with proper error handling + +--- + +## Phase 5: User Story 3 - Prevent Multiple Simultaneous Syncs (Priority: P3) + +**Goal**: Button prevents duplicate requests during sync operation + +**Independent Test**: Click button rapidly multiple times → Only one request sent, button stays disabled + +### Implementation for User Story 3 + +- [x] T027 [US3] Add disabled prop to Button based on isPending state +- [x] T028 [US3] Conditionally render "Syncing..." text when isPending is true +- [x] T029 [US3] Add animate-spin class to RefreshCw icon when isPending is true +- [x] T030 [US3] Verify useTransition() prevents multiple simultaneous calls + +**Files Modified**: +- ✅ `components/search/SyncButton.tsx` - Added disabled state and loading UI + +**Checkpoint**: ✅ All user stories (1, 2, 3) are independently functional with proper UX + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +**Purpose**: Final touches and production readiness + +- [x] T031 Test button on mobile responsive layout +- [x] T032 Verify button styling matches existing search page design +- [x] T033 Test with missing N8N_SYNC_WEBHOOK_URL → Error toast appears +- [x] T034 Test with unauthenticated user → Error toast appears +- [x] T035 Test with mock webhook success → Success toast appears +- [x] T036 Document N8N_SYNC_WEBHOOK_URL in deployment notes +- [x] T037 Commit changes to 002-manual-policy-sync branch +- [x] T038 Create specification and plan documentation + +**Status**: ✅ Complete - All polish tasks finished + +--- + +## Deployment Checklist + +- [x] All tasks completed (T001-T038) +- [x] Code committed to feature branch +- [x] Specification (spec.md) created +- [x] Implementation plan (plan.md) created +- [x] Tasks (tasks.md) documented +- [ ] **TODO**: Configure N8N_SYNC_WEBHOOK_URL in Dokploy environment variables +- [ ] **TODO**: Merge 002-manual-policy-sync → main +- [ ] **TODO**: Deploy to production +- [ ] **TODO**: Verify button appears on /search page in production +- [ ] **TODO**: Test manual sync trigger with real n8n webhook + +--- + +## Dependencies Between Tasks + +**Legend**: `→` means "depends on" + +```text +Setup Phase: +T001, T002, T003 → [Can run in parallel] + +Foundational Phase: +T004, T005, T006 → [Already complete from previous work] + +User Story 1 (Core Sync): +T007 [SyncButton UI] → T013-T018 [UI refinements] +T008 [Server Action] → T009-T012 [Action logic] +T007, T008 → T017 [Page integration] + +User Story 2 (Error Handling): +T008 complete → T019-T025 [Add error cases] +T007 complete → T024, T026 [Error UI feedback] + +User Story 3 (Prevent Duplicates): +T007, T013 complete → T027-T030 [Loading state enhancements] +``` + +**Parallel Opportunities**: +- Phase 1: All tasks (T001-T003) can run in parallel +- User Story 1: T007 (UI) and T008 (Server Action) can be built in parallel +- User Story 2: Error handling can be added to both files simultaneously + +--- + +## Parallel Execution Examples + +### Example 1: User Story 1 (Two Developers) + +**Developer A** (Frontend): +```bash +# Create SyncButton component +- T007: components/search/SyncButton.tsx (stub Server Action call) +- T013-T016: Add loading state, toast, button rendering +- T017-T018: Integrate into search page +``` + +**Developer B** (Backend): +```bash +# Create Server Action +- T008: lib/actions/policySettings.ts (stub return success) +- T009-T012: Add auth check, tenant ID extraction, webhook call +``` + +**Integration**: Once both T007 and T008 are complete, connect them together + +### Example 2: User Story 2 (One Developer) + +```bash +# Add error handling after US1 is complete +- T019-T023: Server Action error cases (5 min each) +- T024-T026: UI error handling (5 min each) +- Total time: ~30 minutes +``` + +--- + +## Implementation Strategy + +### MVP-First Approach + +**MVP = User Story 1 ONLY (P1)**: +- Admin can click button +- Button shows loading state +- Success toast appears +- n8n receives webhook + +This delivers immediate value and can be shipped independently. + +### Incremental Delivery + +**Release 1**: User Story 1 (P1) - Core sync functionality +**Release 2**: Add User Story 2 (P2) - Error handling +**Release 3**: Add User Story 3 (P3) - UX polish + +Each release is independently valuable and deployable. + +--- + +## Testing Notes + +**Manual Testing Checklist** (from plan.md): + +✅ **Happy Path**: +- [x] Login → /search → Click "Sync Policies" → Loading state → Success toast +- [x] Verify n8n receives POST with correct tenantId + +✅ **Error Cases**: +- [x] Not authenticated → Error toast +- [x] No tenantId in session → Error toast +- [x] Webhook URL not configured → Error toast +- [x] Network error → Error toast +- [x] Webhook returns 500 → Error toast + +✅ **UI/UX**: +- [x] Button shows spinner during sync +- [x] Button disabled during sync +- [x] Rapid clicks don't send duplicates +- [x] Mobile responsive + +**No automated tests for this feature** - manual testing deemed sufficient for MVP (as per plan.md). + +--- + +## Task Summary + +**Total Tasks**: 38 +**Completed**: 38 ✅ +**Remaining**: 0 + +**Tasks by User Story**: +- Setup: 3 tasks +- Foundational: 3 tasks +- User Story 1 (P1): 12 tasks +- User Story 2 (P2): 8 tasks +- User Story 3 (P3): 4 tasks +- Polish: 8 tasks + +**Parallel Opportunities**: 15 tasks marked [P] can run in parallel + +**Critical Path**: Setup → Foundational → US1 core (T007-T012) → US2 errors → US3 UX → Polish + +**Estimated Implementation Time** (if starting from scratch): +- Setup: 15 minutes +- Foundational: 0 minutes (already done) +- User Story 1: 2 hours +- User Story 2: 30 minutes +- User Story 3: 20 minutes +- Polish: 30 minutes +- **Total**: ~3.5 hours + +**Actual Time**: Already completed ✅ + +--- + +## Next Feature Recommendations + +Based on this implementation, consider these follow-up features: + +1. **Sync History Tracking** (new spec) + - Track when syncs were triggered + - Display last sync timestamp on search page + - Show sync status (pending, success, failed) + +2. **Rate Limiting** (enhancement to 002) + - Prevent syncs more frequently than once per 5 minutes + - Show countdown timer until next sync allowed + +3. **Bulk Tenant Sync** (admin feature) + - Allow platform admins to trigger sync for all tenants + - New admin dashboard page + - Requires admin role system + +These can each be separate feature specs following the same workflow.