18 Commits

Author SHA1 Message Date
Scott Idem
b7969bc46e Making subtle changes based on actual usage 2026-06-02 17:05:34 -04:00
Scott Idem
99b8eb0b5e fix(badges): focus search input when Search is triggered with too-short query
Pressing the Search button or Enter with fewer than the required min chars
now focuses the input field instead of silently doing nothing, so the user
gets clear feedback about where to type.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 16:55:38 -04:00
Scott Idem
9e361eae9b fix(badges): improve empty-state search hint to reflect actual search fields
Was: "Enter your name above to find your badge."
Now: "Search by name, email, or organization above to find your badge."

Mirrors the actual fast-path fields (given/family name, email, default_qry_str)
and the search input placeholder text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 16:52:35 -04:00
Scott Idem
e735d0c213 refactor(badges): remove redundant loading gate; fix BarChart2 deprecation
The +page.svelte {#if search_status=loading && ids.length===0} gate is now
redundant — ae_comp__badge_obj_li handles all states internally (Searching...,
Enter your name, No results). Removing it also fixes a UX regression where
trusted-user IDB fallback results would be hidden by the gate whenever a new
API search fired. Component is now always mounted and manages its own state.

Also replaces deprecated BarChart2 with ChartColumnBig (lucide rename).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 16:48:44 -04:00
Scott Idem
d05cc63459 fix(badges): remove transition from initial loader to fix double-DOM bounce
transition:fade on the initial spinner caused Svelte to keep the outgoing
element in the DOM for the full 200ms outro while the incoming badge list
was already rendered — both were live simultaneously, colliding on height
and producing the visible bounce. Initial cold-start load doesn't need a
transition; instant swap is fine.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 16:44:35 -04:00
Scott Idem
ac17417f3c fix(badges): fix search-result layout shift + unify empty/loading states
Scrollbar shift:
- Add [scrollbar-gutter:stable] to #ae_main_content in events layout so a
  scrollbar appearing on first results load no longer reflows the centered
  search form (was shifting ~8px left on Linux)

Empty/loading state consistency:
- Move search_status prop into ae_comp__badge_obj_li so it can swap its own
  empty state: spinner + "Searching..." while a search is in progress,
  UserSearch icon + prompt text otherwise
- Unify p-16 / size-3em / mb-2 / text-xl across all three states (initial
  load, searching, no results) so height never jumps between transitions
- Pass search_status from +page.svelte to the component

Transitions:
- transition:fade on initial-load spinner div
- transition:slide on Create/Upload badge button row (appears with edit mode)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 16:38:30 -04:00
Scott Idem
3773758eb5 feat(badges): smooth transitions + polish for badge search UI
- Adds fade/slide transitions throughout the search form: form mount/unmount,
  filter row, QR scan button, QR scanner panel, Show Hidden, Remote First labels
- Min-chars hint switches from class:invisible to opacity-0/opacity-50 +
  transition-opacity so it fades instead of snapping
- Clear button switches from class:hidden to opacity-0 + pointer-events-none
  + transition-all so it fades without causing layout shifts
- "Start Here" button gets transition-opacity for smooth dim on first keystroke
- Replaces FileSearch with UserSearch icon in the empty state
- Adds w-full to empty state div to prevent subtle page-width shift between
  no-results and results states

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 16:25:31 -04:00
Scott Idem
60bdd2fdba feat(badges): public_access kiosk mode + manager access improvements
- Public (attendee) kiosk: unprinted badges link to /review; printed
  badges show green "Checked in · Nx · First/Last" row (non-clickable)
- Public attendees no longer see Print button (staff-only action)
- Printed badges sort to end of list for public non-trusted users
- Manager access: Print (reprint) and Email Link buttons always visible
  without requiring Edit Mode; main row behavior unchanged
- Empty state wording: context-aware — "Enter your name above to find
  your badge" for public users with no query vs "No badges found" after
  an actual search
- Docs: Epson C3500 fanfold section filled in (was empty placeholder);
  style_href/duplex implementation status corrected in badge templates
  doc; Axonius C3500 layout TODO marked complete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 16:15:08 -04:00
Scott Idem
72c8f9b502 docs(todo): archive June snapshot and streamline active task list 2026-06-02 15:00:24 -04:00
Scott Idem
1de87b6c5f chore(cleanup): add journal AI shortcut and align posts tmp_sort 2026-06-02 14:43:16 -04:00
Scott Idem
84a9d0fffc chore(core): remove retired site_domain helper and update docs 2026-06-02 14:34:19 -04:00
Scott Idem
87084f0f71 chore: migrate lucide package and close quick TODO cleanups 2026-06-02 14:19:12 -04:00
Scott Idem
de048a084b chore: remove axios + deprecated electron_native.js; track lucide-svelte migration
- Uninstall axios — only consumer was electron_native.js (legacy V2 file)
- Delete electron_native.js — deprecated 2026-02-10, no active imports; replaced
  by aether_app_native_electron repo + electron_relay.ts contextBridge
- Add TODO: migrate remaining lucide-svelte → @lucide/svelte (5 files) then uninstall

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 14:04:28 -04:00
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
Scott Idem
a5243fa820 docs: document bootstrap account_id race condition and liveQuery stale-record pattern
Adds entry #14 to BOOTSTRAP__AI_Agent_Quickstart.md (section 7 "Mistakes
Agents Have Made") and a new "Bootstrap Race" subsection to
GUIDE__SvelteKit2_Svelte5_DexieJS.md ("Common Gotchas"), capturing the
fix from 5fce14980: gate account-scoped liveQuery triggers on
$slct.account_id (non-persisted), not $ae_loc.account_id (persisted,
potentially stale), and treat IDB records from a different non-null
account as a cache miss. Also fixes five pre-existing MD049 emphasis
style warnings (asterisk → underscore) in the Dexie guide.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 13:39:11 -04:00
Scott Idem
5fce149808 fix(element_data_store): fix stale account_id showing wrong record on fresh load
Two guards added to the trigger effect:

1. Gate on $slct.account_id being set — prevents the fetch from firing before
   the bootstrap Sync Effect has propagated the real account_id. Without this,
   get_object's localStorage scavenge read a stale account_id (e.g. 1 from a
   prior dev/demo session) and the API returned the wrong account's record.

2. Stale-account detection — if liveQuery returns an IDB row with a non-null
   account_id that doesn't match the current account, treat it as a cache miss
   and fetch the correct record. Null (global/default) rows are still accepted.

Root cause: ae_loc is a persisted store that hydrates from localStorage before
the bootstrap Sync Effect runs. Old account-specific IDB rows scored highest in
the liveQuery sort, suppressing the trigger and leaving the wrong record visible
until the next full page refresh.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 12:04:59 -04:00
Scott Idem
a74effa6ff feat(badges): add Cvent Splash XLSX import mode; fix server-side upload timeout
- Add 'Cvent Splash XLSX (registrant export)' upload mode hitting the new
  /event/{id}/badge/import/splash_xlsx endpoint
- Admin controls: begin_at/end_at/return_detail (shared with Zoom mode) +
  import_status_filter (splash only, default 'Attending')
- File picker accept attribute switches between .csv and .xlsx per mode
- Set timeout=300000 and retry_count=1 on both server-side upload paths to
  prevent false 'no response' failures on slow imports; upsert-by-email on
  the backend makes retries safe but a single attempt is sufficient
- Replace misleading 0/0 progress bar with an indeterminate progress bar
  during server-side processing; real counter kept for client-side CSV mode
- Show 'Processing on server…' message once upload completes and server work begins

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 11:37:48 -04:00
Scott Idem
33d48e7e78 Doc updates and project (sort of) temp files 2026-06-02 09:25:03 -04:00
35 changed files with 727 additions and 2359 deletions

View File

@@ -0,0 +1,33 @@
# Plan: Fix IDAA Jitsi Breakout Links
IDAA Jitsi meetings are embedded in an iframe on the `idaa.org` website. To allow members to "break out" of the iframe (for a better experience on mobile or to use full-tab features), the app provides an "Open Meeting Externally" link.
Currently, this link is generated from `$page.url.href`, which often lacks the `key` (site access key) and `uuid` (Novi identity token) required for Aether's authentication gate, especially if the user has navigated internally within SvelteKit.
## 1. Objective
Ensure all "Breakout" and "Copy Link" actions on the IDAA Video Conferences page include the necessary `key` and `uuid` parameters.
## 2. Implementation Steps
### Step 1: Update `src/routes/idaa/(idaa)/video_conferences/+page.svelte`
- Create a reactive `breakout_url` derived from `$page.url.href`.
- In the derivation logic:
- Instantiate a `new URL`.
- Check if `key` is present in `searchParams`. If missing, pull from `$ae_loc.allow_access` or `$ae_loc.site_access_key`.
- Check if `uuid` is present in `searchParams`. If missing, pull from `$idaa_loc.novi_uuid`.
- Return the resulting `href`.
- Update the following UI elements to use `breakout_url`:
- `copy_meeting_link` function (uses `navigator.clipboard.writeText`).
- "Open in New Tab" anchor tag (`href`).
- "Copy Link" fallback textarea (`value`).
- "Copy Break-out Link" in the Jitsi Tools panel (`value`).
### Step 2: Update `documentation/CLIENT__IDAA_and_customized_mods.md`
- Add a note in the "Authentication: Novi UUID System" or "Iframe Integration" section about the requirement for `key` and `uuid` in breakout links.
- Document that the frontend now automatically re-injects these for external links to ensure persistent session access outside the iframe.
## 3. Verification
- Manually verify the logic:
- If `key` and `uuid` are already in the URL (e.g., initial load), the derived URL should remain unchanged (or correctly deduplicated).
- If they are missing (e.g., after navigating from another IDAA page), they should be added to the generated link.
- Run `npx svelte-check` to ensure no syntax regressions.

View File

