Add a popup button that scans the Topstep dashboard's GraphQL API (crystal.topstep.com) for passed evaluations that can still be activated into an Express Funded account. An eval is activatable when its status is "Passed" and its subscription id is not shared by ANY funded account it ever spawned — regardless of that funded account's current status, since activation is one-way. The result and its progress are mirrored to chrome.storage so the popup renders them independent of the message port (avoids the "message port closed" error on this ~20s scan). Activatable accounts link to their Topstep detail page. Adds https://*.topstep.com/* host permission; bumps to 0.15.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
113 lines
4.6 KiB
JavaScript
113 lines
4.6 KiB
JavaScript
const FIELDS = ['apiBase', 'pollSeconds', 'elementSelector'];
|
|
const DEFAULTS = {
|
|
apiBase: 'http://localhost:3000',
|
|
pollSeconds: 3,
|
|
elementSelector: '',
|
|
};
|
|
|
|
async function load() {
|
|
const cfg = { ...DEFAULTS, ...(await chrome.storage.local.get(FIELDS)) };
|
|
for (const f of FIELDS) document.getElementById(f).value = cfg[f];
|
|
render();
|
|
}
|
|
|
|
async function render() {
|
|
const { state = {} } = await chrome.storage.local.get('state');
|
|
|
|
// There is nothing to switch on: either it can reach the dashboard or it
|
|
// cannot. `waiting` below says why a tick captured nothing.
|
|
const color = state.connected ? '#22c55e' : '#ef4444';
|
|
const label = state.connected ? 'Capturing' : 'Disconnected';
|
|
|
|
const cap = state.lastCapture;
|
|
const detail = state.waiting
|
|
? `<div class="muted">${esc(state.waiting)}</div>`
|
|
: cap
|
|
? `<div class="muted">${esc(cap.title || cap.url)}<br>${(cap.bytes / 1024).toFixed(1)} KB · ${new Date(cap.at).toLocaleTimeString()}</div>`
|
|
: '';
|
|
|
|
document.getElementById('status').innerHTML =
|
|
`<span class="dot" style="background:${color}"></span><strong>${label}</strong>` +
|
|
detail +
|
|
(state.error ? `<div class="err">${esc(state.error)}</div>` : '');
|
|
}
|
|
|
|
document.getElementById('save').addEventListener('click', async () => {
|
|
const patch = {};
|
|
for (const f of FIELDS) patch[f] = document.getElementById(f).value.trim();
|
|
patch.pollSeconds = Number(patch.pollSeconds) || DEFAULTS.pollSeconds;
|
|
patch.apiBase = patch.apiBase || DEFAULTS.apiBase;
|
|
await chrome.storage.local.set(patch);
|
|
setTimeout(render, 400);
|
|
});
|
|
|
|
document.getElementById('now').addEventListener('click', () => {
|
|
chrome.runtime.sendMessage({ type: 'capture-now' }, () => setTimeout(render, 200));
|
|
});
|
|
|
|
document.getElementById('tsCheck').addEventListener('click', () => {
|
|
// Kick off the scan and return. The worker writes progress and the result to
|
|
// storage; renderTopstepState (below) reflects whatever is there, so the
|
|
// popup can be closed and reopened mid-scan without losing the answer. The
|
|
// callback only acknowledges the request was received — it carries no result.
|
|
chrome.runtime.sendMessage({ type: 'topstep-check' }, () => void chrome.runtime.lastError);
|
|
});
|
|
|
|
/** Paint the Topstep panel from the stored scan state. */
|
|
function renderTopstepState(state) {
|
|
const btn = document.getElementById('tsCheck');
|
|
const out = document.getElementById('tsResult');
|
|
if (!state) { out.innerHTML = ''; return; }
|
|
|
|
if (state.running) {
|
|
btn.disabled = true;
|
|
btn.textContent = 'Checking…';
|
|
out.innerHTML = '<div class="muted">Scanning the Topstep dashboard…</div>';
|
|
return;
|
|
}
|
|
|
|
btn.disabled = false;
|
|
btn.textContent = 'Check activatable accounts';
|
|
|
|
if (state.error) {
|
|
out.innerHTML = `<div class="err">${esc(state.error)}</div>`;
|
|
return;
|
|
}
|
|
const r = state.result;
|
|
if (!r) { out.innerHTML = ''; return; }
|
|
if (r.signedOut) {
|
|
out.innerHTML = '<div class="err">Not signed in to Topstep. Open the dashboard, sign in, then check again.</div>';
|
|
return;
|
|
}
|
|
if (r.error) {
|
|
out.innerHTML = `<div class="err">${esc(r.error)}</div>`;
|
|
return;
|
|
}
|
|
|
|
const n = r.activatableCount || 0;
|
|
const acctUrl = (id) => `https://dashboard.topstep.com/dashboard/accounts/${encodeURIComponent(id)}`;
|
|
const list = (r.activatable || [])
|
|
.map((a) => `<li><a href="${acctUrl(a.id)}" target="_blank" rel="noopener">${esc(a.name)}</a>${a.type ? ` · ${esc(a.type)}` : ''}</li>`)
|
|
.join('');
|
|
const when = r.checkedAt ? ` · ${new Date(r.checkedAt).toLocaleTimeString()}` : '';
|
|
out.innerHTML =
|
|
`<div class="big">${n} activatable</div>` +
|
|
`<div class="muted">${r.alreadyActivated || 0} already activated · ${r.passedTotal || 0} passed total${when}</div>` +
|
|
(list ? `<ul>${list}</ul>` : '<div class="muted" style="margin-top:6px">No passed accounts waiting on activation.</div>');
|
|
}
|
|
|
|
// Render whatever the last scan left behind when the popup opens…
|
|
chrome.storage.local.get('topstepScan').then(({ topstepScan }) => renderTopstepState(topstepScan));
|
|
// …and keep up with a scan that finishes while the popup is open.
|
|
chrome.storage.onChanged.addListener((c, area) => {
|
|
if (area === 'local' && 'topstepScan' in c) renderTopstepState(c.topstepScan.newValue);
|
|
});
|
|
|
|
chrome.storage.onChanged.addListener((c, area) => { if (area === 'local' && 'state' in c) render(); });
|
|
|
|
function esc(s) {
|
|
return String(s).replace(/[&<>"]/g, (ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[ch]));
|
|
}
|
|
|
|
load();
|