diff --git a/src/lib/ae_events/ae_events__event_presenter.ts b/src/lib/ae_events/ae_events__event_presenter.ts
index 6c91a78e..140ea832 100644
--- a/src/lib/ae_events/ae_events__event_presenter.ts
+++ b/src/lib/ae_events/ae_events__event_presenter.ts
@@ -595,7 +595,6 @@ export async function email_sign_in__event_presenter({
}) {
if (
!to_email ||
- !person_id ||
!person_passcode ||
!event_id ||
!event_presenter_id
@@ -609,10 +608,14 @@ export async function email_sign_in__event_presenter({
// Routes to the session page (which has the sign-in handler mounted) not /presenter/[id]
// which has no sign-in handler. Includes presenter_id + presentation_id so the handler
// can grant presenter-level auth (not just session read access).
- // Per-param encodeURIComponent is required for query values — encodeURI() does not
- // encode '+', which URLSearchParams.get() then decodes as a space, breaking email
- // addresses that contain '+' (e.g. test+alias@example.com).
- const sign_in_url = `${base_url}/events/${event_id}/session/${event_session_id}?person_id=${encodeURIComponent(person_id ?? '')}&person_pass=${encodeURIComponent(person_passcode ?? '')}&presenter_id=${encodeURIComponent(event_presenter_id ?? '')}&presentation_id=${encodeURIComponent(event_presentation_id ?? '')}`;
+ // person_id is omitted for email-only presenters (no Person record) — the sign-in handler
+ // falls back to presenter_id and resolves their identity via Dexie lookup.
+ const sign_in_parts: string[] = [];
+ if (person_id) sign_in_parts.push(`person_id=${encodeURIComponent(person_id)}`);
+ sign_in_parts.push(`person_pass=${encodeURIComponent(person_passcode ?? '')}`);
+ sign_in_parts.push(`presenter_id=${encodeURIComponent(event_presenter_id ?? '')}`);
+ if (event_presentation_id) sign_in_parts.push(`presentation_id=${encodeURIComponent(event_presentation_id)}`);
+ const sign_in_url = `${base_url}/events/${event_id}/session/${event_session_id}?${sign_in_parts.join('&')}`;
const body_html = `
${to_name},
Your sign-in link for ${presentation_name ?? 'Presentation'} (Session: ${session_name ?? 'Session'}): ${sign_in_url}
This link takes you to the session page — your presentation and file upload sections will be available after you sign in.
`;
return await api.send_email({
api_cfg,
diff --git a/src/routes/events/[event_id]/(pres_mgmt)/presenter/ae_comp__event_presenter_obj_li.svelte b/src/routes/events/[event_id]/(pres_mgmt)/presenter/ae_comp__event_presenter_obj_li.svelte
index 3e659b33..bbe50bde 100644
--- a/src/routes/events/[event_id]/(pres_mgmt)/presenter/ae_comp__event_presenter_obj_li.svelte
+++ b/src/routes/events/[event_id]/(pres_mgmt)/presenter/ae_comp__event_presenter_obj_li.svelte
@@ -193,7 +193,8 @@ let ae_tmp: key_val = $state({});
// Prefer Person-record email; fall back to presenter.email for
// presenters without a Person record (common for LCI/iMIS gaps).
const use_email = event_presenter_obj.person_primary_email ?? event_presenter_obj.email;
- const use_person_id = event_presenter_obj.person_id ?? event_presenter_obj.event_presenter_id;
+ // null for email-only presenters — email function omits person_id from URL; sign-in derives identity from presenter_id
+ const use_person_id = event_presenter_obj.person_id;
const use_passcode = event_presenter_obj.person_passcode ?? event_presenter_obj.passcode;
if (!use_email) {
diff --git a/src/routes/events/[event_id]/sign_in_out.svelte b/src/routes/events/[event_id]/sign_in_out.svelte
index b5861453..607cd93a 100644
--- a/src/routes/events/[event_id]/sign_in_out.svelte
+++ b/src/routes/events/[event_id]/sign_in_out.svelte
@@ -57,11 +57,14 @@ onMount(() => {
let url_presenter_id = data.url.searchParams.get('presenter_id');
let url_session_id = data.url.searchParams.get('session_id');
- // Gate on having a sign-in target (presenter or session), not on the passcode value.
- // WHY: passcode may be empty/null for presenter records without one configured;
- // the old gate `if (url_person_pass)` broke when passcode was empty string.
- // The presenter_id / session_id in the URL is the real sign-in signal.
- if (url_person_id !== null && (url_presenter_id || url_session_id)) {
+ // Gate on having a sign-in target (presenter or session) — person_id is optional.
+ // WHY: email-only presenters (no Person record) have no separate person_id to put
+ // in the URL. expand_auth_for_person resolves their identity via presenter_id_hint.
+ // For session POC links, person_id is always present (it's poc_person_id).
+ if (url_presenter_id || url_session_id) {
+ // person_id is absent for email-only presenter links — fall back to presenter_id.
+ // POC links always include person_id, so this branch is only hit for presenters.
+ if (!url_person_id) url_person_id = url_presenter_id ?? url_session_id;
console.log(
`ae_events_pres_mgmt session [slug] +page.svelte: event_session_id=${$events_slct.event_session_id}; person_id=${url_person_id}; person_pass=${url_person_pass}; presentation_id=${url_presentation_id}; presenter_id=${url_presenter_id}`
);