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:
Scott Idem
2026-01-16 16:41:32 -05:00
parent 8b611e7875
commit a10accfaaf
14 changed files with 536 additions and 564 deletions

View File

@@ -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({

View File

@@ -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({

View File

@@ -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}`);
}