10 Commits

Author SHA1 Message Date
Scott Idem
898afd9775 fix(files): refine legacy file upload warnings and trusted-access block bypass
- element_input_files_tbl: only block upload for non-trusted users; trusted_access
  users see the same warnings but can still proceed
- element_input_files_tbl: improved warning message wording for .ppt and .doc
- element_manage_event_file_li: minor tweaks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 13:56:19 -04:00
Scott Idem
74e65ea892 feat(files): block upload and show warning for legacy .ppt/.doc file formats
- Set file_list_status to 'blocked_legacy' when any selected file is .ppt or .doc,
  disabling the Upload button until the file is removed
- Show a red banner at the top when upload is blocked
- Add a per-file warning message row in the file table for all legacy/untrusted
  extensions (previously computed but never rendered — only a pink cell highlight)
- Red styling for blocking extensions (.ppt/.doc), yellow for warn-only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 13:07:45 -04:00
Scott Idem
1ad3d2030d fix(launcher/files): hide admin-purpose files and fix event_file_id in PATCH body
- launcher_file_cont: add 'admin' file_purpose to hide_draft filter (alongside outline/draft)
- element_manage_event_file_li: remove event_file_id from data_kv passed to update_ae_obj;
  it was being sent in the PATCH body causing 'Unknown column event_file_id in SET' (400)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 13:02:56 -04:00
Scott Idem
721facf7ba fix(locations): auto-load locations on page open; fix session query and POC visibility
- Add +page.ts to trigger load_ae_obj_li__event_location on page load (locations
  were never fetched without a manual trigger)
- Fix ae_comp__event_session_obj_li_wrapper: query used event_location_id_random
  (deprecated index) instead of event_location_id, causing empty session lists
  under each location
- Wire hide__session_poc to pres_mgmt_loc.current.show__session_li_poc_field so
  the Options toggle actually takes effect in the per-location session list
- Also set hide__session_location=true since location is implicit in that context

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 11:54:22 -04:00
Scott Idem
a42b49dd50 fix(launcher): auto-set app_mode to native when running in Electron
On a fresh Electron install the events_loc persisted store has no
app_mode value set, causing the native file launch path to fall through
to a browser save dialog. Auto-initialise app_mode='native' in the
launcher layout when is_native is detected so all three modes (default,
onsite, native) continue to work correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 19:51:51 -04:00
Scott Idem
278a40c981 Updated to do list 2026-04-18 18:16:35 -04:00
Scott Idem
5fcf2e86f1 Making things look nicer 2026-04-16 19:48:09 -04:00
Scott Idem
7543bf6ae5 Renamed a directory to be more consistent 2026-04-16 19:15:18 -04:00
Scott Idem
9af5a292b6 Updating to do lists. 2026-04-16 19:11:25 -04:00
Scott Idem
2595664dd1 feat(pres_mgmt): extract session search component + time window filter
- Extract session search form into ae_comp__pres_mgmt_session_search.svelte
  (parallels ae_comp__badge_search.svelte); removes ~145 lines from +page.svelte
- Add time window filter: Clock icon toggle button reveals compact before/after
  selects; trusted users get 3d/7d options; active state highlighted in amber
- Add passes_hide_filter to IDB fast path to mirror API qry_hidden logic and
  eliminate the hidden-session blink on revalidation
- Add passes_time_window applied to both IDB fast path and API results
- Add time window state fields to PresMgmtLocState + pres_mgmt_loc_defaults
- Add contextual warning in "No sessions found" when time filter is active
- badges: hide "Start Here" button for trusted_access users; tweak button shade
- badges: scope placeholder CSS fix to input only (not textarea)
- Add MODULE__AE_Events_PressMgmt_Launcher.md doc

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 19:01:35 -04:00
24 changed files with 1166 additions and 177 deletions

View File

