diff --git a/documentation/PROJECT__AE_Events_Badges_Review_Print.md b/documentation/PROJECT__AE_Events_Badges_Review_Print.md index f744b40d..dec3f813 100644 --- a/documentation/PROJECT__AE_Events_Badges_Review_Print.md +++ b/documentation/PROJECT__AE_Events_Badges_Review_Print.md @@ -1,7 +1,7 @@ # PROJECT: AE Events Badges — Review Form & Print Font Controls **Created:** 2026-02-27 -**Last Updated:** 2026-03-02 +**Last Updated:** 2026-03-12 **Branch:** `ae_app_3x_llm` **Priority:** HIGH — first live event is Axonius, NYC, mid-April 2026 **Owner:** Scott Idem / One Sky IT @@ -9,9 +9,72 @@ --- +## Design Intent — Two Complementary Flows + +### Flow 1: Remote Badge Review (email link) +- Staff emails a review link to the attendee before the event. +- Attendee opens the link on their own device, reviews their badge info, and edits permitted fields. +- **Email address rule:** Always send to `event_badge.email` — never `email_override`. + `email_override` is a display/badge field only. It cannot be trusted as a delivery address + (attendee may have changed it to something different for badge display purposes). +- Component: `ae_comp__badge_review_form.svelte` — plain form, no badge render. +- Route: `/events/[event_id]/badges/[badge_id]/review/` + +### Flow 2: Kiosk / Onsite Badge Station (print page) +- Hardware: a laptop + badge printer (Epson fanfold or Zebra PVC card) at the check-in table. +- At the event, an attendee walks up to a badge station (check-in kiosk). +- A staff member or volunteer pulls up the attendee's badge on the print page. +- The **print page is a kiosk tool**, not just a print queue: + - Attendee reviews their badge info and can edit permitted fields **in real time**, + with the live badge render updating as they make changes. + - Staff/volunteers are present to assist with any questions. + - Once satisfied, staff prints the badge. +- The key differentiator vs the review form: **the live badge render** shows exactly how + the badge will print. Attendees and staff can see changes immediately. +- Component: `ae_comp__badge_obj_view.svelte` / `ae_comp__badge_obj_view_v2.svelte` +- Route: `/events/[event_id]/badges/[badge_id]/print/` + +### Permission Model — Same Logic, Both Flows +Both flows should respect the same permission model: +- **Attendee-level** (basic Authenticated access): can edit `pronouns_override`, + `full_name_override`, `professional_title_override`, `affiliations_override`, + `location_override`, `phone_override`, `email_override`, `allow_tracking`, `agree_to_tc`. +- **Staff-level** (trusted_access+): all attendee fields + `email`, `badge_type_code_override`, + `badge_type_override`, `hide`, `priority`, `notes`, and font size controls. +- Permissions are configured per-event in `event.mod_badges_json.edit_permissions`. + Hardcoded defaults are used until that config is implemented. + +**Current gap (TASK 4):** The print page edit button is currently gated to trusted_access only. +It needs to be accessible to attendees at the kiosk (with appropriate field-level gating), +matching the permission model already implemented in `ae_comp__badge_review_form.svelte`. + +--- + ## Next Up for Badges (TASK 4) -### 1. QR Code on Badge Front — `ae_comp__badge_obj_view.svelte` +### 0. Kiosk Editing — Print Page Permission Model Alignment +**This is the most important gap before the first live event.** + +Currently the print page edit button is staff-only (trusted_access gate). At the kiosk, +attendees need to be able to edit their own fields (same attendee-level permissions as the +review form), with staff-only fields gated appropriately. + +Work needed: +- Wire the same `can_edit_fields` / `can_edit(field)` permission logic into the print page + that `ae_comp__badge_review_form.svelte` already uses. +- The edit panel on the print page should show attendee-editable fields to all authenticated + users, and staff-only fields to trusted_access+. +- The badge render (v1 or v2) should update live as the attendee edits fields. +- Consider whether the print page needs its own inline edit panel (sidebar or overlay) + or whether it should share/reuse the review form component alongside the badge render. +- **Do NOT use `email_override` as the send-to address** — always use `event_badge.email`. + +### 1. Auto-Scaling Badge Text (v2) — In Progress +`ae_comp__badge_obj_view_v2.svelte` using `element_fit_text.svelte` (binary search auto-scale). +Toggle between v1 (heuristic) and v2 (auto-scale) on the print page via the `v1`/`v2` header button. +Heights tuned per layout in `fit_heights` derived object. Still needs visual tuning with real badges. + +### 2. QR Code on Badge Front — `ae_comp__badge_obj_view.svelte` The badge template has a `show_qr` flag (or similar). When toggled on, the QR code should appear on the front face of the printed badge. Currently QR is only shown on the review form. @@ -30,18 +93,38 @@ appear on the front face of the printed badge. Currently QR is only shown on the - Must be hidden on `ae_comp__badge_obj_view.svelte` when `show_qr` is falsy. ### 2. Badge Print Controls — UX Improvements (ae_comp__badge_print_controls.svelte) -- Scott has identified areas for improvement — TBD next session - Consider: keyboard shortcuts (+ / -) for font sizing while a field is active - Consider: "Apply to all badges" workflow for font size presets -### 3. Leads Module +### 4. Leads Module Next major work after badge polish. See `documentation/MODULE__AE_Events_Leads.md` (if it -exists) for context. Exhibitor lead scanning via QR code. +exists) for context. Exhibitor lead scanning via QR code at exhibitor booth → capture attendee +badge data, gated by `allow_tracking` on the badge. --- ## Implementation Status +### ⏳ TASK 4.0: Kiosk Editing — NOT STARTED (2026-03-12) +Print page edit access needs to be opened to attendee-level permissions, not just trusted_access. +The permission model, field list, and `can_edit()` helper from `ae_comp__badge_review_form.svelte` +should be the reference. See Design Intent section above. + +### ⏳ TASK 4.1: Auto-Scaling Badge Text v2 — IN PROGRESS (2026-03-12) +**Files created:** +- `src/lib/elements/action_fit_text.ts` — Svelte action: binary-search font scaling with + MutationObserver + ResizeObserver + requestAnimationFrame. +- `src/lib/elements/element_fit_text.svelte` — Component wrapper. Key prop: `height` (required + for binary search to work — without it, offsetHeight == scrollHeight always). +- `src/routes/events/.../ae_comp__badge_obj_view_v2.svelte` — V2 badge render using + Element_fit_text for name/title/affiliations/location in display mode. + `fit_heights` derived object provides layout-aware heights per field per badge layout. + `font_size_*` props default to `undefined` (auto-scale) rather than numeric defaults (v1 behavior). + Manual overrides from print controls still work — any number disables auto-scale for that field. + +**Toggle:** `v1`/`v2` button in print page header. V1 preserved as fallback. +**Status:** Working — heights in `fit_heights` still need visual tuning with real badge stock. + ### ✅ TASK 3: Badge Print Controls Panel — COMPLETE (2026-03-02) **Files created/modified:** diff --git a/documentation/TODO__Agents.md b/documentation/TODO__Agents.md index 16cf9d4b..bea8af95 100644 --- a/documentation/TODO__Agents.md +++ b/documentation/TODO__Agents.md @@ -2,8 +2,6 @@ > Use this file to track steps for complex features or bug fixes. > **Status:** � Stable — ongoing development. -## 📋 Open: Security -- [x] **PUBLIC_AE_API_SECRET_KEY Audit:** Completed 2026-03-11. Key is `PUBLIC_*` by design (always in client bundle). Highest-risk anonymous path now uses limited-permission `PUBLIC_AE_BOOTSTRAP_KEY`. Full server-side migration would require a major API proxy refactor — not justified given JWT + account_id auth layers. `manifest.webmanifest/+server.ts` is a minor cleanup candidate (could use bootstrap key instead), but no security urgency. Current state is acceptable. ## 🚧 Upcoming High Priority @@ -46,27 +44,10 @@ lead record look like in the DB? - **`window.print()` for badge print button:** Wire the existing `handle_print_badge()` to trigger `window.print()`. Browser print works well across Chrome/Chromium/Firefox — no Electron needed. - **Input Field Audit:** Several input fields are missing `name`/`id` attributes or `data-testid`. Known examples: badge override fields in `ae_comp__badge_obj_view.svelte`; template name input in `ae_comp__badge_template_form.svelte`. Matters for: accessibility, autofill, label associations, and test targeting. (For tests, use `getByLabel()` rather than `input[value*=...]` which only checks the HTML attribute, not the Svelte-bound DOM property.) -### [UX] Session Expired & Access Denied (identified 2026-03-10) - -Two related UX gaps to handle together: - -**1. Session Expired banner (API 401/403 mid-session):** -- `flag_expired` in root `+layout.svelte` is declared but never set — it was always intended for this -- Add a small writable store or custom event (e.g., `ae_auth_error` in `ae_stores`) that API helpers (`api_get_object.ts`, `api_post_object.ts`, `api_patch_object.ts`) can fire when they get a 401 or 403 -- Root layout watches the store and sets `flag_expired = true` -- Render a non-blocking dismissible banner (not full-screen): "Session expired. Please sign in again." with a link to the sign-in control -- Especially relevant for Launcher (event staff on tablets may not notice silent failures) - -**2. Standardize Access Denied UI (non-IDAA routes only — IDAA layout is intentionally custom):** -- Currently inconsistent across the app: - - Root layout: full-screen `flag_denied` (site access key gate — keep this, it's correct) - - `/core` layout: silent redirect to home — should show a brief message instead - - `/events/[event_id]/settings`: inline raw text string — should use a consistent banner component - - `/events/.../badges/.../review`: inline `

