Autarch Will Control The Internet

This commit is contained in:
DigiJ
2026-03-13 15:17:15 -07:00
commit 4d3570781e
401 changed files with 484494 additions and 0 deletions

901
web/static/css/style.css Normal file
View File

@@ -0,0 +1,901 @@
:root {
--bg-primary: #0f1117;
--bg-secondary: #1a1d27;
--bg-card: #222536;
--bg-input: #2a2d3e;
--border: #333650;
--text-primary: #e4e6f0;
--text-secondary: #8b8fa8;
--text-muted: #5c6078;
--accent: #6366f1;
--accent-hover: #818cf8;
--success: #22c55e;
--warning: #f59e0b;
--danger: #ef4444;
--danger-hover: #dc2626;
--defense: #3b82f6;
--offense: #ef4444;
--counter: #a855f7;
--analyze: #06b6d4;
--osint: #22c55e;
--simulate: #f59e0b;
--hardware: #f97316;
--radius: 8px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
}
a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-hover); }
code { background: var(--bg-input); padding: 2px 6px; border-radius: 4px; font-size: 0.85em; word-break: break-all; }
pre { background: var(--bg-primary); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; font-size: 0.85rem; overflow-x: auto; white-space: pre-wrap; word-break: break-all; }
/* Layout */
.layout { display: flex; min-height: 100vh; }
.sidebar {
width: 240px;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
position: fixed;
height: 100vh;
overflow-y: auto;
}
.sidebar-header { padding: 24px 20px 16px; border-bottom: 1px solid var(--border); }
.sidebar-header h2 { font-size: 1.25rem; font-weight: 700; color: var(--danger); }
.subtitle { color: var(--text-secondary); font-size: 0.75rem; letter-spacing: 0.05em; }
.nav-section { padding: 8px 0; }
.nav-section-title { padding: 8px 20px 4px; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); }
.nav-links { list-style: none; padding: 0; }
.nav-links li a {
display: block;
padding: 8px 20px;
color: var(--text-secondary);
transition: all 0.15s;
font-size: 0.9rem;
}
.nav-links li a:hover { color: var(--text-primary); background: var(--bg-card); }
.nav-links li a.active { color: var(--accent); background: rgba(99,102,241,0.1); border-right: 3px solid var(--accent); }
.sidebar-footer {
padding: 16px 20px;
border-top: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.admin-name { color: var(--text-secondary); font-size: 0.85rem; }
.logout-link { color: var(--text-muted); font-size: 0.85rem; }
.logout-link:hover { color: var(--danger); }
.content { flex: 1; margin-left: 240px; padding: 32px; max-width: 1200px; }
/* Login */
.login-wrapper {
display: flex; flex-direction: column; align-items: center;
justify-content: center; min-height: 100vh; padding: 20px;
}
.login-card {
background: var(--bg-secondary); border: 1px solid var(--border);
border-radius: var(--radius); padding: 40px; width: 100%; max-width: 380px; text-align: center;
}
.login-card h1 { margin-bottom: 4px; color: var(--danger); }
.login-subtitle { color: var(--text-secondary); margin-bottom: 28px; font-size: 0.9rem; }
/* Flash messages */
.flash-messages { margin-bottom: 20px; }
.flash { padding: 12px 16px; border-radius: var(--radius); margin-bottom: 8px; font-size: 0.9rem; }
.flash-success { background: rgba(34,197,94,0.15); color: var(--success); border: 1px solid rgba(34,197,94,0.3); }
.flash-error { background: rgba(239,68,68,0.15); color: var(--danger); border: 1px solid rgba(239,68,68,0.3); }
.flash-warning { background: rgba(245,158,11,0.15); color: var(--warning); border: 1px solid rgba(245,158,11,0.3); }
.flash-info { background: rgba(99,102,241,0.15); color: var(--accent); border: 1px solid rgba(99,102,241,0.3); }
/* Forms */
.form-group { margin-bottom: 16px; text-align: left; }
.form-group label { display: block; margin-bottom: 6px; font-size: 0.85rem; color: var(--text-secondary); }
.form-group input, .form-group select, .form-group textarea {
width: 100%; padding: 10px 12px; background: var(--bg-input);
border: 1px solid var(--border); border-radius: var(--radius);
color: var(--text-primary); font-size: 0.9rem;
}
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { outline: none; border-color: var(--accent); }
.form-group textarea { font-family: monospace; resize: vertical; }
.form-row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }
.form-row .form-group { flex: 1; min-width: 150px; }
.form-inline { display: flex; gap: 8px; align-items: flex-end; }
.form-inline .form-group { margin-bottom: 0; }
.settings-form { max-width: 500px; }
.checkbox-label { display: flex; align-items: center; gap: 8px; cursor: pointer; }
.checkbox-label input[type="checkbox"] { width: auto; }
/* Buttons */
.btn {
display: inline-block; padding: 10px 20px; border: none; border-radius: var(--radius);
font-size: 0.9rem; cursor: pointer; transition: all 0.15s; color: var(--text-primary);
background: var(--bg-card); border: 1px solid var(--border); text-align: center;
}
.btn:hover { background: var(--bg-input); color: var(--text-primary); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: #fff; }
.btn-success { background: rgba(34,197,94,0.2); border-color: var(--success); color: var(--success); }
.btn-success:hover { background: rgba(34,197,94,0.3); }
.btn-warning { background: rgba(245,158,11,0.2); border-color: var(--warning); color: var(--warning); }
.btn-warning:hover { background: rgba(245,158,11,0.3); }
.btn-danger { background: rgba(239,68,68,0.2); border-color: var(--danger); color: var(--danger); }
.btn-danger:hover { background: rgba(239,68,68,0.3); }
.btn-small { padding: 6px 12px; font-size: 0.8rem; }
.btn-full { width: 100%; }
.btn-group { display: flex; gap: 8px; flex-wrap: wrap; }
/* Page header */
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
.page-header h1 { font-size: 1.5rem; }
/* Stats grid */
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 28px; }
.stat-card {
background: var(--bg-secondary); border: 1px solid var(--border);
border-radius: var(--radius); padding: 20px;
}
.stat-label { color: var(--text-secondary); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px; }
.stat-value { font-size: 1.4rem; font-weight: 600; }
.stat-value.small { font-size: 1rem; }
/* Category cards */
.category-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; margin-bottom: 28px; }
.category-card {
background: var(--bg-secondary); border: 1px solid var(--border);
border-radius: var(--radius); padding: 20px; transition: border-color 0.15s;
}
.category-card:hover { border-color: var(--accent); }
.category-card h3 { margin-bottom: 8px; }
.category-card p { color: var(--text-secondary); font-size: 0.85rem; margin-bottom: 12px; }
.category-card .badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; }
.cat-defense { border-left: 3px solid var(--defense); }
.cat-offense { border-left: 3px solid var(--offense); }
.cat-counter { border-left: 3px solid var(--counter); }
.cat-analyze { border-left: 3px solid var(--analyze); }
.cat-osint { border-left: 3px solid var(--osint); }
.cat-simulate { border-left: 3px solid var(--simulate); }
/* Sections */
.section {
background: var(--bg-secondary); border: 1px solid var(--border);
border-radius: var(--radius); padding: 20px; margin-bottom: 20px;
}
.section h2 { font-size: 1rem; margin-bottom: 16px; }
.section h3 { font-size: 0.9rem; margin-bottom: 12px; color: var(--text-secondary); }
/* Tables */
.data-table { width: 100%; border-collapse: collapse; }
.data-table th {
text-align: left; padding: 10px 12px; font-size: 0.8rem;
color: var(--text-muted); text-transform: uppercase;
letter-spacing: 0.05em; border-bottom: 1px solid var(--border);
}
.data-table td { padding: 12px; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
.data-table tr:hover { background: rgba(99,102,241,0.03); }
/* Module list */
.module-list { list-style: none; }
.module-item {
display: flex; justify-content: space-between; align-items: center;
padding: 12px 0; border-bottom: 1px solid var(--border);
}
.module-item:last-child { border-bottom: none; }
.module-name { font-weight: 500; }
.module-desc { color: var(--text-secondary); font-size: 0.85rem; }
.module-meta { color: var(--text-muted); font-size: 0.8rem; }
/* Status dots */
.status-dot {
display: inline-block; width: 8px; height: 8px; border-radius: 50%;
background: var(--text-muted); margin-right: 6px; vertical-align: middle;
}
.status-dot.active { background: var(--success); box-shadow: 0 0 6px rgba(34,197,94,0.4); }
.status-dot.inactive { background: var(--danger); }
.status-dot.warning { background: var(--warning); }
/* OSINT Search */
.search-box { display: flex; gap: 8px; margin-bottom: 16px; }
.search-box input { flex: 1; }
.search-options { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 16px; align-items: center; }
.search-options label { font-size: 0.85rem; color: var(--text-secondary); }
.results-stream { max-height: 600px; overflow-y: auto; }
.result-card {
display: flex; justify-content: space-between; align-items: center;
padding: 8px 12px; border-bottom: 1px solid var(--border); font-size: 0.85rem;
}
.result-card.found { border-left: 3px solid var(--success); }
.result-card.not_found { opacity: 0.4; }
.result-card.error { border-left: 3px solid var(--danger); }
.progress-bar { height: 4px; background: var(--bg-input); border-radius: 2px; margin-bottom: 16px; }
.progress-fill { height: 100%; background: var(--accent); border-radius: 2px; transition: width 0.3s; }
.progress-text { font-size: 0.8rem; color: var(--text-muted); margin-bottom: 8px; }
/* Empty state */
.empty-state { color: var(--text-muted); text-align: center; padding: 32px; }
/* Tool grid & cards */
.tool-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin-bottom: 20px; }
.tool-card {
background: var(--bg-card); border: 1px solid var(--border);
border-radius: var(--radius); padding: 16px; cursor: pointer; transition: all 0.15s;
}
.tool-card:hover { border-color: var(--accent); transform: translateY(-1px); }
.tool-card h4 { margin-bottom: 4px; font-size: 0.9rem; }
.tool-card p { color: var(--text-secondary); font-size: 0.8rem; margin-bottom: 10px; }
.tool-card .btn { width: 100%; }
.tool-card .tool-result { margin-top: 12px; display: none; }
.tool-card .tool-result.visible { display: block; }
/* Output panel (terminal-like) */
.output-panel {
background: var(--bg-primary); border: 1px solid var(--border);
border-radius: var(--radius); padding: 14px; font-family: monospace;
font-size: 0.82rem; white-space: pre-wrap; word-break: break-all;
color: var(--text-primary); line-height: 1.5; min-height: 40px;
}
.output-panel.scrollable { max-height: 400px; overflow-y: auto; }
.output-panel:empty::after { content: 'No output yet.'; color: var(--text-muted); }
/* Tab bar */
.tab-bar { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 16px; }
.tab {
padding: 8px 16px; font-size: 0.85rem; color: var(--text-secondary);
cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.15s;
background: none; border-top: none; border-left: none; border-right: none;
}
.tab:hover { color: var(--text-primary); }
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
.tab-content { display: none; }
.tab-content.active { display: block; }
/* Severity/status badges */
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; font-weight: 500; }
.badge-high { background: rgba(239,68,68,0.2); color: var(--danger); }
.badge-medium { background: rgba(245,158,11,0.2); color: var(--warning); }
.badge-low { background: rgba(99,102,241,0.2); color: var(--accent); }
.badge-pass { background: rgba(34,197,94,0.2); color: var(--success); }
.badge-fail { background: rgba(239,68,68,0.2); color: var(--danger); }
.badge-info { background: rgba(6,182,212,0.2); color: var(--analyze); }
/* Score display */
.score-display { text-align: center; padding: 20px; }
.score-value { font-size: 3rem; font-weight: 700; line-height: 1; }
.score-label { font-size: 0.85rem; color: var(--text-secondary); margin-top: 4px; }
/* Tool actions row */
.tool-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
/* Inline form row (like search bars inside sections) */
.input-row { display: flex; gap: 8px; margin-bottom: 12px; align-items: flex-end; }
.input-row input, .input-row select { flex: 1; padding: 8px 12px; background: var(--bg-input); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text-primary); font-size: 0.9rem; }
.input-row input:focus, .input-row select:focus { outline: none; border-color: var(--accent); }
.input-row .btn { white-space: nowrap; }
/* Threat list items */
.threat-item { padding: 10px 12px; border-bottom: 1px solid var(--border); display: flex; gap: 10px; align-items: flex-start; }
.threat-item:last-child { border-bottom: none; }
.threat-item .badge { flex-shrink: 0; margin-top: 2px; }
.threat-message { font-size: 0.85rem; }
.threat-category { font-size: 0.75rem; color: var(--text-muted); }
/* Status indicator */
.status-indicator { display: flex; align-items: center; gap: 6px; font-size: 0.9rem; }
.status-indicator .status-dot { margin-right: 0; }
/* Copy button for payloads */
.payload-item { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid var(--border); font-family: monospace; font-size: 0.82rem; }
.payload-item:last-child { border-bottom: none; }
.payload-item code { flex: 1; margin-right: 8px; }
/* OSINT Category Checkbox Grid */
.category-checkboxes {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 6px;
max-height: 200px;
overflow-y: auto;
padding: 8px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius);
}
.category-checkboxes label {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.8rem;
color: var(--text-secondary);
cursor: pointer;
padding: 4px 6px;
border-radius: 4px;
}
.category-checkboxes label:hover { background: var(--bg-card); color: var(--text-primary); }
.category-checkboxes .cat-count { color: var(--text-muted); font-size: 0.72rem; }
.cat-select-all { margin-bottom: 6px; font-size: 0.8rem; }
/* Advanced Options Panel */
.advanced-panel { margin-top: 12px; }
.advanced-toggle {
font-size: 0.82rem;
color: var(--text-secondary);
cursor: pointer;
user-select: none;
display: inline-flex;
align-items: center;
gap: 4px;
}
.advanced-toggle:hover { color: var(--text-primary); }
.advanced-toggle .arrow { transition: transform 0.2s; display: inline-block; }
.advanced-toggle .arrow.open { transform: rotate(90deg); }
.advanced-body {
display: none;
margin-top: 10px;
padding: 14px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius);
}
.advanced-body.visible { display: block; }
.advanced-body .form-row { margin-bottom: 10px; }
.advanced-body .form-row:last-child { margin-bottom: 0; }
/* Confidence Bar */
.confidence-bar {
display: inline-block;
width: 60px;
height: 6px;
background: var(--bg-input);
border-radius: 3px;
overflow: hidden;
vertical-align: middle;
margin-left: 6px;
}
.confidence-bar .fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s;
}
.confidence-bar .fill.high { background: var(--success); }
.confidence-bar .fill.medium { background: var(--warning); }
.confidence-bar .fill.low { background: var(--danger); }
/* OSINT Result Cards Enhanced */
.result-card .result-detail {
display: none;
margin-top: 6px;
padding: 8px;
background: var(--bg-primary);
border-radius: 4px;
font-size: 0.78rem;
color: var(--text-secondary);
}
.result-card .result-detail.visible { display: block; }
.result-card.found { cursor: pointer; }
.result-card.maybe { border-left: 3px solid var(--warning); cursor: pointer; }
.result-card.filtered { border-left: 3px solid var(--text-muted); opacity: 0.5; }
.result-card.restricted { border-left: 3px solid var(--counter); }
/* Result Filters */
.result-filters { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; flex-wrap: wrap; }
.result-filters .filter-btn {
padding: 4px 10px;
font-size: 0.78rem;
border-radius: 12px;
border: 1px solid var(--border);
background: var(--bg-card);
color: var(--text-secondary);
cursor: pointer;
}
.result-filters .filter-btn.active { border-color: var(--accent); color: var(--accent); background: rgba(99,102,241,0.1); }
.result-filters .filter-btn:hover { color: var(--text-primary); }
/* Summary Stats Bar */
.osint-summary {
display: flex;
gap: 20px;
padding: 12px 16px;
background: var(--bg-card);
border-radius: var(--radius);
margin-bottom: 16px;
flex-wrap: wrap;
}
.osint-summary .stat {
font-size: 0.82rem;
}
.osint-summary .stat-num { font-weight: 600; margin-right: 4px; }
.osint-summary .stat-label { color: var(--text-secondary); }
/* Dossier Cards */
.dossier-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
.dossier-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px;
transition: border-color 0.15s;
}
.dossier-card:hover { border-color: var(--accent); }
.dossier-card h4 { margin-bottom: 6px; font-size: 0.9rem; }
.dossier-card .dossier-meta { font-size: 0.78rem; color: var(--text-muted); margin-bottom: 8px; }
.dossier-card .dossier-stats { font-size: 0.82rem; color: var(--text-secondary); }
.dossier-card .dossier-actions { display: flex; gap: 6px; margin-top: 10px; }
/* Stop button */
.btn-stop { background: rgba(239,68,68,0.2); border-color: var(--danger); color: var(--danger); }
.btn-stop:hover { background: rgba(239,68,68,0.3); }
/* Hardware */
.cat-hardware { border-left: 3px solid var(--hardware); }
.device-list { margin-bottom: 16px; }
.device-row {
display: flex; align-items: center; gap: 12px;
padding: 10px 12px; border-bottom: 1px solid var(--border);
cursor: pointer; transition: background 0.15s; font-size: 0.85rem;
}
.device-row:hover { background: var(--bg-card); }
.device-row.selected { background: rgba(99,102,241,0.08); border-left: 3px solid var(--accent); }
.device-row .device-serial { font-weight: 500; min-width: 120px; }
.device-row .device-model { color: var(--text-secondary); flex: 1; }
.device-row .device-state { font-size: 0.78rem; }
.device-actions { margin-top: 16px; }
.device-info-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 8px; margin-bottom: 16px;
}
.device-info-grid .info-item { font-size: 0.85rem; }
.device-info-grid .info-label { color: var(--text-muted); font-size: 0.75rem; text-transform: uppercase; }
.device-info-grid .info-value { font-weight: 500; }
.serial-monitor {
background: #0a0a0a; border: 1px solid var(--border);
border-radius: var(--radius); padding: 12px; font-family: monospace;
font-size: 0.78rem; color: #00ff41; max-height: 350px; overflow-y: auto;
white-space: pre-wrap; word-break: break-all; min-height: 150px;
}
.serial-input-row {
display: flex; gap: 8px; margin-top: 8px;
}
.serial-input-row input {
flex: 1; padding: 8px 12px; background: var(--bg-input);
border: 1px solid var(--border); border-radius: var(--radius);
color: var(--text-primary); font-family: monospace; font-size: 0.85rem;
}
.serial-input-row input:focus { outline: none; border-color: var(--accent); }
.hw-progress {
background: var(--bg-input); border-radius: 4px; height: 20px;
margin: 12px 0; overflow: hidden; position: relative;
}
.hw-progress-fill {
height: 100%; background: var(--accent); border-radius: 4px;
transition: width 0.3s; position: relative;
}
.hw-progress-text {
position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
font-size: 0.72rem; color: var(--text-primary); font-weight: 500;
}
.confirm-dialog {
background: var(--bg-card); border: 1px solid var(--danger);
border-radius: var(--radius); padding: 16px; margin: 12px 0;
}
.confirm-dialog p { font-size: 0.85rem; margin-bottom: 12px; color: var(--warning); }
/* Hardware Mode Switcher */
.hw-mode-bar {
display: flex; align-items: center; gap: 12px; margin-bottom: 16px;
padding: 12px 16px; background: var(--bg-card); border: 1px solid var(--border);
border-radius: var(--radius); flex-wrap: wrap;
}
.hw-mode-label { font-size: 0.85rem; color: var(--text-muted); font-weight: 500; }
.hw-mode-toggle { display: flex; gap: 0; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.hw-mode-btn {
background: var(--bg-input); border: none; padding: 8px 16px; cursor: pointer;
color: var(--text-muted); font-size: 0.82rem; display: flex; flex-direction: column;
align-items: center; gap: 2px; transition: all 0.2s;
}
.hw-mode-btn:hover { background: var(--bg-hover); }
.hw-mode-btn.active { background: var(--accent); color: #fff; }
.hw-mode-btn.active .hw-mode-desc { color: rgba(255,255,255,0.7); }
.hw-mode-desc { font-size: 0.68rem; color: var(--text-muted); }
.hw-mode-warning {
font-size: 0.82rem; color: var(--warning); padding: 8px 12px;
background: rgba(234,179,8,0.08); border: 1px solid rgba(234,179,8,0.2);
border-radius: var(--radius);
}
.hw-checkbox {
display: flex; align-items: center; gap: 8px; font-size: 0.85rem;
color: var(--text-primary); cursor: pointer;
}
.hw-checkbox input { accent-color: var(--accent); }
/* ── Monitor Popup/Modal ─────────────────────────────────────── */
.tmon-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6); z-index: 1200;
display: flex; align-items: center; justify-content: center;
opacity: 0; pointer-events: none; transition: opacity 0.15s;
}
.tmon-overlay.open { opacity: 1; pointer-events: auto; }
.tmon-popup {
background: var(--bg-secondary); border: 1px solid var(--border);
border-radius: 10px; box-shadow: 0 12px 48px rgba(0,0,0,0.6);
width: 780px; max-width: calc(100vw - 40px);
max-height: calc(100vh - 80px); display: flex; flex-direction: column;
overflow: hidden; transform: scale(0.95); transition: transform 0.15s;
}
.tmon-overlay.open .tmon-popup { transform: scale(1); }
.tmon-popup-header {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 18px; background: var(--bg-card); border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.tmon-popup-header h3 { font-size: 0.95rem; font-weight: 600; margin: 0; }
.tmon-popup-close {
background: none; border: none; color: var(--text-muted); font-size: 1.2rem;
cursor: pointer; padding: 4px 8px; border-radius: 4px; transition: color 0.15s;
}
.tmon-popup-close:hover { color: var(--danger); }
.tmon-popup-body {
flex: 1; overflow-y: auto; padding: 16px 18px;
}
.tmon-popup-body .data-table { width: 100%; font-size: 0.8rem; }
.tmon-popup-status {
font-size: 0.75rem; color: var(--text-muted); padding: 8px 18px;
border-top: 1px solid var(--border); flex-shrink: 0; text-align: right;
}
/* Clickable stat cells in live monitor */
.tmon-stat-clickable {
cursor: pointer; transition: color 0.15s;
}
.tmon-stat-clickable:hover {
color: var(--accent) !important; text-decoration: underline;
}
/* Connection detail card inside popup */
.tmon-detail-card {
background: var(--bg-card); border: 1px solid var(--border);
border-radius: var(--radius); padding: 14px 18px; margin-bottom: 12px;
}
.tmon-detail-card h4 { font-size: 0.88rem; margin-bottom: 10px; color: var(--accent); }
.tmon-detail-card table { width: 100%; font-size: 0.82rem; }
.tmon-detail-card td:first-child { color: var(--text-muted); width: 140px; padding: 3px 0; }
.tmon-detail-card td:last-child { color: var(--text-primary); padding: 3px 0; }
.tmon-row-clickable { cursor: pointer; transition: background 0.1s; }
.tmon-row-clickable:hover { background: var(--bg-card) !important; }
.tmon-back-btn {
background: none; border: 1px solid var(--border); color: var(--text-secondary);
padding: 4px 12px; border-radius: 4px; font-size: 0.78rem; cursor: pointer;
margin-bottom: 12px; transition: all 0.15s;
}
.tmon-back-btn:hover { color: var(--accent); border-color: var(--accent); }
/* Responsive */
@media (max-width: 768px) {
.sidebar { display: none; }
.content { margin-left: 0; padding: 16px; }
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.category-grid { grid-template-columns: 1fr; }
}
/* ── Stream output utility colors ────────────────────────────── */
.err { color: var(--danger, #ef4444); }
.success { color: #22c55e; }
.warn { color: #f97316; }
.info { color: #60a5fa; }
.dim { color: var(--text-secondary, #888); }
/* ── Agent Hal Chat Panel ──────────────────────────────────────── */
.hal-toggle-btn {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1100;
background: var(--accent);
color: #fff;
border: none;
border-radius: 20px;
padding: 8px 18px;
font-size: 0.85rem;
font-weight: 600;
letter-spacing: 0.05em;
cursor: pointer;
box-shadow: 0 2px 12px rgba(0,0,0,0.4);
transition: background 0.15s;
}
.hal-toggle-btn:hover { background: var(--accent-hover, #2563eb); }
.hal-panel {
position: fixed;
bottom: 64px;
right: 20px;
z-index: 1100;
width: 360px;
height: 480px;
background: var(--bg-card, #1a1a2e);
border: 1px solid var(--border, #2a2a3e);
border-radius: 10px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
display: flex;
flex-direction: column;
overflow: hidden;
}
.hal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
background: var(--bg-nav, #12122a);
border-bottom: 1px solid var(--border, #2a2a3e);
font-size: 0.85rem;
font-weight: 600;
color: var(--accent);
}
.hal-close {
background: none;
border: none;
color: var(--text-secondary, #888);
cursor: pointer;
font-size: 0.9rem;
padding: 2px 6px;
border-radius: 4px;
line-height: 1;
}
.hal-close:hover { color: var(--text, #e0e0e0); background: var(--bg-hover, #2a2a3e); }
/* Hal mode toggle switch */
.hal-mode-switch {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
user-select: none;
}
.hal-mode-switch input { display: none; }
.hal-mode-slider {
width: 28px;
height: 14px;
background: var(--bg-input, #2a2d3e);
border-radius: 7px;
position: relative;
transition: background 0.2s;
border: 1px solid var(--border, #333650);
}
.hal-mode-slider::after {
content: '';
position: absolute;
width: 10px;
height: 10px;
background: var(--text-secondary, #888);
border-radius: 50%;
top: 1px;
left: 1px;
transition: transform 0.2s, background 0.2s;
}
.hal-mode-switch input:checked + .hal-mode-slider {
background: var(--accent, #6366f1);
border-color: var(--accent, #6366f1);
}
.hal-mode-switch input:checked + .hal-mode-slider::after {
transform: translateX(14px);
background: #fff;
}
.hal-mode-label {
font-size: 0.7rem;
font-weight: 500;
color: var(--text-secondary, #888);
min-width: 36px;
}
.hal-messages {
flex: 1;
overflow-y: auto;
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.hal-msg {
padding: 7px 10px;
border-radius: 8px;
font-size: 0.82rem;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
max-width: 92%;
}
.hal-msg-user {
background: var(--accent);
color: #fff;
align-self: flex-end;
border-bottom-right-radius: 2px;
}
.hal-msg-bot {
background: var(--bg-hover, #2a2a3e);
color: var(--text, #e0e0e0);
align-self: flex-start;
border-bottom-left-radius: 2px;
}
.hal-footer {
display: flex;
gap: 6px;
padding: 8px 10px;
border-top: 1px solid var(--border, #2a2a3e);
background: var(--bg-nav, #12122a);
}
.hal-footer input {
flex: 1;
font-size: 0.82rem;
padding: 5px 9px;
background: var(--bg-input, #0d0d1a);
border: 1px solid var(--border, #2a2a3e);
border-radius: 6px;
color: var(--text, #e0e0e0);
}
.hal-footer input:focus { outline: none; border-color: var(--accent); }
/* ── Debug Console Window ─────────────────────────────────────── */
.debug-toggle-btn {
position: fixed;
bottom: 56px;
right: 20px;
z-index: 1100;
background: #1a1a1a;
color: #22c55e;
border: 1px solid #22c55e;
border-radius: 6px;
padding: 5px 12px;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.1em;
cursor: pointer;
font-family: monospace;
box-shadow: 0 0 8px rgba(34, 197, 94, 0.25);
transition: box-shadow 0.15s;
}
.debug-toggle-btn:hover { box-shadow: 0 0 14px rgba(34, 197, 94, 0.5); }
.debug-panel {
position: fixed;
top: 60px;
right: 20px;
z-index: 1050;
width: 680px;
max-width: calc(100vw - 40px);
height: 480px;
background: #0a0a0a;
border: 1px solid #1e3a1e;
border-radius: 8px;
box-shadow: 0 8px 40px rgba(0,0,0,0.7), 0 0 20px rgba(34, 197, 94, 0.08);
display: flex;
flex-direction: column;
overflow: hidden;
resize: both;
}
.debug-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 7px 12px;
background: #0d1a0d;
border-bottom: 1px solid #1e3a1e;
cursor: grab;
user-select: none;
flex-shrink: 0;
}
.debug-header:active { cursor: grabbing; }
.debug-live-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #333;
flex-shrink: 0;
transition: background 0.3s;
}
.debug-live-dot.debug-live-active {
background: #22c55e;
box-shadow: 0 0 6px rgba(34, 197, 94, 0.8);
animation: debug-pulse 1.8s ease-in-out infinite;
}
@keyframes debug-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.debug-btn {
background: none;
border: none;
color: #555;
cursor: pointer;
font-size: 0.85rem;
padding: 2px 6px;
border-radius: 4px;
line-height: 1;
}
.debug-btn:hover { color: #22c55e; background: #1a2a1a; }
.debug-output {
flex: 1;
overflow-y: auto;
padding: 6px 10px;
font-family: 'Consolas', 'Fira Code', 'Courier New', monospace;
font-size: 0.78rem;
line-height: 1.55;
color: #c0c0c0;
scrollbar-width: thin;
scrollbar-color: #1e3a1e #080808;
}
.debug-output::-webkit-scrollbar { width: 6px; }
.debug-output::-webkit-scrollbar-thumb { background: #1e3a1e; border-radius: 3px; }
.debug-line { white-space: pre-wrap; word-break: break-all; padding: 1px 0; }
.dbg-debug { color: #555; }
.dbg-info { color: #60a5fa; }
.dbg-warn { color: #f97316; }
.dbg-err { color: #ef4444; }
.dbg-crit { color: #ef4444; font-weight: 700; text-shadow: 0 0 8px rgba(239,68,68,0.5); }
.debug-controls {
display: flex;
align-items: center;
gap: 0;
padding: 6px 10px;
background: #080808;
border-top: 1px solid #1e3a1e;
flex-wrap: wrap;
gap: 4px;
flex-shrink: 0;
}
.debug-check-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.7rem;
font-family: monospace;
color: #666;
cursor: pointer;
padding: 3px 8px;
border-radius: 4px;
border: 1px solid #1a2a1a;
transition: color 0.15s, border-color 0.15s, background 0.15s;
white-space: nowrap;
}
.debug-check-label:hover { color: #22c55e; border-color: #22c55e; background: #0d1a0d; }
.debug-check-label input { accent-color: #22c55e; cursor: pointer; margin: 0; }
.debug-check-label:has(input:checked) {
color: #22c55e;
border-color: #22c55e;
background: #0d1a0d;
}

2434
web/static/js/app.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,817 @@
/**
* AUTARCH Hardware Direct Mode
* Browser-based device access via WebUSB (ADB/Fastboot) and Web Serial (ESP32)
*
* Dependencies (loaded before this file):
* - web/static/js/lib/adb-bundle.js → window.YumeAdb
* - web/static/js/lib/fastboot-bundle.js → window.Fastboot
* - web/static/js/lib/esptool-bundle.js → window.EspTool
*/
var HWDirect = (function() {
'use strict';
// ── Browser Capability Detection ─────────────────────────────
var supported = {
webusb: typeof navigator !== 'undefined' && !!navigator.usb,
webserial: typeof navigator !== 'undefined' && !!navigator.serial,
};
// ── State ────────────────────────────────────────────────────
var adbDevice = null; // Adb instance (ya-webadb)
var adbTransport = null; // AdbDaemonTransport
var adbUsbDevice = null; // AdbDaemonWebUsbDevice (for disconnect)
var adbDeviceInfo = {}; // Cached device props
var fbDevice = null; // FastbootDevice instance
var espLoader = null; // ESPLoader instance
var espTransport = null; // Web Serial Transport
var espPort = null; // SerialPort reference
var espMonitorReader = null;
var espMonitorRunning = false;
// ── ADB Credential Store (IndexedDB) ─────────────────────────
// ADB requires RSA key authentication. Keys are stored in IndexedDB
// so the user only needs to authorize once per browser.
var DB_NAME = 'autarch_adb_keys';
var STORE_NAME = 'keys';
function _openKeyDB() {
return new Promise(function(resolve, reject) {
var req = indexedDB.open(DB_NAME, 1);
req.onupgradeneeded = function(e) {
e.target.result.createObjectStore(STORE_NAME, { keyPath: 'id' });
};
req.onsuccess = function(e) { resolve(e.target.result); };
req.onerror = function(e) { reject(e.target.error); };
});
}
function _saveKey(id, privateKey) {
return _openKeyDB().then(function(db) {
return new Promise(function(resolve, reject) {
var tx = db.transaction(STORE_NAME, 'readwrite');
tx.objectStore(STORE_NAME).put({ id: id, key: privateKey });
tx.oncomplete = function() { resolve(); };
tx.onerror = function(e) { reject(e.target.error); };
});
});
}
function _loadKeys() {
return _openKeyDB().then(function(db) {
return new Promise(function(resolve, reject) {
var tx = db.transaction(STORE_NAME, 'readonly');
var req = tx.objectStore(STORE_NAME).getAll();
req.onsuccess = function() { resolve(req.result || []); };
req.onerror = function(e) { reject(e.target.error); };
});
});
}
// Web Crypto RSA key generation for ADB authentication
var credentialStore = {
generateKey: async function() {
var keyPair = await crypto.subtle.generateKey(
{ name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-1' },
true, ['sign']
);
var exported = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
var keyBuffer = new Uint8Array(exported);
var keyId = 'adb_key_' + Date.now();
await _saveKey(keyId, Array.from(keyBuffer));
return { buffer: keyBuffer, name: 'autarch@browser' };
},
iterateKeys: async function*() {
var keys = await _loadKeys();
for (var i = 0; i < keys.length; i++) {
yield { buffer: new Uint8Array(keys[i].key), name: 'autarch@browser' };
}
}
};
// ══════════════════════════════════════════════════════════════
// ADB (WebUSB)
// ══════════════════════════════════════════════════════════════
/**
* Get list of already-paired ADB devices (no permission prompt).
* Returns array of {serial, name, raw}.
*/
async function adbGetDevices() {
if (!supported.webusb) return [];
var manager = YumeAdb.AdbDaemonWebUsbDeviceManager.BROWSER;
if (!manager) return [];
try {
var devices = await manager.getDevices();
return devices.map(function(d) {
return { serial: d.serial || '', name: d.name || '', raw: d };
});
} catch (e) {
console.error('adbGetDevices:', e);
return [];
}
}
/**
* Request user to select an ADB device (shows browser USB picker).
* Returns {serial, name, raw} or null.
*/
async function adbRequestDevice() {
if (!supported.webusb) throw new Error('WebUSB not supported in this browser');
var manager = YumeAdb.AdbDaemonWebUsbDeviceManager.BROWSER;
if (!manager) throw new Error('WebUSB manager not available');
var device = await manager.requestDevice();
if (!device) return null;
return { serial: device.serial || '', name: device.name || '', raw: device };
}
/**
* Connect to an ADB device (from adbGetDevices or adbRequestDevice).
* Performs USB connection + ADB authentication.
* The device screen will show "Allow USB debugging?" on first connect.
*/
async function adbConnect(deviceObj) {
if (adbDevice) {
await adbDisconnect();
}
var usbDev = deviceObj.raw;
var connection;
try {
connection = await usbDev.connect();
} catch (e) {
var errMsg = e.message || String(e);
var errLow = errMsg.toLowerCase();
// Windows: USB driver conflict — ADB server or another app owns the device
if (errLow.includes('already in used') || errLow.includes('already in use') ||
errLow.includes('in use by another') || errLow.includes('access denied')) {
// Try forcing a release and retrying once
try { await usbDev.close(); } catch (_) {}
try {
connection = await usbDev.connect();
} catch (e2) {
throw new Error(
'USB device is claimed by another program (usually the ADB server).\n\n' +
'Fix: open a terminal and run:\n' +
' adb kill-server\n\n' +
'Then close Android Studio, scrcpy, or any other ADB tool, and click Connect again.'
);
}
} else if (errLow.includes('permission') || errLow.includes('not allowed') ||
errLow.includes('failed to open')) {
throw new Error(
'USB permission denied. On Linux, ensure udev rules are installed:\n' +
' sudo bash -c "echo \'SUBSYSTEM==\"usb\", ATTR{idVendor}==\"18d1\", MODE=\"0666\"\' > /etc/udev/rules.d/99-android.rules"\n' +
' sudo udevadm control --reload-rules'
);
} else {
throw new Error(
'USB connect failed: ' + errMsg + '\n\n' +
'Make sure the device screen is unlocked and USB debugging is enabled in Developer Options.'
);
}
}
try {
adbTransport = await YumeAdb.AdbDaemonTransport.authenticate({
serial: deviceObj.serial,
connection: connection,
credentialStore: credentialStore,
});
} catch (e) {
var msg = e.message || String(e);
if (msg.toLowerCase().includes('auth') || msg.toLowerCase().includes('denied')) {
throw new Error('ADB auth denied — tap "Allow USB Debugging?" on the device screen, then click Connect again.');
}
throw new Error('ADB authentication failed: ' + msg);
}
adbDevice = new YumeAdb.Adb(adbTransport);
adbUsbDevice = usbDev;
adbDeviceInfo = {};
return true;
}
/**
* Run a shell command on the connected ADB device.
* Returns {stdout, stderr, exitCode} or {output} for legacy protocol.
*/
async function adbShell(cmd) {
if (!adbDevice) throw new Error('No ADB device connected');
try {
// Prefer shell v2 protocol (separate stdout/stderr + exit code)
if (adbDevice.subprocess && adbDevice.subprocess.shellProtocol) {
var result = await adbDevice.subprocess.shellProtocol.spawnWaitText(cmd);
return {
stdout: result.stdout || '',
stderr: result.stderr || '',
exitCode: result.exitCode,
output: result.stdout || ''
};
}
// Fallback: none protocol (mixed output)
if (adbDevice.subprocess && adbDevice.subprocess.noneProtocol) {
var output = await adbDevice.subprocess.noneProtocol.spawnWaitText(cmd);
return { output: output, stdout: output, stderr: '', exitCode: 0 };
}
throw new Error('No subprocess protocol available');
} catch (e) {
return { output: '', stdout: '', stderr: e.message, exitCode: -1, error: e.message };
}
}
/**
* Get device info (model, brand, android version, etc.).
* Returns object with property key-value pairs.
*/
async function adbGetInfo() {
if (!adbDevice) throw new Error('No ADB device connected');
var props = [
['model', 'ro.product.model'],
['brand', 'ro.product.brand'],
['device', 'ro.product.device'],
['manufacturer', 'ro.product.manufacturer'],
['android_version', 'ro.build.version.release'],
['sdk', 'ro.build.version.sdk'],
['build', 'ro.build.display.id'],
['security_patch', 'ro.build.version.security_patch'],
['cpu_abi', 'ro.product.cpu.abi'],
['serialno', 'ro.serialno'],
];
var info = {};
for (var i = 0; i < props.length; i++) {
try {
var result = await adbShell('getprop ' + props[i][1]);
info[props[i][0]] = (result.stdout || result.output || '').trim();
} catch (e) {
info[props[i][0]] = '';
}
}
// Battery info
try {
var batt = await adbShell('dumpsys battery');
var battOut = batt.stdout || batt.output || '';
var levelMatch = battOut.match(/level:\s*(\d+)/);
var statusMatch = battOut.match(/status:\s*(\d+)/);
if (levelMatch) info.battery = levelMatch[1] + '%';
if (statusMatch) {
var statuses = {1:'Unknown', 2:'Charging', 3:'Discharging', 4:'Not charging', 5:'Full'};
info.battery_status = statuses[statusMatch[1]] || 'Unknown';
}
} catch (e) {}
// Storage info
try {
var df = await adbShell('df /data');
var dfOut = df.stdout || df.output || '';
var lines = dfOut.trim().split('\n');
if (lines.length >= 2) {
var parts = lines[1].trim().split(/\s+/);
if (parts.length >= 4) {
info.storage_total = parts[1];
info.storage_used = parts[2];
info.storage_free = parts[3];
}
}
} catch (e) {}
adbDeviceInfo = info;
return info;
}
/**
* Reboot ADB device to specified mode.
*/
async function adbReboot(mode) {
if (!adbDevice) throw new Error('No ADB device connected');
if (!adbDevice.power) throw new Error('Power commands not available');
switch (mode) {
case 'system': await adbDevice.power.reboot(); break;
case 'bootloader': await adbDevice.power.bootloader(); break;
case 'recovery': await adbDevice.power.recovery(); break;
case 'fastboot': await adbDevice.power.fastboot(); break;
case 'sideload': await adbDevice.power.sideload(); break;
default: await adbDevice.power.reboot(); break;
}
adbDevice = null;
adbTransport = null;
return { success: true };
}
/**
* Install APK on connected device.
* @param {Blob|File} blob - APK file
*/
async function adbInstall(blob) {
if (!adbDevice) throw new Error('No ADB device connected');
// Push to temp location then pm install
var tmpPath = '/data/local/tmp/autarch_install.apk';
await adbPush(blob, tmpPath);
var result = await adbShell('pm install -r ' + tmpPath);
await adbShell('rm ' + tmpPath);
return result;
}
/**
* Push a file to the device.
* @param {Blob|File|Uint8Array} data - File data
* @param {string} remotePath - Destination path on device
*/
async function adbPush(data, remotePath) {
if (!adbDevice) throw new Error('No ADB device connected');
var sync = await adbDevice.sync();
try {
var bytes;
if (data instanceof Uint8Array) {
bytes = data;
} else if (data instanceof Blob) {
var ab = await data.arrayBuffer();
bytes = new Uint8Array(ab);
} else {
throw new Error('Unsupported data type');
}
var stream = new ReadableStream({
start: function(controller) {
controller.enqueue(bytes);
controller.close();
}
});
await sync.write({
filename: remotePath,
file: stream,
permission: 0o644,
mtime: Math.floor(Date.now() / 1000),
});
return { success: true };
} finally {
await sync.dispose();
}
}
/**
* Pull a file from the device.
* Returns Blob.
*/
async function adbPull(remotePath) {
if (!adbDevice) throw new Error('No ADB device connected');
var sync = await adbDevice.sync();
try {
var readable = sync.read(remotePath);
var reader = readable.getReader();
var chunks = [];
while (true) {
var result = await reader.read();
if (result.done) break;
chunks.push(result.value);
}
var totalLen = chunks.reduce(function(s, c) { return s + c.length; }, 0);
var combined = new Uint8Array(totalLen);
var offset = 0;
for (var i = 0; i < chunks.length; i++) {
combined.set(chunks[i], offset);
offset += chunks[i].length;
}
return new Blob([combined]);
} finally {
await sync.dispose();
}
}
/**
* Get logcat output.
*/
async function adbLogcat(lines) {
lines = lines || 100;
return await adbShell('logcat -d -t ' + lines);
}
/**
* Disconnect from ADB device.
*/
async function adbDisconnect() {
if (adbDevice) {
try { await adbDevice.close(); } catch (e) {}
}
if (adbUsbDevice) {
// Release the USB interface so Windows won't block the next connect()
try { await adbUsbDevice.close(); } catch (e) {}
}
adbDevice = null;
adbTransport = null;
adbUsbDevice = null;
adbDeviceInfo = {};
}
/**
* Check if ADB device is connected.
*/
function adbIsConnected() {
return adbDevice !== null;
}
/**
* Get a human-readable label for the connected ADB device.
* Returns "Model (serial)" or "Connected device" if info not yet fetched.
*/
function adbGetDeviceLabel() {
if (!adbDevice) return 'Not connected';
var model = adbDeviceInfo.model || '';
var serial = adbUsbDevice ? (adbUsbDevice.serial || '') : '';
if (model && serial) return model + ' (' + serial + ')';
if (model) return model;
if (serial) return serial;
return 'Connected device';
}
// ══════════════════════════════════════════════════════════════
// FASTBOOT (WebUSB)
// ══════════════════════════════════════════════════════════════
/**
* Connect to a fastboot device (shows browser USB picker).
*/
async function fbConnect() {
if (!supported.webusb) throw new Error('WebUSB not supported');
if (fbDevice) await fbDisconnect();
fbDevice = new Fastboot.FastbootDevice();
await fbDevice.connect();
return true;
}
/**
* Get fastboot device info (getvar queries).
*/
async function fbGetInfo() {
if (!fbDevice) throw new Error('No fastboot device connected');
var vars = ['product', 'variant', 'serialno', 'secure', 'unlocked',
'current-slot', 'max-download-size', 'battery-voltage',
'battery-soc-ok', 'hw-revision', 'version-bootloader',
'version-baseband', 'off-mode-charge'];
var info = {};
for (var i = 0; i < vars.length; i++) {
try {
info[vars[i]] = await fbDevice.getVariable(vars[i]);
} catch (e) {
info[vars[i]] = '';
}
}
return info;
}
/**
* Flash a partition.
* @param {string} partition - Partition name (boot, recovery, system, etc.)
* @param {Blob|File} blob - Firmware image
* @param {function} progressCb - Called with (progress: 0-1)
*/
async function fbFlash(partition, blob, progressCb) {
if (!fbDevice) throw new Error('No fastboot device connected');
var callback = progressCb || function() {};
await fbDevice.flashBlob(partition, blob, function(progress) {
callback(progress);
});
return { success: true };
}
/**
* Reboot fastboot device.
*/
async function fbReboot(mode) {
if (!fbDevice) throw new Error('No fastboot device connected');
switch (mode) {
case 'bootloader': await fbDevice.reboot('bootloader'); break;
case 'recovery': await fbDevice.reboot('recovery'); break;
default: await fbDevice.reboot(); break;
}
fbDevice = null;
return { success: true };
}
/**
* OEM unlock the bootloader.
*/
async function fbOemUnlock() {
if (!fbDevice) throw new Error('No fastboot device connected');
await fbDevice.runCommand('oem unlock');
return { success: true };
}
/**
* Flash a factory image ZIP (PixelFlasher-style).
* Parses the ZIP, identifies partitions, flashes in sequence.
* @param {Blob|File} zipBlob - Factory image ZIP file
* @param {object} options - Flash options
* @param {function} progressCb - Called with {stage, partition, progress, message}
*/
async function fbFactoryFlash(zipBlob, options, progressCb) {
if (!fbDevice) throw new Error('No fastboot device connected');
options = options || {};
var callback = progressCb || function() {};
callback({ stage: 'init', message: 'Reading factory image ZIP...' });
try {
// Use fastboot.js built-in factory flash support
await fbDevice.flashFactoryZip(zipBlob, !options.wipeData, function(action, item, progress) {
if (action === 'unpack') {
callback({ stage: 'unpack', partition: item, progress: progress,
message: 'Unpacking: ' + item });
} else if (action === 'flash') {
callback({ stage: 'flash', partition: item, progress: progress,
message: 'Flashing: ' + item + ' (' + Math.round(progress * 100) + '%)' });
} else if (action === 'reboot') {
callback({ stage: 'reboot', message: 'Rebooting...' });
}
});
callback({ stage: 'done', message: 'Factory flash complete' });
return { success: true };
} catch (e) {
callback({ stage: 'error', message: 'Flash failed: ' + e.message });
return { success: false, error: e.message };
}
}
/**
* Disconnect from fastboot device.
*/
async function fbDisconnect() {
if (fbDevice) {
try { await fbDevice.disconnect(); } catch (e) {}
}
fbDevice = null;
}
function fbIsConnected() {
return fbDevice !== null;
}
// ══════════════════════════════════════════════════════════════
// ESP32 (Web Serial)
// ══════════════════════════════════════════════════════════════
/**
* Request a serial port (shows browser serial picker).
* Returns port reference for later use.
*/
async function espRequestPort() {
if (!supported.webserial) throw new Error('Web Serial not supported');
espPort = await navigator.serial.requestPort({
filters: [
{ usbVendorId: 0x10C4 }, // Silicon Labs CP210x
{ usbVendorId: 0x1A86 }, // QinHeng CH340
{ usbVendorId: 0x0403 }, // FTDI
{ usbVendorId: 0x303A }, // Espressif USB-JTAG
]
});
return espPort;
}
/**
* Connect to ESP32 and detect chip.
* @param {number} baud - Baud rate (default 115200)
*/
async function espConnect(baud) {
if (!espPort) throw new Error('No serial port selected. Call espRequestPort() first.');
baud = baud || 115200;
if (espTransport) await espDisconnect();
await espPort.open({ baudRate: baud });
espTransport = new EspTool.Transport(espPort);
espLoader = new EspTool.ESPLoader({
transport: espTransport,
baudrate: baud,
romBaudrate: 115200,
});
// Connect and detect chip
await espLoader.main();
return {
success: true,
chip: espLoader.chipName || 'Unknown',
};
}
/**
* Get detected chip info.
*/
function espGetChipInfo() {
if (!espLoader) return null;
return {
chip: espLoader.chipName || 'Unknown',
features: espLoader.chipFeatures || [],
mac: espLoader.macAddr || '',
};
}
/**
* Flash firmware to ESP32.
* @param {Array} fileArray - [{data: Uint8Array|string, address: number}, ...]
* @param {function} progressCb - Called with (fileIndex, written, total)
*/
async function espFlash(fileArray, progressCb) {
if (!espLoader) throw new Error('No ESP32 connected. Call espConnect() first.');
var callback = progressCb || function() {};
await espLoader.writeFlash({
fileArray: fileArray,
flashSize: 'keep',
flashMode: 'keep',
flashFreq: 'keep',
eraseAll: false,
compress: true,
reportProgress: function(fileIndex, written, total) {
callback(fileIndex, written, total);
},
});
return { success: true };
}
/**
* Start serial monitor on the connected port.
* @param {number} baud - Baud rate (default 115200)
* @param {function} outputCb - Called with each line of output
*/
async function espMonitorStart(baud, outputCb) {
if (!espPort) throw new Error('No serial port selected');
baud = baud || 115200;
// If loader is connected, disconnect it first but keep port open
if (espLoader) {
try { await espLoader.hardReset(); } catch (e) {}
espLoader = null;
espTransport = null;
}
// Close and reopen at monitor baud rate
try { await espPort.close(); } catch (e) {}
await espPort.open({ baudRate: baud });
espMonitorRunning = true;
var decoder = new TextDecoderStream();
espPort.readable.pipeTo(decoder.writable).catch(function(e) {
if (espMonitorRunning) console.error('espMonitor pipeTo:', e);
});
espMonitorReader = decoder.readable.getReader();
(async function readLoop() {
try {
while (espMonitorRunning) {
var result = await espMonitorReader.read();
if (result.done) break;
if (result.value && outputCb) {
outputCb(result.value);
}
}
} catch (e) {
if (espMonitorRunning) {
outputCb('[Monitor error: ' + e.message + ']');
}
}
})();
return { success: true };
}
/**
* Send data to serial monitor.
*/
async function espMonitorSend(data) {
if (!espPort || !espPort.writable) throw new Error('No serial monitor active');
var writer = espPort.writable.getWriter();
try {
var encoded = new TextEncoder().encode(data + '\n');
await writer.write(encoded);
} finally {
writer.releaseLock();
}
}
/**
* Stop serial monitor.
*/
async function espMonitorStop() {
espMonitorRunning = false;
if (espMonitorReader) {
try { await espMonitorReader.cancel(); } catch (e) {}
espMonitorReader = null;
}
}
/**
* Disconnect ESP32 and close serial port.
*/
async function espDisconnect() {
espMonitorRunning = false;
if (espMonitorReader) {
try { await espMonitorReader.cancel(); } catch (e) {}
espMonitorReader = null;
}
espLoader = null;
espTransport = null;
if (espPort) {
try { await espPort.close(); } catch (e) {}
espPort = null;
}
}
function espIsConnected() {
return espLoader !== null || espMonitorRunning;
}
// ══════════════════════════════════════════════════════════════
// Utility
// ══════════════════════════════════════════════════════════════
/**
* Read a local file as Uint8Array (from <input type="file">).
*/
function readFileAsBytes(file) {
return new Promise(function(resolve, reject) {
var reader = new FileReader();
reader.onload = function() { resolve(new Uint8Array(reader.result)); };
reader.onerror = function() { reject(reader.error); };
reader.readAsArrayBuffer(file);
});
}
/**
* Read a local file as text.
*/
function readFileAsText(file) {
return new Promise(function(resolve, reject) {
var reader = new FileReader();
reader.onload = function() { resolve(reader.result); };
reader.onerror = function() { reject(reader.error); };
reader.readAsText(file);
});
}
/**
* Download data as a file to the user's machine.
*/
function downloadBlob(blob, filename) {
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
// ── Public API ────────────────────────────────────────────────
return {
supported: supported,
// ADB
adbGetDevices: adbGetDevices,
adbRequestDevice: adbRequestDevice,
adbConnect: adbConnect,
adbShell: adbShell,
adbGetInfo: adbGetInfo,
adbReboot: adbReboot,
adbInstall: adbInstall,
adbPush: adbPush,
adbPull: adbPull,
adbLogcat: adbLogcat,
adbDisconnect: adbDisconnect,
adbIsConnected: adbIsConnected,
adbGetDeviceLabel: adbGetDeviceLabel,
// Fastboot
fbConnect: fbConnect,
fbGetInfo: fbGetInfo,
fbFlash: fbFlash,
fbReboot: fbReboot,
fbOemUnlock: fbOemUnlock,
fbFactoryFlash: fbFactoryFlash,
fbDisconnect: fbDisconnect,
fbIsConnected: fbIsConnected,
// ESP32
espRequestPort: espRequestPort,
espConnect: espConnect,
espGetChipInfo: espGetChipInfo,
espFlash: espFlash,
espMonitorStart: espMonitorStart,
espMonitorSend: espMonitorSend,
espMonitorStop: espMonitorStop,
espDisconnect: espDisconnect,
espIsConnected: espIsConnected,
// Utility
readFileAsBytes: readFileAsBytes,
readFileAsText: readFileAsText,
downloadBlob: downloadBlob,
};
})();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long