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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user