@@ -0,0 +1,464 @@
# Aether Events — Presentation Management & Launcher
Notes on setup, workflow, configuration, and onsite operation for the Events Presentation
Management module and the companion Launcher (podium display) system.
---
## Overview
The Presentation Management (Pres Mgmt) module handles the full lifecycle of conference
content: sessions, presentations, presenters, presentation files, and room/location
assignments. The Launcher module provides the podium display interface that runs on each
session room's kiosk machine.
These two modules are deployed together — Pres Mgmt is the back office, Launcher is the
front-of-house display. Every client show is at least slightly customized. Some clients
have extensive presenter/presentation data; others just have sessions and files. The
platform is flexible enough to handle the full range.
**Reference clients (current/repeat):**
- **BGH** (Business Group on Health) — most basic setup; session-only, no named Presenters
- **LCI** (Lean Construction Institute) — most complex current setup
- **AAPOR**, **ASCM**, **CMSC** — other active/repeat clients
**Module paths:**
- Pres Mgmt: `/events/[event_id]/pres_mgmt`
- Launcher: `/events/[event_id]/launcher`
**Key source directories:**
- `src/routes/events/[event_id]/(pres_mgmt)/`
- `src/routes/events/[event_id]/(launcher)/`
- `src/lib/ae_events/` — data types and API functions for all event objects
---
## Data Model
### Object Hierarchy
```
Event
├── Event File (walk-in/out, hold slides for the whole event)
├── Location (physical room — assigned to Sessions, not the other way around)
├── Track (optional grouping; rarely used — see note below)
└── Session (time block; Location assigned here, but may be unset initially)
├── Session File (moderator slides, group/hold slides for this session)
└── Presentation (a talk within the session; must belong to exactly one Session)
└── Presenter (belongs to exactly one Presentation)
└── Presenter File (their slides/materials — the common case)
```
> **Import note:** When program data is initially imported (sessions, presentations,
> presenters), Locations are often not assigned to Sessions yet — rooms may not be
> finalized or the venue's room list may not be set up in Aether. Location assignment
> typically happens as a separate step once the room list is confirmed.
### Relationships
- **Session → Location:** Many-to-one. A Session is assigned to one Location; a Location
hosts many Sessions across the event timeline. Location may be null initially.
- **Presentation → Session:** Many-to-one. A Presentation belongs to exactly one Session.
A Session can have many Presentations (or none, for session-only setups).
- **Presenter → Presentation:** Many-to-one. A Presenter belongs to exactly one Presentation.
Optionally linked to an `event_person_id` for cross-referencing the person record.
- **Event File:** Can be attached at any level — Presenter, Presentation, Session,
Location, or Event. See the File Attachment Levels table.
### File Attachment Levels
Files (`event_file`) can be attached at five levels:
| Level | When Used | Typical Content |
|---|---|---|
| **Presenter** | 99% of the time for individual speakers | Their PowerPoint/PDF/video |
| **Session** | Moderator slides; group/hold content for a specific session | "Session 3 — Group Discussion.pptx" |
| **Location** | Walk-in/out or hold slides for a room across all sessions | Looped PPTX playing between sessions |
| **Event** | Walk-in/out or hold slides used everywhere | Looped PPTX; branding overlay |
| **Presentation** | File attached to the presentation record itself (less common) | Varies |
### Key Objects
| Object | Table | Purpose |
|---|---|---|
| Session | `event_session` | Time block; Location and datetime range assigned here |
| Location | `event_location` | Physical room; the Launcher's primary unit of display |
| Presentation | `event_presentation` | A talk within a session; belongs to exactly one Session |
| Presenter | `event_presenter` | Person linked to exactly one Presentation; optionally linked to `event_person` |
| Event Person | `event_person` | Person record within the event context |
| Event File | `event_file` | Uploaded file; attached at Presenter, Presentation, Session, Location, or Event level |
| Event Device | `event_device` | Registered Launcher kiosk (Electron native instance) |
| Event Track | `event_track` | Optional content grouping (see note below) |
### Event Tracks
The API supports Event Tracks — an optional grouping layer above Sessions. Used twice
historically; could have been omitted both times. Tracks may become genuinely useful for
larger events running many parallel Locations where thematic grouping helps navigation.
Not in active use currently and not wired into the standard Pres Mgmt UI workflow.
### Session → Location
The Launcher's primary display unit is the Location. It shows the active Session for that
Location based on `datetime_start` / `datetime_end` or manual selection. A Location hosts
many Sessions over the event's run; typically only one is active at a time.
---
## Client Setup Variation
There are no rigid "modes" — events are configured with as much or as little structure
as needed. The platform handles the full range:
**Minimal setup (BGH):**
Sessions have room and time info. No Presentations or Presenters defined.
Staff upload files directly at the session or location level onsite.
**Mid-range setup:**
Sessions defined with named Presentations. Presenters may or may not be tracked.
Mix of pre-uploaded and onsite files. QR codes may be used for quick session/presenter lookup.
**Full setup (LCI):**
Sessions, Presentations, Presenters all defined and managed. External ID labeling
(e.g., "LCI Member ID"). Agreement tracking for presenters. Files managed per-presenter.
Launcher linked from Pres Mgmt views.
The config that drives this is `event.mod_pres_mgmt_json` — see the Configuration section.
---
## Speaker Ready Room (SRR)
The Speaker Ready Room is a dedicated space where presenters check in and staff manage
content before it goes live in the session rooms. Setup varies by client:
- **Small/private:** Only a few client staff and OSIT. Not open to presenters at large.
- **Open SRR:** Open to all presenters as long as sessions are running. People come and go
all day — reviewing silently, editing with a group, practicing at a station.
### SRR Practice Stations
Stations mirror the session room setup exactly:
- Same Mac laptop model and adapter/dongle configuration as the podiums
- Projector and screen (same as session rooms where possible)
- Launcher running in Native (Electron) mode — cached files open immediately
- Full dry-run capability: load their file, start the deck, confirm everything works
### Remote Monitoring
SRR staff typically monitor the session room Launchers in real time via **VNC or RustDesk**.
This lets one person watch multiple podium displays simultaneously without being in each room.
### QR Codes (Session and Presenter)
QR codes are available for Sessions and Presenters and have been useful onsite for quick
lookups — scanning a code takes staff directly to the session or presenter record.
Whether to enable this depends on the SRR flow for each show. It gets toggled on or off
per event via config.
### SRR Staffing Roles
| Role | Access Level | Typical Tasks |
|---|---|---|
| OSIT Staff | `trusted_access` or higher | Upload files, edit sessions/presentations, manage devices, monitor via VNC |
| Client Staff | `authenticated_access` | Upload files, view session list |
| Presenter (self-service) | `authenticated_access` (if enabled) | Upload their own files via QR link |
### SRR Workflow — Day-of-Show
1. **Presenter checks in** — staff looks up their session(s) in Pres Mgmt
2. **File upload** — staff or presenter uploads file to the correct presenter/session record
3. **File verification** — staff opens the file on a practice station to confirm it renders
4. **Launcher sync** — file appears in the Launcher within the next polling cycle
5. **Presenter proceeds to room** — podium kiosk already has the file cached
---
## File Upload Workflows
### Pre-Show (Remote / Staff Ahead of Time)
Files can be uploaded anytime before the event via the Pres Mgmt web UI:
1. Navigate to the presenter, session, or appropriate level
2. Use the file upload panel (drag & drop or browse)
3. File is stored server-side and immediately available to the Launcher
Some clients enable presenter self-upload via a direct link (requires `authenticated_access`).
Controlled per-event via config.
### Day Before — SRR Setup
For higher-volume shows, the SRR opens the day before the event:
- Pre-uploaded files are already loaded and can be verified
- Early-arriving presenters check in; staff upload their files
- Electron Launcher instances on podium Macs begin pre-caching files overnight
- Problems (corrupt files, wrong format, wrong codec) surface with time to fix them
### Live Onsite Upload
For late arrivals and last-minute changes:
1. Presenter arrives at SRR (or sends file via USB/email to staff)
2. Staff uploads via Pres Mgmt web UI
3. File propagates to Launcher within one polling cycle (~30 seconds on fast networks)
4. VNC or RustDesk confirms the podium received the file before the presenter walks in
---
## Onsite Operation — Managing 412 Parallel Rooms
### Overview Page
The Pres Mgmt overview (`/events/[event_id]/pres_mgmt`) shows:
- All sessions, filterable by location and time
- File status per session
- Quick links to each session's file management
For events with multiple parallel rooms, filtering by location and time block is essential
for SRR staff staying on top of what's active right now.
### Per-Room Workflow
Each room/location has its own Launcher display:
- `/events/[event_id]/launcher` → select location → Launcher for that room
- The Launcher shows the active session based on the current time or manual selection
- VNC/RustDesk gives SRR staff a real-time view of all podiums simultaneously
### Session Display Timing
Ideally, sessions would automatically show and hide based on `datetime_start` /
`datetime_end` — appearing a configurable number of minutes before the session starts
and disappearing after it ends. This is a planned/desired behavior. In practice:
- Some clients run tight schedules and could rely on time-based transitions
- Others drift significantly from the published schedule; time-based auto-advance
would cause more problems than it solves
- Currently, session transitions can be managed manually via Launcher controls
> **TODO (future):** Configurable `show_before_minutes` / `hide_after_minutes` per event
> so well-run shows can automate transitions while looser shows stay manual.
### Device (Laptop) Assignment
Each Launcher kiosk Mac is registered as an `event_device` and typically assigned to one
Location for the duration of the event. However, laptops do get moved:
- Venues add or lose rooms as spaces are reconfigured
- A session room may open for one day only
- Devices can be reassigned to a different Location in the `event_device` record as needed
The Electron app reads its location assignment from the API at startup, so reassigning a
device takes effect on the next launch (or app restart).
---
## Alert Fields
Sessions, Presenters, and Locations each have alert fields that can display a visible
notice in the Pres Mgmt UI and/or the Launcher.
Useful for:
- "Presenter requested no recording"
- "Room change — moved to Hall B"
- "File not received — follow up"
- "AV note: needs confidence monitor"
> **Status:** Alert fields exist but the implementation and display behavior needs review
> and cleanup. Not a blocking issue for BGH next week — revisit for a future show.
---
## Launcher Module
### Operational Modes
| Mode | Use Case | File Handling |
|---|---|---|
| **Default** | Browser on any machine | Files downloaded on demand |
| **Onsite** | Browser on event network | Faster polling; browser-managed files |
| **Native** | Electron app on dedicated podium Mac | Background pre-cache; atomic file handover |
For production onsite use, **Native mode on Mac laptops** is the target. The Electron
app pre-caches all session files in the background so presentations open instantly without
a network round-trip at the moment of launch.
### Native Mode — Electron App
- **Repo:** `~/OSIT_dev/aether_app_native_electron/`
- **Platform:** macOS (primary); Linux/Windows as fallback
- **Seed config:** `seed.json` (Device ID + API key) — loaded at startup
- **File cache:** `~/Library/Caches/OSIT/file_cache/` (hashed by SHA-256)
- **Doc:** `documentation/PROJECT__AE_Events_Launcher_Native_integration.md`
The Electron app zero-configs itself:
1. Reads `seed.json` → gets device code
2. Calls Aether API → pulls device config and location assignment
3. Navigates directly to the Launcher for that location
4. Begins pre-caching session files in the background
### Launcher Display Views
| View | Shown When |
|---|---|
| Session view | Active session with session-level files |
| Presentation view | Active session with named presentations |
| Presenter view | Presentation selected; shows presenter bio/photo |
| Poster/group view | Special layout for poster sessions |
| Screensaver | No active session; idle state |
### File Opening (Native Mode) — Safe Handover
1. Verify SHA-256 hash in permanent cache
2. Atomic copy to system `[tmp]` directory
3. Rename to original filename (e.g., `Abstract_101.pptx`)
4. OS opens the file (Keynote, PowerPoint, Preview, etc.)
Versioning is handled automatically: when a presenter uploads an updated file, the new
hash is cached separately and the old one remains intact.
---
## Configuration — `mod_pres_mgmt_json`
The event's Pres Mgmt behavior is controlled by `event.mod_pres_mgmt_json`.
> **Note:** The config schema is being cleaned up — see
> `documentation/PROJECT__AE_Events_PressMgmt_Config_Cleanup.md` for the canonical
> `PressMgmtRemoteCfg` interface and naming conventions.
### Convention
| Prefix | Default state | Meaning |
|---|---|---|
| `hide__` | `false` = visible | Feature is ON by default; set `true` to suppress |
| `show__` | `false` = hidden | Feature is OFF by default; set `true` to enable |
### Common Config Keys
| Key | Default | Notes |
|---|---|---|
| `lock_config` | `false` | `true` = force remote→local sync; prevents user overrides of local config |
| `hide__session_code` | `false` | Hide session code column/field |
| `hide__session_description` | `false` | Hide session description field |
| `hide__session_location` | `false` | Hide location field on session view |
| `hide__session_datetime` | `false` | Hide datetime fields |
| `hide__presentation_code` | `false` | Hide presentation code |
| `hide__presenter_code` | `false` | Hide presenter code |
| `hide__location_code` | `false` | Hide location code |
| `show__launcher_link` | `false` | Show direct Launcher link in session view |
| `show__session_qr` | `false` | Show QR code for session (SRR lookup) |
| `show__presenter_qr` | `false` | Show QR code for presenter (SRR lookup) |
| `label__person_external_id` | `null` | Override label for external ID field (e.g., `"Member ID"`) |
| `label__session_poc_name` | `null` | Override label for session POC (e.g., `"Champion"`) |
| `file_purpose_option_kv` | `{}` | Key-value map of file purpose options (e.g., `{"ppt": "PowerPoint", "pdf": "PDF"}`) |
### Per-Show Config Examples
**BGH (session-only, minimal; no named Presentations or Presenters):**
```json
{
"lock_config": false,
"hide__presentation_code": true,
"hide__presenter_code": true
}
```
**LCI (full setup, member ID label, Launcher link enabled):**
```json
{
"lock_config": true,
"label__person_external_id": "LCI Member ID",
"show__launcher_link": true
}
```
> Admin must currently edit `mod_pres_mgmt_json` directly in the DB or via the event
> settings page. A proper Config UI is planned — see `PROJECT__AE_Events_PressMgmt_Config_Cleanup.md`.
---
## Route Map
| URL | Purpose |
|---|---|
| `/events/[id]/pres_mgmt` | Overview — sessions list, search, filter by location |
| `/events/[id]/pres_mgmt/config` | Config editor (admin only) |
| `/events/[id]/session/[session_id]` | Session detail — files, presentations, timing, alert |
| `/events/[id]/presenter/[presenter_id]` | Presenter detail — bio, files, agreement, alert |
| `/events/[id]/location/[location_id]` | Location detail — session schedule for this room, alert |
| `/events/[id]/locations` | All locations list |
| `/events/[id]/reports` | Reports — sessions, presenters, files |
| `/events/[id]/launcher` | Launcher home — select location |
| `/events/[id]/launcher/[location_id]` | Launcher display for a specific room |
---
## Device Management
Each Electron kiosk is registered as an `event_device` record:
- `code` — matches the device's `seed.json` code
- `name` — human-readable (e.g., "Ballroom A Podium")
- `data_json.location_id` — the `event_location_id` this device is assigned to
Devices can be managed in Pres Mgmt (`/events/[id]/device/device`). Location reassignment
takes effect on the next Electron app launch.
---
## Access Levels
| Feature | Minimum Access |
|---|---|
| View pres_mgmt overview | `authenticated_access` |
| Upload files | `authenticated_access` |
| Edit sessions / presentations | `trusted_access` |
| Edit config | `administrator_access` + `edit_mode` |
| View Launcher display | `authenticated_access` |
| Manual session selection in Launcher | `trusted_access` |
| Device management | `administrator_access` |
---
## Pre-Show Checklist
### 12 Weeks Before
- [ ] Event created in Aether with correct dates
- [ ] `mod_pres_mgmt_json` configured for this client's needs
- [ ] Locations (rooms) created and named
- [ ] Sessions created, assigned to locations, datetime ranges set
- [ ] If using Presentations/Presenters: records imported or entered
- [ ] File purpose options configured in `file_purpose_option_kv`
- [ ] Launcher devices registered (`event_device` records with correct codes)
- [ ] Device-to-location assignments confirmed
- [ ] Decide: QR codes for Sessions / Presenters needed? Enable/disable in config
### Day Before (SRR Setup)
- [ ] Mac laptops at podiums booted and Electron app running
- [ ] Each podium confirms it loaded the correct location's Launcher
- [ ] SRR practice stations confirmed — projector, same Mac/dongle setup as session rooms
- [ ] Pre-loaded files verified in Launcher (open at least one per room to test Safe Handover)
- [ ] SRR staff briefed on upload workflow and VNC/RustDesk monitoring setup
- [ ] VNC/RustDesk connections established to all podium displays
### Day of Show
- [ ] Confirm all session times in Aether are accurate before first session
- [ ] Monitor SRR queue — upload files as presenters check in
- [ ] Verify each file opens on a practice station before the presenter walks to their room
- [ ] Monitor podium displays via VNC/RustDesk — flag any stuck or offline devices
---
## Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Session not showing in Launcher | Session datetime wrong or location not assigned | Verify session location and datetime range |
| File uploaded but not in Launcher | Polling cycle lag; or file attached at wrong level | Wait one cycle; check that file is attached to session/location (not just a presenter record) if using session-only setup |
| Electron app shows wrong location | Device code mismatch or stale device config | Re-check `event_device` record; restart Electron app |
| File opens slowly at podium | Not in native cache yet | Check background sync in Launcher; pre-cache may not have completed |
| File won't open | Corrupt upload, wrong format, or missing codec on Mac | Test on SRR practice station; re-upload or convert |
| Session out of sync with schedule | Timing drifted; manual advance needed | Use Launcher controls to manually select the current session |
| Alert field not showing | Alert fields need implementation review | Known — lower priority than active operations |
| `lock_config: true` resets local changes | Expected behavior — remote config wins | Change the remote config in `mod_pres_mgmt_json` |
| Device needs to move to different room | Location reassigned mid-event | Update `data_json.location_id` on `event_device` record; restart Electron app on that machine |

