Compare commits
10 Commits
v1.0.0
..
1215b95df2
| Author | SHA1 | Date | |
|---|---|---|---|
| 1215b95df2 | |||
| a80074ac57 | |||
| 64f162440f | |||
| e30e0395d3 | |||
| d1013c87a1 | |||
| c3c3fc83e3 | |||
| 14ce5c728d | |||
| 60f56a2dea | |||
| 099cc252fe | |||
| 3a6be50e31 |
@@ -0,0 +1,37 @@
|
||||
name: Deploy GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/workflows/deploy-pages.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Configure Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -1,4 +1,4 @@
|
||||
# Alta Video Camera Proxy
|
||||
# Alta Video Camera Proxy
|
||||
|
||||
An Electron desktop application for managing Alta Video camera proxy connections. Authenticates via a companion Chrome extension that imports your existing Alta session cookie, discovers cameras, and launches proxy connections.
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# build-kit.ps1 — Build the Alta Proxy Tool Kit zip for distribution
|
||||
# Usage: powershell -ExecutionPolicy Bypass -File build-kit.ps1
|
||||
#
|
||||
# Produces: dist/AltaProxyToolKit.zip containing:
|
||||
# - AltaCameraProxy-<version>-portable.exe
|
||||
# - aware-cam-proxy.exe
|
||||
# - chrome-extension/ (folder)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Read version from package.json
|
||||
$pkg = Get-Content "package.json" -Raw | ConvertFrom-Json
|
||||
$version = $pkg.version
|
||||
Write-Host "Building Alta Proxy Tool Kit v$version" -ForegroundColor Cyan
|
||||
|
||||
# Step 1: Build the Electron portable exe
|
||||
Write-Host "`n[1/4] Building Electron app..." -ForegroundColor Yellow
|
||||
npm run build
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Build failed!" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$portableExe = "dist\AltaCameraProxy-$version-portable.exe"
|
||||
if (-not (Test-Path $portableExe)) {
|
||||
Write-Host "Expected output not found: $portableExe" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host " Built: $portableExe" -ForegroundColor Green
|
||||
|
||||
# Step 2: Verify aware-cam-proxy.exe exists
|
||||
Write-Host "`n[2/4] Checking for aware-cam-proxy.exe..." -ForegroundColor Yellow
|
||||
if (-not (Test-Path "aware-cam-proxy.exe")) {
|
||||
Write-Host "aware-cam-proxy.exe not found in project root!" -ForegroundColor Red
|
||||
Write-Host "Place aware-cam-proxy.exe in the project root and try again." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host " Found: aware-cam-proxy.exe" -ForegroundColor Green
|
||||
|
||||
# Step 3: Verify chrome-extension folder exists
|
||||
Write-Host "`n[3/4] Checking for chrome-extension/..." -ForegroundColor Yellow
|
||||
if (-not (Test-Path "chrome-extension\manifest.json")) {
|
||||
Write-Host "chrome-extension/ folder not found or missing manifest.json!" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host " Found: chrome-extension/" -ForegroundColor Green
|
||||
|
||||
# Step 4: Create the kit zip
|
||||
Write-Host "`n[4/4] Creating AltaProxyToolKit.zip..." -ForegroundColor Yellow
|
||||
|
||||
$kitDir = "dist\AltaProxyToolKit"
|
||||
$zipPath = "dist\AltaProxyToolKit.zip"
|
||||
|
||||
# Clean previous kit
|
||||
if (Test-Path $kitDir) { Remove-Item $kitDir -Recurse -Force }
|
||||
if (Test-Path $zipPath) { Remove-Item $zipPath -Force }
|
||||
|
||||
# Assemble kit contents
|
||||
New-Item -ItemType Directory -Path $kitDir | Out-Null
|
||||
Copy-Item $portableExe "$kitDir\AltaCameraProxy.exe"
|
||||
Copy-Item "aware-cam-proxy.exe" "$kitDir\aware-cam-proxy.exe"
|
||||
Copy-Item "chrome-extension" "$kitDir\chrome-extension" -Recurse
|
||||
|
||||
# Create zip
|
||||
Compress-Archive -Path "$kitDir\*" -DestinationPath $zipPath -Force
|
||||
|
||||
# Clean up temp directory
|
||||
Remove-Item $kitDir -Recurse -Force
|
||||
|
||||
$zipSize = [math]::Round((Get-Item $zipPath).Length / 1MB, 1)
|
||||
Write-Host "`nKit ready: $zipPath ($zipSize MB)" -ForegroundColor Green
|
||||
Write-Host "Upload this file as a release asset named 'AltaProxyToolKit.zip'" -ForegroundColor Cyan
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Alta Proxy Tool Bridge",
|
||||
"version": "1.0.0",
|
||||
"description": "Send Alta session cookies to the Alta Proxy Tool desktop app.",
|
||||
"permissions": ["cookies", "activeTab"],
|
||||
"permissions": ["cookies", "activeTab", "clipboardWrite"],
|
||||
"host_permissions": [
|
||||
"https://*.avasecurity.com/*",
|
||||
"https://*.avigilon.com/*",
|
||||
|
||||
@@ -44,7 +44,13 @@ h1 {
|
||||
border-color: #F44336;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
.action-buttons {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.primary-btn,
|
||||
.secondary-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
@@ -59,11 +65,26 @@ h1 {
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.send-btn:hover:not(:disabled) {
|
||||
.primary-btn {
|
||||
background: #0E7AFE;
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
background: #2D2D30;
|
||||
border: 1px solid #0E7AFE;
|
||||
color: #E0E0E0;
|
||||
}
|
||||
|
||||
.primary-btn:hover:not(:disabled) {
|
||||
background: #0A5FD9;
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
.secondary-btn:hover:not(:disabled) {
|
||||
background: #0E7AFE;
|
||||
}
|
||||
|
||||
.primary-btn:disabled,
|
||||
.secondary-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
<div class="popup-container">
|
||||
<h1>Alta Proxy Tool</h1>
|
||||
<div id="tabInfo" class="tab-info">Checking tab...</div>
|
||||
<button id="sendBtn" class="send-btn" disabled>Send Cookie to APT</button>
|
||||
<div class="action-buttons">
|
||||
<button id="sendBtn" class="primary-btn" disabled>Send to APT</button>
|
||||
<button id="copyBtn" class="secondary-btn" disabled>Copy VA Token</button>
|
||||
</div>
|
||||
<div id="statusMsg" class="status-msg"></div>
|
||||
</div>
|
||||
<script src="popup.js"></script>
|
||||
|
||||
+50
-20
@@ -3,6 +3,7 @@ const APT_TOKEN = 'apt-local-bridge-token';
|
||||
|
||||
const tabInfo = document.getElementById('tabInfo');
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
const copyBtn = document.getElementById('copyBtn');
|
||||
const statusMsg = document.getElementById('statusMsg');
|
||||
|
||||
let detectedOrigin = null;
|
||||
@@ -12,6 +13,29 @@ function showStatus(message, type) {
|
||||
statusMsg.className = 'status-msg ' + type;
|
||||
}
|
||||
|
||||
function setActionButtonsDisabled(disabled) {
|
||||
sendBtn.disabled = disabled;
|
||||
copyBtn.disabled = disabled;
|
||||
}
|
||||
|
||||
async function getVaCookieValue() {
|
||||
if (!detectedOrigin) {
|
||||
throw new Error('No Alta deployment detected.');
|
||||
}
|
||||
|
||||
const cookie = await chrome.cookies.get({ url: detectedOrigin, name: 'va' });
|
||||
|
||||
if (!cookie || !cookie.value) {
|
||||
throw new Error('No "va" session cookie found. Are you logged in?');
|
||||
}
|
||||
|
||||
if (cookie.expirationDate && cookie.expirationDate < Date.now() / 1000) {
|
||||
throw new Error('Session cookie has expired. Please log in again.');
|
||||
}
|
||||
|
||||
return cookie.value;
|
||||
}
|
||||
|
||||
// Check the active tab on popup open
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
if (!tabs || tabs.length === 0) {
|
||||
@@ -42,30 +66,18 @@ chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
detectedOrigin = url.origin;
|
||||
tabInfo.textContent = 'Detected: ' + hostname;
|
||||
tabInfo.className = 'tab-info detected';
|
||||
sendBtn.disabled = false;
|
||||
setActionButtonsDisabled(false);
|
||||
});
|
||||
|
||||
// Send cookie on button click
|
||||
sendBtn.addEventListener('click', async () => {
|
||||
if (!detectedOrigin) return;
|
||||
|
||||
sendBtn.disabled = true;
|
||||
showStatus('Retrieving cookie...', 'info');
|
||||
setActionButtonsDisabled(true);
|
||||
showStatus('Retrieving VA token...', 'info');
|
||||
|
||||
try {
|
||||
const cookie = await chrome.cookies.get({ url: detectedOrigin, name: 'va' });
|
||||
|
||||
if (!cookie || !cookie.value) {
|
||||
showStatus('No "va" session cookie found. Are you logged in?', 'error');
|
||||
sendBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (cookie.expirationDate && cookie.expirationDate < Date.now() / 1000) {
|
||||
showStatus('Session cookie has expired. Please log in again.', 'error');
|
||||
sendBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
const cookieValue = await getVaCookieValue();
|
||||
|
||||
showStatus('Sending to Alta Proxy Tool...', 'info');
|
||||
|
||||
@@ -77,17 +89,16 @@ sendBtn.addEventListener('click', async () => {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
deploymentUrl: detectedOrigin,
|
||||
cookieValue: cookie.value
|
||||
cookieValue
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showStatus('Cookie sent successfully!', 'success');
|
||||
showStatus('VA token sent successfully!', 'success');
|
||||
} else {
|
||||
showStatus('Error: ' + (data.message || 'Unknown error'), 'error');
|
||||
sendBtn.disabled = false;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.message && err.message.includes('Failed to fetch')) {
|
||||
@@ -95,6 +106,25 @@ sendBtn.addEventListener('click', async () => {
|
||||
} else {
|
||||
showStatus('Error: ' + err.message, 'error');
|
||||
}
|
||||
sendBtn.disabled = false;
|
||||
} finally {
|
||||
setActionButtonsDisabled(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Copy VA token on button click
|
||||
copyBtn.addEventListener('click', async () => {
|
||||
if (!detectedOrigin) return;
|
||||
|
||||
setActionButtonsDisabled(true);
|
||||
showStatus('Retrieving VA token...', 'info');
|
||||
|
||||
try {
|
||||
const cookieValue = await getVaCookieValue();
|
||||
await navigator.clipboard.writeText(cookieValue);
|
||||
showStatus('VA token copied to clipboard!', 'success');
|
||||
} catch (err) {
|
||||
showStatus('Error: ' + err.message, 'error');
|
||||
} finally {
|
||||
setActionButtonsDisabled(false);
|
||||
}
|
||||
});
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
+341
@@ -0,0 +1,341 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- GitHub Pages rebuild marker: 2026-05-22 -->
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Alta Proxy Tool</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-primary: #1E1E1E;
|
||||
--bg-secondary: #2D2D30;
|
||||
--border: #3C3C3C;
|
||||
--text-primary: #E0E0E0;
|
||||
--text-secondary: #999999;
|
||||
--accent-primary: #0E7AFE;
|
||||
--accent-primary-hover: #0A5FD9;
|
||||
--card-bg: #2D2D30;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 16px 0;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.header .container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-brand img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.header-brand span {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.header-link {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.header-link:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Hero */
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 100px 0 48px;
|
||||
}
|
||||
|
||||
.hero-icon {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.hero .tagline {
|
||||
font-size: 18px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 40px;
|
||||
max-width: 500px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.btn-download {
|
||||
display: inline-block;
|
||||
background: var(--accent-primary);
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
padding: 16px 40px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-download:hover {
|
||||
background: var(--accent-primary-hover);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.hero-note {
|
||||
margin-top: 14px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Kit contents */
|
||||
.kit-info {
|
||||
padding: 48px 0;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.kit-info h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.kit-contents {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.kit-item {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 20px 28px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.kit-item strong {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.kit-item span {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Setup */
|
||||
.setup {
|
||||
padding: 48px 0;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.setup h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.setup-steps {
|
||||
list-style: none;
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
counter-reset: step;
|
||||
}
|
||||
|
||||
.setup-steps li {
|
||||
counter-increment: step;
|
||||
padding: 12px 0 12px 44px;
|
||||
position: relative;
|
||||
font-size: 15px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.setup-steps li + li {
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.setup-steps li::before {
|
||||
content: counter(step);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 12px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: var(--accent-primary);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.setup-steps code {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.setup-note {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 24px 0;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 600px) {
|
||||
.hero {
|
||||
padding: 60px 0 36px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.hero .tagline {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn-download {
|
||||
font-size: 16px;
|
||||
padding: 12px 28px;
|
||||
}
|
||||
|
||||
.kit-contents {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.kit-item {
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
<div class="container">
|
||||
<div class="header-brand">
|
||||
<img src="icon.png" alt="Alta Proxy Tool">
|
||||
<span>Alta Proxy Tool</span>
|
||||
</div>
|
||||
<a href="https://github.com/PageZ948/Alta-Proxy-Tool" class="header-link">GitHub</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="container">
|
||||
|
||||
<div class="hero">
|
||||
<img src="icon.png" alt="" class="hero-icon">
|
||||
<h1>Alta Proxy Tool</h1>
|
||||
<a href="https://github.com/PageZ948/Alta-Proxy-Tool/releases/latest/download/AltaProxyToolKit.zip" class="btn-download">Download Kit for Windows</a>
|
||||
</div>
|
||||
|
||||
<section class="setup">
|
||||
<h2>Setup</h2>
|
||||
<ol class="setup-steps">
|
||||
<li>Extract the zip to any folder</li>
|
||||
<li>Load <code>chrome-extension/</code> in Chrome via <code>chrome://extensions</code> (Developer mode)</li>
|
||||
<li>Log into your Alta deployment in Chrome</li>
|
||||
<li>Click the extension icon and send cookies to the app</li>
|
||||
<li>Run <strong>AltaCameraProxy.exe</strong> and start proxying</li>
|
||||
</ol>
|
||||
<p class="setup-note">Requires Windows 10+, Google Chrome, and an Avigilon Alta account.</p>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<a href="https://github.com/PageZ948/Alta-Proxy-Tool">Alta Proxy Tool on GitHub</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,533 @@
|
||||
# Alta Proxy Tool Security Foundation and Product Roadmap Implementation Plan
|
||||
|
||||
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Remove APT’s Critical/Important security and release blockers, establish a reproducible Windows baseline, recover the Mac implementation into source control, and only then open a controlled lane for new features.
|
||||
|
||||
**Architecture:** Keep Electron’s renderer unprivileged. Move session credentials, URL validation, proxy launching, process ownership, and update verification into small main-process modules with narrow IPC contracts. Development occurs in an isolated GitPeji worktree; existing Tool Hub downloads and production surfaces remain unchanged until a fully tested, exact-commit release is approved.
|
||||
|
||||
**Tech Stack:** Electron, Node.js built-in `node:test`, Chrome Manifest V3 extension APIs, Electron Builder, GitPeji/Gitea Actions, SHA-256 release manifests, Windows Authenticode when available.
|
||||
|
||||
---
|
||||
|
||||
## Executive sequencing
|
||||
|
||||
### Phase A — Security containment and proof
|
||||
|
||||
No feature work enters this phase. It must close:
|
||||
|
||||
1. arbitrary-code self-updater;
|
||||
2. CR/LF batch command injection;
|
||||
3. renderer-controlled URL/cookie exfiltration;
|
||||
4. cookie leakage through logs, temporary files, command lines, and broad clipboard handling;
|
||||
5. stale/vulnerable Electron, Axios, and build dependencies.
|
||||
|
||||
### Phase B — Product/release foundation
|
||||
|
||||
1. canonical GitPeji source and exact tagged builds;
|
||||
2. automated tests and CI;
|
||||
3. reproducible Windows kit with helper provenance;
|
||||
4. recover Mac source, then separately decide Intel/Rosetta versus native Apple Silicon;
|
||||
5. safe, signed/digest-bound updater only after all earlier gates pass.
|
||||
|
||||
### Phase C — New features
|
||||
|
||||
Feature discovery begins only after Phase A passes independent security review and Phase B has a reproducible development build. New functions go into a written backlog with user value, permissions, data touched, risk, and acceptance tests before implementation.
|
||||
|
||||
## Non-negotiable guardrails
|
||||
|
||||
- GitPeji is the only source of truth. Do not fetch from, push to, publish on, or update from GitHub.
|
||||
- Use a fresh worktree from GitPeji `master` at `a80074ac57b7a4517837b5d95754f5e6433df3ac` or newer.
|
||||
- Preserve current Tool Hub downloads and production visibility until Zac approves replacement.
|
||||
- Do not use real Alta cookies in automated tests; use conspicuous synthetic sentinels.
|
||||
- Do not write cookies to logs, disk, command files, shell strings, crash reports, analytics, or test artifacts.
|
||||
- No production release until the exact candidate passes controller verification plus independent security and release reviews.
|
||||
- Mac distribution remains approval-gated under Kanban task `t_42bad4d3`.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 0: Establish an isolated hardening lane
|
||||
|
||||
### Task 1: Create the clean GitPeji worktree and baseline evidence
|
||||
|
||||
**Objective:** Start from an exact, clean canonical source without modifying the archived checkout.
|
||||
|
||||
**Files:**
|
||||
- Create worktree: `/home/peji/worktrees/apt-security-foundation`
|
||||
- Create: `docs/security/2026-08-security-baseline.md`
|
||||
- Reference: `/home/peji/.hermes/reports/alta-proxy-tool-full-review-2026-08-19.md`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Fetch only GitPeji and verify `origin/master`.
|
||||
2. Create branch `hardening/security-foundation` in the isolated worktree.
|
||||
3. Record full starting SHA, remote, branch, clean status, dependency audit counts, current package version, and the review report path.
|
||||
4. Confirm the archived checkout and Tool Hub checkout remain clean.
|
||||
5. Commit only the baseline document.
|
||||
|
||||
**Commands:**
|
||||
|
||||
```bash
|
||||
git fetch origin master
|
||||
git worktree add -b hardening/security-foundation /home/peji/worktrees/apt-security-foundation origin/master
|
||||
git -C /home/peji/worktrees/apt-security-foundation status --short --branch
|
||||
```
|
||||
|
||||
**Expected:** clean branch based on exact GitPeji `origin/master`; no GitHub remote used.
|
||||
|
||||
**Commit:** `docs: record APT security hardening baseline`
|
||||
|
||||
---
|
||||
|
||||
## Milestone 1: Add a testable security boundary
|
||||
|
||||
### Task 2: Introduce the smallest useful test harness
|
||||
|
||||
**Objective:** Make security fixes test-first instead of relying on ad-hoc probes.
|
||||
|
||||
**Files:**
|
||||
- Modify: `package.json`
|
||||
- Create: `test/url-policy.test.js`
|
||||
- Create: `test/error-redaction.test.js`
|
||||
- Create: `test/proxy-launch.test.js`
|
||||
- Create: `test/update-policy.test.js`
|
||||
- Create: `test/bridge-auth.test.js`
|
||||
- Create: `src/url-policy.js`
|
||||
- Create: `src/error-redaction.js`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Add `npm test` using `node --test` and `npm run check` for syntax/tests.
|
||||
2. Write failing URL-policy tests for:
|
||||
- HTTPS Alta subdomains accepted;
|
||||
- HTTP rejected;
|
||||
- non-Alta hosts rejected;
|
||||
- userinfo, fragments, unexpected paths/ports, CR/LF, and malformed URLs rejected.
|
||||
3. Write failing error-redaction tests proving a synthetic `va=HERMES_SENTINEL_SECRET` never appears in formatted output.
|
||||
4. Implement minimal pure helpers.
|
||||
5. Run tests and syntax checks.
|
||||
|
||||
**Commands:**
|
||||
|
||||
```bash
|
||||
npm test
|
||||
npm run check
|
||||
```
|
||||
|
||||
**Expected:** all tests pass; no source code needs Electron to test URL/redaction logic.
|
||||
|
||||
**Commit:** `test: add APT security regression harness`
|
||||
|
||||
---
|
||||
|
||||
## Milestone 2: Remove the Critical updater path
|
||||
|
||||
### Task 3: Disable executable self-update completely
|
||||
|
||||
**Objective:** Remove the current arbitrary-native-code installation surface before redesigning updates.
|
||||
|
||||
**Files:**
|
||||
- Modify: `main.js:454-630`
|
||||
- Modify: `preload.js:25-37`
|
||||
- Modify: `renderer.js:24-34,57-61,602-712,756-760`
|
||||
- Modify: `index.html:38-41,84-106`
|
||||
- Modify: `README.md:13,68-74,166`
|
||||
- Test: `test/update-policy.test.js`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Write failing source/contract tests asserting no `download-and-install-update` IPC method is exposed and no executable replacement code remains.
|
||||
2. Remove updater IPC handlers, redirect downloader, executable overwrite batch script, progress event, and install controls.
|
||||
3. Replace the UI with either no update control or a safe informational link to an authenticated/internal release page; do not download or execute anything.
|
||||
4. Remove startup network update checks.
|
||||
5. Run tests and confirm the app still starts/builds.
|
||||
|
||||
**Acceptance:** no renderer-to-main path can download, overwrite, or launch an executable.
|
||||
|
||||
**Commit:** `fix: remove unsafe in-place updater`
|
||||
|
||||
---
|
||||
|
||||
## Milestone 3: Close command injection and process-control hazards
|
||||
|
||||
### Task 4: Replace `.bat` construction with direct process spawning
|
||||
|
||||
**Objective:** Eliminate command injection and plaintext cookie batch files.
|
||||
|
||||
**Files:**
|
||||
- Create: `src/proxy-launch.js`
|
||||
- Modify: `main.js:26-31,274-387`
|
||||
- Test: `test/proxy-launch.test.js`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Write failing tests for CR/LF, shell metacharacters, malformed UUIDs, invalid domains, oversized cookie values, and missing helper.
|
||||
2. Define strict input contracts:
|
||||
- deployment host comes from validated main-process session state;
|
||||
- device UUID matches the exact supported identifier shape;
|
||||
- cookie length is bounded and never serialised into a command string;
|
||||
- executable path is resolved from an approved fixed location.
|
||||
3. Spawn `aware-cam-proxy.exe` directly with an argument array and `shell: false`.
|
||||
4. Remove `sanitizeBatchInput()`, temporary `.bat` files, `cmd /c start`, and cookie-bearing echo output.
|
||||
5. Capture only process ID, device ID, start time, and safe status metadata.
|
||||
6. Add a Windows-specific smoke harness that substitutes a harmless fixture executable/script and records argv without exposing the cookie.
|
||||
|
||||
**Acceptance:** the CR/LF reproducer is rejected; no batch/command file exists; cookie is not printed or persisted.
|
||||
|
||||
**Commit:** `fix: launch proxy without shell command files`
|
||||
|
||||
### Task 5: Stop only APT-owned proxy processes
|
||||
|
||||
**Objective:** Prevent “Stop Proxy” from killing unrelated helper processes.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/proxy-launch.js`
|
||||
- Modify: `main.js:389-452`
|
||||
- Modify: `renderer.js:147-177`
|
||||
- Test: `test/proxy-launch.test.js`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Write a failing test proving only tracked child process IDs are terminated.
|
||||
2. Replace `taskkill /im aware-cam-proxy.exe` with tracked-process termination.
|
||||
3. Return per-device stop results and remove dead entries from the process map.
|
||||
4. Handle already-exited and permission-denied states honestly.
|
||||
5. Verify two fixture processes: stopping one leaves the other alive.
|
||||
|
||||
**Commit:** `fix: scope proxy termination to APT processes`
|
||||
|
||||
---
|
||||
|
||||
## Milestone 4: Move trust and cookies into the main process
|
||||
|
||||
### Task 6: Replace renderer-supplied URL/cookie IPC parameters
|
||||
|
||||
**Objective:** Prevent a compromised renderer from sending Alta cookies to arbitrary destinations.
|
||||
|
||||
**Files:**
|
||||
- Create: `src/session-store.js`
|
||||
- Create: `src/alta-client.js`
|
||||
- Modify: `main.js:192-272`
|
||||
- Modify: `preload.js:5-12`
|
||||
- Modify: `renderer.js:225-304,714-743`
|
||||
- Test: `test/url-policy.test.js`
|
||||
- Create: `test/alta-client.test.js`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Write failing tests showing API methods reject renderer-provided URL/cookie fields.
|
||||
2. Store deployment origin and cookie only in main-process memory.
|
||||
3. Expose narrow IPC calls: `getDevices()`, `getDeviceSites()`, `getAuthInfo()`, `launchProxy(deviceId)`, `disconnect()`.
|
||||
4. Revalidate HTTPS Alta origin inside the main process before every request.
|
||||
5. Disable ambient proxy use for cookie-bearing requests unless a separately reviewed explicit enterprise-proxy configuration is later required.
|
||||
6. Disable cross-origin redirects or validate every redirect before forwarding credentials.
|
||||
7. Validate `event.senderFrame` against the app’s local file origin/window.
|
||||
8. On disconnect and app quit, overwrite references and clear session state.
|
||||
|
||||
**Acceptance:** renderer messages cannot choose a destination or supply/receive the bearer cookie.
|
||||
|
||||
**Commit:** `fix: keep Alta session authority in the main process`
|
||||
|
||||
### Task 7: Redact all network and process errors
|
||||
|
||||
**Objective:** Ensure failures cannot print cookies or secret-bearing request configuration.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/error-redaction.js`
|
||||
- Modify: `src/alta-client.js`
|
||||
- Modify: `main.js`
|
||||
- Test: `test/error-redaction.test.js`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Add synthetic Axios errors containing secrets in headers, URLs, nested config, causes, and response bodies.
|
||||
2. Format only bounded fields: operation, safe error code, HTTP status, timeout flag, and a sanitised message.
|
||||
3. Remove all full-object `console.error(..., error)` calls from credential-bearing paths.
|
||||
4. Add a source scan test for forbidden logging patterns.
|
||||
5. Run a sentinel failure probe and assert zero secret occurrences in stdout/stderr.
|
||||
|
||||
**Commit:** `fix: redact credentials from APT errors`
|
||||
|
||||
---
|
||||
|
||||
## Milestone 5: Replace the public hard-coded bridge secret
|
||||
|
||||
### Task 8: Add explicit extension pairing
|
||||
|
||||
**Objective:** Stop treating `apt-local-bridge-token` as authentication.
|
||||
|
||||
**Recommended v1 design:** keep loopback temporarily but pair the extension to the desktop app with a random per-install secret and a stable extension identity. Native Messaging is a later option if portability/setup proves acceptable.
|
||||
|
||||
**Files:**
|
||||
- Create: `src/bridge-auth.js`
|
||||
- Modify: `main.js:12-145`
|
||||
- Modify: `chrome-extension/manifest.json`
|
||||
- Modify: `chrome-extension/popup.js`
|
||||
- Create: `chrome-extension/options.html`
|
||||
- Create: `chrome-extension/options.js`
|
||||
- Test: `test/bridge-auth.test.js`
|
||||
- Create: `test/extension-contract.test.js`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Write failing tests for missing/wrong secret, unknown extension origin, replay/rotation, oversized body, slow body, and malformed JSON.
|
||||
2. Generate a cryptographically random pairing secret and store it with user-only permissions under Electron `userData`.
|
||||
3. Add a one-time pairing flow: desktop shows code; extension stores the resulting secret in `chrome.storage.local`.
|
||||
4. Give the extension a stable ID and accept only that exact extension origin.
|
||||
5. Compare secrets in constant time.
|
||||
6. Add request/body deadlines and bounded concurrency before body reads.
|
||||
7. Rotate/revoke pairing from the desktop UI.
|
||||
8. Remove `apt-local-bridge-token` from source.
|
||||
|
||||
**Acceptance:** a different Chrome extension cannot submit data even when it knows the source code; pairing can be revoked without reinstalling APT.
|
||||
|
||||
**Commit:** `feat: add paired authentication for the Chrome bridge`
|
||||
|
||||
### Task 9: Make clipboard token copying explicitly guarded
|
||||
|
||||
**Objective:** Retain Zac’s requested copy function without presenting it as harmless.
|
||||
|
||||
**Files:**
|
||||
- Modify: `chrome-extension/popup.html`
|
||||
- Modify: `chrome-extension/popup.js`
|
||||
- Modify: `chrome-extension/popup.css`
|
||||
- Test: `test/extension-contract.test.js`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Require a deliberate confirmation for copying the full bearer token.
|
||||
2. Show a concise warning that clipboard history/synchronisation may retain it.
|
||||
3. Never auto-copy.
|
||||
4. Clear extension references immediately after the operation.
|
||||
5. Test cancellation, permission denial, missing/expired cookie, and success messaging.
|
||||
|
||||
**Commit:** `fix: guard VA token clipboard export`
|
||||
|
||||
---
|
||||
|
||||
## Milestone 6: Modernise dependencies and desktop hardening
|
||||
|
||||
### Task 10: Upgrade Electron, Axios, and packaging dependencies
|
||||
|
||||
**Objective:** Remove known dependency blockers without mixing in feature changes.
|
||||
|
||||
**Files:**
|
||||
- Modify: `package.json`
|
||||
- Modify: `package-lock.json`
|
||||
- Modify: `main.js:148-167`
|
||||
- Remove dependency if unused: `crypto-js`
|
||||
- Remove/replace: `electron-packager`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Create a dependency-upgrade branch commit separately from behaviour changes.
|
||||
2. Upgrade Electron to a currently supported major compatible with the target Windows estate.
|
||||
3. Upgrade Axios and transitive packages to patched releases.
|
||||
4. Remove unused `crypto-js` and deprecated `electron-packager` if source/runtime scans confirm no use.
|
||||
5. Enable explicit renderer sandboxing and deny unexpected navigation/window creation.
|
||||
6. Run full tests, `npm audit`, Electron smoke, and Windows package build.
|
||||
7. Do not accept Critical/High runtime findings; document any build-only exception with CVE reachability evidence.
|
||||
|
||||
**Acceptance:** zero Critical/High production audit findings; supported Electron line; app smoke passes.
|
||||
|
||||
**Commit:** `chore: modernize Electron runtime and dependencies`
|
||||
|
||||
---
|
||||
|
||||
## Milestone 7: Canonical source and reproducible releases
|
||||
|
||||
### Task 11: Add helper provenance without committing the binary blindly
|
||||
|
||||
**Objective:** Make the Windows kit reproducible and auditable while respecting vendor licensing.
|
||||
|
||||
**Files:**
|
||||
- Create: `vendor/aware-cam-proxy/README.md`
|
||||
- Create: `vendor/aware-cam-proxy/checksums.json`
|
||||
- Modify: `build-kit.ps1`
|
||||
- Modify: `README.md`
|
||||
- Create: `LICENSE`
|
||||
- Create: `THIRD_PARTY_NOTICES.md`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Confirm redistribution rights for Windows/macOS helper binaries; stop if unavailable.
|
||||
2. Record approved filename, platform, architecture, version/source owner, exact size, SHA-256, and acquisition procedure.
|
||||
3. Make the kit build reject any helper that does not match the approved manifest.
|
||||
4. Replace `author: "Your Name"` and add accurate project/third-party licensing.
|
||||
5. Add ZIP content, path-safety, extension-content, executable-presence, and checksum tests.
|
||||
|
||||
**Commit:** `build: verify proxy helper provenance in release kits`
|
||||
|
||||
### Task 12: Recover Mac implementation into GitPeji source
|
||||
|
||||
**Objective:** Stop treating the Tool Hub Mac ZIP as source control.
|
||||
|
||||
**Files:**
|
||||
- Recover into: `src/proxy-launch.js`
|
||||
- Modify: `package.json`
|
||||
- Modify: `README.md`
|
||||
- Create: `docs/platforms/macos.md`
|
||||
- Test: `test/proxy-launch.test.js`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Extract and diff the current Tool Hub Mac `app.asar` against the hardened source.
|
||||
2. Port only reviewed platform logic; do not copy the stripped packaged `package.json` wholesale.
|
||||
3. Reuse the direct-spawn/no-shell/session rules from Windows.
|
||||
4. Add explicit x64 and arm64 packaging targets, but publish only architectures with a matching approved helper.
|
||||
5. Test on Intel/Rosetta and a current Apple Silicon Mac.
|
||||
6. Keep Kanban `t_42bad4d3` open until native Apple Silicon/helper support is proven or Zac explicitly accepts Rosetta-only scope.
|
||||
|
||||
**Commit:** `feat: restore reviewed macOS source and build targets`
|
||||
|
||||
### Task 13: Add GitPeji CI and exact-commit release evidence
|
||||
|
||||
**Objective:** Build every release from one tagged GitPeji commit with retained evidence.
|
||||
|
||||
**Files:**
|
||||
- Create: `.gitea/workflows/ci.yml`
|
||||
- Create: `.gitea/workflows/release.yml`
|
||||
- Remove or retire: `.github/workflows/deploy-pages.yml`
|
||||
- Modify: `docs/index.html`
|
||||
- Create: `scripts/verify-kit.js`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. CI gate: `npm ci`, syntax, tests, audit policy, Windows unpacked build, kit verification, secret scan.
|
||||
2. Release gate: tag/version match, clean source, approved helper checksum, exact artifact hashes, SBOM, checksums, retained logs.
|
||||
3. Point docs/downloads to GitPeji or the approved Tool Hub route, never GitHub.
|
||||
4. Publish no artifact automatically until the first manual release rehearsal passes.
|
||||
5. Rehearse on a non-production candidate tag and verify fresh download/extraction.
|
||||
|
||||
**Commit:** `ci: add GitPeji build and release gates`
|
||||
|
||||
---
|
||||
|
||||
## Milestone 8: Reintroduce updates safely, if still worthwhile
|
||||
|
||||
### Task 14: Decide whether APT actually needs in-place updates
|
||||
|
||||
**Objective:** Avoid rebuilding a dangerous feature merely because it existed before.
|
||||
|
||||
**Decision gate:**
|
||||
|
||||
- Preferred simplest option: notify that a verified update exists and open the trusted Tool Hub/GitPeji release page; user installs it manually.
|
||||
- In-place update is allowed only if code signing, digest verification, rollback, and platform-specific testing are available.
|
||||
|
||||
**Deliverable:** `docs/decisions/0001-update-strategy.md`
|
||||
|
||||
**Commit:** `docs: decide APT update strategy`
|
||||
|
||||
### Task 15: Implement a verified updater only if the decision approves it
|
||||
|
||||
**Objective:** Bind installation to an exact trusted release.
|
||||
|
||||
**Files:**
|
||||
- Create: `src/update-manifest.js`
|
||||
- Create: `src/updater.js`
|
||||
- Modify: `main.js`
|
||||
- Modify: `preload.js`
|
||||
- Test: `test/update-policy.test.js`
|
||||
|
||||
**Required tests:** wrong host, redirect host change, wrong size, wrong SHA-256, wrong signature, downgrade, wrong platform/architecture, truncated body, oversized body, interrupted download, failed replacement, successful rollback.
|
||||
|
||||
**Acceptance:** only a manifest-authenticated, platform-matched artifact can reach replacement code; failure leaves the installed executable intact.
|
||||
|
||||
**Commit:** `feat: add manifest-verified GitPeji updates`
|
||||
|
||||
---
|
||||
|
||||
## Milestone 9: Closure and controlled release
|
||||
|
||||
### Task 16: Run adversarial security closure review
|
||||
|
||||
**Objective:** Independently prove every Critical/Important finding is closed.
|
||||
|
||||
**Required probes:**
|
||||
|
||||
- arbitrary updater URL cannot execute;
|
||||
- CR/LF proxy parameters cannot become commands;
|
||||
- renderer cannot choose request destination or supply cookies;
|
||||
- Axios failures contain no sentinel secret;
|
||||
- bridge rejects unknown extension/wrong secret/slow bodies;
|
||||
- cookies never appear in files, process command strings, logs, tests, ZIP metadata, or analytics;
|
||||
- stop affects only APT-owned child processes;
|
||||
- dependency audit and Electron support policy pass.
|
||||
|
||||
**Output:** finding-to-fix closure matrix with exact tests and commit.
|
||||
|
||||
### Task 17: Build a DEV release candidate and preserve production
|
||||
|
||||
**Objective:** Give Zac a testable candidate without replacing current downloads.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Query usage/confirm release lane before changing Tool Hub visibility.
|
||||
2. Build exact tagged candidate and record artifact SHA-256.
|
||||
3. Publish only to an isolated DEV/download location clearly marked pre-release.
|
||||
4. Run Windows app, extension pairing, device discovery, proxy launch/stop, disconnect, error, and update-notification workflows.
|
||||
5. Verify current production Tool Hub/download hashes did not change.
|
||||
6. Request Zac acceptance.
|
||||
|
||||
### Task 18: Promote only the accepted exact artifact
|
||||
|
||||
**Objective:** Replace the existing release safely and reversibly.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Preserve current Windows/Mac kits and hashes as rollback artifacts.
|
||||
2. Promote the exact accepted candidate—do not rebuild.
|
||||
3. Verify GitPeji tag/release, Tool Hub route, download size/hash, extension contents, Windows smoke, and analytics.
|
||||
4. Keep Mac separate until its own acceptance gate passes.
|
||||
5. Remove temporary DEV artifacts only after production verification and rollback proof.
|
||||
|
||||
---
|
||||
|
||||
## Feature roadmap gate
|
||||
|
||||
### Task 19: Capture and prioritise new functions with Zac
|
||||
|
||||
**Objective:** Turn feature ideas into a safe, ordered backlog after the red flags are closed.
|
||||
|
||||
For each feature record:
|
||||
|
||||
- user/problem and field-engineer value;
|
||||
- exact user journey;
|
||||
- read versus write operations;
|
||||
- Alta endpoints/data required;
|
||||
- cookie/session impact;
|
||||
- local helper/process impact;
|
||||
- Windows/macOS parity expectation;
|
||||
- failure and offline behaviour;
|
||||
- acceptance tests;
|
||||
- release risk and whether it can remain DEV-only.
|
||||
|
||||
**Suggested buckets for the discussion:**
|
||||
|
||||
1. **Proxy workflow improvements:** multi-camera queue, per-camera status, retry/diagnostics, safe logs.
|
||||
2. **Commissioning:** thumbnail/stream health, recording/site checks, readiness report.
|
||||
3. **Troubleshooting:** streams debug, recent alerts, support bundle.
|
||||
4. **Operator UX:** saved non-secret deployment aliases, clearer helper detection, version/support panel.
|
||||
5. **Platform:** native Apple Silicon, managed extension packaging, enterprise deployment.
|
||||
|
||||
Do not implement feature buckets during the hardening branch. Create separate feature branches from the accepted security foundation.
|
||||
|
||||
---
|
||||
|
||||
## Definition of “major red flags fixed”
|
||||
|
||||
All must be true:
|
||||
|
||||
- [ ] Unsafe updater removed or cryptographically verified.
|
||||
- [ ] No shell/batch construction from user or renderer data.
|
||||
- [ ] Main process owns deployment origin and cookies.
|
||||
- [ ] Privileged IPC validates sender and strict schemas.
|
||||
- [ ] No cookie in logs, files, command strings, or automatic clipboard actions.
|
||||
- [ ] Loopback bridge uses revocable pairing and exact extension identity.
|
||||
- [ ] Proxy stop is process-scoped.
|
||||
- [ ] Supported Electron/Axios versions; no Critical/High runtime audit findings.
|
||||
- [ ] Automated tests and GitPeji CI pass.
|
||||
- [ ] Windows kit is exact-commit reproducible with helper provenance.
|
||||
- [ ] Mac source is canonical or Mac distribution remains explicitly withheld.
|
||||
- [ ] Independent security closure review returns PASS.
|
||||
- [ ] Existing production/download state remains unchanged until Zac approves promotion.
|
||||
@@ -0,0 +1,18 @@
|
||||
# APT security hardening baseline
|
||||
|
||||
- Canonical source: GitPeji `peji/Alta-Proxy-Tool`
|
||||
- Starting commit: `a80074ac57b7a4517837b5d95754f5e6433df3ac`
|
||||
- Branch: `hardening/security-foundation`
|
||||
- Original review: `/home/peji/.hermes/reports/alta-proxy-tool-full-review-2026-08-19.md`
|
||||
- Implementation plan: `docs/plans/2026-08-19-apt-security-foundation.md`
|
||||
- Starting package version: `1.0.0`
|
||||
- Starting audit: 25 findings (1 critical, 22 high, 2 moderate); production-only audit 3 findings (2 high, 1 moderate)
|
||||
- Starting release state: GitPeji/GitHub split-brain, no application CI, unsafe unsigned updater, no automated tests
|
||||
|
||||
## Guardrails
|
||||
|
||||
- GitPeji only; GitHub remote removed.
|
||||
- Production Tool Hub and existing downloads remain unchanged.
|
||||
- Synthetic sentinel credentials only in tests.
|
||||
- Work remains on the isolated hardening branch until review gates pass.
|
||||
- No feature additions until Critical/Important security closure.
|
||||
@@ -13,6 +13,16 @@ let cookieServer = null;
|
||||
const COOKIE_SERVER_PORT = 18247;
|
||||
const COOKIE_SERVER_TOKEN = 'apt-local-bridge-token';
|
||||
|
||||
// Get the directory where the user-facing executable resides.
|
||||
// In portable builds, __dirname points to a temp extraction directory,
|
||||
// so we use the actual .exe location instead.
|
||||
function getAppDirectory() {
|
||||
if (app.isPackaged) {
|
||||
return path.dirname(process.env.PORTABLE_EXECUTABLE_FILE || app.getPath('exe'));
|
||||
}
|
||||
return __dirname;
|
||||
}
|
||||
|
||||
// Sanitize strings before embedding in batch files to prevent command injection
|
||||
function sanitizeBatchInput(input) {
|
||||
if (typeof input !== 'string') return '';
|
||||
@@ -208,6 +218,33 @@ ipcMain.handle('api-get-devices', async (event, { deploymentUrl, cookies }) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('api-get-device-sites', async (event, { deploymentUrl, cookies }) => {
|
||||
try {
|
||||
const sitesUrl = `${deploymentUrl}/api/v1/deviceSites`;
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Cookie': cookies ? cookies.join('; ') : ''
|
||||
}
|
||||
});
|
||||
|
||||
const response = await axiosInstance.get(sitesUrl);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
sites: response.data
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Get device sites error:', error);
|
||||
return {
|
||||
success: false,
|
||||
sites: [],
|
||||
message: error.response?.data?.message || error.message || 'Failed to get device sites'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('api-get-auth-info', async (event, { deploymentUrl, cookies }) => {
|
||||
try {
|
||||
const authUrl = `${deploymentUrl}/api/v1/auth`;
|
||||
@@ -238,7 +275,7 @@ ipcMain.handle('api-get-auth-info', async (event, { deploymentUrl, cookies }) =>
|
||||
ipcMain.handle('camera-proxy-cookie-launch', async (event, { deploymentUrl, cookieKey, deviceUuid }) => {
|
||||
try {
|
||||
// Path to the cookie-based camera proxy executable
|
||||
const proxyExePath = path.join(__dirname, 'aware-cam-proxy.exe');
|
||||
const proxyExePath = path.join(getAppDirectory(), 'aware-cam-proxy.exe');
|
||||
|
||||
// Check if the executable exists
|
||||
if (!fs.existsSync(proxyExePath)) {
|
||||
@@ -502,7 +539,7 @@ ipcMain.handle('download-and-install-update', async (event, { downloadUrl }) =>
|
||||
try {
|
||||
// Determine the path to the currently running executable
|
||||
const currentExePath = process.env.PORTABLE_EXECUTABLE_FILE || app.getPath('exe');
|
||||
const currentDir = path.dirname(currentExePath);
|
||||
const currentDir = getAppDirectory();
|
||||
const currentExeName = path.basename(currentExePath);
|
||||
|
||||
// Check write permission on the app directory
|
||||
|
||||
Generated
+1
-615
@@ -15,9 +15,7 @@
|
||||
"devDependencies": {
|
||||
"electron": "^28.0.0",
|
||||
"electron-builder": "^26.4.0",
|
||||
"electron-packager": "^17.1.2",
|
||||
"png-to-ico": "^3.0.1",
|
||||
"sharp": "^0.34.5"
|
||||
"electron-packager": "^17.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@develar/schema-utils": {
|
||||
@@ -459,507 +457,6 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
||||
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/colour": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
|
||||
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
||||
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
||||
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-ppc64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
||||
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-riscv64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
||||
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
@@ -5096,51 +4593,6 @@
|
||||
"node": ">=10.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/png-to-ico": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/png-to-ico/-/png-to-ico-3.0.1.tgz",
|
||||
"integrity": "sha512-S8BOAoaGd9gT5uaemQ62arIY3Jzco7Uc7LwUTqRyqJDTsKqOAiyfyN4dSdT0D+Zf8XvgztgpRbM5wnQd7EgYwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^22.10.3",
|
||||
"minimist": "^1.2.8",
|
||||
"pngjs": "^7.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"png-to-ico": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/png-to-ico/node_modules/@types/node": {
|
||||
"version": "22.19.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.10.tgz",
|
||||
"integrity": "sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/png-to-ico/node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
||||
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postject": {
|
||||
"version": "1.0.0-alpha.6",
|
||||
"resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz",
|
||||
@@ -5522,64 +4974,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@img/colour": "^1.0.0",
|
||||
"detect-libc": "^2.1.2",
|
||||
"semver": "^7.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.34.5",
|
||||
"@img/sharp-darwin-x64": "0.34.5",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||
"@img/sharp-linux-arm": "0.34.5",
|
||||
"@img/sharp-linux-arm64": "0.34.5",
|
||||
"@img/sharp-linux-ppc64": "0.34.5",
|
||||
"@img/sharp-linux-riscv64": "0.34.5",
|
||||
"@img/sharp-linux-s390x": "0.34.5",
|
||||
"@img/sharp-linux-x64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-x64": "0.34.5",
|
||||
"@img/sharp-wasm32": "0.34.5",
|
||||
"@img/sharp-win32-arm64": "0.34.5",
|
||||
"@img/sharp-win32-ia32": "0.34.5",
|
||||
"@img/sharp-win32-x64": "0.34.5"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp/node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -6168,14 +5562,6 @@
|
||||
"utf8-byte-length": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||
|
||||
+5
-6
@@ -8,6 +8,7 @@
|
||||
"dev": "electron . --dev",
|
||||
"build": "electron-builder --win --publish=never",
|
||||
"build-test": "electron-builder --win --dir",
|
||||
"build-kit": "powershell -ExecutionPolicy Bypass -File build-kit.ps1",
|
||||
"prebuild": "echo Checking build requirements..."
|
||||
},
|
||||
"build": {
|
||||
@@ -18,13 +19,13 @@
|
||||
},
|
||||
"win": {
|
||||
"target": "portable",
|
||||
"icon": "assets/icon.png"
|
||||
"icon": "assets/icon.png",
|
||||
"signAndEditExecutable": false
|
||||
},
|
||||
"portable": {
|
||||
"artifactName": "AltaCameraProxy-${version}-portable.exe"
|
||||
},
|
||||
"afterSign": false,
|
||||
"afterAllArtifactBuild": false
|
||||
"forceCodeSigning": false
|
||||
},
|
||||
"keywords": [
|
||||
"electron",
|
||||
@@ -37,9 +38,7 @@
|
||||
"devDependencies": {
|
||||
"electron": "^28.0.0",
|
||||
"electron-builder": "^26.4.0",
|
||||
"electron-packager": "^17.1.2",
|
||||
"png-to-ico": "^3.0.1",
|
||||
"sharp": "^0.34.5"
|
||||
"electron-packager": "^17.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
|
||||
@@ -4,6 +4,7 @@ const { contextBridge, ipcRenderer } = require('electron');
|
||||
// the ipcRenderer without exposing the entire object
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
getDevices: (params) => ipcRenderer.invoke('api-get-devices', params),
|
||||
getDeviceSites: (params) => ipcRenderer.invoke('api-get-device-sites', params),
|
||||
getAuthInfo: (params) => ipcRenderer.invoke('api-get-auth-info', params),
|
||||
|
||||
// Camera proxy functionality
|
||||
|
||||
+178
-30
@@ -37,6 +37,8 @@ const updateLaterBtn = document.getElementById('updateLaterBtn');
|
||||
let selectedDevice = null;
|
||||
let activeCookieProxyConnections = new Map(); // Track cookie-based proxy connections
|
||||
let allDevices = []; // Store all devices for search functionality
|
||||
let allSites = {}; // Store site id → site name mapping
|
||||
let collapsedSites = new Set(); // Track which site groups are collapsed
|
||||
let pendingUpdateInfo = null; // Store update info for install action
|
||||
|
||||
// Event listeners
|
||||
@@ -66,6 +68,8 @@ function handleDisconnect() {
|
||||
selectedDevice = null;
|
||||
activeCookieProxyConnections.clear();
|
||||
allDevices = []; // Clear stored devices
|
||||
allSites = {}; // Clear site data
|
||||
collapsedSites.clear();
|
||||
|
||||
updateConnectionStatus(false);
|
||||
updateButtonStates();
|
||||
@@ -218,6 +222,28 @@ function updateButtonStates() {
|
||||
updateCookieProxyButtonStates();
|
||||
}
|
||||
|
||||
// Fetch device sites and build id → name map
|
||||
async function fetchDeviceSites() {
|
||||
try {
|
||||
const result = await window.electronAPI.getDeviceSites({
|
||||
deploymentUrl: sessionData.deploymentUrl,
|
||||
cookies: sessionData.cookies
|
||||
});
|
||||
|
||||
if (result.success && Array.isArray(result.sites)) {
|
||||
allSites = {};
|
||||
result.sites.forEach(site => {
|
||||
if (site.id) {
|
||||
allSites[site.id] = site.name || 'Unnamed Site';
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Could not fetch device sites:', error.message);
|
||||
allSites = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle get devices (now called automatically)
|
||||
async function handleGetDevices() {
|
||||
if (!sessionData.isConnected) {
|
||||
@@ -229,14 +255,18 @@ async function handleGetDevices() {
|
||||
clearDeviceList();
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.getDevices({
|
||||
deploymentUrl: sessionData.deploymentUrl,
|
||||
cookies: sessionData.cookies
|
||||
});
|
||||
// Fetch devices and sites in parallel
|
||||
const [devicesResult] = await Promise.all([
|
||||
window.electronAPI.getDevices({
|
||||
deploymentUrl: sessionData.deploymentUrl,
|
||||
cookies: sessionData.cookies
|
||||
}),
|
||||
fetchDeviceSites()
|
||||
]);
|
||||
|
||||
if (result.success) {
|
||||
if (devicesResult.success) {
|
||||
// Filter devices to only show non-cloud cameras (localStorage = false)
|
||||
const filteredDevices = result.devices.filter(device => {
|
||||
const filteredDevices = devicesResult.devices.filter(device => {
|
||||
// Check if device has capabilities and localStorage property
|
||||
if (device.capabilities && device.capabilities.localStorage !== undefined) {
|
||||
// Only show devices where localStorage is false (non-cloud cameras)
|
||||
@@ -246,11 +276,15 @@ async function handleGetDevices() {
|
||||
return true;
|
||||
});
|
||||
|
||||
const totalDevices = result.devices.length;
|
||||
const totalDevices = devicesResult.devices.length;
|
||||
const filteredCount = filteredDevices.length;
|
||||
const cloudDevicesHidden = totalDevices - filteredCount;
|
||||
|
||||
const siteCount = Object.keys(allSites).length;
|
||||
let statusMessage = `Found ${filteredCount} local camera${filteredCount !== 1 ? 's' : ''}`;
|
||||
if (siteCount > 0) {
|
||||
statusMessage += ` across ${siteCount} site${siteCount !== 1 ? 's' : ''}`;
|
||||
}
|
||||
if (cloudDevicesHidden > 0) {
|
||||
statusMessage += ` (${cloudDevicesHidden} cloud camera${cloudDevicesHidden !== 1 ? 's' : ''} hidden)`;
|
||||
}
|
||||
@@ -261,7 +295,7 @@ async function handleGetDevices() {
|
||||
allDevices = filteredDevices;
|
||||
displayDevices(filteredDevices);
|
||||
} else {
|
||||
showStatus(deviceStatus, `Failed to get devices: ${result.message}`, 'error');
|
||||
showStatus(deviceStatus, `Failed to get devices: ${devicesResult.message}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Get devices error:', error);
|
||||
@@ -347,25 +381,73 @@ function handleDeviceSearch() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter devices based on search term
|
||||
// Filter devices based on search term (includes site name)
|
||||
const filteredDevices = allDevices.filter(device => {
|
||||
const deviceName = (device.name || '').toLowerCase();
|
||||
const deviceId = (device.guid || device.id || '').toLowerCase();
|
||||
const deviceType = (device.type || '').toLowerCase();
|
||||
const deviceModel = (device.model || '').toLowerCase();
|
||||
const deviceIp = (device.ipAddress || '').toLowerCase();
|
||||
const siteName = (device.server_group_id && allSites[device.server_group_id] || '').toLowerCase();
|
||||
|
||||
return deviceName.includes(searchTerm) ||
|
||||
deviceId.includes(searchTerm) ||
|
||||
deviceType.includes(searchTerm) ||
|
||||
deviceModel.includes(searchTerm) ||
|
||||
deviceIp.includes(searchTerm);
|
||||
deviceIp.includes(searchTerm) ||
|
||||
siteName.includes(searchTerm);
|
||||
});
|
||||
|
||||
displayDevices(filteredDevices);
|
||||
}
|
||||
|
||||
// Display devices in the UI
|
||||
// Group devices by their site using server_group_id → allSites mapping
|
||||
function groupDevicesBySite(devices) {
|
||||
const groups = {};
|
||||
const ungrouped = [];
|
||||
|
||||
devices.forEach(device => {
|
||||
const siteId = device.server_group_id;
|
||||
const siteName = siteId && allSites[siteId] ? allSites[siteId] : null;
|
||||
|
||||
if (siteName) {
|
||||
if (!groups[siteId]) {
|
||||
groups[siteId] = { name: siteName, devices: [] };
|
||||
}
|
||||
groups[siteId].devices.push(device);
|
||||
} else {
|
||||
ungrouped.push(device);
|
||||
}
|
||||
});
|
||||
|
||||
return { groups, ungrouped };
|
||||
}
|
||||
|
||||
// Create a device item DOM element
|
||||
function createDeviceItem(device, index) {
|
||||
const deviceItem = document.createElement('div');
|
||||
deviceItem.className = 'device-item';
|
||||
deviceItem.dataset.deviceIndex = index;
|
||||
deviceItem.dataset.deviceId = device.guid || device.id;
|
||||
|
||||
const status = getDeviceStatus(device);
|
||||
const deviceId = device.guid || device.id;
|
||||
const isCookieProxyActive = activeCookieProxyConnections.has(deviceId);
|
||||
|
||||
if (isCookieProxyActive) {
|
||||
deviceItem.classList.add('cookie-proxy-active');
|
||||
}
|
||||
|
||||
deviceItem.innerHTML = `
|
||||
<div class="device-name">${escapeHtml(device.name || 'Unnamed Device')}</div>
|
||||
<div class="device-status-dot ${status.isOnline ? 'online' : 'offline'}"></div>
|
||||
`;
|
||||
|
||||
deviceItem.addEventListener('click', () => selectDevice(device, deviceItem));
|
||||
return deviceItem;
|
||||
}
|
||||
|
||||
// Display devices in the UI, grouped by site
|
||||
function displayDevices(devices) {
|
||||
clearDeviceList();
|
||||
|
||||
@@ -376,33 +458,99 @@ function displayDevices(devices) {
|
||||
return;
|
||||
}
|
||||
|
||||
devices.forEach((device, index) => {
|
||||
const deviceItem = document.createElement('div');
|
||||
deviceItem.className = 'device-item';
|
||||
deviceItem.dataset.deviceIndex = index;
|
||||
deviceItem.dataset.deviceId = device.guid || device.id;
|
||||
const hasSites = Object.keys(allSites).length > 0;
|
||||
|
||||
const deviceStatus = getDeviceStatus(device);
|
||||
const deviceId = device.guid || device.id;
|
||||
const isCookieProxyActive = activeCookieProxyConnections.has(deviceId);
|
||||
// If no sites were fetched, fall back to flat list
|
||||
if (!hasSites) {
|
||||
devices.forEach((device, index) => {
|
||||
deviceList.appendChild(createDeviceItem(device, index));
|
||||
});
|
||||
updateCookieProxyButtonStates();
|
||||
return;
|
||||
}
|
||||
|
||||
// Add cookie-proxy-active class if this device has an active cookie connection
|
||||
if (isCookieProxyActive) {
|
||||
deviceItem.classList.add('cookie-proxy-active');
|
||||
}
|
||||
const { groups, ungrouped } = groupDevicesBySite(devices);
|
||||
|
||||
deviceItem.innerHTML = `
|
||||
<div class="device-name">${escapeHtml(device.name || 'Unnamed Device')}</div>
|
||||
<div class="device-status-dot ${deviceStatus.isOnline ? 'online' : 'offline'}"></div>
|
||||
// Sort site groups alphabetically by name
|
||||
const sortedSiteIds = Object.keys(groups).sort((a, b) =>
|
||||
groups[a].name.localeCompare(groups[b].name)
|
||||
);
|
||||
|
||||
let globalIndex = 0;
|
||||
|
||||
sortedSiteIds.forEach(siteId => {
|
||||
const group = groups[siteId];
|
||||
const isCollapsed = collapsedSites.has(siteId);
|
||||
|
||||
// Site header
|
||||
const siteHeader = document.createElement('div');
|
||||
siteHeader.className = 'site-group-header' + (isCollapsed ? ' collapsed' : '');
|
||||
siteHeader.innerHTML = `
|
||||
<span class="site-group-arrow">${isCollapsed ? '\u25B6' : '\u25BC'}</span>
|
||||
<span class="site-group-name">${escapeHtml(group.name)}</span>
|
||||
<span class="site-group-count">${group.devices.length}</span>
|
||||
`;
|
||||
|
||||
// Add click handler for device selection
|
||||
deviceItem.addEventListener('click', () => selectDevice(device, deviceItem));
|
||||
siteHeader.addEventListener('click', () => {
|
||||
if (collapsedSites.has(siteId)) {
|
||||
collapsedSites.delete(siteId);
|
||||
} else {
|
||||
collapsedSites.add(siteId);
|
||||
}
|
||||
// Re-render with current search filter
|
||||
const searchTerm = deviceSearch.value.toLowerCase().trim();
|
||||
if (searchTerm) {
|
||||
handleDeviceSearch();
|
||||
} else {
|
||||
displayDevices(allDevices);
|
||||
}
|
||||
});
|
||||
|
||||
deviceList.appendChild(deviceItem);
|
||||
deviceList.appendChild(siteHeader);
|
||||
|
||||
// Device items (hidden if collapsed)
|
||||
if (!isCollapsed) {
|
||||
group.devices.forEach(device => {
|
||||
deviceList.appendChild(createDeviceItem(device, globalIndex++));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update cookie proxy button states after displaying devices
|
||||
// Ungrouped devices at the bottom
|
||||
if (ungrouped.length > 0) {
|
||||
if (sortedSiteIds.length > 0) {
|
||||
const ungroupedHeader = document.createElement('div');
|
||||
ungroupedHeader.className = 'site-group-header' + (collapsedSites.has('__ungrouped') ? ' collapsed' : '');
|
||||
ungroupedHeader.innerHTML = `
|
||||
<span class="site-group-arrow">${collapsedSites.has('__ungrouped') ? '\u25B6' : '\u25BC'}</span>
|
||||
<span class="site-group-name">Ungrouped</span>
|
||||
<span class="site-group-count">${ungrouped.length}</span>
|
||||
`;
|
||||
|
||||
ungroupedHeader.addEventListener('click', () => {
|
||||
if (collapsedSites.has('__ungrouped')) {
|
||||
collapsedSites.delete('__ungrouped');
|
||||
} else {
|
||||
collapsedSites.add('__ungrouped');
|
||||
}
|
||||
const searchTerm = deviceSearch.value.toLowerCase().trim();
|
||||
if (searchTerm) {
|
||||
handleDeviceSearch();
|
||||
} else {
|
||||
displayDevices(allDevices);
|
||||
}
|
||||
});
|
||||
|
||||
deviceList.appendChild(ungroupedHeader);
|
||||
}
|
||||
|
||||
if (!collapsedSites.has('__ungrouped') || sortedSiteIds.length === 0) {
|
||||
ungrouped.forEach(device => {
|
||||
deviceList.appendChild(createDeviceItem(device, globalIndex++));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateCookieProxyButtonStates();
|
||||
}
|
||||
|
||||
|
||||
+47
@@ -167,6 +167,53 @@ body {
|
||||
box-shadow: 0 0 6px rgba(244, 67, 54, 0.6);
|
||||
}
|
||||
|
||||
/* Site Group Headers */
|
||||
.site-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 4px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.site-group-header:hover {
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
|
||||
.site-group-arrow {
|
||||
font-size: 10px;
|
||||
color: var(--text-secondary);
|
||||
width: 12px;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.site-group-name {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
color: var(--accent-primary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.site-group-count {
|
||||
font-size: 10px;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 1px 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
|
||||
Reference in New Issue
Block a user