diff --git a/src/dashboard/BotController.ts b/src/dashboard/BotController.ts index e312611..1728c62 100644 --- a/src/dashboard/BotController.ts +++ b/src/dashboard/BotController.ts @@ -1,5 +1,6 @@ import { EventEmitter } from 'events'; import { logger } from '../util/logger.js'; +import type { LinkMode } from '../types.js'; import type { ConfigStore, SavedLink } from './ConfigStore.js'; export interface TradeLink { @@ -8,6 +9,7 @@ export interface TradeLink { name: string; label: string; active: boolean; + mode: LinkMode; addedAt: string; } @@ -25,6 +27,11 @@ export interface BotStatus { waitForMoreItemsMs: number; betweenTradesDelayMs: number; }; + inventory?: { + grid: boolean[][]; + items: { row: number; col: number; w: number; h: number }[]; + free: number; + }; } export class BotController extends EventEmitter { @@ -35,6 +42,7 @@ export class BotController extends EventEmitter { private tradesFailed = 0; private startTime = Date.now(); private store: ConfigStore; + private _inventory: BotStatus['inventory'] = undefined; constructor(store: ConfigStore) { super(); @@ -69,7 +77,7 @@ export class BotController extends EventEmitter { this.emit('resumed'); } - addLink(url: string, name: string = ''): TradeLink { + addLink(url: string, name: string = '', mode?: LinkMode): TradeLink { url = this.stripLive(url); const id = this.extractId(url); const label = this.extractLabel(url); @@ -81,11 +89,12 @@ export class BotController extends EventEmitter { name: name || savedLink?.name || '', label, active: savedLink?.active !== undefined ? savedLink.active : true, + mode: mode || savedLink?.mode || 'live', addedAt: new Date().toISOString(), }; this.links.set(id, link); - this.store.addLink(url, link.name); - logger.info({ id, url, name: link.name, active: link.active }, 'Trade link added'); + this.store.addLink(url, link.name, link.mode); + logger.info({ id, url, name: link.name, active: link.active, mode: link.mode }, 'Trade link added'); this.emit('link-added', link); return link; } @@ -118,6 +127,15 @@ export class BotController extends EventEmitter { this.store.updateLinkById(id, { name }); } + updateLinkMode(id: string, mode: LinkMode): void { + const link = this.links.get(id); + if (!link) return; + link.mode = mode; + this.store.updateLinkById(id, { mode }); + logger.info({ id, mode }, 'Trade link mode updated'); + this.emit('link-mode-changed', { id, mode, link }); + } + isLinkActive(searchId: string): boolean { const link = this.links.get(searchId); return link ? link.active : false; @@ -153,9 +171,14 @@ export class BotController extends EventEmitter { waitForMoreItemsMs: s.waitForMoreItemsMs, betweenTradesDelayMs: s.betweenTradesDelayMs, }, + inventory: this._inventory, }; } + setInventory(inv: BotStatus['inventory']): void { + this._inventory = inv; + } + getStore(): ConfigStore { return this.store; } diff --git a/src/dashboard/ConfigStore.ts b/src/dashboard/ConfigStore.ts index a9afb98..1e6cc00 100644 --- a/src/dashboard/ConfigStore.ts +++ b/src/dashboard/ConfigStore.ts @@ -1,11 +1,13 @@ import { readFileSync, writeFileSync, existsSync } from 'fs'; import path from 'path'; import { logger } from '../util/logger.js'; +import type { LinkMode } from '../types.js'; export interface SavedLink { url: string; name: string; active: boolean; + mode: LinkMode; addedAt: string; } @@ -55,10 +57,11 @@ export class ConfigStore { const parsed = JSON.parse(raw) as Partial; const merged = { ...DEFAULTS, ...parsed }; // Migrate old links: add name/active fields, strip /live from URLs - merged.links = merged.links.map((l) => ({ + merged.links = merged.links.map((l: any) => ({ url: l.url.replace(/\/live\/?$/, ''), name: l.name || '', active: l.active !== undefined ? l.active : true, + mode: l.mode || 'live', addedAt: l.addedAt || new Date().toISOString(), })); logger.info({ path: this.filePath, linkCount: merged.links.length }, 'Loaded config.json'); @@ -85,10 +88,10 @@ export class ConfigStore { return this.data.links; } - addLink(url: string, name: string = ''): void { + addLink(url: string, name: string = '', mode: LinkMode = 'live'): void { url = url.replace(/\/live\/?$/, ''); if (this.data.links.some((l) => l.url === url)) return; - this.data.links.push({ url, name, active: true, addedAt: new Date().toISOString() }); + this.data.links.push({ url, name, active: true, mode, addedAt: new Date().toISOString() }); this.save(); } @@ -105,7 +108,7 @@ export class ConfigStore { this.save(); } - updateLinkById(id: string, updates: { name?: string; active?: boolean }): SavedLink | null { + updateLinkById(id: string, updates: { name?: string; active?: boolean; mode?: LinkMode }): SavedLink | null { const link = this.data.links.find((l) => { const parts = l.url.split('/'); return parts[parts.length - 1] === id; @@ -113,6 +116,7 @@ export class ConfigStore { if (!link) return null; if (updates.name !== undefined) link.name = updates.name; if (updates.active !== undefined) link.active = updates.active; + if (updates.mode !== undefined) link.mode = updates.mode; this.save(); return link; } diff --git a/src/dashboard/DashboardServer.ts b/src/dashboard/DashboardServer.ts index 82e1a4f..39f2c27 100644 --- a/src/dashboard/DashboardServer.ts +++ b/src/dashboard/DashboardServer.ts @@ -8,6 +8,7 @@ import { logger } from '../util/logger.js'; import { sleep } from '../util/sleep.js'; import type { BotController } from './BotController.js'; import type { ScreenReader } from '../game/ScreenReader.js'; +import type { OcrEngine } from '../game/OcrDaemon.js'; import { GRID_LAYOUTS } from '../game/GridReader.js'; import type { GameController } from '../game/GameController.js'; @@ -54,12 +55,13 @@ export class DashboardServer { // Links CRUD this.app.post('/api/links', (req, res) => { - const { url, name } = req.body as { url: string; name?: string }; + const { url, name, mode } = req.body as { url: string; name?: string; mode?: string }; if (!url || !url.includes('pathofexile.com/trade')) { res.status(400).json({ error: 'Invalid trade URL' }); return; } - this.bot.addLink(url, name || ''); + const linkMode = mode === 'scrap' ? 'scrap' : 'live'; + this.bot.addLink(url, name || '', linkMode); this.broadcastStatus(); res.json({ ok: true }); }); @@ -86,6 +88,18 @@ export class DashboardServer { res.json({ ok: true }); }); + // Change link mode + this.app.post('/api/links/:id/mode', (req, res) => { + const { mode } = req.body as { mode: string }; + if (mode !== 'live' && mode !== 'scrap') { + res.status(400).json({ error: 'Invalid mode. Must be "live" or "scrap".' }); + return; + } + this.bot.updateLinkMode(req.params.id, mode); + this.broadcastStatus(); + res.json({ ok: true }); + }); + // Settings this.app.post('/api/settings', (req, res) => { const updates = req.body as Record; @@ -108,11 +122,29 @@ export class DashboardServer { } }); + // OCR engine selection + this.app.get('/api/debug/ocr-engine', (_req, res) => { + if (!this.debug) { res.status(503).json({ error: 'Debug not available' }); return; } + res.json({ ok: true, engine: this.debug.screenReader.debugOcrEngine }); + }); + + this.app.post('/api/debug/ocr-engine', (req, res) => { + if (!this.debug) { res.status(503).json({ error: 'Debug not available' }); return; } + const { engine } = req.body as { engine: string }; + if (!['tesseract', 'easyocr'].includes(engine)) { + res.status(400).json({ error: 'Invalid engine. Must be tesseract or easyocr.' }); + return; + } + this.debug.screenReader.debugOcrEngine = engine as OcrEngine; + this.broadcastLog('info', `OCR engine set to: ${engine}`); + res.json({ ok: true }); + }); + this.app.post('/api/debug/ocr', async (_req, res) => { if (!this.debug) { res.status(503).json({ error: 'Debug not available' }); return; } try { - const text = await this.debug.screenReader.readFullScreen(); - this.broadcastLog('info', `OCR result (${text.length} chars): ${text.substring(0, 200)}`); + const text = await this.debug.screenReader.debugReadFullScreen(); + this.broadcastLog('info', `OCR [${this.debug.screenReader.debugOcrEngine}] (${text.length} chars): ${text.substring(0, 200)}`); res.json({ ok: true, text }); } catch (err) { logger.error({ err }, 'Debug OCR failed'); @@ -125,11 +157,11 @@ export class DashboardServer { const { text } = req.body as { text: string }; if (!text) { res.status(400).json({ error: 'Missing text parameter' }); return; } try { - const pos = await this.debug.screenReader.findTextOnScreen(text); + const pos = await this.debug.screenReader.debugFindTextOnScreen(text); if (pos) { - this.broadcastLog('info', `Found "${text}" at (${pos.x}, ${pos.y})`); + this.broadcastLog('info', `Found "${text}" at (${pos.x}, ${pos.y}) [${this.debug.screenReader.debugOcrEngine}]`); } else { - this.broadcastLog('warn', `"${text}" not found on screen`); + this.broadcastLog('warn', `"${text}" not found on screen [${this.debug.screenReader.debugOcrEngine}]`); } res.json({ ok: true, found: !!pos, position: pos }); } catch (err) { @@ -233,17 +265,17 @@ export class DashboardServer { this.app.post('/api/debug/find-and-click', async (req, res) => { if (!this.debug) { res.status(503).json({ error: 'Debug not available' }); return; } - const { text } = req.body as { text: string }; + const { text, fuzzy } = req.body as { text: string; fuzzy?: boolean }; if (!text) { res.status(400).json({ error: 'Missing text parameter' }); return; } try { - const pos = await this.debug.screenReader.findTextOnScreen(text); + const pos = await this.debug.screenReader.debugFindTextOnScreen(text, !!fuzzy); if (pos) { await this.debug.gameController.focusGame(); await this.debug.gameController.leftClickAt(pos.x, pos.y); - this.broadcastLog('info', `Found "${text}" at (${pos.x}, ${pos.y}) and clicked`); + this.broadcastLog('info', `Found "${text}" at (${pos.x}, ${pos.y}) and clicked [${this.debug.screenReader.debugOcrEngine}]`); res.json({ ok: true, found: true, position: pos }); } else { - this.broadcastLog('warn', `"${text}" not found on screen`); + this.broadcastLog('warn', `"${text}" not found on screen [${this.debug.screenReader.debugOcrEngine}]`); res.json({ ok: true, found: false, position: null }); } } catch (err) { diff --git a/src/dashboard/index.html b/src/dashboard/index.html index 138125e..986992f 100644 --- a/src/dashboard/index.html +++ b/src/dashboard/index.html @@ -129,6 +129,34 @@ .link-item button { padding: 4px 12px; font-size: 12px; } .link-item.inactive { opacity: 0.5; } + .mode-badge { + display: inline-block; + font-size: 10px; + padding: 2px 8px; + border-radius: 4px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: pointer; + user-select: none; + transition: background 0.15s; + } + .mode-badge.live { background: #1f6feb; color: #fff; } + .mode-badge.live:hover { background: #388bfd; } + .mode-badge.scrap { background: #9e6a03; color: #fff; } + .mode-badge.scrap:hover { background: #d29922; } + + .mode-select { + padding: 6px 10px; + background: #0d1117; + border: 1px solid #30363d; + border-radius: 6px; + color: #e6edf3; + font-size: 13px; + outline: none; + } + .mode-select:focus { border-color: #58a6ff; } + /* Toggle switch */ .toggle { position: relative; width: 36px; height: 20px; cursor: pointer; flex-shrink: 0; } .toggle input { opacity: 0; width: 0; height: 0; } @@ -316,6 +344,41 @@ } .detect-badge.ok { background: #238636; color: #fff; } .detect-badge.fallback { background: #9e6a03; color: #fff; } + + /* Inventory grid */ + .inv-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; + } + .inv-free { + font-size: 12px; + color: #8b949e; + font-weight: 600; + } + .inventory-grid { + display: grid; + grid-template-columns: repeat(12, 1fr); + gap: 2px; + background: #161b22; + border: 1px solid #30363d; + border-radius: 8px; + padding: 10px; + } + .inv-cell { + aspect-ratio: 1; + border-radius: 3px; + background: #0d1117; + min-width: 0; + } + .inv-cell.occupied { + background: #238636; + } + .inv-cell.item-top { border-top: 2px solid #3fb950; } + .inv-cell.item-bottom { border-bottom: 2px solid #3fb950; } + .inv-cell.item-left { border-left: 2px solid #3fb950; } + .inv-cell.item-right { border-right: 2px solid #3fb950; } @@ -359,11 +422,25 @@ +
+
+
Inventory
+ +
+
+
No active scrap session
+
+
+
Trade Links