feat(idaa): add Jitsi URL Builder tool to reports page
New component ae_idaa_comp__jitsi_url_builder.svelte builds and previews Jitsi iframe URLs for testing and Novi page configuration. Features: - Environment selector (prod / dev / local / custom) - Room name, Novi UUID, site key inputs - Moderator toggle (explains JWT + logging implication) - Advanced: domain, start muted/hidden, all 5 sound settings - Output in URL or iframe HTML snippet mode with copy button - "Open in new tab" for quick testing Embedded on jitsi_reports page as a collapsible panel, gated to trusted_access users only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
import { ae_loc } from '$lib/stores/ae_stores';
|
||||
import JitsiUrlBuilder from './ae_idaa_comp__jitsi_url_builder.svelte';
|
||||
|
||||
interface MeetingEvent {
|
||||
timestamp: string;
|
||||
@@ -114,6 +116,7 @@
|
||||
|
||||
// --- Accordion state ---
|
||||
let open_accordions = $state<{ [key: string]: boolean }>({});
|
||||
let show_url_builder = $state(false);
|
||||
|
||||
function toggle_accordion(meeting_id: string) {
|
||||
open_accordions[meeting_id] = !open_accordions[meeting_id];
|
||||
@@ -193,6 +196,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jitsi URL Builder — trusted_access only -->
|
||||
{#if $ae_loc.trusted_access}
|
||||
<div class="bg-surface-100-900 border border-surface-200-800 rounded-xl overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (show_url_builder = !show_url_builder)}
|
||||
class="w-full flex items-center justify-between gap-2 p-3 hover:bg-surface-200-800 transition-colors duration-200 text-left"
|
||||
>
|
||||
<span class="flex items-center gap-2 font-semibold text-sm">
|
||||
<span class="fas fa-tools" aria-hidden="true"></span>
|
||||
Jitsi URL Builder
|
||||
</span>
|
||||
<span
|
||||
class="fas text-xs opacity-60"
|
||||
class:fa-chevron-down={!show_url_builder}
|
||||
class:fa-chevron-up={show_url_builder}
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</button>
|
||||
{#if show_url_builder}
|
||||
<div class="border-t border-surface-200-800 p-4">
|
||||
<JitsiUrlBuilder />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Filter bar -->
|
||||
<div class="bg-surface-100-900 border border-surface-200-800 rounded-xl p-3 flex flex-row flex-wrap gap-3 items-end">
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
<script lang="ts">
|
||||
// ae_idaa_comp__jitsi_url_builder.svelte
|
||||
// Builds and previews Jitsi iframe URLs for testing and Novi page configuration.
|
||||
// Only shown to trusted_access users — not for general IDAA members.
|
||||
|
||||
// --- Environment presets ---
|
||||
const BASE_URL_OPTIONS = [
|
||||
{ label: 'Production', value: 'https://sk-idaa.oneskyit.com/idaa/video_conferences' },
|
||||
{ label: 'Dev / Staging', value: 'https://dev-idaa.oneskyit.com/idaa/video_conferences' },
|
||||
{ label: 'Local', value: 'http://idaa.localhost:5173/idaa/video_conferences' },
|
||||
{ label: 'Custom…', value: 'custom' }
|
||||
];
|
||||
|
||||
// --- State ---
|
||||
let base_url_preset = $state(BASE_URL_OPTIONS[1].value); // dev by default
|
||||
let base_url_custom = $state('');
|
||||
let room_name = $state('IDAA-Test-Meeting');
|
||||
let site_key = $state('restricted-access');
|
||||
let uuid = $state('');
|
||||
let is_moderator = $state(false);
|
||||
let domain = $state('jitsi.dgrzone.com');
|
||||
let start_muted = $state(true);
|
||||
let start_hidden = $state(false);
|
||||
let disable_incoming_msg = $state(true);
|
||||
let disable_participant_joined = $state(false);
|
||||
let disable_participant_left = $state(false);
|
||||
let disable_reaction = $state(true);
|
||||
let disable_raise_hand = $state(true);
|
||||
|
||||
let show_advanced = $state(false);
|
||||
let show_sound = $state(false);
|
||||
let output_mode = $state<'url' | 'iframe'>('url');
|
||||
let copied = $state(false);
|
||||
|
||||
// --- Derived URL ---
|
||||
let effective_base = $derived(
|
||||
base_url_preset === 'custom' ? base_url_custom.trim() : base_url_preset
|
||||
);
|
||||
|
||||
let built_url = $derived.by(() => {
|
||||
if (!effective_base || !room_name.trim()) return '';
|
||||
const p = new URLSearchParams();
|
||||
if (uuid.trim()) p.set('uuid', uuid.trim());
|
||||
p.set('iframe', 'true');
|
||||
p.set('key', site_key.trim() || 'restricted-access');
|
||||
p.set('room', room_name.trim());
|
||||
if (is_moderator) p.set('moderator', 'true');
|
||||
if (domain.trim() && domain.trim() !== 'jitsi.dgrzone.com') p.set('domain', domain.trim());
|
||||
if (start_muted) p.set('start_muted', 'true');
|
||||
if (start_hidden) p.set('start_hidden', 'true');
|
||||
if (disable_incoming_msg) p.set('incoming_msg_sound', 'true');
|
||||
if (disable_participant_joined) p.set('participant_joined_sound', 'true');
|
||||
if (disable_participant_left) p.set('participant_left_sound', 'true');
|
||||
if (disable_reaction) p.set('reaction_sound', 'true');
|
||||
if (disable_raise_hand) p.set('raise_hand_sound', 'true');
|
||||
return `${effective_base}?${p.toString()}`;
|
||||
});
|
||||
|
||||
let iframe_snippet = $derived(`<p>
|
||||
<iframe
|
||||
width="100%"
|
||||
height="950"
|
||||
id="ae_idaa_jitsi_meeting_iframe"
|
||||
src="${built_url}"
|
||||
style="min-height: 750px; height: min-content; max-height: 2048px"
|
||||
class="ae_idaa_iframe"
|
||||
allow="camera; microphone; fullscreen; display-capture; autoplay; clipboard-write"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</p>`);
|
||||
|
||||
let output = $derived(output_mode === 'iframe' ? iframe_snippet : built_url);
|
||||
|
||||
function copy_output() {
|
||||
if (!output) return;
|
||||
navigator.clipboard.writeText(output).then(() => {
|
||||
copied = true;
|
||||
setTimeout(() => (copied = false), 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-3 text-sm">
|
||||
|
||||
<!-- Environment + Room (always visible) -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label for="jub_base_url" class="block text-xs uppercase tracking-wide opacity-40 mb-1">
|
||||
Environment
|
||||
</label>
|
||||
<select
|
||||
id="jub_base_url"
|
||||
bind:value={base_url_preset}
|
||||
class="border border-surface-200-800 rounded px-2 py-1 w-full bg-surface-50-950 text-sm"
|
||||
>
|
||||
{#each BASE_URL_OPTIONS as opt}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if base_url_preset === 'custom'}
|
||||
<input
|
||||
type="url"
|
||||
bind:value={base_url_custom}
|
||||
placeholder="https://…/idaa/video_conferences"
|
||||
class="border border-surface-200-800 rounded px-2 py-1 w-full bg-surface-50-950 text-sm font-mono mt-1"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label for="jub_room" class="block text-xs uppercase tracking-wide opacity-40 mb-1">
|
||||
Room Name <span class="text-error-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="jub_room"
|
||||
bind:value={room_name}
|
||||
placeholder="IDAA-Meeting-Room"
|
||||
class="border border-surface-200-800 rounded px-2 py-1 w-full bg-surface-50-950 text-sm font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- UUID + Key -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label for="jub_uuid" class="block text-xs uppercase tracking-wide opacity-40 mb-1">
|
||||
Novi UUID <span class="opacity-60">(blank = guest)</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="jub_uuid"
|
||||
bind:value={uuid}
|
||||
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
class="border border-surface-200-800 rounded px-2 py-1 w-full bg-surface-50-950 text-sm font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="jub_key" class="block text-xs uppercase tracking-wide opacity-40 mb-1">
|
||||
Site Key
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="jub_key"
|
||||
bind:value={site_key}
|
||||
class="border border-surface-200-800 rounded px-2 py-1 w-full bg-surface-50-950 text-sm font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Moderator toggle -->
|
||||
<label class="flex items-center gap-2 cursor-pointer w-fit">
|
||||
<input type="checkbox" bind:checked={is_moderator} class="checkbox checkbox-sm" />
|
||||
<span class="text-sm">Moderator</span>
|
||||
<span class="text-xs opacity-40">(requests JWT, enables lobby + activity logging)</span>
|
||||
</label>
|
||||
|
||||
<!-- Advanced toggle -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (show_advanced = !show_advanced)}
|
||||
class="flex items-center gap-1 text-xs opacity-60 hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<span class="fas {show_advanced ? 'fa-chevron-up' : 'fa-chevron-down'}" aria-hidden="true"></span>
|
||||
{show_advanced ? 'Hide' : 'Show'} advanced options
|
||||
</button>
|
||||
|
||||
{#if show_advanced}
|
||||
<div class="border border-surface-200-800 rounded-xl p-3 space-y-3 bg-surface-100-900">
|
||||
<!-- Domain -->
|
||||
<div>
|
||||
<label for="jub_domain" class="block text-xs uppercase tracking-wide opacity-40 mb-1">
|
||||
Jitsi Domain
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="jub_domain"
|
||||
bind:value={domain}
|
||||
class="border border-surface-200-800 rounded px-2 py-1 w-full bg-surface-50-950 text-sm font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Start options -->
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" bind:checked={start_muted} class="checkbox checkbox-sm" />
|
||||
<span class="text-xs">Start muted</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" bind:checked={start_hidden} class="checkbox checkbox-sm" />
|
||||
<span class="text-xs">Start hidden (video off)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Sound settings -->
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (show_sound = !show_sound)}
|
||||
class="flex items-center gap-1 text-xs opacity-60 hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<span class="fas {show_sound ? 'fa-chevron-up' : 'fa-chevron-down'}" aria-hidden="true"></span>
|
||||
{show_sound ? 'Hide' : 'Show'} sound settings
|
||||
</button>
|
||||
{#if show_sound}
|
||||
<div class="flex flex-wrap gap-4 mt-2">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" bind:checked={disable_incoming_msg} class="checkbox checkbox-sm" />
|
||||
<span class="text-xs">Disable incoming msg sound</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" bind:checked={disable_participant_joined} class="checkbox checkbox-sm" />
|
||||
<span class="text-xs">Disable participant joined sound</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" bind:checked={disable_participant_left} class="checkbox checkbox-sm" />
|
||||
<span class="text-xs">Disable participant left sound</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" bind:checked={disable_reaction} class="checkbox checkbox-sm" />
|
||||
<span class="text-xs">Disable reaction sound</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" bind:checked={disable_raise_hand} class="checkbox checkbox-sm" />
|
||||
<span class="text-xs">Disable raise hand sound</span>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Output -->
|
||||
<div class="border-t border-surface-200-800 pt-3 space-y-2">
|
||||
<!-- Mode toggle -->
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (output_mode = 'url')}
|
||||
class="btn btn-sm {output_mode === 'url' ? 'preset-tonal-primary' : 'preset-tonal-surface border border-surface-200-800'}"
|
||||
>
|
||||
<span class="fas fa-link mr-1" aria-hidden="true"></span>
|
||||
URL
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (output_mode = 'iframe')}
|
||||
class="btn btn-sm {output_mode === 'iframe' ? 'preset-tonal-primary' : 'preset-tonal-surface border border-surface-200-800'}"
|
||||
>
|
||||
<span class="fas fa-code mr-1" aria-hidden="true"></span>
|
||||
iframe HTML
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if built_url}
|
||||
<div class="flex gap-1 items-stretch">
|
||||
<textarea
|
||||
readonly
|
||||
rows={output_mode === 'iframe' ? 8 : 2}
|
||||
value={output}
|
||||
class="border border-surface-200-800 rounded px-2 py-1.5 w-full bg-surface-50-950 text-xs font-mono resize-none cursor-text"
|
||||
onclick={(e) => (e.target as HTMLTextAreaElement).select()}
|
||||
title="Click to select all"
|
||||
></textarea>
|
||||
<button
|
||||
type="button"
|
||||
onclick={copy_output}
|
||||
title="Copy to clipboard"
|
||||
class="btn btn-sm shrink-0 self-start {copied ? 'preset-tonal-success' : 'preset-tonal-primary'} transition-colors"
|
||||
>
|
||||
<span class="fas {copied ? 'fa-check' : 'fa-copy'}" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
{#if output_mode === 'url'}
|
||||
<a
|
||||
href={built_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-xs text-primary-600 dark:text-primary-400 hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
<span class="fas fa-external-link-alt" aria-hidden="true"></span>
|
||||
Open in new tab
|
||||
</a>
|
||||
{/if}
|
||||
{:else}
|
||||
<p class="text-xs opacity-40 italic">Fill in Room Name to generate a URL.</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
Reference in New Issue
Block a user