feat(badges): print/review pages, 4-button list, Lucide icons, permissions doc

Badge search results list (ae_comp__badge_obj_li):
- 4 action buttons per row: Print, Review (nav link), Copy Link (clipboard), Email Link
- Visibility rules: unprinted-only for non-edit mode; all non-hidden for trusted+edit
- Plain name display (User/EyeOff icon) — name is no longer a print link
- Obscured email for non-trusted users
- Debug row (ID, CR, UP, PC, FP, LP) in edit mode
- All icons converted to Lucide (Font Awesome removed)

Badge print page (/print):
- 3 header action buttons: Print Now, Review (nav), Email Link
- Removed old [badge_id]/+page.svelte placeholder (moved to trash)
- Added is_trusted, is_edit_mode, print state derived vars
- "Already printed Nx — last [timestamp]" warning inline with name
- Removed unused imports (browser, onMount, events_slct)

Badge review page (/review):
- 3 header action buttons: Print (nav), Copy Link (clipboard), Email Link
- Added events_loc for email placeholder + title event name
- Added is_edit_mode, print_count, is_printed, copy_status
- FA icons replaced with Lucide (ShieldCheck, UserCheck, User)
- Title now includes event name (was missing)

Infrastructure:
- print/+page.ts and review/+page.ts added (non-blocking badge loaders)
- ae_comp__badge_review_form.svelte stub created (fields pending)
- Fixed: components no longer write to $ae_loc.edit_mode (critical bug)

Docs:
- NEW: AE__Permissions_and_Security.md — full permissions hierarchy reference
- NEW: PROJECT__AE_Events_Badges_Review_Print.md — agent task brief for review form + print font controls
- UPDATED: MODULE__AE_Events_Badges.md rev 5 — field permissions spec, header buttons, still-needed list by priority

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-02-27 15:12:22 -05:00
parent ee500a9ad5
commit c4e85b1fe3
15 changed files with 2046 additions and 412 deletions

View File

