Compare commits
267 Commits
3553809f27
...
ae_app_3x_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
17b549a75c | ||
|
|
3de01af1a1 | ||
|
|
518a450b91 | ||
|
|
cb767ed115 | ||
|
|
86201f0fc1 | ||
|
|
60e3fc539e | ||
|
|
b3029a4d27 | ||
|
|
ea765d8ad2 | ||
|
|
db5acdd30a | ||
|
|
a000e07647 | ||
|
|
7f9368589a | ||
|
|
55d3d49595 | ||
|
|
f5cf1ef398 | ||
|
|
d5d552a029 | ||
|
|
689bb326cb | ||
|
|
e6db2b4d6a | ||
|
|
cfc5d237c7 | ||
|
|
a56f520d4e | ||
|
|
c0f828ec2c | ||
|
|
5bb06c4904 | ||
|
|
7621e044b4 | ||
|
|
76569a872f | ||
|
|
d9726d062e | ||
|
|
91f40c4a89 | ||
|
|
e63a17865c | ||
|
|
a59e53aec5 | ||
|
|
6042095147 | ||
|
|
932deced12 | ||
|
|
861385b4ff | ||
|
|
42f40e990e | ||
|
|
3ea362c166 | ||
|
|
400312456b | ||
|
|
6755a68b13 | ||
|
|
71e79f032d | ||
|
|
53fd5e7de4 | ||
|
|
14e84884cd | ||
|
|
e921ca973f | ||
|
|
2855e091f7 | ||
|
|
f37c64c68b | ||
|
|
615af58a11 | ||
|
|
76e21b08ff | ||
|
|
4ada5c4a8f | ||
|
|
8850db89c6 | ||
|
|
ccacdc3f4b | ||
|
|
128944c7ab | ||
|
|
c0386f27bc | ||
|
|
ee506832e7 | ||
|
|
bbab9e7c8c | ||
|
|
daf1570781 | ||
|
|
c69e40829f | ||
|
|
429f38996a | ||
|
|
6857f1226c | ||
|
|
bb84117991 | ||
|
|
7a1099bbbe | ||
|
|
3a81887c56 | ||
|
|
730fb19d60 | ||
|
|
b32fb05138 | ||
|
|
12429ccf2e | ||
|
|
2d552b36fd | ||
|
|
3ed1a2a6c4 | ||
|
|
ab9e54d768 | ||
|
|
5bb2df1bd9 | ||
|
|
50c484a4cc | ||
|
|
26fde2a566 | ||
|
|
21a44f96fa | ||
|
|
631a77158c | ||
|
|
1296b1077e | ||
|
|
1fe2f6f4d2 | ||
|
|
73f97ee17b | ||
|
|
ad6b390fd9 | ||
|
|
f297c7c018 | ||
|
|
48a748d314 | ||
|
|
c4e2e64a7e | ||
|
|
76b4adef74 | ||
|
|
95a86b16fa | ||
|
|
042265008c | ||
|
|
a5f2ae3835 | ||
|
|
b3ce65f7f6 | ||
|
|
054775b0f8 | ||
|
|
af28fba263 | ||
|
|
17e522f826 | ||
|
|
324f3a97ac | ||
|
|
530853a78d | ||
|
|
453fcf581d | ||
|
|
530b53aa6d | ||
|
|
cc990084fb | ||
|
|
5fdb0d1d87 | ||
|
|
44cc538ce0 | ||
|
|
978a9a6960 | ||
|
|
82430649db | ||
|
|
cdcec259f7 | ||
|
|
e5883cd53c | ||
|
|
8d5c5e39c9 | ||
|
|
39749c608a | ||
|
|
4923099cfb | ||
|
|
36bd32f172 | ||
|
|
1374f0728e | ||
|
|
c79ae92be0 | ||
|
|
49c6a2351e | ||
|
|
b697126495 | ||
|
|
8dd22912c3 | ||
|
|
f8fe4ac5a2 | ||
|
|
2c1e9d294e | ||
|
|
768fdbfb21 | ||
|
|
e74dc7a388 | ||
|
|
a3f2f17480 | ||
|
|
6c73812187 | ||
|
|
ff824ebbe5 | ||
|
|
422c9c341c | ||
|
|
a3d229c803 | ||
|
|
f72454f379 | ||
|
|
c5c5292715 | ||
|
|
8ed7e0f8d7 | ||
|
|
68e5e01df1 | ||
|
|
611b1e6b51 | ||
|
|
1ef9080cda | ||
|
|
66c0be65c4 | ||
|
|
bdba092de0 | ||
|
|
0fa93d7ee5 | ||
|
|
847d653b5e | ||
|
|
cd01a87143 | ||
|
|
60ecd221b4 | ||
|
|
e3a3ab7de8 |
15
.ae_brief
15
.ae_brief
@@ -1,22 +1,15 @@
|
||||
# Aether Project Brief: aether_app_sveltekit
|
||||
**Last Updated:** 2026-02-09 22:03:56
|
||||
**Last Updated:** 2026-05-21 22:25:05
|
||||
**Current Agent:** mcp_agent
|
||||
|
||||
## 🛠️ What I Just Did
|
||||
Addressed multiple Svelte compiler warnings:
|
||||
1. Converted ~30 decorative labels to spans (a11y).
|
||||
2. Applied Svelte 5 untrack() pattern to initial state from props.
|
||||
3. Fixed CSS scoping for TipTap editor.
|
||||
4. Added rel="noopener noreferrer" to external links.
|
||||
5. Commited changes in two atomic batches.
|
||||
Implemented "Force Sync Location" feature. Optimized file download order with a 4-tier chronological sort (Global > Session > Presentation > Creation Date). Added UI button for onsite operators. Updated project documentation. Verified with npm run check.
|
||||
|
||||
## 🚧 Current Blockers
|
||||
None. Remaining svelte-check warnings (219) require more granular ID/for linking in complex forms.
|
||||
None.
|
||||
|
||||
## ➡️ Exact Next Steps
|
||||
1. Granular fix for remaining 68 label/ID associations in address/person forms.
|
||||
2. Systematic application of untrack() for remaining state-from-props warnings.
|
||||
3. Clean up unused TipTap CSS selectors identified by svelte-check.
|
||||
User to review changes. Ready for onsite testing/deployment.
|
||||
|
||||
---
|
||||
*Generated by ae_brief*
|
||||
|
||||
27
README.md
27
README.md
@@ -163,6 +163,33 @@ npm run deploy:remote:prod
|
||||
- Private runtime variables are passed via the Docker Compose `.env` file in `aether_container_env/`.
|
||||
- **Remote deploy**: `aether_container_env/deploy.sh` handles git pull + Docker build + restart on the server. Triggered via `npm run deploy:remote:*`.
|
||||
|
||||
### Client-Side Cache & IDB Version Management
|
||||
|
||||
The app uses Dexie (IndexedDB) as a local cache for API data (SWR pattern). To prevent
|
||||
stale cached records from persisting across deploys, two version-tracking systems exist
|
||||
in `src/lib/stores/store_versions.ts`:
|
||||
|
||||
**localStorage store versions (`AE_LOC_VERSION`, etc.)**
|
||||
Track the schema of persisted Svelte stores (`ae_loc`, `ae_events_loc`, etc.).
|
||||
Bump when a store's shape changes in a breaking way (field type change, required rename).
|
||||
The check runs synchronously at module import time, before any store hydrates.
|
||||
|
||||
**IDB content versions (`IDB_CONTENT_VERSIONS`)**
|
||||
Track the content shape of Dexie table rows — specifically what `properties_to_save`
|
||||
writes to each table. Bump when `properties_to_save` in an object file changes in a way
|
||||
that makes existing cached rows stale (fields added/removed/renamed, computed field behavior
|
||||
changed). The `check_and_clear_idb_table()` helper reads a localStorage key per table and
|
||||
clears the Dexie table on mismatch. Call it from the module's layout on mount.
|
||||
|
||||
**When to bump `IDB_CONTENT_VERSIONS`:**
|
||||
If you change `properties_to_save` in `ae_events__event.ts` (or any other object file),
|
||||
bump the matching entry here. Failure to do so has historically caused silent "no data"
|
||||
states that are extremely difficult to diagnose — stale rows pass silently, filter to zero,
|
||||
and the error looks identical to a genuinely empty result.
|
||||
|
||||
Currently wired: `events.event` (via `src/routes/idaa/(idaa)/+layout.svelte`).
|
||||
All other tables are defined but not yet wired — see the comment block in `store_versions.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Developing (Local HMR)
|
||||
|
||||
@@ -9,11 +9,15 @@
|
||||
"autofetch",
|
||||
"Axonius",
|
||||
"displayplacer",
|
||||
"elif",
|
||||
"filelist",
|
||||
"gsettings",
|
||||
"onsave"
|
||||
],
|
||||
"git.autofetch": true,
|
||||
"editor.defaultFormatter": "svelte.svelte-vscode"
|
||||
"editor.defaultFormatter": "svelte.svelte-vscode",
|
||||
"chat.tools.terminal.autoApprove": {
|
||||
"npx svelte-check": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,12 +144,30 @@ 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:
|
||||
|
||||
**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
|
||||
@@ -265,76 +285,29 @@ 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.
|
||||
|
||||
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.
|
||||
|
||||
8. **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.
|
||||
|
||||
9. **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.
|
||||
|
||||
10. **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.
|
||||
|
||||
11. **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.
|
||||
|
||||
12. **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.
|
||||
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
|
||||
@@ -366,12 +339,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` |
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
**Client:** International Doctors in Alcoholics Anonymous (IDAA)
|
||||
**Module Path:** `src/routes/idaa/`
|
||||
**State Stores:** `src/lib/stores/ae_idaa_stores.ts`
|
||||
**Last Updated:** 2026-03-09 (Novi UUID verification upgrade)
|
||||
**Last Updated:** 2026-05-18 (Default limit and stepper update)
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
@@ -113,21 +124,22 @@ IDAA members do not log in through Aether — they log in through Novi (idaa.org
|
||||
|
||||
> **Security note (2026-03-09):** The iframe HTML files previously also passed `email` and `full_name`
|
||||
> via URL params. These were unverifiable claims that could be spoofed via URL. They have been removed.
|
||||
> The SvelteKit layout now fetches verified identity directly from the Novi API.
|
||||
> The SvelteKit layout now verifies identity via the Aether server-side Novi proxy — the Novi API
|
||||
> call originates from the server, not the member's browser.
|
||||
> See "Iframe Integration" → "Novi UUID Verification Flow" below.
|
||||
|
||||
### Verification Flow (`(idaa)/+layout.svelte`)
|
||||
|
||||
When a `uuid` param is present in the URL, the layout performs an **async Novi API call** to verify:
|
||||
When a `uuid` param is present in the URL, the layout performs an **async call to the Aether server-side endpoint** (`GET /v3/action/idaa/novi_member/{uuid}`), which proxies to Novi server-to-server:
|
||||
|
||||
1. The UUID actually exists in Novi's system (prevents fake/crafted UUIDs)
|
||||
2. Gets verified name and email directly from Novi — these can't be forged via URL
|
||||
2. Gets verified name and email — these can't be forged via URL
|
||||
3. Sets `$idaa_loc.novi_uuid`, `$idaa_loc.novi_email`, `$idaa_loc.novi_full_name`
|
||||
4. Sets `$idaa_loc.novi_verified = true` on success
|
||||
|
||||
A `novi_verifying` UI state prevents the "Access Denied" screen from flashing during the API round-trip.
|
||||
|
||||
**All or nothing:** If the Novi API key is not configured, or the verification call fails, access is denied. There is no URL-param fallback.
|
||||
**All or nothing:** If the Novi API key is not configured on the site, or the verification call fails, access is denied. There is no URL-param fallback.
|
||||
|
||||
**Required `site_cfg_json` fields:**
|
||||
```json
|
||||
@@ -148,22 +160,26 @@ This section documents the exact way Aether uses the Novi API for the IDAA integ
|
||||
|
||||
- **All-or-nothing policy:** If the Novi API key is not configured or the verification call fails, the Novi-based access path is denied. The layout explicitly prevents child routes from rendering while verification is in-flight to avoid flashing "Access Denied".
|
||||
|
||||
- **Rate limits (Novi API):** 20 calls/second · 600 calls/minute · 100,000 calls/day. The layout handles 429 responses with a 10-second flat backoff and one retry. If the retry also returns 429, access is denied and a "Reload / Retry" button is shown. The 5-minute TTL cache on successful verification prevents repeated calls during normal use.
|
||||
- **Rate limits (Novi API):** 20 calls/second · 600 calls/minute · 100,000 calls/day. The Aether backend handles 429 responses; the frontend receives a `429` and retries once after 10 seconds. The 12-hour TTL cache on successful verification (Redis server-side + `$idaa_loc` client-side) prevents repeated calls during normal use. A `503` (Novi unreachable) is auto-retried once after 3 seconds before surfacing an error to the user.
|
||||
|
||||
### Verification Flow (implementation)
|
||||
|
||||
1. The IDAA iframe loads Aether pages with a `?uuid=<uuid>&iframe=true` param.
|
||||
2. When the `uuid` param is present the IDAA layout performs an authenticated GET against the Novi customers endpoint:
|
||||
2. When the `uuid` param is present the IDAA layout calls the Aether server-side proxy:
|
||||
|
||||
```js
|
||||
// simplified
|
||||
fetch(`${api_root_url}/customers/${uuid}`, {
|
||||
fetch(`${aether_api_url}/v3/action/idaa/novi_member/${uuid}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': `Basic ${api_key}` }
|
||||
headers: {
|
||||
'x-aether-api-key': api_key,
|
||||
'x-account-id': account_id
|
||||
}
|
||||
})
|
||||
// Aether calls Novi server-to-server; member's browser IP is never in the Novi call path.
|
||||
```
|
||||
|
||||
3. On success the layout uses the returned JSON to build a display name and normalized email, then writes these values to the IDAA store and marks verification success.
|
||||
3. On success (`200`), the layout reads `data.full_name` and `data.email` from the response and writes them to the IDAA store, marking verification success.
|
||||
|
||||
4. The layout then determines a target Novi permission level (`authenticated`, `trusted`, `administrator`) by checking configured UUID lists (`novi_trusted_li`, `novi_admin_li`) and upgrades the Aether session only if the Novi-derived level is higher than the current global level.
|
||||
|
||||
@@ -171,9 +187,9 @@ fetch(`${api_root_url}/customers/${uuid}`, {
|
||||
|
||||
### Key `site_cfg_json` fields and where they are used
|
||||
|
||||
- **`novi_idaa_api_key`**: Base64-encoded Basic auth token provided by Novi. Required for the verification request. Accessed in code as `$ae_loc.site_cfg_json.novi_idaa_api_key` and passed in the `Authorization: Basic <key>` header. If missing, Novi-based access is denied.
|
||||
- **`novi_idaa_api_key`**: Base64-encoded Basic auth token provided by Novi. Used by the Aether **server** to authenticate against Novi — the frontend never touches the key itself. The frontend checks only for its *presence* in `site_cfg_json` as a guard meaning "IDAA is configured for this site". If missing, Novi-based access is denied.
|
||||
|
||||
- **`novi_api_root_url`**: Optional API root (defaults to `https://www.idaa.org/api`). Used to form the verification URL.
|
||||
- **`novi_api_root_url`**: Optional Novi API root (defaults to `https://www.idaa.org/api`). Read by the Aether server, not the frontend.
|
||||
|
||||
- **`novi_admin_li`**: Array of UUIDs treated as administrators for IDAA. Merged into `$idaa_loc.novi_admin_li` during layout initialization and used to set `administrator` level.
|
||||
|
||||
@@ -214,12 +230,32 @@ These fields are read elsewhere in the IDAA UI to enable flows for verified user
|
||||
### Security notes and operational guidance
|
||||
|
||||
- The previous implementation leaked `email` and `full_name` via URL params — this was removed because those values are unauthenticated and can be spoofed.
|
||||
- The API key is sensitive — keep it only in site config and do not expose it in client-side code or public repositories.
|
||||
- The verification request uses Basic auth with the provided `novi_idaa_api_key` (already Base64-encoded by Novi) — treat the token like a password.
|
||||
- If Novi changes their customer API shape, update the layout parsing (display name/email normalization) and this documentation.
|
||||
- The API key is sensitive — keep it only in site `cfg_json` and do not expose it in client-side code or public repositories. The key is read and used exclusively by the Aether backend; it is never sent to the browser.
|
||||
- If Novi changes their customer API shape, update `app/methods/idaa_novi_verify_methods.py` in the backend (display name/email normalization) and this documentation.
|
||||
|
||||
If you need a compact checklist for re-creating this flow in another integration, ask and I will add a small runbook with exact request/response field mappings.
|
||||
|
||||
### ~~Planned: Server-Side Novi Verification~~ ✅ Implemented (2026-05-19)
|
||||
|
||||
**Problem solved:** The previous client-side Novi API call originated from the member's browser.
|
||||
Hotel/conference WiFi, VPNs, corporate/hospital networks, and Cloudflare IP reputation filtering
|
||||
could block these calls and produce false "Access Denied" for legitimate members.
|
||||
|
||||
**Solution implemented:** A FastAPI endpoint proxies the Novi call server-to-server
|
||||
(Aether → Novi), with Redis caching. Members' browser IPs are no longer in the call path.
|
||||
|
||||
**Endpoint:** `GET /v3/action/idaa/novi_member/{uuid}`
|
||||
- Standard Aether auth headers (`x-aether-api-key`, `x-account-id`)
|
||||
- Server reads `novi_idaa_api_key` / `novi_api_root_url` from site `cfg_json`
|
||||
- Redis cache: `idaa:novi_member:{uuid}` — 4-hour TTL, only 200s cached
|
||||
- `404` results never cached (recently-joined members not incorrectly denied)
|
||||
|
||||
**Frontend:** `verify_novi_uuid()` in `(idaa)/+layout.svelte` now calls this endpoint with
|
||||
standard Aether headers. The `novi_idaa_api_key` is still checked for presence in
|
||||
`site_cfg_json` as a proxy for "is IDAA configured for this site" (server holds the key itself).
|
||||
|
||||
**Full API spec:** `GUIDE__AE_API_V3_for_Frontend.md` §12.
|
||||
|
||||
### Permission Levels (Ascending)
|
||||
| Level | Condition | Access |
|
||||
|---|---|---|
|
||||
@@ -286,7 +322,12 @@ This ensures that OSIT staff with `super` or `manager` roles retain full access
|
||||
|
||||
### Access Gate (`(idaa)/+layout.svelte`)
|
||||
The inner layout blocks ALL rendering if the user is not authorized:
|
||||
- `novi_verifying = true` → "Verifying identity..." spinner
|
||||
- `novi_verifying = true` → "Verifying identity..." spinner (message updates during retry)
|
||||
- `verify_error_type === 'rate_limited'` → yellow "Identity Verification Unavailable" panel with:
|
||||
- **Try Again** — calls `handle_verify_retry()` (respects retry_count, waits 10 s before re-calling Novi)
|
||||
- **Clear Cache & Reload** — clears IDB + localStorage + sessionStorage, then reloads
|
||||
- **Full Reset** — same clear but also navigates to `/` with `invalidateAll`
|
||||
- `verify_error_type === 'api_error'` → same yellow panel (API returned non-2xx, not a rate limit)
|
||||
- Verification failed or no UUID → "Access Denied" error page
|
||||
- Access check runs before any child routes render
|
||||
|
||||
@@ -376,27 +417,87 @@ Recovery Meetings reuses the Aether Events object to represent AA recovery meeti
|
||||
|
||||
### Search Filters
|
||||
Members can filter meetings by:
|
||||
- **Fulltext search** — name, location
|
||||
- **Physical** — in-person meetings
|
||||
- **Virtual** — online meetings (Zoom, Google Meet, etc.)
|
||||
- **Meeting type** — specific meeting format categories
|
||||
- **Fulltext search** — name, location, day of week, contacts (debounced 250ms; uses SWR pattern)
|
||||
- **Virtual** — online meetings (Zoom, Jitsi, other)
|
||||
- **In-person** — physical location meetings
|
||||
- **Meeting type** — IDAA / Caduceus / Family Recovery
|
||||
- **My Meetings** — star toggle; shows only meetings the member has starred (favorites)
|
||||
|
||||
Search is debounced (250ms) and uses the standard Aether SWR pattern.
|
||||
**Sort options:** Last Updated (default), Meeting Name A–Z, Meeting Name Z–A.
|
||||
|
||||
**Empty state behavior:**
|
||||
- Zero results with active filters → "No meetings found for these filters" + "Clear all filters" button
|
||||
- Zero results with no filters → bare message shown, then after 8s a "Refresh Meeting Cache" escape hatch appears (clears IDB and re-fetches from API — indicates a stale-cache problem, not a real empty set)
|
||||
|
||||
Search uses the standard Aether SWR pattern (IDB cache returned immediately, then API refreshes in background).
|
||||
|
||||
### Search Architecture — What Is and Isn't Searched
|
||||
|
||||
The fulltext search runs against the `default_qry_str` field (backend-computed, contains:
|
||||
`id_random`, type, name, description, timezone, recurring pattern/text, location text).
|
||||
The fulltext search runs against the `default_qry_str` field (backend-computed STORED GENERATED
|
||||
column, contains: `id_random`, type, name, description, timezone, recurring pattern/text,
|
||||
location text, **contact name and email**).
|
||||
|
||||
**Contact names and emails are NOT currently searchable.** The `contact_li_json` field is a
|
||||
JSON longtext — MariaDB cannot efficiently substring-search it directly. The backend already
|
||||
has a `contact_li_json_ext` (STORED GENERATED, indexed) column to work around this, but it
|
||||
has not yet been added to the searchable fields whitelist in the API.
|
||||
**Contact names and emails ARE searchable via the API path.** `default_qry_str` includes
|
||||
contact data, so the API `lk_qry` LIKE search on that field covers contacts automatically.
|
||||
|
||||
**Pending fix (tracked in TODO__Agents.md, 2026-04-08):**
|
||||
- Backend: add `contact_li_json_ext` to the event object searchable fields whitelist
|
||||
- Frontend: add `contact_li_json_ext` as an OR condition in `search__event()`, and update
|
||||
the local IDB fast-path filter to parse `contact_li_json` for instant cache results
|
||||
**IDB fast-path gap:** The local cache (Dexie) fast-path returns all cached meetings without
|
||||
text filtering — users see the unfiltered list immediately, then the API result (with contacts
|
||||
filtered) replaces it after the background refresh completes. The IDB path does not parse
|
||||
`contact_li_json` for instant local text matching.
|
||||
|
||||
**Known history (2026-05-19):** Contact search appeared broken due to two issues now resolved:
|
||||
1. The backend STORED GENERATED columns (`default_qry_str`, `contact_li_json_ext`) had stale
|
||||
values; forced a rebuild via fake updates on each event record.
|
||||
2. The recovery meetings page secondary filter was re-running text matching against response
|
||||
fields — silently dropping results that matched only via `default_qry_str` (e.g. by contact
|
||||
name, since that field may not appear in the response body). Fix: removed text re-filtering
|
||||
from the secondary filter (type / physical / virtual OR-logic only).
|
||||
|
||||
**Remaining enhancement (tracked in TODO__Agents.md):**
|
||||
- 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:
|
||||
- On each card in the meeting list (`ae_idaa_comp__event_obj_li.svelte`)
|
||||
- On the meeting detail page nav bar (`[event_id]/+page.svelte`)
|
||||
|
||||
Favorites are stored in the `data_store` table (code: `idaa_meetings_favorites`, scoped to the
|
||||
IDAA account). The record's `json` field holds `{ [novi_uuid]: [event_id, ...] }` — one shared
|
||||
record per account containing all members' favorites. This means:
|
||||
- Favorites persist across browsers and devices (server-side)
|
||||
- Does **not** write to `ae_event` rows (avoiding the `ON UPDATE current_timestamp()` side effect)
|
||||
- Known last-write-wins race condition if two members toggle simultaneously — acceptable for ~1000 members
|
||||
- Pre-created DB records: ID 150 (`gaTKSVPagFj`, account_id=1, dev/demo), ID 151 (`knJh8zhyKT0`, account_id=13, live IDAA)
|
||||
|
||||
The star button uses inline styles (not `.btn`) to avoid Bootstrap v3 box-model overrides in the iframe.
|
||||
|
||||
### Edit Form — Sections and Key Fields
|
||||
|
||||
@@ -604,18 +705,36 @@ Stores Novi auth context and per-submodule query settings:
|
||||
novi_jitsi_mod_li: string[] // Jitsi moderator UUIDs
|
||||
|
||||
archives: { enabled, hidden, limit, offset, edit__archive_obj, edit__archive_content_obj }
|
||||
bb: { enabled, hidden, limit, offset, edit__post_obj, edit__post_comment_obj }
|
||||
recovery_meetings: { qry__fulltext_str, qry__physical, qry__virtual, qry__type, qry__limit, edit__event_obj }
|
||||
bb: { enabled, hidden, limit, offset, edit__post_obj, edit__post_comment_obj,
|
||||
qry__enabled, qry__hidden, qry__limit, qry__offset, qry__order_by, qry__order_by_li }
|
||||
recovery_meetings: {
|
||||
qry__enabled, qry__hidden, qry__limit, qry__offset,
|
||||
qry__fulltext_str, qry__physical, qry__virtual, qry__type,
|
||||
qry__order_by, qry__order_by_li,
|
||||
qry__favorites_only, // true = show only starred meetings (My Meetings filter)
|
||||
edit__event_obj // null or event_id string when edit form is open
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `idaa_sess` (sessionStorage — cleared on tab close)
|
||||
### `idaa_sess` (in-memory only — resets on page load)
|
||||
UI state per submodule:
|
||||
```typescript
|
||||
{
|
||||
archives: { qry__status, show__modal_edit__archive_id, show__modal_view__archive_id, obj_changed }
|
||||
bb: { qry__status, show__modal_edit__post_id, show__modal_view__post_id, obj_changed }
|
||||
recovery_meetings: { qry__status, show__modal_edit, show__modal_view, attend_platform, obj_changed }
|
||||
archives: { qry__status, show__modal_edit__archive_id, show__modal_view__archive_id,
|
||||
show__modal_edit__archive_content_id, show__modal_view__archive_content_id, obj_changed }
|
||||
bb: { qry__status, edit__post_obj, show__inline_edit__post_obj, show__modal_edit__post_id,
|
||||
show__modal_view__post_id, obj_changed }
|
||||
recovery_meetings: {
|
||||
qry__status, // null | 'loading' | 'done' | 'error'
|
||||
qry__fulltext_str, // session-only copy (separate from persisted loc copy)
|
||||
search_version, // incremented to trigger a new search cycle
|
||||
edit__event_obj, // null | event_id — controls edit form visibility
|
||||
show__modal_edit, show__modal_view,
|
||||
show__modal_edit__event_id, show__modal_view__event_id,
|
||||
attend_platform, // 'Zoom' | 'Jitsi' | null — platform selected in virtual attend section
|
||||
obj_changed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -780,4 +899,4 @@ ae_loc.idaa_loc = { novi_uuid: 'test-uuid-value', ... };
|
||||
---
|
||||
|
||||
**Document Status:** ✅ Current
|
||||
**Last Verified:** 2026-05-06 — added Module 5: Jitsi Reports (grouped view, UUID exclusion, known-meeting whitelist, UUID-based unique counts); fixed route tree (`jitsi_reports/` is inside `(idaa)/`)
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
@@ -638,6 +700,61 @@ const url = URL.createObjectURL(blob);
|
||||
|
||||
---
|
||||
|
||||
## 12. IDAA: Server-Side Novi Member Verification
|
||||
|
||||
Verifies a Novi AMS member UUID by proxying the Novi API call through the Aether backend. This eliminates false "Access Denied" failures for members on hotel/conference WiFi, VPNs, and Cloudflare-filtered networks — the Novi call originates from the server's IP, not the member's browser IP.
|
||||
|
||||
- **Method:** `GET`
|
||||
- **Path:** `/v3/action/idaa/novi_member/{uuid}`
|
||||
- **Auth:** Standard V3 (`x-aether-api-key` + `x-account-id` or `?jwt=`)
|
||||
|
||||
### Request
|
||||
|
||||
| Parameter | Location | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `uuid` | Path | Yes | Novi member UUID (from Novi AMS) |
|
||||
|
||||
### Response on success (`200 OK`)
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"verified": true,
|
||||
"full_name": "Alice S.",
|
||||
"email": "alice+member@idaa.org"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `full_name`: `"{FirstName} {LastName[0]}."` format. Falls back to the Novi `Name` field if first/last are absent.
|
||||
- `email`: Novi `Email` field with space → `+` normalization applied (Novi quirk — `alice member@idaa.org` → `alice+member@idaa.org`).
|
||||
|
||||
### Error responses
|
||||
|
||||
| Status | Meaning | Frontend action |
|
||||
|---|---|---|
|
||||
| `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 | Surface as `'api_error'`; advise retry |
|
||||
|
||||
### Migration from direct Novi call
|
||||
|
||||
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 state |
|
||||
|---|---|---|
|
||||
| `200` with identity data | `200` | `verified` |
|
||||
| `200` with no identity data | `404` | `denied` |
|
||||
| `404` | `404` | `denied` |
|
||||
| `429` | `429` | `'rate_limited'` |
|
||||
| Network error / Novi 5xx | `503` | `'api_error'` |
|
||||
|
||||
### Caching
|
||||
|
||||
Verified results are cached in Redis (`idaa:novi_member:{uuid}`, 4-hour TTL). `404` results are **never** cached so recently-joined members are not incorrectly denied on their next attempt.
|
||||
|
||||
---
|
||||
|
||||
## 11. Troubleshooting 403 Forbidden
|
||||
|
||||
If you receive a 403 on a valid ID:
|
||||
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
138
documentation/GUIDE__AE_Events_Onsite_Runbook.md
Normal file
138
documentation/GUIDE__AE_Events_Onsite_Runbook.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# 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.
|
||||
|
||||
---
|
||||
|
||||
## Badge Printing
|
||||
|
||||
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**.
|
||||
- **Mode:** Use normal browser sessions (not Incognito) to allow PWA caching.
|
||||
|
||||
### Printer Reference: Zebra ZC10L (PVC)
|
||||
- **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.
|
||||
2. **Review:** Open the print page and confirm the layout looks correct.
|
||||
3. **Print:** Click **Print Badge**. `print_count` increments automatically.
|
||||
4. **Handoff:** Verify the card print quality before handing it to the attendee.
|
||||
|
||||
---
|
||||
|
||||
## Exhibitor Leads (Lead Retrieval)
|
||||
|
||||
Exhibitors use a PWA (Progressive Web App) to scan badges and capture leads.
|
||||
|
||||
### Exhibitor Support Workflow
|
||||
1. **Booth Lookup:** Help the exhibitor find their booth in the Leads landing page.
|
||||
2. **Sign-In:** Assist with the **Shared Passcode** or individual **Licensed User** login.
|
||||
3. **App Install:** Encourage them to "Add to Home Screen" (iOS) or click the Install button (Android/Chrome) for offline stability.
|
||||
4. **Scanning Demo:** Show them the **Rapid Scan** mode. Remind them that attendees must have `allow_tracking = true` on their record to be scanned.
|
||||
|
||||
### Managing Licenses
|
||||
- License counts are managed in the **Manage** tab (Admin or Shared Passcode only).
|
||||
- If an exhibitor needs more staff slots, update the `license_max` in the Exhibit record.
|
||||
|
||||
---
|
||||
|
||||
## Speaker Ready Room (SRR)
|
||||
... (rest of the file) ...
|
||||
The SRR is the central hub for content management and presenter support.
|
||||
|
||||
### SRR Practice Stations
|
||||
Stations mirror the session room setup exactly:
|
||||
- Same Mac laptop model and adapter/dongle configuration as the podiums.
|
||||
- Projector and screen (where possible).
|
||||
- Launcher running in **Native** mode — ensures verification matches the podium experience.
|
||||
|
||||
### Staffing Roles
|
||||
|
||||
| Role | Access Level | Typical Tasks |
|
||||
|---|---|---|
|
||||
| **OSIT Staff** | `trusted_access` | Manage devices, monitor via VNC, deep troubleshooting. |
|
||||
| **Client Staff** | `authenticated_access` | Upload files, view session lists, assist presenters. |
|
||||
| **Presenter** | `authenticated_access` | Self-upload via QR link (if enabled). |
|
||||
|
||||
### SRR Workflow — Day-of-Show
|
||||
1. **Check-in:** Staff looks up the presenter's session in Presentation Management.
|
||||
2. **Upload:** File is uploaded to the presenter/session record.
|
||||
3. **Verification:** Staff opens the file on a practice station to confirm rendering.
|
||||
4. **Launcher Sync:** File propagates to the podium. Use **Force Sync Location** in the Launcher config if immediate full-room caching is needed.
|
||||
5. **Proceed:** Presenter walks to the room; the podium kiosk already has the file cached.
|
||||
|
||||
---
|
||||
|
||||
## Onsite Operation (Managing Parallel Rooms)
|
||||
|
||||
### SRR Overview Page
|
||||
The Pres Mgmt overview (`/events/[id]/pres_mgmt`) is the "Command Center":
|
||||
- Monitor file status per session.
|
||||
- Filter by location and time block to stay ahead of active sessions.
|
||||
|
||||
### Per-Room Monitoring
|
||||
- Use **VNC or RustDesk** to monitor all podium screens in real time from the SRR.
|
||||
- Confirm "Native Sync" status chip in the bottom-left of the Launcher is green/idle before sessions start.
|
||||
|
||||
### Session Transitions
|
||||
- **Timing:** Ideally, sessions show/hide based on `datetime_start`.
|
||||
- **Manual Control:** In looser schedules, use Launcher controls to manually select the current session.
|
||||
|
||||
---
|
||||
|
||||
## Pre-Show Checklist
|
||||
|
||||
### 1–2 Weeks Before
|
||||
- [ ] Event created with correct dates and timezone.
|
||||
- [ ] `mod_pres_mgmt_json` configured for client needs.
|
||||
- [ ] Locations (rooms) created and named.
|
||||
- [ ] Sessions created, assigned to locations, and timed.
|
||||
- [ ] Launcher devices (`event_device`) registered with correct codes.
|
||||
- [ ] Device-to-location assignments confirmed.
|
||||
|
||||
### Day Before (SRR Setup)
|
||||
- [ ] Mac laptops at podiums booted; Electron app running.
|
||||
- [ ] Each podium confirms it loaded the correct room's Launcher.
|
||||
- [ ] SRR practice stations confirmed (matching hardware).
|
||||
- [ ] Run **Force Sync Location** on all podiums to pre-cache all day-1 content.
|
||||
- [ ] VNC/RustDesk connections established to all podiums.
|
||||
|
||||
### Day of Show
|
||||
- [ ] Confirm all session times are accurate before the first block.
|
||||
- [ ] Monitor SRR queue and verify every file on a practice station.
|
||||
- [ ] Check VNC wall to ensure all podiums are online and synced.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| Session not in Launcher | Datetime wrong or Location unassigned. | Verify session metadata in Pres Mgmt. |
|
||||
| File uploaded but missing | Polling lag or attached at wrong level. | Wait 30s; check if file is at Session vs Presenter level. |
|
||||
| File opens slowly | Not in native cache yet. | Check "Native Sync" chip; use Force Sync in config. |
|
||||
| File won't open | Corrupt upload or missing Mac codec. | Test on SRR station; convert or re-upload. |
|
||||
| Drifted schedule | Room timing shifted. | Use Launcher controls to manually select the active session. |
|
||||
| `lock_config` resets changes | Remote config is forced. | Edit the master `mod_pres_mgmt_json` in Event Settings. |
|
||||
| Move laptop to new room | Hardware reassignment. | Update `location_id` in `event_device` record; restart Electron. |
|
||||
@@ -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,7 +132,100 @@ $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.
|
||||
|
||||
## Practical Patterns from Aether (Journals & Events)
|
||||
## 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.
|
||||
|
||||
@@ -98,6 +234,12 @@ $effect(() => {
|
||||
- Provide `initial_session_obj` from `+page.ts` as a first-draw fallback to child components.
|
||||
- Use `$derived.by(() => liveQuery(...))` for presentation lists so the observable instance is stable across renders and recreated only when `event_session_id` or `search` changes.
|
||||
|
||||
- Search pages with persisted filters or saved query text should keep the auto-search trigger in a page-level `$effect`, but the duplicate guard should live inside the actual search executor. That preserves the first page-load search while blocking repeated identical reruns from localStorage-backed rerenders. In practice:
|
||||
- derive a single `qry_key` from the search inputs
|
||||
- debounce in the `$effect`
|
||||
- compare `qry_key` against a `last_executed_key` inside `handle_search_refresh()`
|
||||
- keep transient loading flags and trigger counters in session state when the value is only used to force a refresh, not as a persisted preference
|
||||
|
||||
Example (presentation list pattern):
|
||||
```typescript
|
||||
let lq__event_presentation_obj_li = $derived(
|
||||
@@ -114,6 +256,7 @@ let lq__event_presentation_obj_li = $derived(
|
||||
- Add a small `console.log` inside each `liveQuery` closure to confirm when it runs and what `id` it sees.
|
||||
- Verify that `+page.ts` either `await`s critical loads or returns `initial_*` payloads for first-render hydration.
|
||||
- Confirm that dependent store values (selected IDs) are assigned before components subscribe — use `untrack` to prevent extra reactive cycles.
|
||||
- If a search page stops auto-loading after a localStorage change, check whether the duplicate guard was placed in the `$effect` instead of the executor. Guarding too early can suppress the initial search; guard at execution time instead.
|
||||
- If a broad Dexie-backed list shows fewer rows than a narrower filter, look for a limit or revalidation step overwriting the local IDB result set. Broad views should stay unbounded unless the user is actually narrowing by text.
|
||||
- Ensure your `liveQuery` closures return quickly and do not throw; any exception inside the query can stop updates.
|
||||
- If a dependent query appears stale, temporarily add `await 0` in the upstream query or an explicit `Promise.resolve()` after the IDB write to force the microtask queue to flush during debugging.
|
||||
@@ -237,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
|
||||
|
||||
@@ -273,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:
|
||||
|
||||
@@ -357,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
|
||||
|
||||
@@ -421,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
|
||||
|
||||
@@ -180,7 +182,7 @@ The `layout` field encodes physical badge stock dimensions. Standard codes to us
|
||||
| --- | --- | --- | --- |
|
||||
| `badge_4x5_fanfold` | 4" × 5" (101.6 × 127mm) | `badge_layout_epson_4x5_fanfold.css` | Epson ColorWorks C3500 / ExpoBadge fanfold — preferred for general conference use (ISHLT, demos) |
|
||||
| `badge_3.5x5.5_pvc` | 3.5" × 5.5" (88.9 × 139.7mm) | `badge_layout_zebra_zc10l_pvc.css` | PVC card, Zebra ZC10L — single-sided, set `duplex=0` |
|
||||
| `badge_4x6_fanfold` | 4" × 6" (101.6 × 152.4mm) | *(none — Tailwind defaults)* | Generic fanfold fallback; dimensions match the hardcoded Tailwind values |
|
||||
| `badge_4x6_fanfold` | 4" × 6" (101.6 × 152.4mm) | `badge_layout_epson_4x6_fanfold.css` | Single-sided fanfold; Axonius Adapt 2026 (June 2026). Lanyard hole: 5/8in × 1/8in, centered, 1/4in from top. |
|
||||
| `badge_4x6_fanfold_tickets` | 4" × 6" + tear-offs | *(pending)* | Fanfold with ticket stubs |
|
||||
|
||||
Layout CSS files live in `src/lib/ae_events/badges/css/` and are imported by
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
@@ -359,6 +483,9 @@ Firefox users can use "Save to PDF" directly — it just works.
|
||||
- [x] Wire `style_href` via `<svelte:head>` in print page — done in `print/+page.svelte`; also in `properties_to_save`. (2026-03-18 verified)
|
||||
- [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.
|
||||
- [ ] Make `layout` field drive actual card dimensions in the badge component — currently the Zebra ZC10L layout CSS (`badge_layout_zebra_zc10l_pvc.css`) sets dimensions correctly via `[data-layout="..."]` scoping, but fanfold layouts still use Tailwind defaults. Needs proper CSS for each layout code.
|
||||
- [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,843 +1,150 @@
|
||||
# MODULE: Aether Events — Badges
|
||||
# Aether Events — Badges
|
||||
|
||||
**Module Path:** `src/routes/events/[event_id]/(badges)/badges/`
|
||||
**API Module:** `src/lib/ae_events/ae_events__event_badge.ts`
|
||||
**Database:** `db_events.badge` (Dexie IndexedDB table)
|
||||
**Last Updated:** 2026-02-27 (rev 6)
|
||||
**Related Docs:** `documentation/PROJECT__AE_Events_Badges_Review_Print.md` (implementation guide)
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
## Data Model & Hierarchy
|
||||
|
||||
The Badges module manages event attendee badges with support for:
|
||||
- **External system imports as needed** (CSV/Excel, iMIS, Zoom, Novi, Impexium, Confex, Cvent, and others)
|
||||
- **Field override protection** to prevent staff/attendee edits from being overwritten by automated syncs
|
||||
- **Multi-tier access control** for field editing
|
||||
- **QR code generation** for badge scanning
|
||||
- **Print tracking** (count, first/last print datetime)
|
||||
- **Advanced search and filtering**
|
||||
- **HTML rendering** in display fields for rich text formatting
|
||||
- **Accessibility features** (text enlargement toggle)
|
||||
### Core Objects
|
||||
- **Event Badge** (`event_badge`): The attendee record containing name, title, affiliations, and tracking flags.
|
||||
- **Badge Template** (`event_badge_template`): The visual and structural configuration for printing (branding, layout, QR placement).
|
||||
|
||||
### Relationships
|
||||
- **Badge → Event:** Many-to-one.
|
||||
- **Badge → Template:** Many-to-one (via `event_badge_template_id`).
|
||||
- **Badge → Person:** Optional link to core Aether Person record for unified profiles.
|
||||
|
||||
---
|
||||
|
||||
## Critical Design Pattern: Override Fields
|
||||
|
||||
### Purpose
|
||||
The `*_override` fields pattern protects data from being overwritten during scheduled cron syncs from external systems. This is essential because:
|
||||
1. Staff may need to correct imported data
|
||||
2. Attendees may be allowed to self-update certain fields (e.g., preferred name, pronouns)
|
||||
3. External systems often have outdated or incorrect data
|
||||
4. Changes should persist across multiple sync cycles
|
||||
The `*_override` fields pattern (established in 2018) protects data from being overwritten during scheduled cron syncs from external systems (iMIS, Novi, etc.). This ensures that staff corrections or attendee self-updates persist across multiple sync cycles.
|
||||
|
||||
### How It Works
|
||||
1. **Import:** External systems populate **REGULAR** fields only.
|
||||
2. **Display Logic:** The UI displays the `*_override` field if it has a value; otherwise, it falls back to the regular field.
|
||||
3. **HTML Rendering:** Certain display fields (Name, Title, Affiliations, Location) support HTML markup for rich text formatting (bold, italics, line breaks) on the physical badge.
|
||||
|
||||
**Import Behavior:**
|
||||
```
|
||||
External System → Aether API → Populates REGULAR fields only
|
||||
(never touches *_override fields)
|
||||
```
|
||||
### Standard Override Pairs
|
||||
|
||||
**Display Behavior:**
|
||||
```
|
||||
UI Display Logic:
|
||||
1. IF `*_override` field has value → USE IT (highest priority)
|
||||
2. ELSE IF regular field has value → USE IT (fallback)
|
||||
3. ELSE → Display placeholder/empty
|
||||
```
|
||||
|
||||
**HTML Rendering (implemented 2026-02-27):**
|
||||
|
||||
Certain fields support HTML markup for rich text formatting. When viewing (not editing), these fields use Svelte's `{@html}` directive to render the markup:
|
||||
- `full_name` / `full_name_override`
|
||||
- `professional_title` / `professional_title_override`
|
||||
- `affiliations` / `affiliations_override`
|
||||
- `location` / `location_override`
|
||||
|
||||
This allows for formatting like:
|
||||
- Bold/italic: `<b>Dr.</b> Jane Smith` or `<i>Chief Medical Officer</i>`
|
||||
- Line breaks: `Hospital Name<br>Department Name`
|
||||
- Special characters and entities
|
||||
|
||||
**Example — Full Name:**
|
||||
```typescript
|
||||
// API imports from iMIS
|
||||
badge.given_name = "Robert"
|
||||
badge.family_name = "Smith"
|
||||
badge.full_name = "Robert Smith" // Auto-computed
|
||||
|
||||
// Staff edits to preferred name with HTML
|
||||
badge.full_name_override = "<b>Bob</b> Smith"
|
||||
|
||||
// Display in UI (review form)
|
||||
{@html badge.full_name_override || badge.full_name || "— no name —"}
|
||||
// Result: **Bob** Smith (bold rendered)
|
||||
|
||||
// Edit mode shows raw HTML
|
||||
<input bind:value={editable_full_name_override} />
|
||||
// Shows: <b>Bob</b> Smith (editable as text)
|
||||
|
||||
// Next cron sync from iMIS
|
||||
// ✅ badge.full_name updated to "Robert J. Smith" (middle initial added)
|
||||
// ✅ badge.full_name_override remains "<b>Bob</b> Smith" (PROTECTED)
|
||||
// ✅ Display still shows **Bob** Smith (bold rendered)
|
||||
```
|
||||
|
||||
### Override Fields
|
||||
|
||||
| Regular Field | Override Field | Purpose | Editable By | HTML Rendering |
|
||||
|---|---|---|---|---|
|
||||
| `pronouns` | `pronouns_override` | Preferred pronouns | Staff, Attendee | No |
|
||||
| `professional_title` | `professional_title_override` | Job title display | Staff, Attendee | ✅ Yes |
|
||||
| `full_name` | `full_name_override` | Preferred name display | Staff, Attendee | ✅ Yes |
|
||||
| `affiliations` | `affiliations_override` | Organization display | Staff, Attendee | ✅ Yes |
|
||||
| `phone` | `phone_override` | Phone number | Staff, Attendee | No |
|
||||
| `email` | `email_override` | Contact email override | Staff only | No |
|
||||
| `location` | `location_override` | City/State/Country display | Staff, Attendee | ✅ Yes |
|
||||
| `badge_type` | `badge_type_override` | Badge category label text | Staff only | No |
|
||||
| `badge_type_code` | `badge_type_code_override` | Badge access level code | Staff only | No |
|
||||
| `registration_type` | `registration_type_override` | Registration category label text | Staff only | No |
|
||||
| `registration_type_code` | `registration_type_code_override` | Registration category code | Staff only | No |
|
||||
|
||||
> **Note:** `phone`, `phone_override`, `pronouns_override`, `registration_type`, `registration_type_code`, `registration_type_override`, `registration_type_code_override` may need to be confirmed against the DB schema via `ae_describe event_badge` and added to `properties_to_save` in `ae_events__event_badge.ts` if not already present.
|
||||
|
||||
### Sync Safety Rules
|
||||
|
||||
**Automated Sync (Cron Jobs):**
|
||||
- ✅ CAN update: All regular fields (`given_name`, `family_name`, `email`, `affiliations`, etc.)
|
||||
- ❌ CANNOT update: Any `*_override` field
|
||||
- ❌ CANNOT delete: Any `*_override` value
|
||||
|
||||
**Manual Staff Edit:**
|
||||
- ✅ CAN update: Any field (including overrides)
|
||||
- ✅ CAN clear: Override fields (reverts to regular field)
|
||||
|
||||
**Attendee Self-Service Edit:**
|
||||
- ✅ CAN update: Only specific override fields (per event config)
|
||||
- ✅ CAN clear: Their own override fields
|
||||
- ❌ CANNOT edit: Regular fields, badge_type, email_override
|
||||
| Regular Field | Override Field | Editable By | HTML? |
|
||||
|---|---|---|---|
|
||||
| `full_name` | `full_name_override` | Staff, Attendee | ✅ |
|
||||
| `professional_title` | `professional_title_override` | Staff, Attendee | ✅ |
|
||||
| `affiliations` | `affiliations_override` | Staff, Attendee | ✅ |
|
||||
| `location` | `location_override` | Staff, Attendee | ✅ |
|
||||
| `email` | `email_override` | Staff Only | No |
|
||||
| `badge_type` | `badge_type_override` | Staff Only | No |
|
||||
|
||||
---
|
||||
|
||||
## External System Integration
|
||||
|
||||
### Supported Import Sources
|
||||
- **iMIS** (Association Management)
|
||||
- **Zoom** (Virtual event registration)
|
||||
- **Novi AMS** (Association Management)
|
||||
- **Impexium** (Association Management)
|
||||
- **Confex** (Event abstract management)
|
||||
- **Cvent** (Event registration)
|
||||
- **Custom CSV/Excel** imports
|
||||
Aether acts as a **Pull-Only** consumer for registration data. It does not push changes back to external systems, maintaining them as the source of truth for base registration while Aether handles the "Onsite Truth."
|
||||
|
||||
### Data Flow Direction
|
||||
```
|
||||
External Systems ─────────> Aether
|
||||
(READ ONLY) (WRITE + DISPLAY)
|
||||
```
|
||||
|
||||
**Important:** Aether is **pull-only** — does not push changes back to external systems. This prevents sync conflicts and maintains external systems as the source of truth for base data.
|
||||
|
||||
### Sync Behavior
|
||||
- **Frequency:** Scheduled cron jobs (typically hourly, daily, or on-demand)
|
||||
- **Method:** Full sync or incremental (depends on external system API)
|
||||
- **Conflict Resolution:** Override fields always win
|
||||
|
||||
**Pseudocode:**
|
||||
```python
|
||||
def sync_badge_from_external(external_badge_data, existing_badge):
|
||||
# Update regular fields from external source
|
||||
existing_badge.given_name = external_badge_data.first_name
|
||||
existing_badge.family_name = external_badge_data.last_name
|
||||
existing_badge.email = external_badge_data.email
|
||||
existing_badge.affiliations = external_badge_data.organization
|
||||
existing_badge.badge_type_code = external_badge_data.registration_type
|
||||
|
||||
# NEVER TOUCH OVERRIDE FIELDS
|
||||
# existing_badge.full_name_override ← PROTECTED
|
||||
# existing_badge.affiliations_override ← PROTECTED
|
||||
# existing_badge.email_override ← PROTECTED
|
||||
|
||||
return existing_badge
|
||||
```
|
||||
### Supported Sources
|
||||
- **iMIS**, **Novi AMS**, **Impexium** (Associations)
|
||||
- **Zoom**, **Cvent** (Registrations)
|
||||
- **Confex** (Abstracts/Presenters)
|
||||
- **Custom CSV/Excel**
|
||||
|
||||
---
|
||||
|
||||
## Access Control & Edit Permissions
|
||||
## Access Control & Permissions
|
||||
|
||||
### Access Levels (Ascending)
|
||||
1. **Anonymous** — No access to badges
|
||||
2. **Public** — View public event info only (no badge access)
|
||||
3. **Authenticated** — View own badge, limited self-edit
|
||||
4. **Trusted** — Search all badges, view all, edit own
|
||||
5. **Administrator** — Full CRUD, bulk operations, override any field
|
||||
6. **Manager** — All administrator + event configuration
|
||||
7. **Super** — All manager + cross-event operations
|
||||
| Level | Access |
|
||||
|---|---|
|
||||
| **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. |
|
||||
|
||||
### Current Implementation (v3) — 2026-02-27
|
||||
### Attendee Self-Service (`/review`)
|
||||
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.
|
||||
|
||||
#### Badge Search Results Visibility
|
||||
### 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.
|
||||
|
||||
| Access Level | Sees |
|
||||
| --- | --- |
|
||||
| Below Trusted (incl. anonymous) | Only badges where `print_count < 1` and not hidden |
|
||||
| Trusted, not Edit Mode | Only badges where `print_count < 1` and not hidden |
|
||||
| Trusted + Edit Mode | All badges where `hide === false` (including already-printed) |
|
||||
|
||||
#### Print Button Behavior (per result row)
|
||||
|
||||
| Access Level | Print Action |
|
||||
| --- | --- |
|
||||
| Below Trusted | No print action — name shown with User icon, non-interactive |
|
||||
| Trusted, `print_count < 1` | Clickable link → `/print` page, Printer icon |
|
||||
| Trusted, `print_count >= 1`, not Edit Mode | Disabled (already printed safety lock), shows `Nx` count |
|
||||
| Trusted, `print_count >= 1`, Edit Mode | Clickable reprint — shows `Nx` count badge next to icon |
|
||||
|
||||
Print count displayed as `[Printer][2×] Name` when `print_count >= 1`.
|
||||
|
||||
#### Review Area Buttons (per result row, up to 3 buttons total)
|
||||
|
||||
| Button | Visible To | Behavior |
|
||||
| --- | --- | --- |
|
||||
| Email Review Link | All users | Placeholder `alert()` — will trigger email API |
|
||||
| Review Link (clipboard) | Trusted + Edit Mode only | Copies `/review` URL to clipboard; shows `Copied!` feedback |
|
||||
| *(direct Review link)* | *(future)* | *(not yet implemented as separate nav button)* |
|
||||
|
||||
#### Badge Edit Form (`ae_comp__badge_obj_view.svelte`)
|
||||
|
||||
**Currently editable fields (local `edit_mode_active`, not global `edit_mode`):**
|
||||
```typescript
|
||||
editable_full_name_override: string | null
|
||||
editable_professional_title_override: string | null
|
||||
editable_affiliations_override: string | null // textarea
|
||||
editable_location_override: string | null
|
||||
editable_allow_tracking: boolean | null
|
||||
editable_email: string | null
|
||||
editable_badge_type_code: string | null
|
||||
```
|
||||
|
||||
- Save button → `handle_save_changes()` — only changed fields sent to API
|
||||
- Cancel button → `handle_cancel_changes()` — reverts to IDB values
|
||||
- **IMPORTANT:** This component must NEVER write to `$ae_loc.edit_mode` — it uses its own local `edit_mode_active` flag only. (Bug fixed 2026-02-27)
|
||||
|
||||
#### Badge Review Form (`ae_comp__badge_review_form.svelte`)
|
||||
|
||||
Form-based review (NOT a badge render). Used by the `/review` page.
|
||||
|
||||
**Props:**
|
||||
- `can_edit_fields: string[]` prop controls which fields are editable per user level
|
||||
- `['*']` = administrator (all fields)
|
||||
- `is_staff: boolean` prop shows/hides the staff-only fields
|
||||
- Fields show "(overridden)" label when an override value differs from the base field
|
||||
|
||||
**Features (implemented 2026-02-27):**
|
||||
- **HTML Rendering**: `full_name_override`, `professional_title_override`, `affiliations_override`, and `location_override` fields render HTML markup using `{@html}` directive when viewing (not when editing)
|
||||
- **Accessibility**: Text enlargement toggle button switches between text-2xl (normal) and text-4xl (enlarged) for improved readability
|
||||
- **Help Modal**: Flowbite Modal component with 6 help sections (Reviewing Badge, Editing Info, Accessibility, QR Code, Lead Scanning, Assistance)
|
||||
- **QR Code Display**: Generates QR code using `core_func.js_generate_qr_code()` with badge ID, supports hover zoom and click-to-expand
|
||||
- **Print Status**: Shows print count, first print datetime, and last print datetime at top of form
|
||||
- **Local Edit Mode**: Independent `local_edit_active` state (never writes to `$ae_loc.edit_mode`)
|
||||
- **Save/Cancel**: Only changed fields sent to API; revert button for override fields
|
||||
|
||||
**Editable Fields:**
|
||||
- Pronouns, Full Name, Professional Title, Affiliations, Phone, Location (all with override support)
|
||||
- Allow Tracking checkbox (lead scanning permission)
|
||||
- Staff-only: Email, Badge Type, Registration Type, Hide, Priority, Notes
|
||||
- Staff-only: Options (`other_1_code` through `other_8_code`) and Tickets (`ticket_1_code` through `ticket_8_code`)
|
||||
- Agree to Terms & Conditions checkbox (attendee-visible when in can_edit_fields)
|
||||
|
||||
#### Badge Review Page — Header Buttons (implemented 2026-02-27)
|
||||
|
||||
| Button | Visible To | Behavior |
|
||||
| --- | --- | --- |
|
||||
| Back → Search (ArrowLeft) | Staff (`has_staff_access`) only | `<a href="/events/{id}/badges">` |
|
||||
| Print (Printer icon) | Trusted+, not printed OR Trusted+Edit if printed | `<a href="/print">`, shows `Nx` count if reprinting |
|
||||
| Copy Link (clipboard) | Trusted + Edit Mode only | Copies review URL to clipboard; `Copied!` feedback for 2s |
|
||||
| Email Link (Mail icon) | All if not printed; Trusted+Edit if printed | Placeholder `alert()` — email API pending |
|
||||
|
||||
#### Badge Print Page — Header Buttons (implemented 2026-02-27)
|
||||
|
||||
| Button | Visible To | Behavior |
|
||||
| --- | --- | --- |
|
||||
| Back → Search (ArrowLeft) | Always (when badge loaded) | `<a href="/events/{id}/badges">` |
|
||||
| Print Now (Printer icon) | Trusted+, not printed OR Trusted+Edit if printed | Calls `window.print()` directly (convenience duplicate); print count tracked by component button |
|
||||
| Review (Eye icon) | Trusted + Edit Mode only | `<a href="/review">` nav link |
|
||||
| Email Link (Mail icon) | All if not printed; Trusted+Edit if printed | Placeholder `alert()` — email API pending |
|
||||
|
||||
#### Badge Review Page — Display Sections (implemented 2026-02-27)
|
||||
|
||||
The review form (`ae_comp__badge_review_form.svelte`) displays:
|
||||
|
||||
1. **Print status** ✅ — print count + first/last print timestamps (read-only, hidden if never printed)
|
||||
2. **QR Code** ✅ — the attendee's badge QR code for scanning at the badge kiosk (for automatic badge search + print flow). Generated using `core_func.js_generate_qr_code()` with `obj_type: 'event_badge'` and badge ID. Supports hover zoom overlay and click-to-expand.
|
||||
3. **Editable Fields** ✅ — all fields with access-level gating, override support, and HTML rendering for display fields
|
||||
4. **Options** ✅ (`other_1_code` through `other_8_code`) — Staff: editable text inputs; Attendees: shown as `[✓] Option X` checkmark display only when value exists
|
||||
5. **Tickets** ✅ (`ticket_1_code` through `ticket_8_code`) — Staff: editable text inputs; Attendees: shown as `[✓] Ticket X` checkmark display only when value exists
|
||||
6. **Accessibility Toggle** ✅ — Font size enlargement button in sticky header (text-2xl ↔ text-4xl)
|
||||
7. **Help Modal** ✅ — Attendee guidance modal with 6 sections explaining the review process, editing, QR codes, and lead scanning
|
||||
|
||||
#### Default Field Permissions (hardcoded for now — Axonius first show, mid-April 2026)
|
||||
|
||||
These are hardcoded in `review/+page.svelte` pending connection to `mod_badges_json.edit_permissions`.
|
||||
|
||||
**Attendee (passcode-authenticated / anonymous with link):**
|
||||
```typescript
|
||||
[
|
||||
'pronouns_override',
|
||||
'full_name_override',
|
||||
'professional_title_override',
|
||||
'affiliations_override',
|
||||
'phone_override',
|
||||
'location_override',
|
||||
'allow_tracking', // Exhibitor Leads opt-in
|
||||
'agree_to_tc', // Terms & Conditions placeholder
|
||||
]
|
||||
```
|
||||
|
||||
**Trusted Staff and above:**
|
||||
```typescript
|
||||
[
|
||||
'pronouns_override',
|
||||
'full_name_override',
|
||||
'professional_title_override',
|
||||
'affiliations_override',
|
||||
'email_override',
|
||||
'phone_override',
|
||||
'location_override',
|
||||
'badge_type_code_override', // + badge_type_override (text label)
|
||||
'registration_type_code_override', // + registration_type_override (text label)
|
||||
'option_1' ... 'option_8', // i.e. other_1_code ... other_8_code
|
||||
'ticket_1_code' ... 'ticket_8_code',
|
||||
'allow_tracking',
|
||||
'agree_to_tc',
|
||||
'hide',
|
||||
'priority',
|
||||
'notes',
|
||||
]
|
||||
```
|
||||
|
||||
**Administrator** — `can_edit_fields = ['*']` (all fields)
|
||||
|
||||
**Badge type options (hardcoded for now):** `member`, `non-member`, `guest`, `exhibitor`, `staff`, `test`
|
||||
(In future: read from Event Badge Template's configured list)
|
||||
|
||||
**Registration type options:** Same list as badge type for now — identical select options.
|
||||
|
||||
#### Future: Per-Event Configuration
|
||||
|
||||
`event.mod_badges_json.edit_permissions` — placeholder settings UI exists in
|
||||
`ae_comp__event_settings_badges_form.svelte`. Review page uses hardcoded defaults for now.
|
||||
The settings form and review page are not yet connected.
|
||||
|
||||
```json
|
||||
{
|
||||
"authenticated": {
|
||||
"can_edit": ["pronouns_override", "full_name_override", "professional_title_override", "affiliations_override", "phone_override", "location_override", "allow_tracking", "agree_to_tc"]
|
||||
},
|
||||
"trusted": {
|
||||
"can_edit": ["*attendee_fields", "email_override", "badge_type_code_override", "registration_type_code_override", "option_x", "ticket_x_code", "allow_tracking", "agree_to_tc", "hide", "priority", "notes"]
|
||||
}
|
||||
}
|
||||
```
|
||||
### 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`.
|
||||
|
||||
---
|
||||
|
||||
## Search & Filter Capabilities
|
||||
|
||||
### Search Component
|
||||
**File:** `ae_comp__badge_search.svelte`
|
||||
- **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 (Trusted + Edit Mode):** Badge Type, Printed Status, Affiliations, Sort Order.
|
||||
|
||||
### Multi-Word Search Fix (2026-02-26)
|
||||
Fulltext search now correctly handles multi-word queries by splitting on whitespace and applying AND logic per word:
|
||||
```typescript
|
||||
// "scott idem" → LIKE '%scott%' AND LIKE '%idem%'
|
||||
// Previously: LIKE '%scott idem%' (failed to match)
|
||||
const words = qry.split(/\s+/).filter(w => w.length > 0);
|
||||
for (const word of words) {
|
||||
search_query.and.push({ field: 'default_qry_str', op: 'like', value: `%${word}%` });
|
||||
}
|
||||
```
|
||||
**Committed:** dc0f3066
|
||||
### Visibility Filter (Trusted + Edit Mode)
|
||||
|
||||
### Available Filters
|
||||
Three-option select controlling which records are shown:
|
||||
|
||||
**Fulltext Search** (All Users)
|
||||
- Searches: `default_qry_str` database field
|
||||
- Includes: Name, email, external IDs
|
||||
- Type: `LIKE %query%` (case-insensitive)
|
||||
- Trigger: Enter key or 3+ characters typed
|
||||
| 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 |
|
||||
|
||||
**Advanced Filters** (Trusted Access & Above)
|
||||
```typescript
|
||||
// Badge Type Filter
|
||||
badge_type_code: 'current_member' | 'inactive_member' | 'ex_all' | 'staff' | etc.
|
||||
// Note: Badge types are defined per Event and Event Badge Template in database table records.
|
||||
// Common types include: member, nonmember, guest, exhibitor, staff
|
||||
// This is a work in progress - types vary by event configuration.
|
||||
### Result Limit Stepper (Edit Mode)
|
||||
|
||||
// Print Status Filter
|
||||
qry_printed_status: 'all' | 'printed' | 'not_printed'
|
||||
Controls the maximum number of results returned. Only visible in edit mode.
|
||||
|
||||
// Affiliations Search
|
||||
qry_affiliations: string // Separate filter for organization search
|
||||
| Access Level | Range | Step |
|
||||
| --- | --- | --- |
|
||||
| Below Trusted | Fixed 25 | — |
|
||||
| Trusted | 25 – 250 | 25 |
|
||||
| Manager+ | 25 – 2550 | 25 up to 250, then 100 |
|
||||
|
||||
// Sort Options
|
||||
qry_sort_order:
|
||||
- 'name_asc' / 'name_desc'
|
||||
- 'updated_desc' / 'updated_asc'
|
||||
- 'print_count_desc'
|
||||
- 'print_first_desc' / 'print_last_desc'
|
||||
- 'badge_type_asc'
|
||||
- 'affiliations_asc'
|
||||
```
|
||||
### Badge Type Filter — Known Limitation
|
||||
|
||||
### QR Scan Search
|
||||
- Scans badge QR code
|
||||
- Extracts badge ID
|
||||
- Auto-fills search with ID
|
||||
- Jumps to badge detail view
|
||||
|
||||
### Search Implementation Pattern
|
||||
**File:** `badges/+page.svelte` (Lines 117-365)
|
||||
|
||||
**Strategy:** Standardized Reactive Search Pattern (Aether UI V3)
|
||||
1. **Isolate dependencies** into stable `$derived` object
|
||||
2. **Debounced effect** (300ms) triggers search
|
||||
3. **Fast Path:** Search IDB first (if not `remote_first`)
|
||||
4. **Revalidate:** API request updates IDB
|
||||
5. **LiveQuery:** UI auto-updates from IDB changes
|
||||
|
||||
**Search API:** `events_func.search__event_badge()`
|
||||
```typescript
|
||||
await search__event_badge({
|
||||
api_cfg: $ae_api,
|
||||
event_id: event_id,
|
||||
fulltext_search_qry_str: qry_str || null,
|
||||
type_code: type_code || null,
|
||||
printed_status: printed_status,
|
||||
affiliations_qry_str: aff_str || null,
|
||||
order_by_li: order_by_li,
|
||||
limit: 150,
|
||||
log_lvl: 0
|
||||
})
|
||||
```
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Badge Display Logic
|
||||
## Print Rendering and Tracking
|
||||
|
||||
### Name Display Priority
|
||||
```typescript
|
||||
// Component: ae_comp__badge_obj_li.svelte (Lines 113-121)
|
||||
if (event_badge_obj?.full_name_override)
|
||||
display: full_name_override
|
||||
else if (event_badge_obj?.full_name)
|
||||
display: full_name
|
||||
else
|
||||
display: given_name + ' ' + family_name
|
||||
```
|
||||
- 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.
|
||||
|
||||
### Badge View Page
|
||||
**Route:** `/events/[event_id]/badges/[badge_id]`
|
||||
Aether tracks the lifecycle of every physical badge to prevent unauthorized reprints and monitor kiosk activity.
|
||||
|
||||
**Components:**
|
||||
- `+page.svelte` — Container with LiveQuery for badge data
|
||||
- `ae_comp__badge_obj_view.svelte` — Full badge display + edit UI
|
||||
| Field | Purpose |
|
||||
|---|---|
|
||||
| `print_count` | Increments on every "Print Badge" action. |
|
||||
| `print_first_datetime` | Timestamp of the very first print. |
|
||||
| `print_last_datetime` | Timestamp of the most recent print. |
|
||||
|
||||
**LiveQueries:**
|
||||
```typescript
|
||||
lq__event_badge_obj = liveQuery(() => db_events.badge.get(event_badge_id))
|
||||
lq__event_badge_template_obj = liveQuery(() =>
|
||||
db_events.badge_template.get(badge.event_badge_template_id)
|
||||
)
|
||||
```
|
||||
|
||||
**Loading States:**
|
||||
- `is_loading_idb` — Waiting for initial IDB lookup
|
||||
- If badge not found → "Badge Not Found" error with reload button
|
||||
- Loader spinner while fetching
|
||||
> **Operational Note:** Reprints triggered via the Edit Mode shortcut do not increment the count; only the formal "Print Badge" workflow does.
|
||||
|
||||
---
|
||||
|
||||
## Badge Templates
|
||||
|
||||
### Purpose
|
||||
Badge templates define the visual layout and content structure for printed badges:
|
||||
- Header images/logos
|
||||
- Field positions and font sizes
|
||||
- QR code placement
|
||||
- Ticket/option indicator display
|
||||
- WiFi credentials display
|
||||
|
||||
### Template Selection
|
||||
Each badge references an `event_badge_template_id`. The template controls:
|
||||
- Layout (front/back)
|
||||
- Branding elements
|
||||
- Which fields to show
|
||||
- Field formatting rules
|
||||
|
||||
### Template Loading
|
||||
Templates are loaded alongside badges via `inc_template` parameter:
|
||||
```typescript
|
||||
load_ae_obj_id__event_badge({
|
||||
event_badge_id: badge_id,
|
||||
inc_template: true // Also loads template
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Print Tracking
|
||||
|
||||
### Print Fields
|
||||
```typescript
|
||||
print_count: number // Increments each print
|
||||
print_first_datetime: string // ISO datetime of first print
|
||||
print_last_datetime: string // ISO datetime of most recent print
|
||||
```
|
||||
|
||||
### Print Button (Implemented 2026-02-26)
|
||||
The `handle_print_badge()` function in `ae_comp__badge_obj_view.svelte` increments the count and records timestamps:
|
||||
```typescript
|
||||
async function handle_print_badge() {
|
||||
const now = new Date().toISOString();
|
||||
const current_print_count = $lq__event_badge_obj.print_count ?? 0;
|
||||
const data_to_update = {
|
||||
print_count: current_print_count + 1,
|
||||
print_last_datetime: now
|
||||
};
|
||||
if (current_print_count === 0) {
|
||||
data_to_update.print_first_datetime = now; // Only set on first print
|
||||
}
|
||||
await events_func.update_ae_obj__event_badge({ ... });
|
||||
}
|
||||
```
|
||||
|
||||
Button has `data-testid="badge-print-btn"` and shows loading/done/error states with icon feedback.
|
||||
|
||||
### Print Workflow
|
||||
1. **Pre-Print:** Badge print page (`/print`) shows "Already printed N times" warning in screen-only header if `print_count >= 1`
|
||||
2. **Record:** `handle_print_badge()` updates `print_count`, `print_last_datetime`, and `print_first_datetime` (first print only) via API before printing
|
||||
3. **Print:** `window.print()` — standard browser print dialog, wired and working (2026-02-27)
|
||||
4. **Redirect:** After 1 second, `goto(/events/{id}/badges)` returns to search
|
||||
5. **Audit:** `print_first_datetime` and `print_last_datetime` visible in Edit Mode debug row
|
||||
|
||||
**Browser vs Electron:** Badge printing does NOT require the Electron native app. The standard browser print dialog works well across Chrome, Chromium, and Firefox. The Electron native app is specialized for the **Events Pres Mgmt Launcher only** and should not be assumed available for badge stations.
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### IndexedDB Table: `badge`
|
||||
**File:** `src/lib/ae_events/db_events.ts` (Lines 841-852)
|
||||
|
||||
**Indexed Fields:**
|
||||
```typescript
|
||||
badge: `
|
||||
event_badge_id, id,
|
||||
event_id,
|
||||
full_name, full_name_override, email, email_override,
|
||||
affiliations, affiliations_override,
|
||||
badge_type, badge_type_code, badge_type_code_override, badge_type_override,
|
||||
external_event_id, external_id, external_person_id,
|
||||
default_qry_str,
|
||||
alert,
|
||||
tmp_sort_1, tmp_sort_2,
|
||||
print_count, print_first_datetime, print_last_datetime,
|
||||
enable, hide, priority, sort, group, notes, created_on, updated_on
|
||||
`
|
||||
```
|
||||
|
||||
### Saved Properties
|
||||
**File:** `ae_events__event_badge.ts` (Lines 495-563)
|
||||
|
||||
**Complete field list** (67 fields total):
|
||||
- Identity: `id`, `event_badge_id`, `event_id`, `event_badge_template_id`
|
||||
- Name: `pronouns`, `informal_name`, `title_names`, `given_name`, `middle_name`, `family_name`, `designations`
|
||||
- Professional: `professional_title`, `professional_title_override`
|
||||
- Display: `full_name`, `full_name_override`
|
||||
- Organization: `affiliations`, `affiliations_override`
|
||||
- Contact: `email`, `email_override`
|
||||
- Address: `address_line_1`, `address_line_2`, `address_line_3`, `city`, `country_subdivision_code`, `state_province`, `state_province_abb`, `postal_code`, `country_alpha_2_code`, `country`, `full_address`
|
||||
- Location: `location`, `location_override`
|
||||
- Classification: `badge_type`, `badge_type_code`, `badge_type_override`, `badge_type_code_override`
|
||||
- External: `external_event_id`, `external_id`, `external_person_id`
|
||||
- Search: `query_str`, `default_qry_str`
|
||||
- System: `alert`, `enable`, `hide`, `priority`, `sort`, `group`, `notes`, `created_on`, `updated_on`
|
||||
- Print: `print_count`, `print_first_datetime`, `print_last_datetime`
|
||||
- Sorting: `tmp_sort_1`, `tmp_sort_2`
|
||||
- Person Link: `person_external_id`, `person_external_sys_id`, `person_given_name`, `person_family_name`, `person_full_name`, `person_professional_title`, `person_affiliations`, `person_primary_email`, `person_passcode`
|
||||
|
||||
---
|
||||
|
||||
## API Functions
|
||||
|
||||
### CRUD Operations
|
||||
**File:** `src/lib/ae_events/ae_events__event_badge.ts`
|
||||
|
||||
```typescript
|
||||
// Load single badge
|
||||
load_ae_obj_id__event_badge({ event_badge_id, event_id, inc_template })
|
||||
|
||||
// Load badge list
|
||||
load_ae_obj_li__event_badge({ event_id, view, limit, order_by_li })
|
||||
|
||||
// Search badges (V3 API)
|
||||
search__event_badge({
|
||||
event_id,
|
||||
fulltext_search_qry_str,
|
||||
type_code,
|
||||
printed_status,
|
||||
affiliations_qry_str,
|
||||
order_by_li
|
||||
})
|
||||
|
||||
// Create badge
|
||||
create_ae_obj__event_badge({ event_id, data_kv })
|
||||
|
||||
// Update badge
|
||||
update_ae_obj__event_badge({ event_badge_id, event_id, data_kv })
|
||||
|
||||
// Delete badge
|
||||
delete_ae_obj_id__event_badge({ event_badge_id, event_id, method })
|
||||
```
|
||||
|
||||
### Field Processing
|
||||
**Function:** `process_ae_obj__event_badge_props()`
|
||||
|
||||
**Processing Steps:**
|
||||
1. Map `*_random` fields to clean names (`event_badge_id_random` → `event_badge_id`)
|
||||
2. Set primary `id` field from `event_badge_id`
|
||||
3. Ensure `event_id` is set (from function parameter if missing)
|
||||
4. Calculate `tmp_sort_1` and `tmp_sort_2` for efficient sorting
|
||||
5. Return processed objects
|
||||
|
||||
**Critical Fix (2026-02-26):** All CRUD functions now return **processed** data (matches IDB cache) instead of raw API responses. This ensures consistency between function return values and cached data.
|
||||
|
||||
---
|
||||
|
||||
## Component Architecture
|
||||
|
||||
### Route Structure
|
||||
```
|
||||
/events/[event_id]/(badges)/badges/
|
||||
├── +layout.svelte # Layout wrapper (minimal)
|
||||
├── +page.svelte # Badge list + search
|
||||
├── ae_comp__badge_search.svelte # Search form + filters
|
||||
├── ae_comp__badge_obj_li.svelte # Badge list display (results)
|
||||
├── ae_comp__badge_create_form.svelte # (Not actively used)
|
||||
├── ae_comp__badge_upload_form.svelte # Bulk CSV upload
|
||||
└── [badge_id]/
|
||||
├── ae_comp__badge_obj_view.svelte # Badge rendering + staff edit + print button
|
||||
├── ae_comp__badge_review_form.svelte # Form-based field review/edit (attendee + staff)
|
||||
├── print/
|
||||
│ ├── +page.ts # Non-blocking badge loader (inc_template: true)
|
||||
│ └── +page.svelte # Print-focused page — screen header + badge render
|
||||
└── review/
|
||||
├── +page.ts # Non-blocking badge loader (inc_template: false)
|
||||
└── +page.svelte # Passcode-gated review page
|
||||
```
|
||||
|
||||
> **Note:** The old `[badge_id]/+page.svelte` placeholder was removed (2026-02-27). The name link in the search results list now goes directly to `/print`.
|
||||
|
||||
#### Badge Print Page (`/print`)
|
||||
- Screen-only header (`print:hidden`): "Back to Search" link + "Already printed N times" warning
|
||||
- Badge rendered via `ae_comp__badge_obj_view` with `is_review_mode={false}`
|
||||
- Print button inside `ae_comp__badge_obj_view` handles count update → `window.print()` → redirect to search
|
||||
- Page `<title>` includes badge name + event name
|
||||
|
||||
#### Badge Review Page (`/review`)
|
||||
- Passcode-gated for attendees — URL `?passcode=...` matched against `badge.person_passcode`
|
||||
- **Note:** `person_passcode` field is not yet in the DB (as of 2026-02-27). Review page accessible to staff via `trusted_access` without a passcode.
|
||||
- Access hierarchy (checked in order):
|
||||
1. Administrator → full access (`can_edit_fields = ['*']`)
|
||||
2. Trusted Staff → staff field set
|
||||
3. Attendee with valid passcode → attendee field set
|
||||
4. No access → passcode entry form shown
|
||||
- Uses `ae_comp__badge_review_form.svelte` (NOT badge render)
|
||||
- "Back to Search" link shown for staff only
|
||||
|
||||
### Key Components
|
||||
|
||||
**Badge List Page** (`+page.svelte`)
|
||||
- **LiveQuery:** Reactive badge list from IDB
|
||||
- **Search Pattern:** Debounced search with fast path + revalidation
|
||||
- **ID List:** `event_badge_id_li` drives LiveQuery
|
||||
- **Loading State:** Shows spinner when `search_status === 'loading'`
|
||||
|
||||
**Badge Search** (`ae_comp__badge_search.svelte`)
|
||||
- **Form Mode:** Toggle between search form and QR scanner
|
||||
- **Filters:** Badge type, print status, affiliations, sort order (trusted+ only)
|
||||
- **Fulltext:** Name/email search (all users)
|
||||
- **QR Scan:** Integrated QR scanner for badge ID lookup
|
||||
|
||||
**Badge List Display** (`ae_comp__badge_obj_li.svelte`)
|
||||
- **Visibility Filter:** Respects `hide` flag (trusted+ sees all)
|
||||
- **Display Logic:** Override → regular → fallback pattern
|
||||
- **Print Indicator:** Green checkmark badge shows `print_count`
|
||||
- **Metadata:** ID, created/updated timestamps (edit mode only)
|
||||
|
||||
**Badge Detail View** (`ae_comp__badge_obj_view.svelte`)
|
||||
- **Edit Mode:** Activated by edit button (or `#review` URL hash for future self-service)
|
||||
- **Condition:** Renders only when BOTH `$lq__event_badge_obj` AND `$lq__event_badge_template_obj` are non-null
|
||||
- **Form Binding:** Direct `bind:value` on editable fields
|
||||
- **Dynamic Sizing:** Font size adjusts based on text length
|
||||
- **Print Preview:** Full badge layout with template
|
||||
- **Save Handler:** Only sends changed fields to API
|
||||
- **`data-testid` attributes:** `badge-edit-btn`, `badge-save-btn`, `badge-cancel-btn`, `badge-print-btn`, `badge-professional-title-input` — use these in tests
|
||||
|
||||
---
|
||||
|
||||
## Testing Status
|
||||
|
||||
### Current Test Coverage
|
||||
- ✅ Badge list loads (all 6 data integrity tests passing)
|
||||
- ✅ Badge template list loads and displays
|
||||
- ✅ Badge template form renders and populates correctly
|
||||
- ✅ Badge template values persist in edit form
|
||||
- ✅ Electron bridge compatibility (graceful degradation in browser)
|
||||
- ✅ Badge field processor handles missing optional fields
|
||||
- ✅ Badge type filter tests
|
||||
- ✅ Badge template relationship tests
|
||||
- ✅ **Attendee workflow test** — navigate → edit professional title → print → return (d1ded2d4)
|
||||
|
||||
### Key Test Lessons Learned
|
||||
|
||||
**Search API path is FLAT, not nested.** `search_ae_obj` builds `/v3/crud/{obj_type}/search` — always flat regardless of the parent relationship. Mocks must match this:
|
||||
```typescript
|
||||
// CORRECT — flat path
|
||||
url.includes('/v3/crud/event_badge/search') && method === 'POST'
|
||||
// WRONG — nested path, mock will never fire
|
||||
url.includes(`/v3/crud/event/${event_id}/event_badge/search`) && method === 'POST'
|
||||
```
|
||||
|
||||
**List API (GET) is also FLAT with query params.** `get_ae_obj_li` builds `/v3/crud/{obj_type}/?for_obj_id=...` — always flat. Mocks must check `url.includes('/v3/crud/event_badge_template/') && url.includes('for_obj_id')`.
|
||||
|
||||
**CSS `input[value*=...]` selectors don't work with Svelte bind:value.** The CSS selector checks the HTML *attribute*; Svelte's `bind:value` sets the DOM *property* only. In Playwright tests, use `page.getByLabel()` or `locator.inputValue()` instead.
|
||||
|
||||
**Dexie requires `_random` ID fields.** Badge objects saved to IDB must include:
|
||||
```typescript
|
||||
event_badge_id_random: string // Must be present or Dexie skips the object
|
||||
id_random: string // Also checked
|
||||
// Error: "Object is missing a valid ID for table 'badge'"
|
||||
```
|
||||
All API mock responses in tests need these fields.
|
||||
|
||||
**Badge view requires both badge AND template.** `ae_comp__badge_obj_view.svelte` wraps everything in `{#if $lq__event_badge_obj && $lq__event_badge_template_obj}` — if the template isn't loaded, edit/print buttons and the badge itself don't render. Tests must mock the badge template endpoint.
|
||||
|
||||
**Badge GET endpoint (single object):** `/v3/crud/event_badge/{id}` (NOT nested under event). Matches `api.get_ae_obj()` which uses the flat path.
|
||||
|
||||
**Badge PATCH endpoint (update):** `/v3/crud/event/${event_id}/event_badge/${badge_id}` (nested under event). Matches `api.patch_ae_obj()` which uses the nested path.
|
||||
|
||||
**Use `data-testid` for test selectors.** Key buttons have targets: `badge-edit-btn`, `badge-save-btn`, `badge-cancel-btn`, `badge-print-btn`, `badge-professional-title-input`.
|
||||
|
||||
### Remaining Test Issues
|
||||
None — all current badge tests passing as of 2026-02-26 (f5e98b8c).
|
||||
|
||||
---
|
||||
|
||||
## Known Issues & Future Enhancements
|
||||
|
||||
### Known Issues
|
||||
1. **Session Cold-Start:** Potential race condition on first load (same as pres mgmt module)
|
||||
2. **Type Definitions:** Some pre-existing TypeScript errors on external package types (not introduced by badge work)
|
||||
3. **`person_passcode` not in DB:** Attendee-gated review URL (`?passcode=...`) cannot function until this field is added to the `event_badge` schema. The review page falls back to passcode entry form for non-staff.
|
||||
4. **Print page CSS:** Badge print rendering and `@page` print styles not yet fine-tuned — expected to need work
|
||||
5. **`mod_badges_json.edit_permissions` not connected:** Settings UI exists but review page uses hardcoded field defaults
|
||||
|
||||
### Implemented (2026-02-27)
|
||||
- ✅ `window.print()` wired to print button (records count first, then prints, then redirects)
|
||||
- ✅ Dedicated `/print` page — replaces old `[badge_id]/+page.svelte` placeholder
|
||||
- ✅ Dedicated `/review` page — passcode-gated, access-tiered
|
||||
- ✅ `ae_comp__badge_review_form.svelte` — stub created, full form fields pending
|
||||
- ✅ Badge search results visibility rules (unprinted-only for non-edit, all for trusted+edit)
|
||||
- ✅ Badge list: 4 action buttons per row (Print, Review nav, Copy Link, Email Link) — all Lucide icons
|
||||
- ✅ Print page: 3 action buttons in header (Print Now, Review nav, Email Link) — all Lucide icons
|
||||
- ✅ Review page: 3 action buttons in header (Print nav, Copy Link, Email Link) — all Lucide icons
|
||||
- ✅ Print button: not shown when already printed (unless Edit Mode)
|
||||
- ✅ Print count shown as `Nx` badge next to printer icon
|
||||
- ✅ Email obscuring for non-trusted users
|
||||
- ✅ Email Review Link button (placeholder alert — email API pending)
|
||||
- ✅ Direct Review Link clipboard copy (trusted + Edit Mode only)
|
||||
- ✅ Fixed: components no longer write to `$ae_loc.edit_mode`
|
||||
- ✅ Settings UI for `edit_permissions` per event (`ae_comp__event_settings_badges_form.svelte`)
|
||||
- ✅ All badge module icons converted to Lucide (Font Awesome removed from badge routes)
|
||||
|
||||
### Recently Completed (2026-02-27)
|
||||
- ✅ **Badge Review Form** — `ae_comp__badge_review_form.svelte` fully implemented (fields, QR, save/cancel, options/tickets, accessibility, help modal)
|
||||
- ✅ **Print font size controls (v1)** — Screen-only `[−]/[+]/[↺]` panel on print page; 4 px props added to `ae_comp__badge_obj_view.svelte`; auto-sizing unchanged when props absent
|
||||
- ✅ **Bug fix** — `default_authenticated_fields` / `default_trusted_fields` in `review/+page.svelte` corrected (wrong field names caused silent save drops)
|
||||
|
||||
### Still Needed — HIGH PRIORITY (first show: April 2026)
|
||||
|
||||
### Still Needed — MEDIUM PRIORITY
|
||||
1. **Email API for review links:** `send_review_email()` is a placeholder `alert()`. Needs actual email send endpoint.
|
||||
2. **`person_passcode` DB field:** Add to `event_badge` schema to enable attendee-gated review URLs.
|
||||
3. **Connect `edit_permissions` config:** Read `mod_badges_json.edit_permissions` in review page instead of hardcoded defaults.
|
||||
4. **Print page CSS / `@page` styles:** Badge rendering, sizing, and print-specific stylesheet.
|
||||
|
||||
### Still Needed — FUTURE / LOW PRIORITY
|
||||
1. **Batch Operations:** Bulk update, bulk print, bulk export
|
||||
2. **Audit Log:** Track who edited which fields and when
|
||||
3. **Photo Badges:** Support badge photo upload and display
|
||||
4. **Real-Time Sync:** WebSocket updates for multi-device badge printing stations
|
||||
|
||||
---
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Adding New Override Fields
|
||||
1. Add `{field}_override` to database schema
|
||||
2. Add to `properties_to_save` array in `ae_events__event_badge.ts`
|
||||
3. Update display logic to check override first
|
||||
4. Add to editable fields in `ae_comp__badge_obj_view.svelte`
|
||||
5. Update access control config
|
||||
6. Document in this file
|
||||
|
||||
### Testing Override Fields
|
||||
```typescript
|
||||
// Simulate external sync
|
||||
badge.given_name = "External Value"
|
||||
|
||||
// User edits
|
||||
badge.given_name_override = "User Value"
|
||||
|
||||
// Next sync (should NOT change override)
|
||||
badge.given_name = "Updated External Value"
|
||||
|
||||
// Display should still show "User Value"
|
||||
assert(display === badge.given_name_override)
|
||||
```
|
||||
|
||||
### Debugging Search Issues
|
||||
```typescript
|
||||
// Enable search logging
|
||||
log_lvl: 2
|
||||
|
||||
// Check search params object
|
||||
console.log('Search params:', search_params)
|
||||
|
||||
// Verify API request
|
||||
console.log('API request:', { event_id, fulltext_search_qry_str, type_code })
|
||||
|
||||
// Check returned IDs
|
||||
console.log('Badge IDs:', event_badge_id_li)
|
||||
|
||||
// Verify IDB contents
|
||||
db_events.badge.toArray().then(console.log)
|
||||
```
|
||||
## Route Map (Badges)
|
||||
|
||||
| URL | Purpose |
|
||||
|---|---|
|
||||
| `/events/[id]/badges` | Main search and attendee list. |
|
||||
| `/events/[id]/badges/templates` | Badge template management. |
|
||||
| `/events/[id]/badges/[id]/print` | The actual print-ready render page. |
|
||||
| `/events/[id]/badges/[id]/review` | Attendee-facing self-service form. |
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
- [AE API V3 for Frontend](./GUIDE__AE_API_V3_for_Frontend.md)
|
||||
- [Development Guide](./GUIDE__Development.md)
|
||||
- [Events Launcher Native Integration](./PROJECT__AE_Events_Launcher_Native_integration.md)
|
||||
- [Naming Conventions](./AE__Naming_Conventions.md)
|
||||
|
||||
---
|
||||
|
||||
**Document Status:** 🔄 In Progress
|
||||
**Last Verified:** 2026-02-27 (rev 5 — field permissions spec added, header buttons implemented, review form fields pending)
|
||||
**Verified Against:** Code as of 2026-02-27 (branch ae_app_3x_llm)
|
||||
👉 **[MODULE__AE_Events_Badge_Templates.md](./MODULE__AE_Events_Badge_Templates.md)** (Technical reference for layouts)
|
||||
👉 **[GUIDE__AE_Events_Badges_Onsite.md](./GUIDE__AE_Events_Badges_Onsite.md)** (Hardware & station setup)
|
||||
👉 **[GUIDE__AE_Events_Onsite_Runbook.md](./GUIDE__AE_Events_Onsite_Runbook.md)** (Onsite operational checklists)
|
||||
|
||||
81
documentation/MODULE__AE_Events_Launcher.md
Normal file
81
documentation/MODULE__AE_Events_Launcher.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# 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.
|
||||
|
||||
---
|
||||
|
||||
## Operational Modes
|
||||
|
||||
| Mode | Use Case | File Handling |
|
||||
|---|---|---|
|
||||
| **Default** | Browser on any machine | Files downloaded on demand via browser. |
|
||||
| **Onsite** | Browser on event network | Faster polling; browser-managed files. |
|
||||
| **Native** | Electron app on podium Mac | Background pre-cache; atomic file handover. |
|
||||
|
||||
For production onsite use, **Native mode on Mac laptops** is the target. The Electron
|
||||
app pre-caches all session files in the background so presentations open instantly without
|
||||
a network round-trip at the moment of launch.
|
||||
|
||||
---
|
||||
|
||||
## Launcher Display Views
|
||||
|
||||
| View | Shown When |
|
||||
|---|---|
|
||||
| **Session view** | Active session with session-level files. |
|
||||
| **Presentation view** | Active session with named presentations. |
|
||||
| **Presenter view** | Presentation selected; shows presenter bio/photo. |
|
||||
| **Poster/group view** | Special layout for poster sessions. |
|
||||
| **Screensaver** | No active session; idle state. |
|
||||
|
||||
---
|
||||
|
||||
## Sync Engine & File Handling
|
||||
|
||||
### Background Sync (File Warming)
|
||||
When a user navigates to a session in the Launcher UI, the background engine automatically warms the cache for that specific session by downloading all associated files.
|
||||
|
||||
### Force Sync Location
|
||||
To ensure full room readiness (e.g., during SRR setup or overnight), operators can trigger a **Force Sync Location** via the configuration menu. This performs a recursive fetch of all sessions, presentations, and presenters for the room and queues every file for the day for download.
|
||||
|
||||
### Download Priority & Room Readiness
|
||||
To ensure the podium is ready for the day's first sessions, the Launcher sync engine uses a 4-tier chronological sorting priority:
|
||||
|
||||
1. **Global Assets:** Event and Location level files (branding, walk-in slides) are cached first.
|
||||
2. **Session Schedule:** Files for the earliest sessions in the room are prioritized.
|
||||
3. **Presentation Order:** Within a session block, speakers are prioritized by their scheduled start time.
|
||||
4. **First-In Fairness:** When times are equal, older uploads are prioritized over late revisions (respecting on-time presenters).
|
||||
|
||||
### Native File Opening (Safe Handover)
|
||||
1. Verify SHA-256 hash in permanent cache.
|
||||
2. Atomic copy to system `[tmp]` directory.
|
||||
3. Rename to original filename (e.g., `Abstract_101.pptx`).
|
||||
4. OS opens the file via a **Launch Profile** (AppleScript or Shell command).
|
||||
|
||||
---
|
||||
|
||||
## Device & Native Integration
|
||||
|
||||
Each Launcher kiosk is registered as an `event_device` record in Aether. The technical specifications for the Electron bridge, hashed cache protocol, and hardware actuators are documented in:
|
||||
👉 **[MODULE__AE_Events_Launcher_Native.md](./MODULE__AE_Events_Launcher_Native.md)**
|
||||
|
||||
---
|
||||
|
||||
## Route Map (Display)
|
||||
|
||||
| URL | Purpose |
|
||||
|---|---|
|
||||
| `/events/[id]/launcher` | Launcher home — select location |
|
||||
| `/events/[id]/launcher/[location_id]` | Launcher display for a specific room |
|
||||
|
||||
---
|
||||
|
||||
## Access Levels
|
||||
|
||||
| Feature | Minimum Access |
|
||||
|---|---|
|
||||
| View Launcher display | `authenticated_access` |
|
||||
| Manual session selection | `trusted_access` |
|
||||
| Advanced Config / Sync Control | `trusted_access` (via Configuration Drawer) |
|
||||
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).
|
||||
275
documentation/MODULE__AE_Events_Launcher_Native.md
Normal file
275
documentation/MODULE__AE_Events_Launcher_Native.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# Aether Events — Launcher: Native Integration
|
||||
|
||||
> **Status:** Operational / Permanent Reference
|
||||
> **Last Updated:** 2026-05-21 (Reorganized)
|
||||
> **Primary Platform:** macOS (Darwin)
|
||||
> **Fallback Platform:** Linux / Windows
|
||||
|
||||
## 1. Overview
|
||||
The Aether Events Launcher utilizes an Electron-based "Native Shell" to provide OS-level capabilities that are normally restricted by browser sandboxing. This enables persistent file caching, direct control of presentation software (Keynote, PowerPoint), and hardware telemetry.
|
||||
|
||||
### Operational Modes
|
||||
|
||||
| Mode | Purpose | File Handling |
|
||||
| :--- | :--- | :--- |
|
||||
| **Default** | Standard web browser access. | Direct downloads; no local caching. |
|
||||
| **Onsite** | Web access on event networks. | Faster polling; browser-based file management. |
|
||||
| **Native** | Dedicated Podium Kiosk (Electron). | Full background pre-caching; atomic safe-handover. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture: The Three-Layer Bridge
|
||||
|
||||
The integration is built on a decoupled three-layer communication model to ensure security and cross-platform flexibility.
|
||||
|
||||
### 2.1 Layer 1: The Engine (Main Process)
|
||||
- **Repo:** `~/OSIT_dev/aether_app_native_electron/` (separate git repo)
|
||||
- **File:** `aether_app_native_electron/src/main/*.ts`
|
||||
- **Role:** Performs the heavy lifting (Filesystem, Shell, AppleScript).
|
||||
- **Responsibilities:**
|
||||
- Managing the **Hashed Cache** directory.
|
||||
- Executing `osascript` intents for presentation control.
|
||||
- Spawn/Kill process management.
|
||||
|
||||
### 2.2 Layer 2: The Gatekeeper (Preload Script)
|
||||
- **Namespace:** `window.aetherNative`
|
||||
- **Role:** Securely exposes whitelisted IPC channels to the Renderer.
|
||||
- **Standards:** Uses `contextBridge.exposeInMainWorld` to prevent arbitrary code execution.
|
||||
|
||||
### 2.3 Layer 3: The Messenger (SvelteKit Relay)
|
||||
- **File:** `src/lib/electron/electron_relay.ts`
|
||||
- **Role:** Provides a clean, typed API for Svelte components.
|
||||
- **Responsibilities:**
|
||||
- Mapping `camelCase` UI triggers to `snake_case` IPC calls.
|
||||
- Resolving an extension alias to a canonical Launch Profile, then to a single
|
||||
`native_template` string before crossing IPC.
|
||||
|
||||
The reason for this split is simple: Launch Profiles are policy, while Native Templates are
|
||||
executable strings. Keeping that distinction explicit prevents the bridge from mixing config
|
||||
objects with runtime commands.
|
||||
|
||||
---
|
||||
|
||||
## 3. The "Zero-Config" Lifecycle
|
||||
|
||||
To support rapid onsite deployment, the native app requires zero manual setup.
|
||||
|
||||
1. **Seed:** On launch, the Main process reads a local `seed.json` (Device ID + API Key).
|
||||
2. **Identity:** Calls `GET /v3/crud/event_device/{id}` to pull device config and extract `app_base_url` (the event FQDN) and `account_id`.
|
||||
3. **Site Context:** POSTs to `/v3/crud/site_domain/search?limit=1` with the FQDN to resolve the correct site. No JWT — auth is `x-aether-api-key` + `x-account-id` throughout.
|
||||
4. **Launch:** Navigates the SvelteKit frontend directly to the assigned Event Launcher route (`/events/{eventId}/launcher/{locationId}`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Podium Reliability Protocol
|
||||
|
||||
The system is designed to ensure that a presentation never fails due to network instability.
|
||||
|
||||
### 4.1 Hashed Cache Pattern
|
||||
Files are stored persistently using their SHA-256 hash to prevent filename collisions and handle versioning.
|
||||
- **Root:** `~/Library/Caches/OSIT/file_cache/`
|
||||
- **Subdirectory:** First 2 characters of hash (e.g., `ab/`)
|
||||
- **Filename:** `{hash}.file`
|
||||
|
||||
### 4.2 Background Sync (File Warming)
|
||||
When a user navigates to a session in the Launcher UI, the `LauncherBackgroundSync` component warms the cache for that specific session. To ensure full room readiness, a **Force Sync Location** trigger is available in the configuration UI.
|
||||
|
||||
1. **Metadata Fetch:** The system fetches all sessions, presentations, and presenters for the current location into the local database (Dexie).
|
||||
2. **Chronological Priority:** Missing files are added to the download queue and sorted to prioritize the event schedule:
|
||||
- **Tier 1: Global Assets** — Event and Location level files (virtual time 0).
|
||||
- **Tier 2: Session Schedule** — Earliest sessions are prioritized first.
|
||||
- **Tier 3: Presentation Order** — Within a session, speakers are prioritized by their start time.
|
||||
- **Tier 4: Integrity & Fairness** — Tie-breakers use `created_on` (oldest first) to ensure on-time uploads are cached before last-minute revisions.
|
||||
3. **Download:** Triggers background downloads via `aetherNative.download_to_cache` sequentially to preserve network bandwidth and ensure file integrity.
|
||||
|
||||
### 4.3 Safe Handover (Launch Sequence)
|
||||
When a user clicks "Open", the system follows a non-destructive sequence:
|
||||
1. **Verify:** Confirm hash exists in the permanent cache.
|
||||
2. **Copy:** Create an atomic copy in the system `[tmp]` directory.
|
||||
3. **Restore:** Rename the copy to its original filename (e.g., `Abstract_101.pptx`).
|
||||
4. **Execute:** Launch the file via the OS.
|
||||
|
||||
---
|
||||
|
||||
## 5. Automation & Actuators (Phase 5)
|
||||
|
||||
The native shell provides specialized handlers for controlling the "Podium Experience."
|
||||
|
||||
### 5.1 Presentation Acts
|
||||
|
||||
| Action | Handler | Actuator (macOS) |
|
||||
| :--- | :--- | :--- |
|
||||
| **Launch** | `launch_presentation` | `open` or `osascript` (slideshow start) |
|
||||
| **Control** | `control_presentation` | `osascript` (next/prev slide) |
|
||||
| **Clean Up** | `kill_processes` | `killall -INT` (graceful exit) |
|
||||
|
||||
### 5.2 System Management
|
||||
- **Telemetry:** Pushes `cpu_usage`, `memory_free_gb`, and `foreground_app` via heartbeats using the `get_device_info` relay.
|
||||
- **Self-Update (Roadmap):** Plan to monitor Syncthing `admin_share` for newer `.app` versions and perform atomic swaps.
|
||||
|
||||
### 5.3 Implemented Actuators (Phase 5 Complete)
|
||||
- **Recording:** `manage_recording({action})` — Aperture session capture (`start`, `stop`, `status`). macOS only.
|
||||
- **Display Layouts:** `set_display_layout({mode, configStr?})` — Mirror / Extend displays. macOS only. **Primary:** native `display_control` binary (`resources/bin/display_control`) uses CoreGraphics APIs directly — no Homebrew dependency. Built from `scripts/display_control.m` via `scripts/build-display-control.sh` on a Mac; commit the binary to the repo. **Fallback:** [`displayplacer`](https://github.com/jakehilborn/displayplacer) (`brew install displayplacer`) used when binary is absent or `configStr` override is set. Failures are logged to the Electron console but do not block file open. A **Display Mode** toggle (Extend / Mirror) is available in the Launcher config — Native OS section, visible without Technical Mode.
|
||||
- **Power Control:** `power_control({action})` — Shutdown, reboot, sleep. macOS + Linux.
|
||||
- **Window Control:** `window_control({action})` — Maximize, minimize, fullscreen, kiosk mode.
|
||||
- **Wallpaper:** `set_wallpaper({path?, url?, url_external?, display?, api_key?, account_id?})` — Downloads from URL (cached locally) or applies a local path. Per-display targeting (`'all'`/`'primary'`/`'external'`). macOS only in production; Linux returns a dev-mode preview payload.
|
||||
|
||||
> **Note:** `update_app` is implemented as a stub — downloads but does not install. Not yet functional for end users.
|
||||
|
||||
---
|
||||
|
||||
## 6. Launcher Configuration & Management
|
||||
|
||||
The Launcher features a standardized, responsive configuration interface designed for onsite technical management.
|
||||
|
||||
### 6.1 UI Architecture
|
||||
- **Tabbed Navigation:** Categorized into System, Sync, and General settings.
|
||||
- **Section Wrapper (`Launcher_Cfg_Section`):** A shared component providing a consistent header, icon, and responsive grid container.
|
||||
|
||||
### 6.2 3-Way State Logic
|
||||
To manage screen real estate on varying laptop resolutions, all configuration sections utilize a 3-way visibility state:
|
||||
- **`collapsed`**: Content is hidden.
|
||||
- **`auto`**: Expanded by default, but automatically closes if another "auto" section is opened.
|
||||
- **`pinned`**: Expanded and remains open regardless of other section interactions.
|
||||
|
||||
### 6.3 Technical Mode (`edit_mode`)
|
||||
The UI dynamically filters fields based on the user's focus. Enabling Technical Mode (`$ae_loc.edit_mode`) reveals advanced diagnostic and writeable fields.
|
||||
|
||||
| Category | Standard View (Read-Only) | Technical Mode (Read/Write) |
|
||||
| :--- | :--- | :--- |
|
||||
| **Health** | Heartbeat, RAM Usage, Sync Stats | Hostname, IP List, Raw Device JSON |
|
||||
| **OS Bridge** | Folder Buttons, Recording Toggle | Manual Terminal Commands, Reset Wallpaper |
|
||||
| **Sync** | Sync Completion Status | Millisecond Timers, Cache Prefix Logic |
|
||||
| **Update** | Current Version Status | Manual Update Paths, URL Overrides |
|
||||
|
||||
---
|
||||
|
||||
## 7. Implementation Reference (IPC Whitelist)
|
||||
|
||||
All functions below are exported from `src/lib/electron/electron_relay.ts` and safely
|
||||
no-op when `window.aetherNative` is not present (i.e., in browser/non-native mode).
|
||||
|
||||
### Config & Info
|
||||
- `get_device_config()` — Returns hydrated device settings injected by the native shell on startup.
|
||||
- `get_device_info()` — Returns OS metadata, IP list, hostname, and path placeholders (`[home]`, `[tmp]`).
|
||||
|
||||
### File Cache
|
||||
- `check_cache({cache_root, hash, hash_prefix_length?, verify_hash?})` — Verifies a file exists in the local hashed cache. `verify_hash: true` re-hashes to confirm integrity.
|
||||
- `download_to_cache({url, cache_root, hash, api_key, account_id, hash_prefix_length?})` — Streams a file download to the hashed cache with SHA-256 integrity check. Stale `.tmp` files (older than 5 min) from crashed downloads are cleaned up automatically on each call.
|
||||
- `copy_from_cache_to_temp({cache_root, hash, temp_root, filename, hash_prefix_length?})` — **Preferred primitive.** Copies a cached file to temp and returns `{ success, path }`. The Svelte caller decides what to do next (run a script, open it, etc.).
|
||||
- `launch_from_cache({cache_root, hash, temp_root, filename, hash_prefix_length?, native_template?})` — Combines copy + launch in one call. Executes the provided `native_template` string after the file is copied to temp. If no template is supplied, treat it as an error and do not rely on Electron-side defaults.
|
||||
|
||||
> `hash_prefix_length` defaults to `2` throughout. Do not change without coordinating all devices — mismatched values create orphaned cache subdirectories.
|
||||
|
||||
### Shell & OS
|
||||
- `open_folder(path)` — Opens a path in the OS file manager.
|
||||
- `run_cmd({cmd, timeout?, return_stdout?})` — Async shell command execution.
|
||||
- `run_cmd_sync({cmd, return_stdout?})` — Synchronous shell command execution.
|
||||
- `run_osascript(script)` — Executes an AppleScript string. macOS only. **Hardened (2026-05-11):** writes script to a temp `.scpt` file; multi-line scripts and paths with special characters now work correctly. No shell escaping needed in the passed string.
|
||||
- `kill_processes({process_name_li})` — Terminates processes by name. macOS/Linux: `pkill -f`. Windows: `taskkill /F`.
|
||||
- `open_local_file_v2(path)` — Opens a file with its default OS application.
|
||||
|
||||
### Presentations (Phase 5)
|
||||
- `launch_presentation({path, app?, os?})` — Platform-aware launcher. macOS: PowerPoint/Keynote via AppleScript. Linux: LibreOffice Impress. Resolves `[home]`/`[tmp]` placeholders.
|
||||
- `control_presentation({app, action})` — Slide navigation (`next`/`prev`/`start`/`stop`) for PowerPoint or Keynote via AppleScript.
|
||||
|
||||
### System Management (Phase 5)
|
||||
- `set_wallpaper({path?, url?, url_external?, display?, api_key?, account_id?})` — Sets desktop wallpaper. Downloads from `url` (cached to `~/Library/Caches/OSIT/wallpaper/`) or applies a local `path`. `url_external` targets the projector/second display separately. macOS only in production; Linux returns a dev-mode preview payload without applying.
|
||||
- `window_control({action, value?})` — Electron window management: maximize, minimize, fullscreen, kiosk.
|
||||
- `set_display_layout({mode, configStr?})` — Mirror or extend displays via [`displayplacer`](https://github.com/jakehilborn/displayplacer). macOS only. Auto-detects via `displayplacer list`; `configStr` overrides auto-detection when set. Binary lookup order: bundled `resources/bin/displayplacer` → `/opt/homebrew/bin/` (Apple Silicon) → `/usr/local/bin/` (Intel). Requires `brew install displayplacer` on each venue Mac if not bundled.
|
||||
- `power_control({action})` — Shutdown, reboot, or sleep the host machine. macOS + Linux.
|
||||
- `manage_recording({action, options?})` — Aperture capture control (`start`/`stop`/`status`). macOS only.
|
||||
- `open_external({url, app?})` — Opens a URL in Chrome, Firefox, or the default browser.
|
||||
- `update_app(args)` — **Stub only.** Downloads but does not install. Not yet functional.
|
||||
- `list_tools()` — Returns a self-documenting manifest of all available native bridge functions.
|
||||
|
||||
### Path Placeholders
|
||||
All paths passed to native handlers should use tokens rather than hardcoded OS paths:
|
||||
- `[home]` — Resolved to the user's home directory by the native bridge.
|
||||
- `[tmp]` — Resolved to the system temporary directory.
|
||||
|
||||
---
|
||||
|
||||
## 8. Launch Profiles and Native Templates (No-Rebuild File Handling)
|
||||
This launcher uses two related concepts:
|
||||
|
||||
- **Launch Profile**: the Svelte-side config object keyed by file extension. A profile decides
|
||||
which app to use, whether to extend or mirror displays, whether to use an explicit open
|
||||
command, whether to run post-open automation, and how long to wait before running it.
|
||||
- **Native Template**: the single AppleScript or shell command string handed to Electron after
|
||||
Svelte resolves the profile. This is what Electron actually executes.
|
||||
|
||||
The Svelte launcher resolves a profile and then passes a native template string to
|
||||
`launch_from_cache`. Electron only executes the template it receives. If Svelte has not
|
||||
resolved a template yet, it should stop before IPC and surface a missing-profile error.
|
||||
This keeps all fallback logic in Svelte, where it can be edited without rebuilding Electron.
|
||||
The native layer should not invent or guess a default launch path.
|
||||
|
||||
The built-in defaults are organized as canonical profile names plus extension aliases. That
|
||||
lets multiple file types share one profile without repeating the same app/script details.
|
||||
The profile object also carries `post_delay_ms`, and a device-specific per-profile
|
||||
`launch_profiles[profile].post_delay_ms` override can tune the delay without changing the bridge
|
||||
contract. URL-based presentations remain a special pseudo-extension handled separately from
|
||||
the cache open flow.
|
||||
|
||||
### Native Template Formats
|
||||
|
||||
| Format | Example |
|
||||
| :--- | :--- |
|
||||
| **AppleScript** (macOS) | Multi-line AppleScript string with `{{path}}` placeholder |
|
||||
| **Shell command** | String prefixed with `shell:` — e.g. `shell:open "{{path}}"` |
|
||||
|
||||
The placeholder `{{path}}` is replaced with the full resolved path to the file in the temp
|
||||
directory after the atomic copy from cache.
|
||||
|
||||
### Where to Configure
|
||||
|
||||
Launch profiles are resolved in priority order by `get_launch_profile()` in
|
||||
`launcher_file_cont.svelte`:
|
||||
|
||||
1. **`event_device.data_json.launch_profiles`** — API-driven, per-device. Highest priority.
|
||||
Set via the `event_device` record (Pres Mgmt → Device Management or direct DB edit).
|
||||
2. **`$events_loc.launcher.launch_profiles`** — Local persistent config. Editable via the
|
||||
Launcher config UI (planned) or direct `localStorage` manipulation.
|
||||
|
||||
If neither is set, the resolved native template is `null` and the launcher should not call
|
||||
Electron until an explicit template is available.
|
||||
|
||||
Why: this avoids a second hidden source of truth. The profile map can evolve independently of
|
||||
the executable string, and Electron stays a thin executor rather than a policy engine.
|
||||
|
||||
### Key Format
|
||||
|
||||
Keys are lowercase file extensions without the dot. A `"default"` key catches all
|
||||
unrecognised extensions.
|
||||
|
||||
The JSON below illustrates the `native_template` emitted after profile resolution, not the
|
||||
full Launch Profile object schema.
|
||||
|
||||
```json
|
||||
// event_device.data_json.launch_profiles example
|
||||
{
|
||||
"launch_profiles": {
|
||||
"pptx": "tell application \"Microsoft PowerPoint\"\n activate\n open (POSIX file \"{{path}}\")\n delay 3\nend tell\ntell application \"System Events\"\n keystroke return using command down\nend tell",
|
||||
"key": "tell application \"Keynote\"\n activate\n open (POSIX file \"{{path}}\")\n delay 1\n start (front document)\nend tell",
|
||||
"pdf": "shell:open \"{{path}}\"",
|
||||
"default": "shell:open \"{{path}}\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AppleScript Execution — All Handlers Hardened (2026-05-11)
|
||||
|
||||
All AppleScript execution in the native shell now writes scripts to a temp `.scpt` file and
|
||||
runs `osascript "<path>"` rather than the old `osascript -e "<inline>"` approach.
|
||||
|
||||
- **`run_osascript`** — hardened (2026-05-11, earlier batch)
|
||||
- **`launch_from_cache`** — hardened (same batch)
|
||||
- **`launch_presentation`** — hardened (2026-05-11, follow-up fix; was the last handler still using `-e`)
|
||||
- **`control_presentation`** — uses single-line scripts with no path interpolation; `-e` is safe here and retained for simplicity
|
||||
|
||||
The `-e` approach breaks on (1) multi-line scripts and (2) file paths containing spaces,
|
||||
quotes, or parentheses — common in conference presentation filenames.
|
||||
|
||||
### Not Exposed via Relay (intentional)
|
||||
- `get_seed_config` / `get_jwt` — Exposed in the preload but not relayed to the UI. The JWT and seed are injected into the environment at startup; components should not call these directly.
|
||||
@@ -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.
|
||||
|
||||
141
documentation/MODULE__AE_Events_Presentation_Management.md
Normal file
141
documentation/MODULE__AE_Events_Presentation_Management.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# 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.
|
||||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
|
||||
### Object Hierarchy
|
||||
|
||||
```text
|
||||
Event
|
||||
├── Event File (walk-in/out, hold slides for the whole event)
|
||||
├── Location (physical room — assigned to Sessions, not the other way around)
|
||||
├── Track (optional grouping; rarely used)
|
||||
└── Session (time block; Location assigned here, but may be unset initially)
|
||||
├── Session File (moderator slides, group/hold slides for this session)
|
||||
└── Presentation (a talk within the session; must belong to exactly one Session)
|
||||
└── Presenter (belongs to exactly one Presentation)
|
||||
└── Presenter File (their slides/materials — the common case)
|
||||
```
|
||||
|
||||
> **Import note:** When program data is initially imported (sessions, presentations,
|
||||
> presenters), Locations are often not assigned to Sessions yet — rooms may not be
|
||||
> finalized or the venue's room list may not be set up in Aether. Location assignment
|
||||
> typically happens as a separate step once the room list is confirmed.
|
||||
|
||||
### Relationships
|
||||
|
||||
- **Session → Location:** Many-to-one. A Session is assigned to one Location; a Location
|
||||
hosts many Sessions across the event timeline. Location may be null initially.
|
||||
- **Presentation → Session:** Many-to-one. A Presentation belongs to exactly one Session.
|
||||
A Session can have many Presentations (or none, for session-only setups).
|
||||
- **Presenter → Presentation:** Many-to-one. A Presenter belongs to exactly one Presentation.
|
||||
Optionally linked to an `event_person_id` for cross-referencing the person record.
|
||||
- **Event File:** Can be attached at any level — Presenter, Presentation, Session,
|
||||
Location, or Event. See the File Attachment Levels table.
|
||||
|
||||
### File Attachment Levels
|
||||
|
||||
Files (`event_file`) can be attached at five levels:
|
||||
|
||||
| Level | When Used | Typical Content |
|
||||
|---|---|---|
|
||||
| **Presenter** | 99% of the time for individual speakers | Their PowerPoint/PDF/video |
|
||||
| **Session** | Moderator slides; group/hold content for a specific session | "Session 3 — Group Discussion.pptx" |
|
||||
| **Location** | Walk-in/out or hold slides for a room across all sessions | Looped PPTX playing between sessions |
|
||||
| **Event** | Walk-in/out or hold slides used everywhere | Looped PPTX; branding overlay |
|
||||
| **Presentation** | File attached to the presentation record itself (less common) | Varies |
|
||||
|
||||
### Key Objects
|
||||
|
||||
| Object | Table | Purpose |
|
||||
|---|---|---|
|
||||
| Session | `event_session` | Time block; Location and datetime range assigned here |
|
||||
| Location | `event_location` | Physical room |
|
||||
| Presentation | `event_presentation` | A talk within a session; belongs to exactly one Session |
|
||||
| Presenter | `event_presenter` | Person linked to exactly one Presentation |
|
||||
| Event Person | `event_person` | Person record within the event context |
|
||||
| Event File | `event_file` | Uploaded file; attached at Presenter, Presentation, Session, Location, or Event level |
|
||||
|
||||
---
|
||||
|
||||
## Client Setup Variation
|
||||
|
||||
There are no rigid "modes" — events are configured with as much or as little structure
|
||||
as needed. The platform handles the full range:
|
||||
|
||||
**Minimal setup (BGH):**
|
||||
Sessions have room and time info. No Presentations or Presenters defined.
|
||||
Staff upload files directly at the session or location level onsite.
|
||||
|
||||
**Mid-range setup:**
|
||||
Sessions defined with named Presentations. Presenters may or may not be tracked.
|
||||
Mix of pre-uploaded and onsite files. QR codes may be used for quick session/presenter lookup.
|
||||
|
||||
**Full setup (LCI):**
|
||||
Sessions, Presentations, Presenters all defined and managed. External ID labeling
|
||||
(e.g., "LCI Member ID"). Agreement tracking for presenters. Files managed per-presenter.
|
||||
|
||||
The config that drives this is `event.mod_pres_mgmt_json` — see the Configuration section.
|
||||
|
||||
---
|
||||
|
||||
## Configuration — `mod_pres_mgmt_json`
|
||||
|
||||
The event's Presentation Management behavior is controlled by `event.mod_pres_mgmt_json`.
|
||||
|
||||
### Convention
|
||||
|
||||
| Prefix | Default state | Meaning |
|
||||
|---|---|---|
|
||||
| `hide__` | `false` = visible | Feature is ON by default; set `true` to suppress |
|
||||
| `show__` | `false` = hidden | Feature is OFF by default; set `true` to enable |
|
||||
|
||||
### Common Config Keys
|
||||
|
||||
| Key | Default | Notes |
|
||||
|---|---|---|
|
||||
| `lock_config` | `false` | `true` = force remote→local sync; prevents user overrides of local config |
|
||||
| `hide__session_code` | `false` | Hide session code column/field |
|
||||
| `hide__session_description` | `false` | Hide session description field |
|
||||
| `hide__session_location` | `false` | Hide location field on session view |
|
||||
| `hide__session_datetime` | `false` | Hide datetime fields |
|
||||
| `hide__presentation_code` | `false` | Hide presentation code |
|
||||
| `hide__presenter_code` | `false` | Hide presenter code |
|
||||
| `hide__location_code` | `false` | Hide location code |
|
||||
| `show__launcher_link` | `false` | Show direct Launcher link in session view |
|
||||
| `show__session_qr` | `false` | Show QR code for session (SRR lookup) |
|
||||
| `show__presenter_qr` | `false` | Show QR code for presenter (SRR lookup) |
|
||||
| `label__person_external_id` | `null` | Override label for external ID field (e.g., `"Member ID"`) |
|
||||
| `label__session_poc_name` | `null` | Override label for session POC (e.g., `"Champion"`) |
|
||||
| `file_purpose_option_kv` | `{}` | Key-value map of file purpose options (e.g., `{"ppt": "PowerPoint", "pdf": "PDF"}`) |
|
||||
|
||||
---
|
||||
|
||||
## Route Map (Administration)
|
||||
|
||||
| URL | Purpose |
|
||||
|---|---|
|
||||
| `/events/[id]/pres_mgmt` | Overview — sessions list, search, filter by location |
|
||||
| `/events/[id]/pres_mgmt/config` | Config editor (admin only) |
|
||||
| `/events/[id]/session/[session_id]` | Session detail — files, presentations, timing, alert |
|
||||
| `/events/[id]/presenter/[presenter_id]` | Presenter detail — bio, files, agreement, alert |
|
||||
| `/events/[id]/location/[location_id]` | Location detail — session schedule for this room, alert |
|
||||
| `/events/[id]/locations` | All locations list |
|
||||
| `/events/[id]/reports` | Reports — sessions, presenters, files |
|
||||
|
||||
---
|
||||
|
||||
## Access Levels
|
||||
|
||||
| Feature | Minimum Access |
|
||||
|---|---|
|
||||
| View pres_mgmt overview | `authenticated_access` |
|
||||
| Upload files | `authenticated_access` |
|
||||
| Edit sessions / presentations | `trusted_access` |
|
||||
| Edit config | `administrator_access` + `edit_mode` |
|
||||
| Device management | `administrator_access` |
|
||||
@@ -1,464 +0,0 @@
|
||||
# Aether Events — Presentation Management & Launcher
|
||||
|
||||
Notes on setup, workflow, configuration, and onsite operation for the Events Presentation
|
||||
Management module and the companion Launcher (podium display) system.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Presentation Management (Pres Mgmt) module handles the full lifecycle of conference
|
||||
content: sessions, presentations, presenters, presentation files, and room/location
|
||||
assignments. The Launcher module provides the podium display interface that runs on each
|
||||
session room's kiosk machine.
|
||||
|
||||
These two modules are deployed together — Pres Mgmt is the back office, Launcher is the
|
||||
front-of-house display. Every client show is at least slightly customized. Some clients
|
||||
have extensive presenter/presentation data; others just have sessions and files. The
|
||||
platform is flexible enough to handle the full range.
|
||||
|
||||
**Reference clients (current/repeat):**
|
||||
- **BGH** (Business Group on Health) — most basic setup; session-only, no named Presenters
|
||||
- **LCI** (Lean Construction Institute) — most complex current setup
|
||||
- **AAPOR**, **ASCM**, **CMSC** — other active/repeat clients
|
||||
|
||||
**Module paths:**
|
||||
- Pres Mgmt: `/events/[event_id]/pres_mgmt`
|
||||
- Launcher: `/events/[event_id]/launcher`
|
||||
|
||||
**Key source directories:**
|
||||
- `src/routes/events/[event_id]/(pres_mgmt)/`
|
||||
- `src/routes/events/[event_id]/(launcher)/`
|
||||
- `src/lib/ae_events/` — data types and API functions for all event objects
|
||||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
|
||||
### Object Hierarchy
|
||||
|
||||
```
|
||||
Event
|
||||
├── Event File (walk-in/out, hold slides for the whole event)
|
||||
├── Location (physical room — assigned to Sessions, not the other way around)
|
||||
├── Track (optional grouping; rarely used — see note below)
|
||||
└── Session (time block; Location assigned here, but may be unset initially)
|
||||
├── Session File (moderator slides, group/hold slides for this session)
|
||||
└── Presentation (a talk within the session; must belong to exactly one Session)
|
||||
└── Presenter (belongs to exactly one Presentation)
|
||||
└── Presenter File (their slides/materials — the common case)
|
||||
```
|
||||
|
||||
> **Import note:** When program data is initially imported (sessions, presentations,
|
||||
> presenters), Locations are often not assigned to Sessions yet — rooms may not be
|
||||
> finalized or the venue's room list may not be set up in Aether. Location assignment
|
||||
> typically happens as a separate step once the room list is confirmed.
|
||||
|
||||
### Relationships
|
||||
|
||||
- **Session → Location:** Many-to-one. A Session is assigned to one Location; a Location
|
||||
hosts many Sessions across the event timeline. Location may be null initially.
|
||||
- **Presentation → Session:** Many-to-one. A Presentation belongs to exactly one Session.
|
||||
A Session can have many Presentations (or none, for session-only setups).
|
||||
- **Presenter → Presentation:** Many-to-one. A Presenter belongs to exactly one Presentation.
|
||||
Optionally linked to an `event_person_id` for cross-referencing the person record.
|
||||
- **Event File:** Can be attached at any level — Presenter, Presentation, Session,
|
||||
Location, or Event. See the File Attachment Levels table.
|
||||
|
||||
### File Attachment Levels
|
||||
|
||||
Files (`event_file`) can be attached at five levels:
|
||||
|
||||
| Level | When Used | Typical Content |
|
||||
|---|---|---|
|
||||
| **Presenter** | 99% of the time for individual speakers | Their PowerPoint/PDF/video |
|
||||
| **Session** | Moderator slides; group/hold content for a specific session | "Session 3 — Group Discussion.pptx" |
|
||||
| **Location** | Walk-in/out or hold slides for a room across all sessions | Looped PPTX playing between sessions |
|
||||
| **Event** | Walk-in/out or hold slides used everywhere | Looped PPTX; branding overlay |
|
||||
| **Presentation** | File attached to the presentation record itself (less common) | Varies |
|
||||
|
||||
### Key Objects
|
||||
|
||||
| Object | Table | Purpose |
|
||||
|---|---|---|
|
||||
| Session | `event_session` | Time block; Location and datetime range assigned here |
|
||||
| Location | `event_location` | Physical room; the Launcher's primary unit of display |
|
||||
| Presentation | `event_presentation` | A talk within a session; belongs to exactly one Session |
|
||||
| Presenter | `event_presenter` | Person linked to exactly one Presentation; optionally linked to `event_person` |
|
||||
| Event Person | `event_person` | Person record within the event context |
|
||||
| Event File | `event_file` | Uploaded file; attached at Presenter, Presentation, Session, Location, or Event level |
|
||||
| Event Device | `event_device` | Registered Launcher kiosk (Electron native instance) |
|
||||
| Event Track | `event_track` | Optional content grouping (see note below) |
|
||||
|
||||
### Event Tracks
|
||||
|
||||
The API supports Event Tracks — an optional grouping layer above Sessions. Used twice
|
||||
historically; could have been omitted both times. Tracks may become genuinely useful for
|
||||
larger events running many parallel Locations where thematic grouping helps navigation.
|
||||
Not in active use currently and not wired into the standard Pres Mgmt UI workflow.
|
||||
|
||||
### Session → Location
|
||||
|
||||
The Launcher's primary display unit is the Location. It shows the active Session for that
|
||||
Location based on `datetime_start` / `datetime_end` or manual selection. A Location hosts
|
||||
many Sessions over the event's run; typically only one is active at a time.
|
||||
|
||||
---
|
||||
|
||||
## Client Setup Variation
|
||||
|
||||
There are no rigid "modes" — events are configured with as much or as little structure
|
||||
as needed. The platform handles the full range:
|
||||
|
||||
**Minimal setup (BGH):**
|
||||
Sessions have room and time info. No Presentations or Presenters defined.
|
||||
Staff upload files directly at the session or location level onsite.
|
||||
|
||||
**Mid-range setup:**
|
||||
Sessions defined with named Presentations. Presenters may or may not be tracked.
|
||||
Mix of pre-uploaded and onsite files. QR codes may be used for quick session/presenter lookup.
|
||||
|
||||
**Full setup (LCI):**
|
||||
Sessions, Presentations, Presenters all defined and managed. External ID labeling
|
||||
(e.g., "LCI Member ID"). Agreement tracking for presenters. Files managed per-presenter.
|
||||
Launcher linked from Pres Mgmt views.
|
||||
|
||||
The config that drives this is `event.mod_pres_mgmt_json` — see the Configuration section.
|
||||
|
||||
---
|
||||
|
||||
## Speaker Ready Room (SRR)
|
||||
|
||||
The Speaker Ready Room is a dedicated space where presenters check in and staff manage
|
||||
content before it goes live in the session rooms. Setup varies by client:
|
||||
|
||||
- **Small/private:** Only a few client staff and OSIT. Not open to presenters at large.
|
||||
- **Open SRR:** Open to all presenters as long as sessions are running. People come and go
|
||||
all day — reviewing silently, editing with a group, practicing at a station.
|
||||
|
||||
### SRR Practice Stations
|
||||
|
||||
Stations mirror the session room setup exactly:
|
||||
- Same Mac laptop model and adapter/dongle configuration as the podiums
|
||||
- Projector and screen (same as session rooms where possible)
|
||||
- Launcher running in Native (Electron) mode — cached files open immediately
|
||||
- Full dry-run capability: load their file, start the deck, confirm everything works
|
||||
|
||||
### Remote Monitoring
|
||||
|
||||
SRR staff typically monitor the session room Launchers in real time via **VNC or RustDesk**.
|
||||
This lets one person watch multiple podium displays simultaneously without being in each room.
|
||||
|
||||
### QR Codes (Session and Presenter)
|
||||
|
||||
QR codes are available for Sessions and Presenters and have been useful onsite for quick
|
||||
lookups — scanning a code takes staff directly to the session or presenter record.
|
||||
Whether to enable this depends on the SRR flow for each show. It gets toggled on or off
|
||||
per event via config.
|
||||
|
||||
### SRR Staffing Roles
|
||||
|
||||
| Role | Access Level | Typical Tasks |
|
||||
|---|---|---|
|
||||
| OSIT Staff | `trusted_access` or higher | Upload files, edit sessions/presentations, manage devices, monitor via VNC |
|
||||
| Client Staff | `authenticated_access` | Upload files, view session list |
|
||||
| Presenter (self-service) | `authenticated_access` (if enabled) | Upload their own files via QR link |
|
||||
|
||||
### SRR Workflow — Day-of-Show
|
||||
|
||||
1. **Presenter checks in** — staff looks up their session(s) in Pres Mgmt
|
||||
2. **File upload** — staff or presenter uploads file to the correct presenter/session record
|
||||
3. **File verification** — staff opens the file on a practice station to confirm it renders
|
||||
4. **Launcher sync** — file appears in the Launcher within the next polling cycle
|
||||
5. **Presenter proceeds to room** — podium kiosk already has the file cached
|
||||
|
||||
---
|
||||
|
||||
## File Upload Workflows
|
||||
|
||||
### Pre-Show (Remote / Staff Ahead of Time)
|
||||
|
||||
Files can be uploaded anytime before the event via the Pres Mgmt web UI:
|
||||
1. Navigate to the presenter, session, or appropriate level
|
||||
2. Use the file upload panel (drag & drop or browse)
|
||||
3. File is stored server-side and immediately available to the Launcher
|
||||
|
||||
Some clients enable presenter self-upload via a direct link (requires `authenticated_access`).
|
||||
Controlled per-event via config.
|
||||
|
||||
### Day Before — SRR Setup
|
||||
|
||||
For higher-volume shows, the SRR opens the day before the event:
|
||||
- Pre-uploaded files are already loaded and can be verified
|
||||
- Early-arriving presenters check in; staff upload their files
|
||||
- Electron Launcher instances on podium Macs begin pre-caching files overnight
|
||||
- Problems (corrupt files, wrong format, wrong codec) surface with time to fix them
|
||||
|
||||
### Live Onsite Upload
|
||||
|
||||
For late arrivals and last-minute changes:
|
||||
1. Presenter arrives at SRR (or sends file via USB/email to staff)
|
||||
2. Staff uploads via Pres Mgmt web UI
|
||||
3. File propagates to Launcher within one polling cycle (~30 seconds on fast networks)
|
||||
4. VNC or RustDesk confirms the podium received the file before the presenter walks in
|
||||
|
||||
---
|
||||
|
||||
## Onsite Operation — Managing 4–12 Parallel Rooms
|
||||
|
||||
### Overview Page
|
||||
|
||||
The Pres Mgmt overview (`/events/[event_id]/pres_mgmt`) shows:
|
||||
- All sessions, filterable by location and time
|
||||
- File status per session
|
||||
- Quick links to each session's file management
|
||||
|
||||
For events with multiple parallel rooms, filtering by location and time block is essential
|
||||
for SRR staff staying on top of what's active right now.
|
||||
|
||||
### Per-Room Workflow
|
||||
|
||||
Each room/location has its own Launcher display:
|
||||
- `/events/[event_id]/launcher` → select location → Launcher for that room
|
||||
- The Launcher shows the active session based on the current time or manual selection
|
||||
- VNC/RustDesk gives SRR staff a real-time view of all podiums simultaneously
|
||||
|
||||
### Session Display Timing
|
||||
|
||||
Ideally, sessions would automatically show and hide based on `datetime_start` /
|
||||
`datetime_end` — appearing a configurable number of minutes before the session starts
|
||||
and disappearing after it ends. This is a planned/desired behavior. In practice:
|
||||
|
||||
- Some clients run tight schedules and could rely on time-based transitions
|
||||
- Others drift significantly from the published schedule; time-based auto-advance
|
||||
would cause more problems than it solves
|
||||
- Currently, session transitions can be managed manually via Launcher controls
|
||||
|
||||
> **TODO (future):** Configurable `show_before_minutes` / `hide_after_minutes` per event
|
||||
> so well-run shows can automate transitions while looser shows stay manual.
|
||||
|
||||
### Device (Laptop) Assignment
|
||||
|
||||
Each Launcher kiosk Mac is registered as an `event_device` and typically assigned to one
|
||||
Location for the duration of the event. However, laptops do get moved:
|
||||
- Venues add or lose rooms as spaces are reconfigured
|
||||
- A session room may open for one day only
|
||||
- Devices can be reassigned to a different Location in the `event_device` record as needed
|
||||
|
||||
The Electron app reads its location assignment from the API at startup, so reassigning a
|
||||
device takes effect on the next launch (or app restart).
|
||||
|
||||
---
|
||||
|
||||
## Alert Fields
|
||||
|
||||
Sessions, Presenters, and Locations each have alert fields that can display a visible
|
||||
notice in the Pres Mgmt UI and/or the Launcher.
|
||||
|
||||
Useful for:
|
||||
- "Presenter requested no recording"
|
||||
- "Room change — moved to Hall B"
|
||||
- "File not received — follow up"
|
||||
- "AV note: needs confidence monitor"
|
||||
|
||||
> **Status:** Alert fields exist but the implementation and display behavior needs review
|
||||
> and cleanup. Not a blocking issue for BGH next week — revisit for a future show.
|
||||
|
||||
---
|
||||
|
||||
## Launcher Module
|
||||
|
||||
### Operational Modes
|
||||
|
||||
| Mode | Use Case | File Handling |
|
||||
|---|---|---|
|
||||
| **Default** | Browser on any machine | Files downloaded on demand |
|
||||
| **Onsite** | Browser on event network | Faster polling; browser-managed files |
|
||||
| **Native** | Electron app on dedicated podium Mac | Background pre-cache; atomic file handover |
|
||||
|
||||
For production onsite use, **Native mode on Mac laptops** is the target. The Electron
|
||||
app pre-caches all session files in the background so presentations open instantly without
|
||||
a network round-trip at the moment of launch.
|
||||
|
||||
### Native Mode — Electron App
|
||||
|
||||
- **Repo:** `~/OSIT_dev/aether_app_native_electron/`
|
||||
- **Platform:** macOS (primary); Linux/Windows as fallback
|
||||
- **Seed config:** `seed.json` (Device ID + API key) — loaded at startup
|
||||
- **File cache:** `~/Library/Caches/OSIT/file_cache/` (hashed by SHA-256)
|
||||
- **Doc:** `documentation/PROJECT__AE_Events_Launcher_Native_integration.md`
|
||||
|
||||
The Electron app zero-configs itself:
|
||||
1. Reads `seed.json` → gets device code
|
||||
2. Calls Aether API → pulls device config and location assignment
|
||||
3. Navigates directly to the Launcher for that location
|
||||
4. Begins pre-caching session files in the background
|
||||
|
||||
### Launcher Display Views
|
||||
|
||||
| View | Shown When |
|
||||
|---|---|
|
||||
| Session view | Active session with session-level files |
|
||||
| Presentation view | Active session with named presentations |
|
||||
| Presenter view | Presentation selected; shows presenter bio/photo |
|
||||
| Poster/group view | Special layout for poster sessions |
|
||||
| Screensaver | No active session; idle state |
|
||||
|
||||
### File Opening (Native Mode) — Safe Handover
|
||||
|
||||
1. Verify SHA-256 hash in permanent cache
|
||||
2. Atomic copy to system `[tmp]` directory
|
||||
3. Rename to original filename (e.g., `Abstract_101.pptx`)
|
||||
4. OS opens the file (Keynote, PowerPoint, Preview, etc.)
|
||||
|
||||
Versioning is handled automatically: when a presenter uploads an updated file, the new
|
||||
hash is cached separately and the old one remains intact.
|
||||
|
||||
---
|
||||
|
||||
## Configuration — `mod_pres_mgmt_json`
|
||||
|
||||
The event's Pres Mgmt behavior is controlled by `event.mod_pres_mgmt_json`.
|
||||
|
||||
> **Note:** The config schema is being cleaned up — see
|
||||
> `documentation/PROJECT__AE_Events_PressMgmt_Config_Cleanup.md` for the canonical
|
||||
> `PressMgmtRemoteCfg` interface and naming conventions.
|
||||
|
||||
### Convention
|
||||
|
||||
| Prefix | Default state | Meaning |
|
||||
|---|---|---|
|
||||
| `hide__` | `false` = visible | Feature is ON by default; set `true` to suppress |
|
||||
| `show__` | `false` = hidden | Feature is OFF by default; set `true` to enable |
|
||||
|
||||
### Common Config Keys
|
||||
|
||||
| Key | Default | Notes |
|
||||
|---|---|---|
|
||||
| `lock_config` | `false` | `true` = force remote→local sync; prevents user overrides of local config |
|
||||
| `hide__session_code` | `false` | Hide session code column/field |
|
||||
| `hide__session_description` | `false` | Hide session description field |
|
||||
| `hide__session_location` | `false` | Hide location field on session view |
|
||||
| `hide__session_datetime` | `false` | Hide datetime fields |
|
||||
| `hide__presentation_code` | `false` | Hide presentation code |
|
||||
| `hide__presenter_code` | `false` | Hide presenter code |
|
||||
| `hide__location_code` | `false` | Hide location code |
|
||||
| `show__launcher_link` | `false` | Show direct Launcher link in session view |
|
||||
| `show__session_qr` | `false` | Show QR code for session (SRR lookup) |
|
||||
| `show__presenter_qr` | `false` | Show QR code for presenter (SRR lookup) |
|
||||
| `label__person_external_id` | `null` | Override label for external ID field (e.g., `"Member ID"`) |
|
||||
| `label__session_poc_name` | `null` | Override label for session POC (e.g., `"Champion"`) |
|
||||
| `file_purpose_option_kv` | `{}` | Key-value map of file purpose options (e.g., `{"ppt": "PowerPoint", "pdf": "PDF"}`) |
|
||||
|
||||
### Per-Show Config Examples
|
||||
|
||||
**BGH (session-only, minimal; no named Presentations or Presenters):**
|
||||
```json
|
||||
{
|
||||
"lock_config": false,
|
||||
"hide__presentation_code": true,
|
||||
"hide__presenter_code": true
|
||||
}
|
||||
```
|
||||
|
||||
**LCI (full setup, member ID label, Launcher link enabled):**
|
||||
```json
|
||||
{
|
||||
"lock_config": true,
|
||||
"label__person_external_id": "LCI Member ID",
|
||||
"show__launcher_link": true
|
||||
}
|
||||
```
|
||||
|
||||
> Admin must currently edit `mod_pres_mgmt_json` directly in the DB or via the event
|
||||
> settings page. A proper Config UI is planned — see `PROJECT__AE_Events_PressMgmt_Config_Cleanup.md`.
|
||||
|
||||
---
|
||||
|
||||
## Route Map
|
||||
|
||||
| URL | Purpose |
|
||||
|---|---|
|
||||
| `/events/[id]/pres_mgmt` | Overview — sessions list, search, filter by location |
|
||||
| `/events/[id]/pres_mgmt/config` | Config editor (admin only) |
|
||||
| `/events/[id]/session/[session_id]` | Session detail — files, presentations, timing, alert |
|
||||
| `/events/[id]/presenter/[presenter_id]` | Presenter detail — bio, files, agreement, alert |
|
||||
| `/events/[id]/location/[location_id]` | Location detail — session schedule for this room, alert |
|
||||
| `/events/[id]/locations` | All locations list |
|
||||
| `/events/[id]/reports` | Reports — sessions, presenters, files |
|
||||
| `/events/[id]/launcher` | Launcher home — select location |
|
||||
| `/events/[id]/launcher/[location_id]` | Launcher display for a specific room |
|
||||
|
||||
---
|
||||
|
||||
## Device Management
|
||||
|
||||
Each Electron kiosk is registered as an `event_device` record:
|
||||
- `code` — matches the device's `seed.json` code
|
||||
- `name` — human-readable (e.g., "Ballroom A Podium")
|
||||
- `data_json.location_id` — the `event_location_id` this device is assigned to
|
||||
|
||||
Devices can be managed in Pres Mgmt (`/events/[id]/device/device`). Location reassignment
|
||||
takes effect on the next Electron app launch.
|
||||
|
||||
---
|
||||
|
||||
## Access Levels
|
||||
|
||||
| Feature | Minimum Access |
|
||||
|---|---|
|
||||
| View pres_mgmt overview | `authenticated_access` |
|
||||
| Upload files | `authenticated_access` |
|
||||
| Edit sessions / presentations | `trusted_access` |
|
||||
| Edit config | `administrator_access` + `edit_mode` |
|
||||
| View Launcher display | `authenticated_access` |
|
||||
| Manual session selection in Launcher | `trusted_access` |
|
||||
| Device management | `administrator_access` |
|
||||
|
||||
---
|
||||
|
||||
## Pre-Show Checklist
|
||||
|
||||
### 1–2 Weeks Before
|
||||
|
||||
- [ ] Event created in Aether with correct dates
|
||||
- [ ] `mod_pres_mgmt_json` configured for this client's needs
|
||||
- [ ] Locations (rooms) created and named
|
||||
- [ ] Sessions created, assigned to locations, datetime ranges set
|
||||
- [ ] If using Presentations/Presenters: records imported or entered
|
||||
- [ ] File purpose options configured in `file_purpose_option_kv`
|
||||
- [ ] Launcher devices registered (`event_device` records with correct codes)
|
||||
- [ ] Device-to-location assignments confirmed
|
||||
- [ ] Decide: QR codes for Sessions / Presenters needed? Enable/disable in config
|
||||
|
||||
### Day Before (SRR Setup)
|
||||
|
||||
- [ ] Mac laptops at podiums booted and Electron app running
|
||||
- [ ] Each podium confirms it loaded the correct location's Launcher
|
||||
- [ ] SRR practice stations confirmed — projector, same Mac/dongle setup as session rooms
|
||||
- [ ] Pre-loaded files verified in Launcher (open at least one per room to test Safe Handover)
|
||||
- [ ] SRR staff briefed on upload workflow and VNC/RustDesk monitoring setup
|
||||
- [ ] VNC/RustDesk connections established to all podium displays
|
||||
|
||||
### Day of Show
|
||||
|
||||
- [ ] Confirm all session times in Aether are accurate before first session
|
||||
- [ ] Monitor SRR queue — upload files as presenters check in
|
||||
- [ ] Verify each file opens on a practice station before the presenter walks to their room
|
||||
- [ ] Monitor podium displays via VNC/RustDesk — flag any stuck or offline devices
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| Session not showing in Launcher | Session datetime wrong or location not assigned | Verify session location and datetime range |
|
||||
| File uploaded but not in Launcher | Polling cycle lag; or file attached at wrong level | Wait one cycle; check that file is attached to session/location (not just a presenter record) if using session-only setup |
|
||||
| Electron app shows wrong location | Device code mismatch or stale device config | Re-check `event_device` record; restart Electron app |
|
||||
| File opens slowly at podium | Not in native cache yet | Check background sync in Launcher; pre-cache may not have completed |
|
||||
| File won't open | Corrupt upload, wrong format, or missing codec on Mac | Test on SRR practice station; re-upload or convert |
|
||||
| Session out of sync with schedule | Timing drifted; manual advance needed | Use Launcher controls to manually select the current session |
|
||||
| Alert field not showing | Alert fields need implementation review | Known — lower priority than active operations |
|
||||
| `lock_config: true` resets local changes | Expected behavior — remote config wins | Change the remote config in `mod_pres_mgmt_json` |
|
||||
| Device needs to move to different room | Location reassigned mid-event | Update `data_json.location_id` on `event_device` record; restart Electron app on that machine |
|
||||
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,180 +0,0 @@
|
||||
# Aether Events Launcher: Native Electron Integration
|
||||
|
||||
> **Status:** Operational / Phase 5 Implementation
|
||||
> **Last Updated:** 2026-03-11
|
||||
> **Primary Platform:** macOS (Darwin)
|
||||
> **Fallback Platform:** Linux / Windows
|
||||
|
||||
## 1. Overview
|
||||
The Aether Events Launcher utilizes an Electron-based "Native Shell" to provide OS-level capabilities that are normally restricted by browser sandboxing. This enables persistent file caching, direct control of presentation software (Keynote, PowerPoint), and hardware telemetry.
|
||||
|
||||
### Operational Modes
|
||||
| Mode | Purpose | File Handling |
|
||||
| :--- | :--- | :--- |
|
||||
| **Default** | Standard web browser access. | Direct downloads; no local caching. |
|
||||
| **Onsite** | Web access on event networks. | Faster polling; browser-based file management. |
|
||||
| **Native** | Dedicated Podium Kiosk (Electron). | Full background pre-caching; atomic safe-handover. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture: The Three-Layer Bridge
|
||||
|
||||
The integration is built on a decoupled three-layer communication model to ensure security and cross-platform flexibility.
|
||||
|
||||
### 2.1 Layer 1: The Engine (Main Process)
|
||||
- **Repo:** `~/OSIT_dev/aether_app_native_electron/` (separate git repo)
|
||||
- **File:** `aether_app_native_electron/src/main/*.ts`
|
||||
- **Role:** Performs the heavy lifting (Filesystem, Shell, AppleScript).
|
||||
- **Responsibilities:**
|
||||
- Managing the **Hashed Cache** directory.
|
||||
- Executing `osascript` intents for presentation control.
|
||||
- Spawn/Kill process management.
|
||||
|
||||
### 2.2 Layer 2: The Gatekeeper (Preload Script)
|
||||
- **Namespace:** `window.aetherNative`
|
||||
- **Role:** Securely exposes whitelisted IPC channels to the Renderer.
|
||||
- **Standards:** Uses `contextBridge.exposeInMainWorld` to prevent arbitrary code execution.
|
||||
|
||||
### 2.3 Layer 3: The Messenger (SvelteKit Relay)
|
||||
- **File:** `src/lib/electron/electron_relay.ts`
|
||||
- **Role:** Provides a clean, typed API for Svelte components.
|
||||
- **Responsibilities:**
|
||||
- Mapping `camelCase` UI triggers to `snake_case` IPC calls.
|
||||
- Implementing "Smart Fallbacks" (e.g., resolving `[home]` placeholders if the bridge is partially hydrated).
|
||||
|
||||
---
|
||||
|
||||
## 3. The "Zero-Config" Lifecycle
|
||||
|
||||
To support rapid onsite deployment, the native app requires zero manual setup.
|
||||
|
||||
1. **Seed:** On launch, the Main process reads a local `seed.json` (Device ID + API Key).
|
||||
2. **Identity:** Calls `GET /v3/data_store/code/{device_code}` or `GET /v3/crud/event_device/{id}` to pull operational context.
|
||||
3. **Hydrate:** Authenticates with the Aether V3 API and injects the **JWT** and **Device Config** into the UI environment.
|
||||
4. **Launch:** Navigates the SvelteKit frontend directly to the assigned Event Launcher route.
|
||||
|
||||
---
|
||||
|
||||
## 4. Podium Reliability Protocol
|
||||
|
||||
The system is designed to ensure that a presentation never fails due to network instability.
|
||||
|
||||
### 4.1 Hashed Cache Pattern
|
||||
Files are stored persistently using their SHA-256 hash to prevent filename collisions and handle versioning.
|
||||
- **Root:** `~/Library/Caches/OSIT/file_cache/`
|
||||
- **Subdirectory:** First 2 characters of hash (e.g., `ab/`)
|
||||
- **Filename:** `{hash}.file`
|
||||
|
||||
### 4.2 Background Sync (File Warming)
|
||||
When a user navigates to a session in the Launcher UI, the `LauncherBackgroundSync` component:
|
||||
1. Extracts all `event_file_id` values for that session.
|
||||
2. Checks the native cache via `aetherNative.check_cache`.
|
||||
3. Triggers background downloads for missing files via `aetherNative.download_to_cache`.
|
||||
|
||||
### 4.3 Safe Handover (Launch Sequence)
|
||||
When a user clicks "Open", the system follows a non-destructive sequence:
|
||||
1. **Verify:** Confirm hash exists in the permanent cache.
|
||||
2. **Copy:** Create an atomic copy in the system `[tmp]` directory.
|
||||
3. **Restore:** Rename the copy to its original filename (e.g., `Abstract_101.pptx`).
|
||||
4. **Execute:** Launch the file via the OS.
|
||||
|
||||
---
|
||||
|
||||
## 5. Automation & Actuators (Phase 5)
|
||||
|
||||
The native shell provides specialized handlers for controlling the "Podium Experience."
|
||||
|
||||
### 5.1 Presentation Acts
|
||||
| Action | Handler | Actuator (macOS) |
|
||||
| :--- | :--- | :--- |
|
||||
| **Launch** | `launch_presentation` | `open` or `osascript` (slideshow start) |
|
||||
| **Control** | `control_presentation` | `osascript` (next/prev slide) |
|
||||
| **Clean Up**| `kill_processes` | `killall -INT` (graceful exit) |
|
||||
|
||||
### 5.2 System Management
|
||||
- **Telemetry:** Pushes `cpu_usage`, `memory_free_gb`, and `foreground_app` via heartbeats using the `get_device_info` relay.
|
||||
- **Self-Update (Roadmap):** Plan to monitor Syncthing `admin_share` for newer `.app` versions and perform atomic swaps.
|
||||
|
||||
### 5.3 Implemented Actuators (Phase 5 Complete)
|
||||
- **Recording:** `manage_recording({action})` — Aperture session capture (`start`, `stop`, `status`). macOS only.
|
||||
- **Display Layouts:** `set_display_layout({mode})` — Mirror / Extend via `displayplacer`. macOS only.
|
||||
- **Power Control:** `power_control({action})` — Shutdown, reboot, sleep. macOS + Linux.
|
||||
- **Window Control:** `window_control({action})` — Maximize, minimize, fullscreen, kiosk mode.
|
||||
- **Wallpaper:** `set_wallpaper({path})` — macOS (AppleScript) + Linux (gsettings).
|
||||
|
||||
> **Note:** `update_app` is implemented as a stub — downloads but does not install. Not yet functional for end users.
|
||||
|
||||
---
|
||||
|
||||
## 6. Launcher Configuration & Management
|
||||
|
||||
The Launcher features a standardized, responsive configuration interface designed for onsite technical management.
|
||||
|
||||
### 6.1 UI Architecture
|
||||
- **Tabbed Navigation:** Categorized into System, Sync, and General settings.
|
||||
- **Section Wrapper (`Launcher_Cfg_Section`):** A shared component providing a consistent header, icon, and responsive grid container.
|
||||
|
||||
### 6.2 3-Way State Logic
|
||||
To manage screen real estate on varying laptop resolutions, all configuration sections utilize a 3-way visibility state:
|
||||
- **`collapsed`**: Content is hidden.
|
||||
- **`auto`**: Expanded by default, but automatically closes if another "auto" section is opened.
|
||||
- **`pinned`**: Expanded and remains open regardless of other section interactions.
|
||||
|
||||
### 6.3 Technical Mode (`edit_mode`)
|
||||
The UI dynamically filters fields based on the user's focus. Enabling Technical Mode (`$ae_loc.edit_mode`) reveals advanced diagnostic and writeable fields.
|
||||
|
||||
| Category | Standard View (Read-Only) | Technical Mode (Read/Write) |
|
||||
| :--- | :--- | :--- |
|
||||
| **Health** | Heartbeat, RAM Usage, Sync Stats | Hostname, IP List, Raw Device JSON |
|
||||
| **OS Bridge** | Folder Buttons, Recording Toggle | Manual Terminal Commands, Reset Wallpaper |
|
||||
| **Sync** | Sync Completion Status | Millisecond Timers, Cache Prefix Logic |
|
||||
| **Update** | Current Version Status | Manual Update Paths, URL Overrides |
|
||||
|
||||
---
|
||||
|
||||
## 7. Implementation Reference (IPC Whitelist)
|
||||
|
||||
All functions below are exported from `src/lib/electron/electron_relay.ts` and safely
|
||||
no-op when `window.aetherNative` is not present (i.e., in browser/non-native mode).
|
||||
|
||||
### Config & Info
|
||||
- `get_device_config()` — Returns hydrated device settings injected by the native shell on startup.
|
||||
- `get_device_info()` — Returns OS metadata, IP list, hostname, and path placeholders (`[home]`, `[tmp]`).
|
||||
|
||||
### File Cache
|
||||
- `check_hash_file_cache({cache_root, hash, hash_prefix_length?})` — Verifies a file exists in the local hashed cache.
|
||||
- `download_to_cache({url, cache_root, hash, api_key, account_id, hash_prefix_length?})` — Streams a file download to the hashed cache with SHA-256 integrity check.
|
||||
- `launch_from_cache({cache_root, hash, temp_root, filename, hash_prefix_length?})` — Atomic "Safe Handover": copy from cache → tmp → rename → execute.
|
||||
- `cleanup_tmp_files({cache_root, max_age_minutes?})` — Removes stale `*.tmp` download artifacts. Default: 1440 min (24h). Called at launcher startup.
|
||||
|
||||
> `hash_prefix_length` defaults to `2` throughout. Do not change without coordinating all devices — mismatched values create orphaned cache subdirectories.
|
||||
|
||||
### Shell & OS
|
||||
- `open_folder(path)` — Opens a path in the OS file manager.
|
||||
- `run_cmd({cmd, timeout?, return_stdout?})` — Async shell command execution.
|
||||
- `run_cmd_sync({cmd, return_stdout?})` — Synchronous shell command execution.
|
||||
- `run_osascript(script)` — Executes an AppleScript string. macOS only.
|
||||
- `kill_processes({process_name_li})` — Gracefully terminates processes by name.
|
||||
- `open_local_file_v2(path)` — Opens a file with its default OS application.
|
||||
|
||||
### Presentations (Phase 5)
|
||||
- `launch_presentation({path, app?, os?})` — Platform-aware launcher. macOS: PowerPoint/Keynote via AppleScript. Linux: LibreOffice Impress. Resolves `[home]`/`[tmp]` placeholders.
|
||||
- `control_presentation({app, action})` — Slide navigation (`next`/`prev`/`start`/`stop`) for PowerPoint or Keynote via AppleScript.
|
||||
|
||||
### System Management (Phase 5)
|
||||
- `set_wallpaper({path})` — Sets desktop wallpaper. macOS (AppleScript) + Linux (gsettings).
|
||||
- `window_control({action, value?})` — Electron window management: maximize, minimize, fullscreen, kiosk.
|
||||
- `set_display_layout({mode, configStr?})` — Mirror or extend displays via displayplacer. macOS only.
|
||||
- `power_control({action})` — Shutdown, reboot, or sleep the host machine. macOS + Linux.
|
||||
- `manage_recording({action, options?})` — Aperture capture control (`start`/`stop`/`status`). macOS only.
|
||||
- `open_external({url, app?})` — Opens a URL in Chrome, Firefox, or the default browser.
|
||||
- `update_app(args)` — **Stub only.** Downloads but does not install. Not yet functional.
|
||||
- `list_tools()` — Returns a self-documenting manifest of all available native bridge functions.
|
||||
|
||||
### Path Placeholders
|
||||
All paths passed to native handlers should use tokens rather than hardcoded OS paths:
|
||||
- `[home]` — Resolved to the user's home directory by the native bridge.
|
||||
- `[tmp]` — Resolved to the system temporary directory.
|
||||
|
||||
### Not Exposed via Relay (intentional)
|
||||
- `get_seed_config` / `get_jwt` — Exposed in the preload but not relayed to the UI. The JWT and seed are injected into the environment at startup; components should not call these directly.
|
||||
@@ -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:** 🟡 ~70% Complete — Core Working, Cleanup Pass Needed
|
||||
**Priority:** Medium (core features functional; remaining work is deprecation + consistency)
|
||||
**Created:** 2026-04-02
|
||||
**Last Updated:** 2026-06-12 (regression audit)
|
||||
**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
|
||||
@@ -135,7 +136,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 +163,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
|
||||
@@ -201,15 +202,27 @@ 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
|
||||
- [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` ⚠️ **Missing:** version gate in `store_versions.ts`
|
||||
- [x] **Step 3** — Rewrite `sync_config__event_pres_mgmt()` in `ae_events__event.ts` to use canonical keys ⚠️ **Issue:** `show__launcher_link_legacy` hard-coded instead of synced from remote
|
||||
- [x] **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) **BLOCKING**
|
||||
- [x] **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 (blocked on Step 5)
|
||||
- [ ] **Step 8** — `npx svelte-check` clean; commit
|
||||
|
||||
### Regression Fixes Needed (2026-06-12 Audit)
|
||||
|
||||
- [ ] **Add `show__launcher_link_legacy` to `PressMgmtRemoteCfg`** or remove entirely if deprecated
|
||||
- Currently hard-coded to `true` in sync function (line 1054 `ae_events__event.ts`)
|
||||
- Can't be controlled via config UI
|
||||
- [ ] **Resolve `hide__launcher_link*` local/remote conflict**
|
||||
- Menu toggles ([ae_comp__events_menu_opts.svelte](../src/routes/events/ae_comp__events_menu_opts.svelte) lines 462-494) use `hide__launcher_link` for LOCAL UI state
|
||||
- Remote schema uses `show__launcher_link` (inverted)
|
||||
- Decision: Keep separate? Document clearly? Unify?
|
||||
- [ ] **Add `AE_PRES_MGMT_LOC_VERSION` to `store_versions.ts`** (Step 2 requirement)
|
||||
- [ ] **Clean `hide__launcher_link*` from defaults** if truly deprecated (lines 154-155, 333-334 in `pres_mgmt_defaults.ts`)
|
||||
|
||||
### Step 6 scope (mechanical find-replace)
|
||||
|
||||
The `$events_loc.pres_mgmt` pattern appears across:
|
||||
|
||||
@@ -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,122 @@
|
||||
# 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`)
|
||||
```ts
|
||||
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, {
|
||||
serializer: {
|
||||
serialize: JSON.stringify,
|
||||
// Merge with defaults so new fields added after first session get their defaults.
|
||||
deserialize: (raw: string) => ({ ...badges_loc_defaults, ...JSON.parse(raw) })
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
570
documentation/PROPOSAL__AE_UI_UX_Future_Ideas.md
Normal file
570
documentation/PROPOSAL__AE_UI_UX_Future_Ideas.md
Normal file
@@ -0,0 +1,570 @@
|
||||
# Aether UI/UX — Future Ideas
|
||||
|
||||
> Collection of concrete UX improvements for the Aether frontend. Each entry includes
|
||||
> the rationale, current behavior, proposed change, and implementation notes.
|
||||
> **Created:** 2026-05-17
|
||||
> **Last Updated:** 2026-05-18
|
||||
|
||||
---
|
||||
|
||||
## IDAA Recovery Meetings
|
||||
|
||||
### 1. Guided empty state with active filters — ✅ Implemented 2026-05-18
|
||||
|
||||
**Current behavior:** When filters return 0 results, the page shows:
|
||||
"No recovery meetings found matching your criteria."
|
||||
The member has no indication whether this is a bug, genuinely no data, or just
|
||||
overly narrow filters.
|
||||
|
||||
**Proposed change:** When filters are active AND the result count is 0, show a
|
||||
helpful prompt instead of the bare message:
|
||||
|
||||
```
|
||||
No meetings found for these filters.
|
||||
Try broadening your search or [Clear all filters →]
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- Add a `has_active_filters` derived in `+page.svelte` that checks whether any of
|
||||
`qry__physical`, `qry__virtual`, `qry__type`, or `qry__fulltext_str` is set.
|
||||
- In the template's `{:else}` block (line ~443), branch on `has_active_filters`:
|
||||
- `true` → show the guided message + "Clear Filters" button
|
||||
- `false` → show the existing escape-hatch flow (timed "Refresh Meeting Cache" button
|
||||
after 8 seconds, since zero unfiltered results always indicates a problem)
|
||||
- The "Clear Filters" button resets all four filter fields to `null`/`''` and bumps
|
||||
`search_version` to trigger a fresh unfiltered search.
|
||||
- Distinct from the `error` state — this is a successful search (`qry__status === 'done'`)
|
||||
with an empty result set.
|
||||
|
||||
**Implemented:** `src/routes/idaa/(idaa)/recovery_meetings/+page.svelte`. `has_active_filters`
|
||||
derived checks `qry__physical`, `qry__virtual`, `qry__type`, and `qry__fulltext_str`. Empty
|
||||
state branches on `has_active_filters`: active filters → guided message + "Clear Filters"
|
||||
button; no active filters → existing escape-hatch flow (timed "Refresh Meeting Cache" after
|
||||
8 seconds).
|
||||
|
||||
---
|
||||
|
||||
### 2. Quick-filter chips below the search bar — ✅ Implemented 2026-05-18
|
||||
|
||||
**Current behavior:** Members toggle filters via small checkboxes (Virtual, In-person)
|
||||
and radio buttons (All, IDAA, Caduceus, Family Recovery). These require precise
|
||||
mouse/tap targeting and scanning several lines of filter UI to discover and use.
|
||||
|
||||
**Proposed change:** Add a row of preset chip buttons directly below the search input:
|
||||
|
||||
```
|
||||
[🖥 Virtual] [🏠 In-Person] [🩺 IDAA] [Caduceus] [Family Recovery] [All Types]
|
||||
```
|
||||
|
||||
- Each chip toggles the corresponding filter (`qry__virtual`, `qry__physical`, `qry__type`)
|
||||
and triggers an immediate search.
|
||||
- Selected chips get a filled/pressed style; unselected chips are outlined.
|
||||
- "All Types" is the default selected state (no type filter). Clicking another type
|
||||
chip deselects "All Types" (radio behavior for the type dimension). Virtual and
|
||||
In-person are independent toggles (checkbox behavior — can select both).
|
||||
- The existing checkboxes/radio buttons remain as the underlying state storage
|
||||
(`$idaa_loc.recovery_meetings.*`). The chips are a convenience layer — they write
|
||||
to the same store fields and call `handle_search_trigger()`.
|
||||
|
||||
**Implementation notes:**
|
||||
- Place in `ae_idaa_comp__event_obj_qry.svelte` between the search input row and the
|
||||
current filter rows.
|
||||
- Optionally hide the existing checkbox/radio filter rows when the chips are present
|
||||
(or keep both — the checkboxes serve as accessible form controls; the chips are
|
||||
the primary visual interaction).
|
||||
- On mobile, chips wrap to a second row naturally with `flex-wrap`.
|
||||
|
||||
**Implemented:** `ae_idaa_comp__event_obj_qry.svelte`. Chips replaced the old
|
||||
checkbox/radio/select UI entirely rather than layering on top. Two chip rows:
|
||||
Row 1 — My Meetings (first), Virtual, In-Person. Row 2 — All / IDAA / Caduceus /
|
||||
Family Recovery type chips. Cycling sort button replaces separate sort options
|
||||
(see item below). Max Results uses a +/− stepper. Sort and max are in a third
|
||||
row below the chips, inside the same `<form>` constraint.
|
||||
|
||||
---
|
||||
|
||||
### 3. Language: "Searching..." vs "Loading..."
|
||||
|
||||
**Current behavior:** The loading state always shows the same message:
|
||||
|
||||
```
|
||||
🔄 Searching...
|
||||
```
|
||||
|
||||
This appears on initial page load (when the user hasn't typed anything) and after
|
||||
the user clicks Search or toggles a filter. The word "Searching" implies the user
|
||||
initiated a search, which is misleading on initial page load — it's a cold cache
|
||||
load, not an active search.
|
||||
|
||||
**Proposed change:** Distinguish the two loading contexts:
|
||||
|
||||
| Context | Message |
|
||||
|---------|---------|
|
||||
| Initial page load (no filters, no search text) | "Loading meetings..." |
|
||||
| User clicked Search or toggled a filter | "Searching..." (keep current) |
|
||||
|
||||
**Implementation notes:**
|
||||
- In `+page.svelte` template around line 422, check whether `qry__fulltext_str` is
|
||||
empty AND no filter checkboxes/radios are active. If so, show "Loading meetings...";
|
||||
otherwise show "Searching...".
|
||||
- This is purely a label change — no logic changes needed. The condition can be the
|
||||
same `has_active_filters` derived from item #1.
|
||||
- Also update the list component's standalone loading state in
|
||||
`ae_idaa_comp__event_obj_li.svelte` line 556-558 to use the same distinction.
|
||||
|
||||
---
|
||||
|
||||
### 4. Filter row collapsing on mobile
|
||||
|
||||
**Current behavior:** The query bar has three filter rows (Location checkboxes,
|
||||
Type radios, Max/Sort selects) plus the search input row and the action button row.
|
||||
Combined, this takes roughly 200px of vertical space. On mobile — especially inside
|
||||
the Novi iframe on a phone — meeting cards are pushed below the fold.
|
||||
|
||||
**Proposed change:** On viewports below `md` (768px), collapse the Location and Type
|
||||
filter rows behind a "Filters ▾" toggle. The Max Results and Sort selects stay visible
|
||||
since they're used frequently. The action buttons (Show Hidden, Create Meeting, Export)
|
||||
move inside the collapsed panel or stay visible based on available width.
|
||||
|
||||
```
|
||||
[Search input............................] [Search]
|
||||
|
||||
[Filters ▾] [Max: 150 ▾] [Sort: Last Updated ▾]
|
||||
```
|
||||
|
||||
Clicking "Filters ▾" expands the panel with Location checkboxes and Type radios.
|
||||
|
||||
**Implementation notes:**
|
||||
- Use a `$state` boolean `show_filters` (session-only, resets on page load).
|
||||
- Wrap the filter rows in a `{#if show_filters}` block.
|
||||
- Persist in `$idaa_sess.recovery_meetings.show_filters_expanded` if you want the
|
||||
state to survive navigation within the module (same tab session).
|
||||
- The Tailwind `md:` breakpoint works for the collapse trigger: `class:hidden={!show_filters}`
|
||||
combined with `class:md:block` to always show on desktop.
|
||||
- Test inside the Novi iframe — Bootstrap v3 may add its own `hidden` behavior on
|
||||
`md` breakpoints that conflicts with Tailwind's.
|
||||
|
||||
---
|
||||
|
||||
### 5. Human-readable schedule line on cards
|
||||
|
||||
**Current behavior:** The meeting card displays weekdays as a flat, dense span list:
|
||||
|
||||
```
|
||||
Sunday Monday Wednesday Friday
|
||||
```
|
||||
|
||||
The timezone is shown separately as `(America/Chicago)`, and the start time is in
|
||||
a compact `7:00 PM` format. These three pieces of information are visually separated
|
||||
and require the member to mentally assemble the schedule.
|
||||
|
||||
**Proposed change:** Render a computed one-liner that combines them:
|
||||
|
||||
```
|
||||
🕐 Mondays, Wednesdays, Fridays at 7:00 PM CT
|
||||
```
|
||||
|
||||
- Weekday names are built from the `weekday_*` booleans on the event object.
|
||||
- "Mondays, Wednesdays" uses the range-joining convention (comma-separated, "and"
|
||||
before the last item for two days; "Mondays through Fridays" for consecutive spans
|
||||
of 3+ days).
|
||||
- Timezone abbreviation is extracted from `timezone` (e.g., `America/Chicago` → `CT`,
|
||||
`America/New_York` → `ET`). A small lookup table handles the common ones; fall back
|
||||
to the raw timezone string for unknown values.
|
||||
- If `timezone` is null/missing, fall back to the current flat display — don't
|
||||
silently drop information.
|
||||
- Today's meetings could optionally get a subtle "Today" badge or highlight (extra
|
||||
polish, not required for the initial version).
|
||||
|
||||
**Implementation notes:**
|
||||
- Add a `$derived` in `ae_idaa_comp__event_obj_li.svelte` that computes the schedule
|
||||
string from the event object's `weekday_*` fields, `recurring_start_time`, and
|
||||
`timezone`.
|
||||
- Helper function in `ae_util` for the weekday list → natural language string
|
||||
(e.g., `['Monday', 'Wednesday', 'Friday']` → `"Mondays, Wednesdays, and Fridays"`).
|
||||
- Helper function or small lookup for timezone → abbreviation.
|
||||
- Fall back to the current flat display when `timezone` is missing to avoid losing
|
||||
information.
|
||||
|
||||
---
|
||||
|
||||
### 6. Show result count during search, not just after
|
||||
|
||||
**Current behavior:** The result count badge ("Results: 25") only appears inside the
|
||||
list wrapper component (`ae_idaa_comp__event_obj_li.svelte` line 98-108) when the
|
||||
visible result list is non-empty. During loading, the user sees only a spinner with
|
||||
no indication of how many meetings exist or what the search is operating on.
|
||||
|
||||
**Proposed change:** Show a result count line at the page level (in `+page.svelte`)
|
||||
that is always visible once the first search completes:
|
||||
|
||||
```
|
||||
25 of 140 meetings ← after search completes, with result count + total
|
||||
Searching 140 meetings... ← during initial load (cold cache, no prior result)
|
||||
0 results for these filters ← empty but filters are active (ties into item #1)
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- Lift the count display from the list component to `+page.svelte`, placed between
|
||||
the query bar (`Comp__event_obj_qry`) and the list wrapper.
|
||||
- The total count is available from the IDB fast path: after the initial unfiltered
|
||||
search populates `db_events.event`, the total is `db_events.event.count()` (or
|
||||
the count of records matching `account_id`).
|
||||
- The visible count is `event_id_li.length` after search completes.
|
||||
- Store the last known total in a `$state` variable so it persists across searches
|
||||
(the total changes infrequently). Refresh the total on the first search after
|
||||
page load.
|
||||
- Format: `{visible} of {total} meetings` when filters/search are active;
|
||||
`{visible} meetings` when browsing all (no active filters).
|
||||
- During loading with no prior results: show "Loading meetings..." (from item #3)
|
||||
rather than a count.
|
||||
|
||||
---
|
||||
|
||||
### 7. "Live Now" and "Starting Soon" indicators
|
||||
|
||||
**Current behavior:** Meetings are shown in a static list. To find one happening
|
||||
now, a member must scan the "When" line of multiple cards and compare the time
|
||||
to their own clock.
|
||||
|
||||
**Proposed change:** Add a high-visibility badge or pulse indicator for meetings
|
||||
that are currently in progress or starting in the next 15 minutes.
|
||||
|
||||
- "LIVE NOW" (Green pulse badge) → if `current_time` is within `[start, start + 1 hour]`.
|
||||
- "STARTING SOON" (Yellow badge) → if `current_time` is within `[start - 15 min, start]`.
|
||||
- On the card, move the "Join Zoom" or "Join Jitsi" button to the very top or
|
||||
make it significantly larger when the meeting is live.
|
||||
|
||||
**Implementation notes:**
|
||||
- Add a `$derived` state `is_live` and `is_starting_soon` to the card component.
|
||||
- Requires calculating "current time in meeting's timezone" using `Temporal` or
|
||||
a date helper.
|
||||
- Ensure the pulse animation is subtle and respects `prefers-reduced-motion`.
|
||||
|
||||
---
|
||||
|
||||
### 8. Local Timezone Conversion
|
||||
|
||||
**Current behavior:** Meetings show their native timezone (e.g., "7:00 PM America/Chicago").
|
||||
The "Your TZ" line is currently a placeholder and doesn't perform conversion.
|
||||
|
||||
**Proposed change:** Automatically detect the member's browser timezone and
|
||||
show the converted time if it differs from the meeting's native timezone.
|
||||
|
||||
```
|
||||
🕐 7:00 PM CT (8:00 PM ET your time)
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- Use `Intl.DateTimeFormat().resolvedOptions().timeZone` to get the user's TZ.
|
||||
- If `user_tz !== meeting_tz`, perform the conversion.
|
||||
- If the conversion results in a different day (e.g., late night ET vs early morning Europe),
|
||||
prefix with "Tomorrow at..." or "Yesterday at...".
|
||||
|
||||
---
|
||||
|
||||
### 9. Favorites / "My Meetings" — ✅ Implemented 2026-05-18
|
||||
|
||||
**Current behavior:** Members scan the full list every time they want to find
|
||||
their regular weekly meeting.
|
||||
|
||||
**Proposed change:** Add a "Star" icon to every meeting card.
|
||||
- Starring a meeting adds it to a `favorites` list stored in `$idaa_loc`.
|
||||
- Favorited meetings are pinned to the top of the list by default, regardless
|
||||
of other sort orders.
|
||||
- Add a "Favorites" filter toggle in the query bar to show *only* starred meetings.
|
||||
|
||||
**Implementation notes:**
|
||||
- Store as an array of `event_id` strings in `$idaa_loc.recovery_meetings.favorites`.
|
||||
- Update the `visible_event_obj_li` derived in `+page.svelte` to prioritize
|
||||
these IDs in the sort logic.
|
||||
|
||||
**Implemented:** Star toggle on the `[event_id]` detail page
|
||||
(`src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.svelte`).
|
||||
"My Meetings" filter chip is first in the filter chip row on the list page.
|
||||
**Implementation differs from proposal:** favorites stored server-side in a
|
||||
`data_store` record (code: `idaa_meetings_favorites`) as a UUID-keyed JSON map
|
||||
rather than in `$idaa_loc` — this means favorites persist across browsers and
|
||||
devices without Novi write capability. Pinning favorites to the top of the list
|
||||
was not implemented; the filter chip shows only favorites instead.
|
||||
|
||||
---
|
||||
|
||||
### 10. "Add to Calendar" (iCal / Google)
|
||||
|
||||
**Current behavior:** Members must manually create calendar events if they
|
||||
want reminders for recurring meetings.
|
||||
|
||||
**Proposed change:** Add an "Add to Calendar" dropdown button on the meeting
|
||||
detail page (and optionally the card).
|
||||
- Generates a `.ics` file or a Google Calendar URL with the recurring rule
|
||||
(e.g., "Every Wednesday at 7pm").
|
||||
- Includes the meeting name, description, and the Zoom/Jitsi link in the location field.
|
||||
|
||||
**Implementation notes:**
|
||||
- Use a helper to generate RFC 5545 `RRULE` strings from the `weekday_*` and
|
||||
`recurring_pattern` fields.
|
||||
- Include the `attend_url` in the calendar event description for one-tap join
|
||||
from phone lock screens.
|
||||
|
||||
---
|
||||
|
||||
### 11. Geographic Search for In-Person Meetings
|
||||
|
||||
**Current behavior:** The only location filter is a binary "Physical" checkbox.
|
||||
Members must use fulltext search (e.g., "Chicago") to find local meetings.
|
||||
|
||||
**Proposed change:** Add a "City/State" search input or a map view.
|
||||
- When `Physical` is checked, show a "Near [City, State]" input.
|
||||
- Map view (optional): A toggle to switch from "List" to "Map" view, plotting
|
||||
meetings on a map using their `location_address_json` coordinates.
|
||||
|
||||
**Implementation notes:**
|
||||
- The `event` table has `location_address_json` which often contains city/state.
|
||||
- Simple implementation: a city-picker dropdown populated from the distinct
|
||||
`location_address_json->>'$.city'` values in the current result set.
|
||||
|
||||
---
|
||||
|
||||
### 12. Prominent "Join" button for virtual meetings
|
||||
|
||||
**Current behavior:** On each meeting card, the Zoom or Jitsi join link is rendered
|
||||
as a small `btn-sm` inside the content area, visually equivalent to other label/value
|
||||
rows. The "Meeting Details" button at the top of the card is rendered *larger* than the
|
||||
join link — meaning the primary action for a member who wants to attend a meeting right
|
||||
now is visually subordinate to a navigation link.
|
||||
|
||||
The copy-to-clipboard button for the join link is gated behind `$ae_loc.manager_access`,
|
||||
so regular members have no easy way to share the link with a sponsee.
|
||||
|
||||
**Proposed change:** For virtual meetings, elevate the join button to a full-width
|
||||
prominent CTA inside the card header area, directly below the meeting name and badges:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ 📅 Monday Night IDAA Discussion 🖥 Virtual │
|
||||
│ │
|
||||
│ [ 🎥 Join Zoom Meeting ] ← full-width │
|
||||
│ [ 📋 Meeting Details ] │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- The Join button uses `preset-filled` (solid) styling; Meeting Details uses
|
||||
`preset-outlined` (hollow). This makes the action hierarchy visually clear.
|
||||
- On mobile especially, a full-width join button is much easier to tap than a
|
||||
small inline link buried inside label rows.
|
||||
- Replace manager-only clipboard with a Web Share API button for all members on
|
||||
virtual meetings: `navigator.share({ title, url })` on mobile triggers the native
|
||||
OS share sheet. Fall back to clipboard copy on desktop (where `navigator.share`
|
||||
is often unavailable). This lets members easily send a meeting link to a sponsee.
|
||||
- Passcode, if present, moves to the Meeting Details page — exposing it in the
|
||||
list view is unnecessary and clutters the card.
|
||||
|
||||
**Implementation notes:**
|
||||
- In `ae_idaa_comp__event_obj_li.svelte`, move the Zoom/Jitsi attend block from
|
||||
the `event__content` section up into the `ae_options` div (line ~200), rendered
|
||||
only when `idaa_event_obj?.virtual` is true and an attend URL exists.
|
||||
- Keep the existing small label/link in `event__content` as a fallback for when
|
||||
the prominent button is not shown (non-virtual meetings may still have a URL).
|
||||
- Web Share: `{#if navigator?.share}` guard; wrap in a try/catch (user cancels
|
||||
the share sheet throws `AbortError`).
|
||||
- The Live Now / Starting Soon badges from item #7, when implemented, should also
|
||||
interact with this button — e.g., a pulsing green border when the meeting is live.
|
||||
|
||||
---
|
||||
|
||||
### 13. "Today's Meetings" section at the top of the list
|
||||
|
||||
**Current behavior:** The meeting list shows all results sorted by the selected sort
|
||||
order. To find a meeting happening today, a member must scan every card's "When" line
|
||||
and mentally compare it to the current day and time. There is no at-a-glance view
|
||||
of what's available right now or later today.
|
||||
|
||||
This is distinct from the "Live Now" badge in item #7 (which marks individual cards
|
||||
after they're already displayed in a long list). This is a dedicated section pinned
|
||||
above the main results.
|
||||
|
||||
**Proposed change:** Add a collapsible "Today" section at the very top of the results
|
||||
list that shows only meetings scheduled on the current day of the week, sorted by
|
||||
start time:
|
||||
|
||||
```
|
||||
▼ Today — Sunday, May 17 3 meetings
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Sunday Serenity Discussion 7:00 AM ET 🔴Live │
|
||||
│ IDAA Sunday Big Book 2:00 PM CT │
|
||||
│ Sunday Night IDAA 8:00 PM ET │
|
||||
└─────────────────────────────────────────────────┘
|
||||
|
||||
All Meetings (140)
|
||||
...
|
||||
```
|
||||
|
||||
- The section is collapsed by default if it's empty (no meetings today).
|
||||
- Meetings in the "Today" section also appear in the main list below — this is a
|
||||
quick-access shortcut, not a filter.
|
||||
- Past meetings (start time has already passed today) are dimmed but still shown;
|
||||
a meeting may still be in progress.
|
||||
|
||||
**Implementation notes:**
|
||||
- Compute current day-of-week in the browser: `new Date().getDay()` → 0=Sunday, 6=Saturday.
|
||||
Map to the `weekday_*` boolean fields on the event object (e.g., day 0 → `weekday_sunday`).
|
||||
- Filter `visible_event_obj_li` (already computed in the list wrapper) for items where
|
||||
the matching `weekday_*` field is truthy. Sort by `recurring_start_time`.
|
||||
- Store collapse state in `$idaa_sess.recovery_meetings.today_section_expanded` (session
|
||||
only; default true so it's visible on first load).
|
||||
- Renders correctly in the Novi iframe since it's just a filtered sub-list of existing
|
||||
data — no additional API calls needed.
|
||||
- If item #7 (Live Now) is implemented, the "Today" section naturally becomes the host
|
||||
for the live/starting-soon badges, since that's where members will look first.
|
||||
|
||||
---
|
||||
|
||||
### 14. Data freshness indicator *(low priority — deprioritized)*
|
||||
|
||||
**Note:** Meeting records change infrequently — once established, a meeting's schedule,
|
||||
type, and contact info are typically stable for months or years. The occasional update is
|
||||
usually minor wording. Surfacing a freshness indicator for data this static would add
|
||||
visual noise with very little member benefit. The existing error state (item #4 in the
|
||||
bug fix, distinct "Unable to load meetings") and the escape-hatch cache-reset button
|
||||
already handle the reliability-concern case. This idea is recorded for completeness but
|
||||
is not recommended for implementation.
|
||||
|
||||
---
|
||||
|
||||
### 15. "Confirmed meetings only" default filter
|
||||
|
||||
**Current behavior:** All meetings are shown by default, including those with
|
||||
`status === 'unknown'` (not yet confirmed by IDAA Central Office). The "Not Confirmed
|
||||
by IDAA" warning badge on those cards is alarming-looking but does nothing to prevent
|
||||
unverified meetings from dominating the list.
|
||||
|
||||
There is currently no filter to hide unconfirmed meetings. Members have no choice but
|
||||
to see them all.
|
||||
|
||||
**Business rationale:** IDAA staff want meeting chairs to submit their meeting info for
|
||||
verification. Defaulting to "confirmed only" creates a natural incentive: unconfirmed
|
||||
meetings disappear from the default member view, which encourages chairs to contact
|
||||
IDAA staff and get their meeting verified. It also gives members a cleaner, higher-
|
||||
confidence list by default — they're not seeing meetings that may be outdated or
|
||||
inactive.
|
||||
|
||||
**Proposed change:** Add a `qry__confirmed` filter field with three states:
|
||||
|
||||
| Value | Behavior |
|
||||
|-------|----------|
|
||||
| `'confirmed_only'` (default) | Hide meetings where `status === 'unknown'` |
|
||||
| `'all'` | Show all meetings, confirmed and unconfirmed |
|
||||
| `'unconfirmed_only'` | Show only unconfirmed (admin/staff use) |
|
||||
|
||||
The filter UI shows a simple toggle in the query bar:
|
||||
```
|
||||
[✓ Confirmed Only] ← default, shown as active chip or checkbox
|
||||
```
|
||||
|
||||
When the member switches to "All", the unconfirmed meetings appear with their warning
|
||||
badge (see item #15 for making that badge useful on mobile).
|
||||
|
||||
For trusted/admin users, the default should remain `'all'` so staff can see the full
|
||||
picture without having to change a setting.
|
||||
|
||||
**Implementation notes:**
|
||||
- Add `qry__confirmed: 'confirmed_only' | 'all' | 'unconfirmed_only'` to
|
||||
`$idaa_loc.recovery_meetings` defaults, defaulting to `'confirmed_only'`.
|
||||
Trusted users default to `'all'`.
|
||||
- Apply the filter in both the IDB fast path (`db_events.event.filter()`) and the
|
||||
API revalidation secondary filter in `handle_search_refresh`. IDB: check
|
||||
`ev.status !== 'unknown'` when `qry__confirmed === 'confirmed_only'`. API: same
|
||||
post-fetch client-side filter.
|
||||
- Pass `qry__confirmed` to `events_func.search__event` if the API supports a
|
||||
`status` filter param; otherwise handle it client-side only.
|
||||
- The `no_results_no_filters` derived (used for the escape-hatch button) should NOT
|
||||
treat `qry__confirmed === 'confirmed_only'` as an active filter — it's the default
|
||||
state, not a narrowing choice the member made. Only count it as a filter if the
|
||||
member explicitly switched it to `'all'` or `'unconfirmed_only'`.
|
||||
- Add a count badge to the toggle: "Confirmed Only (132 of 140)" so members can see
|
||||
how many unconfirmed meetings exist without having to switch the filter.
|
||||
|
||||
---
|
||||
|
||||
### 16. "Not Confirmed" status — inline explanation on mobile
|
||||
|
||||
**Current behavior:** Meetings with `status === 'unknown'` show a warning badge:
|
||||
`⚠ Not Confirmed by IDAA ⚠`. The badge has a `title` attribute with a full explanation
|
||||
(~2 sentences). Title tooltips are invisible on mobile — tapping the badge does nothing.
|
||||
Members on phones see a alarming-looking warning with no explanation of what it means
|
||||
or what they should do.
|
||||
|
||||
**Proposed change:** Make the badge tappable. On tap (or hover on desktop), show an
|
||||
inline explanation panel directly below the badge:
|
||||
|
||||
```
|
||||
⚠ Not Confirmed by IDAA [?]
|
||||
|
||||
↓ (on tap)
|
||||
|
||||
This meeting has not been confirmed by IDAA Central Office.
|
||||
Please reach out to the chair for current information.
|
||||
If this meeting is active, email info@idaa.org to confirm it.
|
||||
[✕ Close]
|
||||
```
|
||||
|
||||
**Implementation notes:**
|
||||
- Add a `$state show_unconfirmed_info = false` per card (scoped to the `{#each}` block).
|
||||
- Replace the `title` attribute with an `onclick` toggle that sets `show_unconfirmed_info`.
|
||||
- The explanation renders in a `{#if show_unconfirmed_info}` block directly below the
|
||||
badge row — a simple div with a rounded border and the existing tooltip text.
|
||||
- The `mailto:info@idaa.org` link in the explanation is already in the tooltip text;
|
||||
making it a real clickable link here rather than plain text in a tooltip is a direct
|
||||
improvement for mobile members who want to report a confirmed meeting.
|
||||
- This pattern also applies to other `title`-only tooltips on the page if they appear.
|
||||
- Note: with item #15 defaulting to "confirmed only", most members will never encounter
|
||||
this badge unless they switch to the "All" view. The inline explanation is still worth
|
||||
implementing for that audience, but the default filter reduces how often it's seen.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
### 17. Cycling sort button — ✅ Implemented 2026-05-18
|
||||
|
||||
**Problem:** Three separate sort chip buttons (Last Updated / Name A→Z / Name Z→A) took
|
||||
too much horizontal space and caused layout bounce as the selected chip changed width.
|
||||
|
||||
**Implemented:** Single cycling button in `ae_idaa_comp__event_obj_qry.svelte`.
|
||||
Clicking advances through `sort_modes` array (Last Updated → Name A→Z → Name Z→A → repeat)
|
||||
using `$derived` index + `cycle_sort()` function. Button has `min-w-36` to prevent bounce.
|
||||
Icon changes per mode (fa-clock / fa-sort-alpha-down / fa-sort-alpha-up-alt). A small
|
||||
fa-redo icon indicates it's a cycling control.
|
||||
|
||||
---
|
||||
|
||||
### 18. Collapsible "Meeting Info" data store panel — ✅ Implemented 2026-05-18
|
||||
|
||||
**Problem:** The `Element_data_store` panel (code: `recovery_meetings_info`) displays
|
||||
between the filter bar and the meeting results list. Once a member has read it, it
|
||||
consumes vertical space on every page load and pushes results below the fold, especially
|
||||
in the Novi iframe on mobile.
|
||||
|
||||
**Implemented:** Toggle button wrapping the `<Element_data_store>` in
|
||||
`src/routes/idaa/(idaa)/recovery_meetings/+page.svelte`. Button shows
|
||||
"Meeting Info" with a chevron (up = expanded, down = collapsed). Collapse state
|
||||
persisted in `$idaa_loc.recovery_meetings.ds_info_collapsed` (localStorage) so the
|
||||
user's preference survives page reloads. New field added to `idaa_local_data_struct`
|
||||
in `ae_idaa_stores.ts` — no version bump needed (existing users without the field
|
||||
get `undefined` which is falsy = expanded, the correct default).
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- The fulltext search (`qry__fulltext_str`) searches against the `default_qry_str`
|
||||
field, which is a server-side composite that already includes contact info, day-of-week
|
||||
text, meeting type, location, and other metadata. The placeholder text in the search
|
||||
input is accurate — it genuinely searches contacts and schedule information despite
|
||||
those fields being stored in separate columns.
|
||||
- All changes must render correctly inside the Novi iframe context (Bootstrap v3.4.1
|
||||
CSS conflicts — see `CLIENT__IDAA_and_customized_mods.md` for known issues).
|
||||
- Mobile testing should cover Android Chrome specifically — the original "no meetings
|
||||
found" bug disproportionately affected mobile users with intermittent connections.
|
||||
|
||||
68
documentation/README__Docs_Index.md
Normal file
68
documentation/README__Docs_Index.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# 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`
|
||||
|
||||
## 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.
|
||||
171
documentation/REFERENCE__Common_Agent_Mistakes.md
Normal file
171
documentation/REFERENCE__Common_Agent_Mistakes.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# 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.
|
||||
|
||||
---
|
||||
|
||||
## 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,307 +1,197 @@
|
||||
# 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.
|
||||
|
||||
## 🔴 LCI October — Pres Mgmt Restoration (in progress 2026-06-12)
|
||||
|
||||
## 🔴 BGH Conference — April 21 (Must Fix Before Event)
|
||||
These features regressed over the last 6 months and must be working before the LCI conference.
|
||||
Reference commit for original working implementation: `bb993a102`.
|
||||
|
||||
- [x] **[Locations] Event Locations list does not auto-load** — added `+page.ts` to trigger
|
||||
`load_ae_obj_li__event_location` on page load. Also fixed session query using stale
|
||||
`event_location_id_random` index (should be `event_location_id`). (2026-04-19)
|
||||
### Session POC (Champion/Moderator) — `session_view.svelte`
|
||||
|
||||
- [x] **[Files] Warn/error on `.ppt`/`.doc` upload** — warning rows shown per-file in upload table;
|
||||
non-trusted users are fully blocked (`file_list_status = 'blocked_legacy'`); trusted users see
|
||||
warnings but can still upload. Covers `.ppt`, `.doc` (block) and other legacy exts (warn-only).
|
||||
(2026-04-19)
|
||||
**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] **[Files] Hide internal-purpose files from Launcher by default** — renamed `hide_draft` prop
|
||||
to `show_internal_purpose_files` in `launcher_file_cont.svelte`; logic flipped so `false` (the
|
||||
default) hides files with `file_purpose == 'outline'`, `'draft'`, or `'admin'`. Store key renamed
|
||||
from `hide_content__draft_files` (inverted, misleading) to `show_content__internal_files: false`
|
||||
(show-on-opt-in, consistent with all other `show_content__*` flags). Updated across all 8 Launcher
|
||||
templates that pass this prop. (2026-04-19, revised 2026-04-20)
|
||||
- [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] **[Launcher] Remove duplicate session API call on session select** — `menu_session_list.svelte`
|
||||
was calling `load_ae_obj_id__event_session` directly AND then `goto()` triggered `+page.ts` which
|
||||
also called it — two concurrent calls per session click. Removed the direct call entirely;
|
||||
`+page.ts` is now the sole owner of session data loading. `goto()` promise assigned to
|
||||
`ae_promises.slct__event_session_id` to drive the existing `{#await}` spinner. (2026-04-20)
|
||||
- [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.
|
||||
|
||||
- [ ] **[Electron/Launcher] Deploy + test Aether Native Electron app on Mac laptops** — build,
|
||||
deploy, and verify on onsite Mac laptops. Additional testing of cache/launch flow still needed
|
||||
before April 21.
|
||||
- [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] POC column shown in "Sessions at this Location"** — wired
|
||||
`hide__session_poc={!pres_mgmt_loc.current.show__session_li_poc_field}` in
|
||||
`ae_comp__event_location_obj_li.svelte`; also set `hide__session_location={true}` since
|
||||
location is implicit in that context. (2026-04-19)
|
||||
- [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
|
||||
|
||||
- [ ] **[Pres Mgmt] Presenter email sign-in link routes to wrong page**
|
||||
`email_sign_in__event_presenter()` builds a URL to `/presenter/[id]?person_id=...&person_pass=...`.
|
||||
The URL param parser (`sign_in_out.svelte`) is only mounted on the *session* page menu, not the
|
||||
presenter page. A presenter clicking their email link lands on their page with no auth granted.
|
||||
Fix: mount `Sign_in_out` in `presenter_page_menu.svelte` (same way session menu does it), or
|
||||
change the email link to route to the session page (which already has the parser) and include
|
||||
the presenter/presentation IDs as params — which is how it worked originally.
|
||||
|
||||
- [ ] **[Pres Mgmt] Presenter agreement not enforced before file upload**
|
||||
`require__presenter_agree` is stored and displayed but the upload components are gated on
|
||||
`auth__kv.presenter[id]` only, not on `presenter.agree`. A presenter who signs in but has not
|
||||
agreed can still upload. The original blocked the upload section until `agree === true`.
|
||||
|
||||
### Session POC Sign-In
|
||||
|
||||
- [ ] **[Pres Mgmt] `session_page_menu.svelte` sign-in prop still wrong**
|
||||
`event_session_id` prop passed to `Sign_in_out` was just changed from `event_id` to
|
||||
`event_session_id` — verify this is actually `$lq__event_session_obj?.event_session_id`
|
||||
(the real session ID string) not the URL param `url_session_id`. The sign-in component
|
||||
uses this value to set `auth__kv.session[event_session_id]`.
|
||||
|
||||
---
|
||||
|
||||
## 🚧 Upcoming High Priority
|
||||
|
||||
### [Stores] Svelte 4 → Svelte 5 State Migration (prerequisite for Phase 2c)
|
||||
The app uses `svelte-persisted-store` (Svelte 4 store contract) for all core persisted state
|
||||
(`ae_loc`, `idaa_loc`, `ae_api`, `ae_sess`, etc.). In Svelte 5 `$effect`, reading **any field**
|
||||
of a Svelte 4 store subscribes to the **entire store** — coarse-grained reactivity. This is the
|
||||
root cause of the IDAA Novi re-auth bug (2026-03-30): unrelated `$ae_loc` writes (e.g. iframe
|
||||
height, SWR cfg reload) triggered the Novi verification effect repeatedly.
|
||||
|
||||
Migration target: replace `svelte-persisted-store` with Svelte 5 `$state`-based persistence
|
||||
(e.g. `runed` `PersistedState`, or a lightweight custom wrapper). This gives fine-grained
|
||||
reactivity — only effects that actually read a changed field re-run.
|
||||
|
||||
**Phased approach (do NOT do all at once):**
|
||||
|
||||
- [ ] **Phase A — Project plan + wrapper decision:** Write `PROJECT__Stores_Svelte5_Migration.md`.
|
||||
Decide: `runed` library vs. custom `$state` + localStorage wrapper. Audit all store consumers.
|
||||
Identify stores in priority order. Estimate blast radius per store.
|
||||
|
||||
- [ ] **Phase B — Core auth stores (highest impact, start here):**
|
||||
- `ae_loc` (persisted) — auth flags, site cfg, UI state; ~471 consumer sites across 150+ files
|
||||
- `idaa_loc` (persisted) — Novi auth, IDAA query prefs
|
||||
These two cause the most reactive noise. Migrating them also unlocks Phase 2c (separate `ae_auth`
|
||||
store) since the callsite sweep is now required anyway.
|
||||
|
||||
- [ ] **Phase C — Remaining persisted stores:**
|
||||
- `ae_api` (persisted) — API config / JWT
|
||||
- `ae_events_stores` persisted entries (badges, launcher, leads, pres_mgmt loc stores)
|
||||
|
||||
- [ ] **Phase D — Non-persisted writable stores:**
|
||||
- `ae_sess`, `idaa_sess`, `slct`, `slct_trigger`, `ae_auth_error`, `ae_trig`, `ae_snip`, etc.
|
||||
- Lower urgency (no localStorage churn), but fine-grained reactivity still beneficial.
|
||||
|
||||
- [ ] **Phase E — Phase 2c (unblocked after B):** Split `ae_loc` into `ae_auth` + `ae_app`
|
||||
(see entry below — ~471 callsites, but sweep is cheap once already touching every consumer).
|
||||
|
||||
**Project plan doc needed:** Yes — scope is app-wide. Do NOT start Phase B without Phase A.
|
||||
- [ ] **[Launcher/Electron] Wallpaper reliability (post-CMSC)**
|
||||
- [ ] Use timestamp/randomized temp filename so macOS always sees a new path.
|
||||
- [ ] Add resilient reconciliation loop or event-driven reapply on display topology changes.
|
||||
|
||||
---
|
||||
|
||||
### [Stores] Refactor — Phase 2c (deferred)
|
||||
Phases 1, 2a, 2b are complete (see ✅ Completed below). One phase remaining:
|
||||
## 🔴 Axonius DC — June 9 (Badge Printing)
|
||||
**Setup/Registration:** June 8 | **Show:** June 9
|
||||
|
||||
- [ ] **Phase 2c — Actual separate stores (`ae_auth`, `ae_app`):** Requires touching ~471
|
||||
`$ae_loc.*` auth-field read sites across 150+ files. Deferred until a Svelte runes migration
|
||||
of the store layer itself (touching every component anyway makes the callsite sweep cheap).
|
||||
- [x] **[Badges] Epson C3500 fanfold badge layout** — `badge_4x6_fanfold` layout CSS created,
|
||||
wired, and documented. First live use: Axonius Adapt DC, June 9, 2026. (2026-05-15)
|
||||
|
||||
### [Backend] Join event_location_id onto event_presenter API view
|
||||
The `event_presenter` object currently has `event_session_id` but not `event_location_id`.
|
||||
When navigating from the Presenter View to the Launcher, the frontend has to do a secondary
|
||||
session lookup to discover the location (magic redirect in launcher base `+page.svelte`).
|
||||
Joining `event_session.event_location_id` into the presenter view/response would let the
|
||||
frontend pass the location directly in the Launcher URL without the extra lookup.
|
||||
- [x] Backend: added `event_location_id` (and `event_location_id_random`) to the `event_presenter` view or API response (2026-04-09)
|
||||
- [x] Frontend: updated `ae_EventPresenter` type and `properties_to_save`; now pass as `events__launcher_id` in `presenter_page_menu.svelte` (2026-04-09)
|
||||
### Badges follow-ups
|
||||
|
||||
- [ ] **[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`.
|
||||
|
||||
---
|
||||
|
||||
## 🚧 V3 CRUD Migration (Surgical Cleanup)
|
||||
Finalizing the 100% adoption of V3 Standard endpoints and retirement of legacy wrappers.
|
||||
|
||||
- [ ] **[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`.
|
||||
|
||||
### [TypeScript] svelte-check hidden errors — discovered 2026-03-27
|
||||
**HOW WE FOUND THIS:** The `@lucide/svelte` 0.577.0 update (2026-03-10) dropped `class` from
|
||||
`IconProps`. Fixing it required a `declare module '@lucide/svelte'` augmentation. That
|
||||
augmentation was mistakenly placed in `app.d.ts`, which is a *script-context* declaration file
|
||||
(no `export {}`). In that context, `declare module` is an **ambient replacement**, not a merge —
|
||||
it wiped all icon exports from svelte-check's view, surfacing 1368 previously hidden errors.
|
||||
Once moved to `src/lucide-augment.d.ts` (a proper module file with `export {}`), the masking
|
||||
lifted and the real pre-existing errors became visible.
|
||||
---
|
||||
|
||||
**Lesson:** A broken ambient declaration can silently hide unrelated errors. If svelte-check
|
||||
suddenly jumps to 0 errors, verify it's not because a bad `.d.ts` replaced a package's types.
|
||||
## 🚧 High Priority Workstreams
|
||||
|
||||
**Current state (2026-03-31):** 32 errors, 0 warnings — all `ModalProps.children`.
|
||||
### [Security] Site Passcode JWT Migration
|
||||
|
||||
- [ ] **[flowbite-svelte] `ModalProps.children` — 31 errors across 26 files.** The flowbite-svelte
|
||||
`Modal` component API changed; `children` is no longer a direct prop (now Svelte snippet-based).
|
||||
Affected files span journals, pres_mgmt, events/settings, and IDAA archives.
|
||||
Run `npx svelte-check 2>&1 | grep ModalProps` to get the current list.
|
||||
Fix pattern: replace `children` prop binding with Svelte snippet syntax per flowbite-svelte docs.
|
||||
- [ ] **[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
|
||||
`PersistedState` (from `runed`) for fine-grained updates. See `PROJECT__Stores_Svelte5_Migration.md`.
|
||||
|
||||
- [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.
|
||||
|
||||
### [Data Layer] IDB sorting + content version rollout
|
||||
Sorting baseline is now `build_tmp_sort` (ASC chain, no `.reverse()` on tmp-sort lists).
|
||||
|
||||
**⚠️ 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] 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.
|
||||
|
||||
- [ ] **[Journals] Visibility / audience toggle contrast** — the flag buttons need a clearer
|
||||
selected state in both light and dark mode.
|
||||
- [ ] **[Journals] Footer button style** — the actual `Done` button should read like a real button,
|
||||
not a seamless footer spacer.
|
||||
- [ ] **[Journals] Entry passcode secondary auth** — `passcode_hash` stores a hash; compare the
|
||||
entered passcode hash to the stored hash, gate entry loading, and honor the TTL-based access
|
||||
window. This is secondary entry auth, not a plain-text passcode field.
|
||||
- [ ] **[Journals] Summary AI shortcut** — add an AI summarize button next to Entry Details
|
||||
Summary so staff can generate a summary directly from the modal.
|
||||
- [ ] **[Journals] Archive On sizing** — constrain the Archive On control to a reasonable width
|
||||
instead of letting it expand to full width.
|
||||
- [ ] **[Journals] Archive On behavior** — define what Archive On actually means and wire the
|
||||
behavior; it is currently just a UI field with no live effect.
|
||||
---
|
||||
|
||||
- [x] **[IDAA] Do not cache IDAA data in IDB when access is denied (2026-04-19, audited 2026-04-28)**
|
||||
Full audit confirmed all protection layers are in place. No code changes required.
|
||||
- All `+page.ts` / `+layout.ts` under `src/routes/idaa/` are clean — no SWR loads run before auth resolves.
|
||||
- All `$effect` SWR calls in IDAA `+page.svelte` files are gated on `$idaa_loc.novi_verified || $ae_loc.trusted_access`.
|
||||
- `(idaa)/+layout.svelte` purges `db_posts`, `db_archives`, `db_events` on auth failure, no-UUID/no-session, and inconsistent state.
|
||||
- `sign_out()` calls `indexedDB.deleteDatabase()` on all IDAA databases.
|
||||
- API 401/403 responses fail-fast in `api_get_object.ts` (throw before any IDB write).
|
||||
- `idaa_trig` is in-memory `writable()` only — cannot carry stale trigger state across sessions.
|
||||
- `$effect` auth guards in IDAA page components are reactivity guards (prevent spurious SWR calls on coarse `$ae_loc` writes), NOT auth-bypass guards. SvelteKit layout hierarchy already prevents child components from mounting when `(idaa)/+layout.svelte` blocks rendering.
|
||||
- Doc: SvelteKit layout hierarchy security model captured in `GUIDE__SvelteKit2_Svelte5_DexieJS.md` and `BOOTSTRAP__AI_Agent_Quickstart.md` (Mistake #7).
|
||||
## 🧪 Testing & Optimization
|
||||
|
||||
- [ ] **[IDAA] Make `contact_li_json_ext` searchable — Recovery Meeting contact search (2026-04-08)**
|
||||
Members cannot search for meetings by contact name or email. `contact_li_json` data is not
|
||||
included in `default_qry_str` and MariaDB cannot substring-search a JSON longtext directly.
|
||||
The `event` table already has `contact_li_json_ext` (STORED GENERATED, indexed) to work around this.
|
||||
- [ ] **[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.
|
||||
|
||||
**Backend (blocked on this first):** Add `contact_li_json_ext` to the searchable fields
|
||||
whitelist for the `event` object type — likely a one-line change in `ae_obj_types_def.py`
|
||||
or the event object definition. Message sent to backend agent 2026-04-08.
|
||||
---
|
||||
|
||||
**Frontend (after backend ships):**
|
||||
- `src/lib/ae_events/ae_events__event.ts` → `search__event()`: add `contact_li_json_ext`
|
||||
as an OR condition alongside `default_qry_str` when `qry_str` is present.
|
||||
- `src/routes/idaa/(idaa)/recovery_meetings/+page.svelte` fast-path IDB filter: parse
|
||||
`contact_li_json` and include contact names/emails in the local text match check.
|
||||
## ⚙️ DevOps & Backend
|
||||
|
||||
- [ ] **[IDAA / Events] Audit `default_qry_str` coverage in other event search pages.**
|
||||
The backend was updated 2026-03-31 to expose `default_qry_str` in API responses.
|
||||
Frontend fix applied to Recovery Meetings (`+page.svelte` + `properties_to_save`).
|
||||
Check all other event search pages that use `db_events.event.filter()` or a secondary
|
||||
post-API text filter — they may have the same mismatch (local searches `name`/`description`
|
||||
only while server uses `default_qry_str`). Start with: any route under `/events/` or `/idaa/`
|
||||
that has a full-text search input.
|
||||
- [ ] **[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.**
|
||||
- [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`.
|
||||
|
||||
### [IDAA] Jitsi config editor + live site fix
|
||||
- [ ] **Fix live site (id=17) `jitsi_token_endpoint` pointing to dev-api:** DB has
|
||||
`https://dev-api.oneskyit.com/api/jitsi_token` for both site 10 and site 17 (IDAA live).
|
||||
Need to update site 17 in **production** to `https://api.oneskyit.com/api/jitsi_token`.
|
||||
SQL: `UPDATE site SET cfg_json = JSON_SET(cfg_json, '$.jitsi_token_endpoint', 'https://api.oneskyit.com/api/jitsi_token') WHERE id = 17;`
|
||||
---
|
||||
|
||||
- [ ] **Add IDAA Jitsi config editor UI** to the jitsi_reports page (administrator_access only),
|
||||
alongside the existing Jitsi URL Builder section. Should allow editing key fields in
|
||||
`site_cfg_json` without needing phpMyAdmin:
|
||||
- `jitsi_token_endpoint` — the JWT signing endpoint (needs to point to prod)
|
||||
- Jitsi domain default (currently hardcoded as `jitsi.dgrzone.com` fallback in the page)
|
||||
- `novi_jitsi_mod_li` — list of Novi UUIDs who get moderator privileges
|
||||
Read from `$ae_loc.site_cfg_json`, PATCH the site record via V3 CRUD
|
||||
(`PATCH /v3/crud/site/{id}/`), reload `$ae_loc.site_cfg_json` on save so it takes
|
||||
effect without re-login.
|
||||
|
||||
### [IDAA] Jitsi Reports still incomplete
|
||||
- [x] **Finish Jitsi Reports filters** — added Novi UUID exclusion plus meeting-name whitelist
|
||||
filtering, with room-level unique counts based on Novi UUID when present. (2026-05-06)
|
||||
|
||||
### [PWA] Service worker ignoring `chrome-extension://` requests
|
||||
Browser console shows repeated errors:
|
||||
```text
|
||||
TypeError: Failed to execute 'put' on 'Cache': Request scheme 'chrome-extension' is unsupported
|
||||
```
|
||||
The service worker's fetch/install handler is trying to cache requests with `chrome-extension://`
|
||||
URLs (injected by browser extensions), which the Cache API rejects. Fix: filter out non-`http`/`https`
|
||||
requests before attempting to cache. In the service worker fetch handler, add a guard:
|
||||
```js
|
||||
if (!event.request.url.startsWith('http')) return; // skip chrome-extension:// etc.
|
||||
```
|
||||
Locate in `static/service-worker.js` or the Vite PWA plugin config. Low severity — doesn't break
|
||||
functionality, but pollutes the console and may cause unhandled promise rejections.
|
||||
|
||||
### [CSS] Global placeholder text color — too dark in light mode
|
||||
Placeholder text inherits full input text color in light mode (Tailwind CSS default), making
|
||||
placeholders indistinguishable from filled-in values. Most visible in badge print controls
|
||||
where placeholders show the actual badge value (e.g. "John Smith").
|
||||
|
||||
Workaround: scoped `::placeholder` rule added to `ae_comp__badge_print_controls.svelte`
|
||||
(gray-400 light / gray-500 dark) — `commit 7733ef8`.
|
||||
|
||||
**Long-term fix:** Add a global rule to the main CSS (e.g. `src/app.css` or a theme file):
|
||||
```css
|
||||
::placeholder {
|
||||
color: #9ca3af; /* gray-400 */
|
||||
opacity: 1; /* overrides Firefox's 0.54 default */
|
||||
}
|
||||
.dark ::placeholder {
|
||||
color: #6b7280; /* gray-500 */
|
||||
}
|
||||
```
|
||||
Once the global rule is in place, remove the scoped workaround from the badge controls.
|
||||
|
||||
|
||||
|
||||
### [Backend/DevOps] Re-add `Access-Control-Allow-Private-Network: true` CORS header
|
||||
Chrome's Private Network Access (PNA) policy blocks public-origin iframes from fetching
|
||||
private-network addresses. Symptom: when `dev-api.oneskyit.com` resolves to a LAN IP
|
||||
(testing from home), Chrome blocks the site domain lookup → ghost account → `site_cfg_json`
|
||||
never loads → `novi_idaa_api_key` is null → IDAA Novi verifier spins forever → timeout banner.
|
||||
Firefox unaffected. Production unaffected (public IPs only).
|
||||
|
||||
- [ ] **Re-add PNA header to API CORS config** — `dev-api` Nginx or FastAPI CORS middleware
|
||||
must respond with `Access-Control-Allow-Private-Network: true` when Chrome sends
|
||||
`Access-Control-Request-Private-Network: true` in the preflight. This was fixed ~1 month
|
||||
ago and regressed. Check Nginx site config and FastAPI `CORSMiddleware` settings.
|
||||
Low urgency (dev-only, Firefox workaround available), but blocks home-network iframe testing.
|
||||
|
||||
### [DevOps] Remaining deployment items
|
||||
|
||||
- [ ] **Simplify Dockerfile env file selection** — Currently the Dockerfile uses a `BUILD_MODE` arg to
|
||||
select between `.env.dev`, `.env.test`, `.env.prod` during the Docker build. This is unnecessary
|
||||
complexity: each server (test Linode, prod Linode, workstation) only ever runs one environment, so
|
||||
there will only ever be one env file present in that server's app directory.
|
||||
|
||||
**The fix:** Each server's app dir (`/srv/apps/test_aether_app_sveltekit/`, etc.) should have a
|
||||
plain `.env` file (gitignored, placed manually during server setup). The Dockerfile should just
|
||||
`COPY . .` and `cp .env .env.runtime` unconditionally — no `if prod / elif test / else dev`
|
||||
branching for env file selection.
|
||||
|
||||
**What this changes:**
|
||||
- `aether_app_sveltekit/Dockerfile` — remove the `BUILD_MODE`-driven `cp` block; always use `.env`
|
||||
- Each Linode app dir gets a plain `.env` instead of `.env.test` / `.env.prod`
|
||||
- Workstation keeps `.env.local` (for `npm run dev`) and `.env.dev` (for `build:docker:dev`) —
|
||||
those stay as-is since they legitimately coexist locally
|
||||
- `BUILD_MODE` arg can stay if needed for other build differences; just stop using it to pick the env file
|
||||
- Update `.gitignore` in sveltekit to un-ignore `.env.test` / remove stale entries if desired
|
||||
|
||||
**Do not touch before the April 21 show.** Low risk but unnecessary churn right before an event.
|
||||
|
||||
- [ ] **Branch strategy cleanup:** All environments (test, prod, bak) currently pull from the same
|
||||
branches. `deploy.sh` defaults are `ae_app_3x_llm` / `development` — acceptable for now but
|
||||
should establish proper branch separation (e.g. `main`/`master` for prod).
|
||||
|
||||
- [ ] **Tier 2 deploy (Gitea webhook):** Push-triggered deploys via Gitea webhook → listener on
|
||||
Linode → `deploy.sh`. Deferred until Gitea usage is more established.
|
||||
|
||||
|
||||
### [Files] Download button — wrong ID used in `handle_click()` (2026-04-22)
|
||||
`ae_comp__hosted_files_download_button.svelte` resolves `file_id` for the download call as
|
||||
`hosted_file_obj?.id || hosted_file_obj?.hosted_file_id || hosted_file_id`. When called from
|
||||
Manage Files with an `event_file_obj`, `hosted_file_obj.id` = `event_file_id` (set by
|
||||
`_process_generic_props` via the `_random` strip logic), so the chain stops at the wrong value.
|
||||
The download call goes to `/v3/action/hosted_file/{event_file_id}/download` instead of using the
|
||||
correct `hosted_file_id`. May work if the backend accepts event_file_id at that endpoint —
|
||||
needs live verification.
|
||||
|
||||
**Status (2026-04-22):** Tested — downloads ARE working despite the wrong ID. The backend
|
||||
V3 action endpoint appears to silently accept `event_file_id` at the `hosted_file` download
|
||||
path (or maps between the two). Working by accident, not by design. Needs proper fix before
|
||||
it breaks — if the backend ever tightens that endpoint, all Manage Files downloads will 404.
|
||||
|
||||
**Fix:** In `handle_click()` and both `$effect` blocks and the `content` snippet, replace:
|
||||
```ts
|
||||
const file_id = hosted_file_obj?.id || hosted_file_obj?.hosted_file_id || hosted_file_id;
|
||||
```
|
||||
with:
|
||||
```ts
|
||||
const file_id = hosted_file_obj?.hosted_file_id ?? hosted_file_id;
|
||||
```
|
||||
The direct-download `<a>` path is unaffected (already uses `event_file_id` → correct endpoint).
|
||||
|
||||
### [Files] `db_events.file.clear()` on upload clears all cached files (2026-04-22)
|
||||
In `ae_comp__event_files_upload.svelte` line 114, `db_events.file.clear()` wipes the entire
|
||||
`file` Dexie table, not just files for the current session/presenter. Normally harmless (the
|
||||
reload right after repopulates), but if multiple sessions' file lists are open simultaneously
|
||||
they'd briefly flash empty. Low priority — only noticeable in multi-panel workflows.
|
||||
|
||||
### [General]
|
||||
- **Input Field Audit:** Several input fields are missing `name`/`id` attributes or `data-testid`. Known examples: badge override fields in `ae_comp__badge_obj_view.svelte`; template name input in `ae_comp__badge_template_form.svelte`. Matters for: accessibility, autofill, label associations, and test targeting. (For tests, use `getByLabel()` rather than `input[value*=...]` which only checks the HTML attribute, not the Svelte-bound DOM property.)
|
||||
|
||||
## ✅ Completed (2026-04)
|
||||
## ✅ 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)
|
||||
[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:** Active / In Progress
|
||||
> **Last Updated:** 2026-01-20
|
||||
> **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/...`).
|
||||
|
||||
---
|
||||
@@ -21,23 +22,23 @@ While the **Journals** and **Identity (User/Account)** modules have been success
|
||||
The following files have been identified as using legacy CRUD wrappers.
|
||||
|
||||
### 🔴 High Priority: Events Module
|
||||
The entire `ae_events` library is heavily dependent on legacy `v2` list and `v1` CRUD wrappers.
|
||||
|
||||
- [ ] `src/lib/ae_events/ae_events__event_session.ts`
|
||||
- [ ] `src/lib/ae_events/ae_events__event_presenter.ts`
|
||||
- [ ] `src/lib/ae_events/ae_events__event_presentation.ts`
|
||||
- [ ] `src/lib/ae_events/ae_events__event_location.ts`
|
||||
- [ ] `src/lib/ae_events/ae_events__event_badge_template.ts`
|
||||
- [ ] `src/lib/ae_events/ae_events__event_device.ts`
|
||||
- [x] `src/lib/ae_events/ae_events__event_session.ts` (Migrated 2026-01-30)
|
||||
- [x] `src/lib/ae_events/ae_events__event_presenter.ts` (Migrated 2026-01-30)
|
||||
- [x] `src/lib/ae_events/ae_events__event_presentation.ts` (Migrated 2026-01-30)
|
||||
- [x] `src/lib/ae_events/ae_events__event_location.ts` (Migrated 2026-01-30)
|
||||
- [x] `src/lib/ae_events/ae_events__event_badge_template.ts` (Migrated 2026-01-30)
|
||||
- [x] `src/lib/ae_events/ae_events__event_device.ts` (Migrated 2026-01-30)
|
||||
- [x] `src/lib/ae_events/ae_events__exhibit.ts` (Migrated 2026-01-28)
|
||||
- [ ] `src/lib/ae_events/ae_events__event_file.ts`
|
||||
- [x] `src/lib/ae_events/ae_events__event_file.ts` (Migrated 2026-01-30)
|
||||
|
||||
### 🟠 Medium Priority: Core & Sponsorships
|
||||
Legacy patterns persisting in core logic and config modules.
|
||||
|
||||
- [ ] `src/lib/ae_sponsorships/ae_sponsorships_functions.ts`
|
||||
- [ ] `src/lib/ae_core/core__hosted_files.ts` (Uses `get_ae_obj_id_crud`)
|
||||
- [ ] `src/lib/ae_core/core__site.ts` (Uses `get_ae_obj_id_crud`)
|
||||
- [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; 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`
|
||||
- [ ] `src/lib/ae_core/core__countries.ts`
|
||||
@@ -48,9 +49,10 @@ Specific UI components that make direct API calls instead of using store functio
|
||||
- [ ] `src/lib/elements/element_data_store.svelte` (Direct `create_ae_obj_crud`)
|
||||
- [x] `src/lib/elements/element_data_store_v2.svelte`
|
||||
- [ ] `src/routes/events/[event_id]/event_page_menu.svelte`
|
||||
- [ ] `src/routes/events/[event_id]/(pres_mgmt)/session/ae_comp__event_session_alert.svelte`
|
||||
- [x] `src/routes/events/[event_id]/(pres_mgmt)/session/ae_comp__event_session_alert.svelte` (Migrated to `update_ae_obj`)
|
||||
- [ ] `src/routes/events/ae_comp__event_session_obj_li.svelte`
|
||||
- [ ] `src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_id_edit.svelte`
|
||||
- [ ] `src/routes/events/[event_id]/(pres_mgmt)/presenter/[presenter_id]/ae_comp__event_presenter_form_agree.svelte` (STILL USES `update_ae_obj_id_crud`)
|
||||
|
||||
---
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,58 @@
|
||||
# IDAA Recovery Meetings: UI/UX Improvement Roadmap
|
||||
|
||||
This document outlines proposed enhancements for the IDAA Recovery Meeting module. The goal is to make it easier for members to find and attend meetings, especially on mobile devices, while providing IDAA staff with better tools to manage meeting data quality.
|
||||
|
||||
## 🏆 The "Big Wins" (Highest Member Impact)
|
||||
|
||||
### 1. Automatic Timezone Conversion
|
||||
* **The Problem:** Meetings currently show their "native" time (e.g., 7:00 PM Central). Members must manually calculate the time for their own location.
|
||||
* **The Fix:** The app will automatically detect the member's local timezone and show a converted time side-by-side (e.g., *"7:00 PM Central — 8:00 PM your time"*).
|
||||
|
||||
### 2. "Live Now" & "Today’s Meetings"
|
||||
* **The Fix:**
|
||||
* **Live Now:** A high-visibility green "LIVE" badge will pulse next to meetings currently in progress.
|
||||
* **Today’s Section:** A dedicated section at the very top of the list will show only meetings happening today, sorted by time, so members don't have to scroll through the full 140+ meeting list.
|
||||
|
||||
### 3. Clearer Meeting Schedules
|
||||
* **The Problem:** Days of the week are currently listed as a flat string (Sunday Monday Wednesday).
|
||||
* **The Fix:** Convert schedules into natural language one-liners: *"Mondays, Wednesdays, and Fridays at 7:00 PM."* This is much faster for the human eye to scan.
|
||||
|
||||
### 4. Favorites ("My Meetings")
|
||||
* **The Fix:** Members can "Star" their regular meetings. These favorites will be pinned to the top of their list for one-tap access every week.
|
||||
|
||||
### 5. "Add to Calendar"
|
||||
* **The Fix:** A button to automatically add a recurring meeting to a member’s Google, Apple, or Outlook calendar, including the Zoom/Jitsi link in the calendar event description.
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Staff Tools & Data Quality
|
||||
|
||||
### 6. "Confirmed Only" Default View
|
||||
* **The Strategy:** To encourage meeting chairs to keep their information current, we propose defaulting the list to show **only** meetings confirmed by the Central Office.
|
||||
* **Member Benefit:** Higher confidence that the meeting they are about to join is active and the link is correct.
|
||||
* **Staff Benefit:** Creates a natural incentive for chairs to contact IDAA to get "Verified," as unverified meetings would require an extra click to see.
|
||||
|
||||
### 7. Mobile-Friendly "Not Confirmed" Explanations
|
||||
* **The Problem:** On mobile, the warning badge for unconfirmed meetings doesn't explain *why* it's there or *how* to fix it.
|
||||
* **The Fix:** Tapping the badge will show a simple popup: *"This meeting hasn't been verified recently. If you are the chair, please email info@idaa.org to confirm."*
|
||||
|
||||
---
|
||||
|
||||
## 📱 Ease-of-Use & Mobile Polishing
|
||||
|
||||
### 8. Prominent "Join" Buttons & Easy Sharing
|
||||
* **The Fix:** For virtual meetings, we will move the "Join Zoom" button to a prominent, full-width position at the top of the card. We will also add a "Share" button so members can easily text a meeting link to a sponsee.
|
||||
|
||||
### 9. Simplified "Quick-Filter" Chips
|
||||
* **The Fix:** Instead of small checkboxes, we will add large "Chips" (buttons) for common filters: `[🖥 Virtual]` `[🏠 In-Person]` `[🩺 IDAA]` `[Caduceus]`. These are much easier to tap on a phone screen.
|
||||
|
||||
### 10. Intelligent "No Results" Guidance
|
||||
* **The Problem:** If a member filters too narrowly (e.g., "Caduceus meetings in Hawaii on Tuesdays"), they just see a blank screen.
|
||||
* **The Fix:** A helpful prompt will appear: *"No meetings found for these filters. [Clear all filters →]"* to prevent members from thinking the app is broken.
|
||||
|
||||
---
|
||||
|
||||
### Next Steps
|
||||
1. **Feedback:** Staff identifies which 3–4 items are the highest priority for the next update.
|
||||
2. **Prototype:** We implement the high-priority items in the testing environment for staff review.
|
||||
3. **Deployment:** Changes are pushed live to the IDAA website.
|
||||
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)
|
||||
|
||||
54
documentation/archive/TODO__Agents__ARCHIVE_2026-05.md
Normal file
54
documentation/archive/TODO__Agents__ARCHIVE_2026-05.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Frontend Agent Task List (Archived May 2026)
|
||||
|
||||
## ✅ Completed (2026-05)
|
||||
|
||||
### [API] GET/POST retry hardening — differentiate timeout aborts vs intentional aborts
|
||||
**Status:** ✅ Completed (2026-05-21)
|
||||
- GET/POST now explicitly distinguish abort class in helper code.
|
||||
- Timeout-triggered aborts are retryable via existing retry loop; intentional aborts fail fast.
|
||||
- Backoff behavior retained (`2s -> 4s -> 6s -> 8s`).
|
||||
- Validation done via Playwright tests.
|
||||
|
||||
### [API] PATCH/DELETE retry hardening — parity with GET/POST
|
||||
**Status:** ✅ Completed (2026-05-21)
|
||||
- PATCH and DELETE now implement the same retry-classification model used in GET/POST.
|
||||
- Added explicit fail-fast for 400/401/403/422.
|
||||
- DELETE now triggers the session-expired banner on 401/403.
|
||||
|
||||
### [Testing] V3 API performance probe (basic stress rounds)
|
||||
**Status:** ✅ Completed baseline harness (2026-05-21)
|
||||
- Implemented a gated Playwright probe for quick repeated list-query timing against live V3 endpoints.
|
||||
- Writes reports to `tests/results/`.
|
||||
|
||||
### [IDAA] Random "Access Denied" — Root Cause Review & Fixes
|
||||
**Status:** ✅ Resolved (2026-05-19)
|
||||
- Server-side Novi verification migrated to V3 action endpoint.
|
||||
- Extended Novi TTL to 12 hours.
|
||||
- Hardened retry and timeout logic in `+layout.svelte`.
|
||||
|
||||
### [IDAA] Server-side Novi verification — 503 not auto-retried
|
||||
**Status:** ✅ Fixed (2026-05-20)
|
||||
|
||||
### [IDAA] Jitsi Reports filters
|
||||
**Status:** ✅ Finished (2026-05-06)
|
||||
- Added Novi UUID exclusion plus meeting-name whitelist filtering.
|
||||
|
||||
### [PWA] Service worker ignoring `chrome-extension://` requests
|
||||
**Status:** ✅ Fixed (2026-05-14)
|
||||
- Added guard to filter out non-http/https requests before Attempting to cache.
|
||||
|
||||
### [Electron/Launcher] Display mirroring auto-detection
|
||||
**Status:** ✅ Completed (2026-05-20)
|
||||
- `native:set-display-layout` now auto-detects displays via `displayplacer list`.
|
||||
|
||||
### [Launcher] Force Sync Location
|
||||
**Status:** ✅ Completed (2026-05-21)
|
||||
- Implemented manual trigger and background engine logic to pre-cache all location files.
|
||||
|
||||
### [Launcher] Chronological Download Priority
|
||||
**Status:** ✅ Completed (2026-05-21)
|
||||
- Refactored download queue to prioritize Event Assets > Early Sessions > Presentation Order > Created Date.
|
||||
|
||||
### [Launcher] Error handling + fallback
|
||||
**Status:** ✅ Completed (2026-05-14)
|
||||
- Post-script failure surfaces 'fallback' status; `open_cmd` failure falls back to OS default.
|
||||
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.10",
|
||||
"version": "3.00.30",
|
||||
"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",
|
||||
|
||||
@@ -20,7 +20,7 @@ FA_TO_LUCIDE = {
|
||||
'fa-times': 'X',
|
||||
'fa-exclamation-triangle': 'TriangleAlert',
|
||||
'fa-check': 'Check',
|
||||
'fa-check-circle': 'CheckCircle',
|
||||
'fa-check-circle': 'CircleCheck',
|
||||
'fa-plus': 'Plus',
|
||||
'fa-minus': 'Minus',
|
||||
'fa-save': 'Save',
|
||||
|
||||
27
src/app.css
27
src/app.css
@@ -163,9 +163,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 {
|
||||
@@ -233,12 +240,14 @@ html.trusted_access #appShell {
|
||||
font-display: swap;
|
||||
} */
|
||||
|
||||
/* modern theme */
|
||||
@font-face {
|
||||
/* modern theme — @font-face commented out 2026-05-19: Quicksand is declared but no
|
||||
CSS rule applies font-family:'Quicksand' to any element, so the browser never
|
||||
fetches this file. Re-enable if a theme or component starts using it. */
|
||||
/* @font-face {
|
||||
font-family: 'Quicksand';
|
||||
src: url('/fonts/Quicksand.ttf');
|
||||
font-display: swap;
|
||||
}
|
||||
} */
|
||||
|
||||
/* :root [data-theme='modern'] { */
|
||||
/* --theme-rounded-base: 20px;
|
||||
@@ -916,8 +925,14 @@ img.qr_code:focus {
|
||||
/* BEGIN: Overrides and fixes specific to Novi and IDAA */
|
||||
.iframe .novi_btn {
|
||||
border-radius: 60px;
|
||||
/* border-color: hsla(0, 0%, 50%, .5); */
|
||||
/* border-color: hsla(0, 0%, 0%, .15); */
|
||||
/* Bootstrap v3 (.btn) sets border:1px solid transparent and wins over
|
||||
Skeleton/Tailwind preset-outlined classes when loaded last. Use box-shadow
|
||||
instead — Bootstrap does not set box-shadow on .btn so it cannot strip it. */
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.iframe .novi_btn:hover {
|
||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.5);
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.iframe .novi_m0 {
|
||||
|
||||
76
src/app.html
76
src/app.html
@@ -3,11 +3,58 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const hostname = window.location.hostname;
|
||||
const is_local_dev =
|
||||
hostname === 'localhost' ||
|
||||
hostname === '127.0.0.1' ||
|
||||
hostname === '[::1]' ||
|
||||
hostname.endsWith('.localhost');
|
||||
|
||||
if (!is_local_dev || !('serviceWorker' in navigator)) return;
|
||||
|
||||
// Prevent the app bootstrap from re-registering a worker on localhost.
|
||||
// The browser can otherwise keep routing fetches through a stale SW
|
||||
// during iterative iframe testing, which is exactly the noise we want to avoid.
|
||||
try {
|
||||
Object.defineProperty(navigator.serviceWorker, 'register', {
|
||||
configurable: true,
|
||||
value: async () => ({})
|
||||
});
|
||||
} catch {
|
||||
// If the property is not writable in this browser, the unregister
|
||||
// pass below still removes any existing worker registration.
|
||||
}
|
||||
|
||||
// Local iframe testing should not keep an older worker alive, because
|
||||
// Chromium can continue to route fetches through the stale worker until
|
||||
// it is explicitly unregistered.
|
||||
navigator.serviceWorker.getRegistrations().then((registrations) => {
|
||||
for (const registration of registrations) {
|
||||
registration.unregister().catch(() => {});
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
// Clear any stale runtime caches as well; local testing should always
|
||||
// rebuild from the current source rather than reusing old worker output.
|
||||
if ('caches' in window) {
|
||||
caches.keys().then((cache_keys) => {
|
||||
for (const cache_key of cache_keys) {
|
||||
caches.delete(cache_key).catch(() => {});
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<!-- Google Fonts: commented out 2026-05-19 — no theme or component applies these families;
|
||||
all themes use system-ui/sans-serif. Re-enable if a theme is added that references them.
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
@@ -19,14 +66,43 @@
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap"
|
||||
rel="stylesheet" />
|
||||
-->
|
||||
|
||||
<!-- <link href="app.css" rel="stylesheet"> -->
|
||||
|
||||
<!-- Pre-JS loading indicator. Removed by root +layout.svelte onMount once Svelte
|
||||
bootstraps and the existing is_hydrating overlay takes over. Pointer-events:none
|
||||
so it never blocks interaction if something goes wrong with the remove call. -->
|
||||
<style>
|
||||
#ae_loader {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
}
|
||||
#ae_loader::after {
|
||||
content: '';
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(120, 120, 120, 0.15);
|
||||
border-top-color: rgba(120, 120, 120, 0.45);
|
||||
animation: ae_loader_spin 0.75s linear infinite;
|
||||
}
|
||||
@keyframes ae_loader_spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<!-- h-full w-full overflow-auto -->
|
||||
<!-- overflow-x-scroll -->
|
||||
<body data-sveltekit-preload-data="hover" class="h-full w-full">
|
||||
<div id="ae_loader" aria-hidden="true"></div>
|
||||
<div style="display: contents" class="">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { ae_auth_error } from '$lib/stores/ae_stores';
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
|
||||
/**
|
||||
@@ -11,7 +13,7 @@ export const delete_object = async function delete_object({
|
||||
headers = {},
|
||||
params = {},
|
||||
data = {},
|
||||
timeout = 60000,
|
||||
timeout = 20000,
|
||||
return_meta = false,
|
||||
log_lvl = 0,
|
||||
retry_count = 5
|
||||
@@ -97,9 +99,15 @@ export const delete_object = async function delete_object({
|
||||
}
|
||||
|
||||
for (let attempt = 1; attempt <= retry_count; attempt++) {
|
||||
// Keep timeout handle at attempt scope so catch can always clear it.
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
// AbortError alone is ambiguous. Track helper-timeout aborts so
|
||||
// caller/navigation aborts can still fail fast with no retry.
|
||||
let did_timeout_abort = false;
|
||||
timeoutId = setTimeout(() => {
|
||||
did_timeout_abort = true;
|
||||
console.error(
|
||||
`API DELETE request timed out after ${timeout}ms.`
|
||||
);
|
||||
@@ -120,12 +128,48 @@ export const delete_object = async function delete_object({
|
||||
url.toString(),
|
||||
fetchOptions
|
||||
).catch(function (error: any) {
|
||||
if (
|
||||
error?.name === 'AbortError' ||
|
||||
error?.name === 'TypeError' ||
|
||||
error?.message?.includes('aborted')
|
||||
) {
|
||||
if (log_lvl > 1) {
|
||||
console.log(
|
||||
'API DELETE: Request aborted or browser-terminated.',
|
||||
error
|
||||
);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
console.log(
|
||||
'API DELETE Object *fetch* request was aborted or failed in an unexpected way.',
|
||||
error
|
||||
);
|
||||
return error;
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
|
||||
// Error object was returned from fetch catch block; decide retry class.
|
||||
if (
|
||||
response instanceof Error ||
|
||||
(response &&
|
||||
(response.name === 'AbortError' ||
|
||||
response.name === 'TypeError'))
|
||||
) {
|
||||
if (response.name === 'AbortError') {
|
||||
if (did_timeout_abort) {
|
||||
throw new Error(
|
||||
`Timeout abort (attempt ${attempt}/${retry_count}) after ${timeout}ms`
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Network error (attempt ${attempt}): ${response.message}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
throw new Error(
|
||||
@@ -151,7 +195,24 @@ export const delete_object = async function delete_object({
|
||||
errorBody
|
||||
);
|
||||
|
||||
if (response.status >= 400 && response.status < 404) {
|
||||
// Fail fast on client/auth/validation failures.
|
||||
if (
|
||||
response.status === 400 ||
|
||||
response.status === 401 ||
|
||||
response.status === 403 ||
|
||||
response.status === 422
|
||||
) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
console.warn(
|
||||
`AUTH DIAGNOSTICS (DELETE): Headers sent for ${endpoint}:`,
|
||||
{
|
||||
has_api_key: !!headers_cleaned['x-aether-api-key'],
|
||||
has_account_id: !!headers_cleaned['x-account-id']
|
||||
}
|
||||
);
|
||||
// Signal the root layout to show the session-expired banner.
|
||||
if (browser) ae_auth_error.set({ type: 'expired', ts: Date.now() });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -174,6 +235,8 @@ export const delete_object = async function delete_object({
|
||||
? json.data
|
||||
: json;
|
||||
} catch (error) {
|
||||
// Ensure per-attempt timeout is always cleared on failure.
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
console.error(`API DELETE error on attempt ${attempt}:`, error);
|
||||
|
||||
if (attempt === retry_count) {
|
||||
@@ -181,9 +244,12 @@ export const delete_object = async function delete_object({
|
||||
return false;
|
||||
}
|
||||
|
||||
if (log_lvl) {
|
||||
console.log(`Retrying... (${attempt}/${retry_count})`);
|
||||
}
|
||||
// Backoff before retrying. Caps at 8s to match GET/POST/PATCH policy.
|
||||
const delay_ms = Math.min(2000 * attempt, 8000);
|
||||
console.log(
|
||||
`API DELETE: Retrying in ${delay_ms}ms... (attempt ${attempt}/${retry_count})`
|
||||
);
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, delay_ms));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ export const get_object = async function get_object({
|
||||
headers = {},
|
||||
params = {},
|
||||
data = {},
|
||||
timeout = 90000,
|
||||
timeout = 20000,
|
||||
return_meta = false,
|
||||
return_blob = false,
|
||||
filename = '',
|
||||
@@ -73,9 +73,6 @@ export const get_object = async function get_object({
|
||||
url.searchParams.append(key, params[key])
|
||||
);
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
// Clean and merge headers without mutating the original api_cfg
|
||||
const headers_cleaned: key_val = {};
|
||||
const merged_headers = { ...api_cfg['headers'], ...headers };
|
||||
@@ -169,10 +166,11 @@ export const get_object = async function get_object({
|
||||
console.log('Final cleaned headers:', headers_cleaned);
|
||||
}
|
||||
|
||||
// signal is injected per-attempt inside the retry loop so each retry gets
|
||||
// a fresh AbortController with its own independent timeout.
|
||||
const fetchOptions: RequestInit = {
|
||||
method: 'GET',
|
||||
headers: headers_cleaned,
|
||||
signal: controller.signal,
|
||||
// Be explicit about CORS behavior and redirect handling to avoid
|
||||
// environment-dependent defaults that can cause opaque failures.
|
||||
mode: 'cors',
|
||||
@@ -203,10 +201,24 @@ export const get_object = async function get_object({
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fresh AbortController per attempt — ensures each retry has its own
|
||||
// independent timeout. Sharing a single controller across retries leaves
|
||||
// retries unprotected once the first attempt's clearTimeout() runs.
|
||||
const controller = new AbortController();
|
||||
// Track whether THIS helper's timeout fired. AbortError alone is ambiguous:
|
||||
// it can mean timeout OR intentional caller abort (navigation/unmount).
|
||||
// We only retry timeout-aborts; intentional aborts should fail fast.
|
||||
let did_timeout_abort = false;
|
||||
const timeoutId = setTimeout(() => {
|
||||
did_timeout_abort = true;
|
||||
console.warn(`API GET: Request timed out after ${timeout}ms (attempt ${attempt}/${retry_count}).`);
|
||||
controller.abort();
|
||||
}, timeout);
|
||||
|
||||
try {
|
||||
const response = await fetch_method(
|
||||
url.toString(),
|
||||
fetchOptions
|
||||
{ ...fetchOptions, signal: controller.signal }
|
||||
).catch(function (error: any) {
|
||||
// SILENCE NOISE: Aborted requests (common in SWR/Background loads) shouldn't spam logs
|
||||
if (
|
||||
@@ -231,21 +243,36 @@ export const get_object = async function get_object({
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// Check if we should stop due to abort or network failure
|
||||
// Check if we should stop due to abort or network failure.
|
||||
if (
|
||||
response instanceof Error ||
|
||||
(response &&
|
||||
(response.name === 'TypeError' ||
|
||||
response.name === 'AbortError'))
|
||||
) {
|
||||
// If it was an explicit abort, definitely stop
|
||||
if (response.name === 'AbortError') return false;
|
||||
// AbortError can be either timeout or intentional abort.
|
||||
// Retry only helper-owned timeout aborts; fail fast on caller abort.
|
||||
if (response.name === 'AbortError') {
|
||||
if (did_timeout_abort) {
|
||||
throw new Error(
|
||||
`Timeout abort (attempt ${attempt}/${retry_count}) after ${timeout}ms`
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (log_lvl > 1)
|
||||
console.log(
|
||||
'API GET Object: Detected NetworkError or TypeError. Failing fast.'
|
||||
);
|
||||
return false;
|
||||
// TypeError = transient network failure (ERR_NETWORK_CHANGED,
|
||||
// ERR_NETWORK_IO_SUSPENDED, hotel/conference WiFi blip, etc.).
|
||||
// IMPORTANT: throw here so the retry loop's catch block handles it with
|
||||
// backoff. Returning false would bypass retries entirely.
|
||||
//
|
||||
// WHY THIS WAS BROKEN: The Jan 2026 "offline-first fast-paths" commit
|
||||
// (a10accfaa) changed .catch() to return the error as a value instead of
|
||||
// not returning (undefined). The undefined path fell through to the
|
||||
// `if (!response)` throw which DID retry. The explicit `return error` +
|
||||
// this `return false` block silently killed the retry for the most common
|
||||
// failure mode on conference/hotel WiFi.
|
||||
throw new Error(`Network error (attempt ${attempt}): ${response.message}`);
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
@@ -438,6 +465,8 @@ export const get_object = async function get_object({
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ensure the per-attempt timeout timer is always cancelled on failure.
|
||||
clearTimeout(timeoutId);
|
||||
console.log(
|
||||
`API GET object request *fetch* error on attempt ${attempt}:`,
|
||||
error
|
||||
@@ -448,10 +477,13 @@ export const get_object = async function get_object({
|
||||
return false;
|
||||
}
|
||||
|
||||
// Log retry information
|
||||
if (log_lvl) {
|
||||
console.log(`Retrying... (${attempt}/${retry_count})`);
|
||||
}
|
||||
// Backoff before retrying. Without a delay, rapid retries on a flaky
|
||||
// connection accomplish nothing and add noise. Caps at 8s so later
|
||||
// attempts don't wait excessively. Gives the network time to recover
|
||||
// (ERR_NETWORK_CHANGED is typically a sub-second WiFi roam event).
|
||||
const delay_ms = Math.min(2000 * attempt, 8000);
|
||||
console.log(`API GET: Retrying in ${delay_ms}ms... (attempt ${attempt}/${retry_count})`);
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, delay_ms));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ export const patch_object = async function patch_object({
|
||||
headers = {},
|
||||
params = {},
|
||||
data = {},
|
||||
timeout = 60000,
|
||||
timeout = 20000,
|
||||
return_meta = false,
|
||||
log_lvl = 0,
|
||||
retry_count = 5
|
||||
@@ -153,9 +153,15 @@ export const patch_object = async function patch_object({
|
||||
}
|
||||
|
||||
for (let attempt = 1; attempt <= retry_count; attempt++) {
|
||||
// Keep timeout handle at attempt scope so catch can always clear it.
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
// AbortError alone is ambiguous. Track whether the helper timeout
|
||||
// fired so we can retry timeout-aborts but fail fast on caller abort.
|
||||
let did_timeout_abort = false;
|
||||
timeoutId = setTimeout(() => {
|
||||
did_timeout_abort = true;
|
||||
console.error(
|
||||
`API PATCH request timed out after ${timeout}ms.`
|
||||
);
|
||||
@@ -173,12 +179,52 @@ export const patch_object = async function patch_object({
|
||||
url.toString(),
|
||||
fetchOptions
|
||||
).catch(function (error: any) {
|
||||
// Keep noisy abort/network conditions out of high-level logs.
|
||||
if (
|
||||
error?.name === 'AbortError' ||
|
||||
error?.name === 'TypeError' ||
|
||||
error?.message?.includes('aborted')
|
||||
) {
|
||||
if (log_lvl > 1) {
|
||||
console.log(
|
||||
'API PATCH: Request aborted or browser-terminated.',
|
||||
error
|
||||
);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
console.log(
|
||||
'API PATCH Object *fetch* request was aborted or failed in an unexpected way.',
|
||||
error
|
||||
);
|
||||
return error;
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
|
||||
// Error object was returned from fetch catch block; decide retry class.
|
||||
if (
|
||||
response instanceof Error ||
|
||||
(response &&
|
||||
(response.name === 'AbortError' ||
|
||||
response.name === 'TypeError'))
|
||||
) {
|
||||
if (response.name === 'AbortError') {
|
||||
// Retry only helper-timeout aborts. Caller/navigation aborts
|
||||
// should fail fast to avoid duplicate mutation side-effects.
|
||||
if (did_timeout_abort) {
|
||||
throw new Error(
|
||||
`Timeout abort (attempt ${attempt}/${retry_count}) after ${timeout}ms`
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Transient browser/network failure class.
|
||||
throw new Error(
|
||||
`Network error (attempt ${attempt}): ${response.message}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
throw new Error(
|
||||
@@ -292,6 +338,8 @@ export const patch_object = async function patch_object({
|
||||
? json.data
|
||||
: json;
|
||||
} catch (error) {
|
||||
// Ensure per-attempt timeout is always cleared on failure.
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
console.error(`API PATCH error on attempt ${attempt}:`, error);
|
||||
|
||||
if (attempt === retry_count) {
|
||||
@@ -299,9 +347,12 @@ export const patch_object = async function patch_object({
|
||||
return false;
|
||||
}
|
||||
|
||||
if (log_lvl) {
|
||||
console.log(`Retrying... (${attempt}/${retry_count})`);
|
||||
}
|
||||
// Backoff before retrying. Caps at 8s to match GET/POST policy.
|
||||
const delay_ms = Math.min(2000 * attempt, 8000);
|
||||
console.log(
|
||||
`API PATCH: Retrying in ${delay_ms}ms... (attempt ${attempt}/${retry_count})`
|
||||
);
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, delay_ms));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ export const post_object = async function post_object({
|
||||
params = {},
|
||||
data = {},
|
||||
form_data = null,
|
||||
timeout = 90000,
|
||||
timeout = 20000,
|
||||
return_meta = false,
|
||||
return_blob = false,
|
||||
filename = '',
|
||||
@@ -200,13 +200,19 @@ export const post_object = async function post_object({
|
||||
}
|
||||
|
||||
for (let attempt = 1; attempt <= retry_count; attempt++) {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.error(`API POST request timed out after ${timeout}ms.`);
|
||||
controller.abort();
|
||||
}, timeout);
|
||||
// Declared at loop scope (not inside try) so the catch block can clearTimeout.
|
||||
// Fresh controller per attempt — same rationale as api_get_object.ts.
|
||||
const controller = new AbortController();
|
||||
// AbortError is not specific enough by itself. Distinguish timeout-aborts
|
||||
// (retryable transient class) from intentional caller aborts (fail-fast).
|
||||
let did_timeout_abort = false;
|
||||
const timeoutId = setTimeout(() => {
|
||||
did_timeout_abort = true;
|
||||
console.warn(`API POST: Request timed out after ${timeout}ms (attempt ${attempt}/${retry_count}).`);
|
||||
controller.abort();
|
||||
}, timeout);
|
||||
|
||||
try {
|
||||
const fetchOptions: RequestInit = {
|
||||
method: 'POST',
|
||||
headers: headers_cleaned,
|
||||
@@ -245,19 +251,28 @@ export const post_object = async function post_object({
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// Check if we should stop due to abort or network failure
|
||||
// Check if we should stop due to abort or network failure.
|
||||
if (
|
||||
response instanceof Error ||
|
||||
(response &&
|
||||
(response.name === 'TypeError' ||
|
||||
response.name === 'AbortError'))
|
||||
) {
|
||||
if (response.name === 'AbortError') return false;
|
||||
if (log_lvl > 1)
|
||||
console.log(
|
||||
'API POST Object: Detected NetworkError or TypeError. Failing fast.'
|
||||
);
|
||||
return false;
|
||||
// Retry timeout-aborts from this helper; do not retry caller aborts
|
||||
// (route change/unmount/manual cancellation).
|
||||
if (response.name === 'AbortError') {
|
||||
if (did_timeout_abort) {
|
||||
throw new Error(
|
||||
`Timeout abort (attempt ${attempt}/${retry_count}) after ${timeout}ms`
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// TypeError = transient network failure. Throw into the retry loop
|
||||
// so backoff-and-retry applies. Same fix as api_get_object.ts — see
|
||||
// comment there for the full history of why this was broken.
|
||||
throw new Error(`Network error (attempt ${attempt}): ${response.message}`);
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
@@ -411,6 +426,8 @@ export const post_object = async function post_object({
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ensure the per-attempt timeout timer is always cancelled on failure.
|
||||
clearTimeout(timeoutId);
|
||||
console.error(`API POST error on attempt ${attempt}:`, error);
|
||||
|
||||
if (attempt === retry_count) {
|
||||
@@ -418,9 +435,10 @@ export const post_object = async function post_object({
|
||||
return false;
|
||||
}
|
||||
|
||||
if (log_lvl) {
|
||||
console.log(`Retrying... (${attempt}/${retry_count})`);
|
||||
}
|
||||
// Backoff before retrying — same rationale as api_get_object.ts.
|
||||
const delay_ms = Math.min(2000 * attempt, 8000);
|
||||
console.log(`API POST: Retrying in ${delay_ms}ms... (attempt ${attempt}/${retry_count})`);
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, delay_ms));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -35,6 +35,7 @@ interface Props {
|
||||
require_auth?: boolean;
|
||||
classes?: string;
|
||||
click?: () => void | Promise<any>;
|
||||
track_click_promise?: boolean;
|
||||
label?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
@@ -57,6 +58,7 @@ let {
|
||||
require_auth = true,
|
||||
classes = '',
|
||||
click,
|
||||
track_click_promise = true,
|
||||
label
|
||||
}: Props = $props();
|
||||
|
||||
@@ -128,10 +130,7 @@ $effect(() => {
|
||||
let ae_promises: key_val = $state({});
|
||||
|
||||
$effect(() => {
|
||||
const file_id =
|
||||
hosted_file_obj?.id ||
|
||||
hosted_file_obj?.hosted_file_id ||
|
||||
hosted_file_id;
|
||||
const file_id = hosted_file_obj?.hosted_file_id ?? hosted_file_id;
|
||||
if (file_id && $ae_sess?.api_download_kv[file_id]?.percent_completed) {
|
||||
download_percent = $ae_sess.api_download_kv[file_id].percent_completed;
|
||||
}
|
||||
@@ -139,10 +138,7 @@ $effect(() => {
|
||||
|
||||
// Reactive timer to alternate views during active download
|
||||
$effect(() => {
|
||||
const file_id =
|
||||
hosted_file_obj?.id ||
|
||||
hosted_file_obj?.hosted_file_id ||
|
||||
hosted_file_id;
|
||||
const file_id = hosted_file_obj?.hosted_file_id ?? hosted_file_id;
|
||||
const is_actively_downloading =
|
||||
ae_promises[file_id] && download_complete === undefined;
|
||||
|
||||
@@ -178,32 +174,50 @@ let shortened_filename = $derived(
|
||||
})
|
||||
);
|
||||
|
||||
let is_url_file = $derived.by(() => {
|
||||
const raw_filename = (hosted_file_obj?.filename ?? '').toLowerCase();
|
||||
const extension = (hosted_file_obj?.extension ?? '').toLowerCase();
|
||||
return (
|
||||
raw_filename.startsWith('http://') ||
|
||||
raw_filename.startsWith('https://') ||
|
||||
extension === 'url'
|
||||
);
|
||||
});
|
||||
|
||||
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() {
|
||||
const file_id =
|
||||
hosted_file_obj?.id ||
|
||||
hosted_file_obj?.hosted_file_id ||
|
||||
hosted_file_id;
|
||||
const file_id = hosted_file_obj?.hosted_file_id ?? hosted_file_id;
|
||||
|
||||
// URL-backed records are intentionally not downloaded. Callers are expected
|
||||
// to provide a custom click handler that opens the URL directly.
|
||||
if (is_url_file) {
|
||||
if (click) {
|
||||
const result = click();
|
||||
if (track_click_promise && result instanceof Promise) {
|
||||
ae_promises[file_id] = result;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
download_complete = undefined;
|
||||
download_status_msg = 'Downloading...';
|
||||
|
||||
if (click) {
|
||||
const result = click();
|
||||
// If the override returns a promise, track it so the UI shows progress
|
||||
if (result instanceof Promise) {
|
||||
// If the override returns a promise, track it so the UI shows progress.
|
||||
// Launcher open flows can opt out so native status messages stay authoritative.
|
||||
if (track_click_promise && result instanceof Promise) {
|
||||
ae_promises[file_id] = result;
|
||||
}
|
||||
return;
|
||||
@@ -238,10 +252,7 @@ async function handle_click() {
|
||||
</script>
|
||||
|
||||
{#snippet content()}
|
||||
{@const file_id =
|
||||
hosted_file_obj?.id ||
|
||||
hosted_file_obj?.hosted_file_id ||
|
||||
hosted_file_id}
|
||||
{@const file_id = hosted_file_obj?.hosted_file_id ?? hosted_file_id}
|
||||
{#await ae_promises[file_id]}
|
||||
<div class="flex min-h-[1.5rem] w-full items-center">
|
||||
<div
|
||||
@@ -316,8 +327,7 @@ async function handle_click() {
|
||||
{/snippet}
|
||||
|
||||
{#if hosted_file_id && hosted_file_obj}
|
||||
{@const file_id =
|
||||
hosted_file_obj.id || hosted_file_obj.hosted_file_id || hosted_file_id}
|
||||
{@const file_id = hosted_file_obj.hosted_file_id ?? hosted_file_id}
|
||||
|
||||
{#if show_direct_download}
|
||||
<a
|
||||
@@ -333,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
|
||||
|
||||
@@ -221,23 +221,37 @@ async function _refresh_site_domain_background({
|
||||
});
|
||||
|
||||
// WHY: The fast-path returns stale Dexie cache, then this background refresh
|
||||
// runs after the page renders. If cfg_json changed server-side (e.g. a Novi
|
||||
// API key was added), the stale cfg is already in $ae_loc. We push the fresh
|
||||
// cfg_json into the store here so any layout tracking it (e.g. IDAA Novi
|
||||
// verification) gets notified and can retry with the correct config.
|
||||
// runs after the page renders. Push any fields that may have been missing from
|
||||
// the stale cache (e.g. account_name, cfg_json) back into $ae_loc so the UI
|
||||
// reflects the correct values without requiring a second full page reload.
|
||||
const loc_patch: Record<string, unknown> = {};
|
||||
|
||||
if (result.cfg_json) {
|
||||
const current_cfg = get(ae_loc).site_cfg_json;
|
||||
if (
|
||||
JSON.stringify(current_cfg) !==
|
||||
JSON.stringify(result.cfg_json)
|
||||
) {
|
||||
ae_loc.update((loc) => ({
|
||||
...loc,
|
||||
site_cfg_json: result.cfg_json
|
||||
}));
|
||||
if (JSON.stringify(current_cfg) !== JSON.stringify(result.cfg_json)) {
|
||||
loc_patch.site_cfg_json = result.cfg_json;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.account_name) {
|
||||
const current_name = get(ae_loc).account_name;
|
||||
// Only overwrite the default placeholder — don't stomp a real value.
|
||||
if (!current_name || current_name === 'Account Name Not Set' || current_name === 'Ghost Account') {
|
||||
loc_patch.account_name = result.account_name;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.account_code) {
|
||||
const current_code = get(ae_loc).account_code;
|
||||
if (!current_code || current_code === 'not_set' || current_code === 'ghost') {
|
||||
loc_patch.account_code = result.account_code;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(loc_patch).length > 0) {
|
||||
ae_loc.update((loc) => ({ ...loc, ...loc_patch }));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -1,6 +1,53 @@
|
||||
import type { Dexie, Table } from 'dexie';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
/**
|
||||
* Checks IDB table content versions and clears any tables whose version has
|
||||
* changed since the last check.
|
||||
*
|
||||
* Version numbers live in IDB_CONTENT_VERSIONS (store_versions.ts). State is
|
||||
* tracked in localStorage (ae_idb_ver__{module}__{table}) so each table is
|
||||
* cleared exactly once per version bump, not on every page load.
|
||||
*
|
||||
* Call once at module init in each db_*.ts, after the singleton is created.
|
||||
* The clear is intentionally fire-and-forget — the SWR pattern repopulates
|
||||
* from the API naturally after a cache miss.
|
||||
*
|
||||
* A null stored version (never tracked) is treated as outdated so existing
|
||||
* installs with stale cached data are cleaned up on first run.
|
||||
*/
|
||||
export async function check_and_clear_idb_tables({
|
||||
db_instance,
|
||||
module_name,
|
||||
table_versions,
|
||||
log_lvl = 0
|
||||
}: {
|
||||
db_instance: Dexie;
|
||||
module_name: string;
|
||||
table_versions: Record<string, number>;
|
||||
log_lvl?: number;
|
||||
}): Promise<void> {
|
||||
if (!browser) return;
|
||||
|
||||
for (const [table_name, expected_version] of Object.entries(table_versions)) {
|
||||
const ls_key = `ae_idb_ver__${module_name}__${table_name}`;
|
||||
const stored_raw = localStorage.getItem(ls_key);
|
||||
const stored_version = stored_raw !== null ? parseInt(stored_raw, 10) : null;
|
||||
|
||||
if (stored_version === expected_version) continue;
|
||||
|
||||
try {
|
||||
await db_instance.table(table_name).clear();
|
||||
localStorage.setItem(ls_key, String(expected_version));
|
||||
console.log(
|
||||
`[IDB] "${module_name}.${table_name}" cleared — v${stored_version ?? 'new'} → v${expected_version}`
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn(`[IDB] Failed to clear "${module_name}.${table_name}":`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the primary key from an object using a prioritized list of possible key names.
|
||||
* @param obj The object to extract the ID from.
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -157,15 +157,23 @@ function handle_save() {
|
||||
<!-- Trigger Button -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={generate_ai_result}
|
||||
onclick={() => {
|
||||
if (summary) {
|
||||
tmp_summary = summary;
|
||||
active_tab = 'result';
|
||||
show_modal = true;
|
||||
} else {
|
||||
generate_ai_result();
|
||||
}
|
||||
}}
|
||||
class={buttonClass}
|
||||
title="Generate AI summary/analysis">
|
||||
title={summary ? 'View existing AI summary' : 'Generate AI summary/analysis'}>
|
||||
{#await ae_promises}
|
||||
<Loader class="mr-1 inline-block animate-spin" size="1.2em" />
|
||||
<span class="text-sm">Processing...</span>
|
||||
{:then}
|
||||
<BotMessageSquare class="mr-1 inline-block" size="1.2em" />
|
||||
<span class="text-sm">Summarize</span>
|
||||
<span class="text-sm hidden">Summarize</span>
|
||||
{:catch}
|
||||
<span class="text-sm text-red-500">Error</span>
|
||||
{/await}
|
||||
@@ -181,6 +189,7 @@ function handle_save() {
|
||||
class="btn btn-sm preset-tonal-surface shadow-md"
|
||||
title="AI Settings">
|
||||
<Settings size="1.2em" />
|
||||
<span class="text-sm hidden">Settings</span>
|
||||
</button>
|
||||
|
||||
<!-- Unified AI Modal -->
|
||||
|
||||
@@ -787,7 +787,7 @@ export const properties_to_save = [
|
||||
'event_id',
|
||||
'code',
|
||||
'account_id',
|
||||
'account_id_random',
|
||||
// 'account_id_random',
|
||||
'conference',
|
||||
'type',
|
||||
'name',
|
||||
|
||||
@@ -359,7 +359,7 @@ export async function create_event_file_obj_from_hosted_file_async({
|
||||
});
|
||||
|
||||
if (return_obj) return result;
|
||||
return result?.event_file_id || result?.id || result?.event_file_id_random;
|
||||
return result?.event_file_id || result?.id;
|
||||
}
|
||||
|
||||
export async function delete_ae_obj_id__event_file({
|
||||
@@ -527,15 +527,11 @@ export const qry__event_file = search__event_file;
|
||||
export const properties_to_save = [
|
||||
'id',
|
||||
'event_file_id',
|
||||
// 'event_file_id_random', // DO NOT UNCOMMENT
|
||||
'hosted_file_id',
|
||||
// 'hosted_file_id_random', // DO NOT UNCOMMENT
|
||||
'hash_sha256',
|
||||
'for_type',
|
||||
'for_id',
|
||||
// 'for_id_random', // DO NOT UNCOMMENT
|
||||
'event_id',
|
||||
// 'event_id_random', // DO NOT UNCOMMENT
|
||||
'event_session_id',
|
||||
'event_presentation_id',
|
||||
'event_presenter_id',
|
||||
@@ -598,22 +594,9 @@ async function _process_generic_props<T extends Record<string, any>>({
|
||||
const processed_obj_li: T[] = [];
|
||||
for (const original_obj of obj_li) {
|
||||
let processed_obj = { ...original_obj };
|
||||
for (const key in processed_obj) {
|
||||
if (key.endsWith('_random')) {
|
||||
const newKey = key.slice(0, -7);
|
||||
// ONLY overwrite if the random variant has a valid value
|
||||
if (
|
||||
processed_obj[key] !== null &&
|
||||
processed_obj[key] !== undefined &&
|
||||
processed_obj[key] !== ''
|
||||
) {
|
||||
(processed_obj as any)[newKey] = processed_obj[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
const random_id_key = `${obj_type}_id_random`;
|
||||
if (processed_obj[random_id_key])
|
||||
(processed_obj as any).id = processed_obj[random_id_key];
|
||||
const base_id_key = `${obj_type}_id`;
|
||||
if (processed_obj[base_id_key])
|
||||
(processed_obj as any).id = processed_obj[base_id_key];
|
||||
const group = processed_obj.group ?? '0';
|
||||
const priority = processed_obj.priority ? 1 : 0;
|
||||
const sort = processed_obj.sort ?? '0';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -666,6 +666,7 @@ export async function search__event_session({
|
||||
event_id,
|
||||
fulltext_search_qry_str = '',
|
||||
ft_presenter_search_qry_str = '',
|
||||
ft_presentation_search_qry_str = '',
|
||||
like_search_qry_str = '',
|
||||
like_presentation_search_qry_str = '',
|
||||
like_presenter_search_qry_str = '',
|
||||
@@ -688,6 +689,7 @@ export async function search__event_session({
|
||||
event_id: string;
|
||||
fulltext_search_qry_str?: string;
|
||||
ft_presenter_search_qry_str?: string | null;
|
||||
ft_presentation_search_qry_str?: string | null;
|
||||
like_search_qry_str?: string;
|
||||
like_presentation_search_qry_str?: string;
|
||||
like_presenter_search_qry_str?: string;
|
||||
@@ -710,26 +712,32 @@ export async function search__event_session({
|
||||
q: '',
|
||||
and: [{ field: 'event_id', op: 'eq', value: event_id }]
|
||||
};
|
||||
if (fulltext_search_qry_str || ft_presenter_search_qry_str) {
|
||||
if (fulltext_search_qry_str || ft_presenter_search_qry_str || ft_presentation_search_qry_str) {
|
||||
const ft: any = {};
|
||||
if (fulltext_search_qry_str && fulltext_search_qry_str.length > 2)
|
||||
ft['default_qry_str'] = fulltext_search_qry_str;
|
||||
if (
|
||||
ft_presenter_search_qry_str &&
|
||||
ft_presenter_search_qry_str.length > 2
|
||||
)
|
||||
) {
|
||||
ft['event_presenter_li_qry_str'] = ft_presenter_search_qry_str;
|
||||
// These fields only exist in v_event_session_w_file_count (alt view)
|
||||
view = 'alt';
|
||||
}
|
||||
if (
|
||||
ft_presentation_search_qry_str &&
|
||||
ft_presentation_search_qry_str.length > 2
|
||||
) {
|
||||
ft['event_presentation_li_qry_str'] = ft_presentation_search_qry_str;
|
||||
// These fields only exist in v_event_session_w_file_count (alt view)
|
||||
view = 'alt';
|
||||
}
|
||||
if (Object.keys(ft).length) search_query.params = { ft_qry: ft };
|
||||
}
|
||||
if (enabled === 'enabled')
|
||||
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',
|
||||
@@ -761,6 +769,7 @@ export async function search__event_session({
|
||||
view,
|
||||
limit,
|
||||
offset,
|
||||
hidden,
|
||||
log_lvl
|
||||
});
|
||||
|
||||
@@ -820,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({
|
||||
@@ -836,12 +845,10 @@ export async function email_sign_in__event_session({
|
||||
export const properties_to_save = [
|
||||
'id',
|
||||
'event_session_id',
|
||||
'event_session_id_random',
|
||||
'external_id',
|
||||
'code',
|
||||
'for_type',
|
||||
'for_id',
|
||||
'for_id_random',
|
||||
'type_code',
|
||||
'event_id',
|
||||
'event_location_id',
|
||||
@@ -880,7 +887,9 @@ export const properties_to_save = [
|
||||
'event_name',
|
||||
'event_location_code',
|
||||
'event_location_name',
|
||||
'event_presentation_li'
|
||||
'event_presentation_li',
|
||||
'event_presentation_li_qry_str',
|
||||
'event_presenter_li_qry_str'
|
||||
];
|
||||
|
||||
async function _process_generic_props<T extends Record<string, any>>({
|
||||
@@ -898,18 +907,8 @@ async function _process_generic_props<T extends Record<string, any>>({
|
||||
const processed_obj_li: T[] = [];
|
||||
for (const original_obj of obj_li) {
|
||||
let processed_obj = { ...original_obj };
|
||||
for (const key in processed_obj) {
|
||||
if (key.endsWith('_random')) {
|
||||
const newKey = key.slice(0, -7);
|
||||
(processed_obj as any)[newKey] = processed_obj[key];
|
||||
}
|
||||
}
|
||||
const randomIdKey = `${obj_type}_id_random`;
|
||||
const baseIdKey = `${obj_type}_id`;
|
||||
if (processed_obj[randomIdKey]) {
|
||||
(processed_obj as any).id = processed_obj[randomIdKey];
|
||||
(processed_obj as any)[baseIdKey] = processed_obj[randomIdKey];
|
||||
} else if (processed_obj[baseIdKey])
|
||||
if (processed_obj[baseIdKey])
|
||||
(processed_obj as any).id = processed_obj[baseIdKey];
|
||||
|
||||
const group = processed_obj.group ?? '0';
|
||||
|
||||
400
src/lib/ae_events/ae_launcher__default_launch_profiles.ts
Normal file
400
src/lib/ae_events/ae_launcher__default_launch_profiles.ts
Normal file
@@ -0,0 +1,400 @@
|
||||
/**
|
||||
* ae_launcher__default_launch_profiles.ts
|
||||
*
|
||||
* Built-in launch profiles for the Aether Events Launcher — the Svelte-side
|
||||
* replacement for the legacy OSIT MasterKey Swift app.
|
||||
*
|
||||
* These are the last-resort defaults. Override priority (high → low):
|
||||
* 1. event_file.cfg_json.display_override — per-file, display_mode only
|
||||
* 2. event_device.data_json.launch_profiles[profile] — per-profile override, per device (API)
|
||||
* 3. $events_loc.launcher.launch_profiles[profile] — local persistent override
|
||||
* 4. DEFAULT_LAUNCH_PROFILES[profile/alias] — canonical built-ins + aliases
|
||||
* 5. DEFAULT_LAUNCH_PROFILES['default'] — catch-all
|
||||
*
|
||||
* Keys are lowercase file extensions without the dot: "pptx", "key", "pdf", etc.
|
||||
* The special key "default" catches any unrecognised extension.
|
||||
*
|
||||
* post_script formats:
|
||||
* - Plain string → run as AppleScript via run_osascript() (macOS only)
|
||||
* - "shell:..." prefix → run as shell command via run_cmd()
|
||||
*
|
||||
* Reserved for future use:
|
||||
* - speed_factor: number — delay multiplier for slower machines (1.0 = normal)
|
||||
*
|
||||
* Special pseudo-extension:
|
||||
* - url — web-based presentations. Handled by the launcher URL branch rather
|
||||
* than a cache-to-temp open flow.
|
||||
*/
|
||||
|
||||
export interface LaunchProfile {
|
||||
/** Human-readable label for status messages */
|
||||
app: string;
|
||||
/** Display layout to set before opening. 'extend' only applied if external display found. */
|
||||
display_mode: 'extend' | 'mirror' | 'none';
|
||||
/**
|
||||
* Shell command to open the file. {{path}} is replaced with the resolved temp path.
|
||||
* If omitted, falls back to open_local_file_v2(path) — OS default handler.
|
||||
*/
|
||||
open_cmd?: string;
|
||||
/**
|
||||
* Script to run after the file opens and post_delay_ms has elapsed.
|
||||
* Plain string → AppleScript (macOS). "shell:" prefix → shell command.
|
||||
*/
|
||||
post_script?: string;
|
||||
/**
|
||||
* Milliseconds to wait after open_cmd before running post_script.
|
||||
* Default: 2000. Can be overridden per profile via launch_profiles[profile].post_delay_ms.
|
||||
*/
|
||||
post_delay_ms?: number;
|
||||
|
||||
// --- Reserved for future use — not yet implemented ---
|
||||
// speed_factor?: number;
|
||||
// url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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',
|
||||
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`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Linux VLC profile — uses shell command for compatibility.
|
||||
*/
|
||||
function make_vlc_mirror_linux_profile(): LaunchProfile {
|
||||
return {
|
||||
app: 'VLC (Linux)',
|
||||
display_mode: 'mirror',
|
||||
// shell: prefix runs as bash command. Same flags as macOS: `--no-play-and-exit` keeps window open, `--play-and-pause` holds final frame.
|
||||
open_cmd: 'shell:vlc --no-play-and-exit --play-and-pause "{{path}}"',
|
||||
post_delay_ms: 1000
|
||||
// No post_script on Linux — VLC opens fullscreen by default, no need to send F.
|
||||
};
|
||||
}
|
||||
|
||||
const POWERPOINT_MAC_EXTEND_PROFILE: LaunchProfile = {
|
||||
app: 'Microsoft PowerPoint',
|
||||
display_mode: 'extend',
|
||||
open_cmd: 'open -a "Microsoft PowerPoint" "{{path}}"',
|
||||
post_delay_ms: 1000,
|
||||
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
|
||||
end tell
|
||||
end repeat
|
||||
delay 0.3
|
||||
tell application "System Events"
|
||||
tell process "Microsoft PowerPoint"
|
||||
keystroke return using command down
|
||||
end tell
|
||||
end tell`
|
||||
};
|
||||
|
||||
const KEYNOTE_MAC_EXTEND_PROFILE: LaunchProfile = {
|
||||
app: 'Keynote',
|
||||
display_mode: 'extend',
|
||||
open_cmd: 'open -a "Keynote" "{{path}}"',
|
||||
post_delay_ms: 1000,
|
||||
// Keynote uses `start (front document)` which requires the document to actually be loaded —
|
||||
// polling frontmost is not enough here. Poll document count instead.
|
||||
post_script: `tell application "Keynote"
|
||||
activate
|
||||
end tell
|
||||
repeat 20 times
|
||||
delay 0.5
|
||||
tell application "Keynote"
|
||||
if (count of documents) > 0 then exit repeat
|
||||
end tell
|
||||
end repeat
|
||||
delay 0.3
|
||||
tell application "Keynote"
|
||||
start (front document)
|
||||
end tell`
|
||||
};
|
||||
|
||||
const LIBREOFFICE_MAC_EXTEND_PROFILE: LaunchProfile = {
|
||||
app: 'LibreOffice',
|
||||
display_mode: 'extend',
|
||||
open_cmd: 'open -a "LibreOffice" "{{path}}"',
|
||||
post_delay_ms: 1000,
|
||||
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
|
||||
end tell
|
||||
end repeat
|
||||
delay 0.3
|
||||
tell application "System Events"
|
||||
tell process "soffice"
|
||||
key code 96
|
||||
end tell
|
||||
end tell`
|
||||
};
|
||||
|
||||
const ACROBAT_MAC_MIRROR_PROFILE: LaunchProfile = {
|
||||
app: 'Adobe Acrobat Reader DC',
|
||||
display_mode: 'mirror',
|
||||
open_cmd: 'open -a "Adobe Acrobat Reader DC" "{{path}}"',
|
||||
post_delay_ms: 1000,
|
||||
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
|
||||
end tell
|
||||
end repeat
|
||||
delay 0.3
|
||||
tell application "System Events"
|
||||
tell process "AdobeReader"
|
||||
keystroke "l" using command down
|
||||
end tell
|
||||
end tell`
|
||||
};
|
||||
|
||||
const VLC_MIRROR_MAC_PROFILE: LaunchProfile = make_vlc_mirror_mac_profile();
|
||||
const VLC_MIRROR_LINUX_PROFILE: LaunchProfile = make_vlc_mirror_linux_profile();
|
||||
|
||||
const POWERPOINT_WIN_EXTEND_PROFILE: LaunchProfile = {
|
||||
app: 'Microsoft Office PowerPoint (Windows)',
|
||||
display_mode: 'extend',
|
||||
open_cmd: 'open -a "Microsoft Office PowerPoint" "{{path}}"',
|
||||
post_delay_ms: 1500,
|
||||
post_script: `tell application "Microsoft Office PowerPoint"
|
||||
activate
|
||||
end tell
|
||||
repeat 15 times
|
||||
delay 0.5
|
||||
tell application "System Events"
|
||||
if frontmost of process "Microsoft Office PowerPoint" is true then exit repeat
|
||||
end tell
|
||||
end repeat
|
||||
delay 0.3
|
||||
tell application "System Events"
|
||||
key code 96
|
||||
end tell`
|
||||
};
|
||||
|
||||
const LIBREOFFICE_WIN_EXTEND_PROFILE: LaunchProfile = {
|
||||
app: 'LibreOffice (Windows)',
|
||||
display_mode: 'extend',
|
||||
open_cmd: 'open -a "LibreOffice" "{{path}}"',
|
||||
post_delay_ms: 1500,
|
||||
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
|
||||
end tell
|
||||
end repeat
|
||||
delay 0.3
|
||||
tell application "System Events"
|
||||
tell process "soffice"
|
||||
key code 96
|
||||
end tell
|
||||
end tell`
|
||||
};
|
||||
|
||||
const ACROBAT_WIN_MIRROR_PROFILE: LaunchProfile = {
|
||||
app: 'Acrobat Reader (Windows)',
|
||||
display_mode: 'mirror',
|
||||
open_cmd: 'open -a "Acrobat Reader Windows" "{{path}}"',
|
||||
post_delay_ms: 1500,
|
||||
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
|
||||
end tell
|
||||
end repeat
|
||||
delay 0.3
|
||||
tell application "System Events"
|
||||
key code 108 using control down
|
||||
end tell`
|
||||
};
|
||||
|
||||
const URL_WEB_PROFILE: LaunchProfile = {
|
||||
app: 'Chrome',
|
||||
display_mode: 'extend',
|
||||
// No open_cmd or post_script — URL branch in handle_open_file() handles this
|
||||
};
|
||||
|
||||
const DEFAULT_OS_PROFILE: LaunchProfile = {
|
||||
app: 'OS Default',
|
||||
display_mode: 'none',
|
||||
// No open_cmd — execution falls through to open_local_file_v2(path)
|
||||
// No post_script
|
||||
};
|
||||
|
||||
type DefaultLaunchProfileDefinition = {
|
||||
name: string;
|
||||
aliases: string[];
|
||||
profile: LaunchProfile;
|
||||
};
|
||||
|
||||
export const DEFAULT_LAUNCH_PROFILE_DEFS: DefaultLaunchProfileDefinition[] = [
|
||||
{
|
||||
name: 'powerpoint_mac_extend',
|
||||
aliases: ['pptx', 'ppt'],
|
||||
profile: POWERPOINT_MAC_EXTEND_PROFILE
|
||||
},
|
||||
{
|
||||
name: 'keynote_mac_extend',
|
||||
aliases: ['key'],
|
||||
profile: KEYNOTE_MAC_EXTEND_PROFILE
|
||||
},
|
||||
{
|
||||
name: 'libreoffice_mac_extend',
|
||||
aliases: ['odp'],
|
||||
profile: LIBREOFFICE_MAC_EXTEND_PROFILE
|
||||
},
|
||||
{
|
||||
name: 'acrobat_mac_mirror',
|
||||
aliases: ['pdf'],
|
||||
profile: ACROBAT_MAC_MIRROR_PROFILE
|
||||
},
|
||||
{
|
||||
name: 'vlc_mirror_mac',
|
||||
aliases: [],
|
||||
profile: VLC_MIRROR_MAC_PROFILE
|
||||
},
|
||||
{
|
||||
name: 'vlc_mirror_linux',
|
||||
aliases: [],
|
||||
profile: VLC_MIRROR_LINUX_PROFILE
|
||||
},
|
||||
{
|
||||
name: 'vlc_mirror',
|
||||
aliases: ['mp4', 'mkv', 'mov', 'mpeg', 'avi', 'flv', 'ogg', 'ogv', 'mp3', 'm4v', 'm4a', 'webm', 'wmv', 'wav', 'aac', 'flac'],
|
||||
profile: VLC_MIRROR_MAC_PROFILE // Default to macOS (primary deployment platform)
|
||||
},
|
||||
{
|
||||
name: 'powerpoint_win_extend',
|
||||
aliases: ['pptxwin', 'pptwin'],
|
||||
profile: POWERPOINT_WIN_EXTEND_PROFILE
|
||||
},
|
||||
{
|
||||
name: 'libreoffice_win_extend',
|
||||
aliases: ['odpwin'],
|
||||
profile: LIBREOFFICE_WIN_EXTEND_PROFILE
|
||||
},
|
||||
{
|
||||
name: 'acrobat_win_mirror',
|
||||
aliases: ['pdfwin'],
|
||||
profile: ACROBAT_WIN_MIRROR_PROFILE
|
||||
},
|
||||
{
|
||||
name: 'url_web',
|
||||
aliases: ['url'],
|
||||
profile: URL_WEB_PROFILE
|
||||
},
|
||||
{
|
||||
name: 'os_default',
|
||||
aliases: ['default'],
|
||||
profile: DEFAULT_OS_PROFILE
|
||||
}
|
||||
];
|
||||
|
||||
export const DEFAULT_LAUNCH_PROFILE_LIBRARY: Record<string, LaunchProfile> = Object.fromEntries(
|
||||
DEFAULT_LAUNCH_PROFILE_DEFS.map(({ name, profile }) => [name, profile])
|
||||
);
|
||||
|
||||
export const DEFAULT_LAUNCH_PROFILE_ALIASES: Record<string, string> = Object.fromEntries(
|
||||
DEFAULT_LAUNCH_PROFILE_DEFS.flatMap(({ name, aliases }) =>
|
||||
aliases.map((alias) => [alias, name])
|
||||
)
|
||||
);
|
||||
|
||||
export const DEFAULT_LAUNCH_PROFILES: Record<string, LaunchProfile> = Object.fromEntries(
|
||||
DEFAULT_LAUNCH_PROFILE_DEFS.flatMap(({ name, aliases, profile }) => [
|
||||
[name, { ...profile }],
|
||||
...aliases.map((alias) => [alias, { ...profile }])
|
||||
])
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns a shallow copy of the built-in profile for the given extension,
|
||||
* with a display_override applied if provided.
|
||||
*
|
||||
* Falls back to 'default' if no specific profile exists.
|
||||
*/
|
||||
export function resolve_launch_profile(
|
||||
extension: string,
|
||||
display_override?: 'extend' | 'mirror' | 'none' | null,
|
||||
device_profiles?: Record<string, Partial<LaunchProfile>> | null,
|
||||
local_profiles?: Record<string, Partial<LaunchProfile>> | null
|
||||
): LaunchProfile {
|
||||
const ext = (extension || '').toLowerCase().replace(/^\./, '');
|
||||
const canonical_profile_name = DEFAULT_LAUNCH_PROFILE_ALIASES[ext] ?? ext;
|
||||
|
||||
const built_in_profile =
|
||||
DEFAULT_LAUNCH_PROFILE_LIBRARY[canonical_profile_name] ??
|
||||
DEFAULT_LAUNCH_PROFILE_LIBRARY.os_default;
|
||||
|
||||
const local_profile =
|
||||
local_profiles?.[canonical_profile_name] ??
|
||||
local_profiles?.[ext] ??
|
||||
local_profiles?.['default'] ??
|
||||
null;
|
||||
|
||||
const device_profile =
|
||||
device_profiles?.[canonical_profile_name] ??
|
||||
device_profiles?.[ext] ??
|
||||
device_profiles?.['default'] ??
|
||||
null;
|
||||
|
||||
const profile = {
|
||||
...built_in_profile,
|
||||
...(local_profile ?? {}),
|
||||
...(device_profile ?? {})
|
||||
};
|
||||
|
||||
// Per-file display override wins over everything
|
||||
if (display_override) {
|
||||
profile.display_mode = display_override;
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/* =============================================================================
|
||||
Badge Layout: Epson ColorWorks — Fanfold 4" × 6" (Single-sided)
|
||||
layout code: badge_4x6_fanfold
|
||||
Badge stock: 4in wide × 6in per label, single-sided continuous fanfold
|
||||
Used for: Axonius Adapt 2026 (June 2026)
|
||||
|
||||
Physical notes (measured 2026-05-15):
|
||||
Overall: 4.0in × 6.0in (portrait)
|
||||
Lanyard hole: 5/8in wide × 1/8in tall, centered horizontally,
|
||||
1/4in from top edge, 3/8in from each side edge.
|
||||
Keep decorative header content below the hole zone (~3/8in from top).
|
||||
|
||||
Print behavior:
|
||||
Single-sided only. Set duplex=0 on the template — the badge_back section
|
||||
will not render at all. @page size (4in × 6in) is injected dynamically
|
||||
by print/+page.svelte <svelte:head> based on the layout field.
|
||||
|
||||
CSS scope:
|
||||
All rules scoped under [data-layout="badge_4x6_fanfold"] to avoid conflicts
|
||||
with other layouts compiled into the same bundle. These override the
|
||||
Tailwind utility classes (w-[4in], min-h-[6.0in], etc.) hardcoded on the
|
||||
badge sections — attribute + class selectors win over single class selectors.
|
||||
============================================================================= */
|
||||
|
||||
/* --- Badge front --- */
|
||||
|
||||
[data-layout='badge_4x6_fanfold'] .badge_front {
|
||||
min-width: 4in;
|
||||
width: 4in;
|
||||
min-height: 6in;
|
||||
max-height: 6in;
|
||||
|
||||
/* debug */
|
||||
/* outline: thick solid orange; */
|
||||
}
|
||||
|
||||
/*
|
||||
* Header image zone: 624×232px at full 4in badge width → natural height ≈ 1.49in.
|
||||
* Override Tailwind's max-h-[1.00in] to avoid cropping the bottom of the image.
|
||||
* min-h-[.50in] from the component HTML is fine; leave it in place.
|
||||
*/
|
||||
[data-layout='badge_4x6_fanfold'] .badge_header {
|
||||
max-height: 1.5in;
|
||||
}
|
||||
|
||||
/*
|
||||
* Body area: 6in total − 1.5in header − 0.5in footer = 4.0in for name/title/affiliations.
|
||||
*
|
||||
* margin-top: 0 overrides the component-level mt-54 (≈2.25in). That margin was added
|
||||
* for the PVC badge layout (badge_3.5x5.5_pvc) where a full-bleed background_image_path
|
||||
* was used — the body needed to start below the background image's logo zone. For
|
||||
* fanfold badges with a standalone header_path image and no background, mt-54 creates
|
||||
* a large blank gap between the header and the attendee name.
|
||||
*/
|
||||
[data-layout='badge_4x6_fanfold'] .badge_body {
|
||||
margin-top: 0;
|
||||
max-height: 4.0in;
|
||||
}
|
||||
|
||||
/* Outer wrapper: strip the default padding/gap so the outline hugs the badge.
|
||||
badge_front above supplies the exact 4×6in card size. */
|
||||
[data-layout='badge_4x6_fanfold'].event_badge_wrapper {
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
min-height: 0;
|
||||
min-width: 4in;
|
||||
width: 4in;
|
||||
max-width: 4in;
|
||||
}
|
||||
|
||||
@media print {
|
||||
[data-layout='badge_4x6_fanfold'].event_badge_wrapper {
|
||||
width: 4in !important;
|
||||
height: 6in !important;
|
||||
max-width: 4in !important;
|
||||
max-height: 6in !important;
|
||||
/* overflow: visible so any intentional bleed element is not clipped by the
|
||||
wrapper — the Epson driver clips at the physical label edge. */
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
@@ -809,6 +809,9 @@ export interface Session {
|
||||
// A key value list of the presentations
|
||||
event_presentation_kv?: null | key_val;
|
||||
event_presentation_li?: null | [any];
|
||||
// Concatenated search strings from JOINed views (v_event_session_w_file_count)
|
||||
event_presentation_li_qry_str?: null | string;
|
||||
event_presenter_li_qry_str?: null | string;
|
||||
// A key value list of the files
|
||||
event_file_kv?: null | key_val;
|
||||
event_file_li?: null | [any];
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -145,13 +146,18 @@ async function _refresh_journal_id_background({
|
||||
obj_li: [result],
|
||||
log_lvl
|
||||
});
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal',
|
||||
obj_li: processed,
|
||||
properties_to_save,
|
||||
log_lvl
|
||||
});
|
||||
// IDB write is optional caching — quota failures must not discard the API result.
|
||||
try {
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal',
|
||||
obj_li: processed,
|
||||
properties_to_save,
|
||||
log_lvl
|
||||
});
|
||||
} catch (save_error) {
|
||||
console.warn('IDB cache write failed for journal (quota?):', save_error);
|
||||
}
|
||||
// Yield to microtask queue so Dexie liveQuery observers fire before we return
|
||||
await Promise.resolve();
|
||||
}
|
||||
@@ -333,13 +339,18 @@ async function _refresh_journal_li_background({
|
||||
obj_li: results,
|
||||
log_lvl
|
||||
});
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal',
|
||||
obj_li: processed,
|
||||
properties_to_save,
|
||||
log_lvl
|
||||
});
|
||||
// IDB write is optional caching — quota failures must not discard the API result.
|
||||
try {
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal',
|
||||
obj_li: processed,
|
||||
properties_to_save,
|
||||
log_lvl
|
||||
});
|
||||
} catch (save_error) {
|
||||
console.warn('IDB cache write failed for journal (quota?):', save_error);
|
||||
}
|
||||
// Yield to microtask queue so Dexie liveQuery observers fire before we return
|
||||
await Promise.resolve();
|
||||
}
|
||||
@@ -418,14 +429,18 @@ export async function create_ae_obj__journal({
|
||||
if (log_lvl) {
|
||||
console.log('Processed object list:', processed_obj_li);
|
||||
}
|
||||
// Save the updated results list to the database
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal',
|
||||
obj_li: processed_obj_li,
|
||||
properties_to_save: properties_to_save,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
// IDB write is optional caching — quota failures must not discard the API result.
|
||||
try {
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal',
|
||||
obj_li: processed_obj_li,
|
||||
properties_to_save: properties_to_save,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
} catch (save_error) {
|
||||
console.warn('IDB cache write failed for journal (quota?):', save_error);
|
||||
}
|
||||
}
|
||||
return journal_obj_create_result;
|
||||
} else {
|
||||
@@ -531,14 +546,18 @@ export async function update_ae_obj__journal({
|
||||
if (log_lvl) {
|
||||
console.log('Processed object list:', processed_obj_li);
|
||||
}
|
||||
// Save the updated results list to the database
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal',
|
||||
obj_li: processed_obj_li,
|
||||
properties_to_save: properties_to_save,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
// IDB write is optional caching — quota failures must not discard the API result.
|
||||
try {
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal',
|
||||
obj_li: processed_obj_li,
|
||||
properties_to_save: properties_to_save,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
} catch (save_error) {
|
||||
console.warn('IDB cache write failed for journal (quota?):', save_error);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
@@ -708,7 +727,7 @@ const properties_to_save = [
|
||||
'outline',
|
||||
|
||||
'description',
|
||||
'description_md_html', // Use the markdown parser to generate HTML
|
||||
// description_md_html is computed from description on every load — not stored to save IDB quota
|
||||
'description_html',
|
||||
'description_json',
|
||||
|
||||
@@ -867,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';
|
||||
|
||||
@@ -46,14 +47,19 @@ export async function load_ae_obj_id__journal_entry({
|
||||
if (log_lvl) {
|
||||
console.log('Processed object list:', processed_obj_li);
|
||||
}
|
||||
// Save the updated results list to the database
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal_entry',
|
||||
obj_li: processed_obj_li,
|
||||
properties_to_save: properties_to_save,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
// Save the updated results list to the database.
|
||||
// IDB write is optional caching — quota failures must not discard the API result.
|
||||
try {
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal_entry',
|
||||
obj_li: processed_obj_li,
|
||||
properties_to_save: properties_to_save,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
} catch (save_error) {
|
||||
console.warn('IDB cache write failed for journal_entry (quota?):', save_error);
|
||||
}
|
||||
}
|
||||
return journal_entry_obj_get_result;
|
||||
} else {
|
||||
@@ -153,19 +159,24 @@ export async function load_ae_obj_li__journal_entry({
|
||||
if (log_lvl) {
|
||||
console.log('Processed object list:', processed_obj_li);
|
||||
}
|
||||
// Save the updated results list to the database
|
||||
// Save the updated results list to the database.
|
||||
// IDB write is optional caching — quota failures must not discard the API result.
|
||||
if (log_lvl) {
|
||||
console.log('Saving to DB...');
|
||||
}
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal_entry',
|
||||
obj_li: processed_obj_li,
|
||||
properties_to_save: properties_to_save,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
if (log_lvl) {
|
||||
console.log('DB save completed.');
|
||||
try {
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal_entry',
|
||||
obj_li: processed_obj_li,
|
||||
properties_to_save: properties_to_save,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
if (log_lvl) {
|
||||
console.log('DB save completed.');
|
||||
}
|
||||
} catch (save_error) {
|
||||
console.warn('IDB cache write failed for journal_entry (quota?):', save_error);
|
||||
}
|
||||
}
|
||||
return journal_entry_obj_li_get_result;
|
||||
@@ -237,14 +248,18 @@ export async function create_ae_obj__journal_entry({
|
||||
if (log_lvl) {
|
||||
console.log('Processed object list:', processed_obj_li);
|
||||
}
|
||||
// Save the updated results list to the database
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal_entry',
|
||||
obj_li: processed_obj_li,
|
||||
properties_to_save: properties_to_save,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
// IDB write is optional caching — quota failures must not discard the API result.
|
||||
try {
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal_entry',
|
||||
obj_li: processed_obj_li,
|
||||
properties_to_save: properties_to_save,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
} catch (save_error) {
|
||||
console.warn('IDB cache write failed for journal_entry (quota?):', save_error);
|
||||
}
|
||||
}
|
||||
return journal_entry_obj_create_result;
|
||||
} else {
|
||||
@@ -457,13 +472,18 @@ export async function qry__journal_entry({
|
||||
journal_id,
|
||||
log_lvl
|
||||
});
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal_entry',
|
||||
obj_li: processed_obj_li,
|
||||
properties_to_save,
|
||||
log_lvl
|
||||
});
|
||||
// IDB write is optional caching — quota failures must not discard the API result.
|
||||
try {
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal_entry',
|
||||
obj_li: processed_obj_li,
|
||||
properties_to_save,
|
||||
log_lvl
|
||||
});
|
||||
} catch (save_error) {
|
||||
console.warn('IDB cache write failed for journal_entry (quota?):', save_error);
|
||||
}
|
||||
}
|
||||
return valid_result_li;
|
||||
} else {
|
||||
@@ -588,14 +608,18 @@ export async function update_ae_obj__journal_entry({
|
||||
if (log_lvl) {
|
||||
console.log('Processed object list:', processed_obj_li);
|
||||
}
|
||||
// Save the updated results list to the database
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal_entry',
|
||||
obj_li: processed_obj_li,
|
||||
properties_to_save: properties_to_save,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
// IDB write is optional caching — quota failures must not discard the API result.
|
||||
try {
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_journals,
|
||||
table_name: 'journal_entry',
|
||||
obj_li: processed_obj_li,
|
||||
properties_to_save: properties_to_save,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
} catch (save_error) {
|
||||
console.warn('IDB cache write failed for journal_entry (quota?):', save_error);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
@@ -848,13 +872,13 @@ const properties_to_save = [
|
||||
// 'description',
|
||||
|
||||
'content',
|
||||
'content_md_html',
|
||||
// content_md_html is computed from content on every load — not stored to save IDB quota
|
||||
'content_html',
|
||||
'content_json',
|
||||
'content_encrypted',
|
||||
|
||||
'history',
|
||||
'history_md_html',
|
||||
// history_md_html is computed from history on every load — not stored to save IDB quota
|
||||
'history_encrypted',
|
||||
|
||||
'passcode_hash',
|
||||
@@ -1027,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,7 +1,10 @@
|
||||
import Dexie, { type Table } from 'dexie';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
import type { ae_Journal, ae_JournalEntry } from '$lib/types/ae_types';
|
||||
import { IDB_CONTENT_VERSIONS } from '$lib/stores/store_versions';
|
||||
import { check_and_clear_idb_tables } from '$lib/ae_core/core__idb_dexie';
|
||||
|
||||
// li = list
|
||||
// kv = key value list
|
||||
@@ -141,3 +144,14 @@ export class MySubClassedDexie extends Dexie {
|
||||
}
|
||||
|
||||
export const db_journals = new MySubClassedDexie();
|
||||
|
||||
// On each page load, clear any tables whose content version has changed.
|
||||
// Versions are defined in store_versions.ts IDB_CONTENT_VERSIONS.journals.
|
||||
// Each table is only cleared once per version bump (tracked in localStorage).
|
||||
if (browser) {
|
||||
check_and_clear_idb_tables({
|
||||
db_instance: db_journals,
|
||||
module_name: 'journals',
|
||||
table_versions: IDB_CONTENT_VERSIONS.journals
|
||||
}).catch((e) => console.warn('[db_journals] IDB version check failed:', e));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,37 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// Format pairs: [24h base, 12h variant]. Only formats with both variants are listed.
|
||||
// Formats without a counterpart (ISO, date-only, week, etc.) are intentionally omitted —
|
||||
// iso_datetime_formatter passes those through unchanged regardless of use_12h.
|
||||
const FORMAT_PAIRS: [string, string][] = [
|
||||
['datetime_iso_no_seconds', 'datetime_iso_12_no_seconds'],
|
||||
['datetime_short', 'datetime_12_short'],
|
||||
['datetime_medium', 'datetime_12_medium'],
|
||||
['datetime_long', 'datetime_12_long'],
|
||||
['datetime_medium_sec', 'datetime_12_medium_sec'],
|
||||
['time_long', 'time_12_long'],
|
||||
['time_short', 'time_12_short'],
|
||||
['time_short_no_leading', 'time_12_short_no_leading'],
|
||||
];
|
||||
|
||||
// Build lookup maps from the pairs above. Both directions are derived from the same source.
|
||||
const TO_12H: Record<string, string> = Object.fromEntries(
|
||||
FORMAT_PAIRS.map(([h24, h12]) => [h24, h12])
|
||||
);
|
||||
const TO_24H: Record<string, string> = Object.fromEntries(
|
||||
FORMAT_PAIRS.map(([h24, h12]) => [h12, h24])
|
||||
);
|
||||
|
||||
export const iso_datetime_formatter = function iso_datetime_formatter(
|
||||
raw_datetime: null | string | Date = null,
|
||||
named_format: string = 'datetime_iso_no_seconds', // date_iso, datetime_iso_no_seconds
|
||||
time_24_hours: boolean = false
|
||||
// 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,
|
||||
// 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() ***');
|
||||
|
||||
@@ -50,6 +78,18 @@ 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)
|
||||
: (TO_24H[named_format] ?? named_format);
|
||||
}
|
||||
|
||||
let datetime_string = null;
|
||||
|
||||
switch (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.
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
} 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";
|
||||
|
||||
|
||||
@@ -217,26 +217,26 @@ function handle_clear_storage(item: null | string) {
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm preset-tonal-warning"
|
||||
title="Clear the browser storage for this page"
|
||||
onclick={() => {
|
||||
title="Full Reset: Delete ALL IndexedDB databases, clear localStorage and sessionStorage for this origin, then reload."
|
||||
onclick={async () => {
|
||||
if (
|
||||
!confirm(
|
||||
'Are you sure you want to clear the local and session storage?'
|
||||
'FULL RESET: Delete ALL IndexedDB databases, clear localStorage and sessionStorage, then reload? This cannot be undone.'
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
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);
|
||||
}
|
||||
|
||||
// Clear the local and session storage
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
||||
// Clear Indexed DB as well
|
||||
indexedDB.deleteDatabase('ae_core_db');
|
||||
indexedDB.deleteDatabase('ae_events_db');
|
||||
|
||||
window.location.reload();
|
||||
// alert('Local and Session Storage cleared and Indexed DBs deleted. You will probably want to refresh the page.');
|
||||
}}>
|
||||
<Eraser size="1em" class="mx-1" />
|
||||
Clear Storage & DB
|
||||
|
||||
@@ -35,9 +35,6 @@ let {
|
||||
hide_icon = false
|
||||
}: Props = $props();
|
||||
|
||||
// *** Import Svelte specific
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
// *** Import other supporting libraries
|
||||
import {
|
||||
BadgeQuestionMark,
|
||||
@@ -45,17 +42,15 @@ import {
|
||||
ChevronRight,
|
||||
LifeBuoy,
|
||||
RefreshCw,
|
||||
SquareX
|
||||
SquareX,
|
||||
Trash2
|
||||
} from '@lucide/svelte';
|
||||
// *** Import Aether specific variables and functions
|
||||
import {
|
||||
ae_snip,
|
||||
ae_loc,
|
||||
ae_sess,
|
||||
ae_api,
|
||||
ae_trig,
|
||||
slct,
|
||||
slct_trigger,
|
||||
type key_val
|
||||
} from '$lib/stores/ae_stores';
|
||||
import { api } from '$lib/api/api';
|
||||
@@ -144,7 +139,7 @@ class:to-90%={$ae_sess.show_help_tech} -->
|
||||
{!$ae_sess.show_help_tech ? e_class_form_hidden : e_class_form_showing}
|
||||
relative
|
||||
"
|
||||
class:w-xl={$ae_sess.show_help_tech}
|
||||
class:w-lg={$ae_sess.show_help_tech}
|
||||
class:w-fit={!$ae_sess.show_help_tech}
|
||||
class:mx-auto={$ae_sess.show_help_tech}
|
||||
class:m-2={$ae_sess.show_help_tech}
|
||||
@@ -159,7 +154,7 @@ class:to-90%={$ae_sess.show_help_tech} -->
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
w-xl flex-col items-center
|
||||
w-sm sm:w-xl flex-col items-center
|
||||
justify-center gap-1
|
||||
rounded-xl
|
||||
border border-gray-500/20 bg-blue-200
|
||||
@@ -237,7 +232,8 @@ class:to-90%={$ae_sess.show_help_tech} -->
|
||||
<textarea
|
||||
class="
|
||||
form-control
|
||||
h-24 w-full max-w-lg rounded
|
||||
h-24
|
||||
w-full max-w-xs sm:max-w-xl rounded
|
||||
border border-gray-300 bg-white
|
||||
p-2 text-gray-950
|
||||
dark:bg-gray-500 dark:text-gray-50
|
||||
@@ -460,68 +456,30 @@ class:to-90%={$ae_sess.show_help_tech} -->
|
||||
">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
if ($ae_loc.edit_mode) {
|
||||
// Confirm before clearing
|
||||
if (
|
||||
!confirm(
|
||||
'Are you sure you want to clear IndexedDB databases, localStorage, and sessionStorage? This will also reload the page.'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
onclick={async () => {
|
||||
const edit_mode = $ae_loc.edit_mode;
|
||||
const confirm_msg = edit_mode
|
||||
? 'Clear all IDB caches, localStorage, and sessionStorage? Your sign-in will be preserved. This will reload the page.'
|
||||
: 'Clear all IDB caches? This will reload the page.';
|
||||
|
||||
console.log(
|
||||
'Clearing IndexedDB, localStorage, sessionStorage, and reloading the page...'
|
||||
);
|
||||
if (!confirm(confirm_msg)) return;
|
||||
|
||||
// Clear Indexed DB
|
||||
indexedDB.deleteDatabase('ae_archives_db'); // Archives module
|
||||
indexedDB.deleteDatabase('ae_core_db');
|
||||
indexedDB.deleteDatabase('ae_events_db'); // Events module
|
||||
indexedDB.deleteDatabase('ae_journals_db'); // Journals module
|
||||
indexedDB.deleteDatabase('ae_posts_db'); // Posts module
|
||||
indexedDB.deleteDatabase('ae_sponsorships_db'); // Sponsorships module
|
||||
|
||||
// Clear localStorage and sessionStorage
|
||||
// Clearing the localStorage will force it to be re-created.
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
||||
goto('/', { invalidateAll: true });
|
||||
|
||||
// window.location.reload();
|
||||
} else {
|
||||
// Confirm before clearing
|
||||
if (
|
||||
!confirm(
|
||||
'Are you sure you want to clear IndexedDB databases and some caches? This will also reload the page.'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
'Clearing IndexedDB, localStorage, sessionStorage, and reloading the page...'
|
||||
);
|
||||
|
||||
// Clear Indexed DB
|
||||
indexedDB.deleteDatabase('ae_archives_db'); // Archives module
|
||||
indexedDB.deleteDatabase('ae_core_db');
|
||||
indexedDB.deleteDatabase('ae_events_db'); // Events module
|
||||
indexedDB.deleteDatabase('ae_journals_db'); // Journals module
|
||||
indexedDB.deleteDatabase('ae_posts_db'); // Posts module
|
||||
indexedDB.deleteDatabase('ae_sponsorships_db'); // Sponsorships module
|
||||
|
||||
window.location.reload();
|
||||
// Enumerate and delete every IDB database on this origin.
|
||||
const db_list = await indexedDB.databases();
|
||||
console.log('[clear_reload] IDB databases found:', db_list.map((d) => d.name));
|
||||
for (const db of db_list) {
|
||||
if (db.name) indexedDB.deleteDatabase(db.name);
|
||||
}
|
||||
|
||||
// This does not seem to work fast enough or something?
|
||||
// goto('/', {invalidateAll: true});
|
||||
if (edit_mode) {
|
||||
// Preserve ae_loc (sign-in credentials + permissions) across the wipe.
|
||||
const ae_loc_saved = localStorage.getItem('ae_loc');
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
if (ae_loc_saved) localStorage.setItem('ae_loc', ae_loc_saved);
|
||||
}
|
||||
|
||||
// The page does usually seem to reload correctly?
|
||||
// window.location.reload(true); // true only works with Firefox
|
||||
// alert('Local and Session Storage cleared and Indexed DBs deleted. You will probably want to refresh the page.');
|
||||
window.location.reload();
|
||||
}}
|
||||
class="
|
||||
btn btn-sm
|
||||
@@ -531,13 +489,49 @@ class:to-90%={$ae_sess.show_help_tech} -->
|
||||
transition-all
|
||||
{btn_class}
|
||||
"
|
||||
title="Clear App Data & Settings: Clear IndexedDB and reload. If in edit mode localStorage and sessionStorage will also be cleared.">
|
||||
<!-- <span class="fas fa-eraser mx-1"></span> -->
|
||||
<!-- <span class="fas fa-sync mx-1"></span> -->
|
||||
title="Clear & Reload: Delete all IDB caches and reload. In edit mode also clears localStorage/sessionStorage, preserving your sign-in.">
|
||||
<RefreshCw size="1em" class="inline-block" />
|
||||
<span class="md:inline">Clear & Reload</span>
|
||||
</button>
|
||||
|
||||
<!-- Nuclear clear: enumerates all IDB databases dynamically + always clears localStorage/sessionStorage.
|
||||
Useful when the hardcoded list above is stale or when diagnosing quota/storage bugs. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={async () => {
|
||||
if (
|
||||
!confirm(
|
||||
'FULL RESET: Delete ALL IndexedDB databases, clear localStorage and sessionStorage, then reload? This cannot be undone.'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enumerate every IDB database on this origin and delete them all.
|
||||
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();
|
||||
}}
|
||||
class="
|
||||
btn btn-sm
|
||||
preset-tonal-surface
|
||||
preset-outlined-error-100-900
|
||||
hover:preset-filled-error-200-800
|
||||
transition-all
|
||||
{btn_class}
|
||||
"
|
||||
title="Full Reset: Enumerate and delete ALL IndexedDB databases for this origin, clear localStorage and sessionStorage, then reload.">
|
||||
<Trash2 size="1em" class="inline-block" />
|
||||
<span class="md:inline">Full Reset</span>
|
||||
</button>
|
||||
|
||||
<!-- Cancel button -->
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -80,22 +80,21 @@ function toggle_theme_mode() {
|
||||
// DOM sync (class) is handled reactively in +layout.svelte effect #3
|
||||
}
|
||||
|
||||
// ── Dev: clear all browser storage + key IndexedDB tables, then reload ──
|
||||
function handle_clear_storage_db() {
|
||||
// ── Dev: clear all browser storage + all IndexedDB databases, then reload ──
|
||||
async function handle_clear_storage_db() {
|
||||
if (
|
||||
!confirm(
|
||||
'Clear all local/session storage and IndexedDB? The page will reload.'
|
||||
'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();
|
||||
indexedDB.deleteDatabase('ae_archives_db');
|
||||
indexedDB.deleteDatabase('ae_core_db');
|
||||
indexedDB.deleteDatabase('ae_events_db');
|
||||
indexedDB.deleteDatabase('ae_journals_db');
|
||||
indexedDB.deleteDatabase('ae_posts_db');
|
||||
indexedDB.deleteDatabase('ae_sponsorships_db');
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -64,11 +64,68 @@ export async function launch_from_cache({
|
||||
hash,
|
||||
temp_root,
|
||||
filename,
|
||||
hash_prefix_length = 2
|
||||
}: any) {
|
||||
hash_prefix_length = 2,
|
||||
native_template = null
|
||||
}: {
|
||||
cache_root: string;
|
||||
hash: string;
|
||||
temp_root: string;
|
||||
filename: string;
|
||||
hash_prefix_length?: number;
|
||||
/**
|
||||
* Resolved native launch template. If provided, Electron executes this string
|
||||
* after the file is copied to temp.
|
||||
*
|
||||
* Two formats:
|
||||
* - AppleScript: multi-line string with {{path}} placeholder (macOS only)
|
||||
* - Shell command: prefix with "shell:" → e.g. "shell:open \"{{path}}\""
|
||||
*
|
||||
* Configure via per-profile launch_profiles overrides in event_device.data_json or $events_loc.launcher.
|
||||
* If null, Electron should treat that as a missing profile error.
|
||||
*/
|
||||
native_template?: string | null;
|
||||
}) {
|
||||
if (!native)
|
||||
return { success: false, error: 'Native bridge not available' };
|
||||
return await native.launch_from_cache({
|
||||
cache_root,
|
||||
hash,
|
||||
temp_root,
|
||||
filename,
|
||||
hash_prefix_length,
|
||||
native_template
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Thin cache primitive — copies a cached file to the temp directory and returns
|
||||
* the resolved path. The caller decides what happens next.
|
||||
*
|
||||
* Preferred building block for composable launch flows on the Svelte side:
|
||||
* 1. copy_from_cache_to_temp(...) → { path }
|
||||
* 2. run_osascript(template.replace('{{path}}', path))
|
||||
* OR run_cmd(`open "${path}"`)
|
||||
* OR whatever you need
|
||||
*
|
||||
* Use launch_from_cache when the built-in hardcoded logic is sufficient.
|
||||
* Use this when you want full control over what happens after the file lands in temp.
|
||||
*/
|
||||
export async function copy_from_cache_to_temp({
|
||||
cache_root,
|
||||
hash,
|
||||
temp_root,
|
||||
filename,
|
||||
hash_prefix_length = 2
|
||||
}: {
|
||||
cache_root: string;
|
||||
hash: string;
|
||||
temp_root: string;
|
||||
filename: string;
|
||||
hash_prefix_length?: number;
|
||||
}): Promise<{ success: boolean; path?: string; error?: string }> {
|
||||
if (!native)
|
||||
return { success: false, error: 'Native bridge not available' };
|
||||
return await native.copy_from_cache_to_temp({
|
||||
cache_root,
|
||||
hash,
|
||||
temp_root,
|
||||
@@ -129,6 +186,18 @@ export async function cleanup_tmp_files({
|
||||
return await native.run_cmd({ cmd, timeout: 30000, return_stdout: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an AppleScript string. macOS only.
|
||||
*
|
||||
* HARDENED (2026-05-11): The Electron handler now writes the script to a temp .scpt
|
||||
* file and runs `osascript "/path/to/file.scpt"` rather than passing it inline via
|
||||
* the -e flag. This means:
|
||||
* - Multi-line scripts work correctly
|
||||
* - Paths with spaces or special characters work correctly
|
||||
* - No shell escaping required in the script string you pass here
|
||||
*
|
||||
* The .scpt file is deleted immediately after execution.
|
||||
*/
|
||||
export async function run_osascript(script: string) {
|
||||
if (!native)
|
||||
return { success: false, error: 'Native bridge not available' };
|
||||
@@ -315,13 +384,65 @@ export async function control_presentation({
|
||||
|
||||
// 4. System Management (Phase 5+)
|
||||
|
||||
export async function set_wallpaper({ path }: { path: string }) {
|
||||
export async function set_wallpaper({
|
||||
path,
|
||||
url,
|
||||
url_external,
|
||||
display = 'all',
|
||||
api_key,
|
||||
account_id
|
||||
}: {
|
||||
/** Local file path (existing behavior). */
|
||||
path?: string;
|
||||
/** HTTPS URL — downloaded to ~/Library/Caches/OSIT/wallpaper/ before applying. */
|
||||
url?: string;
|
||||
/** Optional separate URL for the external/projector display only. */
|
||||
url_external?: string;
|
||||
/** Which display(s) to target. Defaults to 'all'. */
|
||||
display?: 'all' | 'primary' | 'external';
|
||||
/** Aether API key passed as x-aether-api-key header on the download request. */
|
||||
api_key?: string;
|
||||
/** Aether account ID passed as x-account-id header on the download request. */
|
||||
account_id?: string;
|
||||
}) {
|
||||
if (!native || !native.set_wallpaper)
|
||||
return {
|
||||
success: false,
|
||||
error: 'Native handler set_wallpaper not available'
|
||||
};
|
||||
return await native.set_wallpaper({ path });
|
||||
return await native.set_wallpaper({ path, url, url_external, display, api_key, account_id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the macOS default wallpaper on all displays.
|
||||
* Scans /System/Library/Desktop Pictures/ for the first .heic file — works across
|
||||
* all recent macOS versions without needing to know the version name.
|
||||
* No-op on non-macOS (Linux/Windows return success:false from run_osascript).
|
||||
*/
|
||||
export async function restore_macos_default_wallpaper(
|
||||
display: 'all' | 'primary' | 'external' = 'all'
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const display_target =
|
||||
display === 'primary'
|
||||
? 'tell desktop 1'
|
||||
: display === 'external'
|
||||
? 'tell desktop 2'
|
||||
: 'tell every desktop';
|
||||
|
||||
const script = `
|
||||
set pic_path to do shell script "ls '/System/Library/Desktop Pictures/'*.heic 2>/dev/null | head -1"
|
||||
if pic_path is "" then
|
||||
error "No default macOS wallpaper (.heic) found in /System/Library/Desktop Pictures/"
|
||||
end if
|
||||
tell application "System Events"
|
||||
${display_target}
|
||||
set picture to pic_path
|
||||
end tell
|
||||
end tell
|
||||
`.trim();
|
||||
|
||||
const result = await run_osascript(script);
|
||||
return result ?? { success: false, error: 'Native bridge not available' };
|
||||
}
|
||||
|
||||
export async function update_app(args: {
|
||||
@@ -382,6 +503,30 @@ export async function set_display_layout({
|
||||
return await native.set_display_layout({ mode, configStr });
|
||||
}
|
||||
|
||||
export async function list_display_modes() {
|
||||
if (!native || !native.list_display_modes)
|
||||
return { success: false, error: 'Native handler list_display_modes not available' };
|
||||
return await native.list_display_modes();
|
||||
}
|
||||
|
||||
export async function set_display_mode({
|
||||
display_index,
|
||||
width,
|
||||
height,
|
||||
refresh_rate,
|
||||
hidpi
|
||||
}: {
|
||||
display_index: number;
|
||||
width: number;
|
||||
height: number;
|
||||
refresh_rate?: number;
|
||||
hidpi?: boolean | null;
|
||||
}) {
|
||||
if (!native || !native.set_display_mode)
|
||||
return { success: false, error: 'Native handler set_display_mode not available' };
|
||||
return await native.set_display_mode({ display_index, width, height, refresh_rate, hidpi });
|
||||
}
|
||||
|
||||
export async function power_control({
|
||||
action
|
||||
}: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user