- AE__Architecture.md: Add section 7 -- Runtime Environment: Browser vs Electron. Electron is ONLY for Events Pres Mgmt Launcher. Badge printing (and everything else) works via standard browser window.print(), no Electron needed. - MODULE__AE_Events_Badges.md rev 3: Update print section to browser-first approach, remove Electron from print workflow, add two new test lessons (flat API URLs, CSS attribute vs DOM property), mark all tests passing. - TODO__Agents.md: Add completed data integrity test fixes item, add window.print() wiring to upcoming tasks, expand input field audit note.
23 KiB
MODULE: Aether Events — Badges
Module Path: src/routes/events/[event_id]/(badges)/badges/
API Module: src/lib/ae_events/ae_events__event_badge.ts
Database: db_events.badge (Dexie IndexedDB table)
Last Updated: 2026-02-26 (rev 3)
Overview
The Badges module manages event attendee badges with support for:
- External system imports (iMIS, Zoom, Novi, Impexium, Confex, Cvent, and others)
- Field override protection to prevent staff/attendee edits from being overwritten by automated syncs
- Multi-tier access control for field editing
- QR code generation for badge scanning
- Print tracking (count, first/last print datetime)
- Advanced search and filtering
Critical Design Pattern: Override Fields
Purpose
The *_override fields pattern protects data from being overwritten during scheduled cron syncs from external systems. This is essential because:
- Staff may need to correct imported data
- Attendees may be allowed to self-update certain fields (e.g., preferred name, pronouns)
- External systems often have outdated or incorrect data
- Changes should persist across multiple sync cycles
How It Works
Import Behavior:
External System → Aether API → Populates REGULAR fields only
(never touches *_override fields)
Display Behavior:
UI Display Logic:
1. IF `*_override` field has value → USE IT (highest priority)
2. ELSE IF regular field has value → USE IT (fallback)
3. ELSE → Display placeholder/empty
Example — Full Name:
// API imports from iMIS
badge.given_name = "Robert"
badge.family_name = "Smith"
badge.full_name = "Robert Smith" // Auto-computed
// Staff edits to preferred name
badge.full_name_override = "Bob Smith"
// Display in UI
display_name = badge.full_name_override || badge.full_name || "-- no name --"
// Result: "Bob Smith"
// Next cron sync from iMIS
// ✅ badge.full_name updated to "Robert J. Smith" (middle initial added)
// ✅ badge.full_name_override remains "Bob Smith" (PROTECTED)
// ✅ Display still shows "Bob Smith"
Override Fields (7 total)
| Regular Field | Override Field | Purpose | Editable By |
|---|---|---|---|
professional_title |
professional_title_override |
Job title display | Staff, Attendee |
full_name |
full_name_override |
Preferred name, pronouns | Staff, Attendee |
affiliations |
affiliations_override |
Organization display | Staff, Attendee |
email |
email_override |
Contact email override | Staff only |
location |
location_override |
City/State/Country display | Staff, Attendee |
badge_type |
badge_type_override |
Badge category label (varies per Event/Template) | Staff only |
badge_type_code |
badge_type_code_override |
Badge access level code (varies per Event/Template) | Staff only |
Sync Safety Rules
Automated Sync (Cron Jobs):
- ✅ CAN update: All regular fields (
given_name,family_name,email,affiliations, etc.) - ❌ CANNOT update: Any
*_overridefield - ❌ CANNOT delete: Any
*_overridevalue
Manual Staff Edit:
- ✅ CAN update: Any field (including overrides)
- ✅ CAN clear: Override fields (reverts to regular field)
Attendee Self-Service Edit:
- ✅ CAN update: Only specific override fields (per event config)
- ✅ CAN clear: Their own override fields
- ❌ CANNOT edit: Regular fields, badge_type, email_override
External System Integration
Supported Import Sources
- iMIS (Association Management)
- Zoom (Virtual event registration)
- Novi AMS (Association Management)
- Impexium (Association Management)
- Confex (Event abstract management)
- Cvent (Event registration)
- Custom CSV/Excel imports
Data Flow Direction
External Systems ─────────> Aether
(READ ONLY) (WRITE + DISPLAY)
Important: Aether is pull-only — does not push changes back to external systems. This prevents sync conflicts and maintains external systems as the source of truth for base data.
Sync Behavior
- Frequency: Scheduled cron jobs (typically hourly, daily, or on-demand)
- Method: Full sync or incremental (depends on external system API)
- Conflict Resolution: Override fields always win
Pseudocode:
def sync_badge_from_external(external_badge_data, existing_badge):
# Update regular fields from external source
existing_badge.given_name = external_badge_data.first_name
existing_badge.family_name = external_badge_data.last_name
existing_badge.email = external_badge_data.email
existing_badge.affiliations = external_badge_data.organization
existing_badge.badge_type_code = external_badge_data.registration_type
# NEVER TOUCH OVERRIDE FIELDS
# existing_badge.full_name_override ← PROTECTED
# existing_badge.affiliations_override ← PROTECTED
# existing_badge.email_override ← PROTECTED
return existing_badge
Access Control & Edit Permissions
Access Levels (Ascending)
- Anonymous — No access to badges
- Public — View public event info only (no badge access)
- Authenticated — View own badge, limited self-edit
- Trusted — Search all badges, view all, edit own
- Administrator — Full CRUD, bulk operations, override any field
- Manager — All administrator + event configuration
- Super — All manager + cross-event operations
Current Implementation (v3)
Quick Edit Feature ([badge_id]/+page.svelte → ae_comp__badge_obj_view.svelte)
Edit Mode Trigger:
- URL hash
#reviewenables edit mode - Sets
$ae_loc.edit_mode = true - Shows Save/Cancel buttons
Currently Editable Fields:
// Lines 90-96 in ae_comp__badge_obj_view.svelte
editable_full_name_override: string | null
editable_professional_title_override: string | null
editable_affiliations_override: string | null // textarea
editable_location_override: string | null
editable_allow_tracking: boolean | null
editable_email: string | null
editable_badge_type_code: string | null
UI Components:
- Input fields shown when
edit_mode_active === true - Save button calls
handle_save_changes()(Line 365) - Cancel button calls
handle_cancel_changes()(Line 450) - Only changed fields sent to API (
update_ae_obj__event_badge)
Future Planned Enhancement
Event-Level Configuration: event.mod_badges_json.edit_permissions
{
"authenticated": {
"can_edit": ["full_name_override", "professional_title_override", "affiliations_override", "location_override"],
"requires_approval": false
},
"trusted": {
"can_edit": ["full_name_override", "professional_title_override", "affiliations_override", "location_override", "email_override"],
"can_view_all": true,
"requires_approval": false
},
"administrator": {
"can_edit": "*",
"can_bulk_edit": true,
"can_delete": true
}
}
Status: Placeholder UI exists, JSON config not yet implemented.
Search & Filter Capabilities
Search Component
File: ae_comp__badge_search.svelte
Multi-Word Search Fix (2026-02-26)
Fulltext search now correctly handles multi-word queries by splitting on whitespace and applying AND logic per word:
// "scott idem" → LIKE '%scott%' AND LIKE '%idem%'
// Previously: LIKE '%scott idem%' (failed to match)
const words = qry.split(/\s+/).filter(w => w.length > 0);
for (const word of words) {
search_query.and.push({ field: 'default_qry_str', op: 'like', value: `%${word}%` });
}
Committed: dc0f3066
Available Filters
Fulltext Search (All Users)
- Searches:
default_qry_strdatabase field - Includes: Name, email, external IDs
- Type:
LIKE %query%(case-insensitive) - Trigger: Enter key or 3+ characters typed
Advanced Filters (Trusted Access & Above)
// Badge Type Filter
badge_type_code: 'current_member' | 'inactive_member' | 'ex_all' | 'staff' | etc.
// Note: Badge types are defined per Event and Event Badge Template in database table records.
// Common types include: member, nonmember, guest, exhibitor, staff
// This is a work in progress - types vary by event configuration.
// Print Status Filter
qry_printed_status: 'all' | 'printed' | 'not_printed'
// Affiliations Search
qry_affiliations: string // Separate filter for organization search
// Sort Options
qry_sort_order:
- 'name_asc' / 'name_desc'
- 'updated_desc' / 'updated_asc'
- 'print_count_desc'
- 'print_first_desc' / 'print_last_desc'
- 'badge_type_asc'
- 'affiliations_asc'
QR Scan Search
- Scans badge QR code
- Extracts badge ID
- Auto-fills search with ID
- Jumps to badge detail view
Search Implementation Pattern
File: badges/+page.svelte (Lines 117-365)
Strategy: Standardized Reactive Search Pattern (Aether UI V3)
- Isolate dependencies into stable
$derivedobject - Debounced effect (300ms) triggers search
- Fast Path: Search IDB first (if not
remote_first) - Revalidate: API request updates IDB
- LiveQuery: UI auto-updates from IDB changes
Search API: events_func.search__event_badge()
await search__event_badge({
api_cfg: $ae_api,
event_id: event_id,
fulltext_search_qry_str: qry_str || null,
type_code: type_code || null,
printed_status: printed_status,
affiliations_qry_str: aff_str || null,
order_by_li: order_by_li,
limit: 150,
log_lvl: 0
})
Badge Display Logic
Name Display Priority
// Component: ae_comp__badge_obj_li.svelte (Lines 113-121)
if (event_badge_obj?.full_name_override)
display: full_name_override
else if (event_badge_obj?.full_name)
display: full_name
else
display: given_name + ' ' + family_name
Badge View Page
Route: /events/[event_id]/badges/[badge_id]
Components:
+page.svelte— Container with LiveQuery for badge dataae_comp__badge_obj_view.svelte— Full badge display + edit UI
LiveQueries:
lq__event_badge_obj = liveQuery(() => db_events.badge.get(event_badge_id))
lq__event_badge_template_obj = liveQuery(() =>
db_events.badge_template.get(badge.event_badge_template_id)
)
Loading States:
is_loading_idb— Waiting for initial IDB lookup- If badge not found → "Badge Not Found" error with reload button
- Loader spinner while fetching
Badge Templates
Purpose
Badge templates define the visual layout and content structure for printed badges:
- Header images/logos
- Field positions and font sizes
- QR code placement
- Ticket/option indicator display
- WiFi credentials display
Template Selection
Each badge references an event_badge_template_id. The template controls:
- Layout (front/back)
- Branding elements
- Which fields to show
- Field formatting rules
Template Loading
Templates are loaded alongside badges via inc_template parameter:
load_ae_obj_id__event_badge({
event_badge_id: badge_id,
inc_template: true // Also loads template
})
Print Tracking
Print Fields
print_count: number // Increments each print
print_first_datetime: string // ISO datetime of first print
print_last_datetime: string // ISO datetime of most recent print
Print Button (Implemented 2026-02-26)
The handle_print_badge() function in ae_comp__badge_obj_view.svelte increments the count and records timestamps:
async function handle_print_badge() {
const now = new Date().toISOString();
const current_print_count = $lq__event_badge_obj.print_count ?? 0;
const data_to_update = {
print_count: current_print_count + 1,
print_last_datetime: now
};
if (current_print_count === 0) {
data_to_update.print_first_datetime = now; // Only set on first print
}
await events_func.update_ae_obj__event_badge({ ... });
}
Button has data-testid="badge-print-btn" and shows loading/done/error states with icon feedback.
Print Workflow
- Pre-Print: Check
print_countto warn if already printed - Print:
window.print()— standard browser print dialog. Works well in Chrome, Chromium, and Firefox. Chrome is the recommended browser for onsite badge printing (most stable in production). - Post-Print: Handled by
handle_print_badge()— count + timestamps updated - Audit: Print history available for staff review
Browser vs Electron: Badge printing does NOT require the Electron native app. The standard browser print dialog works well across Chrome, Chromium, and Firefox. The Electron native app is specialized for the Events Pres Mgmt Launcher only and should not be assumed available for badge stations.
Current Status: Button records print event and updates IDB. The window.print() call still needs to be wired to the button — see Future Enhancements.
Database Schema
IndexedDB Table: badge
File: src/lib/ae_events/db_events.ts (Lines 841-852)
Indexed Fields:
badge: `
event_badge_id_random, event_badge_id, id,
event_id, event_id_random,
full_name, full_name_override, email, email_override,
affiliations, affiliations_override,
badge_type, badge_type_code, badge_type_code_override, badge_type_override,
external_event_id, external_id, external_person_id,
default_qry_str,
alert,
tmp_sort_1, tmp_sort_2,
print_count, print_first_datetime, print_last_datetime,
enable, hide, priority, sort, group, notes, created_on, updated_on
`
Saved Properties
File: ae_events__event_badge.ts (Lines 495-563)
Complete field list (67 fields total):
- Identity:
id,event_badge_id,event_id,event_badge_template_id - Name:
pronouns,informal_name,title_names,given_name,middle_name,family_name,designations - Professional:
professional_title,professional_title_override - Display:
full_name,full_name_override - Organization:
affiliations,affiliations_override - Contact:
email,email_override - Address:
address_line_1,address_line_2,address_line_3,city,country_subdivision_code,state_province,state_province_abb,postal_code,country_alpha_2_code,country,full_address - Location:
location,location_override - Classification:
badge_type,badge_type_code,badge_type_override,badge_type_code_override - External:
external_event_id,external_id,external_person_id - Search:
query_str,default_qry_str - System:
alert,enable,hide,priority,sort,group,notes,created_on,updated_on - Print:
print_count,print_first_datetime,print_last_datetime - Sorting:
tmp_sort_1,tmp_sort_2 - Person Link:
person_external_id,person_external_sys_id,person_given_name,person_family_name,person_full_name,person_professional_title,person_affiliations,person_primary_email,person_passcode
API Functions
CRUD Operations
File: src/lib/ae_events/ae_events__event_badge.ts
// Load single badge
load_ae_obj_id__event_badge({ event_badge_id, event_id, inc_template })
// Load badge list
load_ae_obj_li__event_badge({ event_id, view, limit, order_by_li })
// Search badges (V3 API)
search__event_badge({
event_id,
fulltext_search_qry_str,
type_code,
printed_status,
affiliations_qry_str,
order_by_li
})
// Create badge
create_ae_obj__event_badge({ event_id, data_kv })
// Update badge
update_ae_obj__event_badge({ event_badge_id, event_id, data_kv })
// Delete badge
delete_ae_obj_id__event_badge({ event_badge_id, event_id, method })
Field Processing
Function: process_ae_obj__event_badge_props()
Processing Steps:
- Map
*_randomfields to clean names (event_badge_id_random→event_badge_id) - Set primary
idfield fromevent_badge_id - Ensure
event_idis set (from function parameter if missing) - Calculate
tmp_sort_1andtmp_sort_2for efficient sorting - Return processed objects
Critical Fix (2026-02-26): All CRUD functions now return processed data (matches IDB cache) instead of raw API responses. This ensures consistency between function return values and cached data.
Component Architecture
Route Structure
/events/[event_id]/(badges)/badges/
├── +layout.svelte # Layout wrapper (minimal)
├── +page.svelte # Badge list + search
├── ae_comp__badge_search.svelte # Search form + filters
├── ae_comp__badge_obj_li.svelte # Badge list display
├── ae_comp__badge_create_form.svelte # (Not actively used)
├── ae_comp__badge_upload_form.svelte # Bulk CSV upload
└── [badge_id]/
├── +page.svelte # Badge detail container
├── +page.ts # Badge loader (non-blocking)
└── ae_comp__badge_obj_view.svelte # Badge display + edit
Key Components
Badge List Page (+page.svelte)
- LiveQuery: Reactive badge list from IDB
- Search Pattern: Debounced search with fast path + revalidation
- ID List:
event_badge_id_lidrives LiveQuery - Loading State: Shows spinner when
search_status === 'loading'
Badge Search (ae_comp__badge_search.svelte)
- Form Mode: Toggle between search form and QR scanner
- Filters: Badge type, print status, affiliations, sort order (trusted+ only)
- Fulltext: Name/email search (all users)
- QR Scan: Integrated QR scanner for badge ID lookup
Badge List Display (ae_comp__badge_obj_li.svelte)
- Visibility Filter: Respects
hideflag (trusted+ sees all) - Display Logic: Override → regular → fallback pattern
- Print Indicator: Green checkmark badge shows
print_count - Metadata: ID, created/updated timestamps (edit mode only)
Badge Detail View (ae_comp__badge_obj_view.svelte)
- Edit Mode: Activated by edit button (or
#reviewURL hash for future self-service) - Condition: Renders only when BOTH
$lq__event_badge_objAND$lq__event_badge_template_objare non-null - Form Binding: Direct
bind:valueon editable fields - Dynamic Sizing: Font size adjusts based on text length
- Print Preview: Full badge layout with template
- Save Handler: Only sends changed fields to API
data-testidattributes:badge-edit-btn,badge-save-btn,badge-cancel-btn,badge-print-btn,badge-professional-title-input— use these in tests
Testing Status
Current Test Coverage
- ✅ Badge list loads (all 6 data integrity tests passing)
- ✅ Badge template list loads and displays
- ✅ Badge template form renders and populates correctly
- ✅ Badge template values persist in edit form
- ✅ Electron bridge compatibility (graceful degradation in browser)
- ✅ Badge field processor handles missing optional fields
- ✅ Badge type filter tests
- ✅ Badge template relationship tests
- ✅ Attendee workflow test — navigate → edit professional title → print → return (
d1ded2d4)
Key Test Lessons Learned
Search API path is FLAT, not nested. search_ae_obj_v3 builds /v3/crud/{obj_type}/search — always flat regardless of the parent relationship. Mocks must match this:
// CORRECT — flat path
url.includes('/v3/crud/event_badge/search') && method === 'POST'
// WRONG — nested path, mock will never fire
url.includes(`/v3/crud/event/${event_id}/event_badge/search`) && method === 'POST'
List API (GET) is also FLAT with query params. get_ae_obj_li_v3 builds /v3/crud/{obj_type}/?for_obj_id=... — always flat. Mocks must check url.includes('/v3/crud/event_badge_template/') && url.includes('for_obj_id').
CSS input[value*=...] selectors don't work with Svelte bind:value. The CSS selector checks the HTML attribute; Svelte's bind:value sets the DOM property only. In Playwright tests, use page.getByLabel() or locator.inputValue() instead.
Dexie requires _random ID fields. Badge objects saved to IDB must include:
event_badge_id_random: string // Must be present or Dexie skips the object
id_random: string // Also checked
// Error: "Object is missing a valid ID for table 'badge'"
All API mock responses in tests need these fields.
Badge view requires both badge AND template. ae_comp__badge_obj_view.svelte wraps everything in {#if $lq__event_badge_obj && $lq__event_badge_template_obj} — if the template isn't loaded, edit/print buttons and the badge itself don't render. Tests must mock the badge template endpoint.
Badge GET endpoint (single object): /v3/crud/event_badge/{id} (NOT nested under event). Matches api.get_ae_obj_v3() which uses the flat path.
Badge PATCH endpoint (update): /v3/crud/event/${event_id}/event_badge/${badge_id} (nested under event). Matches api.patch_ae_obj_v3() which uses the nested path.
Use data-testid for test selectors. Key buttons have targets: badge-edit-btn, badge-save-btn, badge-cancel-btn, badge-print-btn, badge-professional-title-input.
Remaining Test Issues
None — all current badge tests passing as of 2026-02-26 (f5e98b8c).
Known Issues & Future Enhancements
Known Issues
- Test Infrastructure: Mock API routes not connecting to page requests
- Session Cold-Start: Potential race condition on first load (same as pres mgmt module)
- Type Definitions: Some TypeScript errors on external package types (pre-existing)
Future Enhancements
- Access-Based Edit Permissions: Implement JSON config for field-level access control
- Print Functionality: Wire up
window.print()to the print button. Standard browser print API — works well in Chrome/Chromium/Firefox for badge label printing. Electron is NOT needed. - Batch Operations: Bulk update, bulk print, bulk export
- Audit Log: Track who edited which fields and when
- Photo Badges: Support badge photo upload and display
- Custom Badge Layouts: Dynamic template selection per badge type
- Real-Time Sync: WebSocket updates for multi-device badge printing stations
- Approval Workflow: Require manager approval for certain field changes
Development Guidelines
Adding New Override Fields
- Add
{field}_overrideto database schema - Add to
properties_to_savearray inae_events__event_badge.ts - Update display logic to check override first
- Add to editable fields in
ae_comp__badge_obj_view.svelte - Update access control config
- Document in this file
Testing Override Fields
// Simulate external sync
badge.given_name = "External Value"
// User edits
badge.given_name_override = "User Value"
// Next sync (should NOT change override)
badge.given_name = "Updated External Value"
// Display should still show "User Value"
assert(display === badge.given_name_override)
Debugging Search Issues
// Enable search logging
log_lvl: 2
// Check search params object
console.log('Search params:', search_params)
// Verify API request
console.log('API request:', { event_id, fulltext_search_qry_str, type_code })
// Check returned IDs
console.log('Badge IDs:', event_badge_id_li)
// Verify IDB contents
db_events.badge.toArray().then(console.log)
Related Documentation
Document Status: ✅ Complete
Last Verified: 2026-02-26 (rev 3 — all badge tests passing)
Verified Against: Code commit f5e98b8c (all data integrity tests passing)