Add Jitsi reports to IDAA

This commit is contained in:
Scott Idem
2026-05-05 14:02:52 -04:00
parent 146682a30b
commit 0b04ce7c0c
3 changed files with 737 additions and 225 deletions

View File

@@ -7,6 +7,23 @@ import {
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
import { db_core } from '$lib/ae_core/db_core';
// MariaDB TEXT columns come back as JSON strings from the API — parse safely.
function safe_parse_meta(raw: unknown): Record<string, unknown> {
if (!raw) return {};
if (typeof raw === 'object') return raw as Record<string, unknown>;
try {
return JSON.parse(raw as string) as Record<string, unknown>;
} catch {
return {};
}
}
function parse_duration_seconds(d: string): number {
if (!d || !d.includes(':')) return 0;
const [h, m, s] = d.split(':').map(Number);
return (h || 0) * 3600 + (m || 0) * 60 + (s || 0);
}
/**
* @description Queries all Jitsi-related activity logs and processes them into a structured report,
* grouped by meeting ID.
@@ -96,50 +113,121 @@ export async function qry__jitsi_report({
}
// Step 2: Process the flat list into a structured report.
//
// Participants come from two sources — both are needed for a complete list:
// 1. jitsi_meeting_participant_joined events: meta.full_name (all who ever joined)
// 2. jitsi_meeting_init / stats / end snapshots: meta.participants[].displayName+role
// Source 2 has role info; source 1 may catch participants who left before the snapshot.
// We merge both into a Map<displayName, role> per meeting, deduplicating by name.
//
// Duration: take the MAX across all init/stats/end events — periodic stats may
// have a higher value than the final init summary in some Jitsi configurations.
const meetings = new Map<string, any>();
// Per-meeting participant map: displayName → role
const participant_maps = new Map<string, Map<string, string>>();
// Per-meeting max observed duration in seconds
const max_duration_secs = new Map<string, number>();
for (const log of flat_log_list) {
const meeting_id = log.external_client_id;
if (!meeting_id) continue;
if (!log.name?.startsWith('jitsi_')) continue;
// Make sure the name field is prefixed with "jitsi_"
if (!log.name.startsWith('jitsi_')) continue;
// Ensure a base entry for the meeting exists
if (!meetings.has(meeting_id)) {
meetings.set(meeting_id, {
meeting_id: meeting_id,
meeting_id,
room_name: 'Unknown',
start_time: log.created_on, // Fallback start time
start_time: log.created_on,
final_duration: '00:00:00',
final_participants: [],
final_participant_count: 0,
events: []
});
participant_maps.set(meeting_id, new Map());
max_duration_secs.set(meeting_id, 0);
}
const meeting_report = meetings.get(meeting_id);
const p_map = participant_maps.get(meeting_id)!;
const meta = safe_parse_meta(log.meta_json);
if (log.action === 'jitsi_meeting_init') {
// This is the main log entry, containing the final state.
meeting_report.room_name = log.description;
meeting_report.start_time = log.created_on; // The init log has the true start time
if (log.meta_json) {
meeting_report.final_duration = log.meta_json.duration;
meeting_report.final_participants = log.meta_json.participants;
meeting_report.final_participant_count =
log.meta_json.participant_count;
// Strip "Event in room: " prefix Jitsi sometimes prepends to the description
meeting_report.room_name =
(log.description ?? '').replace(/^Event in room:\s*/i, '').trim() ||
'Unknown';
meeting_report.start_time = log.created_on;
}
// Parse duration from init, stats, or end — keep the maximum seen
if (
log.action === 'jitsi_meeting_init' ||
log.action === 'jitsi_meeting_stats' ||
log.action === 'jitsi_meeting_end'
) {
const dur_str = ((meta.duration ?? meta.final_duration) as string) || '';
if (dur_str) {
const secs = parse_duration_seconds(dur_str);
if (secs > (max_duration_secs.get(meeting_id) ?? 0)) {
max_duration_secs.set(meeting_id, secs);
meeting_report.final_duration = dur_str;
}
}
} else {
// This is a discrete event log.
// Merge snapshot participant list (has role info — preferred source)
const snapshot = meta.participants as
| Array<{ displayName?: string; role?: string }>
| undefined;
if (Array.isArray(snapshot)) {
for (const p of snapshot) {
if (!p.displayName) continue;
// Only overwrite an existing entry if we're upgrading from participant → moderator
const existing_role = p_map.get(p.displayName);
if (!existing_role || (existing_role !== 'moderator' && p.role === 'moderator')) {
p_map.set(p.displayName, p.role ?? 'participant');
}
}
}
}
// Collect participants from join events (may catch people who left before the snapshot)
if (log.action === 'jitsi_meeting_participant_joined') {
const full_name = (meta.full_name as string) || '';
if (full_name && !p_map.has(full_name)) {
p_map.set(full_name, (meta.role as string) ?? 'participant');
}
}
// Discrete events for the timeline (all non-init actions)
if (log.action !== 'jitsi_meeting_init') {
meeting_report.events.push({
timestamp: log.created_on,
action: log.action,
details: log.meta_json
details: {
full_name: (meta.full_name as string) ?? undefined
}
});
}
}
// Compile final participant lists from the deduplicated maps
for (const [meeting_id, p_map] of participant_maps) {
const meeting_report = meetings.get(meeting_id);
if (!meeting_report) continue;
if (p_map.size > 0) {
// Sort: moderators first, then alphabetically
meeting_report.final_participants = Array.from(p_map.entries())
.map(([displayName, role]) => ({ displayName, role }))
.sort((a: any, b: any) => {
if (a.role === 'moderator' && b.role !== 'moderator') return -1;
if (a.role !== 'moderator' && b.role === 'moderator') return 1;
return a.displayName.localeCompare(b.displayName);
});
meeting_report.final_participant_count = p_map.size;
}
}
// Sort events within each meeting chronologically
for (const report of meetings.values()) {
report.events.sort(