Files
OSIT-AE-App-Svelte/documentation/BOOTSTRAP__AI_Agent_Quickstart.md
Scott Idem ee79e33a2a fix(idb-sort): correct tmp_sort_* comparator direction in journals, IDAA recovery meetings, and BB post comments
build_tmp_sort() encodes priority=true as '0' for ascending sort. JS comparators
were using b.localeCompare(a) (descending), inverting the encoding so priority=false
items sorted first. Fixed to a.localeCompare(b) in ae_journals_search_helpers.ts (3
sites in recovery_meetings +page.svelte and wrapper component).

Also fixes a Dexie anti-pattern in bb/[post_id]: .reverse() before .sortBy() is a
no-op in Dexie; moved array .reverse() to after the await.

Documents the encoding rule and legacy inverted-encoding modules in
GUIDE__SvelteKit2_Svelte5_DexieJS.md and adds mistake #15 to BOOTSTRAP quickstart.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 13:50:15 -04:00

508 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 uses `svelte-persisted-store` (Svelte 4 contract) for `$ae_loc`, `$ae_api`,
`$ae_sess`, etc. In Svelte 5 `$effect`, reading **any field** of a Svelte 4 store
subscribes to the **entire store**. This means unrelated writes to `$ae_loc`
(e.g. iframe height, SWR reload) will re-trigger your effect. Be conservative about
what you read from these stores inside `$effect` blocks. See `PROJECT__Stores_Svelte5_Migration.md`
for the long-term fix plan.
For search pages specifically, 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
### `{#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 (20252026).
**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.
---
## 8. Source Layout (Quick Reference)
```
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` |