diff --git a/src/app/api/agents/[id]/hide/route.ts b/src/app/api/agents/[id]/hide/route.ts new file mode 100644 index 00000000..4d0ba954 --- /dev/null +++ b/src/app/api/agents/[id]/hide/route.ts @@ -0,0 +1,70 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getDatabase } from '@/lib/db' +import { requireRole } from '@/lib/auth' +import { logger } from '@/lib/logger' + +/** + * POST /api/agents/[id]/hide - Hide an agent from the UI + */ +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const auth = requireRole(request, 'operator') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + + try { + const db = getDatabase() + const { id } = await params + const workspaceId = auth.user.workspace_id ?? 1 + + const idNum = Number(id) + const agent = isNaN(idNum) + ? db.prepare('SELECT id, name FROM agents WHERE name = ? AND workspace_id = ?').get(id, workspaceId) as any + : db.prepare('SELECT id, name FROM agents WHERE id = ? AND workspace_id = ?').get(idNum, workspaceId) as any + + if (!agent) { + return NextResponse.json({ error: 'Agent not found' }, { status: 404 }) + } + + db.prepare('UPDATE agents SET hidden = 1, updated_at = unixepoch() WHERE id = ?').run(agent.id) + + return NextResponse.json({ success: true, agent_id: agent.id, hidden: true }) + } catch (error) { + logger.error({ err: error }, 'POST /api/agents/[id]/hide error') + return NextResponse.json({ error: 'Failed to hide agent' }, { status: 500 }) + } +} + +/** + * DELETE /api/agents/[id]/hide - Unhide an agent + */ +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const auth = requireRole(request, 'operator') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + + try { + const db = getDatabase() + const { id } = await params + const workspaceId = auth.user.workspace_id ?? 1 + + const idNum = Number(id) + const agent = isNaN(idNum) + ? db.prepare('SELECT id, name FROM agents WHERE name = ? AND workspace_id = ?').get(id, workspaceId) as any + : db.prepare('SELECT id, name FROM agents WHERE id = ? AND workspace_id = ?').get(idNum, workspaceId) as any + + if (!agent) { + return NextResponse.json({ error: 'Agent not found' }, { status: 404 }) + } + + db.prepare('UPDATE agents SET hidden = 0, updated_at = unixepoch() WHERE id = ?').run(agent.id) + + return NextResponse.json({ success: true, agent_id: agent.id, hidden: false }) + } catch (error) { + logger.error({ err: error }, 'DELETE /api/agents/[id]/hide error') + return NextResponse.json({ error: 'Failed to unhide agent' }, { status: 500 }) + } +} diff --git a/src/app/api/agents/route.ts b/src/app/api/agents/route.ts index 8e26b484..28e5bb16 100644 --- a/src/app/api/agents/route.ts +++ b/src/app/api/agents/route.ts @@ -29,12 +29,17 @@ export async function GET(request: NextRequest) { // Parse query parameters const status = searchParams.get('status'); const role = searchParams.get('role'); + const showHidden = searchParams.get('show_hidden') === 'true'; const limit = Math.min(parseInt(searchParams.get('limit') || '50'), 200); const offset = parseInt(searchParams.get('offset') || '0'); - + // Build dynamic query let query = 'SELECT * FROM agents WHERE workspace_id = ?'; const params: any[] = [workspaceId]; + + if (!showHidden) { + query += ' AND hidden = 0'; + } if (status) { query += ' AND status = ?'; @@ -116,6 +121,9 @@ export async function GET(request: NextRequest) { // Get total count for pagination let countQuery = 'SELECT COUNT(*) as total FROM agents WHERE workspace_id = ?'; const countParams: any[] = [workspaceId]; + if (!showHidden) { + countQuery += ' AND hidden = 0'; + } if (status) { countQuery += ' AND status = ?'; countParams.push(status); diff --git a/src/components/panels/agent-squad-panel-phase3.tsx b/src/components/panels/agent-squad-panel-phase3.tsx index ff6a25d7..12fa1029 100644 --- a/src/components/panels/agent-squad-panel-phase3.tsx +++ b/src/components/panels/agent-squad-panel-phase3.tsx @@ -104,6 +104,7 @@ export function AgentSquadPanelPhase3() { const [autoRefresh, setAutoRefresh] = useState(true) const [syncing, setSyncing] = useState(false) const [syncToast, setSyncToast] = useState(null) + const [showHidden, setShowHidden] = useState(false) // Sync agents from gateway config or local disk const syncFromConfig = async (source?: 'local') => { @@ -142,7 +143,8 @@ export function AgentSquadPanelPhase3() { setError(null) if (agents.length === 0) setLoading(true) - const response = await fetch('/api/agents') + const url = showHidden ? '/api/agents?show_hidden=true' : '/api/agents' + const response = await fetch(url) if (response.status === 401) { window.location.assign('/login?next=%2Fagents') return @@ -162,7 +164,7 @@ export function AgentSquadPanelPhase3() { } finally { setLoading(false) } - }, [agents.length, setAgents]) + }, [agents.length, setAgents, showHidden]) // Smart polling with visibility pause useSmartPoll(fetchAgents, 30000, { enabled: autoRefresh, pauseWhenSseConnected: true }) @@ -223,6 +225,25 @@ export function AgentSquadPanelPhase3() { } } + // Re-fetch when showHidden changes + useEffect(() => { + fetchAgents() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [showHidden]) + + const toggleAgentHidden = async (agentId: number, hide: boolean) => { + try { + const response = await fetch(`/api/agents/${agentId}/hide`, { + method: hide ? 'POST' : 'DELETE', + }) + if (!response.ok) throw new Error('Failed to update visibility') + fetchAgents() + } catch (error) { + log.error('Failed to toggle agent visibility:', error) + setError('Failed to update agent visibility') + } + } + const deleteAgent = async (agentId: number, removeWorkspace: boolean) => { const previousAgents = agents setAgents(agents.filter((agent) => agent.id !== agentId)) @@ -333,6 +354,13 @@ export function AgentSquadPanelPhase3() { > {t('syncLocal')} + + diff --git a/src/components/panels/task-board-panel.tsx b/src/components/panels/task-board-panel.tsx index ed601ae9..c8d3bbd0 100644 --- a/src/components/panels/task-board-panel.tsx +++ b/src/components/panels/task-board-panel.tsx @@ -773,13 +773,13 @@ export function TaskBoardPanel() { )} {/* Kanban Board */} -
+
{statusColumns.map(column => (
handleDragEnter(e, column.key)} onDragLeave={handleDragLeave} onDragOver={handleDragOver} diff --git a/src/lib/migrations.ts b/src/lib/migrations.ts index b60bf162..b3a893e3 100644 --- a/src/lib/migrations.ts +++ b/src/lib/migrations.ts @@ -1262,6 +1262,12 @@ const migrations: Migration[] = [ db.exec(`CREATE INDEX IF NOT EXISTS idx_gateway_health_logs_gateway_id ON gateway_health_logs(gateway_id)`) db.exec(`CREATE INDEX IF NOT EXISTS idx_gateway_health_logs_probed_at ON gateway_health_logs(probed_at)`) } + }, + { + id: '042_agent_hidden', + up(db: Database.Database) { + db.exec(`ALTER TABLE agents ADD COLUMN hidden INTEGER NOT NULL DEFAULT 0`) + } } ] diff --git a/src/store/index.ts b/src/store/index.ts index 658343ff..a5b0188f 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -141,6 +141,7 @@ export interface Agent { last_activity?: string created_at: number updated_at: number + hidden?: number config?: JsonValue taskStats?: { total: number