Building a Multi-Agent Dashboard: Real-Time Monitoring with Qdrant and Kanban

How I built a real-time dashboard to monitor 7 AI agents with activity logging, Kanban task management, and zero-build frontend architecture.

• 6 min read
AIDashboardQdrantAutomationWeb Development
Building a Multi-Agent Dashboard: Real-Time Monitoring with Qdrant and Kanban

Building a multi-agent AI system is one thing. Monitoring it is another. When you have 7 AI agents running tasks — researching, writing articles, generating images, deploying code — you need visibility. This is how I built the AgentOS Dashboard: a single-file HTML dashboard with real-time activity logging, Kanban task management, and zero build tools.

The Problem

My agent system at antlatt.com has 7 specialized AI agents:

AgentRole
CoordinatorMain assistant
OrchestratorWorkflow coordinator
ResearcherResearch analyst
WriterContent writer
Image ArtistImage generator
Front-End DevFront-end developer
Back-End DevBack-end developer

Each agent logs its activity to a Qdrant collection. But I had no visibility into what they were doing. I needed a dashboard that showed:

  • Who’s working on what
  • Model usage across agents
  • Task completion rates
  • A Kanban board for task management

The catch: I wanted it fast. No React, no npm install, no webpack. Just a single HTML file I could serve with Python’s http.server.

Architecture

System Architecture

The architecture is deliberately simple:

  1. Frontend: Single HTML file with CDN dependencies (Tailwind, Chart.js, Inter font)
  2. Backend: Qdrant vector database with REST API
  3. Auth: Python HTTP server with Basic Authentication
  4. Logging: Python script called by each agent before responding

Qdrant Collections

I created two collections:

agent_logs — Activity tracking

{
  "agent_name": "researcher",
  "task_description": "Researched dashboard architecture patterns",
  "model_used": "glm-5:cloud",
  "status": "completed",
  "created_at": "2026-03-03T14:30:00Z"
}

todos — Kanban tasks

{
  "title": "Write article about dashboard",
  "category": "Work",
  "priority": "Urgent",
  "track_status": "On Track",
  "status": "in_progress",
  "due_date": "2026-03-05",
  "created_at": "2026-03-03T09:00:00Z"
}

Building the Dashboard

Tech Stack (Zero Build)

<!-- CDN-only, no NPM required -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

This approach means:

  • No npm install
  • No build step
  • Deploy by copying one file
  • Edit and refresh to see changes

Activity Logging: The Hard Gate

The most important feature: every agent must log before responding. This is enforced in their SOUL.md files:

## 📊 Activity Logging (HARD GATE)

**You MUST log your activity before sending your final reply.**

