Records the root cause of the 2026-03-27 hidden-error discovery (broken ambient
declaration masking 31 pre-existing svelte-check errors), the lesson learned, and
two follow-up tasks: fix ModalProps.children across 26 files, remove shadcn-svelte
and bits-ui from package.json.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- lucide-augment.d.ts: add `aria-hidden?: string | boolean` to IconProps
(SVGAttributes drops this too in @lucide/svelte ≥ 0.577.0)
- Remove src/lib/components/ui/ — ShadCN primitives with zero importers;
bits-ui API drift was generating ~20 type errors for dead code
svelte-check: 31 errors remaining (all ModalProps.children — flowbite-svelte
API change, deferred to next session), 0 warnings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
app.d.ts is a script-context declaration file. A `declare module 'x' {}`
in a script file is an ambient module declaration that completely replaces
the package's types — not an augmentation. This caused svelte-check to see
@lucide/svelte as exporting only IconProps, producing 1131 "class" errors
and 237 "no exported member" errors for every icon import.
Moving the augmentation to src/lucide-augment.d.ts with `export {}` makes
it a module file, so `declare module` becomes a proper augmentation that
merges with the package types. Result: Lucide errors drop from 1368 to 0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@lucide/svelte >=0.577.0 dropped `class` from IconProps — it now derives
props purely from SVGAttributes<SVGSVGElement>, which TS types without
`class`. Every <SomeIcon class="..." /> in the codebase errored (1131
errors). Augment IconProps in app.d.ts to re-add `class?: string`.
Root cause: 0.561.0 → 0.577.0 bump in commit 366c6629 (2026-03-10).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full implementation of ae_comp__exhibit_payment.svelte (was a 9-line stub).
Reads Stripe config from $ae_loc.site_cfg_json per-event. License tier
selector (1/3/6/10 users) uses {#key} remount pattern to work around
stripe-buy-button web component ignoring attribute changes after mount.
Three states: paid confirmation (priority=true), not-configured hint, payment
form. client_reference_id=exhibit_id ties payments to booth records.
TypeScript declaration for stripe-buy-button added to app.d.ts via
svelte/elements augmentation. exhibit_id prop wired in +page.svelte and
ae_tab__manage.svelte.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
$ae_loc.trusted_access is only ever upgraded, never downgraded — it sticks
across Novi impersonation even though a different UUID is in the URL. Instead,
check user_id directly against $idaa_loc.novi_admin_li / novi_trusted_li so
the moderator grant is tied to the specific UUID being used, not the inherited
session access level.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
<svelte:head> scripts load asynchronously with no lifecycle hook to await
completion, so onMount could call init_jitsi() before JitsiMeetExternalAPI
was defined. Replace with a dynamic script loader that is awaited between
fetch_novi_data() and init_jitsi(). Also uses the domain from URL params
rather than the hardcoded jitsi.dgrzone.com hostname.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rather than hardcoding the IDAA admins group UUID or making an extra
API call, re-use the access level already established by the IDAA layout.
If $ae_loc.trusted_access is set (verified against novi_trusted_li /
novi_admin_li), the user is a moderator immediately. Only regular
authenticated members fall through to the group membership check.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Older Novi pages that haven't been updated to pass g_uuid still need
the moderator check to work. Use [g_uuid] when present, otherwise fall
back to novi_idaa_group_guid_li from site config.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of checking membership across all groups in novi_idaa_group_guid_li
(site config), pass the single g_uuid from the URL param. Each Novi iframe
page supplies the group relevant to that specific meeting, so checking just
that one group is both more precise and avoids unnecessary Novi API calls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The {#if} gate only allowed the sys bar to mount for admins or
trusted+edit_mode users in an iframe. Trusted staff using show_menu=true
had sys_menu.hide set correctly but the component never mounted. Add
!sys_menu.hide as an escape hatch so the URL override actually works.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
20 calls/sec, 600/min, 100k/day. Notes the 10s flat backoff + single retry
and the 5-min TTL cache that prevents normal-use rate limiting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On a 429 response, waits 10 seconds then retries once. If the retry also
returns 429, throws and denies access (Reload/Retry button covers that case).
verify_in_flight and novi_verifying stay true during the wait so the spinner
remains visible and no concurrent calls can sneak in.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UUID is set by Novi via iframe src at page load and never changes within a
session (impersonation = full iframe reload). Reading it once from
window.location.search eliminates reactive noise from SvelteKit client-side
navigation causing spurious re-verification runs.
Removed:
- verify_dep $derived.by (reactive UUID + site_cfg narrowing)
- dedupe snapshot + last_effect_* tracking variables
- verify_backoff_attempts and exponential backoff retry logic
- novi_rate_limited_until writes and UUID-change guards
- ~80 lines of complexity
Kept:
- site_cfg_json read outside untrack (effect still re-runs when API key loads async)
- verify_in_flight concurrency guard
- TTL cache (prevents duplicate calls on SWR site_cfg updates)
- All permission upgrade and store write logic
NOTE: If Novi adds dynamic impersonation (no full reload), see comment at
url_uuid declaration for what to restore.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Critical bugs fixed:
- $derived(() => {}) stored the function itself; uuid/api_key were always
undefined so verification never fired. Fixed to $derived.by(() => {}).
- novi_verifying pre-initialized to true (flash prevention) was also used as
the concurrency guard — guard saw it as in-flight and exited immediately,
leaving the spinner stuck forever. Split into separate verify_in_flight flag.
- $idaa_loc reads in dedupe snapshot (outside untrack) subscribed the effect
to idaa_loc writes, causing needless re-runs post-verification.
- Rate limit was not UUID-aware: 429 on one UUID blocked impersonation
(new UUID). TTL and rate-limit guards now both bypass when UUID changes.
Also includes: store defaults for novi_verified_ts + novi_rate_limited_until,
docs update, iframe template g_uuid param (prior agent changes).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two-step creation: POST event_person first, then event_badge linked to it.
Badge create route (event_person parent) pending backend fix — frontend is
ready and passing event_person_id + event_badge_template_id in payload.
- ae_events__event_person.ts: new create function (nested under event)
- ae_events_functions.ts: export create_ae_obj__event_person
- ae_comp__badge_create_form.svelte: modal form with live name preview,
conditional display-name override, template selector (auto-selects when
only one template), badge_type_code_li derived from selected template's
badge_type_list JSON, two-step submit status labels
- +page.svelte: load template list via liveQuery, wire Create Badge button
(edit_mode only), native <dialog> modal with backdrop, remote-first
refresh on success
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace stale deploy:staging/deploy:prod references with current
build:docker:*, deploy:remote:*, and .env.dev/test/prod file names.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- deploy:dev/test/prod → build:docker:dev/test/prod to distinguish
local Docker builds from remote server deploys
- Add deploy:remote:test and deploy:remote:prod — SSH to linode.oneskyit.com
and run deploy.sh on the server
- Trailing whitespace cleanup in .env.*.default files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The staging default template had real OSIT account_id and event_id values
in inline comments. These are not secrets but shouldn't be in a committed
template — they'd be misleading on any non-OSIT deployment.
Replaced with plain XXXX placeholders.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added section 7 covering /v3/action/user/ endpoints: authenticate, verify_password,
change_password, new_auth_key, email_auth_key_url — including the legacy→V3
migration table and auth key one-time-use behavior.
Also clarified the response shape note to explicitly list all response types
(GET single, GET list, POST create, PATCH, search) that use the V3 envelope.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added mode, credentials, redirect, and cache options to the GET fetchOptions
object. These were previously left to browser defaults, which vary by environment
and can produce opaque CORS failures that are hard to diagnose. Being explicit
avoids environment-dependent surprises.
Also added a try/catch around response.headers logging (log_lvl >= 1) so header
dumps don't throw in environments that restrict header access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
get_object() returns false on network failure; the .then() handler was
running with result=false and accessing result.hosted_file_id (evaluates
to undefined, valid JS key, no throw) so all success state was set even
though the request failed.
- Guard result in .then(): if !result.hosted_file_id → set status='error'
- Add 'Failed — Retry?' button state in error branch
- Raise client-side AbortController timeout 300s → 1800s (30 min)
- Add comment explaining root cause (get_object returns false, not throw)
Root cause of the connection drop is proxy_send_timeout or NAT hairpin
timeout (both default 60s) — not a frontend issue; tracked separately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tests/README.md — new "IDAA Auth Tests" section with three lessons:
1. ae_idaa_loc seed must include full bb/archives structure or
verify_novi_uuid() throws silently and resets novi_uuid to null
2. StorageEvent pattern for testing reactive persisted-store updates
without pre-seeding Dexie or navigating twice
3. getByText { exact: false } for UUID in multi-field spans
GUIDE__SvelteKit2_Svelte5_DexieJS.md — new "untrack() reactive tracking
trap" section: reading a store value inside untrack() makes it a one-shot
dependency; fix is to hoist the read outside untrack() and add a guard
to avoid redundant work on unrelated store updates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers 5 scenarios with extensive inline comments explaining business
context and the 2026-03-25 stale-cache root-cause fix:
1. Auth gate (Sev-1 regression guard) — no UUID → Access Denied
2. Happy path — valid UUID + fresh cfg → access granted
3. Invalid UUID — Novi 404 → Access Denied
4. Stale cache — StorageEvent delivers fresh site_cfg_json →
Effect 2 retries verification without reload (tests the reactive
tracking fix in (idaa)/+layout.svelte)
5. iframe mode — Reload/Retry button visible on Access Denied
Key lesson found while writing: ae_idaa_loc seed must include the full
bb object or verify_novi_uuid() throws on bb.qry__hidden assignment,
caught silently, resetting novi_uuid to null even after a successful
Novi API call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WHY: Novi UUID verification is async — on first iframe load the API call
may not complete before the access gate renders, leaving the user stuck on
Access Denied with no way to retry without manually reloading the host page.
The Reload/Retry button calls location.reload() to re-trigger verification.
Only shown in iframe mode where the timing race is the known failure path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The legacy /hosted_file/{id}/clip_video route was decommissioned with the
rest of the hosted_file router. Updated to /v3/action/hosted_file/{id}/clip_video.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The legacy /hosted_file/upload_files router was decommissioned (commented
out in registry.py). Both upload components now point to the active V3
endpoint at /v3/action/hosted_file/upload. Response shape is identical
so no consumer-side changes needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Replace incorrect untrack() with idempotent write guard in the
sys_menu trusted-access effect. untrack() prevents new dep reads but
ae_loc was already tracked from the outer condition reads, so the write
still re-notified the effect every run. The guard (only write if value
!= false) breaks the cycle: run 2 finds value already false, skips the
write, effect stops. Max 2 runs vs the previous infinite loop.
2. Hide auth shield, font-size cycler, and dark/light toggle in the sys
bar when in iframe mode — host page owns those concerns. Edit mode
toggle and the main expand button remain visible for staff.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes in the IDAA root layout:
1. Add missing `untrack` import and wrap `$ae_loc.sys_menu.hide = false`
in `untrack()` inside the trusted-access effect. Without this, reading
$ae_loc.iframe/$ae_loc.trusted_access and then writing back to $ae_loc
caused an infinite reactive loop → effect_update_depth_exceeded error.
Only hit by trusted/admin users in iframe mode (regular Novi members
at authenticated_access were unaffected).
2. When iframe URL param is explicitly set to 'false', restore
$ae_loc.sys_menu.hide = false. The root layout sets it to true on
iframe=true but never resets it, leaving the system bar permanently
hidden after leaving iframe mode.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Trusted admins embedded in the Novi iframe can't append show_menu=true
to the src URL, so watch trusted_access reactively and unhide the sys
bar automatically when they authenticate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>