Let the master dashboard drive this instance; drop the AutoBuyer switch
Three changes that ride together because they touch the same surface. A control link to the master. lib/master-link.ts dials out to <master>/ws/instance and holds it open, so the dashboard can trigger a trade or queue an AutoBuyer run here. Commands call the same functions the local pages call — runTrade, startScheduler, queueRun — so nothing reaches the broker by a path the UI does not also use. Reports prefer the socket and fall back to the existing POST, which keeps older masters working unchanged. startMasterLink is idempotent, and that matters more than it looks: getClients() is lazy and starts the link, so the first command re-enters it from inside a command the live socket is waiting to ack. Tearing the socket down there loses the ack and reports a trade that actually executed as a failure — which invites pressing the button twice. queueRun is lifted out of the automations route so the socket and HTTP share one copy of the input clamping that decides how many times a purchase fires. Page capture loses its switch and gains a scope. It now runs every tick, but only against the active tab of the last-focused window that matches host_permissions — so it follows the page in front of you and stops when you switch away, where before it would fall back to any firm tab left open in the background. autobuyer_enabled is gone entirely. The AutoBuyer is ready whenever the runner is; a run happens because someone asked for one. That does mean a queued run executes as soon as clicker/runner.py claims it, with no second gate — stopping the runner is now the way to make a machine idle. Extension to 0.10.0; it needs a manual reload at chrome://extensions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0ba27161ce
commit
25289cb9b3
+37
-28
@@ -1,8 +1,15 @@
|
||||
/* AutoFirmer Capture — MV3 service worker.
|
||||
*
|
||||
* Loop: poll GET /api/autobuyer/status. While `enabled` is true, scrape the
|
||||
* target tab's HTML and POST it to /api/autobuyer/capture. The dashboard's
|
||||
* AutoBuyer page renders whatever landed there.
|
||||
* Loop: every tick, scrape the tab you are looking at and POST it to
|
||||
* /api/autobuyer/capture. The dashboard's AutoBuyer page renders whatever
|
||||
* landed there.
|
||||
*
|
||||
* Nothing here has a switch. Capture is scoped instead: only the ACTIVE tab of
|
||||
* the last-focused window, and only when that tab is one of the firm sites in
|
||||
* host_permissions. Sit on any other page and nothing is read or sent.
|
||||
*
|
||||
* The locate queue is served whenever clicks are waiting. The AutoBuyer is
|
||||
* ready at all times; a run happens because someone asked for one.
|
||||
*/
|
||||
|
||||
const DEFAULTS = {
|
||||
@@ -31,9 +38,11 @@ async function setState(patch) {
|
||||
await chrome.storage.local.set({ state: { ...(prev || {}), ...patch, updatedAt: Date.now() } });
|
||||
}
|
||||
|
||||
function setBadge(connected, enabled) {
|
||||
chrome.action.setBadgeText({ text: !connected ? '!' : enabled ? 'ON' : '' });
|
||||
chrome.action.setBadgeBackgroundColor({ color: !connected ? '#ef4444' : '#22c55e' });
|
||||
/** `!` when the dashboard is unreachable, blank otherwise. A blank badge is the
|
||||
* working state — there is nothing to switch on, so nothing to advertise. */
|
||||
function setBadge(connected) {
|
||||
chrome.action.setBadgeText({ text: connected ? '' : '!' });
|
||||
chrome.action.setBadgeBackgroundColor({ color: '#ef4444' });
|
||||
}
|
||||
|
||||
// ── The injected scraper ────────────────────────────────────────────────────
|
||||
@@ -106,20 +115,26 @@ function firmPatterns() {
|
||||
.filter((p) => !/\/\/(localhost|127\.0\.0\.1)/.test(p));
|
||||
}
|
||||
|
||||
async function pickTab(cfg) {
|
||||
const tabs = (await chrome.tabs.query({ url: firmPatterns() }))
|
||||
.filter((t) => /^https?:/.test(t.url || ''));
|
||||
if (tabs.length === 0) return undefined;
|
||||
|
||||
// Prefer the one being looked at, so with two firms open the capture follows
|
||||
// attention rather than tab order.
|
||||
return tabs.find((t) => t.active) ?? tabs[0];
|
||||
/** The firm tab you are actually looking at, or nothing.
|
||||
*
|
||||
* Deliberately no fallback to "some other firm tab that happens to be open":
|
||||
* capture should report the page in front of you, not a stale background tab
|
||||
* you left on a broker site hours ago. Chrome does the pattern matching, so a
|
||||
* tab outside host_permissions cannot be selected here at all.
|
||||
*/
|
||||
async function pickTab() {
|
||||
const [tab] = await chrome.tabs.query({
|
||||
active: true,
|
||||
lastFocusedWindow: true,
|
||||
url: firmPatterns(),
|
||||
});
|
||||
return tab && /^https?:/.test(tab.url || '') ? tab : undefined;
|
||||
}
|
||||
|
||||
async function captureAndSend(cfg) {
|
||||
const tab = await pickTab(cfg);
|
||||
const tab = await pickTab();
|
||||
if (!tab) {
|
||||
await setState({ connected: true, enabled: true, error: null, waiting: 'No eligible tab to capture' });
|
||||
await setState({ connected: true, error: null, waiting: 'Not on a firm page — nothing captured' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -147,7 +162,6 @@ async function captureAndSend(cfg) {
|
||||
|
||||
await setState({
|
||||
connected: true,
|
||||
enabled: true,
|
||||
error: null,
|
||||
waiting: null,
|
||||
lastCapture: { url: result.url, title: result.title, bytes: result.html.length, at: Date.now() },
|
||||
@@ -454,24 +468,19 @@ async function serveLocateRequest(cfg) {
|
||||
async function tick() {
|
||||
const cfg = await getConfig();
|
||||
|
||||
let enabled;
|
||||
try {
|
||||
const res = await fetch(`${cfg.apiBase}/api/autobuyer/status`, { cache: 'no-store' });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const status = await res.json();
|
||||
enabled = !!status.enabled;
|
||||
pendingLocate = Number(status.pendingLocate) || 0;
|
||||
} catch (err) {
|
||||
setBadge(false, false);
|
||||
await setState({ connected: false, enabled: false, error: `Cannot reach ${cfg.apiBase} — ${err.message}` });
|
||||
setBadge(false);
|
||||
await setState({ connected: false, error: `Cannot reach ${cfg.apiBase} — ${err.message}` });
|
||||
return;
|
||||
}
|
||||
|
||||
setBadge(true, enabled);
|
||||
if (!enabled) {
|
||||
await setState({ connected: true, enabled: false, error: null, waiting: null });
|
||||
return;
|
||||
}
|
||||
setBadge(true);
|
||||
await setState({ connected: true });
|
||||
|
||||
// Serve queued clicks before capturing — the Python side is waiting on these,
|
||||
// and a capture of a megabyte page shouldn't sit in front of them.
|
||||
@@ -482,13 +491,13 @@ async function tick() {
|
||||
if (!(await serveLocateRequest(cfg))) break;
|
||||
}
|
||||
} catch (err) {
|
||||
await setState({ connected: true, enabled: true, error: `Locate queue: ${err.message}` });
|
||||
await setState({ connected: true, error: `Locate queue: ${err.message}` });
|
||||
}
|
||||
|
||||
try {
|
||||
await captureAndSend(cfg);
|
||||
} catch (err) {
|
||||
await setState({ connected: true, enabled: true, error: err.message });
|
||||
await setState({ connected: true, error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user