From 90adb19f5dd7790e57780826fa7619391a45ac6c Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Thu, 30 Apr 2026 16:00:20 -0400 Subject: [PATCH] =?UTF-8?q?fix(core):=20modern=20Svelte=205=20cleanup=20?= =?UTF-8?q?=E2=80=94=20Dexie=20.get()=20bug,=20typed=20API=20calls,=20inli?= =?UTF-8?q?ne=20confirms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - person_view.svelte: fix liveQuery using .get() (primary key, never set by V3) → .where('person_id').equals().first() - people/[person_id]: same Dexie .get() fix for lq__person_obj - person_view.svelte: replace 4x generic api.update_ae_obj → core_func.update_ae_obj__person (removes unused api import) - Replace all browser confirm()/alert() dialogs (9 occurrences, 6 files) with inline two-click confirm state pattern (confirm_action = $state) Affected: users, accounts, contacts, addresses, people, sites - Bootstrap doc: add Dexie .get() trap to Section 5 and Mistake #8 --- .../BOOTSTRAP__AI_Agent_Quickstart.md | 29 ++++- .../core/accounts/[account_id]/+page.svelte | 45 ++++++-- .../core/addresses/[address_id]/+page.svelte | 36 +++++-- .../core/contacts/[contact_id]/+page.svelte | 36 +++++-- .../core/people/[person_id]/+page.svelte | 100 +++++++++++++----- src/routes/core/person_view.svelte | 38 +++---- src/routes/core/sites/[site_id]/+page.svelte | 39 +++++-- src/routes/core/users/[user_id]/+page.svelte | 45 ++++++-- 8 files changed, 270 insertions(+), 98 deletions(-) diff --git a/documentation/BOOTSTRAP__AI_Agent_Quickstart.md b/documentation/BOOTSTRAP__AI_Agent_Quickstart.md index ebb3f8d8..8a82bd3d 100644 --- a/documentation/BOOTSTRAP__AI_Agent_Quickstart.md +++ b/documentation/BOOTSTRAP__AI_Agent_Quickstart.md @@ -179,7 +179,7 @@ async function load_ae_obj_id__my_obj({ api_cfg, obj_id }) { ### 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. Always use the short form: +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 @@ -187,6 +187,7 @@ 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 @@ -205,6 +206,23 @@ x-aether-api-key: x-account-id: ``` +### 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 @@ -253,7 +271,14 @@ These are real incidents — know them before you start. 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. **Treating `$effect` blocks as auth bypass risks** — a `$effect` inside a child +8. **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. + +9. **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 diff --git a/src/routes/core/accounts/[account_id]/+page.svelte b/src/routes/core/accounts/[account_id]/+page.svelte index a350e79e..cd222f41 100644 --- a/src/routes/core/accounts/[account_id]/+page.svelte +++ b/src/routes/core/accounts/[account_id]/+page.svelte @@ -22,6 +22,8 @@ let account_id = $derived($page.params.account_id ?? ''); let account: any = $state(null); let loading = $state(true); let saving = $state(false); +let save_success = $state(false); +let confirm_action = $state(null); async function load_account() { loading = true; @@ -59,13 +61,18 @@ async function handle_save() { }); if (result) { - alert('Account updated successfully'); + save_success = true; + setTimeout(() => (save_success = false), 2500); } saving = false; } async function handle_delete() { - if (!confirm('Are you sure you want to disable this account?')) return; + if (confirm_action !== 'disable_account') { + confirm_action = 'disable_account'; + return; + } + confirm_action = null; const result = await delete_ae_obj_id__account({ api_cfg: $ae_api, @@ -98,13 +105,33 @@ async function handle_delete() { -
- +
+ {#if confirm_action === 'disable_account'} + Disable this account? + + + {:else} + + {/if} + {#if save_success} + + Saved + + {/if}
-
+
- + {#if confirm_action === 'delete_address'} + Delete this address? + + + {:else} + + {/if}
diff --git a/src/routes/core/contacts/[contact_id]/+page.svelte b/src/routes/core/contacts/[contact_id]/+page.svelte index 38587d5b..76907bb7 100644 --- a/src/routes/core/contacts/[contact_id]/+page.svelte +++ b/src/routes/core/contacts/[contact_id]/+page.svelte @@ -32,6 +32,7 @@ let contact_id = $derived($page.params.contact_id ?? ''); let contact: any = $state(null); let loading = $state(true); let is_editing = $state(false); +let confirm_action = $state(null); async function load_data() { loading = true; @@ -52,7 +53,11 @@ onMount(() => { }); async function handle_delete() { - if (!confirm('Permanently delete this contact?')) return; + if (confirm_action !== 'delete_contact') { + confirm_action = 'delete_contact'; + return; + } + confirm_action = null; await delete_ae_obj_id__contact({ api_cfg: $ae_api, contact_id, @@ -87,7 +92,7 @@ async function handle_delete() {
-
+
- + {#if confirm_action === 'delete_contact'} + Delete this contact? + + + {:else} + + {/if}
diff --git a/src/routes/core/people/[person_id]/+page.svelte b/src/routes/core/people/[person_id]/+page.svelte index b425ec86..96a966c7 100644 --- a/src/routes/core/people/[person_id]/+page.svelte +++ b/src/routes/core/people/[person_id]/+page.svelte @@ -70,8 +70,10 @@ $effect(() => { }); let is_editing = $state(false); +let confirm_action = $state(null); -let lq__person_obj = liveQuery(() => db_core.person.get($slct.person_id)); +// .get() uses the primary key (id), which V3 never populates — always use .where().equals().first() +let lq__person_obj = liveQuery(() => db_core.person.where('person_id').equals($slct.person_id ?? '').first()); $slct.lq__person_obj = lq__person_obj; let available_users: any[] = $state([]); @@ -131,7 +133,11 @@ async function load_unlinked_users() { } async function handle_link_user(user_id: string) { - if (!confirm('Link this person to this user account?')) return; + if (confirm_action !== `link_user_${user_id}`) { + confirm_action = `link_user_${user_id}`; + return; + } + confirm_action = null; const result = await update_ae_obj__person({ api_cfg: $ae_api, @@ -147,7 +153,11 @@ async function handle_link_user(user_id: string) { } async function handle_unlink_user() { - if (!confirm('Unlink this person from their user account?')) return; + if (confirm_action !== 'unlink_user') { + confirm_action = 'unlink_user'; + return; + } + confirm_action = null; const result = await update_ae_obj__person({ api_cfg: $ae_api, @@ -268,11 +278,25 @@ $ae_loc.person.show_content__person_page_help = false; User Account Linking
{#if $lq__person_obj?.user_id} - + {#if confirm_action === 'unlink_user'} + Unlink this user? + + + {:else} + + {/if} {:else} + + - + {:else} + + {/if} {/each} {/if} diff --git a/src/routes/core/person_view.svelte b/src/routes/core/person_view.svelte index 51fd5994..ea82000c 100644 --- a/src/routes/core/person_view.svelte +++ b/src/routes/core/person_view.svelte @@ -14,8 +14,6 @@ import { liveQuery } from 'dexie'; import type { key_val } from '$lib/stores/ae_stores'; import { ae_util } from '$lib/ae_utils/ae_utils'; import Element_ae_obj_field_editor from '$lib/elements/element_ae_obj_field_editor.svelte'; -import { api } from '$lib/api/api'; - import { core_func } from '$lib/ae_core/ae_core_functions'; import { ae_snip, @@ -62,11 +60,9 @@ $effect(() => { } }); +// .get() uses the primary key (id), which V3 never populates — always use .where().equals().first() let lq__person_obj = $derived( - liveQuery(async () => { - let results = await db_core.person.get(person_id); - return results; - }) + liveQuery(() => db_core.person.where('person_id').equals(person_id).first()) ); ae_tmp.value__data_json = null; @@ -304,11 +300,10 @@ $effect(() => { - + {#if confirm_action === `delete_domain_${dom.site_domain_id}`} + Remove {dom.fqdn}? + + + {:else} + + {/if} {/each} diff --git a/src/routes/core/users/[user_id]/+page.svelte b/src/routes/core/users/[user_id]/+page.svelte index 1c4925cb..3b0422c8 100644 --- a/src/routes/core/users/[user_id]/+page.svelte +++ b/src/routes/core/users/[user_id]/+page.svelte @@ -30,6 +30,8 @@ interface Props { let { data }: Props = $props(); let user = $state(untrack(() => data.user)); let saving = $state(false); +let save_success = $state(false); +let confirm_action = $state(null); $effect(() => { user = data.user; @@ -58,13 +60,18 @@ async function handle_save() { }); if (result) { - alert('User updated successfully'); + save_success = true; + setTimeout(() => (save_success = false), 2500); } saving = false; } async function handle_delete() { - if (!confirm('Are you sure you want to disable this user account?')) return; + if (confirm_action !== 'disable_user') { + confirm_action = 'disable_user'; + return; + } + confirm_action = null; const result = await delete_ae_obj_id__user({ api_cfg: $ae_api, @@ -104,13 +111,33 @@ async function handle_delete() { -
- +
+ {#if confirm_action === 'disable_user'} + Disable this user? + + + {:else} + + {/if} + {#if save_success} + + Saved + + {/if}