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 = [
`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'`,
`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' : ''}`,
@ -22,7 +22,6 @@ const nextConfig = {
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
{ 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: 'Content-Security-Policy', value: csp },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },

View File

@ -72,34 +72,38 @@ async function handleActivitiesRequest(request: NextRequest) {
const stmt = db.prepare(query);
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
const enhancedActivities = activities.map(activity => {
let entityDetails = null;
try {
// Fetch related entity details based on entity_type
switch (activity.entity_type) {
case 'task':
const task = db.prepare('SELECT id, title, status FROM tasks WHERE id = ?').get(activity.entity_id) as any;
case 'task': {
const task = taskDetailStmt.get(activity.entity_id) as any;
if (task) {
entityDetails = { type: 'task', ...task };
}
break;
case 'agent':
const agent = db.prepare('SELECT id, name, role, status FROM agents WHERE id = ?').get(activity.entity_id) as any;
}
case 'agent': {
const agent = agentDetailStmt.get(activity.entity_id) as any;
if (agent) {
entityDetails = { type: 'agent', ...agent };
}
break;
case 'comment':
const comment = 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 = ?
`).get(activity.entity_id) as any;
}
case 'comment': {
const comment = commentDetailStmt.get(activity.entity_id) as any;
if (comment) {
entityDetails = {
type: 'comment',
@ -108,9 +112,9 @@ async function handleActivitiesRequest(request: NextRequest) {
};
}
break;
}
}
} catch (error) {
// If entity lookup fails, continue without entity details
console.warn(`Failed to fetch entity details for activity ${activity.id}:`, error);
}

View File

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

View File

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

View File

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

View File

@ -50,18 +50,18 @@ export async function GET(request: NextRequest) {
config: agent.config ? JSON.parse(agent.config) : {}
}));
// Get task counts for each 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 = ?
`);
// 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 taskStats = taskCountStmt.get(agent.name) as any;
return {
@ -75,9 +75,24 @@ export async function GET(request: NextRequest) {
};
});
// Get total count for pagination
let countQuery = 'SELECT COUNT(*) as total FROM agents WHERE 1=1';
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: agents.length
total: countRow.total,
page: Math.floor(offset / limit) + 1,
limit
});
} catch (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[]
// 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 lastMsg = db.prepare(`
SELECT * FROM messages
WHERE conversation_id = ?
ORDER BY created_at DESC
LIMIT 1
`).get(conv.conversation_id) as any
const lastMsg = lastMsgStmt.get(conv.conversation_id) as any;
return {
...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) {
console.error('GET /api/chat/conversations error:', error)
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
}))
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) {
console.error('GET /api/chat/messages error:', error)
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 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[] = []
for (const gw of gateways) {
@ -70,9 +78,7 @@ export async function POST(request: NextRequest) {
const latency = Date.now() - start
const status = res.ok ? "online" : "error"
db.prepare(
"UPDATE gateways SET status = ?, latency = ?, last_seen = (unixepoch()), updated_at = (unixepoch()) WHERE id = ?"
).run(status, latency, gw.id)
updateOnlineStmt.run(status, latency, gw.id)
results.push({
id: gw.id,
@ -83,9 +89,7 @@ export async function POST(request: NextRequest) {
sessions_count: 0,
})
} catch (err: any) {
db.prepare(
"UPDATE gateways SET status = ?, latency = NULL, updated_at = (unixepoch()) WHERE id = ?"
).run("offline", gw.id)
updateOfflineStmt.run("offline", gw.id)
results.push({
id: gw.id,

View File

@ -57,6 +57,9 @@ export async function POST(request: NextRequest) {
const errors: 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) {
try {
// Skip if agent doesn't have session key
@ -94,8 +97,7 @@ export async function POST(request: NextRequest) {
// Mark as delivered
const now = Math.floor(Date.now() / 1000);
db.prepare('UPDATE notifications SET delivered_at = ? WHERE id = ?')
.run(now, notification.id);
markDeliveredStmt.run(now, notification.id);
deliveredCount++;
deliveryResults.push({

View File

@ -44,6 +44,16 @@ export async function GET(request: NextRequest) {
const stmt = db.prepare(query);
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
const enhancedNotifications = notifications.map(notification => {
let sourceDetails = null;
@ -51,20 +61,15 @@ export async function GET(request: NextRequest) {
try {
if (notification.source_type && notification.source_id) {
switch (notification.source_type) {
case 'task':
const task = db.prepare('SELECT id, title, status FROM tasks WHERE id = ?').get(notification.source_id) as any;
case 'task': {
const task = taskDetailStmt.get(notification.source_id) as any;
if (task) {
sourceDetails = { type: 'task', ...task };
}
break;
case 'comment':
const comment = 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 = ?
`).get(notification.source_id) as any;
}
case 'comment': {
const comment = commentDetailStmt.get(notification.source_id) as any;
if (comment) {
sourceDetails = {
type: 'comment',
@ -73,13 +78,14 @@ export async function GET(request: NextRequest) {
};
}
break;
case 'agent':
const agent = db.prepare('SELECT id, name, role, status FROM agents WHERE id = ?').get(notification.source_id) as any;
}
case 'agent': {
const agent = agentDetailStmt.get(notification.source_id) as any;
if (agent) {
sourceDetails = { type: 'agent', ...agent };
}
break;
}
}
}
} catch (error) {
@ -99,9 +105,23 @@ export async function GET(request: NextRequest) {
WHERE recipient = ? AND read_at IS NULL
`).get(recipient) as { count: number };
// 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,
total: notifications.length,
total: countRow.total,
page: Math.floor(offset / limit) + 1,
limit,
unreadCount: unreadCount.count
});
} catch (error) {

View File

@ -36,70 +36,66 @@ export async function POST(request: NextRequest) {
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
const standupData = agents.map(agent => {
// Completed tasks today
const completedTasks = 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
`).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 };
const completedTasks = completedTasksStmt.all(agent.name, startOfDay, endOfDay);
const inProgressTasks = inProgressTasksStmt.all(agent.name);
const assignedTasks = assignedTasksStmt.all(agent.name);
const reviewTasks = reviewTasksStmt.all(agent.name);
const blockedTasks = blockedTasksStmt.all(agent.name);
const activityCount = activityCountStmt.get(agent.name, startOfDay, endOfDay) as { count: number };
const commentsToday = commentCountStmt.get(agent.name, startOfDay, endOfDay) as { count: number };
return {
agent: {
@ -239,9 +235,13 @@ export async function GET(request: NextRequest) {
};
});
const countRow = db.prepare('SELECT COUNT(*) as total FROM standup_reports').get() as { total: number };
return NextResponse.json({
history: standupHistory,
total: standupHistory.length
total: countRow.total,
page: Math.floor(offset / limit) + 1,
limit
});
} catch (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) : {}
}));
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) {
console.error('GET /api/tasks error:', error);
return NextResponse.json({ error: 'Failed to fetch tasks' }, { status: 500 });

View File

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

View File

@ -201,7 +201,7 @@ export function TokenDashboardPanel() {
const getAlerts = () => {
const alerts = []
if (usageStats?.summary.totalCost > 100) {
if (usageStats && usageStats.summary.totalCost !== undefined && usageStats.summary.totalCost > 100) {
alerts.push({
type: 'warning',
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({
type: 'info',
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({
type: 'info',
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,
"skipLibCheck": true,
"strict": false,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",