Files
boc/quixzoom-capture-pipeline/replay/engine.js
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- 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
2026-06-29 16:24:48 +00:00

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)}`);
}
});
}