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:
Scott Idem
2026-03-20 10:21:35 -04:00
parent bf9aa9710c
commit 2c570c82dc
6 changed files with 9 additions and 8 deletions

View 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 130135, 299305
**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 1320, 6275
**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 4447
**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 315330
**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.

View File

@@ -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.*

View File

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

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