Run this command:
\`\`\`bash
python3 /path/to/log_agent_activity.py \
  "agent_name" \
  "Brief task description" \
  "model_used" \
  "completed"
\`\`\`

The logging script is simple:

#!/usr/bin/env python3
import sys
import http.client
import json
import uuid
from datetime import datetime, timezone

def log_activity(agent_name, task_description, model_used, status):
    point_id = str(uuid.uuid4())
    vector = [0.1] * 384  # Placeholder vector
    
    payload = {
        "points": [{
            "id": point_id,
            "vector": vector,
            "payload": {
                "agent_name": agent_name,
                "task_description": task_description,
                "model_used": model_used,
                "status": status,
                "created_at": datetime.now(timezone.utc).isoformat()
            }
        }]
    }
    
    conn = http.client.HTTPConnection("localhost", 6333)
    conn.request("PUT", "/collections/agent_logs/points", 
                 json.dumps(payload), {"Content-Type": "application/json"})
    response = conn.getresponse()
    print(f"{{\"success\": true, \"id\": \"{point_id}\"}}")

if __name__ == "__main__":
    log_activity(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])

Fetching Data from Qdrant

The dashboard pulls activity from Qdrant’s scroll API:

async function fetchActivity() {
    const response = await fetch('http://localhost:6333/collections/agent_logs/points/scroll', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            limit: 100,
            with_payload: true,
            with_vector: false
        })
    });
    const data = await response.json();
    return data.result.points.map(p => ({ id: p.id, ...p.payload }));
}

Kanban Board with Drag-and-Drop

Data Flow

The Tasks tab uses native drag-and-drop:

function initDragAndDrop() {
    document.querySelectorAll('.kanban-column').forEach(column => {
        column.addEventListener('dragover', handleDragOver);
        column.addEventListener('drop', handleDrop);
    });
}

async function handleDrop(e) {
    e.preventDefault();
    const newStatus = e.currentTarget.id.replace('-column', '');
    const taskId = draggedTask.dataset.taskId;
    
    // Update status in Qdrant
    await updateTaskStatus(taskId, newStatus);
    
    // Re-render board
    await updateKanbanBoard();
}

Task cards show category, priority, and track status as color-coded badges:

const CATEGORY_COLORS = {
    'Work': { bg: 'rgba(124, 58, 237, 0.2)', text: '#7C3AED' },
    'Marketing': { bg: 'rgba(245, 158, 11, 0.2)', text: '#F59E0B' },
    'Development': { bg: 'rgba(16, 185, 129, 0.2)', text: '#10B981' },
    'Personal': { bg: 'rgba(6, 182, 212, 0.2)', text: '#06B6D4' }
};

Chart.js for Visualizations

The productivity overview uses a donut chart:

new Chart(ctx, {
    type: 'doughnut',
    data: {
        labels: ['To Do', 'Doing', 'Done'],
        datasets: [{
            data: [todo, doing, done],
            backgroundColor: ['#3B82F6', '#F59E0B', '#10B981'],
            borderWidth: 0
        }]
    },
    options: {
        cutout: '60%',
        plugins: { legend: { display: false } }
    }
});

Authentication Layer

A simple Python server wraps the static file with Basic Auth:

class AuthHandler(http.server.SimpleHTTPRequestHandler):
    def check_auth(self):
        auth_header = self.headers.get("Authorization")
        if not auth_header:
            return False
        encoded = auth_header.split(" ")[1]
        decoded = base64.b64decode(encoded).decode("utf-8")
        username, password = decoded.split(":", 1)
        return username == USERNAME and PASSWORD == PASSWORD
    
    def do_GET(self):
        if not self.check_auth():
            self.send_response(401)
            self.send_header("WWW-Authenticate", 'Basic realm="Dashboard"')
            self.end_headers()
            return
        super().do_GET()

Run as a systemd service:

[Unit]
Description=Agent Dashboard HTTP Server with Authentication
After=network.target

[Service]
Type=simple
Environment=DASHBOARD_USER=your_username
Environment=DASHBOARD_PASS=your_secure_password
ExecStart=/usr/bin/python3 /path/to/dashboard/server.py

[Install]
WantedBy=multi-user.target

Design Decisions

Why Single HTML?

When you’re building internal tools, complexity is the enemy. Every build step is a debugging opportunity. Every dependency is a security update waiting to happen. A single HTML file:

  • Loads in any browser
  • Has zero transitive dependencies
  • Can be edited with any text editor
  • Deploys with scp
  • Works offline (once cached)

Why Qdrant Instead of SQLite?

Qdrant was already running for the agent memory system. Using it for the dashboard meant one fewer service to manage. The REST API is simple:

# Create collection
curl -X PUT http://localhost:6333/collections/agent_logs \
  -H 'Content-Type: application/json' \
  -d '{"vectors": {"size": 384, "distance": "Cosine"}}'

# Insert points
curl -X PUT http://localhost:6333/collections/agent_logs/points \
  -H 'Content-Type: application/json' \
  -d '{"points": [...]}'

Why Not React?

For a dashboard with 4 tabs, 7 agents, and ~100 activity entries, React is overkill. The DOM operations are:

  • Fetch data
  • Render cards
  • Update charts
  • Handle drag events

Vanilla JS handles this in under 500 lines. No virtual DOM, no reconciliation, no bundle size concerns.

Results

The dashboard went live in one evening. Key metrics:

MetricValue
Development time~4 hours
File size78KB (unminified)
Dependencies3 CDN scripts
Load time~300ms on local network

The agents now log every task:

$ curl -s "http://localhost:6333/collections/agent_logs/points/scroll" | jq '.result.points | length'
42

The Kanban board has real tasks being tracked:

To Do: 2 | Doing: 3 | Done: 4

What’s Next

  • Content tab: Article drafts and publishing queue
  • WebSocket updates: Real-time activity without refresh
  • Agent chat: Send messages to agents from dashboard
  • Metrics: Model costs, token usage, response times

The dashboard is now the central nervous system for the agent team. When an agent finishes a task, it appears in the feed within 30 seconds. When I need to track a project, I drag it across the Kanban columns. The system works because it’s simple enough to understand and modify in an afternoon.

That’s the power of zero-build architecture: you spend time on features, not configuration.


The AgentOS Dashboard is deployed internally with HTTP Basic Authentication.

Anthony Lattanzio

Anthony Lattanzio

Tech Enthusiast & Builder

I'm a tech enthusiast who loves building things with hardware and software. By night, I run a homelab that's grown way beyond what any reasonable person needs. Check out about me for more.

Comments

Powered by GitHub Discussions