Full-stack implementation: FastAPI backend with PAM auth, WebSocket stats/terminal, and vanilla JS frontend with tiling desktop shell. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
176 lines
8.1 KiB
JavaScript
176 lines
8.1 KiB
JavaScript
/* Atlus — ASI Bridge plugin app */
|
|
(function () {
|
|
'use strict';
|
|
|
|
let container = null;
|
|
let ws = null;
|
|
let statusEl = null;
|
|
let filesEl = null;
|
|
|
|
async function loadStatus() {
|
|
try {
|
|
const res = await Atlus.apiFetch('/api/plugins/asi-bridge/status');
|
|
if (!res.ok) return;
|
|
const data = await res.json();
|
|
renderStatus(data);
|
|
} catch (e) {}
|
|
}
|
|
|
|
function renderStatus(data) {
|
|
if (!statusEl) return;
|
|
|
|
const mountClass = data.mounted ? 'var(--status-green)' : 'var(--status-red)';
|
|
const mountText = data.mounted ? 'Connected' : 'Disconnected';
|
|
|
|
statusEl.innerHTML = `
|
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:24px;">
|
|
<div style="width:12px;height:12px;border-radius:50%;background:${mountClass};"></div>
|
|
<div>
|
|
<div style="font-family:var(--font-ui);font-size:16px;font-weight:500;color:var(--text-primary);">ASI Air</div>
|
|
<div style="font-family:var(--font-mono);font-size:12px;color:var(--text-secondary);">${mountText}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:24px;">
|
|
<div style="background:var(--bg-titlebar);border:1px solid var(--border-structural);border-radius:var(--radius-md);padding:16px;">
|
|
<div style="font-family:var(--font-mono);font-size:10px;color:var(--text-muted);letter-spacing:1px;margin-bottom:4px;">FITS FILES</div>
|
|
<div style="font-family:var(--font-mono);font-size:24px;font-weight:500;color:var(--text-primary);">${data.fits_count}</div>
|
|
</div>
|
|
<div style="background:var(--bg-titlebar);border:1px solid var(--border-structural);border-radius:var(--radius-md);padding:16px;">
|
|
<div style="font-family:var(--font-mono);font-size:10px;color:var(--text-muted);letter-spacing:1px;margin-bottom:4px;">TOTAL SIZE</div>
|
|
<div style="font-family:var(--font-mono);font-size:24px;font-weight:500;color:var(--text-primary);">${Atlus.formatBytes(data.total_size)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="font-family:var(--font-mono);font-size:11px;color:var(--text-muted);margin-bottom:8px;">
|
|
Share: ${data.cifs_share || 'Not configured'}<br>
|
|
Mount: ${data.mount_point}
|
|
</div>
|
|
|
|
<div style="display:flex;gap:8px;margin-top:16px;">
|
|
<button class="settings-btn ${data.mounted ? 'secondary' : ''}" id="asiBridgeMount">
|
|
${data.mounted ? 'Unmount' : 'Mount'}
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
const mountBtn = statusEl.querySelector('#asiBridgeMount');
|
|
mountBtn.addEventListener('click', async () => {
|
|
const endpoint = data.mounted ? '/api/plugins/asi-bridge/unmount' : '/api/plugins/asi-bridge/mount';
|
|
await Atlus.apiFetch(endpoint, { method: 'POST' });
|
|
loadStatus();
|
|
loadFiles();
|
|
});
|
|
|
|
// Latest file
|
|
if (data.latest_file) {
|
|
const latest = document.createElement('div');
|
|
latest.style.cssText = 'margin-top:16px;padding:12px;background:var(--bg-titlebar);border:1px solid var(--border-structural);border-radius:var(--radius-md);';
|
|
latest.innerHTML = `
|
|
<div style="font-family:var(--font-mono);font-size:10px;color:var(--text-muted);letter-spacing:1px;margin-bottom:4px;">LATEST CAPTURE</div>
|
|
<div style="font-family:var(--font-mono);font-size:13px;color:var(--text-primary);">${data.latest_file.name}</div>
|
|
<div style="font-family:var(--font-mono);font-size:11px;color:var(--text-secondary);margin-top:4px;">
|
|
${Atlus.formatBytes(data.latest_file.size)} ·
|
|
${new Date(data.latest_file.modified * 1000).toLocaleString()}
|
|
</div>
|
|
`;
|
|
statusEl.appendChild(latest);
|
|
}
|
|
}
|
|
|
|
async function loadFiles() {
|
|
if (!filesEl) return;
|
|
try {
|
|
const res = await Atlus.apiFetch('/api/plugins/asi-bridge/files');
|
|
if (!res.ok) {
|
|
filesEl.innerHTML = '<div style="color:var(--text-muted);font-family:var(--font-mono);font-size:12px;padding:16px;">Share not mounted.</div>';
|
|
return;
|
|
}
|
|
const files = await res.json();
|
|
renderFiles(files);
|
|
} catch (e) {
|
|
filesEl.innerHTML = '<div style="color:var(--text-muted);font-family:var(--font-mono);font-size:12px;padding:16px;">Unable to load files.</div>';
|
|
}
|
|
}
|
|
|
|
function renderFiles(files) {
|
|
if (!filesEl) return;
|
|
filesEl.innerHTML = '';
|
|
|
|
if (files.length === 0) {
|
|
filesEl.innerHTML = '<div style="color:var(--text-muted);font-family:var(--font-mono);font-size:12px;padding:16px;">No FITS files found.</div>';
|
|
return;
|
|
}
|
|
|
|
const header = document.createElement('div');
|
|
header.style.cssText = 'display:grid;grid-template-columns:1fr 80px 140px;padding:0 16px;height:32px;align-items:center;font-family:var(--font-mono);font-size:10px;color:var(--text-muted);letter-spacing:0.5px;border-bottom:1px solid var(--border-structural);';
|
|
header.innerHTML = '<span>Name</span><span>Size</span><span>Modified</span>';
|
|
filesEl.appendChild(header);
|
|
|
|
files.slice(0, 100).forEach(file => {
|
|
const row = document.createElement('div');
|
|
row.style.cssText = 'display:grid;grid-template-columns:1fr 80px 140px;padding:0 16px;min-height:44px;align-items:center;border-bottom:1px solid var(--border-structural);font-family:var(--font-mono);font-size:12px;';
|
|
row.innerHTML = `
|
|
<span style="color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${file.relative}">${file.name}</span>
|
|
<span style="color:var(--text-secondary);">${Atlus.formatBytes(file.size)}</span>
|
|
<span style="color:var(--text-secondary);">${new Date(file.modified * 1000).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false })}</span>
|
|
`;
|
|
filesEl.appendChild(row);
|
|
});
|
|
}
|
|
|
|
function connectWs() {
|
|
ws = new WebSocket(Atlus.wsUrl('/api/plugins/asi-bridge/ws'));
|
|
ws.onmessage = (e) => {
|
|
const msg = JSON.parse(e.data);
|
|
if (msg.type === 'mount_status') {
|
|
loadStatus();
|
|
} else if (msg.type === 'new_file') {
|
|
loadFiles();
|
|
}
|
|
};
|
|
ws.onclose = () => {
|
|
setTimeout(connectWs, 5000);
|
|
};
|
|
}
|
|
|
|
Atlus.registerApp('asi-bridge', {
|
|
title: 'ASI Bridge',
|
|
|
|
init(el) {
|
|
container = el;
|
|
container.style.display = 'flex';
|
|
container.style.flexDirection = 'column';
|
|
container.style.height = '100%';
|
|
container.style.overflow = 'auto';
|
|
|
|
// Status section
|
|
statusEl = document.createElement('div');
|
|
statusEl.style.padding = '24px';
|
|
statusEl.style.borderBottom = '1px solid var(--border-structural)';
|
|
container.appendChild(statusEl);
|
|
|
|
// Files section
|
|
const filesHeader = document.createElement('div');
|
|
filesHeader.style.cssText = 'padding:16px 24px 8px;font-family:var(--font-mono);font-size:10px;font-weight:500;color:var(--text-muted);letter-spacing:1.5px;';
|
|
filesHeader.textContent = 'CAPTURES';
|
|
container.appendChild(filesHeader);
|
|
|
|
filesEl = document.createElement('div');
|
|
filesEl.style.flex = '1';
|
|
filesEl.style.overflow = 'auto';
|
|
container.appendChild(filesEl);
|
|
|
|
loadStatus();
|
|
loadFiles();
|
|
connectWs();
|
|
},
|
|
|
|
destroy() {
|
|
if (ws) { ws.close(); ws = null; }
|
|
container = null;
|
|
statusEl = null;
|
|
filesEl = null;
|
|
},
|
|
});
|
|
})();
|