bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
387 lines
11 KiB
JavaScript
387 lines
11 KiB
JavaScript
/**
|
|
* QUIXZOOM Replay Engine
|
|
*
|
|
* Spelar upp händelsekedjor i efterhand för debugging och utveckling.
|
|
*
|
|
* Exempel:
|
|
* 08:13:01 Zoomer börjar filma
|
|
* 08:13:07 Street Lamp observeras
|
|
* 08:13:08 Identity Engine hittar objekt
|
|
* 08:13:08 Evidence läggs till
|
|
* 08:13:09 AI ser kunskapsgap
|
|
* 08:13:10 Mission Generator skapar uppdrag
|
|
* 08:13:11 Telefonen får instruktion: "Gå två meter närmare."
|
|
*
|
|
* Användning:
|
|
* - Felsökning: Spela upp exakt vad som hände
|
|
* - Regressionstest: Kör om samma events med ny kod
|
|
* - Algoritm-jämförelse: Jämför resultat mellan versioner
|
|
* - Demo: Visa hur systemet fungerar i realtid
|
|
*/
|
|
|
|
class ReplayEngine {
|
|
constructor(options = {}) {
|
|
this.speed = options.speed || 1.0; // 1.0 = realtid, 2.0 = dubbel hastighet
|
|
this.events = [];
|
|
this.handlers = new Map();
|
|
this.state = 'idle'; // idle, playing, paused, stopped
|
|
this.currentIndex = 0;
|
|
this.startTime = null;
|
|
this.pauseTime = null;
|
|
this.results = [];
|
|
}
|
|
|
|
/**
|
|
* ============================================================
|
|
* REGISTRERA HANDLERS
|
|
* ============================================================
|
|
*/
|
|
|
|
registerHandler(eventType, handler) {
|
|
this.handlers.set(eventType, handler);
|
|
console.log(`[REPLAY] Registered handler for: ${eventType}`);
|
|
}
|
|
|
|
/**
|
|
* ============================================================
|
|
* LADDA EVENTS
|
|
* ============================================================
|
|
*/
|
|
|
|
loadEvents(events) {
|
|
// Sortera efter timestamp
|
|
this.events = events.sort((a, b) => a.timestamp - b.timestamp);
|
|
this.currentIndex = 0;
|
|
console.log(`[REPLAY] Loaded ${events.length} events`);
|
|
}
|
|
|
|
loadFromEventStore(eventStore, filter = {}) {
|
|
// Ladda från Event Store med filter
|
|
const events = eventStore.getEvents(filter);
|
|
this.loadEvents(events);
|
|
}
|
|
|
|
/**
|
|
* ============================================================
|
|
* SPELA UPP
|
|
* ============================================================
|
|
*/
|
|
|
|
async play() {
|
|
if (this.state === 'playing') {
|
|
console.log('[REPLAY] Already playing');
|
|
return;
|
|
}
|
|
|
|
if (this.state === 'paused') {
|
|
// Återuppta
|
|
const pausedDuration = Date.now() - this.pauseTime;
|
|
this.startTime += pausedDuration;
|
|
this.state = 'playing';
|
|
console.log('[REPLAY] Resumed');
|
|
} else {
|
|
// Ny uppspelning
|
|
this.startTime = Date.now();
|
|
this.state = 'playing';
|
|
this.currentIndex = 0;
|
|
this.results = [];
|
|
console.log('[REPLAY] Started');
|
|
}
|
|
|
|
// Spela upp events
|
|
while (this.currentIndex < this.events.length && this.state === 'playing') {
|
|
const event = this.events[this.currentIndex];
|
|
const eventTime = this.getEventTime(event);
|
|
|
|
// Vänta tills det är dags
|
|
const now = Date.now();
|
|
const elapsed = (now - this.startTime) * this.speed;
|
|
const waitTime = eventTime - elapsed;
|
|
|
|
if (waitTime > 0) {
|
|
await this.sleep(waitTime / this.speed);
|
|
}
|
|
|
|
if (this.state !== 'playing') break;
|
|
|
|
// Kör event
|
|
await this.executeEvent(event);
|
|
this.currentIndex++;
|
|
}
|
|
|
|
if (this.currentIndex >= this.events.length) {
|
|
this.state = 'completed';
|
|
console.log('[REPLAY] Completed');
|
|
}
|
|
}
|
|
|
|
pause() {
|
|
if (this.state === 'playing') {
|
|
this.state = 'paused';
|
|
this.pauseTime = Date.now();
|
|
console.log('[REPLAY] Paused');
|
|
}
|
|
}
|
|
|
|
stop() {
|
|
this.state = 'stopped';
|
|
console.log('[REPLAY] Stopped');
|
|
}
|
|
|
|
/**
|
|
* ============================================================
|
|
* EXECUTE EVENT
|
|
* ============================================================
|
|
*/
|
|
|
|
async executeEvent(event) {
|
|
const handler = this.handlers.get(event.type);
|
|
|
|
const context = {
|
|
event,
|
|
index: this.currentIndex,
|
|
total: this.events.length,
|
|
elapsed: Date.now() - this.startTime,
|
|
results: this.results,
|
|
};
|
|
|
|
console.log(`[REPLAY] [${this.formatTime(event.timestamp)}] ${event.type}`);
|
|
|
|
if (handler) {
|
|
try {
|
|
const result = await handler(event.payload, context);
|
|
this.results.push({
|
|
eventId: event.id,
|
|
type: event.type,
|
|
timestamp: event.timestamp,
|
|
result,
|
|
success: true,
|
|
});
|
|
return result;
|
|
} catch (error) {
|
|
console.error(`[REPLAY] Error executing ${event.type}:`, error.message);
|
|
this.results.push({
|
|
eventId: event.id,
|
|
type: event.type,
|
|
timestamp: event.timestamp,
|
|
error: error.message,
|
|
success: false,
|
|
});
|
|
throw error;
|
|
}
|
|
} else {
|
|
console.log(`[REPLAY] No handler for: ${event.type}`);
|
|
this.results.push({
|
|
eventId: event.id,
|
|
type: event.type,
|
|
timestamp: event.timestamp,
|
|
result: null,
|
|
success: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ============================================================
|
|
* JÄMFÖR VERSIONER
|
|
* ============================================================
|
|
*/
|
|
|
|
async compareVersions(events, versionAHandlers, versionBHandlers) {
|
|
console.log('[REPLAY] Comparing versions...');
|
|
|
|
// Kör version A
|
|
const engineA = new ReplayEngine({ speed: 100 }); // Snabb uppspelning
|
|
for (const [type, handler] of versionAHandlers) {
|
|
engineA.registerHandler(type, handler);
|
|
}
|
|
engineA.loadEvents(events);
|
|
await engineA.play();
|
|
|
|
// Kör version B
|
|
const engineB = new ReplayEngine({ speed: 100 });
|
|
for (const [type, handler] of versionBHandlers) {
|
|
engineB.registerHandler(type, handler);
|
|
}
|
|
engineB.loadEvents(events);
|
|
await engineB.play();
|
|
|
|
// Jämför resultat
|
|
const comparison = this.compareResults(engineA.results, engineB.results);
|
|
|
|
return {
|
|
versionA: { results: engineA.results, stats: engineA.getStats() },
|
|
versionB: { results: engineB.results, stats: engineB.getStats() },
|
|
comparison,
|
|
};
|
|
}
|
|
|
|
compareResults(resultsA, resultsB) {
|
|
const differences = [];
|
|
const minLength = Math.min(resultsA.length, resultsB.length);
|
|
|
|
for (let i = 0; i < minLength; i++) {
|
|
const a = resultsA[i];
|
|
const b = resultsB[i];
|
|
|
|
if (JSON.stringify(a.result) !== JSON.stringify(b.result)) {
|
|
differences.push({
|
|
index: i,
|
|
eventId: a.eventId,
|
|
type: a.type,
|
|
versionA: a.result,
|
|
versionB: b.result,
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
totalEvents: minLength,
|
|
differences: differences.length,
|
|
differenceRate: (differences.length / minLength * 100).toFixed(2) + '%',
|
|
details: differences,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* ============================================================
|
|
* EXPORT/IMPORT
|
|
* ============================================================
|
|
*/
|
|
|
|
exportSession() {
|
|
return {
|
|
events: this.events,
|
|
results: this.results,
|
|
stats: this.getStats(),
|
|
exportedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
importSession(session) {
|
|
this.loadEvents(session.events);
|
|
this.results = session.results || [];
|
|
console.log(`[REPLAY] Imported session with ${session.events.length} events`);
|
|
}
|
|
|
|
/**
|
|
* ============================================================
|
|
* STATISTIK
|
|
* ============================================================
|
|
*/
|
|
|
|
getStats() {
|
|
const typeCounts = {};
|
|
for (const event of this.events) {
|
|
typeCounts[event.type] = (typeCounts[event.type] || 0) + 1;
|
|
}
|
|
|
|
const successCount = this.results.filter(r => r.success).length;
|
|
const errorCount = this.results.filter(r => !r.success).length;
|
|
|
|
return {
|
|
totalEvents: this.events.length,
|
|
playedEvents: this.currentIndex,
|
|
remainingEvents: this.events.length - this.currentIndex,
|
|
typeCounts,
|
|
successCount,
|
|
errorCount,
|
|
state: this.state,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* ============================================================
|
|
* HJÄLPMETODER
|
|
* ============================================================
|
|
*/
|
|
|
|
getEventTime(event) {
|
|
// Relativ tid från första eventet
|
|
if (!this.firstEventTime) {
|
|
this.firstEventTime = this.events[0]?.timestamp || Date.now();
|
|
}
|
|
return event.timestamp - this.firstEventTime;
|
|
}
|
|
|
|
formatTime(timestamp) {
|
|
const date = new Date(timestamp);
|
|
return date.toISOString().split('T')[1].split('.')[0];
|
|
}
|
|
|
|
sleep(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
}
|
|
|
|
// Exportera
|
|
module.exports = ReplayEngine;
|
|
|
|
// Demo
|
|
if (require.main === module) {
|
|
const engine = new ReplayEngine({ speed: 10 }); // 10x hastighet för demo
|
|
|
|
console.log('=== REPLAY ENGINE DEMO ===\n');
|
|
|
|
// Registrera handlers
|
|
engine.registerHandler('zoomer_started', async (payload) => {
|
|
console.log(` → Zoomer ${payload.zoomerId} started recording`);
|
|
return { status: 'recording' };
|
|
});
|
|
|
|
engine.registerHandler('observation_created', async (payload) => {
|
|
console.log(` → Object ${payload.objectType} observed`);
|
|
return { objectId: payload.objectId, confidence: 0.92 };
|
|
});
|
|
|
|
engine.registerHandler('identity_matched', async (payload) => {
|
|
console.log(` → Matched to ${payload.objectId} (${(payload.confidence * 100).toFixed(0)}% confidence)`);
|
|
return { matched: true, objectId: payload.objectId };
|
|
});
|
|
|
|
engine.registerHandler('evidence_added', async (payload) => {
|
|
console.log(` → Evidence added: ${payload.evidenceType}`);
|
|
return { evidenceId: payload.evidenceId };
|
|
});
|
|
|
|
engine.registerHandler('knowledge_gap_detected', async (payload) => {
|
|
console.log(` → Knowledge gap: ${payload.gapType}`);
|
|
return { gapId: payload.gapId, priority: payload.priority };
|
|
});
|
|
|
|
engine.registerHandler('mission_created', async (payload) => {
|
|
console.log(` → Mission created: ${payload.missionType}`);
|
|
return { missionId: payload.missionId };
|
|
});
|
|
|
|
engine.registerHandler('instruction_sent', async (payload) => {
|
|
console.log(` → Instruction: "${payload.instruction}"`);
|
|
return { sent: true };
|
|
});
|
|
|
|
// Skapa scenario
|
|
const baseTime = Date.now();
|
|
const events = [
|
|
{ id: 'evt_001', type: 'zoomer_started', timestamp: baseTime, payload: { zoomerId: 'zoomer_00001' } },
|
|
{ id: 'evt_002', type: 'observation_created', timestamp: baseTime + 6000, payload: { objectType: 'street_lamp', objectId: 'obj_001' } },
|
|
{ id: 'evt_003', type: 'identity_matched', timestamp: baseTime + 7000, payload: { objectId: 'street_lamp_000001', confidence: 0.94 } },
|
|
{ id: 'evt_004', type: 'evidence_added', timestamp: baseTime + 7000, payload: { evidenceType: 'image', evidenceId: 'ev_001' } },
|
|
{ id: 'evt_005', type: 'knowledge_gap_detected', timestamp: baseTime + 8000, payload: { gapType: 'angle', gapId: 'gap_001', priority: 'medium' } },
|
|
{ id: 'evt_006', type: 'mission_created', timestamp: baseTime + 9000, payload: { missionType: 'verify_angle', missionId: 'mission_001' } },
|
|
{ id: 'evt_007', type: 'instruction_sent', timestamp: baseTime + 10000, payload: { instruction: 'Gå två meter närmare' } },
|
|
];
|
|
|
|
// Ladda och spela
|
|
engine.loadEvents(events);
|
|
|
|
console.log('Starting playback...\n');
|
|
engine.play().then(() => {
|
|
console.log('\n=== PLAYBACK COMPLETE ===');
|
|
console.log('\nStats:', engine.getStats());
|
|
console.log('\nResults:');
|
|
for (const result of engine.results) {
|
|
console.log(` ${result.type}: ${result.success ? '✓' : '✗'} ${JSON.stringify(result.result)}`);
|
|
}
|
|
});
|
|
}
|