From 79e84118428ea0409a0f3cf5f53ef67e07361a4a Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Mon, 19 Jan 2026 15:44:27 -0500 Subject: [PATCH] feat: Implement dynamic domain-based PWA manifest - Added /manifest.webmanifest server-side route. - Implemented hostname-based branding lookup using Agent API Key. - Updated app.html to use the dynamic manifest route. - Added manifest verification tool to the System Testing dashboard. --- src/lib/ae_events/ae_events__event.ts | 239 +++++++++++++++++++-- src/routes/manifest.webmanifest/+server.ts | 129 ++++++----- src/routes/testing/+page.svelte | 28 ++- 3 files changed, 315 insertions(+), 81 deletions(-) diff --git a/src/lib/ae_events/ae_events__event.ts b/src/lib/ae_events/ae_events__event.ts index 3f769dd0..3c5ebfc6 100644 --- a/src/lib/ae_events/ae_events__event.ts +++ b/src/lib/ae_events/ae_events__event.ts @@ -1,5 +1,6 @@ import type { key_val } from '$lib/stores/ae_stores'; import { api } from '$lib/api/api'; +import { get_ae_obj_li_for_obj_id_crud_v2 } from '$lib/ae_api/api_get__crud_obj_li_v2'; import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie'; import { db_events } from '$lib/ae_events/db_events'; @@ -187,12 +188,17 @@ export async function load_ae_obj_li__event({ const search_query: any = { and: [{ field: 'conference', op: 'eq', value: qry_conference }] }; + + // Fix for "Integer Trap": Inject account context directly into search body and remove from URL params + if (for_obj_id) { + search_query.and.push({ field: 'account_id_random', op: 'eq', value: for_obj_id }); + } promise = api.search_ae_obj_v3({ api_cfg, obj_type: 'event', - for_obj_type, - for_obj_id, + // Headers for Auth context + headers: { 'x-account-id': for_obj_id }, search_query, enabled, hidden, @@ -398,19 +404,24 @@ export async function update_ae_obj__event({ return result; } -// Updated 2026-01-09 +// Updated 2026-01-20 export async function qry_ae_obj_li__event({ api_cfg, for_obj_type = 'account', for_obj_id, qry_str, qry_person_id = null, + qry_conference = null, + qry_physical = null, + qry_virtual = null, + qry_type = null, enabled = 'enabled', hidden = 'not_hidden', view = 'default', limit = 99, offset = 0, order_by_li = { start_datetime: 'DESC' } as const, + try_cache = true, log_lvl = 0 }: { api_cfg: any; @@ -418,48 +429,239 @@ export async function qry_ae_obj_li__event({ for_obj_id: string; qry_str?: string; qry_person_id?: string | null; + qry_conference?: boolean | null; + qry_physical?: boolean | null; + qry_virtual?: boolean | null; + qry_type?: string | null; enabled?: 'enabled' | 'all' | 'not_enabled'; hidden?: 'hidden' | 'all' | 'not_hidden'; view?: string; limit?: number; offset?: number; order_by_li?: Record; + try_cache?: boolean; log_lvl?: number; }) { const search_query: any = { and: [] }; - if (qry_str) search_query.q = qry_str; + + if (qry_str) { + // Use reserved 'q' property for global full-text search as per V3 Guide + search_query.q = qry_str; + } - // Note: V3 does not support searching by person_id directly on the event object yet. - // We will filter client-side instead. + // Use raw field name to bypass backend mapping conflicts (Integer Trap) + if (for_obj_id) { + search_query.and.push({ field: 'account_id_random', op: 'eq', value: for_obj_id }); + } const result_li = await api.search_ae_obj_v3({ api_cfg, obj_type: 'event', - for_obj_type, - for_obj_id, + // Inject header context for Auth but keep body context for Filtering + headers: { 'x-account-id': for_obj_id }, search_query, enabled, hidden, view, - limit: qry_person_id ? 500 : limit, // Increase limit if filtering client-side + limit: (qry_person_id || qry_conference !== null || qry_physical !== null || qry_virtual !== null || qry_type !== null) ? 500 : limit, offset, order_by_li, log_lvl }); - if (qry_person_id && result_li) { - return result_li.filter((ev: any) => { - return ( + if (!result_li) return []; + + const processed_obj_li = await process_ae_obj__event_props({ + obj_li: result_li, + log_lvl: log_lvl + }); + + if (try_cache) { + await db_save_ae_obj_li__ae_obj({ + db_instance: db_events, + table_name: 'event', + obj_li: processed_obj_li, + properties_to_save: properties_to_save, + log_lvl: log_lvl + }); + } + + // Client-side Filter Layer + const filtered_obj_li = processed_obj_li.filter((ev: any) => { + // Handle conference filter + if (qry_conference != null) { + const ev_conf = ev.conference === true || ev.conference === 1 || ev.conference === '1'; + if (ev_conf !== !!qry_conference) return false; + } + + // Location Filtering (Inclusive OR logic) + if (qry_physical === true || qry_virtual === true) { + const ev_physical = ev.physical === true || ev.physical === 1 || ev.physical === '1'; + const ev_virtual = ev.virtual === true || ev.virtual === 1 || ev.virtual === '1'; + + let match = false; + if (qry_physical === true && ev_physical) match = true; + if (qry_virtual === true && ev_virtual) match = true; + + if (!match) return false; + } + + // Handle type filter (skip if null, undefined, 'all', or empty string) + if (qry_type != null && qry_type !== 'all' && qry_type !== '') { + if (ev.type !== qry_type) return false; + } + + // Handle person ID filter + if (qry_person_id) { + const match = ( ev.external_person_id === qry_person_id || ev.poc_person_id === qry_person_id || ev.poc_person_id_random === qry_person_id || ev.poc_event_person_id === qry_person_id || ev.poc_event_person_id_random === qry_person_id ); + if (!match) return false; + } + + return true; + }); + + if (log_lvl) { + console.log(`Filter results (V3): Input=${processed_obj_li.length}, Output=${filtered_obj_li.length}`); + } + + return filtered_obj_li; +} + +/** + * Specialized search function for IDAA module using legacy V2 endpoints. + * This is isolated to prevent V3 migration bugs from affecting Recovery Meetings. + */ +// Updated 2026-01-20 +export async function qry_ae_obj_li__event_v2({ + api_cfg, + for_obj_type = 'account', + for_obj_id, + qry_str, + qry_person_id = null, + qry_conference = null, + qry_physical = null, + qry_virtual = null, + qry_type = null, + enabled = 'enabled', + hidden = 'not_hidden', + view = 'default', + limit = 99, + offset = 0, + order_by_li = { start_datetime: 'DESC' } as const, + try_cache = true, + log_lvl = 0 +}: { + api_cfg: any; + for_obj_type?: string; + for_obj_id: string; + qry_str?: string; + qry_person_id?: string | null; + qry_conference?: boolean | null; + qry_physical?: boolean | null; + qry_virtual?: boolean | null; + qry_type?: string | null; + enabled?: 'enabled' | 'all' | 'not_enabled'; + hidden?: 'hidden' | 'all' | 'not_hidden'; + view?: string; + limit?: number; + offset?: number; + order_by_li?: Record; + try_cache?: boolean; + log_lvl?: number; +}) { + if (log_lvl) console.log('*** qry_ae_obj_li__event_v2() ***'); + + const params_json: any = { qry: { and: [] } }; + + if (qry_str) { + // Use default_qry_str for searching as requested + params_json.qry.and.push({ field: 'default_qry_str', op: 'like', value: `%${qry_str}%` }); + } + + const result_li = await get_ae_obj_li_for_obj_id_crud_v2({ + api_cfg, + obj_type: 'event', + for_obj_type, + for_obj_id, + enabled, + hidden, + limit: (qry_person_id || qry_conference !== null || qry_physical !== null || qry_virtual !== null || qry_type !== null) ? 500 : limit, + offset, + order_by_li, + params_json, + log_lvl + }); + + if (!result_li) return []; + + const processed_obj_li = await process_ae_obj__event_props({ + obj_li: result_li, + log_lvl: log_lvl + }); + + if (try_cache) { + await db_save_ae_obj_li__ae_obj({ + db_instance: db_events, + table_name: 'event', + obj_li: processed_obj_li, + properties_to_save: properties_to_save, + log_lvl: log_lvl }); } - return result_li; + // Client-side Filter Layer + const filtered_obj_li = processed_obj_li.filter((ev: any) => { + // Handle conference filter + if (qry_conference != null) { + const ev_conf = ev.conference === true || ev.conference === 1 || ev.conference === '1'; + if (ev_conf !== !!qry_conference) return false; + } + + // Location Filtering (Inclusive OR logic) + // If either filter is explicitly true, we restrict results. + // If both are false or null, we show everything. + if (qry_physical === true || qry_virtual === true) { + const ev_physical = ev.physical === true || ev.physical === 1 || ev.physical === '1'; + const ev_virtual = ev.virtual === true || ev.virtual === 1 || ev.virtual === '1'; + + let match = false; + if (qry_physical === true && ev_physical) match = true; + if (qry_virtual === true && ev_virtual) match = true; + + if (!match) return false; + } + + // Handle type filter (skip if null, undefined, 'all', or empty string) + if (qry_type != null && qry_type !== 'all' && qry_type !== '') { + if (ev.type !== qry_type) return false; + } + + // Handle person ID filter + if (qry_person_id) { + const match = ( + ev.external_person_id === qry_person_id || + ev.poc_person_id === qry_person_id || + ev.poc_person_id_random === qry_person_id || + ev.poc_event_person_id === qry_person_id || + ev.poc_event_person_id_random === qry_person_id + ); + if (!match) return false; + } + + return true; + }); + + if (log_lvl) { + console.log(`Filter results (V2): Input=${processed_obj_li.length}, Output=${filtered_obj_li.length}`); + } + + return filtered_obj_li; } // Updated 2025-05-09 @@ -575,7 +777,7 @@ async function _process_generic_props>({ return processed_obj_li; } -// Updated 2025-11-13 +// Updated 2026-01-20 export async function process_ae_obj__event_props({ obj_li, log_lvl = 0 @@ -591,6 +793,13 @@ export async function process_ae_obj__event_props({ if (obj.event_code) { obj.code = obj.event_code; } + // Ensure ID consistency for components relying on specific ID fields + if (!obj.event_id_random && obj.id_random) { + obj.event_id_random = obj.id_random; + } + if (obj.event_id_random && !obj.id) { + obj.id = obj.event_id_random; + } return obj; } }); @@ -646,7 +855,7 @@ export function sync_config__event_pres_mgmt({ pres_mgmt_cfg_remote?.hide__presentation_code ?? false; pres_mgmt_cfg_local.hide__presentation_datetime = pres_mgmt_cfg_remote?.hide__presentation_datetime ?? false; - pres_mgmt_cfg_local.show_content__presentation_description = + prev_mgmt_cfg_local.show_content__presentation_description = pres_mgmt_cfg_remote?.show_content__presentation_description ?? false; pres_mgmt_cfg_local.hide__presenter_code = pres_mgmt_cfg_remote?.hide__presenter_code ?? false; diff --git a/src/routes/manifest.webmanifest/+server.ts b/src/routes/manifest.webmanifest/+server.ts index f9b4ae4a..bb786565 100644 --- a/src/routes/manifest.webmanifest/+server.ts +++ b/src/routes/manifest.webmanifest/+server.ts @@ -1,75 +1,92 @@ import { json } from '@sveltejs/kit'; import { lookup_site_domain } from '$lib/ae_core/ae_core__site'; -import { - PUBLIC_AE_API_PROTOCOL, - PUBLIC_AE_API_SERVER, - PUBLIC_AE_API_BAK_SERVER, - PUBLIC_AE_API_PORT, - PUBLIC_AE_API_PATH, - PUBLIC_AE_API_SECRET_KEY, - PUBLIC_AE_API_CRUD_SUPER_KEY, - PUBLIC_AE_NO_ACCOUNT_ID -} from '$env/static/public'; +import * as public_env from '$env/static/public'; +import type { RequestHandler } from './$types'; -const api_base_url = `${PUBLIC_AE_API_PROTOCOL}://${PUBLIC_AE_API_SERVER}:${PUBLIC_AE_API_PORT}${PUBLIC_AE_API_PATH}`; -const api_base_url_bak = `${PUBLIC_AE_API_PROTOCOL}://${PUBLIC_AE_API_BAK_SERVER}:${PUBLIC_AE_API_PORT}${PUBLIC_AE_API_PATH}`; - -const api_init = { - base_url: api_base_url, - base_url_bak: api_base_url_bak, - api_secret_key: PUBLIC_AE_API_SECRET_KEY, - api_crud_super_key: PUBLIC_AE_API_CRUD_SUPER_KEY, - headers: { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - 'x-aether-api-key': PUBLIC_AE_API_SECRET_KEY, - 'x-no-account-id': PUBLIC_AE_NO_ACCOUNT_ID - } -}; - -/** @type {import('./$types').RequestHandler} */ -export async function GET({ url, fetch }) { - const fqdn = url.host; +/** + * Dynamic Web Manifest Generator + * Generates PWA metadata based on the requesting domain. + */ +export const GET: RequestHandler = async ({ url, fetch }) => { + const fqdn = url.hostname; - // Inject SvelteKit fetch for the lookup - const api_cfg = { ...api_init, fetch }; + // Construct api_cfg from public env vars for the server-side lookup + const protocol = public_env.PUBLIC_AE_API_PROTOCOL || 'https'; + const server = public_env.PUBLIC_AE_API_SERVER || 'api.oneskyit.com'; + const port = public_env.PUBLIC_AE_API_PORT || '443'; + const path = public_env.PUBLIC_AE_API_PATH || ''; + + const api_base_url = `${protocol}://${server}${port === '443' || port === '80' ? '' : ':' + port}${path}`; + + const api_cfg = { + base_url: api_base_url, + headers: { + 'x-aether-api-key': public_env.PUBLIC_AE_API_SECRET_KEY, + 'x-no-account-id': public_env.PUBLIC_AE_NO_ACCOUNT_ID + }, + fetch + }; - const site_domain = await lookup_site_domain({ - api_cfg, - fqdn, - log_lvl: 0 - }); - - if (!site_domain) { - return json({ error: 'Site not found' }, { status: 404 }); + let site_domain = null; + try { + site_domain = await lookup_site_domain({ + api_cfg, + fqdn, + log_lvl: 0 + }); + } catch (e) { + console.error(`PWA Manifest: Lookup failed for domain ${fqdn}:`, e); } - const site_name = site_domain.site_name || site_domain.account_name || 'Aether PWA'; - const short_name = site_domain.site_code || 'Aether'; - - // Use the site's header image or logo if available, otherwise fallback to default - const icon_src = site_domain.header_image_path || '/favicon.png'; + // Default branding values + let name = "Aether Platform"; + let short_name = "Aether"; + let background_color = "#1a1a1a"; + let theme_color = "#3a5997"; + let logo_url = "https://static.oneskyit.com/images/OSIT_logo_2022_192px.png"; + let logo_url_large = "https://static.oneskyit.com/images/OSIT_logo_2022_512px.png"; + + if (site_domain) { + // Preference: Account Name > Site Name > Default + name = site_domain.account_name || site_domain.site_name || name; + short_name = site_domain.site_code || site_domain.account_code || short_name; + + // If the site domain has a specific logo, we apply it here + if (site_domain.header_image_path) { + logo_url = site_domain.header_image_path; + logo_url_large = site_domain.header_image_path; + } + } const manifest = { - name: `One Sky IT - ${site_name}`, + name: name, short_name: short_name, - description: `The Aether Progressive Web App for ${site_name}`, - start_url: '/', - display: 'fullscreen', - background_color: 'hsl(220, 65%, 31%)', - theme_color: 'hsl(220, 65%, 31%)', + description: `The ${name} Progressive Web App`, + start_url: "/", + display: "standalone", + background_color: background_color, + theme_color: theme_color, icons: [ { - src: icon_src, - sizes: 'any', - type: 'image/png' + src: logo_url, + sizes: "192x192", + type: "image/png", + purpose: "any maskable" + }, + { + src: logo_url_large, + sizes: "512x512", + type: "image/png", + purpose: "any maskable" } - ] + ], + categories: ["business", "productivity"], + orientation: "any" }; return json(manifest, { headers: { - 'Content-Type': 'application/manifest+json' + 'Cache-Control': 'public, max-age=3600' // Cache for 1 hour to reduce API load } }); -} +}; \ No newline at end of file diff --git a/src/routes/testing/+page.svelte b/src/routes/testing/+page.svelte index 14121c7f..8fb847f7 100644 --- a/src/routes/testing/+page.svelte +++ b/src/routes/testing/+page.svelte @@ -168,6 +168,12 @@ return 'Local cache cleared successfully'; }); + const test_manifest = () => run_test('PWA Manifest Lookup', async () => { + const response = await fetch('/manifest.webmanifest'); + if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`); + return await response.json(); + }); + @@ -381,16 +387,18 @@ {/if}
-
-
- -

Infrastructure

-
- -
-
+
+
+ +

Infrastructure

+
+ + +

Core V3