This seems like a good pause point. The new Jitsi reports page is functional. The meetings log various activities and events.
This commit is contained in:
@@ -7,6 +7,7 @@ const objTypeToEndpointMap: Record<string, string> = {
|
||||
address: '/crud/address/list',
|
||||
archive: '/crud/archive/list',
|
||||
archive_content: '/crud/archive/content/list',
|
||||
activity_log: '/crud/activity_log/list',
|
||||
contact: '/crud/contact/list',
|
||||
data_store: '/crud/data_store/list',
|
||||
event: '/crud/event/list',
|
||||
|
||||
115
src/lib/ae_reports/reports_functions.ts
Normal file
115
src/lib/ae_reports/reports_functions.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
import { api } from '$lib/api/api';
|
||||
|
||||
/**
|
||||
* @description Queries all Jitsi-related activity logs and processes them into a structured report,
|
||||
* grouped by meeting ID.
|
||||
* @param api_cfg The API configuration object.
|
||||
* @param account_id The account ID to query against.
|
||||
* @param log_lvl The logging level.
|
||||
* @returns A structured array of meeting report objects.
|
||||
*/
|
||||
export async function load_jitsi_report({
|
||||
api_cfg,
|
||||
account_id,
|
||||
log_lvl = 0
|
||||
}: {
|
||||
api_cfg: any;
|
||||
account_id: string;
|
||||
log_lvl?: number;
|
||||
}) {
|
||||
if (log_lvl) console.log('*** load_jitsi_report() ***');
|
||||
|
||||
// Step 1: Query all relevant activity logs from the API.
|
||||
const params_json = {
|
||||
or_qry: [
|
||||
{
|
||||
field: 'name',
|
||||
operator: '=',
|
||||
value: 'jitsi_meeting_stats_update'
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
operator: '=',
|
||||
value: 'jitsi_meeting_event'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const flat_log_list = await api.get_ae_obj_li_for_obj_id_crud_v2({
|
||||
api_cfg: api_cfg,
|
||||
obj_type: 'activity_log',
|
||||
for_obj_type: 'account',
|
||||
for_obj_id: account_id,
|
||||
// use_alt_tbl: true,
|
||||
// use_alt_mdl: true,
|
||||
enabled: 'all',
|
||||
hidden: 'all',
|
||||
// order_by_li: { created_on: 'DESC' },
|
||||
limit: 500, // Fetch a reasonable number of recent logs
|
||||
// order_by_li: { created_on: 'DESC' },
|
||||
// params_json: params_json,
|
||||
log_lvl: 2
|
||||
});
|
||||
|
||||
if (!flat_log_list || flat_log_list.length === 0) {
|
||||
if (log_lvl) console.log('No Jitsi activity logs found.');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Step 2: Process the flat list into a structured report.
|
||||
const meetings = new Map<string, any>();
|
||||
|
||||
for (const log of flat_log_list) {
|
||||
const meeting_id = log.external_client_id;
|
||||
if (!meeting_id) 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,
|
||||
room_name: 'Unknown',
|
||||
start_time: log.created_on, // Fallback start time
|
||||
final_duration: '00:00:00',
|
||||
final_participants: [],
|
||||
final_participant_count: 0,
|
||||
events: []
|
||||
});
|
||||
}
|
||||
|
||||
const meeting_report = meetings.get(meeting_id);
|
||||
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
// This is a discrete event log.
|
||||
meeting_report.events.push({
|
||||
timestamp: log.created_on,
|
||||
action: log.action,
|
||||
details: log.meta_json
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
105
src/routes/idaa/jitsi_reports/+page.svelte
Normal file
105
src/routes/idaa/jitsi_reports/+page.svelte
Normal file
@@ -0,0 +1,105 @@
|
||||
<script lang="ts">
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
}
|
||||
let { data }: Props = $props();
|
||||
|
||||
let open_accordions = $state<{ [key: string]: boolean }>({});
|
||||
|
||||
function toggle_accordion(meeting_id: string) {
|
||||
open_accordions[meeting_id] = !open_accordions[meeting_id];
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Æ: Jitsi Meeting Reports</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="p-4 space-y-4">
|
||||
<h1 class="h1">Jitsi Meeting Reports</h1>
|
||||
|
||||
{#if data.meetings && data.meetings.length > 0}
|
||||
<div class="space-y-2">
|
||||
{#each data.meetings as meeting (meeting.meeting_id)}
|
||||
<div class="card card-hover">
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<header class="card-header p-2 cursor-pointer" onclick={() => toggle_accordion(meeting.meeting_id)}>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<div class="flex-1">
|
||||
<!-- NOTE: Normally I would the "h3" class, but Novi classes make things look odd. -->
|
||||
<h3 class="text-base">{meeting.room_name}</h3>
|
||||
<p class="text-sm text-gray-500">{new Date(meeting.start_time).toLocaleString()}</p>
|
||||
</div>
|
||||
<div class="flex-none flex items-center space-x-4 text-sm mr-4">
|
||||
<span>Duration: {meeting.final_duration}</span>
|
||||
<span>Participants: {meeting.final_participant_count}</span>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<span class="transition-transform duration-200" class:rotate-180={open_accordions[meeting.meeting_id]}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{#if open_accordions[meeting.meeting_id]}
|
||||
<div class="p-4 border-t border-gray-200 dark:border-gray-700 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<!-- NOTE: Normally I would the "h4" class, but Novi classes make things look odd. -->
|
||||
<h4 class="text-base">Event Timeline</h4>
|
||||
{#if meeting.events && meeting.events.length > 0}
|
||||
<ul class="list-disc list-inside space-y-2 mt-2">
|
||||
{#each meeting.events as event}
|
||||
<li>
|
||||
<span class="font-mono text-xs">[{new Date(event.timestamp).toLocaleTimeString()}]</span>
|
||||
<span class="font-semibold">{ae_util.to_title_case(event.action.replace('jitsi_meeting_', ''))}</span>
|
||||
{#if event.details.full_name}
|
||||
- by {event.details.full_name}
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<p class="text-gray-500 italic mt-2">No discrete events recorded.</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<!-- NOTE: Normally I would the "h4" class, but Novi classes make things look odd. -->
|
||||
<h4 class="text-base">Final Participants</h4>
|
||||
{#if meeting.final_participants && meeting.final_participants.length > 0}
|
||||
<div class="table-container mt-2">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each meeting.final_participants as participant}
|
||||
<tr>
|
||||
<td>{participant.displayName}</td>
|
||||
<td>{ae_util.to_title_case(participant.role)}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-gray-500 italic mt-2">No participant data available.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="card p-4 text-center">
|
||||
<h3 class="h3">No Meeting Reports Found</h3>
|
||||
<p>There are no Jitsi activity logs to display.</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
28
src/routes/idaa/jitsi_reports/+page.ts
Normal file
28
src/routes/idaa/jitsi_reports/+page.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||
import { load_jitsi_report } from '$lib/ae_reports/reports_functions';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export const load: PageLoad = async ({ fetch }) => {
|
||||
console.log('*** /idaa/jitsi_reports/+page.ts ***');
|
||||
|
||||
const api_cfg = get(ae_api);
|
||||
const account_id = get(ae_loc)?.account_id;
|
||||
|
||||
if (!api_cfg || !account_id) {
|
||||
console.error('API config or Account ID not available for loading Jitsi reports.');
|
||||
return {
|
||||
meetings: []
|
||||
};
|
||||
}
|
||||
|
||||
const meetings = await load_jitsi_report({
|
||||
api_cfg,
|
||||
account_id,
|
||||
log_lvl: 1
|
||||
});
|
||||
|
||||
return {
|
||||
meetings
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user