const $ = (sel) => document.querySelector(sel);
const toast = (msg, kind = '') => {
const t = $('#toast');
t.textContent = msg;
t.className = 'toast show ' + kind;
setTimeout(() => t.classList.remove('show'), 3000);
};
const escapeHtml = (str) => (str || '').replace(/[&<>"']/g, (c) => ({
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
}[c]));
async function loadStorefront() {
const res = await fetch('/api/settings');
if (res.status === 401) { location.href = '/login'; return; }
const s = await res.json();
$('[name=unraid_url]').value = s.unraid_url || '';
$('[name=unraid_token]').value = s.unraid_token || '';
}
async function loadKeys() {
const res = await fetch('/api/asc-keys');
if (res.status === 401) { location.href = '/login'; return; }
const keys = await res.json();
const container = $('#asc-keys-table');
if (!keys.length) {
container.innerHTML = '
No developer accounts configured yet.
';
return;
}
container.innerHTML = `
`;
// Wire row actions
container.querySelectorAll('tr[data-team-id]').forEach((row) => {
const teamId = row.dataset.teamId;
row.querySelector('.p8-input').addEventListener('change', (e) => uploadP8(teamId, e.target.files[0]));
row.querySelector('.test-btn').addEventListener('click', () => testKey(teamId));
row.querySelector('.delete-btn').addEventListener('click', () => deleteKey(teamId));
});
}
async function uploadP8(teamId, file) {
if (!file) return;
const fd = new FormData();
fd.append('p8', file);
const res = await fetch(`/api/asc-keys/${encodeURIComponent(teamId)}/p8`, { method: 'POST', body: fd });
const data = await res.json().catch(() => ({}));
if (res.ok) {
toast('.p8 uploaded', 'success');
loadKeys();
} else {
toast(data.error || 'Upload failed', 'error');
}
}
async function testKey(teamId) {
toast('Testing...', '');
const res = await fetch(`/api/asc-keys/${encodeURIComponent(teamId)}/test`, { method: 'POST' });
const data = await res.json().catch(() => ({}));
if (res.ok) toast(`Team ${teamId} authenticated OK`, 'success');
else toast(data.error || 'Test failed', 'error');
}
async function deleteKey(teamId) {
if (!confirm(`Delete ASC key for team ${teamId}? The .p8 file will be removed from disk.`)) return;
const res = await fetch(`/api/asc-keys/${encodeURIComponent(teamId)}`, { method: 'DELETE' });
const data = await res.json().catch(() => ({}));
if (res.ok) {
toast('Deleted', 'success');
loadKeys();
} else {
toast(data.error || 'Delete failed', 'error');
}
}
$('#add-key-form').addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const payload = Object.fromEntries(fd.entries());
const res = await fetch('/api/asc-keys', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await res.json().catch(() => ({}));
if (res.ok) {
toast('Saved', 'success');
e.target.reset();
loadKeys();
} else {
toast(data.error || 'Save failed', 'error');
}
});
$('#storefront-form').addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
unraid_url: e.target.querySelector('[name=unraid_url]').value,
unraid_token: e.target.querySelector('[name=unraid_token]').value,
};
const res = await fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (res.ok) toast('Saved', 'success');
else toast('Save failed', 'error');
});
$('#test-storefront').addEventListener('click', async () => {
const res = await fetch('/api/settings/test-storefront', { method: 'POST' });
const data = await res.json();
if (res.ok) toast(`Connected -- ${data.app_count} apps on storefront`, 'success');
else toast(data.error || 'Connection failed', 'error');
});
loadStorefront();
loadKeys();