moved to API
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
(function () {
|
||||
let magickManifestCache = null;
|
||||
let magickDataCache = null;
|
||||
let deckOptionsCache = null;
|
||||
const deckManifestCache = new Map();
|
||||
let quizCategoriesCache = null;
|
||||
const quizTemplatesCache = new Map();
|
||||
|
||||
const DATA_ROOT = "data";
|
||||
const MAGICK_ROOT = DATA_ROOT;
|
||||
@@ -92,8 +96,23 @@
|
||||
pluto: "Pluto"
|
||||
};
|
||||
|
||||
function buildRequestHeaders() {
|
||||
const apiKey = getApiKey();
|
||||
return apiKey
|
||||
? {
|
||||
"x-api-key": apiKey
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
async function fetchJson(path) {
|
||||
const response = await fetch(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})`);
|
||||
}
|
||||
@@ -112,6 +131,98 @@
|
||||
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;
|
||||
deckManifestCache.clear();
|
||||
quizTemplatesCache.clear();
|
||||
}
|
||||
|
||||
function normalizeTarotName(value) {
|
||||
return String(value || "")
|
||||
.trim()
|
||||
@@ -207,7 +318,7 @@
|
||||
return magickManifestCache;
|
||||
}
|
||||
|
||||
magickManifestCache = await fetchJson(`${MAGICK_ROOT}/MANIFEST.json`);
|
||||
magickManifestCache = await fetchJson(buildApiUrl("/api/v1/bootstrap/magick-manifest"));
|
||||
return magickManifestCache;
|
||||
}
|
||||
|
||||
@@ -216,163 +327,185 @@
|
||||
return magickDataCache;
|
||||
}
|
||||
|
||||
const manifest = await loadMagickManifest();
|
||||
const files = Array.isArray(manifest?.files) ? manifest.files : [];
|
||||
const jsonFiles = files.filter((file) => file.endsWith(".json"));
|
||||
|
||||
const entries = await Promise.all(
|
||||
jsonFiles.map(async (relativePath) => {
|
||||
const data = await fetchJson(`${MAGICK_ROOT}/${relativePath}`);
|
||||
return [relativePath, data];
|
||||
})
|
||||
);
|
||||
|
||||
const grouped = {};
|
||||
entries.forEach(([relativePath, data]) => {
|
||||
const noExtensionPath = relativePath.replace(/\.json$/i, "");
|
||||
const pathParts = noExtensionPath.split("/").filter(Boolean);
|
||||
if (!pathParts.length) {
|
||||
return;
|
||||
}
|
||||
buildObjectPath(grouped, pathParts, data);
|
||||
});
|
||||
|
||||
magickDataCache = {
|
||||
manifest,
|
||||
grouped,
|
||||
files: Object.fromEntries(entries)
|
||||
};
|
||||
|
||||
magickDataCache = await fetchJson(buildApiUrl("/api/v1/bootstrap/magick-dataset"));
|
||||
return magickDataCache;
|
||||
}
|
||||
|
||||
async function loadReferenceData() {
|
||||
const { groupDecansBySign } = window.TarotCalc;
|
||||
const [
|
||||
planetsJson,
|
||||
signsJson,
|
||||
decansJson,
|
||||
sabianJson,
|
||||
planetScienceJson,
|
||||
gematriaCiphersJson,
|
||||
iChingJson,
|
||||
calendarMonthsJson,
|
||||
celestialHolidaysJson,
|
||||
calendarHolidaysJson,
|
||||
astronomyCyclesJson,
|
||||
tarotDatabaseJson,
|
||||
hebrewCalendarJson,
|
||||
islamicCalendarJson,
|
||||
wheelOfYearJson
|
||||
] = await Promise.all([
|
||||
fetchJson(`${DATA_ROOT}/planetary-correspondences.json`),
|
||||
fetchJson(`${DATA_ROOT}/signs.json`),
|
||||
fetchJson(`${DATA_ROOT}/decans.json`),
|
||||
fetchJson(`${DATA_ROOT}/sabian-symbols.json`),
|
||||
fetchJson(`${DATA_ROOT}/planet-science.json`),
|
||||
fetchJson(`${DATA_ROOT}/gematria-ciphers.json`).catch(() => ({})),
|
||||
fetchJson(`${DATA_ROOT}/i-ching.json`),
|
||||
fetchJson(`${DATA_ROOT}/calendar-months.json`),
|
||||
fetchJson(`${DATA_ROOT}/celestial-holidays.json`),
|
||||
fetchJson(`${DATA_ROOT}/calendar-holidays.json`).catch(() => ({})),
|
||||
fetchJson(`${DATA_ROOT}/astronomy-cycles.json`).catch(() => ({})),
|
||||
fetchJson(`${DATA_ROOT}/tarot-database.json`).catch(() => ({})),
|
||||
fetchJson(`${DATA_ROOT}/hebrew-calendar.json`).catch(() => ({})),
|
||||
fetchJson(`${DATA_ROOT}/islamic-calendar.json`).catch(() => ({})),
|
||||
fetchJson(`${DATA_ROOT}/wheel-of-year.json`).catch(() => ({}))
|
||||
]);
|
||||
return fetchJson(buildApiUrl("/api/v1/bootstrap/reference-data"));
|
||||
}
|
||||
|
||||
const planets = planetsJson.planets || {};
|
||||
const signs = signsJson.signs || [];
|
||||
const decans = decansJson.decans || [];
|
||||
const sabianSymbols = Array.isArray(sabianJson?.symbols) ? sabianJson.symbols : [];
|
||||
const planetScience = Array.isArray(planetScienceJson?.planets)
|
||||
? planetScienceJson.planets
|
||||
: [];
|
||||
const gematriaCiphers = gematriaCiphersJson && typeof gematriaCiphersJson === "object"
|
||||
? gematriaCiphersJson
|
||||
: {};
|
||||
const iChing = {
|
||||
trigrams: Array.isArray(iChingJson?.trigrams) ? iChingJson.trigrams : [],
|
||||
hexagrams: Array.isArray(iChingJson?.hexagrams) ? iChingJson.hexagrams : [],
|
||||
correspondences: {
|
||||
meta: iChingJson?.correspondences?.meta && typeof iChingJson.correspondences.meta === "object"
|
||||
? iChingJson.correspondences.meta
|
||||
: {},
|
||||
tarotToTrigram: Array.isArray(iChingJson?.correspondences?.tarotToTrigram)
|
||||
? iChingJson.correspondences.tarotToTrigram
|
||||
: []
|
||||
}
|
||||
};
|
||||
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
|
||||
}));
|
||||
}
|
||||
|
||||
const calendarMonths = Array.isArray(calendarMonthsJson?.months)
|
||||
? calendarMonthsJson.months.map((month) => enrichCalendarMonth(month))
|
||||
: [];
|
||||
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
|
||||
}));
|
||||
}
|
||||
|
||||
const celestialHolidays = Array.isArray(celestialHolidaysJson?.holidays)
|
||||
? celestialHolidaysJson.holidays.map((holiday) => enrichCelestialHoliday(holiday))
|
||||
: [];
|
||||
async function loadTarotCards(filters = {}) {
|
||||
return fetchJson(buildApiUrl("/api/v1/tarot/cards", {
|
||||
q: filters?.query,
|
||||
arcana: filters?.arcana,
|
||||
suit: filters?.suit
|
||||
}));
|
||||
}
|
||||
|
||||
const calendarHolidays = Array.isArray(calendarHolidaysJson?.holidays)
|
||||
? calendarHolidaysJson.holidays.map((holiday) => enrichCalendarHoliday(holiday))
|
||||
: [];
|
||||
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
|
||||
}));
|
||||
}
|
||||
|
||||
const astronomyCycles = astronomyCyclesJson && typeof astronomyCyclesJson === "object"
|
||||
? astronomyCyclesJson
|
||||
: {};
|
||||
|
||||
const tarotDatabase = tarotDatabaseJson && typeof tarotDatabaseJson === "object"
|
||||
? tarotDatabaseJson
|
||||
: {};
|
||||
|
||||
const sourceMeanings = tarotDatabase.meanings && typeof tarotDatabase.meanings === "object"
|
||||
? tarotDatabase.meanings
|
||||
: {};
|
||||
|
||||
if (!sourceMeanings.majorByTrumpNumber || typeof sourceMeanings.majorByTrumpNumber !== "object") {
|
||||
sourceMeanings.majorByTrumpNumber = {};
|
||||
async function loadDeckOptions(forceRefresh = false) {
|
||||
if (!forceRefresh && deckOptionsCache) {
|
||||
return deckOptionsCache;
|
||||
}
|
||||
|
||||
const existingByCardName = sourceMeanings.byCardName && typeof sourceMeanings.byCardName === "object"
|
||||
? sourceMeanings.byCardName
|
||||
: {};
|
||||
deckOptionsCache = await fetchJson(buildApiUrl("/api/v1/decks/options"));
|
||||
return deckOptionsCache;
|
||||
}
|
||||
|
||||
sourceMeanings.byCardName = existingByCardName;
|
||||
async function loadDeckManifest(deckId, forceRefresh = false) {
|
||||
const normalizedDeckId = String(deckId || "").trim().toLowerCase();
|
||||
if (!normalizedDeckId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
tarotDatabase.meanings = sourceMeanings;
|
||||
if (!forceRefresh && deckManifestCache.has(normalizedDeckId)) {
|
||||
return deckManifestCache.get(normalizedDeckId);
|
||||
}
|
||||
|
||||
const hebrewCalendar = hebrewCalendarJson && typeof hebrewCalendarJson === "object"
|
||||
? hebrewCalendarJson
|
||||
: {};
|
||||
const islamicCalendar = islamicCalendarJson && typeof islamicCalendarJson === "object"
|
||||
? islamicCalendarJson
|
||||
: {};
|
||||
const wheelOfYear = wheelOfYearJson && typeof wheelOfYearJson === "object"
|
||||
? wheelOfYearJson
|
||||
: {};
|
||||
const manifest = await fetchJson(buildApiUrl(`/api/v1/decks/${encodeURIComponent(normalizedDeckId)}/manifest`));
|
||||
deckManifestCache.set(normalizedDeckId, manifest);
|
||||
return manifest;
|
||||
}
|
||||
|
||||
return {
|
||||
planets,
|
||||
signs,
|
||||
decansBySign: groupDecansBySign(decans),
|
||||
sabianSymbols,
|
||||
planetScience,
|
||||
gematriaCiphers,
|
||||
iChing,
|
||||
calendarMonths,
|
||||
celestialHolidays,
|
||||
calendarHolidays,
|
||||
astronomyCycles,
|
||||
tarotDatabase,
|
||||
hebrewCalendar,
|
||||
islamicCalendar,
|
||||
wheelOfYear
|
||||
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,
|
||||
loadQuizCategories,
|
||||
loadQuizTemplates,
|
||||
loadTarotCards,
|
||||
loadReferenceData,
|
||||
loadMagickManifest,
|
||||
loadMagickDataset
|
||||
loadMagickDataset,
|
||||
probeConnection,
|
||||
pullQuizQuestion,
|
||||
pullTarotSpread,
|
||||
toApiAssetUrl
|
||||
};
|
||||
|
||||
document.addEventListener("connection:updated", resetCaches);
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user