feat: add AE_Comp_Site_Config_Editor for managed site configuration
This commit is contained in:
304
src/lib/ae_core/ae_comp__site_config_editor.svelte
Normal file
304
src/lib/ae_core/ae_comp__site_config_editor.svelte
Normal 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>
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user