608 lines
17 KiB
TypeScript
608 lines
17 KiB
TypeScript
|
|
import * as plugins from '../plugins.js';
|
||
|
|
import { logger } from '../logger.js';
|
||
|
|
import type { StorageManager } from '../storage/index.js';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* RADIUS accounting session
|
||
|
|
*/
|
||
|
|
export interface IAccountingSession {
|
||
|
|
/** Unique session ID from RADIUS */
|
||
|
|
sessionId: string;
|
||
|
|
/** Username (often MAC address for MAB) */
|
||
|
|
username: string;
|
||
|
|
/** MAC address of the device */
|
||
|
|
macAddress?: string;
|
||
|
|
/** NAS IP address (switch/AP) */
|
||
|
|
nasIpAddress: string;
|
||
|
|
/** NAS port (physical or virtual) */
|
||
|
|
nasPort?: number;
|
||
|
|
/** NAS port type */
|
||
|
|
nasPortType?: string;
|
||
|
|
/** NAS identifier (name) */
|
||
|
|
nasIdentifier?: string;
|
||
|
|
/** Assigned VLAN */
|
||
|
|
vlanId?: number;
|
||
|
|
/** Assigned IP address (if any) */
|
||
|
|
framedIpAddress?: string;
|
||
|
|
/** Called station ID (usually BSSID for wireless) */
|
||
|
|
calledStationId?: string;
|
||
|
|
/** Calling station ID (usually client MAC) */
|
||
|
|
callingStationId?: string;
|
||
|
|
/** Session start time */
|
||
|
|
startTime: number;
|
||
|
|
/** Session end time (0 if active) */
|
||
|
|
endTime: number;
|
||
|
|
/** Last update time (interim accounting) */
|
||
|
|
lastUpdateTime: number;
|
||
|
|
/** Session status */
|
||
|
|
status: 'active' | 'stopped' | 'terminated';
|
||
|
|
/** Termination cause (if stopped) */
|
||
|
|
terminateCause?: string;
|
||
|
|
/** Input octets (bytes received by NAS from client) */
|
||
|
|
inputOctets: number;
|
||
|
|
/** Output octets (bytes sent by NAS to client) */
|
||
|
|
outputOctets: number;
|
||
|
|
/** Input packets */
|
||
|
|
inputPackets: number;
|
||
|
|
/** Output packets */
|
||
|
|
outputPackets: number;
|
||
|
|
/** Session duration in seconds */
|
||
|
|
sessionTime: number;
|
||
|
|
/** Service type */
|
||
|
|
serviceType?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Accounting summary for a time period
|
||
|
|
*/
|
||
|
|
export interface IAccountingSummary {
|
||
|
|
/** Time period start */
|
||
|
|
periodStart: number;
|
||
|
|
/** Time period end */
|
||
|
|
periodEnd: number;
|
||
|
|
/** Total sessions */
|
||
|
|
totalSessions: number;
|
||
|
|
/** Active sessions */
|
||
|
|
activeSessions: number;
|
||
|
|
/** Total input bytes */
|
||
|
|
totalInputBytes: number;
|
||
|
|
/** Total output bytes */
|
||
|
|
totalOutputBytes: number;
|
||
|
|
/** Total session time (seconds) */
|
||
|
|
totalSessionTime: number;
|
||
|
|
/** Average session duration (seconds) */
|
||
|
|
averageSessionDuration: number;
|
||
|
|
/** Unique users/devices */
|
||
|
|
uniqueUsers: number;
|
||
|
|
/** Sessions by VLAN */
|
||
|
|
sessionsByVlan: Record<number, number>;
|
||
|
|
/** Top users by traffic */
|
||
|
|
topUsersByTraffic: Array<{ username: string; totalBytes: number }>;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Accounting manager configuration
|
||
|
|
*/
|
||
|
|
export interface IAccountingManagerConfig {
|
||
|
|
/** Storage key prefix */
|
||
|
|
storagePrefix?: string;
|
||
|
|
/** Session retention period in days (default: 30) */
|
||
|
|
retentionDays?: number;
|
||
|
|
/** Enable detailed session logging */
|
||
|
|
detailedLogging?: boolean;
|
||
|
|
/** Maximum active sessions to track in memory */
|
||
|
|
maxActiveSessions?: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Manages RADIUS accounting data including:
|
||
|
|
* - Session tracking (start/stop/interim)
|
||
|
|
* - Data usage tracking (bytes in/out)
|
||
|
|
* - Session history and retention
|
||
|
|
* - Billing reports and summaries
|
||
|
|
*/
|
||
|
|
export class AccountingManager {
|
||
|
|
private activeSessions: Map<string, IAccountingSession> = new Map();
|
||
|
|
private config: Required<IAccountingManagerConfig>;
|
||
|
|
private storageManager?: StorageManager;
|
||
|
|
|
||
|
|
// Counters for statistics
|
||
|
|
private stats = {
|
||
|
|
totalSessionsStarted: 0,
|
||
|
|
totalSessionsStopped: 0,
|
||
|
|
totalInputBytes: 0,
|
||
|
|
totalOutputBytes: 0,
|
||
|
|
interimUpdatesReceived: 0,
|
||
|
|
};
|
||
|
|
|
||
|
|
constructor(config?: IAccountingManagerConfig, storageManager?: StorageManager) {
|
||
|
|
this.config = {
|
||
|
|
storagePrefix: config?.storagePrefix ?? '/radius/accounting',
|
||
|
|
retentionDays: config?.retentionDays ?? 30,
|
||
|
|
detailedLogging: config?.detailedLogging ?? false,
|
||
|
|
maxActiveSessions: config?.maxActiveSessions ?? 10000,
|
||
|
|
};
|
||
|
|
this.storageManager = storageManager;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize the accounting manager
|
||
|
|
*/
|
||
|
|
async initialize(): Promise<void> {
|
||
|
|
if (this.storageManager) {
|
||
|
|
await this.loadActiveSessions();
|
||
|
|
}
|
||
|
|
logger.log('info', `AccountingManager initialized with ${this.activeSessions.size} active sessions`);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle accounting start request
|
||
|
|
*/
|
||
|
|
async handleAccountingStart(data: {
|
||
|
|
sessionId: string;
|
||
|
|
username: string;
|
||
|
|
macAddress?: string;
|
||
|
|
nasIpAddress: string;
|
||
|
|
nasPort?: number;
|
||
|
|
nasPortType?: string;
|
||
|
|
nasIdentifier?: string;
|
||
|
|
vlanId?: number;
|
||
|
|
framedIpAddress?: string;
|
||
|
|
calledStationId?: string;
|
||
|
|
callingStationId?: string;
|
||
|
|
serviceType?: string;
|
||
|
|
}): Promise<void> {
|
||
|
|
const now = Date.now();
|
||
|
|
|
||
|
|
const session: IAccountingSession = {
|
||
|
|
sessionId: data.sessionId,
|
||
|
|
username: data.username,
|
||
|
|
macAddress: data.macAddress,
|
||
|
|
nasIpAddress: data.nasIpAddress,
|
||
|
|
nasPort: data.nasPort,
|
||
|
|
nasPortType: data.nasPortType,
|
||
|
|
nasIdentifier: data.nasIdentifier,
|
||
|
|
vlanId: data.vlanId,
|
||
|
|
framedIpAddress: data.framedIpAddress,
|
||
|
|
calledStationId: data.calledStationId,
|
||
|
|
callingStationId: data.callingStationId,
|
||
|
|
serviceType: data.serviceType,
|
||
|
|
startTime: now,
|
||
|
|
endTime: 0,
|
||
|
|
lastUpdateTime: now,
|
||
|
|
status: 'active',
|
||
|
|
inputOctets: 0,
|
||
|
|
outputOctets: 0,
|
||
|
|
inputPackets: 0,
|
||
|
|
outputPackets: 0,
|
||
|
|
sessionTime: 0,
|
||
|
|
};
|
||
|
|
|
||
|
|
// Check if we're at capacity
|
||
|
|
if (this.activeSessions.size >= this.config.maxActiveSessions) {
|
||
|
|
// Remove oldest session
|
||
|
|
const oldest = this.findOldestSession();
|
||
|
|
if (oldest) {
|
||
|
|
await this.evictSession(oldest);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
this.activeSessions.set(data.sessionId, session);
|
||
|
|
this.stats.totalSessionsStarted++;
|
||
|
|
|
||
|
|
if (this.config.detailedLogging) {
|
||
|
|
logger.log('info', `Accounting Start: session=${data.sessionId}, user=${data.username}, NAS=${data.nasIpAddress}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Persist session
|
||
|
|
if (this.storageManager) {
|
||
|
|
await this.persistSession(session);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle accounting interim update request
|
||
|
|
*/
|
||
|
|
async handleAccountingUpdate(data: {
|
||
|
|
sessionId: string;
|
||
|
|
inputOctets?: number;
|
||
|
|
outputOctets?: number;
|
||
|
|
inputPackets?: number;
|
||
|
|
outputPackets?: number;
|
||
|
|
sessionTime?: number;
|
||
|
|
}): Promise<void> {
|
||
|
|
const session = this.activeSessions.get(data.sessionId);
|
||
|
|
|
||
|
|
if (!session) {
|
||
|
|
logger.log('warn', `Interim update for unknown session: ${data.sessionId}`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update session metrics
|
||
|
|
if (data.inputOctets !== undefined) {
|
||
|
|
session.inputOctets = data.inputOctets;
|
||
|
|
}
|
||
|
|
if (data.outputOctets !== undefined) {
|
||
|
|
session.outputOctets = data.outputOctets;
|
||
|
|
}
|
||
|
|
if (data.inputPackets !== undefined) {
|
||
|
|
session.inputPackets = data.inputPackets;
|
||
|
|
}
|
||
|
|
if (data.outputPackets !== undefined) {
|
||
|
|
session.outputPackets = data.outputPackets;
|
||
|
|
}
|
||
|
|
if (data.sessionTime !== undefined) {
|
||
|
|
session.sessionTime = data.sessionTime;
|
||
|
|
}
|
||
|
|
|
||
|
|
session.lastUpdateTime = Date.now();
|
||
|
|
this.stats.interimUpdatesReceived++;
|
||
|
|
|
||
|
|
if (this.config.detailedLogging) {
|
||
|
|
logger.log('debug', `Accounting Interim: session=${data.sessionId}, in=${data.inputOctets}, out=${data.outputOctets}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update persisted session
|
||
|
|
if (this.storageManager) {
|
||
|
|
await this.persistSession(session);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle accounting stop request
|
||
|
|
*/
|
||
|
|
async handleAccountingStop(data: {
|
||
|
|
sessionId: string;
|
||
|
|
terminateCause?: string;
|
||
|
|
inputOctets?: number;
|
||
|
|
outputOctets?: number;
|
||
|
|
inputPackets?: number;
|
||
|
|
outputPackets?: number;
|
||
|
|
sessionTime?: number;
|
||
|
|
}): Promise<void> {
|
||
|
|
const session = this.activeSessions.get(data.sessionId);
|
||
|
|
|
||
|
|
if (!session) {
|
||
|
|
logger.log('warn', `Stop for unknown session: ${data.sessionId}`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update final metrics
|
||
|
|
if (data.inputOctets !== undefined) {
|
||
|
|
session.inputOctets = data.inputOctets;
|
||
|
|
}
|
||
|
|
if (data.outputOctets !== undefined) {
|
||
|
|
session.outputOctets = data.outputOctets;
|
||
|
|
}
|
||
|
|
if (data.inputPackets !== undefined) {
|
||
|
|
session.inputPackets = data.inputPackets;
|
||
|
|
}
|
||
|
|
if (data.outputPackets !== undefined) {
|
||
|
|
session.outputPackets = data.outputPackets;
|
||
|
|
}
|
||
|
|
if (data.sessionTime !== undefined) {
|
||
|
|
session.sessionTime = data.sessionTime;
|
||
|
|
}
|
||
|
|
|
||
|
|
session.endTime = Date.now();
|
||
|
|
session.lastUpdateTime = session.endTime;
|
||
|
|
session.status = 'stopped';
|
||
|
|
session.terminateCause = data.terminateCause;
|
||
|
|
|
||
|
|
// Update global stats
|
||
|
|
this.stats.totalSessionsStopped++;
|
||
|
|
this.stats.totalInputBytes += session.inputOctets;
|
||
|
|
this.stats.totalOutputBytes += session.outputOctets;
|
||
|
|
|
||
|
|
if (this.config.detailedLogging) {
|
||
|
|
logger.log('info', `Accounting Stop: session=${data.sessionId}, duration=${session.sessionTime}s, in=${session.inputOctets}, out=${session.outputOctets}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Archive the session
|
||
|
|
if (this.storageManager) {
|
||
|
|
await this.archiveSession(session);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Remove from active sessions
|
||
|
|
this.activeSessions.delete(data.sessionId);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get an active session by ID
|
||
|
|
*/
|
||
|
|
getSession(sessionId: string): IAccountingSession | undefined {
|
||
|
|
return this.activeSessions.get(sessionId);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get all active sessions
|
||
|
|
*/
|
||
|
|
getActiveSessions(): IAccountingSession[] {
|
||
|
|
return Array.from(this.activeSessions.values());
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get active sessions by username
|
||
|
|
*/
|
||
|
|
getSessionsByUsername(username: string): IAccountingSession[] {
|
||
|
|
return Array.from(this.activeSessions.values()).filter(s => s.username === username);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get active sessions by NAS IP
|
||
|
|
*/
|
||
|
|
getSessionsByNas(nasIpAddress: string): IAccountingSession[] {
|
||
|
|
return Array.from(this.activeSessions.values()).filter(s => s.nasIpAddress === nasIpAddress);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get active sessions by VLAN
|
||
|
|
*/
|
||
|
|
getSessionsByVlan(vlanId: number): IAccountingSession[] {
|
||
|
|
return Array.from(this.activeSessions.values()).filter(s => s.vlanId === vlanId);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get accounting summary for a time period
|
||
|
|
*/
|
||
|
|
async getSummary(startTime: number, endTime: number): Promise<IAccountingSummary> {
|
||
|
|
// Get archived sessions for the time period
|
||
|
|
const archivedSessions = await this.getArchivedSessions(startTime, endTime);
|
||
|
|
|
||
|
|
// Combine with active sessions that started within the period
|
||
|
|
const activeSessions = Array.from(this.activeSessions.values()).filter(
|
||
|
|
s => s.startTime >= startTime && s.startTime <= endTime
|
||
|
|
);
|
||
|
|
|
||
|
|
const allSessions = [...archivedSessions, ...activeSessions];
|
||
|
|
|
||
|
|
// Calculate summary
|
||
|
|
let totalInputBytes = 0;
|
||
|
|
let totalOutputBytes = 0;
|
||
|
|
let totalSessionTime = 0;
|
||
|
|
const uniqueUsers = new Set<string>();
|
||
|
|
const sessionsByVlan: Record<number, number> = {};
|
||
|
|
const userTraffic: Record<string, number> = {};
|
||
|
|
|
||
|
|
for (const session of allSessions) {
|
||
|
|
totalInputBytes += session.inputOctets;
|
||
|
|
totalOutputBytes += session.outputOctets;
|
||
|
|
totalSessionTime += session.sessionTime;
|
||
|
|
uniqueUsers.add(session.username);
|
||
|
|
|
||
|
|
if (session.vlanId !== undefined) {
|
||
|
|
sessionsByVlan[session.vlanId] = (sessionsByVlan[session.vlanId] || 0) + 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
const userBytes = session.inputOctets + session.outputOctets;
|
||
|
|
userTraffic[session.username] = (userTraffic[session.username] || 0) + userBytes;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Top users by traffic
|
||
|
|
const topUsersByTraffic = Object.entries(userTraffic)
|
||
|
|
.sort((a, b) => b[1] - a[1])
|
||
|
|
.slice(0, 10)
|
||
|
|
.map(([username, totalBytes]) => ({ username, totalBytes }));
|
||
|
|
|
||
|
|
return {
|
||
|
|
periodStart: startTime,
|
||
|
|
periodEnd: endTime,
|
||
|
|
totalSessions: allSessions.length,
|
||
|
|
activeSessions: activeSessions.length,
|
||
|
|
totalInputBytes,
|
||
|
|
totalOutputBytes,
|
||
|
|
totalSessionTime,
|
||
|
|
averageSessionDuration: allSessions.length > 0 ? totalSessionTime / allSessions.length : 0,
|
||
|
|
uniqueUsers: uniqueUsers.size,
|
||
|
|
sessionsByVlan,
|
||
|
|
topUsersByTraffic,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get statistics
|
||
|
|
*/
|
||
|
|
getStats(): {
|
||
|
|
activeSessions: number;
|
||
|
|
totalSessionsStarted: number;
|
||
|
|
totalSessionsStopped: number;
|
||
|
|
totalInputBytes: number;
|
||
|
|
totalOutputBytes: number;
|
||
|
|
interimUpdatesReceived: number;
|
||
|
|
} {
|
||
|
|
return {
|
||
|
|
activeSessions: this.activeSessions.size,
|
||
|
|
...this.stats,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Disconnect a session (admin action)
|
||
|
|
*/
|
||
|
|
async disconnectSession(sessionId: string, reason: string = 'AdminReset'): Promise<boolean> {
|
||
|
|
const session = this.activeSessions.get(sessionId);
|
||
|
|
if (!session) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
await this.handleAccountingStop({
|
||
|
|
sessionId,
|
||
|
|
terminateCause: reason,
|
||
|
|
sessionTime: Math.floor((Date.now() - session.startTime) / 1000),
|
||
|
|
});
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Clean up old archived sessions based on retention policy
|
||
|
|
*/
|
||
|
|
async cleanupOldSessions(): Promise<number> {
|
||
|
|
if (!this.storageManager) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
const cutoffTime = Date.now() - this.config.retentionDays * 24 * 60 * 60 * 1000;
|
||
|
|
let deletedCount = 0;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const keys = await this.storageManager.list(`${this.config.storagePrefix}/archive/`);
|
||
|
|
|
||
|
|
for (const key of keys) {
|
||
|
|
try {
|
||
|
|
const session = await this.storageManager.getJSON<IAccountingSession>(key);
|
||
|
|
if (session && session.endTime > 0 && session.endTime < cutoffTime) {
|
||
|
|
await this.storageManager.delete(key);
|
||
|
|
deletedCount++;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
// Ignore individual errors
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (deletedCount > 0) {
|
||
|
|
logger.log('info', `Cleaned up ${deletedCount} old accounting sessions`);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
logger.log('error', `Failed to cleanup old sessions: ${error.message}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
return deletedCount;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Find the oldest active session
|
||
|
|
*/
|
||
|
|
private findOldestSession(): string | null {
|
||
|
|
let oldestTime = Infinity;
|
||
|
|
let oldestSessionId: string | null = null;
|
||
|
|
|
||
|
|
for (const [sessionId, session] of this.activeSessions) {
|
||
|
|
if (session.lastUpdateTime < oldestTime) {
|
||
|
|
oldestTime = session.lastUpdateTime;
|
||
|
|
oldestSessionId = sessionId;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return oldestSessionId;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Evict a session from memory
|
||
|
|
*/
|
||
|
|
private async evictSession(sessionId: string): Promise<void> {
|
||
|
|
const session = this.activeSessions.get(sessionId);
|
||
|
|
if (session) {
|
||
|
|
session.status = 'terminated';
|
||
|
|
session.terminateCause = 'SessionEvicted';
|
||
|
|
session.endTime = Date.now();
|
||
|
|
|
||
|
|
if (this.storageManager) {
|
||
|
|
await this.archiveSession(session);
|
||
|
|
}
|
||
|
|
|
||
|
|
this.activeSessions.delete(sessionId);
|
||
|
|
logger.log('warn', `Evicted session ${sessionId} due to capacity limit`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Load active sessions from storage
|
||
|
|
*/
|
||
|
|
private async loadActiveSessions(): Promise<void> {
|
||
|
|
if (!this.storageManager) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const keys = await this.storageManager.list(`${this.config.storagePrefix}/active/`);
|
||
|
|
|
||
|
|
for (const key of keys) {
|
||
|
|
try {
|
||
|
|
const session = await this.storageManager.getJSON<IAccountingSession>(key);
|
||
|
|
if (session && session.status === 'active') {
|
||
|
|
this.activeSessions.set(session.sessionId, session);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
// Ignore individual errors
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
logger.log('warn', `Failed to load active sessions: ${error.message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Persist a session to storage
|
||
|
|
*/
|
||
|
|
private async persistSession(session: IAccountingSession): Promise<void> {
|
||
|
|
if (!this.storageManager) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const key = `${this.config.storagePrefix}/active/${session.sessionId}.json`;
|
||
|
|
try {
|
||
|
|
await this.storageManager.setJSON(key, session);
|
||
|
|
} catch (error) {
|
||
|
|
logger.log('error', `Failed to persist session ${session.sessionId}: ${error.message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Archive a completed session
|
||
|
|
*/
|
||
|
|
private async archiveSession(session: IAccountingSession): Promise<void> {
|
||
|
|
if (!this.storageManager) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Remove from active
|
||
|
|
const activeKey = `${this.config.storagePrefix}/active/${session.sessionId}.json`;
|
||
|
|
await this.storageManager.delete(activeKey);
|
||
|
|
|
||
|
|
// Add to archive with date-based path
|
||
|
|
const date = new Date(session.endTime);
|
||
|
|
const archiveKey = `${this.config.storagePrefix}/archive/${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}/${session.sessionId}.json`;
|
||
|
|
await this.storageManager.setJSON(archiveKey, session);
|
||
|
|
} catch (error) {
|
||
|
|
logger.log('error', `Failed to archive session ${session.sessionId}: ${error.message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get archived sessions for a time period
|
||
|
|
*/
|
||
|
|
private async getArchivedSessions(startTime: number, endTime: number): Promise<IAccountingSession[]> {
|
||
|
|
if (!this.storageManager) {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
const sessions: IAccountingSession[] = [];
|
||
|
|
|
||
|
|
try {
|
||
|
|
const keys = await this.storageManager.list(`${this.config.storagePrefix}/archive/`);
|
||
|
|
|
||
|
|
for (const key of keys) {
|
||
|
|
try {
|
||
|
|
const session = await this.storageManager.getJSON<IAccountingSession>(key);
|
||
|
|
if (
|
||
|
|
session &&
|
||
|
|
session.endTime > 0 &&
|
||
|
|
session.startTime <= endTime &&
|
||
|
|
session.endTime >= startTime
|
||
|
|
) {
|
||
|
|
sessions.push(session);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
// Ignore individual errors
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
logger.log('warn', `Failed to get archived sessions: ${error.message}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
return sessions;
|
||
|
|
}
|
||
|
|
}
|