polyv-live-cli
Version:
CLI tool for managing PolyV live streaming services.
492 lines • 19.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.AlertHistoryManager = void 0;
const events_1 = require("events");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
class AlertHistoryManager extends events_1.EventEmitter {
constructor(config) {
super();
this.historyEntries = [];
this.isDirty = false;
this.maxHistoryEntries = config?.maxHistoryEntries || 10000;
this.persistenceEnabled = config?.persistenceEnabled !== false;
this.persistenceFilePath = config?.persistenceFile || path.join(process.cwd(), '.polyv-alerts', 'alert-history.json');
this.initializePersistence();
this.setupAutoSave();
}
async initializePersistence() {
if (!this.persistenceEnabled)
return;
try {
const dir = path.dirname(this.persistenceFilePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
await this.loadHistory();
}
catch (error) {
console.warn('Failed to initialize alert history persistence:', error);
this.emit('persistence:error', { action: 'initialize', error });
}
}
setupAutoSave() {
if (!this.persistenceEnabled)
return;
this.autoSaveInterval = setInterval(async () => {
if (this.isDirty) {
await this.saveHistory();
}
}, 30000);
}
async loadHistory() {
if (!this.persistenceEnabled || !fs.existsSync(this.persistenceFilePath)) {
return;
}
try {
const data = fs.readFileSync(this.persistenceFilePath, 'utf8');
const parsed = JSON.parse(data);
if (Array.isArray(parsed.entries)) {
this.historyEntries = parsed.entries.map((entry) => ({
...entry,
alert: { ...entry.alert },
timestamp: typeof entry.timestamp === 'string' ?
new Date(entry.timestamp).getTime() : entry.timestamp
}));
if (this.historyEntries.length > this.maxHistoryEntries) {
this.historyEntries = this.historyEntries
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, this.maxHistoryEntries);
this.isDirty = true;
}
}
this.emit('history:loaded', {
count: this.historyEntries.length,
file: this.persistenceFilePath
});
}
catch (error) {
console.warn('Failed to load alert history:', error);
this.emit('persistence:error', { action: 'load', error });
}
}
async saveHistory() {
if (!this.persistenceEnabled || !this.isDirty)
return;
try {
const data = {
version: '1.0',
timestamp: new Date().toISOString(),
count: this.historyEntries.length,
entries: this.historyEntries
};
const dir = path.dirname(this.persistenceFilePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(this.persistenceFilePath, JSON.stringify(data, null, 2), 'utf8');
this.isDirty = false;
this.emit('history:saved', {
count: this.historyEntries.length,
file: this.persistenceFilePath
});
}
catch (error) {
console.warn('Failed to save alert history:', error);
this.emit('persistence:error', { action: 'save', error });
}
}
addAlert(alert, action = 'created', user, context) {
const entry = {
alert: { ...alert },
timestamp: Date.now(),
action,
...(user && { user }),
...(context && { context })
};
this.historyEntries.unshift(entry);
this.enforceMaxSize();
this.isDirty = true;
this.emit('entry:added', { entry, alert });
}
recordAction(alertId, action, user, context) {
const alertEntry = this.historyEntries.find(entry => entry.alert.id === alertId);
if (!alertEntry)
return null;
const actionEntry = {
alert: { ...alertEntry.alert },
timestamp: Date.now(),
action,
...(user && { user }),
...(context && { context })
};
this.historyEntries.unshift(actionEntry);
this.enforceMaxSize();
this.isDirty = true;
this.emit('action:recorded', { entry: actionEntry, alertId, action });
return actionEntry;
}
updateAlert(updatedAlert, user) {
let updateCount = 0;
this.historyEntries.forEach(entry => {
if (entry.alert.id === updatedAlert.id) {
entry.alert = { ...updatedAlert };
updateCount++;
}
});
if (updateCount > 0) {
this.recordAction(updatedAlert.id, 'updated', user, {
updateCount,
timestamp: Date.now()
});
}
}
getHistory(filter, limit, offset = 0) {
let filtered = this.historyEntries;
if (filter) {
filtered = this.applyFilter(filtered, filter);
}
const start = offset;
const end = limit ? start + limit : undefined;
return filtered.slice(start, end);
}
getAlertHistory(alertId) {
return this.historyEntries.filter(entry => entry.alert.id === alertId);
}
getHistoryByAction(action) {
return this.historyEntries.filter(entry => entry.action === action);
}
getHistoryByUser(user) {
return this.historyEntries.filter(entry => entry.user === user);
}
getHistoryByTimeRange(startTime, endTime) {
return this.historyEntries.filter(entry => entry.timestamp >= startTime && entry.timestamp <= endTime);
}
applyFilter(entries, filter) {
return entries.filter(entry => {
const alert = entry.alert;
if (filter.levels?.length && !filter.levels.includes(alert.level)) {
return false;
}
if (filter.types?.length && !filter.types.includes(alert.type)) {
return false;
}
if (filter.statuses?.length && !filter.statuses.includes(alert.status)) {
return false;
}
if (filter.sources?.length && !filter.sources.includes(alert.source)) {
return false;
}
if (filter.channelIds?.length && !filter.channelIds.includes(alert.channelId || '')) {
return false;
}
if (filter.timeRange) {
if (entry.timestamp < filter.timeRange.start ||
entry.timestamp > filter.timeRange.end) {
return false;
}
}
if (filter.searchText) {
const searchText = filter.searchText.toLowerCase();
return alert.title.toLowerCase().includes(searchText) ||
alert.message.toLowerCase().includes(searchText) ||
alert.source.toLowerCase().includes(searchText) ||
entry.action.toLowerCase().includes(searchText);
}
if (filter.acknowledgedOnly && !alert.acknowledged) {
return false;
}
if (filter.unacknowledgedOnly && alert.acknowledged) {
return false;
}
return true;
});
}
searchHistory(searchText, searchFields = ['title', 'message'], limit = 100) {
if (!searchText || searchText.trim() === '') {
return [];
}
const searchLower = searchText.toLowerCase();
const matches = this.historyEntries.filter(entry => {
return searchFields.some(field => {
switch (field) {
case 'title':
return entry.alert.title.toLowerCase().includes(searchLower);
case 'message':
return entry.alert.message.toLowerCase().includes(searchLower);
case 'source':
return entry.alert.source.toLowerCase().includes(searchLower);
case 'action':
return entry.action.toLowerCase().includes(searchLower);
case 'user':
return entry.user?.toLowerCase().includes(searchLower) || false;
default:
return false;
}
});
});
return matches.slice(0, limit);
}
getStatistics() {
const stats = {
totalEntries: this.historyEntries.length,
uniqueAlerts: new Set(this.historyEntries.map(e => e.alert.id)).size,
actionCounts: {},
userCounts: {},
timeRange: null,
levelCounts: { info: 0, warning: 0, error: 0, critical: 0 },
typeCounts: { system: 0, stream: 0, channel: 0, network: 0 },
statusCounts: { active: 0, acknowledged: 0, resolved: 0, ignored: 0 }
};
if (this.historyEntries.length === 0) {
return stats;
}
const timestamps = this.historyEntries.map(e => e.timestamp);
stats.timeRange = {
earliest: Math.min(...timestamps),
latest: Math.max(...timestamps)
};
this.historyEntries.forEach(entry => {
stats.actionCounts[entry.action] = (stats.actionCounts[entry.action] || 0) + 1;
if (entry.user) {
stats.userCounts[entry.user] = (stats.userCounts[entry.user] || 0) + 1;
}
stats.levelCounts[entry.alert.level]++;
stats.typeCounts[entry.alert.type]++;
stats.statusCounts[entry.alert.status]++;
});
return stats;
}
getRecentActivity(hours = 24) {
const cutoffTime = Date.now() - (hours * 60 * 60 * 1000);
const recentEntries = this.historyEntries.filter(entry => entry.timestamp >= cutoffTime);
const summary = {
totalAlerts: recentEntries.length,
newAlerts: 0,
acknowledgedAlerts: 0,
resolvedAlerts: 0,
activeUsers: [],
topAlertTypes: []
};
const userSet = new Set();
const typeCounts = new Map();
recentEntries.forEach(entry => {
switch (entry.action) {
case 'created':
summary.newAlerts++;
break;
case 'acknowledged':
summary.acknowledgedAlerts++;
break;
case 'resolved':
summary.resolvedAlerts++;
break;
}
if (entry.user) {
userSet.add(entry.user);
}
const currentCount = typeCounts.get(entry.alert.type) || 0;
typeCounts.set(entry.alert.type, currentCount + 1);
});
summary.activeUsers = Array.from(userSet);
summary.topAlertTypes = Array.from(typeCounts.entries())
.map(([type, count]) => ({ type, count }))
.sort((a, b) => b.count - a.count);
return summary;
}
exportHistory(format = 'json') {
if (format === 'csv') {
return this.exportToCSV();
}
else {
return JSON.stringify({
version: '1.0',
exportedAt: new Date().toISOString(),
count: this.historyEntries.length,
entries: this.historyEntries
}, null, 2);
}
}
exportToCSV() {
const headers = [
'Timestamp',
'Action',
'User',
'Alert ID',
'Alert Level',
'Alert Type',
'Alert Status',
'Alert Title',
'Alert Message',
'Alert Source',
'Channel ID'
];
const rows = this.historyEntries.map(entry => [
new Date(entry.timestamp).toISOString(),
entry.action,
entry.user || '',
entry.alert.id,
entry.alert.level,
entry.alert.type,
entry.alert.status,
`"${entry.alert.title.replace(/"/g, '""').replace(/\n/g, '\\n')}"`,
`"${entry.alert.message.replace(/"/g, '""').replace(/\n/g, '\\n')}"`,
entry.alert.source,
entry.alert.channelId || ''
]);
return [headers.join(','), ...rows.map(row => row.join(','))].join('\n');
}
importHistory(data, format = 'json', merge = true) {
const result = { imported: 0, errors: [] };
try {
if (format === 'json') {
const parsed = JSON.parse(data);
const entries = Array.isArray(parsed.entries) ? parsed.entries :
Array.isArray(parsed) ? parsed : [];
if (!merge) {
this.historyEntries = [];
}
entries.forEach((entry, index) => {
try {
if (!entry.alert || !entry.action || !entry.timestamp) {
throw new Error('Missing required fields (alert, action, timestamp)');
}
if (!entry.alert.id || !entry.alert.level || !entry.alert.type) {
throw new Error('Invalid alert data');
}
const historyEntry = {
alert: entry.alert,
timestamp: typeof entry.timestamp === 'string' ?
new Date(entry.timestamp).getTime() : entry.timestamp,
action: entry.action,
user: entry.user,
context: entry.context
};
this.historyEntries.push(historyEntry);
result.imported++;
}
catch (error) {
result.errors.push(`Entry ${index}: ${error instanceof Error ? error.message : 'Invalid format'}`);
}
});
this.historyEntries.sort((a, b) => b.timestamp - a.timestamp);
this.enforceMaxSize();
this.isDirty = true;
}
else {
result.errors.push('CSV import not yet implemented');
}
}
catch (error) {
result.errors.push(`Parse error: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
this.emit('history:imported', result);
return result;
}
clearHistory() {
const count = this.historyEntries.length;
this.historyEntries = [];
this.isDirty = true;
this.emit('history:cleared', { count });
}
clearOldHistory(olderThanMs) {
const cutoffTime = Date.now() - olderThanMs;
const initialCount = this.historyEntries.length;
this.historyEntries = this.historyEntries.filter(entry => entry.timestamp >= cutoffTime);
const clearedCount = initialCount - this.historyEntries.length;
if (clearedCount > 0) {
this.isDirty = true;
this.emit('history:pruned', { clearedCount, remaining: this.historyEntries.length });
}
return clearedCount;
}
enforceMaxSize() {
if (this.historyEntries.length > this.maxHistoryEntries) {
const removedCount = this.historyEntries.length - this.maxHistoryEntries;
this.historyEntries = this.historyEntries.slice(0, this.maxHistoryEntries);
this.emit('history:trimmed', {
removedCount,
remaining: this.historyEntries.length
});
}
}
getConfig() {
return {
maxHistoryEntries: this.maxHistoryEntries,
persistenceEnabled: this.persistenceEnabled,
persistenceFilePath: this.persistenceFilePath,
currentSize: this.historyEntries.length
};
}
updateConfig(updates) {
if (updates.maxHistoryEntries !== undefined) {
this.maxHistoryEntries = updates.maxHistoryEntries;
this.enforceMaxSize();
}
if (updates.persistenceEnabled !== undefined) {
this.persistenceEnabled = updates.persistenceEnabled;
if (this.persistenceEnabled) {
this.setupAutoSave();
}
else if (this.autoSaveInterval) {
clearInterval(this.autoSaveInterval);
this.autoSaveInterval = undefined;
}
}
if (updates.persistenceFilePath !== undefined) {
this.persistenceFilePath = updates.persistenceFilePath;
}
this.emit('config:updated', updates);
}
async forceSave() {
this.isDirty = true;
await this.saveHistory();
}
async destroy() {
if (this.isDirty) {
await this.saveHistory();
}
if (this.autoSaveInterval) {
clearInterval(this.autoSaveInterval);
this.autoSaveInterval = undefined;
}
this.historyEntries = [];
this.removeAllListeners();
this.emit('manager:destroyed');
}
}
exports.AlertHistoryManager = AlertHistoryManager;
//# sourceMappingURL=alert-history-manager.js.map