Compare commits
190 Commits
17b549a75c
...
ae_app_3x_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c148206ac | ||
|
|
3fc0a33044 | ||
|
|
7ce7a9b429 | ||
|
|
5b1c8ed632 | ||
|
|
ed8e7e57c0 | ||
|
|
50d346a36c | ||
|
|
d23f9073c4 | ||
|
|
8146316aaf | ||
|
|
39b23002d9 | ||
|
|
41555cb717 | ||
|
|
7e97928e05 | ||
|
|
528fb9b33f | ||
|
|
49e3fb18b2 | ||
|
|
24dcc04c9a | ||
|
|
b94301336c | ||
|
|
fc6eb01137 | ||
|
|
5321eb0d70 | ||
|
|
fa179eb7f4 | ||
|
|
8f0dbf3d1d | ||
|
|
ba2558fbf7 | ||
|
|
cbc94babe0 | ||
|
|
af2236eea4 | ||
|
|
e5c141e765 | ||
|
|
8eb9444edf | ||
|
|
a37866e5bb | ||
|
|
2e4547f433 | ||
|
|
d75c4bbb9f | ||
|
|
7335721f6d | ||
|
|
2364c0edfb | ||
|
|
a47a2103eb | ||
|
|
0954369ef0 | ||
|
|
2f2e9d554a | ||
|
|
d193004882 | ||
|
|
8b1bf64740 | ||
|
|
4c210e3e36 | ||
|
|
a784395d7b | ||
|
|
06f52bae34 | ||
|
|
affbd0dc20 | ||
|
|
28025df038 | ||
|
|
01b1729a67 | ||
|
|
b90daa0a43 | ||
|
|
37c8e20302 | ||
|
|
6ab4166da0 | ||
|
|
04ae723309 | ||
|
|
b6d162c66e | ||
|
|
fa30acf31c | ||
|
|
03b3c84921 | ||
|
|
e89c982022 | ||
|
|
c6ef729c55 | ||
|
|
fd7ccd7ecc | ||
|
|
7831179970 | ||
|
|
75e7ca541a | ||
|
|
e6fb4b289f | ||
|
|
1e3f541a39 | ||
|
|
e966261324 | ||
|
|
67d2607da1 | ||
|
|
7192cbc0af | ||
|
|
a227c6aaa7 | ||
|
|
e05602b87b | ||
|
|
10f7f04fbc | ||
|
|
94bdeb9a26 | ||
|
|
080ad06a45 | ||
|
|
45f8bb5e58 | ||
|
|
3085d1dc63 | ||
|
|
7fc073053b | ||
|
|
582b43da34 | ||
|
|
98e31f1528 | ||
|
|
573d20e574 | ||
|
|
83c9b9fd4f | ||
|
|
27c775d816 | ||
|
|
5823f18161 | ||
|
|
94e4fad061 | ||
|
|
9a1ba02b59 | ||
|
|
05841350fe | ||
|
|
a5beff4aa8 | ||
|
|
246d4f8ef3 | ||
|
|
666b54bd36 | ||
|
|
89c1decf8d | ||
|
|
b9d70b616f | ||
|
|
e8a49562a9 | ||
|
|
e909c34874 | ||
|
|
48bc52899f | ||
|
|
6b122a065e | ||
|
|
1b81b8873c | ||
|
|
4f74cf1353 | ||
|
|
6dc6be9926 | ||
|
|
97c4c1cd6b | ||
|
|
b6481a3507 | ||
|
|
7b45b548e4 | ||
|
|
a1057fd776 | ||
|
|
d0286f7868 | ||
|
|
1c541cd090 | ||
|
|
868b4017f2 | ||
|
|
3122725610 | ||
|
|
6e04145514 | ||
|
|
b8ceed69d0 | ||
|
|
71aacb6346 | ||
|
|
04f3b82d59 | ||
|
|
cc04411d23 | ||
|
|
55f3e3a5a4 | ||
|
|
955d28d9c5 | ||
|
|
7c5cf53106 | ||
|
|
24b52b8027 | ||
|
|
6b3fb36926 | ||
|
|
5cad150b0a | ||
|
|
8a41f02f0d | ||
|
|
88ab5b27d4 | ||
|
|
b623557795 | ||
|
|
7d2b30b7ce | ||
|
|
c9b0acfa06 | ||
|
|
29a24812f4 | ||
|
|
70fda25c95 | ||
|
|
8f815b7033 | ||
|
|
ba4a0dc828 | ||
|
|
35c1324824 | ||
|
|
1a53a20995 | ||
|
|
04c2042060 | ||
|
|
4831f4b81b | ||
|
|
7bf76bf766 | ||
|
|
3ae5b30c37 | ||
|
|
b04202ecec | ||
|
|
84c4a2aa43 | ||
|
|
399f98ce8e | ||
|
|
f5ccd2e3cf | ||
|
|
94a3cb0644 | ||
|
|
9d904446d4 | ||
|
|
b45a27481a | ||
|
|
26ab5dda75 | ||
|
|
0511d9591f | ||
|
|
59fc7cabc6 | ||
|
|
41b352bc0a | ||
|
|
a4fed750fa | ||
|
|
74e0f752a6 | ||
|
|
be3e56eece | ||
|
|
e2d3c5a822 | ||
|
|
1b6aeb5b02 | ||
|
|
04018a27ed | ||
|
|
4292aebc56 | ||
|
|
3466d6552c | ||
|
|
b7969bc46e | ||
|
|
99b8eb0b5e | ||
|
|
9e361eae9b | ||
|
|
e735d0c213 | ||
|
|
d05cc63459 | ||
|
|
ac17417f3c | ||
|
|
3773758eb5 | ||
|
|
60bdd2fdba | ||
|
|
72c8f9b502 | ||
|
|
1de87b6c5f | ||
|
|
84a9d0fffc | ||
|
|
87084f0f71 | ||
|
|
de048a084b | ||
|
|
ee79e33a2a | ||
|
|
a5243fa820 | ||
|
|
5fce149808 | ||
|
|
a74effa6ff | ||
|
|
33d48e7e78 | ||
|
|
65e48c764e | ||
|
|
f6c950abdf | ||
|
|
3d6f9035c8 | ||
|
|
535efd9c4b | ||
|
|
4a39ca1468 | ||
|
|
182a066d38 | ||
|
|
35fed53e2a | ||
|
|
322abc2691 | ||
|
|
cfaf687717 | ||
|
|
e7620a1c06 | ||
|
|
e4e2174c97 | ||
|
|
d3bf314c62 | ||
|
|
d32355a1a2 | ||
|
|
213eabd8c1 | ||
|
|
872291b0a0 | ||
|
|
25d17841e4 | ||
|
|
6282fb167f | ||
|
|
a90572bcb8 | ||
|
|
194c89f6d1 | ||
|
|
469729ce22 | ||
|
|
d1f5d0e2fd | ||
|
|
9c83567430 | ||
|
|
b4d0d82141 | ||
|
|
15bfe6d5d6 | ||
|
|
dddf4b6170 | ||
|
|
587b815446 | ||
|
|
ca51a82dae | ||
|
|
a38320c7f5 | ||
|
|
c76fb8f2b5 | ||
|
|
a26ea8b49c | ||
|
|
21fad1a698 | ||
|
|
33e9eeef78 | ||
|
|
172ea994c7 |
33
conductor/fix-idaa-breakout-links.md
Normal file
33
conductor/fix-idaa-breakout-links.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Plan: Fix IDAA Jitsi Breakout Links
|
||||
|
||||
IDAA Jitsi meetings are embedded in an iframe on the `idaa.org` website. To allow members to "break out" of the iframe (for a better experience on mobile or to use full-tab features), the app provides an "Open Meeting Externally" link.
|
||||
|
||||
Currently, this link is generated from `$page.url.href`, which often lacks the `key` (site access key) and `uuid` (Novi identity token) required for Aether's authentication gate, especially if the user has navigated internally within SvelteKit.
|
||||
|
||||
## 1. Objective
|
||||
Ensure all "Breakout" and "Copy Link" actions on the IDAA Video Conferences page include the necessary `key` and `uuid` parameters.
|
||||
|
||||
## 2. Implementation Steps
|
||||
|
||||
### Step 1: Update `src/routes/idaa/(idaa)/video_conferences/+page.svelte`
|
||||
- Create a reactive `breakout_url` derived from `$page.url.href`.
|
||||
- In the derivation logic:
|
||||
- Instantiate a `new URL`.
|
||||
- Check if `key` is present in `searchParams`. If missing, pull from `$ae_loc.allow_access` or `$ae_loc.site_access_key`.
|
||||
- Check if `uuid` is present in `searchParams`. If missing, pull from `$idaa_loc.novi_uuid`.
|
||||
- Return the resulting `href`.
|
||||
- Update the following UI elements to use `breakout_url`:
|
||||
- `copy_meeting_link` function (uses `navigator.clipboard.writeText`).
|
||||
- "Open in New Tab" anchor tag (`href`).
|
||||
- "Copy Link" fallback textarea (`value`).
|
||||
- "Copy Break-out Link" in the Jitsi Tools panel (`value`).
|
||||
|
||||
### Step 2: Update `documentation/CLIENT__IDAA_and_customized_mods.md`
|
||||
- Add a note in the "Authentication: Novi UUID System" or "Iframe Integration" section about the requirement for `key` and `uuid` in breakout links.
|
||||
- Document that the frontend now automatically re-injects these for external links to ensure persistent session access outside the iframe.
|
||||
|
||||
## 3. Verification
|
||||
- Manually verify the logic:
|
||||
- If `key` and `uuid` are already in the URL (e.g., initial load), the derived URL should remain unchanged (or correctly deduplicated).
|
||||
- If they are missing (e.g., after navigating from another IDAA page), they should be added to the generated link.
|
||||
- Run `npx svelte-check` to ensure no syntax regressions.
|
||||
53
conductor/launcher-ux-refinement.md
Normal file
53
conductor/launcher-ux-refinement.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Plan: Launcher Config UX Refinement (Cohesion & Stability)
|
||||
|
||||
The goal of this plan is to address the visual "bouncing", layering overload, and the misplaced close button in the new Launcher configuration modal.
|
||||
|
||||
## 1. Dimensional Stability
|
||||
- **Problem:** Switching tabs causes the modal to resize vertically and horizontally, leading to a "bouncing" feel.
|
||||
- **Solution:**
|
||||
- Set a fixed height for the `Launcher_cfg` container (e.g., `h-[750px]`).
|
||||
- Use `overflow-y-auto` only for the right-hand content pane.
|
||||
- Ensure the sidebar has a stable width.
|
||||
|
||||
## 2. Visual Hierarchy & Layering
|
||||
- **Problem:** Too many nested backgrounds (Page > Launcher > Modal > Inner Pane > Section Pane > Section Content).
|
||||
- **Solution:**
|
||||
- Flatten the background of the main content pane.
|
||||
- Simplify `Launcher_Cfg_Section.svelte`:
|
||||
- Remove `shadow-xl` from individual sections.
|
||||
- Use subtler borders instead of strong "preset-outlined" colors.
|
||||
- Remove the secondary background (`bg-white/5`) from the section content area.
|
||||
- Standardize on a single, clean surface color for the right-hand pane.
|
||||
|
||||
## 3. The "Centered Close Button" Bug
|
||||
- **Problem:** A close button is appearing in the middle of the screen.
|
||||
- **Investigation:**
|
||||
- Check for absolute-positioned elements in `Launcher_cfg.svelte` or `+layout.svelte`.
|
||||
- Verify if Flowbite's `Modal` default close button is clashing with internal buttons.
|
||||
- **Solution:**
|
||||
- Consolidate all "Close" actions.
|
||||
- Use the Modal's built-in top-right close button (if available) or a single, well-positioned button in the sidebar.
|
||||
|
||||
## 4. Implementation Steps
|
||||
|
||||
### Step 1: Update `Launcher_cfg.svelte`
|
||||
- Set stable dimensions: `h-[750px] max-h-[90vh] w-[1000px] max-w-[95vw]`.
|
||||
- Remove internal shadows and borders that conflict with the Modal container.
|
||||
- Clean up the sidebar "Close" button.
|
||||
|
||||
### Step 2: Update `Launcher_cfg_section.svelte`
|
||||
- Simplify the styling to reduce visual clutter.
|
||||
- Remove `shadow-xl`.
|
||||
- Use consistent padding and margins.
|
||||
|
||||
### Step 3: Update `+layout.svelte`
|
||||
- Ensure the `Modal` is configured for a stable, large view without default padding issues.
|
||||
- Verify the `modal_cfg_open` logic.
|
||||
|
||||
### Step 4: Add `Launcher_cfg_field.svelte` (Helper)
|
||||
- Implement a unified field helper to standardize Label/Description/Input layouts across all tabs.
|
||||
|
||||
## 5. Verification
|
||||
- Toggle between all 7 tabs. Verify zero layout shift (height/width remains constant).
|
||||
- Check appearance in Light and Dark modes.
|
||||
- Verify "Technical Mode" transitions.
|
||||
@@ -1,5 +1,7 @@
|
||||
# Aether Project Architecture
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
This document outlines the overall architecture and key technologies used in the Aether SvelteKit frontend project.
|
||||
|
||||
## 1. Project Overview
|
||||
@@ -20,7 +22,7 @@ The Aether project is a Svelte and SvelteKit based application, utilizing Tailwi
|
||||
- TipTap (`element_editor_tiptap.svelte`) — WYSIWYG rich-text for content fields (IDAA, Journals, Leads notes)
|
||||
- **Icons:** Lucide Icons (SVG Icons)
|
||||
- **Markdown Parsing:** `marked` library
|
||||
- **State Management:** Svelte stores, potentially with `liveQuery` from Dexie for reactive IndexedDB interactions.
|
||||
- **State Management:** Svelte 5 runes plus Dexie `liveQuery`. Events persisted state uses `runed` `PersistedState`; core and IDAA still contain legacy coarse-grained persisted stores pending migration.
|
||||
|
||||
### 2.1. Journals as the Canonical Frontend Pattern
|
||||
|
||||
@@ -78,8 +80,8 @@ Used for client-side persistence of various application states and configuration
|
||||
|
||||
Used for more structured client-side data storage, often for caching and offline capabilities.
|
||||
|
||||
- `ae_core_db`: Core database instance.
|
||||
- `<module>`: Module-specific database instances.
|
||||
- `db_core`: Core database instance.
|
||||
- `db_<module>`: Module-specific database instances (for example, `db_events` and `db_journals`).
|
||||
- `<custom>`: Custom module-specific database instances (none currently defined).
|
||||
|
||||
## 5. Data Sorting
|
||||
@@ -97,9 +99,9 @@ A set of standardized field names and types are used across Aether objects.
|
||||
|
||||
These fields are expected to be present in most Aether objects.
|
||||
|
||||
- `id`: Primary key for an object (internal use, often a UUID).
|
||||
- `id_random`: Randomly generated ID for an object (often used for external exposure or URL parameters).
|
||||
- `<object_type>_id_random`: Specific random ID for an object (e.g., `person_id_random`).
|
||||
- `<object_type>_id`: Canonical randomized string ID returned by V3 (for example, `person_id`). Use this for URLs, relationships, and Dexie indexed lookups.
|
||||
- `id`: Generic randomized string alias when returned by V3; do not assume it is a DB autonumber.
|
||||
- `id_random` / `<object_type>_id_random`: Legacy aliases. Do not introduce new usage.
|
||||
- `code`: Short, unique identifier.
|
||||
- `name`: Display name.
|
||||
- `enable`: Boolean for active/inactive status.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Aether Project Naming Conventions
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
## 1. General Principles
|
||||
|
||||
- **Clarity:** Names should clearly convey their purpose and meaning.
|
||||
@@ -11,9 +13,10 @@
|
||||
|
||||
- **Logic/Service Files:** `ae_<module>__<concept>.ts` (e.g., `ae_core__account.ts`, `ae_events__event.ts`)
|
||||
- **Database Definition Files:** `db_<module>.ts` (e.g., `db_core.ts`, `db_journals.ts`)
|
||||
- **Svelte Store Files:** `ae_<module>_stores.ts` (e.g., `ae_core_stores.ts`, `ae_journals_stores.ts`)
|
||||
- **Svelte Store Files:** Follow existing module names. Svelte 5 `PersistedState` files use a `.svelte.ts` suffix and are imported via the `.svelte` module path (for example, `ae_events_stores__badges.svelte.ts`).
|
||||
- **Svelte Components:**
|
||||
- **Module-specific components:** `ae_comp__<module>__<component_name>.svelte` (e.g., `ae_comp__events__event_card.svelte`)
|
||||
- **Route-level components:** `ae_comp__<component_name>.svelte`.
|
||||
- **Module-specific components:** `ae_<module>_comp__<component_name>.svelte` (for example, `ae_events_comp__session_list.svelte`).
|
||||
- **Generic/reusable components:** `element_<component_name>.svelte` (e.g., `element_input_file.svelte`, `element_qr_scanner_v2.svelte`)
|
||||
- **SvelteKit Routes:** Follow SvelteKit's standard routing conventions (e.g., `+page.svelte`, `+layout.svelte`, `[id]/+page.svelte`).
|
||||
- **CSS Files:** `ae-<module>-<purpose>.css` (e.g., `ae-c-idaa-light.css`, `ae-osit-default.css`)
|
||||
@@ -37,9 +40,9 @@
|
||||
|
||||
- **Singularity:** Use singular nouns for objects and properties (e.g., `example.id`, not `examples.id`).
|
||||
- **IDs:**
|
||||
- `id`: Primary key for an object (internal use, often a UUID).
|
||||
- `<object_type>_id`: Specific ID for an object (e.g., `person_id`).
|
||||
- `<object_type>_id_random`: Randomly generated ID for an object (often used for external exposure or URL parameters).
|
||||
- `<object_type>_id`: Canonical randomized string ID returned by V3 (for example, `person_id`).
|
||||
- `id`: Generic randomized string alias when V3 returns one; never assume it is an integer autonumber.
|
||||
- `<object_type>_id_random`: Legacy alias; do not introduce new usage.
|
||||
- `account_id`, `site_id`, `user_id`, etc.: Foreign keys.
|
||||
- **Common Properties:**
|
||||
- `code`: Short, unique identifier.
|
||||
@@ -88,6 +91,6 @@
|
||||
- `<module>` (extended modules)
|
||||
- `<custom>` (custom modules)
|
||||
- **IndexedDB:**
|
||||
- `ae_core_db`
|
||||
- `<module>`
|
||||
- `db_core`
|
||||
- `db_<module>` (for example, `db_events`, `db_journals`)
|
||||
- `<custom>`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Aether — Permissions and Security
|
||||
|
||||
**Last updated:** 2026-02-27
|
||||
**Last Updated:** 2026-06-12
|
||||
**Source of truth:** `src/lib/ae_utils/ae_utils__perm_checks.ts`, `src/lib/stores/ae_stores.ts`
|
||||
|
||||
---
|
||||
@@ -76,15 +76,18 @@ $ae_loc.adv_mode // boolean — advanced mode toggle
|
||||
| AE Username + Password | `trusted` and above | Staff with AE accounts |
|
||||
| Novi UUID | `authenticated` | IDAA members (Novi membership system) |
|
||||
|
||||
Passcodes are stored per-level in `$ae_loc.site_access_code_kv`:
|
||||
```typescript
|
||||
site_access_code_kv: {
|
||||
administrator: null, // highest passcode tier
|
||||
trusted: null, // onsite staff passcode
|
||||
public: 'public1980', // example
|
||||
authenticated: 'auth1980'
|
||||
}
|
||||
```
|
||||
### Site Passcode Security Warning
|
||||
|
||||
The current frontend receives every site passcode in `access_code_kv_json`, copies the map into
|
||||
persisted `$ae_loc.site_access_code_kv`, and compares entered passcodes locally. Verbose logging
|
||||
can also expose the complete map. This is a known active security gap, not the target design.
|
||||
|
||||
Do not add new consumers of `site_access_code_kv`, log passcodes, or treat persisted
|
||||
`access_type` as durable proof of authentication. The target flow verifies passcodes through
|
||||
`/authenticate_passcode`, stores a signed JWT with a role-specific TTL, and removes passcodes from
|
||||
the public bootstrap response and client state.
|
||||
|
||||
See `documentation/PROJECT__AE_Site_Passcode_Security.md` for the active migration plan.
|
||||
|
||||
### `x-no-account-id` — Narrow Transport Exception
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Aether SvelteKit — AI Agent Bootstrap / Quickstart
|
||||
> **Doc Owner:** Frontend platform maintainers (OSIT) + active coding agents
|
||||
> **Review Trigger:** Update when module architecture, critical rules, or primary doc paths change.
|
||||
> **Read this first.** This doc is the fast path to being productive on this project.
|
||||
> It covers the rules, patterns, and gotchas that matter most.
|
||||
> Deep dives are in the linked docs at the bottom.
|
||||
@@ -29,7 +31,7 @@ running in Docker. The frontend talks to it exclusively via the V3 REST API.
|
||||
| Editors | CodeMirror 6 (primary), Edra/TipTap (secondary) |
|
||||
| Native | Electron app for onsite launcher (`src/lib/electron/electron_relay.ts`) |
|
||||
| Backend | FastAPI + MariaDB, V3 API (`/v3/crud/`, `/v3/lookup/`) |
|
||||
| Auth | Custom headers: `x-aether-api-key` + `x-account-id`; JWT Bearer is auto-injected when a session exists |
|
||||
| Auth | Custom headers only: `x-aether-api-key` + `x-account-id` — **NOT** Bearer tokens |
|
||||
|
||||
---
|
||||
|
||||
@@ -142,19 +144,31 @@ onDestroy(() => cleanup());
|
||||
- Use `$state()` for local component state with no external binding.
|
||||
|
||||
### Store reactivity trap (important for `$effect`)
|
||||
The app uses `svelte-persisted-store` (Svelte 4 contract) for `$ae_loc`, `$ae_api`,
|
||||
`$ae_sess`, etc. In Svelte 5 `$effect`, reading **any field** of a Svelte 4 store
|
||||
subscribes to the **entire store**. This means unrelated writes to `$ae_loc`
|
||||
(e.g. iframe height, SWR reload) will re-trigger your effect. Be conservative about
|
||||
what you read from these stores inside `$effect` blocks. See `PROJECT__Stores_Svelte5_Migration.md`
|
||||
for the long-term fix plan.
|
||||
The app has two kinds of persisted stores — know which you're reading:
|
||||
|
||||
For search pages specifically, this usually means:
|
||||
**Svelte 4 `svelte-persisted-store` (coarse reactivity) — still used for:**
|
||||
- `$ae_loc`, `$ae_sess`, `$ae_api` (global app state)
|
||||
- `$idaa_loc`, `$idaa_sess` (IDAA module)
|
||||
|
||||
In Svelte 5 `$effect`, reading **any field** of these stores subscribes to the **entire store**.
|
||||
Unrelated writes to `$ae_loc` (e.g. iframe height, SWR reload) will re-trigger your effect.
|
||||
Be conservative about what you read from these stores inside `$effect` blocks.
|
||||
|
||||
**Svelte 5 `PersistedState` (fine-grained reactivity) — Events module stores:**
|
||||
- `badges_loc`, `leads_loc`, `pres_mgmt_loc`, `launcher_loc`, `events_auth_loc`
|
||||
|
||||
These use `runed`'s `PersistedState`. Access via `.current` (no `$` sigil):
|
||||
`badges_loc.current.field`. Writing one field only re-triggers effects that read that field.
|
||||
Import from the `.svelte` extension: `import { badges_loc } from '$lib/stores/ae_events_stores__badges.svelte'`.
|
||||
|
||||
For search pages using the coarse stores, this usually means:
|
||||
- keep true user preferences in persisted local state
|
||||
- keep transient triggers, loading flags, and last-executed search keys in session state when possible
|
||||
- let the page effect schedule the search, but put the duplicate-execution guard inside the search executor so page-load auto-search still runs after hydration
|
||||
- if the search text or filters are mirrored from localStorage on mount, expect that mount-time writes can re-trigger the effect unless the executor has its own guard
|
||||
|
||||
See `PROJECT__Stores_Svelte5_Migration.md` for migration status and the pattern to follow when migrating remaining stores.
|
||||
|
||||
### `{#await}` blocks
|
||||
```svelte
|
||||
{#await somePromise}
|
||||
@@ -271,137 +285,33 @@ When building anything new, model it after Journals.
|
||||
|
||||
---
|
||||
|
||||
## 7. Mistakes Agents Have Made on This Project
|
||||
## 7. Common Mistakes (Reference)
|
||||
|
||||
These are real incidents — know them before you start.
|
||||
The full, curated mistake catalog now lives in
|
||||
`documentation/REFERENCE__Common_Agent_Mistakes.md`.
|
||||
|
||||
1. **IDAA BB exposed publicly** — an agent removed an auth guard from the bulletin board
|
||||
route. All IDAA content must be behind authentication. Always check route guards when
|
||||
touching `/idaa/` routes.
|
||||
Read this section first, then open the reference doc when your task touches one of these areas:
|
||||
|
||||
2. **`event_file_id` in PATCH body (400 error)** — including the object ID in `data_kv`
|
||||
when calling `update_ae_obj__*`. The V3 API tries to `SET event_file_id = ...` which
|
||||
fails because it's a view alias, not a DB column. See Section 2 above.
|
||||
1. **Security/Auth** — private route guards, account scoping, and pre-gate data load risks.
|
||||
2. **V3 API payloads** — object ID in URL, `data_kv` field-only PATCH payloads.
|
||||
3. **Dexie/IDB behavior** — `.get()` primary key trap, stale cache/version mismatches, broad result clipping.
|
||||
4. **Svelte 5 reactivity** — coarse-store `$effect` loops and `$`-sigil misuse on plain props.
|
||||
5. **Sorting and search correctness** — `tmp_sort_*` comparator direction and Dexie sorting caveats.
|
||||
6. **Network reliability** — retry classification in `api_*_object.ts` and timeout behavior.
|
||||
7. **JSON field safety** — `*_json` null reads/writes and wrapper serialization behavior.
|
||||
8. **Service worker rollout behavior** — stale-tab symptoms, activation expectations, and trade-offs.
|
||||
9. **Local/remote config sync** — shadow fields that silently bypass an admin-synced master
|
||||
field, SWR-await-after-write races, and stateful/conditional sync gates that desync local
|
||||
state from history rather than current config. See the Pres Mgmt Config sync overhaul
|
||||
(2026-06-16) in `PROJECT__AE_Events_PressMgmt_Config_Cleanup.md` for the full incident.
|
||||
|
||||
3. **Bad `.d.ts` declaration silently hid 1368 errors** — a `declare module` in `app.d.ts`
|
||||
(a script-context file) replaced the entire `@lucide/svelte` type exports instead of
|
||||
merging. `svelte-check` showed 0 errors, masking real problems. If `svelte-check`
|
||||
suddenly drops to 0 errors, verify it's not because a bad declaration wiped a module.
|
||||
|
||||
4. **Coarse store reactivity loop** — an `$effect` that read `$ae_loc.some_field` was
|
||||
re-triggering repeatedly because unrelated writes to `$ae_loc` (e.g. SWR config reload)
|
||||
fired the effect. In Svelte 5, any read of a Svelte 4 store inside `$effect` subscribes
|
||||
to the whole store. Scope what you read carefully.
|
||||
|
||||
5. **`file_purpose == 'admin'` not hidden in Launcher** — the `hide_draft` prop hid
|
||||
`outline` and `draft` files but not `admin` files. Gaps like this happen when a new
|
||||
enum value is added to a field without auditing all the places that filter on it.
|
||||
|
||||
6. **Deleting files with `rm`** — always move to `~/tmp/agents_trash`. A deleted file may
|
||||
contain context that's not recoverable from git if it was gitignored.
|
||||
|
||||
7. **Dexie `.get()` with a string object ID returns `undefined`** — Dexie `.get(value)`
|
||||
looks up by the table's **primary key**, which is `id` (the first schema field). The V3
|
||||
API never returns `id`, so it is always `undefined` in stored records. Passing a string
|
||||
object ID (e.g. `person_id`) to `.get()` will silently return nothing. Always use
|
||||
`.where('person_id').equals(person_id).first()` instead. This has caused liveQuery
|
||||
blocks to always produce `undefined` even when the record exists in Dexie.
|
||||
|
||||
8. **Treating `$effect` blocks as auth bypass risks** — a `$effect` inside a child
|
||||
component cannot bypass a parent `+layout.svelte` auth gate. Children only mount if
|
||||
the parent calls `{@render children?.()}`. Adding redundant auth guards to `$effect`
|
||||
blocks that can only run after the parent gate already passed is unnecessary — and
|
||||
misleads future readers into thinking the parent gate is not sufficient on its own.
|
||||
The **real** pre-gate risk is `+page.ts` / `+layout.ts`: universal load functions run
|
||||
before any layout mounts and also fire during SvelteKit link prefetch. Keep those files
|
||||
clean of data loads in private modules. See `GUIDE__SvelteKit2_Svelte5_DexieJS.md` →
|
||||
"SvelteKit Layout Hierarchy: Security and Execution Order" for the full explanation.
|
||||
|
||||
9. **Using query `key` as a proxy for bypass stripped `x-account-id`** — this caused
|
||||
valid account-scoped requests to lose account context and 403. `key` can be a valid
|
||||
endpoint/business param, but it is not equivalent to `x-no-account-id: bypass`. Keep
|
||||
`x-no-account-id` usage narrow and temporary; do not expand it without a documented
|
||||
allowlist case.
|
||||
|
||||
10. **Pre-stringifying `*_json` fields before passing to API wrappers** — the API wrappers
|
||||
(`api_post__crud_obj.ts` for V3, `api.ts` for legacy CRUD) automatically serialize any
|
||||
field ending in `_json` (e.g. `cfg_json`, `data_json`). Pass these as plain JS objects.
|
||||
Pre-stringifying with `JSON.stringify()` before calling the wrapper will double-encode
|
||||
the value in the legacy path (stringify sees a string and escapes it), and is at best
|
||||
redundant on the V3 path. Both paths now pretty-print with 2-space indent.
|
||||
See `GUIDE__AE_API_V3_for_Frontend.md` → section 3C for the full explanation.
|
||||
|
||||
11. **Broad Dexie result windows get silently clipped** — if a broad "All" view shows fewer
|
||||
rows than a narrower filter, check for a page-level limit or an API revalidation step
|
||||
replacing the local IDB result set. For empty text searches, the full local result set
|
||||
should drive the display; server refreshes should update cache, not shrink visibility.
|
||||
|
||||
12. **Not bumping `IDB_CONTENT_VERSIONS` when changing `properties_to_save`** — this caused
|
||||
the IDAA Recovery Meetings "no meetings found" bug for approximately one year (2025–2026).
|
||||
|
||||
**What happened:** A deploy changed `properties_to_save` in `ae_events__event.ts`, but no
|
||||
one bumped `IDB_CONTENT_VERSIONS.events.event` in `store_versions.ts`. Existing users kept
|
||||
the old stale event records in IndexedDB indefinitely. On the Recovery Meetings page, the
|
||||
fast path (IDB search) returned those stale records, which all failed the `account_id`
|
||||
filter and returned 0 results. The API call then either errored silently or was filtered
|
||||
to 0 by the secondary client-side filter. Critically, the error state and the genuinely
|
||||
empty state showed the **same** "No meetings found" message — users and staff had no
|
||||
indication a failure had occurred. The manual Full Reset (via the `?` help panel) always
|
||||
fixed it, but no one knew why it worked, making the root cause impossible to track down.
|
||||
|
||||
**The fix (2026-05-16):** `check_and_clear_idb_table()` in `store_versions.ts` is now
|
||||
wired in `src/routes/idaa/(idaa)/+layout.svelte` for `db_events.event`. On a version
|
||||
match it costs one localStorage read. On a mismatch it silently clears the table; the
|
||||
SWR pattern then repopulates from the API on next load.
|
||||
|
||||
**The rule going forward:**
|
||||
- When you change `properties_to_save` in any `ae_events__*.ts` file (or any other
|
||||
object file) in a way that makes existing cached records stale — fields added, removed,
|
||||
renamed, or where a computed field's behavior changes — **bump the matching entry in
|
||||
`IDB_CONTENT_VERSIONS` in `src/lib/stores/store_versions.ts`**.
|
||||
- If the table is not yet wired, wire it first (see the wiring instructions in the
|
||||
`IDB_CONTENT_VERSIONS` comment block in `store_versions.ts`).
|
||||
- Currently wired: `events.event`. All other tables are not yet wired.
|
||||
|
||||
**Also:** Never show the same UI message for both a failed API call and a genuinely empty
|
||||
result. Always distinguish `qry__status === 'error'` from `qry__status === 'done'` with
|
||||
0 results in your templates. Silent failures look like data problems and are extremely
|
||||
difficult to diagnose.
|
||||
|
||||
13. **Breaking the API retry loop by returning errors instead of throwing them** — all four
|
||||
`api_*_object.ts` files (`api_get_object.ts`, `api_post_object.ts`, `api_patch_object.ts`,
|
||||
`api_delete_object.ts`) use a `.catch()` that returns the error as a value, followed by a
|
||||
classification block. That block **must throw** for transient network failures (`TypeError`)
|
||||
so they enter the retry loop. If you change it to `return false`, retries are silently
|
||||
bypassed for the most common failure mode in hotel/conference WiFi — and nothing warns you.
|
||||
|
||||
**What happened (commit a10accfaa, Jan 2026):** A "silence background fetch noise" commit
|
||||
changed `.catch()` to explicitly `return error`, then the classification block was changed
|
||||
from a `throw` to `return false`. `TypeError` from `ERR_NETWORK_CHANGED` — the most common
|
||||
failure on crowded WiFi — stopped retrying. The `retry_count = 5` parameter became dead
|
||||
code for network errors. Went undetected for ~4 months.
|
||||
|
||||
**The retry classification these files must honor:**
|
||||
- `TypeError` (ERR_NETWORK_CHANGED, WiFi blip) → **`throw`** → enters retry loop with backoff
|
||||
- `AbortError` where `did_timeout_abort = true` (helper's own timer) → **`throw`** → retries
|
||||
- `AbortError` where `did_timeout_abort = false` (navigation/unmount abort) → `return false`
|
||||
- HTTP 400/401/403/422 → `return false` immediately (client errors are deterministic)
|
||||
- HTTP 5xx → **`throw`** → retries with backoff
|
||||
|
||||
**How to verify after any change to the error block:** confirm that a `TypeError` still
|
||||
produces up to 5 retry attempts with 2s→4s→6s→8s delays before returning false. A single
|
||||
`return false` after the first network failure means the retry loop is broken.
|
||||
|
||||
**Also:** when reviewing these files, check that all four have:
|
||||
- `ae_auth_error.set()` triggered on 401/403 (shows session-expired banner to the user)
|
||||
- `timeout = 20000` default (was 60s in PATCH/DELETE until 2026-05-21 — 5-min worst case)
|
||||
- `did_timeout_abort` flag per attempt (separates helper timeouts from caller aborts)
|
||||
The reference doc also includes a short archive list for older, less-relevant historical incidents.
|
||||
|
||||
---
|
||||
|
||||
## 8. Source Layout (Quick Reference)
|
||||
|
||||
```
|
||||
```text
|
||||
src/lib/
|
||||
ae_api/ — API helpers (V3 preferred)
|
||||
ae_core/ — Account, User, Person, Site, hosted files
|
||||
@@ -433,12 +343,23 @@ Start here, then go deeper as needed:
|
||||
| What you need | Read |
|
||||
|---|---|
|
||||
| Active tasks + known bugs | `documentation/TODO__Agents.md` ← always first |
|
||||
| Documentation index | `documentation/README__Docs_Index.md` |
|
||||
| Dev workflow + commit rules | `documentation/GUIDE__Development.md` |
|
||||
| V3 API reference | `documentation/GUIDE__AE_API_V3_for_Frontend.md` |
|
||||
| WebSockets / real-time updates | `documentation/GUIDE__AE_API_V3_for_Frontend_websockets.md` |
|
||||
| Dexie / liveQuery patterns | `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md` |
|
||||
| Common mistakes reference | `documentation/REFERENCE__Common_Agent_Mistakes.md` |
|
||||
| Svelte 5 patterns + pitfalls | `documentation/GEMINI__Svelte_and_Me.md` |
|
||||
| Permissions + auth levels | `documentation/AE__Permissions_and_Security.md` |
|
||||
| Electron / native launcher | `documentation/PROJECT__AE_Events_Launcher_Native_integration.md` |
|
||||
| Electron / native launcher | `documentation/MODULE__AE_Events_Launcher_Native.md` |
|
||||
| Store migration plan | `documentation/PROJECT__Stores_Svelte5_Migration.md` |
|
||||
| Exhibitor Leads module | `documentation/MODULE__AE_Events_Exhibitor_Leads.md` |
|
||||
| Journals module overview | `documentation/MODULE__AE_Journals.md` |
|
||||
| Journals settings map | `documentation/MODULE__AE_Journals_Config_Map.md` |
|
||||
| Exhibitor Leads module | `documentation/MODULE__AE_Events_Leads.md` |
|
||||
| Presentation Management module | `documentation/MODULE__AE_Events_Presentation_Management.md` |
|
||||
| IDAA client architecture | `documentation/CLIENT__IDAA_and_customized_mods.md` |
|
||||
| IDAA Archives module | `documentation/MODULE__AE_IDAA_Archives.md` |
|
||||
| IDAA Bulletin Board module | `documentation/MODULE__AE_IDAA_Bulletin_Board.md` |
|
||||
| IDAA Recovery Meetings module | `documentation/MODULE__AE_IDAA_Recovery_Meetings.md` |
|
||||
| IDAA Video Conferences module | `documentation/MODULE__AE_IDAA_Video_Conferences.md` |
|
||||
| Naming conventions | `documentation/AE__Naming_Conventions.md` |
|
||||
|
||||
@@ -31,6 +31,17 @@ IDAA is a private membership organization for physicians in recovery. They use t
|
||||
|
||||
IDAA's Aether instance is embedded as an **iframe inside their existing Novi-powered website** (`idaa.org`). Novi is their external Association Management System (AMS) — it handles membership records and authentication. Aether receives the member context via URL parameters on iframe load.
|
||||
|
||||
### Breakout Links and Iframe Persistence
|
||||
|
||||
Members often need to open Jitsi meetings outside the Novi iframe (e.g., for full-screen features or on mobile). These are referred to as **Breakout Links**.
|
||||
|
||||
- **The Problem:** SvelteKit client-side navigation within the iframe often drops "bootstrap" query parameters like `?key=...` (site access key) and `?uuid=...` (Novi identity token).
|
||||
- **The Requirement:** When a member breaks out of the iframe into a new browser tab, these keys **must** be present in the URL. Without them, the member will hit the site-domain gate or the IDAA auth gate and see "Access Denied."
|
||||
- **The Solution:** The Video Conferences page uses a derived `breakout_url` that proactively re-injects the missing `key` (from `$ae_loc.allow_access`) and `uuid` (from `$idaa_loc.novi_uuid`) before generating the external link.
|
||||
|
||||
**Example Breakout URL:**
|
||||
`https://client.oneskyit.com/idaa/video_conferences?uuid=...&key=...&room=...`
|
||||
|
||||
---
|
||||
|
||||
## Architecture: Composite Module
|
||||
@@ -446,6 +457,32 @@ filtered) replaces it after the background refresh completes. The IDB path does
|
||||
- Add `contact_li_json_ext` to the IDB fast-path filter in `search__event()` and the recovery
|
||||
meetings page so contact matches appear instantly from cache, not only after API refresh.
|
||||
|
||||
### Sort Encoding — Events Use Legacy (Not `build_tmp_sort`)
|
||||
|
||||
`ae_events__event.ts` builds `tmp_sort_1` with the **legacy encoding**: `priority ? 1 : 0`
|
||||
(priority=true → `'1'`). This is the **opposite** of `build_tmp_sort` (priority=true → `'0'`).
|
||||
|
||||
| Module | Encoding | Correct comparator |
|
||||
| --- | --- | --- |
|
||||
| `ae_events__event.ts` (Recovery Meetings) | Legacy: `priority=true→'1'` | **Descending** `b.localeCompare(a)` |
|
||||
| `ae_events__event_session.ts` | Legacy: `priority=true→'1'` | **Descending** `b.localeCompare(a)` |
|
||||
| `ae_events__event_presentation.ts` | `build_tmp_sort` (overrides legacy in `specific_processor`) | **Ascending** `a.localeCompare(b)` |
|
||||
| Journals, Posts, Archives | `build_tmp_sort` | **Ascending** `a.localeCompare(b)` |
|
||||
|
||||
**Do not apply the `build_tmp_sort` ascending rule to raw event or session sorts** until
|
||||
`ae_events__event.ts` is migrated (tracked in TODO__Agents.md under IDB Sort rollout).
|
||||
|
||||
### Search Trigger — Use `$slct.account_id`, Not `$ae_loc.account_id`
|
||||
|
||||
The recovery meetings search `$effect` gates on `$slct.account_id` (set only by the bootstrap
|
||||
Sync Effect, non-persisted). Do NOT change this back to `$ae_loc.account_id`.
|
||||
|
||||
**Why:** `$ae_loc` is a persisted store that hydrates from localStorage on page load. Its
|
||||
`account_id` may be stale from a previous session (e.g., a dev/demo account_id left behind).
|
||||
Using it as the gate fires the API call with the wrong account before bootstrap has run,
|
||||
producing either a 403 or wrong-account data. `$slct.account_id` is null until bootstrap
|
||||
sets it — a reliable gate. See mistake #14 in `BOOTSTRAP__AI_Agent_Quickstart.md`.
|
||||
|
||||
### My Meetings (Favorites)
|
||||
|
||||
Members can star meetings to build a personal "My Meetings" list. The star toggle appears:
|
||||
@@ -862,4 +899,4 @@ ae_loc.idaa_loc = { novi_uuid: 'test-uuid-value', ... };
|
||||
---
|
||||
|
||||
**Document Status:** ✅ Current
|
||||
**Last Verified:** 2026-05-19 — Access Gate: documented new `verify_error_type` error-handling states and retry/reset UI; Search Architecture: corrected contact-search status (now works via `default_qry_str` in API path — two root causes fixed 2026-05-18/19); noted IDB fast-path gap as remaining enhancement
|
||||
**Last Verified:** 2026-06-03 — Recovery Meetings: documented legacy `tmp_sort_1` encoding for events (requires descending sort, not ascending); documented `$slct.account_id` gate pattern for search trigger; noted service worker `skipWaiting`/`clients.claim` requirement for long-lived IDAA iframe sessions (root cause of user-reported loading failures that could not be reproduced in dev)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Aether API V3 Frontend Integration Guide (Svelte/TypeScript)
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
This guide defines the standards for interacting with the **Aether API V3 CRUD** and **Action** endpoints.
|
||||
|
||||
---
|
||||
@@ -96,6 +98,26 @@ The primary way to retrieve data.
|
||||
* **Endpoint:** `POST /v3/crud/{obj_type}/search`
|
||||
* **Security:** Automatically filters results to only show records belonging to your `x-account-id`. If no account context is provided, it will return **0 records** for private objects.
|
||||
|
||||
#### Sorting with `order_by_li`
|
||||
|
||||
Pass a JSON object as the `order_by_li` query parameter to sort results:
|
||||
|
||||
```ts
|
||||
// ?order_by_li={"filename":"ASC","created_on":"DESC"}
|
||||
const params = new URLSearchParams({
|
||||
order_by_li: JSON.stringify({ filename: 'ASC', created_on: 'DESC' })
|
||||
});
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **`order_by_li` only accepts columns from the raw base table** — not view-only join columns.
|
||||
>
|
||||
> Some object types (e.g. `event_file`) have enriched views that JOIN other tables to expose convenience fields like `event_presenter_family_name`. These are available in search results when using `?view=alt`, but they **cannot** be used in `order_by_li`. Attempting to sort by them silently drops those sort keys (the query proceeds without them).
|
||||
>
|
||||
> If you need to sort by a joined field, sort client-side on the returned list.
|
||||
>
|
||||
> **Columns safe to sort on for `event_file`:** any field in the `event_file` table itself — `filename`, `title`, `extension`, `created_on`, `updated_on`, `sort`, `enable`, etc.
|
||||
|
||||
### C. POST Create / PATCH Update
|
||||
Modify data in the system.
|
||||
* **Endpoints:**
|
||||
@@ -266,7 +288,7 @@ When seeding new lookup data (e.g., adding timezones in bulk):
|
||||
|
||||
## 5. Event File Data Retrieval (Hosted Files)
|
||||
|
||||
Every Event File (`event_file`) **must** have a linked Hosted File (`hosted_file`). The Hosted File itself is a metadata record for binary content (files), which is accessed via separate Action endpoints (e.g., `/v3/action/hosted_file/download`). This API endpoint provides metadata about the associated hosted file. To retrieve this additional metadata:
|
||||
Every Event File (`event_file`) **must** have a linked Hosted File (`hosted_file`). The Hosted File is a metadata record for binary content, accessed via dedicated Action endpoints. To download an event file use `/v3/action/event_file/{event_file_id}/download` — not the hosted_file endpoint directly (each endpoint only accepts its own ID type). To retrieve hosted file metadata alongside an event file record:
|
||||
|
||||
* **Endpoint:** `GET /v3/crud/event_file/{event_file_id}`
|
||||
* **Query Parameter:** Add `inc_hosted_file=true`
|
||||
@@ -281,6 +303,48 @@ Every Event File (`event_file`) **must** have a linked Hosted File (`hosted_file
|
||||
* `hosted_file_size` (string - in bytes)
|
||||
2. **Nested Hosted File Object:** A full `hosted_file` object will be nested under the `hosted_file` key. This object (`Hosted_File_Base` model) will contain all its standard fields, including `id` (random string ID), `hash_sha256`, `content_type`, `size`, etc.
|
||||
|
||||
### Direct Download Links (Shareable / External)
|
||||
|
||||
Event files can be downloaded without standard auth headers using one of two bypass mechanisms. This is useful for generating shareable links for staff or external recipients.
|
||||
|
||||
- **Method:** `GET`
|
||||
- **Path:** `/v3/action/event_file/{event_file_id}/download`
|
||||
|
||||
> [!WARNING]
|
||||
> **Breaking change (2026-06-10):** This endpoint now requires an `event_file_id`. Previously it accepted `hosted_file_id` or `archive_content_id` and resolved the chain automatically — that cross-resolution has been removed. Pass the correct ID type for the endpoint you are calling. If you were routing downloads through `/v3/action/hosted_file/{hosted_file_id}/download` as a workaround, switch to this endpoint using `event_file_id`. *(Remove this note after ~2026-06-24.)*
|
||||
|
||||
#### Auth bypass options
|
||||
|
||||
| Query param | Value | When to use |
|
||||
|---|---|---|
|
||||
| `?key=<account_id_random>` | Any valid account random ID | Staff sharing within a known account context |
|
||||
| `?site_key=<site_access_key>` | The site's `access_key` value | Public or semi-public distribution tied to a specific site |
|
||||
|
||||
Either param replaces the need for `x-aether-api-key` / `x-account-id` headers, so the URL is self-contained and works in a plain browser tab or `<a href>` link.
|
||||
|
||||
#### Optional params
|
||||
|
||||
| Query param | Description |
|
||||
|---|---|
|
||||
| `filename` | Override the download filename (min 4 chars). Useful for giving files clean display names. |
|
||||
|
||||
#### Building a shareable link
|
||||
|
||||
```ts
|
||||
// Build a self-contained download URL for staff/external use
|
||||
function makeDownloadUrl(eventFileId: string, accountId: string, displayName?: string): string {
|
||||
const base = `https://dev-api.oneskyit.com/v3/action/event_file/${eventFileId}/download`;
|
||||
const params = new URLSearchParams({ key: accountId });
|
||||
if (displayName) params.set('filename', displayName);
|
||||
return `${base}?${params}`;
|
||||
}
|
||||
```
|
||||
|
||||
The endpoint supports byte-range requests (`Range` header), so it works correctly for in-browser media streaming as well as direct file downloads.
|
||||
|
||||
> [!NOTE]
|
||||
> The `?key=` bypass verifies only that the account ID exists — it does not confirm the file belongs to that account. It is appropriate for internal staff tools. For publicly distributed links, prefer `?site_key=` which ties access to a specific site's configured key.
|
||||
|
||||
---
|
||||
|
||||
## 6. Hosted File Actions: Convert & Clip (Frontend Notes)
|
||||
@@ -301,18 +365,16 @@ These helper endpoints let the frontend request small server-side transformation
|
||||
- Required query params: `link_to_type`, `link_to_id`, `start_time`, `end_time` (format `HH:MM:SS`)
|
||||
- Optional query params: `filename_no_ext` (defaults to `automated_hosted_file_clip_video`), `reencode` (bool), `scale_down` (bool)
|
||||
- Auth: standard V3 headers
|
||||
- Behavior: extracts a clip using `ffmpeg` and saves it as a new `hosted_file`. Defaults to stream-copying to be fast; set `reencode=true` to force H.264 or `scale_down=true` to resize. Returns 400 on failure.
|
||||
- Behavior: extracts a clip using `ffmpeg` and saves it as a new `hosted_file`.
|
||||
- Defaults to stream-copying to be fast; set `reencode=true` to force H.264 or `scale_down=true` to resize.
|
||||
- For longer-running clips you can schedule the job in the background by adding `?background=true`. When scheduled the API returns `202 Accepted` and the clip runs asynchronously on the server; check the returned `hosted_file` record later via the standard V3 `hosted_file` endpoints.
|
||||
- Returns 400 on synchronous failure; returns 202 when scheduled successfully.
|
||||
- Behavior: extracts a clip using `ffmpeg` and saves it as a new `hosted_file`.
|
||||
- Defaults to stream-copying (fast); set `reencode=true` to force H.264 or `scale_down=true` to resize.
|
||||
- Add `?background=true` to schedule the clip asynchronously — returns `202 Accepted` immediately; poll the `hosted_file` record for completion.
|
||||
- Returns 400 on synchronous failure; 202 when scheduled successfully.
|
||||
|
||||
Frontend guidance:
|
||||
|
||||
- Call these routes with the same `link_to_type` / `link_to_id` you plan to associate the resulting hosted_file with — the server resolves random IDs for you.
|
||||
- After a successful response, use the V3 `hosted_file` action endpoints (download/delete) to manage or retrieve the new file.
|
||||
- These endpoints run synchronously and can take time for large inputs; for heavy or batch workloads use a queued job pattern instead.
|
||||
- These endpoints may take time for large inputs. Prefer using `?background=true` to schedule work and receive a `202 Accepted` response for async processing. For heavy or batch workloads use a queued job pattern instead.
|
||||
- Prefer `?background=true` for large inputs to avoid request timeouts. For heavy or batch workloads use a queued job pattern instead.
|
||||
|
||||
---
|
||||
|
||||
@@ -673,19 +735,19 @@ Verifies a Novi AMS member UUID by proxying the Novi API call through the Aether
|
||||
|---|---|---|
|
||||
| `404` | UUID not found in Novi, or Novi returned 200 with no identity data (empty-member anti-pattern — member may have just joined) | Treat as denied / not a member |
|
||||
| `429` | Novi rate limit hit | Surface as `'rate_limited'`; advise retry |
|
||||
| `503` | Novi unreachable or Novi 5xx error | Auto-retry once after 3s; if retry also fails, surface as `'api_error'` |
|
||||
| `503` | Novi unreachable or Novi 5xx error | Surface as `'api_error'`; advise retry |
|
||||
|
||||
### Migration from direct Novi call — ✅ Complete (2026-05-19)
|
||||
### Migration from direct Novi call
|
||||
|
||||
`+layout.svelte:verify_novi_uuid()` now calls this endpoint instead of Novi directly. Response code mapping (for reference):
|
||||
The frontend's `+layout.svelte:verify_novi_uuid()` currently calls Novi directly from the browser. Replace that `fetch()` with this endpoint. Response code mapping:
|
||||
|
||||
| Direct Novi result | This endpoint returns | Frontend behavior |
|
||||
| Direct Novi result | This endpoint returns | Frontend state |
|
||||
|---|---|---|
|
||||
| `200` with identity data | `200` | `verified` |
|
||||
| `200` with no identity data | `404` | `denied` |
|
||||
| `404` | `404` | `denied` |
|
||||
| `429` | `429` | Auto-retry after 10s; `'rate_limited'` if retry fails |
|
||||
| Network error / Novi 5xx | `503` | Auto-retry after 3s; `'api_error'` if retry fails |
|
||||
| `429` | `429` | `'rate_limited'` |
|
||||
| Network error / Novi 5xx | `503` | `'api_error'` |
|
||||
|
||||
### Caching
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Aether API V3 WebSocket Integration Guide
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
This guide explains how to implement real-time communication using the **Aether API V3 WebSocket** protocol. V3 introduces granular routing, strict message schemas, and improved multi-tenant isolation compared to previous versions.
|
||||
|
||||
---
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
# Aether Events — Onsite Badge Printing
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
Notes on setup, process, hardware, and browser behavior for onsite badge printing at events.
|
||||
|
||||
For cross-module onsite operations (SRR, launcher monitoring, exhibitor leads support), see:
|
||||
`documentation/GUIDE__AE_Events_Onsite_Runbook.md`.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
@@ -157,26 +162,58 @@ This layout hides `.badge_back` in `@media print` — only the front face prints
|
||||
|
||||
---
|
||||
|
||||
### Epson — Fan-Fold / Label Printer
|
||||
### Epson ColorWorks C3500 — Fan-Fold Label Printer
|
||||
|
||||
**Status:** Not yet tested. Section to be filled in after testing.
|
||||
**Card stock:** 4" × 6" fan-fold paper label stock
|
||||
**Layout code:** `badge_4x6_fanfold`
|
||||
**Status:** Configured. First live use: Axonius Adapt DC — June 9, 2026.
|
||||
|
||||
Common Epson models used for fan-fold name badge stock: TM-T88 series, C3500, LX series.
|
||||
Fan-fold stock is typically 4" × 3" or 4" × 6" paper labels.
|
||||
The C3500 is a color inkjet label printer — it prints continuous fan-fold paper stock,
|
||||
not individual cards. Badges are separated along the perforation after printing.
|
||||
|
||||
#### Physical Setup
|
||||
|
||||
- Connect via USB or Ethernet
|
||||
- Load 4" × 6" fan-fold stock per Epson instructions
|
||||
- The C3500 is single-sided — only the front face prints. Back section is suppressed in CSS.
|
||||
- The badge has a lanyard hole punch: 5/8" × 1/8", centered, 1/4" from the top.
|
||||
Most fan-fold stock for badge use includes a pre-punched lanyard slot — verify stock matches.
|
||||
|
||||
#### Driver
|
||||
|
||||
- Epson ColorWorks C3500 CUPS driver available from epson.com (ColorWorks section)
|
||||
- On Linux/CUPS: install the provided PPD and add the printer at `http://localhost:631`
|
||||
- Set default paper size to **4" × 6"** in CUPS
|
||||
- Print a test page from CUPS before going live
|
||||
|
||||
#### Chrome Print Settings (C3500)
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
| Destination | Epson C3500 (CUPS name) |
|
||||
| Paper size | 4 × 6 in (set in CUPS driver) |
|
||||
| Margins | **None** |
|
||||
| Background graphics | On |
|
||||
| Pages | 1 (single-sided) |
|
||||
|
||||
#### CSS Layout
|
||||
|
||||
Fan-fold badges would use a layout sized to the specific label stock.
|
||||
A new CSS layout file will need to be created per stock size if not already present.
|
||||
Naming convention: `badge_layout_epson_[model]_[size].css`
|
||||
The C3500 uses the `badge_4x6_fanfold` layout. CSS file:
|
||||
`src/lib/ae_events/badges/css/badge_layout_epson_4x6_fanfold.css`
|
||||
|
||||
#### Setup Notes
|
||||
|
||||
*(To be filled in after testing — cover: driver source, CUPS setup, paper size, Chrome settings)*
|
||||
Created 2026-05-15 for Axonius Adapt DC. Key specs:
|
||||
- `badge_front` 4" × 6", portrait orientation
|
||||
- `badge_header` max-height 1.5in
|
||||
- Lanyard hole: 5/8" × 1/8", centered, 1/4" from top
|
||||
- `@page { size: 4in 6in; margin: 0; }` set in the print page dynamically
|
||||
- `.badge_back` suppressed in `@media print` (single-sided)
|
||||
|
||||
#### Known Behaviors
|
||||
|
||||
*(To be filled in after testing)*
|
||||
- Same Chrome margin rules apply: **Margins → None** prevents URL/date header clipping
|
||||
- Firefox honors `@page { size: 4in 6in }` for PDF proofing — use it to verify layout
|
||||
- Fan-fold stock separates along the perforation — no cutting needed, but verify the
|
||||
perforation lands outside the badge content area
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Guide — Aether Events: Onsite Runbook
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
This guide covers the human-centric logistics and "In the Heat of the Moment" support for onsite event operations.
|
||||
|
||||
---
|
||||
@@ -9,6 +11,10 @@ This guide covers the human-centric logistics and "In the Heat of the Moment" su
|
||||
Aether badge printing uses the browser's native `window.print()` — no special software or print
|
||||
server needed.
|
||||
|
||||
This runbook keeps badge guidance concise for onsite flow. For detailed printer/browser setup,
|
||||
driver notes, and troubleshooting matrix, use:
|
||||
`documentation/GUIDE__AE_Events_Badges_Onsite.md`.
|
||||
|
||||
### Kiosk Station Setup
|
||||
- **Browser:** Use **Chrome (Chromium)** for all kiosk stations.
|
||||
- **Settings:** Set Margins to **None**. Enable **Background Graphics**.
|
||||
@@ -18,6 +24,14 @@ server needed.
|
||||
- **Stock:** 3.5" × 5.5" PVC cards.
|
||||
- **Orientation:** Cards face-up, landscape in the hopper.
|
||||
- **Single-Sided:** Only the front face prints; the back section is hidden via CSS.
|
||||
- **Layout code:** `badge_3.5x5.5_pvc`
|
||||
|
||||
### Printer Reference: Epson ColorWorks C3500 (Fan-Fold)
|
||||
- **Stock:** 4" × 6" fan-fold paper label stock.
|
||||
- **Single-Sided:** Only the front face prints; the back section is hidden via CSS.
|
||||
- **Layout code:** `badge_4x6_fanfold`
|
||||
- **Lanyard hole:** Pre-punched 5/8" × 1/8" slot at top center — verify stock matches.
|
||||
- **First live use:** Axonius Adapt DC, June 9, 2026.
|
||||
|
||||
### Printing Workflow
|
||||
1. **Search:** Find the attendee by name or QR scan in the Badges module.
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# Aether UI — Design System Style Guidelines
|
||||
> **Version:** 1.2 (2026-03-20)
|
||||
> **Last Updated:** 2026-03-20
|
||||
> **Author:** One Sky IT / Scott Idem
|
||||
> **Scope:** All Aether SvelteKit frontend components
|
||||
> **Related:** `AE__UI_Component_Patterns.md`, `ae-firefly.css`, `documentation/AE__Components.md`
|
||||
> **Related:** `ae-firefly.css`, `documentation/MODULE__AE_Journals.md`, `documentation/MODULE__AE_Events_Presentation_Management.md`
|
||||
> **Historical implementation log:** `documentation/archive/PROJECT__AE_Style_Review_2026-03.md`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Aether Development SOP (Frontend)
|
||||
> **Version:** 1.2 (2026-03-17)
|
||||
> **Last Updated:** 2026-03-17
|
||||
> **Location:** documentation/GUIDE__Development.md
|
||||
|
||||
## 1. Verification (The "Test-First" Mandate)
|
||||
@@ -50,7 +51,7 @@ You are not working in a vacuum. Coordinate with the Backend Agent via MCP tools
|
||||
| `documentation/GEMINI__Svelte_and_Me.md` | Svelte 5 runes patterns |
|
||||
| `documentation/AE__Architecture.md` | System architecture overview |
|
||||
| `documentation/AE__Naming_Conventions.md` | Naming rules |
|
||||
| `documentation/PROJECT__AE_Events_Launcher_Native_integration.md` | Electron/Launcher reference |
|
||||
| `documentation/MODULE__AE_Events_Launcher_Native.md` | Electron/Launcher reference |
|
||||
| `tests/README.md` | Playwright test guide — shared helpers, hard-won lessons, demo IDs |
|
||||
|
||||
## 6. Inline Field Editing — `element_ae_obj_field_editor`
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# AE Docker CI Cache Policy (recommendation)
|
||||
# Aether Docker CI Cache Policy
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
Purpose
|
||||
- Provide a straightforward policy to keep build caches useful but bounded.
|
||||
@@ -1,5 +1,7 @@
|
||||
# Stability Patterns for liveQuery + Svelte 5
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
Dexie's `liveQuery` works well with Svelte 5 runes, but the combination requires a few stable patterns so queries don't get recreated unintentionally and components render correctly on a "cold start" (empty IndexedDB).
|
||||
- Keep the observable instance stable: wrap `liveQuery` in a stable `$derived` so the observable isn't recreated on every render. Recreate the `liveQuery` only when explicit dependencies change (IDs, filters, or search keys).
|
||||
|
||||
@@ -25,13 +27,54 @@ let lq__obj = $derived(
|
||||
|
||||
- Cold start (IDB empty) + non-blocking API writes: If you mount a component before data is written to IDB, `liveQuery` may run against an empty DB. The API write will populate IDB later, but sometimes a chain of dependent queries (e.g., presentations -> presenters) won't all rerun in the order you expect. The symptoms you described — session shows after one refresh, presenters only after a second — are consistent with either (a) queries recreated in the wrong order or (b) dependent store values being set only after some subscriptions are already created.
|
||||
|
||||
### Bootstrap Race: Account-scoped Loads Before `account_id` Is Set (2026-06)
|
||||
|
||||
Account-scoped `liveQuery` triggers can fire before `+layout.svelte`'s bootstrap Sync Effect
|
||||
has propagated the real `account_id`. Two failure modes:
|
||||
|
||||
1. **IDB empty:** fetch runs with `account_id = null`. The `localStorage` scavenge in
|
||||
`api_get_object.ts` reads the stale value from a previous session — possibly a different
|
||||
account — and caches that wrong record into IDB.
|
||||
2. **IDB has a stale record:** `liveQuery` returns a cached record from a different account as
|
||||
a valid hit, so the trigger condition (`!entry`) is never true and the correct record is
|
||||
never fetched.
|
||||
|
||||
**Rule:** Gate any trigger `$effect` that loads account-scoped data on `$slct.account_id`,
|
||||
not `$ae_loc.account_id`. `$slct` is a plain writable store (not persisted), initialized to
|
||||
`null` and set _only_ by the bootstrap Sync Effect. `$ae_loc` is a persisted store that
|
||||
hydrates from `localStorage` before effects run and may carry a stale `account_id`.
|
||||
|
||||
Also treat a non-null, non-matching `account_id` in an IDB record as a cache miss:
|
||||
|
||||
```typescript
|
||||
$effect(() => {
|
||||
const account_id = $slct.account_id; // null until bootstrap Sync Effect runs
|
||||
const api_ready = !!$ae_api?.base_url;
|
||||
const entry = $lq__obj as SomeType | null | undefined;
|
||||
|
||||
if (!browser || !account_id || !api_ready) return;
|
||||
|
||||
// null account_id on a record = global/shared fallback — still a valid hit.
|
||||
const entry_is_stale_account =
|
||||
entry !== undefined && entry !== null &&
|
||||
entry.account_id !== null &&
|
||||
entry.account_id !== account_id;
|
||||
|
||||
if (!entry || entry_is_stale_account) {
|
||||
trigger = 'load...';
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
See `BOOTSTRAP__AI_Agent_Quickstart.md` → Section 7, entry 14 for the full incident writeup.
|
||||
|
||||
### Critical Discovery (2026-02-26): The "try_cache: false" Bug
|
||||
|
||||
**Symptom:** Nested data (e.g., Session → Presentations → Presenters) requires multiple manual refreshes to display on cold-start, even when using blocking loads.
|
||||
|
||||
**Root Cause:** Two interconnected issues in nested data loaders:
|
||||
1. **Disabled caching in nested loads**: Parent loads were passing `try_cache: false` to child loads, meaning presentations and presenters were fetched from API but **never written to IndexedDB**.
|
||||
2. **Missing microtask yields**: Even when caching was enabled, components would mount and subscribe to liveQuery *before* IndexedDB writes completed, causing race conditions.
|
||||
2. **Missing microtask yields**: Even when caching was enabled, components would mount and subscribe to liveQuery _before_ IndexedDB writes completed, causing race conditions.
|
||||
|
||||
**Example of the Bug:**
|
||||
```typescript
|
||||
@@ -89,6 +132,99 @@ $effect(() => {
|
||||
|
||||
- When you have chains (presentations depend on session; presenters depend on presentation.person_id), make the dependent liveQuery explicitly wait for the upstream ID and log inside each query to verify the order — adding a small `await Promise.resolve()` or `await 0` inside the `liveQuery` is sometimes useful during debugging to ensure the JS microtask queue has a chance to settle after DB writes.
|
||||
|
||||
## IDB Sort: `build_tmp_sort` Pattern (2026-05)
|
||||
|
||||
All Aether objects support `priority`, `sort`, `group`, and `name` fields. Rather than sorting in JS after a Dexie query (which requires `.reverse()` hacks and duplicated logic), pre-compute up to three `tmp_sort_*` string fields during the processing pipeline and store them in Dexie. Then `.sortBy('tmp_sort_2')` does the right thing in one call, with no `.reverse()`.
|
||||
|
||||
**Utility:** `src/lib/ae_core/core__idb_sort.ts` — `build_tmp_sort()`
|
||||
|
||||
```typescript
|
||||
import { build_tmp_sort } from '$lib/ae_core/core__idb_sort';
|
||||
|
||||
// Inside specific_processor callback:
|
||||
const { tmp_sort_1, tmp_sort_2, tmp_sort_3 } = build_tmp_sort({
|
||||
prefix: [obj.group ?? '0'], // always first
|
||||
priority: obj.priority, // boolean; true→'0' so ASC sorts it first
|
||||
sort: obj.sort, // zero-padded to 8 chars
|
||||
fields_1: [...], // module-specific tier-1 fields
|
||||
fields_2: [...], // tier-2 fields (tmp_sort_2 = base + tier-1 + tier-2)
|
||||
fields_3: [...] // tier-3 fields
|
||||
});
|
||||
obj.tmp_sort_1 = tmp_sort_1;
|
||||
obj.tmp_sort_2 = tmp_sort_2;
|
||||
obj.tmp_sort_3 = tmp_sort_3;
|
||||
```
|
||||
|
||||
**Sort chain convention:** `group → priority DESC → sort ASC → [module-specific] → name`
|
||||
|
||||
**Priority encoding:** `priority ? '0' : '1'` — inverted so that `priority=true` sorts first in ascending order. This means:
|
||||
- **Dexie `.sortBy('tmp_sort_*')`** — always call without `.reverse()` before it (Dexie ignores collection-level `.reverse()` when using `.sortBy()`). If descending is needed for non-tmp_sort fields, call `.reverse()` on the resulting array after `await`.
|
||||
- **JS `.sort()` comparators** — use **ascending** `a.localeCompare(b)`, NOT `b.localeCompare(a)`. Using descending flips the priority encoding and puts `priority=false` items first.
|
||||
|
||||
```ts
|
||||
// ✅ Correct — ascending; priority=true ('0') sorts before priority=false ('1')
|
||||
list.sort((a, b) => (a.tmp_sort_1 ?? '').localeCompare(b.tmp_sort_1 ?? ''));
|
||||
|
||||
// ❌ Wrong — descending inverts the encoding; priority=false ('1') sorts first
|
||||
list.sort((a, b) => (b.tmp_sort_1 ?? '').localeCompare(a.tmp_sort_1 ?? ''));
|
||||
```
|
||||
|
||||
**Modules using `build_tmp_sort`:**
|
||||
- `ae_events__event_presentation.ts` — `tmp_sort_1/2`: group → priority → sort → start_datetime → code → name
|
||||
- `ae_events__event.ts` — `tmp_sort_1/2/3`: group → priority → sort → name → updated_on (used by IDAA recovery meetings)
|
||||
- `ae_journals__journal.ts` — `tmp_sort_1/2/3`: group → priority → sort → name → updated_on
|
||||
- `ae_journals__journal_entry.ts` — same chain as journal
|
||||
|
||||
**Legacy encoding (not yet migrated to `build_tmp_sort`):** `ae_posts__post.ts`, `ae_posts__post_comment.ts`, `ae_archives__archive.ts`, `ae_archives__archive_content.ts`, `ae_sponsorships_functions.ts` use the opposite encoding (`priority ? '1' : '0'`, designed for descending sort). Their current route consumers sort by date/name so there is no visible priority bug today, but they must be migrated before any route starts sorting by `tmp_sort_*`. See `TODO__Agents.md`.
|
||||
|
||||
---
|
||||
|
||||
## `$derived.by` Dependency Capture for Extra Filter State
|
||||
|
||||
When a `liveQuery` has a SCENARIO 2 fallback (broad search with no IDs), it may run before the debounced search fast path populates `event_session_id_li`. If that fallback doesn't apply the same visibility filter as the fast path, hidden items will briefly appear then disappear ("blink").
|
||||
|
||||
**Fix:** capture the filter flag as a `$derived.by` dependency in the outer closure so Svelte recreates the liveQuery instance whenever it changes — SCENARIO 2 then uses the correct filter from first render.
|
||||
|
||||
```typescript
|
||||
let lq__event_session_obj_li = $derived.by(() => {
|
||||
const ids = event_session_id_li; // drives SCENARIO 1 vs 2
|
||||
const event_id = $events_slct?.event_id;
|
||||
const qry_hidden = pres_mgmt_loc.current.qry_hidden; // extra dependency
|
||||
|
||||
return liveQuery(async () => {
|
||||
// SCENARIO 1 — specific IDs (fast path or API result)
|
||||
if (Array.isArray(ids) && ids.length > 0) {
|
||||
const results = await db.session.bulkGet(ids);
|
||||
return results.filter(Boolean);
|
||||
}
|
||||
// SCENARIO 2 — broad fallback, uses captured qry_hidden
|
||||
if (event_id && !someFilter) {
|
||||
const all = await db.session.where('event_id').equals(event_id).sortBy('name');
|
||||
return all.filter((s: any) => {
|
||||
if (qry_hidden === 'not_hidden') return !s.hide;
|
||||
if (qry_hidden === 'hidden') return !!s.hide;
|
||||
return true; // 'all'
|
||||
});
|
||||
}
|
||||
return [];
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Key rule:** anything read inside `$derived.by()`'s outer closure (but outside the `liveQuery` callback) becomes a Svelte reactive dependency. Changes to it recreate the liveQuery. Use this to synchronize filter flags that Dexie doesn't track.
|
||||
|
||||
**Also fix the API call:** use the snapshot value from `params` (captured at debounce time) rather than the live store, so rapid toggling doesn't create a mismatch between fast path and API results:
|
||||
|
||||
```typescript
|
||||
// Bad — uses live store value, can race if user toggles during pending call:
|
||||
hidden: pres_mgmt_loc.current.qry_hidden ?? 'not_hidden'
|
||||
|
||||
// Good — uses snapshot captured when handle_search_refresh was called:
|
||||
hidden: params.qry_hidden ?? 'not_hidden'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Practical Patterns from Aether (Journals & Events & IDAA Recovery Meetings)
|
||||
- Journals: The journaling pages use SWR-style background refreshes but reliably render because either (a) the page `+page.ts` blocks to populate DB for critical views, or (b) components accept `data.initial_*` fallback values until `liveQuery` emits. This hybrid approach avoids the "refresh twice" problem while keeping navigation snappy.
|
||||
- Journals broad views: if text search is empty, let the local IDB result set drive the visible list. The API can revalidate the cache in the background, but it should not replace a broad "All" view with a limited slice that hides valid rows.
|
||||
@@ -244,7 +380,7 @@ The `createLiveQueryStore` function creates a readable store that automatically
|
||||
|
||||
## SvelteKit Layout Hierarchy: Security and Execution Order
|
||||
|
||||
Understanding *when* SvelteKit code runs is critical for private-data modules like IDAA.
|
||||
Understanding _when_ SvelteKit code runs is critical for private-data modules like IDAA.
|
||||
|
||||
### Execution order on any navigation
|
||||
|
||||
@@ -280,7 +416,7 @@ future readers into thinking the parent gate alone is not sufficient.
|
||||
|
||||
### Where the actual pre-gate risk lives: `+page.ts` / `+layout.ts`
|
||||
|
||||
Universal load functions run *before* components mount and *before* layout effects
|
||||
Universal load functions run _before_ components mount and _before_ layout effects
|
||||
execute. They also fire during SvelteKit link prefetch — triggered by the user
|
||||
hovering a link, even if they never navigate. This makes them unsafe for private data:
|
||||
|
||||
@@ -364,7 +500,7 @@ If you must use non-blocking loads, you must pass the initial data to the compon
|
||||
|
||||
## The `untrack()` Reactive-Tracking Trap
|
||||
|
||||
`untrack()` is used inside `$effect` to read reactive values without registering them as tracked dependencies of that effect. This is correct for most "read-once" values (params, IDs) where you don't want the effect re-running on every change. But it has a silent failure mode: if a value you *need* to re-read is consumed inside `untrack()`, the effect becomes a one-shot and never retries when that value changes.
|
||||
`untrack()` is used inside `$effect` to read reactive values without registering them as tracked dependencies of that effect. This is correct for most "read-once" values (params, IDs) where you don't want the effect re-running on every change. But it has a silent failure mode: if a value you _need_ to re-read is consumed inside `untrack()`, the effect becomes a one-shot and never retries when that value changes.
|
||||
|
||||
### Symptom
|
||||
|
||||
@@ -428,7 +564,7 @@ Before wrapping a store read in `untrack()`, ask: **"Do I need this effect to re
|
||||
Svelte 5's `bind:` directive is more restrictive than previous versions. You can only bind to a simple **Identifier** or **MemberExpression**.
|
||||
|
||||
**❌ Invalid Pattern (Causes Compile Error):**
|
||||
Attempting to normalize a value *inside* the binding will fail.
|
||||
Attempting to normalize a value _inside_ the binding will fail.
|
||||
```svelte
|
||||
<!-- Error: Can only bind to an Identifier or MemberExpression -->
|
||||
<Launcher_menu bind:slct__event_session_id={$events_slct.event_session_id || null} />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
**Module Path:** `src/routes/events/[event_id]/(badges)/templates/`
|
||||
**API Module:** `src/lib/ae_events/ae_events__event_badge_template.ts`
|
||||
**Database Table:** `event_badge_template`
|
||||
**Last Updated:** 2026-03-02
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
---
|
||||
|
||||
@@ -114,17 +114,19 @@ corresponding `ticket_N_text` on the template provides the HTML rendered on the
|
||||
| `priority`, `sort`, `group` | int/str | Standard AE sort fields |
|
||||
| `notes` | str | Internal notes |
|
||||
|
||||
### New Field (pending backend addition)
|
||||
### Duplex / Single-Sided
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `duplex` | bool | **Planned** — when `false`, back section is hidden from print (`@media print`) |
|
||||
| `duplex` | bool | When `false`, back section is hidden from print (`@media print`) |
|
||||
|
||||
The `duplex` field controls whether the back-of-badge section renders during printing.
|
||||
When `false` (single-sided), `badge_back` gets `print:hidden` applied so only the front
|
||||
prints. The back section still displays on screen for configuration reference.
|
||||
|
||||
The first event using this system (Axonius, NYC, mid-April 2026) uses single-sided PVC
|
||||
cards on a Zebra ZC10L — `duplex` will be `false` for that event's templates.
|
||||
`duplex` is in `properties_to_save` and `show_badge_back` is derived from it in
|
||||
`ae_comp__badge_obj_view.svelte`. (Verified 2026-03-18)
|
||||
|
||||
Axonius events use `duplex = false` — single-sided printing only.
|
||||
|
||||
---
|
||||
|
||||
@@ -155,7 +157,7 @@ The print page (`print/+page.svelte`) or the badge view should conditionally add
|
||||
</svelte:head>
|
||||
```
|
||||
|
||||
This is not yet implemented — tracked as a pending Phase 1 item.
|
||||
This is implemented — `style_href` loads via `<svelte:head>` in `print/+page.svelte` and is included in `properties_to_save`. (Verified 2026-03-18)
|
||||
|
||||
### CSS Scope
|
||||
|
||||
@@ -192,6 +194,128 @@ wrapper so multiple layouts can coexist in the bundle without conflict.
|
||||
|
||||
---
|
||||
|
||||
## cfg_json Reference
|
||||
|
||||
All keys are optional. Unknown keys are preserved on save (forward-compatible). Managed via the template form's **Advanced** and **Header & Branding** sections, or directly in phpMyAdmin.
|
||||
|
||||
### Visibility
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `hide_badge_header` | bool | `false` | Hides the entire header section (image + logo/text fallback). Auto-true when `background_image_path` is set, unless explicitly overridden. |
|
||||
| `hide_badge_footer` | bool | `false` | Hides the badge type footer stripe. |
|
||||
| `hide_title` | bool | `false` | Suppresses the professional title field on the badge front. |
|
||||
| `hide_affiliations` | bool | `false` | Suppresses the affiliations field. |
|
||||
| `hide_location` | bool | `false` | Suppresses the location field. |
|
||||
|
||||
### QR Codes
|
||||
|
||||
These keys override the top-level DB fields (`show_qr_front`, `show_qr_back`) when present. Prefer setting them here rather than the top-level fields.
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `show_qr_front` | bool | `false` | Show attendee QR on badge front. |
|
||||
| `show_qr_back` | bool | `true` | Show attendee QR (+ ID text) on badge back. |
|
||||
|
||||
### Text Alignment
|
||||
|
||||
Stored under a nested `align` object.
|
||||
|
||||
```json
|
||||
"align": { "name": "left", "title": "left", "affiliations": "left", "location": "center" }
|
||||
```
|
||||
|
||||
| Key | Values | Default |
|
||||
| --- | --- | --- |
|
||||
| `align.name` | `left` \| `center` \| `right` \| `justify` | `center` |
|
||||
| `align.title` | `left` \| `center` \| `right` \| `justify` | `center` |
|
||||
| `align.affiliations` | `left` \| `center` \| `right` \| `justify` | `center` |
|
||||
| `align.location` | `left` \| `center` \| `right` \| `justify` | `center` |
|
||||
|
||||
QR alignment stored under `qr_alignment`:
|
||||
|
||||
| Key | Values | Default |
|
||||
| --- | --- | --- |
|
||||
| `qr_alignment.front` | `left` \| `center` \| `right` | `center` |
|
||||
| `qr_alignment.back` | `left` \| `center` \| `right` \| `justify` | `center` |
|
||||
|
||||
### Header Image
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `header_margin_top` | CSS length | `2rem` | Vertical offset of the header image. Negative = shift up. e.g. `"-0.25in"`, `"1rem"`. |
|
||||
| `header_border_color` | hex color | none | Bottom border drawn below the header div. **Empty = no border.** e.g. `"#FE6111"`. |
|
||||
| `header_border_width` | CSS length | `2px` | Thickness of the header bottom border. Only applied when `header_border_color` is set. |
|
||||
| `header_padding_bottom` | CSS length | none | Space between the header image and the bottom border line. e.g. `"1.45in"`. |
|
||||
|
||||
### Appearance
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `body_text_color` | hex color | `#000000` | Inline color applied to all badge body text. |
|
||||
| `bleed` | CSS length | none | Extends background image past card edges on all sides. Prevents white borders on printers that clip slightly inside the card. e.g. `"0.125in"`, `"3mm"`. |
|
||||
|
||||
### Text Zone Heights (`fit_heights`)
|
||||
|
||||
Per-layout height overrides for the auto-scaling text zones. Set any subset — unset keys fall back to the layout default. Useful when `background_image_path` is set and the designed zones don't align with code defaults.
|
||||
|
||||
```json
|
||||
"fit_heights": { "grp_name_title": "1.8in", "name": "1.4in" }
|
||||
```
|
||||
|
||||
| Key | Notes |
|
||||
|---|---|
|
||||
| `grp_name_title` | Height of the name+title container |
|
||||
| `grp_name_title_flex` | Flex distribution: `around` \| `between` \| `even` \| `center` \| `start` \| `end` |
|
||||
| `name` | Height of the name text zone |
|
||||
| `title` | Height of the title text zone |
|
||||
| `grp_aff_loc` | Height of the affiliations+location container |
|
||||
| `grp_aff_loc_flex` | Flex distribution (same values as above) |
|
||||
| `affiliations` | Height of the affiliations text zone |
|
||||
| `location` | Height of the location text zone |
|
||||
|
||||
### Punch-Out Hole Markers (`punch_holes`)
|
||||
|
||||
Enables X overlays at the physical badge clip slot positions. Slots are pre-perforated on the badge stock — the markers print on the badge so attendees know where to push them out.
|
||||
|
||||
**Slot dimensions:** 5/8″ wide × 1/8″ tall, 1/4″ from top edge, 3/8″ from left/right edges. Center slot is horizontally centered.
|
||||
|
||||
```json
|
||||
"punch_holes": { "left": true, "right": true, "center": false }
|
||||
```
|
||||
|
||||
| Key | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| `punch_holes.left` | `false` | Left clip slot marker |
|
||||
| `punch_holes.right` | `false` | Right clip slot marker |
|
||||
| `punch_holes.center` | `false` | Center clip slot marker (less common) |
|
||||
|
||||
---
|
||||
|
||||
### Controls Panel (`controls_cfg`)
|
||||
|
||||
Controls which fields appear in the print controls panel for non-trusted users, and which fields authenticated users may edit. Trusted + Edit Mode always sees and can edit all fields regardless of this config.
|
||||
|
||||
```json
|
||||
"controls_cfg": {
|
||||
"shown": ["name", "title", "affiliations"],
|
||||
"auth_editable": ["title", "affiliations", "location"]
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Type | Default |
|
||||
| --- | --- | --- |
|
||||
| `controls_cfg.shown` | `string[]` | `["name", "title", "affiliations", "location"]` |
|
||||
| `controls_cfg.auth_editable` | `string[]` | `["title", "affiliations", "location", "allow_tracking", "pronouns"]` |
|
||||
|
||||
Valid field keys: `name`, `title`, `affiliations`, `location`, `pronouns`, `allow_tracking`.
|
||||
|
||||
This config applies to the onsite print controls. Remote review currently uses
|
||||
`event.mod_badges_json.edit_permissions` instead. Consolidating or defining precedence between
|
||||
these two permission sources is tracked in `documentation/TODO__Agents.md`.
|
||||
|
||||
---
|
||||
|
||||
## Template-Derived Features (component behavior)
|
||||
|
||||
### badge_type_list → badge type select
|
||||
@@ -218,12 +342,12 @@ The `properties_to_save` array in `ae_events__event_badge_template.ts` controls
|
||||
gets cached locally. Current state — fields **NOT** in properties_to_save that exist
|
||||
in DB and may be needed:
|
||||
|
||||
- `style_href` — needed once external CSS is wired via `<svelte:head>`
|
||||
- `passcode` — not needed client-side
|
||||
- `footer_title`, `footer_left`, `footer_right` — not needed (legacy)
|
||||
- `header_background`, `footer_background` — not needed (legacy)
|
||||
- `script_src` — do not add; this field should not be used
|
||||
- `duplex` — **add when backend adds the field**
|
||||
|
||||
`duplex` is already saved to IDB and drives single-sided rendering.
|
||||
|
||||
---
|
||||
|
||||
@@ -360,6 +484,8 @@ Firefox users can use "Save to PDF" directly — it just works.
|
||||
- [x] Add `duplex` to `properties_to_save` — done. (2026-03-18 verified)
|
||||
- [x] Add `duplex`-driven suppression to `badge_back` section — done in `ae_comp__badge_obj_view.svelte`; `show_badge_back` derived from `duplex` field.
|
||||
- [x] `badge_4x6_fanfold` layout CSS created (`badge_layout_epson_4x6_fanfold.css`), imported in badge component, `@page 4in 6in` wired in print page. (2026-05-15)
|
||||
- [x] Template form expanded — `layout`, `style_href`, `badge_type_list`, `duplex`, and all `cfg_json` keys now editable via the form. (2026-06-04)
|
||||
- [x] `cfg_json.header_margin_top`, `header_border_color`, `header_border_width`, `header_padding_bottom` added — header image position and bottom border are fully configurable without a code deploy. (2026-06-04)
|
||||
- [ ] Wire `badge_type_list` from the template into the badge search filter — currently the search form uses a hardcoded list. See `ae_comp__badge_search.svelte` TODO comment.
|
||||
- [ ] `badge_4x5_fanfold` layout CSS exists but is stale (not used in 2+ years) — review against actual hardware before next use.
|
||||
- [ ] Remove dead `exhibitor_info` / `presenter_info` / `staff_info` / `vip_info` / `vote_info` `{#if}` blocks from `ae_comp__badge_obj_view.svelte` (if they were carried over from v1)
|
||||
- [ ] Improve `ae_comp__badge_template_form.svelte` to edit all relevant fields (currently minimal)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Aether Events — Badges
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
The Badges module manages event attendee records and their physical badge configurations. It supports multi-source imports, field protection for onsite edits, and multi-tier access control for self-service review.
|
||||
|
||||
---
|
||||
@@ -56,13 +58,25 @@ Aether acts as a **Pull-Only** consumer for registration data. It does not push
|
||||
|
||||
| Level | Access |
|
||||
|---|---|
|
||||
| **Authenticated** | View own badge, limited self-edit (overrides only). |
|
||||
| **Trusted** | Search all badges, view all, reprint existing badges. |
|
||||
| **Administrator** | Full CRUD, bulk operations, override any field. |
|
||||
| **Manager** | All Admin + Event/Template configuration. |
|
||||
| **Public kiosk** | View badge and perform the first print; cannot edit fields without authenticated access. |
|
||||
| **Authenticated** | Edit fields allowed by the active permission config. |
|
||||
| **Trusted** | Search all badges, view all, and correct names; reprint requires global Edit Mode. |
|
||||
| **Administrator** | Full CRUD, bulk operations, and override access. |
|
||||
| **Manager** | All Administrator capabilities plus Event/Template configuration. |
|
||||
|
||||
### Attendee Self-Service (`/review`)
|
||||
Attendees can access their own record via a passcode-gated link (typically `?passcode=...`). This allows them to verify their info and provide preferred name/title overrides before printing.
|
||||
Attendees can access their own record via a passcode-gated link (typically `?passcode=...`).
|
||||
Editable fields come from `event.mod_badges_json.edit_permissions`, with module defaults as fallback.
|
||||
|
||||
### Onsite Kiosk (`/print`)
|
||||
The print controls update the badge preview live. Authenticated field editing is controlled by the
|
||||
badge template's `cfg_json.controls_cfg` (`shown` and `auth_editable`). Trusted + global Edit Mode
|
||||
overrides the template config and exposes all controls. This differs from the review page's
|
||||
event-level permission source; consolidation is an active follow-up.
|
||||
|
||||
### Review-Link Email
|
||||
Email Link actions are placeholders and do not currently send mail. When delivery is implemented,
|
||||
it must use the imported `event_badge.email` address, never attendee-editable `email_override`.
|
||||
|
||||
---
|
||||
|
||||
@@ -71,11 +85,41 @@ Attendees can access their own record via a passcode-gated link (typically `?pas
|
||||
- **Fulltext Search:** Matches against a consolidated `default_qry_str` (Name, email, IDs).
|
||||
- **Multi-Word Logic:** Queries like "Scott Idem" are split and treated as `LIKE %Scott% AND LIKE %Idem%`.
|
||||
- **QR Scan Search:** Scanning an attendee's QR code (from a confirmation email or old badge) immediately jumps to their record.
|
||||
- **Advanced Filters:** Filter by Badge Type, Printed Status, or Affiliations (Staff only).
|
||||
- **Advanced Filters (Trusted + Edit Mode):** Badge Type, Printed Status, Affiliations, Sort Order.
|
||||
|
||||
### Visibility Filter (Trusted + Edit Mode)
|
||||
|
||||
Three-option select controlling which records are shown:
|
||||
|
||||
| Option | Who can set it | Effect |
|
||||
| --- | --- | --- |
|
||||
| **Default** | Any | Hides hidden and disabled badges |
|
||||
| **Show Hidden** | Trusted | Shows hidden badges alongside normal ones |
|
||||
| **Show Disabled + Hidden** | Manager only | Shows all records regardless of enable/hide flags |
|
||||
|
||||
### Result Limit Stepper (Edit Mode)
|
||||
|
||||
Controls the maximum number of results returned. Only visible in edit mode.
|
||||
|
||||
| Access Level | Range | Step |
|
||||
| --- | --- | --- |
|
||||
| Below Trusted | Fixed 25 | — |
|
||||
| Trusted | 25 – 250 | 25 |
|
||||
| Manager+ | 25 – 2550 | 25 up to 250, then 100 |
|
||||
|
||||
### Badge Type Filter — Known Limitation
|
||||
|
||||
The badge type dropdown in the search form uses a **hardcoded list**, not the template's `badge_type_list`. This means the codes shown in the filter may not match the codes used by the current event's template. This is a known gap — the fix requires passing the template object into the search component. Until resolved, staff can still search by name/email and filter results manually.
|
||||
|
||||
---
|
||||
|
||||
## Print Tracking
|
||||
## Print Rendering and Tracking
|
||||
|
||||
- The canonical badge render uses binary-search text fitting for name, title, affiliations, and location.
|
||||
- Template `show_qr_front`/`show_qr_back` settings control QR placement.
|
||||
- Template `style_href` loads event-specific CSS on the print page.
|
||||
- Template `duplex = false` suppresses the badge back for single-sided stock.
|
||||
- Chromium PDF proofing requires margins set to None; physical printer paper size remains driver-controlled.
|
||||
|
||||
Aether tracks the lifecycle of every physical badge to prevent unauthorized reprints and monitor kiosk activity.
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Aether Events — Launcher (Podium Display)
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
The Launcher module provides the podium display interface that runs on each session room's kiosk machine. It is designed to work in standard browsers but is optimized for the **Aether Desktop (Electron)** native shell.
|
||||
|
||||
---
|
||||
|
||||
95
documentation/MODULE__AE_Events_Launcher_Config_Menu.md
Normal file
95
documentation/MODULE__AE_Events_Launcher_Config_Menu.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Aether Events — Launcher Configuration Menu (Inventory)
|
||||
|
||||
> **Status:** Current Reference (v3.0)
|
||||
> **Last Updated:** 2026-06-12
|
||||
> **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).
|
||||
@@ -1,6 +1,7 @@
|
||||
# Aether Events — Exhibitor Leads Module (v3)
|
||||
|
||||
**Status:** Implemented and ready for demo. Core lead capture flow works end-to-end.
|
||||
**Last Updated:** 2026-06-12
|
||||
**Platform:** PWA only — mobile-first, offline-capable.
|
||||
**Target users:** Conference exhibitors scanning attendee badges at their booths.
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Aether Events — Presentation Management
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
The Presentation Management module handles the full lifecycle of conference content: sessions, presentations, presenters, presentation files, and room/location assignments. It serves as the "Back Office" interface for event staff.
|
||||
|
||||
---
|
||||
|
||||
39
documentation/MODULE__AE_IDAA_Archives.md
Normal file
39
documentation/MODULE__AE_IDAA_Archives.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Aether IDAA — Archives Module
|
||||
|
||||
**Status:** Active (private)
|
||||
**Last Updated:** 2026-06-12
|
||||
**Routes:** `src/routes/idaa/(idaa)/archives/`
|
||||
**Underlying library:** `src/lib/ae_archives/`
|
||||
|
||||
IDAA Archives provides authenticated access to archival documents and media for the IDAA community.
|
||||
|
||||
---
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
- List, view, and edit archive records (permission-gated).
|
||||
- Upload and manage archive content files.
|
||||
- Render media/content viewers for archived assets.
|
||||
|
||||
---
|
||||
|
||||
## Security Requirements
|
||||
|
||||
- All IDAA archive content is private.
|
||||
- Auth guard must remain enforced for all archive routes and child views.
|
||||
- Do not add pre-gate data loading in universal `+page.ts`/`+layout.ts` paths.
|
||||
|
||||
---
|
||||
|
||||
## Route Map
|
||||
|
||||
- `/idaa/archives`
|
||||
- `/idaa/archives/[archive_id]`
|
||||
|
||||
---
|
||||
|
||||
## Related Docs
|
||||
|
||||
- `documentation/CLIENT__IDAA_and_customized_mods.md`
|
||||
- `documentation/AE__Permissions_and_Security.md`
|
||||
- `documentation/REFERENCE__Common_Agent_Mistakes.md`
|
||||
39
documentation/MODULE__AE_IDAA_Bulletin_Board.md
Normal file
39
documentation/MODULE__AE_IDAA_Bulletin_Board.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Aether IDAA — Bulletin Board Module
|
||||
|
||||
**Status:** Active (private)
|
||||
**Last Updated:** 2026-06-12
|
||||
**Routes:** `src/routes/idaa/(idaa)/bb/`
|
||||
**Underlying library:** `src/lib/ae_posts/`
|
||||
|
||||
The IDAA Bulletin Board (BB) is a private community posting and comment system for authenticated IDAA users.
|
||||
|
||||
---
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
- Post list and post detail flows.
|
||||
- Comment create/edit workflows.
|
||||
- Priority/visibility and sort behavior aligned with IDAA privacy and moderation rules.
|
||||
|
||||
---
|
||||
|
||||
## Security Requirements
|
||||
|
||||
- All BB routes are private/authenticated.
|
||||
- Do not weaken layout-level auth gating.
|
||||
- Treat any public exposure of BB data as Sev-1.
|
||||
|
||||
---
|
||||
|
||||
## Route Map
|
||||
|
||||
- `/idaa/bb`
|
||||
- `/idaa/bb/[post_id]`
|
||||
|
||||
---
|
||||
|
||||
## Related Docs
|
||||
|
||||
- `documentation/CLIENT__IDAA_and_customized_mods.md`
|
||||
- `documentation/AE__Permissions_and_Security.md`
|
||||
- `documentation/REFERENCE__Common_Agent_Mistakes.md`
|
||||
41
documentation/MODULE__AE_IDAA_Recovery_Meetings.md
Normal file
41
documentation/MODULE__AE_IDAA_Recovery_Meetings.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Aether IDAA — Recovery Meetings Module
|
||||
|
||||
**Status:** Active (private)
|
||||
**Last Updated:** 2026-06-12
|
||||
**Routes:** `src/routes/idaa/(idaa)/recovery_meetings/`
|
||||
**Underlying library:** `src/lib/ae_events/` (repurposed)
|
||||
|
||||
Recovery Meetings adapts Events module primitives for IDAA meeting discovery, filtering, viewing, and trusted editing.
|
||||
|
||||
---
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
- Meeting search/list/detail flows (IDB-first with API revalidation).
|
||||
- Filter and sort controls for member workflows.
|
||||
- Trusted/staff edit flows for meeting records.
|
||||
- Modal and direct-page detail/edit entry paths.
|
||||
|
||||
---
|
||||
|
||||
## Security and Data Integrity
|
||||
|
||||
- Module is private/authenticated.
|
||||
- Keep error state distinct from empty-result state.
|
||||
- When persisted object shape changes, update `IDB_CONTENT_VERSIONS` wiring/version.
|
||||
|
||||
---
|
||||
|
||||
## Route Map
|
||||
|
||||
- `/idaa/recovery_meetings`
|
||||
- `/idaa/recovery_meetings/[event_id]`
|
||||
|
||||
---
|
||||
|
||||
## Related Docs
|
||||
|
||||
- `documentation/CLIENT__IDAA_and_customized_mods.md`
|
||||
- `documentation/PROJECT__IDAA_Stores_Svelte5_Migration_2026.md`
|
||||
- `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md`
|
||||
- `documentation/REFERENCE__Common_Agent_Mistakes.md`
|
||||
35
documentation/MODULE__AE_IDAA_Video_Conferences.md
Normal file
35
documentation/MODULE__AE_IDAA_Video_Conferences.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Aether IDAA — Video Conferences Module
|
||||
|
||||
**Status:** Active (private)
|
||||
**Last Updated:** 2026-06-12
|
||||
**Routes:** `src/routes/idaa/(idaa)/video_conferences/`
|
||||
|
||||
Video Conferences provides IDAA’s Jitsi meeting access experience within the IDAA private module.
|
||||
|
||||
---
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
- Render conference links and meeting access UX.
|
||||
- Support breakout links from Novi iframe context.
|
||||
- Preserve required URL bootstrap parameters when generating breakout URLs.
|
||||
|
||||
---
|
||||
|
||||
## Breakout Link Requirement
|
||||
|
||||
When opening outside the iframe, ensure required keys (for example site key and Novi identity UUID) remain in the URL so users do not hit access denial flows.
|
||||
|
||||
---
|
||||
|
||||
## Security Requirements
|
||||
|
||||
- Module is private/authenticated.
|
||||
- Avoid exposing conference route details publicly.
|
||||
|
||||
---
|
||||
|
||||
## Related Docs
|
||||
|
||||
- `documentation/CLIENT__IDAA_and_customized_mods.md`
|
||||
- `documentation/AE__Permissions_and_Security.md`
|
||||
81
documentation/MODULE__AE_Journals.md
Normal file
81
documentation/MODULE__AE_Journals.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Aether Journals — Module Overview
|
||||
|
||||
**Status:** Active module
|
||||
**Last Updated:** 2026-06-12
|
||||
**Library:** `src/lib/ae_journals/`
|
||||
**Routes:** `src/routes/journals/`
|
||||
|
||||
This module manages private personal journals and journal entries with offline-first behavior and Svelte 5 runes patterns.
|
||||
|
||||
---
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
- Journal and journal-entry CRUD via V3 API wrappers.
|
||||
- Dexie-backed local cache with liveQuery-driven UI updates.
|
||||
- Private/passcode-aware access behavior and client-side content encryption.
|
||||
- Quick Add, Append/Prepend, import/export, and entry auto-save workflows.
|
||||
- Tabbed module, journal, and entry configuration modals.
|
||||
|
||||
---
|
||||
|
||||
## Key Data Objects
|
||||
|
||||
- `journal`
|
||||
- `journal_entry`
|
||||
|
||||
Common fields and behavior follow Aether object conventions (`code`, `name`, `enable`, `hide`, `priority`, `sort`, `cfg_json`, `data_json`).
|
||||
|
||||
---
|
||||
|
||||
## Storage and Reactivity
|
||||
|
||||
- Local cache: Dexie tables in journals DB layer.
|
||||
- UI reactivity: Svelte 5 runes (`$state`, `$derived`, `$effect`) plus liveQuery wrappers (`lq__*`, `lqw__*`).
|
||||
- Persisted module settings: see config map.
|
||||
|
||||
Related config map:
|
||||
- `documentation/MODULE__AE_Journals_Config_Map.md`
|
||||
|
||||
---
|
||||
|
||||
## Implemented Entry Workflows
|
||||
|
||||
- Quick Add creates a plaintext note in a selected journal without opening the full editor.
|
||||
- Append/Prepend injects timestamped content into an existing entry.
|
||||
- Bulk import creates entries from parsed files; export supports centralized templates.
|
||||
- Entry edits support debounced auto-save when `journals_loc.entry.auto_save` is enabled.
|
||||
- Full entry saves encrypt `content` into `content_encrypted` when the entry's `private`
|
||||
flag is enabled; disabling `private` clears encrypted content/history fields.
|
||||
- The non-reactive `decrypt_journal_entry()` helper isolates decryption from Svelte effects.
|
||||
- Entry configuration exposes Actions, Metadata, Security, and JSON views. Trusted users
|
||||
can Remove (disable); managers and administrators can hard Delete.
|
||||
|
||||
## Current Security Limitations
|
||||
|
||||
- `passcode_hash` is editable but is not compared as secondary authentication before
|
||||
decryption. This remains an active task.
|
||||
- Quick Add explicitly creates entries with `private: false`; import creates plaintext
|
||||
content without setting encryption fields. These paths do not currently offer E2EE.
|
||||
- Successful decryption currently logs a short plaintext preview to the browser console.
|
||||
Removal is tracked as an active privacy fix.
|
||||
- Outbound email sharing is not implemented and requires a product/security decision
|
||||
because journal content is private.
|
||||
|
||||
---
|
||||
|
||||
## Access and Privacy
|
||||
|
||||
Journals contain private personal data. The Journals layout renders module content only when
|
||||
the user has `user_id`, `person_id`, and `trusted_access`. Treat all journal and journal-entry
|
||||
routes, API responses, decrypted state, logs, exports, and future sharing features as private.
|
||||
|
||||
---
|
||||
|
||||
## Related Docs
|
||||
|
||||
- `documentation/archive/PROJECT__AE_UI_Journals_Module_Update_2026.md`
|
||||
- `documentation/TODO__Agents.md`
|
||||
- `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md`
|
||||
- `documentation/GUIDE__AE_API_V3_for_Frontend.md`
|
||||
- `documentation/BOOTSTRAP__AI_Agent_Quickstart.md`
|
||||
@@ -1,5 +1,7 @@
|
||||
# Aether Journals: Configuration & Settings Map
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
This document tracks all available settings across the three levels of the Journals module.
|
||||
|
||||
## 1. Module Level (Global)
|
||||
@@ -51,9 +53,23 @@ This document tracks all available settings across the three levels of the Journ
|
||||
| `sort` | integer | Manual sort order weight. | Manual (Done) |
|
||||
| `archive_on` | datetime | Scheduled date for automatic archiving. | Manual (Done) |
|
||||
| `private` | boolean | Trigger for E2EE (Encryption). | Manual (Done) |
|
||||
| `content_encrypted` | encrypted string | Encrypted entry content written during a full save when `private` is enabled. | Generated on save |
|
||||
| `history_encrypted` | encrypted string | Encrypted entry history when history encryption is available. | Generated on save |
|
||||
| `passcode_hash` | string | Entry-level secondary-auth field; comparison logic is not yet implemented. | Manual (Done) |
|
||||
| `alert` | boolean | Trigger for visual "Alert" state. | Manual (Done) |
|
||||
| `group` | string | Grouping key for the list view. | Manual (JSON only) |
|
||||
|
||||
## Encryption Behavior and Gaps
|
||||
|
||||
1. Full entry saves combine the journal `passcode` and `private_passcode` to encrypt
|
||||
plaintext content when the entry's `private` flag is enabled.
|
||||
2. Decryption prefers a passcode typed in the current session, then falls back to the
|
||||
journal `private_passcode`; the journal `passcode` is combined with that private key.
|
||||
3. `passcode_hash` secondary-auth comparison is pending and must not be described as enforced.
|
||||
4. Quick Add currently forces `private: false`, and bulk import creates plaintext entries
|
||||
without encryption fields. Use the full editor to enable encryption until those workflows
|
||||
are updated.
|
||||
|
||||
## 📐 Data Normalization Rules
|
||||
To prevent infinite reactivity loops and trivial save cycles, the following normalizations are applied before comparison:
|
||||
1. **Strings:** Trimmed and `null` treated as `""`.
|
||||
@@ -1,8 +1,9 @@
|
||||
# Project: Pres Mgmt Config Cleanup & Config UI
|
||||
|
||||
**Status:** Planning / Ready to Execute
|
||||
**Priority:** High (BGH conference in ~2 weeks; only one active event using pres_mgmt)
|
||||
**Created:** 2026-04-02
|
||||
**Status:** 🟢 Complete — all Implementation Steps and known regressions resolved
|
||||
**Priority:** Low (maintenance/monitoring only — re-open if new sync inconsistencies surface)
|
||||
**Created:** 2026-04-02
|
||||
**Last Updated:** 2026-06-16 (`lock_config` removed; sync is now fully unconditional)
|
||||
**Related:** `TODO__Agents.md`, `PROJECT__Stores_Svelte5_Migration.md`
|
||||
|
||||
---
|
||||
@@ -17,7 +18,7 @@ The `event.mod_pres_mgmt_json` config grew organically across several conference
|
||||
- Duplicate keys with different names (`file_purpose_option_kv` = `file_purpose_option_li`)
|
||||
- Dead config (`HOLD__*` prefix)
|
||||
- Type inconsistency (`label__person_external_id: false` vs `"LCI member ID"` string)
|
||||
- Keys in the DB not consumed by `sync_config__event_pres_mgmt()`
|
||||
- Keys in the DB not consumed by `sync_config__event_pres_mgmt()`
|
||||
- Bug: `label__session_poc_name_short` is read then immediately overwritten (line 970-972 in ae_events__event.ts)
|
||||
- `hide_launcher_link` / `hide_launcher_link_legacy` missing the `__` separator (inconsistent)
|
||||
- `show_content__presentation_description` uses a third naming convention
|
||||
@@ -60,8 +61,8 @@ persisted store which is part of the paused Svelte 5 migration.
|
||||
|
||||
```typescript
|
||||
interface PressMgmtRemoteCfg {
|
||||
// System
|
||||
lock_config: boolean; // true = force remote→local sync (prevent user overrides)
|
||||
// No system/lock_config field — every field below syncs unconditionally,
|
||||
// to every browser, on every event load. Removed 2026-06-16; see note below.
|
||||
|
||||
// Labels (event-specific terminology overrides)
|
||||
label__person_external_id: string | null; // default: 'External ID'
|
||||
@@ -95,6 +96,7 @@ interface PressMgmtRemoteCfg {
|
||||
show__email_access_link: boolean;
|
||||
show__launcher_link: boolean;
|
||||
show__launcher_link_legacy: boolean;
|
||||
show__session_li_poc_field: boolean; // POC column in session list/table; hide__session_poc still wins
|
||||
|
||||
// Requirements
|
||||
require__presenter_agree: boolean;
|
||||
@@ -135,7 +137,7 @@ interface PressMgmtRemoteCfg {
|
||||
|
||||
## New Svelte 5 Local Store
|
||||
|
||||
**Do NOT touch `events_loc` or the paused Svelte 5 migration.**
|
||||
**Do NOT touch `events_loc` or the paused Svelte 5 migration.**
|
||||
Instead, create a standalone store for pres_mgmt local config.
|
||||
|
||||
**File:** `src/lib/stores/ae_events_stores__pres_mgmt.svelte.ts`
|
||||
@@ -162,9 +164,9 @@ AFTER: pres_mgmt_loc.current.hide__session_code
|
||||
|
||||
## Config UI Page
|
||||
|
||||
**Route:** `/events/[event_id]/(pres_mgmt)/pres_mgmt/config/`
|
||||
**Access:** `$ae_loc.manager_access` only
|
||||
**Button visibility:** Edit mode only (`$ae_loc.edit_mode`)
|
||||
**Route:** `/events/[event_id]/(pres_mgmt)/pres_mgmt/config/`
|
||||
**Access:** `$ae_loc.manager_access` only
|
||||
**Button visibility:** Edit mode only (`$ae_loc.edit_mode`)
|
||||
|
||||
### Page behavior
|
||||
- Loads `event.mod_pres_mgmt_json` fresh from API on page open
|
||||
@@ -174,15 +176,27 @@ AFTER: pres_mgmt_loc.current.hide__session_code
|
||||
|
||||
### Form sections (grouped)
|
||||
|
||||
1. **System** — `lock_config`
|
||||
2. **Labels** — `label__*` fields (text inputs, nullable)
|
||||
3. **Session Visibility** — `hide__session_*` toggles
|
||||
4. **Presenter Visibility** — `hide__presenter_*` toggles
|
||||
5. **Presentation Visibility** — `hide__presentation_*` toggles
|
||||
6. **Code Visibility** — `hide__*_code` toggles
|
||||
No System section — there is no `lock_config` (removed 2026-06-16).
|
||||
|
||||
1. **Labels** — `label__*` fields (text inputs, nullable)
|
||||
2. **Code Visibility** — `hide__*_code` toggles
|
||||
3. **Session Display** — `hide__session_description/location/msg` (just the simple ones —
|
||||
POC split out below 2026-06-16 for clarity)
|
||||
4. **POC Settings** (Point of Contact) — `hide__session_poc` rendered as a visually distinct
|
||||
master switch, with `show__session_li_poc_field` / `hide__session_poc_biography` /
|
||||
`hide__session_poc_profile` / `hide__session_poc_profile_pic` indented and
|
||||
auto-`disabled` underneath it when the master switch is on. Was previously flat in
|
||||
"Session Field Visibility" with `hide__session_poc` and `show__session_li_poc_field`
|
||||
sitting adjacent with opposite checked-state polarity ("Hide POC" checked = remove,
|
||||
"Show POC Column" checked = add) — confusing, per direct user feedback. Splitting it
|
||||
into its own section with explicit master/sub-setting hierarchy fixed it without
|
||||
touching the underlying field semantics (still `hide__`/`show__` per the naming
|
||||
convention — only the *presentation* changed).
|
||||
5. **Presenter Visibility** — `hide__presenter_*` toggles
|
||||
6. **Presentation Visibility** — `hide__presentation_*` toggles
|
||||
7. **Opt-in Features** — `show__*` toggles
|
||||
8. **Requirements** — `require__presenter_agree`, `require__session_agree`
|
||||
9. **Navigation Limits** — `limit__navigation`, `limit__options`
|
||||
9. **Navigation Limits** — `limit__navigation` (`limit__options` removed — YAGNI)
|
||||
10. **File Purpose Config** — `file_purpose_option_kv` (JSON editor or structured form)
|
||||
11. **Report Visibility** — `hide__report_kv` (key-value toggles)
|
||||
|
||||
@@ -201,14 +215,136 @@ Safe and backward compatible — old DB records fall through to `?? false` defau
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
- [ ] **Step 1** — Define `PressMgmtRemoteCfg` TypeScript interface (new file or in `ae_events__event.ts`)
|
||||
- [ ] **Step 2** — New `ae_events_stores__pres_mgmt.svelte.ts` with `PersistedState`; add version gate to `store_versions.ts`
|
||||
- [ ] **Step 3** — Rewrite `sync_config__event_pres_mgmt()` in `ae_events__event.ts` to use canonical keys and write to the new store
|
||||
- [ ] **Step 4** — Build config UI page at `(pres_mgmt)/pres_mgmt/config/+page.svelte` (manager_access + edit_mode gated)
|
||||
- [ ] **Step 5** — Strip `ae_comp__event_settings_pres_mgmt_form.svelte` from settings page (or replace with a link to new page)
|
||||
- [ ] **Step 6** — Migrate all `$events_loc.pres_mgmt.*` references in pres_mgmt templates to `pres_mgmt_loc.current.*`
|
||||
- [ ] **Step 7** — Update BGH (and any other active events) via new UI
|
||||
- [ ] **Step 8** — `npx svelte-check` clean; commit
|
||||
- [x] **Step 1** — Define `PressMgmtRemoteCfg` TypeScript interface (new file or in `ae_events__event.ts`)
|
||||
- [x] **Step 2** — New `ae_events_stores__pres_mgmt.svelte.ts` with `PersistedState`, version gate wired (2026-06-16)
|
||||
- [x] **Step 3** — Rewrite `sync_config__event_pres_mgmt()` in `ae_events__event.ts` to use canonical keys; syncs unconditionally, no `lock_config` gate (2026-06-16)
|
||||
- [x] **Step 4** — Build config UI page at `(pres_mgmt)/pres_mgmt/config/+page.svelte` (manager_access + edit_mode gated)
|
||||
- [x] **Step 5** — `ae_comp__event_settings_pres_mgmt_form.svelte` moved to trash (2026-06-16) — it was already fully orphaned (zero imports anywhere); the settings page already links to the canonical Config page with a raw-JSON fallback
|
||||
- [x] **Step 6** — Migrate all `$events_loc.pres_mgmt.*` references in pres_mgmt templates to `pres_mgmt_loc.current.*`
|
||||
- [x] **Step 7** — Active events' `lock_config` keys are now just inert orphan data in the DB, ignored by the sync function — no migration needed, nothing to update
|
||||
- [x] **Step 8** — `npx svelte-check` clean
|
||||
|
||||
### Regression Fixes Needed (2026-06-12 Audit)
|
||||
|
||||
- [x] **`hide__launcher_link_legacy` removed entirely** (other agent's "config schema cleanup
|
||||
phase 2" commit, 2026-06-16) — Flask launcher is fully retired, no longer hard-coded or
|
||||
present anywhere in `PressMgmtRemoteCfg` / `PresMgmtLocState` / the sync function.
|
||||
- [x] **`hide__launcher_link*` / `show__launcher_link` local/remote conflict resolved
|
||||
(2026-06-16)** — kept separate (they serve different purposes: `hide__launcher_link`
|
||||
gates the launcher link *content*, `show__launcher_link` gates the manual toggle
|
||||
*button*'s visibility), but `show__launcher_link` was never actually assigned by
|
||||
`sync_config__event_pres_mgmt()` — only its inverse `hide__launcher_link` was. So the
|
||||
toggle button's `show__launcher_link || trusted_access` gate (in
|
||||
`ae_comp__events_menu_opts.svelte`, `event_page_menu.svelte`,
|
||||
`location_page_menu.svelte`) always collapsed to trusted-only, ignoring the admin's
|
||||
setting. Added the missing `loc.show__launcher_link = ...` assignment right next to
|
||||
`hide__launcher_link` in the lock-synced block.
|
||||
- [x] **`AE_PRES_MGMT_LOC_VERSION` properly wired into `store_versions.ts` (2026-06-16)**
|
||||
— the other agent's commit bumped this constant to 2 claiming it "forces a localStorage
|
||||
reset," but `_check_and_wipe()` was never actually called for `ae_pres_mgmt_loc`, and
|
||||
even if it had been, the store's serializer never wrote a `__version` field for it to
|
||||
compare against — so the bump was a complete no-op. Fixed: `ae_events_stores__pres_mgmt.svelte.ts`'s
|
||||
custom serializer now stamps `__version` on every write, and `store_versions.ts` calls
|
||||
`_check_and_wipe('ae_pres_mgmt_loc', AE_PRES_MGMT_LOC_VERSION)`. Side effect: every
|
||||
browser's existing `ae_pres_mgmt_loc` (no `__version` ever written before) will wipe
|
||||
once on next load and resync clean from the remote config — this is expected and fine.
|
||||
**Found the same bug already live in `ae_leads_loc`** (actively wiping leads users' local
|
||||
prefs on *every* page load, not just once) and fixed it the same way — see
|
||||
`ae_events_stores__leads.svelte.ts`. `badges_loc`/`launcher_loc`/`events_auth_loc` have
|
||||
version constants declared but not wired into `_check_and_wipe()` at all (dormant, not
|
||||
actively harmful) — not fixed, flagged for whoever picks that up next.
|
||||
- [x] **POC column local/remote conflict fixed (2026-06-16)** — `show__session_li_poc_field` was
|
||||
local-only (never synced) and the session-list-table prop computation ignored the admin's
|
||||
`hide__session_poc` master switch entirely. Fixed: added `show__session_li_poc_field` to
|
||||
`PressMgmtRemoteCfg` + Config UI (Session Field Visibility) + `sync_config__event_pres_mgmt()`
|
||||
lock-synced block; list/table column visibility is now
|
||||
`hide__session_poc || !show__session_li_poc_field` in `pres_mgmt/+page.svelte` and
|
||||
`locations/ae_comp__event_location_obj_li.svelte`. The local per-browser "Show/Hide POC
|
||||
Column" toggle buttons in `ae_comp__events_menu_opts.svelte` and `event_page_menu.svelte`
|
||||
were removed — the field is lock-synced from the per-event Config page now, same as the
|
||||
other session field visibility toggles.
|
||||
- [x] **Presenter QR matched to session QR pattern (2026-06-16)** — `show__session_qr` /
|
||||
`show__presenter_qr` are the admin-set **global default for everyone**, signed in or not.
|
||||
`show_content__*_qr` is a **trusted-staff-only local override**, used when the admin has
|
||||
NOT enabled QR globally. The session QR side already worked this way (fixed earlier today);
|
||||
presenter QR was still on the old buggy logic requiring `show_content__presenter_qr` to be
|
||||
true even when the admin had enabled it for everyone, which non-trusted users (presenters)
|
||||
have no way to set. Fixed `presenter_view.svelte` (generation effect + display block) to
|
||||
`show__presenter_qr || (trusted_access && show_content__presenter_qr)`. Also corrected two
|
||||
toggle-button visibility bugs found along the way: `ae_comp__events_menu_opts.svelte` was
|
||||
showing the QR toggle to all `authenticated_access` users (not just trusted) whenever the
|
||||
admin had enabled QR globally, even though the toggle had no effect for them; and
|
||||
`presenter_page_menu.svelte`'s QR toggle was gated to `administrator_access`, hiding it from
|
||||
plain Trusted onsite staff entirely. Both now use the canonical pattern from
|
||||
`session_page_menu.svelte`: `trusted_access && !show__*_qr`.
|
||||
- [x] **Missing `sync_config__event_pres_mgmt()` calls on deep-link entry pages (2026-06-16)**
|
||||
— root cause of the presenter QR bug above: a browser landing directly on the presenter
|
||||
page (e.g. via a presenter sign-in link) never synced remote config into
|
||||
`pres_mgmt_loc.current` at all, since only `pres_mgmt/+page.svelte` and
|
||||
`session/[session_id]/+page.svelte` called the sync function. Every "always synced" and
|
||||
"lock-synced" field (QR enables, POC visibility, code visibility, labels, etc.) silently
|
||||
stayed at hardcoded local defaults on that browser regardless of admin Config page
|
||||
settings. Added the same sync `$effect` (mirroring the existing comment/pattern in
|
||||
`session/[session_id]/+page.svelte`) to `presenter/[presenter_id]/+page.svelte`,
|
||||
`locations/+page.svelte`, `location/[event_location_id]/+page.svelte`, and
|
||||
`reports/+page.svelte`. Any new pres_mgmt page that can be a first-load entry point
|
||||
(i.e. not always reached via `/pres_mgmt` or `/session/[id]` first) needs this same block.
|
||||
- [x] **Config page save was a race, not deterministic (2026-06-16)** — after PATCHing
|
||||
`mod_pres_mgmt_json`, the save handler only called `load_ae_obj_id__event()` (SWR —
|
||||
returns the stale Dexie cache immediately, refreshes from the API in the background,
|
||||
*not awaited*) and assumed that "picked up the new config." It never actually called
|
||||
`sync_config__event_pres_mgmt()` itself. Whether the editor's own browser reflected the
|
||||
change depended entirely on winning a race against an un-awaited background fetch —
|
||||
explains why specific just-changed fields (QR, POC column, profile-pic visibility, one
|
||||
report key) intermittently looked stale even to the admin who just saved them, while
|
||||
older unchanged fields stayed correct. Fixed: the save handler now calls
|
||||
`sync_config__event_pres_mgmt({ pres_mgmt_cfg_remote: draft })` directly with the
|
||||
just-saved draft, so the editing browser updates instantly with no race. (Kept the
|
||||
`load_ae_obj_id__event()` call too, with its default `try_cache: true` — that's what
|
||||
propagates the fresh record to Dexie for *other* browsers/tabs. Do not pass
|
||||
`try_cache: false` there — that skips the Dexie write entirely, see the documented
|
||||
"try_cache: false Bug" in `GUIDE__SvelteKit2_Svelte5_DexieJS.md`.)
|
||||
- [x] **Removed dead "Lock Config" Sync/Unlink toggle (2026-06-16)** — a Manager-only
|
||||
button in the sign-in panel (`e_app_access_type.svelte`) wrote to
|
||||
`$ae_loc.lock_config`/`sync_local_config` and `pres_mgmt_loc.current.lock_config`/
|
||||
`sync_local_config`. Confirmed via full-repo grep that none of those four fields are
|
||||
read anywhere. It also confusingly shared the name "Lock Config" with the real,
|
||||
functional checkbox on the Pres Mgmt Config page (`draft.lock_config`, part of
|
||||
`PressMgmtRemoteCfg`, which actually gates `sync_config__event_pres_mgmt()`'s
|
||||
remote→local sync). Removed the button and the now-fully-orphaned
|
||||
`lock_config`/`sync_local_config` fields from `PresMgmtLocState`. Left
|
||||
`$ae_loc.lock_config`/`sync_local_config` (the general app store) alone — `lock_config`
|
||||
was never even in `ae_loc`'s declared defaults (a phantom field created ad-hoc by the
|
||||
dead button), and `ae_loc.sync_local_config` is out of scope for a pres_mgmt-only pass;
|
||||
defer to the planned `ae_loc` migration in `PROJECT__Stores_Svelte5_Migration.md`.
|
||||
- [x] **`lock_config` removed from `PressMgmtRemoteCfg` entirely (2026-06-16)** — the real
|
||||
one this time (the item above was a different, dead local-only mirror of the same name).
|
||||
This was the actual root cause of the "POC column / Hide POC sometimes works" reports.
|
||||
`lock_config` made roughly half of `sync_config__event_pres_mgmt()`'s fields conditional:
|
||||
they only updated in a given browser if `lock_config` happened to be `true` at the exact
|
||||
moment that browser last synced. A field's local value therefore depended on the *history*
|
||||
of saves, not the current setting — undebuggable from the UI, and easy to corrupt by
|
||||
toggling Lock Config off even briefly during testing. Checked the DB: every event, old and
|
||||
new, already had `lock_config: true` — the "unlocked, per-browser preference" use case it
|
||||
was built for has never actually been used. Removed it from `PressMgmtRemoteCfg`,
|
||||
`PresMgmtLocState`, the sync function (no more conditional block — every field syncs
|
||||
unconditionally, same as labels/Require Agreements always did), and the Config page UI
|
||||
(no more System section). Old DB records with a `lock_config` key are simply ignored now,
|
||||
same as any other removed key. Moved the now-fully-orphaned
|
||||
`ae_comp__event_settings_pres_mgmt_form.svelte` to trash in the same pass (Step 5).
|
||||
- [x] **Location column ignored the admin's "Hide Location" setting (2026-06-16)** — same
|
||||
bug pattern as the POC column fix earlier, just missed for Location. The Session Search
|
||||
results table's Location column prop only ever read the local-only, never-synced
|
||||
`hide__session_li_location_field` — it never looked at the admin-synced
|
||||
`hide__session_location` (Config page → Session Field Visibility → Hide Location) at all.
|
||||
So the column always showed regardless of that setting or the user's permission level,
|
||||
exactly as reported. Fixed in `pres_mgmt/+page.svelte`:
|
||||
`hide__session_location || hide__session_li_location_field`. The two other usages of this
|
||||
component (`locations/ae_comp__event_location_obj_li.svelte`,
|
||||
`location/[event_location_id]/+page.svelte`) already hardcode `hide__session_location={true}`
|
||||
— correct, since you're already on that location's own page. Worth auditing the other
|
||||
per-event hide__* fields for the same "admin field exists but the list/table prop
|
||||
computation never reads it" gap if more reports come in.
|
||||
|
||||
### Step 6 scope (mechanical find-replace)
|
||||
|
||||
@@ -227,8 +363,11 @@ The `$events_loc.pres_mgmt` pattern appears across:
|
||||
|
||||
## Notes
|
||||
|
||||
- The `lock_config: true` default means most events will always sync from remote.
|
||||
This is intentional — it prevents presenter laptops from drifting into different configs.
|
||||
- All pres_mgmt display config now always syncs from remote, unconditionally, to every
|
||||
browser, on every event load (no `lock_config` toggle — removed 2026-06-16). This is
|
||||
intentional — it prevents presenter laptops from drifting into different configs, which
|
||||
is exactly what `lock_config: true` was meant to guarantee, minus the conditional that
|
||||
made it fragile.
|
||||
- `file_purpose_option_kv` may need a structured editor (not raw JSON) to be usable.
|
||||
Consider a simple key-value form row per purpose type for Phase 2.
|
||||
- QR link keys (`hide__presenter_qr_link`, `hide__session_qr_link`) appeared in LCI config
|
||||
|
||||
202
documentation/PROJECT__AE_Obj_Field_Editor_New.md
Normal file
202
documentation/PROJECT__AE_Obj_Field_Editor_New.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# Project: AE Obj Field Editor — `_new` Rewrite
|
||||
|
||||
**Status:** 🟡 Planning
|
||||
**Priority:** Medium — quality-of-life primitive, no event deadline attached
|
||||
**Created:** 2026-06-16
|
||||
**Last Updated:** 2026-06-16
|
||||
**Related:** `BOOTSTRAP__AI_Agent_Quickstart.md`, `AE__Naming_Conventions.md`
|
||||
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
`src/lib/elements/element_ae_obj_field_editor.svelte` is the generic inline
|
||||
field editor: given `object_type` + `object_id` + `field_name` + `current_value`,
|
||||
it renders a "click pencil → edit one field → Save" UI and PATCHes just that
|
||||
field via `api.update_ae_obj()`. It's used in 8 call sites today (session_view,
|
||||
presenter_view, location_view, person_view, device list, presentation list,
|
||||
location list, leads manage tab).
|
||||
|
||||
This component already went through one rename cycle — it was originally built
|
||||
as `element_ae_obj_field_editor_v3.svelte` (replacing an older CRUD v1/v2
|
||||
pattern), then renamed to drop the `_v3` suffix once it became canonical. That
|
||||
migration (build new → migrate callers one at a time → delete old → drop
|
||||
suffix) worked well and is the template for this project.
|
||||
|
||||
**Review findings (2026-06-16)** — see `REFERENCE__Common_Agent_Mistakes.md`
|
||||
context and the chat log for full detail:
|
||||
|
||||
1. **Built entirely on Skeleton UI classes** (`btn-icon`, `variant-soft-*`,
|
||||
`variant-filled-*`, `.input`/`.select`/`.textarea`/`.checkbox`, `badge`).
|
||||
Conflicts with the stated Tailwind v4 + Flowbite/ShadCN direction. This is
|
||||
the highest-leverage place to do that swap — fix once in `elements/`,
|
||||
every call site benefits.
|
||||
2. **`object_reload` prop is dead.** Declared, defaults to `true`, commented
|
||||
"SWR pattern" — never read in the script. Every call site compensates by
|
||||
hand-rolling identical `on_success={() => events_func.load_ae_obj_id__*(...)}`
|
||||
boilerplate. Either implement it for real or remove it and document
|
||||
`on_success` as the actual refresh mechanism.
|
||||
3. **Datetime fields need manual format conversion at every call site, only
|
||||
on the way in, never on the way out.** Confirmed in `session_view.svelte`:
|
||||
callers must pre-convert with `to_datetime_local(...)` before passing
|
||||
`current_value` because the component does no normalization between the
|
||||
stored datetime string and `<input type="datetime-local">`'s expected
|
||||
format. On save, the native datetime-local string goes straight to the
|
||||
PATCH body with no reverse conversion. Works today because the backend is
|
||||
forgiving, but the contract is leaky.
|
||||
4. **Select-binding type mismatch is a latent landmine.** `Object.entries()`
|
||||
always yields string `val`s for `<option value>`. Today's usages
|
||||
(`event_location_id`, `poc_person_id`) are string IDs already, so it's
|
||||
invisible — but a true numeric/boolean enum field would break the
|
||||
optimistic-clear check (`current_value === draft_value`, strict equality),
|
||||
leaving the field stuck on the optimistic value.
|
||||
5. **Missing field types**: no `email` (no native keyboard/validation), no
|
||||
`url`/`tel`. Original design intent explicitly wanted email support.
|
||||
6. **`current_value`/`draft_value` typed `any`** — no compile-time safety.
|
||||
Svelte 5 supports generics (`<script generics="T">`); this primitive
|
||||
should use them.
|
||||
7. **Minor a11y/polish gaps**: invisible edit-trigger button stays
|
||||
tab-focusable when `$ae_loc.edit_mode` is off; icon-only Save/Cancel
|
||||
buttons lack `aria-label`; error message is tooltip-only (`title` attr,
|
||||
not visible/announced); no Escape-to-cancel; no autofocus on entering
|
||||
edit mode.
|
||||
8. **Coarse-store cost (not a bug, just a known tradeoff)**: reads
|
||||
`$ae_loc.edit_mode` directly in the template. Fine in isolation, but this
|
||||
component is instantiated per-row in list tables, so any unrelated
|
||||
`$ae_loc` write re-renders every instance on the page. Not chasing this
|
||||
now — noting it in case a list ever feels janky.
|
||||
|
||||
What's *not* broken and should be preserved as-is: the optimistic-display
|
||||
state machine (`has_optimistic` / `draft_value` / clearing once `current_value`
|
||||
catches up) is already hardened — it went through a real bug-fix round
|
||||
(layout shift, bindable crash, optimistic display) and works correctly for
|
||||
every field type in production use today.
|
||||
|
||||
---
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Near drop-in replacement.** Same prop contract (names, types, defaults)
|
||||
wherever possible, so migrating a call site is an import swap plus maybe
|
||||
one or two new optional props — not a rewrite of the call site.
|
||||
2. **Don't let "drop-in" block real fixes.** Where the old contract is part
|
||||
of the problem (e.g. `object_reload` doing nothing, datetime conversion
|
||||
being the caller's job), change it — call sites get updated during
|
||||
migration anyway, so this is the moment to fix it properly rather than
|
||||
carry the leak forward.
|
||||
3. **Run both versions in parallel during migration.** The old file stays in
|
||||
place and fully working until every call site has migrated and been
|
||||
verified. No "migrate and pray" — verify each call site, then move to the
|
||||
next.
|
||||
4. **One canonical "_new" rewrite.** Per project naming convention going
|
||||
forward: when rewriting an existing thing, the in-progress replacement is
|
||||
named `_new` (not `_v2`/`_v4`/etc.) regardless of how many older
|
||||
versioned files may have existed historically for other components. There
|
||||
should only ever be one "new version of whatever we're working on" at a
|
||||
time. Once migration is complete, drop the `_new` suffix and delete the
|
||||
old file — exactly as was done for the previous `_v3` → (no suffix) rename.
|
||||
|
||||
---
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Multi-field / batch editing in one widget — out of scope, this stays a
|
||||
single-field editor by design.
|
||||
- JSON sub-field editing (`cfg_json.some_key`) — not a supported field_type
|
||||
today, not adding it here either.
|
||||
- Changing the underlying `api.update_ae_obj()` PATCH semantics.
|
||||
|
||||
---
|
||||
|
||||
## New Component: `element_ae_obj_field_editor_new.svelte`
|
||||
|
||||
### Prop contract changes from the original
|
||||
|
||||
| Prop | Change | Why |
|
||||
|---|---|---|
|
||||
| `object_reload` | **Removed.** | Never implemented; `on_success` is and remains the real refresh hook. Removing rather than silently leaving a dead, misleading prop. |
|
||||
| `field_type` | **Add** `'email'` \| `'url'` \| `'tel'`. | Explicitly wanted in the original design, never added. |
|
||||
| `current_value` / `draft_value` | **Typed via generic** (`<script generics="T">`) instead of `any`. | Compile-time safety for callers; this is meant to be used consistently project-wide. |
|
||||
| Datetime conversion | **Owned by the component**, both directions (stored format ↔ `datetime-local`/`date` input format). | Removes the `to_datetime_local(...)` boilerplate currently required at every datetime call site, and fixes the missing reverse conversion on save. |
|
||||
| Select value coercion | **Coerce `draft_value` back to `typeof current_value`** after a select change (or compare via `String()` in the optimistic-clear check). | Prevents the stuck-optimistic-state landmine for any future numeric/boolean enum field. |
|
||||
| Everything else (`object_type`, `object_id`, `field_name`, `allow_null`, `select_options`, `edit_label`, `display_block`, `display_absolute_edit`, `placeholder`, `class_li`, `textarea_rows`, `log_lvl`, `on_success`, `on_error`, `children`) | **Unchanged.** | Drop-in for every call site that doesn't use `object_reload`. |
|
||||
|
||||
### Styling
|
||||
|
||||
Rebuilt on Tailwind v4 utility classes + Flowbite, matching whatever the
|
||||
current non-Skeleton convention is elsewhere in the app (check a recently
|
||||
touched component, e.g. the Pres Mgmt Config page, for the current baseline
|
||||
look). No Skeleton classes (`btn-icon`, `variant-*`, `.input`/`.select`/etc.)
|
||||
in the new file.
|
||||
|
||||
### Polish fixed opportunistically while rewriting
|
||||
|
||||
- `aria-label` on icon-only Save/Cancel/edit-trigger buttons.
|
||||
- `tabindex="-1"` on the edit-trigger button when invisible (`$ae_loc.edit_mode`
|
||||
off), so it's not in the tab order while non-interactive.
|
||||
- Visible inline error text (not just a `title` tooltip) when `patch_status === 'error'`.
|
||||
- Escape key cancels edit mode (mirrors existing Enter-to-save on text/number).
|
||||
- Autofocus the input when entering edit mode.
|
||||
|
||||
---
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. **Build `element_ae_obj_field_editor_new.svelte`** in `src/lib/elements/`,
|
||||
alongside the existing file. Keep the old file completely untouched and
|
||||
working during this phase.
|
||||
2. **Smoke-test in isolation** — add it to `src/routes/testing/ae_obj_field_editor/+page.svelte`
|
||||
(the existing test playground for the old component) alongside the old
|
||||
one, covering every `field_type` including the new `email`/`url`/`tel`.
|
||||
3. **Migrate call sites one at a time**, in this order (simplest/lowest-risk
|
||||
first, datetime-heavy ones last since they exercise the riskiest contract
|
||||
change):
|
||||
- `src/routes/core/person_view.svelte`
|
||||
- `src/routes/events/[event_id]/(pres_mgmt)/device/device/ae_comp__event_device_obj_li.svelte`
|
||||
- `src/routes/events/[event_id]/(pres_mgmt)/locations/ae_comp__event_location_obj_li.svelte`
|
||||
- `src/routes/events/[event_id]/(pres_mgmt)/location/[event_location_id]/location_view.svelte`
|
||||
- `src/routes/events/ae_comp__event_presentation_obj_li.svelte`
|
||||
- `src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_tab__manage.svelte`
|
||||
- `src/routes/events/[event_id]/(pres_mgmt)/presenter/[presenter_id]/presenter_view.svelte`
|
||||
- `src/routes/events/[event_id]/(pres_mgmt)/session/[session_id]/session_view.svelte`
|
||||
(has the datetime fields — migrate last, drop the `to_datetime_local(...)`
|
||||
wrapper at the call site once the component owns that conversion)
|
||||
4. **Per call site:** swap the import, remove any now-unnecessary
|
||||
`object_reload` usage (there is none today — confirmed dead) and any
|
||||
manual datetime pre-conversion, run `npx svelte-check`, visually verify
|
||||
the field in the running app (view mode, edit mode, save, optimistic
|
||||
display, error path).
|
||||
5. **Once all 8 call sites + the test playground are migrated and verified**:
|
||||
delete `element_ae_obj_field_editor.svelte` (move to `~/tmp/agents_trash`,
|
||||
never `rm`), rename `element_ae_obj_field_editor_new.svelte` →
|
||||
`element_ae_obj_field_editor.svelte`, update the now-stale import paths
|
||||
left by the rename, run `npx svelte-check` one final time.
|
||||
6. **Update docs**: `AE__Naming_Conventions.md` if a "_new" rewrite
|
||||
convention note belongs there, `BOOTSTRAP__AI_Agent_Quickstart.md` /
|
||||
`REFERENCE__Common_Agent_Mistakes.md` if anything generalizable came out
|
||||
of the datetime/select-coercion fixes, and mark this doc complete.
|
||||
|
||||
Both files coexist for the full duration of steps 1–4. Nothing is deleted
|
||||
until every consumer is migrated and verified.
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None blocking the start of step 1. Revisit field-by-field styling choices
|
||||
(e.g. exact Tailwind/Flowbite input classes) against whatever the most
|
||||
recently styled form in the app is using, since Skeleton phase-out is
|
||||
ongoing and conventions may still be settling.
|
||||
|
||||
---
|
||||
|
||||
## Naming Convention Note (for future rewrites like this)
|
||||
|
||||
Going forward, an in-progress full rewrite of an existing component/module
|
||||
is named `_new` regardless of how many historically-versioned files
|
||||
(`_v1`/`_v2`/`_v3`/etc.) may exist elsewhere in the codebase for unrelated
|
||||
components. There should be at most one "new version of the thing we're
|
||||
currently rewriting" in flight at a time. Once migration completes, the
|
||||
suffix is dropped and the old file is deleted — there is no permanent `_new`
|
||||
file left behind, just like there's no permanent `_v3` file left behind from
|
||||
the previous round of this same component.
|
||||
@@ -1,8 +1,14 @@
|
||||
# PROJECT: Site Passcode Security — API-Verified Auth
|
||||
|
||||
**Last updated:** 2026-04-10
|
||||
**Status:** Backend work in progress — frontend pending backend completion
|
||||
**Priority:** High — passcodes for trusted/administrator access currently in localStorage plaintext
|
||||
**Last Updated:** 2026-06-12
|
||||
**Last Verified Against Frontend Source:** 2026-06-12
|
||||
**Status:** Active security gap — frontend migration not started
|
||||
**Priority:** High — passcodes for trusted/administrator access currently remain in localStorage plaintext
|
||||
|
||||
The frontend still caches `access_code_kv_json`, compares passcodes locally, and can log the
|
||||
full passcode map when verbose logging is enabled. No frontend call to `/authenticate_passcode`
|
||||
or passcode-JWT expiry restoration exists. Backend implementation is documented as completed,
|
||||
but deployment must be confirmed in the backend repository/environment before frontend cutover.
|
||||
|
||||
---
|
||||
|
||||
@@ -81,7 +87,11 @@ This gives session expiry without a network call on every page load.
|
||||
|
||||
## Backend Changes Required
|
||||
|
||||
**Note:** The backend fixes described below have been implemented and tested in the `aether_api_fastapi` repository (the `/authenticate_passcode` endpoint now uses explicit role priority, returns a full passcode JWT with `auth_type: 'passcode'`, applies per-role TTLs, and validates passcode length). Frontend changes can proceed once the backend deployment with these fixes is available.
|
||||
**Backend status note:** The fixes below were reported implemented and tested in the
|
||||
`aether_api_fastapi` repository. This frontend-only audit did not verify the backend source or
|
||||
deployment. Confirm that the deployed `/authenticate_passcode` uses explicit role priority,
|
||||
returns a complete passcode JWT with `auth_type: 'passcode'`, applies per-role TTLs, and validates
|
||||
passcode length before starting frontend cutover.
|
||||
|
||||
### Backend Agent Follow-Up
|
||||
|
||||
@@ -316,6 +326,19 @@ async def authenticate_passcode(
|
||||
|
||||
---
|
||||
|
||||
## Frontend Implementation Status
|
||||
|
||||
Verified 2026-06-12:
|
||||
|
||||
- [ ] Confirm the corrected backend endpoint is deployed and reachable.
|
||||
- [ ] Replace local passcode comparison with API verification and JWT storage.
|
||||
- [ ] Add pending/error UI for passcode authentication.
|
||||
- [ ] Stop copying `access_code_kv_json` into frontend auth state.
|
||||
- [ ] Validate passcode JWT expiry during session restoration.
|
||||
- [ ] Remove `site_access_code_kv` from auth store defaults and types.
|
||||
- [ ] Remove any logging of passcode maps or entered passcodes.
|
||||
- [ ] Backend Phase 2: remove `access_code_kv_json` from the public bootstrap model.
|
||||
|
||||
## Frontend Changes Required
|
||||
|
||||
**These depend on the backend fixes above being deployed first.**
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Project: Documentation Refresh and Archive Plan (2026)
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
**Goal:** Keep onboarding docs fast and current while preserving historical context in archive.
|
||||
|
||||
## 1) Naming Standard
|
||||
|
||||
Use one of these prefixes consistently:
|
||||
- `BOOTSTRAP__` for first-read onboarding.
|
||||
- `GUIDE__` for cross-module technical guides.
|
||||
- `MODULE__` for module-specific docs.
|
||||
- `PROJECT__` for active, time-bounded workstreams.
|
||||
- `PROPOSAL__` for design proposals not yet adopted.
|
||||
- `REFERENCE__` for evergreen troubleshooting/reference catalogs.
|
||||
|
||||
## 2) Module Coverage Baseline (Active)
|
||||
|
||||
Minimum one module doc per active module family:
|
||||
- Events Presentation Management -> `MODULE__AE_Events_Presentation_Management.md`
|
||||
- Events Launcher -> `MODULE__AE_Events_Launcher.md`
|
||||
- Events Launcher Native -> `MODULE__AE_Events_Launcher_Native.md`
|
||||
- Events Badges -> `MODULE__AE_Events_Badges.md`
|
||||
- Events Leads -> `MODULE__AE_Events_Leads.md`
|
||||
- Journals -> `MODULE__AE_Journals.md`
|
||||
- IDAA Archives -> `MODULE__AE_IDAA_Archives.md`
|
||||
- IDAA Bulletin Board -> `MODULE__AE_IDAA_Bulletin_Board.md`
|
||||
- IDAA Recovery Meetings -> `MODULE__AE_IDAA_Recovery_Meetings.md`
|
||||
- IDAA Video Conferences -> `MODULE__AE_IDAA_Video_Conferences.md`
|
||||
|
||||
## 3) Archive Strategy
|
||||
|
||||
Archive docs that are superseded, duplicate, or no longer operationally used.
|
||||
Do not delete historical context; move to `documentation/archive/` with clear names.
|
||||
|
||||
### Completed in this pass
|
||||
- Moved `MODULE__AE_Events_Launcher_Config_Menu_new.md` -> `archive/PROPOSAL__AE_Events_Launcher_Config_Menu_Unified_Vision_v3_1.md`.
|
||||
- Moved `PROPOSAL__IDAA_UI_UX_Roadmap_2026.md` -> `archive/PROPOSAL__IDAA_Recovery_Meetings_UI_UX_Roadmap_2026.md`.
|
||||
- Renamed `MODULE__AE Journals_config_map.md` -> `MODULE__AE_Journals_Config_Map.md`.
|
||||
- Renamed `PROJECT__AE_UI_Journals_module_update_2026.md` -> `PROJECT__AE_UI_Journals_Module_Update_2026.md`.
|
||||
- Renamed `AE_Docker_CI_cache_policy.md` -> `AE__Docker_CI_Cache_Policy.md`.
|
||||
- Archived `PROJECT__AE_Style_Review.md` -> `archive/PROJECT__AE_Style_Review_2026-03.md` (active style source is `GUIDE__AE_UI_Style_Guidelines.md`).
|
||||
- Clarified scope split: `GUIDE__AE_Events_Badges_Onsite.md` = deep badge-print reference, `GUIDE__AE_Events_Onsite_Runbook.md` = cross-module onsite operations runbook.
|
||||
- Validated all `documentation/*.md` references in active docs; no missing targets remain.
|
||||
- Added ownership and review-trigger metadata to the bootstrap, task list, and docs index.
|
||||
- Reviewed active project docs for archive eligibility. Object Field Editor and Site Passcode Security remain active and were added to the docs index.
|
||||
- Archived legacy API-object, component-inventory, data-structure, performance, and UI-pattern references that contradicted V3 IDs, Svelte 5, or current private-route execution rules.
|
||||
- Refreshed `AE__Architecture.md` and `AE__Naming_Conventions.md` as the active replacements.
|
||||
- Added `documentation/archive/README.md` to explain archive categories and restoration policy.
|
||||
- Renamed `AE__Docker_CI_Cache_Policy.md` -> `GUIDE__Docker_CI_Cache_Policy.md`.
|
||||
- Renamed `AE__UI_UX_future_ideas.md` -> `PROPOSAL__AE_UI_UX_Future_Ideas.md`.
|
||||
- Audited the Journals UI update against current source and archived
|
||||
`PROJECT__AE_UI_Journals_Module_Update_2026.md`; remaining security work was moved to
|
||||
the active task list and module documentation.
|
||||
- Audited the Badges review/print project against current source and archived
|
||||
`PROJECT__AE_Events_Badges_Review_Print.md`; email delivery and permission-source
|
||||
unification remain active follow-ups.
|
||||
- Audited Site Passcode Security against current source. It remains an active high-priority
|
||||
project because plaintext client storage and local passcode comparison are still present.
|
||||
- Audited V3 CRUD upgrade project against source (2026-06-12). All production code migrated
|
||||
to V3; legacy wrappers remain exported but unused. Archived `PROJECT__Use_AE_API_V3_CRUD_upgrade.md`.
|
||||
Optional cleanup task added to TODO for removing dead wrapper code.
|
||||
- Audited Field Editor V3 project against source (2026-06-12). Component complete with 50+ active
|
||||
usages, GUIDE documentation, and all core field types. Searchable dropdowns deferred as optional
|
||||
enhancement. Archived `PROJECT__AE_Object_Field_Editor_V3_upgrade.md`.
|
||||
- Audited Pres Mgmt Config Cleanup project (2026-06-12). Core infrastructure working (~70% complete)
|
||||
but regressions identified: `show__launcher_link_legacy` missing from schema, old settings form
|
||||
not removed, local/remote key conflicts. Updated project doc with regression fixes; keeping active.
|
||||
|
||||
### Next archive candidates (review + approve)
|
||||
- Older style-review snapshots once current style guide references are centralized.
|
||||
- Closed project docs where TODO/archive already capture final status.
|
||||
- Duplicate docs where one `MODULE__` file and one `PROJECT__` file now overlap heavily.
|
||||
|
||||
## 4) Review Cadence
|
||||
|
||||
Monthly lightweight review:
|
||||
1. Verify `BOOTSTRAP__AI_Agent_Quickstart.md` links still resolve and reflect active work.
|
||||
2. Verify each active module has one current `MODULE__` anchor doc.
|
||||
3. Move stale proposals and completed projects into archive.
|
||||
4. Update `REFERENCE__Common_Agent_Mistakes.md` keep/archive sections.
|
||||
|
||||
## 5) Immediate Follow-Up Tasks
|
||||
|
||||
1. Continue quarterly archive reviews for remaining stale `PROJECT__` docs; the Journals and Badges projects were archived on 2026-06-12, while Site Passcode Security remains active.
|
||||
2. Continue the broader permission-helper and IDAA authentication review; the Site Passcode section was source-verified on 2026-06-12.
|
||||
3. Review module docs against current routes and store names rather than relying only on filename/header freshness.
|
||||
4. Add a lightweight reusable link-check script if manual path validation becomes frequent.
|
||||
267
documentation/PROJECT__IDAA_Stores_Svelte5_Migration_2026.md
Normal file
267
documentation/PROJECT__IDAA_Stores_Svelte5_Migration_2026.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# PROJECT: IDAA `idaa_loc` Migration to Svelte 5 `PersistedState`
|
||||
|
||||
**Last Updated:** 2026-06-12
|
||||
|
||||
## Objective
|
||||
Migrate IDAA persisted local state from legacy `svelte-persisted-store` (`$idaa_loc`) to Svelte 5 `PersistedState` (`idaa_loc.current`) without behavior regressions, auth leaks, or broken page flows.
|
||||
|
||||
Primary target store:
|
||||
- `src/lib/stores/ae_idaa_stores__idaa_loc.svelte.ts` ← new store (created, not yet wired in)
|
||||
|
||||
Legacy source currently used by routes:
|
||||
- `src/lib/stores/ae_idaa_stores.ts` ← remove `idaa_loc` export after migration
|
||||
|
||||
## Why This Matters
|
||||
- Removes coarse-grained reactivity side effects from legacy persisted store access.
|
||||
- Aligns IDAA with the completed Events store migration pattern.
|
||||
- Reduces risk of auth-state corruption from broad re-renders triggered by unrelated
|
||||
writes to the same store key.
|
||||
|
||||
## Scope
|
||||
In scope:
|
||||
- Replace all `idaa_loc` imports from `ae_idaa_stores.ts` with imports from
|
||||
`ae_idaa_stores__idaa_loc.svelte.ts`.
|
||||
- Replace all `$idaa_loc.*` reads/writes with `idaa_loc.current.*`.
|
||||
- Remove `idaa_loc` export and its `persisted()` definition from `ae_idaa_stores.ts`
|
||||
after all consumers are migrated.
|
||||
- Keep `store_versions.ts` wipe call for `ae_idaa_loc` — this cleans old data from
|
||||
browsers of returning users. Keep it for at least one year post-migration (same rule
|
||||
as `ae_events_loc`).
|
||||
|
||||
Out of scope:
|
||||
- Migrating `idaa_sess`, `idaa_slct`, `idaa_trig`, `idaa_prom` — in-memory writables,
|
||||
no coarse-reactivity problem.
|
||||
- Backend/API changes.
|
||||
- `ae_loc` migration (separate project).
|
||||
|
||||
## Consumer File Inventory
|
||||
|
||||
### Files requiring import + `$idaa_loc` → `idaa_loc.current` changes (29 files)
|
||||
|
||||
#### IDAA module root layouts (auth-critical — do these first and last)
|
||||
|
||||
| File | `$idaa_loc` hits | Notes |
|
||||
| --- | --- | --- |
|
||||
| `src/routes/idaa/+layout.svelte` | 3 | Top-level IDAA layout; `/idaa/clear-caches` lives outside this |
|
||||
| `src/routes/idaa/(idaa)/+layout.svelte` | 40 | Most complex. Novi verification loop, auth escalation, admin/trusted list writes |
|
||||
|
||||
#### Bulletin Board (BB)
|
||||
|
||||
| File | `$idaa_loc` hits |
|
||||
| --- | --- |
|
||||
| `src/routes/idaa/(idaa)/bb/+layout.svelte` | 3 |
|
||||
| `src/routes/idaa/(idaa)/bb/+page.svelte` | 11 |
|
||||
| `src/routes/idaa/(idaa)/bb/[post_id]/+page.svelte` | 6 |
|
||||
| `src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_obj_li.svelte` | 4 |
|
||||
| `src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_obj_id_edit.svelte` | 11 |
|
||||
| `src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_obj_id_view.svelte` | low |
|
||||
| `src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_comment_obj_id_edit.svelte` | 7 |
|
||||
| `src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_options.svelte` | 18 |
|
||||
|
||||
#### Archives
|
||||
|
||||
| File | `$idaa_loc` hits |
|
||||
| --- | --- |
|
||||
| `src/routes/idaa/(idaa)/archives/+layout.svelte` | low |
|
||||
| `src/routes/idaa/(idaa)/archives/+page.svelte` | 6 |
|
||||
| `src/routes/idaa/(idaa)/archives/ae_idaa_comp__media_player.svelte` | low |
|
||||
| `src/routes/idaa/(idaa)/archives/[archive_id]/+page.svelte` | 11 |
|
||||
| `src/routes/idaa/(idaa)/archives/[archive_id]/ae_idaa_comp__archive_obj_id_edit.svelte` | 4 |
|
||||
| `src/routes/idaa/(idaa)/archives/[archive_id]/ae_idaa_comp__archive_obj_id_view.svelte` | 16 |
|
||||
| `src/routes/idaa/(idaa)/archives/[archive_id]/ae_idaa_comp__archive_content_obj_id_edit.svelte` | 4 |
|
||||
| `src/routes/idaa/(idaa)/archives/[archive_id]/ae_idaa_comp__archive_content_obj_li.svelte` | low |
|
||||
| `src/routes/idaa/(idaa)/archives/[archive_id]/ae_idaa_comp__modal_media_player.svelte` | low |
|
||||
|
||||
#### Recovery Meetings
|
||||
|
||||
| File | `$idaa_loc` hits |
|
||||
| --- | --- |
|
||||
| `src/routes/idaa/(idaa)/recovery_meetings/+layout.svelte` | low |
|
||||
| `src/routes/idaa/(idaa)/recovery_meetings/+page.svelte` | 37 |
|
||||
| `src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.svelte` | 10 |
|
||||
| `src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte` | 41 |
|
||||
| `src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_li.svelte` | 8 |
|
||||
| `src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_id_edit.svelte` | 12 |
|
||||
| `src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_id_view.svelte` | low |
|
||||
|
||||
#### Other IDAA
|
||||
|
||||
| File | `$idaa_loc` hits |
|
||||
| --- | --- |
|
||||
| `src/routes/idaa/(idaa)/jitsi_reports/+page.svelte` | low |
|
||||
| `src/routes/idaa/(idaa)/video_conferences/+page.svelte` | 12 |
|
||||
|
||||
### Files that reference `ae_idaa_loc` as a raw string only — NO changes needed
|
||||
- `src/routes/events/+layout.svelte` — `localStorage.removeItem('ae_idaa_loc')` (sign-out)
|
||||
- `src/routes/events/[event_id]/(launcher)/cfg_components/launcher_cfg_local_actions.svelte` — same
|
||||
- `src/lib/stores/store_versions.ts` — `_check_and_wipe('ae_idaa_loc', ...)` — keep as-is
|
||||
|
||||
## Safety Rules
|
||||
1. Do this as one atomic migration — no split old/new `idaa_loc` consumers in the same session.
|
||||
2. Do not loosen any IDAA auth gating — IDAA is private data, always authenticated.
|
||||
3. Do not move IDAA private data loads into `+page.ts` / `+layout.ts` where prefetch can run.
|
||||
4. Keep route behavior and permissions exactly as current production behavior.
|
||||
5. The `(idaa)/+layout.svelte` auth gate is the most sensitive file. Review it by hand after
|
||||
the mechanical pass — do not rely solely on `svelte-check` to catch auth logic regressions.
|
||||
|
||||
## Key Syntax Changes
|
||||
|
||||
| Before | After |
|
||||
| --- | --- |
|
||||
| `import { idaa_loc } from '$lib/stores/ae_idaa_stores'` | `import { idaa_loc } from '$lib/stores/ae_idaa_stores__idaa_loc.svelte'` |
|
||||
| `$idaa_loc.novi_uuid` | `idaa_loc.current.novi_uuid` |
|
||||
| `$idaa_loc.bb.qry__hidden = 'not_hidden'` | `idaa_loc.current.bb.qry__hidden = 'not_hidden'` |
|
||||
| `$idaa_loc.recovery_meetings.qry__favorites_only` | `idaa_loc.current.recovery_meetings.qry__favorites_only` |
|
||||
|
||||
Notes:
|
||||
- Keep other imports from `ae_idaa_stores` (`idaa_sess`, `idaa_slct`, `idaa_trig`, `idaa_prom`)
|
||||
unchanged — they stay in that file.
|
||||
- No `$` sigil — access via `.current` property, not Svelte store subscription.
|
||||
|
||||
## Execution Plan
|
||||
|
||||
### Phase 0: Baseline Checkpoint
|
||||
- Ensure clean compile baseline: `npx svelte-check` → 0/0.
|
||||
- Confirm new store file committed: `ae_idaa_stores__idaa_loc.svelte.ts` ✅ (done 2026-06-11)
|
||||
|
||||
Exit criteria:
|
||||
- `svelte-check` 0 errors / 0 warnings.
|
||||
|
||||
### Phase 1: Store Consumer Conversion (Mechanical)
|
||||
|
||||
Recommended order — least to most auth-critical, so layouts are done fresh at the end:
|
||||
|
||||
#### 1a. Sub-module components (BB, Archives, Recovery Meetings)
|
||||
|
||||
All `ae_idaa_comp__*` component files — pure consumers, no auth logic.
|
||||
|
||||
#### 1b. Sub-module pages and layouts (BB, Archives, Recovery Meetings)
|
||||
|
||||
Page and layout files for the three sub-modules. Sub-layouts (`bb/+layout.svelte`,
|
||||
`archives/+layout.svelte`, `recovery_meetings/+layout.svelte`) check auth indirectly
|
||||
but don't own the Novi verification loop.
|
||||
|
||||
#### 1c. Jitsi reports + video conferences
|
||||
|
||||
Low-hit, isolated pages.
|
||||
|
||||
#### 1d. `src/routes/idaa/+layout.svelte` (top-level)
|
||||
|
||||
Reads `$idaa_loc` for display only; writes happen in the (idaa) inner layout.
|
||||
|
||||
#### 1e. `src/routes/idaa/(idaa)/+layout.svelte` (innermost — do last)
|
||||
|
||||
Most complex file (40 hits). Owns the Novi verification loop and all auth escalation writes.
|
||||
The `$idaa_loc.bb.qry__hidden`, `$idaa_loc.novi_admin_li`, etc. writes all live here.
|
||||
Review by hand after mechanical pass.
|
||||
|
||||
For each file:
|
||||
1. Update import — change `idaa_loc` source, keep `idaa_sess` / `idaa_slct` / etc. from `ae_idaa_stores`.
|
||||
2. Replace `$idaa_loc.` → `idaa_loc.current.` (global find-replace within file).
|
||||
3. No other logic changes — mechanical pass only.
|
||||
|
||||
Exit criteria:
|
||||
- No remaining `$idaa_loc.` usages in `src/routes/idaa/`.
|
||||
- No `idaa_loc` imports pointing to `ae_idaa_stores` in route files.
|
||||
|
||||
### Phase 2: Store File Cleanup
|
||||
After all consumers are migrated:
|
||||
- Remove `idaa_loc` export and `persisted('ae_idaa_loc', idaa_local_data_struct)` from
|
||||
`ae_idaa_stores.ts`.
|
||||
- Remove `AE_IDAA_LOC_VERSION` import from `ae_idaa_stores.ts` (no longer needed there).
|
||||
- Keep `store_versions.ts` unchanged — the `_check_and_wipe` call stays to clean old data.
|
||||
|
||||
### Phase 3: Critical Auth Layout Validation
|
||||
Manually review after conversion:
|
||||
- `src/routes/idaa/(idaa)/+layout.svelte` — Novi verification `$effect`, auth escalation, sign-out
|
||||
- `src/routes/idaa/+layout.svelte` — top-level auth gate template
|
||||
- `src/routes/idaa/(idaa)/bb/+layout.svelte`
|
||||
- `src/routes/idaa/(idaa)/archives/+layout.svelte`
|
||||
- `src/routes/idaa/(idaa)/recovery_meetings/+layout.svelte`
|
||||
|
||||
Checks:
|
||||
- Auth gate still blocks unauthenticated users on all sub-routes.
|
||||
- `novi_verified`, `novi_uuid`, trusted/admin flag writes work correctly.
|
||||
- No duplicate or skipped verification loops.
|
||||
- `bb.qry__hidden`, `bb.qry__enabled` reset after Novi verification still fires.
|
||||
|
||||
Exit criteria:
|
||||
- Auth flow matches current production behavior exactly.
|
||||
|
||||
### Phase 4: Compile + Search Guards
|
||||
Run:
|
||||
```bash
|
||||
npx svelte-check
|
||||
grep -rn '\$idaa_loc\.' src/
|
||||
grep -rn "from '\$lib/stores/ae_idaa_stores'" src/routes/idaa/
|
||||
```
|
||||
|
||||
Exit criteria:
|
||||
- `svelte-check`: 0 errors / 0 warnings.
|
||||
- No `$idaa_loc.` references remaining in source.
|
||||
- No `idaa_loc` imports from `ae_idaa_stores` in route files.
|
||||
|
||||
### Phase 5: Test File Updates
|
||||
The IDAA Novi auth test (`tests/idaa_novi_auth.test.ts`) seeds `ae_idaa_loc` via
|
||||
`addInitScript`. After migration:
|
||||
- The seeded structure remains valid (same key, same shape).
|
||||
- Remove any `ver:` field from the seed if present — `PersistedState` stores don't use it.
|
||||
- Verify the full nested structure is still seeded (the `bb`, `archives`, `recovery_meetings`
|
||||
objects must be present — see the "Seed the Full ae_idaa_loc Structure" lesson in `tests/README.md`).
|
||||
|
||||
### Phase 6: Runtime Smoke Test
|
||||
Test flows:
|
||||
1. Direct navigation to `/idaa/` — auth gate behavior correct.
|
||||
2. Bulletin Board: list, post view, post edit, comment.
|
||||
3. Archives: list, archive detail, media player.
|
||||
4. Recovery Meetings: list, search/filter, favorites toggle, edit form.
|
||||
5. Video Conferences page loads.
|
||||
6. Jitsi Reports page loads.
|
||||
7. Cache clear page (`/idaa/clear-caches`) still clears state and posts message to parent.
|
||||
8. Sign-out clears `ae_idaa_loc` (the localStorage key name is unchanged, so this works
|
||||
automatically for any caller using `localStorage.removeItem('ae_idaa_loc')`).
|
||||
|
||||
Exit criteria:
|
||||
- No regressions in primary user paths.
|
||||
|
||||
## Risk Register
|
||||
|
||||
### R1: Split-brain state
|
||||
**Risk:** Mixed old/new `idaa_loc` consumers in the same session can lead to inconsistent state.
|
||||
**Mitigation:** Convert all consumers in one agent pass before committing. Do not commit partial migrations.
|
||||
|
||||
### R2: Auth regression in `(idaa)/+layout.svelte`
|
||||
**Risk:** The Novi verification loop is complex (40 `$idaa_loc` hits). A subtle change to
|
||||
`$effect` dependency tracking between old and new store access could break or skip verification.
|
||||
**Mitigation:** Review this file by hand after the mechanical pass. Test auth flow explicitly.
|
||||
|
||||
### R3: Nested object merge gap
|
||||
**Risk:** The `deserialize` function does a shallow spread at the top level only:
|
||||
`{ ...idaa_loc_defaults, ...JSON.parse(raw) }`. If a new field is added inside `bb`,
|
||||
`archives`, or `recovery_meetings` after a user has stored data, that field will get
|
||||
`undefined` rather than its default.
|
||||
**Mitigation:** This is the same accepted trade-off as the events sub-stores. If a new
|
||||
nested field is added in the future, add a migration step or accept that the old stored
|
||||
value takes over wholesale.
|
||||
|
||||
### R4: Mechanical typo / missed reference
|
||||
**Risk:** High replacement count (29 files, ~300+ hits) introduces missed `$idaa_loc.` references.
|
||||
**Mitigation:** Run the grep guard in Phase 4 before declaring done.
|
||||
|
||||
## Rollback Plan
|
||||
If issues are found after migration:
|
||||
1. `git revert` the migration commit(s).
|
||||
2. Re-run `npx svelte-check`.
|
||||
3. Re-attempt using smaller batches per sub-module (BB only, then Archives, then Recovery Meetings).
|
||||
|
||||
## Deliverables
|
||||
- All 29 consumer files converted from `$idaa_loc.*` → `idaa_loc.current.*`.
|
||||
- `idaa_loc` export removed from `ae_idaa_stores.ts`.
|
||||
- Passing compile check (0/0).
|
||||
- Smoke-tested all IDAA sub-modules.
|
||||
|
||||
## Definition of Done
|
||||
- Full `idaa_loc` migration complete.
|
||||
- No auth/privacy regressions.
|
||||
- `svelte-check` 0/0.
|
||||
- IDAA smoke-tested (auth gate, BB, Archives, Recovery Meetings, cache clear).
|
||||
@@ -1,97 +1,155 @@
|
||||
# Project: Svelte 4 Store → Svelte 5 State Migration
|
||||
|
||||
**Status:** Execution / Phase B (In Progress)
|
||||
**Priority:** High (post-April 2026 conference)
|
||||
**Status:** Events module — COMPLETE. Core / IDAA — In Progress (field cleanup done, PersistedState pending).
|
||||
**Priority:** High
|
||||
**Created:** 2026-03-30
|
||||
**Related:** `TODO__Agents.md` — [Stores] Svelte 5 State Migration entry
|
||||
**Last Updated:** 2026-06-11
|
||||
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
All core Aether stores (`ae_loc`, `idaa_loc`, `ae_events_loc`, etc.) are being migrated from
|
||||
Svelte 4 stores to Svelte 5 `$state` using the `runed` library's `PersistedState`. This provides
|
||||
fine-grained reactivity, ensuring that effects only re-run when specific fields they access are
|
||||
updated, rather than on every write to the store object.
|
||||
All core Aether stores were built with `svelte-persisted-store` (Svelte 4 contract). This provides
|
||||
coarse reactivity: any write to any field notifies *all* subscribers and re-serializes the entire
|
||||
object. For large stores like `ae_loc` and `ae_events_loc`, this caused unnecessary re-renders and
|
||||
was the root cause of the IDAA "Access Denied" corruption bug (a bootstrap write to `ae_loc` would
|
||||
overwrite `authenticated_access` if a persisted value was slightly different, corrupting IDAA
|
||||
member state stored in the same key).
|
||||
|
||||
### Phase B Progress & Learnings (Updated 2026-03-30)
|
||||
|
||||
1. **Dependency Installed**: `runed` is now a project dependency.
|
||||
2. **Module Resolution Strategy**:
|
||||
- Core store files renamed: `ae_stores.ts` → `ae_stores.svelte.ts`, etc.
|
||||
- **Critical Discovery**: SvelteKit and CLI tools (`svelte-check`) struggled to resolve
|
||||
extension-less imports (like `$lib/stores/ae_stores`) when only `.svelte.ts` existed.
|
||||
- **Solution**: Created `.ts` wrapper files (e.g., `src/lib/stores/ae_stores.ts`) that
|
||||
simply re-export everything via `export * from './ae_stores.svelte'`. This maintains
|
||||
backward compatibility for all existing import paths without manual updates.
|
||||
3. **API Confirmation**: Confirmed that `runed`'s `PersistedState` uses `.current` to access
|
||||
the state object, matching the intended migration syntax.
|
||||
4. **Mass Replacement**:
|
||||
- `$ae_loc` → `ae_loc_v5.current`
|
||||
- `$idaa_loc` → `idaa_loc_v5.current`
|
||||
- `$events_loc` → `events_loc_v5.current`
|
||||
- These replacements have been applied across the entire `src/` directory (~2000+ sites).
|
||||
5. **Import Updates**: A robust Python script was used to surgically add `ae_loc_v5`,
|
||||
`idaa_loc_v5`, and `events_loc_v5` to existing import blocks, avoiding `import type`
|
||||
lines and duplicates.
|
||||
The migration target: replace all `persisted()` stores with `runed`'s `PersistedState`, which uses
|
||||
Svelte 5 fine-grained reactivity — a write to one field only triggers effects that read that field.
|
||||
|
||||
---
|
||||
|
||||
## Syntax Changes: Before / After
|
||||
## Completed: Events Module (2026-06-11)
|
||||
|
||||
### Store declaration
|
||||
All `ae_events_stores` sub-modules have been promoted to their own `PersistedState` stores and
|
||||
`events_loc` (the old `persisted()` store) has been **fully retired**.
|
||||
|
||||
```typescript
|
||||
// BEFORE (ae_stores.svelte.ts)
|
||||
export const ae_loc: Writable<key_val> = persisted('ae_loc', defaults);
|
||||
| Store | File | localStorage key | Status |
|
||||
|---|---|---|---|
|
||||
| `badges_loc` | `ae_events_stores__badges.svelte.ts` | `ae_badges_loc` | ✅ Done (2026-04-02) |
|
||||
| `leads_loc` | `ae_events_stores__leads.svelte.ts` | `ae_leads_loc` | ✅ Done (2026-04-03) |
|
||||
| `pres_mgmt_loc` | `ae_events_stores__pres_mgmt.svelte.ts` | `ae_pres_mgmt_loc` | ✅ Done (2026-04-03) |
|
||||
| `launcher_loc` | `ae_events_stores__launcher.svelte.ts` | `ae_launcher_loc` | ✅ Done (2026-06-11) |
|
||||
| `events_auth_loc` | `ae_events_stores__auth.svelte.ts` | `ae_events_auth_loc` | ✅ Done (2026-06-11) |
|
||||
| `events_loc` | *(retired)* | `ae_events_loc` | ✅ Store removed (2026-06-11) |
|
||||
|
||||
// AFTER (ae_stores.svelte.ts)
|
||||
export const ae_loc_v5 = new PersistedState('ae_loc', defaults);
|
||||
```
|
||||
`ae_events_stores.ts` now only exports `events_sess` (in-memory writable, no migration needed),
|
||||
`events_slct`, `events_trig`, `events_trig_kv`, `events_trigger`, and the `EVENTS_MODULE_TITLE`
|
||||
constant. The file no longer imports `svelte-persisted-store`.
|
||||
|
||||
### Reading/Writing (Consumers)
|
||||
|
||||
```svelte
|
||||
<!-- BEFORE -->
|
||||
{$ae_loc.theme_mode}
|
||||
{#if $ae_loc.trusted_access}
|
||||
|
||||
<!-- AFTER -->
|
||||
{ae_loc_v5.current.theme_mode}
|
||||
{#if ae_loc_v5.current.trusted_access}
|
||||
```
|
||||
|
||||
### Batch Update Pattern
|
||||
|
||||
```typescript
|
||||
// Use Object.assign for multi-field updates to maintain fine-grained reactivity
|
||||
Object.assign(ae_loc_v5.current, { theme_mode: 'dark', edit_mode: true });
|
||||
```
|
||||
`store_versions.ts` still calls `_check_and_wipe('ae_events_loc', AE_EVENTS_LOC_VERSION)` to clean
|
||||
old data out of users' browsers — this is intentional and should be kept for at least one year.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
## In Progress / Remaining: `ae_stores.ts` and `ae_idaa_stores.ts`
|
||||
|
||||
### Prettier & Formatting
|
||||
Because the mass migration touches ~250 files, formatting may be inconsistent (especially in
|
||||
import blocks). It is recommended to run `npm run format` (if available) or rely on standard
|
||||
linting after the imports are stabilized.
|
||||
Both stores had their unused default properties pruned (2026-06-11), reducing migration scope:
|
||||
|
||||
### Batching vs. "Big Sweep"
|
||||
While the original plan suggested smaller batches, the global nature of `ae_loc` and the
|
||||
hundreds of interdependent files made a "Big Sweep" more efficient to ensure the app remains
|
||||
in a consistent state. However, verification should still be done module-by-module.
|
||||
**`ae_loc`** (in `ae_stores.ts`) — still `persisted('ae_loc', ...)`:
|
||||
- Remaining fields: auth/identity, theme, permissions, ui config, file upload tracking, query prefs
|
||||
- This is the highest-impact remaining migration — used in nearly every route
|
||||
- Root cause of IDAA "Access Denied" bug (coarse write during bootstrap stomps on permission fields)
|
||||
|
||||
**`idaa_loc`** (in `ae_idaa_stores.ts`) — still `persisted('ae_idaa_loc', ...)`:
|
||||
- Remaining fields: `novi_uuid/verified/ts`, `novi_admin_li/trusted_li`, `archives/bb/recovery_meetings` sub-objects
|
||||
- Fields pruned (2026-06-11): `ds`, `idaa_cfg_json`, top-level `qry__*`, `novi_*_base_url`, `novi_rate_limited_until`
|
||||
|
||||
---
|
||||
|
||||
## Current Status (Pause Point)
|
||||
## Migration Pattern (Established)
|
||||
|
||||
- [x] Phase A: Plan written.
|
||||
- [x] Phase B: Core infrastructure setup (runed, renames, wrappers).
|
||||
- [x] Phase B: Mass variable replacement ($ae_loc -> ae_loc_v5.current).
|
||||
- [/] Phase B: Mass import update (In Progress/Verifying).
|
||||
- [ ] Phase B: Final validation (`svelte-check` clean).
|
||||
Each sub-store follows this pattern, using the `badges` store as the canonical reference:
|
||||
|
||||
**Do NOT stage or commit until `npx svelte-check` is fully verified.**
|
||||
The app currently has a high error count due to the transition period where imports are being
|
||||
re-aligned. Final verification is the next step after the pause.
|
||||
### 1. Defaults file (`*_defaults.ts`)
|
||||
Define the shape and defaults as a plain TypeScript object (and interface if complex):
|
||||
|
||||
```ts
|
||||
export interface BadgesLocState { ... }
|
||||
export const badges_loc_defaults: BadgesLocState = { ... };
|
||||
```
|
||||
|
||||
### 2. Store file (`*.svelte.ts`)
|
||||
|
||||
**Updated 2026-06-16 — stamp `__version` in the serializer.** The original pattern below (bare
|
||||
`JSON.stringify`) looked fine but has a real failure mode: `store_versions.ts`'s
|
||||
`_check_and_wipe()` compares a `__version` field *inside* the stored JSON, and a bare
|
||||
`JSON.stringify` never writes one. Two consequences were found in production code:
|
||||
`pres_mgmt_loc`'s version bump was silently a no-op (never wired into `_check_and_wipe` at
|
||||
all), and `leads_loc`'s *was* wired in, so `parsed?.__version` was always `undefined` —
|
||||
always failing the comparison — wiping `ae_leads_loc` on **every single page load**. Both
|
||||
fixed 2026-06-16. `badges_loc`, `launcher_loc`, and `events_auth_loc` still use the old bare
|
||||
pattern below and are **not yet wired into `_check_and_wipe`** — not actively harmful (the
|
||||
wipe never runs), but apply this fix before wiring any of them in.
|
||||
|
||||
```ts
|
||||
import { PersistedState } from 'runed';
|
||||
import { badges_loc_defaults } from './ae_events_stores__badges_defaults';
|
||||
import { AE_BADGES_LOC_VERSION } from './store_versions';
|
||||
|
||||
export const badges_loc = new PersistedState('ae_badges_loc', badges_loc_defaults, {
|
||||
serializer: {
|
||||
// Stamp __version on every write so store_versions.ts's _check_and_wipe() can
|
||||
// detect a breaking schema change and clear stale browsers on next load. This
|
||||
// import also guarantees store_versions.ts's wipe side-effect runs before this
|
||||
// PersistedState reads from localStorage (ES module execution order).
|
||||
serialize: (value) =>
|
||||
JSON.stringify({ ...value, __version: AE_BADGES_LOC_VERSION }),
|
||||
// Merge with defaults so new fields added after first session get their defaults,
|
||||
// and strip __version back out so it doesn't pollute the typed state object.
|
||||
deserialize: (raw: string) => {
|
||||
const { __version, ...stored } = JSON.parse(raw);
|
||||
return { ...badges_loc_defaults, ...stored };
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Then in `store_versions.ts`'s startup block:
|
||||
```ts
|
||||
_check_and_wipe('ae_badges_loc', AE_BADGES_LOC_VERSION);
|
||||
```
|
||||
|
||||
See `ae_events_stores__pres_mgmt.svelte.ts` and `ae_events_stores__leads.svelte.ts` for the
|
||||
two real, currently-correct examples of this pattern.
|
||||
|
||||
### 3. Consumer syntax
|
||||
```ts
|
||||
// Import (note .svelte extension, not .svelte.ts):
|
||||
import { badges_loc } from '$lib/stores/ae_events_stores__badges.svelte';
|
||||
|
||||
// Read:
|
||||
badges_loc.current.fulltext_search_qry_str
|
||||
|
||||
// Write (fine-grained — only triggers effects that read this field):
|
||||
badges_loc.current.fulltext_search_qry_str = 'hello';
|
||||
|
||||
// Bulk reset:
|
||||
badges_loc.current = { ...badges_loc_defaults };
|
||||
```
|
||||
|
||||
### Key differences from old `svelte-persisted-store` pattern:
|
||||
| | Old (`persisted()`) | New (`PersistedState`) |
|
||||
|---|---|---|
|
||||
| Import | `import { events_loc } from '...ae_events_stores'` | `import { badges_loc } from '...ae_events_stores__badges.svelte'` |
|
||||
| Read | `$events_loc.badges.field` | `badges_loc.current.field` |
|
||||
| Write | `$events_loc.badges.field = x` | `badges_loc.current.field = x` |
|
||||
| Reactivity | Coarse — entire store notified | Fine-grained — only affected fields |
|
||||
| In `$effect` | Subscribes to entire store | Only subscribes to fields you read |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **`idaa_loc` → PersistedState** — Highest priority for IDAA stability. Promotes `novi_uuid/verified`,
|
||||
`archives`, `bb`, `recovery_meetings` sub-objects to their own stores following the same pattern.
|
||||
Primary benefit: eliminates the IDAA "Access Denied" corruption from `ae_loc` bootstrap writes.
|
||||
Use the `__version`-stamping serializer from the start (see Migration Pattern above) and wire
|
||||
it into `_check_and_wipe()` immediately — `idaa_loc` holds auth state, so a real wipe-on-schema-
|
||||
change matters more here than anywhere else this pattern has been applied so far.
|
||||
|
||||
2. **`ae_loc` → PersistedState** — Largest scope (~every route in the app). Defer until after
|
||||
`idaa_loc` is done. Consider extracting `auth_loc` (the identity/permission fields) as the
|
||||
first sub-store since those are the fields implicated in the IDAA corruption bug.
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
> Collection of concrete UX improvements for the Aether frontend. Each entry includes
|
||||
> the rationale, current behavior, proposed change, and implementation notes.
|
||||
> **Date:** 2026-05-17
|
||||
> **Created:** 2026-05-17
|
||||
> **Last Updated:** 2026-05-18
|
||||
|
||||
---
|
||||
|
||||
69
documentation/README__Docs_Index.md
Normal file
69
documentation/README__Docs_Index.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Aether SvelteKit — Documentation Index
|
||||
|
||||
**Doc Owner:** Frontend platform maintainers (OSIT)
|
||||
**Review Trigger:** Update whenever a documentation file is added, renamed, archived, or restored.
|
||||
|
||||
Use this file as the routing map for project documentation.
|
||||
|
||||
## 1) First Read
|
||||
|
||||
- `documentation/BOOTSTRAP__AI_Agent_Quickstart.md`
|
||||
- `documentation/TODO__Agents.md`
|
||||
|
||||
## 2) Core Guides
|
||||
|
||||
- `documentation/GUIDE__Development.md`
|
||||
- `documentation/GUIDE__AE_API_V3_for_Frontend.md`
|
||||
- `documentation/GUIDE__AE_API_V3_for_Frontend_websockets.md`
|
||||
- `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md`
|
||||
- `documentation/GUIDE__AE_UI_Style_Guidelines.md`
|
||||
- `documentation/GUIDE__Docker_CI_Cache_Policy.md`
|
||||
|
||||
## 3) Safety and Reference
|
||||
|
||||
- `documentation/AE__Architecture.md`
|
||||
- `documentation/AE__Permissions_and_Security.md`
|
||||
- `documentation/REFERENCE__Common_Agent_Mistakes.md`
|
||||
- `documentation/AE__Naming_Conventions.md`
|
||||
|
||||
## 4) Module Docs (Active)
|
||||
|
||||
### Events
|
||||
- `documentation/MODULE__AE_Events_Presentation_Management.md`
|
||||
- `documentation/MODULE__AE_Events_Launcher.md`
|
||||
- `documentation/MODULE__AE_Events_Launcher_Native.md`
|
||||
- `documentation/MODULE__AE_Events_Launcher_Config_Menu.md`
|
||||
- `documentation/MODULE__AE_Events_Badges.md`
|
||||
- `documentation/MODULE__AE_Events_Badge_Templates.md`
|
||||
- `documentation/MODULE__AE_Events_Leads.md`
|
||||
|
||||
### Journals
|
||||
- `documentation/MODULE__AE_Journals.md`
|
||||
- `documentation/MODULE__AE_Journals_Config_Map.md`
|
||||
|
||||
### IDAA
|
||||
- `documentation/CLIENT__IDAA_and_customized_mods.md`
|
||||
- `documentation/MODULE__AE_IDAA_Archives.md`
|
||||
- `documentation/MODULE__AE_IDAA_Bulletin_Board.md`
|
||||
- `documentation/MODULE__AE_IDAA_Recovery_Meetings.md`
|
||||
- `documentation/MODULE__AE_IDAA_Video_Conferences.md`
|
||||
|
||||
## 5) Active Projects
|
||||
|
||||
- `documentation/PROJECT__Documentation_Refresh_and_Archive_Plan_2026.md`
|
||||
- `documentation/PROJECT__Stores_Svelte5_Migration.md`
|
||||
- `documentation/PROJECT__IDAA_Stores_Svelte5_Migration_2026.md`
|
||||
- `documentation/PROJECT__AE_Events_PressMgmt_Config_Cleanup.md`
|
||||
- `documentation/PROJECT__AE_Site_Passcode_Security.md`
|
||||
- `documentation/PROJECT__AE_Obj_Field_Editor_New.md`
|
||||
|
||||
## 6) Active Proposals
|
||||
|
||||
- `documentation/PROPOSAL__AE_UI_UX_Future_Ideas.md`
|
||||
|
||||
## 7) Archive
|
||||
|
||||
- `documentation/archive/README.md`
|
||||
|
||||
Archive contains completed historical project notes and superseded proposals.
|
||||
Legacy API-object, component-inventory, data-structure, performance, and UI-pattern snapshots are archived because they describe pre-V3 or pre-runes behavior. Use the active V3, Dexie, style, architecture, and module docs instead.
|
||||
228
documentation/REFERENCE__Common_Agent_Mistakes.md
Normal file
228
documentation/REFERENCE__Common_Agent_Mistakes.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# Aether SvelteKit — Common Agent Mistakes (Reference)
|
||||
|
||||
This is the detailed mistake catalog referenced by `BOOTSTRAP__AI_Agent_Quickstart.md`.
|
||||
Use it as a troubleshooting and prevention guide, not as first-pass onboarding.
|
||||
|
||||
Review policy: balanced curation.
|
||||
- Keep: high-impact or recurring mistakes.
|
||||
- Archive: one-off or lower-signal historical incidents.
|
||||
|
||||
---
|
||||
|
||||
## Active Mistakes (Curated)
|
||||
|
||||
### 1) IDAA content exposed publicly
|
||||
**Impact:** Sev-1 privacy breach.
|
||||
|
||||
**What happened:** A route guard was removed on an IDAA BB route.
|
||||
|
||||
**Rule:** All `/idaa/` content is private. Never remove auth guards on IDAA routes.
|
||||
|
||||
**Verify:** Confirm route/layout auth checks still gate all IDAA pages before data load.
|
||||
|
||||
### 2) Object ID included in PATCH body (`data_kv`)
|
||||
**Impact:** 400 errors (`Unknown column in SET`).
|
||||
|
||||
**What happened:** Object ID (for URL path) was also sent in `data_kv`.
|
||||
|
||||
**Rule:** Keep object ID in the URL only; `data_kv` contains changed fields only.
|
||||
|
||||
**Verify:** In `update_ae_obj__*` calls, confirm `data_kv` excludes `*_id` fields.
|
||||
|
||||
### 3) Coarse-store `$effect` reactivity loops
|
||||
**Impact:** repeated fetches, duplicate side effects, noisy state churn.
|
||||
|
||||
**What happened:** `$effect` read fields from `svelte-persisted-store` stores (`$ae_loc`, `$idaa_loc`), subscribing to entire store.
|
||||
|
||||
**Rule:** Be minimal about coarse-store reads in `$effect`; move transient triggers to session state and keep duplicate guards in executor paths.
|
||||
|
||||
**Verify:** Check effect dependencies and ensure unrelated writes do not retrigger critical effects.
|
||||
|
||||
### 4) Dexie `.get()` used with string object ID
|
||||
**Impact:** false misses (`undefined`) despite cached data.
|
||||
|
||||
**What happened:** `.get()` queried primary key `id`, but V3 records rely on object string IDs (e.g. `person_id`).
|
||||
|
||||
**Rule:** Use `.where('<obj_id>').equals(value).first()` for object lookups.
|
||||
|
||||
**Verify:** Search for `.get(` against object IDs and replace with indexed `where` query.
|
||||
|
||||
### 5) Misunderstanding `$effect` and auth gates
|
||||
**Impact:** security confusion and wrong mitigations.
|
||||
|
||||
**What happened:** Child-component `$effect` blocks were treated as possible bypass vectors.
|
||||
|
||||
**Rule:** Child effects cannot bypass parent layout auth gates; pre-gate risk is in universal `+page.ts` / `+layout.ts` loads and prefetch.
|
||||
|
||||
**Verify:** Keep private-module data loads out of universal load files unless explicitly authenticated.
|
||||
|
||||
### 6) Using query `key` like `x-no-account-id: bypass`
|
||||
**Impact:** dropped `x-account-id`, unexpected 403 on scoped requests.
|
||||
|
||||
**What happened:** `key` was treated as bypass intent and account context was stripped.
|
||||
|
||||
**Rule:** `key` is business data, not bypass intent. Only explicit `x-no-account-id: bypass` drops account context.
|
||||
|
||||
**Verify:** Check request headers for account-scoped calls and preserve `x-account-id` unless bypass is explicitly required.
|
||||
|
||||
### 7) Pre-stringifying `*_json` before API wrappers
|
||||
**Impact:** double encoding risk and inconsistent payloads.
|
||||
|
||||
**What happened:** Callers stringified JSON fields manually before wrapper serialization.
|
||||
|
||||
**Rule:** Pass plain objects for `*_json` fields; wrappers handle serialization.
|
||||
|
||||
**Verify:** Remove `JSON.stringify(...)` around `cfg_json`, `data_json`, and related fields before wrapper calls.
|
||||
|
||||
### 8) Broad Dexie result windows silently clipped
|
||||
**Impact:** “All” views show fewer rows than narrower filters.
|
||||
|
||||
**What happened:** page limits or API revalidation replaced local broad result sets.
|
||||
|
||||
**Rule:** For empty text search, local IDB result set should drive visible results; server refresh updates cache without shrinking display.
|
||||
|
||||
**Verify:** Compare empty-search “All” view vs narrower filter counts and inspect revalidation merge behavior.
|
||||
|
||||
### 9) Missing `IDB_CONTENT_VERSIONS` bump after `properties_to_save` changes
|
||||
**Impact:** long-lived stale cache bugs (e.g. IDAA “no meetings found”).
|
||||
|
||||
**What happened:** shape persisted in IDB changed, but table content version was not bumped.
|
||||
|
||||
**Rule:** When persisted object shape/behavior changes, bump matching `IDB_CONTENT_VERSIONS` entry in `src/lib/stores/store_versions.ts`.
|
||||
|
||||
**Verify:** For relevant object changes, confirm both version increment and table-wiring path are in the same change set.
|
||||
|
||||
### 10) API retry loop broken by returning transient errors
|
||||
**Impact:** no retries on common WiFi/network blips.
|
||||
|
||||
**What happened:** transient error paths returned `false` instead of throwing, bypassing retry loop.
|
||||
|
||||
**Rule:** Keep retry classification strict:
|
||||
- transient `TypeError` and timeout aborts -> `throw`
|
||||
- navigation abort -> `return false`
|
||||
- deterministic 4xx -> `return false`
|
||||
- 5xx -> `throw`
|
||||
|
||||
**Verify:** Simulate transient network failure and confirm retry/backoff attempts occur.
|
||||
|
||||
### 11) Account-scoped trigger fired before bootstrap account is ready
|
||||
**Impact:** wrong-account API fetch and cached cross-account data.
|
||||
|
||||
**What happened:** trigger effects ran before account bootstrap settled, reading stale context.
|
||||
|
||||
**Rule:** Gate account-scoped load triggers on `$slct.account_id` readiness, not persisted `$ae_loc.account_id`.
|
||||
|
||||
**Verify:** Ensure trigger effects require browser + account_id + api readiness before fetch trigger assignment.
|
||||
|
||||
### 12) `tmp_sort_*` comparator direction inverted
|
||||
**Impact:** priority ordering reversed.
|
||||
|
||||
**What happened:** descending comparator used with `build_tmp_sort` encoding (which expects ascending).
|
||||
|
||||
**Rule:** Use ascending compare for `build_tmp_sort` outputs, with documented exceptions for legacy encodings.
|
||||
|
||||
**Verify:** Confirm comparator direction per module encoding and avoid `collection.reverse().sortBy(...)` assumptions.
|
||||
|
||||
### 13) `$` sigil used on plain prop values
|
||||
**Impact:** runtime `store_invalid_shape` errors.
|
||||
|
||||
**What happened:** child component treated normal prop values as Svelte stores.
|
||||
|
||||
**Rule:** In Svelte 5, props passed as values are plain values. Do not use `$prop` unless prop is an actual store.
|
||||
|
||||
**Verify:** In migrated components, replace `$lq__...` prop reads with plain `lq__...` prop access.
|
||||
|
||||
### 14) Null JSON blob fields not guarded
|
||||
**Impact:** read/write crashes (`cannot access property of null`).
|
||||
|
||||
**What happened:** `*_json` / `*_kv_json` DB columns were null before first write.
|
||||
|
||||
**Rule:** Use optional chaining for reads and `?? {}` initialization before object spread writes.
|
||||
|
||||
**Verify:** Audit reads/writes for `cfg_json`, `data_json`, `poc_kv_json`, and similar nullable blob fields.
|
||||
|
||||
### 15) Service worker stale-tab behavior misunderstood
|
||||
**Impact:** users run old code longer than expected, “can’t reproduce” bug reports.
|
||||
|
||||
**What happened:** deployment assumptions ignored SW activation lifecycle.
|
||||
|
||||
**Rule:** Keep SW activation behavior explicit (`skipWaiting`, `clients.claim`) and evaluate trade-offs for session-heavy flows.
|
||||
|
||||
**Verify:** After deploy, validate that long-lived tabs pick up new SW behavior as intended.
|
||||
|
||||
### 16) Local "shadow field" silently bypasses the admin-synced master field
|
||||
**Impact:** an admin config toggle appears to do nothing — the UI updates fine when a local
|
||||
per-browser preference is clicked, but the actual admin setting has zero effect, at any
|
||||
permission level. Looks like a permissions bug; isn't one.
|
||||
|
||||
**What happened:** A list/table column's visibility prop was computed from only a local-only,
|
||||
never-synced preference field (e.g. `hide__session_li_location_field`), while a
|
||||
similarly-named, admin-synced field (`hide__session_location`) existed and was correctly
|
||||
synced — just never read at that particular render call site. Found twice in the same
|
||||
session (Pres Mgmt POC column, then Location column).
|
||||
|
||||
**Rule:** When a remote config field and a local-only preference field could plausibly both
|
||||
affect the same visible thing, combine them explicitly (`admin_field || local_field`) at
|
||||
*every* call site that renders that thing — don't assume one supersedes the other, and don't
|
||||
assume fixing it once means every other render site is also fixed.
|
||||
|
||||
**Verify:** `grep` every consumer of the field name (and near-miss siblings, e.g. `_li_`/`_field`
|
||||
suffixed variants) before trusting that a config toggle does what its label says.
|
||||
|
||||
### 17) SWR `await` after a write does not mean dependent caches are fresh
|
||||
**Impact:** a value you just saved appears stale or "one save behind" immediately after
|
||||
saving — looks like inverted/random behavior when toggling a boolean back and forth, since
|
||||
being one step behind on a 2-state toggle looks exactly like being inverted.
|
||||
|
||||
**What happened:** A save handler `await`ed `load_ae_obj_id__event()` (SWR) and assumed that
|
||||
meant Dexie now had the fresh record. In reality the fast path returns the stale cache
|
||||
immediately and refreshes Dexie via a non-awaited background fetch. A *different* page's own
|
||||
sync `$effect` read Dexie before that background fetch landed, re-syncing from the stale
|
||||
record and overwriting a value that had just been set correctly moments earlier.
|
||||
|
||||
**Rule:** After a write whose result other reactive code depends on, don't rely on an awaited
|
||||
SWR reload to mean downstream caches are updated — for any call with a populated cache, it
|
||||
almost never is. Update dependent local state directly from the data you already have (what
|
||||
you just saved), not by waiting on a refetch. See the "try_cache: false Bug" section of
|
||||
`GUIDE__SvelteKit2_Svelte5_DexieJS.md` for the related, equally counter-intuitive case where
|
||||
disabling caching *also* disables the very write you wanted.
|
||||
|
||||
### 18) Conditional ("locked") sync gates are effectively undebuggable
|
||||
**Impact:** a field's local value silently depends on the *history* of an unrelated flag's
|
||||
state across past syncs, not its current value — looks like inconsistent behavior tied to
|
||||
permission level, browser, or test sequence, none of which is the actual cause.
|
||||
|
||||
**What happened:** `sync_config__event_pres_mgmt()` only applied a subset of fields when a
|
||||
separate `lock_config` flag was `true` *at that exact sync call*. Toggling `lock_config` off
|
||||
even briefly during testing froze those fields at stale values in every browser that synced
|
||||
during that window, with nothing in the UI indicating staleness. Removed entirely — every
|
||||
event already had it set to `true` in production; the conditional gated nothing real.
|
||||
|
||||
**Rule:** Avoid making one config field's sync conditional on another field's *current*
|
||||
value unless that's a genuine, deliberate design choice — and if it is, surface the
|
||||
dependency in the UI (disable the inputs, show a warning), don't bury it in a silent
|
||||
function-level gate.
|
||||
|
||||
**Verify:** Before adding `if (some_flag) { ...many field syncs... }`, check whether removing
|
||||
the gate (sync unconditionally) loses any real behavior — query production data for whether
|
||||
the gate is ever actually in the "off" state before assuming it needs to exist.
|
||||
|
||||
---
|
||||
|
||||
## Archived Historical Items (Pruned)
|
||||
|
||||
These are retained as project memory but removed from the active mistake list because they are lower-signal or one-off.
|
||||
|
||||
1. **Bad `.d.ts` module declaration masking errors** (important incident, low recurrence recently).
|
||||
2. **Launcher `file_purpose == 'admin'` filtering gap** (specific historical enum-audit miss).
|
||||
3. **Deleting files with `rm`** (still a workflow rule, now maintained in bootstrap File Safety section rather than as a mistake entry).
|
||||
|
||||
---
|
||||
|
||||
## Related Docs
|
||||
|
||||
- `documentation/BOOTSTRAP__AI_Agent_Quickstart.md`
|
||||
- `documentation/TODO__Agents.md`
|
||||
- `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md`
|
||||
- `documentation/GUIDE__AE_API_V3_for_Frontend.md`
|
||||
- `documentation/PROJECT__Stores_Svelte5_Migration.md`
|
||||
@@ -1,70 +1,106 @@
|
||||
# Frontend Agent Task List
|
||||
> **Doc Owner:** Active frontend implementation team (human + agent)
|
||||
> **Review Trigger:** Update when work starts, completes, changes priority, or moves to an archive.
|
||||
> Use this file to track steps for complex features or bug fixes.
|
||||
> **Status:** Stable — ongoing development.
|
||||
> **Scope:** Active/open work only. Completed detail lives in archive files.
|
||||
|
||||
## 🔴 CMSC Charlotte — May 27 (Presentation Management)
|
||||
**Drive down:** May 25 | **Setup:** May 26 morning | **Show:** May 27+
|
||||
## ✅ LCI October — Pres Mgmt Restoration (complete 2026-06-16)
|
||||
|
||||
- [ ] **[Launcher] Composable open flow** — refactor `handle_open_file()` to use
|
||||
`copy_from_cache_to_temp` + `run_osascript` / `run_cmd` directly instead of the all-in-one
|
||||
`launch_from_cache`. Finer error handling at each step.
|
||||
- [ ] **[Launcher] Slide control scripts in Svelte config** — Move AppleScript one-liners from
|
||||
Electron to device config or Svelte constants.
|
||||
- [ ] **[Launcher] `kill_processes` target list in config** — Implement UI for manual "Kill Apps"
|
||||
button and auto-cleanup on file open.
|
||||
- [ ] **[Launcher] End-to-end test on macOS** — test pptx and key opens on a real podium Mac.
|
||||
- [ ] **[Launcher/Electron] Wallpaper stops applying after several changes (post-CMSC)** —
|
||||
Append timestamp/random suffix to temp filename so macOS always sees a new path.
|
||||
- [ ] **[Launcher/Electron] Wallpaper drift after display hotplug (post-CMSC)** —
|
||||
Add resilient reconciliation loop or event-driven reapply on topology change.
|
||||
These features regressed over the last 6 months and must be working before the LCI conference. Reference commit for original working implementation: `bb993a102`.
|
||||
|
||||
**2026-06-16:** Pres Mgmt's config sync architecture was overhauled the same day this list's LCI event data was used for live testing — `lock_config` removed (was causing "sometimes works" reports tied to save history, not current settings), POC/Location list-table column bugs fixed (admin setting was being silently ignored), QR display fixed to match the intended global-default-with-trusted-override design, and the Config page got a documentation pass (title tooltips, POC settings split into its own section). None of this touches the open items below, but anyone picking those up should know the sync layer underneath them is in a meaningfully different (and better-understood) state now. Full incident log: `PROJECT__AE_Events_PressMgmt_Config_Cleanup.md`.
|
||||
|
||||
### Session POC (Champion/Moderator) — `session_view.svelte`
|
||||
|
||||
**Root cause of visible bugs:** The POC section is placed *below* the session hero card as a separate disconnected block. In the original it was part of a structured `<ul>` with the session name, code, datetime, location, and description all together. The current layout looks and feels wrong to users.
|
||||
|
||||
- [x] **[Pres Mgmt] POC section — move inside session hero card** (2026-06-12) Restructured hero card as a `<ul>` with datetime, room, and POC as rows inside the card. Session name and code are now always visible (not just in edit_mode — that was a bug).
|
||||
- [x] **[Pres Mgmt] POC assignment — "Select Person" flow broken** (2026-06-12) Gated the select editor on `person_options_loaded` (`Object.keys($slct.person_obj_kv).length > 0`). "Select Person" button renders as "Reload Person" after list is loaded.
|
||||
- [x] **[Pres Mgmt] Email Session POC sign-in link — UI missing** (2026-06-12) Restored email button in POC row with `sending/sent/error` state feedback. Shown when `require__session_agree && show__email_access_link && poc_person_primary_email`.
|
||||
- [x] **[Pres Mgmt] Copy Session POC access link — UI missing from session view** (2026-06-12) Restored inline `MyClipboard` copy button in POC row for trusted staff. Shown when `show__copy_access_link && trusted_access && poc_sign_in_url`.
|
||||
|
||||
### Presenter Sign-In
|
||||
|
||||
- [x] **[Pres Mgmt] Presenter email sign-in link routes to wrong page** (fixed 2026-06-12, commit `e05602b87`; verified 2026-06-16) `email_sign_in__event_presenter()` now builds a URL to `/session/[session_id]?...&presenter_id=...&presentation_id=...` instead of `/presenter/[id]` — confirmed in `ae_events__event_presenter.ts`. Verified `sign_in_out.svelte` (mounted on the session page) reads `presenter_id`/`presentation_id` from the URL and grants presenter-level auth via `auth__kv.presenter`/`auth__kv.presentation`, not just session read access.
|
||||
- [x] **[Pres Mgmt] Presenter agreement not enforced before file upload** (fixed 2026-06-16) `presenter_is_authed` only checked sign-in state, never `.agree`, so a presenter could upload without agreeing whenever `require__presenter_agree` was on. Added a new derived `presenter_agree_ok` (`trusted_access || !require__presenter_agree || auth__event_presenter_obj.agree`) and `presenter_can_upload` (`presenter_is_authed && presenter_agree_ok`) in `presenter/[presenter_id]/+page.svelte`, and swapped it in everywhere the upload UI/file-list permissions are gated (both the default view and the `manage_files` alt view — the latter's `public_access` identity bypass is preserved but still requires `presenter_agree_ok`). Also added an inline warning message in place of the upload section when signed in but pending agreement, instead of it just silently disappearing.
|
||||
|
||||
### Session POC Sign-In
|
||||
|
||||
- [x] **[Pres Mgmt] `session_page_menu.svelte` sign-in prop** (verified correct, 2026-06-16) `event_session_id={$lq__event_session_obj?.event_session_id}` — confirmed this is the real session ID from the loaded session object, not the raw URL param. The sign-in component's separate `url_session_id` (parsed from a `session_id` query param) is intentionally a different thing — it's only used for the POC/Champion sign-in link shape, not the presenter flow, which relies on the route's own `[session_id]` path param via this prop. No bug found.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Axonius DC — June 9 (Badge Printing)
|
||||
**Setup/Registration:** June 8 | **Show:** June 9
|
||||
## 🚧 Launcher/Electron — Wallpaper Reliability (post-CMSC)
|
||||
|
||||
- [ ] **[Badges] Epson C3500 fanfold badge layout** — Create/configure a fanfold badge layout
|
||||
compatible with the Epson C3500 continuous stock format.
|
||||
- [ ] **[Launcher/Electron] Wallpaper reliability**
|
||||
- [ ] Use timestamp/randomized temp filename so macOS always sees a new path.
|
||||
- [ ] Add resilient reconciliation loop or event-driven reapply on display topology changes.
|
||||
|
||||
---
|
||||
|
||||
## 🚧 Badges follow-ups (post-Axonius DC, downgraded 2026-06-16)
|
||||
|
||||
Axonius DC (June 9) is done — the show happened and the badge layout work that was 🔴 for it is complete. Downgraded from 🔴 to 🚧; these are normal backlog now, no event deadline attached. Revisit before the next badge-printing event.
|
||||
|
||||
- [ ] **[Badges] Implement review-link email delivery** — current Email Link actions only show placeholder alerts. Send to `event_badge.email`, never the attendee-editable `email_override`.
|
||||
- [ ] **[Badges] Unify review and kiosk edit permissions** — remote review reads `event.mod_badges_json.edit_permissions`; print controls read template `cfg_json.controls_cfg`. Define precedence or consolidate them so both flows enforce one documented policy.
|
||||
- [ ] **[Badges] Use template badge types in search filter** — replace the hardcoded badge-type list in `ae_comp__badge_search.svelte` with the active template's `badge_type_list`. Checked 2026-06-16: confirmed still not done — the list is still hardcoded ("Axonius 2026 badge type codes") with its own `// TODO: drive this from the event's badge templates` comment above it.
|
||||
|
||||
---
|
||||
|
||||
## 🚧 AE Obj Field Editor — `_new` Rewrite (planning, 2026-06-16)
|
||||
|
||||
`element_ae_obj_field_editor.svelte` is getting a parallel-run rewrite: Skeleton UI → Tailwind/Flowbite, removes the dead `object_reload` prop, fixes datetime format conversion (currently the caller's job, one-directional), fixes a latent select-binding type-coercion landmine, adds `email`/`url`/`tel` field types, adds generics for `current_value`/`draft_value`. Both versions run side by side until all 8 call sites are migrated and verified; see `PROJECT__AE_Obj_Field_Editor_New.md` for the full plan and migration order. Not started yet — no code written.
|
||||
|
||||
---
|
||||
|
||||
## 🚧 V3 CRUD Migration (Surgical Cleanup)
|
||||
Finalizing the 100% adoption of V3 Standard endpoints and retirement of legacy wrappers.
|
||||
|
||||
- [x] **[Badges] Presenter Agreement Form** — migrated to `update_ae_obj` (2026-05-21)
|
||||
- [ ] **[Core] Site Domain Bootstrap Refactor** — `load_ae_obj_by_fqdn__site_domain` in
|
||||
`core__site_domain.ts` still uses legacy ID-lookup-by-FQDN. Refactor to use V3
|
||||
`api.search_ae_obj` with fqdn filter per integration guide.
|
||||
- [ ] **[Core] Legacy Utility Helpers** — Refactor `ae_core_functions.ts` to use V3 helpers.
|
||||
- [ ] **[Cleanup] Delete Legacy Wrappers** — Once all callsites are migrated, remove
|
||||
`src/lib/ae_api/api_get__crud_obj_id.ts` and the legacy exports from `api.ts`.
|
||||
- [ ] **[Cleanup] Delete Legacy Wrappers** — Once all callsites are migrated, remove `src/lib/ae_api/api_get__crud_obj_id.ts` and the legacy exports from `api.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 🚧 High Priority Workstreams
|
||||
|
||||
### [Security] Site Passcode JWT Migration
|
||||
|
||||
- [ ] **[Security] Verify `/authenticate_passcode` deployment** — confirm explicit role priority, complete role flags, `auth_type: 'passcode'`, per-role TTLs, and minimum length validation.
|
||||
- [ ] **[Security] Replace local passcode comparison** — migrate `e_app_access_type.svelte` to server verification, JWT storage, and pending/error UI.
|
||||
- [ ] **[Security] Remove client-side passcode delivery/storage** — stop caching `access_code_kv_json`, remove `site_access_code_kv` from auth state, and remove passcode logging.
|
||||
- [ ] **[Security] Enforce passcode JWT expiry on restore** — expired passcode sessions must return to anonymous without affecting user-login JWT handling.
|
||||
|
||||
Reference: `documentation/PROJECT__AE_Site_Passcode_Security.md`.
|
||||
|
||||
### [Stores] Svelte 4 → Svelte 5 State Migration
|
||||
The app uses `svelte-persisted-store` (coarse reactivity). Migration target: replace with Svelte 5
|
||||
`$state`-based persistence for fine-grained updates.
|
||||
The app uses `svelte-persisted-store` (coarse reactivity). Migration target: replace with Svelte 5 `PersistedState` (from `runed`) for fine-grained updates. See `PROJECT__Stores_Svelte5_Migration.md`.
|
||||
|
||||
- [ ] **Phase A — Project plan + wrapper decision:** Write `PROJECT__Stores_Svelte5_Migration.md`.
|
||||
- [ ] **Phase B — Core auth stores (highest impact):** `ae_loc`, `idaa_loc`.
|
||||
- [ ] **Phase C — Remaining persisted stores:** `ae_api`, `ae_events_stores`.
|
||||
- [ ] **Phase D — Non-persisted writable stores:** `ae_sess`, `slct`, `ae_snip`, etc.
|
||||
- [x] **Events module — COMPLETE (2026-06-11):** `events_loc` fully retired. All 5 sub-stores (`badges_loc`, `leads_loc`, `pres_mgmt_loc`, `launcher_loc`, `events_auth_loc`) are on `PersistedState`. Unused fields also pruned from `ae_stores.ts` and `ae_idaa_stores.ts`.
|
||||
- [ ] **`idaa_loc` → PersistedState** — Highest remaining priority. Root cause of the IDAA "Access Denied" corruption bug (`ae_loc` bootstrap writes stomp on `authenticated_access`). Promote `novi_*` identity fields and `archives/bb/recovery_meetings` sub-objects.
|
||||
- [ ] **`ae_loc` → PersistedState** — Largest scope. Extract `auth_loc` sub-store first (the identity/permission fields are what get corrupted). Defer full migration until after `idaa_loc`.
|
||||
- [ ] **Non-persisted writables** (`ae_sess`, `slct`, etc.) — Low priority; no coarse-reactivity problem.
|
||||
|
||||
### [Stores] IDB Content Version System
|
||||
- [x] Write `check_and_clear_idb_tables()` helper.
|
||||
- [x] Wire helper into `db_journals.ts` and IDAA layout.
|
||||
- [ ] Roll out to `db_events.ts` (module-wide: session, presenter, badge, etc.).
|
||||
- [ ] Roll out to `db_core.ts` (site_domain, person, user).
|
||||
### [Data Layer] IDB sorting + content version rollout
|
||||
Sorting baseline is now `build_tmp_sort` (ASC chain, no `.reverse()` on tmp-sort lists).
|
||||
|
||||
### [TypeScript] svelte-check hidden errors
|
||||
- [ ] **[flowbite-svelte] `ModalProps.children` — 31 errors across 26 files.**
|
||||
Replace `children` prop binding with Svelte snippet syntax.
|
||||
**⚠️ Exception:** `ae_events__event.ts` and `ae_events__event_session.ts` use **legacy encoding** (`priority ? 1 : 0`, priority=true→`'1'`). Their sort comparators must remain **descending** until the modules are migrated to `build_tmp_sort`. `ae_events__event_presentation.ts` already uses `build_tmp_sort` (overrides generic encoding in its `specific_processor`). See `CLIENT__IDAA_and_customized_mods.md` → "Sort Encoding" for full table.
|
||||
|
||||
- [ ] **[IDB Sort] Migrate `ae_events__event.ts` to `build_tmp_sort`** — requires bumping `IDB_CONTENT_VERSIONS.events.event` (currently v3) and switching all event sort comparators to ascending. Check all pages that sort events before doing this.
|
||||
- [ ] **[IDB Sort] Roll out to `ae_events__event_session`** after sort behavior review.
|
||||
- [ ] **[IDB Sort] Roll out to `ae_events__event_presenter`** after sort behavior review.
|
||||
- [ ] **[IDB Sort] Roll out to `ae_events__event_location`** after sort behavior review.
|
||||
- [ ] **[IDB Sort] Roll out to `ae_core__person` + `ae_core__account`** after sort behavior review.
|
||||
- [ ] **[IDB Version] Roll out to `db_events.ts`** (session, presenter, badge, etc.).
|
||||
- [ ] **[IDB Version] Roll out to `db_core.ts`** (site_domain, person, user).
|
||||
|
||||
### [Journals] Journal Entry Config follow-ups
|
||||
|
||||
- [ ] **[Journals] Entry passcode secondary auth** — implement `passcode_hash` comparison.
|
||||
- [ ] **[Journals] Summary AI shortcut** — add button to modal.
|
||||
- [ ] **[Journals] Quick Add/import encryption behavior** — both creation paths currently create plaintext entries; define the intended privacy UX and add encryption support before claiming that these paths honor entry E2EE.
|
||||
- [ ] **[Journals] Remove decrypted-content console preview** — `ae_journals_decryption.ts` logs the first 30 plaintext characters after successful decryption. Never log private journal content.
|
||||
- [ ] **[Journals] Confirm outbound email-sharing requirement** — the archived UI project listed this as unfinished, but no implementation exists. Confirm product/security requirements before creating an email workflow for private journal content.
|
||||
|
||||
---
|
||||
|
||||
@@ -79,8 +115,11 @@ The app uses `svelte-persisted-store` (coarse reactivity). Migration target: rep
|
||||
|
||||
## ⚙️ DevOps & Backend
|
||||
|
||||
- [ ] **[Cleanup] Remove unused legacy API wrappers** — `create_ae_obj_crud()`, `get_ae_obj_id_crud()`, and `update_ae_obj_id_crud()` are still exported from `api.ts` but no longer called anywhere in production code. V3 migration is 100% complete. Safe to delete: definitions in `api.ts` (lines 109-260), `src/lib/ae_api/api_get__crud_obj_id.ts`, unused wrapper in `ae_core_functions.ts` (`get_site_domain_obj_from_fqdn`, `update_ae_obj_id_crud`).
|
||||
- [ ] **[Backend] `event_file` — add `cfg_json` column (post-CMSC)** — The per-file display override currently uses a localStorage workaround (`launcher_loc.current.file_display_overrides`) because `event_file` has no JSON blob column. Proper fix: add `cfg_json` to the `event_file` DB table, expose it through the FastAPI model, then migrate the frontend back to reading/writing the backend field (restoring global/cross-device persistence). Frontend code is in `launcher_file_cont.svelte` — search for `file_display_overrides`.
|
||||
- [ ] **[Backend] Re-add `Access-Control-Allow-Private-Network: true` CORS header.**
|
||||
- [ ] **[DevOps] Nginx caching** — Investigate `index.html` cache-pickup issues.
|
||||
- [x] **[DevOps] Service worker `skipWaiting` + `clients.claim`** — Root cause of "users see old code / can't reproduce in dev testing": the SW sat in waiting state until all tabs closed. IDAA members leave idaa.org open all day. Fixed 2026-06-03: both calls added to `src/service-worker.js`. See mistake #16 in `BOOTSTRAP__AI_Agent_Quickstart.md`.
|
||||
- [ ] **[DevOps] Nginx proxy buffer tuning** — Buffer settings copied from PHP guide; not optimal for Node.js. `proxy_busy_buffers_size` technically exceeds safe limit. Re-examine when enabling compression (now re-enabled) stabilizes.
|
||||
- [ ] **[DevOps] Simplify Dockerfile env file selection** — Use plain `.env` instead of `BUILD_MODE`.
|
||||
|
||||
---
|
||||
@@ -90,3 +129,4 @@ See the full completed history in:
|
||||
[documentation/archive/TODO__Agents__ARCHIVE_2026-03.md](documentation/archive/TODO__Agents__ARCHIVE_2026-03.md)
|
||||
[documentation/archive/TODO__Agents__ARCHIVE_2026-04.md](documentation/archive/TODO__Agents__ARCHIVE_2026-04.md)
|
||||
[documentation/archive/TODO__Agents__ARCHIVE_2026-05.md](documentation/archive/TODO__Agents__ARCHIVE_2026-05.md)
|
||||
[documentation/archive/TODO__Agents__ARCHIVE_2026-06.md](documentation/archive/TODO__Agents__ARCHIVE_2026-06.md)
|
||||
|
||||
@@ -17,7 +17,7 @@ Deliverables (this PR)
|
||||
- `documentation/PROJECT__AE_Docker_CI_BuildKit_implement.md` (this file)
|
||||
- `aether_container_env/Dockerfile.buildkit.example` — BuildKit-friendly multi-stage Dockerfile example.
|
||||
- `aether_container_env/ci_buildx_example.sh` — standalone CI script examples (registry cache + local cache usage).
|
||||
- `documentation/AE_Docker_CI_cache_policy.md` — cache rotation and prune guidance.
|
||||
- `documentation/GUIDE__Docker_CI_Cache_Policy.md` — cache rotation and prune guidance.
|
||||
|
||||
Tasks (implementation checklist)
|
||||
- [ ] Review existing `Dockerfile`(s) under `aether_container_env/` and repository root.
|
||||
@@ -50,4 +50,4 @@ Next steps for the container team
|
||||
Files included in this PR for reference:
|
||||
- `aether_container_env/Dockerfile.buildkit.example`
|
||||
- `aether_container_env/ci_buildx_example.sh`
|
||||
- `documentation/AE_Docker_CI_cache_policy.md`
|
||||
- `documentation/GUIDE__Docker_CI_Cache_Policy.md`
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
# PROJECT: AE Events Badges — Review Form & Print Font Controls
|
||||
# Archived Project: AE Events Badges — Review Form & Print Font Controls
|
||||
|
||||
**Created:** 2026-02-27
|
||||
**Last Updated:** 2026-03-18
|
||||
**Completed and Archived:** 2026-06-12
|
||||
**Last Verified Against Source:** 2026-06-12
|
||||
**Branch:** `ae_app_3x_llm`
|
||||
**Priority:** HIGH — first live event is Axonius, NYC, mid-April 2026
|
||||
**Owner:** Scott Idem / One Sky IT
|
||||
**Status:** ✅ TASK 1 COMPLETE | ✅ TASK 2 COMPLETE | ✅ TASK 3 COMPLETE | ✅ TASK 4.1 COMPLETE | ⏳ TASK 4.0 OPEN
|
||||
**Status:** Complete — review form, kiosk controls, auto-scaling, QR rendering, layouts, and print tracking are implemented.
|
||||
|
||||
The original project scope is complete and this document is retained as implementation history.
|
||||
Current behavior is documented in `documentation/MODULE__AE_Events_Badges.md` and
|
||||
`documentation/MODULE__AE_Events_Badge_Templates.md`. Remaining email-delivery and permission-config
|
||||
unification work is tracked in `documentation/TODO__Agents.md`. Planning statements later in this
|
||||
archived document describe the state at the time they were written and are not current instructions.
|
||||
|
||||
---
|
||||
|
||||
@@ -44,32 +50,24 @@ Both flows should respect the same permission model:
|
||||
- Permissions are configured per-event in `event.mod_badges_json.edit_permissions`.
|
||||
Hardcoded defaults are used until that config is implemented.
|
||||
|
||||
**Current gap (TASK 4):** The print page edit button is currently gated to trusted_access only.
|
||||
It needs to be accessible to attendees at the kiosk (with appropriate field-level gating),
|
||||
matching the permission model already implemented in `ae_comp__badge_review_form.svelte`.
|
||||
**Task 4 outcome:** The print controls now implement field-level editing. Authenticated users
|
||||
can edit template-approved fields, trusted staff can correct names, and trusted staff in global
|
||||
Edit Mode can edit all fields. First printing is available at public kiosk access; reprinting
|
||||
requires trusted access plus Edit Mode. Remote review uses event-level `edit_permissions`, while
|
||||
the print controls currently use template-level `controls_cfg`; unification is tracked separately.
|
||||
|
||||
---
|
||||
|
||||
## Next Up for Badges (TASK 4)
|
||||
## Task 4 Outcomes
|
||||
|
||||
### 0. Kiosk Editing — Print Page Permission Model Alignment
|
||||
**This is the most important gap before the first live event.**
|
||||
### 0. Kiosk Editing — Complete
|
||||
|
||||
Currently the print page edit button is staff-only (trusted_access gate). At the kiosk,
|
||||
attendees need to be able to edit their own fields (same attendee-level permissions as the
|
||||
review form), with staff-only fields gated appropriately.
|
||||
`ae_comp__badge_print_controls.svelte` provides the inline controls and live preview. Its default
|
||||
authenticated fields are title, affiliations, location, lead tracking, and pronouns; template
|
||||
`controls_cfg` can narrow the fields shown and editable. Email delivery remains a placeholder;
|
||||
when implemented it must send to `event_badge.email`, never `email_override`.
|
||||
|
||||
Work needed:
|
||||
- Wire the same `can_edit_fields` / `can_edit(field)` permission logic into the print page
|
||||
that `ae_comp__badge_review_form.svelte` already uses.
|
||||
- The edit panel on the print page should show attendee-editable fields to all authenticated
|
||||
users, and staff-only fields to trusted_access+.
|
||||
- The badge render (v1 or v2) should update live as the attendee edits fields.
|
||||
- Consider whether the print page needs its own inline edit panel (sidebar or overlay)
|
||||
or whether it should share/reuse the review form component alongside the badge render.
|
||||
- **Do NOT use `email_override` as the send-to address** — always use `event_badge.email`.
|
||||
|
||||
### 1. Auto-Scaling Badge Text — In Progress
|
||||
### 1. Auto-Scaling Badge Text — Complete
|
||||
`ae_comp__badge_obj_view.svelte` using `element_fit_text.svelte` (binary search auto-scale).
|
||||
Toggle between v1 (heuristic) and v2 (auto-scale) on the print page via the `v1`/`v2` header button.
|
||||
Heights tuned per layout in `fit_heights` derived object. Still needs visual tuning with real badges.
|
||||
@@ -105,10 +103,11 @@ badge data, gated by `allow_tracking` on the badge.
|
||||
|
||||
## Implementation Status
|
||||
|
||||
### ⏳ TASK 4.0: Kiosk Editing — NOT STARTED (updated 2026-03-18)
|
||||
Print page edit access needs to be opened to attendee-level permissions, not just trusted_access.
|
||||
The permission model, field list, and `can_edit()` helper from `ae_comp__badge_review_form.svelte`
|
||||
should be the reference. See Design Intent section above.
|
||||
### ✅ TASK 4.0: Kiosk Editing — COMPLETE (verified 2026-06-12)
|
||||
The print controls implement authenticated field editing, trusted name correction, trusted + Edit
|
||||
Mode full editing, and live preview. The print path uses template `controls_cfg`; the review path
|
||||
uses event `mod_badges_json.edit_permissions`. Aligning those configuration sources is a follow-up,
|
||||
not a blocker to the completed kiosk controls.
|
||||
|
||||
**Note (2026-03-18):** `style_href` and `duplex` are both fully implemented and verified in code —
|
||||
the MODULE doc TODO list was stale. `duplex` is in `properties_to_save`; v2 badge render gates
|
||||
@@ -1,7 +1,9 @@
|
||||
# Project Plan: Aether AE Obj Field Editor v3 (Consolidated)
|
||||
|
||||
> **Status:** 🟡 Mostly Complete — Phase 3 items + GUIDE update remaining
|
||||
> **Date:** February 13, 2026 (last updated: 2026-03-20)
|
||||
> **Status:** ✅ **Completed and archived 2026-06-12**
|
||||
> **Completion:** V3 component deployed and documented. Legacy V1/V2 removed. Searchable dropdowns deferred as optional enhancement.
|
||||
> **Created:** 2026-02-13
|
||||
> **Last Updated:** 2026-06-12
|
||||
> **Target Component:** `src/lib/elements/element_ae_obj_field_editor.svelte`
|
||||
> **Replaces:** `element_ae_crud.svelte` and `element_ae_crud_v2.svelte`
|
||||
|
||||
@@ -30,15 +32,17 @@ Consolidate the legacy CRUD components into a single, high-performance "Aether O
|
||||
- [x] Add a "Save" loading state with Lucide's `LoaderCircle` spinner.
|
||||
- [x] Implement a clear "Cancel" path that restores the original value.
|
||||
|
||||
### Phase 3: Field Type Parity (IN PROGRESS)
|
||||
### Phase 3: Field Type Parity (COMPLETE)
|
||||
- [x] Support `text`, `textarea`, `select`, `tiptap`, and `checkbox`.
|
||||
- [x] Add `datetime` support using native browser pickers — `date` and `datetime-local` inputs implemented.
|
||||
- [ ] Implement searchable dropdowns for the `select` type.
|
||||
- [x] Add `number` field type support.
|
||||
- [ ] *(Optional)* Implement searchable dropdowns for the `select` type — deferred as UX enhancement; basic select works for all current use cases.
|
||||
|
||||
### Phase 4: Migration & Cleanup
|
||||
### Phase 4: Migration & Cleanup (COMPLETE)
|
||||
- [x] Create a playground route for V3 verification (`/testing/ae_obj_field_editor`).
|
||||
- [x] Deprecate and remove `v1` and `v2` files — `element_ae_crud.svelte` and `element_ae_crud_v2.svelte` removed 2026-03-20.
|
||||
- [ ] Update `GUIDE__Development.md` with the new usage patterns.
|
||||
- [x] Update `GUIDE__Development.md` with the new usage patterns — documented at lines 57-109.
|
||||
- [x] Production deployment — 50+ active usages across session views, person views, locations, presentations, and leads.
|
||||
|
||||
## ⚠️ Security & Reliability Stabilization (NEW)
|
||||
- [x] **Account Context:** Fixed 403 errors by unifying API helpers to the `/v3/crud/` standard.
|
||||
@@ -1,9 +1,15 @@
|
||||
# Aether Journals UI Update (2026)
|
||||
# Archived Project: Aether Journals UI Update (2026)
|
||||
|
||||
> **Status:** 🚧 Phase 4 Active (Security/Encryption Blockers remain; Journal Entry config rework in progress)
|
||||
> **Last Updated:** 2026-05-05
|
||||
> **Status:** Completed and archived 2026-06-12
|
||||
> **Last Verified Against Source:** 2026-06-12
|
||||
> **Primary Agent:** Frontend SvelteKit Agent
|
||||
|
||||
The UI modernization scope is complete: V3 CRUD, Quick Add, Append/Prepend,
|
||||
import/export, auto-save, configuration modals, decryption isolation, and the
|
||||
Journals style pass are implemented. Unfinished security and product follow-ups
|
||||
were transferred to `documentation/TODO__Agents.md`; current operational behavior
|
||||
and limitations live in `documentation/MODULE__AE_Journals.md`.
|
||||
|
||||
## 1. Project Overview
|
||||
This document outlines the modernization of the Journals module UI in the SvelteKit frontend (`aether_app_sveltekit`). The primary goals are to fully leverage the generic V3 API architecture and introduce high-velocity productivity features for journal management.
|
||||
|
||||
@@ -29,7 +35,7 @@ This document outlines the modernization of the Journals module UI in the Svelte
|
||||
* **Definitions:** `app/ae_obj_types_def.py` -> `app/object_definitions/journals.py`
|
||||
* **Endpoints:** `/v3/crud/journal/...` and `/v3/crud/journal_entry/...`
|
||||
|
||||
### Frontend (In Progress)
|
||||
### Frontend (Completed UI modernization scope)
|
||||
* **State Management:** `src/lib/ae_journals/ae_journals_stores.ts`
|
||||
* **Local Storage:** Dexie.js (`db_journals`)
|
||||
* **API Client:** `src/lib/api/api.ts` -> `get_ae_obj`
|
||||
@@ -68,7 +74,7 @@ This document outlines the modernization of the Journals module UI in the Svelte
|
||||
- [x] Implement Bulk Export/Import system.
|
||||
- [x] Establish centralized Export Template engine.
|
||||
|
||||
### Phase 4: Polish & Security (ACTIVE)
|
||||
### Phase 4: Polish & Security (UI scope complete; security follow-ups transferred)
|
||||
- [x] Implement Auto-Save toggle and visual status indicators.
|
||||
- [x] Extract decryption workflow to non-reactive helper.
|
||||
- [x] **Standardize Configuration Modals:** Refactored Module, Journal, and Entry configuration into a unified tabbed UI.
|
||||
@@ -81,9 +87,9 @@ This document outlines the modernization of the Journals module UI in the Svelte
|
||||
- [x] **Dark mode fixes:** Entry content hover, journal view section/description background and text colors.
|
||||
- [x] **Modal close button:** All 3 config modals use `dismissable={false}` + explicit `<X>` button in header snippet for correct right-aligned placement.
|
||||
- [x] **Global select padding:** Added `padding-inline: 0.5rem` to `@layer base` in `app.css` (safe — utility `px-*` classes override it where intentional).
|
||||
- [ ] Solidify E2EE passcode system for Journals and Entries.
|
||||
- [ ] Audit encryption flow for Quick Added and Imported entries.
|
||||
- [ ] Integrate Outbound Email sharing.
|
||||
- [ ] Solidify E2EE passcode system for Journals and Entries. See active task list.
|
||||
- [ ] Audit encryption flow for Quick Added and Imported entries. See active task list.
|
||||
- [ ] Integrate Outbound Email sharing. Deferred pending product confirmation.
|
||||
|
||||
---
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Project: CRUD V3 Final Migration
|
||||
|
||||
> **Status:** 🟡 Surgical Cleanup (90% Complete — Events Module Fully Migrated)
|
||||
> **Last Updated:** 2026-05-21
|
||||
> **Status:** ✅ **Completed and archived 2026-06-12**
|
||||
> **Completion:** All production code migrated to V3. Legacy wrappers remain defined but unused.
|
||||
> **Last Updated:** 2026-06-12
|
||||
> **Goal:** Eliminate all dependency on legacy API wrappers (`create_ae_obj_crud`, `get_ae_obj_id_crud`, etc.) and ensure 100% adoption of the V3 Standard (`/v3/crud/...`).
|
||||
|
||||
---
|
||||
@@ -35,8 +36,8 @@ Legacy patterns persisting in core logic and config modules.
|
||||
|
||||
- [ ] `src/lib/ae_sponsorships/ae_sponsorships_functions.ts`
|
||||
- [x] `src/lib/ae_core/core__hosted_files.ts` (Migrated 2026-01-20)
|
||||
- [x] `src/lib/ae_core/core__site.ts` (Migrated 2026-01-26)
|
||||
- [ ] `src/lib/ae_core/core__site_domain.ts` (STILL USES `get_ae_obj_id_crud` for bootstrap)
|
||||
- [x] `src/lib/ae_core/core__site.ts` (Migrated 2026-01-26; bootstrap path uses V3 `search_ae_obj` by `fqdn`)
|
||||
- [x] `src/lib/ae_core/core__site_domain.ts` (Retired 2026-06-02; helper removed after bootstrap migration to `core__site.ts`)
|
||||
- [ ] `src/lib/ae_core/ae_core_functions.ts` (STILL USES `get_ae_obj_id_crud` / `update_ae_obj_id_crud`)
|
||||
- [ ] `src/lib/ae_core/core__country_subdivisions.ts`
|
||||
- [ ] `src/lib/ae_core/core__time_zones.ts`
|
||||
@@ -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.
|
||||
34
documentation/archive/README.md
Normal file
34
documentation/archive/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Documentation Archive Index
|
||||
|
||||
This directory preserves completed projects, superseded proposals, historical task logs, and legacy technical references.
|
||||
|
||||
Archived files are not authoritative for current implementation. Start with `documentation/README__Docs_Index.md` and the active module/guides directory.
|
||||
|
||||
## Categories
|
||||
|
||||
### Completed or Superseded Projects
|
||||
|
||||
Files prefixed with `PROJECT__` document completed implementation phases or superseded project plans.
|
||||
|
||||
### Historical Proposals
|
||||
|
||||
Files prefixed with `PROPOSAL__` preserve design ideas that are not the current implementation source.
|
||||
|
||||
### Task History
|
||||
|
||||
Files prefixed with `TODO__Agents__ARCHIVE_` contain completed task history by month.
|
||||
|
||||
### Legacy References
|
||||
|
||||
Files prefixed with `REFERENCE__Legacy_` are retained for historical context but contain pre-V3, pre-runes, or otherwise superseded guidance.
|
||||
|
||||
Do not copy implementation patterns from legacy references without validating them against current source and active guides.
|
||||
|
||||
## Restore Policy
|
||||
|
||||
Move an archived doc back to the active documentation root only when:
|
||||
|
||||
1. Its subject is active again.
|
||||
2. Its content has been reviewed against current source.
|
||||
3. Legacy paths, IDs, stores, and API conventions have been updated.
|
||||
4. It is added to `documentation/README__Docs_Index.md`.
|
||||
@@ -2,7 +2,7 @@
|
||||
- [x] **[Stores] Phase 1 — Dead code cleanup** (`ae_stores.ts`, `ae_events_stores.ts`, `ae_idaa_stores.ts`): removed `ver_idb`, stale comments, `console.log` lines, Stripe button block (zero consumers), personal Novi UUIDs, dead alternatives. Net: −202 lines across 3 files. svelte-check: 0 errors. (2026-03-16)
|
||||
- [x] **[Stores] Phase 2a — Split defaults into domain sub-files**: `ae_stores__auth_loc_defaults.ts`; `ae_events_stores__badges/launcher/leads/pres_mgmt_defaults.ts`. Spread-merged back into store structs — zero consumer changes. (2026-03-16)
|
||||
- [x] **[Stores] Phase 2b — TypeScript interfaces for defaults sub-files**: `SiteCfgJson`, `AePerson`, `AeUser`, `AccessType`, `AuthLocState`; `BadgesLocState/SessState`; `SectionState`, `LauncherLocState/SessState`; `LeadsLocState/SessState`, `TmpLicense`; `PresMgmtLocState/SessState`. svelte-check: 0 errors. (2026-03-16)
|
||||
- [x] **[UI]** Style Review Phase 1 & 2 complete — all non-frozen, non-IDAA routes migrated: FA→Lucide (events, pres_mgmt, core, badges, leads, hosted_files), `variant-*`→`preset-*` (all modules), `code_to_html` badge dict refactored to Lucide component map, FA CDN scoped to IDAA layout, global `svg.lucide { display: inline }` CSS rule added to fix icon inline flow. See `documentation/PROJECT__AE_Style_Review.md`. (2026-03-16)
|
||||
- [x] **[UI]** Style Review Phase 1 & 2 complete — all non-frozen, non-IDAA routes migrated: FA→Lucide (events, pres_mgmt, core, badges, leads, hosted_files), `variant-*`→`preset-*` (all modules), `code_to_html` badge dict refactored to Lucide component map, FA CDN scoped to IDAA layout, global `svg.lucide { display: inline }` CSS rule added to fix icon inline flow. See `documentation/archive/PROJECT__AE_Style_Review_2026-03.md`. (2026-03-16)
|
||||
- [x] **[UI]** Pres Mgmt Phase 3 — FA→Lucide icon migration across all 24 pres_mgmt files. (2026-03-16)
|
||||
- [x] **[IDAA]** `ae_idaa_comp__event_obj_id_edit.svelte` — inlined Tailwind utilities, removed `<style>` block; eliminated all 23 `@apply`/`@reference` svelte-check warnings. (2026-03-16)
|
||||
- [x] **[Badges]** Badge print page svelte-check fix: extracted print CSS to `static/ae-print-badge.css`; fixed unclosed `<script>` tag in `print/+page.svelte`. (2026-03-16)
|
||||
|
||||
139
documentation/archive/TODO__Agents__ARCHIVE_2026-06.md
Normal file
139
documentation/archive/TODO__Agents__ARCHIVE_2026-06.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Frontend Agent Task List
|
||||
> Use this file to track steps for complex features or bug fixes.
|
||||
> **Status:** Stable — ongoing development.
|
||||
|
||||
## 🔴 CMSC Charlotte — May 27 (Presentation Management)
|
||||
**Drive down:** May 25 | **Setup:** May 26 morning | **Show:** May 27+
|
||||
|
||||
- [x] **[Launcher] Composable open flow** — `handle_open_file()` uses `copy_from_cache_to_temp` +
|
||||
`run_osascript` / `run_cmd` directly with per-step error handling. Complete.
|
||||
- [x] **[Launcher] Slide control scripts in Svelte config** — AppleScript post_scripts live in
|
||||
`ae_launcher__default_launch_profiles.ts`. VLC focus-stealing fix applied. Complete.
|
||||
- [x] **[Launcher] Kill Apps button** — "Kill Apps" button added to Native OS config (System
|
||||
Actions, edit mode only). Kills PowerPoint, Keynote, Adobe Acrobat Reader DC, VLC, soffice.
|
||||
List overridable via `event_device.other_json.launcher.kill_process_li`. Auto-cleanup on file
|
||||
open (deferred — manual button sufficient for CMSC).
|
||||
- [x] **[Launcher] Hidden/deleted files still visible in Presenter file list** — Fixed by
|
||||
API-to-Dexie stale-record pruning plus Launcher background refresh loops for file lists.
|
||||
`ae_events__event_file.ts` now prunes stale records after refresh, and
|
||||
`launcher_background_sync.svelte` refreshes/prunes selected session and presenter file lists.
|
||||
(`fix(launcher): refresh file lists periodically to prune deleted/hidden files`, 2026-05)
|
||||
- [ ] **[Launcher/Electron] Wallpaper stops applying after several changes (post-CMSC)** —
|
||||
Append timestamp/random suffix to temp filename so macOS always sees a new path.
|
||||
- [ ] **[Launcher/Electron] Wallpaper drift after display hotplug (post-CMSC)** —
|
||||
Add resilient reconciliation loop or event-driven reapply on topology change.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Axonius DC — June 9 (Badge Printing)
|
||||
**Setup/Registration:** June 8 | **Show:** June 9
|
||||
|
||||
- [ ] **[Badges] Epson C3500 fanfold badge layout** — Create/configure a fanfold badge layout
|
||||
compatible with the Epson C3500 continuous stock format.
|
||||
|
||||
---
|
||||
|
||||
## 🚧 V3 CRUD Migration (Surgical Cleanup)
|
||||
Finalizing the 100% adoption of V3 Standard endpoints and retirement of legacy wrappers.
|
||||
|
||||
- [x] **[Badges] Presenter Agreement Form** — migrated to `update_ae_obj` (2026-05-21)
|
||||
- [x] **[Core] Site Domain Bootstrap Refactor** — Bootstrap path is already on V3 in
|
||||
`ae_core__site.ts` via `lookup_site_domain()` using `api.search_ae_obj` with FQDN filter
|
||||
(used by `src/routes/+layout.ts`).
|
||||
Follow-up cleanup complete: retired legacy helper `core__site_domain.ts`. (2026-06-02)
|
||||
- [ ] **[Core] Legacy Utility Helpers** — Refactor `ae_core_functions.ts` to use V3 helpers.
|
||||
- [ ] **[Cleanup] Delete Legacy Wrappers** — Once all callsites are migrated, remove
|
||||
`src/lib/ae_api/api_get__crud_obj_id.ts` and the legacy exports from `api.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 🚧 High Priority Workstreams
|
||||
|
||||
### [Stores] Svelte 4 → Svelte 5 State Migration
|
||||
The app uses `svelte-persisted-store` (coarse reactivity). Migration target: replace with Svelte 5
|
||||
`$state`-based persistence for fine-grained updates.
|
||||
|
||||
- [ ] **Phase A — Project plan + wrapper decision:** Write `PROJECT__Stores_Svelte5_Migration.md`.
|
||||
- [ ] **Phase B — Core auth stores (highest impact):** `ae_loc`, `idaa_loc`.
|
||||
- [ ] **Phase C — Remaining persisted stores:** `ae_api`, `ae_events_stores`.
|
||||
- [ ] **Phase D — Non-persisted writable stores:** `ae_sess`, `slct`, `ae_snip`, etc.
|
||||
|
||||
### [IDB Sort] `build_tmp_sort` rollout
|
||||
Shared utility in `src/lib/ae_core/core__idb_sort.ts` — fixes priority direction (inverted,
|
||||
true→'0' sorts first ASC) and zero-pads sort field (8 chars). No `.reverse()` needed.
|
||||
Sort chain: `group → priority DESC → sort ASC → [module-specific fields] → name`.
|
||||
**⚠️ Never use `.reverse()` on a `tmp_sort_*`-sorted list — inverted priority makes it wrong.**
|
||||
Documented in `GUIDE__SvelteKit2_Svelte5_DexieJS.md` (IDB Sort section).
|
||||
|
||||
- [x] `ae_events__event_presentation` — group + priority + sort + start_datetime + code + name
|
||||
- [x] `ae_journals__journal` + `ae_journals__journal_entry` — group + priority + sort + name + updated_on
|
||||
- [ ] `ae_events__event_session` — roll out when sort behavior is reviewed
|
||||
- [ ] `ae_events__event_presenter` — roll out when sort behavior is reviewed
|
||||
- [ ] `ae_events__event_location` — roll out when sort behavior is reviewed
|
||||
- [x] `ae_posts__post` + `ae_posts__post_comment` — migrated to `build_tmp_sort` with 8-char padding; BB comment list consumer updated for ASC tmp_sort ordering. (2026-06-02)
|
||||
- [ ] `ae_core__person` + `ae_core__account` — roll out when sort behavior is reviewed
|
||||
|
||||
### [Stores] IDB Content Version System
|
||||
- [x] Write `check_and_clear_idb_tables()` helper.
|
||||
- [x] Wire helper into `db_journals.ts` and IDAA layout.
|
||||
- [ ] Roll out to `db_events.ts` (module-wide: session, presenter, badge, etc.).
|
||||
- [ ] Roll out to `db_core.ts` (site_domain, person, user).
|
||||
|
||||
### [TypeScript] svelte-check hidden errors
|
||||
- [x] **[flowbite-svelte] `ModalProps.children` — 31 errors across 26 files.**
|
||||
Verified no remaining `children={...}` bindings on `<Modal>` and `npx svelte-check` is clean. (2026-06-02)
|
||||
|
||||
### [Journals] Journal Entry Config follow-ups
|
||||
- [ ] **[Journals] Entry passcode secondary auth** — implement `passcode_hash` comparison.
|
||||
- [x] **[Journals] Summary AI shortcut** — added Quick Actions button in entry config modal and wired it to close modal + scroll to AI tools panel in entry edit view. (2026-06-02)
|
||||
|
||||
### [Cleanup] Migrate remaining `lucide-svelte` imports to `@lucide/svelte`
|
||||
- [x] **[Cleanup] Migrate remaining `lucide-svelte` imports to `@lucide/svelte`**
|
||||
Migrated all 5 listed files to `@lucide/svelte` and uninstalled `lucide-svelte` from dependencies. (2026-06-02)
|
||||
|
||||
---
|
||||
|
||||
### [Pres Mgmt] Sessions hide/show toggle
|
||||
- [x] **[Pres Mgmt] Hidden sessions blink on initial load** — SCENARIO 2 fallback in
|
||||
`pres_mgmt/+page.svelte` now captures `qry_hidden` as a `$derived.by` dependency and
|
||||
applies the filter in the fallback path. No blink on page load. (2026-05-28)
|
||||
- [x] **[Pres Mgmt] API call uses live store instead of snapshot** — changed
|
||||
`pres_mgmt_loc.current.qry_hidden` → `params.qry_hidden` in `handle_search_refresh`
|
||||
API call to be consistent with fast path snapshot. (2026-05-28)
|
||||
- **Note:** `hide_event_launcher` is still active — used in `menu_session_list.svelte`
|
||||
(Launcher) to CSS-hide sessions from the list. Button to toggle it is in
|
||||
`session_page_menu.svelte`. Not used in Pres Mgmt (intentional — Pres Mgmt always shows all).
|
||||
- **Note:** Non-trusted users always have `!item.hide` applied at the component level
|
||||
in `ae_comp__event_session_obj_li.svelte` regardless of `qry_hidden`. Toggle is
|
||||
trusted-access-only in practice; direct session links still work for non-trusted users.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing & Optimization
|
||||
|
||||
- [ ] **[IDAA] IDB fast-path contact search** — parse `contact_li_json` in `search__event()`.
|
||||
- [ ] **[IDAA] Optimize Recovery Meetings SQL VIEW and indexes.**
|
||||
- [ ] **[IDAA / Events] Audit `default_qry_str` coverage** in all other event search pages.
|
||||
- [ ] **[Launcher/VLC] Linux playback investigation** — fullscreen + pause-on-end flags.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ DevOps & Backend
|
||||
|
||||
- [ ] **[Backend] `event_file` — add `cfg_json` column (post-CMSC)** — The per-file display
|
||||
override currently uses a localStorage workaround (`$events_loc.launcher.file_display_overrides`)
|
||||
because `event_file` has no JSON blob column. Proper fix: add `cfg_json` to the `event_file` DB
|
||||
table, expose it through the FastAPI model, then migrate the frontend back to reading/writing the
|
||||
backend field (restoring global/cross-device persistence). Frontend code is in
|
||||
`launcher_file_cont.svelte` — search for `file_display_overrides`.
|
||||
- [ ] **[Backend] Re-add `Access-Control-Allow-Private-Network: true` CORS header.**
|
||||
- [ ] **[DevOps] Nginx caching** — Investigate `index.html` cache-pickup issues.
|
||||
- [ ] **[DevOps] Simplify Dockerfile env file selection** — Use plain `.env` instead of `BUILD_MODE`.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Completed (archived)
|
||||
See the full completed history in:
|
||||
[documentation/archive/TODO__Agents__ARCHIVE_2026-03.md](documentation/archive/TODO__Agents__ARCHIVE_2026-03.md)
|
||||
[documentation/archive/TODO__Agents__ARCHIVE_2026-04.md](documentation/archive/TODO__Agents__ARCHIVE_2026-04.md)
|
||||
[documentation/archive/TODO__Agents__ARCHIVE_2026-05.md](documentation/archive/TODO__Agents__ARCHIVE_2026-05.md)
|
||||
273
package-lock.json
generated
273
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "osit-aether-app-svelte",
|
||||
"version": "3.00.10",
|
||||
"version": "3.00.20",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "osit-aether-app-svelte",
|
||||
"version": "3.00.10",
|
||||
"version": "3.00.20",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.20.0",
|
||||
"@codemirror/commands": "^6.10.0",
|
||||
@@ -26,12 +26,10 @@
|
||||
"@lucide/svelte": "^0.*.0",
|
||||
"@popperjs/core": "^2.11.0",
|
||||
"@tailwindcss/vite": "^4.1.10",
|
||||
"axios": "^1.7.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"dexie": "^4.0.0",
|
||||
"flowbite-svelte": "^1.28.1",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"lucide-svelte": "^0.*.0",
|
||||
"marked": "^17.0.0",
|
||||
"openai": "^6.10.0",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
@@ -3929,23 +3927,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
|
||||
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
@@ -3976,19 +3957,6 @@
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -4101,18 +4069,6 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commondir": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||
@@ -4240,15 +4196,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
@@ -4299,20 +4246,6 @@
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
@@ -4332,24 +4265,6 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||
@@ -4357,33 +4272,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||
@@ -4935,42 +4823,6 @@
|
||||
"mini-svg-data-uri": "^1.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
@@ -5003,43 +4855,6 @@
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
@@ -5065,18 +4880,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -5092,33 +4895,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -5654,15 +5430,6 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-svelte": {
|
||||
"version": "0.577.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.577.0.tgz",
|
||||
"integrity": "sha512-0i88o57KsaHWnc80J57fY99CWzlZsSdtH5kKjLUJa7z8dum/9/AbINNLzJ7NiRFUdOgMnfAmJt8jFbW2zeC5qQ==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"svelte": "^3 || ^4 || ^5.0.0-next.42"
|
||||
}
|
||||
},
|
||||
"node_modules/lz-string": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||
@@ -5693,36 +5460,6 @@
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mini-svg-data-uri": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
|
||||
@@ -6317,12 +6054,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "osit-aether-app-svelte",
|
||||
"version": "3.00.20",
|
||||
"version": "3.00.60",
|
||||
"description": "One Sky IT's Aether App created with Svelte, SvelteKit, Tailwind CSS, Lucide, Font Awesome, and Skeleton UI. -Scott Idem",
|
||||
"homepage": "https://oneskyit.com/",
|
||||
"private": true,
|
||||
@@ -105,12 +105,10 @@
|
||||
"@lucide/svelte": "^0.*.0",
|
||||
"@popperjs/core": "^2.11.0",
|
||||
"@tailwindcss/vite": "^4.1.10",
|
||||
"axios": "^1.7.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"dexie": "^4.0.0",
|
||||
"flowbite-svelte": "^1.28.1",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"lucide-svelte": "^0.*.0",
|
||||
"marked": "^17.0.0",
|
||||
"openai": "^6.10.0",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
|
||||
346
src/ae-c-lci-new.css
Normal file
346
src/ae-c-lci-new.css
Normal file
@@ -0,0 +1,346 @@
|
||||
/* ============================================================
|
||||
* LCI Theme — NEW VERSION (for review/approval before swap)
|
||||
* Source: LCI Brand Guidelines, March 2023 — leanconstruction.org
|
||||
*
|
||||
* Current live version: src/ae-c-lci.css ← DO NOT TOUCH YET
|
||||
* Theme selector: data-theme="AE_c_LCI_new"
|
||||
*
|
||||
* TO ACTIVATE FOR TESTING: change the app's theme setting from
|
||||
* 'AE_c_LCI' to 'AE_c_LCI_new'. Both files can coexist safely
|
||||
* (different data-theme selectors, no conflicts).
|
||||
*
|
||||
* TO GO LIVE: change app theme to 'AE_c_LCI_new', move this file
|
||||
* to ae-c-lci.css (old file → ~/tmp/agents_trash), update the
|
||||
* selector back to 'AE_c_LCI', and remove this comment block.
|
||||
*
|
||||
* ============================================================
|
||||
* KEY CHANGES FROM CURRENT VERSION
|
||||
* ============================================================
|
||||
*
|
||||
* 1. PRIMARY BLUE — anchored to the official LCI blue #3a5997.
|
||||
* Old primary-500: oklch(47% 0.11 262°) — too light, undersaturated.
|
||||
* New primary-500: oklch(39% 0.14 264°) ≈ #3a5997 (RGB 58/90/152,
|
||||
* CMYK 87/71/13/1, Pantone 19-3952 TCX "Surf the Web").
|
||||
* Effect: buttons, links, and key UI chrome will read as the correct
|
||||
* LCI navy rather than the current washed-out periwinkle.
|
||||
*
|
||||
* 2. SECONDARY — rebuilt from LCI's three-stop blue secondary palette.
|
||||
* Light: #d4e7f7 (RGB 212/231/247) → near secondary-50
|
||||
* Mid: #3598dc (RGB 53/152/220) → secondary-500
|
||||
* Dark: #173378 (RGB 23/51/120) → near secondary-950
|
||||
* The hue shifted from ~264° to ~245° to capture the brighter,
|
||||
* more turquoise-leaning LCI blue correctly.
|
||||
*
|
||||
* 3. TERTIARY (Purple) — corrected to LCI Dewberry #8f44ad.
|
||||
* Old: oklch(52.73% 0.17 315°) — too light, low chroma.
|
||||
* New: oklch(43% 0.20 309°) — darker, more saturated.
|
||||
* Pantone 18-3533 TCX "Dewberry".
|
||||
*
|
||||
* 4. SUCCESS (Teal) — minor adjustment, was already close.
|
||||
* #1bbc9d (RGB 27/188/157), Pantone 15-5728 TCX "Mint Leaf".
|
||||
*
|
||||
* 5. ERROR (Red) — corrected to LCI Cherry Tomato #df3527.
|
||||
* Old: oklch(59.32% 0.21 29°) — too light (6 pts), low chroma.
|
||||
* New: oklch(53% 0.27 28°) — correctly saturated, richer red.
|
||||
* Pantone 17-1563 TCX "Cherry Tomato".
|
||||
*
|
||||
* 6. WARNING — not in LCI brand guide. Keeping warm amber as a
|
||||
* UI convention (slightly adjusted from current).
|
||||
*
|
||||
* 7. TYPOGRAPHY — added per brand guide (p.12):
|
||||
* Headings: 'Proxima Nova', 'Montserrat', system-ui, sans-serif
|
||||
* Body: 'Palatino Linotype', 'Palatino', 'Book Antiqua', serif
|
||||
*
|
||||
* NOTE on Proxima Nova: it is a licensed font (Adobe/Fontspring).
|
||||
* Montserrat is the free Google Fonts alternative — load it via
|
||||
* <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap">
|
||||
* in app.html if it isn't already present.
|
||||
*
|
||||
* NOTE on body serif: Palatino is the LCI print/content spec. In
|
||||
* a data-dense web app interface, the serif can feel heavy. If the
|
||||
* client prefers the clean UI look, override --base-font-family
|
||||
* to 'Proxima Nova', 'Montserrat', system-ui, sans-serif — which
|
||||
* keeps the brand font for both headings and body, just sans-serif.
|
||||
*
|
||||
* 8. HEADING COLOR — set to primary-500 (LCI blue) per the brand
|
||||
* guide PDF, where all section headings are printed in LCI blue.
|
||||
*
|
||||
* 9. SURFACE — subtle hue shift from 196° (teal, wrong direction)
|
||||
* to 248° (blue, aligned with LCI's blue-dominant identity).
|
||||
* ============================================================ */
|
||||
|
||||
[data-theme='AE_c_LCI_new'] {
|
||||
--text-scaling: 1.067;
|
||||
--base-font-color: var(--color-surface-950);
|
||||
--base-font-color-dark: var(--color-surface-50);
|
||||
|
||||
/* LCI brand body font: Palatino (serif). See note above re: web app use. */
|
||||
--base-font-family: 'Palatino Linotype', 'Palatino', 'Book Antiqua', Georgia, serif;
|
||||
--base-font-size: inherit;
|
||||
--base-line-height: inherit;
|
||||
--base-font-weight: normal;
|
||||
--base-font-style: normal;
|
||||
--base-letter-spacing: 0em;
|
||||
|
||||
/* LCI brand heading font: Proxima Nova (fallback: Montserrat).
|
||||
Heading color: LCI blue — as shown in the brand guide PDF. */
|
||||
--heading-font-color: var(--color-primary-500);
|
||||
--heading-font-color-dark: var(--color-primary-300);
|
||||
--heading-font-family: 'Proxima Nova', 'Montserrat', system-ui, sans-serif;
|
||||
--heading-font-weight: bold;
|
||||
--heading-font-style: normal;
|
||||
--heading-letter-spacing: inherit;
|
||||
|
||||
--anchor-font-color: var(--color-primary-500);
|
||||
--anchor-font-color-dark: var(--color-primary-300);
|
||||
--anchor-font-family: inherit;
|
||||
--anchor-font-size: inherit;
|
||||
--anchor-line-height: inherit;
|
||||
--anchor-font-weight: inherit;
|
||||
--anchor-font-style: inherit;
|
||||
--anchor-letter-spacing: inherit;
|
||||
--anchor-text-decoration: none;
|
||||
--anchor-text-decoration-hover: underline;
|
||||
--anchor-text-decoration-active: none;
|
||||
--anchor-text-decoration-focus: none;
|
||||
|
||||
--spacing: 0.25rem;
|
||||
--radius-base: 0.375rem;
|
||||
--radius-container: 0.75rem;
|
||||
--default-border-width: 1px;
|
||||
--default-divide-width: 1px;
|
||||
--default-ring-width: 1px;
|
||||
|
||||
--body-background-color: var(--color-surface-50);
|
||||
--body-background-color-dark: var(--color-surface-950);
|
||||
|
||||
/* ----------------------------------------------------------
|
||||
* PRIMARY — LCI Brand Blue
|
||||
* #3a5997 | RGB 58/90/152 | CMYK 87/71/13/1
|
||||
* Pantone 19-3952 TCX "Surf the Web"
|
||||
*
|
||||
* Anchored at primary-500. Old version was too light (47%)
|
||||
* and undersaturated (C=0.11). Correct value is ~L39%, C0.14.
|
||||
* ---------------------------------------------------------- */
|
||||
--color-primary-50: oklch(94% 0.03 264deg);
|
||||
--color-primary-100: oklch(88% 0.05 264deg);
|
||||
--color-primary-200: oklch(79% 0.07 264deg);
|
||||
--color-primary-300: oklch(68% 0.09 264deg);
|
||||
--color-primary-400: oklch(56% 0.12 264deg);
|
||||
--color-primary-500: oklch(39% 0.14 264deg); /* ≈ #3a5997 — LCI primary blue */
|
||||
--color-primary-600: oklch(33% 0.13 265deg);
|
||||
--color-primary-700: oklch(27% 0.12 265deg);
|
||||
--color-primary-800: oklch(21% 0.11 266deg);
|
||||
--color-primary-900: oklch(15% 0.09 266deg);
|
||||
--color-primary-950: oklch(10% 0.07 267deg);
|
||||
--color-primary-contrast-dark: var(--color-primary-950);
|
||||
--color-primary-contrast-light: var(--color-primary-50);
|
||||
--color-primary-contrast-50: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-100: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-200: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-300: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-400: var(--color-primary-contrast-dark);
|
||||
--color-primary-contrast-500: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-600: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-700: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-800: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-900: var(--color-primary-contrast-light);
|
||||
--color-primary-contrast-950: var(--color-primary-contrast-light);
|
||||
|
||||
/* ----------------------------------------------------------
|
||||
* SECONDARY — LCI Secondary Blue Scale
|
||||
* Light: #d4e7f7 | RGB 212/231/247 | CMYK 15/3/0/0 → near -50
|
||||
* Mid: #3598dc | RGB 53/152/220 | CMYK 71/28/0/0 → -500
|
||||
* Dark: #173378 | RGB 23/51/120 | CMYK 100/92/24/10 → near -950
|
||||
*
|
||||
* Hue shifted from 264° (primary direction) to 245° to correctly
|
||||
* capture the brighter, more sky-blue character of this palette.
|
||||
* ---------------------------------------------------------- */
|
||||
--color-secondary-50: oklch(93% 0.03 245deg); /* ≈ #d4e7f7 — LCI light blue */
|
||||
--color-secondary-100: oklch(88% 0.05 246deg);
|
||||
--color-secondary-200: oklch(82% 0.08 246deg);
|
||||
--color-secondary-300: oklch(74% 0.11 246deg);
|
||||
--color-secondary-400: oklch(67% 0.15 245deg);
|
||||
--color-secondary-500: oklch(60% 0.17 245deg); /* ≈ #3598dc — LCI medium blue */
|
||||
--color-secondary-600: oklch(52% 0.16 248deg);
|
||||
--color-secondary-700: oklch(43% 0.15 252deg);
|
||||
--color-secondary-800: oklch(34% 0.14 257deg);
|
||||
--color-secondary-900: oklch(26% 0.14 262deg);
|
||||
--color-secondary-950: oklch(18% 0.12 265deg); /* ≈ #173378 — LCI dark navy */
|
||||
--color-secondary-contrast-dark: var(--color-secondary-950);
|
||||
--color-secondary-contrast-light: var(--color-secondary-50);
|
||||
--color-secondary-contrast-50: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-100: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-200: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-300: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-400: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-500: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-600: var(--color-secondary-contrast-dark);
|
||||
--color-secondary-contrast-700: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-800: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-900: var(--color-secondary-contrast-light);
|
||||
--color-secondary-contrast-950: var(--color-secondary-contrast-light);
|
||||
|
||||
/* ----------------------------------------------------------
|
||||
* TERTIARY — LCI Accent Purple / Dewberry
|
||||
* #8f44ad | RGB 143/68/173 | CMYK 53/84/0/0
|
||||
* Pantone 18-3533 TCX "Dewberry"
|
||||
*
|
||||
* Old: oklch(52.73% 0.17 315°) — too light by ~10pts, low chroma.
|
||||
* New: oklch(43% 0.20 309°) — matches the actual LCI purple.
|
||||
* ---------------------------------------------------------- */
|
||||
--color-tertiary-50: oklch(94% 0.04 315deg);
|
||||
--color-tertiary-100: oklch(87% 0.07 313deg);
|
||||
--color-tertiary-200: oklch(79% 0.10 312deg);
|
||||
--color-tertiary-300: oklch(70% 0.14 311deg);
|
||||
--color-tertiary-400: oklch(60% 0.18 310deg);
|
||||
--color-tertiary-500: oklch(43% 0.20 309deg); /* ≈ #8f44ad — LCI Dewberry purple */
|
||||
--color-tertiary-600: oklch(37% 0.18 308deg);
|
||||
--color-tertiary-700: oklch(31% 0.16 307deg);
|
||||
--color-tertiary-800: oklch(24% 0.14 306deg);
|
||||
--color-tertiary-900: oklch(18% 0.11 304deg);
|
||||
--color-tertiary-950: oklch(12% 0.08 302deg);
|
||||
--color-tertiary-contrast-dark: var(--color-tertiary-950);
|
||||
--color-tertiary-contrast-light: var(--color-tertiary-50);
|
||||
--color-tertiary-contrast-50: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-100: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-200: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-300: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-400: var(--color-tertiary-contrast-dark);
|
||||
--color-tertiary-contrast-500: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-600: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-700: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-800: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-900: var(--color-tertiary-contrast-light);
|
||||
--color-tertiary-contrast-950: var(--color-tertiary-contrast-light);
|
||||
|
||||
/* ----------------------------------------------------------
|
||||
* SUCCESS — LCI Teal / Mint Leaf
|
||||
* #1bbc9d | RGB 27/188/157 | CMYK 73/0/51/0
|
||||
* Pantone 15-5728 TCX "Mint Leaf"
|
||||
*
|
||||
* Old version was close (L71% vs correct ~L68%). Minor
|
||||
* adjustment; hue and chroma were already accurate.
|
||||
* ---------------------------------------------------------- */
|
||||
--color-success-50: oklch(96% 0.05 178deg);
|
||||
--color-success-100: oklch(90% 0.07 177deg);
|
||||
--color-success-200: oklch(85% 0.09 176deg);
|
||||
--color-success-300: oklch(80% 0.11 176deg);
|
||||
--color-success-400: oklch(74% 0.12 175deg);
|
||||
--color-success-500: oklch(68% 0.14 175deg); /* ≈ #1bbc9d — LCI Mint Leaf teal */
|
||||
--color-success-600: oklch(61% 0.13 173deg);
|
||||
--color-success-700: oklch(54% 0.12 172deg);
|
||||
--color-success-800: oklch(46% 0.10 170deg);
|
||||
--color-success-900: oklch(38% 0.09 168deg);
|
||||
--color-success-950: oklch(29% 0.07 165deg);
|
||||
--color-success-contrast-dark: var(--color-success-950);
|
||||
--color-success-contrast-light: var(--color-success-50);
|
||||
--color-success-contrast-50: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-100: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-200: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-300: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-400: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-500: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-600: var(--color-success-contrast-dark);
|
||||
--color-success-contrast-700: var(--color-success-contrast-light);
|
||||
--color-success-contrast-800: var(--color-success-contrast-light);
|
||||
--color-success-contrast-900: var(--color-success-contrast-light);
|
||||
--color-success-contrast-950: var(--color-success-contrast-light);
|
||||
|
||||
/* ----------------------------------------------------------
|
||||
* WARNING — Warm amber (UI convention; not in LCI brand guide)
|
||||
* LCI does not specify a warning/caution color. Using standard
|
||||
* amber that doesn't conflict with the brand palette.
|
||||
* ---------------------------------------------------------- */
|
||||
--color-warning-50: oklch(97% 0.04 85deg);
|
||||
--color-warning-100: oklch(94% 0.06 82deg);
|
||||
--color-warning-200: oklch(91% 0.09 80deg);
|
||||
--color-warning-300: oklch(88% 0.12 78deg);
|
||||
--color-warning-400: oklch(85% 0.14 76deg);
|
||||
--color-warning-500: oklch(80% 0.15 73deg);
|
||||
--color-warning-600: oklch(73% 0.14 70deg);
|
||||
--color-warning-700: oklch(67% 0.13 67deg);
|
||||
--color-warning-800: oklch(60% 0.12 63deg);
|
||||
--color-warning-900: oklch(53% 0.12 59deg);
|
||||
--color-warning-950: oklch(46% 0.11 54deg);
|
||||
--color-warning-contrast-dark: var(--color-warning-950);
|
||||
--color-warning-contrast-light: var(--color-warning-50);
|
||||
--color-warning-contrast-50: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-100: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-200: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-300: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-400: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-500: var(--color-warning-contrast-dark);
|
||||
--color-warning-contrast-600: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-700: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-800: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-900: var(--color-warning-contrast-light);
|
||||
--color-warning-contrast-950: var(--color-warning-contrast-light);
|
||||
|
||||
/* ----------------------------------------------------------
|
||||
* ERROR — LCI Cherry Tomato
|
||||
* #df3527 | RGB 223/53/39 | CMYK 7/93/99/1
|
||||
* Pantone 17-1563 TCX "Cherry Tomato"
|
||||
*
|
||||
* Old: oklch(59.32% 0.21 29°) — too light by 6pts, chroma too
|
||||
* low (0.21 vs correct ~0.27). The LCI red should read as a
|
||||
* strong, saturated signal red — not a muted tomato-orange.
|
||||
* ---------------------------------------------------------- */
|
||||
--color-error-50: oklch(90% 0.06 38deg);
|
||||
--color-error-100: oklch(84% 0.10 34deg);
|
||||
--color-error-200: oklch(78% 0.14 32deg);
|
||||
--color-error-300: oklch(72% 0.18 30deg);
|
||||
--color-error-400: oklch(65% 0.23 29deg);
|
||||
--color-error-500: oklch(53% 0.27 28deg); /* ≈ #df3527 — LCI Cherry Tomato */
|
||||
--color-error-600: oklch(46% 0.24 28deg);
|
||||
--color-error-700: oklch(39% 0.21 28deg);
|
||||
--color-error-800: oklch(31% 0.17 28deg);
|
||||
--color-error-900: oklch(24% 0.13 28deg);
|
||||
--color-error-950: oklch(16% 0.09 28deg);
|
||||
--color-error-contrast-dark: var(--color-error-950);
|
||||
--color-error-contrast-light: var(--color-error-50);
|
||||
--color-error-contrast-50: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-100: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-200: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-300: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-400: var(--color-error-contrast-dark);
|
||||
--color-error-contrast-500: var(--color-error-contrast-light);
|
||||
--color-error-contrast-600: var(--color-error-contrast-light);
|
||||
--color-error-contrast-700: var(--color-error-contrast-light);
|
||||
--color-error-contrast-800: var(--color-error-contrast-light);
|
||||
--color-error-contrast-900: var(--color-error-contrast-light);
|
||||
--color-error-contrast-950: var(--color-error-contrast-light);
|
||||
|
||||
/* ----------------------------------------------------------
|
||||
* SURFACE — near-neutral with subtle LCI blue tint
|
||||
*
|
||||
* Old version used hue 196° (teal direction). Shifted to 248°
|
||||
* to align with the LCI blue identity without being distracting.
|
||||
* surface-50 stays pure white (brand spec: white is a primary
|
||||
* color in the LCI palette).
|
||||
* ---------------------------------------------------------- */
|
||||
--color-surface-50: oklch(100% 0 none);
|
||||
--color-surface-100: oklch(97.5% 0.003 248deg);
|
||||
--color-surface-200: oklch(95% 0.005 248deg);
|
||||
--color-surface-300: oklch(92% 0.007 250deg);
|
||||
--color-surface-400: oklch(89% 0.008 250deg);
|
||||
--color-surface-500: oklch(86% 0.008 250deg);
|
||||
--color-surface-600: oklch(78% 0.008 252deg);
|
||||
--color-surface-700: oklch(69% 0.007 254deg);
|
||||
--color-surface-800: oklch(60% 0.006 256deg);
|
||||
--color-surface-900: oklch(50% 0.005 258deg);
|
||||
--color-surface-950: oklch(40% 0.004 260deg);
|
||||
--color-surface-contrast-dark: var(--color-surface-950);
|
||||
--color-surface-contrast-light: var(--color-surface-50);
|
||||
--color-surface-contrast-50: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-100: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-200: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-300: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-400: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-500: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-600: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-700: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-800: var(--color-surface-contrast-dark);
|
||||
--color-surface-contrast-900: var(--color-surface-contrast-light);
|
||||
--color-surface-contrast-950: var(--color-surface-contrast-light);
|
||||
}
|
||||
10
src/app.css
10
src/app.css
@@ -36,6 +36,7 @@ html.light {
|
||||
/* @import '@skeletonlabs/skeleton/themes/ae_c_lci'; */
|
||||
@import './ae-osit-default.css';
|
||||
@import './ae-c-lci.css';
|
||||
@import './ae-c-lci-new.css';
|
||||
@import './ae-c-idaa-light.css';
|
||||
@import './ae-firefly.css';
|
||||
@import './ae-firefly-steelblue.css';
|
||||
@@ -163,9 +164,16 @@ html.light {
|
||||
background-color: rgb(55 65 81); /* gray-700 */
|
||||
border-color: rgb(75 85 99); /* gray-600 */
|
||||
}
|
||||
.input::placeholder,
|
||||
.textarea::placeholder {
|
||||
font-style: italic;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.dark .input::placeholder,
|
||||
.dark .textarea::placeholder {
|
||||
color: rgb(156 163 175); /* gray-400 — legible at reduced opacity */
|
||||
color: rgb(156 163 175); /* gray-400 */
|
||||
font-style: italic;
|
||||
opacity: 0.8; /* gray-400 is already dim; subtle additional fade */
|
||||
}
|
||||
/* Option elements in dark selects — forces browser native dark chrome */
|
||||
.dark .select option {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
// *** Import Svelte specific
|
||||
import * as Lucide from 'lucide-svelte';
|
||||
import * as Lucide from '@lucide/svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
// *** Import Aether specific variables and functions
|
||||
@@ -186,16 +186,13 @@ let is_url_file = $derived.by(() => {
|
||||
|
||||
let direct_download_url = $derived.by(() => {
|
||||
if (!show_direct_download || !hosted_file_obj) return '';
|
||||
// IMPORTANT: For Direct Link Mode, we MUST use the V3 Action endpoint to support Random String IDs.
|
||||
// Legacy endpoints often expect integer IDs and will return 404 for string IDs.
|
||||
const file_id =
|
||||
hosted_file_obj.event_file_id ||
|
||||
hosted_file_obj.hosted_file_id ||
|
||||
hosted_file_id;
|
||||
const obj_type_path = hosted_file_obj.event_file_id
|
||||
? 'event_file'
|
||||
: 'hosted_file';
|
||||
return `${$ae_api.base_url}/v3/action/${obj_type_path}/${file_id}/download?filename=${ae_util.clean_filename(final_filename)}&key=${$ae_api.account_id}`;
|
||||
// Use event_file endpoint when event_file_id is present (canonical per API guide §5).
|
||||
// Fall back to hosted_file endpoint for standalone hosted_file objects.
|
||||
if (hosted_file_obj.event_file_id) {
|
||||
return `${$ae_api.base_url}/v3/action/event_file/${hosted_file_obj.event_file_id}/download?filename=${ae_util.clean_filename(final_filename)}&key=${$ae_api.account_id}`;
|
||||
}
|
||||
const file_id = hosted_file_obj.hosted_file_id || hosted_file_id;
|
||||
return `${$ae_api.base_url}/v3/action/hosted_file/${file_id}/download?filename=${ae_util.clean_filename(final_filename)}&key=${$ae_api.account_id}`;
|
||||
});
|
||||
|
||||
async function handle_click() {
|
||||
@@ -346,7 +343,20 @@ async function handle_click() {
|
||||
disabled={require_auth && !$ae_loc.authenticated_access}
|
||||
class={variant_classes}
|
||||
onclick={handle_click}
|
||||
title={`Download this file:\n${final_filename}\n[API] SHA256: ${hosted_file_obj?.hash_sha256?.slice(0, 10)}...\nHosted ID: ${file_id}\n Linked to: ${linked_to_type} ID: ${linked_to_id}`}>
|
||||
title={
|
||||
`Download this file:
|
||||
${final_filename}
|
||||
[API] SHA256: ${hosted_file_obj?.hash_sha256?.slice(0, 10)}...
|
||||
Hosted ID: ${file_id}
|
||||
|
||||
File size: ${hosted_file_obj.file_size ? ae_util.format_bytes(hosted_file_obj.file_size) : 'Unknown size'}
|
||||
Created on: ${ae_util.iso_datetime_formatter(hosted_file_obj.created_on, 'datetime_short')}
|
||||
Updated on: ${ae_util.iso_datetime_formatter(hosted_file_obj.updated_on, 'datetime_short')}
|
||||
|
||||
Open with: ${hosted_file_obj.open_in_os == 'win' ? 'Windows' : hosted_file_obj.open_in_os == 'mac' ? 'macOS' : hosted_file_obj.open_in_os == 'linux' ? 'Linux' : '--not set--'}
|
||||
|
||||
Linked to Type: ${linked_to_type ?? '--none--'} ID: ${linked_to_id ?? '---'}`
|
||||
}>
|
||||
{@render content()}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// untrack import removed — task_id sync now uses direct $effect (no untrack needed)
|
||||
// Imports
|
||||
// Import components and elements
|
||||
import * as Lucide from 'lucide-svelte';
|
||||
import * as Lucide from '@lucide/svelte';
|
||||
import Element_input_files_tbl from '$lib/elements/element_input_files_tbl.svelte';
|
||||
|
||||
// Import storage, functions, and libraries
|
||||
|
||||
54
src/lib/ae_core/core__idb_sort.ts
Normal file
54
src/lib/ae_core/core__idb_sort.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* src/lib/ae_core/core__idb_sort.ts
|
||||
*
|
||||
* Shared utility for computing tmp_sort_* fields stored in Dexie.
|
||||
* All fields are designed for ascending .sortBy() — no .reverse() needed.
|
||||
*
|
||||
* Encoding rules:
|
||||
* priority — inverted boolean: true→'0', false→'1' so priority=true sorts first (ASC)
|
||||
* sort — zero-padded integer string so "00000010" < "00000020" (correct numeric order)
|
||||
* all other fields — appended as-is; ISO 8601 datetimes already sort correctly
|
||||
*
|
||||
* Usage:
|
||||
* const { tmp_sort_1, tmp_sort_2, tmp_sort_3 } = build_tmp_sort({
|
||||
* prefix: [obj.group ?? '0'], // fields before priority (optional)
|
||||
* priority: obj.priority,
|
||||
* sort: obj.sort,
|
||||
* fields_1: [obj.start_datetime], // appended to base for tmp_sort_1
|
||||
* fields_2: [obj.name], // appended after fields_1 for tmp_sort_2
|
||||
* fields_3: [obj.updated_on], // appended after fields_2 for tmp_sort_3
|
||||
* });
|
||||
*/
|
||||
export function build_tmp_sort({
|
||||
prefix = [],
|
||||
priority,
|
||||
sort,
|
||||
fields_1 = [],
|
||||
fields_2 = [],
|
||||
fields_3 = [],
|
||||
pad_width = 8
|
||||
}: {
|
||||
prefix?: (string | null | undefined)[];
|
||||
priority?: boolean | null;
|
||||
sort?: number | string | null;
|
||||
fields_1?: (string | null | undefined)[];
|
||||
fields_2?: (string | null | undefined)[];
|
||||
fields_3?: (string | null | undefined)[];
|
||||
pad_width?: number;
|
||||
}): { tmp_sort_1: string; tmp_sort_2: string; tmp_sort_3: string } {
|
||||
const clean = (v: string | null | undefined): string => v ?? '';
|
||||
|
||||
const p = priority ? '0' : '1';
|
||||
const s = String(Number(sort ?? 0)).padStart(pad_width, '0');
|
||||
|
||||
const parts_base = [...prefix.map(clean), p, s].join('_');
|
||||
const parts_1 = fields_1.map(clean).filter(Boolean).join('_');
|
||||
const parts_2 = fields_2.map(clean).filter(Boolean).join('_');
|
||||
const parts_3 = fields_3.map(clean).filter(Boolean).join('_');
|
||||
|
||||
const tmp_sort_1 = [parts_base, parts_1].filter(Boolean).join('_');
|
||||
const tmp_sort_2 = [tmp_sort_1, parts_2].filter(Boolean).join('_');
|
||||
const tmp_sort_3 = [tmp_sort_2, parts_3].filter(Boolean).join('_');
|
||||
|
||||
return { tmp_sort_1, tmp_sort_2, tmp_sort_3 };
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
export interface Site_Domain {
|
||||
id: string;
|
||||
// id_random: string;
|
||||
site_id: string;
|
||||
site_id_random?: string;
|
||||
|
||||
fqdn: string;
|
||||
access_key?: null | string;
|
||||
required_referrer?: null | string;
|
||||
valid_for?: null | number; // In hours
|
||||
|
||||
enable: null | boolean;
|
||||
hide?: null | boolean;
|
||||
priority?: null | boolean;
|
||||
sort?: null | number;
|
||||
group?: null | string;
|
||||
notes?: null | string;
|
||||
created_on: Date;
|
||||
updated_on?: null | Date;
|
||||
}
|
||||
|
||||
import { api } from '$lib/api/api';
|
||||
|
||||
/**
|
||||
* Fetches a site_domain object by its Fully Qualified Domain Name (FQDN).
|
||||
*
|
||||
* @param api_cfg - The API configuration object.
|
||||
* @param fqdn - The FQDN of the site domain to fetch.
|
||||
* @param timeout - The request timeout in milliseconds.
|
||||
* @param log_lvl - The logging level.
|
||||
* @returns The site domain object or null if not found.
|
||||
*/
|
||||
export async function load_ae_obj_by_fqdn__site_domain({
|
||||
api_cfg,
|
||||
fqdn,
|
||||
timeout = 7000,
|
||||
log_lvl = 0
|
||||
}: {
|
||||
api_cfg: any;
|
||||
fqdn: string;
|
||||
timeout?: number;
|
||||
log_lvl?: number;
|
||||
}): Promise<any> {
|
||||
if (log_lvl) {
|
||||
console.log(
|
||||
`*** load_ae_obj_by_fqdn__site_domain() *** api.base_url=${api_cfg.base_url}, fqdn=${fqdn}, timeout=${timeout}`
|
||||
);
|
||||
}
|
||||
|
||||
const params = {};
|
||||
|
||||
try {
|
||||
const site_domain_obj = await api.get_ae_obj_id_crud({
|
||||
api_cfg: api_cfg,
|
||||
no_account_id: true, // This seems to be a special case for this endpoint
|
||||
obj_type: 'site_domain',
|
||||
obj_id: fqdn, // NOTE: This is the FQDN, not the ID.
|
||||
use_alt_table: true,
|
||||
use_alt_base: true,
|
||||
params: params,
|
||||
timeout: timeout,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
|
||||
if (site_domain_obj) {
|
||||
return site_domain_obj;
|
||||
} else {
|
||||
console.log('No results returned.');
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('No results returned or failed.', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -954,12 +954,20 @@ export async function process_ae_obj__event_props({
|
||||
* Syncs the server-side event pres_mgmt config (mod_pres_mgmt_json) into the
|
||||
* local PersistedState store (pres_mgmt_loc.current).
|
||||
*
|
||||
* Called reactively in pres_mgmt/+page.svelte whenever the event object changes.
|
||||
* Called reactively whenever the event object changes — see the sync $effect
|
||||
* in pres_mgmt/+page.svelte, session/[session_id]/+page.svelte,
|
||||
* presenter/[presenter_id]/+page.svelte, locations/+page.svelte,
|
||||
* location/[event_location_id]/+page.svelte, and reports/+page.svelte. Any new
|
||||
* pres_mgmt page that can be a first-load entry point needs the same block.
|
||||
*
|
||||
* Always-synced fields: labels, requirements, access links, file config.
|
||||
* Lock-synced fields (only when lock_config=true): all visibility/hide__ toggles,
|
||||
* launcher links, navigation limits. This prevents presenter laptops from
|
||||
* drifting into different display configs across a conference.
|
||||
* Every field in PressMgmtRemoteCfg is synced unconditionally, every call —
|
||||
* no opt-out, no gate. (Removed 2026-06-16: a `lock_config` toggle used to make
|
||||
* a subset of these fields conditional. In practice every event always had it
|
||||
* set to true, and the conditional sync it created meant a field's local value
|
||||
* depended on whatever lock_config happened to be during the LAST sync that
|
||||
* touched that particular browser — not its current value. That's effectively
|
||||
* undebuggable from the UI and was the root cause of several "sometimes works"
|
||||
* bug reports. See PROJECT__AE_Events_PressMgmt_Config_Cleanup.md.)
|
||||
*
|
||||
* Canonical key convention (enforced here):
|
||||
* hide__* → feature ON by default; true = turn off (default: false = visible)
|
||||
@@ -981,8 +989,7 @@ export function sync_config__event_pres_mgmt({
|
||||
|
||||
const loc = pres_mgmt_loc.current;
|
||||
|
||||
// --- Always sync: labels, requirements, opt-in features, file config ---
|
||||
// These are safe to always apply — they don't override local display preferences.
|
||||
// --- Labels ---
|
||||
loc.label__person_external_id =
|
||||
pres_mgmt_cfg_remote?.label__person_external_id ?? 'External ID';
|
||||
loc.label__presenter_external_id =
|
||||
@@ -992,75 +999,77 @@ export function sync_config__event_pres_mgmt({
|
||||
loc.label__session_poc_name =
|
||||
pres_mgmt_cfg_remote?.label__session_poc_name ?? 'Point of Contact';
|
||||
|
||||
// --- Requirements ---
|
||||
loc.require__presenter_agree =
|
||||
pres_mgmt_cfg_remote?.require__presenter_agree ?? false;
|
||||
loc.require__session_agree =
|
||||
pres_mgmt_cfg_remote?.require__session_agree ?? false;
|
||||
|
||||
// --- Opt-in features ---
|
||||
loc.show__copy_access_link =
|
||||
pres_mgmt_cfg_remote?.show__copy_access_link ?? false;
|
||||
loc.show__email_access_link =
|
||||
pres_mgmt_cfg_remote?.show__email_access_link ?? false;
|
||||
loc.show__session_qr =
|
||||
pres_mgmt_cfg_remote?.show__session_qr ?? false;
|
||||
loc.show__presenter_qr =
|
||||
pres_mgmt_cfg_remote?.show__presenter_qr ?? false;
|
||||
|
||||
// --- File / report config ---
|
||||
loc.file_purpose_option_kv =
|
||||
pres_mgmt_cfg_remote?.file_purpose_option_kv ?? null;
|
||||
|
||||
loc.hide__report_kv = pres_mgmt_cfg_remote?.hide__report_kv ?? {};
|
||||
|
||||
// --- Lock-synced: visibility, launcher, navigation, session/presenter fields ---
|
||||
// When lock_config=true the remote config wins for ALL display flags, preventing
|
||||
// local browser state from overriding the admin's event-level configuration.
|
||||
if (pres_mgmt_cfg_remote?.lock_config) {
|
||||
if (log_lvl) console.log(`sync_config__event_pres_mgmt: lock_config=true, forcing full sync`);
|
||||
// --- Codes ---
|
||||
loc.hide__location_code =
|
||||
pres_mgmt_cfg_remote?.hide__location_code ?? false;
|
||||
loc.hide__presentation_code =
|
||||
pres_mgmt_cfg_remote?.hide__presentation_code ?? false;
|
||||
loc.hide__presenter_code =
|
||||
pres_mgmt_cfg_remote?.hide__presenter_code ?? false;
|
||||
loc.hide__session_code =
|
||||
pres_mgmt_cfg_remote?.hide__session_code ?? false;
|
||||
|
||||
// Codes
|
||||
loc.hide__location_code =
|
||||
pres_mgmt_cfg_remote?.hide__location_code ?? false;
|
||||
loc.hide__presentation_code =
|
||||
pres_mgmt_cfg_remote?.hide__presentation_code ?? false;
|
||||
loc.hide__presenter_code =
|
||||
pres_mgmt_cfg_remote?.hide__presenter_code ?? false;
|
||||
loc.hide__session_code =
|
||||
pres_mgmt_cfg_remote?.hide__session_code ?? false;
|
||||
// --- Session fields ---
|
||||
loc.hide__session_description =
|
||||
pres_mgmt_cfg_remote?.hide__session_description ?? false;
|
||||
loc.hide__session_location =
|
||||
pres_mgmt_cfg_remote?.hide__session_location ?? false;
|
||||
loc.hide__session_msg =
|
||||
pres_mgmt_cfg_remote?.hide__session_msg ?? false;
|
||||
loc.hide__session_poc =
|
||||
pres_mgmt_cfg_remote?.hide__session_poc ?? false;
|
||||
loc.show__session_li_poc_field =
|
||||
pres_mgmt_cfg_remote?.show__session_li_poc_field ?? false;
|
||||
loc.hide__session_poc_biography =
|
||||
pres_mgmt_cfg_remote?.hide__session_poc_biography ?? false;
|
||||
loc.hide__session_poc_profile =
|
||||
pres_mgmt_cfg_remote?.hide__session_poc_profile ?? false;
|
||||
loc.hide__session_poc_profile_pic =
|
||||
pres_mgmt_cfg_remote?.hide__session_poc_profile_pic ?? false;
|
||||
|
||||
// Session fields
|
||||
loc.hide__session_description =
|
||||
pres_mgmt_cfg_remote?.hide__session_description ?? false;
|
||||
loc.hide__session_location =
|
||||
pres_mgmt_cfg_remote?.hide__session_location ?? false;
|
||||
loc.hide__session_msg =
|
||||
pres_mgmt_cfg_remote?.hide__session_msg ?? false;
|
||||
loc.hide__session_poc =
|
||||
pres_mgmt_cfg_remote?.hide__session_poc ?? false;
|
||||
loc.hide__session_poc_biography =
|
||||
pres_mgmt_cfg_remote?.hide__session_poc_biography ?? false;
|
||||
loc.hide__session_poc_profile =
|
||||
pres_mgmt_cfg_remote?.hide__session_poc_profile ?? false;
|
||||
loc.hide__session_poc_profile_pic =
|
||||
pres_mgmt_cfg_remote?.hide__session_poc_profile_pic ?? false;
|
||||
// --- Presenter fields ---
|
||||
loc.hide__presenter_biography =
|
||||
pres_mgmt_cfg_remote?.hide__presenter_biography ?? false;
|
||||
|
||||
// Presenter fields
|
||||
loc.hide__presenter_biography =
|
||||
pres_mgmt_cfg_remote?.hide__presenter_biography ?? false;
|
||||
// --- Presentation fields ---
|
||||
loc.hide__presentation_datetime =
|
||||
pres_mgmt_cfg_remote?.hide__presentation_datetime ?? false;
|
||||
loc.hide__presentation_description =
|
||||
pres_mgmt_cfg_remote?.hide__presentation_description ?? false;
|
||||
|
||||
// Presentation fields
|
||||
loc.hide__presentation_datetime =
|
||||
pres_mgmt_cfg_remote?.hide__presentation_datetime ?? false;
|
||||
loc.hide__presentation_description =
|
||||
pres_mgmt_cfg_remote?.hide__presentation_description ?? false;
|
||||
// --- Launcher links (show__ in remote → invert to hide__ in local display state) ---
|
||||
loc.hide__launcher_link =
|
||||
!(pres_mgmt_cfg_remote?.show__launcher_link ?? false);
|
||||
// Mirror the raw remote flag too — the toggle BUTTON's own visibility (not the
|
||||
// launcher link content itself) is gated on show__launcher_link directly in
|
||||
// ae_comp__events_menu_opts.svelte / event_page_menu.svelte / location_page_menu.svelte.
|
||||
loc.show__launcher_link =
|
||||
pres_mgmt_cfg_remote?.show__launcher_link ?? false;
|
||||
|
||||
// Launcher links (show__ in remote → invert to hide__ in local display state)
|
||||
loc.hide__launcher_link =
|
||||
!(pres_mgmt_cfg_remote?.show__launcher_link ?? false);
|
||||
// Legacy Flask launcher is retired — always hide regardless of remote config
|
||||
loc.hide__launcher_link_legacy = true;
|
||||
|
||||
// Navigation / UI constraints
|
||||
loc.limit__navigation =
|
||||
pres_mgmt_cfg_remote?.limit__navigation ?? false;
|
||||
loc.limit__options =
|
||||
pres_mgmt_cfg_remote?.limit__options ?? false;
|
||||
}
|
||||
// --- Navigation / UI constraints ---
|
||||
loc.limit__navigation =
|
||||
pres_mgmt_cfg_remote?.limit__navigation ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -357,7 +357,7 @@ async function _handle_nested_loads(
|
||||
for_obj_type: 'event_location',
|
||||
for_obj_id: current_location_id,
|
||||
enabled: 'all',
|
||||
limit: 25,
|
||||
hidden: 'all',
|
||||
log_lvl
|
||||
}).then((res) => (location_obj.event_file_li = res))
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { key_val } from '$lib/stores/ae_stores';
|
||||
import { api } from '$lib/api/api';
|
||||
|
||||
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
|
||||
import { build_tmp_sort } from '$lib/ae_core/core__idb_sort';
|
||||
import { db_events } from '$lib/ae_events/db_events';
|
||||
import type { ae_EventPresentation } from '$lib/types/ae_types';
|
||||
|
||||
@@ -680,6 +681,18 @@ export async function process_ae_obj__event_presentation_props({
|
||||
if (obj.event_session_id_random)
|
||||
obj.event_session_id = obj.event_session_id_random;
|
||||
if (obj.event_id_random) obj.event_id = obj.event_id_random;
|
||||
|
||||
// Override generic tmp_sort_* with presentation-specific encoding via
|
||||
// build_tmp_sort. Order: priority DESC → sort ASC → start_datetime ASC → code ASC → name ASC
|
||||
const { tmp_sort_1, tmp_sort_2 } = build_tmp_sort({
|
||||
prefix: [obj.group ?? '0'],
|
||||
priority: obj.priority,
|
||||
sort: obj.sort,
|
||||
fields_1: [obj.start_datetime, obj.code],
|
||||
fields_2: [obj.name]
|
||||
});
|
||||
obj.tmp_sort_1 = tmp_sort_1;
|
||||
obj.tmp_sort_2 = tmp_sort_2;
|
||||
return obj;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -597,10 +597,13 @@ export async function email_sign_in__event_presenter({
|
||||
return null;
|
||||
}
|
||||
const subject = `Pres Mgmt Hub Sign In Link for Presenter: ${to_name ?? 'Presenter'}`;
|
||||
// Routes to the session page (which has the sign-in handler mounted) not /presenter/[id]
|
||||
// which has no sign-in handler. Includes presenter_id + presentation_id so the handler
|
||||
// can grant presenter-level auth (not just session read access).
|
||||
const sign_in_url = encodeURI(
|
||||
`${base_url}/events/${event_id}/presenter/${event_presenter_id}?person_id=${person_id}&person_pass=${person_passcode}`
|
||||
`${base_url}/events/${event_id}/session/${event_session_id}?person_id=${person_id}&person_pass=${person_passcode}&presenter_id=${event_presenter_id}&presentation_id=${event_presentation_id}`
|
||||
);
|
||||
const body_html = `<div>${to_name},<p>Your sign-in link for ${presentation_name ?? 'Presentation'} (Session: ${session_name ?? 'Session'}): <a href="${sign_in_url}">${sign_in_url}</a></p></div>`;
|
||||
const body_html = `<div>${to_name},<p>Your sign-in link for ${presentation_name ?? 'Presentation'} (Session: ${session_name ?? 'Session'}): <a href="${sign_in_url}">${sign_in_url}</a></p><p>This link takes you to the session page — your presentation and file upload sections will be available after you sign in.</p></div>`;
|
||||
return await api.send_email({
|
||||
api_cfg,
|
||||
from_email: 'noreply+presmgmt@oneskyit.com',
|
||||
|
||||
@@ -738,11 +738,6 @@ export async function search__event_session({
|
||||
search_query.and.push({ field: 'enable', op: 'eq', value: 1 });
|
||||
else if (enabled === 'not_enabled')
|
||||
search_query.and.push({ field: 'enable', op: 'eq', value: 0 });
|
||||
if (hidden === 'hidden')
|
||||
search_query.and.push({ field: 'hide', op: 'eq', value: 1 });
|
||||
else if (hidden === 'not_hidden')
|
||||
search_query.and.push({ field: 'hide', op: 'eq', value: 0 });
|
||||
|
||||
if (location_name) {
|
||||
search_query.and.push({
|
||||
field: 'event_location_name',
|
||||
@@ -774,6 +769,7 @@ export async function search__event_session({
|
||||
view,
|
||||
limit,
|
||||
offset,
|
||||
hidden,
|
||||
log_lvl
|
||||
});
|
||||
|
||||
@@ -833,7 +829,7 @@ export async function email_sign_in__event_session({
|
||||
}) {
|
||||
const subject = `Pres Mgmt Hub Sign In Link for ${session_name}`;
|
||||
const sign_in_url = encodeURI(
|
||||
`${base_url}/events/${event_id}/session/${event_session_id}?person_id=${person_id}&person_pass=${person_passcode}`
|
||||
`${base_url}/events/${event_id}/session/${event_session_id}?person_id=${person_id}&person_pass=${person_passcode}&session_id=${event_session_id}`
|
||||
);
|
||||
const body_html = `<div>${to_name},<p>Your sign-in link for ${session_name}: <a href="${sign_in_url}">${sign_in_url}</a></p></div>`;
|
||||
return await api.send_email({
|
||||
|
||||
@@ -55,32 +55,35 @@ export interface LaunchProfile {
|
||||
/**
|
||||
* macOS VLC profile — uses direct binary path for max reliability.
|
||||
* Bypasses `open -a` argument-handling quirks that could lose file path or re-use existing process.
|
||||
*
|
||||
* WHY nohup + &:
|
||||
* run_cmd uses exec() which blocks until the child process exits (or the 30s timeout fires).
|
||||
* The direct VLC binary forks a GUI process then exits — exec returns early and the code
|
||||
* proceeds to the post_script. The old post_script polled for VLC focus (up to 10s) then
|
||||
* sent Cmd+F, which was firing exactly 10–15 seconds into playback and stopping the video.
|
||||
* nohup + & detaches VLC immediately so exec returns in ~0ms, decoupling run_cmd from
|
||||
* VLC's lifecycle entirely.
|
||||
*
|
||||
* WHY --fullscreen:
|
||||
* Starting VLC fullscreen via flag avoids the need to send Cmd+F via AppleScript. The old
|
||||
* keystroke approach was the proximate cause of the video stopping — Cmd+F may have hit the
|
||||
* wrong VLC window, triggered a menu action, or paused playback during the fullscreen
|
||||
* transition. Using the flag is simpler and more reliable.
|
||||
*
|
||||
* WHY > /dev/null 2>&1:
|
||||
* VLC logs verbosely to stdout/stderr. exec() buffers output (1MB default). Without
|
||||
* redirection the buffer could overflow and kill VLC mid-playback.
|
||||
*/
|
||||
function make_vlc_mirror_mac_profile(): LaunchProfile {
|
||||
return {
|
||||
app: 'VLC (macOS)',
|
||||
display_mode: 'mirror',
|
||||
// Direct binary path ensures VLC receives media file + flags reliably.
|
||||
// `--no-play-and-exit` prevents closing on end, `--play-and-pause` holds final frame.
|
||||
open_cmd: '/Applications/VLC.app/Contents/MacOS/VLC --no-play-and-exit --play-and-pause "{{path}}"',
|
||||
post_delay_ms: 1000,
|
||||
// Poll until VLC is frontmost before sending Cmd+F. A fixed delay is unreliable because
|
||||
// VLC cold-start on a loaded conference Mac can take 3-5 seconds.
|
||||
// Polling (15 × 0.5 s = up to 7.5 s after the initial wait) fires as soon as VLC is ready.
|
||||
open_cmd: 'nohup /Applications/VLC.app/Contents/MacOS/VLC --no-play-and-exit --play-and-pause --fullscreen "{{path}}" > /dev/null 2>&1 &',
|
||||
post_delay_ms: 3000,
|
||||
// Activate VLC after it has had time to open. Fullscreen is already set by the flag
|
||||
// above — this just ensures VLC is the frontmost app and the presenter sees it.
|
||||
post_script: `tell application "VLC"
|
||||
activate
|
||||
end tell
|
||||
repeat 15 times
|
||||
delay 0.5
|
||||
tell application "System Events"
|
||||
if frontmost of process "VLC" is true then exit repeat
|
||||
end tell
|
||||
end repeat
|
||||
delay 0.3
|
||||
tell application "System Events"
|
||||
tell process "VLC"
|
||||
keystroke "f" using command down
|
||||
end tell
|
||||
end tell`
|
||||
};
|
||||
}
|
||||
@@ -104,10 +107,10 @@ const POWERPOINT_MAC_EXTEND_PROFILE: LaunchProfile = {
|
||||
display_mode: 'extend',
|
||||
open_cmd: 'open -a "Microsoft PowerPoint" "{{path}}"',
|
||||
post_delay_ms: 1000,
|
||||
post_script: `tell application "Microsoft PowerPoint"
|
||||
activate
|
||||
end tell
|
||||
repeat 15 times
|
||||
post_script: `repeat 20 times
|
||||
tell application "Microsoft PowerPoint"
|
||||
activate
|
||||
end tell
|
||||
delay 0.5
|
||||
tell application "System Events"
|
||||
if frontmost of process "Microsoft PowerPoint" is true then exit repeat
|
||||
@@ -148,10 +151,10 @@ const LIBREOFFICE_MAC_EXTEND_PROFILE: LaunchProfile = {
|
||||
display_mode: 'extend',
|
||||
open_cmd: 'open -a "LibreOffice" "{{path}}"',
|
||||
post_delay_ms: 1000,
|
||||
post_script: `tell application "LibreOffice"
|
||||
activate
|
||||
end tell
|
||||
repeat 15 times
|
||||
post_script: `repeat 20 times
|
||||
tell application "LibreOffice"
|
||||
activate
|
||||
end tell
|
||||
delay 0.5
|
||||
tell application "System Events"
|
||||
if frontmost of process "soffice" is true then exit repeat
|
||||
@@ -170,10 +173,10 @@ const ACROBAT_MAC_MIRROR_PROFILE: LaunchProfile = {
|
||||
display_mode: 'mirror',
|
||||
open_cmd: 'open -a "Adobe Acrobat Reader DC" "{{path}}"',
|
||||
post_delay_ms: 1000,
|
||||
post_script: `tell application "Adobe Acrobat Reader DC"
|
||||
activate
|
||||
end tell
|
||||
repeat 15 times
|
||||
post_script: `repeat 20 times
|
||||
tell application "Adobe Acrobat Reader DC"
|
||||
activate
|
||||
end tell
|
||||
delay 0.5
|
||||
tell application "System Events"
|
||||
if frontmost of process "AdobeReader" is true then exit repeat
|
||||
@@ -215,10 +218,10 @@ const LIBREOFFICE_WIN_EXTEND_PROFILE: LaunchProfile = {
|
||||
display_mode: 'extend',
|
||||
open_cmd: 'open -a "LibreOffice" "{{path}}"',
|
||||
post_delay_ms: 1500,
|
||||
post_script: `tell application "LibreOffice"
|
||||
activate
|
||||
end tell
|
||||
repeat 15 times
|
||||
post_script: `repeat 20 times
|
||||
tell application "LibreOffice"
|
||||
activate
|
||||
end tell
|
||||
delay 0.5
|
||||
tell application "System Events"
|
||||
if frontmost of process "soffice" is true then exit repeat
|
||||
@@ -237,10 +240,10 @@ const ACROBAT_WIN_MIRROR_PROFILE: LaunchProfile = {
|
||||
display_mode: 'mirror',
|
||||
open_cmd: 'open -a "Acrobat Reader Windows" "{{path}}"',
|
||||
post_delay_ms: 1500,
|
||||
post_script: `tell application "Acrobat Reader Windows"
|
||||
activate
|
||||
end tell
|
||||
repeat 15 times
|
||||
post_script: `repeat 20 times
|
||||
tell application "Acrobat Reader Windows"
|
||||
activate
|
||||
end tell
|
||||
delay 0.5
|
||||
tell application "System Events"
|
||||
if frontmost of process "Acrobat Reader Windows" is true then exit repeat
|
||||
|
||||
@@ -36,6 +36,52 @@ export interface BadgeTemplateCfg {
|
||||
// Leave unset (or "0") for no bleed.
|
||||
bleed?: string;
|
||||
|
||||
// Header image vertical offset. CSS length applied as margin-top on the badge_header div.
|
||||
// Default (unset) = "2rem" (matches the prior hardcoded mt-8).
|
||||
// Negative values shift the image toward the top edge; larger values push it down.
|
||||
// Any CSS length works: "-0.5in", "1rem", "8px".
|
||||
header_margin_top?: string;
|
||||
|
||||
// Border drawn below the badge header image. Set header_border_color to enable.
|
||||
// Unset = no border (default). Any valid CSS hex color.
|
||||
header_border_color?: string;
|
||||
// Thickness of the header bottom border. Any CSS length. Default "2px" when color is set.
|
||||
header_border_width?: string;
|
||||
// Per-side padding of the badge_header div. Any CSS length. Unset = 0.5rem (Tailwind p-2 default).
|
||||
// Bottom padding creates space between the header image and the border line (e.g. "1.45in").
|
||||
header_padding_top?: string;
|
||||
header_padding_right?: string;
|
||||
header_padding_bottom?: string;
|
||||
header_padding_left?: string;
|
||||
|
||||
// Punch-out hole markers: show X overlays at the physical badge clip slot positions.
|
||||
// Slots are pre-perforated on the badge stock — markers guide attendees to push them out.
|
||||
// Hole dimensions: 5/8in wide × 1/8in tall, 1/4in from top, 3/8in from left/right edges.
|
||||
// Center hole: horizontally centered, same vertical position.
|
||||
// Colors: per-slot _fg/_bg override the shared fg/bg fallback. Unset = component defaults.
|
||||
// fg = stroke + line color (hex). bg = rectangle fill color (hex).
|
||||
punch_holes?: {
|
||||
left?: boolean;
|
||||
right?: boolean;
|
||||
center?: boolean;
|
||||
fg?: string; // shared fallback stroke/line color
|
||||
bg?: string; // shared fallback fill color
|
||||
left_fg?: string;
|
||||
left_bg?: string;
|
||||
left_rainbow?: boolean; // animated hue-rotate; overrides fg/bg base color with saturated red
|
||||
right_fg?: string;
|
||||
right_bg?: string;
|
||||
right_rainbow?: boolean;
|
||||
center_fg?: string;
|
||||
center_bg?: string;
|
||||
center_rainbow?: boolean;
|
||||
slow_pulse?: boolean; // when true: slow breathing pulse instead of fast linear cycle
|
||||
// Extra horizontal inset per side (mm) beyond the 1mm base safety margin.
|
||||
// Shrinks the visible marker width to keep it inside the physical hole on
|
||||
// printers or badge stock with variance. Default 2 when unset (see view component).
|
||||
inset_x_mm?: number;
|
||||
};
|
||||
|
||||
// Allow arbitrary extra keys to preserve forward-compatibility.
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { key_val } from '$lib/stores/ae_stores';
|
||||
import { api } from '$lib/api/api';
|
||||
|
||||
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
|
||||
import { build_tmp_sort } from '$lib/ae_core/core__idb_sort';
|
||||
import { db_journals } from '$lib/ae_journals/db_journals';
|
||||
import type { ae_Journal } from '$lib/types/ae_types';
|
||||
|
||||
@@ -885,9 +886,16 @@ export async function process_ae_obj__journal_props({
|
||||
|
||||
const updated =
|
||||
obj.updated_on ?? obj.created_on ?? new Date(0).toISOString();
|
||||
obj.tmp_sort_3 = `${obj.group ?? '0'}_${obj.priority ? 1 : 0}_${obj.sort ?? '0'}_${
|
||||
obj.name
|
||||
}_${updated}`;
|
||||
const { tmp_sort_1, tmp_sort_2, tmp_sort_3 } = build_tmp_sort({
|
||||
prefix: [obj.group ?? '0'],
|
||||
priority: obj.priority,
|
||||
sort: obj.sort,
|
||||
fields_2: [obj.name],
|
||||
fields_3: [updated]
|
||||
});
|
||||
obj.tmp_sort_1 = tmp_sort_1;
|
||||
obj.tmp_sort_2 = tmp_sort_2;
|
||||
obj.tmp_sort_3 = tmp_sort_3;
|
||||
obj.combined_passcode = `${obj.passcode ?? ''}:${obj.private_passcode ?? ''}`;
|
||||
|
||||
return obj;
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { key_val } from '$lib/stores/ae_stores';
|
||||
import { api } from '$lib/api/api';
|
||||
|
||||
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
|
||||
import { build_tmp_sort } from '$lib/ae_core/core__idb_sort';
|
||||
import { db_journals } from '$lib/ae_journals/db_journals';
|
||||
import type { ae_JournalEntry } from '$lib/types/ae_types';
|
||||
|
||||
@@ -1050,19 +1051,20 @@ export async function process_ae_obj__journal_entry_props({
|
||||
obj.history = history;
|
||||
obj.history_md_html = history_md_html;
|
||||
|
||||
// Journal entry-specific computed sort fields, overriding generic ones if needed
|
||||
const sort_val = (obj.sort ?? 0).toString().padStart(3, '0');
|
||||
// Journal entry-specific computed sort fields via build_tmp_sort.
|
||||
// Order: priority DESC → sort ASC → name ASC → updated ASC (all ascending, no .reverse())
|
||||
const updated =
|
||||
obj.updated_on ?? obj.created_on ?? new Date(0).toISOString();
|
||||
obj.tmp_sort_1 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${
|
||||
sort_val
|
||||
}_${updated}`;
|
||||
obj.tmp_sort_2 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${
|
||||
sort_val
|
||||
}_${obj.name ?? ''}_${updated}`;
|
||||
obj.tmp_sort_3 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${
|
||||
sort_val
|
||||
}_${obj.name ?? ''}_${updated}`;
|
||||
const { tmp_sort_1, tmp_sort_2, tmp_sort_3 } = build_tmp_sort({
|
||||
prefix: [obj.group ?? ''],
|
||||
priority: obj.priority,
|
||||
sort: obj.sort,
|
||||
fields_2: [obj.name],
|
||||
fields_3: [updated]
|
||||
});
|
||||
obj.tmp_sort_1 = tmp_sort_1;
|
||||
obj.tmp_sort_2 = tmp_sort_2;
|
||||
obj.tmp_sort_3 = tmp_sort_3;
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -75,8 +75,10 @@ export function journal_entry_matches_search(
|
||||
}
|
||||
|
||||
export function journal_entry_compare_for_list(a: any, b: any): number {
|
||||
// tmp_sort_1 is built by build_tmp_sort() for ascending comparison:
|
||||
// priority=true encodes as '0', priority=false as '1', so ASC puts priority first.
|
||||
return (
|
||||
(b?.tmp_sort_1 ?? '').localeCompare(a?.tmp_sort_1 ?? '') ||
|
||||
(a?.tmp_sort_1 ?? '').localeCompare(b?.tmp_sort_1 ?? '') ||
|
||||
(b?.updated_on ?? '').localeCompare(a?.updated_on ?? '') ||
|
||||
(b?.journal_entry_id ?? '').localeCompare(a?.journal_entry_id ?? '')
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
import { api } from '$lib/api/api';
|
||||
import { build_tmp_sort } from '$lib/ae_core/core__idb_sort';
|
||||
|
||||
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
|
||||
import { db_posts } from '$lib/ae_posts/db_posts';
|
||||
@@ -570,15 +571,16 @@ export async function process_ae_obj__post_props({
|
||||
if (!obj.account_id_random) obj.account_id_random = account_id;
|
||||
}
|
||||
obj.name = obj.title;
|
||||
const sort_val = (obj.sort ?? 0).toString().padStart(3, '0');
|
||||
const updated =
|
||||
obj.updated_on ?? obj.created_on ?? new Date(0).toISOString();
|
||||
obj.tmp_sort_1 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${
|
||||
sort_val
|
||||
}_${updated}`;
|
||||
obj.tmp_sort_2 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${
|
||||
sort_val
|
||||
}_${obj.updated_on ?? ''}_${obj.created_on ?? ''}`;
|
||||
const { tmp_sort_1, tmp_sort_2 } = build_tmp_sort({
|
||||
prefix: [obj.group ?? ''],
|
||||
priority: obj.priority,
|
||||
sort: obj.sort,
|
||||
fields_1: [obj.updated_on ?? obj.created_on ?? ''],
|
||||
fields_2: [obj.updated_on ?? '', obj.created_on ?? ''],
|
||||
pad_width: 8
|
||||
});
|
||||
obj.tmp_sort_1 = tmp_sort_1;
|
||||
obj.tmp_sort_2 = tmp_sort_2;
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
import { api } from '$lib/api/api';
|
||||
import { build_tmp_sort } from '$lib/ae_core/core__idb_sort';
|
||||
|
||||
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
|
||||
import { db_posts } from '$lib/ae_posts/db_posts';
|
||||
@@ -383,15 +384,16 @@ export async function process_ae_obj__post_comment_props({
|
||||
obj_type: 'post_comment',
|
||||
log_lvl,
|
||||
specific_processor: (obj) => {
|
||||
const sort_val = (obj.sort ?? 0).toString().padStart(3, '0');
|
||||
const updated =
|
||||
obj.updated_on ?? obj.created_on ?? new Date(0).toISOString();
|
||||
obj.tmp_sort_1 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${
|
||||
sort_val
|
||||
}_${updated}`;
|
||||
obj.tmp_sort_2 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${
|
||||
sort_val
|
||||
}_${obj.updated_on ?? ''}_${obj.created_on ?? ''}`;
|
||||
const { tmp_sort_1, tmp_sort_2 } = build_tmp_sort({
|
||||
prefix: [obj.group ?? ''],
|
||||
priority: obj.priority,
|
||||
sort: obj.sort,
|
||||
fields_1: [obj.updated_on ?? obj.created_on ?? ''],
|
||||
fields_2: [obj.updated_on ?? '', obj.created_on ?? ''],
|
||||
pad_width: 8
|
||||
});
|
||||
obj.tmp_sort_1 = tmp_sort_1;
|
||||
obj.tmp_sort_2 = tmp_sort_2;
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,11 @@ export const iso_datetime_formatter = function iso_datetime_formatter(
|
||||
named_format: string = 'datetime_iso_no_seconds', // date_iso, datetime_iso_no_seconds
|
||||
// Pass true/false to resolve to the correct 12h or 24h variant automatically.
|
||||
// null (default) leaves named_format unchanged — all existing call sites unaffected.
|
||||
use_12h: boolean | null = null
|
||||
use_12h: boolean | null = null,
|
||||
// When true, treats a naive datetime string (no Z / offset) as UTC so dayjs
|
||||
// converts it to local browser time on display. Use for timestamps stored as
|
||||
// UTC in the DB but returned without a timezone indicator.
|
||||
treat_as_utc: boolean = false
|
||||
) {
|
||||
// console.log('*** iso_datetime_formatter() ***');
|
||||
|
||||
@@ -74,6 +78,12 @@ export const iso_datetime_formatter = function iso_datetime_formatter(
|
||||
raw_datetime = new Date(); // Get the current datetime if one was not passed.
|
||||
}
|
||||
|
||||
// Append 'Z' to naive UTC strings so dayjs converts to local browser time.
|
||||
// Guards against double-appending if the backend ever adds timezone info.
|
||||
if (treat_as_utc && typeof raw_datetime === 'string' && !raw_datetime.match(/Z$|[+-]\d{2}:?\d{2}$/)) {
|
||||
raw_datetime = raw_datetime + 'Z';
|
||||
}
|
||||
|
||||
if (use_12h !== null) {
|
||||
named_format = use_12h
|
||||
? (TO_12H[named_format] ?? named_format)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as Lucide from 'lucide-svelte';
|
||||
import * as Lucide from '@lucide/svelte';
|
||||
|
||||
/**
|
||||
* Returns a Lucide icon component based on the provided file extension.
|
||||
|
||||
@@ -8,12 +8,10 @@ import { afterNavigate } from '$app/navigation';
|
||||
import {
|
||||
Lock,
|
||||
LockOpen,
|
||||
RefreshCw,
|
||||
ShieldEllipsis,
|
||||
ShieldMinus,
|
||||
ShieldPlus,
|
||||
ShieldUser,
|
||||
Unlink,
|
||||
User,
|
||||
UserCheck,
|
||||
UserRound,
|
||||
@@ -29,9 +27,6 @@ import {
|
||||
slct_trigger
|
||||
} from '$lib/stores/ae_stores';
|
||||
// import { core_func } from '$lib/ae_core/ae_core_functions';
|
||||
// Ideally the Event related stores should not be imported here?
|
||||
import { events_loc } from '$lib/stores/ae_events_stores';
|
||||
import { pres_mgmt_loc } from '$lib/stores/ae_events_stores__pres_mgmt.svelte';
|
||||
// import { db_events } from "$lib/db_events";
|
||||
|
||||
// export let hidden: boolean = false;
|
||||
@@ -362,47 +357,13 @@ function handle_clear_access() {
|
||||
|
||||
<div class="transition-all">
|
||||
{#if $ae_loc.trusted_access && $ae_loc.edit_mode}
|
||||
{#if $ae_loc.manager_access}
|
||||
{#if $ae_loc?.sync_local_config}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
$ae_loc.sync_local_config = false;
|
||||
pres_mgmt_loc.current.sync_local_config = false;
|
||||
|
||||
$ae_loc.lock_config = false;
|
||||
pres_mgmt_loc.current.lock_config = false;
|
||||
|
||||
// dispatch_sync_local_config_changed();
|
||||
// tick();
|
||||
return false;
|
||||
}}
|
||||
class="btn btn-sm preset-tonal-success border-success-500 hover:preset-filled-success-500 border transition-all hover:transition-all *:hover:inline"
|
||||
title="Syncing the local configuration with the remote configuration.">
|
||||
<RefreshCw size="1em" class="m-1" />
|
||||
<span class="hidden"> Sync </span>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
$ae_loc.sync_local_config = true;
|
||||
pres_mgmt_loc.current.sync_local_config = true;
|
||||
|
||||
$ae_loc.lock_config = true;
|
||||
pres_mgmt_loc.current.lock_config = true;
|
||||
|
||||
// dispatch_sync_local_config_changed();
|
||||
// tick();
|
||||
return true;
|
||||
}}
|
||||
class="btn btn-sm preset-tonal-warning border-warning-500 hover:preset-filled-warning-500 border transition-all hover:transition-all *:hover:inline"
|
||||
title="Currently not syncing with the remote server. Re-sync the local configuration with the remote configuration?">
|
||||
<Unlink size="1em" class="m-1" />
|
||||
<span class="hidden"> Re-sync? </span>
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
<!-- Removed 2026-06-16: dead "Lock Config" Sync/Unlink toggle. It wrote to
|
||||
$ae_loc.sync_local_config/lock_config and pres_mgmt_loc.current.sync_local_config/
|
||||
lock_config, but nothing in the codebase ever read any of those four fields —
|
||||
confirmed via full-repo grep. It also confusingly shared a name with the Pres
|
||||
Mgmt Config page's own "Lock Config" checkbox, which has since been removed
|
||||
entirely too — sync_config__event_pres_mgmt() now syncs unconditionally.
|
||||
See PROJECT__AE_Events_PressMgmt_Config_Cleanup.md. -->
|
||||
|
||||
<!-- {#if $ae_loc.edit_mode}
|
||||
<button
|
||||
|
||||
@@ -1,331 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Code,
|
||||
Eraser,
|
||||
LockOpen,
|
||||
RefreshCw,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
ShieldUser,
|
||||
Trash2,
|
||||
UserRound,
|
||||
Users
|
||||
} from '@lucide/svelte';
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
import {
|
||||
ae_loc,
|
||||
ae_sess,
|
||||
ae_api,
|
||||
slct,
|
||||
slct_trigger
|
||||
} from '$lib/stores/ae_stores';
|
||||
import E_app_url_builder from '$lib/app_components/e_app_url_builder.svelte';
|
||||
|
||||
// import Element_theme from '$lib/e_app_theme.svelte';
|
||||
|
||||
let notes: null | string = null;
|
||||
let all: boolean = false;
|
||||
|
||||
interface Props {
|
||||
log_lvl?: number;
|
||||
hide?: null | boolean;
|
||||
expand?: boolean;
|
||||
// export let theme_mode: null|string = null;
|
||||
// set_theme_mode?: null|boolean;
|
||||
// export let theme_name: null|string = null;
|
||||
// set_theme_name?: null|boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
log_lvl = $bindable(0),
|
||||
hide = $bindable(false),
|
||||
expand = $bindable(false)
|
||||
// set_theme_mode = null,
|
||||
// set_theme_name = null
|
||||
}: Props = $props();
|
||||
|
||||
// const dispatch = createEventDispatcher();
|
||||
|
||||
// onMount(() => {
|
||||
// // console.log('** Element Mounted: ** Element App Config');
|
||||
// if (set_theme_mode) {
|
||||
// $slct_trigger = 'set_theme_mode';
|
||||
// }
|
||||
// if (set_theme_name) {
|
||||
// $slct_trigger = 'set_theme_name';
|
||||
// }
|
||||
// });
|
||||
|
||||
// $: if ($slct_trigger == 'set_theme_mode' && $ae_loc?.app_cfg?.theme_mode) {
|
||||
// console.log(`$ae_loc.app_cfg.theme_mode=${$ae_loc?.app_cfg?.theme_mode}`);
|
||||
// $slct_trigger = null;
|
||||
// if ($ae_loc.app_cfg.theme_mode == 'light') {
|
||||
// document.documentElement.classList.remove('dark');
|
||||
// document.documentElement.classList.add('light');
|
||||
// } else if ($ae_loc.app_cfg.theme_mode == 'dark') {
|
||||
// document.documentElement.classList.remove('light');
|
||||
// document.documentElement.classList.add('dark');
|
||||
// }
|
||||
// }
|
||||
|
||||
// $: if ($slct_trigger == 'set_theme_name' && $ae_loc?.app_cfg?.theme_name) {
|
||||
// console.log(`$ae_loc?.app_cfg?.theme_name=${$ae_loc?.app_cfg?.theme_name}`);
|
||||
// $slct_trigger = null;
|
||||
// // Update the body attribute named "data-theme" to the current theme name.
|
||||
// document.body.setAttribute('data-theme', $ae_loc?.app_cfg?.theme_name);
|
||||
// }
|
||||
|
||||
// $: if (entered_passcode && entered_passcode.length >= 5) {
|
||||
// console.log(`entered_passcode=${entered_passcode}`);
|
||||
// handle_check_access_type_passcode();
|
||||
// }
|
||||
|
||||
// $: if (trigger && $ae_loc.access_type) {
|
||||
// console.log(`$ae_loc.access_type=${$ae_loc.access_type}`);
|
||||
|
||||
// let access_checks_results = ae_util.process_permission_checks($ae_loc.access_type);
|
||||
|
||||
// $ae_loc = {...$ae_loc, ...access_checks_results};
|
||||
// } else if (trigger) {
|
||||
// console.log(`$ae_loc.access_type=not set`);
|
||||
|
||||
// // Send an empty string to reset the permissions. This is the same as sending 'anonymous'.
|
||||
// let access_checks_results = ae_util.process_permission_checks('');
|
||||
|
||||
// $ae_loc = {...$ae_loc, ...access_checks_results};
|
||||
// }
|
||||
|
||||
function handle_something() {
|
||||
// console.log('*** handle_something() ***');
|
||||
}
|
||||
|
||||
function handle_clear_storage(item: null | string) {
|
||||
// console.log('*** handle_clear_storage() ***');
|
||||
// window.localStorage.setItem('access_type', 'anonymous');
|
||||
// return true;
|
||||
}
|
||||
|
||||
// function dispatch_something_changed() {
|
||||
// console.log('*** dispatch_something_changed() ***');
|
||||
|
||||
// console.log(ae_util);
|
||||
// console.log($ae_loc);
|
||||
|
||||
// dispatch('access_type_changed', {
|
||||
// access_type: $ae_loc.access_type
|
||||
// });
|
||||
// }
|
||||
</script>
|
||||
|
||||
<!-- transition duration-500 delay-1000 hover:duration-500 hover:delay-1000 hover:transition-all -->
|
||||
<section
|
||||
id="AE-App-Cfg"
|
||||
class="
|
||||
ae_app_cfg
|
||||
hidden-print
|
||||
|
||||
flex w-72
|
||||
max-w-72 flex-col
|
||||
|
||||
flex-wrap items-end justify-center gap-1
|
||||
border-2 border-gray-200
|
||||
|
||||
bg-blue-100 p-1
|
||||
text-gray-900
|
||||
transition-all delay-150
|
||||
|
||||
duration-300 hover:delay-1000 hover:ease-out dark:bg-blue-800
|
||||
dark:text-gray-200
|
||||
"
|
||||
class:hidden={hide}>
|
||||
<header class:hidden={!expand} class="ae_header w-full">
|
||||
<h2 class="text-center text-sm font-semibold">Config</h2>
|
||||
</header>
|
||||
|
||||
<div
|
||||
class="ae_cfg_content my-4 space-y-4 text-xs"
|
||||
class:hidden={!expand}
|
||||
data-sveltekit-preload-data="false">
|
||||
<section class="space-y-2">
|
||||
<div>
|
||||
<h2 class="strong">Access Type:</h2>
|
||||
</div>
|
||||
{#if $ae_loc.access_type && $ae_loc.access_type != 'anonymous'}
|
||||
{#if $ae_loc.access_type == 'super'}
|
||||
<ShieldCheck size="1em" class="mx-1" /> Super Access
|
||||
{:else if $ae_loc.access_type == 'manager'}
|
||||
<ShieldUser size="1em" class="mx-1" /> Manager Access
|
||||
{:else if $ae_loc.access_type == 'administrator'}
|
||||
<UserRound size="1em" class="mx-1" /> Administrator Access
|
||||
{:else if $ae_loc.access_type == 'trusted'}
|
||||
<UserRound size="1em" class="mx-1" /> Trusted Access
|
||||
{:else if $ae_loc.access_type == 'authenticated'}
|
||||
<Users size="1em" class="mx-1" /> Authenticated Access
|
||||
{:else if $ae_loc.access_type == 'anonymous'}
|
||||
<Users size="1em" class="mx-1" /> Anonymous Access
|
||||
{:else}
|
||||
<LockOpen size="1em" class="mx-1" /> Unknown Access
|
||||
{/if}
|
||||
|
||||
<!-- <button
|
||||
class="btn btn-sm variant-glass-secondary access_type_lock_btn hover:transition-all"
|
||||
title="Access mode is currently enabled/unlocked. Click to exit and lock."
|
||||
>
|
||||
<span class="fas fa-lock mx-1"></span> Lock
|
||||
</button> -->
|
||||
{:else}
|
||||
Not logged in
|
||||
{/if}
|
||||
</section>
|
||||
<!-- END: Access Type -->
|
||||
|
||||
<section class="space-y-2">
|
||||
<h2 class="strong">Utilities:</h2>
|
||||
<a class="btn btn-sm preset-tonal-secondary" href="/hosted_files">
|
||||
<Code size="1em" class="mx-1" />
|
||||
Util: Convert Videos
|
||||
</a>
|
||||
|
||||
{#if $ae_loc.iframe}
|
||||
<a
|
||||
class="btn btn-sm preset-tonal-secondary"
|
||||
href="/?iframe=false">
|
||||
<Code size="1em" class="mx-1" />
|
||||
Exit iframe Mode
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
class="btn btn-sm preset-tonal-secondary"
|
||||
href="/?iframe=true">
|
||||
<Code size="1em" class="mx-1" />
|
||||
Use iframe Mode
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-sm preset-tonal-warning"
|
||||
title="Reload and clear the page cache"
|
||||
onclick={() => {
|
||||
// $ae_loc.
|
||||
window.location.reload();
|
||||
}}>
|
||||
<RefreshCw size="1em" class="mx-1" />
|
||||
Reload &
|
||||
<Trash2 size="1em" class="mx-1" />
|
||||
Clear Cache
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm preset-tonal-warning"
|
||||
title="Full Reset: Delete ALL IndexedDB databases, clear localStorage and sessionStorage for this origin, then reload."
|
||||
onclick={async () => {
|
||||
if (
|
||||
!confirm(
|
||||
'FULL RESET: Delete ALL IndexedDB databases, clear localStorage and sessionStorage, then reload? This cannot be undone.'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const db_list = await indexedDB.databases();
|
||||
console.log('[clear_all] IDB databases found:', db_list.map((d) => d.name));
|
||||
for (const db of db_list) {
|
||||
if (db.name) indexedDB.deleteDatabase(db.name);
|
||||
}
|
||||
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
||||
window.location.reload();
|
||||
}}>
|
||||
<Eraser size="1em" class="mx-1" />
|
||||
Clear Storage & DB
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<!-- END: Utilities -->
|
||||
|
||||
<section class="space-y-2">
|
||||
<E_app_url_builder />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- class:justify-between={expand}
|
||||
class:justify-end={!expand} -->
|
||||
<div class="flex w-full flex-row items-center justify-between gap-2">
|
||||
<!-- {#if !expand} -->
|
||||
<span>
|
||||
{#if $ae_loc.access_type && $ae_loc.access_type != 'anonymous'}
|
||||
{#if $ae_loc.access_type == 'super'}
|
||||
<ShieldCheck size="1em" class="mx-1" /> Super Access
|
||||
{:else if $ae_loc.access_type == 'manager'}
|
||||
<ShieldUser size="1em" class="mx-1" /> Manager Access
|
||||
{:else if $ae_loc.access_type == 'administrator'}
|
||||
<UserRound size="1em" class="mx-1" /> Administrator Access
|
||||
{:else if $ae_loc.access_type == 'trusted'}
|
||||
<UserRound size="1em" class="mx-1" /> Trusted Access
|
||||
{:else if $ae_loc.access_type == 'authenticated'}
|
||||
<Users size="1em" class="mx-1" /> Authenticated Access
|
||||
{:else if $ae_loc.access_type == 'anonymous'}
|
||||
<Users size="1em" class="mx-1" /> Anonymous Access
|
||||
{:else}
|
||||
<LockOpen size="1em" class="mx-1" /> Unknown Access
|
||||
{/if}
|
||||
|
||||
<!-- <button
|
||||
class="btn btn-sm variant-glass-secondary access_type_lock_btn hover:transition-all"
|
||||
title="Access mode is currently enabled/unlocked. Click to exit and lock."
|
||||
>
|
||||
<span class="fas fa-lock mx-1"></span> Lock
|
||||
</button> -->
|
||||
{:else}
|
||||
Not logged in
|
||||
{/if}
|
||||
</span>
|
||||
<!-- {/if} -->
|
||||
|
||||
<button
|
||||
class="
|
||||
ae_cfg_btn
|
||||
btn btn-sm preset-tonal-warning
|
||||
group
|
||||
text-sm transition-all
|
||||
"
|
||||
onclick={() => {
|
||||
expand = !expand;
|
||||
}}>
|
||||
<!-- <span class="fas fa-cog m-1"></span> -->
|
||||
<span class="inline-block" title="Settings">
|
||||
<Settings class="m-1" />
|
||||
</span>
|
||||
<span
|
||||
class="
|
||||
cfg_text
|
||||
hidden
|
||||
group-hover:inline
|
||||
">
|
||||
Settings
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="postcss">
|
||||
.ae_cfg_btn .cfg_text {
|
||||
/* display: none; */
|
||||
}
|
||||
|
||||
.ae_cfg_btn:hover .cfg_text {
|
||||
/* display: initial; */
|
||||
/* outline: solid thin red; */
|
||||
}
|
||||
|
||||
/* .access_type .current_text {
|
||||
display: none;
|
||||
} */
|
||||
|
||||
/* .access_type:hover .current_text {
|
||||
display: initial;
|
||||
} */
|
||||
/* END: AE's Svelte App Config component */
|
||||
</style>
|
||||
@@ -204,29 +204,34 @@ let access_label = $derived.by(() => {
|
||||
return map[t] ?? t;
|
||||
});
|
||||
|
||||
// Theme options — keep in sync with e_app_url_builder.svelte
|
||||
const theme_options = [
|
||||
{ value: '', label: '-- None --' },
|
||||
{ value: 'cerberus', label: 'Cerberus' },
|
||||
{ value: 'concord', label: 'Concord' },
|
||||
{ value: 'crimson', label: 'Crimson' },
|
||||
{ value: 'hamlindigo', label: 'Hamlindigo' },
|
||||
{ value: 'modern', label: 'Modern' },
|
||||
// Theme picker groups — keep in sync with e_app_url_builder.svelte
|
||||
// Pruned to themes that are actually used. Full Skeleton list had 10 entries;
|
||||
// concord/crimson/hamlindigo/rocket/terminus/vintage removed (never used).
|
||||
const skeleton_themes = [
|
||||
{ value: 'nouveau', label: 'Nouveau' },
|
||||
{ value: 'rocket', label: 'Rocket' },
|
||||
{ value: 'terminus', label: 'Terminus' },
|
||||
{ value: 'vintage', label: 'Vintage' },
|
||||
{ value: 'cerberus', label: 'Cerberus' },
|
||||
{ value: 'modern', label: 'Modern' },
|
||||
{ value: 'wintry', label: 'Wintry' },
|
||||
];
|
||||
// AE custom themes — Firefly variants are event-specific; prune when no longer needed.
|
||||
const ae_themes = [
|
||||
{ value: 'AE_OSIT_default', label: 'OSIT' },
|
||||
{ value: 'AE_Firefly', label: 'Firefly ✦' },
|
||||
{ value: 'AE_Firefly_SteelBlue', label: 'Firefly SteelBlue ✦' },
|
||||
{ value: 'AE_Firefly_Indigo', label: 'Firefly Indigo ✦' },
|
||||
{ value: 'AE_Firefly_Rainbow', label: 'Firefly Rainbow ✨' },
|
||||
{ value: 'AE_Firefly_Axonius', label: 'Firefly Axonius ✦' },
|
||||
{ value: 'AE_Firefly_BGH', label: 'Firefly BGH ✦' },
|
||||
{ value: 'AE_c_IDAA_light', label: 'IDAA – light' },
|
||||
{ value: 'AE_c_LCI', label: 'LCI' }
|
||||
{ value: 'AE_Firefly_SteelBlue', label: 'Steel Blue ✦' },
|
||||
{ value: 'AE_Firefly_Indigo', label: 'Indigo ✦' },
|
||||
{ value: 'AE_Firefly_Rainbow', label: 'Rainbow ✨' },
|
||||
{ value: 'AE_Firefly_Axonius', label: 'Axonius ✦' },
|
||||
{ value: 'AE_Firefly_BGH', label: 'BGH ✦' },
|
||||
{ value: 'AE_c_IDAA_light', label: 'IDAA' },
|
||||
{ value: 'AE_c_LCI', label: 'LCI' },
|
||||
{ value: 'AE_c_LCI_new', label: 'LCI New ✦' },
|
||||
];
|
||||
|
||||
function apply_theme(value: string) {
|
||||
document.documentElement.setAttribute('data-theme', value);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ae_loc.update((l: Record<string, any>) => ({ ...l, theme_name: value, user_theme_selected: true }));
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════════════
|
||||
@@ -253,11 +258,17 @@ const theme_options = [
|
||||
ae_sys_panel
|
||||
flex max-h-[80vh] w-80 max-w-[94vw]
|
||||
flex-col items-stretch
|
||||
gap-0 overflow-x-hidden overflow-y-auto
|
||||
rounded-xl border border-gray-200/70 bg-white/85 text-sm
|
||||
text-gray-900 shadow-2xl backdrop-blur-md
|
||||
dark:border-gray-700/70 dark:bg-gray-900/85
|
||||
dark:text-gray-100
|
||||
gap-0
|
||||
overflow-x-hidden overflow-y-auto
|
||||
rounded-xl border
|
||||
border-primary-400-600/50
|
||||
bg-white/0 dark:bg-gray-900/60
|
||||
shadow-2xl backdrop-blur-md
|
||||
text-sm
|
||||
text-gray-950-50
|
||||
transition-all
|
||||
duration-300
|
||||
|
||||
">
|
||||
<!-- Panel header: person info + close -->
|
||||
<div
|
||||
@@ -280,9 +291,9 @@ const theme_options = [
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-error ml-2 shrink-0 transition-all"
|
||||
class="btn btn-icon btn-sm preset-filled-tertiary-200-800 hover:preset-filled-error ml-2 shrink-0 transition-all"
|
||||
onclick={toggle_expand}
|
||||
title="Close menu">
|
||||
title="Close the Aether system menu">
|
||||
<CircleX size="1.1em" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -292,7 +303,7 @@ const theme_options = [
|
||||
<div class="border-b border-gray-100 dark:border-gray-800">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between px-3 py-2 text-xs font-semibold tracking-wider text-gray-500 uppercase transition-colors hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-800"
|
||||
class="flex w-full items-center justify-between px-3 py-2 text-xs font-semibold tracking-wider text-surface-600-400 uppercase transition-colors hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
onclick={() => (sec_signin = !sec_signin)}>
|
||||
<span class="flex min-w-0 items-center gap-1.5">
|
||||
{#if $ae_loc?.person_id && $ae_loc?.user_id}
|
||||
@@ -332,7 +343,7 @@ const theme_options = [
|
||||
<div class="border-b border-gray-100 dark:border-gray-800">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between px-3 py-2 text-xs font-semibold tracking-wider text-gray-500 uppercase transition-colors hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-800"
|
||||
class="flex w-full items-center justify-between px-3 py-2 text-xs font-semibold tracking-wider text-surface-600-400 uppercase transition-colors hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
onclick={() => (sec_access = !sec_access)}>
|
||||
<span class="flex min-w-0 items-center gap-1.5">
|
||||
{#if $ae_loc?.access_type && $ae_loc?.access_type !== 'anonymous'}
|
||||
@@ -386,7 +397,7 @@ const theme_options = [
|
||||
<div class="border-b border-gray-100 dark:border-gray-800">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between px-3 py-2 text-xs font-semibold tracking-wider text-gray-500 uppercase transition-colors hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-800"
|
||||
class="flex w-full items-center justify-between px-3 py-2 text-xs font-semibold tracking-wider text-surface-600-400 uppercase transition-colors hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
onclick={() => (sec_appearance = !sec_appearance)}>
|
||||
<span class="flex items-center gap-1">
|
||||
<Palette size="1em" class="opacity-60" />
|
||||
@@ -406,8 +417,8 @@ const theme_options = [
|
||||
type="button"
|
||||
class="btn btn-sm flex-1 transition-all
|
||||
{$ae_loc?.theme_mode === 'dark'
|
||||
? 'preset-tonal-secondary hover:preset-filled-secondary-500'
|
||||
: 'preset-tonal-warning hover:preset-filled-warning-500'}"
|
||||
? 'preset-tonal-secondary hover:preset-filled-secondary-400-600'
|
||||
: 'preset-tonal-secondary hover:preset-filled-secondary-400-600'}"
|
||||
onclick={toggle_theme_mode}
|
||||
title="Toggle light/dark mode (currently: {$ae_loc?.theme_mode ??
|
||||
'unknown'})">
|
||||
@@ -421,7 +432,7 @@ const theme_options = [
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-tonal-surface hover:preset-tonal-primary flex-1 transition-all"
|
||||
class="btn btn-sm preset-tonal-secondary hover:preset-filled-secondary-400-600 flex-1 transition-all"
|
||||
onclick={cycle_font_size}
|
||||
title={font_title}>
|
||||
<span class="text-sm leading-none font-bold"
|
||||
@@ -431,24 +442,24 @@ const theme_options = [
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Theme name select -->
|
||||
<div class="space-y-1.5">
|
||||
<div
|
||||
class="text-xs font-medium text-gray-400 dark:text-gray-500">
|
||||
Theme
|
||||
</div>
|
||||
<!-- Theme picker: compact grouped select -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="shrink-0 text-xs text-surface-600-400">Theme</span>
|
||||
<select
|
||||
bind:value={$ae_loc.theme_name}
|
||||
onchange={(e) => {
|
||||
const v = (e.target as HTMLSelectElement).value;
|
||||
document.documentElement.setAttribute('data-theme', v);
|
||||
ae_loc.update((l: any) => ({ ...l, theme_name: v, user_theme_selected: true }));
|
||||
}}
|
||||
class="select w-full border border-gray-300 bg-white text-sm text-gray-900 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100">
|
||||
{#each theme_options as opt (opt.value)}
|
||||
<option value={opt.value}
|
||||
>{opt.label}</option>
|
||||
{/each}
|
||||
onchange={(e) => apply_theme((e.target as HTMLSelectElement).value)}
|
||||
class="flex-1 rounded-md border border-gray-200 px-2 py-1 text-xs text-surface-800-200 dark:border-gray-600 bg-surface-200-800"
|
||||
title="Select theme (current: {$ae_loc.theme_name ?? 'default'})">
|
||||
<optgroup label="System">
|
||||
{#each skeleton_themes as opt (opt.value)}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
{/each}
|
||||
</optgroup>
|
||||
<optgroup label="AE">
|
||||
{#each ae_themes as opt (opt.value)}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
{/each}
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -465,7 +476,7 @@ const theme_options = [
|
||||
<div class="border-b border-gray-100 dark:border-gray-800">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between px-3 py-2 text-xs font-semibold tracking-wider text-gray-500 uppercase transition-colors hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-800"
|
||||
class="flex w-full items-center justify-between px-3 py-2 text-xs font-semibold tracking-wider text-surface-600-400 uppercase transition-colors hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
onclick={() => (sec_dev = !sec_dev)}>
|
||||
<span class="flex items-center gap-1">
|
||||
<Bug size="0.9em" class="opacity-60" />
|
||||
@@ -591,16 +602,16 @@ const theme_options = [
|
||||
class="
|
||||
ae_sys_bar__strip
|
||||
flex h-9 flex-row items-center
|
||||
gap-1
|
||||
gap-1 px-2
|
||||
rounded-xl border
|
||||
border-gray-200/60
|
||||
bg-white/30 px-2 shadow-lg
|
||||
backdrop-blur-sm
|
||||
transition-colors
|
||||
border-primary-400-600/50
|
||||
bg-white/30 dark:bg-gray-900/30
|
||||
shadow-lg backdrop-blur-sm
|
||||
text-gray-900-100
|
||||
transition-all
|
||||
duration-300
|
||||
dark:border-gray-700/60 dark:bg-gray-900/30
|
||||
"
|
||||
class:border-primary-400={expand}>
|
||||
class:border-primary-400-600={expand}>
|
||||
<!-- AUTH STATUS SHIELD — hidden in iframe; host page manages auth context -->
|
||||
{#if !$ae_loc?.iframe}
|
||||
{#if $ae_loc?.access_type && $ae_loc?.access_type !== 'anonymous' && expand}
|
||||
@@ -608,13 +619,14 @@ const theme_options = [
|
||||
type="button"
|
||||
class="
|
||||
btn btn-sm group/shield transition-all duration-200
|
||||
preset-tonal-surface preset-outlined-surface
|
||||
{$ae_loc?.access_type &&
|
||||
$ae_loc?.access_type !== 'anonymous'
|
||||
? $ae_loc?.user_access_type &&
|
||||
$ae_loc?.access_type === $ae_loc?.user_access_type
|
||||
? 'variant-outline-surface hover:variant-ghost-warning'
|
||||
: 'variant-outline-warning hover:variant-ghost-warning'
|
||||
: 'variant-outline-surface hover:variant-ghost-success'}
|
||||
? 'hover:preset-tonal-success'
|
||||
: 'preset-tonal-warning hover:preset-tonal-warning'
|
||||
: 'preset-outlined-surface hover:preset-tonal-success'}
|
||||
"
|
||||
onclick={handle_shield_click}
|
||||
title={$ae_loc?.access_type &&
|
||||
@@ -716,9 +728,9 @@ const theme_options = [
|
||||
<!-- MENU EXPAND / COLLAPSE ───────────────────────────────────── -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-filled-tertiary-400-600 hover:preset-filled-success group/menu transition-all duration-200"
|
||||
class="btn btn-sm preset-filled-tertiary-200-800 hover:preset-filled-success group/menu transition-all duration-200"
|
||||
onclick={toggle_expand}
|
||||
title={expand ? 'Close menu' : 'Open menu'}>
|
||||
title={expand ? 'Close the Aether system menu' : 'Open menu'}>
|
||||
{#if expand}
|
||||
<CircleX size="1.1em" class="shrink-0" />
|
||||
<span
|
||||
|
||||
@@ -1,620 +0,0 @@
|
||||
<script lang="ts">
|
||||
// *** Import Svelte specific
|
||||
// import { tick } from 'svelte';
|
||||
// import { goto, invalidateAll } from '$app/navigation';
|
||||
|
||||
// *** Import other supporting libraries
|
||||
import {
|
||||
// ArrowBigRight,
|
||||
// Bug,
|
||||
CircleX,
|
||||
// Eye, EyeOff,
|
||||
// Key,
|
||||
// LogIn, LogOut, LockKeyhole,
|
||||
// Mail, MailCheck,
|
||||
Menu,
|
||||
// RefreshCw, RefreshCcwDot,
|
||||
ShieldEllipsis,
|
||||
ShieldMinus,
|
||||
ShieldPlus,
|
||||
ShieldUser,
|
||||
ToggleLeft,
|
||||
ToggleRight,
|
||||
User,
|
||||
UserCheck,
|
||||
UserCog,
|
||||
Wand2,
|
||||
X
|
||||
} from '@lucide/svelte';
|
||||
|
||||
// *** Import Aether specific variables and functions
|
||||
// import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
import {
|
||||
ae_loc,
|
||||
ae_sess,
|
||||
ae_api,
|
||||
slct,
|
||||
slct_trigger
|
||||
} from '$lib/stores/ae_stores';
|
||||
|
||||
import Element_access_type from '$lib/app_components/e_app_access_type.svelte';
|
||||
import Element_app_cfg from '$lib/app_components/e_app_cfg.svelte';
|
||||
import Element_sign_in_out from '$lib/app_components/e_app_sign_in_out.svelte';
|
||||
import Element_theme from '$lib/app_components/e_app_theme.svelte';
|
||||
|
||||
// *** Setup Svelte properties
|
||||
interface Props {
|
||||
log_lvl?: number;
|
||||
data: any;
|
||||
hide?: null | boolean;
|
||||
expand?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
log_lvl = $bindable(0),
|
||||
data = null,
|
||||
hide = $bindable(false),
|
||||
expand = $bindable(false)
|
||||
}: Props = $props();
|
||||
|
||||
let trigger_clear_access: null | boolean = $state(null);
|
||||
</script>
|
||||
|
||||
<!-- App System Menu -->
|
||||
<!-- min-h-full
|
||||
max-h-max
|
||||
min-w-full
|
||||
max-w-max -->
|
||||
<!-- <section
|
||||
class="
|
||||
ae_app__menu
|
||||
hidden-print
|
||||
flex flex-col
|
||||
items-end justify-center
|
||||
gap-1
|
||||
absolute right-0 bottom-16
|
||||
hover:bottom-6
|
||||
bg-white dark:bg-gray-800
|
||||
border border-transparent
|
||||
hover:border hover:border-gray-200 hover:dark:border-gray-700
|
||||
rounded-lg
|
||||
p-2
|
||||
*:hover:inline
|
||||
opacity-40
|
||||
hover:opacity-100
|
||||
duration-50 delay-1000 hover:delay-100 hover:ease-out
|
||||
transition hover:transition-all
|
||||
z-10 hover:z-20
|
||||
"
|
||||
class:hidden={!expand_btn}
|
||||
> -->
|
||||
<!-- && !expand -->
|
||||
<!-- !$ae_loc
|
||||
?.sys_menu?.expand_btn -->
|
||||
<!-- bg-blue-100/60 dark:bg-blue-800/50 -->
|
||||
<!-- class:hover:border-transparent={!expand} -->
|
||||
<!-- mx-1 my-2 -->
|
||||
<!-- We need to be able to hide the menu button in certain situations. Mainly iframes. -->
|
||||
<section
|
||||
class="
|
||||
ae_app__sys_menu
|
||||
|
||||
hidden-print
|
||||
md:text-md
|
||||
lg:text-md xl:text-md absolute
|
||||
|
||||
right-0
|
||||
bottom-6
|
||||
|
||||
z-50
|
||||
flex
|
||||
|
||||
w-min max-w-md flex-col-reverse
|
||||
items-end justify-center
|
||||
|
||||
gap-1 rounded-lg border-2 border-blue-300/20 bg-blue-200/90 text-sm
|
||||
|
||||
opacity-90
|
||||
|
||||
transition-all delay-500
|
||||
duration-500 ease-in-out
|
||||
hover:border-blue-500/20 hover:opacity-100
|
||||
|
||||
hover:delay-200
|
||||
hover:duration-200 sm:text-sm
|
||||
2xl:text-lg dark:border-blue-700/20
|
||||
hover:dark:border-blue-500/20
|
||||
"
|
||||
class:top-0={expand && (1 as any) == 3}
|
||||
class:opacity-100={expand}
|
||||
class:w-full={expand}
|
||||
class:hidden={hide}
|
||||
class:border-transparent={!expand}
|
||||
class:bg-transparent={!expand}>
|
||||
<!-- class:hidden={!expand} -->
|
||||
<!-- class:preset-filled-warning-100-900={expand} -->
|
||||
<div
|
||||
class:opacity-50={!expand}
|
||||
class:opacity-100={expand}
|
||||
class:light:bg-blue-500={expand}
|
||||
class:dark:bg-blue-500={expand}
|
||||
class="
|
||||
hidden-print
|
||||
light:bg-blue-200/10 relative flex w-full
|
||||
flex-col
|
||||
|
||||
items-end
|
||||
justify-end
|
||||
|
||||
gap-1
|
||||
|
||||
overflow-y-auto
|
||||
p-1
|
||||
transition-all
|
||||
|
||||
delay-1000
|
||||
|
||||
duration-200
|
||||
ease-in-out hover:opacity-100
|
||||
hover:delay-500 hover:duration-200
|
||||
dark:bg-blue-800/10
|
||||
"
|
||||
title="
|
||||
ID: {$ae_loc?.person_id ?? '-- not set --'} / {$ae_loc?.user_id ??
|
||||
'-- not set --'}
|
||||
Name: {$ae_loc?.person?.full_name ?? '-- not set --'}
|
||||
Username: {$ae_loc?.user?.username ?? '-- not set --'}
|
||||
Email: {$ae_loc?.user?.email ?? '-- not set --'}
|
||||
Access Type: {$ae_loc?.access_type ?? '-- not set --'}
|
||||
">
|
||||
{#if $ae_loc?.person_id}
|
||||
<div
|
||||
class="group flex w-full flex-row items-center justify-end gap-1 transition-all">
|
||||
<User size="1em" class="mx-1 inline-block text-gray-500" />
|
||||
<span class:hidden={!expand} class="group-hover:inline-block">
|
||||
{$ae_loc?.person?.informal_name ??
|
||||
$ae_loc?.person?.given_name}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $ae_loc?.user_id}
|
||||
<!-- This is currently not set to show if not expanded. Saving space. -->
|
||||
<div
|
||||
class:hidden={!expand}
|
||||
class="group flex w-full flex-row items-center justify-end gap-1 transition-all">
|
||||
<ShieldUser
|
||||
size="1em"
|
||||
class="mx-1 inline-block text-gray-500" />
|
||||
<span class:hidden={!expand} class="group-hover:inline-block">
|
||||
{$ae_loc?.user?.username ?? '-- not set --'}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="group flex w-full flex-row items-center justify-end gap-1 transition-all">
|
||||
{#if $ae_loc.access_type && $ae_loc.access_type != 'anonymous'}
|
||||
<span
|
||||
class="group flex flex-row-reverse gap-1 text-base"
|
||||
title={`Current access type/level: ${$ae_loc.access_type}`}>
|
||||
<!-- <span class="fas fa-unlock mx-1"></span> -->
|
||||
<!-- <ShieldPlus class="inline-block" /> -->
|
||||
|
||||
{#if $ae_loc.access_type == 'super'}
|
||||
<Wand2 size="1em" class="m-1 inline-block" />
|
||||
<span
|
||||
class:hidden={!expand}
|
||||
class="hidden group-hover:inline-block">Super</span>
|
||||
{:else if $ae_loc.access_type == 'manager'}
|
||||
<ShieldUser size="1em" class="m-1 inline-block" />
|
||||
<span
|
||||
class:hidden={!expand}
|
||||
class="hidden group-hover:inline-block"
|
||||
>Manager</span>
|
||||
{:else if $ae_loc.access_type == 'administrator'}
|
||||
<UserCog size="1em" class="m-1 inline-block" />
|
||||
<span
|
||||
class:hidden={!expand}
|
||||
class="hidden group-hover:inline-block"
|
||||
>Administrator</span>
|
||||
{:else if $ae_loc.access_type == 'trusted'}
|
||||
<UserCheck size="1em" class="m-1 inline-block" />
|
||||
<span
|
||||
class:hidden={!expand}
|
||||
class="hidden group-hover:inline-block"
|
||||
>Trusted Access</span>
|
||||
{:else if $ae_loc.access_type == 'public'}
|
||||
Public
|
||||
<span
|
||||
class:hidden={!expand}
|
||||
class="hidden group-hover:inline-block"
|
||||
>Access</span>
|
||||
{:else if $ae_loc.access_type == 'authenticated'}
|
||||
Authenticated
|
||||
<span
|
||||
class:hidden={!expand}
|
||||
class="hidden group-hover:inline-block"
|
||||
>Access</span>
|
||||
{:else if $ae_loc.access_type == 'anonymous'}
|
||||
Anonymous Access
|
||||
{:else}
|
||||
Unknown Access
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{#if $ae_loc?.user_access_type && $ae_loc?.access_type == $ae_loc?.user_access_type}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
// handle_clear_access();
|
||||
// trigger_clear_access = true;
|
||||
// $ae_loc.app_cfg.show_element__passcode_input = !$ae_loc.app_cfg.show_element__passcode_input;
|
||||
|
||||
if (!expand) {
|
||||
expand = true;
|
||||
$ae_sess.sys_menu.expand = true;
|
||||
$ae_loc.sys_menu.hide_access_type = false;
|
||||
$ae_loc.sys_menu.expand_access_type = true;
|
||||
// $ae_loc.sys_menu.expand_btn = false;
|
||||
// $ae_loc.app_cfg.show_element__access_type = true;
|
||||
// $ae_loc.app_cfg.show_element__passcode_input = true;
|
||||
} else {
|
||||
// expand = true;
|
||||
// $ae_loc.sys_menu.expand = false;
|
||||
$ae_loc.sys_menu.hide_access_type = false;
|
||||
$ae_loc.sys_menu.expand_access_type = true;
|
||||
|
||||
// $ae_loc.sys_menu.expand_btn = true;
|
||||
}
|
||||
}}
|
||||
class="btn btn-sm variant-outline-surface hover:variant-ghost-warning group text-xs transition-all"
|
||||
title={`Current user access level: "${$ae_loc.user_access_type}". Click use passcode for a different access level.`}>
|
||||
<!-- <span class="fas fa-lock mx-1"></span> -->
|
||||
<!-- <ShieldMinus /> -->
|
||||
<ShieldEllipsis size="2em" class="inline-block" />
|
||||
<span class="hidden group-hover:inline-block"
|
||||
>Passcode?</span>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
trigger_clear_access = true;
|
||||
|
||||
if (expand) {
|
||||
$ae_loc.sys_menu.hide_access_type = false;
|
||||
// $ae_loc.sys_menu.expand_access_type = false;
|
||||
expand = false;
|
||||
|
||||
// $ae_loc.sys_menu.expand = true;
|
||||
// $ae_loc.sys_menu.expand_btn = false;
|
||||
$ae_loc.app_cfg.show_element__access_type = true;
|
||||
$ae_sess.app_cfg.show_element__passcode_input = true;
|
||||
|
||||
// await tick();
|
||||
// console.log('Layout button click: Focus on passcode input!');
|
||||
// /** @type {HTMLElement | null} */
|
||||
// const to_focus = document.getElementById('access_passcode_input');
|
||||
// to_focus?.focus();
|
||||
} else {
|
||||
$ae_loc.sys_menu.hide_access_type = false;
|
||||
$ae_loc.sys_menu.expand_access_type = true;
|
||||
// $ae_loc.sys_menu.expand = false;
|
||||
// $ae_loc.sys_menu.expand_btn = true;
|
||||
}
|
||||
}}
|
||||
class="
|
||||
btn btn-sm variant-outline-surface
|
||||
hover:variant-ghost-warning
|
||||
group flex-row-reverse
|
||||
text-xs transition-all
|
||||
"
|
||||
title={`Current access level: "${$ae_loc.access_type}". Click to clear the temporary access level.`}>
|
||||
<!-- <span class="fas fa-lock mx-1"></span> Lock? -->
|
||||
<ShieldMinus class="inline-block" />
|
||||
<span class="hidden group-hover:inline-block">
|
||||
Clear?
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
onclick={async () => {
|
||||
expand = true;
|
||||
$ae_sess.sys_menu.expand = true;
|
||||
$ae_loc.sys_menu.hide_access_type = false;
|
||||
$ae_loc.sys_menu.expand_access_type = true;
|
||||
|
||||
$ae_sess.app_cfg.show_element__passcode_input = true;
|
||||
$ae_sess.sys_menu.focus_passcode_input = true;
|
||||
|
||||
// $ae_loc.sys_menu.expand = true;
|
||||
// $ae_loc.sys_menu.expand_btn = false;
|
||||
// $ae_loc.app_cfg.show_element__access_type = true;
|
||||
// $ae_loc.app_cfg.show_element__passcode_input = true;
|
||||
// await tick();
|
||||
console.log(
|
||||
'Layout button click: Focus on passcode input!'
|
||||
);
|
||||
/** @type {HTMLElement | null} */
|
||||
const to_focus = document.getElementById(
|
||||
'access_passcode_input'
|
||||
);
|
||||
to_focus?.focus();
|
||||
}}
|
||||
class="
|
||||
btn btn-sm variant-outline-surface
|
||||
hover:variant-ghost-success
|
||||
group flex-row-reverse
|
||||
text-xs transition-all
|
||||
"
|
||||
title="Anonymous public access is currently set. You must Sign In or use a passcode to change your access level.">
|
||||
<!-- <span class="fas fa-lock mx-1 lock_icon"></span> -->
|
||||
<!-- <span class="">Unlock?</span> -->
|
||||
<ShieldUser class="inline-block" />
|
||||
<span class="hidden group-hover:inline-block">Auth?</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $ae_loc.edit_mode}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
$ae_loc.edit_mode = false;
|
||||
// dispatch_edit_mode_changed();
|
||||
}}
|
||||
class="
|
||||
btn btn-base preset-tonal-warning
|
||||
preset-outlined-warning-800-200
|
||||
hover:preset-tonal-success
|
||||
group
|
||||
w-full max-w-fit
|
||||
min-w-22 gap-1 text-sm transition-all
|
||||
md:min-w-30
|
||||
"
|
||||
title="Click to turn off edit mode. Edit mode is currently on.">
|
||||
<ToggleRight size="1em" class="m-1 inline-block" />
|
||||
<span class="text-xs">Edit</span>
|
||||
<span
|
||||
class="hidden group-hover:inline-block group-hover:text-xs">
|
||||
Off
|
||||
</span>
|
||||
</button>
|
||||
{:else if $ae_loc.authenticated_access}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
$ae_loc.edit_mode = true;
|
||||
// dispatch_edit_mode_changed();
|
||||
}}
|
||||
class="
|
||||
btn btn-base preset-tonal-surface
|
||||
preset-outlined-warning-400-600
|
||||
hover:preset-tonal-warning
|
||||
group
|
||||
w-full max-w-fit
|
||||
min-w-22 gap-1 text-sm transition-all
|
||||
md:min-w-30
|
||||
"
|
||||
title="Click to torn on/enable edit mode. Edit mode is currently off/disabled.">
|
||||
<ToggleLeft size="1em" class="m-1 inline-block" />
|
||||
<span class="text-xs">Edit</span>
|
||||
<span
|
||||
class="hidden group-hover:inline-block group-hover:text-xs">
|
||||
On?
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Font Size Cycler: default → larger → smaller → default -->
|
||||
<button
|
||||
type="button"
|
||||
class:w-full={expand}
|
||||
class="
|
||||
btn btn-base preset-tonal-surface
|
||||
hover:preset-tonal-primary
|
||||
group
|
||||
w-full max-w-fit
|
||||
min-w-22 px-1 py-1 text-sm
|
||||
transition-all md:min-w-30
|
||||
"
|
||||
title="Cycle font size: default → larger → smaller"
|
||||
onclick={() => {
|
||||
const mode = $ae_loc.font_size_mode;
|
||||
if (!mode || mode === 'default') {
|
||||
$ae_loc.font_size_mode = 'larger';
|
||||
} else if (mode === 'larger') {
|
||||
$ae_loc.font_size_mode = 'smaller';
|
||||
} else {
|
||||
$ae_loc.font_size_mode = 'default';
|
||||
}
|
||||
}}>
|
||||
{#if !$ae_loc.font_size_mode || $ae_loc.font_size_mode === 'default'}
|
||||
<span class="text-sm leading-none font-bold">A</span>
|
||||
<span class="hidden text-xs group-hover:inline-block"
|
||||
>Font: Normal</span>
|
||||
{:else if $ae_loc.font_size_mode === 'larger'}
|
||||
<span class="text-base leading-none font-bold">A+</span>
|
||||
<span class="hidden text-xs group-hover:inline-block"
|
||||
>Font: Larger</span>
|
||||
{:else}
|
||||
<span class="text-xs leading-none font-bold">A−</span>
|
||||
<span class="hidden text-xs group-hover:inline-block"
|
||||
>Font: Smaller</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- <div> -->
|
||||
<!-- class:visible={expand} -->
|
||||
<!-- class:invisible={!expand} -->
|
||||
<!-- class:hover:visible={true} -->
|
||||
<!-- invisible -->
|
||||
<button
|
||||
type="button"
|
||||
class:w-full={expand}
|
||||
class="
|
||||
btn btn-base preset-filled-tertiary-400-600
|
||||
preset-outlined-tertiary-400-600
|
||||
hover:preset-filled-success
|
||||
active:preset-filled-success group
|
||||
w-full max-w-fit
|
||||
min-w-22 px-1 py-1 text-sm
|
||||
transition-all md:min-w-30
|
||||
"
|
||||
title="Show/Hide the system menu"
|
||||
onclick={async () => {
|
||||
if (!expand) {
|
||||
expand = true;
|
||||
$ae_sess.sys_menu.expand = true;
|
||||
|
||||
// $ae_loc.sys_menu.expand = true;
|
||||
// $ae_loc.sys_menu.expand_btn = false;
|
||||
$ae_loc.app_cfg.show_element__access_type = true;
|
||||
|
||||
if ($ae_loc?.access_type == 'anonymous') {
|
||||
$ae_sess.sys_menu.focus_passcode_input = true;
|
||||
// $ae_sess.app_cfg.show_element__passcode_input = true;
|
||||
} else {
|
||||
// $ae_sess.app_cfg.show_element__passcode_input = false;
|
||||
$ae_loc.sys_menu.expand_user = false; // Not in use yet
|
||||
$ae_sess.show__sign_in_out__fields = false;
|
||||
}
|
||||
// $ae_loc.app_cfg.show_element__passcode_input = true;
|
||||
// await tick();
|
||||
// console.log('Layout button click: Focus on passcode input!');
|
||||
// /** @type {HTMLElement | null} */
|
||||
// const to_focus = document.getElementById('access_passcode_input');
|
||||
// to_focus?.focus();
|
||||
} else {
|
||||
expand = false;
|
||||
$ae_sess.sys_menu.expand = false;
|
||||
// $ae_loc.sys_menu.expand = false;
|
||||
// $ae_loc.sys_menu.expand_btn = true;
|
||||
// $ae_loc.app_cfg.show_element__passcode_input = false;
|
||||
}
|
||||
// $ae_loc.sys_menu.expand_btn = !expand_btn;
|
||||
}}>
|
||||
<!-- <span class=""> -->
|
||||
{#if expand}
|
||||
<CircleX class="m-1 inline-block" />
|
||||
{:else}
|
||||
<Menu class="m-1 inline-block" />
|
||||
{/if}
|
||||
<span class="hidden text-xs group-hover:inline-block"> Menu </span>
|
||||
<!-- </span> -->
|
||||
</button>
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
|
||||
<!-- Show menu on right side of page -->
|
||||
<!-- min-h-96 -->
|
||||
<!-- absolute right-0 bottom-10 -->
|
||||
<!-- opacity-50 -->
|
||||
<!-- hover:opacity-100 -->
|
||||
<!-- bg-white dark:bg-gray-800 -->
|
||||
<div
|
||||
class:preset-filled-warning-200-800={expand}
|
||||
class:hidden={!expand}
|
||||
class="
|
||||
ae_app__sys_menu
|
||||
|
||||
hidden-print
|
||||
z-20 flex
|
||||
min-w-48
|
||||
flex-col
|
||||
items-end
|
||||
|
||||
justify-end
|
||||
|
||||
gap-2 rounded-lg
|
||||
|
||||
border border-gray-200 bg-white
|
||||
px-1
|
||||
py-2 transition-all
|
||||
|
||||
delay-1000
|
||||
duration-100 ease-in-out
|
||||
hover:z-30 hover:delay-100
|
||||
hover:duration-200
|
||||
|
||||
dark:border-gray-700 dark:bg-gray-800
|
||||
">
|
||||
<button
|
||||
type="button"
|
||||
class:w-full={expand}
|
||||
class="
|
||||
btn btn-sm
|
||||
preset-filled-tertiary-400-600
|
||||
preset-outlined-tertiary-400-600
|
||||
hover:preset-filled-success active:preset-filled-success
|
||||
group px-6
|
||||
py-1 transition-all
|
||||
"
|
||||
title="Show/Hide the system menu"
|
||||
onclick={() => {
|
||||
if (!expand) {
|
||||
expand = true;
|
||||
$ae_sess.sys_menu.expand = true;
|
||||
// $ae_loc.sys_menu.expand_btn = false;
|
||||
// $ae_loc.sys_menu.expand_access_type = true;
|
||||
|
||||
// $ae_sess.app_cfg.show_element__passcode_input = true;
|
||||
$ae_sess.sys_menu.focus_passcode_input = true;
|
||||
} else {
|
||||
$ae_sess.sys_menu.expand = false;
|
||||
$ae_loc.sys_menu.expand_user = false; // Not in use yet
|
||||
$ae_sess.show__sign_in_out__fields = false;
|
||||
// $ae_loc.sys_menu.expand_btn = true;
|
||||
}
|
||||
// $ae_loc.sys_menu.expand_btn = !expand_btn;
|
||||
// $ae_loc.sys_menu.expand = !expand;
|
||||
}}>
|
||||
{#if expand}
|
||||
<CircleX class="m-1 inline-block" />
|
||||
<span class="hidden group-hover:inline-block"> Hide Menu </span>
|
||||
{:else}
|
||||
<Menu class="m-1 inline-block" />
|
||||
<span class="hidden group-hover:inline-block"> Menu </span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- {#if $ae_loc?.app_cfg?.show_element__cfg} -->
|
||||
<span class:hidden={!$ae_loc.edit_mode}>
|
||||
<Element_app_cfg
|
||||
hide={$ae_loc.sys_menu.hide_app_cfg}
|
||||
expand={$ae_loc.sys_menu.expand_app_cfg} />
|
||||
</span>
|
||||
<span class:hidden={!$ae_loc.edit_mode}>
|
||||
<Element_theme
|
||||
hide={$ae_loc.sys_menu.hide_app_cfg}
|
||||
expand={$ae_loc.sys_menu.expand_app_cfg}
|
||||
set_theme_mode={true}
|
||||
set_theme_name={true} />
|
||||
</span>
|
||||
<!-- {/if} -->
|
||||
|
||||
{#if $ae_loc?.app_cfg?.show_element__sign_in_out}
|
||||
<!-- hide={$ae_loc.sys_menu.hide_user} -->
|
||||
<!-- expand={$ae_loc.sys_menu.expand_user} -->
|
||||
<Element_sign_in_out
|
||||
{data}
|
||||
hidden={$ae_loc.iframe ||
|
||||
!$ae_loc.app_cfg?.show_element__sign_in_out} />
|
||||
{/if}
|
||||
|
||||
{#if !$ae_loc?.sys_menu?.hide_access_type && !$ae_loc?.iframe}
|
||||
<!-- hidden={$ae_loc?.iframe && !$ae_loc?.trusted_access && !expand} -->
|
||||
<Element_access_type
|
||||
bind:hide={$ae_loc.sys_menu.hide_access_type}
|
||||
bind:focus_input={$ae_sess.sys_menu.focus_passcode_input}
|
||||
bind:expand={$ae_loc.sys_menu.expand_access_type}
|
||||
bind:show_passcode_input={
|
||||
$ae_sess.app_cfg.show_element__passcode_input
|
||||
}
|
||||
bind:trigger_clear_access />
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,235 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Minimize2, Moon, Sun } from '@lucide/svelte';
|
||||
import {
|
||||
ae_loc,
|
||||
ae_sess,
|
||||
ae_api,
|
||||
slct,
|
||||
slct_trigger
|
||||
} from '$lib/stores/ae_stores';
|
||||
|
||||
interface Props {
|
||||
log_lvl?: number;
|
||||
hide?: null | boolean;
|
||||
expand?: boolean;
|
||||
set_theme_mode: any;
|
||||
set_theme_name: any;
|
||||
}
|
||||
|
||||
let {
|
||||
log_lvl = $bindable(0),
|
||||
hide = $bindable(false),
|
||||
expand = $bindable(false),
|
||||
set_theme_mode,
|
||||
set_theme_name
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<!-- Change light and dark mode -->
|
||||
<!--
|
||||
if ($ae_loc.app_cfg.theme_mode == 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
document.documentElement.classList.add('light');
|
||||
} else if ($ae_loc.app_cfg.theme_mode == 'dark') {
|
||||
document.documentElement.classList.remove('light');
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
-->
|
||||
<section
|
||||
id="AE-App-Theme"
|
||||
class="
|
||||
ae_app_theme
|
||||
hidden-print
|
||||
|
||||
bg-surface-100 dark:bg-surface-800
|
||||
text-surface-900 dark:text-surface-100
|
||||
border-surface-200 dark:border-surface-700 flex
|
||||
|
||||
w-72 max-w-72 flex-col flex-wrap
|
||||
items-end justify-center
|
||||
|
||||
gap-1 rounded-lg
|
||||
border
|
||||
p-1
|
||||
shadow-md
|
||||
|
||||
transition-all delay-150 duration-300 hover:delay-1000
|
||||
hover:ease-out
|
||||
"
|
||||
class:hidden={hide}>
|
||||
<div
|
||||
class:hidden={!expand}
|
||||
class="flex w-full flex-row flex-wrap items-center justify-between gap-2">
|
||||
<!-- Theme Name: -->
|
||||
<span class="text-sm font-semibold">
|
||||
{$ae_loc.theme_name}
|
||||
</span>
|
||||
<select
|
||||
onchange={(event) => {
|
||||
let new_theme_name = (event.target as HTMLInputElement).value;
|
||||
// Update the HTML attribute for Tailwind/Skeleton runtime.
|
||||
document.documentElement.setAttribute('data-theme', new_theme_name);
|
||||
// Persist into the ae_loc store and mark user selection so site defaults don't overwrite.
|
||||
ae_loc.update((l: any) => ({ ...l, theme_name: new_theme_name, user_theme_selected: true }));
|
||||
}}
|
||||
bind:value={$ae_loc.theme_name}
|
||||
class="select w-32"
|
||||
title="Theme name">
|
||||
<option value="">-- None --</option>
|
||||
<option value="cerberus">Cerberus</option>
|
||||
<option value="concord">Concord</option>
|
||||
<option value="crimson">Crimson</option>
|
||||
<option value="hamlindigo">Hamlindigo</option>
|
||||
<option value="modern">Modern</option>
|
||||
<option value="nouveau">Nouveau</option>
|
||||
<option value="rocket">Rocket</option>
|
||||
<option value="terminus">Terminus</option>
|
||||
<option value="vintage">Vintage</option>
|
||||
<option value="wintry">Wintry</option>
|
||||
<!-- <option value="ae_c_osit">OSIT</option> -->
|
||||
<option value="AE_OSIT_default">OSIT</option>
|
||||
<option value="AE_Firefly">Firefly ✦</option>
|
||||
<option value="AE_Firefly_SteelBlue">Firefly SteelBlue ✦</option>
|
||||
<option value="AE_Firefly_Indigo">Firefly Indigo ✦</option>
|
||||
<option value="AE_Firefly_Rainbow">Firefly Rainbow ✨</option>
|
||||
<option value="AE_Firefly_Axonius">Firefly Axonius ✦</option>
|
||||
<option value="AE_Firefly_BGH">Firefly BGH ✦</option>
|
||||
<!-- <option value="AE_Firefly_new-name">Firefly new-name ✦</option> -->
|
||||
<option value="AE_c_IDAA_light">IDAA - light</option>
|
||||
<option value="AE_c_LCI">LCI</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex w-full flex-row flex-wrap items-center gap-2"
|
||||
class:justify-between={expand}
|
||||
class:justify-end={!expand}>
|
||||
{#if expand}
|
||||
<!-- Hide theme options -->
|
||||
<button
|
||||
class="
|
||||
btn btn-sm preset-tonal-secondary
|
||||
hover:preset-filled-secondary-500 group
|
||||
text-xs transition-all
|
||||
"
|
||||
onclick={() => {
|
||||
expand = !expand;
|
||||
}}
|
||||
title="Hide Theme Options">
|
||||
<!-- <span class="fas fa-compress-alt"></span> -->
|
||||
<Minimize2 size="1em" class="m-1" />
|
||||
<span
|
||||
class="
|
||||
hidden
|
||||
text-xs
|
||||
group-hover:inline-block
|
||||
">
|
||||
Hide Theme Options
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="
|
||||
btn btn-sm preset-tonal-secondary
|
||||
hover:preset-filled-secondary-500 group
|
||||
text-xs transition-all
|
||||
"
|
||||
onclick={() => {
|
||||
if ($ae_loc.theme_mode == 'light') {
|
||||
$ae_loc.theme_mode = 'dark';
|
||||
} else if ($ae_loc.theme_mode == 'dark') {
|
||||
$ae_loc.theme_mode = 'light';
|
||||
}
|
||||
|
||||
// DOM sync is handled reactively by the layout effect in +layout.svelte
|
||||
}}
|
||||
title="Change light and dark mode">
|
||||
<!-- <span class="fas fa-adjust"></span> -->
|
||||
{#if $ae_loc.theme_mode == 'light'}
|
||||
<Sun class="m-1" />
|
||||
<span
|
||||
class="hidden group-hover:inline-block md:inline-block"
|
||||
>Light Mode</span>
|
||||
{:else if $ae_loc.theme_mode == 'dark'}
|
||||
<Moon class="m-1" />
|
||||
<span class="hidden group-hover:inline-block"
|
||||
>Dark Mode</span>
|
||||
{/if}
|
||||
<span
|
||||
class="
|
||||
hidden
|
||||
text-xs
|
||||
group-hover:inline-block
|
||||
">
|
||||
Change
|
||||
</span>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="btn btn-sm preset-tonal-secondary hover:preset-filled-secondary-500 group"
|
||||
onclick={() => {
|
||||
if ($ae_loc.theme_mode == 'light') {
|
||||
$ae_loc.theme_mode = 'dark';
|
||||
} else if ($ae_loc.theme_mode == 'dark') {
|
||||
$ae_loc.theme_mode = 'light';
|
||||
}
|
||||
|
||||
if ($ae_loc.theme_mode == 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
document.documentElement.classList.add('light');
|
||||
} else if ($ae_loc.theme_mode == 'dark') {
|
||||
document.documentElement.classList.remove('light');
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
|
||||
expand = !expand;
|
||||
}}
|
||||
title="Change light and dark mode">
|
||||
{#if $ae_loc.theme_mode == 'light'}
|
||||
<span class="inline-block" title="Light Mode">
|
||||
<Sun />
|
||||
</span>
|
||||
<span class="hidden group-hover:inline-block"
|
||||
>Light Mode</span>
|
||||
{:else if $ae_loc.theme_mode == 'dark'}
|
||||
<span class="inline-block" title="Dark Mode">
|
||||
<Moon />
|
||||
</span>
|
||||
<span class="hidden group-hover:inline-block"
|
||||
>Dark Mode</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- <section class="space-y-2">
|
||||
<h2 class="strong">Theme:</h2> -->
|
||||
<!-- Light/Dark Theme: -->
|
||||
<!-- <div>
|
||||
<RadioGroup
|
||||
active="variant-glass-success"
|
||||
hover="hover:variant-ringed-surface"
|
||||
>
|
||||
<RadioItem
|
||||
onchange={() => {
|
||||
$slct_trigger = 'set_theme_mode';
|
||||
}}
|
||||
bind:group={$ae_loc.theme_mode}
|
||||
name="theme_light"
|
||||
value={'light'}
|
||||
>
|
||||
Light
|
||||
</RadioItem>
|
||||
<RadioItem
|
||||
onchange={() => {
|
||||
$slct_trigger = 'set_theme_mode';
|
||||
}}
|
||||
bind:group={$ae_loc.theme_mode}
|
||||
name="theme_dark"
|
||||
value={'dark'}
|
||||
>
|
||||
Dark
|
||||
</RadioItem>
|
||||
</RadioGroup>
|
||||
</div> -->
|
||||
</section>
|
||||
@@ -101,7 +101,8 @@ const theme_options = [
|
||||
{ value: 'AE_Firefly_Axonius', label: 'Firefly Axonius ✦' },
|
||||
{ value: 'AE_Firefly_BGH', label: 'Firefly BGH ✦' },
|
||||
{ value: 'AE_c_IDAA_light', label: 'IDAA – light' },
|
||||
{ value: 'AE_c_LCI', label: 'LCI' }
|
||||
{ value: 'AE_c_LCI', label: 'LCI' },
|
||||
{ value: 'AE_c_LCI_new', label: 'LCI (New ✦)' }
|
||||
];
|
||||
</script>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
461
src/lib/elements/element_ae_obj_field_editor_new.svelte
Normal file
461
src/lib/elements/element_ae_obj_field_editor_new.svelte
Normal file
@@ -0,0 +1,461 @@
|
||||
<!--
|
||||
ELEMENT: AE Obj Field Editor — NEW VERSION
|
||||
========================================================================
|
||||
Full plan / rationale: documentation/PROJECT__AE_Obj_Field_Editor_New.md
|
||||
Original (still the live, working version — do not touch it yet):
|
||||
src/lib/elements/element_ae_obj_field_editor.svelte
|
||||
|
||||
This file starts from the original's logic. The optimistic-update state
|
||||
machine (is_editing / draft_value / has_optimistic / display_value) is
|
||||
already hardened in production (see commit c3ec0f88e) — copy it forward
|
||||
as-is. Do not redesign it unless you find an actual bug; it is NOT part
|
||||
of this rewrite's scope.
|
||||
-->
|
||||
<script lang="ts" generics="T">
|
||||
import { untrack } from 'svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import {
|
||||
Check,
|
||||
CircleAlert,
|
||||
Eraser,
|
||||
LoaderCircle,
|
||||
Save,
|
||||
SquarePen,
|
||||
X
|
||||
} from '@lucide/svelte';
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||
import { api } from '$lib/api/api';
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
import AE_Comp_Editor_TipTap from '$lib/elements/element_editor_tiptap.svelte';
|
||||
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
|
||||
|
||||
interface Props {
|
||||
// Core Identifiers
|
||||
id?: string;
|
||||
object_type: string;
|
||||
object_id: string;
|
||||
field_name: string;
|
||||
|
||||
// Value Handling
|
||||
current_value: T;
|
||||
field_type?:
|
||||
| 'text'
|
||||
| 'textarea'
|
||||
| 'select'
|
||||
| 'tiptap'
|
||||
| 'codemirror'
|
||||
| 'checkbox'
|
||||
| 'date'
|
||||
| 'datetime'
|
||||
| 'number'
|
||||
| 'email'
|
||||
| 'url'
|
||||
| 'tel';
|
||||
allow_null?: boolean;
|
||||
|
||||
// Select Options
|
||||
select_options?: key_val; // { value: label }
|
||||
|
||||
// UI Configuration
|
||||
edit_label?: string;
|
||||
display_block?: boolean;
|
||||
display_absolute_edit?: boolean;
|
||||
placeholder?: string;
|
||||
class_li?: string;
|
||||
textarea_rows?: number;
|
||||
|
||||
// Behavior
|
||||
log_lvl?: number;
|
||||
|
||||
// Callbacks
|
||||
on_success?: (data: any) => void;
|
||||
on_error?: (error: any) => void;
|
||||
on_open?: () => void | Promise<void>;
|
||||
|
||||
// Snippets
|
||||
children?: Snippet;
|
||||
|
||||
// State
|
||||
is_editing?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
id,
|
||||
object_type,
|
||||
object_id,
|
||||
field_name,
|
||||
current_value = $bindable(),
|
||||
field_type = 'text',
|
||||
allow_null = false,
|
||||
select_options = {},
|
||||
edit_label = 'Edit Field',
|
||||
display_block = false,
|
||||
display_absolute_edit = false,
|
||||
placeholder = 'Enter value...',
|
||||
class_li = '',
|
||||
textarea_rows = 4,
|
||||
log_lvl = 0,
|
||||
on_success,
|
||||
on_error,
|
||||
on_open,
|
||||
children,
|
||||
is_editing = $bindable(false)
|
||||
}: Props = $props();
|
||||
|
||||
/**
|
||||
* to_input_value: stored value -> what the native <input> wants.
|
||||
*/
|
||||
function to_input_value(value: T, type: typeof field_type): any {
|
||||
if (value === null || value === undefined) return '';
|
||||
|
||||
if (type === 'datetime') {
|
||||
return ae_util.iso_datetime_formatter(value as any, 'datetime_iso_no_seconds').replace(' ', 'T');
|
||||
}
|
||||
|
||||
if (type === 'date') {
|
||||
return ae_util.iso_datetime_formatter(value as any, 'date_iso');
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* from_input_value: native <input> value -> format backend expects.
|
||||
*/
|
||||
function from_input_value(value: any, type: typeof field_type): any {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return allow_null ? null : value;
|
||||
}
|
||||
|
||||
if (type === 'datetime') {
|
||||
return value.replace('T', ' ') + ':00';
|
||||
}
|
||||
|
||||
if (type === 'date') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (type === 'number') {
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* coerce_select_value: Native <select> values are always strings.
|
||||
* Coerce back to the real type based on the shape of current_value.
|
||||
*/
|
||||
function coerce_select_value(raw: string, reference: T): any {
|
||||
if (raw === 'null' || raw === '') return null;
|
||||
if (typeof reference === 'number') return Number(raw);
|
||||
if (typeof reference === 'boolean') return raw === 'true';
|
||||
return raw;
|
||||
}
|
||||
|
||||
// Internal State
|
||||
let patch_status = $state<'idle' | 'processing' | 'success' | 'error'>('idle');
|
||||
let error_message = $state('');
|
||||
let draft_value = $state(to_input_value(current_value, field_type));
|
||||
let input_ref = $state<HTMLElement | null>(null);
|
||||
|
||||
// Optimistic display state machine
|
||||
let has_optimistic = $state(false);
|
||||
let display_value = $derived(has_optimistic ? draft_value : current_value);
|
||||
|
||||
// Sync draft with current_value when not editing
|
||||
$effect(() => {
|
||||
if (!is_editing) {
|
||||
untrack(() => {
|
||||
if (!has_optimistic) {
|
||||
draft_value = to_input_value(current_value, field_type);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Clear optimistic once current_value catches up
|
||||
$effect(() => {
|
||||
const formatted_current = to_input_value(current_value, field_type);
|
||||
if (has_optimistic && formatted_current === draft_value) {
|
||||
has_optimistic = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Autofocus when entering edit mode (for simple inputs)
|
||||
$effect(() => {
|
||||
if (is_editing && input_ref) {
|
||||
untrack(() => input_ref?.focus());
|
||||
}
|
||||
});
|
||||
|
||||
async function handle_patch() {
|
||||
if (log_lvl) console.log(`AE Field Editor (new): Patching ${object_type}.${field_name}...`);
|
||||
|
||||
patch_status = 'processing';
|
||||
error_message = '';
|
||||
|
||||
try {
|
||||
const final_value = from_input_value(draft_value, field_type);
|
||||
|
||||
const result = await api.update_ae_obj({
|
||||
api_cfg: $ae_api,
|
||||
obj_type: object_type,
|
||||
obj_id: object_id,
|
||||
fields: { [field_name]: final_value },
|
||||
log_lvl
|
||||
});
|
||||
|
||||
if (result) {
|
||||
patch_status = 'success';
|
||||
has_optimistic = true;
|
||||
if (on_success) on_success(result);
|
||||
|
||||
setTimeout(() => {
|
||||
if (patch_status === 'success') {
|
||||
patch_status = 'idle';
|
||||
is_editing = false;
|
||||
}
|
||||
}, 800);
|
||||
} else {
|
||||
throw new Error('No data returned from update.');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('AE Field Editor (new): Patch failed.', error);
|
||||
patch_status = 'error';
|
||||
error_message = error?.message || 'Update failed.';
|
||||
if (on_error) on_error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function cancel_edit() {
|
||||
has_optimistic = false;
|
||||
draft_value = to_input_value(current_value, field_type);
|
||||
is_editing = false;
|
||||
patch_status = 'idle';
|
||||
error_message = '';
|
||||
}
|
||||
|
||||
function toggle_edit() {
|
||||
if (is_editing) cancel_edit();
|
||||
else {
|
||||
has_optimistic = false;
|
||||
draft_value = to_input_value(current_value, field_type);
|
||||
is_editing = true;
|
||||
if (on_open) on_open();
|
||||
}
|
||||
}
|
||||
|
||||
function handle_keydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') cancel_edit();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="ae_field_editor group relative {class_li}"
|
||||
class:block={display_block}
|
||||
class:inline-block={!display_block}
|
||||
role="none"
|
||||
onkeydown={handle_keydown}>
|
||||
|
||||
<!-- VIEW MODE -->
|
||||
<div class="view_wrapper flex items-center gap-2" class:hidden={is_editing}>
|
||||
<div class="content_render grow">
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else if field_type === 'checkbox'}
|
||||
<span class="badge {display_value ? 'preset-tonal-success' : 'preset-tonal-surface'}">
|
||||
{display_value ? 'True' : 'False'}
|
||||
</span>
|
||||
{:else if field_type === 'tiptap' || field_type === 'codemirror'}
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html display_value || '<span class="opacity-50 italic">Empty</span>'}
|
||||
</div>
|
||||
{:else}
|
||||
<span class:opacity-50={!display_value}>
|
||||
{display_value || 'Not set'}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon btn-icon-sm preset-tonal-warning opacity-20 transition-opacity hover:opacity-100"
|
||||
class:invisible={!$ae_loc.edit_mode}
|
||||
class:pointer-events-none={!$ae_loc.edit_mode}
|
||||
tabindex={$ae_loc.edit_mode ? 0 : -1}
|
||||
onclick={toggle_edit}
|
||||
aria-label="Edit {edit_label || field_name}"
|
||||
title="Edit {edit_label || field_name}">
|
||||
<SquarePen size="14" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- EDIT MODE -->
|
||||
{#if is_editing}
|
||||
<div
|
||||
class="edit_wrapper border-warning-500/50 bg-surface-50-950 z-50 rounded-lg border-2 border-dashed p-3 shadow-xl"
|
||||
class:absolute={display_absolute_edit}
|
||||
class:top-0={display_absolute_edit}
|
||||
class:left-0={display_absolute_edit}
|
||||
class:w-full={display_absolute_edit}>
|
||||
|
||||
<header class="mb-2 flex items-center justify-between">
|
||||
<span class="text-xs font-bold tracking-wider uppercase opacity-60">
|
||||
{edit_label || field_name}
|
||||
</span>
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-icon btn-icon-sm preset-tonal-surface"
|
||||
onclick={cancel_edit}
|
||||
aria-label="Cancel editing"
|
||||
title="Cancel editing"
|
||||
disabled={patch_status === 'processing'}>
|
||||
<X size="14" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="input_container mb-3">
|
||||
{#if field_type === 'textarea'}
|
||||
<textarea
|
||||
{id}
|
||||
bind:this={input_ref}
|
||||
bind:value={draft_value}
|
||||
rows={textarea_rows}
|
||||
class="textarea"
|
||||
{placeholder}></textarea>
|
||||
{:else if field_type === 'select'}
|
||||
<div class="relative">
|
||||
<select
|
||||
{id}
|
||||
bind:this={input_ref}
|
||||
value={draft_value}
|
||||
onchange={(e) => draft_value = coerce_select_value(e.currentTarget.value, current_value)}
|
||||
class="select pr-10">
|
||||
{#if allow_null}
|
||||
<option value={null}>-- None --</option>
|
||||
{/if}
|
||||
{#if Object.keys(select_options).length === 0}
|
||||
<option value="" disabled>Loading options...</option>
|
||||
{/if}
|
||||
{#each Object.entries(select_options) as [val, label] (val)}
|
||||
<option value={val}>{label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if Object.keys(select_options).length === 0}
|
||||
<div class="absolute inset-y-0 right-8 flex items-center">
|
||||
<LoaderCircle size="14" class="animate-spin opacity-50" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if field_type === 'checkbox'}
|
||||
<label class="flex items-center space-x-2">
|
||||
<input
|
||||
{id}
|
||||
type="checkbox"
|
||||
bind:this={input_ref}
|
||||
bind:checked={draft_value}
|
||||
class="checkbox" />
|
||||
<span>{draft_value ? 'True' : 'False'}</span>
|
||||
</label>
|
||||
{:else if field_type === 'tiptap'}
|
||||
<AE_Comp_Editor_TipTap
|
||||
bind:content={draft_value}
|
||||
{placeholder} />
|
||||
{:else if field_type === 'codemirror'}
|
||||
<AE_Comp_Editor_CodeMirror
|
||||
bind:content={draft_value}
|
||||
{placeholder} />
|
||||
{:else if field_type === 'date'}
|
||||
<input
|
||||
{id}
|
||||
type="date"
|
||||
bind:this={input_ref}
|
||||
bind:value={draft_value}
|
||||
class="input" />
|
||||
{:else if field_type === 'datetime'}
|
||||
<input
|
||||
{id}
|
||||
type="datetime-local"
|
||||
bind:this={input_ref}
|
||||
bind:value={draft_value}
|
||||
class="input" />
|
||||
{:else}
|
||||
<input
|
||||
{id}
|
||||
type={field_type === 'number' ? 'number' : field_type === 'email' ? 'email' : field_type === 'url' ? 'url' : field_type === 'tel' ? 'tel' : 'text'}
|
||||
bind:this={input_ref}
|
||||
bind:value={draft_value}
|
||||
class="input"
|
||||
{placeholder}
|
||||
onkeydown={(e) => e.key === 'Enter' && handle_patch()} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
<footer class="flex items-center justify-between">
|
||||
<div class="status_indicator text-xs">
|
||||
{#if patch_status === 'processing'}
|
||||
<span class="text-primary-500 flex items-center gap-1">
|
||||
<LoaderCircle size="12" class="animate-spin" /> Saving...
|
||||
</span>
|
||||
{:else if patch_status === 'success'}
|
||||
<span class="text-success-500 flex items-center gap-1">
|
||||
<Check size="12" /> Saved
|
||||
</span>
|
||||
{:else if patch_status === 'error'}
|
||||
<div class="text-error-500 flex flex-col gap-0.5">
|
||||
<span class="flex items-center gap-1 font-bold">
|
||||
<CircleAlert size="12" /> Error
|
||||
</span>
|
||||
<span class="opacity-80">{error_message}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="actions flex gap-2">
|
||||
{#if allow_null && draft_value !== null}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-tonal-error"
|
||||
onclick={() => (draft_value = null)}
|
||||
aria-label="Clear value"
|
||||
title="Clear value">
|
||||
<Eraser size="14" class="mr-1" />
|
||||
Clear
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-filled-primary-500"
|
||||
onclick={handle_patch}
|
||||
aria-label="Save changes"
|
||||
title="Save changes"
|
||||
disabled={patch_status === 'processing' || draft_value === to_input_value(current_value, field_type)}>
|
||||
{#if patch_status === 'processing'}
|
||||
<LoaderCircle size="14" class="mr-1 animate-spin" />
|
||||
{:else}
|
||||
<Save size="14" class="mr-1" />
|
||||
{/if}
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.ae_field_editor :global(.btn-icon-sm) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -10,6 +10,20 @@ import { db_core } from '$lib/ae_core/db_core';
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
import type { ae_DataStore } from '$lib/types/ae_types';
|
||||
import {
|
||||
Check,
|
||||
Code,
|
||||
Eye,
|
||||
LoaderCircle,
|
||||
Save,
|
||||
SquarePen,
|
||||
Trash2,
|
||||
X,
|
||||
Info,
|
||||
Database
|
||||
} from '@lucide/svelte';
|
||||
import AE_Comp_Editor_TipTap from '$lib/elements/element_editor_tiptap.svelte';
|
||||
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
|
||||
|
||||
interface Props {
|
||||
log_lvl?: number;
|
||||
@@ -57,10 +71,30 @@ let {
|
||||
|
||||
// Local reactive state
|
||||
let trigger: null | string = $state(null);
|
||||
let ds_submit_results: Promise<any> | key_val | undefined = $state();
|
||||
let draft_value = $state('');
|
||||
let draft_name = $state('');
|
||||
let draft_code = $state('');
|
||||
let draft_type = $state('');
|
||||
let draft_use_account_id = $state(false);
|
||||
let html_edit_mode = $state<'source' | 'visual'>('source');
|
||||
|
||||
// Change detection derived from draft vs current LiveQuery object
|
||||
let has_changes = $derived.by(() => {
|
||||
const entry = $lq__ds_obj as ae_DataStore | null;
|
||||
if (!entry) return true; // Treat new record as having changes
|
||||
|
||||
const current_val = entry.type === 'json'
|
||||
? (typeof entry.json === 'string' ? entry.json : JSON.stringify(entry.json, null, 2))
|
||||
: (entry.text || entry.html || '');
|
||||
|
||||
return draft_value !== current_val ||
|
||||
draft_name !== (entry.name || '') ||
|
||||
draft_code !== (entry.code || '') ||
|
||||
draft_type !== (entry.type || 'text') ||
|
||||
draft_use_account_id !== (!!entry.account_id);
|
||||
});
|
||||
|
||||
// Dexie LiveQuery for data store
|
||||
// This derived observable will automatically update when dependencies change
|
||||
let lq__ds_obj = $derived(
|
||||
liveQuery(async () => {
|
||||
const current_code = ds_code;
|
||||
@@ -70,20 +104,6 @@ let lq__ds_obj = $derived(
|
||||
|
||||
if (!current_code) return null;
|
||||
|
||||
if (log_lvl)
|
||||
console.log(`ae_e_data_store [${current_code}]: LQ Lookup...`, {
|
||||
account_id,
|
||||
current_for_type,
|
||||
current_for_id
|
||||
});
|
||||
|
||||
// Hierarchical Local Lookup (Specific -> Account -> Global)
|
||||
// Mimics backend SQL priority: WHERE code = :code ORDER BY for_id DESC, account_id DESC
|
||||
if (log_lvl)
|
||||
console.log(
|
||||
`ae_e_data_store [${current_code}]: Fetching all matching codes for priority sorting...`
|
||||
);
|
||||
|
||||
const results = await db_core.data_store
|
||||
.where('code')
|
||||
.equals(current_code)
|
||||
@@ -93,86 +113,93 @@ let lq__ds_obj = $derived(
|
||||
|
||||
// Sort by specificity
|
||||
results.sort((a, b) => {
|
||||
// 1. Priority: Specific Context match (for_type + for_id)
|
||||
const a_context =
|
||||
current_for_id &&
|
||||
a.for_id === current_for_id &&
|
||||
a.for_type === current_for_type
|
||||
? 1
|
||||
: 0;
|
||||
const b_context =
|
||||
current_for_id &&
|
||||
b.for_id === current_for_id &&
|
||||
b.for_type === current_for_type
|
||||
? 1
|
||||
: 0;
|
||||
const a_context = current_for_id && a.for_id === current_for_id && a.for_type === current_for_type ? 1 : 0;
|
||||
const b_context = current_for_id && b.for_id === current_for_id && b.for_type === current_for_type ? 1 : 0;
|
||||
if (a_context !== b_context) return b_context - a_context;
|
||||
|
||||
// 2. Priority: Account-specific match
|
||||
const a_account = account_id && a.account_id === account_id ? 1 : 0;
|
||||
const b_account = account_id && b.account_id === account_id ? 1 : 0;
|
||||
if (a_account !== b_account) return b_account - a_account;
|
||||
|
||||
// 3. Tie-breaker: Newest updated
|
||||
const a_time = new Date(
|
||||
a.updated_on || a.created_on || 0
|
||||
).getTime();
|
||||
const b_time = new Date(
|
||||
b.updated_on || b.created_on || 0
|
||||
).getTime();
|
||||
const a_time = new Date(a.updated_on || a.created_on || 0).getTime();
|
||||
const b_time = new Date(b.updated_on || b.created_on || 0).getTime();
|
||||
return b_time - a_time;
|
||||
});
|
||||
|
||||
if (log_lvl)
|
||||
console.log(
|
||||
`ae_e_data_store [${current_code}]: Best match found (ID: ${results[0].id}, Account: ${results[0].account_id})`
|
||||
);
|
||||
return results[0];
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* reset_drafts: Resets all draft fields to the current object's values.
|
||||
* Called when the live data changes OR when the editor is closed/cancelled.
|
||||
*/
|
||||
function reset_drafts() {
|
||||
const entry = $lq__ds_obj as ae_DataStore | null;
|
||||
if (!entry) return;
|
||||
|
||||
draft_name = entry.name || '';
|
||||
draft_code = entry.code || '';
|
||||
draft_type = entry.type || 'text';
|
||||
draft_use_account_id = !!entry.account_id;
|
||||
draft_value = entry.type === 'json'
|
||||
? (typeof entry.json === 'string' ? entry.json : JSON.stringify(entry.json, null, 2))
|
||||
: (entry.text || entry.html || '');
|
||||
}
|
||||
|
||||
// Sync status and bound props when the live data changes
|
||||
$effect(() => {
|
||||
const entry = $lq__ds_obj as ae_DataStore | null;
|
||||
|
||||
untrack(() => {
|
||||
ds_loaded = !!entry;
|
||||
if (entry) {
|
||||
ds_loading_status = 'loaded';
|
||||
// Handle val_sql binding if type is sql
|
||||
if (ds_type === 'sql') {
|
||||
val_sql = entry.text || entry.html || null;
|
||||
}
|
||||
// Initialize draft values when not editing
|
||||
if (!show_edit) {
|
||||
reset_drafts();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Context Change Guard: reset loading status when identity props change.
|
||||
// WHY: ds_loading_status persists after the initial load cycle ('loaded', 'not found', etc.).
|
||||
// When for_id/for_type/ds_code change after mount (e.g. for_id resolves from undefined
|
||||
// to a real event_id), the trigger effect below won't re-fire unless we reset to 'starting'.
|
||||
// untrack() prevents a circular dep — we write ds_loading_status but don't subscribe to it here.
|
||||
// Reset draft values when the editor is closed (discard changes)
|
||||
$effect(() => {
|
||||
void for_id;
|
||||
void for_type;
|
||||
void ds_code;
|
||||
untrack(() => {
|
||||
ds_loading_status = 'starting';
|
||||
});
|
||||
if (!show_edit) {
|
||||
untrack(() => reset_drafts());
|
||||
}
|
||||
});
|
||||
|
||||
// Reset submit status when opening the editor
|
||||
$effect(() => {
|
||||
if (show_edit) {
|
||||
untrack(() => {
|
||||
$ae_sess.ds.submit_status = 'idle';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Context Change Guard
|
||||
$effect(() => {
|
||||
void for_id; void for_type; void ds_code;
|
||||
untrack(() => { ds_loading_status = 'starting'; });
|
||||
});
|
||||
|
||||
// Initial Trigger & Context Change Guard
|
||||
$effect(() => {
|
||||
const account_id = $slct.account_id;
|
||||
const api_ready = !!$ae_api?.base_url;
|
||||
const entry = $lq__ds_obj;
|
||||
const entry = $lq__ds_obj as ae_DataStore | null | undefined;
|
||||
|
||||
if (browser && api_ready && !entry && ds_loading_status === 'starting') {
|
||||
if (!browser || !account_id || !api_ready || ds_loading_status !== 'starting') return;
|
||||
|
||||
const entry_is_stale_account = entry?.account_id !== null && entry?.account_id !== account_id;
|
||||
if (!entry || entry_is_stale_account) {
|
||||
trigger = 'load__ds__code';
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch handler
|
||||
$effect(() => {
|
||||
if (trigger === 'load__ds__code') {
|
||||
untrack(() => {
|
||||
@@ -182,13 +209,9 @@ $effect(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// Mount reload logic
|
||||
onMount(() => {
|
||||
if (mount_reload_sec > 0) {
|
||||
const random_ms = Math.floor(Math.random() * mount_reload_sec * 1000);
|
||||
setTimeout(() => {
|
||||
trigger = 'load__ds__code';
|
||||
}, random_ms);
|
||||
setTimeout(() => { trigger = 'load__ds__code'; }, Math.floor(Math.random() * mount_reload_sec * 1000));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -197,81 +220,33 @@ async function load_data_store() {
|
||||
ds_loading_status = 'loading';
|
||||
const api_cfg = untrack(() => $ae_api);
|
||||
|
||||
if (log_lvl) console.log(`ae_e_data_store [${ds_code}]: Fetching...`);
|
||||
|
||||
try {
|
||||
// Attempt 1: Context-specific fetch
|
||||
let ds_results = await api.get_data_store({
|
||||
api_cfg,
|
||||
code: ds_code,
|
||||
for_type: for_type,
|
||||
for_id: for_id,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
|
||||
// V3 API structured check
|
||||
let ds_results = await api.get_data_store({ api_cfg, code: ds_code, for_type, for_id, log_lvl });
|
||||
const is_error = ds_results?.meta?.success === false;
|
||||
const status_code =
|
||||
ds_results?.meta?.status_code || (ds_results === false ? 500 : 200);
|
||||
const status_code = ds_results?.meta?.status_code || (ds_results === false ? 500 : 200);
|
||||
|
||||
// Fallback to Global if not found (404), unauthorized (403/401), or explicitly failed
|
||||
if (
|
||||
!ds_results ||
|
||||
is_error ||
|
||||
status_code === 404 ||
|
||||
status_code === 403 ||
|
||||
status_code === 401
|
||||
) {
|
||||
if (log_lvl)
|
||||
console.log(
|
||||
`ae_e_data_store [${ds_code}]: Not found in context (Status ${status_code}). Trying global fallback.`
|
||||
);
|
||||
|
||||
// TEMPORARY: same global-default fallback as core__data_store.ts.
|
||||
// This should go away once the backend can answer with JWT-backed,
|
||||
// account-scoped defaults only.
|
||||
ds_results = await api.get_data_store({
|
||||
api_cfg,
|
||||
code: ds_code,
|
||||
no_account_id: true,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
if (!ds_results || is_error || [404, 403, 401].includes(status_code)) {
|
||||
ds_results = await api.get_data_store({ api_cfg, code: ds_code, no_account_id: true, log_lvl });
|
||||
}
|
||||
|
||||
const ds_id = ds_results?.data_store_id || ds_results?.id;
|
||||
|
||||
if (ds_results && ds_id) {
|
||||
// Map fields correctly for V3 alignment
|
||||
const text_val = ds_results.text || '';
|
||||
const json_val =
|
||||
ds_results.json ||
|
||||
(ds_results.json_str ? JSON.parse(ds_results.json_str) : null);
|
||||
const json_val = ds_results.json || (ds_results.json_str ? JSON.parse(ds_results.json_str) : null);
|
||||
|
||||
// Save to Dexie
|
||||
const ds_to_save: ae_DataStore = {
|
||||
...ds_results,
|
||||
id: ds_id,
|
||||
data_store_id: ds_results.data_store_id || ds_id,
|
||||
// data_store_id: ds_id,
|
||||
account_id: ds_results.account_id || ds_results.account_id,
|
||||
// account_id: ds_results.account_id || ds_results.account_id,
|
||||
account_id: ds_results.account_id || null,
|
||||
updated_on: ds_results.updated_on || new Date().toISOString(),
|
||||
text: text_val,
|
||||
html: text_val, // Default map text to html
|
||||
html: text_val,
|
||||
json: json_val
|
||||
};
|
||||
|
||||
await db_core.data_store.put(ds_to_save);
|
||||
if (log_lvl)
|
||||
console.log(
|
||||
`ae_e_data_store [${ds_code}]: Saved to Dexie. ID: ${ds_id}`
|
||||
);
|
||||
} else {
|
||||
ds_loading_status = 'not found';
|
||||
if (log_lvl)
|
||||
console.warn(
|
||||
`ae_e_data_store [${ds_code}]: Result had no valid ID.`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`ae_e_data_store [${ds_code}]: Fetch failed.`, err);
|
||||
@@ -280,100 +255,59 @@ async function load_data_store() {
|
||||
}
|
||||
|
||||
async function handle_submit_form(event: Event) {
|
||||
const target = event.target as HTMLFormElement;
|
||||
$ae_sess.ds.submit_status = 'processing';
|
||||
|
||||
const form_data = new FormData(target);
|
||||
const data_store_di = ae_util.extract_prefixed_form_data({
|
||||
prefix: null,
|
||||
form_data,
|
||||
trim_values: true,
|
||||
bool_tf_str: true
|
||||
});
|
||||
|
||||
const data_store_do: key_val = {
|
||||
code: data_store_di.ds_code ?? ds_code,
|
||||
name: data_store_di.ds_name ?? ds_name,
|
||||
type: data_store_di.ds_type ?? ds_type,
|
||||
for_type: data_store_di.ds_for_type ?? null,
|
||||
for_id: data_store_di.ds_for_id ?? null,
|
||||
access_read: data_store_di.ds_access_read,
|
||||
access_write: data_store_di.ds_access_write,
|
||||
access_delete: data_store_di.ds_access_delete,
|
||||
enable: data_store_di.ds_enable ?? true,
|
||||
account_id: data_store_di.ds_use_account_id
|
||||
? (data_store_di.ds_account_id ?? $slct.account_id)
|
||||
: null
|
||||
code: draft_code,
|
||||
name: draft_name,
|
||||
type: draft_type,
|
||||
for_type: for_type,
|
||||
for_id: for_id,
|
||||
enable: true,
|
||||
account_id: draft_use_account_id ? ($ae_loc.account_id || $slct.account_id) : null
|
||||
};
|
||||
|
||||
const content_val = data_store_di.ds_value;
|
||||
if (data_store_do.type === 'json') {
|
||||
data_store_do.json = content_val;
|
||||
try {
|
||||
// Ensure it's valid JSON if stringified
|
||||
if (typeof content_val === 'string') JSON.parse(content_val);
|
||||
data_store_do.json = JSON.parse(draft_value);
|
||||
} catch (e) {
|
||||
console.error('Invalid JSON content');
|
||||
data_store_do.json = draft_value;
|
||||
}
|
||||
} else {
|
||||
data_store_do.text = content_val;
|
||||
data_store_do.text = draft_value;
|
||||
}
|
||||
|
||||
const api_cfg = untrack(() => $ae_api);
|
||||
const api_call = $lq__ds_obj?.id
|
||||
? api.update_ae_obj({ api_cfg, obj_type: 'data_store', obj_id: $lq__ds_obj.id, fields: data_store_do })
|
||||
: api.create_ae_obj({ api_cfg, obj_type: 'data_store', fields: data_store_do });
|
||||
|
||||
if ($lq__ds_obj?.id) {
|
||||
ds_submit_results = api
|
||||
.update_ae_obj({
|
||||
api_cfg,
|
||||
obj_type: 'data_store',
|
||||
obj_id: $lq__ds_obj.id,
|
||||
fields: data_store_do
|
||||
})
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
$ae_sess.ds.submit_status = 'updated';
|
||||
trigger = 'load__ds__code';
|
||||
}
|
||||
return res;
|
||||
});
|
||||
} else {
|
||||
ds_submit_results = api
|
||||
.create_ae_obj({
|
||||
api_cfg,
|
||||
obj_type: 'data_store',
|
||||
fields: data_store_do
|
||||
})
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
$ae_sess.ds.submit_status = 'created';
|
||||
trigger = 'load__ds__code';
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
api_call.then((res) => {
|
||||
if (res) {
|
||||
$ae_sess.ds.submit_status = $lq__ds_obj?.id ? 'updated' : 'created';
|
||||
trigger = 'load__ds__code';
|
||||
show_edit = false;
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
async function handle_delete() {
|
||||
if (
|
||||
!$lq__ds_obj?.id ||
|
||||
!confirm('Are you sure you want to delete this data store?')
|
||||
)
|
||||
return;
|
||||
|
||||
if (!$lq__ds_obj?.id || !confirm('Are you sure you want to delete this data store?')) return;
|
||||
const api_cfg = untrack(() => $ae_api);
|
||||
const res = await api.delete_ae_obj({
|
||||
api_cfg,
|
||||
obj_type: 'data_store',
|
||||
obj_id: $lq__ds_obj.id,
|
||||
method: 'delete'
|
||||
});
|
||||
|
||||
const res = await api.delete_ae_obj({ api_cfg, obj_type: 'data_store', obj_id: $lq__ds_obj.id, method: 'delete' });
|
||||
if (res) {
|
||||
await db_core.data_store.delete($lq__ds_obj.id);
|
||||
ds_loading_status = 'not found';
|
||||
show_edit = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handle_cancel() {
|
||||
if (has_changes && !confirm('Discard unsaved changes?')) return;
|
||||
show_edit = false;
|
||||
// reset_drafts() will be called by the $effect watching show_edit
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -382,71 +316,37 @@ async function handle_delete() {
|
||||
style={display ? `display: ${display}` : undefined}>
|
||||
{#if $lq__ds_obj}
|
||||
{#if debug || $ae_loc.debug === 'debug'}
|
||||
Debug is ON!
|
||||
<pre
|
||||
class="mb-2 overflow-x-auto rounded bg-black/10 p-2 text-[10px]">
|
||||
ID: {$lq__ds_obj.id}
|
||||
Code: {$lq__ds_obj.code}
|
||||
Name: {$lq__ds_obj.name}
|
||||
Type: {$lq__ds_obj.type}
|
||||
Account: {$lq__ds_obj.account_id || 'Global / NULL'}
|
||||
Created: {$lq__ds_obj.created_on}
|
||||
Updated: {$lq__ds_obj.updated_on}
|
||||
</pre>
|
||||
|
||||
<hr />
|
||||
<div class="preset-tonal-surface mb-2 rounded-lg p-2 text-[10px]">
|
||||
<div class="flex items-center gap-1 font-bold uppercase opacity-50 mb-1">
|
||||
<Database size="12" /> Debug Info
|
||||
</div>
|
||||
<pre class="overflow-x-auto">ID: {$lq__ds_obj.id} | Code: {$lq__ds_obj.code} | Type: {$lq__ds_obj.type} | Account: {$lq__ds_obj.account_id || 'Global'}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Modal
|
||||
title="{$lq__ds_obj.name || 'Unnamed'} - {$lq__ds_obj.code}"
|
||||
title="{draft_name || 'Unnamed'} - {draft_code}"
|
||||
bind:open={show_edit}
|
||||
autoclose={false}
|
||||
outsideclose={!has_changes}
|
||||
size="xl"
|
||||
class="w-full max-w-6xl">
|
||||
<form
|
||||
class="flex flex-col gap-4"
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handle_submit_form(e);
|
||||
}}>
|
||||
<input
|
||||
type="hidden"
|
||||
name="ds_id_random"
|
||||
value={$lq__ds_obj.id} />
|
||||
|
||||
<form class="flex flex-col gap-4" onsubmit={(e) => { e.preventDefault(); handle_submit_form(e); }}>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<label class="label">
|
||||
<span class="text-xs font-bold opacity-70"
|
||||
>Code</span>
|
||||
<input
|
||||
type="text"
|
||||
name="ds_code"
|
||||
class="input font-mono"
|
||||
value={$lq__ds_obj.code}
|
||||
readonly={!$ae_loc.manager_access}
|
||||
required />
|
||||
<span class="text-xs font-bold opacity-70">Code</span>
|
||||
<input type="text" name="ds_code" class="input font-mono" bind:value={draft_code} readonly={!$ae_loc.manager_access} required />
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="text-xs font-bold opacity-70"
|
||||
>Name</span>
|
||||
<input
|
||||
type="text"
|
||||
name="ds_name"
|
||||
class="input"
|
||||
value={$lq__ds_obj.name}
|
||||
required />
|
||||
<span class="text-xs font-bold opacity-70">Name</span>
|
||||
<input type="text" name="ds_name" class="input" bind:value={draft_name} required />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="label">
|
||||
<span class="text-xs font-bold opacity-70"
|
||||
>Type</span>
|
||||
<select
|
||||
name="ds_type"
|
||||
class="select"
|
||||
value={$lq__ds_obj.type}>
|
||||
<span class="text-xs font-bold opacity-70">Type</span>
|
||||
<select name="ds_type" class="select" bind:value={draft_type}>
|
||||
<option value="text">Text</option>
|
||||
<option value="html">HTML</option>
|
||||
<option value="json">JSON</option>
|
||||
@@ -455,53 +355,83 @@ async function handle_delete() {
|
||||
</select>
|
||||
</label>
|
||||
<div class="flex items-center gap-2 pt-6">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="ds_use_account_id"
|
||||
class="checkbox"
|
||||
checked={!!$lq__ds_obj.account_id} />
|
||||
<span class="text-xs">Account Specific</span>
|
||||
<input type="checkbox" name="ds_use_account_id" id="ds_use_account_id" class="checkbox" bind:checked={draft_use_account_id} />
|
||||
<label for="ds_use_account_id" class="text-xs cursor-pointer">Account Specific</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<span class="text-xs font-bold opacity-70">Content</span>
|
||||
<textarea
|
||||
name="ds_value"
|
||||
class="textarea font-mono text-sm"
|
||||
rows="15"
|
||||
placeholder="Enter content here..."
|
||||
>{$lq__ds_obj.type === 'json'
|
||||
? typeof $lq__ds_obj.json === 'string'
|
||||
? $lq__ds_obj.json
|
||||
: JSON.stringify($lq__ds_obj.json, null, 2)
|
||||
: $lq__ds_obj.text ||
|
||||
$lq__ds_obj.html ||
|
||||
''}</textarea>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-bold opacity-70">Content</span>
|
||||
{#if draft_type === 'html'}
|
||||
<div class="flex items-center gap-1 rounded bg-black/5 p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1 rounded px-2 py-0.5 text-[10px] font-bold uppercase transition-all"
|
||||
class:bg-primary-500={html_edit_mode === 'source'}
|
||||
class:text-white={html_edit_mode === 'source'}
|
||||
class:opacity-50={html_edit_mode !== 'source'}
|
||||
onclick={() => (html_edit_mode = 'source')}
|
||||
title="Edit raw HTML source">
|
||||
<Code size="10" /> Source
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1 rounded px-2 py-0.5 text-[10px] font-bold uppercase transition-all"
|
||||
class:bg-primary-500={html_edit_mode === 'visual'}
|
||||
class:text-white={html_edit_mode === 'visual'}
|
||||
class:opacity-50={html_edit_mode !== 'visual'}
|
||||
onclick={() => (html_edit_mode = 'visual')}
|
||||
title="Visual / WYSIWYG editor">
|
||||
<Eye size="10" /> Visual
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="text-surface-500 text-xs">
|
||||
Created on: {$lq__ds_obj.created_on} | Last Updated: {$lq__ds_obj.updated_on}
|
||||
{#if draft_type === 'html'}
|
||||
{#if html_edit_mode === 'source'}
|
||||
<AE_Comp_Editor_CodeMirror bind:content={draft_value} placeholder="Enter HTML Source..." />
|
||||
{:else}
|
||||
<AE_Comp_Editor_TipTap bind:content={draft_value} placeholder="Enter HTML content..." />
|
||||
{/if}
|
||||
{:else if draft_type === 'json' || draft_type === 'sql' || draft_type === 'md'}
|
||||
<AE_Comp_Editor_CodeMirror bind:content={draft_value} placeholder="Enter {draft_type.toUpperCase()} content..." />
|
||||
{:else}
|
||||
<textarea bind:value={draft_value} class="textarea font-mono text-sm" rows="15" placeholder="Enter text content..."></textarea>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-4">
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-filled-error"
|
||||
onclick={handle_delete}>
|
||||
<span class="fas fa-trash mr-2"></span> Delete
|
||||
class="btn preset-tonal-error"
|
||||
onclick={handle_delete}
|
||||
title="Permanently delete this data store — cannot be undone">
|
||||
<Trash2 size="14" class="mr-2" /> Delete
|
||||
</button>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-soft"
|
||||
onclick={() => (show_edit = false)}>Cancel</button>
|
||||
class="btn preset-tonal-surface"
|
||||
onclick={handle_cancel}
|
||||
title="Discard changes and close">
|
||||
<X size="14" class="mr-2" /> Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn variant-filled-primary">
|
||||
<span class="fas fa-save mr-2"></span> Save
|
||||
class="btn preset-filled-primary-500"
|
||||
disabled={!has_changes || $ae_sess.ds.submit_status === 'processing'}
|
||||
title="Save changes to this data store">
|
||||
{#if $ae_sess.ds.submit_status === 'processing'}
|
||||
<LoaderCircle size="14" class="mr-2 animate-spin" />
|
||||
{:else if $ae_sess.ds.submit_status === 'updated' || $ae_sess.ds.submit_status === 'created'}
|
||||
<Check size="14" class="mr-2" />
|
||||
{:else}
|
||||
<Save size="14" class="mr-2" />
|
||||
{/if}
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -514,38 +444,36 @@ async function handle_delete() {
|
||||
{:else if $lq__ds_obj.type === 'text' && $lq__ds_obj.text}
|
||||
<div class="whitespace-pre-wrap">{$lq__ds_obj.text}</div>
|
||||
{:else if $lq__ds_obj.type === 'sql' && $lq__ds_obj.text}
|
||||
{#if debug}<div class="font-mono text-xs opacity-50">
|
||||
SQL: {$lq__ds_obj.text}
|
||||
</div>{/if}
|
||||
{#if debug}<div class="preset-tonal-surface rounded p-2 font-mono text-xs opacity-50"><span class="font-bold">SQL:</span> {$lq__ds_obj.text}</div>{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if $ae_loc.edit_mode && ($ae_loc.manager_access || (show_edit_btn && $ae_loc.administrator_access))}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm variant-soft-warning absolute top-0 right-0 z-10 opacity-20 hover:opacity-100"
|
||||
ondblclick={() => {
|
||||
show_edit = true;
|
||||
show_view = false;
|
||||
}}
|
||||
class="btn-icon btn-icon-sm preset-tonal-warning absolute top-0 right-0 z-10 opacity-20 transition-opacity hover:opacity-100"
|
||||
ondblclick={() => { show_edit = true; }}
|
||||
title="Edit Data Store: {ds_code}">
|
||||
<span class="fas fa-edit"></span>
|
||||
<SquarePen size="14" />
|
||||
</button>
|
||||
{/if}
|
||||
{:else if ds_loading_status === 'not found'}
|
||||
<!-- Only show diagnostic to administrator+ (no edit_mode needed) or trusted staff in edit mode.
|
||||
Anonymous/user/public visitors must never see internal data store codes or gaps. -->
|
||||
{#if $ae_loc.administrator_access || ($ae_loc.trusted_access && $ae_loc.edit_mode)}
|
||||
<div
|
||||
class="border-surface-500/30 rounded border border-dashed p-2 text-xs opacity-50">
|
||||
Data Store not found: {ds_code}
|
||||
<div class="preset-tonal-surface flex items-center gap-2 rounded border-2 border-dashed p-3 text-xs opacity-60">
|
||||
<Info size="14" class="text-warning-500" />
|
||||
<span class="font-bold">Data Store not found:</span>
|
||||
<code class="font-mono text-primary-500">{ds_code}</code>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if ds_loading_status === 'loading'}
|
||||
<div class="absolute bottom-0 left-0 p-1 opacity-50">
|
||||
<span class="fas fa-spinner fa-spin text-xs"></span>
|
||||
<LoaderCircle size="14" class="animate-spin text-primary-500" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.ae__elem__data_store :global(.btn-icon-sm) { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; padding: 0; }
|
||||
</style>
|
||||
|
||||
@@ -16,9 +16,7 @@ import {
|
||||
slct,
|
||||
slct_trigger
|
||||
} from '$lib/stores/ae_stores';
|
||||
import { db_events } from '$lib/ae_events/db_events';
|
||||
import {
|
||||
events_loc,
|
||||
events_sess,
|
||||
events_slct,
|
||||
events_trigger
|
||||
@@ -79,6 +77,9 @@ let ae_promises: key_val = $state({});
|
||||
let ae_tmp: key_val = $state({});
|
||||
ae_tmp.show__file_li = true;
|
||||
ae_tmp.show__direct_download = pres_mgmt_loc.current.show__direct_download;
|
||||
|
||||
// Strip :443 from https URLs — redundant and clutters shareable links.
|
||||
let base_url = $derived($ae_api.base_url.replace(/^(https:\/\/[^/:]+):443(\/|$)/, '$1$2'));
|
||||
// let ae_triggers: key_val = {};
|
||||
|
||||
onMount(() => {
|
||||
@@ -151,30 +152,15 @@ async function handle_convert_pdf_to_image(event_file_obj: key_val) {
|
||||
<div class="float-right flex flex-row items-center">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
console.log('*** Refresh button clicked ***');
|
||||
|
||||
db_events.file.clear();
|
||||
|
||||
// let params = {
|
||||
// qry__enabled: 'all',
|
||||
// qry__hidden: 'all',
|
||||
// }
|
||||
|
||||
events_func.load_ae_obj_li__event_file({
|
||||
onclick={async () => {
|
||||
await events_func.load_ae_obj_li__event_file({
|
||||
api_cfg: $ae_api,
|
||||
for_obj_type: link_to_type,
|
||||
for_obj_id: link_to_id,
|
||||
enabled: 'all',
|
||||
hidden: 'all',
|
||||
// params: params,
|
||||
try_cache: true
|
||||
});
|
||||
|
||||
// ae_tmp.show__file_li = false;
|
||||
// console.log(`$lq__event_file_obj_li:`, $lq__event_file_obj_li);
|
||||
// $slct_trigger = 'load__event_file_obj_li';
|
||||
// ae_tmp.show__file_li = true;
|
||||
}}
|
||||
class="btn btn-sm preset-tonal-tertiary hover:preset-tonal-warning border-warning-500 m-1 border p-1 transition hover:transition-all *:hover:inline"
|
||||
class:hidden={!$ae_loc.edit_mode || !$ae_loc.authenticated_access}
|
||||
@@ -208,11 +194,12 @@ async function handle_convert_pdf_to_image(event_file_obj: key_val) {
|
||||
display_mode != 'default'}>
|
||||
Manage Files:
|
||||
<span
|
||||
class="bg-success-100 border-success-200 rounded-lg border px-4 font-bold"
|
||||
class="preset-filled-surface-300-700 rounded-lg px-4 text-xl font-bold"
|
||||
title="Files for {link_to_type ?? '-- not set --'}: {link_to_id ??
|
||||
'-- not set --'} (files: {$lq__event_file_obj_li?.length ??
|
||||
'None'})">
|
||||
<FolderOpen size="1em" class="mx-1" />
|
||||
<FolderOpen size=".9em" class="text-surface-800-200" />
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html $lq__event_file_obj_li
|
||||
? `${$lq__event_file_obj_li.length}×`
|
||||
: '-- none --'}
|
||||
@@ -333,7 +320,7 @@ async function handle_convert_pdf_to_image(event_file_obj: key_val) {
|
||||
</span>
|
||||
<MyClipboard
|
||||
value={encodeURI(
|
||||
`${$ae_api.base_url}/v3/action/event_file/${event_file_obj?.event_file_id}/download?filename=${ae_util.clean_filename(event_file_obj?.filename)}&key=${$ae_api.account_id}`
|
||||
`${base_url}/v3/action/event_file/${event_file_obj?.event_file_id}/download?filename=${ae_util.clean_filename(event_file_obj?.filename)}&key=${$ae_api.account_id}`
|
||||
)}
|
||||
btn_text="Copy Original"
|
||||
btn_title="Copy the direct download link to the clipboard."
|
||||
@@ -342,10 +329,10 @@ async function handle_convert_pdf_to_image(event_file_obj: key_val) {
|
||||
|
||||
<MyClipboard
|
||||
value={encodeURI(
|
||||
`${$ae_api.base_url}/v3/action/event_file/${event_file_obj?.event_file_id}/download?filename=${event_file_obj?.event_session_code}-${ae_util.clean_filename(event_file_obj?.event_session_name).substring(0, 20)}-${ae_util.clean_filename(event_file_obj?.event_presenter_full_name)}.${event_file_obj?.extension}&key=${$ae_api.account_id}`
|
||||
`${base_url}/v3/action/event_file/${event_file_obj?.event_file_id}/download?filename=${ae_util.clean_filename(event_file_obj?.event_session_code ?? '')}_${ae_util.clean_filename(event_file_obj?.event_presentation_name ?? event_file_obj?.event_session_name ?? '').substring(0, 30)}_${ae_util.clean_filename(event_file_obj?.event_presenter_full_name ?? '')}.${event_file_obj?.extension}&key=${$ae_api.account_id}`
|
||||
)}
|
||||
btn_text="Copy Renamed"
|
||||
btn_title="Copy the renamed download link to the clipboard."
|
||||
btn_title="Copy the renamed download link to the clipboard. Format: [session-code]_[presentation-name]_[presenter-name].[ext]"
|
||||
btn_class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500"
|
||||
></MyClipboard>
|
||||
</div>
|
||||
|
||||
@@ -1,100 +1,26 @@
|
||||
// store_versions MUST be first import — its side-effect wipes stale localStorage
|
||||
// before svelte-persisted-store hydrates from it.
|
||||
import { AE_EVENTS_LOC_VERSION } from '$lib/stores/store_versions';
|
||||
|
||||
import { persisted } from 'svelte-persisted-store';
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
|
||||
import {
|
||||
badges_loc_defaults,
|
||||
badges_sess_defaults
|
||||
} from '$lib/stores/ae_events_stores__badges_defaults';
|
||||
import {
|
||||
launcher_loc_defaults,
|
||||
launcher_sess_defaults
|
||||
} from '$lib/stores/ae_events_stores__launcher_defaults';
|
||||
import {
|
||||
leads_loc_defaults,
|
||||
leads_sess_defaults
|
||||
} from '$lib/stores/ae_events_stores__leads_defaults';
|
||||
import {
|
||||
pres_mgmt_loc_defaults,
|
||||
pres_mgmt_sess_defaults
|
||||
} from '$lib/stores/ae_events_stores__pres_mgmt_defaults';
|
||||
// Static display title for the Events module. Always this value — nothing writes to it.
|
||||
// Use this constant instead of $events_loc.title (which is being retired).
|
||||
export const EVENTS_MODULE_TITLE = `OSIT's Æ Events`;
|
||||
|
||||
import { badges_sess_defaults } from '$lib/stores/ae_events_stores__badges_defaults';
|
||||
import { launcher_sess_defaults } from '$lib/stores/ae_events_stores__launcher_defaults';
|
||||
import { leads_sess_defaults } from '$lib/stores/ae_events_stores__leads_defaults';
|
||||
import { pres_mgmt_sess_defaults } from '$lib/stores/ae_events_stores__pres_mgmt_defaults';
|
||||
|
||||
// Deployment version stamp. Compared against events_sess.ver in events/+layout.svelte
|
||||
// to detect stale persisted data after a deploy (triggers a reload). Bump this alongside
|
||||
// events_session_data_struct.ver. See store_versions.ts for the schema-level wipe mechanism.
|
||||
const ver = '2025-10-16_2139';
|
||||
|
||||
/* *** BEGIN *** Initialize events_local_data_struct */
|
||||
// Persisted to localStorage. Retains user preferences and event-specific config across
|
||||
// browser sessions. See store_versions.ts for the schema-level invalidation mechanism.
|
||||
const events_local_data_struct: key_val = {
|
||||
__version: AE_EVENTS_LOC_VERSION, // Schema version gate — see store_versions.ts
|
||||
ver: ver, // Deployment stamp — compared against events_sess.ver to trigger reloads.
|
||||
|
||||
name: 'Aether - Events',
|
||||
title: `OSIT's Æ Events`,
|
||||
|
||||
ds: {},
|
||||
|
||||
events_cfg_json: {},
|
||||
|
||||
event_id: null,
|
||||
|
||||
// all, disabled, enabled
|
||||
qry__enabled: 'enabled',
|
||||
// all, hidden, not_hidden
|
||||
qry__hidden: 'not_hidden',
|
||||
qry__limit: 20,
|
||||
qry__offset: 0,
|
||||
|
||||
// The show details is intended for things like meta data and additional details that are not always needed.
|
||||
show_details: false,
|
||||
|
||||
auth__person: {}, // allow, id, name, email, passcode, etc
|
||||
// The auth__entered_key (usually email or person_id) and auth__entered_passcode is found under events_sess.entered_key and events_sess.entered_passcode because it should be temporary.
|
||||
// auth__entered_passcode: null,
|
||||
|
||||
// auth__kv tracks which IDs the browser client is permitted to access.
|
||||
// Each entry is an ID mapped to true, false, 'read', or 'write'.
|
||||
// Keys should be no older than a configurable max age (checked on read).
|
||||
auth__kv: {
|
||||
event: {},
|
||||
exhibit: {},
|
||||
location: {},
|
||||
session: {},
|
||||
presentation: {},
|
||||
presenter: {},
|
||||
person: {}
|
||||
},
|
||||
|
||||
// Badge Printing — see ae_events_stores__badges_defaults.ts
|
||||
badges: badges_loc_defaults,
|
||||
|
||||
// Event Presentation Launcher — see ae_events_stores__launcher_defaults.ts
|
||||
launcher: launcher_loc_defaults,
|
||||
|
||||
// Lead Retrievals (Exhibit) — see ae_events_stores__leads_defaults.ts
|
||||
leads: leads_loc_defaults,
|
||||
|
||||
// Presentation Management — see ae_events_stores__pres_mgmt_defaults.ts
|
||||
pres_mgmt: pres_mgmt_loc_defaults
|
||||
};
|
||||
|
||||
export const events_loc: Writable<key_val> = persisted(
|
||||
'ae_events_loc',
|
||||
events_local_data_struct
|
||||
);
|
||||
|
||||
/* *** BEGIN *** Initialize events_session_data_struct */
|
||||
// In-memory only (writable, not persisted). Resets on page load.
|
||||
const events_session_data_struct: key_val = {
|
||||
// Deployment stamp — compared against events_loc.ver in events/+layout.svelte.
|
||||
// Deployment version stamp — bump alongside ver above when pushing breaking changes.
|
||||
ver: ver,
|
||||
log_lvl: 1,
|
||||
|
||||
@@ -102,7 +28,6 @@ const events_session_data_struct: key_val = {
|
||||
ds: {
|
||||
submit_status: null
|
||||
},
|
||||
ds_loaded: {},
|
||||
|
||||
qry__enabled: 'enabled', // all, disabled, enabled
|
||||
qry__hidden: 'not_hidden', // all, hidden, not_hidden
|
||||
@@ -162,31 +87,14 @@ const events_slct_obj_template: key_val = {
|
||||
event_obj_li: [],
|
||||
|
||||
// Sub-level event_
|
||||
abstract_id: null,
|
||||
abstract_obj: {},
|
||||
abstract_obj_li: [],
|
||||
|
||||
badge_id: null,
|
||||
badge_obj: {},
|
||||
badge_obj_li: [],
|
||||
|
||||
badge_template_id: null,
|
||||
badge_template_obj: {},
|
||||
badge_template_obj_li: [],
|
||||
|
||||
device_id: null,
|
||||
device_obj: {},
|
||||
device_obj_li: [],
|
||||
|
||||
exhibit_id: null,
|
||||
exhibit_obj: {},
|
||||
exhibit_obj_li: [],
|
||||
|
||||
// Rename these to badge_tracking_*?
|
||||
exhibit_tracking_id: null,
|
||||
exhibit_tracking_obj: {},
|
||||
exhibit_tracking_obj_li: [],
|
||||
|
||||
file_id: null,
|
||||
file_obj: {},
|
||||
file_obj_li: [],
|
||||
@@ -218,8 +126,6 @@ const events_slct_obj_template: key_val = {
|
||||
session_obj_li: [],
|
||||
event_session_obj: {},
|
||||
|
||||
lq__presenter_obj: {}, // Testing passing a LiveQuery object around...
|
||||
|
||||
auth__event_presenter_id: null,
|
||||
auth__event_presentation_id: null
|
||||
};
|
||||
|
||||
30
src/lib/stores/ae_events_stores__auth.svelte.ts
Normal file
30
src/lib/stores/ae_events_stores__auth.svelte.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { PersistedState } from 'runed';
|
||||
|
||||
const events_auth_loc_defaults = {
|
||||
auth__person: {
|
||||
id: null as string | null,
|
||||
person_id: null as string | null,
|
||||
entered_key: null as string | null,
|
||||
email: null as string | null,
|
||||
full_name: null as string | null,
|
||||
presenter_id: null as string | null,
|
||||
presentation_id: null as string | null,
|
||||
session_id: null as string | null
|
||||
},
|
||||
auth__kv: {
|
||||
event: {} as Record<string, any>,
|
||||
exhibit: {} as Record<string, any>,
|
||||
location: {} as Record<string, any>,
|
||||
session: {} as Record<string, any>,
|
||||
presentation: {} as Record<string, any>,
|
||||
presenter: {} as Record<string, any>,
|
||||
person: {} as Record<string, any>
|
||||
}
|
||||
};
|
||||
|
||||
export const events_auth_loc = new PersistedState('ae_events_auth_loc', events_auth_loc_defaults, {
|
||||
serializer: {
|
||||
serialize: JSON.stringify,
|
||||
deserialize: (raw: string) => ({ ...events_auth_loc_defaults, ...JSON.parse(raw) })
|
||||
}
|
||||
});
|
||||
@@ -15,4 +15,11 @@
|
||||
import { PersistedState } from 'runed';
|
||||
import { badges_loc_defaults } from './ae_events_stores__badges_defaults';
|
||||
|
||||
export const badges_loc = new PersistedState('ae_badges_loc', badges_loc_defaults);
|
||||
// Custom deserializer merges stored JSON with defaults so that new fields added
|
||||
// after a user's first session get their default values rather than undefined.
|
||||
export const badges_loc = new PersistedState('ae_badges_loc', badges_loc_defaults, {
|
||||
serializer: {
|
||||
serialize: JSON.stringify,
|
||||
deserialize: (raw: string) => ({ ...badges_loc_defaults, ...JSON.parse(raw) })
|
||||
}
|
||||
});
|
||||
|
||||
@@ -97,8 +97,8 @@ export const default_trusted_can_edit: string[] = [
|
||||
// ---------------------------------------------------------------------------
|
||||
export interface BadgesLocState {
|
||||
auto_view: boolean;
|
||||
show_hidden: boolean;
|
||||
show_not_enabled: boolean;
|
||||
// 'default' = hide hidden+disabled | 'show_hidden' = show hidden | 'show_all' = show hidden+disabled
|
||||
visibility_filter: 'default' | 'show_hidden' | 'show_all';
|
||||
show_printed: boolean;
|
||||
allow_reprint: boolean;
|
||||
show_element__cfg: boolean;
|
||||
@@ -110,6 +110,7 @@ export interface BadgesLocState {
|
||||
qry_printed_status: string; // 'all' | 'printed' | 'not_printed'
|
||||
qry_affiliations: string | null;
|
||||
qry_sort_order: string;
|
||||
qry_result_limit: number; // UI override for max results (edit mode only; tier max enforced by stepper)
|
||||
status_qry__search: string | null;
|
||||
use_id_li: boolean;
|
||||
search_status: string | null;
|
||||
@@ -137,6 +138,10 @@ export interface BadgesLocState {
|
||||
trusted_search_min_chars: number;
|
||||
// Timestamp when the remote config was last mirrored locally
|
||||
remote_cfg_last_synced_on: string | null;
|
||||
// After-print navigation method. true (default) = SvelteKit goto() — faster, no full reload.
|
||||
// false = window.location.href — full page reload, use as a fallback if goto causes issues.
|
||||
// Per-device: stored in localStorage, not synced to the event config.
|
||||
print_nav_use_goto: boolean;
|
||||
}
|
||||
|
||||
export interface BadgesSessState {
|
||||
@@ -158,8 +163,7 @@ export interface BadgesSessState {
|
||||
export const badges_loc_defaults: BadgesLocState = {
|
||||
auto_view: true,
|
||||
|
||||
show_hidden: false, // Hidden (archived) badges are excluded from the main list.
|
||||
show_not_enabled: false,
|
||||
visibility_filter: 'default', // 'default' | 'show_hidden' | 'show_all'
|
||||
|
||||
show_printed: false,
|
||||
allow_reprint: false,
|
||||
@@ -178,6 +182,7 @@ export const badges_loc_defaults: BadgesLocState = {
|
||||
qry_printed_status: 'all', // 'all' | 'printed' | 'not_printed'
|
||||
qry_affiliations: null, // null = no affiliation filter
|
||||
qry_sort_order: '', // '' = default sort order
|
||||
qry_result_limit: 25,
|
||||
|
||||
status_qry__search: null,
|
||||
use_id_li: true,
|
||||
@@ -202,7 +207,8 @@ export const badges_loc_defaults: BadgesLocState = {
|
||||
auth_search_min_chars: 2,
|
||||
trusted_search_result_limit: 150,
|
||||
trusted_search_min_chars: 1,
|
||||
remote_cfg_last_synced_on: null
|
||||
remote_cfg_last_synced_on: null,
|
||||
print_nav_use_goto: true
|
||||
};
|
||||
|
||||
// In-memory badge state — resets on page load.
|
||||
|
||||
9
src/lib/stores/ae_events_stores__launcher.svelte.ts
Normal file
9
src/lib/stores/ae_events_stores__launcher.svelte.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { PersistedState } from 'runed';
|
||||
import { launcher_loc_defaults } from './ae_events_stores__launcher_defaults';
|
||||
|
||||
export const launcher_loc = new PersistedState('ae_launcher_loc', launcher_loc_defaults, {
|
||||
serializer: {
|
||||
serialize: JSON.stringify,
|
||||
deserialize: (raw: string) => ({ ...launcher_loc_defaults, ...JSON.parse(raw) })
|
||||
}
|
||||
});
|
||||
@@ -7,6 +7,8 @@
|
||||
* launcher_sess_defaults → events_sess.launcher (in-memory, resets on page load)
|
||||
*/
|
||||
|
||||
import type { LaunchProfile } from '$lib/ae_events/ae_launcher__default_launch_profiles';
|
||||
|
||||
/** 3-way section collapse state used throughout the Launcher UI. */
|
||||
export type SectionState = 'collapsed' | 'auto' | 'pinned';
|
||||
|
||||
@@ -91,6 +93,18 @@ export interface LauncherLocState {
|
||||
* device/OS without deploying to the Mac laptop.
|
||||
*/
|
||||
native_test_mode: boolean;
|
||||
/**
|
||||
* Per-file display mode overrides, keyed by event_file_id.
|
||||
* Stored locally (per-device) because event_file has no cfg_json column yet.
|
||||
* TODO: migrate to event_file.cfg_json once the backend column is added.
|
||||
*/
|
||||
file_display_overrides: Record<string, 'extend' | 'mirror' | 'none'>;
|
||||
/**
|
||||
* Local per-device launch profile overrides, keyed by profile/extension name.
|
||||
* Overrides DEFAULT_LAUNCH_PROFILES for this device only.
|
||||
* Priority: device API config > this local override > built-in defaults.
|
||||
*/
|
||||
launch_profiles: Record<string, Partial<LaunchProfile>> | null;
|
||||
}
|
||||
|
||||
export interface LauncherSessState {
|
||||
@@ -219,7 +233,9 @@ export const launcher_loc_defaults: LauncherLocState = {
|
||||
controller_client_id: null,
|
||||
native_test_mode: false,
|
||||
wallpaper_applied_url: null,
|
||||
wallpaper_applied_url_external: null
|
||||
wallpaper_applied_url_external: null,
|
||||
file_display_overrides: {},
|
||||
launch_profiles: null
|
||||
// controller_cmd: null,
|
||||
// controller_trigger_send: null,
|
||||
};
|
||||
|
||||
@@ -17,5 +17,22 @@
|
||||
*/
|
||||
import { PersistedState } from 'runed';
|
||||
import { leads_loc_defaults } from './ae_events_stores__leads_defaults';
|
||||
import { AE_LEADS_LOC_VERSION } from './store_versions';
|
||||
|
||||
export const leads_loc = new PersistedState('ae_leads_loc', leads_loc_defaults);
|
||||
export const leads_loc = new PersistedState('ae_leads_loc', leads_loc_defaults, {
|
||||
serializer: {
|
||||
// Stamp __version on every write so store_versions.ts's _check_and_wipe() can
|
||||
// detect a breaking schema change and clear stale browsers on next load. Found
|
||||
// 2026-06-16: this was previously bare JSON.stringify with no __version field,
|
||||
// which made _check_and_wipe('ae_leads_loc', ...) see undefined !== expected
|
||||
// every time and wipe this store on EVERY page load. This import also guarantees
|
||||
// store_versions.ts's wipe side-effect runs before this PersistedState reads
|
||||
// from localStorage (ES module execution order).
|
||||
serialize: (value) =>
|
||||
JSON.stringify({ ...value, __version: AE_LEADS_LOC_VERSION }),
|
||||
deserialize: (raw: string) => {
|
||||
const { __version, ...stored } = JSON.parse(raw);
|
||||
return { ...leads_loc_defaults, ...stored };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
export interface LeadsLocState {
|
||||
__version: number;
|
||||
show_details: boolean; // Show extra metadata/details in the leads tab (was events_loc.show_details)
|
||||
show_option__paid_tab: boolean;
|
||||
show_content__scan_alert: boolean;
|
||||
show_content__scan_requirements: boolean;
|
||||
@@ -80,6 +81,7 @@ export interface LeadsSessState {
|
||||
// Persisted leads config — survives browser sessions.
|
||||
export const leads_loc_defaults: LeadsLocState = {
|
||||
__version: 1,
|
||||
show_details: false, // Show extra metadata/details in the leads tab (was events_loc.show_details)
|
||||
show_option__paid_tab: true,
|
||||
show_content__scan_alert: true, // Workaround for QR scanner edge-case bug.
|
||||
show_content__scan_requirements: true,
|
||||
|
||||
@@ -14,5 +14,19 @@
|
||||
*/
|
||||
import { PersistedState } from 'runed';
|
||||
import { pres_mgmt_loc_defaults } from './ae_events_stores__pres_mgmt_defaults';
|
||||
import { AE_PRES_MGMT_LOC_VERSION } from './store_versions';
|
||||
|
||||
export const pres_mgmt_loc = new PersistedState('ae_pres_mgmt_loc', pres_mgmt_loc_defaults);
|
||||
export const pres_mgmt_loc = new PersistedState('ae_pres_mgmt_loc', pres_mgmt_loc_defaults, {
|
||||
serializer: {
|
||||
// Stamp __version on every write so store_versions.ts's _check_and_wipe() can
|
||||
// detect a breaking schema change and clear stale browsers on next load. This
|
||||
// import also guarantees store_versions.ts's wipe side-effect runs before this
|
||||
// PersistedState reads from localStorage (ES module execution order).
|
||||
serialize: (value) =>
|
||||
JSON.stringify({ ...value, __version: AE_PRES_MGMT_LOC_VERSION }),
|
||||
deserialize: (raw: string) => {
|
||||
const { __version, ...stored } = JSON.parse(raw);
|
||||
return { ...pres_mgmt_loc_defaults, ...stored };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Remote config — shape of event.mod_pres_mgmt_json
|
||||
// This is the admin-controlled, server-side config for an event.
|
||||
// lock_config: true forces every field below to sync into local state on load.
|
||||
// This is the admin-controlled, server-side config for an event. Every field
|
||||
// here is synced into every browser unconditionally on event load — there is
|
||||
// no opt-out. (Removed 2026-06-16: a `lock_config` toggle used to gate this;
|
||||
// in practice every event always had it set to true and the conditional
|
||||
// sync it created was an undebuggable source of stale local state — see
|
||||
// PROJECT__AE_Events_PressMgmt_Config_Cleanup.md.)
|
||||
// ---------------------------------------------------------------------------
|
||||
export interface PressMgmtRemoteCfg {
|
||||
// System
|
||||
lock_config: boolean; // true = force all fields below into local state
|
||||
|
||||
// Labels — event-specific terminology overrides
|
||||
label__person_external_id: string | null; // default: 'External ID'
|
||||
label__presenter_external_id: string | null; // default: 'External ID'
|
||||
@@ -54,6 +55,14 @@ export interface PressMgmtRemoteCfg {
|
||||
show__copy_access_link: boolean;
|
||||
show__email_access_link: boolean;
|
||||
show__launcher_link: boolean;
|
||||
// POC column in session list/table views. Independent of hide__session_poc
|
||||
// (the overall "is POC used at all" master switch) — that one wins if set,
|
||||
// see sync_config__event_pres_mgmt() and the session list wrapper usages.
|
||||
show__session_li_poc_field: boolean;
|
||||
// QR code sections (off by default; Trusted Access users can override locally
|
||||
// if the admin has enabled the feature for the event)
|
||||
show__session_qr: boolean;
|
||||
show__presenter_qr: boolean;
|
||||
|
||||
// Requirements
|
||||
require__presenter_agree: boolean;
|
||||
@@ -61,7 +70,6 @@ export interface PressMgmtRemoteCfg {
|
||||
|
||||
// Navigation / UI constraints
|
||||
limit__navigation: boolean;
|
||||
limit__options: boolean;
|
||||
|
||||
// File upload options (null = use system default)
|
||||
file_purpose_option_kv: Record<
|
||||
@@ -76,13 +84,9 @@ export interface PressMgmtRemoteCfg {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Local state — persisted to localStorage via PersistedState ('ae_pres_mgmt_loc')
|
||||
// Contains both user preferences AND fields synced from PressMgmtRemoteCfg.
|
||||
// Fields synced from remote are overwritten on every event load when lock_config=true.
|
||||
// Fields synced from remote are overwritten unconditionally on every event load.
|
||||
// ---------------------------------------------------------------------------
|
||||
export interface PresMgmtLocState {
|
||||
// --- System / lock state (mirrored from remote for display) ---
|
||||
sync_local_config: boolean;
|
||||
lock_config: boolean;
|
||||
|
||||
// --- Query / search preferences ---
|
||||
use_12h: boolean;
|
||||
qry_enabled: 'all' | 'not_enabled' | 'enabled';
|
||||
@@ -149,9 +153,10 @@ export interface PresMgmtLocState {
|
||||
hide__device_code: boolean;
|
||||
hide__locations_msg: boolean;
|
||||
hide__session_li_location_field: boolean;
|
||||
// Also mirrors PressMgmtRemoteCfg.show__session_li_poc_field — synced
|
||||
// unconditionally, see sync_config__event_pres_mgmt().
|
||||
show__session_li_poc_field: boolean;
|
||||
// Launcher/location links in session list
|
||||
hide__launcher_link_legacy: boolean; // Flask/legacy launcher
|
||||
hide__launcher_link: boolean; // SvelteKit launcher
|
||||
hide__location_link: boolean;
|
||||
show__direct_download: boolean;
|
||||
@@ -172,8 +177,8 @@ export interface PresMgmtLocState {
|
||||
location_kv: Record<string, any>;
|
||||
|
||||
// --- Synced from PressMgmtRemoteCfg (overwritten by sync_config__event_pres_mgmt) ---
|
||||
// These mirror PressMgmtRemoteCfg fields and are written during event load.
|
||||
// Do not set these manually — they will be overridden when lock_config=true.
|
||||
// These mirror PressMgmtRemoteCfg fields and are written unconditionally on every
|
||||
// event load. Do not set these manually — they will always be overwritten.
|
||||
label__person_external_id: string;
|
||||
label__presenter_external_id: string;
|
||||
label__session_poc_type: string;
|
||||
@@ -195,10 +200,11 @@ export interface PresMgmtLocState {
|
||||
show__copy_access_link: boolean;
|
||||
show__email_access_link: boolean;
|
||||
show__launcher_link: boolean;
|
||||
show__session_qr: boolean;
|
||||
show__presenter_qr: boolean;
|
||||
require__presenter_agree: boolean;
|
||||
require__session_agree: boolean;
|
||||
limit__navigation: boolean;
|
||||
limit__options: boolean;
|
||||
file_purpose_option_kv: Record<
|
||||
string,
|
||||
{ name: string; disabled?: boolean; hidden?: boolean }
|
||||
@@ -244,6 +250,7 @@ export interface PresMgmtSessState {
|
||||
recent_files: string | null;
|
||||
presenters_agree: string | null;
|
||||
presenters_biography: string | null;
|
||||
file_downloads: string | null;
|
||||
};
|
||||
rpt__session_no_files: boolean;
|
||||
rpt__session_poc_agree: boolean;
|
||||
@@ -258,9 +265,6 @@ export interface PresMgmtSessState {
|
||||
|
||||
// Persisted pres_mgmt local config — survives browser sessions.
|
||||
export const pres_mgmt_loc_defaults: PresMgmtLocState = {
|
||||
// System / lock state
|
||||
sync_local_config: false,
|
||||
lock_config: false,
|
||||
|
||||
// Query / search
|
||||
use_12h: true,
|
||||
@@ -329,7 +333,6 @@ export const pres_mgmt_loc_defaults: PresMgmtLocState = {
|
||||
hide__locations_msg: false,
|
||||
hide__session_li_location_field: false,
|
||||
show__session_li_poc_field: false,
|
||||
hide__launcher_link_legacy: true,
|
||||
hide__launcher_link: true,
|
||||
hide__location_link: true,
|
||||
show__direct_download: false,
|
||||
@@ -371,10 +374,11 @@ export const pres_mgmt_loc_defaults: PresMgmtLocState = {
|
||||
show__copy_access_link: false,
|
||||
show__email_access_link: false,
|
||||
show__launcher_link: false,
|
||||
show__session_qr: false,
|
||||
show__presenter_qr: false,
|
||||
require__presenter_agree: false,
|
||||
require__session_agree: false,
|
||||
limit__navigation: false,
|
||||
limit__options: false,
|
||||
file_purpose_option_kv: null,
|
||||
hide__report_kv: {}
|
||||
};
|
||||
@@ -414,7 +418,8 @@ export const pres_mgmt_sess_defaults: PresMgmtSessState = {
|
||||
status_rpt: {
|
||||
recent_files: null,
|
||||
presenters_agree: null,
|
||||
presenters_biography: null
|
||||
presenters_biography: null,
|
||||
file_downloads: null
|
||||
},
|
||||
rpt__session_no_files: true,
|
||||
rpt__session_poc_agree: false,
|
||||
|
||||
@@ -23,30 +23,12 @@ const idaa_local_data_struct: key_val = {
|
||||
// Timestamp (ms since epoch) when the last successful verification occurred.
|
||||
// Used to cache verification results and avoid repeated Novi API calls.
|
||||
novi_verified_ts: null,
|
||||
// If set to a ms timestamp, verification attempts should be skipped until this time.
|
||||
// Used to honor rate-limits and Retry-After behavior.
|
||||
novi_rate_limited_until: null,
|
||||
// Populated from $ae_loc.site_cfg_json at IDAA layout mount — not managed here.
|
||||
// See routes/idaa/(idaa)/+layout.svelte for the override logic.
|
||||
novi_admin_li: [],
|
||||
novi_trusted_li: [],
|
||||
novi_jitsi_mod_li: [],
|
||||
|
||||
novi_archives_base_url: 'https://www.idaa.org/idaa-archives',
|
||||
novi_bb_base_url: 'https://www.idaa.org/idaa-bulletin-board',
|
||||
novi_meetings_base_url: 'https://www.idaa.org/idaa-meetings',
|
||||
|
||||
ds: {},
|
||||
|
||||
idaa_cfg_json: {},
|
||||
|
||||
// all, disabled, enabled
|
||||
qry__enabled: 'enabled',
|
||||
// all, hidden, not_hidden
|
||||
qry__hidden: 'not_hidden',
|
||||
qry__limit: 20,
|
||||
qry__offset: 0,
|
||||
|
||||
archives: {
|
||||
enabled: 'enabled', // all, disabled, enabled
|
||||
hidden: 'not_hidden', // all, hidden, not_hidden
|
||||
|
||||
149
src/lib/stores/ae_idaa_stores__idaa_loc.svelte.ts
Normal file
149
src/lib/stores/ae_idaa_stores__idaa_loc.svelte.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { PersistedState } from 'runed';
|
||||
|
||||
// ---- Types -------------------------------------------------------------------
|
||||
|
||||
interface IdaaArchivesLoc {
|
||||
enabled: string; // 'all' | 'disabled' | 'enabled'
|
||||
hidden: string; // 'all' | 'hidden' | 'not_hidden'
|
||||
limit: number;
|
||||
offset: number;
|
||||
edit_kv: Record<string, any>; // Tracks which archive objects are being edited.
|
||||
edit__archive_obj: any;
|
||||
edit__archive_content_obj: any;
|
||||
}
|
||||
|
||||
interface IdaaBbLoc {
|
||||
enabled: string; // 'all' | 'disabled' | 'enabled'
|
||||
hidden: string; // 'all' | 'hidden' | 'not_hidden'
|
||||
limit: number;
|
||||
offset: number;
|
||||
edit_kv: Record<string, any>; // Tracks which post objects are being edited.
|
||||
edit__post_obj: any;
|
||||
edit__post_comment_obj: any;
|
||||
show_list__post_obj_li: boolean;
|
||||
qry__enabled: string;
|
||||
qry__hidden: string;
|
||||
qry__limit: number;
|
||||
qry__offset: number;
|
||||
qry__order_by: string; // For the IDB index query
|
||||
qry__order_by_li: Record<string, string>; // For the SQL query
|
||||
}
|
||||
|
||||
interface IdaaRecoveryMeetingsLoc {
|
||||
edit_kv: Record<string, any>; // Tracks which meeting objects are being edited.
|
||||
edit__event_obj: any;
|
||||
qry__enabled: string;
|
||||
qry__hidden: string;
|
||||
qry__limit: number;
|
||||
qry__order_by: string; // For the IDB index query; name, updated_on/created_on
|
||||
qry__order_by_li: Record<string, string>; // For the SQL query
|
||||
qry__offset: number;
|
||||
qry__fulltext_str: string | null;
|
||||
qry__physical: string | null;
|
||||
qry__type: string | null;
|
||||
qry__virtual: string | null;
|
||||
// When true, only show meetings the member has starred. Favorites are stored
|
||||
// server-side in event.mod_meetings_json.favorite (array of Novi UUIDs), so
|
||||
// they persist across browsers without requiring a Novi API write capability.
|
||||
qry__favorites_only: boolean;
|
||||
// Collapse the "Meeting Info" data store panel. Persisted so preference survives reloads.
|
||||
ds_info_collapsed: boolean;
|
||||
}
|
||||
|
||||
export interface IdaaLocState {
|
||||
novi_uuid: string | null;
|
||||
novi_email: string | null;
|
||||
novi_full_name: string | null;
|
||||
// True after a successful Novi API verification (UUID confirmed to be a real Novi member).
|
||||
// False on load, on verification failure, or for non-Novi sign-in paths.
|
||||
novi_verified: boolean;
|
||||
// Timestamp (ms since epoch) when the last successful verification occurred.
|
||||
// Used to cache verification results and avoid repeated Novi API calls.
|
||||
novi_verified_ts: number | null;
|
||||
// Populated from $ae_loc.site_cfg_json at IDAA layout mount — not managed here.
|
||||
// See routes/idaa/(idaa)/+layout.svelte for the override logic.
|
||||
novi_admin_li: string[];
|
||||
novi_trusted_li: string[];
|
||||
novi_jitsi_mod_li: string[];
|
||||
|
||||
archives: IdaaArchivesLoc;
|
||||
bb: IdaaBbLoc;
|
||||
recovery_meetings: IdaaRecoveryMeetingsLoc;
|
||||
}
|
||||
|
||||
// ---- Defaults ----------------------------------------------------------------
|
||||
|
||||
export const idaa_loc_defaults: IdaaLocState = {
|
||||
novi_uuid: null,
|
||||
novi_email: null,
|
||||
novi_full_name: null,
|
||||
novi_verified: false,
|
||||
novi_verified_ts: null,
|
||||
novi_admin_li: [],
|
||||
novi_trusted_li: [],
|
||||
novi_jitsi_mod_li: [],
|
||||
|
||||
archives: {
|
||||
enabled: 'enabled',
|
||||
hidden: 'not_hidden',
|
||||
limit: 150,
|
||||
offset: 0,
|
||||
edit_kv: {},
|
||||
edit__archive_obj: null,
|
||||
edit__archive_content_obj: null
|
||||
},
|
||||
|
||||
bb: {
|
||||
enabled: 'enabled',
|
||||
hidden: 'not_hidden',
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
edit_kv: {},
|
||||
edit__post_obj: null,
|
||||
edit__post_comment_obj: null,
|
||||
show_list__post_obj_li: true,
|
||||
qry__enabled: 'enabled',
|
||||
qry__hidden: 'not_hidden',
|
||||
qry__limit: 25,
|
||||
qry__offset: 0,
|
||||
qry__order_by: 'updated_on',
|
||||
qry__order_by_li: { updated_on: 'DESC', created_on: 'DESC' }
|
||||
},
|
||||
|
||||
recovery_meetings: {
|
||||
edit_kv: {},
|
||||
edit__event_obj: null,
|
||||
qry__enabled: 'enabled',
|
||||
qry__hidden: 'not_hidden',
|
||||
qry__limit: 100,
|
||||
qry__order_by: 'updated_on',
|
||||
qry__order_by_li: {
|
||||
priority: 'DESC',
|
||||
sort: 'DESC',
|
||||
updated_on: 'DESC',
|
||||
created_on: 'DESC',
|
||||
name: 'ASC'
|
||||
},
|
||||
qry__offset: 0,
|
||||
qry__fulltext_str: null,
|
||||
qry__physical: null,
|
||||
qry__type: null,
|
||||
qry__virtual: null,
|
||||
qry__favorites_only: false,
|
||||
ds_info_collapsed: false
|
||||
}
|
||||
};
|
||||
|
||||
// ---- Store -------------------------------------------------------------------
|
||||
|
||||
// Note: the top-level spread in deserialize ensures new top-level fields get
|
||||
// their defaults after a browser upgrade. Nested objects (archives, bb,
|
||||
// recovery_meetings) are replaced wholesale from localStorage — if a new field
|
||||
// is added to a nested object, it will not auto-populate until the user clears
|
||||
// their storage. This is the same trade-off as the events sub-stores.
|
||||
export const idaa_loc = new PersistedState('ae_idaa_loc', idaa_loc_defaults, {
|
||||
serializer: {
|
||||
serialize: JSON.stringify,
|
||||
deserialize: (raw: string) => ({ ...idaa_loc_defaults, ...JSON.parse(raw) })
|
||||
}
|
||||
});
|
||||
@@ -91,8 +91,6 @@ const ae_app_local_data_defaults: key_val = {
|
||||
qry__limit: 20,
|
||||
qry__offset: 0,
|
||||
|
||||
qr_scanner_version: 'one',
|
||||
|
||||
admin: {
|
||||
show_element__sql_qry: false,
|
||||
show_element__sql_qry_results: false
|
||||
@@ -143,7 +141,6 @@ const ae_app_local_data_defaults: key_val = {
|
||||
add_to_use_files_method: 'upload' // upload, select
|
||||
},
|
||||
|
||||
ds: {},
|
||||
hub: {
|
||||
show_element__cfg: true,
|
||||
show_element__cfg_detail: false,
|
||||
@@ -156,56 +153,6 @@ const ae_app_local_data_defaults: key_val = {
|
||||
|
||||
qr: {}
|
||||
},
|
||||
mod: {
|
||||
archives: {},
|
||||
|
||||
events: {
|
||||
event_id: null,
|
||||
|
||||
show_edit__event_presenter_obj: false,
|
||||
show_list__event_presenter_obj_li: true,
|
||||
show_view__event_presenter_obj: false,
|
||||
|
||||
submit_status: null,
|
||||
|
||||
// When set, new presenters are automatically assigned to this session.
|
||||
default_session_id: null
|
||||
},
|
||||
|
||||
journals: {},
|
||||
|
||||
posts: {},
|
||||
|
||||
sponsorships: {
|
||||
cfg_id: null,
|
||||
|
||||
for_type: null,
|
||||
for_id: null,
|
||||
|
||||
// Max complimentary guests per sponsorship tier (0 = no tier).
|
||||
// These are fallback defaults; the active event config should override via events_cfg_json.
|
||||
level_guest_max_li: {
|
||||
0: 0,
|
||||
1: 4,
|
||||
2: 8,
|
||||
3: 8,
|
||||
4: 8,
|
||||
5: 8,
|
||||
6: 16,
|
||||
7: 16
|
||||
},
|
||||
|
||||
show_edit__sponsorship_obj: false,
|
||||
show_list__sponsorship_obj_li: true,
|
||||
show_view__sponsorship_obj: false,
|
||||
|
||||
show_question__accommodations: false,
|
||||
|
||||
submit_status: null // 'saving', 'created', 'updated'
|
||||
}
|
||||
|
||||
// testing: {},
|
||||
}
|
||||
};
|
||||
|
||||
export const ae_loc: Writable<key_val> = persisted(
|
||||
@@ -258,31 +205,6 @@ const ae_app_session_data_defaults: key_val = {
|
||||
clip_complete: null
|
||||
},
|
||||
|
||||
hub: {
|
||||
show_xyz: null,
|
||||
account_id_qry_status: null,
|
||||
event_badge_id_status_qry__search: null,
|
||||
event_presenter_id_qry_status: null,
|
||||
site_domain_id_qry_status: null,
|
||||
sponsorship_id_qry_status: null,
|
||||
sponsorship_cfg_id_qry_status: null,
|
||||
|
||||
qr: {}
|
||||
},
|
||||
mod: {
|
||||
archives: {},
|
||||
events: {},
|
||||
journals: {},
|
||||
posts: {},
|
||||
sponsorships: {
|
||||
disable_submit__sponsorship_obj: false,
|
||||
slct__level_num: 0,
|
||||
show_question__accommodations: false,
|
||||
submit_status: null // 'saving', 'created', 'updated', 'saved'
|
||||
},
|
||||
testing: {}
|
||||
},
|
||||
|
||||
person: {
|
||||
show_report__person_li: false,
|
||||
|
||||
@@ -291,7 +213,6 @@ const ae_app_session_data_defaults: key_val = {
|
||||
|
||||
show__modal_change_password: false,
|
||||
|
||||
download: {},
|
||||
// Per-file progress keyed by file_id (downloads) or temp_id (uploads).
|
||||
// Shape: { status, endpoint, filename, size_total, size_loaded, percent_completed }
|
||||
api_download_kv: {},
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
export const AE_LOC_VERSION = 2; // Bumped 2026-03-30: force-clear stale site_cfg_json (novi_idaa_api_key missing bug)
|
||||
export const AE_EVENTS_LOC_VERSION = 1;
|
||||
export const AE_IDAA_LOC_VERSION = 2; // Bumped 2026-05-18: change default qry__limit from 150 to 100
|
||||
export const AE_PRES_MGMT_LOC_VERSION = 1; // Added 2026-04-02: new standalone PersistedState store
|
||||
export const AE_PRES_MGMT_LOC_VERSION = 2; // Bumped 2026-06-16: added show__session_qr / show__presenter_qr; removed limit__options + hide__launcher_link_legacy (Flask launcher retired)
|
||||
export const AE_BADGES_LOC_VERSION = 1; // Added 2026-04-02: promoted from events_loc.badges
|
||||
export const AE_LEADS_LOC_VERSION = 1; // Added 2026-04-03: promoted from events_loc.leads
|
||||
|
||||
@@ -86,7 +86,7 @@ export const IDB_CONTENT_VERSIONS = {
|
||||
journal_entry: 3 // 2026-05-14: removed content_md_html + history_md_html from properties_to_save
|
||||
},
|
||||
events: {
|
||||
event: 2, // Bumped 2026-05-16: force-clear stale IDB data causing "no meetings found" on IDAA
|
||||
event: 3, // Bumped 2026-06-03: precautionary clear after sort-direction and account_id gate fixes
|
||||
event_session: 1,
|
||||
event_presenter: 1,
|
||||
event_badge: 1,
|
||||
@@ -120,11 +120,16 @@ if (
|
||||
_check_and_wipe('ae_loc', AE_LOC_VERSION);
|
||||
_check_and_wipe('ae_events_loc', AE_EVENTS_LOC_VERSION);
|
||||
_check_and_wipe('ae_idaa_loc', AE_IDAA_LOC_VERSION);
|
||||
// ae_leads_loc (PersistedState, runed) stamps __version itself in its custom
|
||||
// serializer — see ae_events_stores__leads.svelte.ts. FIXED 2026-06-16: this was
|
||||
// previously bare JSON.stringify with no __version field, so this check always saw
|
||||
// undefined !== expected and wiped ae_leads_loc on EVERY page load.
|
||||
_check_and_wipe('ae_leads_loc', AE_LEADS_LOC_VERSION);
|
||||
// ae_pres_mgmt_loc uses PersistedState (runed) which stores raw JSON without a __version
|
||||
// field. The _check_and_wipe mechanism requires __version in the stored data — do NOT
|
||||
// add it here until pres_mgmt_loc_defaults includes __version. For now the key is new
|
||||
// (no stale data exists) so no wipe is needed.
|
||||
// ae_pres_mgmt_loc (PersistedState, runed) stamps __version itself in its custom
|
||||
// serializer — see ae_events_stores__pres_mgmt.svelte.ts. Importing that module runs
|
||||
// this file's side effects first (ES module order), so this check sees real stored
|
||||
// data, not a race against the PersistedState constructor.
|
||||
_check_and_wipe('ae_pres_mgmt_loc', AE_PRES_MGMT_LOC_VERSION);
|
||||
}
|
||||
|
||||
function _check_and_wipe(key: string, expected_version: number): void {
|
||||
|
||||
@@ -12,6 +12,8 @@ import '../app.css';
|
||||
// *** Import other supporting libraries
|
||||
import {
|
||||
RefreshCw,
|
||||
WifiOff,
|
||||
ChevronDown,
|
||||
} from '@lucide/svelte';
|
||||
|
||||
// Highlight JS
|
||||
@@ -408,16 +410,45 @@ $effect(() => {
|
||||
</svelte:head>
|
||||
|
||||
{#if browser && (is_offline || api_unreachable)}
|
||||
<div
|
||||
class="preset-filled-error-200-800 fixed top-0 right-0 left-0 z-100 flex items-center justify-center gap-4 p-4 text-center shadow-2xl print:hidden">
|
||||
<span class="text-xl font-bold"
|
||||
>{is_offline ? 'Offline' : api_error_msg}</span>
|
||||
<button
|
||||
class="btn btn-sm preset-tonal-surface"
|
||||
onclick={() => window.location.reload()}>
|
||||
{#if show_connection_details}
|
||||
<!-- Expanded banner -->
|
||||
<div
|
||||
class="preset-filled-error-200-800 fixed top-0 right-0 left-0 z-100 flex items-center justify-center gap-4 p-4 text-center shadow-2xl print:hidden">
|
||||
<WifiOff size="1.2em" class="shrink-0 opacity-70" />
|
||||
<div class="flex flex-col items-center gap-0.5">
|
||||
<span class="text-xl font-bold leading-tight">
|
||||
{is_offline ? 'Device Offline' : api_error_msg}
|
||||
</span>
|
||||
<span class="text-xs opacity-75">
|
||||
{#if is_offline}
|
||||
Cached event data is still available for search and print · Edits require network
|
||||
{:else}
|
||||
The Aether server could not be reached — check your connection or contact support
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-sm preset-tonal-surface"
|
||||
onclick={() => window.location.reload()}>
|
||||
<RefreshCw size="1em" class="opacity-60" /> Retry
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-sm preset-tonal-surface"
|
||||
title="Collapse"
|
||||
onclick={() => { show_connection_details = false; }}>
|
||||
<ChevronDown size="1em" />
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Collapsed indicator — small chip, top-right corner -->
|
||||
<button
|
||||
class="preset-filled-error-200-800 fixed top-2 right-2 z-100 flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-bold shadow-lg print:hidden"
|
||||
title={is_offline ? 'Device Offline — click to expand' : `${api_error_msg} — click to expand`}
|
||||
onclick={() => { show_connection_details = true; }}>
|
||||
<WifiOff size="0.9em" class="shrink-0" />
|
||||
{is_offline ? 'Offline' : 'API Error'}
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if browser && flag_expired}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user