5 Commits

Author SHA1 Message Date
Scott Idem
d32355a1a2 docs(launcher): add cfg menu inventory and v3.1 design docs
MODULE__AE_Events_Launcher_Config_Menu.md — v3.0 inventory of the
3-tab drawer layout (now superseded but kept as reference baseline).
MODULE__AE_Events_Launcher_Config_Menu_new.md — v3.1 unified design
spec that drove the sidebar tab migration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:17:07 -04:00
Scott Idem
213eabd8c1 feat(launcher): migrate cfg menu to 7-tab sidebar layout (v3.1)
Replaces the 3-tab horizontal bar (Setup / Device / Dev) with a vertical
sidebar navigation matching the v3.1 design spec. New tab structure:

  General       — App Modes, Screen Saver (operator-facing)
  Connectivity  — Remote Controller & WebSocket
  Sync & Health — Sync Timers, System Health
  Native OS     — OS controls (native or edit_mode preview)
  Wallpaper     — Desktop wallpaper settings
  Advanced      — Launch Timing, Updates (edit_mode only)
  Maintenance   — Local Resets, Debug Panel (edit_mode only)

Layout changes:
- Sidebar nav (w-48) + scrollable main content area replace inline tab bar
- Tab header shows label + description subtitle
- Technical Mode toggle is now a labeled button (not hidden icon)
- Footer shows Account/Device context; Reload moved to header
- {#key active_tab} wrapper ensures clean component remount on tab switch
- Remove unused icons (SlidersHorizontal, HeartPulse, Timer, CloudDownload)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:14:52 -04:00
Scott Idem
872291b0a0 fix(launcher): replace Flowbite Modal with custom overlay for cfg panel
Two problems with the Flowbite <Modal> approach:
1. Built-in dismissable CloseButton rendered with no functional dismiss path
   (no title/form), appearing centered in the panel.
2. size="xl" (max-w-7xl) left no backdrop area on typical laptop screens,
   making outsideclose impossible to trigger.

Replace with a simple custom overlay: full-screen backdrop div that closes
on click, inner panel with stopPropagation. Matches the original Drawer
pattern. close_cfg() writes to store immediately on backdrop click for
reliable persistence independent of effect timing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:04:34 -04:00
Scott Idem
25d17841e4 fix(launcher): fix cfg modal default-open and outside-click persistence
Two bugs in the Launcher Config Modal after the Drawer→Modal migration:
1. Pre-existing persisted configs (missing hide_drawer__cfg field) caused
   !undefined = true, opening the modal on every fresh load. Fixed by adding
   a field-level initialization guard after the full-object guard.
2. $-syntax writes inside untrack() were suppressed by svelte-persisted-store,
   so outside-click closure was never persisted. Fixed by using events_loc.update()
   directly to ensure the write reaches localStorage serialization. Added equality
   guard to effect 1 to prevent spurious modal flicker from whole-store re-fires.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:36:01 -04:00
Scott Idem
6282fb167f fix(launcher): use $derived.by for session liveQueries to fix stale presentation/presenter data
When switching sessions within the same location, presentations and presenters
were not updating. The root cause: plain $derived(liveQuery(...)) never recreates
the Observable when slct__event_session_id changes, because liveQuery's async
callback runs in Dexie's zone where Svelte tracking is off. Dexie's range-level
change detection then ignores new session data (it arrives under a different
event_session_id index value, outside the originally observed range).

Replaced all four liveQuery declarations with $derived.by(() => { const id = ...;
return liveQuery(...id...); }) — the same pattern already used in +layout.svelte
for location-dependent queries. Svelte tracks the id read in the outer closure
and recreates the Observable on every session change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 19:55:17 -04:00
5 changed files with 564 additions and 241 deletions

View 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).

View 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.

View File

@@ -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}

View File

@@ -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 &amp; 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 &amp; 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 &bull; 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 &bull; 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>

View File

@@ -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;