local-tool/: visionA-local desktop app
- M1: Wails shell + Go server + Next.js UI + Mock mode (macOS dmg ready)
- M2: i18n (zh-TW/en) + Settings 4-tab refactor
- M3: Embedded Python 3.12 runtime (python-build-standalone) + KneronPLUS wheels
- M4: Windows Inno Setup script (build on Windows runner)
- M5: Linux AppImage script + udev rule (build on Linux runner)
- M6: ffmpeg (GPL, pending legal review) + yt-dlp bundled
- Lifecycle: watchServer health check, fatal native dialog,
Wails IPC raise endpoint, stale process cleanup
.autoflow/: full PRD / Design Spec / Architecture / Testing docs
(4 rounds tri-party discussion + cross review)
.github/workflows/: macOS / Windows / Linux build CI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
541 lines
23 KiB
JavaScript
541 lines
23 KiB
JavaScript
// Edge AI Platform Installer — Wizard Controller v0.2
|
||
|
||
// ── i18n Dictionary ───────────────────────────────────────
|
||
const i18n = {
|
||
en: {
|
||
'welcome.title': 'Edge AI Platform Installer',
|
||
'welcome.subtitle': 'Set up your edge AI development environment with Kneron hardware support.',
|
||
'path.title': 'Installation Path',
|
||
'path.subtitle': 'Choose where to install Edge AI Platform.',
|
||
'path.browse': 'Browse',
|
||
'path.required': 'Installation path is required.',
|
||
'path.valid': 'Path is valid.',
|
||
'components.title': 'Select Components',
|
||
'components.subtitle': 'Choose which components to install.',
|
||
'components.server': 'Edge AI Server',
|
||
'components.serverDesc': 'Core server binary for hardware communication (~10 MB)',
|
||
'components.models': 'Kneron Models',
|
||
'components.modelsDesc': 'Pre-trained NEF model files for KL520/KL720 (~50 MB)',
|
||
'components.python': 'Python Environment',
|
||
'components.pythonDesc': 'Python venv with Kneron PLUS SDK and dependencies (~200 MB)',
|
||
'components.libusb': 'libusb',
|
||
'components.libusbDesc': 'USB library required for Kneron device communication',
|
||
'components.symlink': 'CLI Symlink',
|
||
'components.symlinkDesc': "Add 'edge-ai' command to /usr/local/bin",
|
||
'relay.title': 'Relay Configuration',
|
||
'relay.subtitle': 'Configure the relay server for remote access. You can skip this and configure later.',
|
||
'relay.url': 'Relay URL',
|
||
'relay.token': 'Relay Token',
|
||
'relay.port': 'Server Port',
|
||
'relay.hint': 'Leave empty to skip relay configuration. You can set this later in the config file.',
|
||
'progress.title': 'Installing...',
|
||
'progress.subtitle': 'Please wait while components are being installed.',
|
||
'progress.preparing': 'Preparing installation...',
|
||
'hardware.title': 'Hardware Detection',
|
||
'hardware.subtitle': 'Connect your Kneron devices and scan for hardware.',
|
||
'hardware.scanning': 'Scanning for devices...',
|
||
'hardware.noDevices': 'No Kneron devices found. Connect a device and try again.',
|
||
'hardware.rescan': 'Rescan',
|
||
'complete.title': 'Installation Complete',
|
||
'complete.subtitle': 'Edge AI Platform has been installed successfully.',
|
||
'complete.location': 'Install Location',
|
||
'complete.server': 'Edge AI Server',
|
||
'complete.models': 'Kneron Models',
|
||
'complete.python': 'Python Environment',
|
||
'complete.libusb': 'libusb',
|
||
'complete.installed': 'Installed',
|
||
'complete.skipped': 'Skipped',
|
||
'btn.next': 'Next',
|
||
'btn.back': 'Back',
|
||
'btn.install': 'Install',
|
||
'btn.launch': 'Launch Server',
|
||
'btn.openDashboard': 'Open Dashboard',
|
||
'btn.close': 'Close',
|
||
'relay.tokenHint': 'Auto-generated random token. Both the server and browser use this to authenticate with the relay.',
|
||
'relay.dashboardUrl': 'Dashboard URL',
|
||
'relay.dashboardHint': 'The HTTP URL to access the dashboard via relay. Opened after server launch.',
|
||
'existing.detected': 'Existing installation detected',
|
||
'existing.desc': 'An existing installation was found. You can uninstall it or install over it.',
|
||
'existing.uninstall': 'Uninstall',
|
||
'uninstall.title': 'Uninstalling...',
|
||
'uninstall.subtitle': 'Removing installed files.',
|
||
'uninstall.confirm': 'This will remove the Edge AI Platform and all installed files. Continue?',
|
||
'uninstall.complete': 'Uninstall complete. System dependencies (Python, libusb) were preserved.',
|
||
'system.platform': 'Platform',
|
||
'system.python': 'Python',
|
||
'system.libusb': 'libusb',
|
||
'system.ffmpeg': 'FFmpeg',
|
||
'status.installed': 'Installed',
|
||
'status.notFound': 'Not found',
|
||
'status.notInstalled': 'Not installed',
|
||
'status.optional': 'Not installed (optional)',
|
||
},
|
||
'zh-TW': {
|
||
'welcome.title': 'Edge AI 平台安裝程式',
|
||
'welcome.subtitle': '設定您的邊緣 AI 開發環境,支援 Kneron 硬體。',
|
||
'path.title': '安裝路徑',
|
||
'path.subtitle': '選擇 Edge AI 平台的安裝位置。',
|
||
'path.browse': '瀏覽',
|
||
'path.required': '安裝路徑為必填。',
|
||
'path.valid': '路徑有效。',
|
||
'components.title': '選擇元件',
|
||
'components.subtitle': '選擇要安裝的元件。',
|
||
'components.server': 'Edge AI 伺服器',
|
||
'components.serverDesc': '硬體通訊核心伺服器程式 (~10 MB)',
|
||
'components.models': 'Kneron 模型',
|
||
'components.modelsDesc': 'KL520/KL720 預訓練 NEF 模型檔案 (~50 MB)',
|
||
'components.python': 'Python 環境',
|
||
'components.pythonDesc': '包含 Kneron PLUS SDK 的 Python 虛擬環境 (~200 MB)',
|
||
'components.libusb': 'libusb',
|
||
'components.libusbDesc': 'Kneron 裝置通訊所需的 USB 函式庫',
|
||
'components.symlink': 'CLI 捷徑',
|
||
'components.symlinkDesc': "新增 'edge-ai' 指令到 /usr/local/bin",
|
||
'relay.title': 'Relay 設定',
|
||
'relay.subtitle': '設定 Relay 伺服器以進行遠端存取。可以跳過稍後再設定。',
|
||
'relay.url': 'Relay URL',
|
||
'relay.token': 'Relay Token',
|
||
'relay.port': '伺服器連接埠',
|
||
'relay.hint': '留空可跳過 Relay 設定,稍後可在設定檔中修改。',
|
||
'progress.title': '安裝中...',
|
||
'progress.subtitle': '正在安裝元件,請稍候。',
|
||
'progress.preparing': '準備安裝中...',
|
||
'hardware.title': '硬體偵測',
|
||
'hardware.subtitle': '連接您的 Kneron 裝置並掃描硬體。',
|
||
'hardware.scanning': '正在掃描裝置...',
|
||
'hardware.noDevices': '未偵測到 Kneron 裝置。請連接裝置後再試。',
|
||
'hardware.rescan': '重新掃描',
|
||
'complete.title': '安裝完成',
|
||
'complete.subtitle': 'Edge AI 平台已成功安裝。',
|
||
'complete.location': '安裝位置',
|
||
'complete.server': 'Edge AI 伺服器',
|
||
'complete.models': 'Kneron 模型',
|
||
'complete.python': 'Python 環境',
|
||
'complete.libusb': 'libusb',
|
||
'complete.installed': '已安裝',
|
||
'complete.skipped': '已跳過',
|
||
'btn.next': '下一步',
|
||
'btn.back': '上一步',
|
||
'btn.install': '安裝',
|
||
'btn.launch': '啟動伺服器',
|
||
'btn.openDashboard': '開啟控制台',
|
||
'btn.close': '關閉',
|
||
'relay.tokenHint': '自動產生的隨機 Token。伺服器和瀏覽器都透過此 Token 向 Relay 驗證身份。',
|
||
'relay.dashboardUrl': 'Dashboard URL',
|
||
'relay.dashboardHint': '透過 Relay 存取 Dashboard 的 HTTP URL。啟動伺服器後會自動開啟。',
|
||
'existing.detected': '偵測到既有安裝',
|
||
'existing.desc': '發現既有安裝。您可以解除安裝或覆蓋安裝。',
|
||
'existing.uninstall': '解除安裝',
|
||
'uninstall.title': '解除安裝中...',
|
||
'uninstall.subtitle': '正在移除已安裝的檔案。',
|
||
'uninstall.confirm': '這將移除 Edge AI 平台及所有已安裝的檔案。是否繼續?',
|
||
'uninstall.complete': '解除安裝完成。系統相依套件(Python、libusb)已保留。',
|
||
'system.platform': '平台',
|
||
'system.python': 'Python',
|
||
'system.libusb': 'libusb',
|
||
'system.ffmpeg': 'FFmpeg',
|
||
'status.installed': '已安裝',
|
||
'status.notFound': '未找到',
|
||
'status.notInstalled': '未安裝',
|
||
'status.optional': '未安裝(選用)',
|
||
}
|
||
};
|
||
|
||
// ── State ─────────────────────────────────────────────────
|
||
let currentStep = 0;
|
||
let systemInfo = null;
|
||
let installConfig = {
|
||
installDir: '',
|
||
createSymlink: true,
|
||
installPythonEnv: true,
|
||
installLibusb: true,
|
||
relayURL: '',
|
||
relayToken: '',
|
||
dashboardURL: '',
|
||
serverPort: 3721,
|
||
language: 'en',
|
||
};
|
||
|
||
// ── i18n Functions ────────────────────────────────────────
|
||
|
||
function t(key) {
|
||
const dict = i18n[installConfig.language] || i18n.en;
|
||
return dict[key] || i18n.en[key] || key;
|
||
}
|
||
|
||
function setLanguage(lang) {
|
||
installConfig.language = lang;
|
||
|
||
// Update active button state
|
||
document.getElementById('lang-en').classList.toggle('active', lang === 'en');
|
||
document.getElementById('lang-zh').classList.toggle('active', lang === 'zh-TW');
|
||
|
||
// Update all elements with data-i18n attribute
|
||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||
const key = el.getAttribute('data-i18n');
|
||
const text = t(key);
|
||
if (text) {
|
||
el.textContent = text;
|
||
}
|
||
});
|
||
|
||
// Update html lang attribute
|
||
document.documentElement.lang = lang === 'zh-TW' ? 'zh-TW' : 'en';
|
||
}
|
||
|
||
// ── Step Navigation ───────────────────────────────────────
|
||
|
||
function showStep(n) {
|
||
document.querySelectorAll('.step').forEach(s => s.classList.remove('active'));
|
||
document.querySelectorAll('.step-dot').forEach((d, i) => {
|
||
d.classList.remove('active', 'completed');
|
||
if (i < n) d.classList.add('completed');
|
||
if (i === n) d.classList.add('active');
|
||
});
|
||
const step = document.getElementById('step-' + n);
|
||
if (step) step.classList.add('active');
|
||
currentStep = n;
|
||
}
|
||
|
||
// ── Step 0: Welcome ───────────────────────────────────────
|
||
|
||
async function initWelcome() {
|
||
try {
|
||
systemInfo = await window.go.main.Installer.GetSystemInfo();
|
||
|
||
document.getElementById('info-platform').textContent =
|
||
systemInfo.os + ' / ' + systemInfo.arch;
|
||
|
||
const pyEl = document.getElementById('info-python');
|
||
if (systemInfo.pythonAvailable) {
|
||
pyEl.textContent = systemInfo.pythonVersion;
|
||
pyEl.className = 'info-value status-ok';
|
||
} else {
|
||
pyEl.textContent = t('status.notFound');
|
||
pyEl.className = 'info-value status-warn';
|
||
}
|
||
|
||
const luEl = document.getElementById('info-libusb');
|
||
luEl.textContent = systemInfo.libusbInstalled ? t('status.installed') : t('status.notInstalled');
|
||
luEl.className = 'info-value ' + (systemInfo.libusbInstalled ? 'status-ok' : 'status-warn');
|
||
|
||
const ffEl = document.getElementById('info-ffmpeg');
|
||
ffEl.textContent = systemInfo.ffmpegAvailable ? t('status.installed') : t('status.optional');
|
||
ffEl.className = 'info-value ' + (systemInfo.ffmpegAvailable ? 'status-ok' : '');
|
||
|
||
installConfig.installDir = systemInfo.defaultDir;
|
||
|
||
if (systemInfo.existingInstall) {
|
||
document.getElementById('existing-install').style.display = 'block';
|
||
}
|
||
} catch (err) {
|
||
console.error('GetSystemInfo failed:', err);
|
||
}
|
||
}
|
||
|
||
// ── Step 1: Path ──────────────────────────────────────────
|
||
|
||
document.getElementById('btn-browse').addEventListener('click', async () => {
|
||
try {
|
||
const dir = await window.go.main.Installer.BrowseDirectory();
|
||
if (dir) {
|
||
document.getElementById('install-path').value = dir;
|
||
installConfig.installDir = dir;
|
||
const msg = await window.go.main.Installer.ValidatePath(dir);
|
||
const statusEl = document.getElementById('path-status');
|
||
if (msg) {
|
||
statusEl.textContent = msg;
|
||
statusEl.className = 'status-text error';
|
||
} else {
|
||
statusEl.textContent = t('path.valid');
|
||
statusEl.className = 'status-text';
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('BrowseDirectory failed:', err);
|
||
}
|
||
});
|
||
|
||
// ── Step 4: Install Progress ──────────────────────────────
|
||
|
||
function addLogLine(message, type) {
|
||
const log = document.getElementById('progress-log');
|
||
const line = document.createElement('div');
|
||
line.className = 'log-' + (type || 'line');
|
||
line.textContent = message;
|
||
log.appendChild(line);
|
||
log.scrollTop = log.scrollHeight;
|
||
}
|
||
|
||
async function startInstall() {
|
||
showStep(4);
|
||
document.getElementById('progress-title').textContent = t('progress.title');
|
||
document.getElementById('progress-subtitle').textContent = t('progress.subtitle');
|
||
document.getElementById('progress-log').innerHTML = '';
|
||
document.getElementById('progress-fill').style.width = '0%';
|
||
document.getElementById('progress-percent').textContent = '0%';
|
||
document.getElementById('progress-message').textContent = t('progress.preparing');
|
||
|
||
try {
|
||
await window.go.main.Installer.StartInstall(installConfig);
|
||
} catch (err) {
|
||
addLogLine('Error: ' + err, 'error');
|
||
}
|
||
}
|
||
|
||
if (window.runtime && window.runtime.EventsOn) {
|
||
window.runtime.EventsOn('install:progress', (event) => {
|
||
const fill = document.getElementById('progress-fill');
|
||
const percent = document.getElementById('progress-percent');
|
||
const message = document.getElementById('progress-message');
|
||
|
||
fill.style.width = event.percent + '%';
|
||
percent.textContent = Math.round(event.percent) + '%';
|
||
message.textContent = event.message;
|
||
|
||
addLogLine(event.message, event.isError ? 'error' : (event.isComplete ? 'success' : 'line'));
|
||
|
||
if (event.isComplete && !event.isError) {
|
||
setTimeout(() => {
|
||
showStep(5);
|
||
detectHardware();
|
||
}, 500);
|
||
}
|
||
});
|
||
|
||
window.runtime.EventsOn('uninstall:progress', (event) => {
|
||
const message = document.getElementById('progress-message');
|
||
const fill = document.getElementById('progress-fill');
|
||
const percent = document.getElementById('progress-percent');
|
||
|
||
fill.style.width = event.percent + '%';
|
||
percent.textContent = Math.round(event.percent) + '%';
|
||
message.textContent = event.message;
|
||
|
||
addLogLine(event.message, event.isError ? 'error' : 'line');
|
||
|
||
if (event.isComplete) {
|
||
document.getElementById('progress-title').textContent = t('uninstall.title').replace('...', '');
|
||
document.getElementById('progress-subtitle').textContent = t('uninstall.complete');
|
||
addLogLine(t('uninstall.complete'), 'success');
|
||
}
|
||
});
|
||
}
|
||
|
||
// ── Step 5: Hardware Detection ────────────────────────────
|
||
|
||
async function detectHardware() {
|
||
const el = document.getElementById('hardware-results');
|
||
el.innerHTML =
|
||
'<div class="device-scanning">' +
|
||
'<div class="spinner"></div>' +
|
||
'<p>' + t('hardware.scanning') + '</p>' +
|
||
'</div>';
|
||
|
||
try {
|
||
const devices = await window.go.main.Installer.DetectHardware();
|
||
if (!devices || devices.length === 0) {
|
||
el.innerHTML = '<div class="no-devices"><p>' + t('hardware.noDevices') + '</p></div>';
|
||
} else {
|
||
el.innerHTML = devices.map(d =>
|
||
'<div class="device-card">' +
|
||
'<div class="device-icon">⬡</div>' +
|
||
'<div class="device-info">' +
|
||
'<span class="device-name">Kneron ' + (d.model || 'Unknown') + '</span>' +
|
||
'<span class="device-detail">' + (d.product || d.port || '') + '</span>' +
|
||
'</div>' +
|
||
'</div>'
|
||
).join('');
|
||
}
|
||
} catch (err) {
|
||
el.innerHTML = '<div class="no-devices"><p>Detection skipped: ' + err + '</p></div>';
|
||
}
|
||
}
|
||
|
||
document.getElementById('btn-rescan').addEventListener('click', () => {
|
||
detectHardware();
|
||
});
|
||
|
||
// ── Step 6: Complete ──────────────────────────────────────
|
||
|
||
document.getElementById('btn-launch').addEventListener('click', async () => {
|
||
try {
|
||
await window.go.main.Installer.LaunchServer();
|
||
// After launching, if dashboard URL is configured, open it automatically
|
||
const dashUrl = installConfig.dashboardURL || await window.go.main.Installer.GetDashboardURL();
|
||
if (dashUrl) {
|
||
// Give the server a moment to start, then open dashboard
|
||
setTimeout(async () => {
|
||
// Append relay token as query param if available
|
||
let url = dashUrl;
|
||
if (installConfig.relayToken) {
|
||
const sep = url.includes('?') ? '&' : '?';
|
||
url = url + sep + 'token=' + encodeURIComponent(installConfig.relayToken);
|
||
}
|
||
await window.go.main.Installer.OpenBrowser(url);
|
||
}, 2000);
|
||
} else {
|
||
// No relay — open local
|
||
setTimeout(async () => {
|
||
const port = installConfig.serverPort || 3721;
|
||
await window.go.main.Installer.OpenBrowser('http://127.0.0.1:' + port);
|
||
}, 2000);
|
||
}
|
||
} catch (err) {
|
||
alert('Failed to launch: ' + err);
|
||
}
|
||
});
|
||
|
||
document.getElementById('btn-open-dashboard').addEventListener('click', async () => {
|
||
const dashUrl = installConfig.dashboardURL || await window.go.main.Installer.GetDashboardURL();
|
||
if (dashUrl) {
|
||
let url = dashUrl;
|
||
if (installConfig.relayToken) {
|
||
const sep = url.includes('?') ? '&' : '?';
|
||
url = url + sep + 'token=' + encodeURIComponent(installConfig.relayToken);
|
||
}
|
||
await window.go.main.Installer.OpenBrowser(url);
|
||
}
|
||
});
|
||
|
||
document.getElementById('btn-close').addEventListener('click', () => {
|
||
if (window.runtime && window.runtime.Quit) {
|
||
window.runtime.Quit();
|
||
} else {
|
||
window.close();
|
||
}
|
||
});
|
||
|
||
// ── Uninstall ─────────────────────────────────────────────
|
||
|
||
document.getElementById('btn-uninstall').addEventListener('click', async () => {
|
||
try {
|
||
const confirmed = await window.go.main.Installer.ConfirmUninstall();
|
||
if (!confirmed) return;
|
||
} catch (err) {
|
||
// Fallback to JS confirm if native dialog fails
|
||
if (!confirm(t('uninstall.confirm'))) return;
|
||
}
|
||
showStep(4);
|
||
document.getElementById('progress-title').textContent = t('uninstall.title');
|
||
document.getElementById('progress-subtitle').textContent = t('uninstall.subtitle');
|
||
document.getElementById('progress-log').innerHTML = '';
|
||
document.getElementById('progress-fill').style.width = '0%';
|
||
document.getElementById('progress-percent').textContent = '0%';
|
||
|
||
try {
|
||
await window.go.main.Installer.Uninstall();
|
||
} catch (err) {
|
||
addLogLine('Error: ' + err, 'error');
|
||
}
|
||
});
|
||
|
||
// ── Navigation Wiring ─────────────────────────────────────
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// Detect initial language from browser
|
||
const browserLang = navigator.language || navigator.userLanguage || 'en';
|
||
const initialLang = browserLang.startsWith('zh') ? 'zh-TW' : 'en';
|
||
setLanguage(initialLang);
|
||
|
||
initWelcome();
|
||
|
||
// Language switcher
|
||
document.getElementById('lang-en').addEventListener('click', () => setLanguage('en'));
|
||
document.getElementById('lang-zh').addEventListener('click', () => setLanguage('zh-TW'));
|
||
|
||
// Step 0 -> Step 1
|
||
document.getElementById('btn-next-0').addEventListener('click', () => {
|
||
showStep(1);
|
||
document.getElementById('install-path').value = installConfig.installDir;
|
||
});
|
||
|
||
// Step 1 Back -> Step 0
|
||
document.getElementById('btn-back-1').addEventListener('click', () => {
|
||
showStep(0);
|
||
});
|
||
|
||
// Step 1 -> Step 2
|
||
document.getElementById('btn-next-1').addEventListener('click', async () => {
|
||
const msg = await window.go.main.Installer.ValidatePath(installConfig.installDir);
|
||
if (msg) {
|
||
const statusEl = document.getElementById('path-status');
|
||
statusEl.textContent = msg;
|
||
statusEl.className = 'status-text error';
|
||
return;
|
||
}
|
||
showStep(2);
|
||
});
|
||
|
||
// Step 2 Back -> Step 1
|
||
document.getElementById('btn-back-2').addEventListener('click', () => {
|
||
showStep(1);
|
||
});
|
||
|
||
// Step 2 Install -> Step 3 (Relay Config)
|
||
document.getElementById('btn-install').addEventListener('click', async () => {
|
||
installConfig.createSymlink = document.getElementById('comp-symlink').checked;
|
||
installConfig.installPythonEnv = document.getElementById('comp-python').checked;
|
||
installConfig.installLibusb = document.getElementById('comp-libusb').checked;
|
||
showStep(3);
|
||
|
||
// Auto-generate relay token if empty
|
||
const tokenInput = document.getElementById('relay-token');
|
||
if (!tokenInput.value.trim()) {
|
||
try {
|
||
const token = await window.go.main.Installer.GenerateToken();
|
||
tokenInput.value = token;
|
||
} catch (err) {
|
||
console.error('GenerateToken failed:', err);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Step 3 Back -> Step 2
|
||
document.getElementById('btn-back-3').addEventListener('click', () => {
|
||
showStep(2);
|
||
});
|
||
|
||
// Regenerate token button
|
||
document.getElementById('btn-regen-token').addEventListener('click', async () => {
|
||
try {
|
||
const token = await window.go.main.Installer.GenerateToken();
|
||
document.getElementById('relay-token').value = token;
|
||
} catch (err) {
|
||
console.error('GenerateToken failed:', err);
|
||
}
|
||
});
|
||
|
||
// Step 3 Next -> collect relay fields -> Step 4 (Progress) -> start install
|
||
document.getElementById('btn-next-3').addEventListener('click', () => {
|
||
installConfig.relayURL = document.getElementById('relay-url').value.trim();
|
||
installConfig.relayToken = document.getElementById('relay-token').value.trim();
|
||
installConfig.dashboardURL = document.getElementById('dashboard-url').value.trim();
|
||
const portVal = parseInt(document.getElementById('server-port').value, 10);
|
||
installConfig.serverPort = (portVal >= 1024 && portVal <= 65535) ? portVal : 3721;
|
||
startInstall();
|
||
});
|
||
|
||
// Step 5 Next -> Step 6 (Complete)
|
||
document.getElementById('btn-next-5').addEventListener('click', () => {
|
||
showStep(6);
|
||
document.getElementById('summary-path').textContent = installConfig.installDir;
|
||
|
||
const modelsEl = document.getElementById('summary-models');
|
||
modelsEl.textContent = t('complete.installed');
|
||
modelsEl.className = 'info-value status-ok';
|
||
|
||
const pyEl = document.getElementById('summary-python');
|
||
pyEl.textContent = installConfig.installPythonEnv ? t('complete.installed') : t('complete.skipped');
|
||
pyEl.className = 'info-value ' + (installConfig.installPythonEnv ? 'status-ok' : 'status-skipped');
|
||
|
||
const luEl = document.getElementById('summary-libusb');
|
||
luEl.textContent = installConfig.installLibusb ? t('complete.installed') : t('complete.skipped');
|
||
luEl.className = 'info-value ' + (installConfig.installLibusb ? 'status-ok' : 'status-skipped');
|
||
|
||
// Show "Open Dashboard" button if relay dashboard URL is configured
|
||
if (installConfig.dashboardURL) {
|
||
document.getElementById('btn-open-dashboard').style.display = 'inline-flex';
|
||
}
|
||
});
|
||
});
|