docs(events): reorganize badges and leads documentation

Standardized documentation structure for Badges and Leads modules into
focused technical references and practical onsite guides.

- Refined MODULE__AE_Events_Badges.md (Core data integrity & sync logic)
- Renamed MODULE__AE_Events_Exhibitor_Leads.md to MODULE__AE_Events_Leads.md
- Renamed MODULE__AE_Events_Badges_Onsite.md to GUIDE__AE_Events_Badges_Onsite.md
- Expanded GUIDE__AE_Events_Onsite_Runbook.md with Badge and Leads sections.
- Maintained all critical business logic, including the 'Override Fields' pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-21 22:39:57 -04:00
parent cb767ed115
commit 518a450b91
4 changed files with 101 additions and 799 deletions

View File

@@ -1,11 +1,50 @@
# Guide — Aether Events: Onsite Runbook # Guide — Aether Events: Onsite Runbook
This guide covers the human-centric logistics and "In the Heat of the Moment" support for onsite event operations using Presentation Management and the Launcher. This guide covers the human-centric logistics and "In the Heat of the Moment" support for onsite event operations.
---
## Badge Printing
Aether badge printing uses the browser's native `window.print()` — no special software or print
server needed.
### Kiosk Station Setup
- **Browser:** Use **Chrome (Chromium)** for all kiosk stations.
- **Settings:** Set Margins to **None**. Enable **Background Graphics**.
- **Mode:** Use normal browser sessions (not Incognito) to allow PWA caching.
### Printer Reference: Zebra ZC10L (PVC)
- **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.
### Printing Workflow
1. **Search:** Find the attendee by name or QR scan in the Badges module.
2. **Review:** Open the print page and confirm the layout looks correct.
3. **Print:** Click **Print Badge**. `print_count` increments automatically.
4. **Handoff:** Verify the card print quality before handing it to the attendee.
---
## Exhibitor Leads (Lead Retrieval)
Exhibitors use a PWA (Progressive Web App) to scan badges and capture leads.
### Exhibitor Support Workflow
1. **Booth Lookup:** Help the exhibitor find their booth in the Leads landing page.
2. **Sign-In:** Assist with the **Shared Passcode** or individual **Licensed User** login.
3. **App Install:** Encourage them to "Add to Home Screen" (iOS) or click the Install button (Android/Chrome) for offline stability.
4. **Scanning Demo:** Show them the **Rapid Scan** mode. Remind them that attendees must have `allow_tracking = true` on their record to be scanned.
### Managing Licenses
- License counts are managed in the **Manage** tab (Admin or Shared Passcode only).
- If an exhibitor needs more staff slots, update the `license_max` in the Exhibit record.
--- ---
## Speaker Ready Room (SRR) ## Speaker Ready Room (SRR)
... (rest of the file) ...
The SRR is the central hub for content management and presenter support. The SRR is the central hub for content management and presenter support.
### SRR Practice Stations ### SRR Practice Stations

View File

