docs: archive 4 completed project docs, update stale references
Archived to documentation/history/: - PROJECT__AE_Firefly_Theme_Repair_SUMMARY.md (complete) - PROJECT__AE_Pres_Mgmt_Session_view_refactor_2026-02.md (resolved 2026-02-26) - PROJECT__AE_Access_Control_UX.md (all steps done 2026-03-11) - PROJECT__AE_combined_front_back_Docker.md (complete 2026-03-10) Pre-archive housekeeping: - CLAUDE.md: removed 3 resolved active issues; replaced stale session bug doc link with Badges Task 4.0 doc - AE__Architecture.md: corrected stale "TipTap marked for removal" note — both editors are active - PROJECT__AE_Firefly_Theme_Repair_SUMMARY.md: added archival note re element_modal_v1 retirement Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
289
documentation/history/PROJECT__AE_Access_Control_UX.md
Normal file
289
documentation/history/PROJECT__AE_Access_Control_UX.md
Normal file
@@ -0,0 +1,289 @@
|
||||
# PROJECT: Access Control UX — Session Expired & Access Denied
|
||||
|
||||
**Status:** Complete
|
||||
**Priority:** Medium-High
|
||||
**Created:** 2026-02
|
||||
**Updated:** 2026-03-11
|
||||
**Related:** `src/routes/+layout.svelte`, `src/lib/ae_api/`, `src/lib/stores/ae_stores.ts`
|
||||
|
||||
---
|
||||
|
||||
## 1. Objective
|
||||
|
||||
Clean up inconsistent access-denied and session-expired UX across the app:
|
||||
|
||||
1. **Session Expired banner** — When the API returns 401/403, show a non-blocking dismissible banner in the root layout rather than silently failing. The `flag_expired` placeholder in the root layout is already wired for this but nothing sets it.
|
||||
2. **Standardize Access Denied display** — Replace the one-off browser `alert()` in event settings with a proper in-page gate. Create a small reusable component for inline denial cards.
|
||||
3. **Maintain intentional special cases** — IDAA Novi UUID gate and the Root Site Access Key gate are correct and must not be touched.
|
||||
|
||||
---
|
||||
|
||||
## 2. Current State Inventory
|
||||
|
||||
### Pattern A — Root Layout: Site Access Key Gate ✅ Working, keep as-is
|
||||
|
||||
**File:** `src/routes/+layout.svelte` lines 130–135, 299–305
|
||||
**Type:** Full-screen blocker
|
||||
**Logic:** If `site_access_key` is set and `allow_access` key doesn't match + user is not `trusted_access`, `flag_denied = true`.
|
||||
**Current UI:** Minimal full-screen `<h1>Access Denied</h1>` + Reload button.
|
||||
**Verdict:** Works correctly. The minimal styling is intentional (it's a hard site gate). Keep but no code change needed unless you want to polish the styling later.
|
||||
|
||||
---
|
||||
|
||||
### Pattern B — Root Layout: Session Expired Banner 🔴 Declared, never set
|
||||
|
||||
**File:** `src/routes/+layout.svelte` line 63
|
||||
**Variable:** `let flag_expired: boolean = $state(false)` — **never set anywhere.**
|
||||
**Intent:** This should show a non-blocking dismissible banner whenever the API returns 401/403, signaling a stale JWT/session.
|
||||
**Gap:** API helpers detect 401/403 and log diagnostic info but never fire any store event.
|
||||
**Fix:** See Implementation Plan step 1.
|
||||
|
||||
---
|
||||
|
||||
### Pattern C — Core Layout: Manager Access Gate ✅ Already correct
|
||||
|
||||
**File:** `src/routes/core/+layout.svelte` lines 13–20, 62–75
|
||||
**Logic:**
|
||||
- `onMount`: 500ms delay then `goto('/')` if still not `manager_access` (handles slow hydration)
|
||||
- `{:else}` block: Shows a styled "Access Restricted" card with Lock icon, description, "Return Home" button while waiting/denied
|
||||
|
||||
**Verdict:** This pattern is correct and consistent. The `{:else}` visual gate prevents flashing. The delayed redirect is a graceful fallback. **No changes needed.**
|
||||
|
||||
---
|
||||
|
||||
### Pattern D — IDAA Layout: Novi UUID Gate ✅ Intentionally custom, keep as-is
|
||||
|
||||
**File:** `src/routes/idaa/(idaa)/+layout.svelte`
|
||||
**Logic:** Async POST to `https://www.idaa.org/api` to verify Novi UUID. `novi_verifying` flag prevents Access Denied flash during network round-trip.
|
||||
**Verdict:** Intentionally custom to IDAA's member verification flow. **Do not standardize or touch this.**
|
||||
|
||||
---
|
||||
|
||||
### Pattern E — Event Settings: browser `alert()` 🟡 Needs fix
|
||||
|
||||
**File:** `src/routes/events/[event_id]/settings/+page.svelte` lines 44–47
|
||||
**Current code:**
|
||||
```ts
|
||||
if (!$ae_loc.administrator_access) {
|
||||
if (browser) {
|
||||
alert('Access Denied: Administrative privileges and Edit Mode required.');
|
||||
goto(`/events/${event_id}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
**Problems:**
|
||||
1. `alert()` is a blocking browser dialog — ugly, inconsistent with app UX
|
||||
2. Runs in module-level `if` block (not `onMount`) — can fire before hydration is fully complete
|
||||
3. No visual component shown; just redirects
|
||||
|
||||
**Fix:** Remove `alert()`, move check into `onMount` with a small delay (like `/core` pattern), or add an inline gate using a reusable component.
|
||||
|
||||
---
|
||||
|
||||
### Pattern F — Badge Review: Inline "Access Denied" Card 🟡 Acceptable, minor polish
|
||||
|
||||
**File:** `src/routes/events/[event_id]/(badges)/badges/[badge_id]/review/+page.svelte` lines 315–330
|
||||
**Context:** Passcode check failure — attendee entered wrong passcode
|
||||
**Current UI:**
|
||||
```html
|
||||
<div class="card p-6 space-y-4 max-w-sm">
|
||||
<div class="flex items-center gap-2 text-error-500">
|
||||
<h3 class="text-lg font-semibold">Access Denied</h3>
|
||||
</div>
|
||||
<p class="text-sm text-gray-700">{passcode_error}</p>
|
||||
<button ... >Try Again</button>
|
||||
</div>
|
||||
```
|
||||
**Verdict:** Contextually appropriate and functional. The "Try Again" button is good UX. This is a prime candidate to be replaced with a reusable component once one exists, but it is not broken.
|
||||
|
||||
---
|
||||
|
||||
### Pattern G — API Helpers: 401/403 Detection Without UI Feedback 🔴 Gap
|
||||
|
||||
**Files:** `src/lib/ae_api/api_get_object.ts`, `api_post_object.ts`, `api_patch_object.ts` (all ~line 237)
|
||||
**Current behavior:** Logs auth diagnostics to console, returns `false` or `null`. No store event fired.
|
||||
**Gap:** When a JWT expires mid-session, the user sees requests silently fail (data doesn't load/save) with no explanation. They may think the app broke.
|
||||
**Fix:** On 401/403, set `ae_auth_error` store → root layout watches it and sets `flag_expired = true`.
|
||||
|
||||
---
|
||||
|
||||
### Pattern H — Presenter Auth (`auth__person`) — Existing system, no UX issues to fix now
|
||||
|
||||
**Store:** `$events_loc.auth__person` — stores authenticated presenter identity
|
||||
**URL params:** `?person_id=...&person_pass=...&presentation_id=...&presenter_id=...`
|
||||
**Anonymous toggle:** Per-event config allows presenters to upload files without signing in
|
||||
**Verdict:** Auth system is working. The gating UI in the presenter pages is contextually managed. Not in scope for this cleanup. Revisit when building out the Leads feature or a future auth refactor.
|
||||
|
||||
---
|
||||
|
||||
## 3. Issues Ranked by Priority
|
||||
|
||||
| # | Severity | Issue | File | Fix |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 🔴 High | API 401/403 silently fails — users have no feedback | `api_*.ts` | Wire to `ae_auth_error` store |
|
||||
| 2 | 🔴 High | `flag_expired` never set — session expired banner never shows | `+layout.svelte` | Watch `ae_auth_error`, render banner |
|
||||
| 3 | 🟡 Medium | `alert()` in event settings — ugly, blocking, not idiomatic | `settings/+page.svelte` | Replace with `onMount` gate + reusable component |
|
||||
| 4 | 🟢 Low | Badge review inline card — not reusing a component | `badges/.../review/+page.svelte` | Replace when `element_access_denied.svelte` is ready |
|
||||
|
||||
---
|
||||
|
||||
## 4. Design Decisions
|
||||
|
||||
### 4a. Session Expired Banner Design
|
||||
|
||||
- **Non-blocking top bar** — similar to the existing `is_offline` and `api_unreachable` banners in the root layout
|
||||
- **Dismissible** — user clicks X to clear; or auto-hides after signing back in
|
||||
- **Message:** "Your session has expired. Please reload or sign in again." with a Reload button
|
||||
- **Trigger:** Any 401 or 403 from any of the three API helpers
|
||||
|
||||
### 4b. `ae_auth_error` Store
|
||||
|
||||
Simple writable in `ae_stores.ts`:
|
||||
```ts
|
||||
export const ae_auth_error = writable<{ type: 'expired' | 'denied' | null, ts: number | null }>({ type: null, ts: null });
|
||||
```
|
||||
This is intentionally minimal — just enough to signal the root layout.
|
||||
|
||||
### 4c. Reusable `element_access_denied.svelte`
|
||||
|
||||
A small card component for inline access denial within a page:
|
||||
```
|
||||
Props:
|
||||
- title?: string (default: "Access Denied")
|
||||
- message?: string (default: "You do not have permission to view this content.")
|
||||
- show_reload?: boolean
|
||||
- show_return_home?: boolean
|
||||
- action_label?: string (optional extra button)
|
||||
- on_action?: () => void
|
||||
```
|
||||
Location: `src/lib/elements/element_access_denied.svelte`
|
||||
|
||||
### 4d. Event Settings Fix
|
||||
|
||||
The settings page check should mirror the `/core` pattern:
|
||||
- Move to `onMount` with 500ms grace delay
|
||||
- No `alert()` — if not authorized, the redirect fires silently after the delay
|
||||
- Add inline gate (`{:else}` block with "Access Restricted" message) if the user somehow lands here
|
||||
|
||||
### 4e. What We Are NOT Changing
|
||||
|
||||
- Root Layout site access key gate — working correctly
|
||||
- `/core` layout — already correct
|
||||
- IDAA Novi UUID gate — intentionally custom
|
||||
- Presenter auth system (`auth__person`) — not in scope
|
||||
|
||||
---
|
||||
|
||||
## 5. Implementation Plan
|
||||
|
||||
### Step 1: Add `ae_auth_error` store ✅ DONE (2026-03-11)
|
||||
|
||||
**File:** `src/lib/stores/ae_stores.ts`
|
||||
|
||||
Add after the existing store declarations:
|
||||
```ts
|
||||
// Auth error signal — set by API helpers on 401/403 to trigger root layout session-expired banner
|
||||
export const ae_auth_error = writable<{ type: 'expired' | null, ts: number | null }>({ type: null, ts: null });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Wire API helpers to `ae_auth_error` ✅ DONE (2026-03-11)
|
||||
|
||||
**Files:** `src/lib/ae_api/api_get_object.ts`, `api_post_object.ts`, `api_patch_object.ts` (same pattern in all three)
|
||||
|
||||
In the existing `if (response.status === 401 || response.status === 403)` block, add one line after the existing `console.warn(...)`:
|
||||
```ts
|
||||
import { ae_auth_error } from '$lib/stores/ae_stores';
|
||||
// ...
|
||||
ae_auth_error.set({ type: 'expired', ts: Date.now() });
|
||||
```
|
||||
|
||||
**Note:** Only import `ae_auth_error` — no other store changes. Do NOT import `ae_auth_error` at module level if the API helpers are used SSR-side. Use a dynamic import or guard with `browser` check if needed.
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Wire `flag_expired` in root layout ✅ DONE (2026-03-11)
|
||||
|
||||
**File:** `src/routes/+layout.svelte`
|
||||
|
||||
Add an `$effect` that watches `$ae_auth_error` and sets `flag_expired`:
|
||||
```ts
|
||||
$effect(() => {
|
||||
if ($ae_auth_error?.type === 'expired' && $ae_auth_error?.ts) {
|
||||
untrack(() => { flag_expired = true; });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Add the dismissible banner to the template (after/near the existing `is_offline` banner, in the `{#if browser && $ae_loc?.allow_access}` block):
|
||||
```html
|
||||
{#if flag_expired}
|
||||
<div class="fixed top-0 left-0 right-0 z-50 bg-warning-500 text-white px-4 py-2 flex items-center justify-between">
|
||||
<p class="text-sm font-semibold">Your session has expired. Please reload or sign in again.</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-sm preset-filled-surface" onclick={() => window.location.reload()}>Reload</button>
|
||||
<button class="btn btn-sm" onclick={() => { flag_expired = false; ae_auth_error.set({ type: null, ts: null }); }}>Dismiss</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Create `element_access_denied.svelte` ✅ DONE (2026-03-11)
|
||||
|
||||
**File:** `src/lib/elements/element_access_denied.svelte`
|
||||
|
||||
Reusable card for inline access denial. Props per design decision 4c.
|
||||
|
||||
---
|
||||
|
||||
### Step 5: Fix Event Settings `alert()` ✅ DONE (2026-03-11)
|
||||
|
||||
**File:** `src/routes/events/[event_id]/settings/+page.svelte`
|
||||
|
||||
Replace the module-level `if (!$ae_loc.administrator_access)` + `alert()` block with:
|
||||
1. Move check into `onMount` with the same 500ms grace-delay pattern as `/core`
|
||||
2. Add `{:else}` gate in the template using `element_access_denied.svelte`
|
||||
3. Remove the `browser` guard (not needed inside `onMount`)
|
||||
|
||||
---
|
||||
|
||||
### Step 6 (Optional / Low Priority): Swap badge review inline card ✅ DONE (2026-03-11)
|
||||
|
||||
**File:** `src/routes/events/[event_id]/(badges)/badges/[badge_id]/review/+page.svelte`
|
||||
|
||||
Replace inline access denied card with `element_access_denied.svelte` once the component exists. Keep "Try Again" action via `on_action` prop.
|
||||
|
||||
---
|
||||
|
||||
## 6. Files to Modify Summary
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/lib/stores/ae_stores.ts` | Add `ae_auth_error` writable store |
|
||||
| `src/lib/ae_api/api_get_object.ts` | Set `ae_auth_error` on 401/403 |
|
||||
| `src/lib/ae_api/api_post_object.ts` | Set `ae_auth_error` on 401/403 |
|
||||
| `src/lib/ae_api/api_patch_object.ts` | Set `ae_auth_error` on 401/403 |
|
||||
| `src/routes/+layout.svelte` | Watch `ae_auth_error`, render session-expired banner |
|
||||
| `src/routes/events/[event_id]/settings/+page.svelte` | Remove `alert()`, fix auth gate pattern |
|
||||
| `src/lib/elements/element_access_denied.svelte` | **NEW** — reusable inline denial card |
|
||||
| `src/routes/events/[event_id]/(badges)/badges/[badge_id]/review/+page.svelte` | Swap inline card with component (low priority) |
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing Notes
|
||||
|
||||
- **Session expired banner:** Force a 401 by testing with an expired JWT or by calling an API with wrong credentials. Banner should appear. Dismiss should clear it. Reload should reload browser.
|
||||
- **Event settings gate:** Navigate to `/events/{id}/settings` without `administrator_access`. Should redirect without any `alert()` dialog.
|
||||
- **Badge review:** Enter a bad passcode — Access Denied card should appear with "Try Again".
|
||||
- **IDAA, /core, root site key gate:** Verify no regressions.
|
||||
|
||||
---
|
||||
|
||||
## 8. Risks & Notes
|
||||
|
||||
- The API helpers (`api_get_object.ts` etc.) run in a module context that is imported across many components. The `ae_auth_error` store must only be imported/used inside a `browser` guard or dynamically if the helpers are ever called SSR-side. Check `src/lib/ae_core/ae_core__site.ts` (which uses these helpers during SSR hydration) — **do not set the store from SSR context.** Add `if (browser)` before the `ae_auth_error.set(...)` call.
|
||||
- The root layout sync effect runs `untrack()` to prevent circular store updates. The new `$effect` for `ae_auth_error` must also use `untrack()` to be safe.
|
||||
- `flag_expired` should NOT permanently gate the UI — it should only show a top banner. The user may have been mid-editing and should not lose their work. The current `flag_denied` full-screen block is for site key access only.
|
||||
@@ -0,0 +1,45 @@
|
||||
**AE Firefly Theme Repair — Summary (Recovery & Integration Complete)**
|
||||
|
||||
- **Summary**: Investigation, targeted repair, and successful integration of the AE Firefly theme family and key UI components. This document records the final resolution, the migration of the Svelte 5 Modal component, and the validation of the `ae_app_3x_llm` branch as the project's stable baseline.
|
||||
|
||||
### 1. Root Cause Resolution (Themes)
|
||||
- **Variables outside selectors**: Fixed. Custom-property declarations in `src/ae-firefly*.css` were moved into proper `[data-theme='AE_Firefly*']` selector blocks.
|
||||
- **Variable precedence**: Resolved by enforcing `--background: ... !important` within the theme-specific selectors to ensure they override global `:root` defaults.
|
||||
- **Files Repaired**:
|
||||
- `src/ae-firefly.css`
|
||||
- `src/ae-firefly-steelblue.css`
|
||||
- `src/ae-firefly-indigo.css`
|
||||
- `src/ae-firefly-rainbow.css`
|
||||
|
||||
### 2. Actions Taken (Recovery Phase)
|
||||
- **Surgical Integration**: Selective harvesting of "90% done" work from WIP branches while preserving the integrity of the Lucide migration and Svelte 5 baseline.
|
||||
- **`element_modal_v1.svelte`**:
|
||||
- Successfully imported from `wip-modal-fix-attempt`.
|
||||
- **Full Refactor**: Migrated from deprecated `svelte/legacy` and `<slot>` patterns to **Svelte 5 Snippets** and `{@render}` tags.
|
||||
- Verified and tested as the new standard modal component.
|
||||
- **Selective Vetting**:
|
||||
- **Abandoned**: `element_input_v2.svelte` and legacy Badge View v1 (rejected due to legacy FontAwesome regressions and high error counts).
|
||||
- **Removed**: Redundant `element_data_store_v2.svelte` (superseded by `v3`).
|
||||
- **Kept**: Clean, Lucide-based versions of all core components already present on `ae_app_3x_llm`.
|
||||
|
||||
### 3. Repository State (Final Validation)
|
||||
- **Baseline**: `ae_app_3x_llm` is now the unified, verified "known-good" state.
|
||||
- **Validation**: `npx svelte-check` performed on the merged state returned **0 errors and 0 warnings**.
|
||||
- **Cleanup**: Temporary integration branches have been deleted.
|
||||
- **Backups**: `wip-modal-fix-attempt` and `wip/theme-fix` remain as reference points but are no longer active.
|
||||
|
||||
### 4. Merged Files (Key Updates)
|
||||
- `src/ae-firefly*.css` (Repaired themes)
|
||||
- `src/lib/elements/element_modal_v1.svelte` (New Svelte 5 Modal)
|
||||
- `documentation/PROJECT__AE_Firefly_Theme_Repair_SUMMARY.md` (This document)
|
||||
|
||||
### 5. Final Status
|
||||
- **Status**: **COMPLETE / STABLE**
|
||||
- **Branch**: `ae_app_3x_llm`
|
||||
- **Verification**: Verified via `svelte-check` and theme inspections.
|
||||
|
||||
---
|
||||
*Prepared by: Gemini CLI (March 17, 2026)*
|
||||
|
||||
---
|
||||
*Archival note (2026-03-20): `element_modal_v1.svelte` (referenced in §2 as "new standard modal") was subsequently retired — it had zero active importers. Modal usage in the codebase relies on Flowbite `<Modal>` component. See `AE__UI_Component_Patterns.md` §11.*
|
||||
@@ -0,0 +1,163 @@
|
||||
PRES-MGMT Session View — Refactor Plan
|
||||
|
||||
**STATUS: ✅ RESOLVED (2026-02-26)**
|
||||
|
||||
## Resolution Summary
|
||||
|
||||
The "refresh twice" bug was fixed by addressing **two root causes** in the nested data loader chain:
|
||||
|
||||
1. **Disabled caching in nested loads**: Changed `try_cache: false` to `try_cache` (preserve parent value) in:
|
||||
- `ae_events__event_session.ts` → `_refresh_session_id_background()`
|
||||
- `ae_events__event_presentation.ts` → `_refresh_presentation_li_background()`
|
||||
|
||||
2. **Missing microtask yields**: Added `await Promise.resolve()` after each `db_save_ae_obj_li__ae_obj()` call to ensure Dexie liveQuery observers fire before functions return.
|
||||
|
||||
3. **Fire-and-forget nested loads**: Changed `forEach()` to `await Promise.all()` in presentation loader to block until all presenter loads complete.
|
||||
|
||||
**Result:** Session view now renders presentations AND presenters on first load without manual refresh.
|
||||
|
||||
**Performance Impact:** Adds ~100-200ms to initial navigation (time to write 1-5 presenter records + microtask yields), but guarantees correct first-render.
|
||||
|
||||
**Files Modified:**
|
||||
- `src/lib/ae_events/ae_events__event_session.ts` (lines 100-107)
|
||||
- `src/lib/ae_events/ae_events__event_presentation.ts` (lines 187-198)
|
||||
- `src/lib/ae_events/ae_events__event_presenter.ts` (line 157-161)
|
||||
|
||||
**Documentation Updated:**
|
||||
- `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md` - Added "try_cache: false" bug section with detailed explanation
|
||||
- Code comments added explaining the critical fixes in all modified loader functions
|
||||
- Journals module updated with microtask yields for consistency (already preserved try_cache correctly)
|
||||
|
||||
**Test Status:**
|
||||
- Manual testing: ✅ Confirmed working (session + presentations + presenters render on first load)
|
||||
- Playwright test: Created at `tests/coldstart_event_session.test.ts` (requires valid session ID to run)
|
||||
|
||||
**Lessons Learned:**
|
||||
1. **Always preserve `try_cache` through nested data loads** - Disabling caching at any level breaks the entire chain
|
||||
2. **Add microtask yields after IndexedDB writes** - Ensures liveQuery observers fire before functions return
|
||||
3. **Block on nested loads with `await Promise.all()`** - Fire-and-forget forEach() causes race conditions
|
||||
4. **Journals module was already correct** - It preserved `try_cache` for nested entry loads; only added yields for consistency
|
||||
5. **The existing blocking pattern was sufficient** - No special helper function needed; the bug was in the loader implementation, not the architecture
|
||||
|
||||
**What We Implemented:**
|
||||
We effectively implemented **Option A (Blocking Hydration)** but at a lower level than originally planned. Instead of creating a new `load_session_with_relations()` helper, we fixed the existing loader chain to properly block and cache nested data. The `+page.ts` already used `await` for the session load - it just wasn't working because the underlying loaders weren't caching nested data.
|
||||
|
||||
---
|
||||
|
||||
## Original Plan (For Reference)
|
||||
|
||||
Goal
|
||||
|
||||
Make the Presentation Management Session view deterministic on cold-start (empty IndexedDB). The page must render Presentations, Presenters, and Hosted Files without requiring manual refreshes.
|
||||
|
||||
Constraints
|
||||
|
||||
- Svelte 5 runes and Dexie `liveQuery` behavior (observable recreation, subscription timing).
|
||||
- Minimize user-perceived latency — keep navigation snappy where possible.
|
||||
- Avoid large architectural changes unless necessary.
|
||||
|
||||
Options (high level)
|
||||
|
||||
A) Blocking Hydration (recommended for correctness)
|
||||
- Block the route `+page.ts` load until the session and all directly required related objects are fetched from the backend and written into IndexedDB. Return `initial_session_obj` in the load data for immediate rendering.
|
||||
- Pros: simplest to guarantee first-draw correctness; minimal component changes.
|
||||
- Cons: adds latency to navigation (can be mitigated with optimistic UI or progress indicator).
|
||||
|
||||
B) Prefetch Related Records + Hydrate Fallback (hybrid)
|
||||
- Non-blocking load but `+page.ts` returns `initial_session_obj` and small related-objects payloads (presentations, presenter IDs, hosted_file metadata). Components use these fallbacks while `liveQuery` takes over.
|
||||
- Pros: keeps navigation responsive; often sufficient.
|
||||
- Cons: requires careful payload shaping and DB write ordering.
|
||||
|
||||
C) Explicit Dependency Chaining in UI (advanced)
|
||||
- Keep non-blocking loads and use explicit dependency chaining: write session -> await write completion -> then write presentations -> await -> then presenters, ensuring microtask queue flushes between writes. Use targeted `liveQuery` re-creation only when upstream dependency fully resolved.
|
||||
- Pros: minimal route latency; deterministic ordering.
|
||||
- Cons: more complex to implement and test.
|
||||
|
||||
Recommendation
|
||||
|
||||
Start with Option A (Blocking Hydration) for the session page to restore deterministic behavior quickly. After correctness is achieved, consider converting to Option B or C for improved perceived performance if needed.
|
||||
|
||||
Detailed Steps (Option A - Blocking Hydration)
|
||||
|
||||
1) Add a small helper in `events_func` (e.g., `load_session_with_relations`) that:
|
||||
- fetches session by ID from API
|
||||
- fetches related presentations (limit/filters as needed)
|
||||
- fetches presenters referenced by those presentations (deduplicate IDs)
|
||||
- fetches hosted_file metadata for presentation files (if required for the view)
|
||||
- writes all results to IndexedDB in a controlled order (session -> presentations -> presenters -> hosted_files)
|
||||
- returns a compact `initial_session_obj` payload containing fields needed for first-draw (session, presentation list, presenter summary)
|
||||
|
||||
Implementation note: Use `await db.transaction('rw', db_events.session, db_events.presentation, db_events.presenter, async () => {...})` if atomicity helps. Alternatively write in sequential awaits and call `await Promise.resolve()` after each write to let the microtask queue settle.
|
||||
|
||||
2) Update route loader: `src/routes/events/[event_id]/(pres_mgmt)/session/[session_id]/+page.ts` (create if missing) to call and `await` the helper, then return `initial_session_obj` on `data`.
|
||||
|
||||
Example pseudo-code:
|
||||
|
||||
export async function load({ params, parent }) {
|
||||
const data = await parent();
|
||||
if (browser) {
|
||||
const init = await events_func.load_session_with_relations({ api_cfg: data[data.account_id].api, session_id: params.session_id, log_lvl: 0 });
|
||||
data.initial_session_obj = init;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
3) Ensure the page component `+page.svelte` uses the `initial_session_obj` as immediate fallback (it already does in Aether).
|
||||
|
||||
4) Add instrumentation logs inside `liveQuery` closures and the helper to verify ordering during QA.
|
||||
|
||||
5) Add tests (see below) and manual verification steps.
|
||||
|
||||
Alternative (Option B - Hybrid) Implementation Notes
|
||||
|
||||
- If you cannot block the route, return an `initial_session_obj` that includes minimal related object arrays (IDs + small metadata) and have `+page.svelte` write those into IDB before mounting heavy child components.
|
||||
- Use `untrack()` to set selection IDs so stores are updated without causing premature reactivity loops.
|
||||
|
||||
Explicit Dependency Chaining (Option C) Notes
|
||||
|
||||
- Implement a single `prefetch` function that sequentially performs writes and `await Promise.resolve()` between stages.
|
||||
- For debugging, add microtask delays (e.g., `await 0`) between writes to observe behaviour.
|
||||
|
||||
Testing and Verification
|
||||
|
||||
1) Integration test (Playwright recommended)
|
||||
- Clear IndexedDB for the app origin.
|
||||
- Navigate to `/events/<event_id>/.../session/<session_id>` and assert that the presentation list and presenters are visible within N ms without manual refresh.
|
||||
- Repeat on subsequent navigations to ensure no regressions.
|
||||
|
||||
2) Unit tests
|
||||
- For `events_func.load_session_with_relations`, stub API responses and assert DB writes are made in expected order.
|
||||
|
||||
3) Manual QA
|
||||
- With a cold profile or after clearing Site storage, navigate to the session page and confirm content is present after the initial navigation and that no manual refreshes are required.
|
||||
|
||||
Migration and Rollout
|
||||
|
||||
- Implement Option A behind a feature flag if you want to control rollout.
|
||||
- Short-term: apply Option A to the single problematic route to reduce blast radius.
|
||||
- Long-term: consider a library-level helper to standardize "blocking prefetch for nested related records" across other pages.
|
||||
|
||||
Rollback Plan
|
||||
|
||||
- Because changes are additive and limited to one route and helper, revert the `+page.ts` modification and helper call to restore prior behavior.
|
||||
|
||||
Deliverables for tomorrow
|
||||
|
||||
- `events_func.load_session_with_relations` helper (TS) + unit tests
|
||||
- Updated `+page.ts` loader for session route to `await` helper and return `initial_session_obj`
|
||||
- Small test harness / Playwright test that reproduces the cold-start issue and verifies the fix
|
||||
- Instrumentation logs temporarily enabled for QA
|
||||
|
||||
Estimated effort
|
||||
|
||||
- Blocking hydration implementation + tests: 2-4 hours
|
||||
- Hybrid or chaining implementations: additional 2-6 hours depending on thoroughness
|
||||
|
||||
Notes about Svelte 5 + Dexie specifics
|
||||
|
||||
- Keep `liveQuery` closures stable; capture primitive IDs rather than reactive objects.
|
||||
- Use `$derived` and `$derived.by` to keep observable instances stable across renders.
|
||||
- Use `untrack()` when setting selection values to avoid premature subscriptions.
|
||||
- After DB writes, allowing the microtask queue to settle (`await Promise.resolve()`) helps ensure observers are notified in the expected order during development and debugging.
|
||||
|
||||
If you want I can implement Option A for the session route tomorrow (create helper, update loader, add test).
|
||||
106
documentation/history/PROJECT__AE_combined_front_back_Docker.md
Normal file
106
documentation/history/PROJECT__AE_combined_front_back_Docker.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Project: Unified Aether Platform Orchestration (V3)
|
||||
> **Status: ✅ COMPLETE (2026-03-10)**
|
||||
> `ae_app` is live in `aether_container_env/docker-compose.yml`. Both frontend and backend deploy together via `npm run deploy:staging` / `npm run deploy:prod`. Internal `ae_api` networking active. Healthcheck wired. Old `ae_env_node_app` is superseded (archive when ready).
|
||||
|
||||
> **Goal:** Consolidate the SvelteKit Frontend and FastAPI Backend into a single Docker Compose environment within `aether_container_env`.
|
||||
|
||||
## 1. Overview & Benefits
|
||||
Currently, the platform runs in two separate Docker environments (`ae_env_node_app` and `aether_container_env`). Combining them into one allows for:
|
||||
- **Unified Lifecycle:** Start/Stop/Update the entire stack with one command.
|
||||
- **Internal Networking:** The Frontend (SvelteKit) can communicate with the Backend (FastAPI) via the high-speed internal Docker network (`ae_api:5005`) instead of routing through external IPs.
|
||||
- **Shared Infrastructure:** Single instances of Redis, Dozzle, and Nginx serving both tiers.
|
||||
- **Simplified Deployment:** Reduces the need for sibling directory dependencies on production servers.
|
||||
|
||||
---
|
||||
|
||||
## 2. Target Architecture (Workstation & Prod)
|
||||
The unified environment will reside in `~/OSIT_dev/aether_container_env/`.
|
||||
|
||||
### Directory Mapping
|
||||
- **API Source:** `~/OSIT_dev/aether_api_fastapi` -> Mounted to `ae_api`
|
||||
- **App Source:** `~/OSIT_dev/aether_app_sveltekit` -> Mounted to `ae_app`
|
||||
- **Nginx Config:** `~/OSIT_dev/aether_container_env/conf/nginx/`
|
||||
- **Logs:** `~/OSIT_dev/aether_container_env/logs/`
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation Plan
|
||||
|
||||
### Step 1: Update `aether_container_env/.env`
|
||||
Add these new variables to the master `.env` file:
|
||||
```bash
|
||||
# --- SvelteKit Frontend settings ---
|
||||
AE_APP_SRC=/home/scott/OSIT_dev/aether_app_sveltekit
|
||||
CONTAINER_AE_APP=ae_app_dev
|
||||
AE_APP_REPLICAS=4
|
||||
AE_APP_NODE_PORT=3001
|
||||
# Note: Use internal DNS 'ae_api' for PUBLIC_AE_API_SERVER_INTERNAL
|
||||
```
|
||||
|
||||
### Step 2: Integrate `ae_app` into `docker-compose.yml`
|
||||
Add the consolidated SvelteKit service. This replaces the legacy Flask service:
|
||||
```yaml
|
||||
ae_app:
|
||||
restart: always
|
||||
build:
|
||||
context: ${AE_APP_SRC}
|
||||
dockerfile: Dockerfile
|
||||
target: deploy-node
|
||||
scale: ${AE_APP_REPLICAS}
|
||||
env_file:
|
||||
- ./.env
|
||||
ports:
|
||||
- "${AE_APP_NODE_PORT}:3000"
|
||||
extra_hosts:
|
||||
- "${DOCKER_AE_SERVER_EXTRA_HOST}"
|
||||
- "${DOCKER_AE_APP_SERVER_EXTRA_HOST}"
|
||||
- "${DOCKER_AE_API_SERVER_EXTRA_HOST}"
|
||||
volumes:
|
||||
# Mount source for real-time dev (Optional for production)
|
||||
- ${AE_APP_SRC}:/srv/aether_app
|
||||
- ${HOSTED_FILES_SRC}:/srv/hosted_files
|
||||
- ${HOSTED_TMP_SRC}:/srv/hosted_tmp
|
||||
depends_on:
|
||||
- ae_api
|
||||
- redis
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
```
|
||||
|
||||
### Step 3: Nginx Gateway Configuration
|
||||
Update (or create) the Nginx template in `conf/nginx/` to route traffic to the internal `ae_app` service.
|
||||
|
||||
**Example Upstream Block:**
|
||||
```nginx
|
||||
upstream svelte_backend {
|
||||
# Internal Docker DNS handles load balancing across replicas
|
||||
server ae_app:3000;
|
||||
# ip_hash; # Enable if session persistence is needed
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Verification & Migration
|
||||
1. **Healthcheck:** Ensure `src/routes/health/+server.ts` is active.
|
||||
2. **Internal Proxy Test:** Update SvelteKit's `PUBLIC_AE_API_SERVER` to `ae_api` (internal) and verify connectivity.
|
||||
3. **Clean Up:** Once verified, the old `ae_env_node_app` directory can be archived.
|
||||
|
||||
---
|
||||
|
||||
## 4. Scaling & Performance
|
||||
- **API Scaling:** Controlled by `AE_API_REPLICAS`.
|
||||
- **App Scaling:** Controlled by `AE_APP_REPLICAS`.
|
||||
- **Memory Management:** Each replica is isolated. Shared state (caching) is handled via the internal **Redis** service.
|
||||
- **Healthchecks:** Docker will automatically restart any `ae_app` replica that fails the `/health` check.
|
||||
|
||||
---
|
||||
|
||||
## 5. Deployment Commands (Unified)
|
||||
```bash
|
||||
# From aether_container_env/
|
||||
docker compose up -d --build --remove-orphans
|
||||
docker compose ps
|
||||
docker compose logs -f ae_app
|
||||
```
|
||||
Reference in New Issue
Block a user