Add Jitsi reports to IDAA
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user