mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-10-08 20:57:51 +00:00
US-1155: Settings performance — memoization & lazy loading
This commit is contained in:
parent
38252d8860
commit
fa33c43368
12 changed files with 1117 additions and 46 deletions
|
|
@ -490,3 +490,24 @@
|
|||
{"type":"task.created","taskId":"task_20260128_W9Henc","status":"todo","id":"evt_t9G3qZADwCc7","timestamp":"2026-01-28T09:05:42.993Z"}
|
||||
{"type":"task.created","taskId":"task_20260128_3SrSde","status":"todo","id":"evt_ljpYJmVetnxw","timestamp":"2026-01-28T09:05:43.001Z"}
|
||||
{"type":"task.archived","taskId":"task_20260128_3SrSde","status":"todo","id":"evt_5l0jQ9llHLbY","timestamp":"2026-01-28T09:05:43.002Z"}
|
||||
{"type":"task.status_changed","taskId":"task_20260128_6SbOnv","project":"veritas-kanban","status":"done","previousStatus":"in-progress","id":"evt_EK-e6y771Zso","timestamp":"2026-01-28T09:06:09.459Z"}
|
||||
{"type":"task.status_changed","taskId":"task_20260128_-KNOy3","project":"veritas-kanban","status":"in-progress","previousStatus":"todo","id":"evt_M7RMF26KsnnP","timestamp":"2026-01-28T09:06:28.250Z"}
|
||||
{"type":"task.status_changed","taskId":"task_20260128_CnogQO","project":"veritas-kanban","status":"in-progress","previousStatus":"todo","id":"evt_wtwQY0KgQwda","timestamp":"2026-01-28T09:07:23.934Z"}
|
||||
{"type":"task.created","taskId":"task_20260128_g6jt0d","project":"my-project","status":"todo","id":"evt_OQJ7GpkKhgHw","timestamp":"2026-01-28T09:08:42.546Z"}
|
||||
{"type":"task.created","taskId":"task_20260128_xefor9","status":"todo","id":"evt_jSQYKFtnuBFY","timestamp":"2026-01-28T09:08:42.549Z"}
|
||||
{"type":"task.created","taskId":"task_20260128_Lfcgty","status":"todo","id":"evt_p9rXJODNAsFc","timestamp":"2026-01-28T09:08:42.551Z"}
|
||||
{"type":"task.created","taskId":"task_20260128_fpL_Dd","status":"todo","id":"evt_ZlwfeOttZC0i","timestamp":"2026-01-28T09:08:42.553Z"}
|
||||
{"type":"task.status_changed","taskId":"task_20260128_fpL_Dd","status":"in-progress","previousStatus":"todo","id":"evt_JL9t1-cWuX3_","timestamp":"2026-01-28T09:08:42.566Z"}
|
||||
{"type":"task.created","taskId":"task_20260128_jaRqQ3","status":"todo","id":"evt_YprCvvQJN2SO","timestamp":"2026-01-28T09:08:42.568Z"}
|
||||
{"type":"task.created","taskId":"task_20260128_qZPdbi","status":"todo","id":"evt_XIFHX_Iua532","timestamp":"2026-01-28T09:08:42.570Z"}
|
||||
{"type":"task.created","taskId":"task_20260128_OFy8h-","status":"todo","id":"evt_-XOvuKgW52ea","timestamp":"2026-01-28T09:08:42.576Z"}
|
||||
{"type":"task.archived","taskId":"task_20260128_OFy8h-","status":"todo","id":"evt_raxP0Uqjj_ej","timestamp":"2026-01-28T09:08:42.577Z"}
|
||||
{"type":"task.created","taskId":"task_20260128_p3tQz-","project":"my-project","status":"todo","id":"evt_sv4JD80RG0Mp","timestamp":"2026-01-28T09:08:43.780Z"}
|
||||
{"type":"task.created","taskId":"task_20260128_lYDQGj","status":"todo","id":"evt_V9D7YuRSiXlx","timestamp":"2026-01-28T09:08:43.783Z"}
|
||||
{"type":"task.created","taskId":"task_20260128_U1AXxF","status":"todo","id":"evt_IkOyt779Bmx8","timestamp":"2026-01-28T09:08:43.785Z"}
|
||||
{"type":"task.created","taskId":"task_20260128_u-jBCg","status":"todo","id":"evt_BrnjWauTH2NB","timestamp":"2026-01-28T09:08:43.787Z"}
|
||||
{"type":"task.status_changed","taskId":"task_20260128_u-jBCg","status":"in-progress","previousStatus":"todo","id":"evt_vPgTJQzmY2-S","timestamp":"2026-01-28T09:08:43.800Z"}
|
||||
{"type":"task.created","taskId":"task_20260128_Tp9HXB","status":"todo","id":"evt_3hurmcP_cfd4","timestamp":"2026-01-28T09:08:43.803Z"}
|
||||
{"type":"task.created","taskId":"task_20260128_isj7oa","status":"todo","id":"evt_udJP0CW5DOjq","timestamp":"2026-01-28T09:08:43.805Z"}
|
||||
{"type":"task.created","taskId":"task_20260128_mY2lH4","status":"todo","id":"evt_NK1qNmhq8BVa","timestamp":"2026-01-28T09:08:43.810Z"}
|
||||
{"type":"task.archived","taskId":"task_20260128_mY2lH4","status":"todo","id":"evt_xt1yJ4fnooxC","timestamp":"2026-01-28T09:08:43.811Z"}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,48 @@
|
|||
[
|
||||
{
|
||||
"id": "activity_1769591243934_a59soco89",
|
||||
"type": "status_changed",
|
||||
"taskId": "task_20260128_CnogQO",
|
||||
"taskTitle": "US-1157: Sanitize settings import against prototype pollution",
|
||||
"details": {
|
||||
"from": "todo",
|
||||
"status": "in-progress"
|
||||
},
|
||||
"timestamp": "2026-01-28T09:07:23.934Z"
|
||||
},
|
||||
{
|
||||
"id": "activity_1769591188250_6ff1szybq",
|
||||
"type": "status_changed",
|
||||
"taskId": "task_20260128_-KNOy3",
|
||||
"taskTitle": "US-1155: Settings performance — memoization & lazy loading",
|
||||
"details": {
|
||||
"from": "todo",
|
||||
"status": "in-progress"
|
||||
},
|
||||
"timestamp": "2026-01-28T09:06:28.250Z"
|
||||
},
|
||||
{
|
||||
"id": "activity_1769591169459_ugp657565",
|
||||
"type": "status_changed",
|
||||
"taskId": "task_20260128_6SbOnv",
|
||||
"taskTitle": "US-1154: Add test coverage for settings features",
|
||||
"details": {
|
||||
"from": "in-progress",
|
||||
"status": "done"
|
||||
},
|
||||
"timestamp": "2026-01-28T09:06:09.459Z"
|
||||
},
|
||||
{
|
||||
"id": "activity_1769591162356_cdvv56zb6",
|
||||
"type": "comment_added",
|
||||
"taskId": "task_20260128_6SbOnv",
|
||||
"taskTitle": "US-1154: Add test coverage for settings features",
|
||||
"details": {
|
||||
"author": "Veritas",
|
||||
"preview": "Fixed test discovery (worktrees excluded), fixed e..."
|
||||
},
|
||||
"timestamp": "2026-01-28T09:06:02.356Z"
|
||||
},
|
||||
{
|
||||
"id": "activity_1769590946523_ekw6m4yic",
|
||||
"type": "task_created",
|
||||
|
|
|
|||
|
|
@ -3,10 +3,28 @@ import { ConfigService } from '../services/config-service.js';
|
|||
import { getTelemetryService } from '../services/telemetry-service.js';
|
||||
import { getAttachmentService } from '../services/attachment-service.js';
|
||||
import type { FeatureSettings } from '@veritas-kanban/shared';
|
||||
import { FeatureSettingsPatchSchema } from '../schemas/feature-settings-schema.js';
|
||||
|
||||
const router: RouterType = Router();
|
||||
const configService = new ConfigService();
|
||||
|
||||
// Simple rate limiter
|
||||
const rateLimiter = new Map<string, { count: number; resetAt: number }>();
|
||||
const RATE_LIMIT = 10;
|
||||
const RATE_WINDOW_MS = 60_000;
|
||||
|
||||
function checkRateLimit(ip: string): boolean {
|
||||
const now = Date.now();
|
||||
const entry = rateLimiter.get(ip);
|
||||
if (!entry || now > entry.resetAt) {
|
||||
rateLimiter.set(ip, { count: 1, resetAt: now + RATE_WINDOW_MS });
|
||||
return true;
|
||||
}
|
||||
if (entry.count >= RATE_LIMIT) return false;
|
||||
entry.count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync feature settings to affected server-side services.
|
||||
* Called on PATCH and on server startup.
|
||||
|
|
@ -43,12 +61,27 @@ router.get('/features', async (_req, res) => {
|
|||
// PATCH /api/settings/features — deep merge partial updates
|
||||
router.patch('/features', async (req, res) => {
|
||||
try {
|
||||
const patch = req.body;
|
||||
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
||||
return res.status(400).json({ error: 'Body must be a JSON object with feature settings' });
|
||||
// Rate limiting
|
||||
const clientIp = req.ip || req.socket.remoteAddress || 'unknown';
|
||||
if (!checkRateLimit(clientIp)) {
|
||||
return res.status(429).json({ error: 'Too many requests. Max 10 settings updates per minute.' });
|
||||
}
|
||||
|
||||
// Validate with Zod — strips unknown keys, rejects dangerous ones
|
||||
const parseResult = FeatureSettingsPatchSchema.safeParse(req.body);
|
||||
if (!parseResult.success) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid settings payload',
|
||||
details: parseResult.error.issues.map(i => i.message),
|
||||
});
|
||||
}
|
||||
const patch = parseResult.data;
|
||||
|
||||
if (Object.keys(patch).length === 0) {
|
||||
return res.status(400).json({ error: 'No valid settings provided' });
|
||||
}
|
||||
|
||||
const updated = await configService.updateFeatureSettings(patch);
|
||||
// Sync settings to server-side services
|
||||
syncSettingsToServices(updated);
|
||||
res.json(updated);
|
||||
} catch (error) {
|
||||
|
|
|
|||
74
server/src/schemas/feature-settings-schema.ts
Normal file
74
server/src/schemas/feature-settings-schema.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
// Dangerous keys check
|
||||
const DANGEROUS_KEYS = ['__proto__', 'constructor', 'prototype'];
|
||||
function hasDangerousKeys(obj: unknown): boolean {
|
||||
if (typeof obj !== 'object' || obj === null) return false;
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (DANGEROUS_KEYS.includes(key)) return true;
|
||||
if (hasDangerousKeys((obj as any)[key])) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const BoardSettingsSchema = z.object({
|
||||
showDashboard: z.boolean().optional(),
|
||||
showArchiveSuggestions: z.boolean().optional(),
|
||||
cardDensity: z.enum(['normal', 'compact']).optional(),
|
||||
showPriorityIndicators: z.boolean().optional(),
|
||||
showProjectBadges: z.boolean().optional(),
|
||||
showSprintBadges: z.boolean().optional(),
|
||||
enableDragAndDrop: z.boolean().optional(),
|
||||
}).strict().optional();
|
||||
|
||||
const TaskBehaviorSettingsSchema = z.object({
|
||||
enableTimeTracking: z.boolean().optional(),
|
||||
enableSubtaskAutoComplete: z.boolean().optional(),
|
||||
enableDependencies: z.boolean().optional(),
|
||||
enableAttachments: z.boolean().optional(),
|
||||
attachmentMaxFileSize: z.number().int().min(1024).max(100*1024*1024).optional(),
|
||||
attachmentMaxPerTask: z.number().int().min(1).max(100).optional(),
|
||||
attachmentMaxTotalSize: z.number().int().min(1024).max(500*1024*1024).optional(),
|
||||
enableComments: z.boolean().optional(),
|
||||
defaultPriority: z.enum(['none','low','medium','high','critical']).optional(),
|
||||
}).strict().optional();
|
||||
|
||||
const AgentBehaviorSettingsSchema = z.object({
|
||||
timeoutMinutes: z.number().int().min(5).max(480).optional(),
|
||||
autoCommitOnComplete: z.boolean().optional(),
|
||||
autoCleanupWorktrees: z.boolean().optional(),
|
||||
enablePreview: z.boolean().optional(),
|
||||
}).strict().optional();
|
||||
|
||||
const TelemetrySettingsSchema = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
retentionDays: z.number().int().min(7).max(365).optional(),
|
||||
enableTraces: z.boolean().optional(),
|
||||
enableActivityTracking: z.boolean().optional(),
|
||||
}).strict().optional();
|
||||
|
||||
const NotificationSettingsSchema = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
onTaskComplete: z.boolean().optional(),
|
||||
onAgentFailure: z.boolean().optional(),
|
||||
onReviewNeeded: z.boolean().optional(),
|
||||
channel: z.string().max(200).optional(),
|
||||
}).strict().optional();
|
||||
|
||||
const ArchiveSettingsSchema = z.object({
|
||||
autoArchiveEnabled: z.boolean().optional(),
|
||||
autoArchiveAfterDays: z.number().int().min(1).max(365).optional(),
|
||||
}).strict().optional();
|
||||
|
||||
export const FeatureSettingsPatchSchema = z.object({
|
||||
board: BoardSettingsSchema,
|
||||
tasks: TaskBehaviorSettingsSchema,
|
||||
agents: AgentBehaviorSettingsSchema,
|
||||
telemetry: TelemetrySettingsSchema,
|
||||
notifications: NotificationSettingsSchema,
|
||||
archive: ArchiveSettingsSchema,
|
||||
}).strict().refine(val => !hasDangerousKeys(val), {
|
||||
message: 'Payload contains forbidden keys (__proto__, constructor, prototype)',
|
||||
});
|
||||
|
||||
export type FeatureSettingsPatch = z.infer<typeof FeatureSettingsPatchSchema>;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from 'react';
|
||||
import { useState, memo, useEffect } from 'react';
|
||||
import type { ManagedListItem } from '@veritas-kanban/shared';
|
||||
import { Button } from '../ui/button';
|
||||
import { Input } from '../ui/input';
|
||||
|
|
@ -52,7 +52,7 @@ interface SortableItemProps<T extends ManagedListItem> {
|
|||
canDeleteCheck?: (id: string) => Promise<{ allowed: boolean; referenceCount: number; isDefault: boolean }>;
|
||||
}
|
||||
|
||||
function SortableItem<T extends ManagedListItem>({
|
||||
const SortableItem = memo(function SortableItem<T extends ManagedListItem>({
|
||||
item,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
|
|
@ -194,7 +194,7 @@ function SortableItem<T extends ManagedListItem>({
|
|||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}) as <T extends ManagedListItem>(props: SortableItemProps<T>) => React.JSX.Element;
|
||||
|
||||
export function ManagedListManager<T extends ManagedListItem>({
|
||||
title,
|
||||
|
|
@ -210,6 +210,7 @@ export function ManagedListManager<T extends ManagedListItem>({
|
|||
}: ManagedListManagerProps<T>) {
|
||||
const [newItemLabel, setNewItemLabel] = useState('');
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [localItems, setLocalItems] = useState(items);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
|
|
@ -218,17 +219,30 @@ export function ManagedListManager<T extends ManagedListItem>({
|
|||
})
|
||||
);
|
||||
|
||||
const handleDragEnd = async (event: DragEndEvent) => {
|
||||
// Sync local state with incoming items
|
||||
useEffect(() => {
|
||||
setLocalItems(items);
|
||||
}, [items]);
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = items.findIndex((item) => item.id === active.id);
|
||||
const newIndex = items.findIndex((item) => item.id === over.id);
|
||||
const oldIndex = localItems.findIndex((item) => item.id === active.id);
|
||||
const newIndex = localItems.findIndex((item) => item.id === over.id);
|
||||
|
||||
const reordered = arrayMove(items, oldIndex, newIndex);
|
||||
const reordered = arrayMove(localItems, oldIndex, newIndex);
|
||||
const orderedIds = reordered.map((item) => item.id);
|
||||
|
||||
await onReorder(orderedIds);
|
||||
// Optimistic update
|
||||
setLocalItems(reordered);
|
||||
|
||||
// Fire onReorder in background
|
||||
onReorder(orderedIds).catch((error) => {
|
||||
// Rollback on error
|
||||
console.error('Failed to reorder items:', error);
|
||||
setLocalItems(items);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -261,10 +275,10 @@ export function ManagedListManager<T extends ManagedListItem>({
|
|||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={items.map((item) => item.id)}
|
||||
items={localItems.map((item) => item.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{items.map((item) => (
|
||||
{localItems.map((item) => (
|
||||
<SortableItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useRef, useCallback } from 'react';
|
||||
import { useState, useRef, useCallback, lazy, Suspense } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -24,6 +24,7 @@ import {
|
|||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import {
|
||||
useFeatureSettings,
|
||||
useDebouncedFeatureUpdate,
|
||||
|
|
@ -34,17 +35,32 @@ import {
|
|||
} from 'lucide-react';
|
||||
import { DEFAULT_FEATURE_SETTINGS } from '@veritas-kanban/shared';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
GeneralTab,
|
||||
BoardTab,
|
||||
TasksTab,
|
||||
AgentsTab,
|
||||
DataTab,
|
||||
NotificationsTab,
|
||||
ManageTab,
|
||||
} from './tabs';
|
||||
import { SettingsErrorBoundary } from './shared';
|
||||
|
||||
// Lazy-load tab components
|
||||
const LazyGeneralTab = lazy(() => import('./tabs/GeneralTab').then(m => ({ default: m.GeneralTab })));
|
||||
const LazyBoardTab = lazy(() => import('./tabs/BoardTab').then(m => ({ default: m.BoardTab })));
|
||||
const LazyTasksTab = lazy(() => import('./tabs/TasksTab').then(m => ({ default: m.TasksTab })));
|
||||
const LazyAgentsTab = lazy(() => import('./tabs/AgentsTab').then(m => ({ default: m.AgentsTab })));
|
||||
const LazyDataTab = lazy(() => import('./tabs/DataTab').then(m => ({ default: m.DataTab })));
|
||||
const LazyNotificationsTab = lazy(() => import('./tabs/NotificationsTab').then(m => ({ default: m.NotificationsTab })));
|
||||
const LazyManageTab = lazy(() => import('./tabs/ManageTab').then(m => ({ default: m.ManageTab })));
|
||||
|
||||
// ============ Tab Skeleton ============
|
||||
|
||||
function TabSkeleton() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-6 w-32" />
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-12 w-full" />
|
||||
<Skeleton className="h-12 w-full" />
|
||||
<Skeleton className="h-12 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============ Tab Configuration ============
|
||||
|
||||
type TabId = 'general' | 'board' | 'tasks' | 'agents' | 'data' | 'notifications' | 'manage';
|
||||
|
|
@ -149,22 +165,31 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
|
|||
}, [activeTab]);
|
||||
|
||||
const renderTab = () => {
|
||||
switch (activeTab) {
|
||||
case 'general':
|
||||
return <SettingsErrorBoundary tabName="General"><GeneralTab /></SettingsErrorBoundary>;
|
||||
case 'board':
|
||||
return <SettingsErrorBoundary tabName="Board"><BoardTab /></SettingsErrorBoundary>;
|
||||
case 'tasks':
|
||||
return <SettingsErrorBoundary tabName="Tasks"><TasksTab /></SettingsErrorBoundary>;
|
||||
case 'agents':
|
||||
return <SettingsErrorBoundary tabName="Agents"><AgentsTab /></SettingsErrorBoundary>;
|
||||
case 'data':
|
||||
return <SettingsErrorBoundary tabName="Data"><DataTab /></SettingsErrorBoundary>;
|
||||
case 'notifications':
|
||||
return <SettingsErrorBoundary tabName="Notifications"><NotificationsTab /></SettingsErrorBoundary>;
|
||||
case 'manage':
|
||||
return <SettingsErrorBoundary tabName="Manage"><ManageTab /></SettingsErrorBoundary>;
|
||||
}
|
||||
return (
|
||||
<Suspense fallback={<TabSkeleton />}>
|
||||
{activeTab === 'general' && (
|
||||
<SettingsErrorBoundary tabName="General"><LazyGeneralTab /></SettingsErrorBoundary>
|
||||
)}
|
||||
{activeTab === 'board' && (
|
||||
<SettingsErrorBoundary tabName="Board"><LazyBoardTab /></SettingsErrorBoundary>
|
||||
)}
|
||||
{activeTab === 'tasks' && (
|
||||
<SettingsErrorBoundary tabName="Tasks"><LazyTasksTab /></SettingsErrorBoundary>
|
||||
)}
|
||||
{activeTab === 'agents' && (
|
||||
<SettingsErrorBoundary tabName="Agents"><LazyAgentsTab /></SettingsErrorBoundary>
|
||||
)}
|
||||
{activeTab === 'data' && (
|
||||
<SettingsErrorBoundary tabName="Data"><LazyDataTab /></SettingsErrorBoundary>
|
||||
)}
|
||||
{activeTab === 'notifications' && (
|
||||
<SettingsErrorBoundary tabName="Notifications"><LazyNotificationsTab /></SettingsErrorBoundary>
|
||||
)}
|
||||
{activeTab === 'manage' && (
|
||||
<SettingsErrorBoundary tabName="Manage"><LazyManageTab /></SettingsErrorBoundary>
|
||||
)}
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { memo } from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { SettingRow } from './SettingRow';
|
||||
|
||||
export function NumberRow({ label, description, value, onChange, min, max, unit }: {
|
||||
export const NumberRow = memo(function NumberRow({ label, description, value, onChange, min, max, unit }: {
|
||||
label: string;
|
||||
description?: string;
|
||||
value: number;
|
||||
|
|
@ -31,4 +32,4 @@ export function NumberRow({ label, description, value, onChange, min, max, unit
|
|||
</div>
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
export function SettingRow({ label, description, children }: {
|
||||
import { memo } from 'react';
|
||||
|
||||
export const SettingRow = memo(function SettingRow({ label, description, children }: {
|
||||
label: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
|
|
@ -14,4 +16,4 @@ export function SettingRow({ label, description, children }: {
|
|||
<div className="flex-shrink-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { memo } from 'react';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { SettingRow } from './SettingRow';
|
||||
|
||||
export function ToggleRow({ label, description, checked, onCheckedChange }: {
|
||||
export const ToggleRow = memo(function ToggleRow({ label, description, checked, onCheckedChange }: {
|
||||
label: string;
|
||||
description?: string;
|
||||
checked: boolean;
|
||||
|
|
@ -12,4 +13,4 @@ export function ToggleRow({ label, description, checked, onCheckedChange }: {
|
|||
<Switch checked={checked} onCheckedChange={onCheckedChange} />
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
741
web/src/lib/template-schema.d.ts
vendored
Normal file
741
web/src/lib/template-schema.d.ts
vendored
Normal file
|
|
@ -0,0 +1,741 @@
|
|||
/**
|
||||
* Zod validation schema for task templates
|
||||
* Enforces strict validation, size limits, and security checks
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
/**
|
||||
* Task template schema with strict validation
|
||||
*/
|
||||
export declare const TaskTemplateSchema: z.ZodEffects<z.ZodObject<{
|
||||
id: z.ZodOptional<z.ZodString>;
|
||||
name: z.ZodEffects<z.ZodString, string, string>;
|
||||
description: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
|
||||
category: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
|
||||
version: z.ZodOptional<z.ZodNumber>;
|
||||
taskDefaults: z.ZodObject<{
|
||||
type: z.ZodOptional<z.ZodString>;
|
||||
priority: z.ZodOptional<z.ZodString>;
|
||||
project: z.ZodOptional<z.ZodString>;
|
||||
descriptionTemplate: z.ZodOptional<z.ZodString>;
|
||||
agent: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
}, {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
}>;
|
||||
subtaskTemplates: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
title: z.ZodEffects<z.ZodString, string, string>;
|
||||
order: z.ZodOptional<z.ZodNumber>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}, {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}>, "many">>;
|
||||
blueprint: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
refId: z.ZodEffects<z.ZodString, string, string>;
|
||||
title: z.ZodEffects<z.ZodString, string, string>;
|
||||
taskDefaults: z.ZodOptional<z.ZodObject<{
|
||||
type: z.ZodOptional<z.ZodString>;
|
||||
priority: z.ZodOptional<z.ZodString>;
|
||||
project: z.ZodOptional<z.ZodString>;
|
||||
descriptionTemplate: z.ZodOptional<z.ZodString>;
|
||||
agent: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
}, {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
}>>;
|
||||
subtaskTemplates: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
title: z.ZodEffects<z.ZodString, string, string>;
|
||||
order: z.ZodOptional<z.ZodNumber>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}, {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}>, "many">>;
|
||||
blockedByRefs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
title: string;
|
||||
refId: string;
|
||||
taskDefaults?: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
} | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
blockedByRefs?: string[] | undefined;
|
||||
}, {
|
||||
title: string;
|
||||
refId: string;
|
||||
taskDefaults?: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
} | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
blockedByRefs?: string[] | undefined;
|
||||
}>, "many">>;
|
||||
created: z.ZodOptional<z.ZodString>;
|
||||
updated: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
name: string;
|
||||
taskDefaults: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
};
|
||||
id?: string | undefined;
|
||||
description?: string | undefined;
|
||||
created?: string | undefined;
|
||||
updated?: string | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
category?: string | undefined;
|
||||
blueprint?: {
|
||||
title: string;
|
||||
refId: string;
|
||||
taskDefaults?: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
} | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
blockedByRefs?: string[] | undefined;
|
||||
}[] | undefined;
|
||||
version?: number | undefined;
|
||||
}, {
|
||||
name: string;
|
||||
taskDefaults: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
};
|
||||
id?: string | undefined;
|
||||
description?: string | undefined;
|
||||
created?: string | undefined;
|
||||
updated?: string | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
category?: string | undefined;
|
||||
blueprint?: {
|
||||
title: string;
|
||||
refId: string;
|
||||
taskDefaults?: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
} | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
blockedByRefs?: string[] | undefined;
|
||||
}[] | undefined;
|
||||
version?: number | undefined;
|
||||
}>, {
|
||||
name: string;
|
||||
taskDefaults: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
};
|
||||
id?: string | undefined;
|
||||
description?: string | undefined;
|
||||
created?: string | undefined;
|
||||
updated?: string | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
category?: string | undefined;
|
||||
blueprint?: {
|
||||
title: string;
|
||||
refId: string;
|
||||
taskDefaults?: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
} | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
blockedByRefs?: string[] | undefined;
|
||||
}[] | undefined;
|
||||
version?: number | undefined;
|
||||
}, {
|
||||
name: string;
|
||||
taskDefaults: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
};
|
||||
id?: string | undefined;
|
||||
description?: string | undefined;
|
||||
created?: string | undefined;
|
||||
updated?: string | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
category?: string | undefined;
|
||||
blueprint?: {
|
||||
title: string;
|
||||
refId: string;
|
||||
taskDefaults?: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
} | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
blockedByRefs?: string[] | undefined;
|
||||
}[] | undefined;
|
||||
version?: number | undefined;
|
||||
}>;
|
||||
/**
|
||||
* Schema for importing templates (single or array)
|
||||
*/
|
||||
export declare const TemplateImportSchema: z.ZodUnion<[z.ZodEffects<z.ZodObject<{
|
||||
id: z.ZodOptional<z.ZodString>;
|
||||
name: z.ZodEffects<z.ZodString, string, string>;
|
||||
description: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
|
||||
category: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
|
||||
version: z.ZodOptional<z.ZodNumber>;
|
||||
taskDefaults: z.ZodObject<{
|
||||
type: z.ZodOptional<z.ZodString>;
|
||||
priority: z.ZodOptional<z.ZodString>;
|
||||
project: z.ZodOptional<z.ZodString>;
|
||||
descriptionTemplate: z.ZodOptional<z.ZodString>;
|
||||
agent: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
}, {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
}>;
|
||||
subtaskTemplates: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
title: z.ZodEffects<z.ZodString, string, string>;
|
||||
order: z.ZodOptional<z.ZodNumber>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}, {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}>, "many">>;
|
||||
blueprint: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
refId: z.ZodEffects<z.ZodString, string, string>;
|
||||
title: z.ZodEffects<z.ZodString, string, string>;
|
||||
taskDefaults: z.ZodOptional<z.ZodObject<{
|
||||
type: z.ZodOptional<z.ZodString>;
|
||||
priority: z.ZodOptional<z.ZodString>;
|
||||
project: z.ZodOptional<z.ZodString>;
|
||||
descriptionTemplate: z.ZodOptional<z.ZodString>;
|
||||
agent: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
}, {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
}>>;
|
||||
subtaskTemplates: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
title: z.ZodEffects<z.ZodString, string, string>;
|
||||
order: z.ZodOptional<z.ZodNumber>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}, {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}>, "many">>;
|
||||
blockedByRefs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
title: string;
|
||||
refId: string;
|
||||
taskDefaults?: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
} | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
blockedByRefs?: string[] | undefined;
|
||||
}, {
|
||||
title: string;
|
||||
refId: string;
|
||||
taskDefaults?: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
} | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
blockedByRefs?: string[] | undefined;
|
||||
}>, "many">>;
|
||||
created: z.ZodOptional<z.ZodString>;
|
||||
updated: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
name: string;
|
||||
taskDefaults: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
};
|
||||
id?: string | undefined;
|
||||
description?: string | undefined;
|
||||
created?: string | undefined;
|
||||
updated?: string | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
category?: string | undefined;
|
||||
blueprint?: {
|
||||
title: string;
|
||||
refId: string;
|
||||
taskDefaults?: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
} | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
blockedByRefs?: string[] | undefined;
|
||||
}[] | undefined;
|
||||
version?: number | undefined;
|
||||
}, {
|
||||
name: string;
|
||||
taskDefaults: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
};
|
||||
id?: string | undefined;
|
||||
description?: string | undefined;
|
||||
created?: string | undefined;
|
||||
updated?: string | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
category?: string | undefined;
|
||||
blueprint?: {
|
||||
title: string;
|
||||
refId: string;
|
||||
taskDefaults?: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
} | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
blockedByRefs?: string[] | undefined;
|
||||
}[] | undefined;
|
||||
version?: number | undefined;
|
||||
}>, {
|
||||
name: string;
|
||||
taskDefaults: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
};
|
||||
id?: string | undefined;
|
||||
description?: string | undefined;
|
||||
created?: string | undefined;
|
||||
updated?: string | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
category?: string | undefined;
|
||||
blueprint?: {
|
||||
title: string;
|
||||
refId: string;
|
||||
taskDefaults?: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
} | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
blockedByRefs?: string[] | undefined;
|
||||
}[] | undefined;
|
||||
version?: number | undefined;
|
||||
}, {
|
||||
name: string;
|
||||
taskDefaults: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
};
|
||||
id?: string | undefined;
|
||||
description?: string | undefined;
|
||||
created?: string | undefined;
|
||||
updated?: string | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
category?: string | undefined;
|
||||
blueprint?: {
|
||||
title: string;
|
||||
refId: string;
|
||||
taskDefaults?: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
} | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
blockedByRefs?: string[] | undefined;
|
||||
}[] | undefined;
|
||||
version?: number | undefined;
|
||||
}>, z.ZodArray<z.ZodEffects<z.ZodObject<{
|
||||
id: z.ZodOptional<z.ZodString>;
|
||||
name: z.ZodEffects<z.ZodString, string, string>;
|
||||
description: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
|
||||
category: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
|
||||
version: z.ZodOptional<z.ZodNumber>;
|
||||
taskDefaults: z.ZodObject<{
|
||||
type: z.ZodOptional<z.ZodString>;
|
||||
priority: z.ZodOptional<z.ZodString>;
|
||||
project: z.ZodOptional<z.ZodString>;
|
||||
descriptionTemplate: z.ZodOptional<z.ZodString>;
|
||||
agent: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
}, {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
}>;
|
||||
subtaskTemplates: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
title: z.ZodEffects<z.ZodString, string, string>;
|
||||
order: z.ZodOptional<z.ZodNumber>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}, {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}>, "many">>;
|
||||
blueprint: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
refId: z.ZodEffects<z.ZodString, string, string>;
|
||||
title: z.ZodEffects<z.ZodString, string, string>;
|
||||
taskDefaults: z.ZodOptional<z.ZodObject<{
|
||||
type: z.ZodOptional<z.ZodString>;
|
||||
priority: z.ZodOptional<z.ZodString>;
|
||||
project: z.ZodOptional<z.ZodString>;
|
||||
descriptionTemplate: z.ZodOptional<z.ZodString>;
|
||||
agent: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
}, {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
}>>;
|
||||
subtaskTemplates: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
title: z.ZodEffects<z.ZodString, string, string>;
|
||||
order: z.ZodOptional<z.ZodNumber>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}, {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}>, "many">>;
|
||||
blockedByRefs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
title: string;
|
||||
refId: string;
|
||||
taskDefaults?: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
} | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
blockedByRefs?: string[] | undefined;
|
||||
}, {
|
||||
title: string;
|
||||
refId: string;
|
||||
taskDefaults?: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
} | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
blockedByRefs?: string[] | undefined;
|
||||
}>, "many">>;
|
||||
created: z.ZodOptional<z.ZodString>;
|
||||
updated: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
name: string;
|
||||
taskDefaults: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
};
|
||||
id?: string | undefined;
|
||||
description?: string | undefined;
|
||||
created?: string | undefined;
|
||||
updated?: string | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
category?: string | undefined;
|
||||
blueprint?: {
|
||||
title: string;
|
||||
refId: string;
|
||||
taskDefaults?: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
} | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
blockedByRefs?: string[] | undefined;
|
||||
}[] | undefined;
|
||||
version?: number | undefined;
|
||||
}, {
|
||||
name: string;
|
||||
taskDefaults: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
};
|
||||
id?: string | undefined;
|
||||
description?: string | undefined;
|
||||
created?: string | undefined;
|
||||
updated?: string | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
category?: string | undefined;
|
||||
blueprint?: {
|
||||
title: string;
|
||||
refId: string;
|
||||
taskDefaults?: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
} | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
blockedByRefs?: string[] | undefined;
|
||||
}[] | undefined;
|
||||
version?: number | undefined;
|
||||
}>, {
|
||||
name: string;
|
||||
taskDefaults: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
};
|
||||
id?: string | undefined;
|
||||
description?: string | undefined;
|
||||
created?: string | undefined;
|
||||
updated?: string | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
category?: string | undefined;
|
||||
blueprint?: {
|
||||
title: string;
|
||||
refId: string;
|
||||
taskDefaults?: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
} | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
blockedByRefs?: string[] | undefined;
|
||||
}[] | undefined;
|
||||
version?: number | undefined;
|
||||
}, {
|
||||
name: string;
|
||||
taskDefaults: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
};
|
||||
id?: string | undefined;
|
||||
description?: string | undefined;
|
||||
created?: string | undefined;
|
||||
updated?: string | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
category?: string | undefined;
|
||||
blueprint?: {
|
||||
title: string;
|
||||
refId: string;
|
||||
taskDefaults?: {
|
||||
type?: string | undefined;
|
||||
project?: string | undefined;
|
||||
priority?: string | undefined;
|
||||
agent?: string | undefined;
|
||||
descriptionTemplate?: string | undefined;
|
||||
} | undefined;
|
||||
subtaskTemplates?: {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
blockedByRefs?: string[] | undefined;
|
||||
}[] | undefined;
|
||||
version?: number | undefined;
|
||||
}>, "many">]>;
|
||||
/**
|
||||
* Type exports
|
||||
*/
|
||||
export type ValidatedTemplate = z.infer<typeof TaskTemplateSchema>;
|
||||
export type TemplateImport = z.infer<typeof TemplateImportSchema>;
|
||||
114
web/src/lib/template-schema.js
Normal file
114
web/src/lib/template-schema.js
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
/**
|
||||
* Zod validation schema for task templates
|
||||
* Enforces strict validation, size limits, and security checks
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
// Dangerous keys that could lead to prototype pollution
|
||||
const DANGEROUS_KEYS = ['__proto__', 'constructor', 'prototype'];
|
||||
/**
|
||||
* Check if a string contains dangerous keys
|
||||
*/
|
||||
function hasDangerousKeys(str) {
|
||||
return DANGEROUS_KEYS.some(key => str.includes(key));
|
||||
}
|
||||
/**
|
||||
* Subtask template schema
|
||||
*/
|
||||
const SubtaskTemplateSchema = z.object({
|
||||
title: z.string()
|
||||
.min(1, 'Subtask title cannot be empty')
|
||||
.refine(val => !hasDangerousKeys(val), {
|
||||
message: 'Subtask title contains forbidden keys',
|
||||
}),
|
||||
order: z.number().int().min(0).optional(),
|
||||
}).strict();
|
||||
/**
|
||||
* Blueprint task schema
|
||||
*/
|
||||
const BlueprintTaskSchema = z.object({
|
||||
refId: z.string()
|
||||
.min(1, 'Blueprint task refId cannot be empty')
|
||||
.refine(val => !hasDangerousKeys(val), {
|
||||
message: 'Blueprint refId contains forbidden keys',
|
||||
}),
|
||||
title: z.string()
|
||||
.min(1, 'Blueprint task title cannot be empty')
|
||||
.refine(val => !hasDangerousKeys(val), {
|
||||
message: 'Blueprint task title contains forbidden keys',
|
||||
}),
|
||||
taskDefaults: z.object({
|
||||
type: z.string().optional(),
|
||||
priority: z.string().optional(),
|
||||
project: z.string().optional(),
|
||||
descriptionTemplate: z.string().optional(),
|
||||
agent: z.string().optional(),
|
||||
}).strict().optional(),
|
||||
subtaskTemplates: z.array(SubtaskTemplateSchema).optional(),
|
||||
blockedByRefs: z.array(z.string()).optional(),
|
||||
}).strict();
|
||||
/**
|
||||
* Task template schema with strict validation
|
||||
*/
|
||||
export const TaskTemplateSchema = z.object({
|
||||
id: z.string().optional(), // Optional for imports
|
||||
name: z.string()
|
||||
.min(1, 'Template name is required')
|
||||
.max(100, 'Template name must be 100 characters or less')
|
||||
.refine(val => !hasDangerousKeys(val), {
|
||||
message: 'Template name contains forbidden keys',
|
||||
}),
|
||||
description: z.string()
|
||||
.max(500, 'Template description must be 500 characters or less')
|
||||
.optional()
|
||||
.refine(val => !val || !hasDangerousKeys(val), {
|
||||
message: 'Template description contains forbidden keys',
|
||||
}),
|
||||
category: z.string()
|
||||
.optional()
|
||||
.refine(val => !val || !hasDangerousKeys(val), {
|
||||
message: 'Template category contains forbidden keys',
|
||||
}),
|
||||
version: z.number().int().min(0).optional(),
|
||||
taskDefaults: z.object({
|
||||
type: z.string().optional(),
|
||||
priority: z.string().optional(),
|
||||
project: z.string().optional(),
|
||||
descriptionTemplate: z.string().optional(),
|
||||
agent: z.string().optional(),
|
||||
}).strict(),
|
||||
subtaskTemplates: z.array(SubtaskTemplateSchema)
|
||||
.max(50, 'Maximum 50 subtask templates allowed')
|
||||
.optional(),
|
||||
blueprint: z.array(BlueprintTaskSchema)
|
||||
.max(20, 'Maximum 20 blueprint tasks allowed')
|
||||
.optional(),
|
||||
created: z.string().optional(),
|
||||
updated: z.string().optional(),
|
||||
}).strict()
|
||||
.refine((data) => {
|
||||
// Validate blueprint dependency references
|
||||
if (!data.blueprint || data.blueprint.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const refIds = new Set(data.blueprint.map(task => task.refId));
|
||||
for (const task of data.blueprint) {
|
||||
if (task.blockedByRefs) {
|
||||
for (const ref of task.blockedByRefs) {
|
||||
if (!refIds.has(ref)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}, {
|
||||
message: 'Blueprint dependency references must point to valid refIds within the same blueprint',
|
||||
});
|
||||
/**
|
||||
* Schema for importing templates (single or array)
|
||||
*/
|
||||
export const TemplateImportSchema = z.union([
|
||||
TaskTemplateSchema,
|
||||
z.array(TaskTemplateSchema),
|
||||
]);
|
||||
//# sourceMappingURL=template-schema.js.map
|
||||
1
web/src/lib/template-schema.js.map
Normal file
1
web/src/lib/template-schema.js.map
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"template-schema.js","sourceRoot":"","sources":["template-schema.ts"],"names":[],"mappings":"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"}
|
||||
Loading…
Add table
Reference in a new issue