@@ -0,0 +1,53 @@
# Plan: Launcher Config UX Refinement (Cohesion & Stability)
The goal of this plan is to address the visual "bouncing", layering overload, and the misplaced close button in the new Launcher configuration modal.
## 1. Dimensional Stability
- **Problem:** Switching tabs causes the modal to resize vertically and horizontally, leading to a "bouncing" feel.
- **Solution:**
- Set a fixed height for the `Launcher_cfg` container (e.g., `h-[750px]`).
- Use `overflow-y-auto` only for the right-hand content pane.
- Ensure the sidebar has a stable width.
## 2. Visual Hierarchy & Layering
- **Problem:** Too many nested backgrounds (Page > Launcher > Modal > Inner Pane > Section Pane > Section Content).
- **Solution:**
- Flatten the background of the main content pane.
- Simplify `Launcher_Cfg_Section.svelte`:
- Remove `shadow-xl` from individual sections.
- Use subtler borders instead of strong "preset-outlined" colors.
- Remove the secondary background (`bg-white/5`) from the section content area.
- Standardize on a single, clean surface color for the right-hand pane.
## 3. The "Centered Close Button" Bug
- **Problem:** A close button is appearing in the middle of the screen.
- **Investigation:**
- Check for absolute-positioned elements in `Launcher_cfg.svelte` or `+layout.svelte`.
- Verify if Flowbite's `Modal` default close button is clashing with internal buttons.
- **Solution:**
- Consolidate all "Close" actions.
- Use the Modal's built-in top-right close button (if available) or a single, well-positioned button in the sidebar.
## 4. Implementation Steps
### Step 1: Update `Launcher_cfg.svelte`
- Set stable dimensions: `h-[750px] max-h-[90vh] w-[1000px] max-w-[95vw]`.
- Remove internal shadows and borders that conflict with the Modal container.
- Clean up the sidebar "Close" button.
### Step 2: Update `Launcher_cfg_section.svelte`
- Simplify the styling to reduce visual clutter.
- Remove `shadow-xl`.
- Use consistent padding and margins.
### Step 3: Update `+layout.svelte`
- Ensure the `Modal` is configured for a stable, large view without default padding issues.
- Verify the `modal_cfg_open` logic.
### Step 4: Add `Launcher_cfg_field.svelte` (Helper)
- Implement a unified field helper to standardize Label/Description/Input layouts across all tabs.
## 5. Verification
- Toggle between all 7 tabs. Verify zero layout shift (height/width remains constant).
- Check appearance in Light and Dark modes.
- Verify "Technical Mode" transitions.

View File

@@ -397,6 +397,69 @@ These are real incidents — know them before you start.
- `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)

View File

@@ -157,26 +157,58 @@ This layout hides `.badge_back` in `@media print` — only the front face prints
---
### Epson — Fan-Fold / Label Printer
### Epson ColorWorks C3500 — Fan-Fold Label Printer
**Status:** Not yet tested. Section to be filled in after testing.
**Card stock:** 4" × 6" fan-fold paper label stock
**Layout code:** `badge_4x6_fanfold`
**Status:** Configured. First live use: Axonius Adapt DC — June 9, 2026.
Common Epson models used for fan-fold name badge stock: TM-T88 series, C3500, LX series.
Fan-fold stock is typically 4" × 3" or 4" × 6" paper labels.
The C3500 is a color inkjet label printer — it prints continuous fan-fold paper stock,
not individual cards. Badges are separated along the perforation after printing.
#### Physical Setup
- Connect via USB or Ethernet
- Load 4" × 6" fan-fold stock per Epson instructions
- The C3500 is single-sided — only the front face prints. Back section is suppressed in CSS.
- The badge has a lanyard hole punch: 5/8" × 1/8", centered, 1/4" from the top.
Most fan-fold stock for badge use includes a pre-punched lanyard slot — verify stock matches.
#### Driver
- Epson ColorWorks C3500 CUPS driver available from epson.com (ColorWorks section)
- On Linux/CUPS: install the provided PPD and add the printer at `http://localhost:631`
- Set default paper size to **4" × 6"** in CUPS
- Print a test page from CUPS before going live
#### Chrome Print Settings (C3500)
| Setting | Value |
|---|---|
| Destination | Epson C3500 (CUPS name) |
| Paper size | 4 × 6 in (set in CUPS driver) |
| Margins | **None** |
| Background graphics | On |
| Pages | 1 (single-sided) |
#### CSS Layout
Fan-fold badges would use a layout sized to the specific label stock.
A new CSS layout file will need to be created per stock size if not already present.
Naming convention: `badge_layout_epson_[model]_[size].css`
The C3500 uses the `badge_4x6_fanfold` layout. CSS file:
`src/lib/ae_events/badges/css/badge_layout_epson_4x6_fanfold.css`
#### Setup Notes
*(To be filled in after testing — cover: driver source, CUPS setup, paper size, Chrome settings)*
Created 2026-05-15 for Axonius Adapt DC. Key specs:
- `badge_front` 4" × 6", portrait orientation
- `badge_header` max-height 1.5in
- Lanyard hole: 5/8" × 1/8", centered, 1/4" from top
- `@page { size: 4in 6in; margin: 0; }` set in the print page dynamically
- `.badge_back` suppressed in `@media print` (single-sided)
#### Known Behaviors
*(To be filled in after testing)*
- Same Chrome margin rules apply: **Margins → None** prevents URL/date header clipping
- Firefox honors `@page { size: 4in 6in }` for PDF proofing — use it to verify layout
- Fan-fold stock separates along the perforation — no cutting needed, but verify the
perforation lands outside the badge content area
---

View File

@@ -18,6 +18,14 @@ server needed.
- **Stock:** 3.5" × 5.5" PVC cards.
- **Orientation:** Cards face-up, landscape in the hopper.
- **Single-Sided:** Only the front face prints; the back section is hidden via CSS.
- **Layout code:** `badge_3.5x5.5_pvc`
### Printer Reference: Epson ColorWorks C3500 (Fan-Fold)
- **Stock:** 4" × 6" fan-fold paper label stock.
- **Single-Sided:** Only the front face prints; the back section is hidden via CSS.
- **Layout code:** `badge_4x6_fanfold`
- **Lanyard hole:** Pre-punched 5/8" × 1/8" slot at top center — verify stock matches.
- **First live use:** Axonius Adapt DC, June 9, 2026.
### Printing Workflow
1. **Search:** Find the attendee by name or QR scan in the Badges module.

View File

