1214 lines
37 KiB
HTML
1214 lines
37 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||
|
|
<title>Driver Manager</title>
|
||
|
|
<style>
|
||
|
|
:root {
|
||
|
|
--bg: #0f0f1a;
|
||
|
|
--surface: #1a1a2e;
|
||
|
|
--surface2: #16213e;
|
||
|
|
--accent: #4f6df5;
|
||
|
|
--accent-dim: #3a52b5;
|
||
|
|
--danger: #e94560;
|
||
|
|
--success: #2ecc71;
|
||
|
|
--warning: #f39c12;
|
||
|
|
--text: #e8e8f0;
|
||
|
|
--text-dim: #8888a0;
|
||
|
|
--border: #2a2a40;
|
||
|
|
--radius: 12px;
|
||
|
|
--radius-sm: 8px;
|
||
|
|
}
|
||
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
|
body {
|
||
|
|
font-family: -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||
|
|
background: var(--bg);
|
||
|
|
color: var(--text);
|
||
|
|
min-height: 100vh;
|
||
|
|
overflow-x: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- Header --- */
|
||
|
|
.header {
|
||
|
|
background: var(--surface);
|
||
|
|
padding: 16px 20px;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 12px;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
position: sticky;
|
||
|
|
top: 0;
|
||
|
|
z-index: 100;
|
||
|
|
}
|
||
|
|
.header-icon {
|
||
|
|
width: 36px; height: 36px;
|
||
|
|
background: var(--accent);
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
display: flex; align-items: center; justify-content: center;
|
||
|
|
font-size: 18px; font-weight: 700;
|
||
|
|
}
|
||
|
|
.header h1 { font-size: 18px; font-weight: 600; flex: 1; }
|
||
|
|
.header-version {
|
||
|
|
font-size: 12px; color: var(--text-dim);
|
||
|
|
background: var(--surface2);
|
||
|
|
padding: 4px 8px;
|
||
|
|
border-radius: 4px;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- Navigation --- */
|
||
|
|
.nav {
|
||
|
|
display: flex;
|
||
|
|
background: var(--surface);
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
overflow-x: auto;
|
||
|
|
scrollbar-width: none;
|
||
|
|
}
|
||
|
|
.nav::-webkit-scrollbar { display: none; }
|
||
|
|
.nav-item {
|
||
|
|
flex: 1;
|
||
|
|
min-width: 60px;
|
||
|
|
padding: 12px 8px;
|
||
|
|
text-align: center;
|
||
|
|
font-size: 11px;
|
||
|
|
font-weight: 500;
|
||
|
|
color: var(--text-dim);
|
||
|
|
cursor: pointer;
|
||
|
|
border-bottom: 2px solid transparent;
|
||
|
|
transition: all 0.2s;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
.nav-item.active {
|
||
|
|
color: var(--accent);
|
||
|
|
border-bottom-color: var(--accent);
|
||
|
|
}
|
||
|
|
.nav-item:hover { color: var(--text); }
|
||
|
|
.nav-icon { font-size: 18px; display: block; margin-bottom: 2px; }
|
||
|
|
|
||
|
|
/* --- Tab Content --- */
|
||
|
|
.tab { display: none; padding: 16px; }
|
||
|
|
.tab.active { display: block; }
|
||
|
|
|
||
|
|
/* --- Cards --- */
|
||
|
|
.card {
|
||
|
|
background: var(--surface);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
margin-bottom: 12px;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
.card-header {
|
||
|
|
padding: 14px 16px;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
}
|
||
|
|
.card-title { font-size: 14px; font-weight: 600; }
|
||
|
|
.card-body { padding: 16px; }
|
||
|
|
|
||
|
|
/* --- Status cards grid --- */
|
||
|
|
.status-grid {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(2, 1fr);
|
||
|
|
gap: 10px;
|
||
|
|
margin-bottom: 16px;
|
||
|
|
}
|
||
|
|
.stat-card {
|
||
|
|
background: var(--surface);
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
padding: 14px;
|
||
|
|
}
|
||
|
|
.stat-value { font-size: 24px; font-weight: 700; color: var(--accent); }
|
||
|
|
.stat-label { font-size: 11px; color: var(--text-dim); margin-top: 4px; }
|
||
|
|
|
||
|
|
/* --- Driver list --- */
|
||
|
|
.driver-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
padding: 12px 16px;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
gap: 12px;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: background 0.15s;
|
||
|
|
}
|
||
|
|
.driver-item:last-child { border-bottom: none; }
|
||
|
|
.driver-item:hover { background: var(--surface2); }
|
||
|
|
.driver-icon {
|
||
|
|
width: 40px; height: 40px;
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
background: var(--surface2);
|
||
|
|
display: flex; align-items: center; justify-content: center;
|
||
|
|
font-size: 18px; flex-shrink: 0;
|
||
|
|
}
|
||
|
|
.driver-icon.gpu { background: #1a3a2e; color: #2ecc71; }
|
||
|
|
.driver-icon.wifi { background: #1a2a3e; color: #3498db; }
|
||
|
|
.driver-icon.bluetooth { background: #2a1a3e; color: #9b59b6; }
|
||
|
|
.driver-icon.audio { background: #3e2a1a; color: #e67e22; }
|
||
|
|
.driver-icon.camera { background: #3e1a2a; color: #e74c3c; }
|
||
|
|
.driver-icon.sensor { background: #1a3e3e; color: #1abc9c; }
|
||
|
|
.driver-icon.kernel { background: #2a2a1a; color: #f1c40f; }
|
||
|
|
.driver-info { flex: 1; min-width: 0; }
|
||
|
|
.driver-name { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
|
|
.driver-path { font-size: 11px; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
|
|
.driver-badge {
|
||
|
|
font-size: 10px;
|
||
|
|
padding: 3px 8px;
|
||
|
|
border-radius: 10px;
|
||
|
|
font-weight: 600;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
.badge-protected { background: rgba(46,204,113,0.15); color: var(--success); }
|
||
|
|
.badge-scoped { background: rgba(79,109,245,0.15); color: var(--accent); }
|
||
|
|
.badge-changed { background: rgba(233,69,96,0.15); color: var(--danger); }
|
||
|
|
|
||
|
|
/* --- Toggle switch --- */
|
||
|
|
.toggle {
|
||
|
|
position: relative;
|
||
|
|
width: 44px; height: 24px;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
.toggle input { opacity: 0; width: 0; height: 0; }
|
||
|
|
.toggle-slider {
|
||
|
|
position: absolute; inset: 0;
|
||
|
|
background: var(--border);
|
||
|
|
border-radius: 12px;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: 0.2s;
|
||
|
|
}
|
||
|
|
.toggle-slider::before {
|
||
|
|
content: '';
|
||
|
|
position: absolute;
|
||
|
|
width: 18px; height: 18px;
|
||
|
|
left: 3px; bottom: 3px;
|
||
|
|
background: var(--text);
|
||
|
|
border-radius: 50%;
|
||
|
|
transition: 0.2s;
|
||
|
|
}
|
||
|
|
.toggle input:checked + .toggle-slider { background: var(--accent); }
|
||
|
|
.toggle input:checked + .toggle-slider::before { transform: translateX(20px); }
|
||
|
|
|
||
|
|
/* --- App list (LSPosed-style) --- */
|
||
|
|
.scope-header {
|
||
|
|
padding: 12px 16px;
|
||
|
|
display: flex;
|
||
|
|
gap: 10px;
|
||
|
|
align-items: center;
|
||
|
|
}
|
||
|
|
.scope-select {
|
||
|
|
flex: 1;
|
||
|
|
background: var(--surface2);
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
color: var(--text);
|
||
|
|
padding: 10px 12px;
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
font-size: 14px;
|
||
|
|
outline: none;
|
||
|
|
}
|
||
|
|
.scope-select:focus { border-color: var(--accent); }
|
||
|
|
.scope-mode-btn {
|
||
|
|
padding: 10px 16px;
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
background: var(--surface2);
|
||
|
|
color: var(--text);
|
||
|
|
font-size: 12px;
|
||
|
|
font-weight: 600;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
.scope-mode-btn.system { background: var(--accent); border-color: var(--accent); }
|
||
|
|
|
||
|
|
.search-bar {
|
||
|
|
margin: 0 16px 12px;
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
.search-bar input {
|
||
|
|
width: 100%;
|
||
|
|
background: var(--surface2);
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
color: var(--text);
|
||
|
|
padding: 10px 12px 10px 36px;
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
font-size: 14px;
|
||
|
|
outline: none;
|
||
|
|
}
|
||
|
|
.search-bar input:focus { border-color: var(--accent); }
|
||
|
|
.search-bar::before {
|
||
|
|
content: '\1F50D';
|
||
|
|
position: absolute;
|
||
|
|
left: 12px;
|
||
|
|
top: 50%;
|
||
|
|
transform: translateY(-50%);
|
||
|
|
font-size: 14px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.app-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
padding: 10px 16px;
|
||
|
|
gap: 12px;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
}
|
||
|
|
.app-item:last-child { border-bottom: none; }
|
||
|
|
.app-icon {
|
||
|
|
width: 36px; height: 36px;
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
background: var(--surface2);
|
||
|
|
display: flex; align-items: center; justify-content: center;
|
||
|
|
font-size: 16px; flex-shrink: 0;
|
||
|
|
}
|
||
|
|
.app-info { flex: 1; min-width: 0; }
|
||
|
|
.app-name { font-size: 13px; font-weight: 500; }
|
||
|
|
.app-pkg { font-size: 11px; color: var(--text-dim); }
|
||
|
|
.app-system { font-size: 10px; color: var(--warning); }
|
||
|
|
|
||
|
|
.checkbox {
|
||
|
|
width: 22px; height: 22px;
|
||
|
|
border: 2px solid var(--border);
|
||
|
|
border-radius: 4px;
|
||
|
|
cursor: pointer;
|
||
|
|
display: flex; align-items: center; justify-content: center;
|
||
|
|
flex-shrink: 0;
|
||
|
|
transition: all 0.15s;
|
||
|
|
}
|
||
|
|
.checkbox.checked {
|
||
|
|
background: var(--accent);
|
||
|
|
border-color: var(--accent);
|
||
|
|
}
|
||
|
|
.checkbox.checked::after {
|
||
|
|
content: '\2713';
|
||
|
|
color: white;
|
||
|
|
font-size: 14px;
|
||
|
|
font-weight: 700;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- Module item --- */
|
||
|
|
.mod-item {
|
||
|
|
padding: 14px 16px;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
}
|
||
|
|
.mod-item:last-child { border-bottom: none; }
|
||
|
|
.mod-row {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 12px;
|
||
|
|
}
|
||
|
|
.mod-icon {
|
||
|
|
width: 36px; height: 36px;
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
background: #2a2a1a;
|
||
|
|
color: #f1c40f;
|
||
|
|
display: flex; align-items: center; justify-content: center;
|
||
|
|
font-size: 16px; flex-shrink: 0;
|
||
|
|
}
|
||
|
|
.mod-info { flex: 1; min-width: 0; }
|
||
|
|
.mod-name { font-size: 14px; font-weight: 500; }
|
||
|
|
.mod-desc { font-size: 11px; color: var(--text-dim); margin-top: 2px; }
|
||
|
|
.mod-meta {
|
||
|
|
display: flex;
|
||
|
|
gap: 12px;
|
||
|
|
margin-top: 8px;
|
||
|
|
padding-left: 48px;
|
||
|
|
}
|
||
|
|
.mod-meta span {
|
||
|
|
font-size: 11px;
|
||
|
|
color: var(--text-dim);
|
||
|
|
}
|
||
|
|
.mod-actions {
|
||
|
|
display: flex;
|
||
|
|
gap: 8px;
|
||
|
|
align-items: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- Protection --- */
|
||
|
|
.prot-status {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 10px;
|
||
|
|
padding: 16px;
|
||
|
|
}
|
||
|
|
.prot-dot {
|
||
|
|
width: 12px; height: 12px;
|
||
|
|
border-radius: 50%;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
.prot-dot.ok { background: var(--success); box-shadow: 0 0 8px rgba(46,204,113,0.5); }
|
||
|
|
.prot-dot.warn { background: var(--warning); box-shadow: 0 0 8px rgba(243,156,18,0.5); }
|
||
|
|
.prot-dot.error { background: var(--danger); box-shadow: 0 0 8px rgba(233,69,96,0.5); }
|
||
|
|
.prot-dot.off { background: var(--text-dim); }
|
||
|
|
|
||
|
|
/* --- Buttons --- */
|
||
|
|
.btn {
|
||
|
|
padding: 8px 16px;
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
border: none;
|
||
|
|
font-size: 13px;
|
||
|
|
font-weight: 600;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: all 0.15s;
|
||
|
|
}
|
||
|
|
.btn-primary { background: var(--accent); color: white; }
|
||
|
|
.btn-primary:hover { background: var(--accent-dim); }
|
||
|
|
.btn-danger { background: var(--danger); color: white; }
|
||
|
|
.btn-outline {
|
||
|
|
background: transparent;
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
color: var(--text);
|
||
|
|
}
|
||
|
|
.btn-outline:hover { background: var(--surface2); }
|
||
|
|
.btn-sm { padding: 5px 10px; font-size: 11px; }
|
||
|
|
|
||
|
|
/* --- Logs --- */
|
||
|
|
.log-view {
|
||
|
|
background: #0a0a14;
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
padding: 12px;
|
||
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||
|
|
font-size: 11px;
|
||
|
|
line-height: 1.6;
|
||
|
|
color: var(--text-dim);
|
||
|
|
max-height: 60vh;
|
||
|
|
overflow-y: auto;
|
||
|
|
white-space: pre-wrap;
|
||
|
|
word-break: break-all;
|
||
|
|
}
|
||
|
|
.log-view .log-error { color: var(--danger); }
|
||
|
|
.log-view .log-warn { color: var(--warning); }
|
||
|
|
.log-view .log-ok { color: var(--success); }
|
||
|
|
|
||
|
|
/* --- Select group --- */
|
||
|
|
.select-group {
|
||
|
|
display: flex;
|
||
|
|
gap: 6px;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
padding: 8px 0;
|
||
|
|
}
|
||
|
|
.select-group .chip {
|
||
|
|
padding: 6px 14px;
|
||
|
|
border-radius: 16px;
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
font-size: 12px;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: all 0.15s;
|
||
|
|
background: var(--surface2);
|
||
|
|
color: var(--text-dim);
|
||
|
|
}
|
||
|
|
.select-group .chip.active {
|
||
|
|
background: var(--accent);
|
||
|
|
border-color: var(--accent);
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- Empty state --- */
|
||
|
|
.empty {
|
||
|
|
text-align: center;
|
||
|
|
padding: 40px 20px;
|
||
|
|
color: var(--text-dim);
|
||
|
|
}
|
||
|
|
.empty-icon { font-size: 40px; margin-bottom: 12px; }
|
||
|
|
.empty-text { font-size: 14px; }
|
||
|
|
.empty-sub { font-size: 12px; margin-top: 6px; }
|
||
|
|
|
||
|
|
/* --- Detail panel (slide-up) --- */
|
||
|
|
.detail-overlay {
|
||
|
|
position: fixed;
|
||
|
|
inset: 0;
|
||
|
|
background: rgba(0,0,0,0.6);
|
||
|
|
z-index: 200;
|
||
|
|
display: none;
|
||
|
|
align-items: flex-end;
|
||
|
|
}
|
||
|
|
.detail-overlay.show { display: flex; }
|
||
|
|
.detail-panel {
|
||
|
|
width: 100%;
|
||
|
|
max-height: 80vh;
|
||
|
|
background: var(--surface);
|
||
|
|
border-radius: var(--radius) var(--radius) 0 0;
|
||
|
|
overflow-y: auto;
|
||
|
|
animation: slideUp 0.25s ease;
|
||
|
|
}
|
||
|
|
@keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
|
||
|
|
.detail-header {
|
||
|
|
padding: 16px;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
position: sticky;
|
||
|
|
top: 0;
|
||
|
|
background: var(--surface);
|
||
|
|
}
|
||
|
|
.detail-close {
|
||
|
|
width: 32px; height: 32px;
|
||
|
|
border-radius: 50%;
|
||
|
|
background: var(--surface2);
|
||
|
|
border: none;
|
||
|
|
color: var(--text);
|
||
|
|
font-size: 18px;
|
||
|
|
cursor: pointer;
|
||
|
|
display: flex; align-items: center; justify-content: center;
|
||
|
|
}
|
||
|
|
.detail-body { padding: 16px; }
|
||
|
|
.detail-row {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
padding: 10px 0;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
font-size: 13px;
|
||
|
|
}
|
||
|
|
.detail-row:last-child { border-bottom: none; }
|
||
|
|
.detail-label { color: var(--text-dim); }
|
||
|
|
.detail-value { font-weight: 500; text-align: right; max-width: 60%; word-break: break-all; }
|
||
|
|
|
||
|
|
/* --- Toast --- */
|
||
|
|
.toast {
|
||
|
|
position: fixed;
|
||
|
|
bottom: 80px;
|
||
|
|
left: 50%;
|
||
|
|
transform: translateX(-50%);
|
||
|
|
background: var(--surface2);
|
||
|
|
color: var(--text);
|
||
|
|
padding: 10px 20px;
|
||
|
|
border-radius: 20px;
|
||
|
|
font-size: 13px;
|
||
|
|
z-index: 300;
|
||
|
|
opacity: 0;
|
||
|
|
transition: opacity 0.3s;
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
}
|
||
|
|
.toast.show { opacity: 1; }
|
||
|
|
|
||
|
|
/* --- Loading --- */
|
||
|
|
.loading {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
padding: 40px;
|
||
|
|
color: var(--text-dim);
|
||
|
|
gap: 8px;
|
||
|
|
}
|
||
|
|
.spinner {
|
||
|
|
width: 20px; height: 20px;
|
||
|
|
border: 2px solid var(--border);
|
||
|
|
border-top-color: var(--accent);
|
||
|
|
border-radius: 50%;
|
||
|
|
animation: spin 0.6s linear infinite;
|
||
|
|
}
|
||
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
|
|
||
|
|
.filter-bar {
|
||
|
|
padding: 8px 16px;
|
||
|
|
display: flex;
|
||
|
|
gap: 6px;
|
||
|
|
overflow-x: auto;
|
||
|
|
scrollbar-width: none;
|
||
|
|
}
|
||
|
|
.filter-bar::-webkit-scrollbar { display: none; }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
|
||
|
|
<!-- Header -->
|
||
|
|
<div class="header">
|
||
|
|
<div class="header-icon">DM</div>
|
||
|
|
<h1>Driver Manager</h1>
|
||
|
|
<span class="header-version">v2.0.0</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Navigation -->
|
||
|
|
<div class="nav">
|
||
|
|
<div class="nav-item active" onclick="switchTab('dashboard')">
|
||
|
|
<span class="nav-icon">◉</span>Dashboard
|
||
|
|
</div>
|
||
|
|
<div class="nav-item" onclick="switchTab('drivers')">
|
||
|
|
<span class="nav-icon">⚙</span>Drivers
|
||
|
|
</div>
|
||
|
|
<div class="nav-item" onclick="switchTab('apps')">
|
||
|
|
<span class="nav-icon">▦</span>Apps
|
||
|
|
</div>
|
||
|
|
<div class="nav-item" onclick="switchTab('modules')">
|
||
|
|
<span class="nav-icon">◆</span>Modules
|
||
|
|
</div>
|
||
|
|
<div class="nav-item" onclick="switchTab('protection')">
|
||
|
|
<span class="nav-icon">▣</span>Protect
|
||
|
|
</div>
|
||
|
|
<div class="nav-item" onclick="switchTab('logs')">
|
||
|
|
<span class="nav-icon">☰</span>Logs
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Dashboard Tab -->
|
||
|
|
<div id="tab-dashboard" class="tab active">
|
||
|
|
<div class="status-grid">
|
||
|
|
<div class="stat-card">
|
||
|
|
<div class="stat-value" id="stat-drivers">--</div>
|
||
|
|
<div class="stat-label">Drivers</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-card">
|
||
|
|
<div class="stat-value" id="stat-scopes">--</div>
|
||
|
|
<div class="stat-label">Active Scopes</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-card">
|
||
|
|
<div class="stat-value" id="stat-modules">--</div>
|
||
|
|
<div class="stat-label">Kernel Modules</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-card">
|
||
|
|
<div class="stat-value" id="stat-protection">--</div>
|
||
|
|
<div class="stat-label">Protected</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="card">
|
||
|
|
<div class="card-header">
|
||
|
|
<span class="card-title">Device Info</span>
|
||
|
|
</div>
|
||
|
|
<div class="card-body" id="device-info">
|
||
|
|
<div class="loading"><div class="spinner"></div> Loading...</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="card">
|
||
|
|
<div class="card-header">
|
||
|
|
<span class="card-title">Quick Actions</span>
|
||
|
|
</div>
|
||
|
|
<div class="card-body" style="display:flex;gap:8px;flex-wrap:wrap;">
|
||
|
|
<button class="btn btn-primary" onclick="rescanDrivers()">Scan Drivers</button>
|
||
|
|
<button class="btn btn-outline" onclick="checkIntegrity()">Check Integrity</button>
|
||
|
|
<button class="btn btn-outline" onclick="applyScopes()">Apply Scopes</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Drivers Tab -->
|
||
|
|
<div id="tab-drivers" class="tab">
|
||
|
|
<div class="filter-bar" id="driver-filters">
|
||
|
|
<span class="chip active" onclick="filterDrivers('all')">All</span>
|
||
|
|
<span class="chip" onclick="filterDrivers('gpu')">GPU</span>
|
||
|
|
<span class="chip" onclick="filterDrivers('wifi')">WiFi</span>
|
||
|
|
<span class="chip" onclick="filterDrivers('bluetooth')">Bluetooth</span>
|
||
|
|
<span class="chip" onclick="filterDrivers('audio')">Audio</span>
|
||
|
|
<span class="chip" onclick="filterDrivers('camera')">Camera</span>
|
||
|
|
<span class="chip" onclick="filterDrivers('sensor')">Sensor</span>
|
||
|
|
<span class="chip" onclick="filterDrivers('kernel')">Kernel</span>
|
||
|
|
</div>
|
||
|
|
<div class="card">
|
||
|
|
<div id="driver-list">
|
||
|
|
<div class="loading"><div class="spinner"></div> Loading drivers...</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Apps Tab (LSPosed-style) -->
|
||
|
|
<div id="tab-apps" class="tab">
|
||
|
|
<div class="scope-header">
|
||
|
|
<select class="scope-select" id="scope-driver-select" onchange="onScopeDriverChange()">
|
||
|
|
<option value="">Select a driver to scope...</option>
|
||
|
|
</select>
|
||
|
|
<button class="scope-mode-btn" id="scope-mode-btn" onclick="toggleScopeMode()">Per-App</button>
|
||
|
|
</div>
|
||
|
|
<div class="search-bar">
|
||
|
|
<input type="text" id="app-search" placeholder="Search apps..." oninput="filterApps()">
|
||
|
|
</div>
|
||
|
|
<div class="card">
|
||
|
|
<div id="app-list">
|
||
|
|
<div class="empty">
|
||
|
|
<div class="empty-icon">▦</div>
|
||
|
|
<div class="empty-text">Select a driver above</div>
|
||
|
|
<div class="empty-sub">Then choose which apps should use it</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Modules Tab -->
|
||
|
|
<div id="tab-modules" class="tab">
|
||
|
|
<div class="card">
|
||
|
|
<div class="card-header">
|
||
|
|
<span class="card-title">Kernel Modules (.ko)</span>
|
||
|
|
<button class="btn btn-sm btn-outline" onclick="refreshModules()">Refresh</button>
|
||
|
|
</div>
|
||
|
|
<div id="module-list">
|
||
|
|
<div class="loading"><div class="spinner"></div> Loading modules...</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="card">
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="empty">
|
||
|
|
<div class="empty-icon">◆</div>
|
||
|
|
<div class="empty-text">Place .ko files in modules/</div>
|
||
|
|
<div class="empty-sub">/data/adb/modules/driver-manager/modules/</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Protection Tab -->
|
||
|
|
<div id="tab-protection" class="tab">
|
||
|
|
<div class="card">
|
||
|
|
<div class="card-header">
|
||
|
|
<span class="card-title">Protection Status</span>
|
||
|
|
</div>
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="prot-status">
|
||
|
|
<div class="prot-dot" id="prot-dot"></div>
|
||
|
|
<div>
|
||
|
|
<div id="prot-mode-label" style="font-size:14px;font-weight:600;">Loading...</div>
|
||
|
|
<div id="prot-detail" style="font-size:11px;color:var(--text-dim);">--</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="card">
|
||
|
|
<div class="card-header">
|
||
|
|
<span class="card-title">Protection Mode</span>
|
||
|
|
</div>
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="select-group" id="prot-mode-group">
|
||
|
|
<span class="chip" onclick="setProtMode('off')">Off</span>
|
||
|
|
<span class="chip active" onclick="setProtMode('monitor')">Monitor</span>
|
||
|
|
<span class="chip" onclick="setProtMode('enforce')">Enforce</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="card">
|
||
|
|
<div class="card-header">
|
||
|
|
<span class="card-title">Actions</span>
|
||
|
|
</div>
|
||
|
|
<div class="card-body" style="display:flex;gap:8px;flex-wrap:wrap;">
|
||
|
|
<button class="btn btn-primary" onclick="checkIntegrity()">Run Check</button>
|
||
|
|
<button class="btn btn-outline" onclick="rebaseline()">New Baseline</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="card">
|
||
|
|
<div class="card-header">
|
||
|
|
<span class="card-title">Change Log</span>
|
||
|
|
</div>
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="log-view" id="prot-changelog" style="max-height:200px;">No changes detected</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Logs Tab -->
|
||
|
|
<div id="tab-logs" class="tab">
|
||
|
|
<div class="filter-bar">
|
||
|
|
<span class="chip active" onclick="loadLog('service')">Service</span>
|
||
|
|
<span class="chip" onclick="loadLog('boot')">Boot</span>
|
||
|
|
<span class="chip" onclick="loadLog('scope')">Scope</span>
|
||
|
|
<span class="chip" onclick="loadLog('ko')">Modules</span>
|
||
|
|
<span class="chip" onclick="loadLog('protect')">Protect</span>
|
||
|
|
<span class="chip" onclick="loadLog('registry')">Registry</span>
|
||
|
|
</div>
|
||
|
|
<div class="card">
|
||
|
|
<div class="card-header">
|
||
|
|
<span class="card-title" id="log-title">Service Log</span>
|
||
|
|
<button class="btn btn-sm btn-outline" onclick="refreshCurrentLog()">Refresh</button>
|
||
|
|
</div>
|
||
|
|
<div class="card-body">
|
||
|
|
<div class="log-view" id="log-content">Loading...</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Detail Panel (slide-up overlay) -->
|
||
|
|
<div class="detail-overlay" id="detail-overlay" onclick="closeDetail(event)">
|
||
|
|
<div class="detail-panel" id="detail-panel">
|
||
|
|
<div class="detail-header">
|
||
|
|
<span class="card-title" id="detail-title">Driver Details</span>
|
||
|
|
<button class="detail-close" onclick="closeDetail()">×</button>
|
||
|
|
</div>
|
||
|
|
<div class="detail-body" id="detail-body"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Toast -->
|
||
|
|
<div class="toast" id="toast"></div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
const MODDIR = '/data/adb/modules/driver-manager';
|
||
|
|
const API = `${MODDIR}/scripts/api.sh`;
|
||
|
|
|
||
|
|
// --- State ---
|
||
|
|
let drivers = [];
|
||
|
|
let apps = [];
|
||
|
|
let scopes = [];
|
||
|
|
let currentFilter = 'all';
|
||
|
|
let currentLog = 'service';
|
||
|
|
let scopeMode = 'app';
|
||
|
|
let selectedScopeDriver = null;
|
||
|
|
|
||
|
|
// --- KSU exec wrapper ---
|
||
|
|
async function exec(cmd) {
|
||
|
|
try {
|
||
|
|
const r = await ksu.exec(cmd);
|
||
|
|
return r.stdout || r;
|
||
|
|
} catch(e) {
|
||
|
|
console.error('exec error:', e);
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function api(route, ...args) {
|
||
|
|
const cmd = `sh ${API} ${route} ${args.join(' ')}`;
|
||
|
|
const r = await exec(cmd);
|
||
|
|
try { return JSON.parse(r); } catch(e) { return r; }
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Tab switching ---
|
||
|
|
function switchTab(name) {
|
||
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
|
|
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
||
|
|
document.getElementById('tab-' + name).classList.add('active');
|
||
|
|
document.querySelectorAll('.nav-item')[
|
||
|
|
['dashboard','drivers','apps','modules','protection','logs'].indexOf(name)
|
||
|
|
].classList.add('active');
|
||
|
|
|
||
|
|
if (name === 'dashboard') loadDashboard();
|
||
|
|
if (name === 'drivers') loadDrivers();
|
||
|
|
if (name === 'apps') loadApps();
|
||
|
|
if (name === 'modules') loadModules();
|
||
|
|
if (name === 'protection') loadProtection();
|
||
|
|
if (name === 'logs') loadLog(currentLog);
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Toast ---
|
||
|
|
function toast(msg) {
|
||
|
|
const t = document.getElementById('toast');
|
||
|
|
t.textContent = msg;
|
||
|
|
t.classList.add('show');
|
||
|
|
setTimeout(() => t.classList.remove('show'), 2500);
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Dashboard ---
|
||
|
|
async function loadDashboard() {
|
||
|
|
const [sys, drvCount, scopeData, protStatus] = await Promise.all([
|
||
|
|
api('system', 'info'),
|
||
|
|
api('drivers', 'count'),
|
||
|
|
api('scope', 'list'),
|
||
|
|
api('protect', 'status')
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Stats
|
||
|
|
document.getElementById('stat-drivers').textContent = drvCount.count || 0;
|
||
|
|
const scopeCount = scopeData.scopes ? scopeData.scopes.length : 0;
|
||
|
|
document.getElementById('stat-scopes').textContent = scopeCount;
|
||
|
|
document.getElementById('stat-protection').textContent = protStatus.protected_count || 0;
|
||
|
|
|
||
|
|
// Module count
|
||
|
|
const mods = await api('ko', 'list');
|
||
|
|
const loadedCount = mods.modules ? mods.modules.filter(m => m.loaded).length : 0;
|
||
|
|
const totalMods = mods.modules ? mods.modules.length : 0;
|
||
|
|
document.getElementById('stat-modules').textContent = `${loadedCount}/${totalMods}`;
|
||
|
|
|
||
|
|
// Device info
|
||
|
|
const dev = sys.device || {};
|
||
|
|
document.getElementById('device-info').innerHTML = `
|
||
|
|
<div class="detail-row"><span class="detail-label">Model</span><span class="detail-value">${dev.model || 'Unknown'}</span></div>
|
||
|
|
<div class="detail-row"><span class="detail-label">Device</span><span class="detail-value">${dev.device || 'Unknown'}</span></div>
|
||
|
|
<div class="detail-row"><span class="detail-label">SoC</span><span class="detail-value">${dev.soc || 'Unknown'}</span></div>
|
||
|
|
<div class="detail-row"><span class="detail-label">Kernel</span><span class="detail-value">${sys.kernel || 'Unknown'}</span></div>
|
||
|
|
<div class="detail-row"><span class="detail-label">Architecture</span><span class="detail-value">${dev.arch || 'Unknown'}</span></div>
|
||
|
|
<div class="detail-row"><span class="detail-label">API Level</span><span class="detail-value">${dev.api || 'Unknown'}</span></div>
|
||
|
|
<div class="detail-row"><span class="detail-label">Uptime</span><span class="detail-value">${formatUptime(sys.uptime)}</span></div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatUptime(s) {
|
||
|
|
if (!s) return '--';
|
||
|
|
const sec = parseInt(s);
|
||
|
|
const h = Math.floor(sec / 3600);
|
||
|
|
const m = Math.floor((sec % 3600) / 60);
|
||
|
|
return `${h}h ${m}m`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Drivers ---
|
||
|
|
async function loadDrivers() {
|
||
|
|
const data = await api('drivers', 'list');
|
||
|
|
drivers = data.drivers || [];
|
||
|
|
renderDrivers();
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderDrivers() {
|
||
|
|
const list = document.getElementById('driver-list');
|
||
|
|
const filtered = currentFilter === 'all' ? drivers : drivers.filter(d => d.category === currentFilter);
|
||
|
|
|
||
|
|
if (filtered.length === 0) {
|
||
|
|
list.innerHTML = `<div class="empty"><div class="empty-icon">⚙</div><div class="empty-text">No drivers found</div><div class="empty-sub">Tap "Scan Drivers" on Dashboard</div></div>`;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
list.innerHTML = filtered.map(d => {
|
||
|
|
const icon = categoryIcon(d.category);
|
||
|
|
const badges = [];
|
||
|
|
if (d.protected) badges.push('<span class="driver-badge badge-protected">Protected</span>');
|
||
|
|
if (d.variants && d.variants.length > 1) badges.push('<span class="driver-badge badge-scoped">Variants</span>');
|
||
|
|
|
||
|
|
return `
|
||
|
|
<div class="driver-item" onclick="showDriverDetail('${d.id}')">
|
||
|
|
<div class="driver-icon ${d.category}">${icon}</div>
|
||
|
|
<div class="driver-info">
|
||
|
|
<div class="driver-name">${d.name}</div>
|
||
|
|
<div class="driver-path">${d.path}</div>
|
||
|
|
</div>
|
||
|
|
${badges.join('')}
|
||
|
|
</div>`;
|
||
|
|
}).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
function categoryIcon(cat) {
|
||
|
|
const icons = { gpu: '◆', wifi: '◈', bluetooth: '◉', audio: '♫', camera: '◉', sensor: '◎', kernel: '◆' };
|
||
|
|
return icons[cat] || '◉';
|
||
|
|
}
|
||
|
|
|
||
|
|
function filterDrivers(cat) {
|
||
|
|
currentFilter = cat;
|
||
|
|
document.querySelectorAll('#driver-filters .chip').forEach(c => c.classList.remove('active'));
|
||
|
|
event.target.classList.add('active');
|
||
|
|
renderDrivers();
|
||
|
|
}
|
||
|
|
|
||
|
|
function showDriverDetail(id) {
|
||
|
|
const d = drivers.find(x => x.id === id);
|
||
|
|
if (!d) return;
|
||
|
|
|
||
|
|
document.getElementById('detail-title').textContent = d.name;
|
||
|
|
document.getElementById('detail-body').innerHTML = `
|
||
|
|
<div class="detail-row"><span class="detail-label">ID</span><span class="detail-value">${d.id}</span></div>
|
||
|
|
<div class="detail-row"><span class="detail-label">Category</span><span class="detail-value">${d.category}</span></div>
|
||
|
|
<div class="detail-row"><span class="detail-label">Path</span><span class="detail-value">${d.path}</span></div>
|
||
|
|
<div class="detail-row"><span class="detail-label">SHA256</span><span class="detail-value" style="font-size:10px;">${d.hash || 'N/A'}</span></div>
|
||
|
|
<div class="detail-row"><span class="detail-label">SELinux</span><span class="detail-value">${d.selinux || 'N/A'}</span></div>
|
||
|
|
<div class="detail-row"><span class="detail-label">Size</span><span class="detail-value">${formatSize(d.size)}</span></div>
|
||
|
|
<div class="detail-row"><span class="detail-label">Protected</span><span class="detail-value">${d.protected ? 'Yes' : 'No'}</span></div>
|
||
|
|
<div class="detail-row">
|
||
|
|
<span class="detail-label">Variants</span>
|
||
|
|
<span class="detail-value">${d.variants ? d.variants.map(v => v.name).join(', ') : 'stock'}</span>
|
||
|
|
</div>
|
||
|
|
<div style="margin-top:16px;display:flex;gap:8px;">
|
||
|
|
<button class="btn btn-sm ${d.protected ? 'btn-danger' : 'btn-primary'}" onclick="toggleProtect('${d.id}', ${!d.protected})">
|
||
|
|
${d.protected ? 'Unprotect' : 'Protect'}
|
||
|
|
</button>
|
||
|
|
<button class="btn btn-sm btn-outline" onclick="verifyDriver('${d.id}')">Verify Hash</button>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
document.getElementById('detail-overlay').classList.add('show');
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatSize(bytes) {
|
||
|
|
if (!bytes) return '0 B';
|
||
|
|
const k = 1024;
|
||
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeDetail(e) {
|
||
|
|
if (e && e.target !== document.getElementById('detail-overlay') && !e.target.classList.contains('detail-close')) return;
|
||
|
|
document.getElementById('detail-overlay').classList.remove('show');
|
||
|
|
}
|
||
|
|
|
||
|
|
async function toggleProtect(id, val) {
|
||
|
|
await api('protect', 'driver', id, val);
|
||
|
|
toast(val ? 'Driver protected' : 'Driver unprotected');
|
||
|
|
closeDetail();
|
||
|
|
loadDrivers();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function verifyDriver(id) {
|
||
|
|
const r = await api('drivers', 'verify', id);
|
||
|
|
if (r.status === 'ok') toast('Hash verified OK');
|
||
|
|
else if (r.status === 'changed') toast('WARNING: Driver hash changed!');
|
||
|
|
else toast(r.status || 'Unknown');
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Apps (LSPosed-style scoping) ---
|
||
|
|
async function loadApps() {
|
||
|
|
if (apps.length === 0) {
|
||
|
|
document.getElementById('app-list').innerHTML = '<div class="loading"><div class="spinner"></div> Loading apps...</div>';
|
||
|
|
const data = await api('apps', 'list');
|
||
|
|
apps = data.apps || [];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Populate driver select dropdown
|
||
|
|
if (drivers.length === 0) {
|
||
|
|
const data = await api('drivers', 'list');
|
||
|
|
drivers = data.drivers || [];
|
||
|
|
}
|
||
|
|
|
||
|
|
const select = document.getElementById('scope-driver-select');
|
||
|
|
if (select.options.length <= 1) {
|
||
|
|
drivers.forEach(d => {
|
||
|
|
const opt = document.createElement('option');
|
||
|
|
opt.value = d.id;
|
||
|
|
opt.textContent = `[${d.category.toUpperCase()}] ${d.name}`;
|
||
|
|
select.appendChild(opt);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load current scopes
|
||
|
|
const scopeData = await api('scope', 'list');
|
||
|
|
scopes = scopeData.scopes || [];
|
||
|
|
|
||
|
|
if (selectedScopeDriver) renderAppList();
|
||
|
|
}
|
||
|
|
|
||
|
|
function onScopeDriverChange() {
|
||
|
|
const select = document.getElementById('scope-driver-select');
|
||
|
|
selectedScopeDriver = select.value;
|
||
|
|
if (selectedScopeDriver) renderAppList();
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderAppList() {
|
||
|
|
const search = document.getElementById('app-search').value.toLowerCase();
|
||
|
|
const filtered = apps.filter(a =>
|
||
|
|
a.label.toLowerCase().includes(search) || a.package.toLowerCase().includes(search)
|
||
|
|
);
|
||
|
|
|
||
|
|
// Find current scope for selected driver
|
||
|
|
const currentScope = scopes.find(s => s.driver_id === selectedScopeDriver);
|
||
|
|
const scopedApps = currentScope ? (currentScope.apps || []) : [];
|
||
|
|
const isSystemWide = currentScope ? currentScope.mode === 'system' : false;
|
||
|
|
|
||
|
|
// Update mode button
|
||
|
|
const modeBtn = document.getElementById('scope-mode-btn');
|
||
|
|
scopeMode = isSystemWide ? 'system' : 'app';
|
||
|
|
modeBtn.textContent = isSystemWide ? 'System' : 'Per-App';
|
||
|
|
modeBtn.classList.toggle('system', isSystemWide);
|
||
|
|
|
||
|
|
const list = document.getElementById('app-list');
|
||
|
|
list.innerHTML = filtered.map(a => {
|
||
|
|
const checked = isSystemWide || scopedApps.includes(a.package);
|
||
|
|
return `
|
||
|
|
<div class="app-item">
|
||
|
|
<div class="app-icon">${a.label.charAt(0).toUpperCase()}</div>
|
||
|
|
<div class="app-info">
|
||
|
|
<div class="app-name">${a.label}</div>
|
||
|
|
<div class="app-pkg">${a.package}</div>
|
||
|
|
${a.system ? '<span class="app-system">System</span>' : ''}
|
||
|
|
</div>
|
||
|
|
<div class="checkbox ${checked ? 'checked' : ''}" onclick="toggleAppScope('${a.package}', this)"></div>
|
||
|
|
</div>`;
|
||
|
|
}).join('');
|
||
|
|
|
||
|
|
if (filtered.length === 0) {
|
||
|
|
list.innerHTML = '<div class="empty"><div class="empty-text">No apps match</div></div>';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function filterApps() { if (selectedScopeDriver) renderAppList(); }
|
||
|
|
|
||
|
|
async function toggleAppScope(pkg, el) {
|
||
|
|
if (!selectedScopeDriver) return;
|
||
|
|
|
||
|
|
const isChecked = el.classList.contains('checked');
|
||
|
|
|
||
|
|
// Find the driver
|
||
|
|
const drv = drivers.find(d => d.id === selectedScopeDriver);
|
||
|
|
if (!drv) return;
|
||
|
|
|
||
|
|
// Get current scope for this driver
|
||
|
|
const currentScope = scopes.find(s => s.driver_id === selectedScopeDriver);
|
||
|
|
let scopedApps = currentScope ? [...(currentScope.apps || [])] : [];
|
||
|
|
|
||
|
|
if (isChecked) {
|
||
|
|
scopedApps = scopedApps.filter(a => a !== pkg);
|
||
|
|
el.classList.remove('checked');
|
||
|
|
} else {
|
||
|
|
scopedApps.push(pkg);
|
||
|
|
el.classList.add('checked');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get variant path (use first non-stock variant, or stock)
|
||
|
|
const variant = drv.variants && drv.variants.length > 1 ? drv.variants[1] : drv.variants[0];
|
||
|
|
const variantPath = variant ? variant.path : drv.path;
|
||
|
|
|
||
|
|
await api('scope', 'set', selectedScopeDriver, drv.path, variantPath, 'app', scopedApps.join(','));
|
||
|
|
toast(`Scope updated for ${pkg}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function toggleScopeMode() {
|
||
|
|
if (!selectedScopeDriver) return;
|
||
|
|
scopeMode = scopeMode === 'app' ? 'system' : 'app';
|
||
|
|
|
||
|
|
const drv = drivers.find(d => d.id === selectedScopeDriver);
|
||
|
|
if (!drv) return;
|
||
|
|
|
||
|
|
const variant = drv.variants && drv.variants.length > 1 ? drv.variants[1] : drv.variants[0];
|
||
|
|
const variantPath = variant ? variant.path : drv.path;
|
||
|
|
|
||
|
|
if (scopeMode === 'system') {
|
||
|
|
await api('scope', 'set', selectedScopeDriver, drv.path, variantPath, 'system', '');
|
||
|
|
toast('Set to system-wide');
|
||
|
|
} else {
|
||
|
|
await api('scope', 'remove', selectedScopeDriver);
|
||
|
|
toast('Set to per-app mode');
|
||
|
|
}
|
||
|
|
|
||
|
|
const scopeData = await api('scope', 'list');
|
||
|
|
scopes = scopeData.scopes || [];
|
||
|
|
renderAppList();
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Modules ---
|
||
|
|
async function loadModules() {
|
||
|
|
const data = await api('ko', 'list');
|
||
|
|
const mods = data.modules || [];
|
||
|
|
|
||
|
|
const list = document.getElementById('module-list');
|
||
|
|
|
||
|
|
if (mods.length === 0) {
|
||
|
|
list.innerHTML = '';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
list.innerHTML = mods.map(m => `
|
||
|
|
<div class="mod-item">
|
||
|
|
<div class="mod-row">
|
||
|
|
<div class="mod-icon">◆</div>
|
||
|
|
<div class="mod-info">
|
||
|
|
<div class="mod-name">${m.name || m.filename}</div>
|
||
|
|
<div class="mod-desc">${m.description || 'No description'}</div>
|
||
|
|
</div>
|
||
|
|
<div class="mod-actions">
|
||
|
|
<label class="toggle">
|
||
|
|
<input type="checkbox" ${m.loaded ? 'checked' : ''} onchange="toggleModule('${m.filename}', this.checked)">
|
||
|
|
<span class="toggle-slider"></span>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="mod-meta">
|
||
|
|
<span>v${m.version || '?'}</span>
|
||
|
|
<span>${formatSize(m.size)}</span>
|
||
|
|
<span class="${m.compatible === true ? '' : (m.compatible === false ? 'log-error' : '')}">${m.compatible === true ? 'Compatible' : (m.compatible === false ? 'Incompatible' : 'Unknown')}</span>
|
||
|
|
<span>
|
||
|
|
<label style="cursor:pointer;">
|
||
|
|
<input type="checkbox" ${m.autoload ? 'checked' : ''} onchange="toggleAutoload('${m.filename}', this.checked)" style="margin-right:4px;">
|
||
|
|
Autoload
|
||
|
|
</label>
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
async function toggleModule(filename, load) {
|
||
|
|
if (load) {
|
||
|
|
const r = await api('ko', 'load', filename);
|
||
|
|
toast(r.status === 'ok' ? `Loaded ${filename}` : (r.message || 'Load failed'));
|
||
|
|
} else {
|
||
|
|
const name = filename.replace('.ko', '');
|
||
|
|
const r = await api('ko', 'unload', name);
|
||
|
|
toast(r.status === 'ok' ? `Unloaded ${name}` : (r.message || 'Unload failed'));
|
||
|
|
}
|
||
|
|
loadModules();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function toggleAutoload(filename, enabled) {
|
||
|
|
await api('ko', 'autoload', filename, enabled);
|
||
|
|
toast(enabled ? `${filename} will autoload` : `${filename} autoload disabled`);
|
||
|
|
}
|
||
|
|
|
||
|
|
function refreshModules() { loadModules(); }
|
||
|
|
|
||
|
|
// --- Protection ---
|
||
|
|
async function loadProtection() {
|
||
|
|
const status = await api('protect', 'status');
|
||
|
|
|
||
|
|
const dot = document.getElementById('prot-dot');
|
||
|
|
const label = document.getElementById('prot-mode-label');
|
||
|
|
const detail = document.getElementById('prot-detail');
|
||
|
|
|
||
|
|
dot.className = 'prot-dot';
|
||
|
|
if (status.mode === 'enforce') { dot.classList.add('ok'); label.textContent = 'Enforce Mode'; }
|
||
|
|
else if (status.mode === 'monitor') { dot.classList.add('warn'); label.textContent = 'Monitor Mode'; }
|
||
|
|
else { dot.classList.add('off'); label.textContent = 'Protection Off'; }
|
||
|
|
|
||
|
|
detail.textContent = `${status.protected_count || 0} drivers protected | Watcher: ${status.watcher_running ? 'Running' : 'Stopped'} | Last check: ${status.last_check || 'never'}`;
|
||
|
|
|
||
|
|
// Update mode chips
|
||
|
|
document.querySelectorAll('#prot-mode-group .chip').forEach(c => {
|
||
|
|
c.classList.toggle('active', c.textContent.toLowerCase() === status.mode);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Changelog
|
||
|
|
const changelog = await api('protect', 'changelog', '30');
|
||
|
|
document.getElementById('prot-changelog').textContent = typeof changelog === 'string' ? changelog : JSON.stringify(changelog, null, 2);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function setProtMode(mode) {
|
||
|
|
await api('protect', 'mode', mode);
|
||
|
|
toast(`Protection: ${mode}`);
|
||
|
|
loadProtection();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function checkIntegrity() {
|
||
|
|
toast('Running integrity check...');
|
||
|
|
const r = await api('protect', 'check');
|
||
|
|
if (r.changes > 0) toast(`WARNING: ${r.changes} changes detected!`);
|
||
|
|
else toast(`All ${r.ok || 0} drivers OK`);
|
||
|
|
loadProtection();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function rebaseline() {
|
||
|
|
await api('protect', 'baseline');
|
||
|
|
toast('New baseline created');
|
||
|
|
loadProtection();
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Logs ---
|
||
|
|
async function loadLog(name) {
|
||
|
|
currentLog = name;
|
||
|
|
document.getElementById('log-title').textContent = name.charAt(0).toUpperCase() + name.slice(1) + ' Log';
|
||
|
|
|
||
|
|
// Update active chip
|
||
|
|
document.querySelectorAll('#tab-logs .filter-bar .chip').forEach(c => {
|
||
|
|
c.classList.toggle('active', c.textContent.toLowerCase() === name);
|
||
|
|
});
|
||
|
|
|
||
|
|
const data = await api('system', 'logs', name, '200');
|
||
|
|
const content = typeof data === 'object' && data.log ? data.log : (data || 'No log data');
|
||
|
|
const lines = content.replace(/\|/g, '\n');
|
||
|
|
|
||
|
|
document.getElementById('log-content').innerHTML = lines
|
||
|
|
.split('\n')
|
||
|
|
.map(l => {
|
||
|
|
if (l.includes('ERROR')) return `<span class="log-error">${escHtml(l)}</span>`;
|
||
|
|
if (l.includes('WARN') || l.includes('FAILED')) return `<span class="log-warn">${escHtml(l)}</span>`;
|
||
|
|
if (l.includes('ok') || l.includes('Loaded') || l.includes('complete')) return `<span class="log-ok">${escHtml(l)}</span>`;
|
||
|
|
return escHtml(l);
|
||
|
|
})
|
||
|
|
.join('\n');
|
||
|
|
}
|
||
|
|
|
||
|
|
function refreshCurrentLog() { loadLog(currentLog); }
|
||
|
|
|
||
|
|
function escHtml(s) {
|
||
|
|
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Quick actions ---
|
||
|
|
async function rescanDrivers() {
|
||
|
|
toast('Scanning drivers...');
|
||
|
|
await api('drivers', 'scan');
|
||
|
|
toast('Scan complete');
|
||
|
|
loadDashboard();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function applyScopes() {
|
||
|
|
toast('Applying scopes...');
|
||
|
|
await api('scope', 'apply');
|
||
|
|
toast('Scopes applied');
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Init ---
|
||
|
|
document.addEventListener('DOMContentLoaded', () => {
|
||
|
|
loadDashboard();
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|