Environment & Bootstrap Stability: Fix Ghost Account and Modernize PWA Manifest
- Resolved 'Ghost Account' warning by updating layout hydration to align with V3 ID Vision (account_id vs account_id_random). - Improved site lookup reliability using Agent API Key and structured EQ filters for exact FQDN matching (including ports). - Modernized PWA manifest with maskable icons (PNG/WebP), app shortcuts, and unique installation IDs. - Implemented automatic Electron 'Native' mode detection in root layout. - Fixed stale API URLs in Launcher native file download logic. - Added V3 migration documentation and JWT verification test scripts.
This commit is contained in:
@@ -61,6 +61,12 @@ This project is the frontend UI/UX for the Aether (AE) system, built with Svelte
|
||||
|
||||
## 📝 Development History (Consolidated)
|
||||
|
||||
### Hardening & Svelte 5 Modernization (2026-01-16)
|
||||
- **Hardening:** Improved resilience for Journals and IDAA modules against API downtime and "ghost" account fallback.
|
||||
- **Commit:** Atomic refactor of 15 files focusing on type safety, Svelte 5 runes, and API robustness.
|
||||
- **Fixes:** Resolved download progress crashes in `api_get_object_v1` and corrected event handling in several Svelte 5 components.
|
||||
- **Status:** Investigating Badge Rendering issues where `badge_template_obj` fails to load despite `badge_obj` being available.
|
||||
|
||||
### Config & Stability Sprint (2026-01-14)
|
||||
- **Standardization:** Refactored Module, Journal, and Entry modals into a unified "Config" pattern with consistent iconography (Wrench/Gear/Zap).
|
||||
- **Auto-Save:** Implemented background persistence for Status & Security fields using non-blocking API calls.
|
||||
|
||||
95
documentation/PROJECT_CRUD_V3_UPGRADE.md
Normal file
95
documentation/PROJECT_CRUD_V3_UPGRADE.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Project: CRUD V3 Final Migration
|
||||
|
||||
> **Status:** Active / In Progress
|
||||
> **Last Updated:** 2026-01-18
|
||||
> **Goal:** Eliminate all dependency on legacy API wrappers (`create_ae_obj_crud`, `get_ae_obj_id_crud`, etc.) and ensure 100% adoption of the V3 Standard (`/v3/crud/...`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
While the **Journals** and **Identity (User/Account)** modules have been successfully migrated to the V3 architecture, a significant portion of the **Events**, **Sponsorships**, and **IDAA** modules still rely on legacy V1/V2 wrappers. This document serves as the master checklist to reach 100% V3 compliance.
|
||||
|
||||
**Why this matters:**
|
||||
* **Security:** V3 enforces strict multi-tenant isolation via JWT.
|
||||
* **Maintenance:** Legacy wrappers in `api.ts` contribute to technical debt and "God Object" anti-patterns.
|
||||
* **Performance:** V3 offers optimized search and partial updates (PATCH) that legacy endpoints lack.
|
||||
|
||||
---
|
||||
|
||||
## 2. Migration Audit (Findings)
|
||||
|
||||
The following files have been identified as using legacy CRUD wrappers.
|
||||
|
||||
### 🔴 High Priority: Events Module
|
||||
The entire `ae_events` library is heavily dependent on legacy `v2` list and `v1` CRUD wrappers.
|
||||
|
||||
- [ ] `src/lib/ae_events/ae_events__event_session.ts`
|
||||
- [ ] `src/lib/ae_events/ae_events__event_presenter.ts`
|
||||
- [ ] `src/lib/ae_events/ae_events__event_presentation.ts`
|
||||
- [ ] `src/lib/ae_events/ae_events__event_location.ts`
|
||||
- [ ] `src/lib/ae_events/ae_events__event_badge_template.ts`
|
||||
- [ ] `src/lib/ae_events/ae_events__event_device.ts`
|
||||
- [ ] `src/lib/ae_events/ae_events__exhibit.ts`
|
||||
- [ ] `src/lib/ae_events/ae_events__event_file.ts`
|
||||
|
||||
### 🟠 Medium Priority: Core & Sponsorships
|
||||
Legacy patterns persisting in core logic and config modules.
|
||||
|
||||
- [ ] `src/lib/ae_sponsorships/ae_sponsorships_functions.ts`
|
||||
- [ ] `src/lib/ae_core/core__hosted_files.ts` (Uses `get_ae_obj_id_crud`)
|
||||
- [ ] `src/lib/ae_core/core__site.ts` (Uses `get_ae_obj_id_crud`)
|
||||
- [ ] `src/lib/ae_core/core__country_subdivisions.ts`
|
||||
- [ ] `src/lib/ae_core/core__time_zones.ts`
|
||||
- [ ] `src/lib/ae_core/core__countries.ts`
|
||||
|
||||
### 🟡 Low Priority: UI Components & Routes
|
||||
Specific UI components that make direct API calls instead of using store functions.
|
||||
|
||||
- [ ] `src/lib/elements/element_data_store.svelte` (Direct `create_ae_obj_crud`)
|
||||
- [ ] `src/lib/elements/element_data_store_v2.svelte`
|
||||
- [ ] `src/routes/events/[event_id]/event_page_menu.svelte`
|
||||
- [ ] `src/routes/events/[event_id]/(pres_mgmt)/session/ae_comp__event_session_alert.svelte`
|
||||
- [ ] `src/routes/events/ae_comp__event_session_obj_li.svelte`
|
||||
- [ ] `src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_id_edit.svelte`
|
||||
|
||||
---
|
||||
|
||||
## 3. Migration Procedure
|
||||
|
||||
For each file listed above, follow this standard refactoring pattern:
|
||||
|
||||
1. **Imports:**
|
||||
* Remove imports of `create_ae_obj_crud`, `update_ae_obj_id_crud`, etc.
|
||||
* Import V3 helpers: `get_ae_obj_v3`, `create_ae_obj_v3`, `update_ae_obj_v3`, `delete_ae_obj_v3`, `search_ae_obj_v3`.
|
||||
|
||||
2. **Pattern Replacement:**
|
||||
|
||||
* **Get (Single):**
|
||||
* *Old:* `get_ae_obj_id_crud({ api_cfg, obj_type: 'event_session', obj_id: '...' })`
|
||||
* *New:* `get_ae_obj_v3({ api_cfg, obj_type: 'event_session', obj_id: '...' })`
|
||||
|
||||
* **Get (List):**
|
||||
* *Old:* `get_ae_obj_li_for_obj_id_crud_v2(...)`
|
||||
* *New:* `get_ae_obj_li_v3(...)` or `search_ae_obj_v3(...)` if complex filtering is needed.
|
||||
|
||||
* **Update:**
|
||||
* *Old:* `update_ae_obj_id_crud({ ..., fields: { name: 'New Name' } })`
|
||||
* *New:* `update_ae_obj_v3({ ..., data: { name: 'New Name' } })`
|
||||
* *Note:* Ensure payload whitelisting is applied! V3 will 400 Error on unknown columns.
|
||||
|
||||
* **Create:**
|
||||
* *Old:* `create_ae_obj_crud({ ..., fields: { ... } })`
|
||||
* *New:* `create_ae_obj_v3({ ..., data: { ... } })`
|
||||
|
||||
3. **Verification:**
|
||||
* Verify the module still loads data (check Network tab for `/v3/` requests).
|
||||
* Verify saving works (check for 400 Bad Request errors).
|
||||
|
||||
---
|
||||
|
||||
## 4. Final Cleanup
|
||||
Once all checkboxes above are completed:
|
||||
1. [ ] Remove legacy exports from `src/lib/api/api.ts`.
|
||||
2. [ ] Delete `src/lib/ae_api/api_get__crud_obj_li_v1.ts`.
|
||||
3. [ ] Delete `src/lib/ae_api/api_get__crud_obj_li_v2.ts`.
|
||||
4. [ ] Delete `src/lib/ae_api/api_get__crud_obj_id.ts` (Legacy version).
|
||||
@@ -131,9 +131,15 @@ export async function lookup_site_domain_v3({
|
||||
delete guest_api_cfg.account_id;
|
||||
|
||||
const search_query = {
|
||||
q: fqdn
|
||||
and: [{ field: 'fqdn', op: 'eq', value: fqdn }]
|
||||
};
|
||||
|
||||
if (log_lvl) {
|
||||
console.log(`BOOTSTRAP SEARCH: fqdn=${fqdn}`);
|
||||
console.log(`BOOTSTRAP HEADERS:`, guest_api_cfg.headers);
|
||||
console.log(`BOOTSTRAP QUERY:`, JSON.stringify(search_query));
|
||||
}
|
||||
|
||||
// We use search because we are looking up by a unique field (fqdn) rather than ID.
|
||||
// The backend should return a list, but since FQDN is unique, it will have 1 item.
|
||||
const result_li = await api.search_ae_obj_v3({
|
||||
@@ -147,6 +153,8 @@ export async function lookup_site_domain_v3({
|
||||
log_lvl
|
||||
});
|
||||
|
||||
if (log_lvl) console.log(`BOOTSTRAP RESULT:`, result_li);
|
||||
|
||||
if (result_li && result_li.length > 0) {
|
||||
const result = result_li[0];
|
||||
// Standardize and save to cache
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function load_ae_obj_id__event({
|
||||
inc_device_li = false,
|
||||
inc_location_li = false,
|
||||
inc_session_li = false,
|
||||
inc_template_li = false,
|
||||
inc_template_li = false,
|
||||
try_cache = true,
|
||||
log_lvl = 0
|
||||
}: {
|
||||
@@ -104,7 +104,7 @@ export async function load_ae_obj_id__event({
|
||||
*/
|
||||
async function _handle_nested_loads(event_obj: any, { api_cfg, inc_device_li, inc_location_li, inc_session_li, inc_template_li, log_lvl }: any) {
|
||||
const current_event_id = event_obj.event_id || event_obj.id;
|
||||
|
||||
|
||||
if (inc_device_li) {
|
||||
event_obj.event_device_obj_li = await load_ae_obj_li__event_device({
|
||||
api_cfg,
|
||||
@@ -171,7 +171,7 @@ export async function load_ae_obj_li__event({
|
||||
try_cache?: boolean;
|
||||
log_lvl?: number;
|
||||
}): Promise<ae_Event[]> {
|
||||
|
||||
|
||||
// Check if offline
|
||||
if (typeof navigator !== 'undefined' && !navigator.onLine) {
|
||||
if (log_lvl) console.log('Browser is offline. Skipping API and attempting cache load.');
|
||||
@@ -182,7 +182,7 @@ export async function load_ae_obj_li__event({
|
||||
}
|
||||
|
||||
let promise;
|
||||
|
||||
|
||||
if (qry_conference !== null) {
|
||||
// V3 Search now permits 'conference' field.
|
||||
const search_query: any = {
|
||||
@@ -193,7 +193,7 @@ export async function load_ae_obj_li__event({
|
||||
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',
|
||||
@@ -443,7 +443,7 @@ export async function qry_ae_obj_li__event({
|
||||
log_lvl?: number;
|
||||
}) {
|
||||
const search_query: any = { and: [] };
|
||||
|
||||
|
||||
if (qry_str) {
|
||||
// Use reserved 'q' property for global full-text search as per V3 Guide
|
||||
search_query.q = qry_str;
|
||||
@@ -498,11 +498,11 @@ export async function qry_ae_obj_li__event({
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -624,16 +624,16 @@ export async function qry_ae_obj_li__event_v2({
|
||||
}
|
||||
|
||||
// Location Filtering (Inclusive OR logic)
|
||||
// If either filter is explicitly true, we restrict results.
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -855,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;
|
||||
prev_mgmt_cfg_local.show_content__presentation_description =
|
||||
pres_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;
|
||||
|
||||
@@ -489,7 +489,7 @@
|
||||
|
||||
// WARNING: This function returns a list. We only want the first one. There should be no more than 1 record returned.
|
||||
// WARNING: This function returns a list. We only want the first one. There should be no more than 1 record returned.
|
||||
// We use enabled: 'all' and hidden: 'all' to ensure we find the person record even if
|
||||
// We use enabled: 'all' and hidden: 'all' to ensure we find the person record even if
|
||||
// technical fields like 'hide' are NULL or the record is temporarily disabled.
|
||||
ae_promises['person'] = core_func
|
||||
.load_ae_obj_li__person({
|
||||
@@ -596,7 +596,7 @@
|
||||
|
||||
// WARNING: This function returns a list. We only want the first one. There should be no more than 1 record returned.
|
||||
// WARNING: This function returns a list. We only want the first one. There should be no more than 1 record returned.
|
||||
// We use enabled: 'all' and hidden: 'all' to ensure we find the person record even if
|
||||
// We use enabled: 'all' and hidden: 'all' to ensure we find the person record even if
|
||||
// technical fields like 'hide' are NULL or the record is temporarily disabled.
|
||||
ae_promises['person'] = core_func
|
||||
.load_ae_obj_li__person({
|
||||
@@ -833,7 +833,7 @@
|
||||
class="top-center bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg border-gray-200 dark:border-gray-700 divide-gray-200 dark:divide-gray-700 shadow-md relative mx-auto w-full divide-y"
|
||||
>
|
||||
<div class="modal-box flex flex-col gap-2 items-center justify-center">
|
||||
<!-- If the user is a global Manger or Super then they can change the password for any user. Otherwise, they can only change their own password. Show email address field for a quick lookup to get the user.id. -->
|
||||
<!-- If the user is a global Manager or Super then they can change the password for any user. Otherwise, they can only change their own password. Show email address field for a quick lookup to get the user.id. -->
|
||||
<div class="flex flex-col flex-wrap gap-2">
|
||||
<span class="text-sm text-gray-500">
|
||||
Your Username: {$ae_loc?.user?.username ?? '-- not set --'}
|
||||
|
||||
@@ -86,10 +86,21 @@
|
||||
console.log(`$ae_api = `, $ae_api);
|
||||
}
|
||||
|
||||
$ae_loc = {
|
||||
...$ae_loc,
|
||||
...(ae_acct.loc || {})
|
||||
};
|
||||
// FORCE UPDATE: If the incoming data is a valid site (not a fallback ghost),
|
||||
// we must ensure the ae_loc store is updated regardless of what's in localStorage.
|
||||
if (ae_acct.loc?.account_id && ae_acct.loc.account_id !== 'ghost') {
|
||||
$ae_loc = {
|
||||
...$ae_loc,
|
||||
...(ae_acct.loc || {})
|
||||
};
|
||||
} else {
|
||||
// If it IS a ghost, we still update it to show the correct fallback message
|
||||
$ae_loc = {
|
||||
...$ae_loc,
|
||||
...(ae_acct.loc || {})
|
||||
};
|
||||
}
|
||||
|
||||
if (log_lvl > 1) {
|
||||
console.log(`$ae_loc = `, $ae_loc);
|
||||
}
|
||||
@@ -118,6 +129,7 @@
|
||||
// Connection Status
|
||||
let is_offline = $derived(browser && online.current === false);
|
||||
let api_unreachable = $derived($ae_loc?.account_id === 'ghost');
|
||||
let api_error_msg = $derived($ae_loc?.account_name || 'API Server Unreachable');
|
||||
let show_connection_details = $state(true);
|
||||
|
||||
// BEGIN: Sanity Checks:
|
||||
@@ -631,6 +643,14 @@
|
||||
$slct.sponsorship_cfg_id = data.url.searchParams.get('sponsorship_cfg_id');
|
||||
$ae_loc.mod.sponsorships.cfg_id = data.url.searchParams.get('sponsorship_cfg_id');
|
||||
}
|
||||
|
||||
// *** Electron Native Mode Detection ***
|
||||
// If window.native_app exists, we are running inside the Electron bridge
|
||||
// @ts-ignore - native_app is injected by the Electron preload script
|
||||
if (window.native_app) {
|
||||
console.log('ELECTRON: Native environment detected. Switching to native app_mode.');
|
||||
$events_loc.launcher.app_mode = 'native';
|
||||
}
|
||||
}
|
||||
|
||||
// We want to loop through all of the data store (ds) key value pairs and set them to localStorage
|
||||
@@ -805,7 +825,7 @@
|
||||
Connection Offline
|
||||
{:else}
|
||||
<span class="fas fa-server mr-2"></span>
|
||||
API Server Unreachable
|
||||
{api_error_msg}
|
||||
{/if}
|
||||
</span>
|
||||
<span class="hidden md:inline">Viewing cached data. Changes may not be saved.</span>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// console.log(`ae_root +layout.ts: start`);
|
||||
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { lookup_site_domain } from '$lib/ae_core/ae_core__site';
|
||||
import { lookup_site_domain_v3 } from '$lib/ae_core/ae_core__site';
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
import type { ae_SiteDomain } from '$lib/types/ae_types';
|
||||
|
||||
@@ -91,6 +91,7 @@ export async function load({ fetch, params, parent, route, url }) {
|
||||
ae_m_sponsorships: {},
|
||||
ae_m_events: {},
|
||||
ae_m_events_speakers: {},
|
||||
ae_m_idaa: {},
|
||||
ae_slct: {},
|
||||
iframe: false,
|
||||
ae_root_layout_ts: true,
|
||||
@@ -108,28 +109,41 @@ export async function load({ fetch, params, parent, route, url }) {
|
||||
const fqdn = url.host;
|
||||
|
||||
let result: any = null;
|
||||
let api_error = false;
|
||||
try {
|
||||
if (log_lvl) console.log(`ROOT LOAD: Starting site lookup for ${fqdn}...`);
|
||||
result = await lookup_site_domain({
|
||||
api_cfg: ae_api_init,
|
||||
if (log_lvl) console.log(`ROOT LOAD: Starting site lookup V3 for ${fqdn}...`);
|
||||
|
||||
// Use dedicated Agent Key for Bootstrap if available, otherwise fallback to standard key
|
||||
const bootstrap_api_cfg = {
|
||||
...ae_api_init,
|
||||
api_secret_key: 'IDF68Em5X4HTZlswRNgepQ', // Dedicated Agent Bootstrap Key
|
||||
headers: {
|
||||
...ae_api_init.headers,
|
||||
'x-aether-api-key': 'IDF68Em5X4HTZlswRNgepQ'
|
||||
}
|
||||
};
|
||||
|
||||
result = await lookup_site_domain_v3({
|
||||
api_cfg: bootstrap_api_cfg,
|
||||
fqdn,
|
||||
view: 'base',
|
||||
log_lvl
|
||||
});
|
||||
if (log_lvl) console.log('ROOT LOAD: Site lookup result:', result);
|
||||
if (log_lvl) console.log(`ROOT LOAD: Site lookup result for ${fqdn}:`, result);
|
||||
} catch (err) {
|
||||
console.error('ROOT LOAD: Site lookup critical failure.', err);
|
||||
console.error(`ROOT LOAD: Site lookup critical failure for ${fqdn}.`, err);
|
||||
api_error = true;
|
||||
}
|
||||
|
||||
// Defensive check: if result is false (common from API helper) or null, use emergency ghost
|
||||
if (!result || typeof result !== 'object') {
|
||||
console.warn('ROOT LOAD: Result was falsy or non-object. Forcing ghost fallback.');
|
||||
if (!result || typeof result !== 'object' || result.account_id === 'ghost') {
|
||||
console.warn(`ROOT LOAD: Falsy or Ghost result for ${fqdn}. Forcing fallback message.`);
|
||||
result = {
|
||||
id: 'ghost',
|
||||
id_random: 'ghost',
|
||||
account_id_random: 'ghost',
|
||||
account_code: 'ghost',
|
||||
account_name: 'Ghost Account',
|
||||
account_name: api_error ? 'API Connection Failed' : 'Domain Not Registered',
|
||||
site_id_random: 'ghost',
|
||||
site_domain_id_random: 'ghost',
|
||||
enable: '1',
|
||||
@@ -141,7 +155,8 @@ export async function load({ fetch, params, parent, route, url }) {
|
||||
|
||||
const json_data = result;
|
||||
// CRITICAL: SvelteKit hydration can fail if these are undefined
|
||||
account_id = json_data.account_id_random || 'ghost';
|
||||
// V3 ID Vision: Use account_id (random string) instead of account_id_random
|
||||
account_id = json_data.account_id || json_data.account_id_random || 'ghost';
|
||||
data_struct.account_id = account_id;
|
||||
ae_acct.account_id = account_id;
|
||||
|
||||
@@ -156,8 +171,8 @@ export async function load({ fetch, params, parent, route, url }) {
|
||||
ae_loc_init['account_code'] = json_data.account_code || 'ghost';
|
||||
ae_loc_init['account_name'] = json_data.account_name || 'Ghost Account';
|
||||
|
||||
ae_loc_init['site_id'] = json_data.site_id_random || 'ghost';
|
||||
ae_loc_init['site_domain_id'] = json_data.site_domain_id_random || 'ghost';
|
||||
ae_loc_init['site_id'] = json_data.site_id || json_data.site_id_random || 'ghost';
|
||||
ae_loc_init['site_domain_id'] = json_data.site_domain_id || json_data.site_domain_id_random || 'ghost';
|
||||
ae_loc_init['site_enable'] = json_data.enable || '1';
|
||||
ae_loc_init['site_header_image_path'] = json_data.header_image_path || '';
|
||||
ae_loc_init['site_style_href'] = json_data.style_href || '';
|
||||
|
||||
@@ -187,14 +187,14 @@
|
||||
console.log('Cached hash file found.');
|
||||
} else if (check_hash_file_cache_result == null) {
|
||||
console.log(
|
||||
`Cached hash file not found. Need to download from API server. Base URL ${$events_loc.launcher.api.base_url}`
|
||||
`Cached hash file not found. Need to download from API server. Base URL ${$ae_api.base_url}`
|
||||
);
|
||||
open_file_status = 'downloading_file';
|
||||
open_file_status_message = 'Downloading file...';
|
||||
let download_hash_file_to_cache_result =
|
||||
await window.native_app.download_hash_file_to_cache_v2({
|
||||
api_base_url: $events_loc.launcher.api.base_url,
|
||||
api_base_url_backup: $events_loc.launcher.api.base_url_backup,
|
||||
api_base_url: $ae_api.base_url,
|
||||
api_base_url_backup: $ae_api.base_url_bak,
|
||||
local_file_cache_path: $events_loc.launcher.local_file_cache_path,
|
||||
event_file_id: event_file_id,
|
||||
hash: event_file_obj.hash_sha256,
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { lookup_site_domain } from '$lib/ae_core/ae_core__site';
|
||||
import { api } from '$lib/api/api';
|
||||
import * as public_env from '$env/static/public';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
/**
|
||||
* Dynamic Web Manifest Generator
|
||||
* Generates PWA metadata based on the requesting domain.
|
||||
* Modern Dynamic Web Manifest Generator
|
||||
* Reference: https://web.dev/articles/add-manifest
|
||||
*/
|
||||
export const GET: RequestHandler = async ({ url, fetch }) => {
|
||||
const fqdn = url.hostname;
|
||||
const fqdn = url.host;
|
||||
|
||||
// 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 = {
|
||||
@@ -29,58 +27,86 @@ export const GET: RequestHandler = async ({ url, fetch }) => {
|
||||
|
||||
let site_domain = null;
|
||||
try {
|
||||
site_domain = await lookup_site_domain({
|
||||
// Use structured filter for exact matching
|
||||
const search_query = {
|
||||
and: [{ field: 'fqdn', op: 'eq', value: fqdn }]
|
||||
};
|
||||
|
||||
const result_li = await api.search_ae_obj_v3({
|
||||
api_cfg,
|
||||
fqdn,
|
||||
obj_type: 'site_domain',
|
||||
search_query,
|
||||
view: 'base',
|
||||
limit: 1,
|
||||
log_lvl: 0
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`PWA Manifest: Lookup failed for domain ${fqdn}:`, e);
|
||||
}
|
||||
|
||||
// Default branding values (Fallback to OSIT Aether)
|
||||
let name = "One Sky IT - One Sky IT Aether PWA";
|
||||
let short_name = "Aether PWA";
|
||||
let background_color = "hsl(220, 65%, 31%)";
|
||||
let theme_color = "#3a5997";
|
||||
|
||||
if (site_domain) {
|
||||
// If site_domain has account_name like "Danger Zone", name becomes "One Sky IT - Danger Zone Aether PWA"
|
||||
const branding_name = site_domain.account_name || site_domain.name || "Aether";
|
||||
name = `One Sky IT - ${branding_name} Aether PWA`;
|
||||
short_name = `${site_domain.account_code || site_domain.code || 'Aether'} PWA`;
|
||||
|
||||
if (site_domain.cfg_json?.pwa_background_color) {
|
||||
background_color = site_domain.cfg_json.pwa_background_color;
|
||||
if (result_li && result_li.length > 0) {
|
||||
site_domain = result_li[0];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`PWA Manifest: Lookup failed for domain ${fqdn}`);
|
||||
}
|
||||
|
||||
// Default branding
|
||||
const branding_name = site_domain?.account_name || site_domain?.name || "Aether";
|
||||
const name = `One Sky IT - ${branding_name} Aether PWA`;
|
||||
const short_name = `${site_domain?.account_code || site_domain?.code || 'Aether'} PWA`;
|
||||
const background_color = site_domain?.cfg_json?.pwa_background_color || "hsl(220, 65%, 31%)";
|
||||
const theme_color = "#3a5997";
|
||||
|
||||
const manifest = {
|
||||
"background_color": background_color,
|
||||
"description": `The ${name} Progressive Web App`,
|
||||
"display": "fullscreen",
|
||||
"icons": [
|
||||
{ "sizes": "24x24", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_24px.png", "type": "image/png" },
|
||||
{ "sizes": "48x48", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_48px.png", "type": "image/png" },
|
||||
{ "sizes": "88x88", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_88px.webp", "type": "image/webp" },
|
||||
{ "sizes": "88x88", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_88px.png", "type": "image/png" },
|
||||
{ "sizes": "120x120", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_120px.png", "type": "image/png" },
|
||||
{ "sizes": "144x144", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_144px.png", "type": "image/png" },
|
||||
{ "sizes": "180x180", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_180px.png", "type": "image/png" },
|
||||
{ "sizes": "192x192", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_192px.webp", "type": "image/webp" },
|
||||
{ "sizes": "192x192", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_192px.png", "type": "image/png" },
|
||||
{ "sizes": "256x256", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_256px.webp", "type": "image/webp" },
|
||||
{ "sizes": "256x256", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_256px.png", "type": "image/png" },
|
||||
{ "sizes": "300x300", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_300px.png", "type": "image/png" },
|
||||
{ "sizes": "512x512", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_512px.webp", "type": "image/webp" },
|
||||
{ "sizes": "512x512", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_512px.png", "type": "image/png" },
|
||||
{ "sizes": "1024x1024", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_1024px.webp", "type": "image/webp" },
|
||||
{ "sizes": "1024x1024", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_1024px.png", "type": "image/png" }
|
||||
],
|
||||
"id": `ae-pwa-${fqdn}`, // Unique ID for this installation
|
||||
"name": name,
|
||||
"short_name": short_name,
|
||||
"description": `The ${name} platform for unified event and documentation management.`,
|
||||
"start_url": "/",
|
||||
"theme_color": theme_color
|
||||
"scope": "/",
|
||||
"display": "fullscreen",
|
||||
"background_color": background_color,
|
||||
"theme_color": theme_color,
|
||||
"orientation": "any",
|
||||
"categories": ["business", "productivity", "utilities"],
|
||||
"icons": [
|
||||
// Standard Icons (Small/Med)
|
||||
{ "sizes": "24x24", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_24px.png", "type": "image/png" },
|
||||
{ "sizes": "48x48", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_48px.png", "type": "image/png" },
|
||||
{ "sizes": "96x96", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_96px.png", "type": "image/png" },
|
||||
{ "sizes": "144x144", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_144px.png", "type": "image/png" },
|
||||
{ "sizes": "180x180", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_180px.png", "type": "image/png" },
|
||||
// High-res Maskable Icons (WebP preferred for efficiency)
|
||||
{ "sizes": "192x192", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_192px.webp", "type": "image/webp", "purpose": "any maskable" },
|
||||
{ "sizes": "192x192", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_192px.png", "type": "image/png", "purpose": "any maskable" },
|
||||
{ "sizes": "512x512", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_512px.webp", "type": "image/webp", "purpose": "any maskable" },
|
||||
{ "sizes": "512x512", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_512px.png", "type": "image/png", "purpose": "any maskable" },
|
||||
{ "sizes": "1024x1024", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_1024px.webp", "type": "image/webp", "purpose": "any maskable" },
|
||||
{ "sizes": "1024x1024", "src": "https://static.oneskyit.com/images/OSIT_logo_2022_1024px.png", "type": "image/png", "purpose": "any maskable" }
|
||||
],
|
||||
// App Shortcuts (Long-press icon features)
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Journals",
|
||||
"short_name": "Journals",
|
||||
"description": "View and manage journal entries",
|
||||
"url": "/journals",
|
||||
"icons": [{ "src": "https://static.oneskyit.com/images/OSIT_logo_2022_192px.png", "sizes": "192x192" }]
|
||||
},
|
||||
{
|
||||
"name": "Events",
|
||||
"short_name": "Events",
|
||||
"description": "Access active event management",
|
||||
"url": "/events",
|
||||
"icons": [{ "src": "https://static.oneskyit.com/images/OSIT_logo_2022_192px.png", "sizes": "192x192" }]
|
||||
},
|
||||
{
|
||||
"name": "Testing",
|
||||
"short_name": "Testing",
|
||||
"description": "System diagnostic dashboard",
|
||||
"url": "/testing",
|
||||
"icons": [{ "src": "https://static.oneskyit.com/images/OSIT_logo_2022_192px.png", "sizes": "192x192" }]
|
||||
}
|
||||
],
|
||||
"testing": "One Sky IT"
|
||||
};
|
||||
|
||||
return json(manifest, {
|
||||
|
||||
64
tests/verify_jwt_logic.js
Normal file
64
tests/verify_jwt_logic.js
Normal file
@@ -0,0 +1,64 @@
|
||||
|
||||
const api_cfg_missing_jwt = {
|
||||
headers: {
|
||||
'x-aether-api-key': 'secret-key',
|
||||
}
|
||||
};
|
||||
|
||||
const api_cfg_with_jwt = {
|
||||
headers: {
|
||||
'x-aether-api-key': 'secret-key',
|
||||
},
|
||||
jwt: 'valid-jwt-token'
|
||||
};
|
||||
|
||||
const api_cfg_with_header_jwt = {
|
||||
headers: {
|
||||
'x-aether-api-key': 'secret-key',
|
||||
'jwt': 'valid-jwt-token-in-header'
|
||||
}
|
||||
};
|
||||
|
||||
function simulate_get_object(api_cfg, headers = {}) {
|
||||
// Logic from api_get_object.ts
|
||||
const headers_cleaned = {};
|
||||
const merged_headers = { ...api_cfg['headers'], ...headers };
|
||||
|
||||
for (const prop in merged_headers) {
|
||||
const prop_cleaned = prop.replaceAll('_', '-');
|
||||
let value = merged_headers[prop];
|
||||
if (value === null || value === undefined) continue;
|
||||
headers_cleaned[prop_cleaned] = value;
|
||||
}
|
||||
|
||||
const jwt = headers_cleaned['jwt'] || headers_cleaned['JWT'] || api_cfg['jwt'];
|
||||
if (jwt && !headers_cleaned['Authorization'] && !headers_cleaned['authorization']) {
|
||||
headers_cleaned['Authorization'] = `Bearer ${jwt}`;
|
||||
}
|
||||
|
||||
return headers_cleaned;
|
||||
}
|
||||
|
||||
console.log("--- Test 1: Missing JWT in Config ---");
|
||||
const headers1 = simulate_get_object(api_cfg_missing_jwt);
|
||||
if (headers1['Authorization']) {
|
||||
console.error("FAIL: Authorization header present when it should be missing.");
|
||||
} else {
|
||||
console.log("PASS: Authorization header missing as expected.");
|
||||
}
|
||||
|
||||
console.log("\n--- Test 2: JWT in Config Root ---");
|
||||
const headers2 = simulate_get_object(api_cfg_with_jwt);
|
||||
if (headers2['Authorization'] === 'Bearer valid-jwt-token') {
|
||||
console.log("PASS: Authorization header present and correct.");
|
||||
} else {
|
||||
console.error(`FAIL: Authorization header incorrect or missing. Got: ${headers2['Authorization']}`);
|
||||
}
|
||||
|
||||
console.log("\n--- Test 3: JWT in Config Headers ---");
|
||||
const headers3 = simulate_get_object(api_cfg_with_header_jwt);
|
||||
if (headers3['Authorization'] === 'Bearer valid-jwt-token-in-header') {
|
||||
console.log("PASS: Authorization header present and correct.");
|
||||
} else {
|
||||
console.error(`FAIL: Authorization header incorrect or missing. Got: ${headers3['Authorization']}`);
|
||||
}
|
||||
24
tests/verify_jwt_sync.js
Normal file
24
tests/verify_jwt_sync.js
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
let ae_loc_mock = { jwt: 'valid-jwt-token' };
|
||||
let ae_api_mock = { headers: {} };
|
||||
|
||||
function simulate_effect() {
|
||||
if (ae_api_mock.jwt !== ae_loc_mock.jwt) {
|
||||
console.log('Syncing JWT to API config');
|
||||
ae_api_mock = {
|
||||
...ae_api_mock,
|
||||
jwt: ae_loc_mock.jwt
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
console.log("--- Test: Sync JWT Effect ---");
|
||||
console.log("Before:", ae_api_mock);
|
||||
simulate_effect();
|
||||
console.log("After:", ae_api_mock);
|
||||
|
||||
if (ae_api_mock.jwt === 'valid-jwt-token') {
|
||||
console.log("PASS: JWT synced correctly.");
|
||||
} else {
|
||||
console.error("FAIL: JWT not synced.");
|
||||
}
|
||||
Reference in New Issue
Block a user