feat(events): restore inc_file_counts opt-in, session list layout + button polish
- Add `inc_file_counts` flag to `load_ae_obj_id__event_session` — maps to backend alt view (v_event_session_w_file_count) when true; default stays lightweight. Callers never pass raw view names. - Preserve-on-write fallback in `_refresh_session_id_background` keeps cached file_count/file_count_all if API response omits them. - Session detail +page.ts uses `inc_file_counts: true` so SvelteKit prefetch no longer clobbers counts via bulkPut on hover. - Remove explicit `view: 'alt'` from launcher +page.ts (now invalid param). - Session list link: flex-1 + min-w-0 for full-row width; name flex-1 pushes badge group right; code + file_count stacked in flex-col items-end. - Hover styling: button-like appearance with slow fade-out (duration-500) / fast snap-in (hover:duration-150). - Session +page.svelte: use url_session_id (string) for link_to_id props and auth__kv.session[] index — fixes TS type error from number|undefined. - IDAA layout: dormant tech notice banner (guarded by 1==3, remove when ready). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,9 +20,9 @@ export async function load_ae_obj_id__event_session({
|
|||||||
inc_all_file_li = false,
|
inc_all_file_li = false,
|
||||||
inc_presentation_li = false,
|
inc_presentation_li = false,
|
||||||
inc_presenter_li = false,
|
inc_presenter_li = false,
|
||||||
|
inc_file_counts = false,
|
||||||
enabled = 'enabled',
|
enabled = 'enabled',
|
||||||
hidden = 'not_hidden',
|
hidden = 'not_hidden',
|
||||||
view = 'default',
|
|
||||||
limit = 100,
|
limit = 100,
|
||||||
offset = 0,
|
offset = 0,
|
||||||
try_cache = true,
|
try_cache = true,
|
||||||
@@ -34,14 +34,18 @@ export async function load_ae_obj_id__event_session({
|
|||||||
inc_all_file_li?: boolean;
|
inc_all_file_li?: boolean;
|
||||||
inc_presentation_li?: boolean;
|
inc_presentation_li?: boolean;
|
||||||
inc_presenter_li?: boolean;
|
inc_presenter_li?: boolean;
|
||||||
|
// When true, uses v_event_session_w_file_count (backend 'alt' view) which includes
|
||||||
|
// file_count / file_count_all. Default false — the base view is cheaper and sufficient
|
||||||
|
// for most callers. Use true when the caller needs counts (e.g. session detail page load).
|
||||||
|
inc_file_counts?: boolean;
|
||||||
enabled?: 'enabled' | 'all' | 'not_enabled';
|
enabled?: 'enabled' | 'all' | 'not_enabled';
|
||||||
hidden?: 'hidden' | 'all' | 'not_hidden';
|
hidden?: 'hidden' | 'all' | 'not_hidden';
|
||||||
view?: string;
|
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
try_cache?: boolean;
|
try_cache?: boolean;
|
||||||
log_lvl?: number;
|
log_lvl?: number;
|
||||||
}): Promise<ae_EventSession | null> {
|
}): Promise<ae_EventSession | null> {
|
||||||
|
const view = inc_file_counts ? 'alt' : 'default';
|
||||||
const start_time = performance.now();
|
const start_time = performance.now();
|
||||||
if (log_lvl) {
|
if (log_lvl) {
|
||||||
console.log(
|
console.log(
|
||||||
@@ -178,6 +182,26 @@ async function _refresh_session_id_background({
|
|||||||
`📦 [Trace] _refresh_session_id: Received from API at ${elapsed}ms (id=${processed_obj.id})`
|
`📦 [Trace] _refresh_session_id: Received from API at ${elapsed}ms (id=${processed_obj.id})`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// PRESERVE AGGREGATE COUNTS: The individual session API view (view=default)
|
||||||
|
// does not compute file_count / file_count_all — those come from the list
|
||||||
|
// view SQL query. bulkPut replaces the full IDB record, so if we write
|
||||||
|
// undefined here we clobber the counts the list search already stored.
|
||||||
|
// WHY: SvelteKit link prefetching triggers this path on hover over session
|
||||||
|
// links in the search results list, causing the file count badge to blip.
|
||||||
|
// FIX: Read the cached counts and keep them if the API didn't return new ones.
|
||||||
|
if (try_cache && (processed_obj.file_count_all == null || processed_obj.file_count == null)) {
|
||||||
|
try {
|
||||||
|
const cached_id = processed_obj.id || processed_obj.event_session_id;
|
||||||
|
const cached = cached_id ? await db_events.session.get(cached_id) : null;
|
||||||
|
if (cached) {
|
||||||
|
if (processed_obj.file_count_all == null && cached.file_count_all != null)
|
||||||
|
processed_obj.file_count_all = cached.file_count_all;
|
||||||
|
if (processed_obj.file_count == null && cached.file_count != null)
|
||||||
|
processed_obj.file_count = cached.file_count;
|
||||||
|
}
|
||||||
|
} catch (_) { /* non-critical — best-effort count preservation */ }
|
||||||
|
}
|
||||||
|
|
||||||
if (try_cache) {
|
if (try_cache) {
|
||||||
await db_save_ae_obj_li__ae_obj({
|
await db_save_ae_obj_li__ae_obj({
|
||||||
db_instance: db_events,
|
db_instance: db_events,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export interface PresMgmtLocState {
|
|||||||
show_content__session_search_room_name: boolean;
|
show_content__session_search_room_name: boolean;
|
||||||
show_content__session_view: string | null;
|
show_content__session_view: string | null;
|
||||||
show_content__session_qr: boolean;
|
show_content__session_qr: boolean;
|
||||||
|
hide__session_code: boolean;
|
||||||
hide__session_msg: boolean;
|
hide__session_msg: boolean;
|
||||||
hide__session_poc: boolean;
|
hide__session_poc: boolean;
|
||||||
hide__session_poc_biography: boolean;
|
hide__session_poc_biography: boolean;
|
||||||
@@ -159,6 +160,7 @@ export const pres_mgmt_loc_defaults: PresMgmtLocState = {
|
|||||||
show_content__session_search_room_name: false,
|
show_content__session_search_room_name: false,
|
||||||
show_content__session_view: null,
|
show_content__session_view: null,
|
||||||
show_content__session_qr: false,
|
show_content__session_qr: false,
|
||||||
|
hide__session_code: true, // Default hidden; toggle in ae_comp__events_menu_opts to show
|
||||||
hide__session_msg: true,
|
hide__session_msg: true,
|
||||||
hide__session_poc: true,
|
hide__session_poc: true,
|
||||||
hide__session_poc_biography: true,
|
hide__session_poc_biography: true,
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ export async function load({ params, parent, url }) {
|
|||||||
inc_all_file_li: true,
|
inc_all_file_li: true,
|
||||||
inc_presentation_li: true,
|
inc_presentation_li: true,
|
||||||
inc_presenter_li: true,
|
inc_presenter_li: true,
|
||||||
view: 'alt',
|
|
||||||
try_cache: true,
|
try_cache: true,
|
||||||
log_lvl: 0
|
log_lvl: 0
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ import Session_view from './session_view.svelte';
|
|||||||
import Session_page_menu from './session_page_menu.svelte';
|
import Session_page_menu from './session_page_menu.svelte';
|
||||||
import Comp_event_presentation_obj_li from '../../../../ae_comp__event_presentation_obj_li.svelte';
|
import Comp_event_presentation_obj_li from '../../../../ae_comp__event_presentation_obj_li.svelte';
|
||||||
import Comp_event_presenter_form_agree from '../../presenter/[presenter_id]/ae_comp__event_presenter_form_agree.svelte';
|
import Comp_event_presenter_form_agree from '../../presenter/[presenter_id]/ae_comp__event_presenter_form_agree.svelte';
|
||||||
import { LoaderCircle } from '@lucide/svelte';
|
import Comp_event_files_upload from '../../../../ae_comp__event_files_upload.svelte';
|
||||||
|
import Element_manage_event_file_li_wrap from '$lib/elements/element_manage_event_file_li_direct.svelte';
|
||||||
|
import { Archive, FileText, Info, LoaderCircle, Upload } from '@lucide/svelte';
|
||||||
// STABILITY FIX: Capture URL params reactively via $derived so liveQuery
|
// STABILITY FIX: Capture URL params reactively via $derived so liveQuery
|
||||||
// closures see a stable identifier that updates on same-route navigation.
|
// closures see a stable identifier that updates on same-route navigation.
|
||||||
let url_session_id = $derived(data.params.session_id);
|
let url_session_id = $derived(data.params.session_id);
|
||||||
@@ -114,31 +116,101 @@ if (!$events_sess.pres_mgmt) $events_sess.pres_mgmt = {};
|
|||||||
{lq__event_session_obj}
|
{lq__event_session_obj}
|
||||||
{lq__auth__event_presenter_obj} />
|
{lq__auth__event_presenter_obj} />
|
||||||
|
|
||||||
<!-- Metadata Section: Session identity card — name, time, room, host, description -->
|
<!-- Session Info / Files toggle — mirrors the same pattern on the Presenter page.
|
||||||
<div
|
WHY: Admins and session POCs need to upload/manage session-level files (e.g. agendas,
|
||||||
class="bg-surface-50-950 border-surface-200-800 rounded-xl border p-4 shadow-sm">
|
handouts) separately from presenter files. The toggle was present for years and was
|
||||||
<Session_view
|
inadvertently dropped during the V3 migration. -->
|
||||||
{lq__event_presenter_obj}
|
{#if $ae_loc.authenticated_access}
|
||||||
{lq__event_session_obj}
|
<header class="ae_module_header">
|
||||||
{lq__auth__event_presenter_obj}
|
<button
|
||||||
{lq__event_presentation_obj_li} />
|
type="button"
|
||||||
</div>
|
onclick={() => {
|
||||||
|
$events_loc.pres_mgmt.show_content__session_view =
|
||||||
|
$events_loc.pres_mgmt.show_content__session_view === 'manage_files'
|
||||||
|
? null
|
||||||
|
: 'manage_files';
|
||||||
|
}}
|
||||||
|
class="btn btn-md"
|
||||||
|
class:preset-filled-secondary-500={$events_loc.pres_mgmt.show_content__session_view === 'manage_files'}
|
||||||
|
class:preset-filled-tertiary-500={$events_loc.pres_mgmt.show_content__session_view !== 'manage_files'}
|
||||||
|
title="Toggle between session info and session file management">
|
||||||
|
{#if $events_loc.pres_mgmt.show_content__session_view === 'manage_files'}
|
||||||
|
<Info size="1em" class="m-1" />
|
||||||
|
Session Info?
|
||||||
|
{:else}
|
||||||
|
<Archive size="1em" class="m-1" />
|
||||||
|
Session Files?
|
||||||
|
<span
|
||||||
|
class="badge preset-tonal-success"
|
||||||
|
class:hidden={!$lq__event_session_obj?.file_count}>
|
||||||
|
<FileText size="1em" class="m-1" />
|
||||||
|
{$lq__event_session_obj?.file_count}×
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Presentation List Section -->
|
{#if $events_loc.pres_mgmt.show_content__session_view === 'manage_files' && $ae_loc.authenticated_access}
|
||||||
<div class="w-full">
|
<!-- Session Files Section -->
|
||||||
<!--
|
<div>
|
||||||
|
{#if $ae_loc.trusted_access || $events_loc.auth__kv.session[url_session_id]}
|
||||||
|
<Comp_event_files_upload
|
||||||
|
class_li="border border-surface-200-800 rounded-xl p-4 bg-surface-50-900 hover:bg-surface-100-900 transition-colors duration-200"
|
||||||
|
link_to_type="event_session"
|
||||||
|
link_to_id={url_session_id}>
|
||||||
|
{#snippet label()}
|
||||||
|
<span>
|
||||||
|
<div class="text-lg">
|
||||||
|
<Upload size="1em" />
|
||||||
|
<strong>Upload session files</strong>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm italic opacity-60">
|
||||||
|
Session-level handouts, agendas, or supplemental materials
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
{/snippet}
|
||||||
|
</Comp_event_files_upload>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="w-max max-w-full overflow-x-auto">
|
||||||
|
<Element_manage_event_file_li_wrap
|
||||||
|
link_to_type="event_session"
|
||||||
|
link_to_id={url_session_id}
|
||||||
|
allow_basic={$ae_loc.trusted_access ||
|
||||||
|
!!$events_loc.auth__kv.session[url_session_id]}
|
||||||
|
allow_moderator={$ae_loc.trusted_access ||
|
||||||
|
!!$events_loc.auth__kv.session[url_session_id]}
|
||||||
|
container_class_li={''} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Metadata Section: Session identity card — name, time, room, host, description -->
|
||||||
|
<div
|
||||||
|
class="bg-surface-50-950 border-surface-200-800 rounded-xl border p-4 shadow-sm">
|
||||||
|
<Session_view
|
||||||
|
{lq__event_presenter_obj}
|
||||||
|
{lq__event_session_obj}
|
||||||
|
{lq__auth__event_presenter_obj}
|
||||||
|
{lq__event_presentation_obj_li} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Presentation List Section -->
|
||||||
|
<div class="w-full">
|
||||||
|
<!--
|
||||||
CRITICAL FIX: Use the pre-loaded data (data.initial_session_obj) as a fallback
|
CRITICAL FIX: Use the pre-loaded data (data.initial_session_obj) as a fallback
|
||||||
until the `liveQuery` store ($lq...) emits its first value. This avoids
|
until the `liveQuery` store ($lq...) emits its first value. This avoids
|
||||||
a blank first-draw when IndexedDB is empty on a cold start — the LQ
|
a blank first-draw when IndexedDB is empty on a cold start — the LQ
|
||||||
will take over once the DB write completes. Prefer blocking loads
|
will take over once the DB write completes. Prefer blocking loads
|
||||||
where possible; use this fallback when you must load asynchronously.
|
where possible; use this fallback when you must load asynchronously.
|
||||||
-->
|
-->
|
||||||
<Comp_event_presentation_obj_li
|
<Comp_event_presentation_obj_li
|
||||||
lq__event_presentation_obj_li={$lq__event_presentation_obj_li ??
|
lq__event_presentation_obj_li={$lq__event_presentation_obj_li ??
|
||||||
data.initial_session_obj?.event_presentation_li ??
|
data.initial_session_obj?.event_presentation_li ??
|
||||||
[]}
|
[]}
|
||||||
{log_lvl} />
|
{log_lvl} />
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Background Connection Status (Non-blocking) -->
|
<!-- Background Connection Status (Non-blocking) -->
|
||||||
{#if !$lq__event_session_obj}
|
{#if !$lq__event_session_obj}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export async function load({ params, parent }) {
|
|||||||
inc_file_li: true,
|
inc_file_li: true,
|
||||||
inc_presentation_li: true,
|
inc_presentation_li: true,
|
||||||
inc_presenter_li: true,
|
inc_presenter_li: true,
|
||||||
|
inc_file_counts: true, // Use richer view so file_count badge is accurate on session detail page
|
||||||
try_cache: true,
|
try_cache: true,
|
||||||
log_lvl: log_lvl
|
log_lvl: log_lvl
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ function toggle_details(id: string) {
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<a
|
<a
|
||||||
href="/events/{session_obj?.event_id}/session/{session_obj?.event_session_id}"
|
href="/events/{session_obj?.event_id}/session/{session_obj?.event_session_id}"
|
||||||
class="hover:text-primary-500 flex flex-row items-center gap-2 text-left text-lg font-bold transition-colors duration-200">
|
class="hover:text-primary-800-200 hover:bg-surface-400-600 active:bg-surface-200-700 flex flex-1 flex-row items-center gap-2 rounded-lg px-2 py-2 text-left text-lg font-bold transition-colors duration-1000 hover:duration-300 min-w-0">
|
||||||
{#if session_obj?.hide}
|
{#if session_obj?.hide}
|
||||||
<EyeOff
|
<EyeOff
|
||||||
size="1em"
|
size="1em"
|
||||||
@@ -160,15 +160,24 @@ function toggle_details(id: string) {
|
|||||||
class="text-primary-500 flex-none" />
|
class="text-primary-500 flex-none" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<span>{session_obj?.name}</span>
|
<span class="flex-1">{session_obj?.name}</span>
|
||||||
|
|
||||||
{#if session_obj?.file_count_all}
|
{#if (!$events_loc.pres_mgmt.hide__session_code && session_obj?.code) || session_obj?.file_count_all}
|
||||||
<span
|
<div class="flex flex-col items-end gap-0.5">
|
||||||
class="badge preset-tonal-success flex items-center gap-1 px-1 py-0 text-xs">
|
{#if !$events_loc.pres_mgmt.hide__session_code && session_obj?.code}
|
||||||
<Check size="1em" />
|
<span class="border-surface-300-700 text-surface-700-300 bg-surface-200-800 rounded border px-1.5 py-0.5 font-mono text-xs select-all hover:bg-surface-100-900">
|
||||||
|
{session_obj.code}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{session_obj.file_count_all}
|
{#if session_obj?.file_count_all}
|
||||||
</span>
|
<span
|
||||||
|
class="badge preset-tonal-success flex items-center gap-1 px-1 py-0 text-xs">
|
||||||
|
<Check size="1em" />
|
||||||
|
{session_obj.file_count_all}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,17 @@ let verify_failed_for_uuid: string | null = null;
|
|||||||
// Handles the case where site_cfg_json loads without novi_idaa_api_key (stale cache)
|
// Handles the case where site_cfg_json loads without novi_idaa_api_key (stale cache)
|
||||||
// or the Novi API call hangs — the user would otherwise be stuck with no escape.
|
// or the Novi API call hangs — the user would otherwise be stuck with no escape.
|
||||||
const VERIFY_TIMEOUT_MS = 8000;
|
const VERIFY_TIMEOUT_MS = 8000;
|
||||||
|
|
||||||
|
// One-time technical notice banner — persisted in localStorage so it only shows once.
|
||||||
|
// TEMPORARY (2026-04-01): Remove this block after a few days.
|
||||||
|
const TECH_NOTICE_KEY = 'idaa_tech_notice_2026_04_01_dismissed';
|
||||||
|
let show_tech_notice: boolean = $state(
|
||||||
|
browser ? localStorage.getItem(TECH_NOTICE_KEY) !== 'true' : false
|
||||||
|
);
|
||||||
|
function dismiss_tech_notice() {
|
||||||
|
show_tech_notice = false;
|
||||||
|
try { localStorage.setItem(TECH_NOTICE_KEY, 'true'); } catch { /* storage unavailable */ }
|
||||||
|
}
|
||||||
let verifying_timed_out: boolean = $state(false);
|
let verifying_timed_out: boolean = $state(false);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -353,6 +364,28 @@ async function verify_novi_uuid(
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if $ae_loc.trusted_access || ($ae_loc.authenticated_access && $idaa_loc.novi_uuid && $idaa_loc.novi_verified)}
|
{:else if $ae_loc.trusted_access || ($ae_loc.authenticated_access && $idaa_loc.novi_uuid && $idaa_loc.novi_verified)}
|
||||||
|
{#if show_tech_notice && 1 == 3}
|
||||||
|
<!-- TEMPORARY (2026-04-01): One-time notice about last night's technical issues. Remove after a few days. -->
|
||||||
|
<div class="m-2 flex items-start gap-3 rounded-lg border border-yellow-400 bg-yellow-50 p-3 text-yellow-900 dark:border-yellow-600 dark:bg-yellow-950 dark:text-yellow-100">
|
||||||
|
<span class="fas fa-exclamation-triangle mt-0.5 shrink-0 text-yellow-500"></span>
|
||||||
|
<p class="flex-1 text-sm">
|
||||||
|
Opt 1:
|
||||||
|
<strong>Notice:</strong> We experienced technical difficulties last night (March 31) and this morning (April 1). Things are back to normal. We apologize for any inconvenience.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
Opt 2:
|
||||||
|
<strong>Notice:</strong> We have experienced some technical difficulties recently. Things are back to normal. We apologize for any inconvenience.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="shrink-0 rounded px-2 py-1 text-xs font-semibold hover:bg-yellow-200 dark:hover:bg-yellow-800 btn"
|
||||||
|
onclick={dismiss_tech_notice}>
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
{#if $idaa_loc.novi_uuid}
|
{#if $idaa_loc.novi_uuid}
|
||||||
<span class="text-sm text-gray-500">
|
<span class="text-sm text-gray-500">
|
||||||
|
|||||||
Reference in New Issue
Block a user