docs: restructure bootstrap, add module references, and normalize docs

This commit is contained in:
Scott Idem
2026-06-12 16:48:35 -04:00
parent e05602b87b
commit a227c6aaa7
11 changed files with 511 additions and 237 deletions

View File

@@ -283,246 +283,23 @@ When building anything new, model it after Journals.
---
## 7. Mistakes Agents Have Made on This Project
## 7. Common Mistakes (Reference)
These are real incidents — know them before you start.
The full, curated mistake catalog now lives in
`documentation/REFERENCE__Common_Agent_Mistakes.md`.
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.
Read this section first, then open the reference doc when your task touches one of these areas:
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.
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 rollout behavior** — stale-tab symptoms, activation expectations, and trade-offs.
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.
**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. **`$` sigil on a plain prop value → `store_invalid_shape` at runtime** — when a parent passes
`prop={resolved_value}`, the child receives a plain JS value. Using `$prop` inside the child
tries to subscribe to it as a Svelte store and throws `store_invalid_shape: X is not a store
with a subscribe method`. This often happens in files migrated from Svelte 4 where `$lq__*`
usage was correct for the old store-subscription pattern. In Svelte 5, props are plain values:
```svelte
<!-- ❌ Wrong — treats prop as a Svelte store -->
{$lq__event_session_obj?.name}
<!-- ✅ Correct — prop is already the resolved value -->
{lq__event_session_obj?.name}
```
Also note: `PersistedState` stores (events module) use `.current` access, never the `$` sigil.
17. **`*_json` / `*_kv_json` DB columns start as `null`** — JSON blob columns (`cfg_json`,
`data_json`, `poc_kv_json`, etc.) are `null` in the database until first written. Two failure
modes:
- **Read crash:** `obj.poc_kv_json[poc_type]` throws `TypeError: can't access property "X",
poc_kv_json is null`. Fix: always use optional chaining: `obj.poc_kv_json?.[poc_type]`.
- **Write crash:** spreading `null` — `{ ...obj.poc_kv_json }` throws. Fix: initialize with
`{ ...(obj.poc_kv_json ?? {}) }` before writing any nested keys.
```ts
// ✅ Safe read
const bio_updated = obj.poc_kv_json?.[poc_type]?.biography_updated_on;
// ✅ Safe write
const kv = { ...(obj.poc_kv_json ?? {}) };
if (!kv[poc_type]) kv[poc_type] = {};
kv[poc_type]['biography'] = new_value;
await update_obj({ data_kv: { poc_kv_json: kv } });
```
18. **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.
The reference doc also includes a short archive list for older, less-relevant historical incidents.
---
@@ -560,13 +337,23 @@ 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/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` |
| 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/PROJECT__AE_Events_PressMgmt_Config_Cleanup.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` |