Add specification for 002-manual-policy-sync feature
This commit is contained in:
parent
c4600ba68c
commit
88c1474884
@ -4,6 +4,7 @@ import { useState, useTransition, useCallback } from 'react';
|
|||||||
import { SearchInput } from '@/components/search/SearchInput';
|
import { SearchInput } from '@/components/search/SearchInput';
|
||||||
import { ResultsTable } from '@/components/search/ResultsTable';
|
import { ResultsTable } from '@/components/search/ResultsTable';
|
||||||
import { EmptyState } from '@/components/search/EmptyState';
|
import { EmptyState } from '@/components/search/EmptyState';
|
||||||
|
import { SyncButton } from '@/components/search/SyncButton';
|
||||||
import {
|
import {
|
||||||
searchPolicySettings,
|
searchPolicySettings,
|
||||||
seedMyTenantData,
|
seedMyTenantData,
|
||||||
@ -78,10 +79,15 @@ export default function SearchPage() {
|
|||||||
<div className="mx-auto w-full max-w-6xl">
|
<div className="mx-auto w-full max-w-6xl">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
<CardTitle>Global Policy Search</CardTitle>
|
<CardTitle>Global Policy Search</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Search across all your Intune policy settings by keyword
|
Search across all your Intune policy settings by keyword
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<SyncButton />
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
|
|||||||
48
components/search/SyncButton.tsx
Normal file
48
components/search/SyncButton.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useTransition } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { RefreshCw } from 'lucide-react';
|
||||||
|
import { triggerPolicySync } from '@/lib/actions/policySettings';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export function SyncButton() {
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const handleSync = () => {
|
||||||
|
startTransition(async () => {
|
||||||
|
try {
|
||||||
|
const result = await triggerPolicySync();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(result.message ?? 'Policy sync triggered successfully');
|
||||||
|
} else {
|
||||||
|
toast.error(result.error ?? 'Failed to trigger sync');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('An unexpected error occurred');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={handleSync}
|
||||||
|
disabled={isPending}
|
||||||
|
variant="default"
|
||||||
|
size="default"
|
||||||
|
>
|
||||||
|
{isPending ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Syncing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
|
Sync Policies
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@
|
|||||||
import { db, policySettings, type PolicySetting } from '@/lib/db';
|
import { db, policySettings, type PolicySetting } from '@/lib/db';
|
||||||
import { getUserAuth } from '@/lib/auth/utils';
|
import { getUserAuth } from '@/lib/auth/utils';
|
||||||
import { eq, ilike, or, desc, and } from 'drizzle-orm';
|
import { eq, ilike, or, desc, and } from 'drizzle-orm';
|
||||||
|
import { env } from '@/lib/env.mjs';
|
||||||
|
|
||||||
export interface PolicySettingSearchResult {
|
export interface PolicySettingSearchResult {
|
||||||
id: string;
|
id: string;
|
||||||
@ -288,3 +289,61 @@ export async function seedMyTenantData(): Promise<{
|
|||||||
return { success: false, error: 'Failed to seed data' };
|
return { success: false, error: 'Failed to seed data' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger manual policy sync via n8n webhook
|
||||||
|
*
|
||||||
|
* **Security**: This function enforces tenant isolation by:
|
||||||
|
* 1. Validating user session via getUserAuth()
|
||||||
|
* 2. Extracting tenantId from session
|
||||||
|
* 3. Sending only the authenticated user's tenantId to n8n
|
||||||
|
*
|
||||||
|
* @returns Success/error result
|
||||||
|
*/
|
||||||
|
export async function triggerPolicySync(): Promise<{ success: boolean; message?: string; error?: string }> {
|
||||||
|
try {
|
||||||
|
const { session } = await getUserAuth();
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return { success: false, error: 'Not authenticated' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenantId = session.user.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
return { success: false, error: 'No tenant ID found in session' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const webhookUrl = env.N8N_SYNC_WEBHOOK_URL;
|
||||||
|
if (!webhookUrl) {
|
||||||
|
return { success: false, error: 'Sync webhook not configured' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger n8n workflow
|
||||||
|
const response = await fetch(webhookUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
tenantId,
|
||||||
|
source: 'manual_trigger',
|
||||||
|
triggeredAt: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Webhook responded with status ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Policy sync triggered successfully',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to trigger policy sync:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to trigger sync. Please try again later.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -23,6 +23,9 @@ export const env = createEnv({
|
|||||||
|
|
||||||
// Policy Settings Ingestion API
|
// Policy Settings Ingestion API
|
||||||
POLICY_API_SECRET: z.string().optional(),
|
POLICY_API_SECRET: z.string().optional(),
|
||||||
|
|
||||||
|
// n8n Webhook for manual policy sync
|
||||||
|
N8N_SYNC_WEBHOOK_URL: z.string().optional(),
|
||||||
},
|
},
|
||||||
client: {
|
client: {
|
||||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().optional(),
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().optional(),
|
||||||
|
|||||||
43
specs/002-manual-policy-sync/checklists/requirements.md
Normal file
43
specs/002-manual-policy-sync/checklists/requirements.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Specification Quality Checklist: Manual Policy Sync Button
|
||||||
|
|
||||||
|
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||||
|
**Created**: 2025-12-06
|
||||||
|
**Feature**: [spec.md](../spec.md)
|
||||||
|
|
||||||
|
## Content Quality
|
||||||
|
|
||||||
|
- [x] No implementation details (languages, frameworks, APIs)
|
||||||
|
- [x] Focused on user value and business needs
|
||||||
|
- [x] Written for non-technical stakeholders
|
||||||
|
- [x] All mandatory sections completed
|
||||||
|
|
||||||
|
## Requirement Completeness
|
||||||
|
|
||||||
|
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||||
|
- [x] Requirements are testable and unambiguous
|
||||||
|
- [x] Success criteria are measurable
|
||||||
|
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||||
|
- [x] All acceptance scenarios are defined
|
||||||
|
- [x] Edge cases are identified
|
||||||
|
- [x] Scope is clearly bounded
|
||||||
|
- [x] Dependencies and assumptions identified
|
||||||
|
|
||||||
|
## Feature Readiness
|
||||||
|
|
||||||
|
- [x] All functional requirements have clear acceptance criteria
|
||||||
|
- [x] User scenarios cover primary flows
|
||||||
|
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||||
|
- [x] No implementation details leak into specification
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
All checklist items passed. Specification is ready for planning phase (`/speckit.plan`).
|
||||||
|
|
||||||
|
**Key Strengths**:
|
||||||
|
- Clear prioritization (P1, P2, P3) of user stories
|
||||||
|
- Comprehensive edge case coverage
|
||||||
|
- Well-defined success criteria with measurable metrics
|
||||||
|
- Technology-agnostic requirements
|
||||||
|
- Clear scope boundaries (Out of Scope section)
|
||||||
|
|
||||||
|
**Ready for**: `/speckit.plan` to generate implementation plan
|
||||||
120
specs/002-manual-policy-sync/spec.md
Normal file
120
specs/002-manual-policy-sync/spec.md
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# Feature Specification: Manual Policy Sync Button
|
||||||
|
|
||||||
|
**Feature Branch**: `002-manual-policy-sync`
|
||||||
|
**Created**: 2025-12-06
|
||||||
|
**Status**: Draft
|
||||||
|
**Input**: User description: "Manual Policy Sync Button - Allow admins to trigger immediate policy synchronization from Intune without waiting for scheduled sync"
|
||||||
|
|
||||||
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
|
### User Story 1 - Trigger Immediate Sync (Priority: P1)
|
||||||
|
|
||||||
|
An IT administrator has just made changes to an Intune policy and wants to see those changes reflected in TenantPilot immediately without waiting for the scheduled nightly sync.
|
||||||
|
|
||||||
|
**Why this priority**: Core functionality that directly addresses the pain point of waiting for scheduled syncs. Delivers immediate value and is the primary reason for this feature.
|
||||||
|
|
||||||
|
**Independent Test**: Admin can click "Sync Policies" button on search page, receives confirmation that sync started, and n8n workflow is triggered for their tenant.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** an authenticated admin is on the `/search` page, **When** they click the "Sync Policies" button, **Then** the button shows a loading state with "Syncing..." text and a spinner
|
||||||
|
2. **Given** a sync request is successfully sent to n8n, **When** the request completes, **Then** a success toast message appears saying "Policy sync triggered successfully"
|
||||||
|
3. **Given** an authenticated admin with a valid tenant ID, **When** they trigger a sync, **Then** the n8n webhook receives a POST request with their tenant ID and source "manual_trigger"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 2 - Handle Sync Errors Gracefully (Priority: P2)
|
||||||
|
|
||||||
|
When the sync cannot be triggered (network error, webhook down, etc.), the admin needs clear feedback about what went wrong.
|
||||||
|
|
||||||
|
**Why this priority**: Essential for production reliability. Without error handling, users won't know if sync failed and may think it succeeded.
|
||||||
|
|
||||||
|
**Independent Test**: Can be tested by simulating webhook failures (wrong URL, network timeout) and verifying error messages appear.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** the n8n webhook is unreachable, **When** admin clicks "Sync Policies", **Then** an error toast appears with message "Failed to trigger sync. Please try again later."
|
||||||
|
2. **Given** a sync request fails, **When** the error occurs, **Then** the button returns to its normal state (not stuck in loading)
|
||||||
|
3. **Given** user is not authenticated, **When** they attempt to trigger sync, **Then** they receive "Not authenticated" error
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 3 - Prevent Multiple Simultaneous Syncs (Priority: P3)
|
||||||
|
|
||||||
|
Admin should not be able to trigger multiple syncs simultaneously for the same tenant to avoid duplicate processing.
|
||||||
|
|
||||||
|
**Why this priority**: Nice-to-have for UX polish. Prevents confusion but not critical for MVP since n8n workflows can handle duplicates.
|
||||||
|
|
||||||
|
**Independent Test**: Click sync button multiple times rapidly and verify only one request is sent, button stays disabled during processing.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a sync is in progress, **When** admin clicks the button again, **Then** the button remains disabled and no additional request is sent
|
||||||
|
2. **Given** a sync completes (success or error), **When** the toast dismisses, **Then** the button becomes clickable again
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- What happens when user's session has no tenant ID? (Show error: "No tenant ID found in session")
|
||||||
|
- What happens when N8N_SYNC_WEBHOOK_URL environment variable is not configured? (Show error: "Sync webhook not configured")
|
||||||
|
- What happens when n8n webhook returns non-200 status? (Log error, show user-friendly message)
|
||||||
|
- What happens if user navigates away while sync is in progress? (No issue - webhook call already sent, no cleanup needed)
|
||||||
|
|
||||||
|
## Requirements *(mandatory)*
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- **FR-001**: System MUST provide a "Sync Policies" button on the `/search` page visible to authenticated users
|
||||||
|
- **FR-002**: System MUST validate user authentication before allowing sync trigger
|
||||||
|
- **FR-003**: System MUST extract tenant ID from user session and include it in webhook payload
|
||||||
|
- **FR-004**: System MUST send POST request to n8n webhook with JSON body containing `{ tenantId, source: "manual_trigger", triggeredAt: ISO timestamp }`
|
||||||
|
- **FR-005**: System MUST show loading state (spinner + "Syncing..." text) while request is in progress
|
||||||
|
- **FR-006**: System MUST display success toast notification when sync is triggered successfully
|
||||||
|
- **FR-007**: System MUST display error toast notification when sync fails with user-friendly error message
|
||||||
|
- **FR-008**: System MUST disable button during sync operation to prevent duplicate requests
|
||||||
|
- **FR-009**: System MUST log sync errors to console for debugging
|
||||||
|
- **FR-010**: System MUST validate that N8N_SYNC_WEBHOOK_URL environment variable is configured before attempting request
|
||||||
|
|
||||||
|
### Key Entities *(include if feature involves data)*
|
||||||
|
|
||||||
|
- **Sync Request**: Represents a manual sync trigger event
|
||||||
|
- Attributes: tenantId (from session), source ("manual_trigger"), triggeredAt (ISO timestamp)
|
||||||
|
- Sent to n8n webhook, not persisted in database
|
||||||
|
- Used for audit trail in n8n workflow logs
|
||||||
|
|
||||||
|
## Success Criteria *(mandatory)*
|
||||||
|
|
||||||
|
### Measurable Outcomes
|
||||||
|
|
||||||
|
- **SC-001**: Admin can trigger policy sync with single button click from search page
|
||||||
|
- **SC-002**: Sync request reaches n8n webhook within 2 seconds of button click
|
||||||
|
- **SC-003**: Admin receives visual feedback (loading state + toast) within 1 second of clicking button
|
||||||
|
- **SC-004**: Error scenarios (network failure, missing config) show clear error messages to user
|
||||||
|
- **SC-005**: Button correctly handles rapid multiple clicks without sending duplicate requests
|
||||||
|
- **SC-006**: 100% of sync triggers include correct tenant ID from authenticated user's session
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
- N8N_SYNC_WEBHOOK_URL will be configured in environment variables before feature deployment
|
||||||
|
- n8n workflow already exists and can handle manual sync requests (separate from scheduled sync)
|
||||||
|
- Webhook endpoint accepts POST requests with JSON body
|
||||||
|
- Tenant ID is always available in authenticated user's session (handled by existing auth logic)
|
||||||
|
- No database changes required - this is a client-side UI + server action feature
|
||||||
|
- Sync is asynchronous - user doesn't wait for actual Intune data fetch to complete
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Existing authentication system (NextAuth with tenant ID in session)
|
||||||
|
- Existing server actions infrastructure (`lib/actions/policySettings.ts`)
|
||||||
|
- Existing UI components (Shadcn Button, Toast from sonner)
|
||||||
|
- n8n webhook endpoint must be deployed and accessible
|
||||||
|
- Environment variable validation system (`lib/env.mjs`)
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Showing sync progress or status (user only gets confirmation that sync was triggered)
|
||||||
|
- Displaying last sync timestamp on the page
|
||||||
|
- Limiting sync frequency (e.g., rate limiting to once per 5 minutes)
|
||||||
|
- Syncing specific policies (all policies for tenant are synced)
|
||||||
|
- Admin controls for other tenants (admin can only sync their own tenant)
|
||||||
Loading…
Reference in New Issue
Block a user