@@ -0,0 +1,174 @@
# Aether — Permissions and Security
**Last updated:** 2026-02-27
**Source of truth:** `src/lib/ae_utils/ae_utils__perm_checks.ts`, `src/lib/stores/ae_stores.ts`
---
## Access Level Hierarchy
Highest to lowest. Each level **inherits all access from every level below it**.
| Level | `access_type` string | Typical Use |
| --- | --- | --- |
| Super | `super` | OSIT internal — full system access |
| Manager | `manager` | Account managers |
| Administrator | `administrator` | Event/account admins |
| Trusted | `trusted` | **Onsite staff** — site passcode or AE login |
| Public | `public` | Site-wide passcode granted |
| Authenticated | `authenticated` | Identity verified (e.g. IDAA Novi UUID) |
| Anonymous | `anonymous` | Default — not signed in |
> **Note on Public vs Authenticated:** `public` is a *site-wide* unlock (anyone with the passcode). `authenticated` verifies a *specific identity*. In the hierarchy, public outranks authenticated because it implies broader site access.
---
## `$ae_loc` Store — Permission Flags
`$ae_loc` is a `persisted()` store (backed by localStorage). Key fields:
```typescript
$ae_loc.access_type // string: current access type ('anonymous', 'trusted', etc.)
// Cumulative boolean flags (true = "you have AT LEAST this level")
$ae_loc.anonymous_access // always true
$ae_loc.authenticated_access // true from authenticated and above
$ae_loc.public_access // true from public and above
$ae_loc.trusted_access // true from trusted and above ← most-used gate
$ae_loc.administrator_access // true from administrator and above
$ae_loc.manager_access // true from manager and above
$ae_loc.super_access // true only at super
// Exclusive check flags (true = "you are EXACTLY this level")
$ae_loc.trusted_check // true only if access_type === 'trusted'
$ae_loc.administrator_check // etc.
// (rarely needed — prefer the _access flags)
// Behavior flags
$ae_loc.edit_mode // boolean — user preference, see below
$ae_loc.adv_mode // boolean — advanced mode toggle
```
### Additional intermediate levels (in permission checks, not in hierarchy order)
`support`, `assistant`, `verified`, `provisional` — appear in `_access` flags but are not part of the canonical `access_level_order`. Treat as internal/intermediate.
---
## Edit Mode — Critical Rules
`$ae_loc.edit_mode` is a **user preference**, not a permission level.
**Rules that must never be broken:**
1. **Components must never write to `$ae_loc.edit_mode`** — only the system menu toggle and sign-out/permission-drop handlers may change it.
2. Edit mode is only available to `trusted` and above in 95% of modules (the toggle is hidden from lower-access users).
3. Edit mode persists across navigation — it is NOT reset by page loads or component mounts.
4. Sign-out and permission drops to below `authenticated` should reset `edit_mode` to `false`.
> **Background:** A bug was fixed (2026-02-27) where `ae_comp__badge_obj_view.svelte` was writing `$ae_loc.edit_mode = false` in a data-loading `$effect`, silently overriding the user's preference on every navigation to the badge print page.
---
## Authentication Methods
| Method | Grants | Used For |
| --- | --- | --- |
| Site passcode (`site_access_code_kv`) | `trusted`, `public`, or `authenticated` | Onsite staff and event attendees |
| AE Username + Password | `trusted` and above | Staff with AE accounts |
| Novi UUID | `authenticated` | IDAA members (Novi membership system) |
Passcodes are stored per-level in `$ae_loc.site_access_code_kv`:
```typescript
site_access_code_kv: {
administrator: null, // highest passcode tier
trusted: null, // onsite staff passcode
public: 'public1980', // example
authenticated: 'auth1980'
}
```
---
## Utility Functions
### `process_permission_checks(access_type: string)`
Returns a full permission object (`_check` and `_access` flags) for a given access type string. Used when access type changes to update `$ae_loc`.
```typescript
import { process_permission_checks } from '$lib/ae_utils/ae_utils__perm_checks';
const checks = process_permission_checks('trusted');
// checks.trusted_access === true
// checks.administrator_access === false
```
### `compare_access_levels(level_a, level_b)`
Returns `1` if `level_a` is higher, `-1` if lower, `0` if equal. Useful for threshold comparisons.
---
## Privacy and Security Rules
### IDAA — International Doctors in Alcoholics Anonymous
- **ALL IDAA content is private. Always. No exceptions.**
- BB (Bulletin Board / Posts), Archives, Recovery Meetings — all require authentication.
- IDAA users authenticate via Novi UUID at `authenticated` level or higher.
- A prior agent accidentally exposed IDAA BB data publicly — treat any IDAA exposure as Sev-1.
### Journals
- Private personal data. Always authenticated. Passcode/encryption features exist.
- Never expose journal content publicly.
### `PUBLIC_AE_API_SECRET_KEY`
- Ongoing Sev-1 audit. Do not introduce new usages.
- Prefer per-request API key headers (`x-aether-api-key` + `x-account-id`).
### Email Display
Non-trusted users must never see a full email address. Obscure using:
```typescript
// joh***@example.com
function obscure_email(email: string): string {
const at = email.indexOf('@');
if (at < 0) return email;
return `${email.slice(0, Math.min(3, at))}***${email.slice(at)}`;
}
```
This pattern lives in `ae_comp__badge_obj_li.svelte` — move to `ae_utils` if needed elsewhere.
---
## Module-Specific Permission Patterns
### Events — Badges
| Scenario | Visibility | Print Action | Review Actions |
| --- | --- | --- | --- |
| Anonymous / below trusted | Unprinted only | None (name display only) | Email Review Link button (→ email API) |
| Trusted, not Edit Mode | Unprinted only | Clickable (first print) | Email Review Link button |
| Trusted, Edit Mode | All non-hidden | Clickable incl. reprint; shows `Nx` count | Email Review Link + direct Review Link (clipboard) |
- Print count badge: shown as `Nx` (e.g. `2×`) next to the printer icon when `print_count >= 1`
- Edit mode for badges: limited to `trusted_access` users (toggle hidden from lower levels)
- `person_passcode` field (for attendee-gated review URL): **not yet in DB** as of 2026-02-27
### IDAA
- Auth gate test must be the **first test** in any test file — privacy enforcement is a hard requirement.
- Default required permission: `trusted_access` or higher for module access.
---
## Common Template Patterns
```svelte
<!-- Gate on trusted access -->
{#if $ae_loc.trusted_access}
<!-- Gate on edit mode (always check trusted too — edit mode alone is insufficient) -->
{#if $ae_loc.trusted_access && $ae_loc.edit_mode}
<!-- Gate on administrator -->
{#if $ae_loc.administrator_access}
<!-- Show full vs obscured email -->
{$ae_loc.trusted_access ? email : obscure_email(email)}
```
> Never gate purely on `$ae_loc.edit_mode` without also checking a permission level. Edit mode is a UI preference, not a permission grant.

