20 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
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
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 Workflow (Future)
- Pre-Print: Check
print_countto warn if already printed - Print: Send to printer via Electron native bridge or browser print
- Post-Print: Increment
print_count, updateprint_last_datetime - Audit: Print history available for staff review
Current Status: UI placeholder for print button, tracking fields exist in database, print functionality pending.
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
#reviewURL hash or edit button - 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
Testing Status
Current Test Coverage
- ❌ Badge list cold-start (failing — mock API issue)
- ❌ Badge data integrity (failing — mock API issue)
- ❌ Badge template list (failing — route mismatch)
- ✅ Badge type filter tests
- ✅ Badge template relationship tests
- ✅ Electron bridge compatibility (graceful degradation)
Test Issues
Root Cause: Mock API routes not matching actual request patterns. Tests provide correct mock data but page shows "No badges found".
Browser Error: Object is missing a valid ID for table "badge" — Mock data reaching IDB with only {tmp_sort_1, tmp_sort_2, event_id}, all other fields stripped.
Fix Required: Debug actual API request URLs, adjust mock route patterns.
Manual Testing Recommended: Navigate to /events/{event_id}/badges in browser to verify actual functionality before fixing test infrastructure.
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 print button to Electron native print or browser print API
- 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
Verified Against: Code commit b28595da (badge data fixes)