Merge pull request #26 from builderz-labs/fix/p2-quality

fix: P2 quality — strict mode, tests, pagination, N+1, CSP
This commit is contained in:
nyk 2026-02-27 14:03:34 +07:00 committed by GitHub
commit 5e94d79e66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 266 additions and 150 deletions

View File

@ -8,7 +8,7 @@ const nextConfig = {
const csp = [ const csp = [
`default-src 'self'`, `default-src 'self'`,
`script-src 'self' 'unsafe-inline' 'unsafe-eval'${googleEnabled ? ' https://accounts.google.com' : ''}`, `script-src 'self' 'unsafe-inline'${googleEnabled ? ' https://accounts.google.com' : ''}`,
`style-src 'self' 'unsafe-inline'`, `style-src 'self' 'unsafe-inline'`,
`connect-src 'self' ws: wss: http://127.0.0.1:* http://localhost:*`, `connect-src 'self' ws: wss: http://127.0.0.1:* http://localhost:*`,
`img-src 'self' data: blob:${googleEnabled ? ' https://*.googleusercontent.com https://lh3.googleusercontent.com' : ''}`, `img-src 'self' data: blob:${googleEnabled ? ' https://*.googleusercontent.com https://lh3.googleusercontent.com' : ''}`,
@ -22,7 +22,6 @@ const nextConfig = {
headers: [ headers: [
{ key: 'X-Frame-Options', value: 'DENY' }, { key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-XSS-Protection', value: '1; mode=block' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Content-Security-Policy', value: csp }, { key: 'Content-Security-Policy', value: csp },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }, { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },

View File

@ -72,48 +72,52 @@ async function handleActivitiesRequest(request: NextRequest) {
const stmt = db.prepare(query); const stmt = db.prepare(query);
const activities = stmt.all(...params) as Activity[]; const activities = stmt.all(...params) as Activity[];
// Prepare entity detail statements once (avoids N+1)
const taskDetailStmt = db.prepare('SELECT id, title, status FROM tasks WHERE id = ?');
const agentDetailStmt = db.prepare('SELECT id, name, role, status FROM agents WHERE id = ?');
const commentDetailStmt = db.prepare(`
SELECT c.id, c.content, c.task_id, t.title as task_title
FROM comments c
LEFT JOIN tasks t ON c.task_id = t.id
WHERE c.id = ?
`);
// Parse JSON data field and enhance with related entity data // Parse JSON data field and enhance with related entity data
const enhancedActivities = activities.map(activity => { const enhancedActivities = activities.map(activity => {
let entityDetails = null; let entityDetails = null;
try { try {
// Fetch related entity details based on entity_type
switch (activity.entity_type) { switch (activity.entity_type) {
case 'task': case 'task': {
const task = db.prepare('SELECT id, title, status FROM tasks WHERE id = ?').get(activity.entity_id) as any; const task = taskDetailStmt.get(activity.entity_id) as any;
if (task) { if (task) {
entityDetails = { type: 'task', ...task }; entityDetails = { type: 'task', ...task };
} }
break; break;
}
case 'agent': case 'agent': {
const agent = db.prepare('SELECT id, name, role, status FROM agents WHERE id = ?').get(activity.entity_id) as any; const agent = agentDetailStmt.get(activity.entity_id) as any;
if (agent) { if (agent) {
entityDetails = { type: 'agent', ...agent }; entityDetails = { type: 'agent', ...agent };
} }
break; break;
}
case 'comment': case 'comment': {
const comment = db.prepare(` const comment = commentDetailStmt.get(activity.entity_id) as any;
SELECT c.id, c.content, c.task_id, t.title as task_title
FROM comments c
LEFT JOIN tasks t ON c.task_id = t.id
WHERE c.id = ?
`).get(activity.entity_id) as any;
if (comment) { if (comment) {
entityDetails = { entityDetails = {
type: 'comment', type: 'comment',
...comment, ...comment,
content_preview: comment.content?.substring(0, 100) || '' content_preview: comment.content?.substring(0, 100) || ''
}; };
} }
break; break;
}
} }
} catch (error) { } catch (error) {
// If entity lookup fails, continue without entity details
console.warn(`Failed to fetch entity details for activity ${activity.id}:`, error); console.warn(`Failed to fetch entity details for activity ${activity.id}:`, error);
} }
return { return {
...activity, ...activity,
data: activity.data ? JSON.parse(activity.data) : null, data: activity.data ? JSON.parse(activity.data) : null,

View File

@ -25,7 +25,7 @@ export async function GET(
const agentId = resolvedParams.id; const agentId = resolvedParams.id;
// Get agent by ID or name // Get agent by ID or name
let agent; let agent: any;
if (isNaN(Number(agentId))) { if (isNaN(Number(agentId))) {
// Lookup by name // Lookup by name
agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId); agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId);

View File

@ -21,7 +21,7 @@ export async function GET(
const agentId = resolvedParams.id; const agentId = resolvedParams.id;
// Get agent by ID or name // Get agent by ID or name
let agent; let agent: any;
if (isNaN(Number(agentId))) { if (isNaN(Number(agentId))) {
agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId); agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId);
} else { } else {
@ -81,7 +81,7 @@ export async function PUT(
const { working_memory, append } = body; const { working_memory, append } = body;
// Get agent by ID or name // Get agent by ID or name
let agent; let agent: any;
if (isNaN(Number(agentId))) { if (isNaN(Number(agentId))) {
agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId); agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId);
} else { } else {
@ -168,7 +168,7 @@ export async function DELETE(
const agentId = resolvedParams.id; const agentId = resolvedParams.id;
// Get agent by ID or name // Get agent by ID or name
let agent; let agent: any;
if (isNaN(Number(agentId))) { if (isNaN(Number(agentId))) {
agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId); agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId);
} else { } else {

View File

@ -22,7 +22,7 @@ export async function GET(
const agentId = resolvedParams.id; const agentId = resolvedParams.id;
// Get agent by ID or name // Get agent by ID or name
let agent; let agent: any;
if (isNaN(Number(agentId))) { if (isNaN(Number(agentId))) {
agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId); agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId);
} else { } else {
@ -81,7 +81,7 @@ export async function PUT(
const { soul_content, template_name } = body; const { soul_content, template_name } = body;
// Get agent by ID or name // Get agent by ID or name
let agent; let agent: any;
if (isNaN(Number(agentId))) { if (isNaN(Number(agentId))) {
agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId); agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId);
} else { } else {

View File

@ -50,20 +50,20 @@ export async function GET(request: NextRequest) {
config: agent.config ? JSON.parse(agent.config) : {} config: agent.config ? JSON.parse(agent.config) : {}
})); }));
// Get task counts for each agent // Get task counts for each agent (prepare once, reuse per agent)
const taskCountStmt = db.prepare(`
SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'assigned' THEN 1 ELSE 0 END) as assigned,
SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as completed
FROM tasks
WHERE assigned_to = ?
`);
const agentsWithStats = agentsWithParsedData.map(agent => { const agentsWithStats = agentsWithParsedData.map(agent => {
const taskCountStmt = db.prepare(`
SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'assigned' THEN 1 ELSE 0 END) as assigned,
SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as completed
FROM tasks
WHERE assigned_to = ?
`);
const taskStats = taskCountStmt.get(agent.name) as any; const taskStats = taskCountStmt.get(agent.name) as any;
return { return {
...agent, ...agent,
taskStats: { taskStats: {
@ -75,9 +75,24 @@ export async function GET(request: NextRequest) {
}; };
}); });
return NextResponse.json({ // Get total count for pagination
agents: agentsWithStats, let countQuery = 'SELECT COUNT(*) as total FROM agents WHERE 1=1';
total: agents.length const countParams: any[] = [];
if (status) {
countQuery += ' AND status = ?';
countParams.push(status);
}
if (role) {
countQuery += ' AND role = ?';
countParams.push(role);
}
const countRow = db.prepare(countQuery).get(...countParams) as { total: number };
return NextResponse.json({
agents: agentsWithStats,
total: countRow.total,
page: Math.floor(offset / limit) + 1,
limit
}); });
} catch (error) { } catch (error) {
console.error('GET /api/agents error:', error); console.error('GET /api/agents error:', error);

View File

@ -55,14 +55,16 @@ export async function GET(request: NextRequest) {
const conversations = db.prepare(query).all(...params) as any[] const conversations = db.prepare(query).all(...params) as any[]
// Fetch the last message for each conversation // Prepare last message statement once (avoids N+1)
const lastMsgStmt = db.prepare(`
SELECT * FROM messages
WHERE conversation_id = ?
ORDER BY created_at DESC
LIMIT 1
`);
const withLastMessage = conversations.map((conv) => { const withLastMessage = conversations.map((conv) => {
const lastMsg = db.prepare(` const lastMsg = lastMsgStmt.get(conv.conversation_id) as any;
SELECT * FROM messages
WHERE conversation_id = ?
ORDER BY created_at DESC
LIMIT 1
`).get(conv.conversation_id) as any
return { return {
...conv, ...conv,
@ -75,7 +77,22 @@ export async function GET(request: NextRequest) {
} }
}) })
return NextResponse.json({ conversations: withLastMessage, total: withLastMessage.length }) // Get total count for pagination
let countQuery: string
const countParams: any[] = []
if (agent) {
countQuery = `
SELECT COUNT(DISTINCT m.conversation_id) as total
FROM messages m
WHERE m.from_agent = ? OR m.to_agent = ? OR m.to_agent IS NULL
`
countParams.push(agent, agent)
} else {
countQuery = 'SELECT COUNT(DISTINCT conversation_id) as total FROM messages'
}
const countRow = db.prepare(countQuery).get(...countParams) as { total: number }
return NextResponse.json({ conversations: withLastMessage, total: countRow.total, page: Math.floor(offset / limit) + 1, limit })
} catch (error) { } catch (error) {
console.error('GET /api/chat/conversations error:', error) console.error('GET /api/chat/conversations error:', error)
return NextResponse.json({ error: 'Failed to fetch conversations' }, { status: 500 }) return NextResponse.json({ error: 'Failed to fetch conversations' }, { status: 500 })

View File

@ -143,7 +143,28 @@ export async function GET(request: NextRequest) {
metadata: msg.metadata ? JSON.parse(msg.metadata) : null metadata: msg.metadata ? JSON.parse(msg.metadata) : null
})) }))
return NextResponse.json({ messages: parsed, total: parsed.length }) // Get total count for pagination
let countQuery = 'SELECT COUNT(*) as total FROM messages WHERE 1=1'
const countParams: any[] = []
if (conversation_id) {
countQuery += ' AND conversation_id = ?'
countParams.push(conversation_id)
}
if (from_agent) {
countQuery += ' AND from_agent = ?'
countParams.push(from_agent)
}
if (to_agent) {
countQuery += ' AND to_agent = ?'
countParams.push(to_agent)
}
if (since) {
countQuery += ' AND created_at > ?'
countParams.push(parseInt(since))
}
const countRow = db.prepare(countQuery).get(...countParams) as { total: number }
return NextResponse.json({ messages: parsed, total: countRow.total, page: Math.floor(offset / limit) + 1, limit })
} catch (error) { } catch (error) {
console.error('GET /api/chat/messages error:', error) console.error('GET /api/chat/messages error:', error)
return NextResponse.json({ error: 'Failed to fetch messages' }, { status: 500 }) return NextResponse.json({ error: 'Failed to fetch messages' }, { status: 500 })

View File

@ -47,6 +47,14 @@ export async function POST(request: NextRequest) {
const db = getDatabase() const db = getDatabase()
const gateways = db.prepare("SELECT * FROM gateways ORDER BY is_primary DESC, name ASC").all() as GatewayEntry[] const gateways = db.prepare("SELECT * FROM gateways ORDER BY is_primary DESC, name ASC").all() as GatewayEntry[]
// Prepare update statements once (avoids N+1)
const updateOnlineStmt = db.prepare(
"UPDATE gateways SET status = ?, latency = ?, last_seen = (unixepoch()), updated_at = (unixepoch()) WHERE id = ?"
)
const updateOfflineStmt = db.prepare(
"UPDATE gateways SET status = ?, latency = NULL, updated_at = (unixepoch()) WHERE id = ?"
)
const results: HealthResult[] = [] const results: HealthResult[] = []
for (const gw of gateways) { for (const gw of gateways) {
@ -70,9 +78,7 @@ export async function POST(request: NextRequest) {
const latency = Date.now() - start const latency = Date.now() - start
const status = res.ok ? "online" : "error" const status = res.ok ? "online" : "error"
db.prepare( updateOnlineStmt.run(status, latency, gw.id)
"UPDATE gateways SET status = ?, latency = ?, last_seen = (unixepoch()), updated_at = (unixepoch()) WHERE id = ?"
).run(status, latency, gw.id)
results.push({ results.push({
id: gw.id, id: gw.id,
@ -83,9 +89,7 @@ export async function POST(request: NextRequest) {
sessions_count: 0, sessions_count: 0,
}) })
} catch (err: any) { } catch (err: any) {
db.prepare( updateOfflineStmt.run("offline", gw.id)
"UPDATE gateways SET status = ?, latency = NULL, updated_at = (unixepoch()) WHERE id = ?"
).run("offline", gw.id)
results.push({ results.push({
id: gw.id, id: gw.id,

View File

@ -56,7 +56,10 @@ export async function POST(request: NextRequest) {
let errorCount = 0; let errorCount = 0;
const errors: any[] = []; const errors: any[] = [];
const deliveryResults: any[] = []; const deliveryResults: any[] = [];
// Prepare update statement once (avoids N+1)
const markDeliveredStmt = db.prepare('UPDATE notifications SET delivered_at = ? WHERE id = ?');
for (const notification of undeliveredNotifications) { for (const notification of undeliveredNotifications) {
try { try {
// Skip if agent doesn't have session key // Skip if agent doesn't have session key
@ -94,8 +97,7 @@ export async function POST(request: NextRequest) {
// Mark as delivered // Mark as delivered
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
db.prepare('UPDATE notifications SET delivered_at = ? WHERE id = ?') markDeliveredStmt.run(now, notification.id);
.run(now, notification.id);
deliveredCount++; deliveredCount++;
deliveryResults.push({ deliveryResults.push({

View File

@ -44,48 +44,54 @@ export async function GET(request: NextRequest) {
const stmt = db.prepare(query); const stmt = db.prepare(query);
const notifications = stmt.all(...params) as Notification[]; const notifications = stmt.all(...params) as Notification[];
// Prepare source detail statements once (avoids N+1)
const taskDetailStmt = db.prepare('SELECT id, title, status FROM tasks WHERE id = ?');
const commentDetailStmt = db.prepare(`
SELECT c.id, c.content, c.task_id, t.title as task_title
FROM comments c
LEFT JOIN tasks t ON c.task_id = t.id
WHERE c.id = ?
`);
const agentDetailStmt = db.prepare('SELECT id, name, role, status FROM agents WHERE id = ?');
// Enhance notifications with related entity data // Enhance notifications with related entity data
const enhancedNotifications = notifications.map(notification => { const enhancedNotifications = notifications.map(notification => {
let sourceDetails = null; let sourceDetails = null;
try { try {
if (notification.source_type && notification.source_id) { if (notification.source_type && notification.source_id) {
switch (notification.source_type) { switch (notification.source_type) {
case 'task': case 'task': {
const task = db.prepare('SELECT id, title, status FROM tasks WHERE id = ?').get(notification.source_id) as any; const task = taskDetailStmt.get(notification.source_id) as any;
if (task) { if (task) {
sourceDetails = { type: 'task', ...task }; sourceDetails = { type: 'task', ...task };
} }
break; break;
}
case 'comment': case 'comment': {
const comment = db.prepare(` const comment = commentDetailStmt.get(notification.source_id) as any;
SELECT c.id, c.content, c.task_id, t.title as task_title
FROM comments c
LEFT JOIN tasks t ON c.task_id = t.id
WHERE c.id = ?
`).get(notification.source_id) as any;
if (comment) { if (comment) {
sourceDetails = { sourceDetails = {
type: 'comment', type: 'comment',
...comment, ...comment,
content_preview: comment.content?.substring(0, 100) || '' content_preview: comment.content?.substring(0, 100) || ''
}; };
} }
break; break;
}
case 'agent': case 'agent': {
const agent = db.prepare('SELECT id, name, role, status FROM agents WHERE id = ?').get(notification.source_id) as any; const agent = agentDetailStmt.get(notification.source_id) as any;
if (agent) { if (agent) {
sourceDetails = { type: 'agent', ...agent }; sourceDetails = { type: 'agent', ...agent };
} }
break; break;
}
} }
} }
} catch (error) { } catch (error) {
console.warn(`Failed to fetch source details for notification ${notification.id}:`, error); console.warn(`Failed to fetch source details for notification ${notification.id}:`, error);
} }
return { return {
...notification, ...notification,
source: sourceDetails source: sourceDetails
@ -99,9 +105,23 @@ export async function GET(request: NextRequest) {
WHERE recipient = ? AND read_at IS NULL WHERE recipient = ? AND read_at IS NULL
`).get(recipient) as { count: number }; `).get(recipient) as { count: number };
return NextResponse.json({ // Get total count for pagination
let countQuery = 'SELECT COUNT(*) as total FROM notifications WHERE recipient = ?';
const countParams: any[] = [recipient];
if (unread_only) {
countQuery += ' AND read_at IS NULL';
}
if (type) {
countQuery += ' AND type = ?';
countParams.push(type);
}
const countRow = db.prepare(countQuery).get(...countParams) as { total: number };
return NextResponse.json({
notifications: enhancedNotifications, notifications: enhancedNotifications,
total: notifications.length, total: countRow.total,
page: Math.floor(offset / limit) + 1,
limit,
unreadCount: unreadCount.count unreadCount: unreadCount.count
}); });
} catch (error) { } catch (error) {

View File

@ -36,71 +36,67 @@ export async function POST(request: NextRequest) {
const agents = db.prepare(agentQuery).all(...agentParams) as any[]; const agents = db.prepare(agentQuery).all(...agentParams) as any[];
// Prepare statements once (avoids N+1 per agent)
const completedTasksStmt = db.prepare(`
SELECT id, title, status, updated_at
FROM tasks
WHERE assigned_to = ?
AND status = 'done'
AND updated_at BETWEEN ? AND ?
ORDER BY updated_at DESC
`);
const inProgressTasksStmt = db.prepare(`
SELECT id, title, status, created_at, due_date
FROM tasks
WHERE assigned_to = ?
AND status = 'in_progress'
ORDER BY created_at ASC
`);
const assignedTasksStmt = db.prepare(`
SELECT id, title, status, created_at, due_date, priority
FROM tasks
WHERE assigned_to = ?
AND status = 'assigned'
ORDER BY priority DESC, created_at ASC
`);
const reviewTasksStmt = db.prepare(`
SELECT id, title, status, updated_at
FROM tasks
WHERE assigned_to = ?
AND status IN ('review', 'quality_review')
ORDER BY updated_at ASC
`);
const blockedTasksStmt = db.prepare(`
SELECT id, title, status, priority, created_at, metadata
FROM tasks
WHERE assigned_to = ?
AND (priority = 'urgent' OR metadata LIKE '%blocked%')
AND status NOT IN ('done')
ORDER BY priority DESC, created_at ASC
`);
const activityCountStmt = db.prepare(`
SELECT COUNT(*) as count
FROM activities
WHERE actor = ?
AND created_at BETWEEN ? AND ?
`);
const commentCountStmt = db.prepare(`
SELECT COUNT(*) as count
FROM comments
WHERE author = ?
AND created_at BETWEEN ? AND ?
`);
// Generate standup data for each agent // Generate standup data for each agent
const standupData = agents.map(agent => { const standupData = agents.map(agent => {
// Completed tasks today const completedTasks = completedTasksStmt.all(agent.name, startOfDay, endOfDay);
const completedTasks = db.prepare(` const inProgressTasks = inProgressTasksStmt.all(agent.name);
SELECT id, title, status, updated_at const assignedTasks = assignedTasksStmt.all(agent.name);
FROM tasks const reviewTasks = reviewTasksStmt.all(agent.name);
WHERE assigned_to = ? const blockedTasks = blockedTasksStmt.all(agent.name);
AND status = 'done' const activityCount = activityCountStmt.get(agent.name, startOfDay, endOfDay) as { count: number };
AND updated_at BETWEEN ? AND ? const commentsToday = commentCountStmt.get(agent.name, startOfDay, endOfDay) as { count: number };
ORDER BY updated_at DESC
`).all(agent.name, startOfDay, endOfDay);
// Currently in progress tasks
const inProgressTasks = db.prepare(`
SELECT id, title, status, created_at, due_date
FROM tasks
WHERE assigned_to = ?
AND status = 'in_progress'
ORDER BY created_at ASC
`).all(agent.name);
// Assigned but not started tasks
const assignedTasks = db.prepare(`
SELECT id, title, status, created_at, due_date, priority
FROM tasks
WHERE assigned_to = ?
AND status = 'assigned'
ORDER BY priority DESC, created_at ASC
`).all(agent.name);
// Review tasks
const reviewTasks = db.prepare(`
SELECT id, title, status, updated_at
FROM tasks
WHERE assigned_to = ?
AND status IN ('review', 'quality_review')
ORDER BY updated_at ASC
`).all(agent.name);
// Blocked/high priority tasks
const blockedTasks = db.prepare(`
SELECT id, title, status, priority, created_at, metadata
FROM tasks
WHERE assigned_to = ?
AND (priority = 'urgent' OR metadata LIKE '%blocked%')
AND status NOT IN ('done')
ORDER BY priority DESC, created_at ASC
`).all(agent.name);
// Recent activity count
const activityCount = db.prepare(`
SELECT COUNT(*) as count
FROM activities
WHERE actor = ?
AND created_at BETWEEN ? AND ?
`).get(agent.name, startOfDay, endOfDay) as { count: number };
// Comments made today
const commentsToday = db.prepare(`
SELECT COUNT(*) as count
FROM comments
WHERE author = ?
AND created_at BETWEEN ? AND ?
`).get(agent.name, startOfDay, endOfDay) as { count: number };
return { return {
agent: { agent: {
name: agent.name, name: agent.name,
@ -239,9 +235,13 @@ export async function GET(request: NextRequest) {
}; };
}); });
return NextResponse.json({ const countRow = db.prepare('SELECT COUNT(*) as total FROM standup_reports').get() as { total: number };
return NextResponse.json({
history: standupHistory, history: standupHistory,
total: standupHistory.length total: countRow.total,
page: Math.floor(offset / limit) + 1,
limit
}); });
} catch (error) { } catch (error) {
console.error('GET /api/standup/history error:', error); console.error('GET /api/standup/history error:', error);

View File

@ -64,7 +64,24 @@ export async function GET(request: NextRequest) {
metadata: task.metadata ? JSON.parse(task.metadata) : {} metadata: task.metadata ? JSON.parse(task.metadata) : {}
})); }));
return NextResponse.json({ tasks: tasksWithParsedData, total: tasks.length }); // Get total count for pagination
let countQuery = 'SELECT COUNT(*) as total FROM tasks WHERE 1=1';
const countParams: any[] = [];
if (status) {
countQuery += ' AND status = ?';
countParams.push(status);
}
if (assigned_to) {
countQuery += ' AND assigned_to = ?';
countParams.push(assigned_to);
}
if (priority) {
countQuery += ' AND priority = ?';
countParams.push(priority);
}
const countRow = db.prepare(countQuery).get(...countParams) as { total: number };
return NextResponse.json({ tasks: tasksWithParsedData, total: countRow.total, page: Math.floor(offset / limit) + 1, limit });
} catch (error) { } catch (error) {
console.error('GET /api/tasks error:', error); console.error('GET /api/tasks error:', error);
return NextResponse.json({ error: 'Failed to fetch tasks' }, { status: 500 }); return NextResponse.json({ error: 'Failed to fetch tasks' }, { status: 500 });

View File

@ -129,7 +129,7 @@ export function MemoryBrowserPanel() {
// Enhanced editing functionality // Enhanced editing functionality
const startEditing = () => { const startEditing = () => {
setIsEditing(true) setIsEditing(true)
setEditedContent(memoryContent) setEditedContent(memoryContent ?? '')
} }
const cancelEditing = () => { const cancelEditing = () => {

View File

@ -201,7 +201,7 @@ export function TokenDashboardPanel() {
const getAlerts = () => { const getAlerts = () => {
const alerts = [] const alerts = []
if (usageStats?.summary.totalCost > 100) { if (usageStats && usageStats.summary.totalCost !== undefined && usageStats.summary.totalCost > 100) {
alerts.push({ alerts.push({
type: 'warning', type: 'warning',
title: 'High Usage Cost', title: 'High Usage Cost',
@ -210,7 +210,7 @@ export function TokenDashboardPanel() {
}) })
} }
if (performanceMetrics?.savingsPercentage > 20) { if (performanceMetrics && performanceMetrics.savingsPercentage !== undefined && performanceMetrics.savingsPercentage > 20) {
alerts.push({ alerts.push({
type: 'info', type: 'info',
title: 'Optimization Opportunity', title: 'Optimization Opportunity',
@ -219,7 +219,7 @@ export function TokenDashboardPanel() {
}) })
} }
if (usageStats?.summary.requestCount > 1000) { if (usageStats && usageStats.summary.requestCount !== undefined && usageStats.summary.requestCount > 1000) {
alerts.push({ alerts.push({
type: 'info', type: 'info',
title: 'High Request Volume', title: 'High Request Volume',

View File

@ -0,0 +1,17 @@
import { describe, it, expect, vi } from 'vitest'
// Test stubs for auth utilities
// safeCompare will be added by fix/p0-security-critical branch
describe('requireRole', () => {
it.todo('returns user when authenticated with sufficient role')
it.todo('returns 401 when no authentication provided')
it.todo('returns 403 when role is insufficient')
})
describe('safeCompare', () => {
it.todo('returns true for matching strings')
it.todo('returns false for non-matching strings')
it.todo('returns false for different length strings')
it.todo('handles empty strings')
})

View File

@ -8,7 +8,7 @@
], ],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": false, "strict": true,
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,
"module": "esnext", "module": "esnext",