feat: publish Alta Local Camera Discovery
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
+153
@@ -0,0 +1,153 @@
|
||||
const scanButton = document.querySelector('#scanButton');
|
||||
const buttonText = document.querySelector('#buttonText');
|
||||
const results = document.querySelector('#results');
|
||||
const empty = document.querySelector('#empty');
|
||||
const errorBox = document.querySelector('#error');
|
||||
const list = document.querySelector('#cameraList');
|
||||
const title = document.querySelector('#resultTitle');
|
||||
const meta = document.querySelector('#scanMeta');
|
||||
const errorText = document.querySelector('#errorText');
|
||||
const exportButton = document.querySelector('#exportCsvButton');
|
||||
const actionStatus = document.querySelector('#actionStatus');
|
||||
let currentCameras = [];
|
||||
|
||||
function hidePanels() {
|
||||
results.hidden = true;
|
||||
empty.hidden = true;
|
||||
errorBox.hidden = true;
|
||||
actionStatus.textContent = '';
|
||||
}
|
||||
|
||||
function cameraCard(camera) {
|
||||
const article = document.createElement('article');
|
||||
article.className = 'camera';
|
||||
|
||||
const details = document.createElement('div');
|
||||
const key = document.createElement('div');
|
||||
key.className = 'camera-key';
|
||||
key.textContent = camera.alta_key;
|
||||
const host = document.createElement('a');
|
||||
host.className = 'camera-host';
|
||||
host.href = camera.web_ui_url;
|
||||
host.target = '_blank';
|
||||
host.rel = 'noopener noreferrer';
|
||||
host.textContent = camera.hostname;
|
||||
host.title = 'Open camera Web UI';
|
||||
details.append(key, host);
|
||||
|
||||
const network = document.createElement('div');
|
||||
network.className = 'network-data';
|
||||
const ip = document.createElement('span');
|
||||
ip.className = 'camera-ip';
|
||||
ip.textContent = camera.ip_address;
|
||||
const networkLabel = document.createElement('span');
|
||||
networkLabel.className = 'network-label';
|
||||
networkLabel.textContent = 'HTTPS · mDNS';
|
||||
network.append(ip, networkLabel);
|
||||
|
||||
const copy = document.createElement('button');
|
||||
copy.className = 'copy-button';
|
||||
copy.type = 'button';
|
||||
copy.dataset.altaKey = camera.alta_key;
|
||||
copy.textContent = 'Copy key';
|
||||
copy.setAttribute('aria-label', `Copy Alta Key ${camera.alta_key}`);
|
||||
|
||||
article.append(details, network, copy);
|
||||
return article;
|
||||
}
|
||||
|
||||
async function copyText(text) {
|
||||
if (window.altaDiscovery) {
|
||||
await window.altaDiscovery.copyText(text);
|
||||
return;
|
||||
}
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.setAttribute('readonly', '');
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.append(textarea);
|
||||
textarea.select();
|
||||
const copied = document.execCommand('copy');
|
||||
textarea.remove();
|
||||
if (!copied) throw new Error('Clipboard access was blocked by the browser.');
|
||||
}
|
||||
|
||||
list.addEventListener('click', async (event) => {
|
||||
const copyButton = event.target.closest('.copy-button');
|
||||
if (!copyButton) return;
|
||||
try {
|
||||
await copyText(copyButton.dataset.altaKey);
|
||||
copyButton.textContent = 'Copied';
|
||||
copyButton.classList.add('copied');
|
||||
actionStatus.textContent = `${copyButton.dataset.altaKey} copied to clipboard.`;
|
||||
window.setTimeout(() => {
|
||||
copyButton.textContent = 'Copy key';
|
||||
copyButton.classList.remove('copied');
|
||||
}, 1800);
|
||||
} catch (error) {
|
||||
actionStatus.textContent = error.message;
|
||||
}
|
||||
});
|
||||
|
||||
exportButton.addEventListener('click', async () => {
|
||||
if (window.altaDiscovery) {
|
||||
try {
|
||||
const result = await window.altaDiscovery.exportCsv();
|
||||
actionStatus.textContent = result.canceled ? 'CSV export canceled.' : 'CSV exported successfully.';
|
||||
} catch (error) {
|
||||
actionStatus.textContent = error.message;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const csv = AltaExportUtils.buildCsv(currentCameras);
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'alta-camera-keys.csv';
|
||||
document.body.append(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
actionStatus.textContent = 'CSV exported with Alta Keys and camera Web UI URLs.';
|
||||
});
|
||||
|
||||
scanButton.addEventListener('click', async () => {
|
||||
hidePanels();
|
||||
currentCameras = [];
|
||||
scanButton.disabled = true;
|
||||
scanButton.classList.add('scanning');
|
||||
buttonText.textContent = 'Scanning…';
|
||||
try {
|
||||
let data;
|
||||
if (window.altaDiscovery) {
|
||||
data = await window.altaDiscovery.scan();
|
||||
} else {
|
||||
const response = await fetch('/api/scan', { cache: 'no-store' });
|
||||
data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || 'The scanner returned an error.');
|
||||
}
|
||||
if (!data.ok) throw new Error(data.error || 'The scanner returned an error.');
|
||||
currentCameras = data.cameras;
|
||||
if (!currentCameras.length) {
|
||||
empty.hidden = false;
|
||||
} else {
|
||||
list.replaceChildren(...currentCameras.map(cameraCard));
|
||||
title.textContent = `${data.count} camera${data.count === 1 ? '' : 's'} found`;
|
||||
meta.textContent = `Scan completed in ${data.duration_seconds}s`;
|
||||
results.hidden = false;
|
||||
}
|
||||
} catch (error) {
|
||||
errorText.textContent = error.message;
|
||||
errorBox.hidden = false;
|
||||
} finally {
|
||||
scanButton.disabled = false;
|
||||
scanButton.classList.remove('scanning');
|
||||
buttonText.textContent = 'Discover again';
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
(function(root,factory){const api=factory();if(typeof module==='object'&&module.exports)module.exports=api;else root.AltaExportUtils=api;})(typeof globalThis!=='undefined'?globalThis:this,function(){
|
||||
function csvCell(value){const text=String(value??'');return /[",\r\n]/.test(text)?`"${text.replaceAll('"','""')}"`:text}
|
||||
function buildCsv(cameras){const rows=[['Alta Key','IP Address','Hostname URL'],...cameras.map(camera=>[camera.alta_key,camera.ip_address,camera.web_ui_url])];return rows.map(row=>row.map(csvCell).join(',')).join('\r\n')+'\r\n'}
|
||||
return{buildCsv};
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'">
|
||||
<title>Local Alta Camera Discovery</title>
|
||||
<link rel="stylesheet" href="styles.css?v=13">
|
||||
<script src="export-utils.js?v=13" defer></script>
|
||||
<script src="app.js?v=13" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div class="product-brand">
|
||||
<img class="alta-logo" src="alta-mark.png" alt="Alta">
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="app-shell">
|
||||
<section class="page-heading">
|
||||
<h1>Local Alta Camera Discovery</h1>
|
||||
<p>Locate cloud-native Alta cameras available on this network and recover the information required for deployment.</p>
|
||||
</section>
|
||||
|
||||
<section class="discovery-panel" aria-labelledby="scan-title">
|
||||
<div class="discovery-copy">
|
||||
<span class="network-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 64 64" focusable="false">
|
||||
<circle class="radar-field" cx="32" cy="32" r="28"></circle>
|
||||
<path class="radar-sector" d="M32 32V4A28 28 0 0 1 54.7 48.4Z"></path>
|
||||
<circle class="radar-ring" cx="32" cy="32" r="20.5"></circle>
|
||||
<circle class="radar-ring" cx="32" cy="32" r="13"></circle>
|
||||
<circle class="radar-ring" cx="32" cy="32" r="6.2"></circle>
|
||||
<path class="radar-sweep" d="M32 32V4M32 32l22.7 16.4"></path>
|
||||
<circle class="radar-target" cx="23.3" cy="13.4" r="3.8"></circle>
|
||||
<circle class="radar-target" cx="40" cy="21.8" r="3.8"></circle>
|
||||
<circle class="radar-target" cx="52.3" cy="34.8" r="3.8"></circle>
|
||||
<circle class="radar-target" cx="16.3" cy="45.2" r="3.8"></circle>
|
||||
</svg>
|
||||
</span>
|
||||
<div>
|
||||
<h2 id="scan-title">Discover cameras on this network</h2>
|
||||
<p>The scan uses local mDNS announcements and does not access or modify camera configuration.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scan-action">
|
||||
<button id="scanButton" class="primary-button" type="button"><span class="radar"></span><span id="buttonText">Start discovery</span></button>
|
||||
<span>Usually completes in 5 seconds</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="results" class="results-panel" hidden>
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<p class="section-label">DISCOVERY RESULTS</p>
|
||||
<h2 id="resultTitle">Cameras found</h2>
|
||||
<span id="scanMeta"></span>
|
||||
</div>
|
||||
<button id="exportCsvButton" class="secondary-button" type="button">Export CSV</button>
|
||||
</div>
|
||||
<div class="table-head" aria-hidden="true">
|
||||
<span>Alta camera</span><span>Network address</span><span>Action</span>
|
||||
</div>
|
||||
<div id="cameraList" class="camera-list"></div>
|
||||
<div class="panel-footer"><span id="actionStatus" role="status" aria-live="polite"></span></div>
|
||||
</section>
|
||||
|
||||
<section id="empty" class="state-panel" hidden>
|
||||
<h2>No Alta cameras found</h2>
|
||||
<p>Confirm the cameras are powered and connected to the same local network or VLAN as this device.</p>
|
||||
</section>
|
||||
|
||||
<section id="error" class="state-panel error" hidden>
|
||||
<h2>Discovery could not be completed</h2><p id="errorText"></p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="footer"><span>Alta Video Local Camera Discovery</span></footer>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user