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:
@@ -15,6 +15,7 @@ interface MeetingEvent {
|
||||
interface MeetingParticipant {
|
||||
displayName: string;
|
||||
role: string;
|
||||
novi_uuid?: string;
|
||||
}
|
||||
|
||||
interface MeetingReport {
|
||||
@@ -39,6 +40,45 @@ let meetings_loading = $state(true);
|
||||
let meetings_error = $state<string | null>(null);
|
||||
let meetings_load_started = $state(false);
|
||||
|
||||
function normalize_text(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function normalize_uuid(value: unknown): string {
|
||||
return normalize_text(value).toLowerCase();
|
||||
}
|
||||
|
||||
function normalize_meeting_name(value: unknown): string {
|
||||
return normalize_text(value).toLowerCase();
|
||||
}
|
||||
|
||||
function normalize_list(value: unknown): string[] {
|
||||
return Array.isArray(value)
|
||||
? value
|
||||
.map((item) => normalize_text(item))
|
||||
.filter((item) => item.length > 0)
|
||||
: [];
|
||||
}
|
||||
|
||||
function participant_identity(p: MeetingParticipant): string {
|
||||
const uuid = normalize_uuid(p.novi_uuid);
|
||||
if (uuid) return `uuid:${uuid}`;
|
||||
return `name:${normalize_meeting_name(p.displayName)}`;
|
||||
}
|
||||
|
||||
function format_date_input_value(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = `${date.getMonth() + 1}`.padStart(2, '0');
|
||||
const day = `${date.getDate()}`.padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const default_filter_date_to = format_date_input_value(today);
|
||||
const default_filter_date_from = format_date_input_value(
|
||||
new Date(today.getFullYear(), today.getMonth(), today.getDate() - 60)
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (meetings_load_started) return;
|
||||
if (!$idaa_loc.novi_verified && !$ae_loc.trusted_access) return;
|
||||
@@ -70,13 +110,29 @@ $effect(() => {
|
||||
});
|
||||
|
||||
// --- Staff/test exclusion ---
|
||||
// Read display names to exclude from site_cfg_json.jitsi_exclude_names.
|
||||
// Matched case-insensitively against MeetingParticipant.displayName.
|
||||
let exclude_names = $derived<string[]>(
|
||||
(($ae_loc.site_cfg_json as Record<string, unknown>)
|
||||
?.jitsi_exclude_names as string[] | undefined)
|
||||
?.map((n) => n.toLowerCase().trim()) ?? []
|
||||
// UUID-based exclusion is the canonical path; the legacy name list stays as a fallback
|
||||
// for older site config values until those are cleaned up.
|
||||
let exclude_uuids = $derived<string[]>(
|
||||
normalize_list(
|
||||
($ae_loc.site_cfg_json as Record<string, unknown>)
|
||||
?.jitsi_exclude_uuids
|
||||
).map((uuid) => uuid.toLowerCase())
|
||||
);
|
||||
let exclude_names = $derived<string[]>(
|
||||
normalize_list(
|
||||
($ae_loc.site_cfg_json as Record<string, unknown>)
|
||||
?.jitsi_exclude_names
|
||||
).map((name) => name.toLowerCase())
|
||||
);
|
||||
let known_meetings = $derived<string[]>(
|
||||
normalize_list(
|
||||
($ae_loc.site_cfg_json as Record<string, unknown>)
|
||||
?.jitsi_known_meetings
|
||||
).map((name) => name.toLowerCase())
|
||||
);
|
||||
let exclude_uuid_set = $derived(new Set(exclude_uuids));
|
||||
let exclude_name_set = $derived(new Set(exclude_names));
|
||||
let known_meeting_set = $derived(new Set(known_meetings));
|
||||
|
||||
// Apply exclusion to every meeting — produces the "real" participant list
|
||||
// used by all downstream filters, stats, and display.
|
||||
@@ -84,15 +140,19 @@ let meetings_enriched = $derived<MeetingReportEnriched[]>(
|
||||
meetings_all.map((m) => {
|
||||
// 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 =
|
||||
exclude_names.length > 0
|
||||
? all_participants.filter(
|
||||
(p) =>
|
||||
!exclude_names.includes(
|
||||
p.displayName.toLowerCase().trim()
|
||||
)
|
||||
)
|
||||
: all_participants;
|
||||
const real_participants = all_participants.filter((p) => {
|
||||
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)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
return {
|
||||
...m,
|
||||
real_participants,
|
||||
@@ -108,23 +168,23 @@ let group_by_room = $state(true);
|
||||
let filter_real_only = $state(false);
|
||||
let filter_min_participants = $state(0);
|
||||
let filter_room_name = $state('');
|
||||
let filter_date_from = $state('');
|
||||
let filter_date_to = $state('');
|
||||
let filter_date_from = $state(default_filter_date_from);
|
||||
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_date_from !== '' ||
|
||||
filter_date_to !== ''
|
||||
filter_date_from !== default_filter_date_from ||
|
||||
filter_date_to !== default_filter_date_to
|
||||
);
|
||||
|
||||
function reset_filters() {
|
||||
filter_real_only = false;
|
||||
filter_min_participants = 0;
|
||||
filter_room_name = '';
|
||||
filter_date_from = '';
|
||||
filter_date_to = '';
|
||||
filter_date_from = default_filter_date_from;
|
||||
filter_date_to = default_filter_date_to;
|
||||
}
|
||||
|
||||
// --- Duration helpers ---
|
||||
@@ -154,6 +214,11 @@ 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 (!known_meeting_set.has(normalize_meeting_name(m.room_name))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filter_real_only) {
|
||||
const dur_secs = parse_duration_seconds(m.final_duration);
|
||||
if (m.real_participant_count < 2 && dur_secs <= 300) return false;
|
||||
@@ -230,7 +295,7 @@ function room_stats(sessions: MeetingReportEnriched[]) {
|
||||
0
|
||||
);
|
||||
const unique_names = new Set(
|
||||
sessions.flatMap((m) => m.real_participants.map((p) => p.displayName))
|
||||
sessions.flatMap((m) => m.real_participants.map((p) => participant_identity(p)))
|
||||
);
|
||||
return {
|
||||
meeting_count: sessions.length,
|
||||
@@ -424,17 +489,19 @@ function export_json() {
|
||||
class="border-surface-200-800 bg-surface-50-950 w-20 rounded border px-2 py-1" />
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="filter_room"
|
||||
class="mb-1 block text-xs tracking-wide uppercase opacity-40">
|
||||
Room Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="filter_room"
|
||||
placeholder="Search rooms..."
|
||||
bind:value={filter_room_name}
|
||||
class="border-surface-200-800 bg-surface-50-950 rounded border px-2 py-1" />
|
||||
{#if $ae_loc.edit_mode}
|
||||
<label
|
||||
for="filter_room"
|
||||
class="mb-1 block text-xs tracking-wide uppercase opacity-40">
|
||||
Room Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="filter_room"
|
||||
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>
|
||||
<label
|
||||
@@ -564,7 +631,7 @@ function export_json() {
|
||||
? 'session'
|
||||
: 'sessions'}
|
||||
</span>
|
||||
<span>
|
||||
<span title="Unique participants (by Novi UUID when available)">
|
||||
<span
|
||||
class="fas fa-users mr-1"
|
||||
aria-hidden="true"></span>
|
||||
|
||||
Reference in New Issue
Block a user