// Automations live inside the firm whose site they drive. The dashboard renders // a tab per firm and a button per automation within it; the Python runner claims // a queued run, resolves each selector through the extension, and drives the real // mouse. // // Adding a button means adding an entry to that firm's `automations` — nothing in // the page, the API or the runner changes. // // Adding a NEW FIRM also needs its host added to extension/manifest.json // host_permissions, and the extension reloaded. Without that the extension is not // permitted to read that site and every step fails to locate. export type AutomationStep = | { action: 'click'; selector: string; index?: number; urlPattern?: string; label?: string } | { action: 'type'; selector: string; text: string; index?: number; clear?: boolean; urlPattern?: string; label?: string } | { action: 'wait'; seconds: number; label?: string } // Point the firm's tab at a page. Omit `url` to use the firm's own. Skipped // when the tab is already there, so it doesn't reload and lose page state. | { action: 'navigate'; url?: string; label?: string }; export interface Automation { id: string; label: string; description: string; /** Shown as a confirmation before the run. Set it on anything that spends money. */ confirm?: string; steps: AutomationStep[]; } /** A step as handed to the runner: the firm's tab pattern and fallback URL * filled in, so the runner never has to know which firm it is working on. */ export type ResolvedStep = AutomationStep & { urlPattern?: string; openUrl?: string; }; export interface Firm { id: string; label: string; /** Chrome match pattern for this firm's tab. Steps inherit it unless they set * their own, which keeps one firm's automation from acting on another's tab. */ urlPattern: string; /** Concrete page to open when no tab matches `urlPattern`. A match pattern * can't be navigated to, so this has to be spelled out separately. */ url: string; automations: Automation[]; } export const FIRMS: Firm[] = [ { id: 'tradeify', label: 'Tradeify', urlPattern: 'https://app-f.tradeify.co/*', url: 'https://app-f.tradeify.co/', automations: [ { id: 'buy-accounts', label: 'Buy Accounts', description: 'Opens the Add Account flow.', confirm: 'This drives the real mouse against Tradeify and can spend money. Continue?', steps: [ // The tab is routinely left on another Tradeify page (/the-circuit, // an account view). The Add Account link only exists on the // dashboard, so go there first rather than assuming. { action: 'navigate', label: 'Open the Tradeify dashboard' }, // `a.add_account_btn` is the authored class on the Add Account // link — confirmed against a real capture, matchCount 1. The MUI // hash classes on the same element (mui-*) are regenerated on // every site build, so they are not safe to select on. { action: 'click', selector: 'a.add_account_btn', label: 'Open Add Account' }, { action: 'wait', seconds: 2, label: 'Wait for the form' }, // TODO: the rest of the purchase flow. Confirm each selector with // `clicker.py locate` before adding it here. ], }, ], }, ]; /** Runs store one string, so it has to identify the automation globally — and * every firm will plausibly have its own "buy-accounts". Hence firm:automation * rather than the bare id. */ export function automationKey(firmId: string, automationId: string): string { return `${firmId}:${automationId}`; } export function getFirm(id: string): Firm | undefined { return FIRMS.find((f) => f.id === id); } export function findAutomation(key: string): { firm: Firm; automation: Automation } | undefined { for (const firm of FIRMS) { for (const automation of firm.automations) { if (automationKey(firm.id, automation.id) === key) return { firm, automation }; } } return undefined; } /** Fill in the firm's tab pattern for any step that didn't name one, so the * runner never has to know which firm it is working on. */ export function resolveSteps(firm: Firm, automation: Automation): ResolvedStep[] { return automation.steps.map((step) => { if (step.action === 'wait') return step; if (step.action === 'navigate') { return { ...step, url: step.url ?? firm.url, urlPattern: firm.urlPattern, openUrl: firm.url }; } return { ...step, urlPattern: step.urlPattern ?? firm.urlPattern, openUrl: firm.url }; }); } /** What a step is doing, for the run log and the dashboard. */ export function describeStep(step: AutomationStep): string { if (step.label) return step.label; switch (step.action) { case 'click': return `click ${step.selector}`; case 'type': return `type into ${step.selector}`; case 'wait': return `wait ${step.seconds}s`; case 'navigate': return `open ${step.url ?? 'the firm page'}`; } }