(function () { let magickManifestCache = null; let magickDataCache = null; let deckOptionsCache = null; const deckManifestCache = new Map(); let quizCategoriesCache = null; const quizTemplatesCache = new Map(); let textLibraryCache = null; const textSourceCache = new Map(); const textSectionCache = new Map(); const textLexiconCache = new Map(); const textLexiconOccurrencesCache = new Map(); const textSearchCache = new Map(); const DATA_ROOT = "data"; const MAGICK_ROOT = DATA_ROOT; const TAROT_TRUMP_NUMBER_BY_NAME = { "the fool": 0, fool: 0, "the magus": 1, magus: 1, magician: 1, "the high priestess": 2, "high priestess": 2, "the empress": 3, empress: 3, "the emperor": 4, emperor: 4, "the hierophant": 5, hierophant: 5, "the lovers": 6, lovers: 6, "the chariot": 7, chariot: 7, strength: 8, lust: 8, "the hermit": 9, hermit: 9, fortune: 10, "wheel of fortune": 10, justice: 11, "the hanged man": 12, "hanged man": 12, death: 13, temperance: 14, art: 14, "the devil": 15, devil: 15, "the tower": 16, tower: 16, "the star": 17, star: 17, "the moon": 18, moon: 18, "the sun": 19, sun: 19, aeon: 20, judgement: 20, judgment: 20, universe: 21, world: 21, "the world": 21 }; const HEBREW_BY_TRUMP_NUMBER = { 0: { hebrewLetterId: "alef", kabbalahPathNumber: 11 }, 1: { hebrewLetterId: "bet", kabbalahPathNumber: 12 }, 2: { hebrewLetterId: "gimel", kabbalahPathNumber: 13 }, 3: { hebrewLetterId: "dalet", kabbalahPathNumber: 14 }, 4: { hebrewLetterId: "he", kabbalahPathNumber: 15 }, 5: { hebrewLetterId: "vav", kabbalahPathNumber: 16 }, 6: { hebrewLetterId: "zayin", kabbalahPathNumber: 17 }, 7: { hebrewLetterId: "het", kabbalahPathNumber: 18 }, 8: { hebrewLetterId: "tet", kabbalahPathNumber: 19 }, 9: { hebrewLetterId: "yod", kabbalahPathNumber: 20 }, 10: { hebrewLetterId: "kaf", kabbalahPathNumber: 21 }, 11: { hebrewLetterId: "lamed", kabbalahPathNumber: 22 }, 12: { hebrewLetterId: "mem", kabbalahPathNumber: 23 }, 13: { hebrewLetterId: "nun", kabbalahPathNumber: 24 }, 14: { hebrewLetterId: "samekh", kabbalahPathNumber: 25 }, 15: { hebrewLetterId: "ayin", kabbalahPathNumber: 26 }, 16: { hebrewLetterId: "pe", kabbalahPathNumber: 27 }, 17: { hebrewLetterId: "tsadi", kabbalahPathNumber: 28 }, 18: { hebrewLetterId: "qof", kabbalahPathNumber: 29 }, 19: { hebrewLetterId: "resh", kabbalahPathNumber: 30 }, 20: { hebrewLetterId: "shin", kabbalahPathNumber: 31 }, 21: { hebrewLetterId: "tav", kabbalahPathNumber: 32 } }; const ICHING_PLANET_BY_PLANET_ID = { sol: "Sun", luna: "Moon", mercury: "Mercury", venus: "Venus", mars: "Mars", jupiter: "Jupiter", saturn: "Saturn", earth: "Earth", uranus: "Uranus", neptune: "Neptune", pluto: "Pluto" }; function buildRequestHeaders() { const apiKey = getApiKey(); return apiKey ? { "x-api-key": apiKey } : undefined; } async function fetchJson(path) { if (!path) { throw new Error("API connection is not configured."); } const response = await fetch(path, { headers: buildRequestHeaders() }); if (!response.ok) { throw new Error(`Failed to load ${path} (${response.status})`); } return response.json(); } function buildObjectPath(target, pathParts, value) { let cursor = target; for (let index = 0; index < pathParts.length - 1; index += 1) { const part = pathParts[index]; if (!cursor[part] || typeof cursor[part] !== "object") { cursor[part] = {}; } cursor = cursor[part]; } cursor[pathParts[pathParts.length - 1]] = value; } function normalizeApiBaseUrl(value) { return String(value || "") .trim() .replace(/\/+$/, ""); } function getApiBaseUrl() { return normalizeApiBaseUrl( window.TarotAppConfig?.getApiBaseUrl?.() || window.TarotAppConfig?.apiBaseUrl || "" ); } function getApiKey() { return String(window.TarotAppConfig?.getApiKey?.() || window.TarotAppConfig?.apiKey || "") .trim(); } function isApiEnabled() { return Boolean(getApiBaseUrl()); } function encodePathSegments(pathValue) { return String(pathValue || "") .split("/") .filter(Boolean) .map((segment) => { try { return encodeURIComponent(decodeURIComponent(segment)); } catch { return encodeURIComponent(segment); } }) .join("/"); } function buildApiUrl(path, query = {}) { const apiBaseUrl = getApiBaseUrl(); if (!apiBaseUrl) { return ""; } const url = new URL(path, `${apiBaseUrl}/`); Object.entries(query || {}).forEach(([key, value]) => { if (value == null) { return; } const normalizedValue = String(value).trim(); if (!normalizedValue) { return; } url.searchParams.set(key, normalizedValue); }); const apiKey = getApiKey(); if (apiKey && !url.searchParams.has("api_key")) { url.searchParams.set("api_key", apiKey); } return url.toString(); } function toApiAssetUrl(assetPath) { const apiBaseUrl = getApiBaseUrl(); const normalizedAssetPath = String(assetPath || "") .trim() .replace(/^\/+/, "") .replace(/^asset\//i, ""); if (!apiBaseUrl || !normalizedAssetPath) { return ""; } const url = new URL(`/api/v1/assets/${encodePathSegments(normalizedAssetPath)}`, `${apiBaseUrl}/`); const apiKey = getApiKey(); if (apiKey) { url.searchParams.set("api_key", apiKey); } return url.toString(); } function resetCaches() { magickManifestCache = null; magickDataCache = null; deckOptionsCache = null; quizCategoriesCache = null; textLibraryCache = null; deckManifestCache.clear(); quizTemplatesCache.clear(); textSourceCache.clear(); textSectionCache.clear(); textLexiconCache.clear(); textLexiconOccurrencesCache.clear(); textSearchCache.clear(); } function normalizeTarotName(value) { return String(value || "") .trim() .toLowerCase() .replace(/\s+/g, " "); } function resolveTarotTrumpNumber(cardName) { const key = normalizeTarotName(cardName); if (!key) { return null; } if (Object.prototype.hasOwnProperty.call(TAROT_TRUMP_NUMBER_BY_NAME, key)) { return TAROT_TRUMP_NUMBER_BY_NAME[key]; } const withoutLeadingThe = key.replace(/^the\s+/, ""); if (Object.prototype.hasOwnProperty.call(TAROT_TRUMP_NUMBER_BY_NAME, withoutLeadingThe)) { return TAROT_TRUMP_NUMBER_BY_NAME[withoutLeadingThe]; } return null; } function enrichAssociation(associations) { if (!associations || typeof associations !== "object") { return associations; } const next = { ...associations }; if (next.tarotCard) { const trumpNumber = resolveTarotTrumpNumber(next.tarotCard); if (trumpNumber != null) { if (!Number.isFinite(Number(next.tarotTrumpNumber))) { next.tarotTrumpNumber = trumpNumber; } const hebrew = HEBREW_BY_TRUMP_NUMBER[trumpNumber]; if (hebrew) { if (!next.hebrewLetterId) { next.hebrewLetterId = hebrew.hebrewLetterId; } if (!Number.isFinite(Number(next.kabbalahPathNumber))) { next.kabbalahPathNumber = hebrew.kabbalahPathNumber; } } } } const planetId = String(next.planetId || "").trim().toLowerCase(); if (!next.iChingPlanetaryInfluence && planetId) { const influence = ICHING_PLANET_BY_PLANET_ID[planetId]; if (influence) { next.iChingPlanetaryInfluence = influence; } } return next; } function enrichCalendarMonth(month) { const events = Array.isArray(month?.events) ? month.events.map((event) => ({ ...event, associations: enrichAssociation(event?.associations) })) : []; return { ...month, associations: enrichAssociation(month?.associations), events }; } function enrichCelestialHoliday(holiday) { return { ...holiday, associations: enrichAssociation(holiday?.associations) }; } function enrichCalendarHoliday(holiday) { return { ...holiday, associations: enrichAssociation(holiday?.associations) }; } async function loadMagickManifest() { if (magickManifestCache) { return magickManifestCache; } magickManifestCache = await fetchJson(buildApiUrl("/api/v1/bootstrap/magick-manifest")); return magickManifestCache; } async function loadMagickDataset() { if (magickDataCache) { return magickDataCache; } magickDataCache = await fetchJson(buildApiUrl("/api/v1/bootstrap/magick-dataset")); return magickDataCache; } async function loadReferenceData() { return fetchJson(buildApiUrl("/api/v1/bootstrap/reference-data")); } async function fetchWeekEvents(geo, anchorDate = new Date()) { return fetchJson(buildApiUrl("/api/v1/calendar/week-events", { latitude: geo?.latitude, longitude: geo?.longitude, date: anchorDate instanceof Date ? anchorDate.toISOString() : anchorDate })); } async function fetchNowSnapshot(geo, timestamp = new Date()) { return fetchJson(buildApiUrl("/api/v1/now", { latitude: geo?.latitude, longitude: geo?.longitude, date: timestamp instanceof Date ? timestamp.toISOString() : timestamp })); } async function loadTarotCards(filters = {}) { return fetchJson(buildApiUrl("/api/v1/tarot/cards", { q: filters?.query, arcana: filters?.arcana, suit: filters?.suit })); } async function pullTarotSpread(spreadId, options = {}) { const normalizedSpreadId = String(spreadId || "").trim() || "three-card"; return fetchJson(buildApiUrl(`/api/v1/tarot/spreads/${encodeURIComponent(normalizedSpreadId)}/pull`, { seed: options?.seed })); } async function loadGematriaWordsByValue(value) { return fetchJson(buildApiUrl("/api/v1/gematria/words", { value })); } async function loadWordAnagrams(text) { return fetchJson(buildApiUrl("/api/v1/words/anagrams", { text })); } async function loadWordsByPrefix(prefix) { return fetchJson(buildApiUrl("/api/v1/words/prefix", { prefix })); } async function loadTextLibrary(forceRefresh = false) { if (!forceRefresh && textLibraryCache) { return textLibraryCache; } textLibraryCache = await fetchJson(buildApiUrl("/api/v1/texts")); return textLibraryCache; } async function loadTextSource(sourceId, forceRefresh = false) { const normalizedSourceId = String(sourceId || "").trim().toLowerCase(); if (!normalizedSourceId) { return null; } if (!forceRefresh && textSourceCache.has(normalizedSourceId)) { return textSourceCache.get(normalizedSourceId); } const payload = await fetchJson(buildApiUrl(`/api/v1/texts/${encodeURIComponent(normalizedSourceId)}`)); textSourceCache.set(normalizedSourceId, payload); return payload; } async function loadTextSection(sourceId, workId, sectionId, forceRefresh = false) { const normalizedSourceId = String(sourceId || "").trim().toLowerCase(); const normalizedWorkId = String(workId || "").trim().toLowerCase(); const normalizedSectionId = String(sectionId || "").trim().toLowerCase(); if (!normalizedSourceId || !normalizedWorkId || !normalizedSectionId) { return null; } const cacheKey = `${normalizedSourceId}::${normalizedWorkId}::${normalizedSectionId}`; if (!forceRefresh && textSectionCache.has(cacheKey)) { return textSectionCache.get(cacheKey); } const payload = await fetchJson(buildApiUrl( `/api/v1/texts/${encodeURIComponent(normalizedSourceId)}/works/${encodeURIComponent(normalizedWorkId)}/sections/${encodeURIComponent(normalizedSectionId)}` )); textSectionCache.set(cacheKey, payload); return payload; } async function loadTextLexiconEntry(lexiconId, entryId, forceRefresh = false) { const normalizedLexiconId = String(lexiconId || "").trim().toLowerCase(); const normalizedEntryId = String(entryId || "").trim().toUpperCase(); if (!normalizedLexiconId || !normalizedEntryId) { return null; } const cacheKey = `${normalizedLexiconId}::${normalizedEntryId}`; if (!forceRefresh && textLexiconCache.has(cacheKey)) { return textLexiconCache.get(cacheKey); } const payload = await fetchJson(buildApiUrl( `/api/v1/texts/lexicons/${encodeURIComponent(normalizedLexiconId)}/entries/${encodeURIComponent(normalizedEntryId)}` )); textLexiconCache.set(cacheKey, payload); return payload; } async function loadTextLexiconOccurrences(lexiconId, entryId, options = {}, forceRefresh = false) { const normalizedLexiconId = String(lexiconId || "").trim().toLowerCase(); const normalizedEntryId = String(entryId || "").trim().toUpperCase(); const normalizedLimit = Number.parseInt(options?.limit, 10); const limit = Number.isFinite(normalizedLimit) ? normalizedLimit : 100; if (!normalizedLexiconId || !normalizedEntryId) { return null; } const cacheKey = `${normalizedLexiconId}::${normalizedEntryId}::${limit}`; if (!forceRefresh && textLexiconOccurrencesCache.has(cacheKey)) { return textLexiconOccurrencesCache.get(cacheKey); } const payload = await fetchJson(buildApiUrl( `/api/v1/texts/lexicons/${encodeURIComponent(normalizedLexiconId)}/entries/${encodeURIComponent(normalizedEntryId)}/occurrences`, { limit } )); textLexiconOccurrencesCache.set(cacheKey, payload); return payload; } async function searchTextLibrary(query, options = {}, forceRefresh = false) { const normalizedQuery = String(query || "").trim(); const normalizedSourceId = String(options?.sourceId || "").trim().toLowerCase(); const normalizedLimit = Number.parseInt(options?.limit, 10); const limit = Number.isFinite(normalizedLimit) ? normalizedLimit : 50; if (!normalizedQuery) { return { query: "", normalizedQuery: "", scope: normalizedSourceId ? { type: "source", sourceId: normalizedSourceId } : { type: "global" }, limit, totalMatches: 0, resultCount: 0, truncated: false, results: [] }; } const cacheKey = `${normalizedSourceId || "global"}::${limit}::${normalizedQuery.toLowerCase()}`; if (!forceRefresh && textSearchCache.has(cacheKey)) { return textSearchCache.get(cacheKey); } const path = normalizedSourceId ? `/api/v1/texts/${encodeURIComponent(normalizedSourceId)}/search` : "/api/v1/texts/search"; const payload = await fetchJson(buildApiUrl(path, { q: normalizedQuery, limit })); textSearchCache.set(cacheKey, payload); return payload; } async function loadDeckOptions(forceRefresh = false) { if (!forceRefresh && deckOptionsCache) { return deckOptionsCache; } deckOptionsCache = await fetchJson(buildApiUrl("/api/v1/decks/options")); return deckOptionsCache; } async function loadDeckManifest(deckId, forceRefresh = false) { const normalizedDeckId = String(deckId || "").trim().toLowerCase(); if (!normalizedDeckId) { return null; } if (!forceRefresh && deckManifestCache.has(normalizedDeckId)) { return deckManifestCache.get(normalizedDeckId); } const manifest = await fetchJson(buildApiUrl(`/api/v1/decks/${encodeURIComponent(normalizedDeckId)}/manifest`)); deckManifestCache.set(normalizedDeckId, manifest); return manifest; } async function loadQuizCategories(forceRefresh = false) { if (!forceRefresh && quizCategoriesCache) { return quizCategoriesCache; } quizCategoriesCache = await fetchJson(buildApiUrl("/api/v1/quiz/categories")); return quizCategoriesCache; } async function loadQuizTemplates(query = {}, forceRefresh = false) { const categoryId = String(query?.categoryId || "").trim(); const cacheKey = categoryId || "__all__"; if (!forceRefresh && quizTemplatesCache.has(cacheKey)) { return quizTemplatesCache.get(cacheKey); } const templates = await fetchJson(buildApiUrl("/api/v1/quiz/templates", { categoryId })); quizTemplatesCache.set(cacheKey, templates); return templates; } async function pullQuizQuestion(query = {}) { return fetchJson(buildApiUrl("/api/v1/quiz/questions/pull", { categoryId: query?.categoryId, templateKey: query?.templateKey, difficulty: query?.difficulty, seed: query?.seed, includeAnswer: query?.includeAnswer })); } async function probeConnection() { const apiBaseUrl = getApiBaseUrl(); if (!apiBaseUrl) { return { ok: false, reason: "missing-base-url", message: "Enter an API Base URL to load TaroTime." }; } const requestOptions = { headers: buildRequestHeaders() }; try { const healthResponse = await fetch(buildApiUrl("/api/v1/health"), requestOptions); if (!healthResponse.ok) { return { ok: false, reason: "health-check-failed", message: `The API responded with ${healthResponse.status} during the health check.` }; } const health = await healthResponse.json().catch(() => null); const protectedResponse = await fetch(buildApiUrl("/api/v1/decks/options"), requestOptions); if (protectedResponse.status === 401 || protectedResponse.status === 403) { return { ok: false, reason: "auth-required", message: health?.apiKeyRequired ? "The API requires a valid API key." : "The API rejected this connection." }; } if (!protectedResponse.ok) { return { ok: false, reason: "protected-route-failed", message: `The API responded with ${protectedResponse.status} when loading protected data.` }; } const decksPayload = await protectedResponse.json().catch(() => null); return { ok: true, reason: "connected", message: "Connected.", health, deckCount: Array.isArray(decksPayload?.decks) ? decksPayload.decks.length : null }; } catch (_error) { return { ok: false, reason: "network-error", message: "Unable to reach the API. Check the URL and make sure the server is running." }; } } window.TarotDataService = { buildApiUrl, fetchNowSnapshot, fetchWeekEvents, getApiBaseUrl, getApiKey, isApiEnabled, loadDeckManifest, loadDeckOptions, loadGematriaWordsByValue, loadWordAnagrams, loadWordsByPrefix, loadQuizCategories, loadQuizTemplates, loadTarotCards, loadReferenceData, loadMagickManifest, loadMagickDataset, loadTextLibrary, loadTextSource, searchTextLibrary, loadTextSection, loadTextLexiconEntry, loadTextLexiconOccurrences, probeConnection, pullQuizQuestion, pullTarotSpread, toApiAssetUrl }; document.addEventListener("connection:updated", resetCaches); })();