Files
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

256 lines
8.2 KiB
JavaScript

// OpenFang Logs Page — Real-time log viewer (SSE streaming + polling fallback) + Audit Trail tab
'use strict';
function logsPage() {
return {
tab: 'live',
// -- Live logs state --
entries: [],
levelFilter: '',
textFilter: '',
autoRefresh: true,
hovering: false,
loading: true,
loadError: '',
_pollTimer: null,
// -- SSE streaming state --
_eventSource: null,
streamConnected: false,
streamPaused: false,
// -- Audit state --
auditEntries: [],
tipHash: '',
chainValid: null,
filterAction: '',
auditLoading: false,
auditLoadError: '',
startStreaming: function() {
var self = this;
if (this._eventSource) { this._eventSource.close(); this._eventSource = null; }
var url = '/api/logs/stream';
var sep = '?';
var token = OpenFangAPI.getToken();
if (token) { url += sep + 'token=' + encodeURIComponent(token); sep = '&'; }
try {
this._eventSource = new EventSource(url);
} catch(e) {
// EventSource not supported or blocked; fall back to polling
this.streamConnected = false;
this.startPolling();
return;
}
this._eventSource.onopen = function() {
self.streamConnected = true;
self.loading = false;
self.loadError = '';
};
this._eventSource.onmessage = function(event) {
if (self.streamPaused) return;
try {
var entry = JSON.parse(event.data);
// Avoid duplicate entries by checking seq
var dominated = false;
for (var i = 0; i < self.entries.length; i++) {
if (self.entries[i].seq === entry.seq) { dominated = true; break; }
}
if (!dominated) {
self.entries.push(entry);
// Cap at 500 entries (remove oldest)
if (self.entries.length > 500) {
self.entries.splice(0, self.entries.length - 500);
}
// Auto-scroll to bottom
if (self.autoRefresh && !self.hovering) {
self.$nextTick(function() {
var el = document.getElementById('log-container');
if (el) el.scrollTop = el.scrollHeight;
});
}
}
} catch(e) {
// Ignore parse errors (heartbeat comments are not delivered to onmessage)
}
};
this._eventSource.onerror = function() {
self.streamConnected = false;
if (self._eventSource) {
self._eventSource.close();
self._eventSource = null;
}
// Fall back to polling
self.startPolling();
};
},
startPolling: function() {
var self = this;
this.streamConnected = false;
this.fetchLogs();
if (this._pollTimer) clearInterval(this._pollTimer);
this._pollTimer = setInterval(function() {
if (self.autoRefresh && !self.hovering && self.tab === 'live' && !self.streamPaused) {
self.fetchLogs();
}
}, 2000);
},
async fetchLogs() {
if (this.loading) this.loadError = '';
try {
var data = await OpenFangAPI.get('/api/audit/recent?n=200');
this.entries = data.entries || [];
if (this.autoRefresh && !this.hovering) {
this.$nextTick(function() {
var el = document.getElementById('log-container');
if (el) el.scrollTop = el.scrollHeight;
});
}
if (this.loading) this.loading = false;
} catch(e) {
if (this.loading) {
this.loadError = e.message || 'Could not load logs.';
this.loading = false;
}
}
},
async loadData() {
this.loading = true;
return this.fetchLogs();
},
togglePause: function() {
this.streamPaused = !this.streamPaused;
if (!this.streamPaused && this.streamConnected) {
// Resume: scroll to bottom
var self = this;
this.$nextTick(function() {
var el = document.getElementById('log-container');
if (el) el.scrollTop = el.scrollHeight;
});
}
},
clearLogs: function() {
this.entries = [];
},
classifyLevel: function(action) {
if (!action) return 'info';
var a = action.toLowerCase();
if (a.indexOf('error') !== -1 || a.indexOf('fail') !== -1 || a.indexOf('crash') !== -1) return 'error';
if (a.indexOf('warn') !== -1 || a.indexOf('deny') !== -1 || a.indexOf('block') !== -1) return 'warn';
return 'info';
},
get filteredEntries() {
var self = this;
var levelF = this.levelFilter;
var textF = this.textFilter.toLowerCase();
return this.entries.filter(function(e) {
if (levelF && self.classifyLevel(e.action) !== levelF) return false;
if (textF) {
var haystack = ((e.action || '') + ' ' + (e.detail || '') + ' ' + (e.agent_id || '')).toLowerCase();
if (haystack.indexOf(textF) === -1) return false;
}
return true;
});
},
get connectionLabel() {
if (this.streamPaused) return 'Paused';
if (this.streamConnected) return 'Live';
if (this._pollTimer) return 'Polling';
return 'Disconnected';
},
get connectionClass() {
if (this.streamPaused) return 'paused';
if (this.streamConnected) return 'live';
if (this._pollTimer) return 'polling';
return 'disconnected';
},
exportLogs: function() {
var lines = this.filteredEntries.map(function(e) {
return new Date(e.timestamp).toISOString() + ' [' + e.action + '] ' + (e.detail || '');
});
var blob = new Blob([lines.join('\n')], { type: 'text/plain' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'openfang-logs-' + new Date().toISOString().slice(0, 10) + '.txt';
a.click();
URL.revokeObjectURL(url);
},
// -- Audit methods --
get filteredAuditEntries() {
var self = this;
if (!self.filterAction) return self.auditEntries;
return self.auditEntries.filter(function(e) { return e.action === self.filterAction; });
},
async loadAudit() {
this.auditLoading = true;
this.auditLoadError = '';
try {
var data = await OpenFangAPI.get('/api/audit/recent?n=200');
this.auditEntries = data.entries || [];
this.tipHash = data.tip_hash || '';
} catch(e) {
this.auditEntries = [];
this.auditLoadError = e.message || 'Could not load audit log.';
}
this.auditLoading = false;
},
auditAgentName: function(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) + '...';
},
friendlyAction: function(action) {
if (!action) return 'Unknown';
var map = {
'AgentSpawn': 'Agent Created', 'AgentKill': 'Agent Stopped', 'AgentTerminated': 'Agent Stopped',
'ToolInvoke': 'Tool Used', 'ToolResult': 'Tool Completed', 'AgentMessage': 'Message',
'NetworkAccess': 'Network Access', 'ShellExec': 'Shell Command', 'FileAccess': 'File Access',
'MemoryAccess': 'Memory Access', 'AuthAttempt': 'Login Attempt', 'AuthSuccess': 'Login Success',
'AuthFailure': 'Login Failed', 'CapabilityDenied': 'Permission Denied', 'RateLimited': 'Rate Limited'
};
return map[action] || action.replace(/([A-Z])/g, ' $1').trim();
},
async verifyChain() {
try {
var data = await OpenFangAPI.get('/api/audit/verify');
this.chainValid = data.valid === true;
if (this.chainValid) {
OpenFangToast.success('Audit chain verified — ' + (data.entries || 0) + ' entries valid');
} else {
OpenFangToast.error('Audit chain broken!');
}
} catch(e) {
this.chainValid = false;
OpenFangToast.error('Chain verification failed: ' + e.message);
}
},
destroy: function() {
if (this._eventSource) { this._eventSource.close(); this._eventSource = null; }
if (this._pollTimer) { clearInterval(this._pollTimer); this._pollTimer = null; }
}
};
}