docs + fix: Leads module doc, badges onsite doc, license access fix, QR log cleanup

- Add MODULE__AE_Events_Exhibitor_Leads.md — full module reference (auth model, tabs,
  data model, routes, offline/PWA notes, OSIT admin notes)
- Add MODULE__AE_Events_Badges_Onsite.md — onsite printing guide (browser settings,
  CUPS/Linux setup, Zebra ZC10L section with test results, Epson stub, troubleshooting)
- Archive PROJECT__AE_Events_Exhibitor_Leads_v3*.md + Zebra test day doc → history/
- Fix leads license management access: ae_tab__manage.svelte license section now visible
  to administrator_access OR shared-passcode sign-in (type === 'shared'); matches spec
- Add type field to LeadsLocState.auth_exhibit_kv interface (was set at runtime but missing from type)
- Silence QR code console noise: entry log gated at log_lvl >= 2, data URL log removed
- vite.config.ts: exclude documentation/ and tests/ from Vite HMR file watcher

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-03-20 12:58:57 -04:00
parent 9673cbefe3
commit 14c2635df4
11 changed files with 462 additions and 12 deletions

View File

@@ -11,7 +11,7 @@
## Overview
The Badges module manages event attendee badges with support for:
- **External system imports** (iMIS, Zoom, Novi, Impexium, Confex, Cvent, and others)
- **External system imports as needed** (CSV/Excel, 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

View File

@@ -0,0 +1,207 @@
# Aether Events — Onsite Badge Printing
Notes on setup, process, hardware, and browser behavior for onsite badge printing at events.
---
## Overview
Aether badge printing uses the browser's native `window.print()` — no special software or print
server needed. The badge render page (`/events/[event_id]/badges/print/[badge_id]`) outputs
print-ready HTML/CSS, and the browser sends it directly to the connected printer via CUPS (Linux)
or the OS print system (macOS/Windows).
Chrome (Chromium) is the recommended browser for onsite kiosk stations.
Firefox is a solid alternative, especially for Save-to-PDF workflows.
---
## Recommended Workflow — Onsite Kiosk
1. Open the event's badge printing page: `/events/[event_id]/badges`
2. Search for the attendee (name, badge ID, or QR scan)
3. Open the badge print page — review the rendered badge
4. Click **Print Badge** in the controls panel (or use keyboard shortcut)
5. In the browser print dialog:
- Set Margins to **None** (Chrome) or leave defaults (Firefox)
- Confirm paper/card size matches the stock loaded in the printer
- Print
6. `print_count` increments automatically on each print via the Print Badge button
For high-volume events, consider the **rapid QR scan** mode in the Leads module or using a
dedicated kiosk session where the operator only handles physical card handoff.
---
## Browser Settings
### Chrome / Chromium (Recommended for kiosk use)
Chrome is recommended for onsite badge printing stations. Key print dialog settings:
| Setting | Correct value | Notes |
|---|---|---|
| Margins | **None** or **Minimum** | Default margins add URL/date headers — breaks badge centering |
| Paper size | Match card stock (e.g. 3.5" × 5.5") | Zebra driver may override this automatically |
| Background graphics | **On** | Required for colored header/footer stripe to print |
| Pages | 1 | PVC single-sided — only front should print |
**Important:** Chrome ignores CSS `@page { size }` for Save to PDF — it defaults to letter/A4.
For physical printer output, the printer driver controls paper size. This is expected behavior.
To lock Chrome settings for a kiosk, set Margins to "None" once and Chrome remembers per-printer.
### Firefox
Firefox honors CSS `@page { size }` which makes it ideal for PDF generation.
For physical printing, Firefox generally "just works" without margin adjustments.
| Setting | Notes |
|---|---|
| Paper size | Can be set in dialog, but CSS `@page { size }` is honored |
| Margins | Default is usually fine; remove headers/footers if they appear |
| Background graphics | Enable for colored stripes and header images to print |
### General Notes
- **Background graphics must be enabled** in any browser — otherwise header images, footer
color stripes, and tonal backgrounds will not print.
- Private/incognito mode blocks PWA install prompts — use normal browser sessions for kiosk.
- For highest reliability, set the kiosk machine to auto-login and open Chrome to the event URL.
---
## Linux / CUPS Setup
For Linux workstations and dedicated kiosk machines running Linux:
1. Install CUPS if not already present: `sudo pacman -S cups` (Arch) or equivalent
2. Start the CUPS service: `sudo systemctl enable --now cups`
3. Open the CUPS web UI: `http://localhost:631`
4. Add the printer and install the appropriate driver (see per-printer sections below)
5. Print a test page from CUPS to confirm card feed and quality
6. In Chrome: select the CUPS printer name under Destination in the print dialog
On macOS and Windows, use the vendor-provided driver installer.
---
## Printers
---
### Zebra ZC10L — PVC Card Printer
**Card stock:** 3.5" × 5.5" PVC cards (CR80 extended)
**Tested:** 2026-03-17 (rental test day, Arch Linux)
**Status:** Working. Confirmed suitable for Axonius NYC (mid-April 2026).
#### Physical Setup
- Connect via USB (the ZC10L supports USB and Ethernet)
- Load PVC card stock per the Zebra loading instructions — cards face-up, landscape
- The ZC10L prints one side (single-sided dye-sub thermal); do not attempt duplex on PVC stock
#### Linux Driver
- Download the Zebra ZC10L CUPS driver from zebra.com (ZC Series Linux support)
- Install the `.deb` or extract the PPD file and add to CUPS manually
- In CUPS (`http://localhost:631`), add the printer and select the ZC10L PPD
- Set default paper size to **3.5" × 5.5"** (or CR80 Extended if listed)
- Print a blank test page from CUPS before using Chrome
> **Note:** Driver version tested: *(update here after confirming)*
> CUPS printer name used: *(update here after setup)*
#### Chrome Print Settings (ZC10L)
| Setting | Value |
|---|---|
| Destination | Zebra ZC10L (CUPS name) |
| Paper size | 3.5 × 5.5 in (or as set in CUPS) |
| Margins | **None** |
| Background graphics | On |
| Pages | 1 (front only) |
#### CSS Layout
The ZC10L uses the `badge_3.5x5.5_pvc` layout. The PVC layout CSS is at:
`src/routes/events/[event_id]/(badges)/badges/print/badge_layout_zebra_zc10l_pvc.css`
This layout hides `.badge_back` in `@media print` — only the front face prints.
`@page { size: 3.5in 5.5in; margin: 0; }` is set in the CSS.
#### Known Behaviors / Watch-outs
- Chrome with **Default** margins: inserts URL/date headers, offsets badge — use **None**
- Chrome with **None** or **Minimum** margins: correct output
- Firefox: works correctly out of the box with this layout
- Physical card alignment: if the badge appears offset on the card, a CSS margin tweak may
be needed in the PVC layout file — note the offset and adjust `print_margin_cfg` once that
field is wired to the UI
- Font sizes: if name/affiliation text appears too small at physical scale, adjust via the
font size controls (+ / ) in the print controls panel; note the preferred values for this
event's template
#### Test Results (2026-03-19)
- Card feeds and prints without jam: ✅
- Single-sided PVC confirmed (back does not print): ✅
- Chrome margins None/Minimum: correct output ✅
- Firefox: correct output ✅
- QR code scannable from printed card: ✅
- Print tracking (`print_count` increment): ✅
- Font sizes / visual quality: tested; specific calibration pending client design direction
- Driver version: *(record here)*
- Physical offset needed: *(record if any)*
---
### Epson — Fan-Fold / Label Printer
**Status:** Not yet tested. Section to be filled in after testing.
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.
#### 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`
#### Setup Notes
*(To be filled in after testing — cover: driver source, CUPS setup, paper size, Chrome settings)*
#### Known Behaviors
*(To be filled in after testing)*
---
## Print Tracking
The badge print page tracks print counts per badge:
- `print_count` — increments on each **Print Badge** button click
- `print_first_datetime` — timestamp of first print
- An amber "Printed N×" chip appears in the print page header after the first print
The reprint shortcut (trusted access + edit mode) does **not** increment the count.
Only the **Print Badge** button path increments the count. This is intentional — reprints
for alignment or quality checks should not inflate the print count.
---
## Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| White border around printed badge | Chrome Default margins | Change to None or Minimum |
| URL / date printed at top or bottom | Chrome Default margins | Change to None |
| Header image / stripe not printing | Background graphics disabled | Enable in print dialog |
| Badge appears on wrong-size output | Paper size mismatch | Set correct size in CUPS and/or print dialog |
| Card jams | Card stock misloaded | Re-seat cards per printer manual; check stock orientation |
| Badge content clipped | Layout overflow | Check font size — use control to reduce if needed |
| Second blank card ejected | Duplex triggered on PVC | Confirm `.badge_back { display: none }` in print CSS for this layout |

View File

@@ -0,0 +1,235 @@
# Aether Events — Exhibitor Leads Module (v3)
**Status:** Implemented and ready for demo. Core lead capture flow works end-to-end.
**Platform:** PWA only — mobile-first, offline-capable.
**Target users:** Conference exhibitors scanning attendee badges at their booths.
---
## What It Does
The Exhibitor Leads module lets conference exhibitors capture and manage attendee leads directly
from their booth. Exhibitors scan or search attendee badges and build a list of contacts they met.
All data is cached locally (IndexedDB / Dexie.js) for spotty or offline venue Wi-Fi, with
background SWR revalidation against the API when the network is available.
Key capabilities:
- **Badge scanning** — QR scan or text search (name, email, affiliations, badge ID)
- **Lead list** — filterable/sortable, per-exhibitor or per-staff-member view
- **Lead detail** — custom question responses, notes (rich text), priority flag, hide/unhide
- **Export** — CSV/XLSX download of all leads for an exhibit
- **License management** — assign staff accounts (email + passcode) per max license count
- **Custom questions** — configurable per-exhibit follow-up questions (ratings, dropdowns, text)
- **Offline-first** — IndexedDB cache survives network drops; syncs on reconnect
- **PWA install** — Chrome/Android native install prompt; iOS Safari "Add to Home Screen" nudge
---
## Access Levels
Three sign-in levels are supported within this module:
| Level | How to sign in | What they can do |
|---|---|---|
| **Aether Platform Auth** | Standard Aether login (manager/trusted access) | Full admin bypass; all exhibit data |
| **Shared Exhibit Passcode** | Enter booth's `staff_passcode` | Manage licenses, view/add leads |
| **Licensed User** | Email + individual passcode from `license_li_json` | Add and manage leads for this booth |
Auth state is persisted in `$events_loc.leads.auth_exhibit_kv[exhibit_id]` (localStorage-backed).
A booth only shows in the landing page search to non-admins if it is marked `priority = true` (i.e. paid).
### `allow_tracking` Opt-In
Attendees must have `allow_tracking = true` on their badge record to be added as a lead.
Attendees without this flag are blocked at both the QR scanner and the manual search:
- QR scan shows a "Tracking Blocked" warning card (`ShieldOff` icon)
- Manual search shows an "Opt-Out" badge per result row; the "Add as Lead" button is suppressed
---
## Route Structure
```
/events/[event_id]/leads/
→ Exhibit search / landing page — find your booth
/events/[event_id]/leads/exhibit/[exhibit_id]/
→ Main exhibitor view — all 4 tabs
/events/[event_id]/leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/
→ Lead detail view — edit notes, custom responses, flags
```
---
## Module Tabs
### Tab 1 — Start / Sign In
The only tab visible when not signed in as a licensed leads user.
- **Sign in with shared passcode** — grants booth management access (license management, passcode change)
- **Sign in as licensed user** — grants lead capture access (email + passcode)
- **PWA install prompt** — Chrome/Android native install button; iOS "Share → Add to Home Screen" instructions
- **License list** — shown when signed in via shared passcode or Aether admin; add/edit/remove staff slots
### Tab 2 — Add Leads
Visible only when signed in (licensed user or Aether auth).
- **Text search** — search by name, email, affiliations, badge ID
- **QR scan** — two modes:
- **Rapid** — auto-resets after 2 seconds for the next scan (high volume)
- **Qualify** — navigates immediately to lead detail to capture notes per scan
- Results show "Add as Lead" or "View Lead" depending on whether already captured
- All new leads are linked to the capturing user's email (`external_person_id`)
### Tab 3 — Leads List
The main lead management view.
- **Search** — full-text across name, email, notes (local IDB fast path + API revalidation)
- **Sort** — Newest first, Oldest first, Name A→Z, Name Z→A
- **Filter by staff member** — "All Leads" or filter by individual licensed user
- **Show/hide hidden records** — toggles `hide` filter on IDB and API results
- **Export** — downloads CSV/XLSX for the exhibit (`leads_api_access` required)
### Tab 4 — Manage / Config
Exhibit configuration and app settings.
**Admin Tools** (manager_access only):
- Payment status toggle (`priority` field)
- Max licenses, small/large device counts
**Booth Profile** (all signed-in users):
- Exhibitor name, booth description (rich text)
**Access & Security**:
- View/change shared staff passcode
- Sign out button
**Lead Retrieval Config**:
- Exhibit Leads Licensees — manage staff accounts (`administrator_access` only; gap: should also allow shared-passcode users — see Known Gaps)
- Qualifiers & Questions — custom question config
- Licenses & Billing — stub (Stripe not yet implemented)
**App Settings**:
- Auto-hide header/footer toggle
- Show Payment Tab toggle
- Show Extra Details toggle
- Refresh interval (1120 seconds, default 25s), countdown timer, last-refresh timestamp
- Reload App, Clear IDB, Hard Reset (clears localStorage)
---
## Data Model
### `event_exhibit`
One exhibitor's presence at an event.
| Field | Purpose |
|---|---|
| `event_exhibit_id` / `event_exhibit_id_random` | Primary / URL-safe ID |
| `name` | Exhibitor display name |
| `code` | Booth number |
| `staff_passcode` | Shared sign-in code |
| `priority` | `1` = paid/active |
| `license_max` | Max licensed staff slots |
| `license_li_json` | Array of `{ full_name, email, passcode }` |
| `leads_custom_questions_json` | Array of question definitions |
| `leads_device_sm_qty` / `leads_device_lg_qty` | Device count tracking |
### `event_exhibit_tracking`
One captured lead — links an exhibit to a badge.
| Field | Purpose |
|---|---|
| `event_exhibit_tracking_id` | Primary key |
| `event_exhibit_id` | Parent exhibit |
| `event_badge_id` | Captured attendee's badge |
| `external_person_id` | Capturing staff's email (from license) |
| `exhibitor_notes` | Rich text notes (HTML via TipTap) |
| `responses_json` | `{ [question_code]: { response: value } }` |
| `priority` | Star/flag for high-priority leads |
| `hide` | Soft-delete / hide from list |
| Denormalized badge fields | `event_badge_full_name`, `event_badge_email`, `event_badge_affiliations`, `event_badge_professional_title` |
---
## Key Files
### Routes
| File | Role |
|---|---|
| `leads/+page.svelte` | Exhibit search/landing |
| `leads/exhibit/[exhibit_id]/+page.svelte` | Main exhibitor view — orchestrates all tabs |
| `leads/exhibit/[exhibit_id]/+layout.svelte` / `+layout.ts` | Layout / data load |
| `leads/exhibit/[exhibit_id]/lead/[exhibit_tracking_id]/+page.svelte` | Lead detail |
### Components
| File | Role |
|---|---|
| `ae_tab__start.svelte` | Tab 1 — welcome, sign-in, license list |
| `ae_tab__add.svelte` | Tab 2 — QR scan + text search toggle |
| `ae_tab__manage.svelte` | Tab 4 — admin tools, booth config, app settings |
| `ae_comp__exhibit_signin.svelte` | Sign-in UI (shared passcode + licensed user) |
| `ae_comp__lead_qr_scanner.svelte` | QR scanner (rapid / qualify mode) |
| `ae_comp__lead_manual_search.svelte` | Manual badge search + add |
| `ae_comp__exhibit_tracking_search.svelte` | Lead list search/filter/sort bar |
| `ae_comp__exhibit_tracking_obj_li.svelte` | Lead list item renderer |
| `ae_comp__exhibit_license_list.svelte` | License slot manager |
| `ae_comp__exhibit_custom_questions.svelte` | Custom question config editor |
| `ae_comp__exhibit_payment.svelte` | **STUB** — Stripe placeholder |
| `ae_comp__exhibit_search.svelte` | Exhibit search on the landing page |
| `lead/ae_comp__lead_detail_form.svelte` | Custom question response editor |
### Lib Functions
| File | Role |
|---|---|
| `src/lib/ae_events/ae_events__exhibit.ts` | Exhibit load, search, create, update |
| `src/lib/ae_events/ae_events__exhibit_tracking.ts` | Tracking load, search, create, update, export |
Both aggregated into `events_func` via `src/lib/ae_events/ae_events_functions.ts`.
---
## Offline / PWA Notes
- All data is stored in `db_events` (Dexie.js) — `exhibit` and `exhibit_tracking` tables
- SWR pattern: IDB cache returned immediately; background API fetch updates IDB and triggers UI refresh
- Search: local IDB first pass (fast), then API revalidation via `search__exhibit_tracking`
- `beforeinstallprompt` event captured at module load time (`src/lib/pwa/pwa_install.svelte.ts`)
— fires within ~1 second of page load, before any Svelte `$effect` runs
- iOS Safari: no native install prompt; shows "Share → Add to Home Screen" instructions instead
---
## Known Gaps
### Payment / Stripe
`ae_comp__exhibit_payment.svelte` is a stub. The Stripe integration is not implemented.
The payment tab can be hidden via "Show Payment Tab" in App Settings.
### License Management — Shared Passcode Access
Implemented. The license section in the Manage tab is visible to Aether admins and to anyone
signed in via the shared exhibit passcode (`auth_exhibit_kv[exhibit_id].type === 'shared'`).
Guard in [ae_tab__manage.svelte](src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_tab__manage.svelte):
```svelte
{#if $ae_loc.administrator_access || $events_loc.leads.auth_exhibit_kv?.[exhibit_id]?.type === 'shared'}
```
---
## OSIT Admin Notes
- Mark `priority = 1` on an exhibit to make it visible in public search and to enable lead capture
- `license_max` controls how many licensed staff slots an exhibit can have
- Export endpoint: `GET /v3/action/event_exhibit/{id}/tracking_export` — requires `leads_api_access`
- Custom questions are stored per-exhibit in `leads_custom_questions_json` (not global)
- The exhibitor landing page link format: `/events/[event_id]/leads/exhibit/[exhibit_exhibit_id_random]/`

View File

@@ -175,10 +175,11 @@ export async function js_generate_qr_code(qr_type: string, params: key_val = {})
key,
val,
js,
str
str,
log_lvl = 0
} = params;
console.log(`*** js_generate_qr_code() *** qr_type=${qr_type}`, params);
if (log_lvl >= 2) console.log(`*** js_generate_qr_code() *** qr_type=${qr_type}`, params);
let qr_data: string | null = null;
@@ -251,7 +252,6 @@ export async function js_generate_qr_code(qr_type: string, params: key_val = {})
scale: 10, // Corresponds to box_size
type: 'image/png'
});
console.log('Generated QR code data URL:', data_url);
return data_url;
} catch (error) {
console.error('Error generating QR code:', error);

View File

@@ -29,8 +29,8 @@ export interface LeadsLocState {
tracking__qry__sort_order: string;
tracking__qry__licensee_email: string;
entered_passcode: string | null;
// Value shape: { key: string; updated_on: string } — entries expire after a configurable max age.
auth_exhibit_kv: Record<string, { key: string; updated_on: string }>;
// Value shape: key = passcode/email used; type = 'shared' | 'licensed'; entries expire after a configurable max age.
auth_exhibit_kv: Record<string, { key: string; type: string; updated_on: string }>;
edit_license_li: boolean;
// Key = exhibit ID (random), value = last-used tab name.
tab: Record<string, string>;

View File

@@ -111,9 +111,6 @@ Two scan modes (toggled per exhibit):
- **Payment / Stripe** — `ae_comp__exhibit_payment.svelte` is a stub. The payment tab can be
hidden via the "Show Payment Tab" toggle in the Manage tab's App Settings.
- **License management access for shared-passcode users** — spec says booth staff signed in via
the shared passcode should be able to manage licenses. Currently gated behind
`$ae_loc.administrator_access` only (in `ae_tab__manage.svelte`).
## Implemented (previously listed as gaps)
@@ -125,6 +122,9 @@ Two scan modes (toggled per exhibit):
`/v3/action/event_exhibit/{id}/tracking_export`. Gated on `leads_api_access === true`.
- **PWA install prompt** — `element_pwa_install_prompt.svelte` placed on the Start tab.
Handles Chrome/Android native install and iOS Safari manual instructions.
- **License management access for shared-passcode users** — `ae_tab__manage.svelte` license
section now visible to `administrator_access` OR `auth_exhibit_kv[id].type === 'shared'`.
Also added `type` field to `LeadsLocState.auth_exhibit_kv` interface.
---

View File

@@ -225,8 +225,10 @@
</div>
<div class="card p-0 divide-y divide-surface-500/10 overflow-hidden shadow-md">
<!-- Licenses (Administrator Access Only for now) -->
{#if $ae_loc.administrator_access}
<!-- Licenses — visible to: Aether admins OR someone signed in with the shared exhibit passcode.
Spec: "A client staff (Trusted Access or above) or someone signed in with an Exhibit passcode
can add/edit/remove licenses." — PROJECT__AE_Events_Exhibitor_Leads_v3.md -->
{#if $ae_loc.administrator_access || $events_loc.leads.auth_exhibit_kv?.[exhibit_id]?.type === 'shared'}
<div class="p-0">
<button
class="w-full p-4 flex items-center justify-between hover:bg-surface-500/5 transition-colors group"
@@ -236,7 +238,7 @@
<div class="bg-primary-500/10 p-2 rounded-lg text-primary-500"><Users size="1.2em" /></div>
<div class="text-left">
<div class="font-bold text-sm">Exhibit Leads Licensees</div>
<div class="text-xs opacity-50">Manage assigned users and codes (Admin Only)</div>
<div class="text-xs opacity-50">Manage assigned users and codes</div>
</div>
</div>
{#if show_license_mgmt}

View File

@@ -7,6 +7,12 @@ export default defineConfig({
tailwindcss(),
sveltekit() // <-- Must come after Tailwind
],
server: {
watch: {
// Do not trigger HMR/reload for documentation or test files
ignored: ['**/documentation/**', '**/tests/**']
}
},
build: {
rollupOptions: {
output: {