# Aether SvelteKit — AI Agent Bootstrap / Quickstart
> **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: `x-aether-api-key` + `x-account-id`; JWT Bearer is auto-injected when a session exists |

---

## 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. Mistakes Agents Have Made on This Project

These are real incidents — know them before you start.

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.

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.

3. **Bad `.d.ts` declaration silently hid 1368 errors** — a `declare module` in `app.d.ts`
   (a script-context file) replaced the entire `@lucide/svelte` type exports instead of
   merging. `svelte-check` showed 0 errors, masking real problems. If `svelte-check`
   suddenly drops to 0 errors, verify it's not because a bad declaration wiped a module.

4. **Coarse store reactivity loop** — an `$effect` that read `$ae_loc.some_field` was
   re-triggering repeatedly because unrelated writes to `$ae_loc` (e.g. SWR config reload)
   fired the effect. In Svelte 5, any read of a Svelte 4 store inside `$effect` subscribes
   to the whole store. Scope what you read carefully.

5. **`file_purpose == 'admin'` not hidden in Launcher** — the `hide_draft` prop hid
   `outline` and `draft` files but not `admin` files. Gaps like this happen when a new
   enum value is added to a field without auditing all the places that filter on it.

6. **Deleting files with `rm`** — always move to `~/tmp/agents_trash`. A deleted file may
   contain context that's not recoverable from git if it was gitignored.

7. **Dexie `.get()` with a string object ID returns `undefined`** — Dexie `.get(value)`
   looks up by the table's **primary key**, which is `id` (the first schema field). The V3
   API never returns `id`, so it is always `undefined` in stored records. Passing a string
   object ID (e.g. `person_id`) to `.get()` will silently return nothing. Always use
   `.where('person_id').equals(person_id).first()` instead. This has caused liveQuery
   blocks to always produce `undefined` even when the record exists in Dexie.

8. **Treating `$effect` blocks as auth bypass risks** — a `$effect` inside a child
   component cannot bypass a parent `+layout.svelte` auth gate. Children only mount if
   the parent calls `{@render children?.()}`. Adding redundant auth guards to `$effect`
   blocks that can only run after the parent gate already passed is unnecessary — and
   misleads future readers into thinking the parent gate is not sufficient on its own.
   The **real** pre-gate risk is `+page.ts` / `+layout.ts`: universal load functions run
   before any layout mounts and also fire during SvelteKit link prefetch. Keep those files
   clean of data loads in private modules. See `GUIDE__SvelteKit2_Svelte5_DexieJS.md` →
   "SvelteKit Layout Hierarchy: Security and Execution Order" for the full explanation.

9. **Using query `key` as a proxy for bypass stripped `x-account-id`** — this caused
   valid account-scoped requests to lose account context and 403. `key` can be a valid
   endpoint/business param, but it is not equivalent to `x-no-account-id: bypass`. Keep
   `x-no-account-id` usage narrow and temporary; do not expand it without a documented
   allowlist case.

10. **Pre-stringifying `*_json` fields before passing to API wrappers** — the API wrappers
   (`api_post__crud_obj.ts` for V3, `api.ts` for legacy CRUD) automatically serialize any
   field ending in `_json` (e.g. `cfg_json`, `data_json`). Pass these as plain JS objects.
   Pre-stringifying with `JSON.stringify()` before calling the wrapper will double-encode
   the value in the legacy path (stringify sees a string and escapes it), and is at best
   redundant on the V3 path. Both paths now pretty-print with 2-space indent.
   See `GUIDE__AE_API_V3_for_Frontend.md` → section 3C for the full explanation.

11. **Broad Dexie result windows get silently clipped** — if a broad "All" view shows fewer
    rows than a narrower filter, check for a page-level limit or an API revalidation step
    replacing the local IDB result set. For empty text searches, the full local result set
    should drive the display; server refreshes should update cache, not shrink visibility.

