Initial commit: Agent Command Center dashboard + weather briefing agent

This commit is contained in:
2026-04-13 01:06:42 +00:00
commit c86eb9ccc5
8 changed files with 857 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
data/
__pycache__/
*.pyc
.DS_Store
._*
*.db
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env python3
"""
Weather Briefing Agent
Fetches weather for Providence, UT and posts to Outline wiki.
Logs run to the Agent Dashboard API.
"""
import json
import os
import sys
from datetime import datetime, timezone
from urllib import request, error, parse
# --- Config ---
DASHBOARD_API = os.environ.get("DASHBOARD_API", "http://localhost:8550")
AGENT_ID = "weather-briefing"
WIKI_API = "https://wiki.jfamily.io/api"
WIKI_TOKEN = os.environ.get("WIKI_TOKEN", "ol_api_yHXypRyqf4CscWDzPluGfPev9GhdFg6mwrXwkT")
WIKI_COLLECTION_ID = os.environ.get("WIKI_COLLECTION_ID", "9d9e471c-84cd-4ba7-bae5-c70f61805228") # Set after collection creation
# Providence, UT
LAT = 41.7064
LON = -111.8133
WEATHER_URL = (
f"https://api.open-meteo.com/v1/forecast?"
f"latitude={LAT}&longitude={LON}"
f"&current=temperature_2m,apparent_temperature,weather_code,wind_speed_10m,relative_humidity_2m"
f"&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,wind_speed_10m_max,sunrise,sunset"
f"&temperature_unit=fahrenheit&wind_speed_unit=mph&precipitation_unit=inch"
f"&timezone=America/Denver&forecast_days=7"
)
WMO_CODES = {
0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
45: "Foggy", 48: "Icy fog", 51: "Light drizzle", 53: "Drizzle",
55: "Heavy drizzle", 61: "Light rain", 63: "Rain", 65: "Heavy rain",
66: "Freezing rain", 67: "Heavy freezing rain",
71: "Light snow", 73: "Snow", 75: "Heavy snow", 77: "Snow grains",
80: "Light showers", 81: "Showers", 82: "Heavy showers",
85: "Light snow showers", 86: "Heavy snow showers",
95: "Thunderstorm", 96: "Thunderstorm w/ hail", 99: "Severe thunderstorm",
}
DAY_NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
def api_request(url, data=None, headers=None, method="GET"):
"""Simple HTTP helper using urllib."""
if data is not None:
data = json.dumps(data).encode("utf-8")
req = request.Request(url, data=data, headers=headers or {}, method=method)
if data:
req.add_header("Content-Type", "application/json")
with request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode())
def fetch_weather():
"""Fetch weather data from Open-Meteo."""
return api_request(WEATHER_URL)
def format_briefing(weather):
"""Format weather data into markdown."""
now = datetime.now()
date_str = now.strftime("%A, %B %d, %Y")
current = weather["current"]
daily = weather["daily"]
condition = WMO_CODES.get(current["weather_code"], "Unknown")
temp = round(current["temperature_2m"])
feels = round(current["apparent_temperature"])
wind = round(current["wind_speed_10m"])
humidity = current["relative_humidity_2m"]
md = f"# Daily Weather Briefing\n"
md += f"**{date_str}** | Providence, Utah\n\n"
md += f"---\n\n"
md += f"## Current Conditions\n\n"
md += f"| | |\n|---|---|\n"
md += f"| **Condition** | {condition} |\n"
md += f"| **Temperature** | {temp}°F (feels like {feels}°F) |\n"
md += f"| **Wind** | {wind} mph |\n"
md += f"| **Humidity** | {humidity}% |\n\n"
md += f"## 7-Day Forecast\n\n"
md += f"| Day | Condition | High | Low | Precip | Wind |\n"
md += f"|-----|-----------|------|-----|--------|------|\n"
for i in range(len(daily["time"])):
d = datetime.strptime(daily["time"][i], "%Y-%m-%d")
day_name = DAY_NAMES[d.weekday()]
if i == 0:
day_name = "**Today**"
elif i == 1:
day_name = "Tomorrow"
cond = WMO_CODES.get(daily["weather_code"][i], "?")
hi = round(daily["temperature_2m_max"][i])
lo = round(daily["temperature_2m_min"][i])
precip = daily["precipitation_sum"][i]
wind_max = round(daily["wind_speed_10m_max"][i])
precip_str = f'{precip}"' if precip > 0 else "-"
md += f"| {day_name} | {cond} | {hi}°F | {lo}°F | {precip_str} | {wind_max} mph |\n"
# Sunrise/sunset for today
sunrise = daily["sunrise"][0].split("T")[1] if daily["sunrise"][0] else "?"
sunset = daily["sunset"][0].split("T")[1] if daily["sunset"][0] else "?"
md += f"\n**Sunrise:** {sunrise} | **Sunset:** {sunset}\n"
md += f"\n---\n*Generated at {now.strftime('%I:%M %p MT')} by Weather Briefing Agent*\n"
return md, f"{condition}, {temp}°F (feels like {feels}°F), wind {wind} mph"
def post_to_wiki(markdown, date_str):
"""Create or update a daily briefing doc in Outline."""
headers = {
"Authorization": f"Bearer {WIKI_TOKEN}",
}
# Search for existing doc with today's date
title = f"Daily Briefing — {date_str}"
search_result = api_request(
f"{WIKI_API}/documents.search",
data={"query": title, "collectionId": WIKI_COLLECTION_ID},
headers=headers,
method="POST",
)
doc_id = None
for doc in search_result.get("data", []):
if doc.get("document", {}).get("title") == title:
doc_id = doc["document"]["id"]
break
if doc_id:
api_request(
f"{WIKI_API}/documents.update",
data={"id": doc_id, "text": markdown, "publish": True},
headers=headers,
method="POST",
)
return doc_id, "updated"
else:
result = api_request(
f"{WIKI_API}/documents.create",
data={
"title": title,
"text": markdown,
"collectionId": WIKI_COLLECTION_ID,
"publish": True,
},
headers=headers,
method="POST",
)
return result["data"]["id"], "created"
def log_run(status, output="", err="", metadata=None):
"""Log this run to the dashboard API."""
try:
api_request(
f"{DASHBOARD_API}/api/agents/{AGENT_ID}/runs",
data={
"status": status,
"output": output,
"error": err,
"metadata": metadata or {},
},
method="POST",
)
except Exception as e:
print(f"Warning: failed to log run to dashboard: {e}", file=sys.stderr)
def main():
try:
print("Fetching weather...")
weather = fetch_weather()
print("Formatting briefing...")
date_str = datetime.now().strftime("%Y-%m-%d")
markdown, summary = format_briefing(weather)
print("Posting to wiki...")
doc_id, action = post_to_wiki(markdown, date_str)
print(f"Wiki doc {action}: {doc_id}")
output = f"Weather: {summary}. Wiki doc {action}."
log_run("success", output=output, metadata={"wiki_doc_id": doc_id, "action": action})
print(f"Done: {output}")
except Exception as e:
err_msg = f"{type(e).__name__}: {e}"
print(f"Error: {err_msg}", file=sys.stderr)
log_run("failed", err=err_msg)
sys.exit(1)
if __name__ == "__main__":
main()
+11
View File
@@ -0,0 +1,11 @@
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir fastapi uvicorn sqlalchemy
COPY . .
EXPOSE 8550
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8550"]
+202
View File
@@ -0,0 +1,202 @@
from fastapi import FastAPI, Depends, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from pydantic import BaseModel
from datetime import datetime, timezone
from typing import Optional
import json
from database import get_db, init_db
from models import Agent, Run
app = FastAPI(title="Agent Command Center", version="1.0.0")
# --- Pydantic schemas ---
class AgentCreate(BaseModel):
id: str
name: str
description: str = ""
schedule: str = "manual"
class AgentUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
schedule: Optional[str] = None
status: Optional[str] = None
class RunCreate(BaseModel):
status: str = "running"
output: str = ""
error: str = ""
metadata: dict = {}
class RunUpdate(BaseModel):
status: Optional[str] = None
output: Optional[str] = None
error: Optional[str] = None
finished_at: Optional[str] = None
metadata: Optional[dict] = None
# --- API routes ---
@app.get("/api/health")
def health():
return {"status": "ok", "service": "agent-command-center"}
@app.get("/api/agents")
def list_agents(db: Session = Depends(get_db)):
agents = db.query(Agent).all()
result = []
for a in agents:
last_run = db.query(Run).filter(Run.agent_id == a.id).order_by(Run.started_at.desc()).first()
recent_runs = db.query(Run).filter(Run.agent_id == a.id).order_by(Run.started_at.desc()).limit(10).all()
success_streak = 0
for r in recent_runs:
if r.status == "success":
success_streak += 1
else:
break
result.append({
"id": a.id,
"name": a.name,
"description": a.description,
"schedule": a.schedule,
"status": a.status,
"created_at": a.created_at.isoformat() if a.created_at else None,
"last_run": {
"status": last_run.status,
"started_at": last_run.started_at.isoformat() if last_run.started_at else None,
"finished_at": last_run.finished_at.isoformat() if last_run.finished_at else None,
} if last_run else None,
"success_streak": success_streak,
"total_runs": db.query(Run).filter(Run.agent_id == a.id).count(),
})
return result
@app.post("/api/agents")
def create_agent(agent: AgentCreate, db: Session = Depends(get_db)):
existing = db.query(Agent).filter(Agent.id == agent.id).first()
if existing:
raise HTTPException(status_code=409, detail="Agent already exists")
new_agent = Agent(
id=agent.id,
name=agent.name,
description=agent.description,
schedule=agent.schedule,
)
db.add(new_agent)
db.commit()
return {"id": new_agent.id, "status": "created"}
@app.get("/api/agents/{agent_id}")
def get_agent(agent_id: str, db: Session = Depends(get_db)):
agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent:
raise HTTPException(status_code=404, detail="Agent not found")
runs = db.query(Run).filter(Run.agent_id == agent_id).order_by(Run.started_at.desc()).limit(50).all()
return {
"id": agent.id,
"name": agent.name,
"description": agent.description,
"schedule": agent.schedule,
"status": agent.status,
"created_at": agent.created_at.isoformat() if agent.created_at else None,
"runs": [{
"id": r.id,
"started_at": r.started_at.isoformat() if r.started_at else None,
"finished_at": r.finished_at.isoformat() if r.finished_at else None,
"status": r.status,
"output": r.output,
"error": r.error,
"metadata": r.metadata_,
} for r in runs],
}
@app.put("/api/agents/{agent_id}")
def update_agent(agent_id: str, update: AgentUpdate, db: Session = Depends(get_db)):
agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent:
raise HTTPException(status_code=404, detail="Agent not found")
for field, value in update.model_dump(exclude_none=True).items():
setattr(agent, field, value)
db.commit()
return {"id": agent.id, "status": "updated"}
@app.post("/api/agents/{agent_id}/runs")
def create_run(agent_id: str, run: RunCreate, db: Session = Depends(get_db)):
agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent:
raise HTTPException(status_code=404, detail="Agent not found")
new_run = Run(
agent_id=agent_id,
status=run.status,
output=run.output,
error=run.error,
metadata_=run.metadata,
)
if run.status in ("success", "failed"):
new_run.finished_at = datetime.now(timezone.utc)
db.add(new_run)
db.commit()
return {"id": new_run.id, "status": new_run.status}
@app.put("/api/runs/{run_id}")
def update_run(run_id: int, update: RunUpdate, db: Session = Depends(get_db)):
run = db.query(Run).filter(Run.id == run_id).first()
if not run:
raise HTTPException(status_code=404, detail="Run not found")
if update.status is not None:
run.status = update.status
if update.output is not None:
run.output = update.output
if update.error is not None:
run.error = update.error
if update.metadata is not None:
run.metadata_ = update.metadata
if update.finished_at is not None:
run.finished_at = datetime.fromisoformat(update.finished_at)
elif update.status in ("success", "failed"):
run.finished_at = datetime.now(timezone.utc)
db.commit()
return {"id": run.id, "status": run.status}
@app.get("/api/runs")
def list_runs(limit: int = 50, db: Session = Depends(get_db)):
runs = db.query(Run).order_by(Run.started_at.desc()).limit(limit).all()
return [{
"id": r.id,
"agent_id": r.agent_id,
"started_at": r.started_at.isoformat() if r.started_at else None,
"finished_at": r.finished_at.isoformat() if r.finished_at else None,
"status": r.status,
"output": r.output,
"error": r.error,
"metadata": r.metadata_,
} for r in runs]
# --- Static files (frontend) ---
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/")
def root():
return FileResponse("static/index.html")
# --- Startup ---
@app.on_event("startup")
def startup():
init_db()
+23
View File
@@ -0,0 +1,23 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase
import os
DB_PATH = os.environ.get("DB_PATH", "/app/data/agents.db")
engine = create_engine(f"sqlite:///{DB_PATH}", connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(bind=engine)
class Base(DeclarativeBase):
pass
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
def init_db():
Base.metadata.create_all(bind=engine)
+32
View File
@@ -0,0 +1,32 @@
from sqlalchemy import Column, String, Text, DateTime, Integer, ForeignKey, JSON
from sqlalchemy.orm import relationship
from datetime import datetime, timezone
from database import Base
class Agent(Base):
__tablename__ = "agents"
id = Column(String, primary_key=True)
name = Column(String, nullable=False)
description = Column(Text, default="")
schedule = Column(String, default="manual")
status = Column(String, default="active")
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
runs = relationship("Run", back_populates="agent", order_by="Run.started_at.desc()")
class Run(Base):
__tablename__ = "runs"
id = Column(Integer, primary_key=True, autoincrement=True)
agent_id = Column(String, ForeignKey("agents.id"), nullable=False)
started_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
finished_at = Column(DateTime, nullable=True)
status = Column(String, default="running")
output = Column(Text, default="")
error = Column(Text, default="")
metadata_ = Column("metadata", JSON, default=dict)
agent = relationship("Agent", back_populates="runs")
+366
View File
@@ -0,0 +1,366 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agent Command Center</title>
<style>
:root {
--bg: #0f1117;
--surface: #1a1d27;
--surface2: #232733;
--border: #2e3345;
--text: #e4e6ed;
--text-dim: #8b8fa3;
--accent: #6c5ce7;
--green: #00b894;
--red: #e17055;
--yellow: #fdcb6e;
--blue: #74b9ff;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
.header {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 1rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.header h1 {
font-size: 1.4rem;
font-weight: 600;
letter-spacing: -0.02em;
}
.header .status {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
color: var(--text-dim);
}
.header .status .dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--green);
}
.container { max-width: 1200px; margin: 0 auto; padding: 1.5rem 2rem; }
/* Agent Cards */
.agents-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.agent-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 1.25rem;
cursor: pointer;
transition: border-color 0.2s, transform 0.1s;
}
.agent-card:hover {
border-color: var(--accent);
transform: translateY(-1px);
}
.agent-card .card-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.75rem;
}
.agent-card h3 { font-size: 1.05rem; font-weight: 600; }
.agent-card .desc { color: var(--text-dim); font-size: 0.85rem; margin-bottom: 1rem; }
.agent-card .card-stats {
display: flex;
gap: 1.25rem;
font-size: 0.8rem;
color: var(--text-dim);
}
.agent-card .card-stats span { display: flex; align-items: center; gap: 0.3rem; }
.badge {
display: inline-block;
padding: 0.15rem 0.6rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.badge.active { background: rgba(0,184,148,0.15); color: var(--green); }
.badge.paused { background: rgba(253,203,110,0.15); color: var(--yellow); }
.badge.error { background: rgba(225,112,85,0.15); color: var(--red); }
.badge.success { background: rgba(0,184,148,0.15); color: var(--green); }
.badge.failed { background: rgba(225,112,85,0.15); color: var(--red); }
.badge.running { background: rgba(116,185,255,0.15); color: var(--blue); }
/* Section headers */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.section-header h2 { font-size: 1.1rem; font-weight: 600; }
/* Run History Table */
.runs-table {
width: 100%;
border-collapse: collapse;
background: var(--surface);
border-radius: 10px;
overflow: hidden;
border: 1px solid var(--border);
}
.runs-table th {
text-align: left;
padding: 0.75rem 1rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-dim);
background: var(--surface2);
border-bottom: 1px solid var(--border);
}
.runs-table td {
padding: 0.65rem 1rem;
font-size: 0.85rem;
border-bottom: 1px solid var(--border);
}
.runs-table tr:last-child td { border-bottom: none; }
.runs-table tr:hover td { background: var(--surface2); }
.output-preview {
max-width: 350px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--text-dim);
}
/* Detail Modal */
.modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6);
z-index: 100;
justify-content: center;
align-items: flex-start;
padding-top: 5vh;
}
.modal-overlay.open { display: flex; }
.modal {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
width: 90%;
max-width: 800px;
max-height: 85vh;
overflow-y: auto;
padding: 1.5rem;
}
.modal h2 { margin-bottom: 0.5rem; }
.modal .meta { color: var(--text-dim); font-size: 0.85rem; margin-bottom: 1.5rem; }
.modal .close-btn {
float: right;
background: none;
border: none;
color: var(--text-dim);
font-size: 1.5rem;
cursor: pointer;
}
.modal .close-btn:hover { color: var(--text); }
.run-output {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 1rem;
font-family: 'SF Mono', Monaco, Consolas, monospace;
font-size: 0.8rem;
white-space: pre-wrap;
max-height: 200px;
overflow-y: auto;
margin-top: 0.5rem;
color: var(--text-dim);
}
.empty-state {
text-align: center;
padding: 3rem;
color: var(--text-dim);
}
.empty-state h3 { margin-bottom: 0.5rem; color: var(--text); }
.time-ago { color: var(--text-dim); }
</style>
</head>
<body>
<div class="header">
<h1>Agent Command Center</h1>
<div class="status">
<div class="dot"></div>
<span id="agent-count">0 agents</span>
</div>
</div>
<div class="container">
<div class="section-header">
<h2>Agents</h2>
</div>
<div class="agents-grid" id="agents-grid"></div>
<div class="section-header">
<h2>Recent Runs</h2>
</div>
<div id="runs-container"></div>
</div>
<div class="modal-overlay" id="modal-overlay">
<div class="modal" id="modal-content"></div>
</div>
<script>
const API = '';
function timeAgo(dateStr) {
if (!dateStr) return 'never';
const d = new Date(dateStr + (dateStr.endsWith('Z') ? '' : 'Z'));
const now = new Date();
const sec = Math.floor((now - d) / 1000);
if (sec < 60) return 'just now';
if (sec < 3600) return Math.floor(sec / 60) + 'm ago';
if (sec < 86400) return Math.floor(sec / 3600) + 'h ago';
return Math.floor(sec / 86400) + 'd ago';
}
function formatTime(dateStr) {
if (!dateStr) return '-';
const d = new Date(dateStr + (dateStr.endsWith('Z') ? '' : 'Z'));
return d.toLocaleString();
}
function renderAgents(agents) {
const grid = document.getElementById('agents-grid');
document.getElementById('agent-count', agents.length + ' agent' + (agents.length !== 1 ? 's' : ''));
if (agents.length === 0) {
grid.innerHTML = '<div class="empty-state"><h3>No agents registered</h3><p>Register your first agent via the API</p></div>';
return;
}
grid.innerHTML = agents.map(a => `
<div class="agent-card" onclick="showAgent('${a.id}')">
<div class="card-top">
<h3>${a.name}</h3>
<span class="badge ${a.status}">${a.status}</span>
</div>
<div class="desc">${a.description}</div>
<div class="card-stats">
<span>Last: ${a.last_run ? timeAgo(a.last_run.started_at) : 'never'}</span>
<span>${a.last_run ? '<span class="badge ' + a.last_run.status + '">' + a.last_run.status + '</span>' : ''}</span>
<span>${a.total_runs} runs</span>
<span>${a.success_streak > 0 ? a.success_streak + ' streak' : ''}</span>
</div>
</div>
`).join('');
}
function renderRuns(runs) {
const container = document.getElementById('runs-container');
if (runs.length === 0) {
container.innerHTML = '<div class="empty-state"><p>No runs yet</p></div>';
return;
}
container.innerHTML = `
<table class="runs-table">
<thead>
<tr><th>Agent</th><th>Status</th><th>Started</th><th>Duration</th><th>Output</th></tr>
</thead>
<tbody>
${runs.map(r => {
let dur = '-';
if (r.started_at && r.finished_at) {
const s = new Date(r.started_at + (r.started_at.endsWith('Z') ? '' : 'Z'));
const e = new Date(r.finished_at + (r.finished_at.endsWith('Z') ? '' : 'Z'));
const ms = e - s;
dur = ms < 1000 ? ms + 'ms' : (ms / 1000).toFixed(1) + 's';
}
return `<tr>
<td>${r.agent_id}</td>
<td><span class="badge ${r.status}">${r.status}</span></td>
<td class="time-ago">${timeAgo(r.started_at)}</td>
<td>${dur}</td>
<td class="output-preview">${r.output || r.error || '-'}</td>
</tr>`;
}).join('')}
</tbody>
</table>
`;
}
async function showAgent(id) {
const res = await fetch(API + '/api/agents/' + id);
const agent = await res.json();
const modal = document.getElementById('modal-content');
modal.innerHTML = `
<button class="close-btn" onclick="closeModal()">&times;</button>
<h2>${agent.name}</h2>
<div class="meta">
<span class="badge ${agent.status}">${agent.status}</span>
&nbsp; Schedule: ${agent.schedule} &nbsp; Created: ${formatTime(agent.created_at)}
</div>
<p style="margin-bottom:1.5rem;color:var(--text-dim)">${agent.description}</p>
<h3 style="margin-bottom:0.75rem">Run History</h3>
${agent.runs.length === 0 ? '<p style="color:var(--text-dim)">No runs yet</p>' :
agent.runs.map(r => `
<div style="border:1px solid var(--border);border-radius:8px;padding:0.75rem;margin-bottom:0.75rem">
<div style="display:flex;justify-content:space-between;margin-bottom:0.5rem">
<span class="badge ${r.status}">${r.status}</span>
<span class="time-ago">${formatTime(r.started_at)}</span>
</div>
${r.output ? '<div class="run-output">' + r.output + '</div>' : ''}
${r.error ? '<div class="run-output" style="border-color:var(--red)">' + r.error + '</div>' : ''}
</div>
`).join('')
}
`;
document.getElementById('modal-overlay').classList.add('open');
}
function closeModal() {
document.getElementById('modal-overlay').classList.remove('open');
}
document.getElementById('modal-overlay').addEventListener('click', e => {
if (e.target === e.currentTarget) closeModal();
});
async function refresh() {
try {
const [agentsRes, runsRes] = await Promise.all([
fetch(API + '/api/agents'),
fetch(API + '/api/runs?limit=25'),
]);
renderAgents(await agentsRes.json());
renderRuns(await runsRes.json());
} catch (err) {
console.error('Failed to fetch:', err);
}
}
refresh();
setInterval(refresh, 30000);
</script>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
services:
dashboard:
build: ./dashboard
container_name: agent-dashboard
ports:
- "8550:8550"
volumes:
- ./data:/app/data
environment:
- DB_PATH=/app/data/agents.db
restart: unless-stopped