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 ## Overview
The Badges module manages event attendee badges with support for: 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 - **Field override protection** to prevent staff/attendee edits from being overwritten by automated syncs
- **Multi-tier access control** for field editing - **Multi-tier access control** for field editing
- **QR code generation** for badge scanning - **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, key,
val, val,
js, js,
str str,
log_lvl = 0
} = params; } = 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; 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 scale: 10, // Corresponds to box_size
type: 'image/png' type: 'image/png'
}); });
console.log('Generated QR code data URL:', data_url);
return data_url; return data_url;
} catch (error) { } catch (error) {
console.error('Error generating QR code:', error); console.error('Error generating QR code:', error);

View File

@@ -29,8 +29,8 @@ export interface LeadsLocState {
tracking__qry__sort_order: string; tracking__qry__sort_order: string;
tracking__qry__licensee_email: string; tracking__qry__licensee_email: string;
entered_passcode: string | null; entered_passcode: string | null;
// Value shape: { key: string; updated_on: string } — entries expire after a configurable max age. // Value shape: key = passcode/email used; type = 'shared' | 'licensed'; entries expire after a configurable max age.
auth_exhibit_kv: Record<string, { key: string; updated_on: string }>; auth_exhibit_kv: Record<string, { key: string; type: string; updated_on: string }>;
edit_license_li: boolean; edit_license_li: boolean;
// Key = exhibit ID (random), value = last-used tab name. // Key = exhibit ID (random), value = last-used tab name.
tab: Record<string, string>; 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 - **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. 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) ## 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`. `/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. - **PWA install prompt** — `element_pwa_install_prompt.svelte` placed on the Start tab.
Handles Chrome/Android native install and iOS Safari manual instructions. 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>
<div class="card p-0 divide-y divide-surface-500/10 overflow-hidden shadow-md"> <div class="card p-0 divide-y divide-surface-500/10 overflow-hidden shadow-md">
<!-- Licenses (Administrator Access Only for now) --> <!-- Licenses — visible to: Aether admins OR someone signed in with the shared exhibit passcode.
{#if $ae_loc.administrator_access} 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"> <div class="p-0">
<button <button
class="w-full p-4 flex items-center justify-between hover:bg-surface-500/5 transition-colors group" 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="bg-primary-500/10 p-2 rounded-lg text-primary-500"><Users size="1.2em" /></div>
<div class="text-left"> <div class="text-left">
<div class="font-bold text-sm">Exhibit Leads Licensees</div> <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>
</div> </div>
{#if show_license_mgmt} {#if show_license_mgmt}

View File

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