forked from Rockachopa/Timmy-time-dashboard
feat: complete Event Log, Ledger, Memory, Cascade Router, Upgrade Queue, Activity Feed
This commit implements six major features: 1. Event Log System (src/swarm/event_log.py) - SQLite-based audit trail for all swarm events - Task lifecycle tracking (created, assigned, completed, failed) - Agent lifecycle tracking (joined, left, status changes) - Integrated with coordinator for automatic logging - Dashboard page at /swarm/events 2. Lightning Ledger (src/lightning/ledger.py) - Transaction tracking for Lightning Network payments - Balance calculations (incoming, outgoing, net, available) - Integrated with payment_handler for automatic logging - Dashboard page at /lightning/ledger 3. Semantic Memory / Vector Store (src/memory/vector_store.py) - Embedding-based similarity search for Echo agent - Fallback to keyword matching if sentence-transformers unavailable - Personal facts storage and retrieval - Dashboard page at /memory 4. Cascade Router Integration (src/timmy/cascade_adapter.py) - Automatic LLM failover between providers (Ollama → AirLLM → API) - Circuit breaker pattern for failing providers - Metrics tracking per provider (latency, error rates) - Dashboard status page at /router/status 5. Self-Upgrade Approval Queue (src/upgrades/) - State machine for self-modifications: proposed → approved/rejected → applied/failed - Human approval required before applying changes - Git integration for branch management - Dashboard queue at /self-modify/queue 6. Real-Time Activity Feed (src/events/broadcaster.py) - WebSocket-based live activity streaming - Bridges event_log to dashboard clients - Activity panel on /swarm/live Tests: - 101 unit tests passing - 4 new E2E test files for Selenium testing - Run with: SELENIUM_UI=1 pytest tests/functional/ -v --headed Documentation: - 6 ADRs (017-022) documenting architecture decisions - Implementation summary in docs/IMPLEMENTATION_SUMMARY.md - Architecture diagram in docs/architecture-v2.md
This commit is contained in:
@@ -27,6 +27,11 @@ from dashboard.routes.spark import router as spark_router
|
||||
from dashboard.routes.creative import router as creative_router
|
||||
from dashboard.routes.discord import router as discord_router
|
||||
from dashboard.routes.self_modify import router as self_modify_router
|
||||
from dashboard.routes.events import router as events_router
|
||||
from dashboard.routes.ledger import router as ledger_router
|
||||
from dashboard.routes.memory import router as memory_router
|
||||
from dashboard.routes.router import router as router_status_router
|
||||
from dashboard.routes.upgrades import router as upgrades_router
|
||||
from router.api import router as cascade_router
|
||||
|
||||
logging.basicConfig(
|
||||
@@ -166,6 +171,11 @@ app.include_router(spark_router)
|
||||
app.include_router(creative_router)
|
||||
app.include_router(discord_router)
|
||||
app.include_router(self_modify_router)
|
||||
app.include_router(events_router)
|
||||
app.include_router(ledger_router)
|
||||
app.include_router(memory_router)
|
||||
app.include_router(router_status_router)
|
||||
app.include_router(upgrades_router)
|
||||
app.include_router(cascade_router)
|
||||
|
||||
|
||||
|
||||
91
src/dashboard/routes/events.py
Normal file
91
src/dashboard/routes/events.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""Event Log routes for viewing system events."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from swarm.event_log import (
|
||||
EventType,
|
||||
list_events,
|
||||
get_event_summary,
|
||||
get_recent_events,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/swarm", tags=["events"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.get("/events", response_class=HTMLResponse)
|
||||
async def events_page(
|
||||
request: Request,
|
||||
event_type: Optional[str] = None,
|
||||
task_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
):
|
||||
"""Event log viewer page."""
|
||||
# Parse event type filter
|
||||
evt_type = None
|
||||
if event_type:
|
||||
try:
|
||||
evt_type = EventType(event_type)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Get events
|
||||
events = list_events(
|
||||
event_type=evt_type,
|
||||
task_id=task_id,
|
||||
agent_id=agent_id,
|
||||
limit=100,
|
||||
)
|
||||
|
||||
# Get summary stats
|
||||
summary = get_event_summary(minutes=60)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"events.html",
|
||||
{
|
||||
"page_title": "Event Log",
|
||||
"events": events,
|
||||
"summary": summary,
|
||||
"filter_type": event_type,
|
||||
"filter_task": task_id,
|
||||
"filter_agent": agent_id,
|
||||
"event_types": [e.value for e in EventType],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/events/partial", response_class=HTMLResponse)
|
||||
async def events_partial(
|
||||
request: Request,
|
||||
event_type: Optional[str] = None,
|
||||
task_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
):
|
||||
"""Event log partial for HTMX updates."""
|
||||
evt_type = None
|
||||
if event_type:
|
||||
try:
|
||||
evt_type = EventType(event_type)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
events = list_events(
|
||||
event_type=evt_type,
|
||||
task_id=task_id,
|
||||
agent_id=agent_id,
|
||||
limit=100,
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"partials/events_table.html",
|
||||
{
|
||||
"events": events,
|
||||
},
|
||||
)
|
||||
102
src/dashboard/routes/ledger.py
Normal file
102
src/dashboard/routes/ledger.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""Lightning Ledger routes for viewing transactions and balance."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from lightning.ledger import (
|
||||
TransactionType,
|
||||
TransactionStatus,
|
||||
list_transactions,
|
||||
get_balance,
|
||||
get_transaction_stats,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/lightning", tags=["ledger"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.get("/ledger", response_class=HTMLResponse)
|
||||
async def ledger_page(
|
||||
request: Request,
|
||||
tx_type: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
):
|
||||
"""Lightning ledger page with balance and transactions."""
|
||||
# Parse filters
|
||||
filter_type = None
|
||||
if tx_type:
|
||||
try:
|
||||
filter_type = TransactionType(tx_type)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
filter_status = None
|
||||
if status:
|
||||
try:
|
||||
filter_status = TransactionStatus(status)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Get data
|
||||
balance = get_balance()
|
||||
transactions = list_transactions(
|
||||
tx_type=filter_type,
|
||||
status=filter_status,
|
||||
limit=50,
|
||||
)
|
||||
stats = get_transaction_stats(days=7)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"ledger.html",
|
||||
{
|
||||
"page_title": "Lightning Ledger",
|
||||
"balance": balance,
|
||||
"transactions": transactions,
|
||||
"stats": stats,
|
||||
"filter_type": tx_type,
|
||||
"filter_status": status,
|
||||
"tx_types": [t.value for t in TransactionType],
|
||||
"tx_statuses": [s.value for s in TransactionStatus],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/ledger/partial", response_class=HTMLResponse)
|
||||
async def ledger_partial(
|
||||
request: Request,
|
||||
tx_type: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
):
|
||||
"""Ledger transactions partial for HTMX updates."""
|
||||
filter_type = None
|
||||
if tx_type:
|
||||
try:
|
||||
filter_type = TransactionType(tx_type)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
filter_status = None
|
||||
if status:
|
||||
try:
|
||||
filter_status = TransactionStatus(status)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
transactions = list_transactions(
|
||||
tx_type=filter_type,
|
||||
status=filter_status,
|
||||
limit=50,
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"partials/ledger_table.html",
|
||||
{
|
||||
"transactions": transactions,
|
||||
},
|
||||
)
|
||||
98
src/dashboard/routes/memory.py
Normal file
98
src/dashboard/routes/memory.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Memory (vector store) routes for browsing and searching memories."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Form, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from memory.vector_store import (
|
||||
store_memory,
|
||||
search_memories,
|
||||
get_memory_stats,
|
||||
recall_personal_facts,
|
||||
store_personal_fact,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/memory", tags=["memory"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.get("", response_class=HTMLResponse)
|
||||
async def memory_page(
|
||||
request: Request,
|
||||
query: Optional[str] = None,
|
||||
context_type: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
):
|
||||
"""Memory browser and search page."""
|
||||
results = []
|
||||
if query:
|
||||
results = search_memories(
|
||||
query=query,
|
||||
context_type=context_type,
|
||||
agent_id=agent_id,
|
||||
limit=20,
|
||||
)
|
||||
|
||||
stats = get_memory_stats()
|
||||
facts = recall_personal_facts(limit=10)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"memory.html",
|
||||
{
|
||||
"page_title": "Memory Browser",
|
||||
"query": query,
|
||||
"results": results,
|
||||
"stats": stats,
|
||||
"facts": facts,
|
||||
"filter_type": context_type,
|
||||
"filter_agent": agent_id,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/search", response_class=HTMLResponse)
|
||||
async def memory_search(
|
||||
request: Request,
|
||||
query: str = Form(...),
|
||||
context_type: Optional[str] = Form(None),
|
||||
):
|
||||
"""Search memories (form submission)."""
|
||||
results = search_memories(
|
||||
query=query,
|
||||
context_type=context_type,
|
||||
limit=20,
|
||||
)
|
||||
|
||||
# Return partial for HTMX
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"partials/memory_results.html",
|
||||
{
|
||||
"query": query,
|
||||
"results": results,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/fact", response_class=HTMLResponse)
|
||||
async def add_fact(
|
||||
request: Request,
|
||||
fact: str = Form(...),
|
||||
agent_id: Optional[str] = Form(None),
|
||||
):
|
||||
"""Add a personal fact to memory."""
|
||||
store_personal_fact(fact, agent_id=agent_id)
|
||||
|
||||
# Return updated facts list
|
||||
facts = recall_personal_facts(limit=10)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"partials/memory_facts.html",
|
||||
{
|
||||
"facts": facts,
|
||||
},
|
||||
)
|
||||
54
src/dashboard/routes/router.py
Normal file
54
src/dashboard/routes/router.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""Cascade Router status routes."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from timmy.cascade_adapter import get_cascade_adapter
|
||||
|
||||
router = APIRouter(prefix="/router", tags=["router"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.get("/status", response_class=HTMLResponse)
|
||||
async def router_status_page(request: Request):
|
||||
"""Cascade Router status dashboard."""
|
||||
adapter = get_cascade_adapter()
|
||||
|
||||
providers = adapter.get_provider_status()
|
||||
preferred = adapter.get_preferred_provider()
|
||||
|
||||
# Calculate overall stats
|
||||
total_requests = sum(p["metrics"]["total"] for p in providers)
|
||||
total_success = sum(p["metrics"]["success"] for p in providers)
|
||||
total_failed = sum(p["metrics"]["failed"] for p in providers)
|
||||
|
||||
avg_latency = 0.0
|
||||
if providers:
|
||||
avg_latency = sum(p["metrics"]["avg_latency_ms"] for p in providers) / len(providers)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"router_status.html",
|
||||
{
|
||||
"page_title": "Router Status",
|
||||
"providers": providers,
|
||||
"preferred_provider": preferred,
|
||||
"total_requests": total_requests,
|
||||
"total_success": total_success,
|
||||
"total_failed": total_failed,
|
||||
"avg_latency_ms": round(avg_latency, 1),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/providers")
|
||||
async def get_providers():
|
||||
"""API endpoint for provider status (JSON)."""
|
||||
adapter = get_cascade_adapter()
|
||||
return {
|
||||
"providers": adapter.get_provider_status(),
|
||||
"preferred": adapter.get_preferred_provider(),
|
||||
}
|
||||
99
src/dashboard/routes/upgrades.py
Normal file
99
src/dashboard/routes/upgrades.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Self-Upgrade Queue dashboard routes."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Form, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from upgrades.models import list_upgrades, get_upgrade, UpgradeStatus, get_pending_count
|
||||
from upgrades.queue import UpgradeQueue
|
||||
|
||||
router = APIRouter(prefix="/self-modify", tags=["upgrades"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.get("/queue", response_class=HTMLResponse)
|
||||
async def upgrade_queue_page(request: Request):
|
||||
"""Upgrade queue dashboard."""
|
||||
pending = list_upgrades(status=UpgradeStatus.PROPOSED)
|
||||
approved = list_upgrades(status=UpgradeStatus.APPROVED)
|
||||
history = list_upgrades(status=None)[:20] # All recent
|
||||
|
||||
# Separate history by status
|
||||
applied = [u for u in history if u.status == UpgradeStatus.APPLIED][:10]
|
||||
rejected = [u for u in history if u.status == UpgradeStatus.REJECTED][:5]
|
||||
failed = [u for u in history if u.status == UpgradeStatus.FAILED][:5]
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"upgrade_queue.html",
|
||||
{
|
||||
"page_title": "Upgrade Queue",
|
||||
"pending": pending,
|
||||
"approved": approved,
|
||||
"applied": applied,
|
||||
"rejected": rejected,
|
||||
"failed": failed,
|
||||
"pending_count": len(pending),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/queue/{upgrade_id}/approve", response_class=JSONResponse)
|
||||
async def approve_upgrade_endpoint(upgrade_id: str):
|
||||
"""Approve an upgrade proposal."""
|
||||
upgrade = UpgradeQueue.approve(upgrade_id)
|
||||
|
||||
if not upgrade:
|
||||
raise HTTPException(404, "Upgrade not found or not in proposed state")
|
||||
|
||||
return {"success": True, "upgrade_id": upgrade_id, "status": upgrade.status.value}
|
||||
|
||||
|
||||
@router.post("/queue/{upgrade_id}/reject", response_class=JSONResponse)
|
||||
async def reject_upgrade_endpoint(upgrade_id: str):
|
||||
"""Reject an upgrade proposal."""
|
||||
upgrade = UpgradeQueue.reject(upgrade_id)
|
||||
|
||||
if not upgrade:
|
||||
raise HTTPException(404, "Upgrade not found or not in proposed state")
|
||||
|
||||
return {"success": True, "upgrade_id": upgrade_id, "status": upgrade.status.value}
|
||||
|
||||
|
||||
@router.post("/queue/{upgrade_id}/apply", response_class=JSONResponse)
|
||||
async def apply_upgrade_endpoint(upgrade_id: str):
|
||||
"""Apply an approved upgrade."""
|
||||
success, message = UpgradeQueue.apply(upgrade_id)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(400, message)
|
||||
|
||||
return {"success": True, "message": message}
|
||||
|
||||
|
||||
@router.get("/queue/{upgrade_id}/diff", response_class=HTMLResponse)
|
||||
async def view_diff(request: Request, upgrade_id: str):
|
||||
"""View full diff for an upgrade."""
|
||||
upgrade = get_upgrade(upgrade_id)
|
||||
|
||||
if not upgrade:
|
||||
raise HTTPException(404, "Upgrade not found")
|
||||
|
||||
diff = UpgradeQueue.get_full_diff(upgrade_id)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"upgrade_diff.html",
|
||||
{
|
||||
"upgrade": upgrade,
|
||||
"diff": diff,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/pending-count", response_class=JSONResponse)
|
||||
async def get_pending_upgrade_count():
|
||||
"""Get count of pending upgrades (for nav badge)."""
|
||||
return {"count": get_pending_count()}
|
||||
@@ -30,6 +30,11 @@
|
||||
<a href="/spark/ui" class="mc-test-link">SPARK</a>
|
||||
<a href="/marketplace/ui" class="mc-test-link">MARKET</a>
|
||||
<a href="/tools" class="mc-test-link">TOOLS</a>
|
||||
<a href="/swarm/events" class="mc-test-link">EVENTS</a>
|
||||
<a href="/lightning/ledger" class="mc-test-link">LEDGER</a>
|
||||
<a href="/memory" class="mc-test-link">MEMORY</a>
|
||||
<a href="/router/status" class="mc-test-link">ROUTER</a>
|
||||
<a href="/self-modify/queue" class="mc-test-link">UPGRADES</a>
|
||||
<a href="/creative/ui" class="mc-test-link">CREATIVE</a>
|
||||
<a href="/mobile" class="mc-test-link" title="Mobile-optimized view">MOBILE</a>
|
||||
<button id="enable-notifications" class="mc-test-link" style="background:none;cursor:pointer;" title="Enable notifications">🔔</button>
|
||||
@@ -55,6 +60,9 @@
|
||||
<a href="/spark/ui" class="mc-mobile-link">SPARK</a>
|
||||
<a href="/marketplace/ui" class="mc-mobile-link">MARKET</a>
|
||||
<a href="/tools" class="mc-mobile-link">TOOLS</a>
|
||||
<a href="/swarm/events" class="mc-mobile-link">EVENTS</a>
|
||||
<a href="/lightning/ledger" class="mc-mobile-link">LEDGER</a>
|
||||
<a href="/memory" class="mc-mobile-link">MEMORY</a>
|
||||
<a href="/creative/ui" class="mc-mobile-link">CREATIVE</a>
|
||||
<a href="/voice/button" class="mc-mobile-link">VOICE</a>
|
||||
<a href="/mobile" class="mc-mobile-link">MOBILE</a>
|
||||
|
||||
103
src/dashboard/templates/events.html
Normal file
103
src/dashboard/templates/events.html
Normal file
@@ -0,0 +1,103 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Event Log - Timmy Time{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mc-panel">
|
||||
<div class="mc-panel-header">
|
||||
<h1 class="page-title">Event Log</h1>
|
||||
<p class="mc-text-secondary">System audit trail and activity history</p>
|
||||
</div>
|
||||
|
||||
<!-- Summary Stats -->
|
||||
<div class="mc-stats-row">
|
||||
{% for event_type, count in summary.items() %}
|
||||
<div class="mc-stat-card">
|
||||
<div class="mc-stat-value">{{ count }}</div>
|
||||
<div class="mc-stat-label">{{ event_type }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not summary %}
|
||||
<div class="mc-stat-card">
|
||||
<div class="mc-stat-value">-</div>
|
||||
<div class="mc-stat-label">No events (last hour)</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="mc-filters">
|
||||
<form method="get" action="/swarm/events" class="mc-filter-form">
|
||||
<select name="event_type" class="mc-select" onchange="this.form.submit()">
|
||||
<option value="">All Event Types</option>
|
||||
{% for et in event_types %}
|
||||
<option value="{{ et }}" {% if filter_type == et %}selected{% endif %}>{{ et }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
{% if filter_task %}
|
||||
<input type="hidden" name="task_id" value="{{ filter_task }}">
|
||||
<span class="mc-filter-tag">Task: {{ filter_task[:8] }}... <a href="/swarm/events">✕</a></span>
|
||||
{% endif %}
|
||||
|
||||
{% if filter_agent %}
|
||||
<input type="hidden" name="agent_id" value="{{ filter_agent }}">
|
||||
<span class="mc-filter-tag">Agent: {{ filter_agent[:8] }}... <a href="/swarm/events">✕</a></span>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Events Table -->
|
||||
<div class="mc-table-container">
|
||||
{% if events %}
|
||||
<table class="mc-table events-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Type</th>
|
||||
<th>Source</th>
|
||||
<th>Task</th>
|
||||
<th>Agent</th>
|
||||
<th>Data</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for event in events %}
|
||||
<tr class="event-row" data-type="{{ event.event_type.value }}">
|
||||
<td class="event-time">{{ event.timestamp[11:19] }}</td>
|
||||
<td>
|
||||
<span class="mc-badge mc-badge-{{ event.event_type.value.split('.')[0] }}">
|
||||
{{ event.event_type.value }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ event.source }}</td>
|
||||
<td>
|
||||
{% if event.task_id %}
|
||||
<a href="/swarm/events?task_id={{ event.task_id }}">{{ event.task_id[:8] }}...</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if event.agent_id %}
|
||||
<a href="/swarm/events?agent_id={{ event.agent_id }}">{{ event.agent_id[:8] }}...</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="event-data">
|
||||
{% if event.data %}
|
||||
<code>{{ event.data[:60] }}{% if event.data|length > 60 %}...{% endif %}</code>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="mc-empty-state">
|
||||
<p>No events found.</p>
|
||||
{% if filter_type or filter_task or filter_agent %}
|
||||
<p><a href="/swarm/events" class="mc-link">Clear filters</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
133
src/dashboard/templates/ledger.html
Normal file
133
src/dashboard/templates/ledger.html
Normal file
@@ -0,0 +1,133 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Lightning Ledger - Timmy Time{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mc-panel">
|
||||
<div class="mc-panel-header">
|
||||
<h1 class="page-title">Lightning Ledger</h1>
|
||||
<p class="mc-text-secondary">Bitcoin Lightning Network transaction history</p>
|
||||
</div>
|
||||
|
||||
<!-- Balance Cards -->
|
||||
<div class="mc-stats-row balance-row">
|
||||
<div class="mc-stat-card sats-balance">
|
||||
<div class="mc-stat-label">Available Balance</div>
|
||||
<div class="mc-stat-value">{{ balance.available_sats }} <small>sats</small></div>
|
||||
</div>
|
||||
<div class="mc-stat-card">
|
||||
<div class="mc-stat-label">Total Received</div>
|
||||
<div class="mc-stat-value">{{ balance.incoming_total_sats }} <small>sats</small></div>
|
||||
</div>
|
||||
<div class="mc-stat-card">
|
||||
<div class="mc-stat-label">Total Sent</div>
|
||||
<div class="mc-stat-value">{{ balance.outgoing_total_sats }} <small>sats</small></div>
|
||||
</div>
|
||||
<div class="mc-stat-card">
|
||||
<div class="mc-stat-label">Fees Paid</div>
|
||||
<div class="mc-stat-value">{{ balance.fees_paid_sats }} <small>sats</small></div>
|
||||
</div>
|
||||
<div class="mc-stat-card net-balance">
|
||||
<div class="mc-stat-label">Net</div>
|
||||
<div class="mc-stat-value {% if balance.net_sats >= 0 %}positive{% else %}negative{% endif %}">
|
||||
{{ balance.net_sats }} <small>sats</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pending Summary -->
|
||||
{% if balance.pending_incoming_sats > 0 or balance.pending_outgoing_sats > 0 %}
|
||||
<div class="mc-pending-row">
|
||||
{% if balance.pending_incoming_sats > 0 %}
|
||||
<span class="mc-pending-badge incoming">
|
||||
Pending incoming: {{ balance.pending_incoming_sats }} sats
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if balance.pending_outgoing_sats > 0 %}
|
||||
<span class="mc-pending-badge outgoing">
|
||||
Pending outgoing: {{ balance.pending_outgoing_sats }} sats
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="mc-filters">
|
||||
<form method="get" action="/lightning/ledger" class="mc-filter-form">
|
||||
<select name="tx_type" class="mc-select" onchange="this.form.submit()">
|
||||
<option value="">All Types</option>
|
||||
{% for t in tx_types %}
|
||||
<option value="{{ t }}" {% if filter_type == t %}selected{% endif %}>{{ t }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<select name="status" class="mc-select" onchange="this.form.submit()">
|
||||
<option value="">All Statuses</option>
|
||||
{% for s in tx_statuses %}
|
||||
<option value="{{ s }}" {% if filter_status == s %}selected{% endif %}>{{ s }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Transactions Table -->
|
||||
<div class="mc-table-container">
|
||||
{% if transactions %}
|
||||
<table class="mc-table transactions-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Amount</th>
|
||||
<th>Hash</th>
|
||||
<th>Memo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for tx in transactions %}
|
||||
<tr class="transaction-row" data-type="{{ tx.tx_type.value }}" data-status="{{ tx.status.value }}">
|
||||
<td>{{ tx.created_at[11:19] }}</td>
|
||||
<td>
|
||||
<span class="mc-badge mc-badge-{{ tx.tx_type.value }}">
|
||||
{{ tx.tx_type.value }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="mc-status mc-status-{{ tx.status.value }}">
|
||||
{{ tx.status.value }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="amount {% if tx.tx_type.value == 'incoming' %}positive{% else %}negative{% endif %}">
|
||||
{% if tx.tx_type.value == 'incoming' %}+{% endif %}{{ tx.amount_sats }} sats
|
||||
</td>
|
||||
<td class="mono">{{ tx.payment_hash[:16] }}...</td>
|
||||
<td>{{ tx.memo }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="mc-empty-state">
|
||||
<p>No transactions yet.</p>
|
||||
<p class="mc-text-secondary">Invoices and payments will appear here.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Weekly Stats -->
|
||||
{% if stats %}
|
||||
<div class="mc-stats-section">
|
||||
<h3>Activity (Last 7 Days)</h3>
|
||||
<div class="mc-mini-chart">
|
||||
{% for date, day_stats in stats.items() %}
|
||||
<div class="mc-chart-bar" title="{{ date }}">
|
||||
<div class="bar incoming" style="height: {{ day_stats.incoming.count * 10 }}px"></div>
|
||||
<div class="bar outgoing" style="height: {{ day_stats.outgoing.count * 10 }}px"></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
119
src/dashboard/templates/memory.html
Normal file
119
src/dashboard/templates/memory.html
Normal file
@@ -0,0 +1,119 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Memory Browser - Timmy Time{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mc-panel">
|
||||
<div class="mc-panel-header">
|
||||
<h1 class="page-title">Memory Browser</h1>
|
||||
<p class="mc-text-secondary">Semantic search through conversation history and facts</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="mc-stats-row">
|
||||
<div class="mc-stat-card">
|
||||
<div class="mc-stat-value">{{ stats.total_entries }}</div>
|
||||
<div class="mc-stat-label">Total Memories</div>
|
||||
</div>
|
||||
<div class="mc-stat-card">
|
||||
<div class="mc-stat-value">{{ stats.with_embeddings }}</div>
|
||||
<div class="mc-stat-label">With Embeddings</div>
|
||||
</div>
|
||||
<div class="mc-stat-card">
|
||||
<div class="mc-stat-value">{% if stats.has_embedding_model %}✓{% else %}○{% endif %}</div>
|
||||
<div class="mc-stat-label">AI Search</div>
|
||||
</div>
|
||||
{% for type, count in stats.by_type.items() %}
|
||||
<div class="mc-stat-card">
|
||||
<div class="mc-stat-value">{{ count }}</div>
|
||||
<div class="mc-stat-label">{{ type }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="mc-search-section">
|
||||
<form method="get" action="/memory" class="mc-search-form">
|
||||
<input
|
||||
type="search"
|
||||
name="query"
|
||||
class="mc-search-input"
|
||||
placeholder="Search memories..."
|
||||
value="{{ query or '' }}"
|
||||
autofocus
|
||||
>
|
||||
<button type="submit" class="mc-btn mc-btn-primary">Search</button>
|
||||
</form>
|
||||
|
||||
{% if query %}
|
||||
<p class="mc-search-info">Searching for: "{{ query }}"</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Search Results -->
|
||||
{% if query %}
|
||||
<div class="mc-results-section">
|
||||
<h3>Search Results</h3>
|
||||
|
||||
{% if results %}
|
||||
<div class="memory-results">
|
||||
{% for mem in results %}
|
||||
<div class="memory-entry" data-relevance="{{ mem.relevance_score }}">
|
||||
<div class="memory-header">
|
||||
<span class="memory-source">{{ mem.source }}</span>
|
||||
<span class="memory-type mc-badge">{{ mem.context_type }}</span>
|
||||
{% if mem.relevance_score %}
|
||||
<span class="memory-score">{{ "%.2f"|format(mem.relevance_score) }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="memory-content">{{ mem.content }}</div>
|
||||
<div class="memory-meta">
|
||||
<span class="memory-time">{{ mem.timestamp[11:16] }}</span>
|
||||
{% if mem.agent_id %}
|
||||
<span class="memory-agent">Agent: {{ mem.agent_id[:8] }}...</span>
|
||||
{% endif %}
|
||||
{% if mem.task_id %}
|
||||
<span class="memory-task">Task: {{ mem.task_id[:8] }}...</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mc-empty-state">
|
||||
<p>No results found for "{{ query }}"</p>
|
||||
<p class="mc-text-secondary">Try different keywords or check spelling.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Personal Facts -->
|
||||
<div class="mc-facts-section">
|
||||
<div class="mc-section-header">
|
||||
<h3>Personal Facts</h3>
|
||||
<button class="mc-btn mc-btn-small" onclick="document.getElementById('add-fact-form').style.display='block'">
|
||||
+ Add Fact
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form id="add-fact-form" class="mc-inline-form" method="post" action="/memory/fact" style="display:none;" hx-post="/memory/fact" hx-target=".mc-facts-list">
|
||||
<input type="text" name="fact" class="mc-input" placeholder="Enter a fact..." required>
|
||||
<button type="submit" class="mc-btn mc-btn-primary">Save</button>
|
||||
<button type="button" class="mc-btn" onclick="document.getElementById('add-fact-form').style.display='none'">Cancel</button>
|
||||
</form>
|
||||
|
||||
<div class="mc-facts-list">
|
||||
{% if facts %}
|
||||
<ul class="mc-fact-list">
|
||||
{% for fact in facts %}
|
||||
<li class="memory-fact">{{ fact }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="mc-text-secondary">No personal facts stored yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
202
src/dashboard/templates/router_status.html
Normal file
202
src/dashboard/templates/router_status.html
Normal file
@@ -0,0 +1,202 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Router Status - Timmy Time{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mc-panel">
|
||||
<div class="mc-panel-header">
|
||||
<h1 class="page-title">Router Status</h1>
|
||||
<p class="mc-text-secondary">LLM provider health and metrics</p>
|
||||
</div>
|
||||
|
||||
<!-- Overall Stats -->
|
||||
<div class="mc-stats-row">
|
||||
<div class="mc-stat-card">
|
||||
<div class="mc-stat-value">{{ providers|length }}</div>
|
||||
<div class="mc-stat-label">Providers</div>
|
||||
</div>
|
||||
<div class="mc-stat-card">
|
||||
<div class="mc-stat-value">{{ total_requests }}</div>
|
||||
<div class="mc-stat-label">Total Requests</div>
|
||||
</div>
|
||||
<div class="mc-stat-card">
|
||||
<div class="mc-stat-value">{{ total_success }}</div>
|
||||
<div class="mc-stat-label">Successful</div>
|
||||
</div>
|
||||
<div class="mc-stat-card">
|
||||
<div class="mc-stat-value">{{ total_failed }}</div>
|
||||
<div class="mc-stat-label">Failed</div>
|
||||
</div>
|
||||
<div class="mc-stat-card">
|
||||
<div class="mc-stat-value">{{ avg_latency_ms }}<small>ms</small></div>
|
||||
<div class="mc-stat-label">Avg Latency</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preferred Provider -->
|
||||
{% if preferred_provider %}
|
||||
<div class="mc-alert mc-alert-success">
|
||||
<strong>Preferred Provider:</strong> {{ preferred_provider }}
|
||||
<span class="mc-badge mc-badge-success">ACTIVE</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mc-alert mc-alert-warning">
|
||||
<strong>Warning:</strong> No healthy providers available
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Provider Cards -->
|
||||
<div class="mc-providers-grid">
|
||||
{% for provider in providers %}
|
||||
<div class="mc-provider-card provider-{{ provider.status }}">
|
||||
<div class="provider-header">
|
||||
<h3>{{ provider.name }}</h3>
|
||||
<span class="mc-badge mc-badge-{{ provider.status }}">
|
||||
{{ provider.status }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="provider-meta">
|
||||
<span class="provider-type">{{ provider.type }}</span>
|
||||
<span class="provider-priority">Priority: {{ provider.priority }}</span>
|
||||
{% if not provider.enabled %}
|
||||
<span class="mc-badge mc-badge-disabled">DISABLED</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="provider-circuit">
|
||||
Circuit: <span class="circuit-{{ provider.circuit_state }}">{{ provider.circuit_state }}</span>
|
||||
</div>
|
||||
|
||||
<div class="provider-metrics">
|
||||
<div class="metric">
|
||||
<span class="metric-value">{{ provider.metrics.total }}</span>
|
||||
<span class="metric-label">Requests</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-value">{{ provider.metrics.success }}</span>
|
||||
<span class="metric-label">Success</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-value">{{ provider.metrics.failed }}</span>
|
||||
<span class="metric-label">Failed</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-value">{{ provider.metrics.avg_latency_ms }}ms</span>
|
||||
<span class="metric-label">Latency</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-value">{{ "%.1f"|format(provider.metrics.error_rate * 100) }}%</span>
|
||||
<span class="metric-label">Error Rate</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if provider.metrics.error_rate > 0.1 %}
|
||||
<div class="mc-alert mc-alert-warning mc-alert-small">
|
||||
High error rate detected
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if not providers %}
|
||||
<div class="mc-empty-state">
|
||||
<p>No providers configured.</p>
|
||||
<p class="mc-text-secondary">Check config/providers.yaml</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.mc-providers-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.mc-provider-card {
|
||||
background: rgba(10, 15, 30, 0.6);
|
||||
border: 1px solid var(--mc-border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.mc-provider-card.provider-healthy {
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.mc-provider-card.provider-degraded {
|
||||
border-left: 4px solid #ffc107;
|
||||
}
|
||||
|
||||
.mc-provider-card.provider-unhealthy {
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
|
||||
.provider-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.provider-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.provider-meta {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--mc-text-secondary);
|
||||
}
|
||||
|
||||
.provider-circuit {
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.circuit-closed { color: #28a745; }
|
||||
.circuit-open { color: #dc3545; }
|
||||
.circuit-half_open { color: #ffc107; }
|
||||
|
||||
.provider-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric {
|
||||
padding: 0.5rem;
|
||||
background: rgba(0,0,0,0.2);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
display: block;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--mc-gold);
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: var(--mc-text-secondary);
|
||||
}
|
||||
|
||||
.mc-alert-small {
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -35,6 +35,89 @@
|
||||
.swarm-title { font-size: 1rem; }
|
||||
.swarm-log-box { height: 160px; font-size: 11px; }
|
||||
}
|
||||
|
||||
/* Activity Feed Styles */
|
||||
.activity-feed-panel {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.activity-feed {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: rgba(24, 10, 45, 0.6);
|
||||
padding: 12px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
.activity-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-5px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.activity-icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.activity-label {
|
||||
font-weight: 600;
|
||||
color: var(--text-bright);
|
||||
font-size: 12px;
|
||||
}
|
||||
.activity-desc {
|
||||
color: var(--text-dim);
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.activity-meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.activity-time {
|
||||
font-family: var(--font);
|
||||
color: var(--amber);
|
||||
}
|
||||
.activity-source {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.activity-empty {
|
||||
color: var(--text-dim);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.activity-badge {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #28a745;
|
||||
border-radius: 50%;
|
||||
margin-left: 8px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -76,6 +159,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activity Feed Panel -->
|
||||
<div class="card mc-panel activity-feed-panel">
|
||||
<div class="card-header mc-panel-header">
|
||||
// LIVE ACTIVITY FEED
|
||||
<span class="activity-badge" id="activity-badge"></span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="activity-feed" id="activity-feed">
|
||||
<div class="activity-empty">Waiting for events...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mc-panel">
|
||||
<div class="card-header mc-panel-header">// SWARM LOG</div>
|
||||
<div class="card-body p-0">
|
||||
@@ -125,6 +221,16 @@ function connect() {
|
||||
}
|
||||
|
||||
function handleMessage(message) {
|
||||
// Handle activity feed events (from event_log broadcaster)
|
||||
if (message.type === 'event' && message.payload) {
|
||||
addActivityEvent(message.payload);
|
||||
// Also add to log
|
||||
var evt = message.payload;
|
||||
var logMsg = evt.event_type + ': ' + (evt.source || '');
|
||||
addLog(logMsg, 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'initial_state' || message.type === 'state_update') {
|
||||
var data = message.data;
|
||||
document.getElementById('stat-agents').textContent = data.agents.total;
|
||||
@@ -158,6 +264,87 @@ function handleMessage(message) {
|
||||
}
|
||||
}
|
||||
|
||||
// Activity Feed Functions
|
||||
const EVENT_ICONS = {
|
||||
'task.created': '📝',
|
||||
'task.bidding': '⏳',
|
||||
'task.assigned': '👤',
|
||||
'task.started': '▶️',
|
||||
'task.completed': '✅',
|
||||
'task.failed': '❌',
|
||||
'agent.joined': '🟢',
|
||||
'agent.left': '🔴',
|
||||
'bid.submitted': '💰',
|
||||
'auction.closed': '🏁',
|
||||
'tool.called': '🔧',
|
||||
'system.error': '⚠️',
|
||||
};
|
||||
|
||||
const EVENT_LABELS = {
|
||||
'task.created': 'New task',
|
||||
'task.assigned': 'Task assigned',
|
||||
'task.completed': 'Task completed',
|
||||
'task.failed': 'Task failed',
|
||||
'agent.joined': 'Agent joined',
|
||||
'agent.left': 'Agent left',
|
||||
'bid.submitted': 'Bid submitted',
|
||||
};
|
||||
|
||||
function addActivityEvent(evt) {
|
||||
var container = document.getElementById('activity-feed');
|
||||
|
||||
// Remove empty message if present
|
||||
var empty = container.querySelector('.activity-empty');
|
||||
if (empty) empty.remove();
|
||||
|
||||
// Create activity item
|
||||
var item = document.createElement('div');
|
||||
item.className = 'activity-item';
|
||||
|
||||
var icon = EVENT_ICONS[evt.event_type] || '•';
|
||||
var label = EVENT_LABELS[evt.event_type] || evt.event_type;
|
||||
var time = evt.timestamp ? evt.timestamp.split('T')[1].slice(0, 8) : '--:--:--';
|
||||
|
||||
// Build description from data
|
||||
var desc = '';
|
||||
if (evt.data) {
|
||||
try {
|
||||
var data = typeof evt.data === 'string' ? JSON.parse(evt.data) : evt.data;
|
||||
if (data.description) desc = data.description.slice(0, 50);
|
||||
else if (data.reason) desc = data.reason.slice(0, 50);
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="activity-icon">${icon}</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-label">${label}</div>
|
||||
${desc ? `<div class="activity-desc">${desc}</div>` : ''}
|
||||
<div class="activity-meta">
|
||||
<span class="activity-time">${time}</span>
|
||||
<span class="activity-source">${evt.source || 'system'}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add to top
|
||||
container.insertBefore(item, container.firstChild);
|
||||
|
||||
// Keep only last 50 items
|
||||
while (container.children.length > 50) {
|
||||
container.removeChild(container.lastChild);
|
||||
}
|
||||
|
||||
// Update badge
|
||||
var badge = document.getElementById('activity-badge');
|
||||
if (badge) {
|
||||
badge.style.background = '#28a745';
|
||||
setTimeout(() => {
|
||||
badge.style.background = '';
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshStats() {
|
||||
fetch('/swarm').then(function(r) { return r.json(); }).then(function(data) {
|
||||
document.getElementById('stat-agents').textContent = data.agents || 0;
|
||||
|
||||
290
src/dashboard/templates/upgrade_queue.html
Normal file
290
src/dashboard/templates/upgrade_queue.html
Normal file
@@ -0,0 +1,290 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Upgrade Queue - Timmy Time{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mc-panel">
|
||||
<div class="mc-panel-header">
|
||||
<h1 class="page-title">Upgrade Queue</h1>
|
||||
<p class="mc-text-secondary">Review and approve self-modification proposals</p>
|
||||
</div>
|
||||
|
||||
<!-- Pending Upgrades -->
|
||||
<div class="mc-section">
|
||||
<h2 class="mc-section-title">
|
||||
Pending Upgrades
|
||||
{% if pending_count > 0 %}
|
||||
<span class="mc-badge mc-badge-warning">{{ pending_count }}</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
||||
{% if pending %}
|
||||
<div class="upgrades-list">
|
||||
{% for upgrade in pending %}
|
||||
<div class="upgrade-card upgrade-pending" data-id="{{ upgrade.id }}">
|
||||
<div class="upgrade-header">
|
||||
<h3>{{ upgrade.description }}</h3>
|
||||
<span class="mc-badge mc-badge-warning">PENDING</span>
|
||||
</div>
|
||||
|
||||
<div class="upgrade-meta">
|
||||
<span class="upgrade-branch">Branch: {{ upgrade.branch_name }}</span>
|
||||
<span class="upgrade-time">Proposed: {{ upgrade.proposed_at[11:16] }}</span>
|
||||
</div>
|
||||
|
||||
<div class="upgrade-files">
|
||||
Files: {{ upgrade.files_changed|join(', ') }}
|
||||
</div>
|
||||
|
||||
<div class="upgrade-test-status">
|
||||
{% if upgrade.test_passed %}
|
||||
<span class="test-passed">✓ Tests passed</span>
|
||||
{% else %}
|
||||
<span class="test-failed">✗ Tests failed</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="upgrade-actions">
|
||||
<button class="mc-btn mc-btn-primary" onclick="approveUpgrade('{{ upgrade.id }}')">
|
||||
Approve
|
||||
</button>
|
||||
<button class="mc-btn" onclick="rejectUpgrade('{{ upgrade.id }}')">
|
||||
Reject
|
||||
</button>
|
||||
<a href="/self-modify/queue/{{ upgrade.id }}/diff" class="mc-btn mc-btn-secondary">
|
||||
View Diff
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mc-empty-state">
|
||||
<p>No pending upgrades.</p>
|
||||
<p class="mc-text-secondary">Proposed modifications will appear here for review.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Approved (Waiting to Apply) -->
|
||||
{% if approved %}
|
||||
<div class="mc-section">
|
||||
<h2 class="mc-section-title">Approved (Ready to Apply)</h2>
|
||||
<div class="upgrades-list">
|
||||
{% for upgrade in approved %}
|
||||
<div class="upgrade-card upgrade-approved">
|
||||
<div class="upgrade-header">
|
||||
<h3>{{ upgrade.description }}</h3>
|
||||
<span class="mc-badge mc-badge-success">APPROVED</span>
|
||||
</div>
|
||||
<div class="upgrade-actions">
|
||||
<button class="mc-btn mc-btn-primary" onclick="applyUpgrade('{{ upgrade.id }}')">
|
||||
Apply Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- History -->
|
||||
<div class="mc-section">
|
||||
<h2 class="mc-section-title">History</h2>
|
||||
|
||||
{% if applied %}
|
||||
<h4>Applied</h4>
|
||||
<div class="upgrades-list upgrades-history">
|
||||
{% for upgrade in applied %}
|
||||
<div class="upgrade-card upgrade-applied">
|
||||
<span class="upgrade-desc">{{ upgrade.description }}</span>
|
||||
<span class="mc-badge mc-badge-success">APPLIED</span>
|
||||
<span class="upgrade-time">{{ upgrade.applied_at[11:16] if upgrade.applied_at else '' }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if rejected %}
|
||||
<h4>Rejected</h4>
|
||||
<div class="upgrades-list upgrades-history">
|
||||
{% for upgrade in rejected %}
|
||||
<div class="upgrade-card upgrade-rejected">
|
||||
<span class="upgrade-desc">{{ upgrade.description }}</span>
|
||||
<span class="mc-badge mc-badge-secondary">REJECTED</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if failed %}
|
||||
<h4>Failed</h4>
|
||||
<div class="upgrades-list upgrades-history">
|
||||
{% for upgrade in failed %}
|
||||
<div class="upgrade-card upgrade-failed">
|
||||
<span class="upgrade-desc">{{ upgrade.description }}</span>
|
||||
<span class="mc-badge mc-badge-danger">FAILED</span>
|
||||
<span class="upgrade-error" title="{{ upgrade.error_message }}">⚠️</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function approveUpgrade(id) {
|
||||
if (!confirm('Approve this upgrade?')) return;
|
||||
|
||||
const response = await fetch(`/self-modify/queue/${id}/approve`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Failed to approve: ' + await response.text());
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectUpgrade(id) {
|
||||
if (!confirm('Reject this upgrade? The branch will be deleted.')) return;
|
||||
|
||||
const response = await fetch(`/self-modify/queue/${id}/reject`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Failed to reject: ' + await response.text());
|
||||
}
|
||||
}
|
||||
|
||||
async function applyUpgrade(id) {
|
||||
if (!confirm('Apply this upgrade? This will merge to main.')) return;
|
||||
|
||||
const response = await fetch(`/self-modify/queue/${id}/apply`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Upgrade applied successfully!');
|
||||
window.location.reload();
|
||||
} else {
|
||||
const error = await response.text();
|
||||
alert('Failed to apply: ' + error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.mc-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.mc-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.upgrades-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.upgrade-card {
|
||||
background: rgba(10, 15, 30, 0.6);
|
||||
border: 1px solid var(--mc-border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.upgrade-pending {
|
||||
border-left: 4px solid #ffc107;
|
||||
}
|
||||
|
||||
.upgrade-approved {
|
||||
border-left: 4px solid #17a2b8;
|
||||
}
|
||||
|
||||
.upgrade-applied {
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.upgrade-rejected {
|
||||
border-left: 4px solid #6c757d;
|
||||
}
|
||||
|
||||
.upgrade-failed {
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
|
||||
.upgrade-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.upgrade-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.upgrade-meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--mc-text-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.upgrade-files {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.upgrade-test-status {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.test-passed {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.test-failed {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.upgrade-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.upgrades-history .upgrade-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.upgrade-desc {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.upgrade-time {
|
||||
font-size: 0.85rem;
|
||||
color: var(--mc-text-secondary);
|
||||
}
|
||||
|
||||
.upgrade-error {
|
||||
color: #dc3545;
|
||||
cursor: help;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user