Access Denied

` with no context or action -- Create a reusable `element_access_denied.svelte` component (small: icon + message + optional action button) -- Swap the ad-hoc patterns to use it consistently - ## ✅ Completed (2026-03) +- [x] **[Security]** `PUBLIC_AE_API_SECRET_KEY` audit complete. Key is `PUBLIC_*` by design (always in client bundle). Highest-risk anonymous path uses limited-permission `PUBLIC_AE_BOOTSTRAP_KEY`. Full server-side migration not justified given JWT + account_id auth layers. Current state acceptable. (2026-03-11) +- [x] **[UX]** Session Expired banner — `ae_auth_error` store wired to API helpers; root layout sets `flag_expired` on 401/403; non-blocking dismissible banner rendered. (2026-03-12) +- [x] **[UX]** Access Denied UI standardized — `element_access_denied.svelte` created; `/core` layout, `/events/settings`, and `/events/badges/review` updated to use it. (2026-03-12) - [x] **[Build]** Rollup/Vite circular dependency warnings eliminated — `manualChunks` in `vite.config.ts` colocates all `svelte/*` internals into a single `svelte-vendor` chunk, preventing `runtime.js` / `index-client.js` split (~35 warnings gone). (2026-03-11) - [x] **[Refactor]** `try_cache` audit + sponsorship/event_file/hosted_file SWR alignment — removed vestigial `try_cache` params from `generate_qr_code`, `ae_core_functions` wrappers; added SWR fast/slow path to sponsorship loaders; changed `event_file` and `hosted_file` single-object loader defaults from `false` → `true` for consistency. (2026-03-11) - [x] **[DevOps]** Frontend + Backend unified into single `aether_container_env` Docker Compose. `ae_app` service live with healthcheck, single exposed port (`AE_APP_NODE_PORT`), internal `ae_api` networking. Deploy scripts in `package.json` both target `../aether_container_env/docker-compose.yml`. (2026-03-10) diff --git a/src/lib/elements/action_fit_text.ts b/src/lib/elements/action_fit_text.ts new file mode 100644 index 00000000..4e0c02b1 --- /dev/null +++ b/src/lib/elements/action_fit_text.ts @@ -0,0 +1,87 @@ +/** + * action_fit_text.ts + * + * Svelte action that auto-scales text inside a fixed-size container using + * binary search. Reacts to content changes (MutationObserver) and container + * resize (ResizeObserver). + * + * Usage: + *
Some text
+ * + * Pass null/undefined to disable (e.g. when a manual font size is active): + *
+ */ + +export interface FitTextParams { + min?: number; + max?: number; +} + +export function fit_text( + node: HTMLElement, + params: FitTextParams | null | undefined +) { + if (!params) return {}; + + let { min = 16, max = 80 } = params; + + // overflow:hidden is required so scrollWidth/scrollHeight accurately reflect overflow + const prev_overflow = node.style.overflow; + node.style.overflow = 'hidden'; + + function fits(): boolean { + return node.scrollWidth <= node.offsetWidth && node.scrollHeight <= node.offsetHeight; + } + + function fit() { + if (!params) return; + + // Try max first — if it fits, no search needed + node.style.fontSize = max + 'px'; + if (fits()) return; + + // Binary search between min and max + let lo = min; + let hi = max; + while (lo < hi - 1) { + const mid = Math.floor((lo + hi) / 2); + node.style.fontSize = mid + 'px'; + if (fits()) { + lo = mid; + } else { + hi = mid; + } + } + node.style.fontSize = lo + 'px'; + } + + // Re-fit when text content changes (handles {@html} updates) + const mutation_observer = new MutationObserver(fit); + mutation_observer.observe(node, { childList: true, subtree: true, characterData: true }); + + // Re-fit when the container is resized (e.g. window resize, panel open/close) + const resize_observer = new ResizeObserver(fit); + resize_observer.observe(node); + + // Defer initial fit to after the DOM has laid out + requestAnimationFrame(fit); + + return { + update(new_params: FitTextParams | null | undefined) { + if (!new_params) { + node.style.overflow = prev_overflow; + return; + } + params = new_params; + min = new_params.min ?? 16; + max = new_params.max ?? 80; + node.style.overflow = 'hidden'; + requestAnimationFrame(fit); + }, + destroy() { + mutation_observer.disconnect(); + resize_observer.disconnect(); + node.style.overflow = prev_overflow; + } + }; +} diff --git a/src/lib/elements/element_fit_text.svelte b/src/lib/elements/element_fit_text.svelte new file mode 100644 index 00000000..a55044ab --- /dev/null +++ b/src/lib/elements/element_fit_text.svelte @@ -0,0 +1,98 @@ + + +
+ {@render children?.()} +
diff --git a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view_v2.svelte b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view_v2.svelte new file mode 100644 index 00000000..1605f391 --- /dev/null +++ b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view_v2.svelte @@ -0,0 +1,1471 @@ + + + +
+ {$lq__event_badge_template_obj?.name ?? '—'} + | + {$lq__event_badge_template_obj?.layout ?? '(no layout)'} + | + v2 +
+ + + +
+

Debug Information

+
+    {JSON.stringify($lq__event_badge_obj, null, 2)}
+
+ +
+    {JSON.stringify($lq__event_badge_template_obj, null, 2)}
+
+
diff --git a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/print/+page.svelte b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/print/+page.svelte index 14254954..8a133ad7 100644 --- a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/print/+page.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/print/+page.svelte @@ -17,8 +17,13 @@ import { ArrowLeft, Eye, LoaderCircle, Mail, Printer } from 'lucide-svelte'; import Comp_badge_obj_view from '../ae_comp__badge_obj_view.svelte'; + import Comp_badge_obj_view_v2 from '../ae_comp__badge_obj_view_v2.svelte'; import Comp_badge_print_controls from '../ae_comp__badge_print_controls.svelte'; + // V2 toggle: temporary — lets staff compare auto-scaling text (v2) vs heuristic sizing (v1). + // Remove once v2 is verified and v1 is retired. + let use_v2 = $state(false); + let event_badge_id = $derived(page.params.badge_id); let event_id = $derived(page.params.event_id); @@ -171,6 +176,16 @@
+ + + {#if is_trusted && (!is_printed || is_edit_mode)}