View File

@@ -3,7 +3,7 @@
**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)
**Last Updated:** 2026-02-27 (rev 5)
---
@@ -64,17 +64,23 @@ display_name = badge.full_name_override || badge.full_name || "-- no name --"
// ✅ Display still shows "Bob Smith"
```
### Override Fields (7 total)
### Override Fields
| Regular Field | Override Field | Purpose | Editable By |
|---|---|---|---|
| `pronouns` | `pronouns_override` | Preferred pronouns | Staff, Attendee |
| `professional_title` | `professional_title_override` | Job title display | Staff, Attendee |
| `full_name` | `full_name_override` | Preferred name, pronouns | Staff, Attendee |
| `full_name` | `full_name_override` | Preferred name display | Staff, Attendee |
| `affiliations` | `affiliations_override` | Organization display | Staff, Attendee |
| `phone` | `phone_override` | Phone number | 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 |
| `badge_type` | `badge_type_override` | Badge category label text | Staff only |
| `badge_type_code` | `badge_type_code_override` | Badge access level code | Staff only |
| `registration_type` | `registration_type_override` | Registration category label text | Staff only |
| `registration_type_code` | `registration_type_code_override` | Registration category code | Staff only |
> **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
@@ -149,18 +155,39 @@ def sync_badge_from_external(external_badge_data, existing_badge):
6. **Manager** — All administrator + event configuration
7. **Super** — All manager + cross-event operations
### Current Implementation (v3)
### Current Implementation (v3) — 2026-02-27
**Quick Edit Feature** ([badge_id]/+page.svelte → ae_comp__badge_obj_view.svelte)
#### Badge Search Results Visibility
**Edit Mode Trigger:**
- URL hash `#review` enables edit mode
- Sets `$ae_loc.edit_mode = true`
- Shows Save/Cancel buttons
| 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) |
**Currently Editable Fields:**
#### 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
// 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
@@ -170,37 +197,109 @@ 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`)
- 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)
### Future Planned Enhancement
#### Badge Review Form (`ae_comp__badge_review_form.svelte`)
**Event-Level Configuration:** `event.mod_badges_json.edit_permissions`
Form-based review (NOT a badge render). Used by the `/review` page.
- `can_edit_fields: string[]` prop controls which fields are editable per user level
- `['*']` = administrator (all fields)
- `is_staff: boolean` prop shows/hides the source-data panel
- Fields show "(overridden)" label when an override value differs from the base field
#### 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 (planned, not yet built)
In addition to the editable form, the review page will display:
1. **Print status** — print count + first/last print timestamps (read-only)
2. **QR Code** — the attendee's badge QR code for scanning at the badge kiosk (for automatic badge search + print flow). QR generation code may be recoverable from the legacy AE Badge version.
3. **Options** (`other_1_code` through `other_8_code`) — shown as `[✓] Option X` if the field has a value; hidden if empty
4. **Tickets** (`ticket_1_code` through `ticket_8_code`) — shown as `[✓] Ticket X` if the field has a value; hidden if empty
#### 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": ["full_name_override", "professional_title_override", "affiliations_override", "location_override"],
"requires_approval": false
"can_edit": ["pronouns_override", "full_name_override", "professional_title_override", "affiliations_override", "phone_override", "location_override", "allow_tracking", "agree_to_tc"]
},
"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
"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"]
}
}
```
**Status:** Placeholder UI exists, JSON config not yet implemented.
---
## Search & Filter Capabilities
@@ -377,15 +476,14 @@ async function handle_print_badge() {
Button has `data-testid="badge-print-btn"` and shows loading/done/error states with icon feedback.
### Print Workflow
1. **Pre-Print:** Check `print_count` to warn if already printed
2. **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).
3. **Post-Print:** Handled by `handle_print_badge()` — count + timestamps updated
4. **Audit:** Print history available for staff review
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.
**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
@@ -483,18 +581,42 @@ delete_ae_obj_id__event_badge({ event_badge_id, event_id, method })
### 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
├── +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]/
├── +page.svelte # Badge detail container
├── +page.ts # Badge loader (non-blocking)
── ae_comp__badge_obj_view.svelte # Badge display + edit
├── 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`)
@@ -577,19 +699,45 @@ None — all current badge tests passing as of 2026-02-26 (f5e98b8c).
## Known Issues & Future Enhancements
### Known Issues
1. **Test Infrastructure:** Mock API routes not connecting to page requests
2. **Session Cold-Start:** Potential race condition on first load (same as pres mgmt module)
3. **Type Definitions:** Some TypeScript errors on external package types (pre-existing)
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
### Future Enhancements
1. **Access-Based Edit Permissions:** Implement JSON config for field-level access control
2. **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.
3. **Batch Operations:** Bulk update, bulk print, bulk export
4. **Audit Log:** Track who edited which fields and when
5. **Photo Badges:** Support badge photo upload and display
6. **Custom Badge Layouts:** Dynamic template selection per badge type
7. **Real-Time Sync:** WebSocket updates for multi-device badge printing stations
8. **Approval Workflow:** Require manager approval for certain field changes
### 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)
### Still Needed — HIGH PRIORITY (first show: April 2026)
1. **Badge Review Form — actual fields:** `ae_comp__badge_review_form.svelte` is a stub. Needs full field rendering, edit inputs, save/cancel, and the display-only sections (QR code, print status, option/ticket checkmarks). See `PROJECT__AE_Events_Badges_Review_Print.md` for full spec.
2. **Badge Print Page — font size controls:** Screen-only controls to adjust font size for name, professional title, affiliations, and location sections before printing. See project brief.
### 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
---
@@ -646,6 +794,6 @@ db_events.badge.toArray().then(console.log)
---
**Document Status:** ✅ Complete
**Last Verified:** 2026-02-26 (rev 3all badge tests passing)
**Verified Against:** Code commit f5e98b8c (all data integrity tests passing)
**Document Status:** 🔄 In Progress
**Last Verified:** 2026-02-27 (rev 5field permissions spec added, header buttons implemented, review form fields pending)
**Verified Against:** Code as of 2026-02-27 (branch ae_app_3x_llm)

View File

@@ -0,0 +1,339 @@
# PROJECT: AE Events Badges — Review Form & Print Font Controls
**Created:** 2026-02-27
**Branch:** `ae_app_3x_llm`
**Priority:** HIGH — first live event is Axonius, NYC, mid-April 2026
**Owner:** Scott Idem / One Sky IT
---
## Context
The Events Badges module is mostly complete for navigation and search. Two key pieces of
functional UI remain unbuilt and are needed before the first show:
1. **Badge Review Form**`ae_comp__badge_review_form.svelte` is currently a stub. It
needs actual field rendering, edit inputs gated by access level, save/cancel API calls,
and display-only sections (QR code, print status, option/ticket checkmarks).
2. **Badge Print Font Controls** — The print page header needs screen-only controls
(hidden during `window.print()`) to bump font sizes for the name, professional title,
affiliations, and location sections before printing. These only affect the `ae_comp__badge_obj_view.svelte` render — not the page layout/template structural dimensions.
Read `documentation/MODULE__AE_Events_Badges.md` for full module context before starting.
---
## MANDATORY: Before You Start
1. Run `ae_describe event_badge` (MCP tool) to confirm which fields actually exist in the
DB. Several fields in the spec below may need to be added to `properties_to_save` in
`src/lib/ae_events/ae_events__event_badge.ts` if they are not already saved to IDB.
2. Fields to specifically confirm exist in `event_badge` schema:
- `pronouns`, `pronouns_override`
- `phone`, `phone_override`
- `allow_tracking`
- `agree_to_tc`
- `other_1_code` through `other_8_code` (the "option" fields)
- `ticket_1_code` through `ticket_8_code`
- `registration_type`, `registration_type_code`
- `registration_type_override`, `registration_type_code_override`
3. Run `npx svelte-check` before committing. Baseline is **77 errors** (all pre-existing,
none in the badge module files). Do not introduce new errors.
4. Do NOT write to `$ae_loc.edit_mode` from any badge component. This was a critical
bug (fixed 2026-02-27). See `documentation/AE__Permissions_and_Security.md`.
---
## TASK 1: Badge Review Form (HIGH PRIORITY)
### File to build
`src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_review_form.svelte`
This component is already imported and used by `review/+page.svelte`. Props it receives:
```typescript
interface Props {
event_id: string;
event_badge_id: string;
lq__event_badge_obj: any; // Svelte 5 store from liveQuery
can_edit_fields: string[]; // Which fields this user can edit
is_staff: boolean; // True if trusted_access or higher
log_lvl?: number;
}
```
`can_edit_fields` values:
- `['*']` — administrator (all fields)
- Array of field names — specific editable fields
- `[]` — read-only (shouldn't normally reach this component, but handle it)
### Helper
Use a helper derived inside the component:
```typescript
function can_edit(field: string): boolean {
return can_edit_fields.includes('*') || can_edit_fields.includes(field);
}
```
### Save / Cancel Pattern
Follow the Journals module pattern (`src/lib/ae_journals/`). Key points:
- Use `import { events_func } from '$lib/ae_events_functions'`
- Call `events_func.update_ae_obj__event_badge({ event_badge_id, event_id, data_kv })`
- Only send changed fields in `data_kv` (compare against `$lq__event_badge_obj` values)
- Show save/cancel buttons only when something has changed (`has_changes` derived)
- Show a success/error state briefly after save (1-2 seconds, then reset)
- Cancel resets local state back to `$lq__event_badge_obj` values
- Use `data-testid="badge-review-save-btn"` and `data-testid="badge-review-cancel-btn"`
### Save API Call
```typescript
await events_func.update_ae_obj__event_badge({
api_cfg: $ae_api, // from ae_loc store or passed as prop — check how ae_comp__badge_obj_view.svelte does it
event_badge_id: event_badge_id,
event_id: event_id,
data_kv: { /* only changed fields */ }
});
```
Check `ae_comp__badge_obj_view.svelte` for the existing save pattern — it already works
and can be used as reference.
---
### Section 1: Display-Only Status Bar (all access levels)
Always show at top of form. Read-only. No edit controls.
```
Print Status: [Not yet printed] OR [Printed 3× — first: Jan 5 2026, last: Jan 5 2026]
```
Use `$lq__event_badge_obj.print_count`, `print_first_datetime`, `print_last_datetime`.
Format datetimes with `ae_util.iso_datetime_formatter(dt, 'datetime_iso_12_no_seconds')`.
Import `ae_util` from `$lib/ae_utils/ae_utils`.
---
### Section 2: QR Code (all access levels)
Display the attendee's badge QR code. This is the same QR code shown on the printed badge
itself — scanning it at the badge station triggers automatic badge search and print.
**Check the legacy AE Badge version for existing QR generation code.** Look in:
- `src/lib/ae_events/` for any QR-related utilities
- `ae_comp__badge_obj_view.svelte` — the badge render component almost certainly generates
a QR code already for the printed badge. Reuse that logic/component if possible.
The QR code value should encode the badge ID or a URL that resolves to the badge.
---
### Section 3: Editable Fields
Render each field as: read-only display when `!can_edit(field)`, or an `<input>` /
`<select>` / `<textarea>` when `can_edit(field)`.
Show `(overridden)` label next to override fields when the override value differs from
the base field value.
#### Attendee-Editable Fields (shown to all access levels with link)
| Field | Input Type | Notes |
|---|---|---|
| `pronouns_override` | text input | Fallback display: `pronouns` |
| `full_name_override` | text input | Fallback display: `full_name` |
| `professional_title_override` | text input | Fallback display: `professional_title` |
| `affiliations_override` | textarea | Fallback display: `affiliations` |
| `phone_override` | text input (tel) | Fallback display: `phone` |
| `location_override` | text input | Fallback display: `location` |
| `allow_tracking` | checkbox | Label: "Allow exhibitor lead scanning" |
| `agree_to_tc` | checkbox | Label: "I agree to the Terms and Conditions" + placeholder T&C text block |
#### Staff-Only Additional Fields (shown when `is_staff === true`)
| Field | Input Type | Notes |
|---|---|---|
| `email_override` | email input | Fallback display: `email` |
| `badge_type_code_override` | select | Options: member, non-member, guest, exhibitor, staff, test; also updates `badge_type_override` text |
| `registration_type_code_override` | select | Same options as badge_type for now; also updates `registration_type_override` |
| `hide` | checkbox | Label: "Hidden from search results" |
| `priority` | number input | |
| `notes` | textarea | |
#### Staff-Only: Options & Tickets (read-edit, shown when `is_staff === true`)
**Other/Options** (`other_1_code` through `other_8_code`):
- If field has a value: show as editable text input with label "Option X"
- If field is empty/null: show faintly as "Option X (empty)" — staff can still set it
- These represent event-specific add-ons or membership indicators
**Tickets** (`ticket_1_code` through `ticket_8_code`):
- Same pattern as options above, label "Ticket X"
#### Attendee-Only: Options & Tickets (display only)
When `!is_staff` and the field has a value: show `[✓] Option X` or `[✓] Ticket X`.
When the field is empty: hide entirely (attendees don't see empty slots).
---
### Section 4: Terms & Conditions Block (all, only when `agree_to_tc` in can_edit_fields)
Placeholder text for now:
```
By checking this box, I confirm that the information on my badge is correct to the best
of my knowledge. I agree that this badge may be used for identification purposes during
the event and that my attendance may be recorded by exhibitors using the lead scanning
feature if I permit it.
```
Show this before the `agree_to_tc` checkbox. If `agree_to_tc` is not in `can_edit_fields`,
hide the entire block.
---
### Field State Pattern (Svelte 5 runes)
```typescript
// Initialize local editable state from badge object
let local_full_name_override = $state($lq__event_badge_obj?.full_name_override ?? '');
let local_pronouns_override = $state($lq__event_badge_obj?.pronouns_override ?? '');
// ... etc for each editable field
// Detect changes
let has_changes = $derived(
local_full_name_override !== ($lq__event_badge_obj?.full_name_override ?? '')
|| local_pronouns_override !== ($lq__event_badge_obj?.pronouns_override ?? '')
// ... etc
);
// Build changed-fields-only payload
function build_save_payload(): Record<string, any> {
const payload: Record<string, any> = {};
if (local_full_name_override !== ($lq__event_badge_obj?.full_name_override ?? ''))
payload.full_name_override = local_full_name_override || null; // empty string → null
// ... etc
return payload;
}
```
**Important:** Empty string inputs should save as `null` (clears the override, falls back
to base field). Use `value || null` in the payload.
---
## TASK 2: Badge Print Font Size Controls (MEDIUM PRIORITY)
### Where to add
`src/routes/events/[event_id]/(badges)/badges/[badge_id]/print/+page.svelte`
Add a screen-only (`print:hidden`) control panel between the header and the badge render.
This panel lets staff adjust font sizes for the four text-heavy sections before clicking Print.
### Controls needed
```
Font Size Controls (screen only, hidden during print):
[Name] [] [14px] [+]
[Title] [] [12px] [+]
[Affiliations] [] [11px] [+]
[Location] [] [10px] [+]
```
- Start with sensible defaults (match what `ae_comp__badge_obj_view.svelte` currently uses)
- Min/max per field (e.g., 8px24px for name, 7px18px for others)
- Pass the sizes as props into `ae_comp__badge_obj_view`
### Props to add to `ae_comp__badge_obj_view.svelte`
`ae_comp__badge_obj_view.svelte` currently has internal font size logic. It needs to
accept optional override props:
```typescript
// New optional props:
font_size_name?: number; // px
font_size_title?: number; // px
font_size_affiliations?: number; // px
font_size_location?: number; // px
```
When these props are provided, use them instead of the internally computed sizes.
When not provided, fall back to existing auto-sizing behavior.
**IMPORTANT:** Do NOT touch structural dimensions (overall badge width/height, header/footer
sizes, template layout). Only the text content font sizes.
---
## Key Files
| File | Role |
|---|---|
| `[badge_id]/ae_comp__badge_review_form.svelte` | **BUILD THIS** — review form stub |
| `[badge_id]/ae_comp__badge_obj_view.svelte` | Badge render + print button; add font size props |
| `[badge_id]/print/+page.svelte` | Print page; add font size control panel |
| `[badge_id]/review/+page.svelte` | Review page; already wired, passes `can_edit_fields` |
| `src/lib/ae_events/ae_events__event_badge.ts` | API functions: `update_ae_obj__event_badge` |
| `src/lib/ae_events/db_events.ts` | Dexie schema — `properties_to_save` for badge |
| `src/lib/ae_utils/ae_utils.ts` | `ae_util.iso_datetime_formatter()` |
| `documentation/MODULE__AE_Events_Badges.md` | Full module reference |
| `documentation/AE__Permissions_and_Security.md` | Permission flags, edit_mode rules |
| `documentation/GUIDE__AE_API_V3_for_Frontend.md` | V3 API reference |
## Access Level Reference
```typescript
// From $ae_loc store (persisted localStorage)
$ae_loc.trusted_access // true = trusted and above (onsite staff)
$ae_loc.administrator_access // true = administrator and above
$ae_loc.edit_mode // boolean — user preference toggle (NEVER write to this from components)
```
`is_staff` prop on the review form = `administrator_access || trusted_access`.
---
## Patterns to Follow
- **Canonical module reference:** `src/lib/ae_journals/` — most complete, most advanced
- **Svelte 5 runes:** `$state`, `$derived`, `$derived.by()`, `$effect` — no legacy `$:` syntax
- **Icons:** Lucide Svelte only — `import { Save, X, Check, ... } from 'lucide-svelte'`
- **No Font Awesome** (`fas fa-*`) anywhere in the badge module
- **Styling:** Tailwind CSS v4 + Skeleton UI utility classes (`btn`, `preset-tonal-*`, `input`, `card`)
- **Commits:** Atomic — one component per commit; run `npx svelte-check` before every commit
---
## What NOT to Do
- Do NOT touch `@page` CSS or badge template structural dimensions — print layout is out of scope
- Do NOT write to `$ae_loc.edit_mode` from any component
- Do NOT connect `mod_badges_json.edit_permissions` yet — hardcoded field lists are intentional for now
- Do NOT implement the email API — `send_review_email()` placeholder stays as `alert()`
- Do NOT add `person_passcode` DB field — out of scope for this sprint
---
## Testing
Run existing badge tests after any changes:
```bash
npm run test:unit
npx playwright test tests/events/badges/
```
Baseline: all badge tests passing as of 2026-02-26 (`f5e98b8c`).
Add `data-testid` attributes to key interactive elements:
- `badge-review-save-btn`
- `badge-review-cancel-btn`
- `badge-review-full-name-input`
- `badge-review-agree-to-tc-checkbox`