refactor(idaa): migrate favorites storage from mod_meetings_json to data_store

Favorites are now stored in a dedicated data_store record (code:
idaa_meetings_favorites, scoped to the IDAA account_id) so toggling
never touches ae_event rows or their updated_on timestamps. Structure:
{ [novi_uuid]: [event_id, ...] }. Pre-created DB records for dev (id 150)
and live IDAA (id 151) accounts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-18 16:38:05 -04:00
parent b32fb05138
commit 730fb19d60

View File

@@ -19,6 +19,7 @@ let {
}: Props = $props();
// *** Import Svelte specific
import { onMount } from 'svelte';
// *** Import Aether specific variables and functions
import { ae_util } from '$lib/ae_utils/ae_utils';
@@ -37,54 +38,81 @@ import {
idaa_slct,
idaa_trig
} from '$lib/stores/ae_idaa_stores';
import { db_events } from '$lib/ae_events/db_events';
import { events_func } from '$lib/ae_events/ae_events_functions';
import { api } from '$lib/api/api';
import MyClipboard from '$lib/app_components/e_app_clipboard.svelte';
// Favorites are stored in a dedicated data_store record (code: idaa_meetings_favorites,
// scoped to the IDAA account_id) so toggling never touches ae_event rows or their
// updated_on timestamps. Structure: { [novi_uuid]: [event_id, ...] }.
// One shared record per account — known race condition if two members toggle at
// the exact same moment (last write wins). Acceptable for ~1000 members.
let ds_fav_id = $state<string | null>(null);
let ds_fav_json = $state<Record<string, string[]>>({});
// Tracks event IDs currently being toggled to prevent double-clicks
let favorites_in_progress = $state<Set<string>>(new Set());
onMount(async () => {
if (!$idaa_loc.novi_uuid || !$ae_api?.base_url) return;
try {
const result = await api.get_data_store({
api_cfg: $ae_api,
code: 'idaa_meetings_favorites'
});
const rec_id = result?.data_store_id || result?.id;
if (rec_id) {
ds_fav_id = rec_id;
let raw = result.json ?? result.json_str ?? null;
if (typeof raw === 'string') {
try { raw = JSON.parse(raw); } catch { raw = {}; }
}
ds_fav_json = (raw as Record<string, string[]>) ?? {};
}
} catch (err) {
console.warn('[favorites] Failed to load favorites data store:', err);
}
});
function check_fav(event_obj: Record<string, unknown>): boolean {
const my_uuid = $idaa_loc.novi_uuid;
if (!my_uuid) return false;
const mod = event_obj.mod_meetings_json as Record<string, unknown> | null | undefined;
return Array.isArray(mod?.favorite) && (mod.favorite as string[]).includes(my_uuid);
const my_favs = ds_fav_json[my_uuid];
return Array.isArray(my_favs) && my_favs.includes(event_obj.event_id as string);
}
async function toggle_favorite(event_obj: Record<string, unknown>) {
const my_uuid = $idaa_loc.novi_uuid;
const obj_id = event_obj.id as string; // Dexie primary key (same value as event_id)
const event_id = event_obj.event_id as string; // V3 API UUID
const event_id = event_obj.event_id as string;
if (!my_uuid || !event_id || favorites_in_progress.has(event_id)) return;
if (!my_uuid || !event_id || !ds_fav_id || favorites_in_progress.has(event_id)) return;
favorites_in_progress.add(event_id);
const current_json = (event_obj.mod_meetings_json as Record<string, unknown>) ?? {};
const current_list: string[] = Array.isArray(current_json.favorite) ? [...(current_json.favorite as string[])] : [];
const already_fav = current_list.includes(my_uuid);
const new_list = already_fav ? current_list.filter((u) => u !== my_uuid) : [...current_list, my_uuid];
const new_json = { ...current_json, favorite: new_list };
const current_user_list: string[] = Array.isArray(ds_fav_json[my_uuid])
? [...ds_fav_json[my_uuid]]
: [];
const already_fav = current_user_list.includes(event_id);
const new_user_list = already_fav
? current_user_list.filter((id) => id !== event_id)
: [...current_user_list, event_id];
// Optimistic IDB update — liveQuery reactively re-renders the card instantly
await db_events.event.update(obj_id, { mod_meetings_json: new_json });
const prev_json = ds_fav_json;
const new_full_json = { ...ds_fav_json, [my_uuid]: new_user_list };
// Optimistic update — ds_fav_json is reactive $state, so check_fav() re-evaluates
// instantly everywhere it's called (derived list sort + each block template).
ds_fav_json = new_full_json;
// NOTE: This PATCH triggers ae_event.updated_on to update (MariaDB ON UPDATE
// current_timestamp() fires on every row write). Side effect: starring a meeting
// shifts its position when sorted by updated_on, and admins see a spurious "last modified"
// timestamp. Long-term fix: either bypass ON UPDATE for this specific write (pass
// updated_on = updated_on in the SET clause server-side), or store favorites in a
// dedicated per-user table that doesn't touch the event row at all.
try {
await events_func.update_ae_obj__event({
await api.update_ae_obj({
api_cfg: $ae_api,
event_id,
data_kv: { mod_meetings_json: new_json },
try_cache: true
obj_type: 'data_store',
obj_id: ds_fav_id,
fields: { json: JSON.stringify(new_full_json) }
});
} catch (err) {
console.warn('[favorites] API persist failed, rolling back:', err);
await db_events.event.update(obj_id, { mod_meetings_json: current_json });
ds_fav_json = prev_json;
} finally {
favorites_in_progress.delete(event_id);
}