12. **Not bumping `IDB_CONTENT_VERSIONS` when changing `properties_to_save`** — this caused
    the IDAA Recovery Meetings "no meetings found" bug for approximately one year (2025–2026).

    **What happened:** A deploy changed `properties_to_save` in `ae_events__event.ts`, but no
    one bumped `IDB_CONTENT_VERSIONS.events.event` in `store_versions.ts`. Existing users kept
    the old stale event records in IndexedDB indefinitely. On the Recovery Meetings page, the
    fast path (IDB search) returned those stale records, which all failed the `account_id`
    filter and returned 0 results. The API call then either errored silently or was filtered
    to 0 by the secondary client-side filter. Critically, the error state and the genuinely
    empty state showed the **same** "No meetings found" message — users and staff had no
    indication a failure had occurred. The manual Full Reset (via the `?` help panel) always
    fixed it, but no one knew why it worked, making the root cause impossible to track down.

    **The fix (2026-05-16):** `check_and_clear_idb_table()` in `store_versions.ts` is now
    wired in `src/routes/idaa/(idaa)/+layout.svelte` for `db_events.event`. On a version
    match it costs one localStorage read. On a mismatch it silently clears the table; the
    SWR pattern then repopulates from the API on next load.

    **The rule going forward:**
    - When you change `properties_to_save` in any `ae_events__*.ts` file (or any other
      object file) in a way that makes existing cached records stale — fields added, removed,
      renamed, or where a computed field's behavior changes — **bump the matching entry in
      `IDB_CONTENT_VERSIONS` in `src/lib/stores/store_versions.ts`**.
    - If the table is not yet wired, wire it first (see the wiring instructions in the
      `IDB_CONTENT_VERSIONS` comment block in `store_versions.ts`).
    - Currently wired: `events.event`. All other tables are not yet wired.

    **Also:** Never show the same UI message for both a failed API call and a genuinely empty
    result. Always distinguish `qry__status === 'error'` from `qry__status === 'done'` with
    0 results in your templates. Silent failures look like data problems and are extremely
    difficult to diagnose.

