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>
508 lines
24 KiB
Markdown
508 lines
24 KiB
Markdown
# 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 (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.
|
||
|
||
---
|
||
|
||
## 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` |
|