@@ -1,843 +1,106 @@
# MODULE: Aether Events — Badges # Aether Events — Badges
**Module Path:** `src/routes/events/[event_id]/(badges)/badges/` The Badges module manages event attendee records and their physical badge configurations. It supports multi-source imports, field protection for onsite edits, and multi-tier access control for self-service review.
**API Module:** `src/lib/ae_events/ae_events__event_badge.ts`
**Database:** `db_events.badge` (Dexie IndexedDB table)
**Last Updated:** 2026-02-27 (rev 6)
**Related Docs:** `documentation/PROJECT__AE_Events_Badges_Review_Print.md` (implementation guide)
--- ---
## Overview ## Data Model & Hierarchy
The Badges module manages event attendee badges with support for: ### Core Objects
- **External system imports as needed** (CSV/Excel, iMIS, Zoom, Novi, Impexium, Confex, Cvent, and others) - **Event Badge** (`event_badge`): The attendee record containing name, title, affiliations, and tracking flags.
- **Field override protection** to prevent staff/attendee edits from being overwritten by automated syncs - **Badge Template** (`event_badge_template`): The visual and structural configuration for printing (branding, layout, QR placement).
- **Multi-tier access control** for field editing
- **QR code generation** for badge scanning ### Relationships
- **Print tracking** (count, first/last print datetime) - **Badge → Event:** Many-to-one.
- **Advanced search and filtering** - **Badge → Template:** Many-to-one (via `event_badge_template_id`).
- **HTML rendering** in display fields for rich text formatting - **Badge → Person:** Optional link to core Aether Person record for unified profiles.
- **Accessibility features** (text enlargement toggle)
--- ---
## Critical Design Pattern: Override Fields ## Critical Design Pattern: Override Fields
### Purpose ### Purpose
The `*_override` fields pattern protects data from being overwritten during scheduled cron syncs from external systems. This is essential because: The `*_override` fields pattern (established in 2018) protects data from being overwritten during scheduled cron syncs from external systems (iMIS, Novi, etc.). This ensures that staff corrections or attendee self-updates persist across multiple sync cycles.
1. Staff may need to correct imported data
2. Attendees may be allowed to self-update certain fields (e.g., preferred name, pronouns)
3. External systems often have outdated or incorrect data
4. Changes should persist across multiple sync cycles
### How It Works ### How It Works
1. **Import:** External systems populate **REGULAR** fields only.
2. **Display Logic:** The UI displays the `*_override` field if it has a value; otherwise, it falls back to the regular field.
3. **HTML Rendering:** Certain display fields (Name, Title, Affiliations, Location) support HTML markup for rich text formatting (bold, italics, line breaks) on the physical badge.
**Import Behavior:** ### Standard Override Pairs
```
External System → Aether API → Populates REGULAR fields only
(never touches *_override fields)
```
**Display Behavior:** | Regular Field | Override Field | Editable By | HTML? |
``` |---|---|---|---|
UI Display Logic: | `full_name` | `full_name_override` | Staff, Attendee | ✅ |
1. IF `*_override` field has value → USE IT (highest priority) | `professional_title` | `professional_title_override` | Staff, Attendee | ✅ |
2. ELSE IF regular field has value → USE IT (fallback) | `affiliations` | `affiliations_override` | Staff, Attendee | ✅ |
3. ELSE → Display placeholder/empty | `location` | `location_override` | Staff, Attendee | ✅ |
``` | `email` | `email_override` | Staff Only | No |
| `badge_type` | `badge_type_override` | Staff Only | No |
**HTML Rendering (implemented 2026-02-27):**
Certain fields support HTML markup for rich text formatting. When viewing (not editing), these fields use Svelte's `{@html}` directive to render the markup:
- `full_name` / `full_name_override`
- `professional_title` / `professional_title_override`
- `affiliations` / `affiliations_override`
- `location` / `location_override`
This allows for formatting like:
- Bold/italic: `<b>Dr.</b> Jane Smith` or `<i>Chief Medical Officer</i>`
- Line breaks: `Hospital Name<br>Department Name`
- Special characters and entities
**Example — Full Name:**
```typescript
// API imports from iMIS
badge.given_name = "Robert"
badge.family_name = "Smith"
badge.full_name = "Robert Smith" // Auto-computed
// Staff edits to preferred name with HTML
badge.full_name_override = "<b>Bob</b> Smith"
// Display in UI (review form)
{@html badge.full_name_override || badge.full_name || "— no name —"}
// Result: **Bob** Smith (bold rendered)
// Edit mode shows raw HTML
<input bind:value={editable_full_name_override} />
// Shows: <b>Bob</b> Smith (editable as text)
// Next cron sync from iMIS
// ✅ badge.full_name updated to "Robert J. Smith" (middle initial added)
// ✅ badge.full_name_override remains "<b>Bob</b> Smith" (PROTECTED)
// ✅ Display still shows **Bob** Smith (bold rendered)
```
### Override Fields
| Regular Field | Override Field | Purpose | Editable By | HTML Rendering |
|---|---|---|---|---|
| `pronouns` | `pronouns_override` | Preferred pronouns | Staff, Attendee | No |
| `professional_title` | `professional_title_override` | Job title display | Staff, Attendee | ✅ Yes |
| `full_name` | `full_name_override` | Preferred name display | Staff, Attendee | ✅ Yes |
| `affiliations` | `affiliations_override` | Organization display | Staff, Attendee | ✅ Yes |
| `phone` | `phone_override` | Phone number | Staff, Attendee | No |
| `email` | `email_override` | Contact email override | Staff only | No |
| `location` | `location_override` | City/State/Country display | Staff, Attendee | ✅ Yes |
| `badge_type` | `badge_type_override` | Badge category label text | Staff only | No |
| `badge_type_code` | `badge_type_code_override` | Badge access level code | Staff only | No |
| `registration_type` | `registration_type_override` | Registration category label text | Staff only | No |
| `registration_type_code` | `registration_type_code_override` | Registration category code | Staff only | No |
> **Note:** `phone`, `phone_override`, `pronouns_override`, `registration_type`, `registration_type_code`, `registration_type_override`, `registration_type_code_override` may need to be confirmed against the DB schema via `ae_describe event_badge` and added to `properties_to_save` in `ae_events__event_badge.ts` if not already present.
### Sync Safety Rules
**Automated Sync (Cron Jobs):**
- ✅ CAN update: All regular fields (`given_name`, `family_name`, `email`, `affiliations`, etc.)
- ❌ CANNOT update: Any `*_override` field
- ❌ CANNOT delete: Any `*_override` value
**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 ## External System Integration
### Supported Import Sources Aether acts as a **Pull-Only** consumer for registration data. It does not push changes back to external systems, maintaining them as the source of truth for base registration while Aether handles the "Onsite Truth."
- **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 ### Supported Sources
``` - **iMIS**, **Novi AMS**, **Impexium** (Associations)
External Systems ─────────> Aether - **Zoom**, **Cvent** (Registrations)
(READ ONLY) (WRITE + DISPLAY) - **Confex** (Abstracts/Presenters)
``` - **Custom CSV/Excel**
**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:**
```python
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 Control & Permissions
### Access Levels (Ascending) | Level | Access |
1. **Anonymous** — No access to badges |---|---|
2. **Public** View public event info only (no badge access) | **Authenticated** | View own badge, limited self-edit (overrides only). |
3. **Authenticated** — View own badge, limited self-edit | **Trusted** | Search all badges, view all, reprint existing badges. |
4. **Trusted** — Search all badges, view all, edit own | **Administrator** | Full CRUD, bulk operations, override any field. |
5. **Administrator** — Full CRUD, bulk operations, override any field | **Manager** | All Admin + Event/Template configuration. |
6. **Manager** — All administrator + event configuration
7. **Super** — All manager + cross-event operations
### Current Implementation (v3) — 2026-02-27 ### Attendee Self-Service (`/review`)
Attendees can access their own record via a passcode-gated link (typically `?passcode=...`). This allows them to verify their info and provide preferred name/title overrides before printing.
#### Badge Search Results Visibility
| Access Level | Sees |
| --- | --- |
| Below Trusted (incl. anonymous) | Only badges where `print_count < 1` and not hidden |
| Trusted, not Edit Mode | Only badges where `print_count < 1` and not hidden |
| Trusted + Edit Mode | All badges where `hide === false` (including already-printed) |
#### Print Button Behavior (per result row)
| Access Level | Print Action |
| --- | --- |
| Below Trusted | No print action — name shown with User icon, non-interactive |
| Trusted, `print_count < 1` | Clickable link → `/print` page, Printer icon |
| Trusted, `print_count >= 1`, not Edit Mode | Disabled (already printed safety lock), shows `Nx` count |
| Trusted, `print_count >= 1`, Edit Mode | Clickable reprint — shows `Nx` count badge next to icon |
Print count displayed as `[Printer][2×] Name` when `print_count >= 1`.
#### Review Area Buttons (per result row, up to 3 buttons total)
| Button | Visible To | Behavior |
| --- | --- | --- |
| Email Review Link | All users | Placeholder `alert()` — will trigger email API |
| Review Link (clipboard) | Trusted + Edit Mode only | Copies `/review` URL to clipboard; shows `Copied!` feedback |
| *(direct Review link)* | *(future)* | *(not yet implemented as separate nav button)* |
#### Badge Edit Form (`ae_comp__badge_obj_view.svelte`)
**Currently editable fields (local `edit_mode_active`, not global `edit_mode`):**
```typescript
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
```
- Save button → `handle_save_changes()` — only changed fields sent to API
- Cancel button → `handle_cancel_changes()` — reverts to IDB values
- **IMPORTANT:** This component must NEVER write to `$ae_loc.edit_mode` — it uses its own local `edit_mode_active` flag only. (Bug fixed 2026-02-27)
#### Badge Review Form (`ae_comp__badge_review_form.svelte`)
Form-based review (NOT a badge render). Used by the `/review` page.
**Props:**
- `can_edit_fields: string[]` prop controls which fields are editable per user level
- `['*']` = administrator (all fields)
- `is_staff: boolean` prop shows/hides the staff-only fields
- Fields show "(overridden)" label when an override value differs from the base field
**Features (implemented 2026-02-27):**
- **HTML Rendering**: `full_name_override`, `professional_title_override`, `affiliations_override`, and `location_override` fields render HTML markup using `{@html}` directive when viewing (not when editing)
- **Accessibility**: Text enlargement toggle button switches between text-2xl (normal) and text-4xl (enlarged) for improved readability
- **Help Modal**: Flowbite Modal component with 6 help sections (Reviewing Badge, Editing Info, Accessibility, QR Code, Lead Scanning, Assistance)
- **QR Code Display**: Generates QR code using `core_func.js_generate_qr_code()` with badge ID, supports hover zoom and click-to-expand
- **Print Status**: Shows print count, first print datetime, and last print datetime at top of form
- **Local Edit Mode**: Independent `local_edit_active` state (never writes to `$ae_loc.edit_mode`)
- **Save/Cancel**: Only changed fields sent to API; revert button for override fields
**Editable Fields:**
- Pronouns, Full Name, Professional Title, Affiliations, Phone, Location (all with override support)
- Allow Tracking checkbox (lead scanning permission)
- Staff-only: Email, Badge Type, Registration Type, Hide, Priority, Notes
- Staff-only: Options (`other_1_code` through `other_8_code`) and Tickets (`ticket_1_code` through `ticket_8_code`)
- Agree to Terms & Conditions checkbox (attendee-visible when in can_edit_fields)
#### Badge Review Page — Header Buttons (implemented 2026-02-27)
| Button | Visible To | Behavior |
| --- | --- | --- |
| Back → Search (ArrowLeft) | Staff (`has_staff_access`) only | `<a href="/events/{id}/badges">` |
| Print (Printer icon) | Trusted+, not printed OR Trusted+Edit if printed | `<a href="/print">`, shows `Nx` count if reprinting |
| Copy Link (clipboard) | Trusted + Edit Mode only | Copies review URL to clipboard; `Copied!` feedback for 2s |
| Email Link (Mail icon) | All if not printed; Trusted+Edit if printed | Placeholder `alert()` — email API pending |
#### Badge Print Page — Header Buttons (implemented 2026-02-27)
| Button | Visible To | Behavior |
| --- | --- | --- |
| Back → Search (ArrowLeft) | Always (when badge loaded) | `<a href="/events/{id}/badges">` |
| Print Now (Printer icon) | Trusted+, not printed OR Trusted+Edit if printed | Calls `window.print()` directly (convenience duplicate); print count tracked by component button |
| Review (Eye icon) | Trusted + Edit Mode only | `<a href="/review">` nav link |
| Email Link (Mail icon) | All if not printed; Trusted+Edit if printed | Placeholder `alert()` — email API pending |
#### Badge Review Page — Display Sections (implemented 2026-02-27)
The review form (`ae_comp__badge_review_form.svelte`) displays:
1. **Print status** ✅ — print count + first/last print timestamps (read-only, hidden if never printed)
2. **QR Code** ✅ — the attendee's badge QR code for scanning at the badge kiosk (for automatic badge search + print flow). Generated using `core_func.js_generate_qr_code()` with `obj_type: 'event_badge'` and badge ID. Supports hover zoom overlay and click-to-expand.
3. **Editable Fields** ✅ — all fields with access-level gating, override support, and HTML rendering for display fields
4. **Options** ✅ (`other_1_code` through `other_8_code`) — Staff: editable text inputs; Attendees: shown as `[✓] Option X` checkmark display only when value exists
5. **Tickets** ✅ (`ticket_1_code` through `ticket_8_code`) — Staff: editable text inputs; Attendees: shown as `[✓] Ticket X` checkmark display only when value exists
6. **Accessibility Toggle** ✅ — Font size enlargement button in sticky header (text-2xl ↔ text-4xl)
7. **Help Modal** ✅ — Attendee guidance modal with 6 sections explaining the review process, editing, QR codes, and lead scanning
#### Default Field Permissions (hardcoded for now — Axonius first show, mid-April 2026)
These are hardcoded in `review/+page.svelte` pending connection to `mod_badges_json.edit_permissions`.
**Attendee (passcode-authenticated / anonymous with link):**
```typescript
[
'pronouns_override',
'full_name_override',
'professional_title_override',
'affiliations_override',
'phone_override',
'location_override',
'allow_tracking', // Exhibitor Leads opt-in
'agree_to_tc', // Terms & Conditions placeholder
]
```
**Trusted Staff and above:**
```typescript
[
'pronouns_override',
'full_name_override',
'professional_title_override',
'affiliations_override',
'email_override',
'phone_override',
'location_override',
'badge_type_code_override', // + badge_type_override (text label)
'registration_type_code_override', // + registration_type_override (text label)
'option_1' ... 'option_8', // i.e. other_1_code ... other_8_code
'ticket_1_code' ... 'ticket_8_code',
'allow_tracking',
'agree_to_tc',
'hide',
'priority',
'notes',
]
```
**Administrator**`can_edit_fields = ['*']` (all fields)
**Badge type options (hardcoded for now):** `member`, `non-member`, `guest`, `exhibitor`, `staff`, `test`
(In future: read from Event Badge Template's configured list)
**Registration type options:** Same list as badge type for now — identical select options.
#### Future: Per-Event Configuration
`event.mod_badges_json.edit_permissions` — placeholder settings UI exists in
`ae_comp__event_settings_badges_form.svelte`. Review page uses hardcoded defaults for now.
The settings form and review page are not yet connected.
```json
{
"authenticated": {
"can_edit": ["pronouns_override", "full_name_override", "professional_title_override", "affiliations_override", "phone_override", "location_override", "allow_tracking", "agree_to_tc"]
},
"trusted": {
"can_edit": ["*attendee_fields", "email_override", "badge_type_code_override", "registration_type_code_override", "option_x", "ticket_x_code", "allow_tracking", "agree_to_tc", "hide", "priority", "notes"]
}
}
```
--- ---
## Search & Filter Capabilities ## Search & Filter Capabilities
### Search Component - **Fulltext Search:** Matches against a consolidated `default_qry_str` (Name, email, IDs).
**File:** `ae_comp__badge_search.svelte` - **Multi-Word Logic:** Queries like "Scott Idem" are split and treated as `LIKE %Scott% AND LIKE %Idem%`.
- **QR Scan Search:** Scanning an attendee's QR code (from a confirmation email or old badge) immediately jumps to their record.
### Multi-Word Search Fix (2026-02-26) - **Advanced Filters:** Filter by Badge Type, Printed Status, or Affiliations (Staff only).
Fulltext search now correctly handles multi-word queries by splitting on whitespace and applying AND logic per word:
```typescript
// "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_str` database field
- Includes: Name, email, external IDs
- Type: `LIKE %query%` (case-insensitive)
- Trigger: Enter key or 3+ characters typed
**Advanced Filters** (Trusted Access & Above)
```typescript
// 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)
1. **Isolate dependencies** into stable `$derived` object
2. **Debounced effect** (300ms) triggers search
3. **Fast Path:** Search IDB first (if not `remote_first`)
4. **Revalidate:** API request updates IDB
5. **LiveQuery:** UI auto-updates from IDB changes
**Search API:** `events_func.search__event_badge()`
```typescript
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
```typescript
// 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 data
- `ae_comp__badge_obj_view.svelte` — Full badge display + edit UI
**LiveQueries:**
```typescript
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:
```typescript
load_ae_obj_id__event_badge({
event_badge_id: badge_id,
inc_template: true // Also loads template
})
```
--- ---
## Print Tracking ## Print Tracking
### Print Fields Aether tracks the lifecycle of every physical badge to prevent unauthorized reprints and monitor kiosk activity.
```typescript
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) | Field | Purpose |
The `handle_print_badge()` function in `ae_comp__badge_obj_view.svelte` increments the count and records timestamps: |---|---|
```typescript | `print_count` | Increments on every "Print Badge" action. |
async function handle_print_badge() { | `print_first_datetime` | Timestamp of the very first print. |
const now = new Date().toISOString(); | `print_last_datetime` | Timestamp of the most recent print. |
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. > **Operational Note:** Reprints triggered via the Edit Mode shortcut do not increment the count; only the formal "Print Badge" workflow does.
### Print Workflow
1. **Pre-Print:** Badge print page (`/print`) shows "Already printed N times" warning in screen-only header if `print_count >= 1`
2. **Record:** `handle_print_badge()` updates `print_count`, `print_last_datetime`, and `print_first_datetime` (first print only) via API before printing
3. **Print:** `window.print()` — standard browser print dialog, wired and working (2026-02-27)
4. **Redirect:** After 1 second, `goto(/events/{id}/badges)` returns to search
5. **Audit:** `print_first_datetime` and `print_last_datetime` visible in Edit Mode debug row
**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.
--- ---
## Database Schema ## Route Map (Badges)
### IndexedDB Table: `badge` | URL | Purpose |
**File:** `src/lib/ae_events/db_events.ts` (Lines 841-852) |---|---|
| `/events/[id]/badges` | Main search and attendee list. |
**Indexed Fields:** | `/events/[id]/badges/templates` | Badge template management. |
```typescript | `/events/[id]/badges/[id]/print` | The actual print-ready render page. |
badge: ` | `/events/[id]/badges/[id]/review` | Attendee-facing self-service form. |
event_badge_id, id,
event_id,
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`
```typescript
// 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:**
1. Map `*_random` fields to clean names (`event_badge_id_random``event_badge_id`)
2. Set primary `id` field from `event_badge_id`
3. Ensure `event_id` is set (from function parameter if missing)
4. Calculate `tmp_sort_1` and `tmp_sort_2` for efficient sorting
5. 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 (results)
├── ae_comp__badge_create_form.svelte # (Not actively used)
├── ae_comp__badge_upload_form.svelte # Bulk CSV upload
└── [badge_id]/
├── ae_comp__badge_obj_view.svelte # Badge rendering + staff edit + print button
├── ae_comp__badge_review_form.svelte # Form-based field review/edit (attendee + staff)
├── print/
│ ├── +page.ts # Non-blocking badge loader (inc_template: true)
│ └── +page.svelte # Print-focused page — screen header + badge render
└── review/
├── +page.ts # Non-blocking badge loader (inc_template: false)
└── +page.svelte # Passcode-gated review page
```
> **Note:** The old `[badge_id]/+page.svelte` placeholder was removed (2026-02-27). The name link in the search results list now goes directly to `/print`.
#### Badge Print Page (`/print`)
- Screen-only header (`print:hidden`): "Back to Search" link + "Already printed N times" warning
- Badge rendered via `ae_comp__badge_obj_view` with `is_review_mode={false}`
- Print button inside `ae_comp__badge_obj_view` handles count update → `window.print()` → redirect to search
- Page `<title>` includes badge name + event name
#### Badge Review Page (`/review`)
- Passcode-gated for attendees — URL `?passcode=...` matched against `badge.person_passcode`
- **Note:** `person_passcode` field is not yet in the DB (as of 2026-02-27). Review page accessible to staff via `trusted_access` without a passcode.
- Access hierarchy (checked in order):
1. Administrator → full access (`can_edit_fields = ['*']`)
2. Trusted Staff → staff field set
3. Attendee with valid passcode → attendee field set
4. No access → passcode entry form shown
- Uses `ae_comp__badge_review_form.svelte` (NOT badge render)
- "Back to Search" link shown for staff only
### 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_li` drives 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 `hide` flag (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 `#review` URL hash for future self-service)
- **Condition:** Renders only when BOTH `$lq__event_badge_obj` AND `$lq__event_badge_template_obj` are non-null
- **Form Binding:** Direct `bind:value` on 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-testid` attributes:** `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` builds `/v3/crud/{obj_type}/search` — always flat regardless of the parent relationship. Mocks must match this:
```typescript
// 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` 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:
```typescript
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()` 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()` 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
1. **Session Cold-Start:** Potential race condition on first load (same as pres mgmt module)
2. **Type Definitions:** Some pre-existing TypeScript errors on external package types (not introduced by badge work)
3. **`person_passcode` not in DB:** Attendee-gated review URL (`?passcode=...`) cannot function until this field is added to the `event_badge` schema. The review page falls back to passcode entry form for non-staff.
4. **Print page CSS:** Badge print rendering and `@page` print styles not yet fine-tuned — expected to need work
5. **`mod_badges_json.edit_permissions` not connected:** Settings UI exists but review page uses hardcoded field defaults
### Implemented (2026-02-27)
-`window.print()` wired to print button (records count first, then prints, then redirects)
- ✅ Dedicated `/print` page — replaces old `[badge_id]/+page.svelte` placeholder
- ✅ Dedicated `/review` page — passcode-gated, access-tiered
-`ae_comp__badge_review_form.svelte` — stub created, full form fields pending
- ✅ Badge search results visibility rules (unprinted-only for non-edit, all for trusted+edit)
- ✅ Badge list: 4 action buttons per row (Print, Review nav, Copy Link, Email Link) — all Lucide icons
- ✅ Print page: 3 action buttons in header (Print Now, Review nav, Email Link) — all Lucide icons
- ✅ Review page: 3 action buttons in header (Print nav, Copy Link, Email Link) — all Lucide icons
- ✅ Print button: not shown when already printed (unless Edit Mode)
- ✅ Print count shown as `Nx` badge next to printer icon
- ✅ Email obscuring for non-trusted users
- ✅ Email Review Link button (placeholder alert — email API pending)
- ✅ Direct Review Link clipboard copy (trusted + Edit Mode only)
- ✅ Fixed: components no longer write to `$ae_loc.edit_mode`
- ✅ Settings UI for `edit_permissions` per event (`ae_comp__event_settings_badges_form.svelte`)
- ✅ All badge module icons converted to Lucide (Font Awesome removed from badge routes)
### Recently Completed (2026-02-27)
-**Badge Review Form**`ae_comp__badge_review_form.svelte` fully implemented (fields, QR, save/cancel, options/tickets, accessibility, help modal)
-**Print font size controls (v1)** — Screen-only `[]/[+]/[↺]` panel on print page; 4 px props added to `ae_comp__badge_obj_view.svelte`; auto-sizing unchanged when props absent
-**Bug fix**`default_authenticated_fields` / `default_trusted_fields` in `review/+page.svelte` corrected (wrong field names caused silent save drops)
### Still Needed — HIGH PRIORITY (first show: April 2026)
### Still Needed — MEDIUM PRIORITY
1. **Email API for review links:** `send_review_email()` is a placeholder `alert()`. Needs actual email send endpoint.
2. **`person_passcode` DB field:** Add to `event_badge` schema to enable attendee-gated review URLs.
3. **Connect `edit_permissions` config:** Read `mod_badges_json.edit_permissions` in review page instead of hardcoded defaults.
4. **Print page CSS / `@page` styles:** Badge rendering, sizing, and print-specific stylesheet.
### Still Needed — FUTURE / LOW PRIORITY
1. **Batch Operations:** Bulk update, bulk print, bulk export
2. **Audit Log:** Track who edited which fields and when
3. **Photo Badges:** Support badge photo upload and display
4. **Real-Time Sync:** WebSocket updates for multi-device badge printing stations
---
## Development Guidelines
### Adding New Override Fields
1. Add `{field}_override` to database schema
2. Add to `properties_to_save` array in `ae_events__event_badge.ts`
3. Update display logic to check override first
4. Add to editable fields in `ae_comp__badge_obj_view.svelte`
5. Update access control config
6. Document in this file
### Testing Override Fields
```typescript
// 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
```typescript
// 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 ## Related Documentation
- [AE API V3 for Frontend](./GUIDE__AE_API_V3_for_Frontend.md) 👉 **[MODULE__AE_Events_Badge_Templates.md](./MODULE__AE_Events_Badge_Templates.md)** (Technical reference for layouts)
- [Development Guide](./GUIDE__Development.md) 👉 **[GUIDE__AE_Events_Badges_Onsite.md](./GUIDE__AE_Events_Badges_Onsite.md)** (Hardware & station setup)
- [Events Launcher Native Integration](./PROJECT__AE_Events_Launcher_Native_integration.md) 👉 **[GUIDE__AE_Events_Onsite_Runbook.md](./GUIDE__AE_Events_Onsite_Runbook.md)** (Onsite operational checklists)
- [Naming Conventions](./AE__Naming_Conventions.md)
---
**Document Status:** 🔄 In Progress
**Last Verified:** 2026-02-27 (rev 5 — field permissions spec added, header buttons implemented, review form fields pending)
**Verified Against:** Code as of 2026-02-27 (branch ae_app_3x_llm)