@@ -25,13 +25,54 @@ let lq__obj = $derived(
- Cold start (IDB empty) + non-blocking API writes: If you mount a component before data is written to IDB, `liveQuery` may run against an empty DB. The API write will populate IDB later, but sometimes a chain of dependent queries (e.g., presentations -> presenters) won't all rerun in the order you expect. The symptoms you described — session shows after one refresh, presenters only after a second — are consistent with either (a) queries recreated in the wrong order or (b) dependent store values being set only after some subscriptions are already created.
### Bootstrap Race: Account-scoped Loads Before `account_id` Is Set (2026-06)
Account-scoped `liveQuery` triggers can fire before `+layout.svelte`'s bootstrap Sync Effect
has propagated the real `account_id`. Two failure modes:
1. **IDB empty:** fetch runs with `account_id = null`. The `localStorage` scavenge in
`api_get_object.ts` reads the stale value from a previous session — possibly a different
account — and caches that wrong record into IDB.
2. **IDB has a stale record:** `liveQuery` returns a cached record from a different account as
a valid hit, so the trigger condition (`!entry`) is never true and the correct record is
never fetched.
**Rule:** Gate any trigger `$effect` that loads account-scoped data on `$slct.account_id`,
not `$ae_loc.account_id`. `$slct` is a plain writable store (not persisted), initialized to
`null` and set _only_ by the bootstrap Sync Effect. `$ae_loc` is a persisted store that
hydrates from `localStorage` before effects run and may carry a stale `account_id`.
Also treat a non-null, non-matching `account_id` in an IDB record as a cache miss:
```typescript
$effect(() => {
const account_id = $slct.account_id; // null until bootstrap Sync Effect runs
const api_ready = !!$ae_api?.base_url;
const entry = $lq__obj as SomeType | null | undefined;
if (!browser || !account_id || !api_ready) return;
// null account_id on a record = global/shared fallback — still a valid 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...';
}
});
```
See `BOOTSTRAP__AI_Agent_Quickstart.md` → Section 7, entry 14 for the full incident writeup.
### Critical Discovery (2026-02-26): The "try_cache: false" Bug
**Symptom:** Nested data (e.g., Session → Presentations → Presenters) requires multiple manual refreshes to display on cold-start, even when using blocking loads.
**Root Cause:** Two interconnected issues in nested data loaders:
1. **Disabled caching in nested loads**: Parent loads were passing `try_cache: false` to child loads, meaning presentations and presenters were fetched from API but **never written to IndexedDB**.
2. **Missing microtask yields**: Even when caching was enabled, components would mount and subscribe to liveQuery *before* IndexedDB writes completed, causing race conditions.
2. **Missing microtask yields**: Even when caching was enabled, components would mount and subscribe to liveQuery _before_ IndexedDB writes completed, causing race conditions.
**Example of the Bug:**
```typescript
@@ -114,14 +155,25 @@ obj.tmp_sort_3 = tmp_sort_3;
**Sort chain convention:** `group → priority DESC → sort ASC → [module-specific] → name`
**Priority encoding:** `priority ? '0' : '1'` — inverted so that `priority=true` sorts first in ascending order. This means **never use `.reverse()`** on a list sorted by `tmp_sort_*``.reverse()` would flip priority-true to sort last.
**Priority encoding:** `priority ? '0' : '1'` — inverted so that `priority=true` sorts first in ascending order. This means:
- **Dexie `.sortBy('tmp_sort_*')`** — always call without `.reverse()` before it (Dexie ignores collection-level `.reverse()` when using `.sortBy()`). If descending is needed for non-tmp_sort fields, call `.reverse()` on the resulting array after `await`.
- **JS `.sort()` comparators** — use **ascending** `a.localeCompare(b)`, NOT `b.localeCompare(a)`. Using descending flips the priority encoding and puts `priority=false` items first.
```ts
// ✅ Correct — ascending; priority=true ('0') sorts before priority=false ('1')
list.sort((a, b) => (a.tmp_sort_1 ?? '').localeCompare(b.tmp_sort_1 ?? ''));
// ❌ Wrong — descending inverts the encoding; priority=false ('1') sorts first
list.sort((a, b) => (b.tmp_sort_1 ?? '').localeCompare(a.tmp_sort_1 ?? ''));
```
**Modules using `build_tmp_sort`:**
- `ae_events__event_presentation.ts``tmp_sort_1/2`: group → priority → sort → start_datetime → code → name
- `ae_events__event.ts``tmp_sort_1/2/3`: group → priority → sort → name → updated_on (used by IDAA recovery meetings)
- `ae_journals__journal.ts``tmp_sort_1/2/3`: group → priority → sort → name → updated_on
- `ae_journals__journal_entry.ts` — same chain as journal
Remaining modules (sessions, presenters, locations, posts, core) scheduled for rollout; see `TODO__Agents.md`.
**Legacy encoding (not yet migrated to `build_tmp_sort`):** `ae_posts__post.ts`, `ae_posts__post_comment.ts`, `ae_archives__archive.ts`, `ae_archives__archive_content.ts`, `ae_sponsorships_functions.ts` use the opposite encoding (`priority ? '1' : '0'`, designed for descending sort). Their current route consumers sort by date/name so there is no visible priority bug today, but they must be migrated before any route starts sorting by `tmp_sort_*`. See `TODO__Agents.md`.
---
@@ -326,7 +378,7 @@ The `createLiveQueryStore` function creates a readable store that automatically
## SvelteKit Layout Hierarchy: Security and Execution Order
Understanding *when* SvelteKit code runs is critical for private-data modules like IDAA.
Understanding _when_ SvelteKit code runs is critical for private-data modules like IDAA.
### Execution order on any navigation
@@ -362,7 +414,7 @@ future readers into thinking the parent gate alone is not sufficient.
### Where the actual pre-gate risk lives: `+page.ts` / `+layout.ts`
Universal load functions run *before* components mount and *before* layout effects
Universal load functions run _before_ components mount and _before_ layout effects
execute. They also fire during SvelteKit link prefetch — triggered by the user
hovering a link, even if they never navigate. This makes them unsafe for private data:
@@ -446,7 +498,7 @@ If you must use non-blocking loads, you must pass the initial data to the compon
## The `untrack()` Reactive-Tracking Trap
`untrack()` is used inside `$effect` to read reactive values without registering them as tracked dependencies of that effect. This is correct for most "read-once" values (params, IDs) where you don't want the effect re-running on every change. But it has a silent failure mode: if a value you *need* to re-read is consumed inside `untrack()`, the effect becomes a one-shot and never retries when that value changes.
`untrack()` is used inside `$effect` to read reactive values without registering them as tracked dependencies of that effect. This is correct for most "read-once" values (params, IDs) where you don't want the effect re-running on every change. But it has a silent failure mode: if a value you _need_ to re-read is consumed inside `untrack()`, the effect becomes a one-shot and never retries when that value changes.
### Symptom
@@ -510,7 +562,7 @@ Before wrapping a store read in `untrack()`, ask: **"Do I need this effect to re
Svelte 5's `bind:` directive is more restrictive than previous versions. You can only bind to a simple **Identifier** or **MemberExpression**.
**❌ Invalid Pattern (Causes Compile Error):**
Attempting to normalize a value *inside* the binding will fail.
Attempting to normalize a value _inside_ the binding will fail.
```svelte
<!-- Error: Can only bind to an Identifier or MemberExpression -->
<Launcher_menu bind:slct__event_session_id={$events_slct.event_session_id || null} />

View File

@@ -114,17 +114,19 @@ corresponding `ticket_N_text` on the template provides the HTML rendered on the
| `priority`, `sort`, `group` | int/str | Standard AE sort fields |
| `notes` | str | Internal notes |
### New Field (pending backend addition)
### Duplex / Single-Sided
| Field | Type | Notes |
|---|---|---|
| `duplex` | bool | **Planned** — when `false`, back section is hidden from print (`@media print`) |
| `duplex` | bool | When `false`, back section is hidden from print (`@media print`) |
The `duplex` field controls whether the back-of-badge section renders during printing.
When `false` (single-sided), `badge_back` gets `print:hidden` applied so only the front
prints. The back section still displays on screen for configuration reference.
The first event using this system (Axonius, NYC, mid-April 2026) uses single-sided PVC
cards on a Zebra ZC10L — `duplex` will be `false` for that event's templates.
`duplex` is in `properties_to_save` and `show_badge_back` is derived from it in
`ae_comp__badge_obj_view.svelte`. (Verified 2026-03-18)
Axonius events use `duplex = false` — single-sided printing only.
---
@@ -155,7 +157,7 @@ The print page (`print/+page.svelte`) or the badge view should conditionally add
</svelte:head>
```
This is not yet implemented — tracked as a pending Phase 1 item.
This is implemented — `style_href` loads via `<svelte:head>` in `print/+page.svelte` and is included in `properties_to_save`. (Verified 2026-03-18)
### CSS Scope
@@ -218,7 +220,6 @@ The `properties_to_save` array in `ae_events__event_badge_template.ts` controls
gets cached locally. Current state — fields **NOT** in properties_to_save that exist
in DB and may be needed:
- `style_href` — needed once external CSS is wired via `<svelte:head>`
- `passcode` — not needed client-side
- `footer_title`, `footer_left`, `footer_right` — not needed (legacy)
- `header_background`, `footer_background` — not needed (legacy)

View File

@@ -35,8 +35,8 @@ Legacy patterns persisting in core logic and config modules.
- [ ] `src/lib/ae_sponsorships/ae_sponsorships_functions.ts`
- [x] `src/lib/ae_core/core__hosted_files.ts` (Migrated 2026-01-20)
- [x] `src/lib/ae_core/core__site.ts` (Migrated 2026-01-26)
- [ ] `src/lib/ae_core/core__site_domain.ts` (STILL USES `get_ae_obj_id_crud` for bootstrap)
- [x] `src/lib/ae_core/core__site.ts` (Migrated 2026-01-26; bootstrap path uses V3 `search_ae_obj` by `fqdn`)
- [x] `src/lib/ae_core/core__site_domain.ts` (Retired 2026-06-02; helper removed after bootstrap migration to `core__site.ts`)
- [ ] `src/lib/ae_core/ae_core_functions.ts` (STILL USES `get_ae_obj_id_crud` / `update_ae_obj_id_crud`)
- [ ] `src/lib/ae_core/core__country_subdivisions.ts`
- [ ] `src/lib/ae_core/core__time_zones.ts`

View File

@@ -1,41 +1,28 @@
# Frontend Agent Task List
> Use this file to track steps for complex features or bug fixes.
> **Status:** Stable — ongoing development.
> **Scope:** Active/open work only. Completed detail lives in archive files.
## 🔴 CMSC Charlotte — May 27 (Presentation Management)
**Drive down:** May 25 | **Setup:** May 26 morning | **Show:** May 27+
**Post-show hardening only**
- [x] **[Launcher] Composable open flow** — `handle_open_file()` uses `copy_from_cache_to_temp` +
`run_osascript` / `run_cmd` directly with per-step error handling. Complete.
- [x] **[Launcher] Slide control scripts in Svelte config** — AppleScript post_scripts live in
`ae_launcher__default_launch_profiles.ts`. VLC focus-stealing fix applied. Complete.
- [x] **[Launcher] Kill Apps button** — "Kill Apps" button added to Native OS config (System
Actions, edit mode only). Kills PowerPoint, Keynote, Adobe Acrobat Reader DC, VLC, soffice.
List overridable via `event_device.other_json.launcher.kill_process_li`. Auto-cleanup on file
open (deferred — manual button sufficient for CMSC).
- [ ] **[Launcher] End-to-end test on macOS** — test pptx and key opens on a real podium Mac.
- [ ] **[Launcher/Electron] Wallpaper stops applying after several changes (post-CMSC)** —
Append timestamp/random suffix to temp filename so macOS always sees a new path.
- [ ] **[Launcher/Electron] Wallpaper drift after display hotplug (post-CMSC)** —
Add resilient reconciliation loop or event-driven reapply on topology change.
- [ ] **[Launcher/Electron] Wallpaper reliability (post-CMSC)**
- [ ] Use timestamp/randomized temp filename so macOS always sees a new path.
- [ ] Add resilient reconciliation loop or event-driven reapply on display topology changes.
---
## 🔴 Axonius DC — June 9 (Badge Printing)
**Setup/Registration:** June 8 | **Show:** June 9
- [ ] **[Badges] Epson C3500 fanfold badge layout** — Create/configure a fanfold badge layout
compatible with the Epson C3500 continuous stock format.
- [x] **[Badges] Epson C3500 fanfold badge layout** — `badge_4x6_fanfold` layout CSS created,
wired, and documented. First live use: Axonius Adapt DC, June 9, 2026. (2026-05-15)
---
## 🚧 V3 CRUD Migration (Surgical Cleanup)
Finalizing the 100% adoption of V3 Standard endpoints and retirement of legacy wrappers.
- [x] **[Badges] Presenter Agreement Form** — migrated to `update_ae_obj` (2026-05-21)
- [ ] **[Core] Site Domain Bootstrap Refactor** — `load_ae_obj_by_fqdn__site_domain` in
`core__site_domain.ts` still uses legacy ID-lookup-by-FQDN. Refactor to use V3
`api.search_ae_obj` with fqdn filter per integration guide.
- [ ] **[Core] Legacy Utility Helpers** — Refactor `ae_core_functions.ts` to use V3 helpers.
- [ ] **[Cleanup] Delete Legacy Wrappers** — Once all callsites are migrated, remove
`src/lib/ae_api/api_get__crud_obj_id.ts` and the legacy exports from `api.ts`.
@@ -53,50 +40,18 @@ The app uses `svelte-persisted-store` (coarse reactivity). Migration target: rep
- [ ] **Phase C — Remaining persisted stores:** `ae_api`, `ae_events_stores`.
- [ ] **Phase D — Non-persisted writable stores:** `ae_sess`, `slct`, `ae_snip`, etc.
### [IDB Sort] `build_tmp_sort` rollout
Shared utility in `src/lib/ae_core/core__idb_sort.ts` — fixes priority direction (inverted,
true→'0' sorts first ASC) and zero-pads sort field (8 chars). No `.reverse()` needed.
Sort chain: `group → priority DESC → sort ASC → [module-specific fields] → name`.
**⚠️ Never use `.reverse()` on a `tmp_sort_*`-sorted list — inverted priority makes it wrong.**
Documented in `GUIDE__SvelteKit2_Svelte5_DexieJS.md` (IDB Sort section).
### [Data Layer] IDB sorting + content version rollout
Sorting baseline is now `build_tmp_sort` (ASC chain, no `.reverse()` on tmp-sort lists).
- [x] `ae_events__event_presentation` — group + priority + sort + start_datetime + code + name
- [x] `ae_journals__journal` + `ae_journals__journal_entry` — group + priority + sort + name + updated_on
- [ ] `ae_events__event_session` — roll out when sort behavior is reviewed
- [ ] `ae_events__event_presenter` — roll out when sort behavior is reviewed
- [ ] `ae_events__event_location` — roll out when sort behavior is reviewed
- [ ] `ae_posts__post` + `ae_posts__post_comment` — note: currently use 3-char padding; align to 8
- [ ] `ae_core__person` + `ae_core__account` — roll out when sort behavior is reviewed
### [Stores] IDB Content Version System
- [x] Write `check_and_clear_idb_tables()` helper.
- [x] Wire helper into `db_journals.ts` and IDAA layout.
- [ ] Roll out to `db_events.ts` (module-wide: session, presenter, badge, etc.).
- [ ] Roll out to `db_core.ts` (site_domain, person, user).
### [TypeScript] svelte-check hidden errors
- [ ] **[flowbite-svelte] `ModalProps.children` — 31 errors across 26 files.**
Replace `children` prop binding with Svelte snippet syntax.
- [ ] **[IDB Sort] Roll out to `ae_events__event_session`** after sort behavior review.
- [ ] **[IDB Sort] Roll out to `ae_events__event_presenter`** after sort behavior review.
- [ ] **[IDB Sort] Roll out to `ae_events__event_location`** after sort behavior review.
- [ ] **[IDB Sort] Roll out to `ae_core__person` + `ae_core__account`** after sort behavior review.
- [ ] **[IDB Version] Roll out to `db_events.ts`** (session, presenter, badge, etc.).
- [ ] **[IDB Version] Roll out to `db_core.ts`** (site_domain, person, user).
### [Journals] Journal Entry Config follow-ups
- [ ] **[Journals] Entry passcode secondary auth** — implement `passcode_hash` comparison.
- [ ] **[Journals] Summary AI shortcut** — add button to modal.
---
### [Pres Mgmt] Sessions hide/show toggle
- [x] **[Pres Mgmt] Hidden sessions blink on initial load** — SCENARIO 2 fallback in
`pres_mgmt/+page.svelte` now captures `qry_hidden` as a `$derived.by` dependency and
applies the filter in the fallback path. No blink on page load. (2026-05-28)
- [x] **[Pres Mgmt] API call uses live store instead of snapshot** — changed
`pres_mgmt_loc.current.qry_hidden``params.qry_hidden` in `handle_search_refresh`
API call to be consistent with fast path snapshot. (2026-05-28)
- **Note:** `hide_event_launcher` is still active — used in `menu_session_list.svelte`
(Launcher) to CSS-hide sessions from the list. Button to toggle it is in
`session_page_menu.svelte`. Not used in Pres Mgmt (intentional — Pres Mgmt always shows all).
- **Note:** Non-trusted users always have `!item.hide` applied at the component level
in `ae_comp__event_session_obj_li.svelte` regardless of `qry_hidden`. Toggle is
trusted-access-only in practice; direct session links still work for non-trusted users.
---
@@ -128,3 +83,4 @@ See the full completed history in:
[documentation/archive/TODO__Agents__ARCHIVE_2026-03.md](documentation/archive/TODO__Agents__ARCHIVE_2026-03.md)
[documentation/archive/TODO__Agents__ARCHIVE_2026-04.md](documentation/archive/TODO__Agents__ARCHIVE_2026-04.md)
[documentation/archive/TODO__Agents__ARCHIVE_2026-05.md](documentation/archive/TODO__Agents__ARCHIVE_2026-05.md)
[documentation/archive/TODO__Agents__ARCHIVE_2026-06.md](documentation/archive/TODO__Agents__ARCHIVE_2026-06.md)

View File

@@ -0,0 +1,139 @@
# Frontend Agent Task List
> Use this file to track steps for complex features or bug fixes.
> **Status:** Stable — ongoing development.
## 🔴 CMSC Charlotte — May 27 (Presentation Management)
**Drive down:** May 25 | **Setup:** May 26 morning | **Show:** May 27+
- [x] **[Launcher] Composable open flow** — `handle_open_file()` uses `copy_from_cache_to_temp` +
`run_osascript` / `run_cmd` directly with per-step error handling. Complete.
- [x] **[Launcher] Slide control scripts in Svelte config** — AppleScript post_scripts live in
`ae_launcher__default_launch_profiles.ts`. VLC focus-stealing fix applied. Complete.
- [x] **[Launcher] Kill Apps button** — "Kill Apps" button added to Native OS config (System
Actions, edit mode only). Kills PowerPoint, Keynote, Adobe Acrobat Reader DC, VLC, soffice.
List overridable via `event_device.other_json.launcher.kill_process_li`. Auto-cleanup on file
open (deferred — manual button sufficient for CMSC).
- [x] **[Launcher] Hidden/deleted files still visible in Presenter file list** — Fixed by
API-to-Dexie stale-record pruning plus Launcher background refresh loops for file lists.
`ae_events__event_file.ts` now prunes stale records after refresh, and
`launcher_background_sync.svelte` refreshes/prunes selected session and presenter file lists.
(`fix(launcher): refresh file lists periodically to prune deleted/hidden files`, 2026-05)
- [ ] **[Launcher/Electron] Wallpaper stops applying after several changes (post-CMSC)** —
Append timestamp/random suffix to temp filename so macOS always sees a new path.
- [ ] **[Launcher/Electron] Wallpaper drift after display hotplug (post-CMSC)** —
Add resilient reconciliation loop or event-driven reapply on topology change.
---
## 🔴 Axonius DC — June 9 (Badge Printing)
**Setup/Registration:** June 8 | **Show:** June 9
- [ ] **[Badges] Epson C3500 fanfold badge layout** — Create/configure a fanfold badge layout
compatible with the Epson C3500 continuous stock format.
---
## 🚧 V3 CRUD Migration (Surgical Cleanup)
Finalizing the 100% adoption of V3 Standard endpoints and retirement of legacy wrappers.
- [x] **[Badges] Presenter Agreement Form** — migrated to `update_ae_obj` (2026-05-21)
- [x] **[Core] Site Domain Bootstrap Refactor** — Bootstrap path is already on V3 in
`ae_core__site.ts` via `lookup_site_domain()` using `api.search_ae_obj` with FQDN filter
(used by `src/routes/+layout.ts`).
Follow-up cleanup complete: retired legacy helper `core__site_domain.ts`. (2026-06-02)
- [ ] **[Core] Legacy Utility Helpers** — Refactor `ae_core_functions.ts` to use V3 helpers.
- [ ] **[Cleanup] Delete Legacy Wrappers** — Once all callsites are migrated, remove
`src/lib/ae_api/api_get__crud_obj_id.ts` and the legacy exports from `api.ts`.
---
## 🚧 High Priority Workstreams
### [Stores] Svelte 4 → Svelte 5 State Migration
The app uses `svelte-persisted-store` (coarse reactivity). Migration target: replace with Svelte 5
`$state`-based persistence for fine-grained updates.
- [ ] **Phase A — Project plan + wrapper decision:** Write `PROJECT__Stores_Svelte5_Migration.md`.
- [ ] **Phase B — Core auth stores (highest impact):** `ae_loc`, `idaa_loc`.
- [ ] **Phase C — Remaining persisted stores:** `ae_api`, `ae_events_stores`.
- [ ] **Phase D — Non-persisted writable stores:** `ae_sess`, `slct`, `ae_snip`, etc.
### [IDB Sort] `build_tmp_sort` rollout
Shared utility in `src/lib/ae_core/core__idb_sort.ts` — fixes priority direction (inverted,
true→'0' sorts first ASC) and zero-pads sort field (8 chars). No `.reverse()` needed.
Sort chain: `group → priority DESC → sort ASC → [module-specific fields] → name`.
**⚠️ Never use `.reverse()` on a `tmp_sort_*`-sorted list — inverted priority makes it wrong.**
Documented in `GUIDE__SvelteKit2_Svelte5_DexieJS.md` (IDB Sort section).
- [x] `ae_events__event_presentation` — group + priority + sort + start_datetime + code + name
- [x] `ae_journals__journal` + `ae_journals__journal_entry` — group + priority + sort + name + updated_on
- [ ] `ae_events__event_session` — roll out when sort behavior is reviewed
- [ ] `ae_events__event_presenter` — roll out when sort behavior is reviewed
- [ ] `ae_events__event_location` — roll out when sort behavior is reviewed
- [x] `ae_posts__post` + `ae_posts__post_comment` — migrated to `build_tmp_sort` with 8-char padding; BB comment list consumer updated for ASC tmp_sort ordering. (2026-06-02)
- [ ] `ae_core__person` + `ae_core__account` — roll out when sort behavior is reviewed
### [Stores] IDB Content Version System
- [x] Write `check_and_clear_idb_tables()` helper.
- [x] Wire helper into `db_journals.ts` and IDAA layout.
- [ ] Roll out to `db_events.ts` (module-wide: session, presenter, badge, etc.).
- [ ] Roll out to `db_core.ts` (site_domain, person, user).
### [TypeScript] svelte-check hidden errors
- [x] **[flowbite-svelte] `ModalProps.children` — 31 errors across 26 files.**
Verified no remaining `children={...}` bindings on `<Modal>` and `npx svelte-check` is clean. (2026-06-02)
### [Journals] Journal Entry Config follow-ups
- [ ] **[Journals] Entry passcode secondary auth** — implement `passcode_hash` comparison.
- [x] **[Journals] Summary AI shortcut** — added Quick Actions button in entry config modal and wired it to close modal + scroll to AI tools panel in entry edit view. (2026-06-02)
### [Cleanup] Migrate remaining `lucide-svelte` imports to `@lucide/svelte`
- [x] **[Cleanup] Migrate remaining `lucide-svelte` imports to `@lucide/svelte`**
Migrated all 5 listed files to `@lucide/svelte` and uninstalled `lucide-svelte` from dependencies. (2026-06-02)
---
### [Pres Mgmt] Sessions hide/show toggle
- [x] **[Pres Mgmt] Hidden sessions blink on initial load** — SCENARIO 2 fallback in
`pres_mgmt/+page.svelte` now captures `qry_hidden` as a `$derived.by` dependency and
applies the filter in the fallback path. No blink on page load. (2026-05-28)
- [x] **[Pres Mgmt] API call uses live store instead of snapshot** — changed
`pres_mgmt_loc.current.qry_hidden``params.qry_hidden` in `handle_search_refresh`
API call to be consistent with fast path snapshot. (2026-05-28)
- **Note:** `hide_event_launcher` is still active — used in `menu_session_list.svelte`
(Launcher) to CSS-hide sessions from the list. Button to toggle it is in
`session_page_menu.svelte`. Not used in Pres Mgmt (intentional — Pres Mgmt always shows all).
- **Note:** Non-trusted users always have `!item.hide` applied at the component level
in `ae_comp__event_session_obj_li.svelte` regardless of `qry_hidden`. Toggle is
trusted-access-only in practice; direct session links still work for non-trusted users.
---
## 🧪 Testing & Optimization
- [ ] **[IDAA] IDB fast-path contact search** — parse `contact_li_json` in `search__event()`.
- [ ] **[IDAA] Optimize Recovery Meetings SQL VIEW and indexes.**
- [ ] **[IDAA / Events] Audit `default_qry_str` coverage** in all other event search pages.
- [ ] **[Launcher/VLC] Linux playback investigation** — fullscreen + pause-on-end flags.
---
## ⚙️ DevOps & Backend
- [ ] **[Backend] `event_file` — add `cfg_json` column (post-CMSC)** — The per-file display
override currently uses a localStorage workaround (`$events_loc.launcher.file_display_overrides`)
because `event_file` has no JSON blob column. Proper fix: add `cfg_json` to the `event_file` DB
table, expose it through the FastAPI model, then migrate the frontend back to reading/writing the
backend field (restoring global/cross-device persistence). Frontend code is in
`launcher_file_cont.svelte` — search for `file_display_overrides`.
- [ ] **[Backend] Re-add `Access-Control-Allow-Private-Network: true` CORS header.**
- [ ] **[DevOps] Nginx caching** — Investigate `index.html` cache-pickup issues.
- [ ] **[DevOps] Simplify Dockerfile env file selection** — Use plain `.env` instead of `BUILD_MODE`.
---
## ✅ Completed (archived)
See the full completed history in:
[documentation/archive/TODO__Agents__ARCHIVE_2026-03.md](documentation/archive/TODO__Agents__ARCHIVE_2026-03.md)
[documentation/archive/TODO__Agents__ARCHIVE_2026-04.md](documentation/archive/TODO__Agents__ARCHIVE_2026-04.md)
[documentation/archive/TODO__Agents__ARCHIVE_2026-05.md](documentation/archive/TODO__Agents__ARCHIVE_2026-05.md)

273
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "osit-aether-app-svelte",
"version": "3.00.10",
"version": "3.00.20",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "osit-aether-app-svelte",
"version": "3.00.10",
"version": "3.00.20",
"dependencies": {
"@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.0",
@@ -26,12 +26,10 @@
"@lucide/svelte": "^0.*.0",
"@popperjs/core": "^2.11.0",
"@tailwindcss/vite": "^4.1.10",
"axios": "^1.7.0",
"dayjs": "^1.11.10",
"dexie": "^4.0.0",
"flowbite-svelte": "^1.28.1",
"html5-qrcode": "^2.3.8",
"lucide-svelte": "^0.*.0",
"marked": "^17.0.0",
"openai": "^6.10.0",
"prettier-plugin-tailwindcss": "^0.7.2",
@@ -3929,23 +3927,6 @@
"node": ">=12"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -3976,19 +3957,6 @@
"node": "18 || 20 || >=22"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -4101,18 +4069,6 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@@ -4240,15 +4196,6 @@
"node": ">=0.10.0"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -4299,20 +4246,6 @@
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -4332,24 +4265,6 @@
"node": ">=10.13.0"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-module-lexer": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
@@ -4357,33 +4272,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
@@ -4935,42 +4823,6 @@
"mini-svg-data-uri": "^1.4.3"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
@@ -5003,43 +4855,6 @@
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -5065,18 +4880,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -5092,33 +4895,6 @@
"node": ">=8"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -5654,15 +5430,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/lucide-svelte": {
"version": "0.577.0",
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.577.0.tgz",
"integrity": "sha512-0i88o57KsaHWnc80J57fY99CWzlZsSdtH5kKjLUJa7z8dum/9/AbINNLzJ7NiRFUdOgMnfAmJt8jFbW2zeC5qQ==",
"license": "ISC",
"peerDependencies": {
"svelte": "^3 || ^4 || ^5.0.0-next.42"
}
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
@@ -5693,36 +5460,6 @@
"node": ">= 20"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mini-svg-data-uri": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
@@ -6317,12 +6054,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",

View File

@@ -105,12 +105,10 @@
"@lucide/svelte": "^0.*.0",
"@popperjs/core": "^2.11.0",
"@tailwindcss/vite": "^4.1.10",
"axios": "^1.7.0",
"dayjs": "^1.11.10",
"dexie": "^4.0.0",
"flowbite-svelte": "^1.28.1",
"html5-qrcode": "^2.3.8",
"lucide-svelte": "^0.*.0",
"marked": "^17.0.0",
"openai": "^6.10.0",
"prettier-plugin-tailwindcss": "^0.7.2",

View File

@@ -1,6 +1,6 @@
<script lang="ts">
// *** Import Svelte specific
import * as Lucide from 'lucide-svelte';
import * as Lucide from '@lucide/svelte';
import { fade } from 'svelte/transition';
// *** Import Aether specific variables and functions

View File

@@ -2,7 +2,7 @@
// untrack import removed — task_id sync now uses direct $effect (no untrack needed)
// Imports
// Import components and elements
import * as Lucide from 'lucide-svelte';
import * as Lucide from '@lucide/svelte';
import Element_input_files_tbl from '$lib/elements/element_input_files_tbl.svelte';
// Import storage, functions, and libraries

View File

@@ -1,75 +0,0 @@
export interface Site_Domain {
id: string;
// id_random: string;
site_id: string;
site_id_random?: string;
fqdn: string;
access_key?: null | string;
required_referrer?: null | string;
valid_for?: null | number; // In hours
enable: null | boolean;
hide?: null | boolean;
priority?: null | boolean;
sort?: null | number;
group?: null | string;
notes?: null | string;
created_on: Date;
updated_on?: null | Date;
}
import { api } from '$lib/api/api';
/**
* Fetches a site_domain object by its Fully Qualified Domain Name (FQDN).
*
* @param api_cfg - The API configuration object.
* @param fqdn - The FQDN of the site domain to fetch.
* @param timeout - The request timeout in milliseconds.
* @param log_lvl - The logging level.
* @returns The site domain object or null if not found.
*/
export async function load_ae_obj_by_fqdn__site_domain({
api_cfg,
fqdn,
timeout = 7000,
log_lvl = 0
}: {
api_cfg: any;
fqdn: string;
timeout?: number;
log_lvl?: number;
}): Promise<any> {
if (log_lvl) {
console.log(
`*** load_ae_obj_by_fqdn__site_domain() *** api.base_url=${api_cfg.base_url}, fqdn=${fqdn}, timeout=${timeout}`
);
}
const params = {};
try {
const site_domain_obj = await api.get_ae_obj_id_crud({
api_cfg: api_cfg,
no_account_id: true, // This seems to be a special case for this endpoint
obj_type: 'site_domain',
obj_id: fqdn, // NOTE: This is the FQDN, not the ID.
use_alt_table: true,
use_alt_base: true,
params: params,
timeout: timeout,
log_lvl: log_lvl
});
if (site_domain_obj) {
return site_domain_obj;
} else {
console.log('No results returned.');
return null;
}
} catch (error) {
console.log('No results returned or failed.', error);
return null;
}
}

View File

@@ -75,8 +75,10 @@ export function journal_entry_matches_search(
}
export function journal_entry_compare_for_list(a: any, b: any): number {
// tmp_sort_1 is built by build_tmp_sort() for ascending comparison:
// priority=true encodes as '0', priority=false as '1', so ASC puts priority first.
return (
(b?.tmp_sort_1 ?? '').localeCompare(a?.tmp_sort_1 ?? '') ||
(a?.tmp_sort_1 ?? '').localeCompare(b?.tmp_sort_1 ?? '') ||
(b?.updated_on ?? '').localeCompare(a?.updated_on ?? '') ||
(b?.journal_entry_id ?? '').localeCompare(a?.journal_entry_id ?? '')
);

View File

@@ -1,5 +1,6 @@
import type { key_val } from '$lib/stores/ae_stores';
import { api } from '$lib/api/api';
import { build_tmp_sort } from '$lib/ae_core/core__idb_sort';
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
import { db_posts } from '$lib/ae_posts/db_posts';
@@ -570,15 +571,16 @@ export async function process_ae_obj__post_props({
if (!obj.account_id_random) obj.account_id_random = account_id;
}
obj.name = obj.title;
const sort_val = (obj.sort ?? 0).toString().padStart(3, '0');
const updated =
obj.updated_on ?? obj.created_on ?? new Date(0).toISOString();
obj.tmp_sort_1 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${
sort_val
}_${updated}`;
obj.tmp_sort_2 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${
sort_val
}_${obj.updated_on ?? ''}_${obj.created_on ?? ''}`;
const { tmp_sort_1, tmp_sort_2 } = build_tmp_sort({
prefix: [obj.group ?? ''],
priority: obj.priority,
sort: obj.sort,
fields_1: [obj.updated_on ?? obj.created_on ?? ''],
fields_2: [obj.updated_on ?? '', obj.created_on ?? ''],
pad_width: 8
});
obj.tmp_sort_1 = tmp_sort_1;
obj.tmp_sort_2 = tmp_sort_2;
return obj;
}

View File

@@ -1,5 +1,6 @@
import type { key_val } from '$lib/stores/ae_stores';
import { api } from '$lib/api/api';
import { build_tmp_sort } from '$lib/ae_core/core__idb_sort';
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
import { db_posts } from '$lib/ae_posts/db_posts';
@@ -383,15 +384,16 @@ export async function process_ae_obj__post_comment_props({
obj_type: 'post_comment',
log_lvl,
specific_processor: (obj) => {
const sort_val = (obj.sort ?? 0).toString().padStart(3, '0');
const updated =
obj.updated_on ?? obj.created_on ?? new Date(0).toISOString();
obj.tmp_sort_1 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${
sort_val
}_${updated}`;
obj.tmp_sort_2 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${
sort_val
}_${obj.updated_on ?? ''}_${obj.created_on ?? ''}`;
const { tmp_sort_1, tmp_sort_2 } = build_tmp_sort({
prefix: [obj.group ?? ''],
priority: obj.priority,
sort: obj.sort,
fields_1: [obj.updated_on ?? obj.created_on ?? ''],
fields_2: [obj.updated_on ?? '', obj.created_on ?? ''],
pad_width: 8
});
obj.tmp_sort_1 = tmp_sort_1;
obj.tmp_sort_2 = tmp_sort_2;
return obj;
}

View File

@@ -1,4 +1,4 @@
import * as Lucide from 'lucide-svelte';
import * as Lucide from '@lucide/svelte';
/**
* Returns a Lucide icon component based on the provided file extension.

File diff suppressed because it is too large Load Diff

View File

@@ -165,9 +165,25 @@ $effect(() => {
$effect(() => {
const account_id = $slct.account_id;
const api_ready = !!$ae_api?.base_url;
const entry = $lq__ds_obj;
const entry = $lq__ds_obj as ae_DataStore | null | undefined;
if (browser && api_ready && !entry && ds_loading_status === 'starting') {
// Don't fire until the bootstrap Sync Effect has set a real account_id.
// Without this guard, the fetch runs with null account_id and the
// localStorage scavenge in get_object picks up a stale account_id from a
// previous session, returning the wrong account's record.
if (!browser || !account_id || !api_ready || ds_loading_status !== 'starting') return;
// Also reload when IDB has a record but it belongs to a different account
// (not null/global and not the current account). This handles the case where
// a previous dev/demo session left account-specific rows in IDB that score
// as the "best" liveQuery match even though they are for the wrong account.
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__ds__code';
}
});

View File

@@ -28,7 +28,7 @@ The Aether system is built on a foundation of several core modules. The UI compo
### Sites & Site Domains
- **Description:** Manages different sites or instances of the application and their associated domain names. This allows for multi-tenant configurations.
- **Logic:** `src/lib/ae_core/core__site.ts` and `core__site_domain.ts`
- **Logic:** `src/lib/ae_core/core__site.ts`
### Users

View File

@@ -210,7 +210,7 @@ function clear_sess() {
container m-auto flex h-full min-h-full
w-full max-w-7xl
min-w-full flex-col gap-1
overflow-auto
overflow-auto [scrollbar-gutter:stable]
bg-gray-50 text-gray-800
dark:bg-gray-900 dark:text-gray-200

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { untrack } from 'svelte';
import { slide } from 'svelte/transition';
interface Props {
/** @type {import('./$types').PageData} */
data: any;
@@ -31,7 +32,7 @@ import Comp_badge_obj_li from './ae_comp__badge_obj_li.svelte';
import Comp_badge_create_form from './ae_comp__badge_create_form.svelte';
import Comp_badge_upload_form from './ae_comp__badge_upload_form.svelte';
import { LoaderCircle, UserPlus, Printer, Upload, FileText, BarChart2 } from '@lucide/svelte';
import { UserPlus, Printer, Upload, FileText, ChartColumnBig } from '@lucide/svelte';
// Load templates for this event so the create form can show the selector and
// derive badge_type_code_li from whichever template the user picks.
@@ -472,7 +473,7 @@ async function handle_search_refresh(params: any) {
></Comp_badge_search>
{#if $ae_loc.trusted_access && $ae_loc.edit_mode}
<div class="flex flex-row gap-1 items-center justify-center">
<div transition:slide={{ duration: 200 }} class="flex flex-row gap-1 items-center justify-center">
{#if badges_loc.current.enable_add_badge_btn ?? true}
<button
type="button"
@@ -523,7 +524,7 @@ async function handle_search_refresh(params: any) {
<a
href={`/events/${$events_slct?.event_id}/badges/stats`}
class="btn btn-tertiary">
<BarChart2 size="1em" /> Badge Printing Stats
<ChartColumnBig size="1em" /> Badge Printing Stats
</a>
</div>
{/if}
@@ -591,15 +592,10 @@ async function handle_search_refresh(params: any) {
{/if}
</dialog>
{#if $events_sess?.badges?.search_status === 'loading' && event_badge_id_li.length === 0}
<div
class="flex flex-col items-center justify-center p-10 text-center opacity-50">
<LoaderCircle size="3em" class="mx-auto mb-4 animate-spin" />
<p class="text-xl">Loading badges...</p>
</div>
{:else}
<Comp_badge_obj_li {lq__event_badge_obj_li} log_lvl={1}></Comp_badge_obj_li>
{/if}
<Comp_badge_obj_li
{lq__event_badge_obj_li}
search_status={$events_sess?.badges?.search_status ?? null}
log_lvl={1} />
<style>
dialog {

View File

@@ -2,6 +2,7 @@
interface Props {
container_class_li?: string | Array<string>;
lq__event_badge_obj_li: any;
search_status?: string | null;
log_lvl?: number;
show_sensitive_fields?: boolean;
hide_affiliations?: boolean;
@@ -12,6 +13,7 @@ interface Props {
let {
container_class_li = [],
lq__event_badge_obj_li,
search_status = null,
log_lvl = $bindable(0),
show_sensitive_fields = true,
hide_affiliations = false,
@@ -27,14 +29,14 @@ import {
Check,
Eye,
EyeOff,
FileSearch,
Link,
LoaderCircle,
Mail,
MapPin,
Printer,
Tags,
User
User,
UserSearch
} from '@lucide/svelte';
// Track per-badge copy state for the "Review Link" clipboard button
let copy_status: Record<string, 'idle' | 'copied'> = $state({});
@@ -42,6 +44,7 @@ let copy_status: Record<string, 'idle' | 'copied'> = $state({});
// Access level shortcuts
let is_trusted = $derived($ae_loc.trusted_access === true);
let is_admin = $derived($ae_loc.administrator_access === true);
let is_manager = $derived($ae_loc.manager_access === true);
let is_public = $derived($ae_loc.public_access === true); // public passcode or higher — may print first prints
let is_edit_mode = $derived($ae_loc.edit_mode === true);
@@ -113,12 +116,24 @@ let visible_badge_obj_li = $derived(
if (ps === 'not_printed') return (item.print_count ?? 0) < 1 && hide_ok;
return hide_ok; // 'all'
}
// Public (kiosk) / authenticated / anonymous: only unprinted, never hidden.
// Badge kiosks run at public_access — attendees should only see their own
// unprinted badge, never a list of already-printed ones.
// Public/Attendee (public_access, not trusted): show all non-hidden.
// Printed badges appear read-only so attendees can see their check-in status.
if (is_public) return !item.hide;
// Below public (anonymous, authenticated): only unprinted, never hidden.
return (item.print_count ?? 0) < 1 && !item.hide;
});
// For public (attendee) kiosk: sort unprinted first so attendees find their
// own badge at the top; already-checked-in records fall to the bottom.
if (is_public && !is_trusted && filtered.length > 1) {
filtered.sort((a: any, b: any) => {
const a_p = (a.print_count ?? 0) > 0;
const b_p = (b.print_count ?? 0) > 0;
if (a_p !== b_p) return a_p ? 1 : -1;
return 0; // preserve existing name order within each group
});
}
if (log_lvl)
console.log(
`visible_badge_obj_li: Input=${list.length}, Output=${filtered.length}`
@@ -154,9 +169,13 @@ let visible_badge_obj_li = $derived(
event_badge_obj?.full_name_override ??
event_badge_obj?.full_name ??
`${event_badge_obj?.given_name ?? ''} ${event_badge_obj?.family_name ?? ''}`.trim()}
{@const can_print = (is_public && !is_printed) || (is_trusted && is_edit_mode)}
{@const is_attendee_view = is_public && !is_trusted}
{@const is_attendee_printed = is_attendee_view && is_printed}
{@const can_print = is_trusted && (!is_printed || is_edit_mode)}
{@const row_clickable = can_print || (is_attendee_view && !is_printed)}
{@const print_href = `/events/${event_badge_obj?.event_id}/badges/${event_badge_obj?.event_badge_id}/print`}
{@const review_href = build_review_url(event_badge_obj)}
{@const row_href = is_trusted ? print_href : review_href}
<li
class="
@@ -169,18 +188,13 @@ let visible_badge_obj_li = $derived(
class="flex w-full flex-row flex-wrap items-center justify-between gap-2">
<!-- Left cluster: main action (name + email + affiliations) -->
<div class="flex min-w-0 grow flex-row flex-wrap items-center gap-x-3 gap-y-1">
{#if can_print}
{#if row_clickable}
<a
href={print_href}
href={row_href}
class="hover:text-primary-800-200 hover:bg-primary-200-800 active:bg-surface-200-700 flex items-center gap-3 justify-start px-3 py-2 text-left text-lg font-bold flex-1 transition-colors duration-1000 hover:duration-300 min-w-0 preset-tonal-primary rounded-lg"
title="
Print badge for
{display_name}
email: <{is_trusted ? event_badge_obj?.email : obscure_email(event_badge_obj?.email)}>
{event_badge_obj?.affiliations_override ?? event_badge_obj?.affiliations}
type: {event_badge_obj?.badge_type}
id: {event_badge_obj.event_badge_id}
"
title={is_trusted
? `Print badge for ${display_name} · ${event_badge_obj?.affiliations_override ?? event_badge_obj?.affiliations ?? ''} · ${event_badge_obj?.badge_type ?? ''} · id: ${event_badge_obj.event_badge_id}`
: `Review your badge info`}
>
{#if event_badge_obj?.hide}
<EyeOff size="1.1em" class="text-gray-400" />
@@ -208,6 +222,23 @@ let visible_badge_obj_li = $derived(
{/if}
</div>
</a>
{:else if is_attendee_printed}
<!-- Public attendee, badge already printed: show check-in status, no action -->
<div class="flex items-center gap-3 justify-start px-3 py-2 flex-1 min-w-0 rounded-lg bg-success-500/10">
<Check size="1.1em" class="text-success-600 dark:text-success-400 shrink-0" />
<div class="flex flex-col min-w-0">
<span class="truncate font-semibold">{display_name}</span>
<span class="text-xs text-success-700 dark:text-success-300 opacity-80">
Checked in &middot; {print_count}&times;
{#if event_badge_obj.print_first_datetime}
&middot; First: {ae_util.iso_datetime_formatter(event_badge_obj.print_first_datetime, 'datetime_iso_12_no_seconds')}
{/if}
{#if event_badge_obj.print_last_datetime}
&middot; Last: {ae_util.iso_datetime_formatter(event_badge_obj.print_last_datetime, 'datetime_iso_12_no_seconds')}
{/if}
</span>
</div>
</div>
{:else}
<div
class="hover:text-primary-800-200 hover:bg-primary-200-800 active:bg-surface-200-700 flex items-center gap-3 justify-start px-3 py-2 text-left text-lg font-bold flex-1 transition-colors duration-1000 hover:duration-300 min-w-0 preset-tonal-primary rounded-lg cursor-not-allowed"
@@ -256,8 +287,8 @@ let visible_badge_obj_li = $derived(
<!-- Right: up to 4 action buttons -->
<div class="flex shrink-0 flex-row items-center gap-1">
<!-- 1. Print Badge: Public+ first print; Trusted + Edit Mode for reprint -->
{#if (is_public && !is_printed) || (is_trusted && is_edit_mode)}
<!-- 1. Print Badge: Trusted — first print anytime, reprint in Edit Mode or Manager+ -->
{#if is_trusted && (!is_printed || is_edit_mode || is_manager)}
<a
href={`/events/${event_badge_obj?.event_id}/badges/${event_badge_obj?.event_badge_id}/print`}
class="hover:text-primary-800-200 hover:bg-primary-200-800 active:bg-surface-200-700 flex items-center gap-1 px-3 py-2 text-base font-bold transition-colors duration-1000 hover:duration-300 min-w-0 preset-tonal-primary rounded-lg border border-primary-200-800
@@ -312,9 +343,8 @@ let visible_badge_obj_li = $derived(
</button>
{/if}
<!-- 4. Email Review Link: Administrator + Edit Mode only
Temporarily restricted — TODO: restore broader access after Axonius 2026 -->
{#if is_admin && is_edit_mode}
<!-- 4. Email Review Link: Manager+ always; Administrator + Edit Mode otherwise -->
{#if is_admin && (is_edit_mode || is_manager)}
<button
type="button"
class="hover:text-primary-800-200 hover:bg-primary-200-800 active:bg-surface-200-700 flex items-center gap-1 px-3 py-2 text-base font-bold transition-colors duration-1000 hover:duration-300 min-w-0 preset-tonal-primary rounded-lg border border-primary-200-800"
@@ -380,12 +410,17 @@ let visible_badge_obj_li = $derived(
</ul>
{:else}
<div
class="flex flex-col items-center justify-center p-20 text-center opacity-50">
<FileSearch size="3em" class="mx-auto mb-2 opacity-20" />
<p>
No badges found matching your criteria. Try adjusting your
filters.
</p>
class="flex w-full flex-col items-center justify-center p-16 text-center opacity-50">
{#if search_status === 'loading'}
<LoaderCircle size="3em" class="mx-auto mb-2 animate-spin" />
<p class="text-xl">Searching...</p>
{:else if !is_trusted && !(badges_loc.current.fulltext_search_qry_str ?? '').trim()}
<UserSearch size="3em" class="mx-auto mb-2 opacity-20" />
<p class="text-xl">Search by name, email, or organization above to find your badge.</p>
{:else}
<UserSearch size="3em" class="mx-auto mb-2 opacity-20" />
<p class="text-xl">No badges found matching your criteria.</p>
{/if}
</div>
{/if}
</section>

View File

@@ -17,6 +17,7 @@ import {
StepForward
} from '@lucide/svelte';
import { fade, slide } from 'svelte/transition';
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { events_sess } from '$lib/stores/ae_events_stores';
import { badges_loc } from '$lib/stores/ae_events_stores__badges.svelte';
@@ -55,7 +56,10 @@ let effective_min_chars = $derived.by(() => {
function handle_search_trigger() {
const qry = (badges_loc.current.fulltext_search_qry_str ?? '').trim();
if (qry.length < effective_min_chars) return;
if (qry.length < effective_min_chars) {
document.getElementById('badge_fulltext_search_qry_str')?.focus();
return;
}
badges_loc.current.search_version++;
}
@@ -87,6 +91,7 @@ function handle_qr_scan_result(event: {
class="ae_group filters_and_search flex w-full flex-col items-center justify-center gap-2">
{#if $events_sess.badges.show_form__search}
<form
transition:fade={{ duration: 200 }}
onsubmit={prevent_default(() => {
handle_search_trigger();
})}
@@ -95,7 +100,7 @@ function handle_qr_scan_result(event: {
<div
class="flex grow flex-col xl:flex-row flex-wrap items-center justify-center gap-2">
{#if ($ae_loc.trusted_access && $ae_loc.edit_mode) || $ae_loc.manager_access}
<div class="flex flex-row flex-wrap items-center justify-center gap-1">
<div transition:slide={{ duration: 200 }} class="flex flex-row flex-wrap items-center justify-center gap-1">
<span class="flex flex-row flex-wrap items-center justify-center gap-1">
<select
bind:value={badges_loc.current.search_badge_type_code}
@@ -159,6 +164,7 @@ function handle_qr_scan_result(event: {
{#if (badges_loc.current.enable_search_qr && $ae_loc.edit_mode)}
<button
transition:fade={{ duration: 150 }}
type="button"
onclick={() => {
$events_sess.badges.show_form__search = false;
@@ -177,12 +183,12 @@ function handle_qr_scan_result(event: {
onclick={() => document.getElementById('badge_fulltext_search_qry_str')?.focus()}
aria-label="Start here focus search field"
data-testid="badge-start-btn"
class="btn btn-sm preset-filled-secondary-200-800 font-semibold"
class="btn btn-sm preset-filled-secondary-200-800 font-semibold transition-opacity duration-300"
class:opacity-30={!!badges_loc.current.fulltext_search_qry_str}
class:hidden={$ae_loc.trusted_access}
>
<StepForward size="1em" class="mx-1" />
Start Here
Start Here:
</button>
<!-- svelte-ignore a11y_autofocus -->
@@ -203,10 +209,11 @@ function handle_qr_scan_result(event: {
bg-red-50-900 dark:bg-red-50-950
focus:bg-white
focus:text-black
border border-l-2 border-b-2
border-l-primary-500 border-b-primary-500
border-r-primary-300 border-t-primary-300
dark:border-l-primary-800 dark:border-b-primary-800
border border-l-2 border-r-2 border-b-3
border-l-success-300 border-b-success-500
border-r-success-300 border-t-success-300
dark:border-l-success-800 dark:border-b-success-800
preset-tonal-success
"
onkeyup={(e) => {
if (e.key === 'Enter') {
@@ -218,17 +225,19 @@ function handle_qr_scan_result(event: {
</div>
</div>
{#if (badges_loc.current.fulltext_search_qry_str ?? '').trim().length < effective_min_chars}
<p class="w-full text-center text-xs opacity-50">
Enter at least {effective_min_chars} character{effective_min_chars === 1 ? '' : 's'} to search
</p>
{/if}
<p class="w-full text-center text-xs transition-opacity duration-200"
class:opacity-0={(badges_loc.current.fulltext_search_qry_str ?? '').trim().length >= effective_min_chars}
class:opacity-50={(badges_loc.current.fulltext_search_qry_str ?? '').trim().length < effective_min_chars}>
Enter at least {effective_min_chars} character{effective_min_chars === 1 ? '' : 's'} to search
</p>
<div class="flex flex-row items-center justify-center gap-1">
<button
type="submit"
class:opacity-50={($events_sess.badges.search_status === 'loading') || (badges_loc.current.fulltext_search_qry_str ?? '').trim().length < effective_min_chars}
class:pointer-events-none={($events_sess.badges.search_status === 'loading') || (badges_loc.current.fulltext_search_qry_str ?? '').trim().length < effective_min_chars}
class="
hover:text-primary-800-200 hover:bg-primary-200-800 active:bg-surface-200-700 flex items-center justify-center gap-1 px-3 py-2 text-2xl font-bold transition-all duration-1000 hover:duration-300 min-w-0 preset-tonal-success rounded-lg border border-success-200-800
hover:text-primary-800-200 hover:bg-primary-200-800 active:bg-surface-200-700 flex items-center justify-center gap-1 px-3 py-2 text-2xl font-bold transition-all duration-1000 hover:duration-50 min-w-0 preset-tonal-success rounded-lg border border-success-200-800
w-48
">
{#if $events_sess.badges.search_status === 'loading'}
@@ -241,7 +250,10 @@ function handle_qr_scan_result(event: {
<button
type="button"
class:hidden={!badges_loc.current.fulltext_search_qry_str &&
class:opacity-0={!badges_loc.current.fulltext_search_qry_str &&
!badges_loc.current.search_badge_type_code &&
badges_loc.current.qry_printed_status === 'all'}
class:pointer-events-none={!badges_loc.current.fulltext_search_qry_str &&
!badges_loc.current.search_badge_type_code &&
badges_loc.current.qry_printed_status === 'all'}
onclick={() => {
@@ -254,7 +266,7 @@ function handle_qr_scan_result(event: {
document.getElementById('badge_fulltext_search_qry_str')?.focus();
}}
class="
hover:text-tertiary-800-200 hover:bg-tertiary-200-800 active:bg-surface-200-700 flex items-center justify-center gap-1 px-3 py-2 text-sm font-bold transition-all duration-1000 hover:duration-300 min-w-0 preset-outlined-tertiary rounded-lg border border-tertiary-200-800
hover:text-tertiary-800-200 hover:bg-tertiary-200-800 active:bg-surface-200-700 flex items-center justify-center gap-1 px-3 py-2 text-sm font-bold transition-all duration-200 hover:duration-100 min-w-0 preset-outlined-tertiary rounded-lg border border-tertiary-200-800
"
title="Clear search query">
@@ -264,6 +276,7 @@ function handle_qr_scan_result(event: {
{#if $ae_loc.trusted_access && $ae_loc.edit_mode}
<label
transition:fade={{ duration: 150 }}
class="bg-surface-200-800 rounded-token flex cursor-pointer items-center gap-1 px-2 py-1 text-xs font-semibold">
<span> Show Hidden </span>
<input
@@ -275,6 +288,7 @@ function handle_qr_scan_result(event: {
{/if}
{#if $ae_loc.edit_mode}
<label
transition:fade={{ duration: 150 }}
class="bg-surface-200-800 rounded-token flex cursor-pointer items-center gap-1 px-2 py-1 text-xs font-semibold">
<span> Remote First </span>
<input
@@ -288,6 +302,7 @@ function handle_qr_scan_result(event: {
</form>
{:else if $events_sess.badges.show_form__scan}
<div
transition:fade={{ duration: 200 }}
class="bg-surface-100-900 mx-auto w-full max-w-2xl rounded-lg p-4 shadow-lg">
<Element_qr_scanner
bind:start_qr_scanner={$events_sess.badges.qr_scan_start}

View File

@@ -23,10 +23,11 @@ let upload_status: string = $state('idle'); // idle, loading, processing, succes
let upload_message: string = $state('');
let processed_badges_count: number = $state(0);
let total_badges_in_file: number = $state(0);
let upload_mode: 'client_csv' | 'axonius_zoom' = $state('axonius_zoom');
let upload_mode: 'client_csv' | 'axonius_zoom' | 'axonius_splash_xlsx' = $state('axonius_zoom');
let begin_at: number | null = $state(null);
let end_at: number | null = $state(null);
let return_detail: boolean = $state(false);
let import_status_filter: string = $state('Attending'); // splash only; set to '' to import all statuses
// A very basic CSV parser that assumes the first row is headers
function parse_csv(text: string): key_val[] {
@@ -64,6 +65,72 @@ async function handle_upload(event: Event) {
return;
}
// Server-side import for Cvent Splash XLSX
if (upload_mode === 'axonius_splash_xlsx') {
upload_status = 'loading';
upload_message = 'Uploading to server...';
const formData = new FormData();
formData.append('file', selected_file, selected_file.name);
const params: any = {};
if (begin_at !== null && begin_at !== undefined) params.begin_at = String(begin_at);
if (end_at !== null && end_at !== undefined) params.end_at = String(end_at);
if (import_status_filter !== 'Attending') params.import_status_filter = import_status_filter;
if (return_detail) params.return_detail = String(true);
const task_id = (crypto && (crypto as any).randomUUID)
? (crypto as any).randomUUID()
: String(Date.now());
try {
upload_status = 'processing';
upload_message = 'Processing on server… this may take a minute or two.';
const endpoint = `/event/${event_id}/badge/import/splash_xlsx`;
const result = await api.post_object({
api_cfg: $ae_api,
endpoint,
form_data: formData,
params,
return_meta: true,
task_id,
timeout: 300000, // 5 min — server-side import can be slow; no retry to avoid duplicate imports
retry_count: 1,
log_lvl: 0
});
if (!result) {
upload_status = 'error';
upload_message = 'Server import failed (no response).';
if (onerror) onerror(result);
return;
}
const meta = result.meta ?? result;
if (meta && meta.success === false) {
upload_status = 'error';
upload_message = meta?.details?.message || 'Server import failed';
if (onerror) onerror(result);
return;
}
const processed = meta?.processed ?? meta?.data_list_count ?? (Array.isArray(result?.data) ? result.data.length : 0);
processed_badges_count = processed || 0;
total_badges_in_file = meta?.total ?? processed_badges_count;
upload_status = 'success';
upload_message = `Server processed ${processed_badges_count} badges.`;
if (onsuccess) onsuccess();
} catch (err: any) {
upload_status = 'error';
upload_message = `Upload failed: ${err?.message || 'Unknown error'}`;
console.error('Upload error:', err);
if (onerror) onerror(err);
}
return;
}
// Server-side import for Axonius Zoom CSV
if (upload_mode === 'axonius_zoom') {
upload_status = 'loading';
@@ -83,6 +150,7 @@ async function handle_upload(event: Event) {
try {
upload_status = 'processing';
upload_message = 'Processing on server… this may take a minute or two.';
const endpoint = `/event/${event_id}/badge/import/zoom_csv`;
const result = await api.post_object({
api_cfg: $ae_api,
@@ -91,6 +159,8 @@ async function handle_upload(event: Event) {
params,
return_meta: true,
task_id,
timeout: 300000, // 5 min — server-side import can be slow; no retry to avoid duplicate imports
retry_count: 1,
log_lvl: 0
});
@@ -208,23 +278,27 @@ function handle_cancel() {
<code>badge_type_code</code>.
</p> -->
<p>
Upload the standard Axonius Zoom event tickets CSV export file here. This will trigger server-side processing to import the new and changed badges. This can take a few seconds to a few minutes.
{#if upload_mode === 'axonius_splash_xlsx'}
Upload the Cvent Splash XLSX registrant export file here. Only registrants with status "Attending" are imported by default. Server-side processing can take a few seconds to a few minutes.
{:else}
Upload the standard Axonius Zoom event tickets CSV export file here. This will trigger server-side processing to import the new and changed badges. This can take a few seconds to a few minutes.
{/if}
</p>
<fieldset class="space-y-2">
<legend class="label"><span>Upload format</span></legend>
<div class="flex gap-4 items-center">
<!-- <label class="label flex items-center gap-2">
<input type="radio" name="upload_mode" value="client_csv" bind:group={upload_mode} />
<span>Standard CSV (client-side parse)</span>
</label> -->
<label class="label flex items-center gap-2">
<input type="radio" name="upload_mode" value="axonius_zoom" bind:group={upload_mode} />
<span>Axonius Zoom CSV (event tickets)</span>
</label>
<label class="label flex items-center gap-2">
<input type="radio" name="upload_mode" value="axonius_splash_xlsx" bind:group={upload_mode} />
<span>Cvent Splash XLSX (registrant export)</span>
</label>
</div>
{#if $ae_loc.administrator_access && upload_mode === 'axonius_zoom'}
{#if $ae_loc.administrator_access && (upload_mode === 'axonius_zoom' || upload_mode === 'axonius_splash_xlsx')}
<div class="grid grid-cols-3 gap-2 items-center">
<input type="number" min="0" placeholder="begin_at (0)" bind:value={begin_at} class="input" />
<input type="number" min="0" placeholder="end_at (20000)" bind:value={end_at} class="input" />
@@ -233,14 +307,25 @@ function handle_cancel() {
<span>Return detail</span>
</label>
</div>
{#if upload_mode === 'axonius_splash_xlsx'}
<div class="flex items-center gap-2">
<label class="label" for="import_status_filter">Status filter</label>
<input
id="import_status_filter"
type="text"
placeholder="Attending (leave blank for all)"
bind:value={import_status_filter}
class="input flex-1" />
</div>
{/if}
{/if}
</fieldset>
<label class="label">
<span>Select CSV File</span>
<span>Select {upload_mode === 'axonius_splash_xlsx' ? 'XLSX' : 'CSV'} File</span>
<input
type="file"
accept=".csv"
accept={upload_mode === 'axonius_splash_xlsx' ? '.xlsx' : '.csv'}
bind:this={file_input}
onchange={handle_file_change}
class="input" />
@@ -257,13 +342,15 @@ function handle_cancel() {
class:preset-tonal-surface={upload_status !== 'error'}>
<p>{upload_message}</p>
{#if upload_status === 'processing' || upload_status === 'loading'}
<progress
class="progress"
value={processed_badges_count}
max={total_badges_in_file}></progress>
<p>
Processed: {processed_badges_count} / {total_badges_in_file}
</p>
{#if upload_mode === 'client_csv'}
<progress
class="progress"
value={processed_badges_count}
max={total_badges_in_file}></progress>
<p>Processed: {processed_badges_count} / {total_badges_in_file}</p>
{:else}
<progress class="progress"></progress>
{/if}
{/if}
</div>
{/if}

View File

@@ -1,7 +1,7 @@
<script lang="ts">
// *** Import Svelte core
import { onMount } from 'svelte';
import * as Lucide from 'lucide-svelte';
import * as Lucide from '@lucide/svelte';
// *** Import Aether core variables and functions
import type { key_val } from '$lib/stores/ae_stores';

View File

@@ -73,12 +73,14 @@ let lq__post_comment_obj_li = $derived.by(() => {
return liveQuery(async () => {
if (!post_id) return [];
return await db_posts.comment
// tmp_sort_1 is built with build_tmp_sort() and is designed for ASC sort:
// priority=true -> '0', so priority records naturally sort first.
const comments = await db_posts.comment
.where('post_id')
.equals(post_id)
.reverse()
.limit(limit)
.sortBy('tmp_sort_1');
return comments;
});
});

View File

@@ -255,8 +255,9 @@ async function handle_search_refresh(qry_key: string) {
} else {
// Robust Chronological Sort using pre-computed tmp_sort_1
// Handles Priority, Manual Sort, and the updated_on/created_on fallback
// tmp_sort_1 built by build_tmp_sort(): priority=true→'0', so ASC puts priority first.
local_results.sort((a, b) =>
(b.tmp_sort_1 ?? '').localeCompare(a.tmp_sort_1 ?? '')
(a.tmp_sort_1 ?? '').localeCompare(b.tmp_sort_1 ?? '')
);
}
@@ -335,8 +336,9 @@ async function handle_search_refresh(qry_key: string) {
(b.name ?? '').localeCompare(a.name ?? '')
);
} else {
// tmp_sort_1 built by build_tmp_sort(): priority=true→'0', so ASC puts priority first.
api_results.sort((a, b) =>
(b.tmp_sort_1 ?? '').localeCompare(a.tmp_sort_1 ?? '')
(a.tmp_sort_1 ?? '').localeCompare(b.tmp_sort_1 ?? '')
);
}

View File

@@ -95,8 +95,10 @@ let lq__event_obj_li = $derived.by(() => {
} else {
// Robust Chronological Sort using pre-computed tmp_sort_1 (Refactored 2026-02-16)
// This handles Group > Priority > Manual Sort > Date (with updated_on fallback)
// tmp_sort_1 is built by build_tmp_sort() for ascending comparison:
// priority=true encodes as '0', priority=false as '1', so ASC puts priority first.
results.sort((a, b) =>
(b.tmp_sort_1 ?? '').localeCompare(a.tmp_sort_1 ?? '')
(a.tmp_sort_1 ?? '').localeCompare(b.tmp_sort_1 ?? '')
);
}
return results;

View File

@@ -432,6 +432,20 @@ async function handle_force_reset() {
let show_append_modal = $state(false);
let modal_mode: 'append' | 'prepend' | 'auto' = $state('auto');
function handle_show_ai_tools() {
show_config_modal = false;
if (typeof window === 'undefined') return;
const entry_id = $lq__journal_entry_obj?.journal_entry_id ?? 'default';
const ai_tools_anchor_id = `journal_entry_ai_tools_${entry_id}`;
queueMicrotask(() => {
const ai_tools_el = document.getElementById(ai_tools_anchor_id);
ai_tools_el?.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
}
</script>
<div class="group/entry-view relative w-full min-w-0">
@@ -488,13 +502,16 @@ let modal_mode: 'append' | 'prepend' | 'auto' = $state('auto');
? 'ring-primary-500/40 ring-2 ring-inset'
: ''}">
{#if $ae_loc.edit_mode && $journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id] === 'current'}
<AE_Comp_Journal_Entry_AiTools
content={typeof tmp_entry_obj.content === 'string'
? tmp_entry_obj.content
: ''}
bind:summary={tmp_entry_obj.summary}
on_save={() => update_journal_entry()}
{log_lvl} />
<div
id={`journal_entry_ai_tools_${$lq__journal_entry_obj?.journal_entry_id ?? 'default'}`}>
<AE_Comp_Journal_Entry_AiTools
content={typeof tmp_entry_obj.content === 'string'
? tmp_entry_obj.content
: ''}
bind:summary={tmp_entry_obj.summary}
on_save={() => update_journal_entry()}
{log_lvl} />
</div>
{/if}
<AE_Comp_Journal_Entry_Editor
@@ -539,6 +556,7 @@ let modal_mode: 'append' | 'prepend' | 'auto' = $state('auto');
on_save={() => update_journal_entry()}
on_force_reset={handle_force_reset}
{on_show_export}
on_show_ai={handle_show_ai_tools}
on_append={() => {
modal_mode = 'append';
show_append_modal = true;

View File

@@ -37,6 +37,7 @@ interface Props {
on_save: () => void;
on_force_reset?: () => void;
on_show_export?: () => void;
on_show_ai?: () => void;
on_append?: () => void;
on_prepend?: () => void;
}
@@ -49,6 +50,7 @@ let {
on_save,
on_force_reset,
on_show_export,
on_show_ai,
on_append,
on_prepend
}: Props = $props();
@@ -254,6 +256,16 @@ async function handle_admin_delete_action() {
<FileDown size="1.2em" />
Export Entry
</button>
<button
type="button"
class="btn preset-tonal-primary w-full justify-start gap-2"
onclick={() => {
show = false;
on_show_ai?.();
}}>
<Zap size="1.2em" />
Summary AI
</button>
<button
type="button"
class="btn preset-tonal-surface w-full justify-start gap-2"

View File

@@ -3,7 +3,7 @@ import { onMount } from 'svelte';
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
import { load_ae_obj_id__hosted_file } from '$lib/ae_core/core__hosted_files';
import AE_Comp_Hosted_Files_Download_Button from '$lib/ae_core/ae_comp__hosted_files_download_button.svelte';
import * as Lucide from 'lucide-svelte';
import * as Lucide from '@lucide/svelte';
let hosted_files: any[] = $state([]);
let large_file_obj: any = $state(null);

View File

@@ -45,13 +45,4 @@ declare module '@lucide/svelte' {
export default _default;
}
declare module 'lucide-svelte' {
import type { SvelteComponentTyped } from 'svelte';
export class IconBase extends SvelteComponentTyped<Record<string, any>, Record<string, any>, Record<string, any>> {}
export class Star extends IconBase {}
export class User extends IconBase {}
const _default: { [key: string]: typeof IconBase | any };
export default _default;
}
export {};