(function () { const root = document.getElementById('vici-account'); if (!root) return; document.body.classList.add('va-account-page'); const ICON_COPY = ` `; const ICON_EYE = ` `; const ICON_EYE_SLASH = ` `; const ICON_SPINNER = ` `; const toast = (type, msg) => { if (typeof window.showToast === 'function') { window.showToast(type === 'ok' ? 'success' : 'error', msg); } else { console.log(type, msg); } }; const $ = (sel) => root.querySelector(sel); const $$ = (sel) => Array.from(root.querySelectorAll(sel)); const apiPassEl = $('#va-api-pass'); const emailForm = $('#va-email-form'); const emailCurrentEl = $('#va-email-current'); const emailNewEl = $('#va-email-new'); const emailPassEl = $('#va-email-pass'); const emailUpdateBtn = $('#va-email-update-btn'); function escapeHtml(s) { return String(s ?? '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function setButtonLoading(btn, on, label) { if (!btn) return; if (!btn.dataset.vaLabel) btn.dataset.vaLabel = btn.innerHTML; if (on) { btn.disabled = true; btn.classList.add('is-loading'); btn.setAttribute('aria-busy', 'true'); btn.setAttribute('aria-disabled', 'true'); const text = escapeHtml(label || 'Generating'); btn.innerHTML = ` ${text} `; } else { btn.disabled = false; btn.classList.remove('is-loading'); btn.removeAttribute('aria-busy'); btn.removeAttribute('aria-disabled'); btn.innerHTML = btn.dataset.vaLabel || 'Generate new key'; } } function loadingInlineHTML(label) { const safe = escapeHtml(label || 'Loading'); return `
${safe}
`; } function clamp(n, a, b) { return Math.max(a, Math.min(b, n)); } function isPlainObject(v) { return !!v && typeof v === 'object' && !Array.isArray(v); } function deepMerge(base, patch) { if (!isPlainObject(base)) base = {}; if (!isPlainObject(patch)) return base; const out = { ...base }; for (const [k, v] of Object.entries(patch)) { if (isPlainObject(v) && isPlainObject(base[k])) out[k] = deepMerge(base[k], v); else out[k] = v; } return out; } function safeNum(v) { const n = Number(v); return Number.isFinite(n) ? n : null; } function epochToDate(v) { const n = safeNum(v); if (n == null) return null; const ms = n > 1e12 ? n : n * 1000; const d = new Date(ms); return Number.isNaN(d.getTime()) ? null : d; } function parseAnyDate(v) { if (v == null) return null; if (v instanceof Date) { return Number.isNaN(v.getTime()) ? null : v; } const n = safeNum(v); if (n != null) return epochToDate(n); if (typeof v === 'string') { const s = v.trim(); if (!s) return null; const d = new Date(s); if (!Number.isNaN(d.getTime())) return d; const s2 = s.replace(' ', 'T'); const d2 = new Date(s2); return Number.isNaN(d2.getTime()) ? null : d2; } return null; } function formatDate(d) { try { return d.toLocaleString(undefined, { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } catch { return String(d); } } function formatCountdown4(ms) { const s = Math.max(0, Math.floor(ms / 1000)); const days = Math.floor(s / 86400); const hrs = Math.floor((s % 86400) / 3600); const mins = Math.floor((s % 3600) / 60); const secs = s % 60; const pad2 = (n) => String(n).padStart(2, '0'); return `${pad2(days)}:${pad2(hrs)}:${pad2(mins)}:${pad2(secs)}`; } function fmtNum(n) { if (!Number.isFinite(n)) return 'N/A'; const abs = Math.abs(n); const decimals = abs % 1 === 0 ? 0 : (abs < 1 ? 2 : 1); return n.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); } function nextMonthFirstMidnightUTC(now) { const y = now.getUTCFullYear(); const m = now.getUTCMonth(); return new Date(Date.UTC(y, m + 1, 1, 0, 0, 0, 0)); } function firstOfMonthMidnightUTC(now) { const y = now.getUTCFullYear(); const m = now.getUTCMonth(); return new Date(Date.UTC(y, m, 1, 0, 0, 0, 0)); } function formatDateUTC(d) { try { const s = d.toLocaleString(undefined, { timeZone: 'UTC', year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }); return `${s}`; } catch { return `${d.toISOString()}`; } } async function loadEmailPanel() { const member = await getMember(); const currentEmail = member?.auth?.email || member?.email || ''; if (emailCurrentEl) emailCurrentEl.value = currentEmail; if (emailPassEl) scrubIfLooksLikeEmail(emailPassEl); } async function getMS() { const start = Date.now(); while (!window.$memberstackDom && Date.now() - start < 12000) { await new Promise((r) => setTimeout(r, 60)); } return window.$memberstackDom || null; } async function getMember() { const ms = await getMS(); if (!ms?.getCurrentMember) return null; try { const res = await ms.getCurrentMember(); return res?.data || null; } catch { return null; } } function getCookieValue(name) { const m = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[-.$?*|{}()[\]\\/+^]/g, '\\$&') + '=([^;]*)')); return m ? decodeURIComponent(m[1]) : ''; } async function reauthWithPassword(currentPassword) { const ms = await getMS(); if (!ms?.loginMemberEmailPassword) throw new Error('Memberstack auth not ready.'); const member = await getMember(); const email = member?.auth?.email || member?.email || ''; if (!email) throw new Error('Could not determine your current email.'); if (!currentPassword) throw new Error('Enter your current password.'); await ms.loginMemberEmailPassword({ email, password: currentPassword }); const token = await window.ViciAuth?.getJwt?.(); if (!token) throw new Error('Could not obtain session token after reauth.'); return token; } async function getUserId() { const m = await getMember(); return m?.id || null; } async function msGetMemberJSON() { const ms = await getMS(); if (!ms?.getMemberJSON) return {}; const res = await ms.getMemberJSON(); const json = (isPlainObject(res) && 'json' in res) ? res.json : (isPlainObject(res) && 'data' in res && isPlainObject(res.data) && 'json' in res.data) ? res.data.json : (isPlainObject(res) && 'data' in res) ? res.data : res; return isPlainObject(json) ? json : {}; } async function msUpdateMemberJSON(nextJson) { const ms = await getMS(); if (!ms?.updateMemberJSON) throw new Error('Account update unavailable.'); if (!isPlainObject(nextJson)) throw new Error('Invalid account data.'); return ms.updateMemberJSON({ json: nextJson }); } const KEYGEN = "https://vici-bio--api-generate-api-key.modal.run"; const tokenIdEl = $('#va-token-id'); const tokenSecretEl = $('#va-token-secret'); const genBtn = $('#va-generate-key'); const apiCtxTabs = $('#va-api-ctx-tabs'); const apiCtxBtns = apiCtxTabs ? Array.from(apiCtxTabs.querySelectorAll('.side-tab[data-va-scope]')) : []; let apiCtxBound = false; function setApiCtxState(state) { if (!apiCtxTabs) return; apiCtxTabs.dataset.state = state || 'ready'; } function setApiCtxActive(scope) { if (!apiCtxTabs) return; apiCtxBtns.forEach(btn => { const s = btn.getAttribute('data-va-scope'); const on = s === scope; btn.classList.toggle('is-active', on); btn.setAttribute('aria-selected', on ? 'true' : 'false'); }); } async function ensureApiCtxValid() { const userId = await getUserId(); const member = await getMember(); const teamIds = extractTeamIds(member || {}); const ctx = parseCtx(); const teamBtn = apiCtxTabs?.querySelector('.side-tab[data-va-scope="team"]') || null; if (teamBtn) teamBtn.disabled = teamIds.length === 0; if (ctx.scope === 'team' && teamIds.length === 0) { if (userId) setCtxString(`user:${userId}`); emitCtxChange(); return { scope: 'user', teamId: '' }; } if (ctx.scope === 'team') { const resolved = ctx.teamId || sessionStorage.getItem('vici:lastTeam') || teamIds[0] || ''; if (resolved) { sessionStorage.setItem('vici:lastTeam', resolved); if (ctx.teamId !== resolved) { setCtxString(`team:${resolved}`); emitCtxChange(); } return { scope: 'team', teamId: resolved }; } if (userId) setCtxString(`user:${userId}`); emitCtxChange(); return { scope: 'user', teamId: '' }; } return { scope: 'user', teamId: '' }; } async function bindApiCtxTabsOnce() { if (!apiCtxTabs || apiCtxBound) return; apiCtxBound = true; apiCtxTabs.addEventListener('click', async (e) => { const btn = e.target.closest('.side-tab[data-va-scope]'); if (!btn || btn.disabled) return; const targetScope = btn.getAttribute('data-va-scope') || 'user'; const userId = await getUserId(); if (!userId) { toast('err', 'Not signed in.'); return; } setApiCtxState('loading'); if (targetScope === 'team') { const member = await getMember(); const teamIds = extractTeamIds(member || {}); if (!teamIds.length) { toast('err', 'No team found on your account.'); setCtxString(`user:${userId}`); emitCtxChange(); setApiCtxActive('user'); setApiCtxState('ready'); await loadApiFromMemberstack(); return; } const resolved = parseCtx().teamId || sessionStorage.getItem('vici:lastTeam') || teamIds[0]; sessionStorage.setItem('vici:lastTeam', resolved); setCtxString(`team:${resolved}`); emitCtxChange(); setApiCtxActive('team'); } else { setCtxString(`user:${userId}`); emitCtxChange(); setApiCtxActive('user'); } setApiCtxState('ready'); await loadApiFromMemberstack(); }); } function setApiFields({ token_id = '', token_secret = '' } = {}) { if (tokenIdEl) tokenIdEl.value = token_id || ''; if (tokenSecretEl) tokenSecretEl.value = token_secret || ''; if (tokenIdEl) tokenIdEl.type = 'password'; if (tokenSecretEl) tokenSecretEl.type = 'password'; $$('[data-toggle-pass="#va-token-id"], [data-toggle-pass="#va-token-secret"]').forEach((btn) => { if (!btn) return; btn.innerHTML = ICON_EYE; btn.setAttribute('aria-label', 'Show value'); }); } async function loadApiFromMemberstack() { await bindApiCtxTabsOnce(); setApiCtxState('loading'); const ctxResolved = await ensureApiCtxValid(); setApiCtxActive(ctxResolved.scope); setApiCtxState('ready'); const user_id = await getUserId(); if (!user_id) return; const json = await msGetMemberJSON(); window.__viciMemberJSON = json; const token_id = getApiStoredTokenId(json, parseCtx()); setApiFields({ token_id, token_secret: '' }); } async function postJson(url, payload, headers, opts) { const token = (opts && opts.token) ? opts.token : await window.ViciAuth?.getJwt?.(); if (!token) throw new Error('Missing session token. Please sign in again.'); const resp = await fetch(url, { method: 'POST', headers: Object.assign( { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, headers || {} ), body: JSON.stringify(payload || {}) }); const data = await resp.json().catch(() => null); if (!resp.ok) throw new Error(data?.error || data?.detail || 'Request failed.'); return data; } function extractOwnedTeamIds(member) { const set = new Set(); const owned = member?.teams?.ownedTeams || []; for (const t of owned) if (t?.teamId && typeof t.teamId === 'string') set.add(t.teamId); return Array.from(set); } async function generateKey() { const user_id = await getUserId(); if (!user_id) { toast('err', 'Not signed in.'); return; } if (genBtn?.disabled) return; const ctx = parseCtx(); if (ctx.scope === 'team') { const member = await getMember(); const ownedTeamIds = extractOwnedTeamIds(member || {}); if (!ownedTeamIds.includes(ctx.teamId)) { toast('err', 'Only the team owner can generate or rotate a team API key.'); setButtonLoading(genBtn, false); return; } } const currentPassword = (apiPassEl?.value || '').trim(); if (!currentPassword) { toast('err', 'Enter your current password to generate (rotate) a key.'); return; } setButtonLoading(genBtn, true, 'Generating key'); try { const token = await reauthWithPassword(currentPassword); const payload = getCtxPayload(user_id); const data = await postJson(KEYGEN, payload, null, { token }); const token_id = String(data?.token_id || '').trim(); const token_secret = String(data?.token_secret || '').trim(); if (!token_id || !token_secret) throw new Error('Unexpected response from key generator.'); setApiFields({ token_id, token_secret }); const current = await msGetMemberJSON(); const ctx = parseCtx(); let next; if (ctx.scope === 'team' && ctx.teamId) { next = deepMerge(current, { vici: { api: { teams: { [ctx.teamId]: { token_id, updated_at: new Date().toISOString() } } } } }); } else { next = deepMerge(current, { vici: { api: { user: { token_id, updated_at: new Date().toISOString() }, token_id } } }); } await msUpdateMemberJSON(next); if (apiPassEl) apiPassEl.value = ''; toast('ok', 'New key generated. Copy Token Secret now, it will not be stored.'); } catch (err) { toast('err', err?.message || 'Key generation failed.'); } finally { setButtonLoading(genBtn, false); } } const elCreditsValue = $('#va-credits-value'); const elCreditsSub = $('#va-credits-sub'); const elCreditsMeta = $('#va-credits-meta'); const elCreditsRing = $('#va-credits-ring'); const elCreditsMarker = $('#va-credits-marker'); const elParallelValue = $('#va-parallel-value'); const elParallelSub = $('#va-parallel-sub'); const elParallelMeta = $('#va-parallel-meta'); const elParallelRing = $('#va-parallel-ring'); const elParallelMarker = $('#va-parallel-marker'); const elRenewValue = $('#va-renew-value'); const elRenewSub = $('#va-renew-sub'); const elRenewMeta = $('#va-renew-meta'); const elRenewRing = $('#va-renew-ring'); const elRenewMarker = $('#va-renew-marker'); const elPlansList = $('#va-plans-list'); let renewTimer = null; let lastDashboardModel = null; function setLoading(valueEl, subEl, metaEl) { if (metaEl) metaEl.textContent = ''; if (subEl) subEl.textContent = ''; if (valueEl) { valueEl.classList.add('is-loading'); delete valueEl.dataset.frac; valueEl.innerHTML = ``; } } function setValueText(valueEl, text) { if (!valueEl) return; valueEl.classList.remove('is-loading'); valueEl.textContent = text; } function ensureFractionTemplate(valueEl) { if (!valueEl) return null; const aEl = valueEl.querySelector('[data-frac="a"]'); const bEl = valueEl.querySelector('[data-frac="b"]'); const templateIsValid = (aEl && bEl); if (valueEl.dataset.frac === '1' && templateIsValid) return { a: aEl, b: bEl }; valueEl.classList.remove('is-loading'); valueEl.dataset.frac = '1'; valueEl.innerHTML = ` / `; return { a: valueEl.querySelector('[data-frac="a"]'), b: valueEl.querySelector('[data-frac="b"]') }; } function setFraction(valueEl, a, b) { const refs = ensureFractionTemplate(valueEl); if (!refs) return; if (refs.a) refs.a.textContent = fmtNum(a); if (refs.b) refs.b.textContent = fmtNum(b); } function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); } function setGaugeProgress(pathEl, markerEl, pct) { if (!pathEl) return; const p = clamp(Number(pct) || 0, 0, 100); let len = safeNum(pathEl.dataset.len); if (!len && typeof pathEl.getTotalLength === 'function') { try { len = pathEl.getTotalLength(); } catch {} } if (!len) len = 145; pathEl.dataset.len = String(len); pathEl.style.strokeDasharray = String(len); pathEl.style.strokeDashoffset = String(len - (len * (p / 100))); if (markerEl) { const r = 46; const ang = Math.PI * (1 - p / 100); const cx = 60 + r * Math.cos(ang); const cy = 60 - r * Math.sin(ang); markerEl.setAttribute('cx', cx.toFixed(2)); markerEl.setAttribute('cy', cy.toFixed(2)); markerEl.style.opacity = p > 0 ? '1' : '0'; } } function animateFractionNumerator({ valueEl, ringEl, markerEl, toA, denom, duration = 650 }) { const start = performance.now(); const fromA = 0; ensureFractionTemplate(valueEl); const isInt = Number.isInteger(toA) && Number.isInteger(denom); const step = (now) => { const t = clamp((now - start) / duration, 0, 1); const e = easeOutCubic(t); let curA = fromA + (toA - fromA) * e; if (isInt) curA = Math.round(curA); setFraction(valueEl, curA, denom); const pct = denom > 0 ? (curA / denom) * 100 : 0; setGaugeProgress(ringEl, markerEl, pct); if (t < 1) requestAnimationFrame(step); }; requestAnimationFrame(step); } function toMs(x) { if (!x) return null; const d = new Date(x); if (!Number.isNaN(d.getTime())) return d.getTime(); const t2 = String(x).replace(' ', 'T').replace(/\.\d+$/, ''); const d2 = new Date(t2); return Number.isNaN(d2.getTime()) ? null : d2.getTime(); } function countRunningFromStatus(results) { if (!results) return 0; const arr = Array.isArray(results) ? results : Object.values(results || {}); let total = 0; for (const wf of arr) { const wfError = wf?.error === true || (typeof wf?.error === 'string' && wf.error.trim()); const wfFinished = wf?.finished === true; if (wfError || wfFinished) continue; const cls = String(wf?.class || '').trim().toLowerCase(); if (cls === 'nexo') { const nodesObj = (wf && typeof wf.status === 'object') ? wf.status : {}; const reportedIds = Object.keys(nodesObj); const nField = Number(wf?.num_nodes); const totalNodes = (Number.isFinite(nField) && nField > 0) ? Math.max(nField, reportedIds.length) : reportedIds.length; let finishedNodes = 0; for (const nid of reportedIds) { const n = nodesObj[nid]; const e1 = toMs(n?.end_time); const e2 = toMs(n?.status?.end_time); const e3 = toMs(n?.status?.output?.end_time); if (e1 != null || e2 != null || e3 != null) finishedNodes++; } let activeNodes = totalNodes - finishedNodes; if (!(totalNodes > 0)) activeNodes = 1; total += Math.max(0, activeNodes); } else { total += 1; } } return total; } function extractTeamIds(member) { const set = new Set(); const owned = member?.teams?.ownedTeams || []; const joined = member?.teams?.joinedTeams || []; for (const t of owned) if (t?.teamId && typeof t.teamId === 'string') set.add(t.teamId); for (const t of joined) if (t?.teamId && typeof t.teamId === 'string') set.add(t.teamId); return Array.from(set); } function getCtxPayload(userId) { if (window.ViciContext?.payloadFor) return window.ViciContext.payloadFor(userId); const ctx = sessionStorage.getItem('vici:ctx'); if (ctx && typeof ctx === 'string') { const [type, id] = ctx.split(':'); const p = { user_id: userId }; if (type === 'team' && id) p.team_id = id; return p; } return { user_id: userId }; } function getCtxString() { return (window.ViciContext?.get?.() || sessionStorage.getItem('vici:ctx') || '').trim(); } function setCtxString(v) { if (window.ViciContext?.set) window.ViciContext.set(v); else sessionStorage.setItem('vici:ctx', v); } function parseCtx() { const s = getCtxString(); if (s.startsWith('team:')) return { scope: 'team', teamId: s.slice(5) }; if (s.startsWith('user:')) return { scope: 'user' }; return { scope: 'user' }; } async function loadStatusFromModal() { const url = window.VICI_CREDITS_URL || window.MODAL_CREDITS_URL || window.MODAL_STATUS_URL || ''; if (!url) return { ok: false, msg: 'Missing credits URL.' }; const userId = await getUserId(); if (!userId) return { ok: false, msg: 'Not signed in.' }; const payload = getCtxPayload(userId); try { const data = await postJson(url, payload); window.__viciLastCreditsResponse = data; const credit = safeNum(data?.credit_balance); const cap = safeNum(data?.max_num_credits); const pCap = safeNum(data?.max_num_jobs_parallel); const running = Number.isFinite(safeNum(data?.running)) ? safeNum(data?.running) : countRunningFromStatus(data?.results); if (credit == null) { const keys = Object.keys(data || {}).slice(0, 12).join(', '); return { ok: false, msg: `Endpoint response missing credit_balance. Keys: ${keys || 'none'}`, data }; } return { ok: true, credit, cap, pCap, running, data }; } catch (err) { return { ok: false, msg: err?.message || 'Failed to load credits.' }; } } function findTeamIdFromConnection(p) { return ( p?.teamId || p?.team_id || p?.team?.id || p?.team?.teamId || p?.team?.team_id || p?.payment?.teamId || p?.payment?.team_id || null ); } function planNameFromMS(p) { const n = p?.plan?.name || p?.plan?.title || p?.plan?.displayName || p?.planName || p?.name || ''; return String(n || '').trim(); } function canonicalPlanName(raw) { const s = String(raw || '').trim().toLowerCase(); if (!s) return ''; if (s.includes('enterprise')) return 'Enterprise'; if (s.includes('team')) return 'Team'; if (s.includes('pro')) return 'Pro'; if (s.includes('standard')) return 'Standard'; if (s.includes('free')) return 'Free'; return ''; } function normalizePlanConnections(member) { const teamIds = extractTeamIds(member || {}); const pcs = Array.isArray(member?.planConnections) ? member.planConnections : []; return pcs.map(p => { const statusRaw = String(p?.status || p?.state || 'UNKNOWN'); const status = statusRaw.toUpperCase(); const active = p?.active === true || /ACTIVE|TRIAL|TRIALING|PAID|SUCCEEDED|CURRENT|LIVE/.test(status); const nextBilling = parseAnyDate(p?.payment?.nextBillingDate) || parseAnyDate(p?.payment?.currentPeriodEnd) || parseAnyDate(p?.payment?.current_period_end) || null; const currentPeriodStart = parseAnyDate(p?.payment?.currentPeriodStart) || parseAnyDate(p?.payment?.current_period_start) || null; const currentPeriodEnd = parseAnyDate(p?.payment?.currentPeriodEnd) || parseAnyDate(p?.payment?.current_period_end) || null; const lastBilling = parseAnyDate(p?.payment?.lastBillingDate) || parseAnyDate(p?.payment?.last_billing_date) || null; const periodStart = currentPeriodStart || lastBilling || null; const rawName = planNameFromMS(p) || p?.plan?.slug || p?.plan?.code || p?.type || ''; const kind = canonicalPlanName(rawName); const teamIdRaw = findTeamIdFromConnection(p); const isFree = (kind === 'Free') || (String(p?.type || '').toUpperCase() === 'FREE'); const assumeTeam = !isFree && teamIds.length > 0 && !teamIdRaw && !kind; const scope = (kind === 'Team' || assumeTeam || teamIdRaw) ? 'team' : 'user'; const teamId = (scope === 'team') ? (teamIdRaw || teamIds[0] || '') : ''; const name = (kind && ['Free', 'Standard', 'Pro', 'Team', 'Enterprise'].includes(kind)) ? kind : (scope === 'team' ? 'Team' : 'Standard'); return { key: String(p?.id || p?.planId || p?.plan?.id || Math.random()), name, status, active, isFree, scope, teamId, nextBilling, lastBilling, periodStart, currentPeriodStart, currentPeriodEnd }; }).filter(c => c.active); } function getSelectedPlanKey() { return (sessionStorage.getItem('vici:selectedPlanKey') || 'user:free').trim(); } function setSelectedPlanKey(key) { sessionStorage.setItem('vici:selectedPlanKey', String(key || 'user:free')); } function applyPlansSelection() { if (!elPlansList) return; let key = getSelectedPlanKey(); let found = false; elPlansList.querySelectorAll('.va-planrow').forEach(row => { const isOn = row.getAttribute('data-va-key') === key; if (isOn) found = true; row.classList.toggle('is-selected', isOn); }); if (!found) { key = 'user:free'; setSelectedPlanKey(key); elPlansList.querySelectorAll('.va-planrow').forEach(row => { row.classList.toggle('is-selected', row.getAttribute('data-va-key') === key); }); } } function plansSignature(conns) { const parts = (conns || []).map(c => [ c.scope, c.teamId || '', c.name || '', c.status || '', c.nextBilling ? c.nextBilling.getTime() : 0 ].join('|')); parts.sort(); return parts.join('||'); } function renderPlansList({ conns }) { if (!elPlansList) return; const now = new Date(); const nextFree = nextMonthFirstMidnightUTC(now); const rows = []; const addRow = ({ key, scope, teamId, title, meta, statusText }) => { const ok = /ACTIVE|TRIAL|TRIALING|PAID|SUCCEEDED|CURRENT|LIVE/.test(String(statusText || '').toUpperCase()); const pillClass = ok ? 'va-pill-green' : 'va-pill-red'; rows.push(`
${escapeHtml(title)}
${escapeHtml(meta || '')}
${escapeHtml(statusText || '')}
`); }; addRow({ key: 'user:free', scope: 'user', teamId: '', title: 'Free', meta: `Resets ${formatDateUTC(nextFree)}`, statusText: 'ACTIVE' }); const seen = new Set(); for (const c of conns) { if (c.isFree) continue; const k = `${c.scope}:${c.teamId || ''}:${c.name}:${c.status}`; if (seen.has(k)) continue; seen.add(k); const renew = c.nextBilling ? `Renews ${formatDate(c.nextBilling)}` : 'Renew date unavailable'; addRow({ key: (c.scope === 'team' && c.teamId) ? `team:${c.teamId}` : `user:${c.key}`, scope: c.scope, teamId: c.teamId || '', title: c.name, meta: renew, statusText: c.status || (c.active ? 'ACTIVE' : 'INACTIVE') }); } elPlansList.innerHTML = rows.join('') || `
No plans found.
`; elPlansList.dataset.loaded = '1'; applyPlansSelection(); } function bindPlansEventsOnce() { if (!elPlansList || elPlansList.dataset.bound === '1') return; elPlansList.dataset.bound = '1'; const activateFromRow = async (row) => { const userId = await getUserId(); if (!userId) return; const key = row.getAttribute('data-va-key') || 'user:free'; const scope = row.getAttribute('data-va-scope') || 'user'; const teamId = (row.getAttribute('data-va-team-id') || '').trim(); setSelectedPlanKey(key); applyPlansSelection(); if (scope === 'team') { const tid = teamId || sessionStorage.getItem('vici:lastTeam') || ''; if (!tid) { toast('err', 'Could not determine team id for this plan.'); return; } sessionStorage.setItem('vici:lastTeam', tid); setCtxString(`team:${tid}`); } else { setCtxString(`user:${userId}`); } if (currentKey === 'dashboard') loadDashboard(); }; elPlansList.addEventListener('click', (e) => { const row = e.target.closest('.va-planrow'); if (!row) return; activateFromRow(row); }); elPlansList.addEventListener('keydown', (e) => { if (e.key !== 'Enter' && e.key !== ' ') return; const row = e.target.closest('.va-planrow'); if (!row) return; e.preventDefault(); activateFromRow(row); }); } async function loadDashboard() { if (renewTimer) { clearInterval(renewTimer); renewTimer = null; } bindPlansEventsOnce(); setLoading(elCreditsValue, elCreditsSub, elCreditsMeta); setLoading(elParallelValue, elParallelSub, elParallelMeta); setLoading(elRenewValue, elRenewSub, elRenewMeta); setGaugeProgress(elCreditsRing, elCreditsMarker, 0); setGaugeProgress(elParallelRing, elParallelMarker, 0); setGaugeProgress(elRenewRing, elRenewMarker, 0); if (elPlansList && elPlansList.dataset.loaded !== '1' && !elPlansList.querySelector('.va-planrow')) { elPlansList.innerHTML = loadingInlineHTML('Loading'); } const [statusRes, member] = await Promise.all([ loadStatusFromModal(), getMember() ]); const conns = normalizePlanConnections(member || {}); const teamIds = extractTeamIds(member || {}); const ctx = parseCtx(); if (!sessionStorage.getItem('vici:selectedPlanKey')) { if (ctx.scope === 'team' && ctx.teamId) setSelectedPlanKey(`team:${ctx.teamId}`); else setSelectedPlanKey('user:free'); } const sig = plansSignature(conns); if (!lastDashboardModel || lastDashboardModel.__sig !== sig || elPlansList?.dataset.loaded !== '1') { lastDashboardModel = { conns, teamIds, ctx, __sig: sig }; renderPlansList(lastDashboardModel); } else { applyPlansSelection(); } if (!statusRes.ok) { if (elCreditsMeta) elCreditsMeta.textContent = 'Credits'; setValueText(elCreditsValue, '-- / --'); if (elCreditsSub) elCreditsSub.textContent = statusRes.msg || 'Credits unavailable.'; setGaugeProgress(elCreditsRing, elCreditsMarker, 0); if (elParallelMeta) elParallelMeta.textContent = 'Parallel jobs'; setValueText(elParallelValue, '-- / --'); if (elParallelSub) elParallelSub.textContent = 'Unavailable'; setGaugeProgress(elParallelRing, elParallelMarker, 0); } else { const credit = statusRes.credit; const cap = statusRes.cap ?? credit; const running = safeNum(statusRes.running) ?? 0; const pCap = statusRes.pCap ?? 0; if (elCreditsMeta) elCreditsMeta.textContent = (ctx.scope === 'team') ? 'Team credits' : 'Solo credits'; setFraction(elCreditsValue, 0, cap); if (elCreditsSub) elCreditsSub.textContent = 'Remaining / total'; animateFractionNumerator({ valueEl: elCreditsValue, ringEl: elCreditsRing, markerEl: elCreditsMarker, toA: credit, denom: cap, duration: 650 }); if (elParallelMeta) elParallelMeta.textContent = 'In-flight / limit'; setFraction(elParallelValue, 0, pCap); if (elParallelSub) elParallelSub.textContent = 'Jobs currently running'; animateFractionNumerator({ valueEl: elParallelValue, ringEl: elParallelRing, markerEl: elParallelMarker, toA: running, denom: pCap, duration: 650 }); } const now = new Date(); let renewAt = null; let periodStart = null; let label = ''; let selectedPlan = null; const paidTeam = conns.filter(c => c.active && !c.isFree && c.scope === 'team'); const paidUser = conns.filter(c => c.active && !c.isFree && c.scope === 'user'); if (ctx.scope === 'team') { const resolved = ctx.teamId || sessionStorage.getItem('vici:lastTeam') || teamIds[0] || ''; if (resolved && ctx.teamId !== resolved) { ctx.teamId = resolved; sessionStorage.setItem('vici:lastTeam', resolved); setCtxString(`team:${resolved}`); const sk = getSelectedPlanKey(); if (sk.startsWith('team:')) setSelectedPlanKey(`team:${resolved}`); } } if (ctx.scope === 'team') { const tid = ctx.teamId || ''; const plan = paidTeam.find(c => String(c.teamId || '') === String(tid)) || paidTeam[0] || null; if (!plan) { renewAt = nextMonthFirstMidnightUTC(now); periodStart = firstOfMonthMidnightUTC(now); label = 'Free reset'; } else { renewAt = plan?.nextBilling || null; periodStart = plan?.periodStart || plan?.lastBilling || null; label = 'Team renewal'; } } else { const plan = paidUser[0] || null; if (!plan) { renewAt = nextMonthFirstMidnightUTC(now); periodStart = firstOfMonthMidnightUTC(now); label = 'Free reset'; } else { renewAt = plan?.nextBilling || null; periodStart = plan?.periodStart || plan?.lastBilling || null; label = 'Solo renewal'; } } const DAY = 24 * 3600 * 1000; let CYCLE = 30 * DAY; if (renewAt) { const endMs = renewAt.getTime(); const cpStart = selectedPlan?.currentPeriodStart; const cpEnd = selectedPlan?.currentPeriodEnd; const candidateFromCurrentPeriod = (cpStart && cpEnd && cpEnd.getTime() > cpStart.getTime()) ? (cpEnd.getTime() - cpStart.getTime()) : null; const lb = selectedPlan?.lastBilling; const candidateFromLastBilling = (lb && endMs > lb.getTime()) ? (endMs - lb.getTime()) : null; const plausible = (ms) => Number.isFinite(ms) && ms > 3 * DAY && ms < 800 * DAY; if (plausible(candidateFromCurrentPeriod)) { CYCLE = candidateFromCurrentPeriod; } else if (plausible(candidateFromLastBilling)) { CYCLE = candidateFromLastBilling; } const alignedStart = new Date(endMs - CYCLE); const startCandidates = [ selectedPlan?.currentPeriodStart, selectedPlan?.lastBilling, periodStart ].filter(Boolean); let bestStart = null; for (const s of startCandidates) { const delta = endMs - s.getTime(); if (delta > 0 && delta <= CYCLE * 1.15) { bestStart = s; break; } } periodStart = bestStart || alignedStart; } if (renewAt && renewAt.getTime() <= now.getTime()) { const delta = now.getTime() - renewAt.getTime(); const steps = Math.ceil((delta + 1000) / CYCLE); renewAt = new Date(renewAt.getTime() + steps * CYCLE); } if (renewAt) { if (!periodStart) periodStart = new Date(renewAt.getTime() - CYCLE); if (periodStart.getTime() >= renewAt.getTime()) periodStart = new Date(renewAt.getTime() - CYCLE); } if (elRenewMeta) { elRenewMeta.textContent = (label === 'Free reset') ? (renewAt ? formatDateUTC(renewAt) : 'Renew date unavailable') : (renewAt ? formatDate(renewAt) : 'Renew date unavailable'); } const tick = () => { const t = new Date(); if (!renewAt) { setValueText(elRenewValue, '--:--:--:--'); if (elRenewSub) elRenewSub.textContent = label || 'No renewal date found.'; setGaugeProgress(elRenewRing, elRenewMarker, 0); return; } const diff = renewAt.getTime() - t.getTime(); setValueText(elRenewValue, formatCountdown4(diff)); if (elRenewSub) elRenewSub.textContent = label; const startMs = periodStart ? periodStart.getTime() : (renewAt.getTime() - CYCLE); const endMs = renewAt.getTime(); const total = Math.max(1, endMs - startMs); const elapsed = clamp(t.getTime() - startMs, 0, total); const pctElapsed = (elapsed / total) * 100; setGaugeProgress(elRenewRing, elRenewMarker, pctElapsed); }; tick(); renewTimer = setInterval(tick, 1000); } window.addEventListener('vici:ctxchange', () => { if (currentKey === 'dashboard') loadDashboard(); if (currentKey === 'api') loadApiFromMemberstack(); }); function lockAutofill(el) { if (!el) return; el.setAttribute('readonly', 'readonly'); const unlock = () => { el.removeAttribute('readonly'); el.removeEventListener('focus', unlock); el.removeEventListener('pointerdown', unlock); }; el.addEventListener('focus', unlock); el.addEventListener('pointerdown', unlock); } function scrubIfLooksLikeEmail(el) { if (!el) return; if (document.activeElement === el) return; const v = String(el.value || '').trim(); if (v.includes('@')) el.value = ''; } function installFieldUX() { $$('[data-copy]').forEach((btn) => { if (btn) btn.innerHTML = ICON_COPY; }); $$('[data-toggle-pass]').forEach((btn) => { if (btn) btn.innerHTML = ICON_EYE; }); root.addEventListener('click', (e) => { const btn = e.target.closest('[data-toggle-pass]'); if (!btn) return; const sel = btn.getAttribute('data-toggle-pass'); const scope = btn.closest('.va-panel') || root; const input = sel ? scope.querySelector(sel) : null; if (!input) return; const wasHidden = input.type === 'password'; input.type = wasHidden ? 'text' : 'password'; btn.innerHTML = wasHidden ? ICON_EYE_SLASH : ICON_EYE; btn.setAttribute('aria-label', wasHidden ? 'Hide value' : 'Show value'); }); async function copyText(text) { const t = String(text || ''); if (!t) return false; try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(t); return true; } } catch {} try { const ta = document.createElement('textarea'); ta.value = t; ta.setAttribute('readonly', ''); ta.style.position = 'fixed'; ta.style.left = '-9999px'; ta.style.top = '-9999px'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); return true; } catch { return false; } } root.addEventListener('click', async (e) => { const btn = e.target.closest('[data-copy]'); if (!btn) return; const sel = btn.getAttribute('data-copy'); const input = sel ? $(sel) : null; const ok = await copyText(input?.value || ''); if (ok) { btn.classList.add('is-copied'); setTimeout(() => btn.classList.remove('is-copied'), 650); toast('ok', 'Copied.'); } else { toast('err', 'Copy failed.'); } }); } const tabs = $$('.va-tab[data-tab]'); const panels = $$('.va-panel[data-panel]'); const panelIndex = (key) => tabs.findIndex(t => t.dataset.tab === key); let currentKey = 'dashboard'; function syncTeamBodyClass() { document.body.classList.toggle('va-on-team', currentKey === 'team'); } function cleanupPanelClasses(p) { p.classList.remove( 'is-enter-from-right', 'is-enter-from-left', 'is-leave-to-left', 'is-leave-to-right' ); } function setPanelInteractivity(panel, isActive) { if (!panel) return; panel.setAttribute('aria-hidden', isActive ? 'false' : 'true'); panel.style.pointerEvents = isActive ? '' : 'none'; if ('inert' in panel) { panel.inert = !isActive; } else { panel.querySelectorAll('a,button,input,select,textarea,[tabindex]').forEach(el => { if (!isActive) { if (!el.hasAttribute('data-va-prev-tabindex')) { el.setAttribute('data-va-prev-tabindex', el.getAttribute('tabindex') ?? ''); } el.setAttribute('tabindex', '-1'); } else { const prev = el.getAttribute('data-va-prev-tabindex'); if (prev !== null) { if (prev === '') el.removeAttribute('tabindex'); else el.setAttribute('tabindex', prev); el.removeAttribute('data-va-prev-tabindex'); } } }); } } function setTeamHostInteractivity(on) { const host = document.getElementById('va-team-host'); if (!host) return; host.style.pointerEvents = on ? '' : 'none'; } function setTab(nextKey, { pushHash = true } = {}) { const prevKey = currentKey; if (!nextKey || nextKey === currentKey) return; const curIdx = panelIndex(currentKey); const nextIdx = panelIndex(nextKey); const forward = nextIdx > curIdx; const nextTab = tabs.find(t => t.dataset.tab === nextKey); const curPanel = panels.find(p => p.dataset.panel === currentKey); const nextPanel = panels.find(p => p.dataset.panel === nextKey); if (!nextTab || !nextPanel) return; tabs.forEach(b => b.classList.toggle('is-active', b === nextTab)); if (curPanel) { setPanelInteractivity(curPanel, false); cleanupPanelClasses(curPanel); curPanel.classList.add(forward ? 'is-leave-to-left' : 'is-leave-to-right'); } cleanupPanelClasses(nextPanel); nextPanel.classList.add('is-active'); nextPanel.classList.add(forward ? 'is-enter-from-right' : 'is-enter-from-left'); setPanelInteractivity(nextPanel, true); requestAnimationFrame(() => { nextPanel.classList.remove('is-enter-from-right', 'is-enter-from-left'); }); const finalizeLeave = () => { if (!curPanel) return; curPanel.classList.remove('is-active'); cleanupPanelClasses(curPanel); }; let done = false; const onDone = (e) => { if (done) return; if (e && e.target !== curPanel) return; done = true; curPanel.removeEventListener('transitionend', onDone); finalizeLeave(); }; if (curPanel) { curPanel.addEventListener('transitionend', onDone); setTimeout(onDone, 450); } currentKey = nextKey; syncTeamBodyClass(); setTeamHostInteractivity(currentKey === 'team'); if (prevKey === 'team' && nextKey !== 'team') { killScrollLocks(); try { hardKillOverlays(); } catch {} } if (pushHash) { const next = `#${encodeURIComponent(nextKey)}`; if (window.location.hash !== next) history.replaceState(null, '', next); } if (nextKey === 'team') dockMemberstackTeam(); if (nextKey === 'api') loadApiFromMemberstack(); if (nextKey === 'dashboard') loadDashboard(); } let teamDocking = false; let overlayKiller = null; function killScrollLocks() { const b = document.body; const h = document.documentElement; b.style.overflow = ''; b.style.position = ''; b.style.top = ''; b.style.left = ''; b.style.right = ''; b.style.width = ''; b.style.paddingRight = ''; h.style.overflow = ''; h.style.position = ''; h.style.top = ''; h.style.left = ''; h.style.right = ''; h.style.width = ''; b.classList.remove('ms-modal-open', 'ms-modal--open', 'no-scroll', 'modal-open'); h.classList.remove('ms-modal-open', 'ms-modal--open', 'no-scroll', 'modal-open'); } function looksLikeFullscreenOverlay(el) { if (!el || el.nodeType !== 1) return false; const cs = getComputedStyle(el); if (cs.position !== 'fixed') return false; const rect = el.getBoundingClientRect(); const vw = window.innerWidth; const vh = window.innerHeight; const covers = rect.width >= vw * 0.90 && rect.height >= vh * 0.90 && rect.left <= 2 && rect.top <= 2; if (!covers) return false; const z = Number.parseInt(cs.zIndex || '0', 10); const zOk = Number.isFinite(z) ? z >= 1000 : true; const name = `${el.id || ''} ${el.className || ''}`.toLowerCase(); const nameOk = /overlay|backdrop|modal|ms-/.test(name); const bg = cs.backgroundColor; const hasBg = bg && bg !== 'transparent' && bg !== 'rgba(0, 0, 0, 0)'; const hasBackdrop = cs.backdropFilter && cs.backdropFilter !== 'none'; return zOk && (nameOk || hasBg || hasBackdrop); } function hardKillOverlays() { killScrollLocks(); const host = document.getElementById('va-team-host'); const modal = document.getElementById('ProfileModal') || document.querySelector('.ms-modal__content--profile')?.closest('.ms-modal') || null; const knownSelectors = [ '.ms-modal__overlay', '.ms-modal__backdrop', '.ms-modal__background', '.ms-overlay', '.ms-backdrop', '.ms-modal-overlay', '#ms-modal-overlay', '#ms-overlay', '[data-ms-modal-overlay]', '[data-ms-overlay]' ]; const targets = new Set(); knownSelectors.forEach(sel => { document.querySelectorAll(sel).forEach(el => { if (!el) return; if (modal && (el === modal || el.contains(modal))) return; targets.add(el); }); }); Array.from(document.body.children).forEach(el => { if (!el || el.nodeType !== 1) return; if (host && host.contains(el)) return; if (modal && (el === modal || el.contains(modal))) return; if (looksLikeFullscreenOverlay(el)) targets.add(el); }); targets.forEach(el => { try { el.style.setProperty('display', 'none', 'important'); el.style.setProperty('opacity', '0', 'important'); el.style.setProperty('visibility', 'hidden', 'important'); el.style.setProperty('pointer-events', 'none', 'important'); el.style.setProperty('background', 'transparent', 'important'); el.style.setProperty('backdrop-filter', 'none', 'important'); el.setAttribute('aria-hidden', 'true'); } catch {} }); if (modal && host && host.contains(modal)) { try { modal.style.setProperty('display', 'block', 'important'); modal.style.setProperty('visibility', 'visible', 'important'); modal.style.setProperty('opacity', '1', 'important'); modal.style.setProperty('pointer-events', 'auto', 'important'); modal.setAttribute('data-va-docked', '1'); } catch {} } } let overlayWatch = null; function kickOverlayWatch(ms = 1400) { const until = Date.now() + ms; if (overlayWatch) clearInterval(overlayWatch); overlayWatch = setInterval(() => { try { hardKillOverlays(); } catch {} if (Date.now() > until) { clearInterval(overlayWatch); overlayWatch = null; } }, 250); } function startOverlayKiller() { if (overlayKiller) clearInterval(overlayKiller); overlayKiller = setInterval(() => { if (currentKey !== 'team') return; hardKillOverlays(); }, 250); setTimeout(() => { if (overlayKiller) clearInterval(overlayKiller); overlayKiller = null; }, 2500); } function findProfileModal() { return document.getElementById('ProfileModal') || document.querySelector('.ms-modal__content--profile')?.closest('.ms-modal') || null; } function emitCtxChange() { try { window.dispatchEvent(new Event('vici:ctxchange')); } catch {} } function getApiStoredTokenId(json, ctx) { if (ctx?.scope === 'team' && ctx?.teamId) { return ( json?.vici?.api?.teams?.[ctx.teamId]?.token_id || '' ); } return ( json?.vici?.api?.user?.token_id || json?.vici?.api?.token_id || '' ); } async function dockMemberstackTeam() { const host = document.getElementById('va-team-host'); const trigger = document.getElementById('va-ms-team-trigger'); if (!host || !trigger) return; const already = host.querySelector('#ProfileModal') || host.querySelector('.ms-modal'); if (already) { hardKillOverlays(); kickOverlayWatch(900); return; } if (teamDocking) return; teamDocking = true; host.innerHTML = loadingInlineHTML('Loading team'); const prevStyle = trigger.getAttribute('style') || ''; trigger.style.display = 'block'; trigger.style.position = 'absolute'; trigger.style.left = '-9999px'; trigger.style.top = '-9999px'; trigger.style.width = '1px'; trigger.style.height = '1px'; trigger.style.opacity = '0'; startOverlayKiller(); kickOverlayWatch(2500); const clickTrigger = () => { try { trigger.click(); } catch {} }; clickTrigger(); const start = Date.now(); let attempts = 1; const poll = () => { if (currentKey !== 'team') { try { trigger.setAttribute('style', prevStyle); } catch {} teamDocking = false; return; } const modal = document.getElementById('ProfileModal') || document.querySelector('.ms-modal__content--profile')?.closest('.ms-modal') || null; const content = modal && modal.querySelector('.ms-modal__content--profile'); if (modal && content) { host.innerHTML = ''; host.appendChild(modal); hardKillOverlays(); try { trigger.setAttribute('style', prevStyle); } catch {} teamDocking = false; return; } const elapsed = Date.now() - start; if (attempts === 1 && elapsed > 1200) { attempts++; clickTrigger(); } if (attempts === 2 && elapsed > 2600) { attempts++; clickTrigger(); } if (elapsed > 9000) { host.innerHTML = `
Could not load Team UI.
`; hardKillOverlays(); try { trigger.setAttribute('style', prevStyle); } catch {} teamDocking = false; return; } requestAnimationFrame(poll); }; poll(); } function init() { installFieldUX(); [ document.getElementById('va-email-pass'), document.getElementById('va-api-pass'), document.getElementById('va-current-pass'), document.getElementById('va-new-pass') ].forEach((el) => { lockAutofill(el); scrubIfLooksLikeEmail(el); }); setTimeout(() => { ['va-email-pass','va-api-pass','va-current-pass','va-new-pass'].forEach((id) => { const el = document.getElementById(id); scrubIfLooksLikeEmail(el); }); }, 600); if (emailForm) { emailForm.addEventListener('submit', async (e) => { e.preventDefault(); const newEmail = (emailNewEl?.value || '').trim(); const currentPassword = (emailPassEl?.value || '').trim(); if (!newEmail) { toast('err', 'Enter a new email.'); return; } if (!currentPassword) { toast('err', 'Enter your current password.'); return; } setButtonLoading(emailUpdateBtn, true, 'Updating email'); try { await reauthWithPassword(currentPassword); const ms = await getMS(); if (!ms?.updateMemberAuth) throw new Error('Email update unavailable.'); await ms.updateMemberAuth({ email: newEmail }); if (emailCurrentEl) emailCurrentEl.value = newEmail; if (emailPassEl) emailPassEl.value = ''; toast('ok', 'Email updated.'); } catch (err) { toast('err', err?.message || 'Email update failed.'); } finally { setButtonLoading(emailUpdateBtn, false); } }); } if (genBtn) genBtn.addEventListener('click', generateKey); $$('#va-refresh-dashboard').forEach((b) => { if (b) b.addEventListener('click', loadDashboard); }); const nav = $('.va-nav'); if (nav) { nav.addEventListener('click', (e) => { const btn = e.target.closest('.va-tab[data-tab]'); if (!btn) return; setTab(btn.dataset.tab); }); } const hash = decodeURIComponent((window.location.hash || '').replace('#', '')).trim(); const initial = (hash && panels.some(p => p.dataset.panel === hash)) ? hash : 'dashboard'; currentKey = initial; syncTeamBodyClass(); tabs.forEach(b => b.classList.toggle('is-active', b.dataset.tab === initial)); panels.forEach(p => { const on = p.dataset.panel === initial; p.classList.toggle('is-active', on); p.setAttribute('aria-hidden', on ? 'false' : 'true'); }); if (initial === 'team') dockMemberstackTeam(); if (initial === 'api') loadApiFromMemberstack(); if (initial === 'dashboard') loadDashboard(); kickOverlayWatch(2500); } loadEmailPanel(); init(); })();