iso-bot/web/dev/index.html
Hoid a665253d4d Fix dashboard + vision: region checkboxes, upload auto-switch, state detection, analyze endpoint
- Region checkboxes now control which overlays are drawn (?regions= param)
- Upload auto-switches capture source (no restart needed)
- State detection: check in-game BEFORE loading, require 90% dark for loading
- Tighten HSV ranges: health H 0-30 S 50+ V 30+, mana H 200-240 S 40+ V 20+
- Add /api/analyze endpoint with per-region color analysis
2026-02-14 11:27:46 +00:00

684 lines
No EOL
23 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ISO-BOT Dev Dashboard</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
background-color: #1a1a1a;
color: #e0e0e0;
line-height: 1.4;
font-size: 14px;
}
.header {
background: linear-gradient(135deg, #2d2d2d, #404040);
padding: 15px 20px;
border-bottom: 2px solid #555;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.header h1 {
color: #00ff88;
font-size: 24px;
margin-bottom: 5px;
}
.status-line {
color: #888;
font-size: 12px;
}
.status-running { color: #00ff88; }
.status-paused { color: #ffaa00; }
.status-stopped { color: #ff4444; }
.main-container {
display: grid;
grid-template-columns: 2fr 1fr;
grid-template-rows: 1fr auto;
height: calc(100vh - 80px);
gap: 10px;
padding: 10px;
}
.capture-panel {
background: #2a2a2a;
border: 1px solid #555;
border-radius: 8px;
overflow: hidden;
position: relative;
}
.capture-viewer {
width: 100%;
height: calc(100% - 100px);
position: relative;
overflow: hidden;
}
.capture-image {
max-width: 100%;
max-height: 100%;
cursor: crosshair;
border: none;
}
.pixel-info {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(0,0,0,0.8);
padding: 8px 12px;
border-radius: 4px;
font-size: 11px;
border: 1px solid #666;
}
.upload-zone {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0,0,0,0.7);
padding: 10px;
border-radius: 4px;
border: 2px dashed #666;
cursor: pointer;
transition: all 0.3s ease;
}
.upload-zone:hover {
border-color: #00ff88;
background: rgba(0,255,136,0.1);
}
.upload-zone.dragover {
border-color: #00ff88;
background: rgba(0,255,136,0.2);
}
.side-panel {
display: flex;
flex-direction: column;
gap: 10px;
}
.panel {
background: #2a2a2a;
border: 1px solid #555;
border-radius: 8px;
padding: 15px;
}
.panel h3 {
color: #00ff88;
margin-bottom: 10px;
font-size: 14px;
border-bottom: 1px solid #444;
padding-bottom: 5px;
}
.stat-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
font-size: 11px;
}
.stat-item {
display: flex;
justify-content: space-between;
}
.stat-label {
color: #aaa;
}
.stat-value {
color: #00ff88;
font-weight: bold;
}
.health-bar, .mana-bar {
width: 100%;
height: 20px;
background: #333;
border-radius: 4px;
overflow: hidden;
margin: 5px 0;
border: 1px solid #555;
}
.health-fill {
height: 100%;
background: linear-gradient(90deg, #ff4444, #ff6666);
transition: width 0.3s ease;
}
.mana-fill {
height: 100%;
background: linear-gradient(90deg, #4444ff, #6666ff);
transition: width 0.3s ease;
}
.regions-list {
display: flex;
flex-direction: column;
gap: 5px;
}
.region-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
}
.region-checkbox {
accent-color: #00ff88;
}
.region-name {
color: #ccc;
}
.log-panel {
grid-column: 1 / -1;
background: #1e1e1e;
border: 1px solid #555;
border-radius: 8px;
padding: 15px;
max-height: 200px;
overflow-y: auto;
}
.log-output {
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
font-size: 11px;
white-space: pre-wrap;
}
.log-line {
margin-bottom: 2px;
}
.log-info { color: #00ff88; }
.log-warn { color: #ffaa00; }
.log-error { color: #ff4444; }
.hidden {
display: none;
}
.button {
background: linear-gradient(135deg, #444, #666);
color: #fff;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
transition: all 0.3s ease;
}
.button:hover {
background: linear-gradient(135deg, #555, #777);
}
.button.primary {
background: linear-gradient(135deg, #00aa66, #00ff88);
}
.button.primary:hover {
background: linear-gradient(135deg, #00cc77, #00ffaa);
}
</style>
</head>
<body>
<div class="header">
<h1>ISO-BOT DEV DASHBOARD</h1>
<div class="status-line">
<span>Status: <span id="bot-status" class="status-stopped">Loading...</span></span>
<span style="margin-left: 20px;">Game: <span id="game-name">Unknown</span></span>
<span style="margin-left: 20px;">FPS: <span id="fps-display">0</span></span>
</div>
</div>
<div class="main-container">
<!-- Capture Viewer Panel -->
<div class="capture-panel">
<div class="capture-viewer">
<img id="capture-image" class="capture-image" alt="Game capture" />
<div class="upload-zone" id="upload-zone">
<div>📁 Drop image here</div>
<input type="file" id="file-input" accept="image/*" style="display: none;">
</div>
<div class="pixel-info" id="pixel-info">
Mouse: (0, 0)<br>
RGB: (0, 0, 0)<br>
HSV: (0, 0, 0)
</div>
</div>
</div>
<!-- Side Panel -->
<div class="side-panel">
<!-- State Detection Panel -->
<div class="panel">
<h3>State Detection</h3>
<div class="stat-grid">
<div class="stat-item">
<span class="stat-label">Game State:</span>
<span class="stat-value" id="game-state">unknown</span>
</div>
</div>
<div style="margin-top: 10px;">
<div style="display: flex; justify-content: space-between; font-size: 11px;">
<span>Health:</span>
<span id="health-pct">0%</span>
</div>
<div class="health-bar">
<div class="health-fill" id="health-fill" style="width: 0%"></div>
</div>
<div style="display: flex; justify-content: space-between; font-size: 11px;">
<span>Mana:</span>
<span id="mana-pct">0%</span>
</div>
<div class="mana-bar">
<div class="mana-fill" id="mana-fill" style="width: 0%"></div>
</div>
</div>
</div>
<!-- Capture Stats Panel -->
<div class="panel">
<h3>Capture Stats</h3>
<div class="stat-grid">
<div class="stat-item">
<span class="stat-label">FPS:</span>
<span class="stat-value" id="capture-fps">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Frames:</span>
<span class="stat-value" id="frame-count">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Backend:</span>
<span class="stat-value" id="backend-name">file</span>
</div>
<div class="stat-item">
<span class="stat-label">Avg Ms:</span>
<span class="stat-value" id="avg-capture-ms">0</span>
</div>
</div>
</div>
<!-- Regions Panel -->
<div class="panel">
<h3>Regions</h3>
<div class="regions-list" id="regions-list">
<!-- Populated by JavaScript -->
</div>
</div>
<!-- Loot & Routines Panel -->
<div class="panel">
<h3>Loot Filter</h3>
<div class="stat-grid">
<div class="stat-item">
<span class="stat-label">Rules:</span>
<span class="stat-value" id="loot-rule-count">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Last Match:</span>
<span class="stat-value">None</span>
</div>
</div>
<h3 style="margin-top: 15px;">Routines</h3>
<div id="routines-list">
<!-- Populated by JavaScript -->
</div>
</div>
</div>
<!-- Log Output Panel -->
<div class="log-panel">
<h3 style="color: #00ff88; margin-bottom: 10px;">Log Output</h3>
<div class="log-output" id="log-output">
<div class="log-line log-info">[INFO] Dev dashboard loaded</div>
</div>
</div>
</div>
<script>
// Global state
let regions = [];
let visibleRegions = new Set();
let isPolling = true;
// DOM elements
const captureImage = document.getElementById('capture-image');
const pixelInfo = document.getElementById('pixel-info');
const uploadZone = document.getElementById('upload-zone');
const fileInput = document.getElementById('file-input');
const logOutput = document.getElementById('log-output');
// Initialize dashboard
async function init() {
await loadRegions();
await updateStatus();
await updateState();
await updateStats();
await updateLootRules();
await updateRoutines();
startPolling();
setupEventListeners();
addLogLine('INFO', 'Dashboard initialized');
}
// Load region definitions
async function loadRegions() {
try {
const response = await fetch('/api/regions');
regions = await response.json();
renderRegionsList();
// Enable all regions by default
if (regions && Array.isArray(regions)) {
regions.forEach(region => visibleRegions.add(region.name));
}
addLogLine('INFO', `Loaded ${regions.length} regions`);
} catch (error) {
addLogLine('ERROR', `Failed to load regions: ${error.message}`);
}
}
// Render regions list with checkboxes
function renderRegionsList() {
const container = document.getElementById('regions-list');
container.innerHTML = '';
if (regions && Array.isArray(regions)) {
regions.forEach(region => {
const item = document.createElement('div');
item.className = 'region-item';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'region-checkbox';
checkbox.checked = visibleRegions.has(region.name);
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
visibleRegions.add(region.name);
} else {
visibleRegions.delete(region.name);
}
updateCaptureImage();
});
const label = document.createElement('span');
label.className = 'region-name';
label.textContent = region.name;
item.appendChild(checkbox);
item.appendChild(label);
container.appendChild(item);
});
}
}
// Update bot status
async function updateStatus() {
try {
const response = await fetch('/api/status');
const status = await response.json();
document.getElementById('bot-status').textContent =
status.running ? (status.paused ? 'Paused' : 'Running') : 'Stopped';
document.getElementById('bot-status').className =
'status-' + (status.running ? (status.paused ? 'paused' : 'running') : 'stopped');
document.getElementById('fps-display').textContent = status.captureFps.toFixed(1);
document.getElementById('frame-count').textContent = status.frameCount;
} catch (error) {
addLogLine('ERROR', `Failed to update status: ${error.message}`);
}
}
// Update game state and vitals
async function updateState() {
try {
const response = await fetch('/api/state');
const state = await response.json();
document.getElementById('game-state').textContent = state.gameState;
const healthPct = Math.round(state.healthPct * 100);
const manaPct = Math.round(state.manaPct * 100);
document.getElementById('health-pct').textContent = healthPct + '%';
document.getElementById('health-fill').style.width = healthPct + '%';
document.getElementById('mana-pct').textContent = manaPct + '%';
document.getElementById('mana-fill').style.width = manaPct + '%';
} catch (error) {
addLogLine('ERROR', `Failed to update state: ${error.message}`);
}
}
// Update capture stats
async function updateStats() {
try {
const response = await fetch('/api/capture/stats');
const stats = await response.json();
document.getElementById('capture-fps').textContent = stats.fps.toFixed(1);
document.getElementById('avg-capture-ms').textContent = stats.avgCaptureMs.toFixed(1);
document.getElementById('backend-name').textContent = stats.backend;
} catch (error) {
addLogLine('ERROR', `Failed to update stats: ${error.message}`);
}
}
// Update loot rules
async function updateLootRules() {
try {
const response = await fetch('/api/loot/rules');
const data = await response.json();
document.getElementById('loot-rule-count').textContent = data.count;
} catch (error) {
addLogLine('ERROR', `Failed to update loot rules: ${error.message}`);
}
}
// Update routines
async function updateRoutines() {
try {
const response = await fetch('/api/routines');
const routines = await response.json();
const container = document.getElementById('routines-list');
container.innerHTML = '';
if (routines && Array.isArray(routines)) {
routines.forEach(routine => {
const item = document.createElement('div');
item.style.fontSize = '11px';
item.style.marginBottom = '3px';
item.innerHTML = `${routine.name} <span style="color: #888;">[${routine.phase}]</span>`;
container.appendChild(item);
});
}
} catch (error) {
addLogLine('ERROR', `Failed to update routines: ${error.message}`);
}
}
// Update capture image with overlays
async function updateCaptureImage() {
try {
let url = '/api/capture/frame/annotated';
// Always pass regions param to control overlays (empty = no overlays)
const regionNames = Array.from(visibleRegions).join(',');
url += `?regions=${encodeURIComponent(regionNames)}`;
const response = await fetch(url);
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);
// Clean up previous URL
if (captureImage.src && captureImage.src.startsWith('blob:')) {
URL.revokeObjectURL(captureImage.src);
}
captureImage.src = imageUrl;
} catch (error) {
addLogLine('ERROR', `Failed to update capture image: ${error.message}`);
}
}
// Get pixel info at coordinates
async function getPixelInfo(x, y) {
try {
const response = await fetch(`/api/pixel?x=${x}&y=${y}`);
const pixel = await response.json();
pixelInfo.innerHTML = `
Mouse: (${pixel.x}, ${pixel.y})<br>
RGB: (${pixel.rgb[0]}, ${pixel.rgb[1]}, ${pixel.rgb[2]})<br>
HSV: (${pixel.hsv[0]}, ${pixel.hsv[1]}, ${pixel.hsv[2]})
`;
} catch (error) {
// Fail silently for pixel info to avoid spam
}
}
// Start polling updates
function startPolling() {
setInterval(async () => {
if (!isPolling) return;
await updateStatus();
await updateState();
await updateStats();
await updateCaptureImage();
}, 1000); // 1 FPS update rate for dev (less aggressive)
// Slower updates for less frequent data
setInterval(async () => {
if (!isPolling) return;
await updateLootRules();
await updateRoutines();
}, 5000);
}
// Set up event listeners
function setupEventListeners() {
// Mouse tracking on capture image
captureImage.addEventListener('mousemove', (e) => {
const rect = captureImage.getBoundingClientRect();
const scaleX = captureImage.naturalWidth / rect.width;
const scaleY = captureImage.naturalHeight / rect.height;
const x = Math.round((e.clientX - rect.left) * scaleX);
const y = Math.round((e.clientY - rect.top) * scaleY);
getPixelInfo(x, y);
});
// File upload
uploadZone.addEventListener('click', () => fileInput.click());
uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
uploadZone.classList.add('dragover');
});
uploadZone.addEventListener('dragleave', () => {
uploadZone.classList.remove('dragover');
});
uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
uploadZone.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
uploadFile(files[0]);
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
uploadFile(e.target.files[0]);
}
});
}
// Upload file
async function uploadFile(file) {
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/capture/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
addLogLine('INFO', `Uploaded: ${file.name}`);
addLogLine('INFO', result.message);
} catch (error) {
addLogLine('ERROR', `Upload failed: ${error.message}`);
}
}
// Add log line
function addLogLine(level, message) {
const line = document.createElement('div');
line.className = `log-line log-${level.toLowerCase()}`;
const timestamp = new Date().toTimeString().split(' ')[0];
line.textContent = `[${timestamp}] [${level}] ${message}`;
logOutput.appendChild(line);
// Keep only last 100 lines
while (logOutput.children.length > 100) {
logOutput.removeChild(logOutput.firstChild);
}
// Auto-scroll to bottom
logOutput.scrollTop = logOutput.scrollHeight;
}
// Initialize when page loads
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>