View File

@@ -3,6 +3,30 @@
> **Status:** Stable — ongoing development.
## 🔴 BGH Conference — April 21 (Must Fix Before Event)
- [x] **[Locations] Event Locations list does not auto-load** — added `+page.ts` to trigger
`load_ae_obj_li__event_location` on page load. Also fixed session query using stale
`event_location_id_random` index (should be `event_location_id`). (2026-04-19)
- [ ] **[Files] Warn/error on `.ppt` upload** — if a presenter tries to upload a `.ppt` file (legacy
PowerPoint format), show a clear warning or block the upload with an error message instructing them
to use `.pptx` instead. Modern format required for processing.
- [ ] **[Files] Hide draft/flagged files from list** — files marked with an X flag (draft or
purpose-excluded) should not appear in the file listing. Filter them out before display.
- [ ] **[Electron/Launcher] Deploy updated Aether Native Electron app to Mac laptops** — double-check
the build and deployment process; verify the latest version is installed and functional on the
onsite Mac laptops before April 21.
- [x] **[Pres Mgmt] POC column shown in "Sessions at this Location"** — wired
`hide__session_poc={!pres_mgmt_loc.current.show__session_li_poc_field}` in
`ae_comp__event_location_obj_li.svelte`; also set `hide__session_location={true}` since
location is implicit in that context. (2026-04-19)
---
## 🚧 Upcoming High Priority
### [Stores] Svelte 4 → Svelte 5 State Migration (prerequisite for Phase 2c)
@@ -106,11 +130,6 @@ suddenly jumps to 0 errors, verify it's not because a bad `.d.ts` replaced a pac
only while server uses `default_qry_str`). Start with: any route under `/events/` or `/idaa/`
that has a full-text search input.
- [x] **[package.json] Remove orphaned ShadCN/bits-ui packages.** `shadcn-svelte` and `bits-ui`
remain in `package.json` but have no usages — `src/lib/components/ui/` was removed 2026-03-27
(trashed to `~/tmp/gemini_trash/shadcn_components_ui_2026-03-27`). Removed from `package.json` and
`package-lock.json` on 2026-04-02.
### [IDAA] Jitsi config editor + live site fix
- [ ] **Fix live site (id=17) `jitsi_token_endpoint` pointing to dev-api:** DB has
`https://dev-api.oneskyit.com/api/jitsi_token` for both site 10 and site 17 (IDAA live).
@@ -141,10 +160,6 @@ if (!event.request.url.startsWith('http')) return; // skip chrome-extension:// e
Locate in `static/service-worker.js` or the Vite PWA plugin config. Low severity — doesn't break
functionality, but pollutes the console and may cause unhandled promise rejections.
### [Badges] Remaining badge work before first live event
- **Badge print controls UX polish:** Scott has improvements in mind — TBD next session.
File: `ae_comp__badge_print_controls.svelte`.
### [CSS] Global placeholder text color — too dark in light mode
Placeholder text inherits full input text color in light mode (Tailwind CSS default), making
placeholders indistinguishable from filled-in values. Most visible in badge print controls
@@ -167,44 +182,6 @@ Once the global rule is in place, remove the scoped workaround from the badge co
### [Leads] Exhibitor Lead Scanning — IN PROGRESS (demo-ready prep)
Module is substantially built as a PWA (no Electron). Core flow works end-to-end.
Spec: `documentation/PROJECT__AE_Events_Exhibitor_Leads_v3.md` and `_detail.md`.
Full audit: `src/routes/events/[event_id]/(leads)/` and `src/lib/ae_events/ae_events__exhibit*.ts`.
**What's working:**
- Exhibit search/landing (`/leads/`) — SWR, local + API search, sort
- Exhibit detail page — 4-tab layout, sticky header with Add/List toggle, auto-refresh timer
- Tab 1 (Start): sign-in via shared passcode OR licensed user (email + passcode)
- Tab 2 (Add): QR scan (confirm mode — replaced rapid/qualify) + manual badge search; duplicate/re-enable detection on both
- Tab 3 (List): SWR lead list, licensee filter (All / My Leads), sort options, export button
- Tab 4 (Manage): admin tools, booth profile edit, passcode, license mgmt, custom questions config, app settings (refresh interval, clear IDB/localStorage, reload)
- Lead detail page: view/edit custom question responses, exhibitor notes (TipTap), priority/enable flags
- Export wired to V3 action endpoint `/v3/action/event_exhibit/{id}/tracking_export` (CSV/XLSX)
**Remaining before demo:**
- [x] **Export endpoint** — V3 action endpoint confirmed live on backend (2026-03-16). Returns 403 if
`leads_api_access` is not enabled on the exhibit — expected behavior. Export button now gated in
UI: only renders when `$lq__exhibit_obj?.leads_api_access === true`. Enable via:
`PATCH /v3/crud/event_exhibit/{id}` with `{ "leads_api_access": true }`.
- [x] **`allow_tracking` gate** — implemented (2026-03-16). QR scanner shows a warning card and
blocks the add. Manual search shows a ShieldOff "Opt-Out" badge per row and guards `add_as_lead`.
Opt-in model: `allow_tracking` must be explicitly `true` on the badge. Also added `allow_tracking`
and `agree_to_tc` to `ae_EventBadge` in `ae_types.ts`.
**Demo note:** ensure test badges have `allow_tracking = true` or no one can be added.
- [x] **Payment component**`ae_comp__exhibit_payment.svelte` fully implemented (2026-03-27).
Reads Stripe config from `$ae_loc.site_cfg_json` (`stripe_publishable_key`, `stripe_btn_1/3/6/10_license`).
License tier selector (1/3/6/10 users) with `{#key}` remount pattern for Stripe web component.
3 states: paid confirmation (priority=true), admin setup hint / "contact organizer" (no Stripe config),
payment form. `client_reference_id=exhibit_id`. TypeScript declaration in `app.d.ts`.
Stripe keys verified visible in `$ae_loc.site_cfg_json` on dev/demo site. Keys need validity check in Stripe dashboard.
- [x] **End-to-end smoke test (canceled by client)** — sign in with shared passcode, scan/search a badge, add a lead, view detail, add notes/responses, export CSV; canceled 2026-04-09.
- [x] **Install prompt** — PWA install nudge implemented (2026-03-16). `pwa_install.svelte.ts`
singleton captures `beforeinstallprompt` (Chrome/Android/desktop) and detects iOS Safari
for manual "Share → Add to Home Screen" instructions. Reusable `element_pwa_install_prompt.svelte`
placed on the Leads Start tab between the feature grid and sign-in. `pwa_install.init()` wired
into root `+layout.svelte`; dismiss persists 7 days via localStorage. svelte-check: 0 errors.
### [DevOps] Remaining deployment items
- [x] **Wire AE_APP_REPLICAS:** `docker-compose.yml` line 147 already has `scale: ${AE_APP_REPLICAS:-1}`. (verified 2026-03-11)
- [x] **Archive ae_env_node_app:** Archived as tar.gz under `~/OSIT_dev/backups/`; old history/docs moved to `~/OSIT_dev/for_reference_only/`. (2026-03-11)
@@ -222,6 +199,8 @@ Full audit: `src/routes/events/[event_id]/(leads)/` and `src/lib/ae_events/ae_ev
- [x] **`window.print()` for badge print button:** Wired in `ae_comp__badge_print_controls.svelte` — increments count, fires `window.print()`, redirects to badge search. (done)
- **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.)
## ✅ Completed (2026-03)
## ✅ Completed (2026-04)
## ✅ Completed (archived)
See the full completed history in [documentation/TODO__Agents__ARCHIVE_2026-03.md](documentation/TODO__Agents__ARCHIVE_2026-03.md).
See the full completed history in:
[documentation/archive/TODO__Agents__ARCHIVE_2026-03.md](documentation/archive/TODO__Agents__ARCHIVE_2026-03.md)
[documentation/archive/TODO__Agents__ARCHIVE_2026-04.md](documentation/archive/TODO__Agents__ARCHIVE_2026-04.md)

View File

@@ -0,0 +1,227 @@
# Frontend Agent Task List
> Use this file to track steps for complex features or bug fixes.
> **Status:** Stable — ongoing development.
## 🚧 Upcoming High Priority
### [Stores] Svelte 4 → Svelte 5 State Migration (prerequisite for Phase 2c)
The app uses `svelte-persisted-store` (Svelte 4 store contract) for all core persisted state
(`ae_loc`, `idaa_loc`, `ae_api`, `ae_sess`, etc.). In Svelte 5 `$effect`, reading **any field**
of a Svelte 4 store subscribes to the **entire store** — coarse-grained reactivity. This is the
root cause of the IDAA Novi re-auth bug (2026-03-30): unrelated `$ae_loc` writes (e.g. iframe
height, SWR cfg reload) triggered the Novi verification effect repeatedly.
Migration target: replace `svelte-persisted-store` with Svelte 5 `$state`-based persistence
(e.g. `runed` `PersistedState`, or a lightweight custom wrapper). This gives fine-grained
reactivity — only effects that actually read a changed field re-run.
**Phased approach (do NOT do all at once):**
- [ ] **Phase A — Project plan + wrapper decision:** Write `PROJECT__Stores_Svelte5_Migration.md`.
Decide: `runed` library vs. custom `$state` + localStorage wrapper. Audit all store consumers.
Identify stores in priority order. Estimate blast radius per store.
- [ ] **Phase B — Core auth stores (highest impact, start here):**
- `ae_loc` (persisted) — auth flags, site cfg, UI state; ~471 consumer sites across 150+ files
- `idaa_loc` (persisted) — Novi auth, IDAA query prefs
These two cause the most reactive noise. Migrating them also unlocks Phase 2c (separate `ae_auth`
store) since the callsite sweep is now required anyway.
- [ ] **Phase C — Remaining persisted stores:**
- `ae_api` (persisted) — API config / JWT
- `ae_events_stores` persisted entries (badges, launcher, leads, pres_mgmt loc stores)
- [ ] **Phase D — Non-persisted writable stores:**
- `ae_sess`, `idaa_sess`, `slct`, `slct_trigger`, `ae_auth_error`, `ae_trig`, `ae_snip`, etc.
- Lower urgency (no localStorage churn), but fine-grained reactivity still beneficial.
- [ ] **Phase E — Phase 2c (unblocked after B):** Split `ae_loc` into `ae_auth` + `ae_app`
(see entry below — ~471 callsites, but sweep is cheap once already touching every consumer).
**Project plan doc needed:** Yes — scope is app-wide. Do NOT start Phase B without Phase A.
---
### [Stores] Refactor — Phase 2c (deferred)
Phases 1, 2a, 2b are complete (see ✅ Completed below). One phase remaining:
- [ ] **Phase 2c — Actual separate stores (`ae_auth`, `ae_app`):** Requires touching ~471
`$ae_loc.*` auth-field read sites across 150+ files. Deferred until a Svelte runes migration
of the store layer itself (touching every component anyway makes the callsite sweep cheap).
### [Backend] Join event_location_id onto event_presenter API view
The `event_presenter` object currently has `event_session_id` but not `event_location_id`.
When navigating from the Presenter View to the Launcher, the frontend has to do a secondary
session lookup to discover the location (magic redirect in launcher base `+page.svelte`).
Joining `event_session.event_location_id` into the presenter view/response would let the
frontend pass the location directly in the Launcher URL without the extra lookup.
- [x] Backend: added `event_location_id` (and `event_location_id_random`) to the `event_presenter` view or API response (2026-04-09)
- [x] Frontend: updated `ae_EventPresenter` type and `properties_to_save`; now pass as `events__launcher_id` in `presenter_page_menu.svelte` (2026-04-09)
### [TypeScript] svelte-check hidden errors — discovered 2026-03-27
**HOW WE FOUND THIS:** The `@lucide/svelte` 0.577.0 update (2026-03-10) dropped `class` from
`IconProps`. Fixing it required a `declare module '@lucide/svelte'` augmentation. That
augmentation was mistakenly placed in `app.d.ts`, which is a *script-context* declaration file
(no `export {}`). In that context, `declare module` is an **ambient replacement**, not a merge —
it wiped all icon exports from svelte-check's view, surfacing 1368 previously hidden errors.
Once moved to `src/lucide-augment.d.ts` (a proper module file with `export {}`), the masking
lifted and the real pre-existing errors became visible.
**Lesson:** A broken ambient declaration can silently hide unrelated errors. If svelte-check
suddenly jumps to 0 errors, verify it's not because a bad `.d.ts` replaced a package's types.
**Current state (2026-03-31):** 32 errors, 0 warnings — all `ModalProps.children`.
- [ ] **[flowbite-svelte] `ModalProps.children` — 31 errors across 26 files.** The flowbite-svelte
`Modal` component API changed; `children` is no longer a direct prop (now Svelte snippet-based).
Affected files span journals, pres_mgmt, events/settings, and IDAA archives.
Run `npx svelte-check 2>&1 | grep ModalProps` to get the current list.
Fix pattern: replace `children` prop binding with Svelte snippet syntax per flowbite-svelte docs.
- [ ] **[IDAA] Make `contact_li_json_ext` searchable — Recovery Meeting contact search (2026-04-08)**
Members cannot search for meetings by contact name or email. `contact_li_json` data is not
included in `default_qry_str` and MariaDB cannot substring-search a JSON longtext directly.
The `event` table already has `contact_li_json_ext` (STORED GENERATED, indexed) to work around this.
**Backend (blocked on this first):** Add `contact_li_json_ext` to the searchable fields
whitelist for the `event` object type — likely a one-line change in `ae_obj_types_def.py`
or the event object definition. Message sent to backend agent 2026-04-08.
**Frontend (after backend ships):**
- `src/lib/ae_events/ae_events__event.ts``search__event()`: add `contact_li_json_ext`
as an OR condition alongside `default_qry_str` when `qry_str` is present.
- `src/routes/idaa/(idaa)/recovery_meetings/+page.svelte` fast-path IDB filter: parse
`contact_li_json` and include contact names/emails in the local text match check.
- [ ] **[IDAA / Events] Audit `default_qry_str` coverage in other event search pages.**
The backend was updated 2026-03-31 to expose `default_qry_str` in API responses.
Frontend fix applied to Recovery Meetings (`+page.svelte` + `properties_to_save`).
Check all other event search pages that use `db_events.event.filter()` or a secondary
post-API text filter — they may have the same mismatch (local searches `name`/`description`
only while server uses `default_qry_str`). Start with: any route under `/events/` or `/idaa/`
that has a full-text search input.
- [x] **[package.json] Remove orphaned ShadCN/bits-ui packages.** `shadcn-svelte` and `bits-ui`
remain in `package.json` but have no usages — `src/lib/components/ui/` was removed 2026-03-27
(trashed to `~/tmp/gemini_trash/shadcn_components_ui_2026-03-27`). Removed from `package.json` and
`package-lock.json` on 2026-04-02.
### [IDAA] Jitsi config editor + live site fix
- [ ] **Fix live site (id=17) `jitsi_token_endpoint` pointing to dev-api:** DB has
`https://dev-api.oneskyit.com/api/jitsi_token` for both site 10 and site 17 (IDAA live).
Need to update site 17 in **production** to `https://api.oneskyit.com/api/jitsi_token`.
SQL: `UPDATE site SET cfg_json = JSON_SET(cfg_json, '$.jitsi_token_endpoint', 'https://api.oneskyit.com/api/jitsi_token') WHERE id = 17;`
- [ ] **Add IDAA Jitsi config editor UI** to the jitsi_reports page (administrator_access only),
alongside the existing Jitsi URL Builder section. Should allow editing key fields in
`site_cfg_json` without needing phpMyAdmin:
- `jitsi_token_endpoint` — the JWT signing endpoint (needs to point to prod)
- Jitsi domain default (currently hardcoded as `jitsi.dgrzone.com` fallback in the page)
- `novi_jitsi_mod_li` — list of Novi UUIDs who get moderator privileges
Read from `$ae_loc.site_cfg_json`, PATCH the site record via V3 CRUD
(`PATCH /v3/crud/site/{id}/`), reload `$ae_loc.site_cfg_json` on save so it takes
effect without re-login.
### [PWA] Service worker ignoring `chrome-extension://` requests
Browser console shows repeated errors:
```text
TypeError: Failed to execute 'put' on 'Cache': Request scheme 'chrome-extension' is unsupported
```
The service worker's fetch/install handler is trying to cache requests with `chrome-extension://`
URLs (injected by browser extensions), which the Cache API rejects. Fix: filter out non-`http`/`https`
requests before attempting to cache. In the service worker fetch handler, add a guard:
```js
if (!event.request.url.startsWith('http')) return; // skip chrome-extension:// etc.
```
Locate in `static/service-worker.js` or the Vite PWA plugin config. Low severity — doesn't break
functionality, but pollutes the console and may cause unhandled promise rejections.
### [Badges] Remaining badge work before first live event
- **Badge print controls UX polish:** Scott has improvements in mind — TBD next session.
File: `ae_comp__badge_print_controls.svelte`.
### [CSS] Global placeholder text color — too dark in light mode
Placeholder text inherits full input text color in light mode (Tailwind CSS default), making
placeholders indistinguishable from filled-in values. Most visible in badge print controls
where placeholders show the actual badge value (e.g. "John Smith").
Workaround: scoped `::placeholder` rule added to `ae_comp__badge_print_controls.svelte`
(gray-400 light / gray-500 dark) — `commit 7733ef8`.
**Long-term fix:** Add a global rule to the main CSS (e.g. `src/app.css` or a theme file):
```css
::placeholder {
color: #9ca3af; /* gray-400 */
opacity: 1; /* overrides Firefox's 0.54 default */
}
.dark ::placeholder {
color: #6b7280; /* gray-500 */
}
```
Once the global rule is in place, remove the scoped workaround from the badge controls.
### [Leads] Exhibitor Lead Scanning — IN PROGRESS (demo-ready prep)
Module is substantially built as a PWA (no Electron). Core flow works end-to-end.
Spec: `documentation/PROJECT__AE_Events_Exhibitor_Leads_v3.md` and `_detail.md`.
Full audit: `src/routes/events/[event_id]/(leads)/` and `src/lib/ae_events/ae_events__exhibit*.ts`.
**What's working:**
- Exhibit search/landing (`/leads/`) — SWR, local + API search, sort
- Exhibit detail page — 4-tab layout, sticky header with Add/List toggle, auto-refresh timer
- Tab 1 (Start): sign-in via shared passcode OR licensed user (email + passcode)
- Tab 2 (Add): QR scan (confirm mode — replaced rapid/qualify) + manual badge search; duplicate/re-enable detection on both
- Tab 3 (List): SWR lead list, licensee filter (All / My Leads), sort options, export button
- Tab 4 (Manage): admin tools, booth profile edit, passcode, license mgmt, custom questions config, app settings (refresh interval, clear IDB/localStorage, reload)
- Lead detail page: view/edit custom question responses, exhibitor notes (TipTap), priority/enable flags
- Export wired to V3 action endpoint `/v3/action/event_exhibit/{id}/tracking_export` (CSV/XLSX)
**Remaining before demo:**
- [x] **Export endpoint** — V3 action endpoint confirmed live on backend (2026-03-16). Returns 403 if
`leads_api_access` is not enabled on the exhibit — expected behavior. Export button now gated in
UI: only renders when `$lq__exhibit_obj?.leads_api_access === true`. Enable via:
`PATCH /v3/crud/event_exhibit/{id}` with `{ "leads_api_access": true }`.
- [x] **`allow_tracking` gate** — implemented (2026-03-16). QR scanner shows a warning card and
blocks the add. Manual search shows a ShieldOff "Opt-Out" badge per row and guards `add_as_lead`.
Opt-in model: `allow_tracking` must be explicitly `true` on the badge. Also added `allow_tracking`
and `agree_to_tc` to `ae_EventBadge` in `ae_types.ts`.
**Demo note:** ensure test badges have `allow_tracking = true` or no one can be added.
- [x] **Payment component**`ae_comp__exhibit_payment.svelte` fully implemented (2026-03-27).
Reads Stripe config from `$ae_loc.site_cfg_json` (`stripe_publishable_key`, `stripe_btn_1/3/6/10_license`).
License tier selector (1/3/6/10 users) with `{#key}` remount pattern for Stripe web component.
3 states: paid confirmation (priority=true), admin setup hint / "contact organizer" (no Stripe config),
payment form. `client_reference_id=exhibit_id`. TypeScript declaration in `app.d.ts`.
Stripe keys verified visible in `$ae_loc.site_cfg_json` on dev/demo site. Keys need validity check in Stripe dashboard.
- [x] **End-to-end smoke test (canceled by client)** — sign in with shared passcode, scan/search a badge, add a lead, view detail, add notes/responses, export CSV; canceled 2026-04-09.
- [x] **Install prompt** — PWA install nudge implemented (2026-03-16). `pwa_install.svelte.ts`
singleton captures `beforeinstallprompt` (Chrome/Android/desktop) and detects iOS Safari
for manual "Share → Add to Home Screen" instructions. Reusable `element_pwa_install_prompt.svelte`
placed on the Leads Start tab between the feature grid and sign-in. `pwa_install.init()` wired
into root `+layout.svelte`; dismiss persists 7 days via localStorage. svelte-check: 0 errors.
### [DevOps] Remaining deployment items
- [x] **Wire AE_APP_REPLICAS:** `docker-compose.yml` line 147 already has `scale: ${AE_APP_REPLICAS:-1}`. (verified 2026-03-11)
- [x] **Archive ae_env_node_app:** Archived as tar.gz under `~/OSIT_dev/backups/`; old history/docs moved to `~/OSIT_dev/for_reference_only/`. (2026-03-11)
- [x] **Build Optimization:** Current state finalized. Local Gitea instance stood up at `git.dgrzone.com` (Docker, home server) — future: migrate repos from Bitbucket, verify Backblaze/restic backups cover Gitea data. (2026-03-11)
- [x] **Remote deploy script:** `aether_container_env/deploy.sh` — SSH-triggered from workstation via `npm run deploy:remote:test/prod`. Handles git pull (ff-only) + docker build + restart. Tested and working on test env. (2026-03-25)
- [x] **`.env.default` cleanup:** Removed 16 dead variables, added missing `AE_NETWORK_NAME`/`CONTAINER_DOZZLE`/`AE_DOZZLE_PORT`, parameterized all container names (`CONTAINER_MARIADB`, `CONTAINER_PMA`, `CONTAINER_AE_OPS`) with `:-default` fallbacks in compose. ("Dozzle" = log viewer container.) (2026-03-26)
- [x] **Prod deploy:** Run `npm run deploy:remote:prod` (off-peak). Prerequisites: both repos pushed to Bitbucket ✓; verify `.env.prod` exists in `/srv/apps/prod_aether_app_sveltekit/` on Linode before running. (2026-03-30)
- [x] **Bitbucket → SSH migration:** Switched all three repos (`aether_app_sveltekit`, `aether_container_env`, `aether_api_fastapi`) to SSH remotes (`git@bitbucket.org`) on workstation. App passwords deprecated — SSH unaffected. (2026-03-27)
- [ ] **Branch strategy cleanup:** All environments (test, prod, bak) currently pull from same branches. `deploy.sh` defaults are `ae_app_3x_llm` / `development` — acceptable for now but should establish proper branch separation (e.g. `main`/`master` for prod).
- [ ] **Tier 2 deploy (Gitea webhook):** Push-triggered deploys via Gitea webhook → listener on Linode → `deploy.sh`. Deferred until Gitea usage is more established.
### [General]
- [x] **Temp Cleanup:** `cleanup_tmp_files` wired in `launcher_background_sync.svelte`; called at launcher startup. Confirmed working. (2026-03-11)
- [x] **`window.print()` for badge print button:** Wired in `ae_comp__badge_print_controls.svelte` — increments count, fires `window.print()`, redirects to badge search. (done)
- **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.)
## ✅ Completed (2026-03)
## ✅ Completed (archived)
See the full completed history in [documentation/TODO__Agents__ARCHIVE_2026-03.md](documentation/TODO__Agents__ARCHIVE_2026-03.md).

