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:
@@ -490,6 +490,13 @@ Both modes use the same filtered data set — switching views does not reset fil
|
||||
|
||||
A "Reset Filters" button appears whenever any filter is non-default.
|
||||
|
||||
In edit mode, two extra toggles appear:
|
||||
- **Show excluded IDs** — temporarily include the UUIDs listed in `jitsi_exclude_uuids`
|
||||
- **Show all meetings** — temporarily ignore `jitsi_known_meetings`
|
||||
|
||||
An "Active Exclusions" panel below the filter bar shows the currently applied Novi UUID exclusions
|
||||
and known meeting-name whitelist values. Each list is collapsible so the page stays compact.
|
||||
|
||||
### Staff / Meeting Filtering
|
||||
|
||||
**Problem:** Staff/test accounts and one-off test rooms distort the reports.
|
||||
@@ -505,12 +512,16 @@ A "Reset Filters" button appears whenever any filter is non-default.
|
||||
**How it works:**
|
||||
|
||||
1. The page reads `$ae_loc.site_cfg_json?.jitsi_exclude_uuids` and excludes matching participants by Novi UUID.
|
||||
The UUID comes from the Jitsi log `url_params.uuid` field. `g_uuid` is the meeting/group UUID and is not used here.
|
||||
2. If a participant record does not include a UUID in the activity log, it is left visible; UUIDs are used whenever available.
|
||||
3. `real_participant_count = real_participants.length` drives filters, exports, and the per-meeting attendee count.
|
||||
4. Room-level unique participant counts are computed from Novi UUIDs when present, with display-name fallback only for UUID-less records.
|
||||
5. If `$ae_loc.site_cfg_json?.jitsi_known_meetings` is non-empty, only meetings whose `room_name` matches one of the listed names are shown.
|
||||
6. The Room Name filter is only shown when global edit mode is enabled.
|
||||
|
||||
**Temporary stopgap:** the report also hides these staff display names through the same UUID-exclusion toggle until the long-term logging fix lands:
|
||||
`Scott I.`, `Brie P.`, `Michelle V.`
|
||||
|
||||
**Note:** matching is case-insensitive on the stored `room_name` / meeting name.
|
||||
|
||||
### Summary Stats
|
||||
@@ -524,6 +535,12 @@ Shown above the meeting list when data is loaded. Stats reflect the **filtered +
|
||||
|
||||
In grouped view, each room header also shows its own subtotals (meeting count, unique participants by Novi UUID when available).
|
||||
|
||||
### Caching / Load Behavior
|
||||
|
||||
The page now reads cached `activity_log` rows from IndexedDB first, renders that result immediately,
|
||||
then refreshes from the API in the background. That keeps the report usable even when the network
|
||||
round-trip is slow.
|
||||
|
||||
### Jitsi URL Builder
|
||||
|
||||
Collapsible panel, visible to `trusted_access` users only. Generates properly-formatted Jitsi meeting URLs for IDAA rooms. Component: `ae_idaa_comp__jitsi_url_builder.svelte`.
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
import { load_jitsi_report } from '$lib/ae_reports/reports_functions';
|
||||
import {
|
||||
load_jitsi_report,
|
||||
load_jitsi_report_from_cache
|
||||
} from '$lib/ae_reports/reports_functions';
|
||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||
import { idaa_loc } from '$lib/stores/ae_idaa_stores';
|
||||
import JitsiUrlBuilder from './ae_idaa_comp__jitsi_url_builder.svelte';
|
||||
@@ -92,20 +95,42 @@ $effect(() => {
|
||||
meetings_error = null;
|
||||
|
||||
untrack(() => {
|
||||
void load_jitsi_report({
|
||||
api_cfg,
|
||||
account_id,
|
||||
log_lvl: 1
|
||||
})
|
||||
.then((m: MeetingReport[]) => {
|
||||
meetings_all = m ?? [];
|
||||
meetings_loading = false;
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
meetings_error =
|
||||
err instanceof Error ? err.message : String(err);
|
||||
meetings_loading = false;
|
||||
void (async () => {
|
||||
const cached = await load_jitsi_report_from_cache({
|
||||
account_id,
|
||||
log_lvl: 1
|
||||
});
|
||||
|
||||
if (cached && cached.length > 0) {
|
||||
meetings_all = cached;
|
||||
meetings_loading = false;
|
||||
void load_jitsi_report({
|
||||
api_cfg,
|
||||
account_id,
|
||||
log_lvl: 1
|
||||
})
|
||||
.then((m: MeetingReport[]) => {
|
||||
meetings_all = m ?? [];
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.warn('Jitsi report background refresh failed.', err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fresh = await load_jitsi_report({
|
||||
api_cfg,
|
||||
account_id,
|
||||
log_lvl: 1
|
||||
});
|
||||
meetings_all = fresh ?? [];
|
||||
} catch (err: unknown) {
|
||||
meetings_error = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
meetings_loading = false;
|
||||
}
|
||||
})();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,6 +143,7 @@ let exclude_uuids = $derived<string[]>(
|
||||
?.jitsi_exclude_uuids
|
||||
).map((uuid) => uuid.toLowerCase())
|
||||
);
|
||||
const temp_excluded_names = ['Scott I.', 'Brie P.', 'Michelle V.'];
|
||||
let exclude_names = $derived<string[]>(
|
||||
normalize_list(
|
||||
($ae_loc.site_cfg_json as Record<string, unknown>)
|
||||
@@ -132,7 +158,12 @@ let known_meetings = $derived<string[]>(
|
||||
);
|
||||
let exclude_uuid_set = $derived(new Set(exclude_uuids));
|
||||
let exclude_name_set = $derived(new Set(exclude_names));
|
||||
let temp_excluded_name_set = $derived(
|
||||
new Set(temp_excluded_names.map((name) => name.toLowerCase()))
|
||||
);
|
||||
let known_meeting_set = $derived(new Set(known_meetings));
|
||||
let show_excluded_uuids = $state(false);
|
||||
let show_all_meetings = $state(false);
|
||||
|
||||
// Apply exclusion to every meeting — produces the "real" participant list
|
||||
// used by all downstream filters, stats, and display.
|
||||
@@ -141,13 +172,17 @@ let meetings_enriched = $derived<MeetingReportEnriched[]>(
|
||||
// final_participants can be null/undefined if meta_json.participants was null in the API response
|
||||
const all_participants: MeetingParticipant[] = m.final_participants ?? [];
|
||||
const real_participants = all_participants.filter((p) => {
|
||||
if (show_excluded_uuids) return true;
|
||||
const participant_uuid = normalize_uuid(p.novi_uuid);
|
||||
if (participant_uuid && exclude_uuid_set.has(participant_uuid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const participant_name = normalize_meeting_name(p.displayName);
|
||||
if (exclude_name_set.has(participant_name)) {
|
||||
if (
|
||||
exclude_name_set.has(participant_name) ||
|
||||
temp_excluded_name_set.has(participant_name)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -174,7 +209,9 @@ let filter_date_to = $state(default_filter_date_to);
|
||||
let filters_are_modified = $derived(
|
||||
filter_real_only ||
|
||||
filter_min_participants !== 0 ||
|
||||
filter_room_name !== '' ||
|
||||
(filter_room_name !== '' && $ae_loc.edit_mode) ||
|
||||
show_excluded_uuids ||
|
||||
show_all_meetings ||
|
||||
filter_date_from !== default_filter_date_from ||
|
||||
filter_date_to !== default_filter_date_to
|
||||
);
|
||||
@@ -183,6 +220,8 @@ function reset_filters() {
|
||||
filter_real_only = false;
|
||||
filter_min_participants = 0;
|
||||
filter_room_name = '';
|
||||
show_excluded_uuids = false;
|
||||
show_all_meetings = false;
|
||||
filter_date_from = default_filter_date_from;
|
||||
filter_date_to = default_filter_date_to;
|
||||
}
|
||||
@@ -214,7 +253,7 @@ function compute_end_time(start_time: string, duration: string): string {
|
||||
// All counts and thresholds use real_participant_count (post-exclusion).
|
||||
let meetings_filtered = $derived.by(() => {
|
||||
return meetings_enriched.filter((m) => {
|
||||
if (known_meeting_set.size > 0) {
|
||||
if (!show_all_meetings && known_meeting_set.size > 0) {
|
||||
if (!known_meeting_set.has(normalize_meeting_name(m.room_name))) {
|
||||
return false;
|
||||
}
|
||||
@@ -224,11 +263,14 @@ let meetings_filtered = $derived.by(() => {
|
||||
if (m.real_participant_count < 2 && dur_secs <= 300) return false;
|
||||
}
|
||||
if (m.real_participant_count < filter_min_participants) return false;
|
||||
if (
|
||||
filter_room_name &&
|
||||
!m.room_name?.toLowerCase().includes(filter_room_name.toLowerCase())
|
||||
)
|
||||
return false;
|
||||
if ($ae_loc.edit_mode && filter_room_name) {
|
||||
if (
|
||||
!m.room_name
|
||||
?.toLowerCase()
|
||||
.includes(filter_room_name.toLowerCase())
|
||||
)
|
||||
return false;
|
||||
}
|
||||
if (filter_date_from) {
|
||||
if (Date.parse(m.start_time) < Date.parse(filter_date_from))
|
||||
return false;
|
||||
@@ -475,6 +517,25 @@ function export_json() {
|
||||
>Real meetings only</span>
|
||||
</label>
|
||||
<div class="bg-surface-200-800 hidden h-8 w-px self-end sm:block"></div>
|
||||
{#if $ae_loc.edit_mode}
|
||||
<label class="flex cursor-pointer items-center gap-2 self-end pb-1.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={show_excluded_uuids}
|
||||
class="checkbox checkbox-sm" />
|
||||
<span class="text-sm font-medium whitespace-nowrap"
|
||||
>Show excluded IDs</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center gap-2 self-end pb-1.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={show_all_meetings}
|
||||
class="checkbox checkbox-sm" />
|
||||
<span class="text-sm font-medium whitespace-nowrap"
|
||||
>Show all meetings</span>
|
||||
</label>
|
||||
<div class="bg-surface-200-800 hidden h-8 w-px self-end sm:block"></div>
|
||||
{/if}
|
||||
<div>
|
||||
<label
|
||||
for="filter_min_p"
|
||||
@@ -488,8 +549,8 @@ function export_json() {
|
||||
bind:value={filter_min_participants}
|
||||
class="border-surface-200-800 bg-surface-50-950 w-20 rounded border px-2 py-1" />
|
||||
</div>
|
||||
<div>
|
||||
{#if $ae_loc.edit_mode}
|
||||
{#if $ae_loc.edit_mode}
|
||||
<div>
|
||||
<label
|
||||
for="filter_room"
|
||||
class="mb-1 block text-xs tracking-wide uppercase opacity-40">
|
||||
@@ -501,8 +562,8 @@ function export_json() {
|
||||
placeholder="Search rooms..."
|
||||
bind:value={filter_room_name}
|
||||
class="border-surface-200-800 bg-surface-50-950 rounded border px-2 py-1" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<label
|
||||
for="filter_date_from"
|
||||
@@ -539,6 +600,40 @@ function export_json() {
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if exclude_uuids.length > 0 || known_meetings.length > 0}
|
||||
<div class="bg-surface-100-900 border-surface-200-800 rounded-xl border p-3">
|
||||
<div class="mb-2 text-xs tracking-wide uppercase opacity-50">
|
||||
Active Exclusions
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||
<details class="bg-surface-50-950 rounded-lg border border-dashed border-opacity-30 p-2">
|
||||
<summary class="cursor-pointer list-none font-semibold">
|
||||
Excluded Novi UUIDs ({exclude_uuids.length})
|
||||
</summary>
|
||||
<div class="mt-2 space-y-1 text-xs font-mono">
|
||||
{#each exclude_uuids as uuid (uuid)}
|
||||
<div class="bg-surface-200-800 rounded px-2 py-1">
|
||||
{uuid}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</details>
|
||||
<details class="bg-surface-50-950 rounded-lg border border-dashed border-opacity-30 p-2">
|
||||
<summary class="cursor-pointer list-none font-semibold">
|
||||
Known Meeting Names ({known_meetings.length})
|
||||
</summary>
|
||||
<div class="mt-2 space-y-1 text-xs font-mono">
|
||||
{#each known_meetings as meeting_name (meeting_name)}
|
||||
<div class="bg-surface-200-800 rounded px-2 py-1">
|
||||
{meeting_name}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if meetings_loading}
|
||||
<!-- Loading skeleton -->
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user