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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user