View File

@@ -175,16 +175,19 @@ async function process_file_list(file_list: FileList) {
warning_legacy_extension = true;
if (guessed_extension == 'ppt') {
warning_message =
'It appears this is a legacy PowerPoint file and has not been officially supported since Office PowerPoint 2003. This file is known to have issues and may not work well. It is <strong>strongly</strong> recommended that this file be saved using the modern PPTX format.';
'<strong>WARNING</strong>: It appears this is a legacy Microsoft PowerPoint file and has not been officially supported since Office PowerPoint <strong>2003</strong>. This file type is known to have issues and may not work well. It is <strong>strongly</strong> recommended that this file be saved using the modern PPTX format in PowerPoint. <a href="https://support.microsoft.com/en-us/office/saving-ppt-to-pptx-or-pptm-e8ebc946-98d1-41bb-bb3e-cf00c65e3a55" class="underline font-semibold text-sm">More Help?</a>';
} else if (guessed_extension == 'doc') {
warning_message =
'<strong>WARNING</strong>: It appears this is a legacy Microsoft Word Document file and has not been officially supported since Office Word <strong>2003</strong>. This file type is known to have issues and may not work well. It is <strong>strongly</strong> recommended that this file be saved using the modern DOCX format in Word. <a href="https://support.microsoft.com/en-us/office/saving-doc-to-docx-or-dorm-0107099d-dc1e-4897-8851-f8c9f483f7cd" class="underline font-semibold text-sm">More Help?</a>';
} else if (guessed_extension == 'avi') {
warning_message =
'It appears this is a video file using the AVI format. It is <strong>strongly</strong> recommended that this file be re-saved as an MP4, MOV, MKV, or MPG/MPEG. The file will also likely be much smaller.';
'<strong>WARNING</strong>: It appears this is a video file using the AVI format. It is <strong>strongly</strong> recommended that this file be re-saved as an MP4, MOV, MKV, or MPG/MPEG. The file will also likely be much smaller.';
} else if (guessed_extension == 'wmv') {
warning_message =
"It appears this is a video file using Microsoft's WMV format. It is <strong>strongly</strong> recommended that this file be re-saved as an MP4, MOV, MKV, or MPG/MPEG.";
"<strong>WARNING</strong>: It appears this is a video file using Microsoft's WMV format. It is <strong>strongly</strong> recommended that this file be re-saved as an MP4, MOV, MKV, or MPG/MPEG.";
} else {
warning_message =
'It appears this is a legacy or not very well supported file format. It is <strong>strongly</strong> recommended that it be saved in an alternative format if possible.';
'<strong>WARNING</strong>: It appears this is a legacy or not very well supported file format. It is <strong>strongly</strong> recommended that it be saved in an alternative format if possible.';
}
} else if (file_size_bytes > 52428800) {
// 50 MB = 52428800 bytes
@@ -215,6 +218,10 @@ async function process_file_list(file_list: FileList) {
file_data['warning_message'] = warning_message;
// .ppt and .doc must be converted before upload — block, don't just warn
file_data['block_upload'] =
guessed_extension === 'ppt' || guessed_extension === 'doc' || guessed_extension === 'xls';
file_data['uploaded'] = null;
file_data['uploaded_bytes'] = null;
@@ -272,7 +279,8 @@ async function process_file_list(file_list: FileList) {
// input_file_list_processed.push(file_data);
}
file_list_status = 'ready';
const has_blocked = processed_file_list.some((f) => f.block_upload);
file_list_status = has_blocked && !$ae_loc.trusted_access ? 'blocked_legacy' : 'ready';
console.log(processed_file_list);
// return JSON.parse(JSON.stringify(processed_file_list));
@@ -361,6 +369,13 @@ run(() => {
</div>
{/if}
{#if file_list_status == 'blocked_legacy'}
<div class="m-1 rounded-md border-2 border-red-500 bg-red-100 p-3 text-sm text-red-800 dark:bg-red-900/30 dark:text-red-200">
<strong>⚠ Upload blocked — legacy file format detected.</strong>
This Microsoft file type has not been supported in more than 10 years. Please remove the flagged file(s) below and re-save in the modern format before uploading.
</div>
{/if}
<!-- {#await processed_file_list} -->
<!-- {:then} -->
{#if use_selected_file_table && processed_file_list && processed_file_list.length}
@@ -368,18 +383,18 @@ run(() => {
<table class="slct_file_list text-sm {table_class_li.join(' ')}">
<thead>
<tr>
<th>Remove</th>
<th>Filename</th>
<th>Modified</th>
<th>Size</th>
<th class="text-xs">Remove</th>
<th class="text-xs">Filename</th>
<th class="text-xs">Modified</th>
<th class="text-xs">Size</th>
<!-- <th>Type</th> -->
<th>Ext</th>
<th>Hash</th>
<th class="text-xs">Ext</th>
<th class="text-xs">Hash</th>
</tr>
</thead>
{#each processed_file_list as file_list_item, file_index (file_index)}
<tbody>
<tr>
<tr class="border">
<td class="file_remove">
<button
onclick={preventDefault(() => {
@@ -426,6 +441,19 @@ run(() => {
})}
</td>
</tr>
{#if file_list_item.warning_message}
<tr>
<td colspan="6"
class="text-left text-xs m-1 border border-red-500 p-3"
class:bg-red-100={file_list_item.block_upload}
class:text-red-800={file_list_item.block_upload}
class:bg-yellow-50={!file_list_item.block_upload}
class:text-yellow-900={!file_list_item.block_upload}>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html file_list_item.warning_message}
</td>
</tr>
{/if}
</tbody>
{/each}
</table>

View File

@@ -37,6 +37,7 @@ import {
Laptop,
LoaderCircle,
Monitor,
OctagonAlert,
Pencil,
RefreshCw,
Save,
@@ -636,8 +637,6 @@ async function handle_convert_pdf_to_image(event_file_obj: key_val) {
);
let event_file_data = {
event_file_id:
event_file_obj.event_file_id,
file_purpose: (
e.target as HTMLInputElement
).value
@@ -717,14 +716,24 @@ async function handle_convert_pdf_to_image(event_file_obj: key_val) {
<span
class="flex w-full flex-col justify-between lg:flex-row">
<span
class:preset-tonal-error={event_file_obj.extension == 'doc' || event_file_obj.extension == 'ppt' || event_file_obj.extension == 'xls'}
class:hidden={display_mode !=
'default'}>
'default'}
title={(event_file_obj.extension == 'doc' || event_file_obj.extension == 'ppt' || event_file_obj.extension == 'xls') ? `WARNING: Microsoft 2003 legacy file type. Please resave this in a modern PowerPoint, Word, or Excel format!\nFile type = ${event_file_obj.extension}\nContent type = ${event_file_obj.content_type}` : `File type = ${event_file_obj.extension}\nContent type = ${event_file_obj.content_type}`}
>
Type:
<strong
>{event_file_obj.extension}
>
<ExtIcon
size="1em"
class="inline-block" />
{event_file_obj.extension}
{#if event_file_obj.extension == 'doc' || event_file_obj.extension == 'ppt' || event_file_obj.extension == 'xls'}
<!-- Legacy warning icon -->
<OctagonAlert
size="1em"
class="inline-block" />
{/if}
</strong>
<!-- {#if event_file_obj.open_in_os == 'win'}
<strong>

View File

@@ -106,6 +106,11 @@ export interface PresMgmtLocState {
location_name_qry_str: string | null; // persisted location filter text
refresh_interval: number; // auto-refresh interval in seconds (0 = disabled)
// --- Time window filter (onsite use: show only sessions near current time) ---
enable_time_window: boolean; // false = off (show all); true = apply window
time_window_before_minutes: number; // include sessions that started up to N min ago
time_window_after_minutes: number; // include sessions starting within next N min
// --- Report display preferences (user-controlled, persisted) ---
rpt__session_no_files: boolean; // show "sessions with no files" report section
rpt__session_poc_agree: boolean; // show "session POC agreement" report section
@@ -282,6 +287,11 @@ export const pres_mgmt_loc_defaults: PresMgmtLocState = {
location_name_qry_str: null,
refresh_interval: 0,
// Time window filter
enable_time_window: false,
time_window_before_minutes: 30,
time_window_after_minutes: 720,
// Report display preferences
rpt__session_no_files: true,
rpt__session_poc_agree: false,

View File

@@ -313,13 +313,15 @@ let step_label = $derived(
/* Placeholder text must read as a hint, not as filled content.
Tailwind v4 sets placeholder color too dark on light backgrounds.
Same fix as ae_comp__badge_print_controls.svelte. */
form input::placeholder,
form textarea::placeholder {
/* form textarea::placeholder */
form input::placeholder {
color: #9ca3af; /* gray-400 */
opacity: 1; /* Firefox: override default 0.54 opacity */
}
:global(.dark) form input::placeholder,
:global(.dark) form textarea::placeholder {
/* :global(.dark) form textarea::placeholder */
:global(.dark) form input::placeholder {
color: #6b7280; /* gray-500 in dark mode */
}
</style>

View File

@@ -177,8 +177,9 @@ function handle_qr_scan_result(event: {
onclick={() => document.getElementById('badge_fulltext_search_qry_str')?.focus()}
aria-label="Start here focus search field"
data-testid="badge-start-btn"
class="btn btn-sm preset-filled-secondary-300-700 font-semibold"
class="btn btn-sm preset-filled-secondary-200-800 font-semibold"
class:opacity-30={!!badges_loc.current.fulltext_search_qry_str}
class:hidden={$ae_loc.trusted_access}
>
<StepForward size="1em" class="mx-1" />
Start Here

View File

@@ -4,7 +4,8 @@
* Root layout for the launcher area.
* Ensures background sync runs globally regardless of active tab.
*/
// import { ae_loc } from '$lib/stores/ae_stores';
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
import Launcher_Background_Sync from './launcher_background_sync.svelte';
interface Props {
@@ -12,6 +13,17 @@ interface Props {
}
let { children }: Props = $props();
// WHY: When running inside Electron (is_native), app_mode must be 'native' for
// the file launch path to work correctly. On a fresh install the persisted store
// has no value set, so we auto-initialise it here. This preserves the three-mode
// design (default / onsite / native) — browser sessions are unaffected because
// is_native is only ever true inside the Electron shell.
$effect(() => {
if ($ae_loc.is_native && $events_loc.launcher.app_mode !== 'native') {
$events_loc.launcher.app_mode = 'native';
}
});
</script>
<!-- Background Sync Process (Invisible) -->

View File

@@ -237,7 +237,8 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
class:justify-center={hide_meta}
class:hidden={hide_draft &&
(event_file_obj.file_purpose == 'outline' ||
event_file_obj.file_purpose == 'draft')}
event_file_obj.file_purpose == 'draft' ||
event_file_obj.file_purpose == 'admin')}
class="event_launcher_file_cont flex max-w-full grow flex-col flex-wrap items-center justify-center gap-1 transition-all md:flex-row">
{#if open_file_clicked}
<div

View File

@@ -0,0 +1,23 @@
/** @type {import('./$types').PageLoad} */
import { browser } from '$app/environment';
import { events_func } from '$lib/ae_events/ae_events_functions';
export async function load({ parent }) {
const parent_data = await parent();
const account_id = parent_data.account_id;
const ae_acct = parent_data[account_id];
const event_id = ae_acct.slct.event_id;
if (browser && event_id) {
events_func.load_ae_obj_li__event_location({
api_cfg: ae_acct.api,
for_obj_type: 'event',
for_obj_id: event_id,
hidden: 'all'
});
}
return parent_data;
}

View File

@@ -386,6 +386,8 @@ if (!pres_mgmt_loc.current.location_kv) {
link_to_type={'event_location'}
link_to_id={event_location_obj?.event_location_id}
event_session_id_random_li={[]}
hide__session_location={true}
hide__session_poc={!pres_mgmt_loc.current.show__session_li_poc_field}
{log_lvl}></Comp_event_session_obj_li>
{/if}
{:else if pres_mgmt_loc.current.show_content__location_devices_sessions == 'devices'}

View File

@@ -46,12 +46,11 @@ import {
ListChecks,
LoaderCircle,
Mails,
MapPin,
RemoveFormatting,
Search,
TriangleAlert,
Upload
} from '@lucide/svelte';
import Comp_pres_mgmt_session_search from './ae_comp__pres_mgmt_session_search.svelte';
// Quickly save the data passed from the parent(s) to the Svelte stores, localStorage, and other.
// NOTE: Derived from data.account_id (prop) instead of $slct.account_id (store)
// to prevent circular dependency loops during hydration.
@@ -156,7 +155,11 @@ let search_params = $derived({
.trim(),
location: pres_mgmt_loc.current.location_name_qry_str,
event_id: $events_slct?.event_id,
remote_first: pres_mgmt_loc.current.qry__remote_first
remote_first: pres_mgmt_loc.current.qry__remote_first,
qry_hidden: pres_mgmt_loc.current.qry_hidden,
enable_time_window: pres_mgmt_loc.current.enable_time_window,
time_window_before_minutes: pres_mgmt_loc.current.time_window_before_minutes,
time_window_after_minutes: pres_mgmt_loc.current.time_window_after_minutes
});
$effect(() => {
@@ -180,6 +183,35 @@ async function handle_search_refresh(params: any) {
const event_id = params.event_id;
const remote_first = params.remote_first;
// Snapshot filter params at search time — avoids stale reads across async boundaries.
const { enable_time_window, time_window_before_minutes, time_window_after_minutes, qry_hidden } = params;
// Mirror the API's hide filter in the fast path so IDB results match what the
// API will return. Without this, hidden sessions briefly appear for trusted users
// before the API revalidation removes them (the blink).
//
// NOTE: only event_session.hide is checked here — NOT hide_event_launcher.
// hide_event_launcher removes sessions from the Launcher only (used to drop past
// blocks from the kiosk view). Those sessions remain fully accessible in pres_mgmt.
function passes_hide_filter(session: { hide?: boolean | null }): boolean {
if (qry_hidden === 'not_hidden') return !session.hide;
if (qry_hidden === 'hidden') return !!session.hide;
return true; // 'all'
}
// Sessions with no start_datetime are always shown when the time filter is active
// (e.g. unscheduled placeholder sessions at BGH).
function passes_time_window(session: { start_datetime?: Date | string | null }): boolean {
if (!enable_time_window) return true;
if (!session.start_datetime) return true;
const now = Date.now();
const start = new Date(session.start_datetime).getTime();
return (
start >= now - time_window_before_minutes * 60_000 &&
start <= now + time_window_after_minutes * 60_000
);
}
if (log_lvl)
console.log(
`[Session Search #${current_search_id}] Refreshing (remote=${remote_first}, event=${event_id}, str=${params.str})...`
@@ -200,6 +232,9 @@ async function handle_search_refresh(params: any) {
.where('event_id')
.equals(event_id)
.filter((session) => {
if (!passes_hide_filter(session)) return false;
if (!passes_time_window(session)) return false;
if (
location_name &&
session.event_location_name !== location_name
@@ -276,6 +311,7 @@ async function handle_search_refresh(params: any) {
// Client-side Filter Guard: Ensure API results match local criteria (Backup filter)
api_results = api_results.filter((session) => {
if (!passes_time_window(session)) return false;
if (
location_name &&
session.event_location_name !== location_name
@@ -339,16 +375,6 @@ if (
pres_mgmt_loc.current.saved_search__session_location_name;
}
function handle_search_trigger() {
pres_mgmt_loc.current.search_version++;
}
function prevent_default<T extends Event>(fn: (event: T) => void) {
return function (event: T) {
event.preventDefault();
fn(event);
};
}
</script>
<svelte:head>
@@ -422,97 +448,8 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
</header>
{#if !pres_mgmt_loc.current.show_content__event_view || pres_mgmt_loc.current.show_content__event_view == 'default'}
<div class="preset-tonal-primary rounded-xl">
<form
onsubmit={prevent_default(() => handle_search_trigger())}
autocomplete="off"
class="form flex w-full grow flex-row flex-wrap items-center justify-center gap-1">
<button
type="button"
class="btn btn-sm ae_btn_warning mx-1"
class:hidden={!$ae_loc.authenticated_access}
onclick={() => {
pres_mgmt_loc.current.location_name_qry_str = '';
pres_mgmt_loc.current.show_content__session_search_room_name =
!pres_mgmt_loc.current.show_content__session_search_room_name;
handle_search_trigger();
}}
title="Search by location name">
<MapPin size="1em" />
</button>
<select
name="location_name_list"
id="session_location_name_list"
bind:value={pres_mgmt_loc.current.location_name_qry_str}
class="input mx-1 w-min max-w-40 min-w-fit font-mono text-xs font-bold transition-all"
class:hidden={!$ae_loc.authenticated_access ||
!pres_mgmt_loc.current.show_content__session_search_room_name}
onchange={() => handle_search_trigger()}
title="Select to filter based on the location/room name">
{#if $lq__event_location_obj_li}
<option value="">Location / Room</option>
{#each $lq__event_location_obj_li as event_location_obj (event_location_obj.event_location_id)}
<option value={event_location_obj?.name}
>{event_location_obj.name}</option>
{/each}
{/if}
</select>
<button
type="button"
onclick={() => {
pres_mgmt_loc.current.fulltext_search_qry_str = '';
handle_search_trigger();
}}
class:hidden={!pres_mgmt_loc.current.fulltext_search_qry_str}
class="btn btn-sm ae_btn_warning mx-1"
title="Clear search text">
<RemoveFormatting size="1em" />
</button>
<!-- svelte-ignore a11y_autofocus -->
<input
type="search"
placeholder="Search for a session"
id="session_fulltext_search_qry_str"
bind:value={pres_mgmt_loc.current.fulltext_search_qry_str}
class="input text-1xl ae_btn_info mx-1 w-80 font-mono font-bold transition-all hover:text-2xl"
onkeyup={(e) => {
if (e.key === 'Enter') handle_search_trigger();
}}
autofocus
data-ignore="true" />
<button
type="submit"
class="btn btn-lg ae_btn_primary mx-1 w-48 text-2xl font-bold"
title="Search for a session">
{#if $events_sess.pres_mgmt.status_qry__search == 'loading'}
<LoaderCircle
size="1em"
class="text-success-800-200 mx-1 animate-spin" />
{:else}
<Search size="1em" class="mx-1 text-neutral-800/80" />
{/if}
Search
</button>
{#if $ae_loc.edit_mode}
<label
class="bg-surface-200-800 rounded-token flex cursor-pointer items-center gap-1 px-2 py-1 text-xs font-semibold opacity-70 transition-all hover:opacity-100">
<span> Remote First </span>
<input
type="checkbox"
bind:checked={
pres_mgmt_loc.current.qry__remote_first
}
onchange={() => handle_search_trigger()}
class="checkbox checkbox-sm" />
</label>
{/if}
</form>
</div>
<Comp_pres_mgmt_session_search
event_location_obj_li={$lq__event_location_obj_li} />
{#if event_session_id_li.length}
<Comp_event_session_obj_li_wrapper
@@ -541,6 +478,15 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
Use the search bar above to find your session.
</p>
</div>
{#if pres_mgmt_loc.current.enable_time_window}
<!-- Time filter is ON — most common cause of "no results" when
sessions exist but are scheduled outside the current window. -->
<div class="preset-tonal-error rounded-lg p-3 text-center text-sm font-semibold">
⏰ Time filter is ON — sessions outside the current window are hidden.
Disable the clock button in the search bar to see all sessions.
</div>
{/if}
<div class="bg-surface-50-900/60 rounded-lg p-3">
<span
class="mb-2 block text-xs font-bold tracking-wide uppercase opacity-50"

View File

@@ -0,0 +1,285 @@
<script lang="ts">
interface Props {
event_location_obj_li: any[] | undefined;
log_lvl?: number;
}
let { event_location_obj_li, log_lvl = 0 }: Props = $props();
import { Clock, LoaderCircle, MapPin, RemoveFormatting, Search } from '@lucide/svelte';
import { ae_loc } from '$lib/stores/ae_stores';
import { events_sess } from '$lib/stores/ae_events_stores';
import { pres_mgmt_loc } from '$lib/stores/ae_events_stores__pres_mgmt.svelte';
function handle_search_trigger() {
pres_mgmt_loc.current.search_version++;
}
function prevent_default<T extends Event>(fn: (event: T) => void) {
return function (event: T) {
event.preventDefault();
fn(event);
};
}
</script>
<div class="ae_group pres_mgmt_session_search preset-tonal-primary rounded-xl">
<form
onsubmit={prevent_default(() => handle_search_trigger())}
autocomplete="off"
class="form flex w-full grow flex-row flex-wrap items-center justify-center gap-1 px-2 py-2">
<span>
<!-- Location filter toggle + select (authenticated users only) -->
<button
type="button"
class="btn btn-sm mx-1"
class:hidden={!$ae_loc.authenticated_access}
class:opacity-10={pres_mgmt_loc.current.show_content__session_search_room_name}
class:preset-filled-primary-300-700={pres_mgmt_loc.current.show_content__session_search_room_name}
class:preset-filled-primary-100-900={!pres_mgmt_loc.current.show_content__session_search_room_name}
onclick={() => {
pres_mgmt_loc.current.location_name_qry_str = '';
pres_mgmt_loc.current.show_content__session_search_room_name =
!pres_mgmt_loc.current.show_content__session_search_room_name;
handle_search_trigger();
}}
title="Filter by room / location">
<MapPin size="1em" />
</button>
<!-- Time window toggle icon button (authenticated only).
Highlighted when active so onsite staff can see at a glance whether the
filter is on. The controls below only appear when enabled. -->
<!-- TEMP: hide clock button for now, even in edit mode, since time filtering is a bit niche and the controls are a bit complex for non-technical users. Can re-enable when we have a better UI for the time filter controls. -->
<button
type="button"
onclick={() => {
pres_mgmt_loc.current.enable_time_window =
!pres_mgmt_loc.current.enable_time_window;
handle_search_trigger();
}}
title={pres_mgmt_loc.current.enable_time_window
? 'Time filter ON — click to disable'
: 'Show only sessions near current time'}
class="btn btn-sm mx-1"
class:hidden={!$ae_loc.edit_mode && 1==3}
class:opacity-10={pres_mgmt_loc.current.enable_time_window}
class:preset-filled-primary-300-700={pres_mgmt_loc.current.enable_time_window}
class:preset-filled-primary-100-900={!pres_mgmt_loc.current.enable_time_window}>
<Clock size="1em" />
</button>
</span>
<!-- Main search input -->
<!-- svelte-ignore a11y_autofocus -->
<input
type="search"
autofocus
placeholder="Search for a session"
id="session_fulltext_search_qry_str"
bind:value={pres_mgmt_loc.current.fulltext_search_qry_str}
autocomplete="off"
data-ignore="true"
class="input text-1xl ae_btn_info mx-1 w-80 font-mono font-bold transition-all hover:text-2xl"
onkeyup={(e) => {
if (e.key === 'Enter') handle_search_trigger();
}}
title="Search sessions by title, description, speaker, etc."
/>
<!-- Search submit button -->
<button
type="submit"
class="btn btn-lg ae_btn_primary mx-1 w-48 text-2xl font-bold"
title="Search for a session">
{#if $events_sess.pres_mgmt.status_qry__search == 'loading'}
<LoaderCircle size="1em" class="text-success-800-200 mx-1 animate-spin" />
{:else}
<Search size="1em" class="mx-1 text-neutral-800/80" />
{/if}
Search
</button>
<!-- Clear search text button -->
<button
type="button"
onclick={() => {
pres_mgmt_loc.current.fulltext_search_qry_str = '';
handle_search_trigger();
}}
class:hidden={!pres_mgmt_loc.current.fulltext_search_qry_str}
class="btn btn-sm ae_btn_warning mx-1"
title="Clear search text">
<RemoveFormatting size="1em" />
</button>
<!-- Remote First (edit_mode only) -->
{#if $ae_loc.edit_mode}
<label
class="
flex flex-wrap items-center justify-center gap-1
text-xs font-semibold
bg-surface-50-950/50
opacity-70 hover:opacity-100
px-2 py-1
rounded-md
cursor-pointer
transition-all
"
>
<span
class="opacity-50"
class:opacity-100={pres_mgmt_loc.current.qry__remote_first}
>
Remote First
</span>
<input
type="checkbox"
bind:checked={pres_mgmt_loc.current.qry__remote_first}
onchange={() => handle_search_trigger()}
class="checkbox checkbox-sm" />
</label>
{/if}
{#if pres_mgmt_loc.current.enable_time_window ||
pres_mgmt_loc.current.show_content__session_search_room_name}
<div class="
w-full
flex flex-wrap items-center justify-center gap-2
px-2
text-xs
"
>
<span class="
flex flex-wrap items-center justify-center gap-1
text-xs
bg-surface-50-950/50
opacity-70 hover:opacity-100
px-2 py-1
rounded-md
transition-all
"
>
<span class="opacity-50">
<MapPin size="0.875em" class="opacity-70" /> Location/Room:
</span>
<select
name="location_name_list"
id="session_location_name_list"
bind:value={pres_mgmt_loc.current.location_name_qry_str}
class="input w-min max-w-40 min-w-fit font-mono text-xs font-semibold transition-all"
class:hidden={!$ae_loc.authenticated_access ||
!pres_mgmt_loc.current.show_content__session_search_room_name}
onchange={() => handle_search_trigger()}
title="Filter by location / room name">
{#if event_location_obj_li}
<option value="">-- none --</option>
{#each event_location_obj_li as loc (loc.event_location_id)}
<option value={loc?.name}>{loc.name}</option>
{/each}
{/if}
</select>
<!-- Location filter toggle + select (authenticated users only) -->
<button
type="button"
class="btn btn-sm mx-1"
class:hidden={!pres_mgmt_loc.current.show_content__session_search_room_name}
class:preset-filled-warning-300-700={pres_mgmt_loc.current.show_content__session_search_room_name}
onclick={() => {
pres_mgmt_loc.current.location_name_qry_str = '';
pres_mgmt_loc.current.show_content__session_search_room_name =
!pres_mgmt_loc.current.show_content__session_search_room_name;
handle_search_trigger();
}}
title="Filter by room / location">
<MapPin size="1em" />
</button>
</span>
<!-- Time window controls — compact row, shown only when filter is active.
Two simple selects (before / after current time) keep this readable for
all experience levels without the noise of preset chip arrays. -->
{#if pres_mgmt_loc.current.enable_time_window}
<span
class="
flex flex-wrap items-center justify-center gap-1
text-xs
bg-surface-50-950/50
opacity-70 hover:opacity-100
px-2 py-1
rounded-md
transition-all
"
>
<span class="opacity-50">
<Clock size="0.875em" class="opacity-70" /> Showing sessions:
</span>
<span class="opacity-50">&minus;</span>
<select
bind:value={pres_mgmt_loc.current.time_window_before_minutes}
onchange={handle_search_trigger}
class="select select-sm max-w-fit px-1 py-0 text-xs font-semibold">
<!-- <option value="">0m</option> -->
<option value={0}>0m</option>
<option value={25}>25m</option>
<option value={90}>90m</option>
<option value={120}>2h</option>
<option value={240}>4h</option>
<option value={480}>8h</option>
<option value={720}>12h</option>
<option value={1440}>24h</option>
{#if $ae_loc.trusted_access}<option value={4320}>3d</option>{/if}
{#if $ae_loc.trusted_access}<option value={10080}>7d</option>{/if}
</select>
<span class="opacity-50">to &plus;</span>
<select
bind:value={pres_mgmt_loc.current.time_window_after_minutes}
onchange={handle_search_trigger}
class="select select-sm max-w-fit px-1 py-0 text-xs font-semibold">
<option value={90}>90m</option>
<option value={120}>2h</option>
<option value={240}>4h</option>
<option value={480}>8h</option>
<option value={720}>12h</option>
<option value={1440}>24h</option>
{#if $ae_loc.trusted_access}<option value={4320}>3d</option>{/if}
{#if $ae_loc.trusted_access}<option value={10080}>7d</option>{/if}
</select>
<!-- <span class="opacity-50">from now</span> -->
<button
type="button"
onclick={() => {
pres_mgmt_loc.current.enable_time_window =
!pres_mgmt_loc.current.enable_time_window;
handle_search_trigger();
}}
title={pres_mgmt_loc.current.enable_time_window
? 'Time filter ON — click to disable'
: 'Show only sessions near current time'}
class="btn btn-sm mx-1"
class:hidden={!pres_mgmt_loc.current.enable_time_window}
class:preset-filled-warning-300-700={pres_mgmt_loc.current.enable_time_window}
>
<Clock size="1em" />
</button>
</span>
{/if}
</div>
{/if}
</form>
</div>

View File

@@ -41,7 +41,7 @@ let lq__event_session_obj_li = $derived.by(() => {
return liveQuery(async () => {
if (link_to_type && link_to_id) {
const results = await db_events.session
.where(`${link_to_type}_id_random`)
.where(`${link_to_type}_id`)
.equals(link_to_id)
.sortBy('start_datetime');
return results;