// API helper — 帶 ctk cookie 跨子域 fetch;401 → 跳回 admin.ocard.co 登入 (function () { // Vite/CDN-style:不打包,直接掛 window // Ocard 後台登入入口(CRM);登入後會 set ctk cookie (Domain=.ocard.co) // URL 由 /api/config(後端 settings.ADMIN_URL)動態提供;index.html 在 bootstrap 前 // 會先呼叫 loadConfig() 把它寫到 window.ADMIN_LOGIN_URL,未設則 fallback 到 crm.ocard.co。 const FALLBACK_ADMIN_LOGIN_URL = 'https://crm.ocard.co/'; function getAdminLoginUrl() { return window.ADMIN_LOGIN_URL || FALLBACK_ADMIN_LOGIN_URL; } async function loadConfig() { try { const res = await fetch('/api/config', { credentials: 'include' }); if (res.ok) { const cfg = await res.json(); if (cfg && cfg.admin_url) window.ADMIN_LOGIN_URL = cfg.admin_url; } } catch (_) { // 後端拉不到就 fallback;不阻擋 bootstrap } if (!window.ADMIN_LOGIN_URL) window.ADMIN_LOGIN_URL = FALLBACK_ADMIN_LOGIN_URL; } async function _fetch(path, opts = {}) { const res = await fetch(path, { ...opts, credentials: 'include', // 帶 ctk cookie 過去(必要) headers: { 'Accept': 'application/json', ...(opts.headers || {}), }, }); if (res.status === 401) { // ctk 過期 / 缺 cookie / 不是員工 → 跳回 ocard 後台登入 window.location.href = getAdminLoginUrl(); return null; } if (res.status === 403) { throw new Error('FORBIDDEN: 非 Ocard 員工'); } if (!res.ok) { throw new Error(`API ${path} failed: ${res.status}`); } return res.json(); } window.AdminAPI = { loadConfig, me: () => _fetch('/api/me'), overview: { kpis: (range = '30d') => _fetch(`/api/overview/kpis?range=${encodeURIComponent(range)}`), costTrend: (range = '30d', topN = 5) => _fetch(`/api/overview/cost-trend?range=${encodeURIComponent(range)}&top_n=${topN}`), topBrands: (range = '30d', limit = 20) => _fetch(`/api/overview/top-brands?range=${encodeURIComponent(range)}&limit=${limit}`), hourly: (date) => _fetch(`/api/overview/hourly${date ? '?date=' + encodeURIComponent(date) : ''}`), }, brands: { list: (range = '30d', limit = 100) => _fetch(`/api/brands?range=${encodeURIComponent(range)}&limit=${limit}`), summary: (brandId, range = '30d') => _fetch(`/api/brands/${encodeURIComponent(brandId)}/summary?range=${encodeURIComponent(range)}`), users: (brandId, limit = 50) => _fetch(`/api/brands/${encodeURIComponent(brandId)}/users?limit=${limit}`), }, conversations: { list: ({ brandId, limit = 50, offset = 0 } = {}) => { const qs = new URLSearchParams(); if (brandId) qs.set('brand_id', brandId); qs.set('limit', limit); qs.set('offset', offset); return _fetch(`/api/conversations?${qs}`); }, get: (threadId) => _fetch(`/api/conversations/${encodeURIComponent(threadId)}`), }, cost: { byModel: (range = '30d') => _fetch(`/api/cost/by-model?range=${encodeURIComponent(range)}`), cacheRate: (range = '30d') => _fetch(`/api/cost/cache-rate?range=${encodeURIComponent(range)}`), tokenMix: (range = '30d') => _fetch(`/api/cost/token-mix?range=${encodeURIComponent(range)}`), }, }; })();