Tighten Jitsi report exclusions

Use Jitsi url_params.uuid for exclusion where available, preserve url_params in cached activity logs, and add the temporary staff-name fallback behind the same edit-mode toggle.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Scott Idem
2026-05-06 11:47:43 -04:00
parent 3ae9d0a884
commit 7497bfb9f8
5 changed files with 408 additions and 207 deletions

View File

@@ -270,6 +270,9 @@ export const properties_to_save = [
'user_id',
'user_id_random',
'external_client_id',
'url_root',
'url_full_path',
'url_params',
'source',
'object_type',
'object_id',

View File

@@ -7,6 +7,28 @@ 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';
interface MeetingEvent {
timestamp: string;
action: string;
details: { full_name?: string };
}
interface MeetingParticipant {
displayName: string;
role: string;
novi_uuid?: string;
}
interface MeetingReport {
meeting_id: string;
room_name: string;
start_time: string;
final_duration: string;
final_participant_count: number;
final_participants: MeetingParticipant[];
events: MeetingEvent[];
}
// 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 {};
@@ -32,6 +54,16 @@ function normalize_uuid(value: unknown): string {
return normalize_text(value).toLowerCase();
}
function extract_uuid_from_url_params(raw: unknown): string {
if (typeof raw !== 'string' || !raw.trim()) return '';
try {
const params = new URLSearchParams(raw);
return normalize_uuid(params.get('uuid'));
} catch {
return '';
}
}
function extract_participant_uuid(source: Record<string, unknown>): string {
const nested_user = source.user as Record<string, unknown> | undefined;
const nested_context = source.context as Record<string, unknown> | undefined;
@@ -45,12 +77,8 @@ function extract_participant_uuid(source: Record<string, unknown>): string {
source.novi_customer_uid,
nested_user?.novi_uuid,
nested_user?.uuid,
nested_user?.id,
nested_context_user?.novi_uuid,
nested_context_user?.uuid,
nested_context_user?.id,
source.user_id,
source.id
nested_context_user?.uuid
];
for (const candidate of candidates) {
@@ -73,6 +101,236 @@ function extract_participant_uuid(source: Record<string, unknown>): string {
* @param log_lvl The logging level.
* @returns A structured array of meeting report objects.
*/
function build_jitsi_report_from_logs(
flat_log_list: any[],
log_lvl = 0
): MeetingReport[] {
// 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, participant> per meeting, keeping the
// stable display-name key used by the existing report while preserving Novi UUIDs
// when the log payload includes them.
//
// 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, MeetingReport>();
const participant_maps = new Map<string, Map<string, MeetingParticipant>>();
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;
if (!meetings.has(meeting_id)) {
meetings.set(meeting_id, {
meeting_id,
room_name: 'Unknown',
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);
const log_novi_uuid = extract_uuid_from_url_params(log.url_params);
if (log.action === 'jitsi_meeting_init') {
// 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;
}
}
// Merge snapshot participant list (has role info — preferred source)
const snapshot = meta.participants as
| Array<Record<string, unknown>>
| undefined;
if (Array.isArray(snapshot)) {
for (const p of snapshot) {
const display_name = normalize_text(p.displayName);
if (!display_name) continue;
const role =
typeof p.role === 'string' ? p.role : 'participant';
const novi_uuid =
extract_participant_uuid(p) ||
(p.role === 'moderator'
? normalize_uuid(meta.moderator_novi_uuid)
: '');
const existing_participant = p_map.get(display_name);
if (!existing_participant) {
p_map.set(display_name, {
displayName: display_name,
role,
...(novi_uuid ? { novi_uuid } : {})
});
continue;
}
if (
existing_participant.role !== 'moderator' &&
role === 'moderator'
) {
existing_participant.role = 'moderator';
}
if (!existing_participant.novi_uuid && novi_uuid) {
existing_participant.novi_uuid = novi_uuid;
}
}
}
}
// Collect participants from join events (may catch people who left before the snapshot)
if (log.action === 'jitsi_meeting_participant_joined') {
const display_name = normalize_text(meta.full_name);
if (display_name) {
const role =
typeof meta.role === 'string' ? meta.role : 'participant';
const novi_uuid = log_novi_uuid || extract_participant_uuid(meta);
const existing_participant = p_map.get(display_name);
if (!existing_participant) {
p_map.set(display_name, {
displayName: display_name,
role,
...(novi_uuid ? { novi_uuid } : {})
});
} else {
if (
existing_participant.role !== 'moderator' &&
role === 'moderator'
) {
existing_participant.role = 'moderator';
}
if (!existing_participant.novi_uuid && novi_uuid) {
existing_participant.novi_uuid = novi_uuid;
}
}
}
}
// 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: {
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.values()).sort(
(a: MeetingParticipant, b: MeetingParticipant) => {
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(
(a: MeetingEvent, b: MeetingEvent) =>
new Date(a.timestamp).getTime() -
new Date(b.timestamp).getTime()
);
}
const final_report = Array.from(meetings.values());
final_report.sort(
(a, b) =>
new Date(b.start_time).getTime() - new Date(a.start_time).getTime()
);
if (log_lvl) console.log('Final Jitsi report:', final_report);
return final_report;
}
export async function load_jitsi_report_from_cache({
account_id,
limit = 500,
log_lvl = 0
}: {
account_id: string;
limit?: number;
log_lvl?: number;
}): Promise<MeetingReport[] | null> {
try {
const cached_logs = await db_core.activity_log
.where('account_id_random')
.equals(account_id)
.and(
(log) =>
log.name === 'jitsi_meeting_event' ||
log.name === 'jitsi_meeting_stats'
)
.limit(limit)
.toArray();
if (cached_logs.length === 0) return null;
if (
cached_logs.some(
(log) => typeof log.url_params !== 'string' || !log.url_params
)
) {
if (log_lvl) {
console.log(
'Jitsi report cache is missing url_params; using API refresh for accurate UUID filtering.'
);
}
return null;
}
if (log_lvl) {
console.log(
`Jitsi report cache hit: ${cached_logs.length} activity_log rows`
);
}
return build_jitsi_report_from_logs(cached_logs, log_lvl);
} catch (err) {
if (log_lvl) console.warn('Jitsi report cache read failed.', err);
return null;
}
}
export async function qry__jitsi_report({
api_cfg,
account_id,
@@ -149,182 +407,7 @@ 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, participant> per meeting, keeping the
// stable display-name key used by the existing report while preserving Novi UUIDs
// when the log payload includes them.
//
// 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 → participant record
const participant_maps = new Map<string, Map<string, any>>();
// 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;
if (!meetings.has(meeting_id)) {
meetings.set(meeting_id, {
meeting_id,
room_name: 'Unknown',
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') {
// 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;
}
}
// Merge snapshot participant list (has role info — preferred source)
const snapshot = meta.participants as
| Array<Record<string, unknown>>
| undefined;
if (Array.isArray(snapshot)) {
for (const p of snapshot) {
const display_name = normalize_text(p.displayName);
if (!display_name) continue;
const role =
typeof p.role === 'string' ? p.role : 'participant';
const novi_uuid = extract_participant_uuid(p);
const existing_participant = p_map.get(display_name);
if (!existing_participant) {
p_map.set(display_name, {
displayName: display_name,
role,
...(novi_uuid ? { novi_uuid } : {})
});
continue;
}
if (
existing_participant.role !== 'moderator' &&
role === 'moderator'
) {
existing_participant.role = 'moderator';
}
if (!existing_participant.novi_uuid && novi_uuid) {
existing_participant.novi_uuid = novi_uuid;
}
}
}
}
// Collect participants from join events (may catch people who left before the snapshot)
if (log.action === 'jitsi_meeting_participant_joined') {
const display_name = normalize_text(meta.full_name);
if (display_name) {
const role =
typeof meta.role === 'string' ? meta.role : 'participant';
const novi_uuid = extract_participant_uuid(meta);
const existing_participant = p_map.get(display_name);
if (!existing_participant) {
p_map.set(display_name, {
displayName: display_name,
role,
...(novi_uuid ? { novi_uuid } : {})
});
} else {
if (
existing_participant.role !== 'moderator' &&
role === 'moderator'
) {
existing_participant.role = 'moderator';
}
if (!existing_participant.novi_uuid && novi_uuid) {
existing_participant.novi_uuid = novi_uuid;
}
}
}
}
// 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: {
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.values())
.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(
(a: any, b: any) =>
new Date(a.timestamp).getTime() -
new Date(b.timestamp).getTime()
);
}
const final_report = Array.from(meetings.values());
final_report.sort(
(a, b) =>
new Date(b.start_time).getTime() - new Date(a.start_time).getTime()
);
if (log_lvl) console.log('Final Jitsi report:', final_report);
return final_report;
return build_jitsi_report_from_logs(flat_log_list, log_lvl);
}
export const load_jitsi_report = qry__jitsi_report;

View File

@@ -415,6 +415,9 @@ export interface ae_ActivityLog extends ae_BaseObj {
external_client_id?: string;
source?: string;
url_root?: string;
url_full_path?: string;
url_params?: string;
object_type?: string;
object_id_random?: string;