Refine IDAA Jitsi reports UX
Add Novi UUID exclusion and known-meeting filtering, default the report date range to the last 60 days, and hide Room Name unless global edit mode is enabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -51,9 +51,9 @@ let show_llm_api_token = $state(false);
|
||||
// Ensure we have a valid object
|
||||
if (!cfg_json) cfg_json = {};
|
||||
|
||||
function add_to_list(key: string) {
|
||||
function add_to_list(key: string, prompt_label: string) {
|
||||
if (!cfg_json[key]) cfg_json[key] = [];
|
||||
const val = prompt('Enter Novi UUID:');
|
||||
const val = prompt(prompt_label);
|
||||
if (val) cfg_json[key].push(val);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@ function remove_from_list(key: string, index: number) {
|
||||
cfg_json[key].splice(index, 1);
|
||||
}
|
||||
|
||||
function list_count(key: string): number {
|
||||
return Array.isArray(cfg_json[key]) ? cfg_json[key].length : 0;
|
||||
}
|
||||
|
||||
// Sync Raw JSON string when entering the tab
|
||||
$effect(() => {
|
||||
if (active_tab === 'raw') {
|
||||
@@ -365,24 +369,32 @@ $effect(() => {
|
||||
|
||||
<!-- UUID Lists -->
|
||||
<section class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{#each [{ key: 'novi_admin_li', label: 'Novi Admins', color: 'text-error-500' }, { key: 'novi_trusted_li', label: 'Novi Trusted', color: 'text-warning-500' }, { key: 'novi_jitsi_mod_li', label: 'Jitsi Moderators', color: 'text-primary-500' }, { key: 'novi_idaa_group_guid_li', label: 'Member Group GUIDs', color: 'text-secondary-500' }] as list (list.key)}
|
||||
<div class="bg-surface-500/5 space-y-2 rounded-lg p-3">
|
||||
<header class="flex items-center justify-between">
|
||||
<span
|
||||
class="text-[10px] font-black tracking-wider uppercase {list.color}"
|
||||
>{list.label}</span>
|
||||
{#each [{ key: 'novi_admin_li', label: 'Novi Admins', color: 'text-error-500', prompt: 'Enter Novi UUID:' }, { key: 'novi_trusted_li', label: 'Novi Trusted', color: 'text-warning-500', prompt: 'Enter Novi UUID:' }, { key: 'novi_jitsi_mod_li', label: 'Jitsi Moderators', color: 'text-primary-500', prompt: 'Enter Novi UUID:' }, { key: 'novi_idaa_group_guid_li', label: 'Member Group GUIDs', color: 'text-secondary-500', prompt: 'Enter Group GUID:' }, { key: 'jitsi_exclude_uuids', label: 'Jitsi Excluded UUIDs', color: 'text-error-500', prompt: 'Enter Novi UUID to exclude:' }, { key: 'jitsi_known_meetings', label: 'Known IDAA Meetings', color: 'text-primary-500', prompt: 'Enter meeting name to allow:' }] as list (list.key)}
|
||||
<details class="bg-surface-500/5 rounded-lg p-3">
|
||||
<summary
|
||||
class="flex cursor-pointer list-none items-center justify-between gap-2 [&::-webkit-details-marker]:hidden">
|
||||
<span class="min-w-0">
|
||||
<span
|
||||
class="text-[10px] font-black tracking-wider uppercase {list.color}"
|
||||
>{list.label}</span>
|
||||
<span class="ml-2 text-[10px] opacity-50"
|
||||
>({list_count(list.key)})</span>
|
||||
</span>
|
||||
<button
|
||||
class="btn btn-icon btn-icon-sm variant-soft-primary"
|
||||
onclick={() => add_to_list(list.key)}>
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
add_to_list(list.key, list.prompt);
|
||||
}}>
|
||||
<Plus size="12" />
|
||||
</button>
|
||||
</header>
|
||||
<div class="space-y-1">
|
||||
{#each cfg_json[list.key] ?? [] as uuid, i (uuid)}
|
||||
</summary>
|
||||
<div class="mt-3 space-y-1">
|
||||
{#each cfg_json[list.key] ?? [] as item, i (i)}
|
||||
<div
|
||||
class="bg-surface-500/10 flex items-center gap-1 rounded p-1 font-mono text-[10px]">
|
||||
<span class="grow truncate"
|
||||
>{uuid}</span>
|
||||
<span class="grow truncate">{item}</span>
|
||||
<button
|
||||
class="text-error-500 transition-transform hover:scale-110"
|
||||
onclick={() =>
|
||||
@@ -392,7 +404,7 @@ $effect(() => {
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
|
||||
@@ -24,6 +24,43 @@ function parse_duration_seconds(d: string): number {
|
||||
return (h || 0) * 3600 + (m || 0) * 60 + (s || 0);
|
||||
}
|
||||
|
||||
function normalize_text(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function normalize_uuid(value: unknown): string {
|
||||
return normalize_text(value).toLowerCase();
|
||||
}
|
||||
|
||||
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;
|
||||
const nested_context_user = nested_context?.user as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
|
||||
const candidates = [
|
||||
source.novi_uuid,
|
||||
source.uuid,
|
||||
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
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const uuid = normalize_uuid(candidate);
|
||||
if (uuid) return uuid;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Queries all Jitsi-related activity logs and processes them into a structured report,
|
||||
* grouped by meeting ID.
|
||||
@@ -118,14 +155,16 @@ export async function qry__jitsi_report({
|
||||
// 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.
|
||||
// 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 → role
|
||||
const participant_maps = new Map<string, Map<string, string>>();
|
||||
// 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>();
|
||||
|
||||
@@ -177,15 +216,35 @@ export async function qry__jitsi_report({
|
||||
|
||||
// Merge snapshot participant list (has role info — preferred source)
|
||||
const snapshot = meta.participants as
|
||||
| Array<{ displayName?: string; role?: string }>
|
||||
| Array<Record<string, unknown>>
|
||||
| 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');
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,9 +252,30 @@ export async function qry__jitsi_report({
|
||||
|
||||
// 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');
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,8 +297,7 @@ export async function qry__jitsi_report({
|
||||
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 }))
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user