Implemented offline-first fast-paths and hardened API/Layout resilience. Added reactive offline banner, root error page, and ghost site fallbacks to handle server downtime and connection loss without crashing.
This commit is contained in:
@@ -26,23 +26,69 @@ export async function lookup_site_domain({
|
||||
console.log(`*** lookup_site_domain() *** fqdn=${fqdn}`);
|
||||
}
|
||||
|
||||
// We use get_ae_obj_id_crud because we are looking up by a unique field (fqdn) rather than ID.
|
||||
// This is the older method that uses the /crud/site/domain/:id endpoint.
|
||||
const result = await api.get_ae_obj_id_crud({
|
||||
api_cfg,
|
||||
no_account_id: true,
|
||||
obj_type: 'site_domain',
|
||||
obj_id: fqdn,
|
||||
use_alt_table: true,
|
||||
use_alt_base: true,
|
||||
log_lvl
|
||||
});
|
||||
try {
|
||||
// We use get_ae_obj_id_crud because we are looking up by a unique field (fqdn) rather than ID.
|
||||
// This is the older method that uses the /crud/site/domain/:id endpoint.
|
||||
const result = await api.get_ae_obj_id_crud({
|
||||
api_cfg,
|
||||
no_account_id: true,
|
||||
obj_type: 'site_domain',
|
||||
obj_id: fqdn,
|
||||
use_alt_table: true,
|
||||
use_alt_base: true,
|
||||
log_lvl
|
||||
});
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
if (result) {
|
||||
// Standardize and save to cache
|
||||
const processed_obj_li = await process_ae_obj__site_domain_props({
|
||||
obj_li: [result],
|
||||
log_lvl
|
||||
});
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_core,
|
||||
table_name: 'site_domain',
|
||||
obj_li: processed_obj_li,
|
||||
properties_to_save: properties_to_save__site_domain,
|
||||
log_lvl
|
||||
});
|
||||
return result;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log('Site domain lookup failed (API Error).', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
if (log_lvl) console.log('Attempting to load site domain from local cache...');
|
||||
const cached = await db_core.site_domain.where('fqdn').equals(fqdn).first();
|
||||
|
||||
if (cached) {
|
||||
return cached as any;
|
||||
}
|
||||
|
||||
// CRITICAL FALLBACK: If both API and Cache fail, return a "Ghost" site domain object
|
||||
// to prevent the 403 error from blocking the UI. Page components will handle empty data.
|
||||
console.error('AE_SITE_CRITICAL: Site domain lookup failed API and CACHE. Returning ghost object.');
|
||||
return {
|
||||
id: 'ghost',
|
||||
id_random: 'ghost',
|
||||
site_domain_id: 'ghost',
|
||||
site_domain_id_random: 'ghost',
|
||||
site_id: 'ghost',
|
||||
site_id_random: 'ghost',
|
||||
account_id: 'ghost',
|
||||
account_id_random: 'ghost',
|
||||
account_code: 'ghost',
|
||||
account_name: 'Ghost Account (Offline)',
|
||||
fqdn: fqdn,
|
||||
enable: '1',
|
||||
header_image_path: '',
|
||||
style_href: '',
|
||||
google_tracking_id: '',
|
||||
access_code_kv_json: {},
|
||||
cfg_json: {},
|
||||
access_key: '',
|
||||
site_domain_access_key: ''
|
||||
} as any;
|
||||
}
|
||||
|
||||
// Updated 2026-01-07
|
||||
@@ -61,50 +107,69 @@ export async function lookup_site_domain_v3({
|
||||
console.log(`*** lookup_site_domain_v3() *** fqdn=${fqdn}`);
|
||||
}
|
||||
|
||||
// CRITICAL: For the unauthenticated Bootstrap lookup, we must NOT send
|
||||
// any existing auth tokens or account IDs that might be in the global config.
|
||||
const guest_api_cfg = { ...api_cfg };
|
||||
guest_api_cfg.headers = { ...api_cfg.headers };
|
||||
|
||||
const auth_props = [
|
||||
'x-account-id',
|
||||
'x-aether-api-token',
|
||||
'Authorization',
|
||||
'authorization',
|
||||
'jwt',
|
||||
'JWT'
|
||||
];
|
||||
|
||||
auth_props.forEach(prop => {
|
||||
delete guest_api_cfg.headers[prop];
|
||||
delete guest_api_cfg.headers[prop.toLowerCase()];
|
||||
delete guest_api_cfg.headers[prop.replaceAll('-', '_')];
|
||||
});
|
||||
delete guest_api_cfg.jwt;
|
||||
delete guest_api_cfg.account_id;
|
||||
try {
|
||||
// CRITICAL: For the unauthenticated Bootstrap lookup, we must NOT send
|
||||
// any existing auth tokens or account IDs that might be in the global config.
|
||||
const guest_api_cfg = { ...api_cfg };
|
||||
guest_api_cfg.headers = { ...api_cfg.headers };
|
||||
|
||||
const auth_props = [
|
||||
'x-account-id',
|
||||
'x-aether-api-token',
|
||||
'Authorization',
|
||||
'authorization',
|
||||
'jwt',
|
||||
'JWT'
|
||||
];
|
||||
|
||||
auth_props.forEach(prop => {
|
||||
delete guest_api_cfg.headers[prop];
|
||||
delete guest_api_cfg.headers[prop.toLowerCase()];
|
||||
delete guest_api_cfg.headers[prop.replaceAll('-', '_')];
|
||||
});
|
||||
delete guest_api_cfg.jwt;
|
||||
delete guest_api_cfg.account_id;
|
||||
|
||||
const search_query = {
|
||||
q: fqdn
|
||||
};
|
||||
const search_query = {
|
||||
q: fqdn
|
||||
};
|
||||
|
||||
// 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({
|
||||
api_cfg: guest_api_cfg,
|
||||
obj_type: 'site_domain',
|
||||
search_query,
|
||||
view, // This view should ideally join with site and account for the root lookup
|
||||
enabled: 'enabled',
|
||||
hidden: 'all',
|
||||
limit: 1,
|
||||
log_lvl
|
||||
});
|
||||
// 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({
|
||||
api_cfg: guest_api_cfg,
|
||||
obj_type: 'site_domain',
|
||||
search_query,
|
||||
view, // This view should ideally join with site and account for the root lookup
|
||||
enabled: 'enabled',
|
||||
hidden: 'all',
|
||||
limit: 1,
|
||||
log_lvl
|
||||
});
|
||||
|
||||
if (result_li && result_li.length > 0) {
|
||||
return result_li[0];
|
||||
if (result_li && result_li.length > 0) {
|
||||
const result = result_li[0];
|
||||
// Standardize and save to cache
|
||||
const processed_obj_li = await process_ae_obj__site_domain_props({
|
||||
obj_li: [result],
|
||||
log_lvl
|
||||
});
|
||||
await db_save_ae_obj_li__ae_obj({
|
||||
db_instance: db_core,
|
||||
table_name: 'site_domain',
|
||||
obj_li: processed_obj_li,
|
||||
properties_to_save: properties_to_save__site_domain,
|
||||
log_lvl
|
||||
});
|
||||
return result;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log('Site domain lookup V3 failed.', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
if (log_lvl) console.log('Attempting to load site domain from local cache (V3 fallback)...');
|
||||
const cached = await db_core.site_domain.where('fqdn').equals(fqdn).first();
|
||||
return (cached as any) || null;
|
||||
}
|
||||
|
||||
export async function load_ae_obj_id__site({
|
||||
|
||||
@@ -385,180 +385,49 @@ async function update_ae_obj_id_crud_v2({
|
||||
new_field_value
|
||||
);
|
||||
}
|
||||
let patch_result: any = null;
|
||||
|
||||
ae_promises.api_update__ae_obj = await api
|
||||
.update_ae_obj_id_crud({
|
||||
api_cfg: api_cfg,
|
||||
obj_type: object_type,
|
||||
obj_id: object_id,
|
||||
field_name: field_name,
|
||||
field_value: new_field_value,
|
||||
// fields: data,
|
||||
key: api_cfg.api_crud_super_key,
|
||||
// jwt: null,
|
||||
// params: params,
|
||||
// data: patch_data,
|
||||
log_lvl: log_lvl
|
||||
})
|
||||
.then(function (results) {
|
||||
console.log('PATCH Promise', results);
|
||||
const results = await api.update_ae_obj_id_crud({
|
||||
api_cfg: api_cfg,
|
||||
obj_type: object_type,
|
||||
obj_id: object_id,
|
||||
field_name: field_name,
|
||||
field_value: new_field_value,
|
||||
key: api_cfg.api_crud_super_key,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
|
||||
if (results) {
|
||||
console.log(
|
||||
`Patched - Field Name: ${field_name} with new Field Value: ${new_field_value}`
|
||||
);
|
||||
patch_result = 'PATCH complete';
|
||||
if (results) {
|
||||
if (log_lvl) {
|
||||
console.log(`Patched - Field Name: ${field_name} with new Field Value: ${new_field_value}`);
|
||||
}
|
||||
|
||||
if (object_reload) {
|
||||
if (log_lvl) {
|
||||
console.log(`Reloading the object after patching...`);
|
||||
}
|
||||
// Reload the object to get the latest data. There is a special case for each type.
|
||||
if (object_type == 'person') {
|
||||
const load_person_obj = load_ae_obj_id__person({
|
||||
api_cfg: api_cfg,
|
||||
person_id: object_id,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
return load_person_obj;
|
||||
}
|
||||
// if (object_type == 'user') {
|
||||
// let load_user_obj = load_ae_obj_id__user({
|
||||
// api_cfg: api_cfg,
|
||||
// user_id: object_id,
|
||||
// log_lvl: log_lvl
|
||||
// });
|
||||
// return load_user_obj;
|
||||
// }
|
||||
|
||||
if (object_type == 'archive') {
|
||||
const load_archive_obj = load_ae_obj_id__archive({
|
||||
api_cfg: api_cfg,
|
||||
archive_id: object_id,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
return load_archive_obj;
|
||||
}
|
||||
if (object_type == 'archive_content') {
|
||||
const load_archive_content_obj = load_ae_obj_id__archive_content({
|
||||
api_cfg: api_cfg,
|
||||
archive_content_id: object_id,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
return load_archive_content_obj;
|
||||
}
|
||||
|
||||
if (object_type == 'journal') {
|
||||
const load_journal_obj = load_ae_obj_id__journal({
|
||||
api_cfg: api_cfg,
|
||||
journal_id: object_id,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
return load_journal_obj;
|
||||
}
|
||||
if (object_type == 'journal_entry') {
|
||||
const load_journal_entry_obj = load_ae_obj_id__journal_entry({
|
||||
api_cfg: api_cfg,
|
||||
journal_entry_id: object_id,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
return load_journal_entry_obj;
|
||||
}
|
||||
|
||||
if (object_type == 'event') {
|
||||
const load_event_obj = load_ae_obj_id__event({
|
||||
api_cfg: api_cfg,
|
||||
event_id: object_id,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
return load_event_obj;
|
||||
}
|
||||
if (object_type == 'event_device') {
|
||||
const load_event_device_obj = load_ae_obj_id__event_device({
|
||||
api_cfg: api_cfg,
|
||||
event_device_id: object_id,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
return load_event_device_obj;
|
||||
}
|
||||
if (object_type == 'event_file') {
|
||||
const load_event_file_obj = load_ae_obj_id__event_file({
|
||||
api_cfg: api_cfg,
|
||||
event_file_id: object_id,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
return load_event_file_obj;
|
||||
}
|
||||
if (object_type == 'event_location') {
|
||||
const load_event_location_obj = load_ae_obj_id__event_location({
|
||||
api_cfg: api_cfg,
|
||||
event_location_id: object_id,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
return load_event_location_obj;
|
||||
}
|
||||
if (object_type == 'event_presentation') {
|
||||
const load_event_presentation_obj = load_ae_obj_id__event_presentation({
|
||||
api_cfg: api_cfg,
|
||||
event_presentation_id: object_id,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
return load_event_presentation_obj;
|
||||
}
|
||||
if (object_type == 'event_presenter') {
|
||||
const load_event_presenter_obj = load_ae_obj_id__event_presenter({
|
||||
api_cfg: api_cfg,
|
||||
event_presenter_id: object_id,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
return load_event_presenter_obj;
|
||||
}
|
||||
if (object_type == 'event_session') {
|
||||
const load_event_session_obj = load_ae_obj_id__event_session({
|
||||
api_cfg: api_cfg,
|
||||
event_session_id: object_id,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
return load_event_session_obj;
|
||||
}
|
||||
|
||||
if (object_type == 'post') {
|
||||
const load_post_obj = load_ae_obj_id__post({
|
||||
api_cfg: api_cfg,
|
||||
post_id: object_id,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
return load_post_obj;
|
||||
}
|
||||
if (object_type == 'post_comment') {
|
||||
const load_post_comment_obj = load_ae_obj_id__post_comment({
|
||||
api_cfg: api_cfg,
|
||||
post_comment_id: object_id,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
return load_post_comment_obj;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`Not Patched - Field Name: ${field_name} with new Field Value: ${new_field_value}; Account ID: ${api_cfg.account_id}`
|
||||
);
|
||||
patch_result = 'PATCH failed';
|
||||
return null;
|
||||
if (object_reload) {
|
||||
if (log_lvl) {
|
||||
console.log(`Reloading the object after patching...`);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.catch(function (error: any) {
|
||||
console.log('Something went wrong patching the record.');
|
||||
console.log(error);
|
||||
return null;
|
||||
})
|
||||
.finally(function () {
|
||||
console.log('PATCH Promise finally');
|
||||
});
|
||||
// Trigger reloads based on object type. These are fire-and-forget or awaited internally by the library functions.
|
||||
if (object_type == 'person') load_ae_obj_id__person({ api_cfg, person_id: object_id, log_lvl });
|
||||
if (object_type == 'archive') load_ae_obj_id__archive({ api_cfg, archive_id: object_id, log_lvl });
|
||||
if (object_type == 'archive_content') load_ae_obj_id__archive_content({ api_cfg, archive_content_id: object_id, log_lvl });
|
||||
if (object_type == 'journal') load_ae_obj_id__journal({ api_cfg, journal_id: object_id, log_lvl });
|
||||
if (object_type == 'journal_entry') load_ae_obj_id__journal_entry({ api_cfg, journal_entry_id: object_id, log_lvl });
|
||||
if (object_type == 'event') load_ae_obj_id__event({ api_cfg, event_id: object_id, log_lvl });
|
||||
if (object_type == 'event_device') load_ae_obj_id__event_device({ api_cfg, event_device_id: object_id, log_lvl });
|
||||
if (object_type == 'event_file') load_ae_obj_id__event_file({ api_cfg, event_file_id: object_id, log_lvl });
|
||||
if (object_type == 'event_location') load_ae_obj_id__event_location({ api_cfg, event_location_id: object_id, log_lvl });
|
||||
if (object_type == 'event_presentation') load_ae_obj_id__event_presentation({ api_cfg, event_presentation_id: object_id, log_lvl });
|
||||
if (object_type == 'event_presenter') load_ae_obj_id__event_presenter({ api_cfg, event_presenter_id: object_id, log_lvl });
|
||||
if (object_type == 'event_session') load_ae_obj_id__event_session({ api_cfg, event_session_id: object_id, log_lvl });
|
||||
if (object_type == 'post') load_ae_obj_id__post({ api_cfg, post_id: object_id, log_lvl });
|
||||
if (object_type == 'post_comment') load_ae_obj_id__post_comment({ api_cfg, post_comment_id: object_id, log_lvl });
|
||||
}
|
||||
} else {
|
||||
if (log_lvl) {
|
||||
console.log(`PATCH failed for ${object_type} ${object_id}`);
|
||||
}
|
||||
}
|
||||
|
||||
return ae_promises.api_update__ae_obj;
|
||||
return results;
|
||||
}
|
||||
|
||||
async function download_export__obj_type({
|
||||
|
||||
@@ -38,8 +38,9 @@ export interface GenericCrudArgs {
|
||||
inc_obj_type_li?: string[]; // Optional list of object types to include
|
||||
|
||||
data_kv?: key_val;
|
||||
enabled?: 'enabled' | 'disabled' | 'all';
|
||||
enabled?: 'enabled' | 'not_enabled' | 'all';
|
||||
hidden?: 'not_hidden' | 'hidden' | 'all';
|
||||
method?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
order_by_li?: Record<string, 'ASC' | 'DESC'> | Record<string, 'ASC' | 'DESC'>[] | null;
|
||||
@@ -52,6 +53,11 @@ export interface GenericCrudArgs {
|
||||
export async function load_ae_obj_id(args: GenericCrudArgs): Promise<any> {
|
||||
const { api_cfg, obj_type, obj_id, log_lvl = 0 } = args;
|
||||
|
||||
if (!obj_id) {
|
||||
if (log_lvl) console.warn('load_ae_obj_id called without obj_id');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (log_lvl) {
|
||||
console.log(`*** load_ae_obj_id() *** obj_type=${obj_type} obj_id=${obj_id}`);
|
||||
}
|
||||
@@ -97,7 +103,7 @@ export async function load_ae_obj_li(args: GenericCrudArgs): Promise<any> {
|
||||
api_cfg,
|
||||
obj_type,
|
||||
for_obj_type,
|
||||
for_obj_id,
|
||||
for_obj_id: for_obj_id ?? '',
|
||||
enabled,
|
||||
hidden,
|
||||
order_by_li,
|
||||
@@ -136,6 +142,11 @@ export async function create_ae_obj(args: GenericCrudArgs): Promise<any> {
|
||||
export async function update_ae_obj(args: GenericCrudArgs): Promise<any> {
|
||||
const { api_cfg, obj_type, obj_id, data_kv, log_lvl = 0 } = args;
|
||||
|
||||
if (!obj_id) {
|
||||
if (log_lvl) console.warn('update_ae_obj called without obj_id');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (log_lvl) {
|
||||
console.log(`*** update_ae_obj() *** obj_type=${obj_type} obj_id=${obj_id}`, data_kv);
|
||||
}
|
||||
@@ -158,6 +169,11 @@ export async function update_ae_obj(args: GenericCrudArgs): Promise<any> {
|
||||
export async function delete_ae_obj_id(args: GenericCrudArgs): Promise<any> {
|
||||
const { api_cfg, obj_type, obj_id, method = 'delete', log_lvl = 0 } = args;
|
||||
|
||||
if (!obj_id) {
|
||||
if (log_lvl) console.warn('delete_ae_obj_id called without obj_id');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (log_lvl) {
|
||||
console.log(`*** delete_ae_obj_id() *** obj_type=${obj_type} obj_id=${obj_id}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user