feat: add AE_Comp_Site_Config_Editor for managed site configuration

This commit is contained in:
Scott Idem
2026-01-29 15:09:51 -05:00
parent ce9e06eb31
commit 6c8118fc82
2 changed files with 320 additions and 1 deletions

View File

@@ -0,0 +1,304 @@
<script lang="ts">
/**
* AE_Comp_Site_Config_Editor.svelte
* Specialized UI for managing site.cfg_json settings.
* Supports General, AI, Performance, and IDAA-specific configurations.
*/
import {
Palette, Mail, Brain, Timer,
ShieldCheck, CodeXml, Save,
Plus, Minus, Globe, ExternalLink
} from 'lucide-svelte';
import AE_Comp_Editor_CodeMirror from '$lib/elements/AE_Comp_Editor_CodeMirror.svelte';
import { ae_loc } from '$lib/stores/ae_stores';
interface Props {
cfg_json: any;
on_save?: () => void;
}
let { cfg_json = $bindable({}), on_save }: Props = $props();
// Ensure we have a valid object (handle strings/nulls)
$effect(() => {
if (typeof cfg_json === 'string') {
try {
cfg_json = JSON.parse(cfg_json);
} catch (e) {
cfg_json = {};
}
}
if (!cfg_json) cfg_json = {};
});
// Internal State
let active_tab: 'visuals' | 'email' | 'ai' | 'refresh' | 'idaa' | 'raw' = $state('visuals');
let raw_json_str = $state('');
// Ensure we have a valid object
if (!cfg_json) cfg_json = {};
function add_to_list(key: string) {
if (!cfg_json[key]) cfg_json[key] = [];
const val = prompt('Enter Novi UUID:');
if (val) cfg_json[key].push(val);
}
function remove_from_list(key: string, index: number) {
cfg_json[key].splice(index, 1);
}
// Sync Raw JSON string when entering the tab
$effect(() => {
if (active_tab === 'raw') {
untrack(() => {
raw_json_str = JSON.stringify(cfg_json, null, 2);
});
}
});
// Update cfg_json when raw string changes
$effect(() => {
if (active_tab === 'raw' && raw_json_str) {
try {
const parsed = JSON.parse(raw_json_str);
cfg_json = parsed;
} catch (e) {
// Ignore invalid JSON while typing
}
}
});
</script>
<div class="ae-site-config-editor flex flex-col h-full space-y-4">
<!-- Tab Navigation -->
<div class="flex flex-wrap gap-1 p-1 bg-surface-500/10 rounded-lg max-w-fit">
<button class="btn btn-sm transition-all {active_tab === 'visuals' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => active_tab = 'visuals'}>
<Palette size="1.1em" class="mr-1" /> Visuals
</button>
<button class="btn btn-sm transition-all {active_tab === 'email' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => active_tab = 'email'}>
<Mail size="1.1em" class="mr-1" /> Email
</button>
<button class="btn btn-sm transition-all {active_tab === 'ai' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => active_tab = 'ai'}>
<Brain size="1.1em" class="mr-1" /> AI/LLM
</button>
<button class="btn btn-sm transition-all {active_tab === 'refresh' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => active_tab = 'refresh'}>
<Timer size="1.1em" class="mr-1" /> Refresh
</button>
<button class="btn btn-sm transition-all {active_tab === 'idaa' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => active_tab = 'idaa'}>
<ShieldCheck size="1.1em" class="mr-1" /> IDAA
</button>
<button class="btn btn-sm transition-all {active_tab === 'raw' ? 'variant-filled-primary' : 'variant-soft-surface'}" onclick={() => active_tab = 'raw'}>
<CodeXml size="1.1em" class="mr-1" /> Raw JSON
</button>
</div>
<!-- Scrollable Content Area -->
<div class="grow overflow-y-auto p-1 pr-2 space-y-6 max-h-[60vh]">
{#if active_tab === 'visuals'}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 animate-in fade-in duration-200">
<label class="label">
<span class="text-xs font-bold uppercase opacity-50">Theme Name</span>
<input type="text" bind:value={cfg_json.theme_name} class="input variant-form-material" placeholder="e.g. AE_OSIT_default" />
</label>
<label class="label">
<span class="text-xs font-bold uppercase opacity-50">Theme Mode</span>
<select bind:value={cfg_json.theme_mode} class="select variant-form-material">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="auto">Auto (System)</option>
</select>
</label>
<label class="label md:col-span-2">
<span class="text-xs font-bold uppercase opacity-50">Header Image Path (URL)</span>
<div class="flex gap-2">
<input type="text" bind:value={cfg_json.header_image_path} class="input variant-form-material grow" placeholder="https://..." />
{#if cfg_json.header_image_path}
<a href={cfg_json.header_image_path} target="_blank" class="btn-icon variant-soft-surface"><ExternalLink size="1.2em" /></a>
{/if}
</div>
</label>
</div>
{:else if active_tab === 'email'}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 animate-in fade-in duration-200">
<section class="space-y-4 border-r border-surface-500/10 pr-4">
<h4 class="text-sm font-black text-primary-500">Admin Contact</h4>
<label class="label">
<span class="text-xs font-bold uppercase opacity-50">Admin Name</span>
<input type="text" bind:value={cfg_json.admin_name} class="input variant-form-material" />
</label>
<label class="label">
<span class="text-xs font-bold uppercase opacity-50">Admin Email</span>
<input type="email" bind:value={cfg_json.admin_email} class="input variant-form-material" />
</label>
</section>
<section class="space-y-4">
<h4 class="text-sm font-black text-secondary-500">System (No-Reply)</h4>
<label class="label">
<span class="text-xs font-bold uppercase opacity-50">No-Reply Name</span>
<input type="text" bind:value={cfg_json.noreply_name} class="input variant-form-material" />
</label>
<label class="label">
<span class="text-xs font-bold uppercase opacity-50">No-Reply Email</span>
<input type="email" bind:value={cfg_json.noreply_email} class="input variant-form-material" />
</label>
</section>
</div>
{:else if active_tab === 'ai'}
<div class="space-y-4 animate-in fade-in duration-200">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<label class="label">
<span class="text-xs font-bold uppercase opacity-50">LLM API Base URL</span>
<input type="text" bind:value={cfg_json.llm__api_base_url} class="input variant-form-material" />
</label>
<label class="label">
<span class="text-xs font-bold uppercase opacity-50">LLM Model</span>
<input type="text" bind:value={cfg_json.llm__api_model} class="input variant-form-material" />
</label>
</div>
<label class="label">
<span class="text-xs font-bold uppercase opacity-50">API Token</span>
<input type="password" bind:value={cfg_json.llm__api_token} class="input variant-form-material font-mono" />
</label>
<label class="label">
<span class="text-xs font-bold uppercase opacity-50">System Prompt</span>
<textarea bind:value={cfg_json.llm__system_prompt} class="textarea variant-form-material h-24 text-sm"></textarea>
</label>
<label class="flex items-center space-x-2">
<input type="checkbox" bind:checked={cfg_json.llm__api_dangerous_browser} class="checkbox" />
<span class="text-xs font-bold uppercase opacity-50">Allow Browser Fetch (Dangerously)</span>
</label>
</div>
{:else if active_tab === 'refresh'}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 animate-in fade-in duration-200">
<label class="label">
<span class="text-xs font-bold uppercase opacity-50">Default (Minutes)</span>
<input type="number" bind:value={cfg_json.default_refresh_minutes} class="input variant-form-material" />
</label>
<label class="label">
<span class="text-xs font-bold uppercase opacity-50">Authenticated (Minutes)</span>
<input type="number" bind:value={cfg_json.authenticated_refresh_time} class="input variant-form-material" />
</label>
<label class="label">
<span class="text-xs font-bold uppercase opacity-50">Trusted (Minutes)</span>
<input type="number" bind:value={cfg_json.trusted_refresh_minutes} class="input variant-form-material" />
</label>
<label class="label">
<span class="text-xs font-bold uppercase opacity-50">Manager (Minutes)</span>
<input type="number" bind:value={cfg_json.manager_refresh_minutes} class="input variant-form-material" />
</label>
</div>
{:else if active_tab === 'idaa'}
<div class="space-y-6 animate-in fade-in duration-200">
<!-- Novi API -->
<section class="space-y-4 p-4 bg-surface-500/5 rounded-xl border border-surface-500/10">
<h4 class="text-sm font-black flex items-center gap-2">
<Globe size="1.1em" class="text-primary-500" /> Novi API Connection
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<label class="label">
<span class="text-xs font-bold uppercase opacity-50">Root URL</span>
<input type="text" bind:value={cfg_json.novi_api_root_url} class="input variant-form-material" />
</label>
<label class="label">
<span class="text-xs font-bold uppercase opacity-50">API Key</span>
<input type="password" bind:value={cfg_json.novi_idaa_api_key} class="input variant-form-material font-mono" />
</label>
</div>
</section>
<!-- UUID Lists -->
<section class="grid grid-cols-1 md:grid-cols-2 gap-4">
{#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}
<div class="space-y-2 p-3 bg-surface-500/5 rounded-lg">
<header class="flex justify-between items-center">
<span class="text-[10px] font-black uppercase tracking-wider {list.color}">{list.label}</span>
<button class="btn btn-icon btn-icon-sm variant-soft-primary" onclick={() => add_to_list(list.key)}>
<Plus size="12" />
</button>
</header>
<div class="space-y-1">
{#each cfg_json[list.key] ?? [] as uuid, i}
<div class="flex gap-1 items-center bg-surface-500/10 p-1 rounded font-mono text-[10px]">
<span class="grow truncate">{uuid}</span>
<button class="text-error-500 hover:scale-110 transition-transform" onclick={() => remove_from_list(list.key, i)}>
<Minus size="12" />
</button>
</div>
{/each}
</div>
</div>
{/each}
</section>
<!-- Notifications -->
<section class="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-surface-500/5 rounded-xl">
<div class="space-y-2">
<h4 class="text-[10px] font-black uppercase opacity-50">Bulletin Board</h4>
<label class="flex items-center space-x-2 text-xs">
<input type="checkbox" bind:checked={cfg_json.bb_send_staff_new_email} class="checkbox checkbox-sm" />
<span>Notify Staff (New)</span>
</label>
<label class="flex items-center space-x-2 text-xs">
<input type="checkbox" bind:checked={cfg_json.bb_send_staff_update_email} class="checkbox checkbox-sm" />
<span>Notify Staff (Update)</span>
</label>
<label class="flex items-center space-x-2 text-xs">
<input type="checkbox" bind:checked={cfg_json.bb_send_poster_email} class="checkbox checkbox-sm" />
<span>Notify Poster</span>
</label>
<label class="flex items-center space-x-2 text-xs">
<input type="checkbox" bind:checked={cfg_json.bb_send_commenter_email} class="checkbox checkbox-sm" />
<span>Notify Commenters</span>
</label>
</div>
<div class="space-y-2">
<h4 class="text-[10px] font-black uppercase opacity-50">Recovery Meetings</h4>
<label class="flex items-center space-x-2 text-xs">
<input type="checkbox" bind:checked={cfg_json.recovery_mtg_send_staff_new_email} class="checkbox checkbox-sm" />
<span>Notify Staff (New)</span>
</label>
<label class="flex items-center space-x-2 text-xs">
<input type="checkbox" bind:checked={cfg_json.recovery_mtg_send_staff_update_email} class="checkbox checkbox-sm" />
<span>Notify Staff (Update)</span>
</label>
</div>
</section>
</div>
{:else if active_tab === 'raw'}
<div class="h-[50vh] animate-in fade-in duration-200">
<AE_Comp_Editor_CodeMirror
content={raw_json_str}
bind:new_content={raw_json_str}
language="json"
theme_mode={$ae_loc.theme_mode}
class_li="h-full border border-surface-500/20 rounded-lg shadow-inner"
/>
</div>
{/if}
</div>
<!-- Action Bar -->
<div class="flex justify-between items-center pt-4 border-t border-surface-500/10">
<div class="flex items-center gap-2">
<label class="flex items-center space-x-2 cursor-pointer">
<input type="checkbox" bind:checked={cfg_json.test} class="checkbox" />
<span class="text-xs font-bold uppercase text-warning-500">Test Mode</span>
</label>
</div>
<button class="btn btn-sm variant-filled-primary font-bold shadow-lg" onclick={on_save}>
<Save size="1.1em" class="mr-2" /> Save Config
</button>
</div>
</div>

View File

@@ -14,7 +14,8 @@
import { editable_fields__site_domain } from '$lib/ae_core/ae_core__site_domain.editable_fields';
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
import { goto } from '$app/navigation';
import { Save, Trash2, ArrowLeft, Globe, Plus, ExternalLink, Key, Settings, Activity, Info } from 'lucide-svelte';
import { Save, Trash2, ArrowLeft, Globe, Plus, ExternalLink, Key, Settings, Activity, Info, Database } from 'lucide-svelte';
import AE_Comp_Site_Config_Editor from '$lib/ae_core/ae_comp__site_config_editor.svelte';
let site_id = $page.params.site_id;
let site: any = $state(null);
@@ -56,6 +57,9 @@
}
});
// Ensure cfg_json is included explicitly
data_kv.cfg_json = site.cfg_json;
await update_ae_obj__site({
api_cfg: $ae_api,
site_id,
@@ -157,6 +161,17 @@
</div>
</div>
<!-- Advanced Site JSON Config -->
<div class="card p-6 space-y-4 shadow-xl variant-glass-surface border border-surface-500/10">
<h3 class="h4 font-bold border-b border-surface-500/30 pb-2 flex items-center gap-2">
<Database size={18} class="text-warning-500" /> Site Settings (cfg_json)
</h3>
<AE_Comp_Site_Config_Editor
bind:cfg_json={site.cfg_json}
on_save={handle_save_site}
/>
</div>
<div class="card p-6 space-y-4 shadow-xl variant-glass-surface border border-surface-500/10">
<h3 class="h4 font-bold border-b border-surface-500/30 pb-2 flex items-center gap-2">
<Key size={18} class="text-secondary-500" /> Access Control