Compare commits
5 Commits
a90572bcb8
...
d32355a1a2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d32355a1a2 | ||
|
|
213eabd8c1 | ||
|
|
872291b0a0 | ||
|
|
25d17841e4 | ||
|
|
6282fb167f |
94
documentation/MODULE__AE_Events_Launcher_Config_Menu.md
Normal file
94
documentation/MODULE__AE_Events_Launcher_Config_Menu.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Aether Events — Launcher Configuration Menu (Inventory)
|
||||
|
||||
> **Status:** Current Reference (v3.0)
|
||||
> **Location:** `src/routes/events/[event_id]/(launcher)/launcher_cfg.svelte`
|
||||
|
||||
This document provides a detailed inventory of the Launcher's configuration menu settings as of May 2026. This serves as the baseline for the v3.1 reorganization into a modal-based tabbed interface.
|
||||
|
||||
---
|
||||
|
||||
## 1. UI Architecture & Visibility
|
||||
|
||||
The configuration menu currently resides in a slide-out **Drawer** (sidebar).
|
||||
|
||||
### 1.1 Visibility Modes
|
||||
- **Standard Mode:** Default view for onsite operators. Hides advanced technical and destructive controls.
|
||||
- **Technical Mode (`$ae_loc.edit_mode`):** Toggled via a subtle pencil icon. Reveals advanced diagnostic fields, manual overrides, and debug tools.
|
||||
- **Native Mode (`$ae_loc.is_native`):** Automatically detected when running in the Electron shell. Shows OS-level controls (Filesystem, Power, Apps).
|
||||
|
||||
### 1.2 Section Expansion Logic
|
||||
- **`collapsed`**: Content hidden.
|
||||
- **`auto`**: Expanded by default; collapses when another "auto" section opens.
|
||||
- **`pinned`**: Remains expanded regardless of other interactions.
|
||||
|
||||
---
|
||||
|
||||
## 2. Menu Inventory (Tabbed View)
|
||||
|
||||
### Tab 1: Setup (Onsite Operator Focus)
|
||||
|
||||
| Section | Feature | Technical Mode Only |
|
||||
| :--- | :--- | :--- |
|
||||
| **Display & App Modes** | Session Mode Preset (Oral vs Poster Kiosk) | |
|
||||
| | Operational Env (Web / App / Onsite) | |
|
||||
| | Interface Visibility (Hide Header/Menu/Footer/Times) | |
|
||||
| | Clock Format (12/24 hour) | |
|
||||
| | WebSocket Debugger Toggle | Yes |
|
||||
| | Poster Modal Title Toggle | Yes |
|
||||
| | Native Test Mode (Simulation) | Yes |
|
||||
| **Remote Controller** | WS Connection Status Badge | |
|
||||
| | Controller Strategy (Local / Remote / Local Push) | |
|
||||
| | Connect / Disconnect Action | |
|
||||
| | Group Reload (WS trigger) | |
|
||||
| | Channel Group Code (Locked/Unlockable) | Yes |
|
||||
| **Poster Screen Saver** | Idle Timeout Summary | |
|
||||
| | Timer Overrides (Idle / Cycle / Loop) | Yes |
|
||||
|
||||
### Tab 2: Device (Technical & Native Focus)
|
||||
|
||||
| Section | Feature | Technical Mode Only |
|
||||
| :--- | :--- | :--- |
|
||||
| **Sync Engine & Timers** | Pause / Resume Sync | |
|
||||
| | Force Sync Location (Recursive fetch) | |
|
||||
| | Polling Periods (Event/Device/Loc/Sess/Pres/Presenter) | Yes |
|
||||
| | Cache Hash Prefix Length (1-3 chars) | Yes |
|
||||
| **System & Sync Health** | CPU & RAM Usage Gauges | |
|
||||
| | Heartbeat Status & Timestamp | |
|
||||
| | Sync Progress (Cached vs Total) | |
|
||||
| | Active Sync Filename (Animated) | |
|
||||
| | Hostname & IP List | Yes |
|
||||
| | Raw Device JSON Inspector | Yes |
|
||||
| **Native OS Management** | Open Cache / Temp Folders | |
|
||||
| | Window Control (Maximize / Kiosk) | |
|
||||
| | Display Mode (Extend / Mirror) | |
|
||||
| | Presentation Remote (Prev/Start/Stop/Next) | |
|
||||
| | Reset Wallpaper (Site Header) | Yes |
|
||||
| | Kill Presentation Apps (PowerPoint/Keynote/etc) | Yes |
|
||||
| | Power Actions (Reboot / Shutdown) | Yes |
|
||||
| | Manual Terminal Command Entry | Yes |
|
||||
| **Wallpaper** | Primary Display URL Preset/Input | |
|
||||
| | External Display URL Preset/Input | |
|
||||
| | Save & Apply Wallpaper | |
|
||||
| | Restore macOS Default | |
|
||||
| **Launch Timing** | Per-Profile Post-Open Delay (ms) Overrides | Yes |
|
||||
| **Application Updates** | Update Source (File / URL) | Yes |
|
||||
| | Check for Updates | |
|
||||
| | Install & Relaunch | |
|
||||
|
||||
### Tab 3: Dev (Technical/Developer Focus)
|
||||
|
||||
| Section | Feature | Technical Mode Only |
|
||||
| :--- | :--- | :--- |
|
||||
| **Local Reset & Actions** | Maintenance Select (Wipe IDB / LocalStorage) | Yes |
|
||||
| | Global Sys Menu Toggle | Yes |
|
||||
| | Global Debug Menu Toggle | Yes |
|
||||
| | Cache .tmp Cleanup (Native Only) | Yes |
|
||||
| | API Endpoint & Account ID Summary | Yes |
|
||||
|
||||
---
|
||||
|
||||
## 3. Global Actions (Footer)
|
||||
|
||||
- **Close:** Dismisses the configuration menu.
|
||||
- **Reload:** Performs a full browser `location.reload()`.
|
||||
- **Debug Panel:** Opens the raw state inspector (Technical Mode Only).
|
||||
110
documentation/MODULE__AE_Events_Launcher_Config_Menu_new.md
Normal file
110
documentation/MODULE__AE_Events_Launcher_Config_Menu_new.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Aether Events — Unified Launcher Configuration (Vision v3.1)
|
||||
|
||||
> **Status:** Strategic Design / Unified Proposal
|
||||
> **Author:** Gemini CLI (Interactive Agent)
|
||||
> **Target:** Full consistency across all configuration modules.
|
||||
|
||||
## 1. Unified Design Language
|
||||
|
||||
To eliminate the "created by 3 different people" feel, all components must strictly adhere to this shared specification.
|
||||
|
||||
### 1.1 Color Palette & Semantics
|
||||
- **Primary (Blue):** Main actions, active tabs, and standard configuration toggles.
|
||||
- **Secondary (Green):** Safe actions (Connect, Sync, Apply).
|
||||
- **Warning (Orange):** Technical overrides that require caution (Timers, Native Shell).
|
||||
- **Error (Red):** Destructive actions (Resets, Shutdown, Kill Apps).
|
||||
- **Surface (Gray):** Containers, input backgrounds, and inactive states.
|
||||
|
||||
### 1.2 Typography & Spacing
|
||||
- **Section Headers:** `text-sm font-bold uppercase tracking-tight` (Provided by Wrapper).
|
||||
- **Field Labels:** `text-[10px] font-bold uppercase tracking-wider opacity-60 mb-1`.
|
||||
- **Sub-Descriptions:** `text-[9px] italic opacity-40 leading-snug mt-1`.
|
||||
- **Status Badges:** `text-[8px] font-bold uppercase tracking-tighter`.
|
||||
- **Grid Standard:**
|
||||
* Single Column for complex fields.
|
||||
* `grid-cols-2` with `gap-4` for standard inputs.
|
||||
* `grid-cols-3` or `grid-cols-4` only for small buttons or icon toggles.
|
||||
|
||||
---
|
||||
|
||||
## 2. Structural Reorganization (The "Aether" Layout)
|
||||
|
||||
The menu is now a **Vertical Sidebar Modal**. This allows for persistent navigation while dedicating the large right pane to content.
|
||||
|
||||
### Tab 1: 🖥️ Display (General Operator)
|
||||
*Focus: What the screen looks like.*
|
||||
- **Category: Layout & UI**
|
||||
- Presets: Oral/Default vs Poster Kiosk (One-tap setup).
|
||||
- Toggles: Header, Menu, Footer, Times visibility.
|
||||
- Formatting: Clock (12/24h), Date formats.
|
||||
- **Category: Screen Saver**
|
||||
- Idle Timeout (Minutes).
|
||||
- Mode: Image Cycle vs Video vs Custom.
|
||||
|
||||
### Tab 2: 🔌 Connectivity (Onsite Tech)
|
||||
*Focus: How it talks to the network.*
|
||||
- **Category: WebSocket Control**
|
||||
- Connection Status & Signal Strength.
|
||||
- Controller Mode: Local vs Remote vs Push.
|
||||
- Group Code: Channel sharding for multi-room management.
|
||||
- **Category: API Context**
|
||||
- Current Endpoint, Account, and Site context.
|
||||
|
||||
### Tab 3: 🔄 Sync & Health (Onsite Tech)
|
||||
*Focus: Data integrity and performance.*
|
||||
- **Category: Sync Engine**
|
||||
- Status: Active vs Paused.
|
||||
- Action: Force Sync Location (recursive metadata fetch).
|
||||
- Stats: Cached Files vs Total Files (Progress bar).
|
||||
- **Category: System Telemetry**
|
||||
- CPU & RAM usage (Visual gauges).
|
||||
- Heartbeat monitor (Last success timestamp).
|
||||
- Device Identity: Hostname, IP list, Local paths.
|
||||
|
||||
### Tab 4: 🛠️ Native Shell (Specialized / Mac)
|
||||
*Focus: OS-level capabilities.*
|
||||
- **Category: App Control**
|
||||
- Window: Maximize, Kiosk Mode, Fullscreen.
|
||||
- Automation: Kill presentation apps (Clean slate).
|
||||
- Remote: Virtual clicker (Prev/Next/Start/Stop).
|
||||
- **Category: System Action**
|
||||
- Displays: Extend vs Mirror (Native bridge).
|
||||
- Folders: Open Cache / Open Temp.
|
||||
- Power: Reboot / Shutdown (With confirmation).
|
||||
|
||||
### Tab 5: 🖼️ Wallpaper (Branding)
|
||||
*Focus: Event-specific aesthetics.*
|
||||
- **Category: Customization**
|
||||
- Primary Display: URL/Preset.
|
||||
- Secondary/Projector: URL/Preset.
|
||||
- Action: Apply to OS (Native) + Preview (Web).
|
||||
|
||||
### Tab 6: 🧪 Advanced (Developer Mode)
|
||||
*Focus: Fine-tuning and updates.*
|
||||
- **Category: Performance**
|
||||
- Polling Intervals (Event, Device, Room, Session, Presenter).
|
||||
- Cache Sharding (Prefix length).
|
||||
- **Category: Launch Logic**
|
||||
- Per-Profile Post-Open Delays (ms).
|
||||
- **Category: Updates**
|
||||
- Source: File vs URL.
|
||||
- Version: Current vs Target.
|
||||
- Action: Download/Install.
|
||||
|
||||
### Tab 7: 🧹 Maintenance (Emergency)
|
||||
*Focus: Troubleshooting.*
|
||||
- **Category: Resets**
|
||||
- Wipe IndexedDB (Module selective).
|
||||
- Clear LocalStorage (Reset config).
|
||||
- **Category: Diagnostics**
|
||||
- Raw Device JSON inspector.
|
||||
- Terminal Command Entry.
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation Plan: The "Cohesion" Refactor
|
||||
|
||||
1. **Standardize `Launcher_Cfg_Section.svelte`:** Ensure padding and spacing are baked into the wrapper so children don't have to define it.
|
||||
2. **Create `Launcher_Cfg_Field.svelte`:** A new helper component to handle the Label + Description + Input pattern consistently.
|
||||
3. **Audit Sub-Components:** Update all 10 components to use the new colors, grid patterns, and typography.
|
||||
4. **Polish Transitions:** Ensure the Modal entry and Tab switching are butter-smooth with Svelte 5 transitions.
|
||||
@@ -17,6 +17,7 @@ import { sineIn } from 'svelte/easing';
|
||||
// *** Import other supporting libraries
|
||||
import { liveQuery } from 'dexie';
|
||||
import { Drawer, Modal } from 'flowbite-svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { listen, idle, onIdle, restartCountdown } from 'svelte-idle';
|
||||
|
||||
import {
|
||||
@@ -91,6 +92,45 @@ if (!$events_loc?.launcher) {
|
||||
hide_drawer__debug: true
|
||||
};
|
||||
}
|
||||
// WHY: The initialization block above only runs when launcher is completely absent.
|
||||
// If the user has an older persisted config (from before the Modal migration),
|
||||
// hide_drawer__cfg may be missing → undefined → !undefined = true → modal opens
|
||||
// on every load. Explicitly initialize it here to ensure it is always a boolean.
|
||||
if ($events_loc.launcher.hide_drawer__cfg === undefined) {
|
||||
$events_loc.launcher.hide_drawer__cfg = true;
|
||||
}
|
||||
|
||||
let modal_cfg_open = $state(!$events_loc.launcher.hide_drawer__cfg);
|
||||
|
||||
// Sync store → modal: biohazard button writes hide_drawer__cfg = false to open.
|
||||
// Equality guard prevents spurious writes from unrelated $events_loc updates
|
||||
// (Svelte 4 whole-store subscription fires on every field write to the store).
|
||||
$effect(() => {
|
||||
const should_open = !$events_loc.launcher.hide_drawer__cfg;
|
||||
if (modal_cfg_open !== should_open) {
|
||||
modal_cfg_open = should_open;
|
||||
}
|
||||
});
|
||||
|
||||
// Sync modal → store: use events_loc.update() directly rather than $-syntax so
|
||||
// the write always reaches the persisted store's serialization. $-syntax writes
|
||||
// inside $effect contexts may be suppressed and not trigger localStorage persistence.
|
||||
$effect(() => {
|
||||
const should_hide = !modal_cfg_open;
|
||||
events_loc.update((loc) => {
|
||||
if (loc.launcher) loc.launcher.hide_drawer__cfg = should_hide;
|
||||
return loc;
|
||||
});
|
||||
});
|
||||
|
||||
// Called by backdrop click. Writes to store immediately (don't rely on effect timing).
|
||||
function close_cfg() {
|
||||
modal_cfg_open = false;
|
||||
events_loc.update((loc) => {
|
||||
if (loc.launcher) loc.launcher.hide_drawer__cfg = true;
|
||||
return loc;
|
||||
});
|
||||
}
|
||||
|
||||
// Generate a stable per-device client ID on first load and persist it.
|
||||
// events_loc is backed by svelte-persisted-store (localStorage) so this
|
||||
@@ -898,59 +938,53 @@ $effect(() => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Drawer
|
||||
dismissable={false}
|
||||
onclick={() => ($events_loc.launcher.hide_drawer__cfg = true)}
|
||||
class="w-full border border-gray-300 bg-orange-50 opacity-90 transition-all duration-300 hover:opacity-97 md:w-96 lg:w-[32rem] dark:border-gray-600 dark:bg-slate-800"
|
||||
placement="left"
|
||||
{...{
|
||||
transitionType: 'fly',
|
||||
transitionParams: {
|
||||
x: -520,
|
||||
duration: 200,
|
||||
easing: sineIn
|
||||
}
|
||||
}}
|
||||
bind:hidden={$events_loc.launcher.hide_drawer__cfg}
|
||||
id="sidebar1">
|
||||
<!-- Stop-propagation wrapper: prevents clicks inside the visual panel from
|
||||
bubbling up to the <dialog> element. The onclick on the <Drawer> above
|
||||
is spread through to the native <dialog> by Flowbite, overriding its
|
||||
broken outsideclose detection. With this wrapper, ONLY genuine backdrop
|
||||
clicks (outside the visible panel) reach the dialog and close the drawer. -->
|
||||
{#if modal_cfg_open}
|
||||
<!-- Backdrop: full-screen overlay. Clicking anywhere on it closes the cfg panel. -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||
<div role="presentation" onclick={(e) => e.stopPropagation()}>
|
||||
<Launcher_cfg></Launcher_cfg>
|
||||
|
||||
<hr class="my-2 border-gray-300 dark:border-gray-600" />
|
||||
|
||||
<div
|
||||
role="presentation"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/60 p-4"
|
||||
onclick={close_cfg}
|
||||
transition:fade={{ duration: 200 }}>
|
||||
<!-- Panel: stop-propagation so clicks inside don't reach the backdrop handler. -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="flex max-w-md flex-row flex-wrap items-center justify-center gap-0.5">
|
||||
<a
|
||||
href="/events/{$events_slct.event_id}"
|
||||
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500">
|
||||
<Search size="1em" class="m-1" />
|
||||
Session Search
|
||||
</a>
|
||||
{#if $events_slct?.event_location_id}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Launcher Settings"
|
||||
tabindex="-1"
|
||||
class="flex max-h-[90vh] w-full max-w-5xl flex-col overflow-hidden rounded-lg shadow-2xl"
|
||||
onclick={(e) => e.stopPropagation()}>
|
||||
<Launcher_cfg></Launcher_cfg>
|
||||
|
||||
<div
|
||||
class="bg-surface-100-900 flex flex-row flex-wrap items-center justify-center gap-2 border-t border-surface-500/10 p-4">
|
||||
<a
|
||||
href="/events/{$events_slct.event_id}/location/{$events_slct.event_location_id}"
|
||||
href="/events/{$events_slct.event_id}"
|
||||
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500">
|
||||
<MapPin size="1em" class="m-1" />
|
||||
View Selected Location
|
||||
<Search size="1em" class="m-1" />
|
||||
Session Search
|
||||
</a>
|
||||
{/if}
|
||||
{#if $events_slct?.event_session_id}
|
||||
<a
|
||||
href="/events/{$events_slct.event_id}/session/{$events_slct.event_session_id}"
|
||||
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500">
|
||||
<GraduationCap size="1em" class="m-1" />
|
||||
View Selected Session
|
||||
</a>
|
||||
{/if}
|
||||
{#if $events_slct?.event_location_id}
|
||||
<a
|
||||
href="/events/{$events_slct.event_id}/location/{$events_slct.event_location_id}"
|
||||
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500">
|
||||
<MapPin size="1em" class="m-1" />
|
||||
View Selected Location
|
||||
</a>
|
||||
{/if}
|
||||
{#if $events_slct?.event_session_id}
|
||||
<a
|
||||
href="/events/{$events_slct.event_id}/session/{$events_slct.event_session_id}"
|
||||
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500">
|
||||
<GraduationCap size="1em" class="m-1" />
|
||||
View Selected Session
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
{/if}
|
||||
|
||||
<Drawer
|
||||
activateClickOutside={false}
|
||||
|
||||
@@ -37,19 +37,74 @@ import Launcher_Cfg_Wallpaper from './cfg_components/launcher_cfg_wallpaper.svel
|
||||
import {
|
||||
Bug,
|
||||
Code,
|
||||
Gamepad2,
|
||||
Image,
|
||||
LayoutGrid,
|
||||
Monitor,
|
||||
Pencil,
|
||||
RefreshCw,
|
||||
Settings,
|
||||
SlidersHorizontal,
|
||||
Wrench,
|
||||
X
|
||||
} from '@lucide/svelte';
|
||||
|
||||
// UI Tab State
|
||||
// Tabs are audience-oriented:
|
||||
// setup — what every onsite operator needs (mode preset, display, WS, screen saver)
|
||||
// device — sync engine (all devices) + native/Electron OS controls (native or edit_mode)
|
||||
// dev — developer/debug tools; only useful when edit_mode is on
|
||||
let active_tab: 'setup' | 'device' | 'dev' = $state('setup');
|
||||
type TabId =
|
||||
| 'general'
|
||||
| 'connectivity'
|
||||
| 'sync'
|
||||
| 'native'
|
||||
| 'wallpaper'
|
||||
| 'advanced'
|
||||
| 'maintenance';
|
||||
let active_tab: TabId = $state('general');
|
||||
|
||||
const TABS = [
|
||||
{
|
||||
id: 'general' as TabId,
|
||||
label: 'General',
|
||||
icon: LayoutGrid,
|
||||
title: 'App Modes, UI & Screen Saver'
|
||||
},
|
||||
{
|
||||
id: 'connectivity' as TabId,
|
||||
label: 'Connectivity',
|
||||
icon: Gamepad2,
|
||||
title: 'Remote Controller & WebSocket'
|
||||
},
|
||||
{
|
||||
id: 'sync' as TabId,
|
||||
label: 'Sync & Health',
|
||||
icon: RefreshCw,
|
||||
title: 'Sync Engine & System Status'
|
||||
},
|
||||
{
|
||||
id: 'native' as TabId,
|
||||
label: 'Native OS',
|
||||
icon: Monitor,
|
||||
title: 'Folders, Displays & Native Apps'
|
||||
},
|
||||
{
|
||||
id: 'wallpaper' as TabId,
|
||||
label: 'Wallpaper',
|
||||
icon: Image,
|
||||
title: 'Desktop Wallpaper Settings'
|
||||
},
|
||||
{
|
||||
id: 'advanced' as TabId,
|
||||
label: 'Advanced',
|
||||
icon: Code,
|
||||
title: 'Launch Timing & Updates',
|
||||
edit_only: true
|
||||
},
|
||||
{
|
||||
id: 'maintenance' as TabId,
|
||||
label: 'Maintenance',
|
||||
icon: Wrench,
|
||||
title: 'Local Resets & Debug Tools',
|
||||
edit_only: true
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Auto-Collapse Coordinator
|
||||
@@ -70,185 +125,210 @@ function handle_section_expand(current_key: string) {
|
||||
});
|
||||
$events_loc.launcher = launcher; // Trigger store update
|
||||
}
|
||||
|
||||
const current_tab_info = $derived(TABS.find((t) => t.id === active_tab));
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="
|
||||
flex w-full
|
||||
max-w-full flex-col items-center justify-start gap-4
|
||||
">
|
||||
class="bg-surface-100-900 flex h-full min-h-[500px] w-full flex-row overflow-hidden">
|
||||
<!-- Sidebar Navigation -->
|
||||
<div
|
||||
class="border-surface-500/20 flex w-full flex-row items-center justify-between border-b pb-2">
|
||||
<h2
|
||||
class="text-center text-lg font-bold text-gray-700 dark:text-gray-200">
|
||||
<Settings size="1em" class="mr-2 opacity-50" />
|
||||
Launcher Configuration
|
||||
</h2>
|
||||
class="bg-surface-500/5 flex w-48 flex-shrink-0 flex-col border-r border-surface-500/20 p-2">
|
||||
<div class="mb-4 flex items-center gap-2 px-2 py-3">
|
||||
<Settings size="1.2em" class="text-primary-500" />
|
||||
<span class="text-xs font-bold tracking-widest uppercase opacity-80"
|
||||
>Launcher</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- Subtle Edit Mode toggle — intentionally low-key so kiosk operators
|
||||
don't stumble on it, but accessible for setup/admin use.
|
||||
Glows primary when active; nearly invisible when off. -->
|
||||
<nav class="flex flex-grow flex-col gap-1">
|
||||
{#each TABS as tab}
|
||||
{#if !tab.edit_only || $ae_loc.edit_mode}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (active_tab = tab.id)}
|
||||
class="btn btn-sm justify-start gap-3 px-3 py-2 text-[11px] font-medium transition-all"
|
||||
class:preset-filled-primary={active_tab === tab.id}
|
||||
class:preset-tonal-surface={active_tab !== tab.id}
|
||||
class:hover:preset-tonal-primary={active_tab !==
|
||||
tab.id}>
|
||||
<tab.icon size="1.1em" class="shrink-0" />
|
||||
{tab.label}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<div class="mt-auto flex flex-col gap-2 p-1">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => ($ae_loc.edit_mode = !$ae_loc.edit_mode)}
|
||||
class="btn btn-icon transition-all duration-300"
|
||||
class="btn btn-sm justify-start gap-3 px-3 py-2 text-[10px] font-medium transition-all"
|
||||
class:text-primary-500={$ae_loc.edit_mode}
|
||||
class:opacity-20={!$ae_loc.edit_mode}
|
||||
class:hover:opacity-60={!$ae_loc.edit_mode}
|
||||
title="{$ae_loc.edit_mode ? 'Disable' : 'Enable'} Edit Mode">
|
||||
<Pencil size="0.75em" />
|
||||
<span class="sr-only">Toggle Edit Mode</span>
|
||||
class:opacity-50={!$ae_loc.edit_mode}
|
||||
title="{$ae_loc.edit_mode ? 'Disable' : 'Enable'} Technical Mode">
|
||||
<Pencil size="1em" />
|
||||
<span>Technical Mode</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => ($events_loc.launcher.hide_drawer__cfg = true)}
|
||||
class="btn btn-icon hover:bg-surface-500/10 transition-colors dark:text-white">
|
||||
class="btn btn-sm preset-tonal-surface hover:preset-filled-surface-500 justify-start gap-3 px-3 py-2 text-[10px] transition-all">
|
||||
<X size="1em" />
|
||||
<span class="sr-only">Close Config</span>
|
||||
<span>Close Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Tabs -->
|
||||
<!-- Dev tab is only shown when Edit Mode is active — keeps the UI uncluttered
|
||||
for onsite operators who never need those tools. Edit Mode is toggled via
|
||||
the pencil icon in the header above. -->
|
||||
<div
|
||||
class="bg-surface-500/10 w-full gap-1 rounded-lg p-1"
|
||||
class:grid={true}
|
||||
class:grid-cols-2={!$ae_loc.edit_mode}
|
||||
class:grid-cols-3={$ae_loc.edit_mode}>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (active_tab = 'setup')}
|
||||
class="btn btn-sm text-[10px] font-bold uppercase transition-all"
|
||||
class:preset-filled-primary={active_tab === 'setup'}
|
||||
class:preset-tonal-surface={active_tab !== 'setup'}
|
||||
title="Display presets, interface toggles, WS controller, screen saver">
|
||||
<SlidersHorizontal size="0.85em" class="mr-1" /> Setup
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (active_tab = 'device')}
|
||||
class="btn btn-sm text-[10px] font-bold uppercase transition-all"
|
||||
class:preset-filled-primary={active_tab === 'device'}
|
||||
class:preset-tonal-surface={active_tab !== 'device'}
|
||||
title="Sync engine, device health & native OS controls">
|
||||
<Monitor size="0.85em" class="mr-1" /> Device
|
||||
</button>
|
||||
{#if $ae_loc.edit_mode}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (active_tab = 'dev')}
|
||||
class="btn btn-sm text-[10px] font-bold uppercase transition-all"
|
||||
class:preset-filled-warning={active_tab === 'dev'}
|
||||
class:preset-tonal-surface={active_tab !== 'dev'}
|
||||
title="Developer & debug tools">
|
||||
<Code size="0.85em" class="mr-1" /> Dev
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="flex min-h-100 w-full flex-col gap-2">
|
||||
<!-- SETUP: everything onsite operators need day-to-day -->
|
||||
{#if active_tab === 'setup'}
|
||||
<div
|
||||
class="animate-in fade-in slide-in-from-left-2 flex flex-col gap-2 duration-300">
|
||||
<!-- Mode preset is the #1 onsite action — give it prominent placement -->
|
||||
<Launcher_Cfg_App_Modes
|
||||
on_expand={() => handle_section_expand('app_modes')} />
|
||||
<Launcher_Cfg_Controller
|
||||
on_expand={() => handle_section_expand('controller')} />
|
||||
<Launcher_Cfg_Screen_Saver
|
||||
on_expand={() => handle_section_expand('screen_saver')} />
|
||||
<!-- Main Content Area -->
|
||||
<div class="flex flex-grow flex-col overflow-hidden">
|
||||
<!-- Tab Header -->
|
||||
<header
|
||||
class="border-surface-500/10 flex flex-shrink-0 items-center justify-between border-b px-6 py-4">
|
||||
<div class="flex flex-col">
|
||||
<h2 class="text-lg font-bold text-gray-800 dark:text-gray-100">
|
||||
{current_tab_info?.label}
|
||||
</h2>
|
||||
<p class="text-[10px] opacity-50">{current_tab_info?.title}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- DEVICE: sync engine first (all devices) + native OS controls (native or edit_mode preview) -->
|
||||
{#if active_tab === 'device'}
|
||||
<div
|
||||
class="animate-in fade-in slide-in-from-bottom-2 flex flex-col gap-2 duration-300">
|
||||
<!-- Sync pause/timers — relevant to every device, not just native -->
|
||||
<Launcher_Cfg_Sync_Timers
|
||||
on_expand={() => handle_section_expand('sync_timers')} />
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => location.reload()}
|
||||
class="btn btn-sm preset-tonal-surface hover:preset-tonal-secondary text-[10px] font-bold"
|
||||
title="Reload Application">
|
||||
<RefreshCw size="1em" class="mr-1" /> Reload
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Native sections: always in Electron; visible in edit_mode for dev preview.
|
||||
electron_relay.ts guards all calls — safe to import/render without Electron. -->
|
||||
{#if $ae_loc.is_native || $ae_loc.edit_mode}
|
||||
<Launcher_Cfg_Health
|
||||
on_expand={() => handle_section_expand('health')} />
|
||||
<Launcher_Cfg_Native_OS
|
||||
on_expand={() => handle_section_expand('native_os')} />
|
||||
<Launcher_Cfg_Wallpaper
|
||||
on_expand={() => handle_section_expand('wallpaper')} />
|
||||
<Launcher_Cfg_Launch_Timing
|
||||
on_expand={() => handle_section_expand('launch_timing')} />
|
||||
{#if $ae_loc.is_native}
|
||||
<Launcher_Cfg_Updates
|
||||
on_expand={() =>
|
||||
handle_section_expand('updates')} />
|
||||
<div class="flex-grow overflow-y-auto p-6">
|
||||
{#key active_tab}
|
||||
<div class="mx-auto flex w-full max-w-2xl flex-col gap-6">
|
||||
{#if active_tab === 'general'}
|
||||
<div
|
||||
class="animate-in fade-in slide-in-from-bottom-2 flex flex-col gap-6 duration-300">
|
||||
<Launcher_Cfg_App_Modes
|
||||
on_expand={() =>
|
||||
handle_section_expand('app_modes')} />
|
||||
<Launcher_Cfg_Screen_Saver
|
||||
on_expand={() =>
|
||||
handle_section_expand('screen_saver')} />
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div
|
||||
class="flex flex-col items-center gap-1 py-3 text-center text-xs italic opacity-40">
|
||||
<Monitor size="1.2em" class="opacity-30" />
|
||||
<p>Native OS controls available in Aether Desktop.</p>
|
||||
<p class="text-[9px]">Enable Edit Mode to preview.</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- DEV: developer/debug tools — only reachable when Edit Mode is on -->
|
||||
{#if active_tab === 'dev' && $ae_loc.edit_mode}
|
||||
<div
|
||||
class="animate-in fade-in slide-in-from-right-2 flex flex-col gap-2 duration-300">
|
||||
<Launcher_Cfg_Local_Actions
|
||||
on_expand={() => handle_section_expand('local_actions')} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if active_tab === 'connectivity'}
|
||||
<div
|
||||
class="animate-in fade-in slide-in-from-bottom-2 flex flex-col gap-6 duration-300">
|
||||
<Launcher_Cfg_Controller
|
||||
on_expand={() =>
|
||||
handle_section_expand('controller')} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Global Actions Footer -->
|
||||
<div
|
||||
class="border-surface-500/20 mt-auto flex w-full flex-col gap-2 border-t pt-4">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<!-- Close button — always visible in lower-left as a second dismissal point.
|
||||
Useful in kiosk/iframe mode where the top-right close btn may scroll out of view. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => ($events_loc.launcher.hide_drawer__cfg = true)}
|
||||
class="btn btn-sm preset-tonal-surface hover:preset-filled-surface-500 transition-all">
|
||||
<X size="0.85em" class="mr-1" />
|
||||
Close
|
||||
</button>
|
||||
{#if active_tab === 'sync'}
|
||||
<div
|
||||
class="animate-in fade-in slide-in-from-bottom-2 flex flex-col gap-6 duration-300">
|
||||
<Launcher_Cfg_Sync_Timers
|
||||
on_expand={() =>
|
||||
handle_section_expand('sync_timers')} />
|
||||
<Launcher_Cfg_Health
|
||||
on_expand={() =>
|
||||
handle_section_expand('health')} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => location.reload()}
|
||||
class="btn btn-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition-all">
|
||||
<RefreshCw size="0.85em" class="mr-1" />
|
||||
Reload
|
||||
</button>
|
||||
{#if active_tab === 'native'}
|
||||
<div
|
||||
class="animate-in fade-in slide-in-from-bottom-2 flex flex-col gap-6 duration-300">
|
||||
{#if $ae_loc.is_native || $ae_loc.edit_mode}
|
||||
<Launcher_Cfg_Native_OS
|
||||
on_expand={() =>
|
||||
handle_section_expand('native_os')} />
|
||||
{:else}
|
||||
<div
|
||||
class="flex flex-col items-center gap-3 py-12 text-center text-sm italic opacity-40">
|
||||
<Monitor size="3em" class="opacity-20" />
|
||||
<div class="flex flex-col gap-1">
|
||||
<p>
|
||||
Native OS controls are only
|
||||
available when running in Aether
|
||||
Desktop.
|
||||
</p>
|
||||
<p class="text-[10px]">
|
||||
Enable Technical Mode to preview
|
||||
layout.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if active_tab === 'wallpaper'}
|
||||
<div
|
||||
class="animate-in fade-in slide-in-from-bottom-2 flex flex-col gap-6 duration-300">
|
||||
<Launcher_Cfg_Wallpaper
|
||||
on_expand={() =>
|
||||
handle_section_expand('wallpaper')} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if active_tab === 'advanced' && $ae_loc.edit_mode}
|
||||
<div
|
||||
class="animate-in fade-in slide-in-from-bottom-2 flex flex-col gap-6 duration-300">
|
||||
<Launcher_Cfg_Launch_Timing
|
||||
on_expand={() =>
|
||||
handle_section_expand('launch_timing')} />
|
||||
{#if $ae_loc.is_native || $ae_loc.edit_mode}
|
||||
<Launcher_Cfg_Updates
|
||||
on_expand={() =>
|
||||
handle_section_expand('updates')} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if active_tab === 'maintenance' && $ae_loc.edit_mode}
|
||||
<div
|
||||
class="animate-in fade-in slide-in-from-bottom-2 flex flex-col gap-6 duration-300">
|
||||
<Launcher_Cfg_Local_Actions
|
||||
on_expand={() =>
|
||||
handle_section_expand('local_actions')} />
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<p
|
||||
class="ml-1 text-[9px] font-bold uppercase opacity-50">
|
||||
Debug Access
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() =>
|
||||
($events_loc.launcher.hide_drawer__debug = false)}
|
||||
class="btn btn-sm preset-tonal-warning hover:preset-filled-warning-500 w-full transition-all">
|
||||
<Bug size="1.2em" class="mr-2" />
|
||||
Open Debug Panel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
{#if $ae_loc.edit_mode}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() =>
|
||||
($events_loc.launcher.hide_drawer__debug = false)}
|
||||
class="btn btn-sm preset-tonal-warning hover:preset-filled-warning-500 w-full transition-all">
|
||||
<Bug size="0.85em" class="mr-1" />
|
||||
Debug Panel
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<p
|
||||
class="mt-2 text-center text-[9px] font-bold tracking-widest uppercase opacity-40">
|
||||
Aether Platform • Events Launcher v3.0
|
||||
</p>
|
||||
<!-- Content Footer -->
|
||||
<footer
|
||||
class="border-surface-500/10 flex flex-shrink-0 items-center justify-between border-t px-6 py-3">
|
||||
<p class="text-[9px] font-bold tracking-widest uppercase opacity-30">
|
||||
Aether Platform • Events Launcher v3.1
|
||||
</p>
|
||||
<div class="flex items-center gap-4 text-[9px] opacity-40">
|
||||
<span>Account: {$ae_loc.account_id || 'None'}</span>
|
||||
<span
|
||||
>Device: {$ae_loc.native_device?.event_device_id ||
|
||||
'Web'}</span
|
||||
>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,10 +50,17 @@ import {
|
||||
Users
|
||||
} from '@lucide/svelte';
|
||||
// Event Session (Main View Trigger)
|
||||
// WHY: We use a simple derived observable. The template handles the $ prefix.
|
||||
let lq__event_session_obj = $derived(
|
||||
liveQuery(() => db_events.session.get(slct__event_session_id))
|
||||
);
|
||||
// WHY: $derived.by captures the id in the outer closure so Svelte tracks it as a
|
||||
// reactive dependency and recreates the Observable when slct__event_session_id changes.
|
||||
// Plain $derived(liveQuery(...)) does NOT work here: liveQuery's async callback runs in
|
||||
// Dexie's zone where Svelte tracking is off, so $derived never sees the id read and never
|
||||
// recreates the Observable when the session changes. Dexie's range-level change tracking
|
||||
// then keeps the stale Observable alive — it only re-fires when data in the originally
|
||||
// observed key range changes, not when a different session's data arrives.
|
||||
let lq__event_session_obj = $derived.by(() => {
|
||||
const id = slct__event_session_id;
|
||||
return liveQuery(() => db_events.session.get(id));
|
||||
});
|
||||
|
||||
// WHY: type_code drives poster vs. oral UI branching throughout this component.
|
||||
// It was previously a prop that was never passed by the parent, so all poster
|
||||
@@ -62,65 +69,63 @@ let lq__event_session_obj = $derived(
|
||||
let type_code = $derived($lq__event_session_obj?.type_code ?? '');
|
||||
|
||||
// Event File (for a Session)
|
||||
// WHY: Pure data retrieval. Side effects (updating global stores) are removed
|
||||
// to prevent circular reactivity loops during rapid navigation.
|
||||
let lq__event_file_obj_li = $derived(
|
||||
liveQuery(async () => {
|
||||
if (!slct__event_session_id) return [];
|
||||
// WHY: $derived.by — same reason as lq__event_session_obj above. Without recreating
|
||||
// the Observable when slct__event_session_id changes, Dexie never re-fires for the new
|
||||
// session's files (they land in a different for_id range than what was originally observed).
|
||||
let lq__event_file_obj_li = $derived.by(() => {
|
||||
const id = slct__event_session_id;
|
||||
return liveQuery(async () => {
|
||||
if (!id) return [];
|
||||
|
||||
if (log_lvl > 1) {
|
||||
console.log(
|
||||
`[LQ] Fetching files for session: ${slct__event_session_id}`
|
||||
);
|
||||
console.log(`[LQ] Fetching files for session: ${id}`);
|
||||
}
|
||||
|
||||
return await db_events.file
|
||||
.where('for_id')
|
||||
.equals(slct__event_session_id)
|
||||
.equals(id)
|
||||
.reverse()
|
||||
.sortBy('created_on');
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Event Presentation
|
||||
let lq__event_presentation_obj_li = $derived(
|
||||
liveQuery(async () => {
|
||||
if (!slct__event_session_id) return [];
|
||||
// WHY: $derived.by — same reason as above. Captures both id and sort_by in the outer
|
||||
// closure so a new Observable is created whenever the session or its type changes.
|
||||
let lq__event_presentation_obj_li = $derived.by(() => {
|
||||
const id = slct__event_session_id;
|
||||
const sort_by = type_code == 'poster' ? 'name' : 'start_datetime';
|
||||
return liveQuery(async () => {
|
||||
if (!id) return [];
|
||||
|
||||
if (log_lvl > 1) {
|
||||
console.log(
|
||||
`[LQ] Fetching presentations for session: ${slct__event_session_id}`
|
||||
);
|
||||
console.log(`[LQ] Fetching presentations for session: ${id}`);
|
||||
}
|
||||
|
||||
let sort_by = 'start_datetime';
|
||||
if (type_code == 'poster') {
|
||||
sort_by = 'name';
|
||||
}
|
||||
return await db_events.presentation
|
||||
.where('event_session_id')
|
||||
.equals(slct__event_session_id)
|
||||
.equals(id)
|
||||
.sortBy(sort_by);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Event Presenter
|
||||
let lq__event_presenter_obj_li = $derived(
|
||||
liveQuery(async () => {
|
||||
if (!slct__event_session_id) return [];
|
||||
// WHY: $derived.by — same reason as above.
|
||||
let lq__event_presenter_obj_li = $derived.by(() => {
|
||||
const id = slct__event_session_id;
|
||||
return liveQuery(async () => {
|
||||
if (!id) return [];
|
||||
|
||||
if (log_lvl > 1) {
|
||||
console.log(
|
||||
`[LQ] Fetching presenters for session: ${slct__event_session_id}`
|
||||
);
|
||||
console.log(`[LQ] Fetching presenters for session: ${id}`);
|
||||
}
|
||||
|
||||
return await db_events.presenter
|
||||
.where('event_session_id')
|
||||
.equals(slct__event_session_id)
|
||||
.equals(id)
|
||||
.sortBy('full_name');
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// let show_modal_upload_files: boolean = false;
|
||||
// let link_to_type: null|string = null;
|
||||
|
||||
Reference in New Issue
Block a user