(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 `
${ICON_SPINNER}
${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 = `${ICON_SPINNER}`;
}
}
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();
})();