fix: Implement Phase 2 major fixes

Database:
- Add missing indexes for api_key_hash, status, metrics queries
- New migration: 005_add_missing_indexes.sql

Server:
- Fix WebSocket Ping/Pong protocol (RFC 6455 compliance)
- Use separate channel for Pong responses

Agent:
- Replace format!() path construction with PathBuf::join()
- Replace todo!() macros with proper errors for macOS support

Dashboard:
- Fix duplicate filter values in Agents page (__unassigned__ sentinel)
- Add onError handlers to all mutations in Agents, Clients, Sites pages

All changes reviewed and approved.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 21:23:36 -07:00
parent 65086f4407
commit b298a8aa17
7 changed files with 127 additions and 26 deletions

View File

@@ -0,0 +1,26 @@
-- Migration: Add missing indexes for performance
-- These indexes were identified during code review as missing
-- Date: 2026-01-20
-- Index for agent API key lookups (authentication)
-- Supports legacy authentication where agents have their own api_key_hash
CREATE INDEX IF NOT EXISTS idx_agents_api_key_hash ON agents(api_key_hash);
-- Index for site API key lookups
-- Note: idx_sites_api_key already exists from migration 002, but using consistent naming
-- This is a no-op if the index already exists
CREATE INDEX IF NOT EXISTS idx_sites_api_key_hash ON sites(api_key_hash);
-- Index for command status queries (find all pending/running commands)
-- Note: idx_commands_agent_status exists for (agent_id, status)
-- This index is for querying by status alone (e.g., find all pending commands)
CREATE INDEX IF NOT EXISTS idx_commands_status ON commands(status);
-- Index for metrics time-range queries
-- Note: idx_metrics_agent_time already exists for (agent_id, timestamp DESC)
-- This is equivalent - creating with requested name for compatibility
CREATE INDEX IF NOT EXISTS idx_metrics_agent_timestamp ON metrics(agent_id, timestamp DESC);
-- Index for finding online agents quickly with last_seen ordering
-- Allows efficient queries like: WHERE status = 'online' ORDER BY last_seen DESC
CREATE INDEX IF NOT EXISTS idx_agents_status_last_seen ON agents(status, last_seen DESC);

View File

@@ -7,7 +7,6 @@
//! - Watchdog event handling
use std::collections::HashMap;
use std::sync::Arc;
use axum::{
extract::{
@@ -18,7 +17,7 @@ use axum::{
};
use futures_util::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use tokio::sync::{mpsc, RwLock};
use tokio::sync::mpsc;
use tracing::{debug, error, info, warn};
use uuid::Uuid;
@@ -253,9 +252,13 @@ pub async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) ->
async fn handle_socket(socket: WebSocket, state: AppState) {
let (mut sender, mut receiver) = socket.split();
// Create channel for outgoing messages
// Create channel for outgoing protocol messages (ServerMessage)
let (tx, mut rx) = mpsc::channel::<ServerMessage>(100);
// Create separate channel for raw WebSocket frames (Pong responses)
// This allows proper WebSocket protocol compliance without changing the public API
let (pong_tx, mut pong_rx) = mpsc::channel::<Vec<u8>>(16);
// Wait for authentication message
let auth_result = match authenticate(&mut receiver, &mut sender, &state).await {
Ok(result) => {
@@ -358,12 +361,26 @@ async fn handle_socket(socket: WebSocket, state: AppState) {
let agent_id = auth_result.agent_id;
// Spawn task to forward outgoing messages
// Handles both protocol messages (ServerMessage) and raw Pong frames
let send_task = tokio::spawn(async move {
while let Some(msg) = rx.recv().await {
if let Ok(json) = serde_json::to_string(&msg) {
if sender.send(Message::Text(json)).await.is_err() {
break;
loop {
tokio::select! {
// Handle protocol messages (ServerMessage -> JSON text)
Some(msg) = rx.recv() => {
if let Ok(json) = serde_json::to_string(&msg) {
if sender.send(Message::Text(json)).await.is_err() {
break;
}
}
}
// Handle Pong responses (WebSocket protocol compliance)
Some(data) = pong_rx.recv() => {
if sender.send(Message::Pong(data)).await.is_err() {
break;
}
}
// Both channels closed
else => break,
}
}
});
@@ -377,11 +394,10 @@ async fn handle_socket(socket: WebSocket, state: AppState) {
}
}
Ok(Message::Ping(data)) => {
if tx
.send(ServerMessage::Ack { message_id: None })
.await
.is_err()
{
// WebSocket protocol requires Pong response with same payload
// Send via pong channel to the send task
if pong_tx.send(data).await.is_err() {
warn!("Failed to send Pong response for agent {}", agent_id);
break;
}
}