8 Commits

Author SHA1 Message Date
peji 64f162440f Deploy Pages via GitHub Actions 2026-05-23 00:32:58 +00:00
peji e30e0395d3 Refresh GitHub Pages site 2026-05-23 00:26:57 +00:00
peji d1013c87a1 Trigger GitHub Pages rebuild 2026-05-23 00:22:02 +00:00
peji c3c3fc83e3 Add site-grouped device list in sidebar
Fetch device sites from /api/v1/deviceSites in parallel with devices,
then group cameras by site with collapsible headers. Search now also
matches site names. Falls back to flat list if sites API is unavailable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 22:27:05 +00:00
PageZ948 14ce5c728d Fix header formatting in README.md 2026-03-02 09:19:01 -05:00
peji 60f56a2dea Simplify landing page: remove tagline, note, and kit contents section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 19:34:27 -05:00
peji 099cc252fe Fix proxy path resolution for portable builds, simplify landing page
The proxy launch used __dirname to find aware-cam-proxy.exe, which
points to a temp extraction directory in portable builds. Added
getAppDirectory() helper that resolves the actual .exe location via
PORTABLE_EXECUTABLE_FILE. Rewrote landing page as a simple kit download
page. Added build-kit script to bundle app + proxy + extension into zip.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 19:23:48 -05:00
peji 3a6be50e31 Add GitHub Pages landing page and fix electron-builder config
- Create docs/ landing page with dark theme, download link, features,
  setup instructions, and requirements
- Fix build config: add signAndEditExecutable=false to avoid winCodeSign
  symlink errors, replace broken afterSign/afterAllArtifactBuild with
  forceCodeSigning=false
- Clean up removed dev dependencies (sharp, png-to-ico) from lockfile

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 22:33:38 -05:00
11 changed files with 722 additions and 654 deletions
+37
View File
@@ -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
+72
View File
@@ -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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

+341
View File
@@ -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>
+39 -2
View File
@@ -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
+1 -615
View File
@@ -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
View File
@@ -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",
+1
View File
@@ -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
+177 -29
View File
@@ -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({
// 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;
}
const hasSites = Object.keys(allSites).length > 0;
// If no sites were fetched, fall back to flat list
if (!hasSites) {
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 deviceStatus = getDeviceStatus(device);
const deviceId = device.guid || device.id;
const isCookieProxyActive = activeCookieProxyConnections.has(deviceId);
// Add cookie-proxy-active class if this device has an active cookie connection
if (isCookieProxyActive) {
deviceItem.classList.add('cookie-proxy-active');
deviceList.appendChild(createDeviceItem(device, index));
});
updateCookieProxyButtonStates();
return;
}
deviceItem.innerHTML = `
<div class="device-name">${escapeHtml(device.name || 'Unnamed Device')}</div>
<div class="device-status-dot ${deviceStatus.isOnline ? 'online' : 'offline'}"></div>
const { groups, ungrouped } = groupDevicesBySite(devices);
// 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));
deviceList.appendChild(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);
}
});
// Update cookie proxy button states after displaying devices
deviceList.appendChild(siteHeader);
// Device items (hidden if collapsed)
if (!isCollapsed) {
group.devices.forEach(device => {
deviceList.appendChild(createDeviceItem(device, globalIndex++));
});
}
});
// 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
View File
@@ -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;