13. **Breaking the API retry loop by returning errors instead of throwing them** — all four
    `api_*_object.ts` files (`api_get_object.ts`, `api_post_object.ts`, `api_patch_object.ts`,
    `api_delete_object.ts`) use a `.catch()` that returns the error as a value, followed by a
    classification block. That block **must throw** for transient network failures (`TypeError`)
    so they enter the retry loop. If you change it to `return false`, retries are silently
    bypassed for the most common failure mode in hotel/conference WiFi — and nothing warns you.

    **What happened (commit a10accfaa, Jan 2026):** A "silence background fetch noise" commit
    changed `.catch()` to explicitly `return error`, then the classification block was changed
    from a `throw` to `return false`. `TypeError` from `ERR_NETWORK_CHANGED` — the most common
    failure on crowded WiFi — stopped retrying. The `retry_count = 5` parameter became dead
    code for network errors. Went undetected for ~4 months.

    **The retry classification these files must honor:**
    - `TypeError` (ERR_NETWORK_CHANGED, WiFi blip) → **`throw`** → enters retry loop with backoff
    - `AbortError` where `did_timeout_abort = true` (helper's own timer) → **`throw`** → retries
    - `AbortError` where `did_timeout_abort = false` (navigation/unmount abort) → `return false`
    - HTTP 400/401/403/422 → `return false` immediately (client errors are deterministic)
    - HTTP 5xx → **`throw`** → retries with backoff

    **How to verify after any change to the error block:** confirm that a `TypeError` still
    produces up to 5 retry attempts with 2s→4s→6s→8s delays before returning false. A single
    `return false` after the first network failure means the retry loop is broken.

    **Also:** when reviewing these files, check that all four have:
    - `ae_auth_error.set()` triggered on 401/403 (shows session-expired banner to the user)
    - `timeout = 20000` default (was 60s in PATCH/DELETE until 2026-05-21 — 5-min worst case)
    - `did_timeout_abort` flag per attempt (separates helper timeouts from caller aborts)

14. **Account-scoped `liveQuery` trigger firing before bootstrap completes** — components
    that load account-specific data via `liveQuery` must not trigger the API fetch until the
    bootstrap Sync Effect in `+layout.svelte` has set the real `account_id`.

    **What happened:** `element_data_store.svelte` triggered its load when `entry` was falsy.
    On a fresh load with no IDB cache, `$ae_api.account_id` was still `null` (bootstrap hadn't
    run yet). The `localStorage` scavenge in `api_get_object.ts` then read the stale
    `account_id = 1` from a previous dev/demo session and made the API call with the wrong
    account. The response was cached in IDB, and the next page load showed the wrong account's
    record.

    A second failure mode: if IDB _did_ have a cached record from a previous session with a
    different account, `liveQuery` returned it as a valid hit (`entry` truthy), so the trigger
    never fired to fetch the correct record.

    **The fix pattern** for any trigger `$effect` that depends on bootstrapped account context:
    ```typescript
    $effect(() => {
        // Use $slct.account_id (non-persisted), NOT $ae_loc.account_id (persisted, stale).
        // $slct is initialized to null and set only by the bootstrap Sync Effect, so it
        // reliably gates the fetch until bootstrap has completed.
        const account_id = $slct.account_id;
        const api_ready  = !!$ae_api?.base_url;
        const entry      = $lq__ds_obj as SomeType | null | undefined;

        if (!browser || !account_id || !api_ready) return;

        // Also re-fetch when IDB holds a record from a different (non-null) account.
        // null account_id = global/shared fallback — that is still a valid cache 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...';
        }
    });
    ```

    **Why `$slct` not `$ae_loc`:**
    `$ae_loc` is a `svelte-persisted-store` — it hydrates from `localStorage` before any
    effects run, so its `account_id` may be a stale value from a previous session. `$slct`
    is a plain writable store initialized to `null`; the bootstrap Sync Effect is the only
    thing that sets it. Until that runs, `$slct.account_id` is `null`, providing a reliable
    gate. See `GUIDE__SvelteKit2_Svelte5_DexieJS.md` → "Bootstrap Race" for the Dexie-side
    context.

15. **`tmp_sort_*` comparators written descending instead of ascending** — `build_tmp_sort()` encodes `priority=true` as `'0'` and `priority=false` as `'1'`, designed for **ascending** sort so priority items appear first. Writing a JS `.sort()` comparator as `b.localeCompare(a)` (descending) inverts the encoding and sends priority items to the bottom.

    Found in journals (2026-06), IDAA recovery meetings fast-path and API re-sort (2026-06), and as a Dexie anti-pattern in BB post comments.

    ```ts
    // ❌ Wrong — descending puts priority=false ('1') before priority=true ('0')
    list.sort((a, b) => (b.tmp_sort_1 ?? '').localeCompare(a.tmp_sort_1 ?? ''));

    // ✅ Correct — ascending matches build_tmp_sort encoding
    list.sort((a, b) => (a.tmp_sort_1 ?? '').localeCompare(b.tmp_sort_1 ?? ''));
    ```

    **Companion Dexie trap:** `collection.reverse().sortBy('tmp_sort_*')` — Dexie ignores a collection-level `.reverse()` when `.sortBy()` is called. The sort is always ascending. To reverse the result, call `.reverse()` on the returned array after `await`. See `GUIDE__SvelteKit2_Svelte5_DexieJS.md` → `build_tmp_sort` section.

    **Exception — legacy `ae_events__event.ts` encoding:** `ae_events__event.ts` (and `ae_events__event_session.ts`) do NOT use `build_tmp_sort`. They use `priority ? 1 : 0` (priority=true→`'1'`), which requires **descending** sort to put priority items first. `ae_events__event_presentation.ts` DOES use `build_tmp_sort` (it overrides the generic encoding in its `specific_processor`). Do not apply the ascending rule to raw event or session sorts until those modules are migrated to `build_tmp_sort`.

16. **Service worker without `skipWaiting()` + `clients.claim()` silently serves stale code to long-lived tabs** — The default SvelteKit service worker template does NOT include these calls. Without them, a new SW installs in the background but waits in a **"waiting"** state until every tab running the old version is closed before it activates. Users who leave a page open all day (especially IDAA members in the Novi iframe on idaa.org) run old buggy JS indefinitely after a fix is deployed.

    **Symptom that should trigger this check:** Bug reports from users that developers cannot reproduce. Developers constantly refresh and open/close tabs — the new SW activates immediately for them. End users with persistent tabs never get it.

    **The fix** (already applied to `src/service-worker.js` as of 2026-06-03):
    ```js
    self.addEventListener('install', (event) => {
        event.waitUntil(addFilesToCache());
        self.skipWaiting(); // activate immediately, don't wait for tabs to close
    });
    self.addEventListener('activate', (event) => {
        event.waitUntil(deleteOldCaches());
        self.clients.claim(); // take control of all open tabs right away
    });
    ```

    **Trade-off:** A tab mid-session gets new JS without a page reload. For a read-heavy app like IDAA (browsing meetings) this is harmless. For a form-heavy app the risk is higher — weigh accordingly.

---

## 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. 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 |
| Dev workflow + commit rules | `documentation/GUIDE__Development.md` |
| V3 API reference | `documentation/GUIDE__AE_API_V3_for_Frontend.md` |
| Dexie / liveQuery patterns | `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.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` |
| Store migration plan | `documentation/PROJECT__Stores_Svelte5_Migration.md` |
| Exhibitor Leads module | `documentation/MODULE__AE_Events_Exhibitor_Leads.md` |
| Naming conventions | `documentation/AE__Naming_Conventions.md` |
