Compare commits
10 Commits
e4265f69af
...
898afd9775
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
898afd9775 | ||
|
|
74e65ea892 | ||
|
|
1ad3d2030d | ||
|
|
721facf7ba | ||
|
|
a42b49dd50 | ||
|
|
278a40c981 | ||
|
|
5fcf2e86f1 | ||
|
|
7543bf6ae5 | ||
|
|
9af5a292b6 | ||
|
|
2595664dd1 |
464
documentation/MODULE__AE_Events_PressMgmt_Launcher.md
Normal file
464
documentation/MODULE__AE_Events_PressMgmt_Launcher.md
Normal 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 4–12 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
|
||||
|
||||
### 1–2 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 |
|
||||
@@ -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)
|
||||
|
||||
227
documentation/archive/TODO__Agents__ARCHIVE_2026-04.md
Normal file
227
documentation/archive/TODO__Agents__ARCHIVE_2026-04.md
Normal 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).
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) -->
|
||||
|
||||
@@ -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
|
||||
|
||||
23
src/routes/events/[event_id]/(pres_mgmt)/locations/+page.ts
Normal file
23
src/routes/events/[event_id]/(pres_mgmt)/locations/+page.ts
Normal 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;
|
||||
}
|
||||
@@ -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'}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">−</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 +</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>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user