Files
openfang/crates/openfang-api/static/js/pages/overview.js
iven 92e5def702
Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled
初始化提交
2026-03-01 16:24:24 +08:00

293 lines
10 KiB
JavaScript

// OpenFang Overview Dashboard — Landing page with system stats + provider status
'use strict';
function overviewPage() {
return {
health: {},
status: {},
usageSummary: {},
recentAudit: [],
channels: [],
providers: [],
mcpServers: [],
skillCount: 0,
loading: true,
loadError: '',
refreshTimer: null,
lastRefresh: null,
async loadOverview() {
this.loading = true;
this.loadError = '';
try {
await Promise.all([
this.loadHealth(),
this.loadStatus(),
this.loadUsage(),
this.loadAudit(),
this.loadChannels(),
this.loadProviders(),
this.loadMcpServers(),
this.loadSkills()
]);
this.lastRefresh = Date.now();
} catch(e) {
this.loadError = e.message || 'Could not load overview data.';
}
this.loading = false;
},
async loadData() { return this.loadOverview(); },
// Silent background refresh (no loading spinner)
async silentRefresh() {
try {
await Promise.all([
this.loadHealth(),
this.loadStatus(),
this.loadUsage(),
this.loadAudit(),
this.loadChannels(),
this.loadProviders(),
this.loadMcpServers(),
this.loadSkills()
]);
this.lastRefresh = Date.now();
} catch(e) { /* silent */ }
},
startAutoRefresh() {
this.stopAutoRefresh();
this.refreshTimer = setInterval(() => this.silentRefresh(), 30000);
},
stopAutoRefresh() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
},
async loadHealth() {
try {
this.health = await OpenFangAPI.get('/api/health');
} catch(e) { this.health = { status: 'unreachable' }; }
},
async loadStatus() {
try {
this.status = await OpenFangAPI.get('/api/status');
} catch(e) { this.status = {}; throw e; }
},
async loadUsage() {
try {
var data = await OpenFangAPI.get('/api/usage');
var agents = data.agents || [];
var totalTokens = 0;
var totalTools = 0;
var totalCost = 0;
agents.forEach(function(a) {
totalTokens += (a.total_tokens || 0);
totalTools += (a.tool_calls || 0);
totalCost += (a.cost_usd || 0);
});
this.usageSummary = {
total_tokens: totalTokens,
total_tools: totalTools,
total_cost: totalCost,
agent_count: agents.length
};
} catch(e) {
this.usageSummary = { total_tokens: 0, total_tools: 0, total_cost: 0, agent_count: 0 };
}
},
async loadAudit() {
try {
var data = await OpenFangAPI.get('/api/audit/recent?n=8');
this.recentAudit = data.entries || [];
} catch(e) { this.recentAudit = []; }
},
async loadChannels() {
try {
var data = await OpenFangAPI.get('/api/channels');
this.channels = (data.channels || []).filter(function(ch) { return ch.has_token; });
} catch(e) { this.channels = []; }
},
async loadProviders() {
try {
var data = await OpenFangAPI.get('/api/providers');
this.providers = data.providers || [];
} catch(e) { this.providers = []; }
},
async loadMcpServers() {
try {
var data = await OpenFangAPI.get('/api/mcp/servers');
this.mcpServers = data.servers || [];
} catch(e) { this.mcpServers = []; }
},
async loadSkills() {
try {
var data = await OpenFangAPI.get('/api/skills');
this.skillCount = (data.skills || []).length;
} catch(e) { this.skillCount = 0; }
},
get configuredProviders() {
return this.providers.filter(function(p) { return p.auth_status === 'configured'; });
},
get unconfiguredProviders() {
return this.providers.filter(function(p) { return p.auth_status === 'not_set' || p.auth_status === 'missing'; });
},
get connectedMcp() {
return this.mcpServers.filter(function(s) { return s.status === 'connected'; });
},
// Provider health badge color
providerBadgeClass(p) {
if (p.auth_status === 'configured') {
if (p.health === 'cooldown' || p.health === 'open') return 'badge-warn';
return 'badge-success';
}
if (p.auth_status === 'not_set' || p.auth_status === 'missing') return 'badge-muted';
return 'badge-dim';
},
// Provider health tooltip
providerTooltip(p) {
if (p.health === 'cooldown') return p.display_name + ' \u2014 cooling down (rate limited)';
if (p.health === 'open') return p.display_name + ' \u2014 circuit breaker open';
if (p.auth_status === 'configured') return p.display_name + ' \u2014 ready';
return p.display_name + ' \u2014 not configured';
},
// Audit action badge color
actionBadgeClass(action) {
if (!action) return 'badge-dim';
if (action === 'AgentSpawn' || action === 'AuthSuccess') return 'badge-success';
if (action === 'AgentKill' || action === 'AgentTerminated' || action === 'AuthFailure' || action === 'CapabilityDenied') return 'badge-error';
if (action === 'RateLimited' || action === 'ToolInvoke') return 'badge-warn';
return 'badge-created';
},
// ── Setup Checklist ──
checklistDismissed: localStorage.getItem('of-checklist-dismissed') === 'true',
get setupChecklist() {
return [
{ key: 'provider', label: 'Configure an LLM provider', done: this.configuredProviders.length > 0, action: '#settings' },
{ key: 'agent', label: 'Create your first agent', done: (Alpine.store('app').agents || []).length > 0, action: '#agents' },
{ key: 'chat', label: 'Send your first message', done: localStorage.getItem('of-first-msg') === 'true', action: '#chat' },
{ key: 'channel', label: 'Connect a messaging channel', done: this.channels.length > 0, action: '#channels' },
{ key: 'skill', label: 'Browse or install a skill', done: localStorage.getItem('of-skill-browsed') === 'true', action: '#skills' }
];
},
get setupProgress() {
var done = this.setupChecklist.filter(function(item) { return item.done; }).length;
return (done / 5) * 100;
},
get setupDoneCount() {
return this.setupChecklist.filter(function(item) { return item.done; }).length;
},
dismissChecklist() {
this.checklistDismissed = true;
localStorage.setItem('of-checklist-dismissed', 'true');
},
formatUptime(secs) {
if (!secs) return '-';
var d = Math.floor(secs / 86400);
var h = Math.floor((secs % 86400) / 3600);
var m = Math.floor((secs % 3600) / 60);
if (d > 0) return d + 'd ' + h + 'h';
if (h > 0) return h + 'h ' + m + 'm';
return m + 'm';
},
formatNumber(n) {
if (!n) return '0';
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
return String(n);
},
formatCost(n) {
if (!n || n === 0) return '$0.00';
if (n < 0.01) return '<$0.01';
return '$' + n.toFixed(2);
},
// Relative time formatting ("2m ago", "1h ago", "just now")
timeAgo(timestamp) {
if (!timestamp) return '';
var now = Date.now();
var ts = new Date(timestamp).getTime();
var diff = Math.floor((now - ts) / 1000);
if (diff < 10) return 'just now';
if (diff < 60) return diff + 's ago';
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
return Math.floor(diff / 86400) + 'd ago';
},
// Map raw audit action names to user-friendly labels
friendlyAction(action) {
if (!action) return 'Unknown';
var map = {
'AgentSpawn': 'Agent Created',
'AgentKill': 'Agent Stopped',
'AgentTerminated': 'Agent Stopped',
'ToolInvoke': 'Tool Used',
'ToolResult': 'Tool Completed',
'MessageReceived': 'Message In',
'MessageSent': 'Response Sent',
'SessionReset': 'Session Reset',
'SessionCompact': 'Compacted',
'ModelSwitch': 'Model Changed',
'AuthAttempt': 'Login Attempt',
'AuthSuccess': 'Login OK',
'AuthFailure': 'Login Failed',
'CapabilityDenied': 'Denied',
'RateLimited': 'Rate Limited',
'WorkflowRun': 'Workflow Run',
'TriggerFired': 'Trigger Fired',
'SkillInstalled': 'Skill Installed',
'McpConnected': 'MCP Connected'
};
return map[action] || action.replace(/([A-Z])/g, ' $1').trim();
},
// Audit action icon (small inline SVG)
actionIcon(action) {
if (!action) return '';
var icons = {
'AgentSpawn': '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 8v8M8 12h8"/></svg>',
'AgentKill': '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M15 9l-6 6M9 9l6 6"/></svg>',
'AgentTerminated': '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M15 9l-6 6M9 9l6 6"/></svg>',
'ToolInvoke': '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>',
'MessageReceived': '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
'MessageSent': '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/></svg>'
};
return icons[action] || '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg>';
},
// Resolve agent UUID to name if possible
agentName(agentId) {
if (!agentId) return '-';
var agents = Alpine.store('app').agents || [];
var agent = agents.find(function(a) { return a.id === agentId; });
return agent ? agent.name : agentId.substring(0, 8) + '\u2026';
}
};
}