All cache-clearing buttons and the IDAA clear-caches page previously cleared IDB/localStorage but left service worker registrations and Cache Storage intact. On the next reload the SW re-served the old JS bundle, leaving users stuck on stale code despite appearing to reload. This caused recurring stale-state reports from IDAA and other clients for 4+ months. Two gaps closed: 1. Every clear path (root page buttons, sys bar, help tech, idaa/clear-caches) now unregisters SW registrations and clears Cache Storage before touching IDB and localStorage. Order: SW → Cache Storage → IDB → localStorage. 2. Added controllerchange listener in +layout.svelte effect 4. When the new SW activates and calls clients.claim(), this listener reloads the page so open tabs run the new JS bundle instead of keeping old code in memory indefinitely. Without this, skipWaiting + clients.claim work correctly on the SW side but the page side never picks up the update. Also added thorough code comments and updated REFERENCE__Common_Agent_Mistakes (#15) and BOOTSTRAP doc (#8) to document the full root cause so this cannot be silently re-broken by a future agent or refactor. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
403 lines
17 KiB
Markdown
403 lines
17 KiB
Markdown
# 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.
|
|
|
|
---
|
|
|
|
## 1. What This Project Is
|
|
|
|
**Aether** is an event management platform built by One Sky IT (Scott Idem).
|
|
This repo is the frontend: **Svelte 5 (runes mode) + SvelteKit v2**.
|
|
|
|
The backend is a separate repo (`aether_api_fastapi`) — a FastAPI + MariaDB app
|
|
running in Docker. The frontend talks to it exclusively via the V3 REST API.
|
|
|
|
**Key clients:**
|
|
- **Conference organizers** — Presentation Management (pres_mgmt), Launcher, Badges
|
|
- **Exhibitors** — Leads capture
|
|
- **IDAA** — International Doctors in Alcoholics Anonymous (private medical/recovery community)
|
|
|
|
**Stack at a glance:**
|
|
|
|
| Layer | Technology |
|
|
|---|---|
|
|
| Framework | Svelte 5 (runes mode) + SvelteKit v2 |
|
|
| Styling | Tailwind CSS v4 + Flowbite (Skeleton UI being phased out) |
|
|
| State | `$state`/`$derived` runes + Dexie.js IndexedDB (`liveQuery`) |
|
|
| Icons | Lucide (`@lucide/svelte`) |
|
|
| 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 only: `x-aether-api-key` + `x-account-id` — **NOT** Bearer tokens |
|
|
|
|
---
|
|
|
|
## 2. Critical Rules — Read Before Touching Any Code
|
|
|
|
### Privacy (Sev-1 class failures if violated)
|
|
- **IDAA content is ALWAYS private.** All routes under `/idaa/` require authentication.
|
|
A previous AI agent accidentally made IDAA bulletin board data publicly accessible.
|
|
This is the single most serious class of mistake on this project. When in doubt — it's private.
|
|
- **Journals** are private personal data. Always authenticated.
|
|
|
|
### File Safety
|
|
- **Never use `rm`** to delete files. Move to `~/tmp/agents_trash` instead.
|
|
- Never commit `.env` files, API keys, or passwords.
|
|
|
|
### Before Every Commit
|
|
- Run `npx svelte-check` — zero errors, zero warnings. No exceptions.
|
|
- Atomic commits: one component or one fix per commit.
|
|
|
|
### Before Starting Any Task
|
|
- Read `documentation/TODO__Agents.md` — it has active tasks, known bugs, and context
|
|
about what was recently changed and why.
|
|
|
|
### V3 API — Never Include the Object ID in PATCH Body Fields
|
|
The ID is in the URL. Including it in `data_kv` causes a `400: Unknown column in SET`.
|
|
```ts
|
|
// WRONG — causes 400 error:
|
|
update_ae_obj__event_file({ event_file_id, data_kv: { event_file_id, file_purpose: 'final' } })
|
|
|
|
// CORRECT:
|
|
update_ae_obj__event_file({ event_file_id, data_kv: { file_purpose: 'final' } })
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Environment & Deploy Cheat Sheet
|
|
|
|
There are **two separate `.env` systems** — do not confuse them:
|
|
|
|
| System | File | Controls |
|
|
|---|---|---|
|
|
| `aether_container_env/.env` | Docker orchestration | Ports, `AE_CFG_ID`, replicas, paths |
|
|
| `aether_app_sveltekit/.env.*` | Vite/SvelteKit build | `PUBLIC_*` API vars baked into the JS bundle |
|
|
|
|
**The 4 commands you run and which env file each uses:**
|
|
|
|
| Command | Env file read |
|
|
|---|---|
|
|
| `npm run dev` | `aether_app_sveltekit/.env.local` (Vite dev server, localhost:5173) |
|
|
| `npm run build:docker:dev` | `aether_app_sveltekit/.env.dev` (baked into local Docker image) |
|
|
| `npm run deploy:remote:test` | `/srv/apps/test_aether_app_sveltekit/.env.test` on Linode |
|
|
| `npm run deploy:remote:prod` | `/srv/apps/prod_aether_app_sveltekit/.env.prod` on Linode |
|
|
|
|
**The `.env.*` files are gitignored** (only `.default` templates are tracked). They must be
|
|
placed manually on each server during initial setup. On the workstation you only need
|
|
`.env.local` and `.env.dev`. The Linode servers each have exactly one env file for their environment.
|
|
|
|
**What goes in every SvelteKit env file** (same 8 vars, different values per env):
|
|
```env
|
|
PUBLIC_AE_API_PROTOCOL=https
|
|
PUBLIC_AE_API_SERVER=<api server hostname>
|
|
PUBLIC_AE_API_BAK_SERVER=<bak api hostname>
|
|
PUBLIC_AE_API_PORT=443
|
|
PUBLIC_AE_API_PATH=
|
|
PUBLIC_AE_API_SECRET_KEY=<key>
|
|
PUBLIC_AE_CRUD_SUPER_KEY=<key>
|
|
PUBLIC_AE_BOOTSTRAP_KEY=<key>
|
|
PUBLIC_AE_NO_ACCOUNT_ID=No_Account_ID_Here
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Svelte 5 Runes Mode — Key Patterns & Gotchas
|
|
|
|
This codebase is **fully Svelte 5 runes mode**. No Svelte 4 syntax.
|
|
|
|
### The basics
|
|
```svelte
|
|
<script lang="ts">
|
|
// Props — with optional two-way binding
|
|
interface Props { count?: number; label: string; }
|
|
let { count = $bindable(0), label }: Props = $props();
|
|
|
|
// Reactive state
|
|
let value = $state('');
|
|
let upper = $derived(value.toUpperCase());
|
|
|
|
// Side effects (replaces onMount + $: reactive)
|
|
$effect(() => {
|
|
console.log('value changed:', value);
|
|
return () => { /* cleanup */ };
|
|
});
|
|
</script>
|
|
```
|
|
|
|
### What NOT to use (Svelte 4 patterns — do not introduce)
|
|
```ts
|
|
// ❌ No writable() stores for component state
|
|
import { writable } from 'svelte/store';
|
|
|
|
// ❌ No reactive declarations
|
|
$: doubled = count * 2;
|
|
|
|
// ❌ No onDestroy for cleanup — use $effect return instead
|
|
onDestroy(() => cleanup());
|
|
```
|
|
|
|
### `$bindable()` vs `$state()`
|
|
- Use `$bindable()` when the parent needs two-way binding on a prop.
|
|
- Use `$state()` for local component state with no external binding.
|
|
|
|
### Store reactivity trap (important for `$effect`)
|
|
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
|
|
{#await somePromise}
|
|
<LoadingSpinner />
|
|
{:then result}
|
|
<div>{result}</div>
|
|
{:catch error}
|
|
<ErrorMessage {error} />
|
|
{/await}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. V3 API Patterns
|
|
|
|
### SWR (Stale-While-Revalidate) — the standard load pattern
|
|
Return cached Dexie data immediately, refresh from API in background.
|
|
```ts
|
|
async function load_ae_obj_id__my_obj({ api_cfg, obj_id }) {
|
|
// 1. Return stale cache immediately (fast)
|
|
const cached = await db.my_obj.get(obj_id);
|
|
if (cached) my_obj_state = cached;
|
|
|
|
// 2. Fetch fresh from API in background
|
|
_refresh_my_obj_background({ api_cfg, obj_id });
|
|
}
|
|
```
|
|
|
|
### Shared/Common Aether object fields
|
|
The core fields for almost all Aether objects are:
|
|
* id/id_random
|
|
* code - string
|
|
* name - string
|
|
* summary - string
|
|
* content - string
|
|
* alert - boolean
|
|
* alert_msg - text
|
|
* priority - boolean
|
|
* sort - int
|
|
* group - string
|
|
* hide - boolean
|
|
* enable - boolean
|
|
* default_qry_str - special concat string index
|
|
* notes - text
|
|
* created_on - timestamp
|
|
* updated_on - timestamp
|
|
|
|
### ID convention — never use `_id_random` fields
|
|
The V3 API uses random string IDs (e.g. `event_file_id = "aBc123"`). The `*_id_random`
|
|
fields are legacy aliases. The integer version of the ID is never returned by the API. Always use the short form:
|
|
```ts
|
|
// ✅ Correct
|
|
event_file_obj.event_file_id
|
|
|
|
// ❌ Wrong — legacy alias, don't use
|
|
event_file_obj.event_file_id_random
|
|
```
|
|
The short ".id" is also the randomized string, **not an integer** (autonum).
|
|
|
|
### PATCH — only field values in the body
|
|
```ts
|
|
// The obj_id goes in the URL (handled by update_ae_obj__* function).
|
|
// Only the fields you want to update go in data_kv.
|
|
await events_func.update_ae_obj__event_file({
|
|
api_cfg: $ae_api,
|
|
event_file_id: 'aBc123', // → becomes the URL path param
|
|
data_kv: { file_purpose: 'final' } // → only changed fields
|
|
});
|
|
```
|
|
|
|
### Auth headers (set automatically by `api.ts`)
|
|
```
|
|
x-aether-api-key: <PUBLIC_AE_API_SECRET_KEY>
|
|
x-account-id: <account_id>
|
|
```
|
|
|
|
**Do not treat `params.key` as an auth bypass.**
|
|
Only explicit `x-no-account-id: bypass` means "drop account context".
|
|
If `key` is present for business logic, keep `x-account-id` intact.
|
|
|
|
### Dexie queries — always use the object ID index, not `.get()`
|
|
All `db_core` (and other module) Dexie tables define their schema with `id` as the first
|
|
field (primary key), followed by the object's string ID (e.g. `person_id`). V3 **never**
|
|
returns `id`, so every record stored in Dexie has `id = undefined`. Calling `.get(value)`
|
|
does a primary key lookup — it will always miss when passed a string object ID.
|
|
|
|
```ts
|
|
// ❌ Wrong — .get() uses the primary key (id), which V3 never populates:
|
|
liveQuery(() => db_core.person.get(person_id))
|
|
|
|
// ✅ Correct — use .where() on the indexed object ID field:
|
|
liveQuery(() => db_core.person.where('person_id').equals(person_id).first())
|
|
```
|
|
|
|
This applies to every table in every module (`db_core`, `db_events`, etc.).
|
|
When looking up a single object by its string ID, always use `.where().equals().first()`.
|
|
|
|
---
|
|
|
|
## 6. Naming Conventions (snake_case; no camelCase)
|
|
|
|
| Pattern | Example | Used for |
|
|
|---|---|---|
|
|
| `ae_comp__*` | `ae_comp__event_badge.svelte` | Route-level components |
|
|
| `ae_<module>_comp__*` | `ae_events_comp__session_list.svelte` | Module-scoped components |
|
|
| `element_*` | `element_input_files_tbl.svelte` | Reusable library primitives |
|
|
| `lq__*` | `lq__journal_obj` | Read-only liveQuery |
|
|
| `lqw__*` | `lqw__journal_obj` | Writable form snapshot liveQuery |
|
|
| `ae_<module>__<obj>.ts` | `ae_journals__journal.ts` | Object type + functions |
|
|
| `db_<module>.ts` | `db_journals.ts` | Dexie instance per module |
|
|
|
|
The **canonical pattern reference** is the Journals module (`src/lib/ae_journals/`).
|
|
When building anything new, model it after Journals.
|
|
|
|
---
|
|
|
|
## 7. Common Mistakes (Reference)
|
|
|
|
The full, curated mistake catalog now lives in
|
|
`documentation/REFERENCE__Common_Agent_Mistakes.md`.
|
|
|
|
Read this section first, then open the reference doc when your task touches one of these areas:
|
|
|
|
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 + cache clearing — MANDATORY four-layer wipe** — clearing IDB/localStorage
|
|
alone is a placebo. The SW Cache Storage is separate and must be cleared first or the SW
|
|
re-serves the old JS bundle on the very next reload. The `controllerchange` event listener in
|
|
`+layout.svelte` (effect 4) is also required or open tabs stay on old code after deploys.
|
|
See mistake #15 in the reference doc — this caused a recurring client issue for 4+ months.
|
|
9. **Local/remote config sync** — shadow fields that silently bypass an admin-synced master
|
|
field, SWR-await-after-write races, and stateful/conditional sync gates that desync local
|
|
state from history rather than current config. See the Pres Mgmt Config sync overhaul
|
|
(2026-06-16) in `PROJECT__AE_Events_PressMgmt_Config_Cleanup.md` for the full incident.
|
|
|
|
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
|
|
ae_events/ — Events, sessions, presenters, badges, locations, files
|
|
ae_journals/ — Journals (canonical/frontier model — copy patterns from here)
|
|
ae_idaa/ — IDAA custom module (PRIVATE — always authenticated)
|
|
elements/ — Reusable UI: V3 field editor, data store, CodeMirror, QR scanner
|
|
electron/ — Native Electron bridge (electron_relay.ts)
|
|
stores/ — ae_stores.ts, ae_events_stores.ts, ae_idaa_stores.ts
|
|
|
|
src/routes/
|
|
/core/ — Admin (accounts, people, sites, users)
|
|
/events/[id]/
|
|
/(pres_mgmt)/ — Presentation management
|
|
/(launcher)/ — Event launcher (kiosk display)
|
|
/(badges)/ — Badge printing
|
|
/(leads)/ — Exhibitor leads
|
|
/journals/ — Journals
|
|
/idaa/ — IDAA module (PRIVATE)
|
|
/hosted_files/ — File management
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Agent Messaging (ae_send_message / ae_inbox)
|
|
|
|
Use MCP tools to coordinate with other agents — do not write directly to the inbox directories.
|
|
|
|
```ts
|
|
// Send a message to the backend agent
|
|
ae_send_message({ recipient: 'backend', sender: 'frontend', content: '...' })
|
|
|
|
// Check your inbox (reads from frontend_svelte/ by default in this project)
|
|
ae_inbox({ command: 'list', status: 'unread' })
|
|
```
|
|
|
|
### Valid `ae_send_message` recipient names
|
|
|
|
| Recipient name | Inbox directory | Agent |
|
|
| --- | --- | --- |
|
|
| `frontend` | `~/agents_sync/inbox/frontend_svelte/` | SvelteKit frontend agent (this agent) |
|
|
| `backend` | `~/agents_sync/inbox/backend_fastapi/` | FastAPI backend agent |
|
|
| `mcp_agent` | `~/agents_sync/inbox/mcp_agent/` | MCP/orchestration agent |
|
|
| `scott_wks` | `~/agents_sync/inbox/scott_wks/` | Scott's workstation |
|
|
| `scott_lpt` | `~/agents_sync/inbox/scott_lpt/` | Scott's laptop |
|
|
| `gemini_cli` | `~/agents_sync/inbox/gemini_cli/` | Gemini CLI agent |
|
|
|
|
> **Gotcha:** The inbox directory names (`frontend_svelte`, `backend_fastapi`) do NOT match the
|
|
> `ae_send_message` recipient names (`frontend`, `backend`). Always use the recipient names above —
|
|
> using the directory name will produce an `invalid choice` error.
|
|
|
|
### Checking your own inbox
|
|
|
|
The `ae_inbox` MCP tool reads from the `frontend_svelte` inbox by default when invoked in this project directory. Use `status='unread'` to see new messages, `status='all'` to see everything. New messages can also arrive as files in `~/agents_sync/inbox/frontend_svelte/` — check there if `ae_inbox` shows nothing but the user says a message was sent.
|
|
|
|
---
|
|
|
|
## 10. Reading Order for Deeper Dives
|
|
|
|
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/MODULE__AE_Events_Launcher_Native.md` |
|
|
| Store migration plan | `documentation/PROJECT__Stores_Svelte5_Migration.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` |
|