Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9ec212406 |
-28
@@ -1,28 +0,0 @@
|
|||||||
# Dependencies
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Build output
|
|
||||||
dist/
|
|
||||||
|
|
||||||
# External executables (not bundled via npm)
|
|
||||||
aware-cam-proxy-win.exe
|
|
||||||
aware-cam-proxy.exe
|
|
||||||
|
|
||||||
# Reference documents
|
|
||||||
*.pdf
|
|
||||||
|
|
||||||
# OS files
|
|
||||||
Thumbs.db
|
|
||||||
Desktop.ini
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Editor
|
|
||||||
.vscode/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
|
|
||||||
# Claude Code
|
|
||||||
.claude/
|
|
||||||
|
|
||||||
# Windows null device artifact
|
|
||||||
nul
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
Alta Proxy Tool (APT) — an Electron desktop app that authenticates with Avigilon Alta Video deployments via a companion Chrome extension, discovers cameras, and launches `aware-cam-proxy.exe` to establish camera connections. Authentication uses cookie import from Chrome — no username/password login flow. Windows-only due to the proxy executable.
|
|
||||||
|
|
||||||
## Repository
|
|
||||||
|
|
||||||
- **GitHub**: https://github.com/PageZ948/Alta-Proxy-Tool (private)
|
|
||||||
- **Branch**: master
|
|
||||||
- **Git identity**: Zac <zpage948@gmail.com> (repo-local config)
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm start # Run the app
|
|
||||||
npm run dev # Run with DevTools open (--dev flag)
|
|
||||||
npm run build # Build portable Windows .exe (output: dist/)
|
|
||||||
npm run build-test # Build to directory without packaging
|
|
||||||
```
|
|
||||||
|
|
||||||
No test framework is configured. No linter is configured.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
This is a vanilla Electron app (no React/Vue/framework). Core files:
|
|
||||||
|
|
||||||
```
|
|
||||||
main.js → Electron main process: IPC handlers, API calls (axios),
|
|
||||||
cookie proxy process spawning, local HTTP cookie server
|
|
||||||
preload.js → contextBridge exposing window.electronAPI with IPC wrappers
|
|
||||||
renderer.js → All UI logic: DOM manipulation, state management, event handlers
|
|
||||||
index.html → Static HTML shell, no inline scripts (CSP enforced)
|
|
||||||
styles.css → Dark theme using CSS custom properties
|
|
||||||
```
|
|
||||||
|
|
||||||
A companion Chrome extension lives in `chrome-extension/`:
|
|
||||||
|
|
||||||
```
|
|
||||||
chrome-extension/
|
|
||||||
manifest.json → Manifest V3, cookies + activeTab permissions
|
|
||||||
popup.html → Extension popup UI
|
|
||||||
popup.css → Dark theme matching the Electron app
|
|
||||||
popup.js → Tab detection, cookie retrieval, POST to localhost
|
|
||||||
icon*.png → Placeholder icons
|
|
||||||
```
|
|
||||||
|
|
||||||
### Authentication Flow
|
|
||||||
|
|
||||||
There is no login form or profile system. Authentication works exclusively through the Chrome extension cookie bridge:
|
|
||||||
|
|
||||||
1. User logs into Alta deployment in Chrome
|
|
||||||
2. Clicks the Chrome extension popup → "Send Cookie to APT"
|
|
||||||
3. Extension POSTs `{deploymentUrl, cookieValue}` to `http://127.0.0.1:18247/cookie` with `X-APT-Token` header
|
|
||||||
4. `main.js` HTTP server validates and forwards via IPC push to renderer
|
|
||||||
5. `renderer.js` `handleExtensionCookie()` sets session state, auto-populates cookie key, fetches devices
|
|
||||||
|
|
||||||
The extension is loaded unpacked via `chrome://extensions/` → Developer mode → Load unpacked → select `chrome-extension/`.
|
|
||||||
|
|
||||||
### IPC Communication Pattern
|
|
||||||
|
|
||||||
Most cross-process communication follows the request/response pattern:
|
|
||||||
1. `main.js` registers handler: `ipcMain.handle('channel-name', async (event, params) => { ... })`
|
|
||||||
2. `preload.js` exposes it: `channelName: (params) => ipcRenderer.invoke('channel-name', params)`
|
|
||||||
3. `renderer.js` calls it: `const result = await window.electronAPI.channelName(params)`
|
|
||||||
|
|
||||||
All handlers return `{ success: boolean, message?: string, ...data }`.
|
|
||||||
|
|
||||||
There is one **push-pattern** channel for the Chrome extension cookie bridge:
|
|
||||||
- `main.js` sends: `mainWindow.webContents.send('extension-cookie-received', data)`
|
|
||||||
- `preload.js` bridges: `ipcRenderer.on('extension-cookie-received', callback)`
|
|
||||||
- `renderer.js` listens via `window.electronAPI.onExtensionCookie(callback)`
|
|
||||||
|
|
||||||
### IPC Channels
|
|
||||||
|
|
||||||
| Channel | Purpose |
|
|
||||||
|---------|---------|
|
|
||||||
| `api-get-devices` | GET /api/v1/devices with cookie auth |
|
|
||||||
| `api-get-auth-info` | GET /api/v1/auth to verify session |
|
|
||||||
| `camera-proxy-cookie-launch` | Spawns aware-cam-proxy.exe (cookie method) |
|
|
||||||
| `camera-proxy-stop` | Kills all proxy processes via taskkill/powershell |
|
|
||||||
| `extension-cookie-received` | Push channel: cookie data from Chrome extension → renderer |
|
|
||||||
|
|
||||||
### State Management (renderer.js)
|
|
||||||
|
|
||||||
All connection state lives in the `sessionData` object (deploymentUrl, cookies, isConnected). There is no separate `isConnected` flag — always use `sessionData.isConnected`.
|
|
||||||
|
|
||||||
Active cookie proxy processes are tracked in `activeCookieProxyConnections` Map, keyed by device GUID.
|
|
||||||
|
|
||||||
### Security Model
|
|
||||||
|
|
||||||
- Context isolation enabled, nodeIntegration disabled
|
|
||||||
- CSP meta tag: `script-src 'self'` — no inline scripts or onclick handlers allowed
|
|
||||||
- Batch file inputs are sanitized via `sanitizeBatchInput()` to prevent command injection
|
|
||||||
- Local HTTP cookie server (port 18247) bound to `127.0.0.1` only
|
|
||||||
- Cookie server validates: shared token header, CORS restricted to `chrome-extension://` origins, deployment URL must be `*.avasecurity.com` or `*.avigilon.com` over HTTPS, type/length limits on all inputs, 64KB body size limit
|
|
||||||
|
|
||||||
## Key Conventions
|
|
||||||
|
|
||||||
- No inline event handlers in HTML — all use `addEventListener` in renderer.js
|
|
||||||
- All user-provided content rendered to DOM must go through `escapeHtml()` (XSS prevention)
|
|
||||||
- External processes spawned with `detached: true` + `unref()` so they survive if the app closes
|
|
||||||
- Device list filters out cloud cameras (`capabilities.localStorage === false` only)
|
|
||||||
- `clearDeviceList()` must NOT clear proxy connection Maps (proxies may still be running)
|
|
||||||
|
|
||||||
## External Executable
|
|
||||||
|
|
||||||
- `aware-cam-proxy.exe` — cookie-based auth proxy (required)
|
|
||||||
|
|
||||||
Not bundled via npm. Must be in the app root directory. Gitignored along with `*.pdf`, `node_modules/`, and `dist/`.
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Chrome Extension Authentication**: Import your Alta session cookie from Chrome with one click — no manual login
|
|
||||||
- **API Integration**: Device discovery via Alta Video API using cookie auth
|
|
||||||
- **Camera Proxy Management**: Launch and manage camera proxy connections
|
|
||||||
- **Device Filtering**: Automatically filters to show only local (non-cloud) cameras
|
|
||||||
- **Device Search**: Quick search functionality to find cameras by name, ID, IP, or model
|
|
||||||
- **Real-time Status**: Live connection status and device online/offline indicators
|
|
||||||
- **Auto-Update**: Checks for updates on startup via GitHub Releases, with one-click in-place update
|
|
||||||
- **Modern Dark UI**: Professional dark-mode interface with responsive design
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- **Node.js** (version 14 or higher)
|
|
||||||
- **npm** (comes with Node.js)
|
|
||||||
- **Google Chrome** (for the authentication extension)
|
|
||||||
- **Windows OS** (required for camera proxy executable)
|
|
||||||
- **aware-cam-proxy.exe** (camera proxy executable) — must be placed in the application directory
|
|
||||||
- **An active Alta Video session** in Chrome (logged in to your deployment)
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
1. Clone or download this project
|
|
||||||
2. Install dependencies:
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
3. Place `aware-cam-proxy.exe` in the project root directory
|
|
||||||
|
|
||||||
### Chrome Extension Setup
|
|
||||||
|
|
||||||
1. Open Chrome and navigate to `chrome://extensions/`
|
|
||||||
2. Enable **Developer mode** (toggle in top-right)
|
|
||||||
3. Click **Load unpacked**
|
|
||||||
4. Select the `chrome-extension/` folder from this project
|
|
||||||
5. The extension icon will appear in the Chrome toolbar
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Starting the Application
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
Or for development mode with DevTools:
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Connecting to Alta
|
|
||||||
|
|
||||||
1. **Log into your Alta deployment** in Chrome (e.g., `https://your-site.eu1.aware.avasecurity.com`)
|
|
||||||
2. **Click the extension icon** in Chrome — it will detect the Alta tab
|
|
||||||
3. **Click "Send Cookie to APT"** — the app will connect and load devices automatically
|
|
||||||
|
|
||||||
### Launching Camera Proxy
|
|
||||||
|
|
||||||
1. **Connect via Chrome extension** (above)
|
|
||||||
2. **Select a device** from the left sidebar
|
|
||||||
3. **Click "Start Camera Proxy"**
|
|
||||||
4. A command prompt window will open with the proxy connection
|
|
||||||
|
|
||||||
### Updating
|
|
||||||
|
|
||||||
The app checks for updates automatically 2 seconds after launch. If a newer version is available on GitHub Releases, the "Check for Updates" button in the header will show a green badge.
|
|
||||||
|
|
||||||
- **Manual check**: Click "Check for Updates" in the top-right corner
|
|
||||||
- **Install**: The update modal shows release notes — click "Install Update" to download and replace the current executable
|
|
||||||
- The app will quit, swap the `.exe`, and relaunch automatically
|
|
||||||
|
|
||||||
## API Endpoints Used
|
|
||||||
|
|
||||||
- **Device List**: `GET /api/v1/devices` — Retrieve all devices
|
|
||||||
- **Auth Info**: `GET /api/v1/auth` — Verify authentication status
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
```
|
|
||||||
Chrome Extension (popup click)
|
|
||||||
→ POST http://127.0.0.1:18247/cookie
|
|
||||||
→ Electron app HTTP server receives cookie
|
|
||||||
→ Sets session state, fetches devices
|
|
||||||
→ User selects device → launches aware-cam-proxy.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
The Electron app runs a local HTTP server on port 18247 that only accepts requests from Chrome extensions with a shared token. The Chrome extension reads the `va` session cookie from the active Alta tab and sends it to the app.
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
- **Context Isolation**: Renderer process is isolated from Node.js APIs
|
|
||||||
- **Preload Script**: Secure IPC communication between main and renderer processes
|
|
||||||
- **CSP Enforced**: `script-src 'self'` — no inline scripts allowed
|
|
||||||
- **Localhost Only**: Cookie server binds to `127.0.0.1`, not accessible from network
|
|
||||||
- **CORS Restricted**: Only `chrome-extension://` origins accepted
|
|
||||||
- **Domain Validation**: Only `*.avasecurity.com` and `*.avigilon.com` URLs accepted
|
|
||||||
- **Input Sanitization**: Batch file inputs sanitized to prevent command injection
|
|
||||||
- **Size Limits**: 64KB body limit on cookie server, type/length validation on all inputs
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
├── main.js # Main process (IPC, API calls, proxy spawning, cookie server)
|
|
||||||
├── renderer.js # Renderer process (UI logic, state management)
|
|
||||||
├── preload.js # Secure IPC bridge (contextBridge)
|
|
||||||
├── index.html # Static HTML shell (CSP enforced)
|
|
||||||
├── styles.css # Dark theme styling
|
|
||||||
├── package.json # Dependencies and build config
|
|
||||||
├── chrome-extension/ # Chrome extension for cookie import
|
|
||||||
│ ├── manifest.json # Manifest V3
|
|
||||||
│ ├── popup.html # Extension popup UI
|
|
||||||
│ ├── popup.css # Dark theme styling
|
|
||||||
│ ├── popup.js # Tab detection, cookie retrieval
|
|
||||||
│ └── icon*.png # Extension icons
|
|
||||||
├── assets/
|
|
||||||
│ └── icon.png # Application icon
|
|
||||||
├── CLAUDE.md # Claude Code project instructions
|
|
||||||
└── README.md # This file
|
|
||||||
```
|
|
||||||
|
|
||||||
**External executable** (not included in repo):
|
|
||||||
- `aware-cam-proxy.exe` — cookie-based auth proxy (required, place in app root)
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Connection Issues
|
|
||||||
- Ensure you are **logged into Alta in Chrome** before clicking the extension
|
|
||||||
- Verify the extension shows "Detected: [hostname]" in green
|
|
||||||
- If extension shows "Alta Proxy Tool is not running" — start the Electron app first
|
|
||||||
- If "Session cookie has expired" — log into Alta again in Chrome
|
|
||||||
- Check that the app console shows "Cookie server listening on http://127.0.0.1:18247"
|
|
||||||
|
|
||||||
### Camera Proxy Issues
|
|
||||||
- **Executable not found**: Ensure `aware-cam-proxy.exe` is in the application directory
|
|
||||||
- **Proxy won't start**: Check that you're connected and have selected a device
|
|
||||||
- **Command window closes immediately**: Check network connectivity to the deployment
|
|
||||||
|
|
||||||
### Device List Issues
|
|
||||||
- Ensure you're connected via the Chrome extension first
|
|
||||||
- Check that your user account has permissions to view devices
|
|
||||||
- **No devices shown**: You may only have cloud cameras which are filtered out
|
|
||||||
- Use the search box to find specific devices
|
|
||||||
|
|
||||||
## Building for Distribution
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build portable Windows executable
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Output: dist/AltaCameraProxy-1.0.0-portable.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important**: Copy `aware-cam-proxy.exe` to the same directory as the built executable before distribution.
|
|
||||||
|
|
||||||
## Limitations
|
|
||||||
|
|
||||||
- **Windows Only**: Camera proxy executable is Windows-specific
|
|
||||||
- **Chrome Required**: Authentication requires the Chrome extension
|
|
||||||
- **Local Cameras Only**: Automatically filters out cloud-based cameras
|
|
||||||
- **No Session Refresh**: Sessions may expire and require re-import from Chrome
|
|
||||||
- **Executable Required**: `aware-cam-proxy.exe` must be obtained separately
|
|
||||||
- **Update Requires Write Access**: Self-update needs write permission to the directory containing the `.exe`
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
To modify or extend this application:
|
|
||||||
|
|
||||||
1. **Main Process** ([main.js](main.js)): App lifecycle, API requests, proxy spawning, cookie server
|
|
||||||
2. **Renderer Process** ([renderer.js](renderer.js)): UI interactions and state management
|
|
||||||
3. **Preload Script** ([preload.js](preload.js)): Secure IPC bridge with context isolation
|
|
||||||
4. **Chrome Extension** ([chrome-extension/](chrome-extension/)): Cookie import from browser
|
|
||||||
5. **Styling** ([styles.css](styles.css)): Dark mode theme and responsive design
|
|
||||||
|
|
||||||
### Adding New IPC Endpoints
|
|
||||||
|
|
||||||
1. Add handler in [main.js](main.js) using `ipcMain.handle()`
|
|
||||||
2. Expose method in [preload.js](preload.js) via `contextBridge.exposeInMainWorld()`
|
|
||||||
3. Call from [renderer.js](renderer.js) using `window.electronAPI.yourMethod()`
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT License - Feel free to modify and distribute as needed.
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 279 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 675 B |
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 KiB |
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"manifest_version": 3,
|
|
||||||
"name": "Alta Proxy Tool Bridge",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Send Alta session cookies to the Alta Proxy Tool desktop app.",
|
|
||||||
"permissions": ["cookies", "activeTab"],
|
|
||||||
"host_permissions": [
|
|
||||||
"https://*.avasecurity.com/*",
|
|
||||||
"https://*.avigilon.com/*",
|
|
||||||
"http://127.0.0.1:18247/*"
|
|
||||||
],
|
|
||||||
"action": {
|
|
||||||
"default_popup": "popup.html",
|
|
||||||
"default_icon": {
|
|
||||||
"16": "icon16.png",
|
|
||||||
"48": "icon48.png",
|
|
||||||
"128": "icon128.png"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"icons": {
|
|
||||||
"16": "icon16.png",
|
|
||||||
"48": "icon48.png",
|
|
||||||
"128": "icon128.png"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
/* Dark theme matching the Electron app */
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif;
|
|
||||||
background: #1E1E1E;
|
|
||||||
color: #E0E0E0;
|
|
||||||
font-size: 14px;
|
|
||||||
min-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-container {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #0E7AFE;
|
|
||||||
margin: 0 0 12px 0;
|
|
||||||
text-align: center;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-info {
|
|
||||||
background: #2D2D30;
|
|
||||||
border: 1px solid #3C3C3C;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #999999;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-info.detected {
|
|
||||||
color: #4CAF50;
|
|
||||||
border-color: #4CAF50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-info.not-detected {
|
|
||||||
color: #F44336;
|
|
||||||
border-color: #F44336;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-btn {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 16px;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
background: #0E7AFE;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-btn:hover:not(:disabled) {
|
|
||||||
background: #0A5FD9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-msg {
|
|
||||||
margin-top: 10px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: bold;
|
|
||||||
display: none;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-msg.success {
|
|
||||||
display: block;
|
|
||||||
background: rgba(76, 175, 80, 0.1);
|
|
||||||
color: #4CAF50;
|
|
||||||
border-color: #4CAF50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-msg.error {
|
|
||||||
display: block;
|
|
||||||
background: rgba(244, 67, 54, 0.1);
|
|
||||||
color: #F44336;
|
|
||||||
border-color: #F44336;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-msg.info {
|
|
||||||
display: block;
|
|
||||||
background: rgba(14, 122, 254, 0.1);
|
|
||||||
color: #0E7AFE;
|
|
||||||
border-color: #0E7AFE;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src http://127.0.0.1:18247;">
|
|
||||||
<title>Alta Proxy Tool Bridge</title>
|
|
||||||
<link rel="stylesheet" href="popup.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<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 id="statusMsg" class="status-msg"></div>
|
|
||||||
</div>
|
|
||||||
<script src="popup.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
const APT_URL = 'http://127.0.0.1:18247/cookie';
|
|
||||||
const APT_TOKEN = 'apt-local-bridge-token';
|
|
||||||
|
|
||||||
const tabInfo = document.getElementById('tabInfo');
|
|
||||||
const sendBtn = document.getElementById('sendBtn');
|
|
||||||
const statusMsg = document.getElementById('statusMsg');
|
|
||||||
|
|
||||||
let detectedOrigin = null;
|
|
||||||
|
|
||||||
function showStatus(message, type) {
|
|
||||||
statusMsg.textContent = message;
|
|
||||||
statusMsg.className = 'status-msg ' + type;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the active tab on popup open
|
|
||||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
|
||||||
if (!tabs || tabs.length === 0) {
|
|
||||||
tabInfo.textContent = 'No active tab found.';
|
|
||||||
tabInfo.className = 'tab-info not-detected';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tab = tabs[0];
|
|
||||||
let url;
|
|
||||||
try {
|
|
||||||
url = new URL(tab.url);
|
|
||||||
} catch {
|
|
||||||
tabInfo.textContent = 'Cannot read this tab URL.';
|
|
||||||
tabInfo.className = 'tab-info not-detected';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hostname = url.hostname;
|
|
||||||
const isAlta = hostname.endsWith('.avasecurity.com') || hostname.endsWith('.avigilon.com');
|
|
||||||
|
|
||||||
if (!isAlta) {
|
|
||||||
tabInfo.textContent = 'This tab is not an Alta deployment.';
|
|
||||||
tabInfo.className = 'tab-info not-detected';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
detectedOrigin = url.origin;
|
|
||||||
tabInfo.textContent = 'Detected: ' + hostname;
|
|
||||||
tabInfo.className = 'tab-info detected';
|
|
||||||
sendBtn.disabled = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send cookie on button click
|
|
||||||
sendBtn.addEventListener('click', async () => {
|
|
||||||
if (!detectedOrigin) return;
|
|
||||||
|
|
||||||
sendBtn.disabled = true;
|
|
||||||
showStatus('Retrieving cookie...', '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;
|
|
||||||
}
|
|
||||||
|
|
||||||
showStatus('Sending to Alta Proxy Tool...', 'info');
|
|
||||||
|
|
||||||
const response = await fetch(APT_URL, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-APT-Token': APT_TOKEN
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
deploymentUrl: detectedOrigin,
|
|
||||||
cookieValue: cookie.value
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
showStatus('Cookie 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')) {
|
|
||||||
showStatus('Alta Proxy Tool is not running.', 'error');
|
|
||||||
} else {
|
|
||||||
showStatus('Error: ' + err.message, 'error');
|
|
||||||
}
|
|
||||||
sendBtn.disabled = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
+326
-95
@@ -1,110 +1,341 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
<!-- GitHub Pages rebuild marker: 2026-05-22 -->
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">
|
<title>Alta Proxy Tool</title>
|
||||||
<title>Alta Video Camera Proxy with API</title>
|
<style>
|
||||||
<link rel="stylesheet" href="styles.css">
|
: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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-container">
|
|
||||||
<!-- Main Content Layout -->
|
|
||||||
<div class="main-layout">
|
|
||||||
<!-- Left Sidebar - Available Devices -->
|
|
||||||
<aside class="devices-sidebar">
|
|
||||||
<div class="sidebar-header">
|
|
||||||
<h2>Available Devices</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="deviceStatus" class="status-message"></div>
|
<header class="header">
|
||||||
|
<div class="container">
|
||||||
<!-- Device Search -->
|
<div class="header-brand">
|
||||||
<div class="device-search-container">
|
<img src="icon.png" alt="Alta Proxy Tool">
|
||||||
<input type="text" id="deviceSearch" placeholder="Search devices..." class="device-search-input">
|
<span>Alta Proxy Tool</span>
|
||||||
</div>
|
</div>
|
||||||
|
<a href="https://github.com/PageZ948/Alta-Proxy-Tool" class="header-link">GitHub</a>
|
||||||
<div class="device-list-container">
|
|
||||||
<div id="deviceList" class="device-list">
|
|
||||||
<p class="placeholder-text">Connect to API to load devices</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- Main Content Area -->
|
|
||||||
<main class="main-content">
|
|
||||||
<div class="content-header">
|
|
||||||
<h1>Alta Video Camera Proxy</h1>
|
|
||||||
<button type="button" id="checkUpdateBtn" class="btn-update" title="Check for Updates">
|
|
||||||
<span class="update-icon">↻</span>
|
|
||||||
<span class="update-text">Check for Updates</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- API Connection Section -->
|
|
||||||
<section class="content-section">
|
|
||||||
<h2>API Connection</h2>
|
|
||||||
<div class="connection-status">
|
|
||||||
<div class="status-row">
|
|
||||||
<label>Status:</label>
|
|
||||||
<div class="status-indicator" id="statusIndicator">
|
|
||||||
<span class="status-dot offline"></span>
|
|
||||||
<span class="status-text">Disconnected</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="connection-controls">
|
|
||||||
<button type="button" id="disconnectBtn" class="btn-outline" disabled>Disconnect</button>
|
|
||||||
</div>
|
|
||||||
<div id="connectionStatus" class="status-message"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Cookie-Based Camera Proxy Section -->
|
|
||||||
<section class="content-section">
|
|
||||||
<h2>Camera Proxy</h2>
|
|
||||||
<div class="proxy-controls">
|
|
||||||
<div class="input-row">
|
|
||||||
<label for="cookieDeviceUUID">Device UUID:</label>
|
|
||||||
<input type="text" id="cookieDeviceUUID" placeholder="(Auto-filled when you select a device from the list)" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="input-row" style="display: none;">
|
|
||||||
<label for="cookieKey">Cookie Key:</label>
|
|
||||||
<input type="text" id="cookieKey" placeholder="Paste your cookie key here">
|
|
||||||
</div>
|
|
||||||
<div class="proxy-buttons">
|
|
||||||
<button type="button" id="startCookieProxyBtn" class="btn-primary" disabled>Start Proxy</button>
|
|
||||||
<button type="button" id="stopCookieProxyBtn" class="btn-outline" disabled>Stop Proxy</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
|
|
||||||
<!-- Update Modal -->
|
<main>
|
||||||
<div id="updateModalOverlay" class="update-modal-overlay" style="display: none;">
|
<div class="container">
|
||||||
<div class="update-modal-card">
|
|
||||||
<div class="update-modal-header">
|
<div class="hero">
|
||||||
<h3>Update Available</h3>
|
<img src="icon.png" alt="" class="hero-icon">
|
||||||
<button type="button" id="updateModalCloseBtn" class="update-modal-close">×</button>
|
<h1>Alta Proxy Tool</h1>
|
||||||
</div>
|
<a href="https://github.com/PageZ948/Alta-Proxy-Tool/releases/latest/download/AltaProxyToolKit.zip" class="btn-download">Download Kit for Windows</a>
|
||||||
<div class="update-modal-body">
|
|
||||||
<p id="updateModalMessage" class="update-modal-message"></p>
|
|
||||||
<div id="updateModalNotes" class="update-modal-notes"></div>
|
|
||||||
<div id="updateProgressContainer" class="update-progress-container" style="display: none;">
|
|
||||||
<div class="update-progress-track">
|
|
||||||
<div id="updateProgressFill" class="update-progress-fill" style="width: 0%"></div>
|
|
||||||
</div>
|
|
||||||
<span id="updateProgressText" class="update-progress-text">0%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="update-modal-footer">
|
|
||||||
<button type="button" id="updateInstallBtn" class="btn-primary">Install Update</button>
|
|
||||||
<button type="button" id="updateLaterBtn" class="btn-outline">Later</button>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<script src="renderer.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,594 +0,0 @@
|
|||||||
const { app, BrowserWindow, ipcMain, shell } = require('electron');
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
const os = require('os');
|
|
||||||
const axios = require('axios');
|
|
||||||
const https = require('https');
|
|
||||||
const { spawn } = require('child_process');
|
|
||||||
const http = require('http');
|
|
||||||
|
|
||||||
let mainWindow;
|
|
||||||
let activeProxyProcesses = new Map(); // Track active camera proxy processes
|
|
||||||
let cookieServer = null;
|
|
||||||
const COOKIE_SERVER_PORT = 18247;
|
|
||||||
const COOKIE_SERVER_TOKEN = 'apt-local-bridge-token';
|
|
||||||
|
|
||||||
// Sanitize strings before embedding in batch files to prevent command injection
|
|
||||||
function sanitizeBatchInput(input) {
|
|
||||||
if (typeof input !== 'string') return '';
|
|
||||||
// Remove characters that have special meaning in batch/cmd: & | < > ^ % " ` !
|
|
||||||
return input.replace(/[&|<>^%"`!]/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function startCookieServer() {
|
|
||||||
cookieServer = http.createServer((req, res) => {
|
|
||||||
// CORS headers — only allow Chrome extension origins
|
|
||||||
const origin = req.headers.origin || '';
|
|
||||||
if (origin.startsWith('chrome-extension://')) {
|
|
||||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
||||||
}
|
|
||||||
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
||||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-APT-Token');
|
|
||||||
|
|
||||||
// Handle preflight
|
|
||||||
if (req.method === 'OPTIONS') {
|
|
||||||
res.writeHead(204);
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only accept POST /cookie
|
|
||||||
if (req.method !== 'POST' || req.url !== '/cookie') {
|
|
||||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ success: false, message: 'Not found' }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify shared token
|
|
||||||
if (req.headers['x-apt-token'] !== COOKIE_SERVER_TOKEN) {
|
|
||||||
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ success: false, message: 'Forbidden' }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read body with 64KB size limit
|
|
||||||
let body = '';
|
|
||||||
let bodySize = 0;
|
|
||||||
const MAX_BODY_SIZE = 65536;
|
|
||||||
|
|
||||||
req.on('data', (chunk) => {
|
|
||||||
bodySize += chunk.length;
|
|
||||||
if (bodySize > MAX_BODY_SIZE) {
|
|
||||||
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ success: false, message: 'Payload too large' }));
|
|
||||||
req.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
body += chunk;
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('end', () => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(body);
|
|
||||||
const { deploymentUrl, cookieValue } = data;
|
|
||||||
|
|
||||||
if (!deploymentUrl || !cookieValue) {
|
|
||||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ success: false, message: 'Missing deploymentUrl or cookieValue' }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate types and lengths
|
|
||||||
if (typeof deploymentUrl !== 'string' || typeof cookieValue !== 'string' ||
|
|
||||||
deploymentUrl.length > 512 || cookieValue.length > 4096) {
|
|
||||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ success: false, message: 'Invalid parameter types or lengths' }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate deployment URL is an Alta domain
|
|
||||||
try {
|
|
||||||
const parsed = new URL(deploymentUrl);
|
|
||||||
const isAltaDomain = parsed.hostname.endsWith('.avasecurity.com') ||
|
|
||||||
parsed.hostname.endsWith('.avigilon.com');
|
|
||||||
if (!isAltaDomain || parsed.protocol !== 'https:') {
|
|
||||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ success: false, message: 'Invalid deployment URL domain' }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ success: false, message: 'Invalid deployment URL' }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
const cookies = ['va=' + cookieValue];
|
|
||||||
mainWindow.webContents.send('extension-cookie-received', {
|
|
||||||
deploymentUrl: deploymentUrl.replace(/\/$/, ''),
|
|
||||||
cookies,
|
|
||||||
cookieValue
|
|
||||||
});
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ success: true, message: 'Cookie received' }));
|
|
||||||
} else {
|
|
||||||
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ success: false, message: 'Application window not available' }));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ success: false, message: 'Invalid JSON' }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cookieServer.listen(COOKIE_SERVER_PORT, '127.0.0.1', () => {
|
|
||||||
console.log(`Cookie server listening on http://127.0.0.1:${COOKIE_SERVER_PORT}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
cookieServer.on('error', (err) => {
|
|
||||||
if (err.code === 'EADDRINUSE') {
|
|
||||||
console.error(`Cookie server error: Port ${COOKIE_SERVER_PORT} is already in use`);
|
|
||||||
} else {
|
|
||||||
console.error('Cookie server error:', err.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createWindow() {
|
|
||||||
mainWindow = new BrowserWindow({
|
|
||||||
width: 1400,
|
|
||||||
height: 900,
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: false,
|
|
||||||
contextIsolation: true,
|
|
||||||
preload: path.join(__dirname, 'preload.js')
|
|
||||||
},
|
|
||||||
icon: path.join(__dirname, 'assets', 'icon.png'), // Optional icon
|
|
||||||
title: 'Alta Video Camera Proxy with API'
|
|
||||||
});
|
|
||||||
|
|
||||||
mainWindow.loadFile('index.html');
|
|
||||||
|
|
||||||
// Open DevTools in development
|
|
||||||
if (process.argv.includes('--dev')) {
|
|
||||||
mainWindow.webContents.openDevTools();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
|
||||||
createWindow();
|
|
||||||
startCookieServer();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('before-quit', () => {
|
|
||||||
if (cookieServer) {
|
|
||||||
cookieServer.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('activate', () => {
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
createWindow();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// IPC handlers for API communication
|
|
||||||
ipcMain.handle('api-get-devices', async (event, { deploymentUrl, cookies }) => {
|
|
||||||
try {
|
|
||||||
const devicesUrl = `${deploymentUrl}/api/v1/devices`;
|
|
||||||
|
|
||||||
// Create axios instance with cookies
|
|
||||||
const axiosInstance = axios.create({
|
|
||||||
timeout: 10000,
|
|
||||||
headers: {
|
|
||||||
'Cookie': cookies ? cookies.join('; ') : ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await axiosInstance.get(devicesUrl);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
devices: response.data,
|
|
||||||
message: `Found ${response.data.length} devices`
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Get devices error:', error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: error.response?.data?.message || error.message || 'Failed to get devices'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('api-get-auth-info', async (event, { deploymentUrl, cookies }) => {
|
|
||||||
try {
|
|
||||||
const authUrl = `${deploymentUrl}/api/v1/auth`;
|
|
||||||
|
|
||||||
const axiosInstance = axios.create({
|
|
||||||
timeout: 10000,
|
|
||||||
headers: {
|
|
||||||
'Cookie': cookies ? cookies.join('; ') : ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await axiosInstance.get(authUrl);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
authInfo: response.data
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Get auth info error:', error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: error.response?.data?.message || error.message || 'Failed to get auth info'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cookie-based camera proxy functionality
|
|
||||||
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');
|
|
||||||
|
|
||||||
// Check if the executable exists
|
|
||||||
if (!fs.existsSync(proxyExePath)) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: 'Cookie-based camera proxy executable not found. Please ensure aware-cam-proxy.exe is in the application directory.'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the domain from the deployment URL
|
|
||||||
let domain = deploymentUrl;
|
|
||||||
if (domain.startsWith('https://')) {
|
|
||||||
domain = domain.substring(8);
|
|
||||||
} else if (domain.startsWith('http://')) {
|
|
||||||
domain = domain.substring(7);
|
|
||||||
}
|
|
||||||
// Remove trailing path segments
|
|
||||||
domain = domain.split('/')[0];
|
|
||||||
|
|
||||||
// Sanitize all inputs before embedding in batch file
|
|
||||||
const safeDomain = sanitizeBatchInput(domain);
|
|
||||||
const safeDeviceUuid = sanitizeBatchInput(deviceUuid);
|
|
||||||
const safeCookieKey = sanitizeBatchInput(cookieKey);
|
|
||||||
|
|
||||||
if (!safeDomain || !safeDeviceUuid || !safeCookieKey) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: 'Invalid characters detected in connection parameters.'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a batch file to launch the cookie-based camera proxy
|
|
||||||
const truncatedKey = safeCookieKey.length > 20 ? safeCookieKey.substring(0, 20) + '...' : safeCookieKey;
|
|
||||||
const batchContent = `@echo off
|
|
||||||
title APT-Proxy-${safeDeviceUuid}
|
|
||||||
echo Launching Alta Video Camera Proxy (Cookie Method)...
|
|
||||||
echo Domain: ${safeDomain}
|
|
||||||
echo Device UUID: ${safeDeviceUuid}
|
|
||||||
echo Cookie Key: ${truncatedKey}
|
|
||||||
echo.
|
|
||||||
"${proxyExePath}" -a "${safeDomain}" -d "${safeDeviceUuid}" -k "${safeCookieKey}"
|
|
||||||
echo.
|
|
||||||
echo Cookie-based camera proxy has finished. Press any key to close this window.
|
|
||||||
pause >nul`;
|
|
||||||
|
|
||||||
const tempDir = os.tmpdir();
|
|
||||||
const batchPath = path.join(tempDir, `cookie-proxy-${Date.now()}.bat`);
|
|
||||||
|
|
||||||
// Write the batch file
|
|
||||||
fs.writeFileSync(batchPath, batchContent);
|
|
||||||
|
|
||||||
console.log('Launching cookie-based camera proxy via batch file:', batchPath);
|
|
||||||
console.log('Command will be: aware-cam-proxy.exe -a', safeDomain, '-d', safeDeviceUuid, '-k [REDACTED]');
|
|
||||||
|
|
||||||
// Launch the batch file in a new command prompt window with unique title
|
|
||||||
const windowTitle = `APT-Proxy-${safeDeviceUuid}`;
|
|
||||||
const cmdProcess = spawn('cmd', ['/c', 'start', `"${windowTitle}"`, 'cmd', '/c', batchPath], {
|
|
||||||
detached: true,
|
|
||||||
stdio: 'ignore'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Store the process information for later termination
|
|
||||||
const processInfo = {
|
|
||||||
process: cmdProcess,
|
|
||||||
batchPath: batchPath,
|
|
||||||
deviceUuid: safeDeviceUuid,
|
|
||||||
startTime: Date.now(),
|
|
||||||
cookieKey: truncatedKey,
|
|
||||||
domain: safeDomain,
|
|
||||||
windowTitle: windowTitle, // Store window title for targeted cleanup
|
|
||||||
type: 'cookie' // Mark as cookie-based proxy
|
|
||||||
};
|
|
||||||
|
|
||||||
activeProxyProcesses.set(cmdProcess.pid, processInfo);
|
|
||||||
|
|
||||||
// Clean up the batch file after a delay
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(batchPath)) {
|
|
||||||
fs.unlinkSync(batchPath);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Could not clean up cookie proxy batch file:', error.message);
|
|
||||||
}
|
|
||||||
}, 60000); // Clean up after 1 minute
|
|
||||||
|
|
||||||
cmdProcess.unref(); // Allow the parent process to exit independently
|
|
||||||
|
|
||||||
// Clean up process tracking when it exits
|
|
||||||
cmdProcess.on('exit', () => {
|
|
||||||
activeProxyProcesses.delete(cmdProcess.pid);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: `Cookie-based camera proxy launched for ${deviceUuid}!`,
|
|
||||||
processId: cmdProcess.pid,
|
|
||||||
deviceUuid: deviceUuid,
|
|
||||||
type: 'cookie'
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to launch cookie-based camera proxy:', error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `Failed to launch cookie-based camera proxy: ${error.message}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Stop camera proxy functionality
|
|
||||||
ipcMain.handle('camera-proxy-stop', async (event, { processId }) => {
|
|
||||||
try {
|
|
||||||
console.log('Attempting to stop camera proxy processes...');
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
// Kill all aware-cam-proxy.exe processes by name
|
|
||||||
const killProxy = spawn('taskkill', ['/f', '/im', 'aware-cam-proxy.exe'], {
|
|
||||||
stdio: ['ignore', 'pipe', 'pipe']
|
|
||||||
});
|
|
||||||
|
|
||||||
let proxyOutput = '';
|
|
||||||
let proxyError = '';
|
|
||||||
|
|
||||||
killProxy.stdout.on('data', (data) => {
|
|
||||||
proxyOutput += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
killProxy.stderr.on('data', (data) => {
|
|
||||||
proxyError += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
killProxy.on('close', (code) => {
|
|
||||||
// Clean up our process tracking
|
|
||||||
activeProxyProcesses.clear();
|
|
||||||
|
|
||||||
if (code === 0 || proxyOutput.includes('SUCCESS')) {
|
|
||||||
console.log('Camera proxy processes terminated successfully');
|
|
||||||
resolve({
|
|
||||||
success: true,
|
|
||||||
message: 'Camera proxy processes stopped successfully'
|
|
||||||
});
|
|
||||||
} else if (proxyError.includes('not found') || proxyError.includes('No tasks')) {
|
|
||||||
console.log('No camera proxy processes were running');
|
|
||||||
resolve({
|
|
||||||
success: true,
|
|
||||||
message: 'No camera proxy processes were running'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resolve({
|
|
||||||
success: true,
|
|
||||||
message: 'Attempted to stop all camera proxy processes'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
killProxy.on('error', (error) => {
|
|
||||||
console.error('Error with taskkill by name:', error);
|
|
||||||
activeProxyProcesses.clear();
|
|
||||||
resolve({
|
|
||||||
success: false,
|
|
||||||
message: `Failed to stop camera proxy: ${error.message}`
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to stop camera proxy:', error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `Failed to stop camera proxy: ${error.message}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Self-Update Functionality ---
|
|
||||||
|
|
||||||
// Compare semver versions: returns -1 if a < b, 0 if equal, 1 if a > b
|
|
||||||
function compareVersions(a, b) {
|
|
||||||
// Strip pre-release tags (e.g. "1.2.3-beta.1" → "1.2.3")
|
|
||||||
const cleanA = a.replace(/-.*$/, '');
|
|
||||||
const cleanB = b.replace(/-.*$/, '');
|
|
||||||
const partsA = cleanA.split('.').map(Number);
|
|
||||||
const partsB = cleanB.split('.').map(Number);
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const numA = partsA[i] || 0;
|
|
||||||
const numB = partsB[i] || 0;
|
|
||||||
if (numA < numB) return -1;
|
|
||||||
if (numA > numB) return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Follow HTTPS redirects and return the final response (for GitHub asset downloads)
|
|
||||||
function httpsGetFollowRedirects(url, callback, redirectCount = 0) {
|
|
||||||
if (redirectCount >= 5) {
|
|
||||||
return callback(null, new Error('Too many redirects'));
|
|
||||||
}
|
|
||||||
const parsed = new URL(url);
|
|
||||||
if (parsed.protocol !== 'https:') {
|
|
||||||
return callback(null, new Error('Only HTTPS URLs are allowed'));
|
|
||||||
}
|
|
||||||
https.get(url, { headers: { 'User-Agent': 'Alta-Proxy-Tool' } }, (res) => {
|
|
||||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
||||||
const redirectUrl = new URL(res.headers.location, url).href;
|
|
||||||
httpsGetFollowRedirects(redirectUrl, callback, redirectCount + 1);
|
|
||||||
} else {
|
|
||||||
callback(res);
|
|
||||||
}
|
|
||||||
}).on('error', (err) => {
|
|
||||||
callback(null, err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.handle('get-current-version', async () => {
|
|
||||||
return { success: true, version: app.getVersion() };
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('check-for-updates', async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
'https://api.github.com/repos/PageZ948/Alta-Proxy-Tool/releases/latest',
|
|
||||||
{
|
|
||||||
timeout: 10000,
|
|
||||||
headers: { 'User-Agent': 'Alta-Proxy-Tool', 'Accept': 'application/vnd.github.v3+json' }
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const release = response.data;
|
|
||||||
const latestVersion = release.tag_name.replace(/^v/, '');
|
|
||||||
const currentVersion = app.getVersion();
|
|
||||||
|
|
||||||
const updateAvailable = compareVersions(currentVersion, latestVersion) < 0;
|
|
||||||
|
|
||||||
// Find the portable .exe asset
|
|
||||||
const exeAsset = release.assets.find(a => /AltaCameraProxy-.*-portable\.exe$/i.test(a.name));
|
|
||||||
const downloadUrl = exeAsset ? exeAsset.browser_download_url : null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
updateAvailable,
|
|
||||||
currentVersion,
|
|
||||||
latestVersion,
|
|
||||||
downloadUrl,
|
|
||||||
releaseNotes: release.body || '',
|
|
||||||
releaseName: release.name || `v${latestVersion}`
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response && error.response.status === 404) {
|
|
||||||
return { success: true, updateAvailable: false, currentVersion: app.getVersion(), message: 'No releases available yet' };
|
|
||||||
}
|
|
||||||
if (error.response && error.response.status === 403) {
|
|
||||||
return { success: false, message: 'GitHub API rate limit exceeded. Try again later.' };
|
|
||||||
}
|
|
||||||
console.error('Check for updates error:', error.message);
|
|
||||||
return { success: false, message: error.message || 'Failed to check for updates' };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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 currentExeName = path.basename(currentExePath);
|
|
||||||
|
|
||||||
// Check write permission on the app directory
|
|
||||||
try {
|
|
||||||
fs.accessSync(currentDir, fs.constants.W_OK);
|
|
||||||
} catch {
|
|
||||||
return { success: false, message: 'No write permission to the application directory. Try running as administrator.' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const tempDir = os.tmpdir();
|
|
||||||
const tempExePath = path.join(tempDir, `AltaCameraProxy-update-${Date.now()}.exe`);
|
|
||||||
|
|
||||||
// Download the file with progress reporting
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
httpsGetFollowRedirects(downloadUrl, (res, err) => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
if (res.statusCode !== 200) {
|
|
||||||
res.resume();
|
|
||||||
return reject(new Error(`Download failed with status ${res.statusCode}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalSize = parseInt(res.headers['content-length'], 10) || 0;
|
|
||||||
let downloadedSize = 0;
|
|
||||||
const fileStream = fs.createWriteStream(tempExePath);
|
|
||||||
|
|
||||||
res.on('data', (chunk) => {
|
|
||||||
downloadedSize += chunk.length;
|
|
||||||
if (totalSize > 0 && mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
const percent = Math.round((downloadedSize / totalSize) * 100);
|
|
||||||
mainWindow.webContents.send('update-download-progress', { percent, downloadedSize, totalSize });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
res.pipe(fileStream);
|
|
||||||
|
|
||||||
fileStream.on('finish', () => {
|
|
||||||
fileStream.close();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
fileStream.on('error', (err) => {
|
|
||||||
fs.unlink(tempExePath, () => {});
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify downloaded file size (sanity check: > 10MB for an Electron portable exe)
|
|
||||||
const stats = fs.statSync(tempExePath);
|
|
||||||
if (stats.size < 10 * 1024 * 1024) {
|
|
||||||
fs.unlinkSync(tempExePath);
|
|
||||||
return { success: false, message: 'Downloaded file is too small — update may be corrupt.' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create batch script to replace the exe after this process exits
|
|
||||||
const batchPath = path.join(tempDir, `apt-update-${Date.now()}.bat`);
|
|
||||||
const pid = process.pid;
|
|
||||||
const batchContent = `@echo off\r\ntitle APT-Updater\r\necho Waiting for Alta Proxy Tool to close...\r\n:waitloop\r\ntasklist /fi "PID eq ${pid}" 2>nul | find "${pid}" >nul\r\nif not errorlevel 1 (\r\n timeout /t 1 /nobreak >nul\r\n goto waitloop\r\n)\r\necho Applying update...\r\ncopy /y "${tempExePath}" "${path.join(currentDir, currentExeName)}"\r\nif errorlevel 1 (\r\n echo Update failed! Could not copy new version.\r\n pause\r\n del "${tempExePath}" >nul 2>&1\r\n del "%~f0" >nul 2>&1\r\n exit /b 1\r\n)\r\necho Update complete. Launching new version...\r\nstart "" "${path.join(currentDir, currentExeName)}"\r\ndel "${tempExePath}" >nul 2>&1\r\ndel "%~f0" >nul 2>&1\r\n`;
|
|
||||||
|
|
||||||
fs.writeFileSync(batchPath, batchContent);
|
|
||||||
|
|
||||||
// Spawn the updater batch script detached
|
|
||||||
let updater;
|
|
||||||
try {
|
|
||||||
updater = spawn('cmd', ['/c', batchPath], {
|
|
||||||
detached: true,
|
|
||||||
stdio: 'ignore',
|
|
||||||
windowsHide: true
|
|
||||||
});
|
|
||||||
updater.unref();
|
|
||||||
} catch (spawnError) {
|
|
||||||
console.error('Failed to spawn updater:', spawnError);
|
|
||||||
try { fs.unlinkSync(tempExePath); } catch {}
|
|
||||||
try { fs.unlinkSync(batchPath); } catch {}
|
|
||||||
return { success: false, message: 'Failed to start updater process.' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quit the app after a delay to let the IPC response return to renderer
|
|
||||||
setTimeout(() => {
|
|
||||||
app.quit();
|
|
||||||
}, 1500);
|
|
||||||
|
|
||||||
return { success: true, message: 'Update is being installed. The app will restart shortly.' };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Download and install update error:', error);
|
|
||||||
return { success: false, message: error.message || 'Failed to download and install update' };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Generated
-6438
File diff suppressed because it is too large
Load Diff
@@ -1,48 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "alta-api-client",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Electron app for connecting to Alta API",
|
|
||||||
"main": "main.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "electron .",
|
|
||||||
"dev": "electron . --dev",
|
|
||||||
"build": "electron-builder --win --publish=never",
|
|
||||||
"build-test": "electron-builder --win --dir",
|
|
||||||
"prebuild": "echo Checking build requirements..."
|
|
||||||
},
|
|
||||||
"build": {
|
|
||||||
"appId": "com.internal.alta-camera-proxy",
|
|
||||||
"productName": "Alta Camera Proxy",
|
|
||||||
"directories": {
|
|
||||||
"output": "dist"
|
|
||||||
},
|
|
||||||
"win": {
|
|
||||||
"target": "portable",
|
|
||||||
"icon": "assets/icon.png"
|
|
||||||
},
|
|
||||||
"portable": {
|
|
||||||
"artifactName": "AltaCameraProxy-${version}-portable.exe"
|
|
||||||
},
|
|
||||||
"afterSign": false,
|
|
||||||
"afterAllArtifactBuild": false
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"electron",
|
|
||||||
"alta",
|
|
||||||
"api",
|
|
||||||
"avigilon"
|
|
||||||
],
|
|
||||||
"author": "Your Name",
|
|
||||||
"license": "MIT",
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"axios": "^1.6.0",
|
|
||||||
"crypto-js": "^4.2.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-37
@@ -1,37 +0,0 @@
|
|||||||
const { contextBridge, ipcRenderer } = require('electron');
|
|
||||||
|
|
||||||
// Expose protected methods that allow the renderer process to use
|
|
||||||
// the ipcRenderer without exposing the entire object
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
|
||||||
getDevices: (params) => ipcRenderer.invoke('api-get-devices', params),
|
|
||||||
getAuthInfo: (params) => ipcRenderer.invoke('api-get-auth-info', params),
|
|
||||||
|
|
||||||
// Camera proxy functionality
|
|
||||||
launchCookieCameraProxy: (params) => ipcRenderer.invoke('camera-proxy-cookie-launch', params),
|
|
||||||
stopCameraProxy: (processId) => ipcRenderer.invoke('camera-proxy-stop', { processId }),
|
|
||||||
|
|
||||||
// Extension cookie bridge (push from main process)
|
|
||||||
onExtensionCookie: (callback) => {
|
|
||||||
ipcRenderer.on('extension-cookie-received', (event, data) => {
|
|
||||||
try {
|
|
||||||
callback(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Extension cookie handler error:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Self-update functionality
|
|
||||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
|
||||||
downloadAndInstallUpdate: (params) => ipcRenderer.invoke('download-and-install-update', params),
|
|
||||||
getCurrentVersion: () => ipcRenderer.invoke('get-current-version'),
|
|
||||||
onUpdateDownloadProgress: (callback) => {
|
|
||||||
ipcRenderer.on('update-download-progress', (event, data) => {
|
|
||||||
try {
|
|
||||||
callback(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Update download progress handler error:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
-616
@@ -1,616 +0,0 @@
|
|||||||
// Global variables to store session data
|
|
||||||
let sessionData = {
|
|
||||||
deploymentUrl: '',
|
|
||||||
cookies: null,
|
|
||||||
isConnected: false
|
|
||||||
};
|
|
||||||
|
|
||||||
// DOM elements
|
|
||||||
const connectionStatus = document.getElementById('connectionStatus');
|
|
||||||
const deviceStatus = document.getElementById('deviceStatus');
|
|
||||||
const deviceList = document.getElementById('deviceList');
|
|
||||||
const statusIndicator = document.getElementById('statusIndicator');
|
|
||||||
const deviceSearch = document.getElementById('deviceSearch');
|
|
||||||
|
|
||||||
// Connection buttons
|
|
||||||
const disconnectBtn = document.getElementById('disconnectBtn');
|
|
||||||
|
|
||||||
// Cookie proxy elements
|
|
||||||
const cookieDeviceUUID = document.getElementById('cookieDeviceUUID');
|
|
||||||
const cookieKey = document.getElementById('cookieKey');
|
|
||||||
const startCookieProxyBtn = document.getElementById('startCookieProxyBtn');
|
|
||||||
const stopCookieProxyBtn = document.getElementById('stopCookieProxyBtn');
|
|
||||||
|
|
||||||
// Update elements
|
|
||||||
const checkUpdateBtn = document.getElementById('checkUpdateBtn');
|
|
||||||
const updateModalOverlay = document.getElementById('updateModalOverlay');
|
|
||||||
const updateModalCloseBtn = document.getElementById('updateModalCloseBtn');
|
|
||||||
const updateModalMessage = document.getElementById('updateModalMessage');
|
|
||||||
const updateModalNotes = document.getElementById('updateModalNotes');
|
|
||||||
const updateProgressContainer = document.getElementById('updateProgressContainer');
|
|
||||||
const updateProgressFill = document.getElementById('updateProgressFill');
|
|
||||||
const updateProgressText = document.getElementById('updateProgressText');
|
|
||||||
const updateInstallBtn = document.getElementById('updateInstallBtn');
|
|
||||||
const updateLaterBtn = document.getElementById('updateLaterBtn');
|
|
||||||
|
|
||||||
// Track selected device
|
|
||||||
let selectedDevice = null;
|
|
||||||
let activeCookieProxyConnections = new Map(); // Track cookie-based proxy connections
|
|
||||||
let allDevices = []; // Store all devices for search functionality
|
|
||||||
let pendingUpdateInfo = null; // Store update info for install action
|
|
||||||
|
|
||||||
// Event listeners
|
|
||||||
disconnectBtn.addEventListener('click', handleDisconnect);
|
|
||||||
|
|
||||||
// Cookie proxy event listeners
|
|
||||||
startCookieProxyBtn.addEventListener('click', handleStartCookieProxy);
|
|
||||||
stopCookieProxyBtn.addEventListener('click', handleStopCookieProxy);
|
|
||||||
|
|
||||||
// Cookie key input listener to update button states
|
|
||||||
cookieKey.addEventListener('input', updateCookieProxyButtonStates);
|
|
||||||
|
|
||||||
// Device search event listener
|
|
||||||
deviceSearch.addEventListener('input', handleDeviceSearch);
|
|
||||||
|
|
||||||
// Update event listeners
|
|
||||||
checkUpdateBtn.addEventListener('click', handleCheckForUpdates);
|
|
||||||
updateInstallBtn.addEventListener('click', handleInstallUpdate);
|
|
||||||
updateLaterBtn.addEventListener('click', closeUpdateModal);
|
|
||||||
updateModalCloseBtn.addEventListener('click', closeUpdateModal);
|
|
||||||
|
|
||||||
// Handle disconnect
|
|
||||||
function handleDisconnect() {
|
|
||||||
sessionData.isConnected = false;
|
|
||||||
sessionData.cookies = null;
|
|
||||||
sessionData.deploymentUrl = '';
|
|
||||||
selectedDevice = null;
|
|
||||||
activeCookieProxyConnections.clear();
|
|
||||||
allDevices = []; // Clear stored devices
|
|
||||||
|
|
||||||
updateConnectionStatus(false);
|
|
||||||
updateButtonStates();
|
|
||||||
clearDeviceList();
|
|
||||||
cookieDeviceUUID.value = '';
|
|
||||||
cookieKey.value = '';
|
|
||||||
deviceSearch.value = ''; // Clear search input
|
|
||||||
|
|
||||||
// Clear device status message
|
|
||||||
deviceStatus.style.display = 'none';
|
|
||||||
deviceStatus.textContent = '';
|
|
||||||
|
|
||||||
showStatus(connectionStatus, 'Disconnected from API', 'info');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle start cookie proxy
|
|
||||||
async function handleStartCookieProxy() {
|
|
||||||
if (!selectedDevice) {
|
|
||||||
showStatus(connectionStatus, 'Please select a device first', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cookieKeyValue = cookieKey.value.trim();
|
|
||||||
if (!cookieKeyValue) {
|
|
||||||
showStatus(connectionStatus, 'Please enter a cookie key', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sessionData.deploymentUrl) {
|
|
||||||
showStatus(connectionStatus, 'Please connect to API first to get deployment URL', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this device already has an active cookie connection
|
|
||||||
const deviceId = selectedDevice.guid || selectedDevice.id;
|
|
||||||
if (activeCookieProxyConnections.has(deviceId)) {
|
|
||||||
showStatus(connectionStatus, `Cookie proxy already running for device ${selectedDevice.name}`, 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable button during launch
|
|
||||||
startCookieProxyBtn.disabled = true;
|
|
||||||
showStatus(connectionStatus, `Starting cookie proxy for device ${selectedDevice.name}...`, 'info');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await window.electronAPI.launchCookieCameraProxy({
|
|
||||||
deploymentUrl: sessionData.deploymentUrl,
|
|
||||||
cookieKey: cookieKeyValue,
|
|
||||||
deviceUuid: deviceId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
// Track this connection
|
|
||||||
activeCookieProxyConnections.set(deviceId, {
|
|
||||||
processId: result.processId,
|
|
||||||
deviceName: selectedDevice.name,
|
|
||||||
deviceId: deviceId,
|
|
||||||
startTime: Date.now(),
|
|
||||||
type: 'cookie'
|
|
||||||
});
|
|
||||||
|
|
||||||
updateCookieProxyButtonStates();
|
|
||||||
showStatus(connectionStatus, `${result.message} (Cookie proxy active for ${selectedDevice.name})`, 'success');
|
|
||||||
} else {
|
|
||||||
showStatus(connectionStatus, `Failed to start cookie proxy: ${result.message}`, 'error');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Cookie proxy launch error:', error);
|
|
||||||
showStatus(connectionStatus, `Error launching cookie proxy: ${error.message}`, 'error');
|
|
||||||
} finally {
|
|
||||||
startCookieProxyBtn.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle stop cookie proxy
|
|
||||||
async function handleStopCookieProxy() {
|
|
||||||
if (activeCookieProxyConnections.size === 0) {
|
|
||||||
showStatus(connectionStatus, 'No active cookie proxy connections found', 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showStatus(connectionStatus, 'Stopping cookie proxy connections...', 'info');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await window.electronAPI.stopCameraProxy(null); // Stop all processes
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
activeCookieProxyConnections.clear();
|
|
||||||
showStatus(connectionStatus, 'Cookie proxy connections stopped successfully', 'success');
|
|
||||||
|
|
||||||
// Update visual indicators for all devices
|
|
||||||
const deviceItems = deviceList.querySelectorAll('.device-item');
|
|
||||||
deviceItems.forEach(item => {
|
|
||||||
item.classList.remove('cookie-proxy-active');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
showStatus(connectionStatus, `Failed to stop cookie proxy: ${result.message}`, 'warning');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Stop cookie proxy error:', error);
|
|
||||||
showStatus(connectionStatus, 'Error stopping cookie proxy', 'error');
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCookieProxyButtonStates();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update cookie proxy button states
|
|
||||||
function updateCookieProxyButtonStates() {
|
|
||||||
const hasSelectedDevice = selectedDevice !== null;
|
|
||||||
const hasCookieKey = cookieKey.value.trim().length > 0;
|
|
||||||
const hasActiveConnection = activeCookieProxyConnections.size > 0;
|
|
||||||
|
|
||||||
if (sessionData.isConnected && hasSelectedDevice && hasCookieKey) {
|
|
||||||
const deviceId = selectedDevice.guid || selectedDevice.id;
|
|
||||||
const isThisDeviceActive = activeCookieProxyConnections.has(deviceId);
|
|
||||||
|
|
||||||
startCookieProxyBtn.disabled = isThisDeviceActive;
|
|
||||||
stopCookieProxyBtn.disabled = !hasActiveConnection;
|
|
||||||
} else {
|
|
||||||
startCookieProxyBtn.disabled = true;
|
|
||||||
stopCookieProxyBtn.disabled = !hasActiveConnection;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update connection status indicator
|
|
||||||
function updateConnectionStatus(connected) {
|
|
||||||
const statusDot = statusIndicator.querySelector('.status-dot');
|
|
||||||
const statusText = statusIndicator.querySelector('.status-text');
|
|
||||||
|
|
||||||
if (connected) {
|
|
||||||
statusDot.className = 'status-dot online';
|
|
||||||
statusText.textContent = 'Connected';
|
|
||||||
} else {
|
|
||||||
statusDot.className = 'status-dot offline';
|
|
||||||
statusText.textContent = 'Disconnected';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update button states based on connection status
|
|
||||||
function updateButtonStates() {
|
|
||||||
if (sessionData.isConnected) {
|
|
||||||
disconnectBtn.disabled = false;
|
|
||||||
} else {
|
|
||||||
disconnectBtn.disabled = true;
|
|
||||||
startCookieProxyBtn.disabled = true;
|
|
||||||
stopCookieProxyBtn.disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always update cookie proxy button states
|
|
||||||
updateCookieProxyButtonStates();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle get devices (now called automatically)
|
|
||||||
async function handleGetDevices() {
|
|
||||||
if (!sessionData.isConnected) {
|
|
||||||
showStatus(deviceStatus, 'Please connect to the API first', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showStatus(deviceStatus, 'Fetching devices...', 'info');
|
|
||||||
clearDeviceList();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await window.electronAPI.getDevices({
|
|
||||||
deploymentUrl: sessionData.deploymentUrl,
|
|
||||||
cookies: sessionData.cookies
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
// Filter devices to only show non-cloud cameras (localStorage = false)
|
|
||||||
const filteredDevices = result.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)
|
|
||||||
return device.capabilities.localStorage === false;
|
|
||||||
}
|
|
||||||
// If no capabilities or localStorage property, include the device (fallback)
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalDevices = result.devices.length;
|
|
||||||
const filteredCount = filteredDevices.length;
|
|
||||||
const cloudDevicesHidden = totalDevices - filteredCount;
|
|
||||||
|
|
||||||
let statusMessage = `Found ${filteredCount} local camera${filteredCount !== 1 ? 's' : ''}`;
|
|
||||||
if (cloudDevicesHidden > 0) {
|
|
||||||
statusMessage += ` (${cloudDevicesHidden} cloud camera${cloudDevicesHidden !== 1 ? 's' : ''} hidden)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
showStatus(deviceStatus, statusMessage, 'success');
|
|
||||||
|
|
||||||
// Store all devices for search functionality
|
|
||||||
allDevices = filteredDevices;
|
|
||||||
displayDevices(filteredDevices);
|
|
||||||
} else {
|
|
||||||
showStatus(deviceStatus, `Failed to get devices: ${result.message}`, 'error');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Get devices error:', error);
|
|
||||||
showStatus(deviceStatus, `Error getting devices: ${error.message}`, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to determine device status from API data
|
|
||||||
function getDeviceStatus(device) {
|
|
||||||
// Check for live.display_status first (Alta API standard)
|
|
||||||
if (device.live && device.live.display_status) {
|
|
||||||
const status = device.live.display_status.toLowerCase();
|
|
||||||
|
|
||||||
// Handle color-based status responses from Alta API
|
|
||||||
if (status === 'green') {
|
|
||||||
return {
|
|
||||||
isOnline: true,
|
|
||||||
statusText: 'Online'
|
|
||||||
};
|
|
||||||
} else if (status === 'red') {
|
|
||||||
return {
|
|
||||||
isOnline: false,
|
|
||||||
statusText: 'Offline'
|
|
||||||
};
|
|
||||||
} else if (status === 'yellow' || status === 'orange') {
|
|
||||||
return {
|
|
||||||
isOnline: false,
|
|
||||||
statusText: 'Warning'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle text-based status responses
|
|
||||||
return {
|
|
||||||
isOnline: status === 'online' || status === 'live' || status === 'connected',
|
|
||||||
statusText: status === 'online' || status === 'live' || status === 'connected' ? 'Online' : 'Offline'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to other possible status fields
|
|
||||||
if (device.online !== undefined) {
|
|
||||||
return {
|
|
||||||
isOnline: device.online,
|
|
||||||
statusText: device.online ? 'Online' : 'Offline'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device.status) {
|
|
||||||
const status = device.status.toLowerCase();
|
|
||||||
|
|
||||||
// Handle color-based status in other fields
|
|
||||||
if (status === 'green') {
|
|
||||||
return {
|
|
||||||
isOnline: true,
|
|
||||||
statusText: 'Online'
|
|
||||||
};
|
|
||||||
} else if (status === 'red') {
|
|
||||||
return {
|
|
||||||
isOnline: false,
|
|
||||||
statusText: 'Offline'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isOnline: status === 'online' || status === 'live' || status === 'connected',
|
|
||||||
statusText: status === 'online' || status === 'live' || status === 'connected' ? 'Online' : 'Offline'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to offline if no status information available
|
|
||||||
return {
|
|
||||||
isOnline: false,
|
|
||||||
statusText: 'Offline'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle device search
|
|
||||||
function handleDeviceSearch() {
|
|
||||||
const searchTerm = deviceSearch.value.toLowerCase().trim();
|
|
||||||
|
|
||||||
if (!searchTerm) {
|
|
||||||
// Show all devices if search is empty
|
|
||||||
displayDevices(allDevices);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter devices based on search term
|
|
||||||
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();
|
|
||||||
|
|
||||||
return deviceName.includes(searchTerm) ||
|
|
||||||
deviceId.includes(searchTerm) ||
|
|
||||||
deviceType.includes(searchTerm) ||
|
|
||||||
deviceModel.includes(searchTerm) ||
|
|
||||||
deviceIp.includes(searchTerm);
|
|
||||||
});
|
|
||||||
|
|
||||||
displayDevices(filteredDevices);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display devices in the UI
|
|
||||||
function displayDevices(devices) {
|
|
||||||
clearDeviceList();
|
|
||||||
|
|
||||||
if (!devices || devices.length === 0) {
|
|
||||||
const searchTerm = deviceSearch.value.toLowerCase().trim();
|
|
||||||
const message = searchTerm ? 'No devices match your search' : 'No devices found';
|
|
||||||
deviceList.innerHTML = `<p class="no-devices">${message}</p>`;
|
|
||||||
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 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');
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceItem.innerHTML = `
|
|
||||||
<div class="device-name">${escapeHtml(device.name || 'Unnamed Device')}</div>
|
|
||||||
<div class="device-status-dot ${deviceStatus.isOnline ? 'online' : 'offline'}"></div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Add click handler for device selection
|
|
||||||
deviceItem.addEventListener('click', () => selectDevice(device, deviceItem));
|
|
||||||
|
|
||||||
deviceList.appendChild(deviceItem);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update cookie proxy button states after displaying devices
|
|
||||||
updateCookieProxyButtonStates();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle device selection
|
|
||||||
function selectDevice(device, deviceElement) {
|
|
||||||
// Remove previous selection
|
|
||||||
const previousSelected = deviceList.querySelector('.device-item.selected');
|
|
||||||
if (previousSelected) {
|
|
||||||
previousSelected.classList.remove('selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select current device
|
|
||||||
deviceElement.classList.add('selected');
|
|
||||||
selectedDevice = device;
|
|
||||||
|
|
||||||
// Auto-populate cookie device UUID field
|
|
||||||
const uuid = device.guid || device.id || '';
|
|
||||||
cookieDeviceUUID.value = uuid;
|
|
||||||
|
|
||||||
// Show device selection feedback with connection status
|
|
||||||
if (uuid) {
|
|
||||||
const isCookieActive = activeCookieProxyConnections.has(uuid);
|
|
||||||
const cookieStatusText = isCookieActive ? ' (COOKIE PROXY ACTIVE)' : '';
|
|
||||||
showStatus(connectionStatus, `Selected device: ${device.name || 'Unnamed Device'} (UUID: ${uuid})${cookieStatusText}`, 'info');
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCookieProxyButtonStates();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility functions
|
|
||||||
function showStatus(element, message, type) {
|
|
||||||
element.textContent = message;
|
|
||||||
element.className = `status-message ${type}`;
|
|
||||||
element.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearDeviceList() {
|
|
||||||
deviceList.innerHTML = '<p class="placeholder-text">Connect to API to load devices</p>';
|
|
||||||
selectedDevice = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(text) {
|
|
||||||
if (typeof text !== 'string') return text;
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = text;
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Self-Update Functions ---
|
|
||||||
|
|
||||||
async function handleCheckForUpdates() {
|
|
||||||
checkUpdateBtn.disabled = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await window.electronAPI.checkForUpdates();
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
showStatus(connectionStatus, result.message || 'Failed to check for updates', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.message === 'No releases available yet') {
|
|
||||||
showStatus(connectionStatus, 'No releases available yet', 'info');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.updateAvailable) {
|
|
||||||
showUpdateModal(result);
|
|
||||||
} else {
|
|
||||||
showStatus(connectionStatus, `You're on the latest version (v${result.currentVersion})`, 'success');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Check for updates error:', error);
|
|
||||||
showStatus(connectionStatus, 'Could not check for updates. Check your internet connection.', 'error');
|
|
||||||
} finally {
|
|
||||||
checkUpdateBtn.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showUpdateModal(updateInfo) {
|
|
||||||
pendingUpdateInfo = updateInfo;
|
|
||||||
updateModalMessage.textContent = `A new version is available: v${updateInfo.latestVersion} (current: v${updateInfo.currentVersion})`;
|
|
||||||
updateModalNotes.textContent = updateInfo.releaseNotes || '';
|
|
||||||
|
|
||||||
// Reset progress state
|
|
||||||
updateProgressContainer.style.display = 'none';
|
|
||||||
updateProgressFill.style.width = '0%';
|
|
||||||
updateProgressText.textContent = '0%';
|
|
||||||
|
|
||||||
// Reset button states
|
|
||||||
updateInstallBtn.disabled = !updateInfo.downloadUrl;
|
|
||||||
updateLaterBtn.disabled = false;
|
|
||||||
updateInstallBtn.textContent = 'Install Update';
|
|
||||||
|
|
||||||
if (!updateInfo.downloadUrl) {
|
|
||||||
updateModalMessage.textContent += '\n(No downloadable asset found for this release)';
|
|
||||||
}
|
|
||||||
|
|
||||||
updateModalOverlay.style.display = 'flex';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleInstallUpdate() {
|
|
||||||
if (!pendingUpdateInfo || !pendingUpdateInfo.downloadUrl) return;
|
|
||||||
|
|
||||||
// Disable controls during download
|
|
||||||
updateInstallBtn.disabled = true;
|
|
||||||
updateInstallBtn.textContent = 'Downloading...';
|
|
||||||
updateLaterBtn.disabled = true;
|
|
||||||
updateModalCloseBtn.style.display = 'none';
|
|
||||||
updateProgressContainer.style.display = 'flex';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await window.electronAPI.downloadAndInstallUpdate({
|
|
||||||
downloadUrl: pendingUpdateInfo.downloadUrl
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
updateInstallBtn.textContent = 'Restarting...';
|
|
||||||
updateProgressFill.style.width = '100%';
|
|
||||||
updateProgressText.textContent = '100%';
|
|
||||||
} else {
|
|
||||||
showStatus(connectionStatus, `Update failed: ${result.message}`, 'error');
|
|
||||||
closeUpdateModal();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Install update error:', error);
|
|
||||||
showStatus(connectionStatus, 'Update failed. Please try again.', 'error');
|
|
||||||
closeUpdateModal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeUpdateModal() {
|
|
||||||
updateModalOverlay.style.display = 'none';
|
|
||||||
pendingUpdateInfo = null;
|
|
||||||
updateProgressContainer.style.display = 'none';
|
|
||||||
updateProgressFill.style.width = '0%';
|
|
||||||
updateProgressText.textContent = '0%';
|
|
||||||
updateInstallBtn.textContent = 'Install Update';
|
|
||||||
updateInstallBtn.disabled = false;
|
|
||||||
updateLaterBtn.disabled = false;
|
|
||||||
updateModalCloseBtn.style.display = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkForUpdatesOnStartup() {
|
|
||||||
try {
|
|
||||||
const result = await window.electronAPI.checkForUpdates();
|
|
||||||
|
|
||||||
if (result.success && result.updateAvailable) {
|
|
||||||
checkUpdateBtn.classList.add('update-available');
|
|
||||||
checkUpdateBtn.title = `Update available: v${result.latestVersion}`;
|
|
||||||
showStatus(connectionStatus, `Update available: v${result.latestVersion}`, 'info');
|
|
||||||
// Store for quick modal open
|
|
||||||
pendingUpdateInfo = result;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Silent fail on startup check
|
|
||||||
console.log('Startup update check failed:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle cookie received from Chrome extension via local HTTP bridge
|
|
||||||
async function handleExtensionCookie(data) {
|
|
||||||
const { deploymentUrl, cookies, cookieValue } = data;
|
|
||||||
|
|
||||||
// If already connected, disconnect first
|
|
||||||
if (sessionData.isConnected) {
|
|
||||||
handleDisconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set session state from extension cookie
|
|
||||||
sessionData.deploymentUrl = deploymentUrl;
|
|
||||||
sessionData.cookies = cookies;
|
|
||||||
sessionData.isConnected = true;
|
|
||||||
|
|
||||||
showStatus(connectionStatus, `Connected via Chrome extension to ${deploymentUrl}`, 'success');
|
|
||||||
updateConnectionStatus(true);
|
|
||||||
updateButtonStates();
|
|
||||||
|
|
||||||
// Auto-populate cookie key
|
|
||||||
cookieKey.value = cookieValue;
|
|
||||||
updateCookieProxyButtonStates();
|
|
||||||
|
|
||||||
// Fetch devices
|
|
||||||
try {
|
|
||||||
await handleGetDevices();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to fetch devices after extension cookie:', err);
|
|
||||||
showStatus(deviceStatus, 'Connected, but failed to load devices.', 'warning');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the app
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
console.log('Alta Video Camera Proxy loaded');
|
|
||||||
|
|
||||||
// Initialize connection status
|
|
||||||
updateConnectionStatus(false);
|
|
||||||
updateButtonStates();
|
|
||||||
|
|
||||||
// Listen for cookies pushed from Chrome extension
|
|
||||||
window.electronAPI.onExtensionCookie(handleExtensionCookie);
|
|
||||||
|
|
||||||
// Listen for update download progress
|
|
||||||
window.electronAPI.onUpdateDownloadProgress((data) => {
|
|
||||||
updateProgressFill.style.width = `${data.percent}%`;
|
|
||||||
updateProgressText.textContent = `${data.percent}%`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto-check for updates after 2 seconds
|
|
||||||
setTimeout(checkForUpdatesOnStartup, 2000);
|
|
||||||
});
|
|
||||||
-643
@@ -1,643 +0,0 @@
|
|||||||
/* Dark Mode Professional UI - Alta Video Camera Proxy */
|
|
||||||
|
|
||||||
/* CSS Variables for Dark Mode Color Palette */
|
|
||||||
:root {
|
|
||||||
--bg-primary: #1E1E1E;
|
|
||||||
--bg-secondary: #2D2D30;
|
|
||||||
--border: #3C3C3C;
|
|
||||||
--text-primary: #E0E0E0;
|
|
||||||
--text-secondary: #999999;
|
|
||||||
--accent-primary: #0E7AFE;
|
|
||||||
--accent-primary-hover: #0A5FD9;
|
|
||||||
--success: #4CAF50;
|
|
||||||
--error: #F44336;
|
|
||||||
--warning: #FF9800;
|
|
||||||
--input-bg: #1E1E1E;
|
|
||||||
--button-outline: #555555;
|
|
||||||
--card-bg: #2D2D30;
|
|
||||||
--hover-bg: #3C3C3C;
|
|
||||||
--tab-active: #0E7AFE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reset and Base Styles */
|
|
||||||
* {
|
|
||||||
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: 14px;
|
|
||||||
line-height: 1.4;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* App Container */
|
|
||||||
.app-container {
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main Layout */
|
|
||||||
.main-layout {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Left Sidebar - Available Devices */
|
|
||||||
.devices-sidebar {
|
|
||||||
width: 220px;
|
|
||||||
background: var(--card-bg);
|
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header {
|
|
||||||
padding: 16px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header h2 {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin: 0;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Device Search */
|
|
||||||
.device-search-container {
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-search-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid var(--button-outline);
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
background: var(--input-bg);
|
|
||||||
color: var(--text-primary);
|
|
||||||
transition: border-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-search-input::placeholder {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-search-input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Device List */
|
|
||||||
.device-list-container {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-list {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-item {
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 12px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-item:hover {
|
|
||||||
background: var(--hover-bg);
|
|
||||||
border-color: var(--accent-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-item.selected {
|
|
||||||
background: var(--accent-primary);
|
|
||||||
color: white;
|
|
||||||
border-color: var(--accent-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-name {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: inherit;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Device Status Dot */
|
|
||||||
.device-status-dot {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-left: 8px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-status-dot.online {
|
|
||||||
background: var(--success);
|
|
||||||
box-shadow: 0 0 6px rgba(76, 175, 80, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-status-dot.offline {
|
|
||||||
background: var(--error);
|
|
||||||
box-shadow: 0 0 6px rgba(244, 67, 54, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-text {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-style: italic;
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main Content Area */
|
|
||||||
.main-content {
|
|
||||||
flex: 1;
|
|
||||||
padding: 24px;
|
|
||||||
overflow-y: auto;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-header {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-header h1 {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--tab-active);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Content Sections */
|
|
||||||
.content-section {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-section h2 {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--tab-active);
|
|
||||||
margin: 0 0 16px 0;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
button {
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
text-transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: var(--accent-primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
|
||||||
background: var(--accent-primary-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success {
|
|
||||||
background: var(--success);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success:hover:not(:disabled) {
|
|
||||||
background: #45a049;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline {
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-primary);
|
|
||||||
border: 1px solid var(--button-outline);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline:hover:not(:disabled) {
|
|
||||||
background: var(--hover-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Connection Status */
|
|
||||||
.connection-status {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-row label {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--text-primary);
|
|
||||||
min-width: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--error);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot.online {
|
|
||||||
background: var(--success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot.offline {
|
|
||||||
background: var(--error);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-text {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Connection Controls */
|
|
||||||
.connection-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Proxy Controls */
|
|
||||||
.proxy-controls {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-row label {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--text-primary);
|
|
||||||
min-width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-row input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 6px 10px;
|
|
||||||
border: 1px solid var(--button-outline);
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
background: var(--input-bg);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-row input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.proxy-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Status Messages */
|
|
||||||
.status-message {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 8px 0;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14px;
|
|
||||||
display: none;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-message.success {
|
|
||||||
background-color: rgba(76, 175, 80, 0.1);
|
|
||||||
color: var(--success);
|
|
||||||
border-color: var(--success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-message.error {
|
|
||||||
background-color: rgba(244, 67, 54, 0.1);
|
|
||||||
color: var(--error);
|
|
||||||
border-color: var(--error);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-message.warning {
|
|
||||||
background-color: rgba(255, 152, 0, 0.1);
|
|
||||||
color: var(--warning);
|
|
||||||
border-color: var(--warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-message.info {
|
|
||||||
background-color: rgba(14, 122, 254, 0.1);
|
|
||||||
color: var(--accent-primary);
|
|
||||||
border-color: var(--accent-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark Scrollbars */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--button-outline);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--hover-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading Animation */
|
|
||||||
@keyframes pulse {
|
|
||||||
0% { opacity: 1; }
|
|
||||||
50% { opacity: 0.6; }
|
|
||||||
100% { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Update Button */
|
|
||||||
.btn-update {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
border: 1px solid var(--button-outline);
|
|
||||||
padding: 6px 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
white-space: nowrap;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-update:hover:not(:disabled) {
|
|
||||||
color: var(--text-primary);
|
|
||||||
border-color: var(--accent-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-update:hover:not(:disabled) .update-icon {
|
|
||||||
animation: spin 0.6s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-update:disabled .update-icon {
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-update.update-available {
|
|
||||||
color: var(--success);
|
|
||||||
border-color: var(--success);
|
|
||||||
box-shadow: 0 0 8px rgba(76, 175, 80, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-update.update-available::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -3px;
|
|
||||||
right: -3px;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
background: var(--success);
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow: 0 0 6px rgba(76, 175, 80, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-icon {
|
|
||||||
font-size: 14px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from { transform: rotate(0deg); }
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Update Modal */
|
|
||||||
.update-modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0, 0, 0, 0.6);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-modal-card {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 480px;
|
|
||||||
max-width: 90%;
|
|
||||||
max-height: 80vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-modal-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px 20px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-modal-header h3 {
|
|
||||||
font-size: 16px;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-modal-close {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0 4px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-modal-close:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-modal-body {
|
|
||||||
padding: 20px;
|
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-modal-message {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin: 0 0 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-modal-notes {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
line-height: 1.5;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 12px;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-modal-notes:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-progress-container {
|
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-progress-track {
|
|
||||||
flex: 1;
|
|
||||||
height: 8px;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
background: var(--accent-primary);
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-progress-text {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
min-width: 36px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-modal-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 16px 20px;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Design */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.devices-sidebar {
|
|
||||||
width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-controls,
|
|
||||||
.proxy-buttons {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Focus States for Accessibility */
|
|
||||||
button:focus,
|
|
||||||
input:focus,
|
|
||||||
select:focus {
|
|
||||||
outline: 2px solid var(--accent-primary);
|
|
||||||
outline-offset: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* High Contrast Support */
|
|
||||||
@media (prefers-contrast: high) {
|
|
||||||
:root {
|
|
||||||
--border: #666666;
|
|
||||||
--text-secondary: #CCCCCC;
|
|
||||||
--button-outline: #777777;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user