poe2-bot/src-old/server/index.html
2026-02-13 01:12:11 -05:00

1341 lines
49 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>POE2 Trade Bot</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: #0d1117;
color: #e6edf3;
min-height: 100vh;
}
.container { max-width: 900px; margin: 0 auto; padding: 20px; }
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
border-bottom: 1px solid #30363d;
margin-bottom: 20px;
}
header h1 { font-size: 20px; font-weight: 600; }
.header-right { display: flex; align-items: center; gap: 14px; }
.settings-btn {
background: none; border: none; cursor: pointer; padding: 4px;
color: #8b949e; transition: color 0.15s; line-height: 1;
}
.settings-btn:hover { background: none; color: #e6edf3; }
.settings-btn svg { display: block; }
.status-dot {
width: 10px; height: 10px; border-radius: 50%;
display: inline-block; margin-right: 8px;
}
.status-dot.running { background: #3fb950; box-shadow: 0 0 6px #3fb950; }
.status-dot.paused { background: #d29922; box-shadow: 0 0 6px #d29922; }
.status-dot.idle { background: #8b949e; }
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.stat-card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
padding: 14px;
text-align: center;
}
.stat-card .value { font-size: 24px; font-weight: 700; color: #58a6ff; }
.stat-card .label { font-size: 11px; color: #8b949e; text-transform: uppercase; margin-top: 4px; }
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
button {
padding: 8px 20px;
border: 1px solid #30363d;
border-radius: 6px;
background: #21262d;
color: #e6edf3;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: background 0.15s;
}
button:hover { background: #30363d; }
button.primary { background: #238636; border-color: #2ea043; }
button.primary:hover { background: #2ea043; }
button.danger { background: #da3633; border-color: #f85149; }
button.danger:hover { background: #f85149; }
button.warning { background: #9e6a03; border-color: #d29922; }
button.warning:hover { background: #d29922; }
.section { margin-bottom: 20px; }
.section-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
color: #8b949e;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.add-link {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.add-link input {
flex: 1;
padding: 8px 12px;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
color: #e6edf3;
font-size: 13px;
outline: none;
}
.add-link input:focus { border-color: #58a6ff; }
.add-link input::placeholder { color: #484f58; }
.links-list { display: flex; flex-direction: column; gap: 6px; }
.link-item {
display: flex;
align-items: center;
justify-content: space-between;
background: #161b22;
border: 1px solid #30363d;
border-radius: 6px;
padding: 10px 14px;
}
.link-left { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; }
.link-info { flex: 1; min-width: 0; }
.link-name { font-size: 13px; font-weight: 600; color: #e6edf3; }
.link-label { font-size: 12px; color: #8b949e; }
.link-url {
font-size: 11px; color: #484f58;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
max-width: 600px;
}
.link-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.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; }
.toggle .slider {
position: absolute; inset: 0; background: #30363d;
border-radius: 20px; transition: background 0.2s;
}
.toggle .slider::before {
content: ''; position: absolute; left: 2px; top: 2px;
width: 16px; height: 16px; background: #8b949e;
border-radius: 50%; transition: transform 0.2s, background 0.2s;
}
.toggle input:checked + .slider { background: #238636; }
.toggle input:checked + .slider::before { transform: translateX(16px); background: #fff; }
.log-panel {
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
height: 280px;
overflow-y: auto;
font-family: 'Cascadia Code', 'Fira Code', monospace;
font-size: 12px;
padding: 10px;
}
.log-line { padding: 2px 0; line-height: 1.5; }
.log-line .time { color: #484f58; }
.log-line.info .msg { color: #58a6ff; }
.log-line.warn .msg { color: #d29922; }
.log-line.error .msg { color: #f85149; }
.log-line.debug .msg { color: #8b949e; }
.empty-state {
color: #484f58;
text-align: center;
padding: 30px;
font-size: 13px;
}
.settings-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.settings-grid.full { grid-template-columns: 1fr; }
.setting-row {
display: flex;
flex-direction: column;
gap: 4px;
}
.setting-row label {
font-size: 11px;
color: #8b949e;
text-transform: uppercase;
}
.setting-row input {
padding: 6px 10px;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
color: #e6edf3;
font-size: 13px;
outline: none;
}
.setting-row input:focus { border-color: #58a6ff; }
.settings-actions {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.saved-badge {
font-size: 12px;
color: #3fb950;
margin-right: 12px;
line-height: 32px;
opacity: 0;
transition: opacity 0.3s;
}
.saved-badge.show { opacity: 1; }
/* Modal overlay */
.modal-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 100;
justify-content: center;
align-items: center;
}
.modal-overlay.open { display: flex; }
.modal {
background: #161b22;
border: 1px solid #30363d;
border-radius: 10px;
width: 480px;
max-width: 90vw;
max-height: 85vh;
overflow-y: auto;
padding: 24px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.modal-header h2 { font-size: 16px; font-weight: 600; }
.modal-close {
background: none; border: none; color: #8b949e;
cursor: pointer; padding: 4px; font-size: 18px; line-height: 1;
}
.modal-close:hover { background: none; color: #e6edf3; }
.debug-panel {
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
padding: 14px;
}
.debug-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
align-items: center;
}
.debug-row:last-of-type { margin-bottom: 0; }
.debug-row input {
padding: 6px 10px;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
color: #e6edf3;
font-size: 13px;
outline: none;
}
.debug-row input:focus { border-color: #58a6ff; }
.debug-row input[type="text"] { flex: 1; }
.debug-result {
margin-top: 8px;
font-family: 'Cascadia Code', 'Fira Code', monospace;
font-size: 12px;
color: #8b949e;
white-space: pre-wrap;
}
.debug-result:empty { display: none; }
.grid-debug {
display: flex;
gap: 8px;
margin-top: 8px;
align-items: flex-start;
}
.grid-debug img {
border: 1px solid #30363d;
border-radius: 4px;
max-width: 280px;
max-height: 280px;
object-fit: contain;
}
.grid-view {
display: inline-grid;
gap: 1px;
background: #30363d;
border: 1px solid #30363d;
border-radius: 4px;
overflow: hidden;
flex-shrink: 0;
}
.grid-cell {
width: 12px; height: 12px;
background: #0d1117;
}
.grid-cell.occupied {
background: #238636;
}
.detect-badge {
display: inline-block;
font-size: 10px;
padding: 1px 6px;
border-radius: 4px;
margin-left: 6px;
font-weight: 600;
}
.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; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>POE2 Trade Bot</h1>
<div class="header-right">
<div id="statusBadge">
<span class="status-dot idle"></span>
<span id="statusText">Connecting...</span>
</div>
<button class="settings-btn" onclick="openSettings()" title="Settings">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="10" cy="10" r="3"/>
<path d="M10 1.5v2M10 16.5v2M3.4 3.4l1.4 1.4M15.2 15.2l1.4 1.4M1.5 10h2M16.5 10h2M3.4 16.6l1.4-1.4M15.2 4.8l1.4-1.4"/>
</svg>
</button>
</div>
</header>
<div class="stats">
<div class="stat-card">
<div class="value" id="stateValue">IDLE</div>
<div class="label">State</div>
</div>
<div class="stat-card">
<div class="value" id="linksValue">0</div>
<div class="label">Active Links</div>
</div>
<div class="stat-card">
<div class="value" id="completedValue">0</div>
<div class="label">Trades Done</div>
</div>
<div class="stat-card">
<div class="value" id="failedValue">0</div>
<div class="label">Failed</div>
</div>
</div>
<div class="controls" id="controls">
<button class="warning" id="pauseBtn" onclick="togglePause()">Pause</button>
</div>
<div class="section">
<div class="inv-header">
<div class="section-title" style="margin-bottom:0">Inventory</div>
<span class="inv-free" id="invFreeCount"></span>
</div>
<div class="inventory-grid" id="inventoryGrid">
<div class="empty-state" style="grid-column:1/-1">No active scrap session</div>
</div>
</div>
<div class="section">
<div class="section-title">Trade Links</div>
<div class="add-link">
<input type="text" id="nameInput" placeholder="Name (optional)" style="max-width:180px" />
<input type="text" id="urlInput" placeholder="Paste trade URL..." />
<select id="modeInput" class="mode-select" style="width:90px">
<option value="live">Live</option>
<option value="scrap">Scrap</option>
</select>
<button class="primary" onclick="addLink()">Add</button>
</div>
<div class="links-list" id="linksList">
<div class="empty-state">No trade links added yet</div>
</div>
</div>
<div class="section">
<div class="section-title">Debug Tools</div>
<div class="debug-panel">
<div class="debug-row">
<button onclick="openOcrSettings()" title="OCR Settings" style="display:flex;align-items:center;gap:4px">
<svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="10" cy="10" r="3"/><path d="M10 1.5v2M10 16.5v2M3.4 3.4l1.4 1.4M15.2 15.2l1.4 1.4M1.5 10h2M16.5 10h2M3.4 16.6l1.4-1.4M15.2 4.8l1.4-1.4"/></svg>
OCR Settings
</button>
<button onclick="debugScreenshot()">Screenshot</button>
<button onclick="debugOcr()">OCR Screen</button>
<button onclick="debugHideout()">Go Hideout</button>
</div>
<div class="debug-row">
<button onclick="debugFindAndClick('ANGE')">ANGE</button>
<button onclick="debugFindAndClick('STASH')">STASH</button>
<button onclick="debugFindAndClick('SALVAGE BENCH', true)">SALVAGE</button>
</div>
<div class="debug-row">
<button onclick="debugAngeOption('Currency')">Currency Exchange</button>
<button onclick="debugAngeOption('Manage')">Manage Shop</button>
<button onclick="debugAngeOption('Buy')">Buy or Sell</button>
<button onclick="debugAngeOption('Purchase')">Purchase Items</button>
</div>
<div class="debug-row">
<button onclick="debugGridScan('inventory')">Scan Inventory</button>
<button onclick="debugGridScan('stash12')">Scan Stash 12x12</button>
<button onclick="debugGridScan('stash24')">Scan Stash 24x24</button>
<button onclick="debugGridScan('seller')">Scan Seller</button>
<button onclick="debugGridScan('shop')">Scan Shop</button>
<button onclick="debugGridScan('vendor')">Scan Vendor</button>
</div>
<div class="debug-row">
<select id="matchLayout" style="padding:6px 10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px">
<option value="shop">Shop</option>
<option value="seller">Seller</option>
<option value="stash12">Stash 12x12</option>
<option value="stash12_folder">Stash 12x12 (folder)</option>
<option value="inventory">Inventory</option>
<option value="vendor">Vendor</option>
</select>
<input type="number" id="matchRow" placeholder="Row" value="8" style="width:60px" />
<input type="number" id="matchCol" placeholder="Col" value="0" style="width:60px" />
<button class="primary" onclick="debugTestMatchHover()">Match & Hover</button>
</div>
<div class="debug-row">
<input type="text" id="debugTextInput" placeholder="Text to find (e.g. Stash, Ange)" />
<button onclick="debugFindText()">Find</button>
<button class="primary" onclick="debugFindAndClick()">Find & Click</button>
</div>
<div class="debug-row">
<input type="number" id="debugClickX" placeholder="X" style="width:80px" />
<input type="number" id="debugClickY" placeholder="Y" style="width:80px" />
<button onclick="debugClick()">Click At</button>
</div>
<div class="debug-result" id="debugResult"></div>
</div>
</div>
<div class="section">
<div class="section-title">Activity Log</div>
<div class="log-panel" id="logPanel"></div>
</div>
</div>
<div class="modal-overlay" id="settingsModal">
<div class="modal">
<div class="modal-header">
<h2>Settings</h2>
<button class="modal-close" onclick="closeSettings()">&times;</button>
</div>
<div class="settings-grid full">
<div class="setting-row">
<label>POE2 Client.txt Path</label>
<input type="text" id="settLogPath" />
</div>
</div>
<div class="settings-grid" style="margin-top:10px">
<div class="setting-row">
<label>Window Title</label>
<input type="text" id="settWindowTitle" />
</div>
<div class="setting-row">
<label>Travel Timeout (ms)</label>
<input type="number" id="settTravelTimeout" />
</div>
<div class="setting-row">
<label>Wait for More Items (ms)</label>
<input type="number" id="settWaitMore" />
</div>
<div class="setting-row">
<label>Delay Between Trades (ms)</label>
<input type="number" id="settTradeDelay" />
</div>
</div>
<div class="settings-actions">
<span class="saved-badge" id="savedBadge">Saved</span>
<button class="primary" onclick="saveSettings()">Save Settings</button>
</div>
</div>
</div>
<div class="modal-overlay" id="ocrSettingsModal">
<div class="modal">
<div class="modal-header">
<h2>OCR Settings</h2>
<button class="modal-close" onclick="closeOcrSettings()">&times;</button>
</div>
<div style="margin-bottom:16px">
<div class="section-title" style="margin-bottom:6px">Engine</div>
<select id="ocrEngine" class="mode-select" style="width:100%" onchange="toggleOcrSections()">
<option value="tesseract">Tesseract</option>
<option value="easyocr">EasyOCR</option>
<option value="paddleocr">PaddleOCR</option>
</select>
</div>
<div style="margin-bottom:16px">
<div class="section-title" style="margin-bottom:6px">Screen OCR</div>
<select id="ocrScreenPreprocess" class="mode-select" style="width:100%" onchange="toggleOcrSections()">
<option value="none">None</option>
<option value="tophat">TopHat</option>
</select>
<div id="screenTophatParams" style="display:none;margin-top:8px">
<div class="settings-grid">
<div class="setting-row">
<label>Kernel Size</label>
<input type="number" id="ocrScreenKernel" value="41" />
</div>
</div>
</div>
</div>
<div style="margin-bottom:16px">
<div class="section-title" style="margin-bottom:6px">Tooltip Method</div>
<select id="ocrTooltipMethod" class="mode-select" style="width:100%" onchange="toggleOcrSections()">
<option value="diff">Diff Detection</option>
<option value="edge">Edge Detection</option>
</select>
</div>
<div style="margin-bottom:16px">
<div class="section-title" style="margin-bottom:6px">Tooltip Preprocess</div>
<select id="ocrTooltipPreprocess" class="mode-select" style="width:100%" onchange="toggleOcrSections()">
<option value="none">None</option>
<option value="bgsub">Background Subtraction</option>
<option value="tophat">TopHat</option>
</select>
</div>
<div id="diffCropParams" style="margin-bottom:16px">
<div class="section-title" style="margin-bottom:6px">Crop Detection (Diff)</div>
<div class="settings-grid">
<div class="setting-row">
<label>Diff Threshold</label>
<input type="number" id="ocrDiffThresh" value="20" />
</div>
<div class="setting-row">
<label>Max Gap</label>
<input type="number" id="ocrMaxGap" value="20" />
</div>
<div class="setting-row">
<label>Trim Cutoff</label>
<input type="number" id="ocrTrimCutoff" value="0.4" step="0.05" />
</div>
</div>
</div>
<div id="edgeCropParams" style="margin-bottom:16px;display:none">
<div class="section-title" style="margin-bottom:6px">Crop Detection (Edge)</div>
<div class="settings-grid">
<div class="setting-row">
<label>Canny Low</label>
<input type="number" id="ocrCannyLow" value="50" />
</div>
<div class="setting-row">
<label>Canny High</label>
<input type="number" id="ocrCannyHigh" value="150" />
</div>
<div class="setting-row">
<label>Min Line Length</label>
<input type="number" id="ocrMinLineLength" value="100" />
</div>
<div class="setting-row">
<label>ROI Size</label>
<input type="number" id="ocrRoiSize" value="1400" />
</div>
<div class="setting-row">
<label>Density Threshold</label>
<input type="number" id="ocrDensityThreshold" value="0.15" step="0.01" />
</div>
</div>
</div>
<div style="margin-bottom:16px">
<div class="section-title" style="margin-bottom:6px">OCR Processing</div>
<div class="settings-grid">
<div class="setting-row">
<label>Upscale</label>
<input type="number" id="ocrUpscale" value="2" />
</div>
<div class="setting-row">
<label>Merge Gap (px)</label>
<input type="number" id="ocrMergeGap" value="0" />
</div>
</div>
<div id="tooltipTophatParams" style="display:none;margin-top:8px">
<div class="settings-grid">
<div class="setting-row">
<label>Kernel Size</label>
<input type="number" id="ocrTooltipKernel" value="41" />
</div>
</div>
</div>
<div id="tooltipBgsubParams" style="display:none;margin-top:8px">
<div class="settings-grid">
<div class="setting-row">
<label>Dim Percentile</label>
<input type="number" id="ocrDimPercentile" value="40" />
</div>
<div class="setting-row">
<label>Text Threshold</label>
<input type="number" id="ocrTextThresh" value="60" />
</div>
</div>
<div style="display:flex;align-items:center;gap:8px;margin-top:8px">
<label class="toggle">
<input type="checkbox" id="ocrSoftThreshold" />
<span class="slider"></span>
</label>
<span style="font-size:12px;color:#8b949e">Soft Threshold</span>
</div>
</div>
<div id="easyocrParams" style="display:none;margin-top:8px">
<div class="settings-grid">
<div class="setting-row">
<label>Link Threshold</label>
<input type="number" id="ocrLinkThreshold" step="0.05" />
</div>
<div class="setting-row">
<label>Text Threshold</label>
<input type="number" id="ocrTextThreshold" step="0.05" />
</div>
<div class="setting-row">
<label>Low Text</label>
<input type="number" id="ocrLowText" step="0.05" />
</div>
<div class="setting-row">
<label>Width Threshold</label>
<input type="number" id="ocrWidthThs" step="0.05" />
</div>
</div>
</div>
</div>
<div style="margin-bottom:16px">
<div class="section-title" style="margin-bottom:6px">Options</div>
<div style="display:flex;align-items:center;gap:8px">
<label class="toggle">
<input type="checkbox" id="ocrSaveDebugImages" checked />
<span class="slider"></span>
</label>
<span style="font-size:12px;color:#8b949e">Save debug images to disk</span>
</div>
</div>
<div class="settings-actions">
<span class="saved-badge" id="ocrSavedBadge">Saved</span>
<button onclick="closeOcrSettings()" style="margin-right:8px">Close</button>
<button class="primary" onclick="saveOcrSettings()">Save</button>
</div>
</div>
</div>
<script>
let ws;
let status = { paused: false, state: 'IDLE', links: [], tradesCompleted: 0, tradesFailed: 0, uptime: 0, settings: {} };
let settingsLoaded = false;
function connect() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${proto}//${location.host}`);
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'status') {
status = msg.data;
render();
} else if (msg.type === 'log') {
addLog(msg.data);
} else if (msg.type === 'debug') {
handleDebugResult(msg.data);
}
};
ws.onclose = () => {
addLog({ level: 'warn', message: 'Dashboard disconnected. Reconnecting...', time: new Date().toISOString() });
setTimeout(connect, 2000);
};
ws.onerror = () => {};
}
function render() {
// Status badge
const dot = document.querySelector('.status-dot');
const text = document.getElementById('statusText');
dot.className = 'status-dot ' + (status.paused ? 'paused' : status.state === 'IDLE' ? 'idle' : 'running');
text.textContent = status.paused ? 'Paused' : status.state;
// Stats
document.getElementById('stateValue').textContent = status.state;
document.getElementById('linksValue').textContent = status.links.length;
document.getElementById('completedValue').textContent = status.tradesCompleted;
document.getElementById('failedValue').textContent = status.tradesFailed;
// Pause button
const btn = document.getElementById('pauseBtn');
btn.textContent = status.paused ? 'Resume' : 'Pause';
btn.className = status.paused ? 'primary' : 'warning';
// Settings (populate once on first status)
if (status.settings) populateSettings(status.settings);
// Inventory grid
renderInventory();
// Active links count
document.getElementById('linksValue').textContent = status.links.filter(l => l.active).length;
// Links list
const list = document.getElementById('linksList');
if (status.links.length === 0) {
list.innerHTML = '<div class="empty-state">No trade links added yet</div>';
} else {
list.innerHTML = status.links.map(link => `
<div class="link-item${link.active ? '' : ' inactive'}">
<div class="link-left">
<label class="toggle" title="${link.active ? 'Active' : 'Inactive'}">
<input type="checkbox" ${link.active ? 'checked' : ''} onchange="toggleLink('${esc(link.id)}', this.checked)" />
<span class="slider"></span>
</label>
<span class="mode-badge ${link.mode || 'live'}" onclick="cycleMode('${esc(link.id)}', '${link.mode || 'live'}')" title="Click to change mode">${esc(link.mode || 'live')}</span>
<div class="link-info">
<div class="link-name" contenteditable="true" spellcheck="false"
onblur="renameLink('${esc(link.id)}', this.textContent)"
title="Click to edit name">${esc(link.name || link.label)}</div>
<div class="link-url">${esc(link.url)}</div>
</div>
</div>
<div class="link-actions">
<button class="danger" onclick="removeLink('${esc(link.id)}')">Remove</button>
</div>
</div>
`).join('');
}
}
function renderInventory() {
const container = document.getElementById('inventoryGrid');
const freeLabel = document.getElementById('invFreeCount');
const grid = status.inventory ? status.inventory.grid : null;
const items = status.inventory ? status.inventory.items : [];
const free = status.inventory ? status.inventory.free : 60;
freeLabel.textContent = `${free}/60 free`;
let html = '';
for (let r = 0; r < 5; r++) {
for (let c = 0; c < 12; c++) {
const occupied = grid && grid[r] && grid[r][c] ? 'occupied' : '';
html += `<div class="inv-cell ${occupied}" data-r="${r}" data-c="${c}"></div>`;
}
}
container.innerHTML = html;
for (const item of items) {
for (let r = item.row; r < item.row + item.h; r++) {
for (let c = item.col; c < item.col + item.w; c++) {
const cell = container.querySelector(`[data-r="${r}"][data-c="${c}"]`);
if (cell) {
if (r === item.row) cell.classList.add('item-top');
if (r === item.row + item.h - 1) cell.classList.add('item-bottom');
if (c === item.col) cell.classList.add('item-left');
if (c === item.col + item.w - 1) cell.classList.add('item-right');
}
}
}
}
}
function addLog(data) {
const panel = document.getElementById('logPanel');
const line = document.createElement('div');
line.className = 'log-line ' + (data.level || 'info');
const t = new Date(data.time).toLocaleTimeString();
line.innerHTML = `<span class="time">${t}</span> <span class="msg">${esc(data.message)}</span>`;
panel.appendChild(line);
if (panel.children.length > 500) panel.removeChild(panel.firstChild);
panel.scrollTop = panel.scrollHeight;
}
// --- Handle debug results arriving via WebSocket ---
function handleDebugResult(data) {
switch (data.action) {
case 'screenshot':
if (data.error) { showDebugResult(`Error: ${data.error}`); break; }
showDebugResult(`Screenshots saved: ${(data.files || []).map(f => f.split(/[\\/]/).pop()).join(', ')}`);
break;
case 'ocr':
showDebugResult(data.error ? `Error: ${data.error}` : data.text);
break;
case 'find-text':
if (data.error) { showDebugResult(`Error: ${data.error}`); break; }
if (data.found) {
showDebugResult(`Found "${data.searchText}" at (${data.position.x}, ${data.position.y})`);
document.getElementById('debugClickX').value = data.position.x;
document.getElementById('debugClickY').value = data.position.y;
} else {
showDebugResult(`"${data.searchText}" not found on screen`);
}
break;
case 'find-and-click':
if (data.error) { showDebugResult(`Error: ${data.error}`); break; }
showDebugResult(data.found
? `Clicked "${data.searchText}" at (${data.position.x}, ${data.position.y})`
: `"${data.searchText}" not found on screen`);
break;
case 'click':
showDebugResult(data.error ? `Error: ${data.error}` : `Clicked at (${data.x}, ${data.y})`);
break;
case 'hideout':
showDebugResult(data.error ? `Error: ${data.error}` : 'Sent /hideout command');
break;
case 'click-then-click':
if (data.error) { showDebugResult(`Error: ${data.error}`); break; }
if (!data.found && data.step === 'first') showDebugResult(`"${data.first}" not found on screen`);
else if (!data.found) showDebugResult(`"${data.second}" not found after clicking "${data.first}" (timed out)`);
else showDebugResult(`Clicked "${data.second}" at (${data.position.x}, ${data.position.y})`);
break;
case 'grid-scan':
if (data.error) { showDebugResult(`Error: ${data.error}`); break; }
renderGridScanResult(data);
break;
case 'test-match-hover':
if (data.error) { showDebugResult(`Error: ${data.error}`); break; }
showDebugResult(`Done: ${data.itemSize} item, ${data.matchCount} matches, hovered ${data.hoveredCount} cells`);
break;
}
}
function renderGridScanResult(data) {
const layout = data.layout;
const hasTarget = data.targetRow != null && data.targetCol != null;
const targetRow = data.targetRow;
const targetCol = data.targetCol;
const el = document.getElementById('debugResult');
const count = data.occupied.length;
const items = data.items || [];
const matches = data.matches || [];
const r = data.region;
let html = `<b>${layout}</b> ${data.cols}x${data.rows}`;
html += `${count} occupied, ${items.length} items`;
if (matches.length > 0) html += `, <span style="color:#f0883e;font-weight:bold">${matches.length} matches</span>`;
if (r) html += `<br><span style="color:#484f58">Region: (${r.x}, ${r.y}) ${r.width}x${r.height}</span>`;
if (items.length > 0) {
const sizes = {};
items.forEach(i => { const k = i.w + 'x' + i.h; sizes[k] = (sizes[k]||0) + 1; });
html += `<br><span style="color:#58a6ff">` + Object.entries(sizes).map(([k,v]) => `${v}x ${k}`).join(', ') + `</span>`;
}
if (hasTarget) {
html += `<br><span style="color:#f0883e">Target: (${targetRow},${targetCol})`;
if (matches.length > 0) html += `${matches.map(m => `(${m.row},${m.col}) ${(m.similarity*100).toFixed(0)}%`).join(', ')}`;
html += `</span>`;
} else {
html += `<br><span style="color:#484f58">Click a cell to find matching items</span>`;
}
html += '<div class="grid-debug">';
if (data.image) {
html += `<img src="data:image/png;base64,${data.image}" alt="Grid capture" />`;
}
const matchSet = new Set(matches.map(m => m.row + ',' + m.col));
const targetKey = hasTarget ? targetRow + ',' + targetCol : null;
const itemMap = {};
const colors = ['#238636','#1f6feb','#8957e5','#da3633','#d29922','#3fb950','#388bfd','#a371f7','#f85149','#e3b341'];
items.forEach((item, idx) => {
const color = colors[idx % colors.length];
for (let dr = 0; dr < item.h; dr++)
for (let dc = 0; dc < item.w; dc++)
itemMap[(item.row+dr)+','+(item.col+dc)] = { item, color, isOrigin: dr===0 && dc===0 };
});
html += `<div class="grid-view" style="grid-template-columns:repeat(${data.cols},12px)">`;
const set = new Set(data.occupied.map(c => c.row + ',' + c.col));
for (let gr = 0; gr < data.rows; gr++) {
for (let gc = 0; gc < data.cols; gc++) {
const key = gr+','+gc;
const isTarget = key === targetKey;
const isMatch = matchSet.has(key);
const info = itemMap[key];
let bg;
if (isTarget) bg = '#f0883e';
else if (isMatch) bg = '#d29922';
else if (info) bg = info.color;
else if (set.has(key)) bg = '#238636';
else bg = '';
const outline = (isTarget || isMatch) ? 'outline:2px solid #f0883e;z-index:1;' : '';
const cursor = set.has(key) ? 'cursor:pointer;' : '';
const bgStyle = bg ? `background:${bg};` : '';
const style = (bgStyle || outline || cursor) ? ` style="${bgStyle}${outline}${cursor}"` : '';
let title = info ? `(${gr},${gc}) ${info.item.w}x${info.item.h}` : `(${gr},${gc})`;
if (isTarget) title += ' [TARGET]';
if (isMatch) { const m = matches.find(m => m.row===gr && m.col===gc); title += ` [MATCH ${(m.similarity*100).toFixed(0)}%]`; }
const onclick = set.has(key) ? ` onclick="debugGridScan('${layout}',${gr},${gc})"` : '';
html += `<div class="grid-cell${set.has(key) ? ' occupied' : ''}"${style}${onclick} title="${title}"></div>`;
}
}
html += '</div></div>';
el.innerHTML = html;
}
async function togglePause() {
const endpoint = status.paused ? '/api/resume' : '/api/pause';
await fetch(endpoint, { method: 'POST' });
}
async function addLink() {
const urlEl = document.getElementById('urlInput');
const nameEl = document.getElementById('nameInput');
const modeEl = document.getElementById('modeInput');
const url = urlEl.value.trim();
if (!url) return;
await fetch('/api/links', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, name: nameEl.value.trim(), mode: modeEl.value }),
});
urlEl.value = '';
nameEl.value = '';
}
async function removeLink(id) {
await fetch('/api/links/' + encodeURIComponent(id), { method: 'DELETE' });
}
async function toggleLink(id, active) {
await fetch('/api/links/' + encodeURIComponent(id) + '/toggle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ active }),
});
}
let renameTimer = null;
async function renameLink(id, name) {
clearTimeout(renameTimer);
renameTimer = setTimeout(async () => {
await fetch('/api/links/' + encodeURIComponent(id) + '/name', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name.trim() }),
});
}, 300);
}
async function cycleMode(id, currentMode) {
const newMode = currentMode === 'live' ? 'scrap' : 'live';
await fetch('/api/links/' + encodeURIComponent(id) + '/mode', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: newMode }),
});
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function populateSettings(s) {
if (!s || settingsLoaded) return;
settingsLoaded = true;
document.getElementById('settLogPath').value = s.poe2LogPath || '';
document.getElementById('settWindowTitle').value = s.poe2WindowTitle || '';
document.getElementById('settTravelTimeout').value = s.travelTimeoutMs || 15000;
document.getElementById('settWaitMore').value = s.waitForMoreItemsMs || 20000;
document.getElementById('settTradeDelay').value = s.betweenTradesDelayMs || 5000;
}
function openSettings() {
if (status.settings) {
const s = status.settings;
document.getElementById('settLogPath').value = s.poe2LogPath || '';
document.getElementById('settWindowTitle').value = s.poe2WindowTitle || '';
document.getElementById('settTravelTimeout').value = s.travelTimeoutMs || 15000;
document.getElementById('settWaitMore').value = s.waitForMoreItemsMs || 20000;
document.getElementById('settTradeDelay').value = s.betweenTradesDelayMs || 5000;
}
document.getElementById('settingsModal').classList.add('open');
}
function closeSettings() {
document.getElementById('settingsModal').classList.remove('open');
}
document.getElementById('settingsModal').addEventListener('click', (e) => {
if (e.target === e.currentTarget) closeSettings();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { closeSettings(); closeOcrSettings(); }
});
async function saveSettings() {
const body = {
poe2LogPath: document.getElementById('settLogPath').value,
poe2WindowTitle: document.getElementById('settWindowTitle').value,
travelTimeoutMs: parseInt(document.getElementById('settTravelTimeout').value) || 15000,
waitForMoreItemsMs: parseInt(document.getElementById('settWaitMore').value) || 20000,
betweenTradesDelayMs: parseInt(document.getElementById('settTradeDelay').value) || 5000,
};
await fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const badge = document.getElementById('savedBadge');
badge.classList.add('show');
setTimeout(() => badge.classList.remove('show'), 2000);
}
// --- Debug functions (fire-and-forget: POST returns instantly, results arrive via WS) ---
function debugScreenshot() {
showDebugResult('Taking screenshots...');
fetch('/api/debug/screenshot', { method: 'POST' });
}
function debugOcr() {
showDebugResult('Running OCR...');
fetch('/api/debug/ocr', { method: 'POST' });
}
function debugHideout() {
showDebugResult('Sending /hideout...');
fetch('/api/debug/hideout', { method: 'POST' });
}
function debugFindText() {
const text = document.getElementById('debugTextInput').value.trim();
if (!text) return;
showDebugResult(`Searching for "${text}"...`);
fetch('/api/debug/find-text', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
});
}
function debugFindAndClick(directText, fuzzy) {
const text = directText || document.getElementById('debugTextInput').value.trim();
if (!text) return;
showDebugResult(`Finding and clicking "${text}"${fuzzy ? ' (fuzzy)' : ''}...`);
fetch('/api/debug/find-and-click', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, fuzzy: !!fuzzy }),
});
}
function debugClick() {
const x = parseInt(document.getElementById('debugClickX').value);
const y = parseInt(document.getElementById('debugClickY').value);
if (isNaN(x) || isNaN(y)) return;
showDebugResult(`Clicking at (${x}, ${y})...`);
fetch('/api/debug/click', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ x, y }),
});
}
function debugAngeOption(option) {
showDebugResult(`Clicking ANGE → ${option}...`);
fetch('/api/debug/click-then-click', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ first: 'ANGE', second: option }),
});
}
function debugGridScan(layout, targetRow, targetCol) {
const hasTarget = targetRow != null && targetCol != null;
showDebugResult(hasTarget ? `Matching (${targetRow},${targetCol}) in ${layout}...` : `Scanning ${layout}...`);
const body = { layout };
if (hasTarget) { body.targetRow = targetRow; body.targetCol = targetCol; }
fetch('/api/debug/grid-scan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
}
function debugTestMatchHover() {
const layout = document.getElementById('matchLayout').value;
const targetRow = parseInt(document.getElementById('matchRow').value);
const targetCol = parseInt(document.getElementById('matchCol').value);
if (isNaN(targetRow) || isNaN(targetCol)) { showDebugResult('Invalid row/col'); return; }
showDebugResult(`Scanning ${layout} and matching (${targetRow},${targetCol})...`);
fetch('/api/debug/test-match-hover', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ layout, targetRow, targetCol }),
});
}
function showDebugResult(text) {
document.getElementById('debugResult').textContent = text;
}
// Enter key handlers
document.getElementById('debugTextInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') debugFindText();
});
document.getElementById('urlInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') addLink();
});
// --- OCR Settings modal ---
function openOcrSettings() {
loadOcrSettings();
document.getElementById('ocrSettingsModal').classList.add('open');
}
function closeOcrSettings() {
document.getElementById('ocrSettingsModal').classList.remove('open');
}
document.getElementById('ocrSettingsModal').addEventListener('click', (e) => {
if (e.target === e.currentTarget) closeOcrSettings();
});
function toggleOcrSections() {
const screenPp = document.getElementById('ocrScreenPreprocess').value;
document.getElementById('screenTophatParams').style.display = screenPp === 'tophat' ? '' : 'none';
const method = document.getElementById('ocrTooltipMethod').value;
const isEdge = method === 'edge';
// Show/hide method-specific crop params
document.getElementById('diffCropParams').style.display = isEdge ? 'none' : '';
document.getElementById('edgeCropParams').style.display = isEdge ? '' : 'none';
// Disable bgsub when edge (no reference frame)
const ppSelect = document.getElementById('ocrTooltipPreprocess');
const bgsubOption = ppSelect.querySelector('option[value="bgsub"]');
if (isEdge) {
bgsubOption.disabled = true;
if (ppSelect.value === 'bgsub') ppSelect.value = 'tophat';
} else {
bgsubOption.disabled = false;
}
const tooltipPp = ppSelect.value;
document.getElementById('tooltipBgsubParams').style.display = tooltipPp === 'bgsub' ? '' : 'none';
document.getElementById('tooltipTophatParams').style.display = tooltipPp === 'tophat' ? '' : 'none';
const engine = document.getElementById('ocrEngine').value;
document.getElementById('easyocrParams').style.display = engine === 'easyocr' ? '' : 'none';
}
async function loadOcrSettings() {
try {
const res = await fetch('/api/debug/ocr-settings');
const data = await res.json();
if (!data.ok) return;
document.getElementById('ocrEngine').value = data.engine || 'easyocr';
document.getElementById('ocrScreenPreprocess').value = data.screenPreprocess || 'none';
document.getElementById('ocrTooltipMethod').value = data.tooltipMethod || 'diff';
document.getElementById('ocrTooltipPreprocess').value = data.tooltipPreprocess || 'tophat';
document.getElementById('ocrSaveDebugImages').checked = data.saveDebugImages !== false;
const tp = data.tooltipParams || {};
const crop = tp.crop || {};
const ocr = tp.ocr || {};
// Edge params
const ep = data.edgeParams || {};
const edgeCrop = ep.crop || {};
document.getElementById('ocrCannyLow').value = edgeCrop.cannyLow ?? 50;
document.getElementById('ocrCannyHigh').value = edgeCrop.cannyHigh ?? 150;
document.getElementById('ocrMinLineLength').value = edgeCrop.minLineLength ?? 100;
document.getElementById('ocrRoiSize').value = edgeCrop.roiSize ?? 1400;
document.getElementById('ocrDensityThreshold').value = edgeCrop.densityThreshold ?? 0.15;
document.getElementById('ocrDiffThresh').value = crop.diffThresh ?? 20;
document.getElementById('ocrMaxGap').value = crop.maxGap ?? 20;
document.getElementById('ocrTrimCutoff').value = crop.trimCutoff ?? 0.4;
document.getElementById('ocrUpscale').value = ocr.upscale ?? 2;
document.getElementById('ocrMergeGap').value = ocr.mergeGap ?? 0;
document.getElementById('ocrTooltipKernel').value = ocr.kernelSize ?? 41;
document.getElementById('ocrDimPercentile').value = ocr.dimPercentile ?? 40;
document.getElementById('ocrTextThresh').value = ocr.textThresh ?? 60;
document.getElementById('ocrSoftThreshold').checked = !!ocr.softThreshold;
document.getElementById('ocrLinkThreshold').value = ocr.linkThreshold ?? '';
document.getElementById('ocrTextThreshold').value = ocr.textThreshold ?? '';
document.getElementById('ocrLowText').value = ocr.lowText ?? '';
document.getElementById('ocrWidthThs').value = ocr.widthThs ?? '';
toggleOcrSections();
} catch {}
}
async function saveOcrSettings() {
const tooltipPp = document.getElementById('ocrTooltipPreprocess').value;
const screenPp = document.getElementById('ocrScreenPreprocess').value;
const engine = document.getElementById('ocrEngine').value;
const tooltipParams = {
crop: {
diffThresh: parseInt(document.getElementById('ocrDiffThresh').value) || 20,
maxGap: parseInt(document.getElementById('ocrMaxGap').value) || 20,
trimCutoff: parseFloat(document.getElementById('ocrTrimCutoff').value) || 0.4,
},
ocr: {
upscale: parseInt(document.getElementById('ocrUpscale').value) || 2,
useBackgroundSub: tooltipPp === 'bgsub',
},
};
const mg = parseInt(document.getElementById('ocrMergeGap').value);
if (mg > 0) tooltipParams.ocr.mergeGap = mg;
if (tooltipPp === 'tophat') {
tooltipParams.ocr.kernelSize = parseInt(document.getElementById('ocrTooltipKernel').value) || 21;
}
if (tooltipPp === 'bgsub') {
tooltipParams.ocr.dimPercentile = parseInt(document.getElementById('ocrDimPercentile').value) || 40;
tooltipParams.ocr.textThresh = parseInt(document.getElementById('ocrTextThresh').value) || 60;
tooltipParams.ocr.softThreshold = document.getElementById('ocrSoftThreshold').checked;
}
if (engine === 'easyocr') {
const lt = parseFloat(document.getElementById('ocrLinkThreshold').value);
const tt = parseFloat(document.getElementById('ocrTextThreshold').value);
const low = parseFloat(document.getElementById('ocrLowText').value);
const wt = parseFloat(document.getElementById('ocrWidthThs').value);
if (!isNaN(lt)) tooltipParams.ocr.linkThreshold = lt;
if (!isNaN(tt)) tooltipParams.ocr.textThreshold = tt;
if (!isNaN(low)) tooltipParams.ocr.lowText = low;
if (!isNaN(wt)) tooltipParams.ocr.widthThs = wt;
}
const tooltipMethod = document.getElementById('ocrTooltipMethod').value;
const edgeParams = {
crop: {
cannyLow: parseInt(document.getElementById('ocrCannyLow').value) || 50,
cannyHigh: parseInt(document.getElementById('ocrCannyHigh').value) || 150,
minLineLength: parseInt(document.getElementById('ocrMinLineLength').value) || 100,
roiSize: parseInt(document.getElementById('ocrRoiSize').value) || 1400,
densityThreshold: parseFloat(document.getElementById('ocrDensityThreshold').value) || 0.15,
},
ocr: {
upscale: parseInt(document.getElementById('ocrUpscale').value) || 2,
kernelSize: parseInt(document.getElementById('ocrTooltipKernel').value) || 21,
},
};
const body = {
engine,
screenPreprocess: screenPp,
tooltipMethod,
tooltipPreprocess: tooltipPp,
tooltipParams,
edgeParams,
saveDebugImages: document.getElementById('ocrSaveDebugImages').checked,
};
await fetch('/api/debug/ocr-settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const badge = document.getElementById('ocrSavedBadge');
badge.classList.add('show');
setTimeout(() => badge.classList.remove('show'), 2000);
}
connect();
loadOcrSettings();
</script>
</body>
</html>