Phase 1-3 of the builder subsystem on the Mac mini: - Express + SQLite + sessions scaffolding, LAN-only service on port 3090 - App Store Connect JWT client (ES256 signing, devices/profiles/bundleIds) - Device management UI with Apple-side registration - Fastlane sigh wrapper with profile cache + auto-install to ~/Library/ - launchd plist + deploy script for Mac mini supervision
84 lines
2.7 KiB
JavaScript
84 lines
2.7 KiB
JavaScript
const $ = (s) => document.querySelector(s);
|
|
const esc = (s) => { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; };
|
|
|
|
function toast(msg, kind = '') {
|
|
const t = $('#toast');
|
|
t.textContent = msg;
|
|
t.className = 'toast show ' + kind;
|
|
setTimeout(() => t.classList.remove('show'), 3500);
|
|
}
|
|
|
|
async function load() {
|
|
const res = await fetch('/api/devices');
|
|
if (res.status === 401) { location.href = '/login'; return; }
|
|
const devices = await res.json();
|
|
const container = $('#devices-container');
|
|
|
|
if (!devices.length) {
|
|
container.innerHTML = '<div class="card"><p style="color:var(--text-muted)">No devices registered yet.</p></div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<table class="table">
|
|
<thead>
|
|
<tr><th>Name</th><th>UDID</th><th>Status</th><th>Added</th><th></th></tr>
|
|
</thead>
|
|
<tbody>
|
|
${devices.map(d => `
|
|
<tr>
|
|
<td>${esc(d.name) || '<span class="mono" style="color:var(--text-muted)">unnamed</span>'}</td>
|
|
<td class="mono">${esc(d.udid)}</td>
|
|
<td>${d.synced_at
|
|
? '<span class="badge synced">Synced</span>'
|
|
: '<span class="badge unsynced">Local only</span>'}</td>
|
|
<td class="mono">${esc(d.added_at)}</td>
|
|
<td><button class="delete-btn" data-udid="${esc(d.udid)}" title="Delete">×</button></td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
|
|
container.querySelectorAll('.delete-btn').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
if (!confirm('Remove this device locally? (It will remain in the Apple portal.)')) return;
|
|
const udid = btn.getAttribute('data-udid');
|
|
const r = await fetch(`/api/devices/${udid}`, { method: 'DELETE' });
|
|
if (r.ok) { toast('Removed', 'success'); load(); }
|
|
else toast('Delete failed', 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
$('#add-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
const body = {
|
|
udid: form.udid.value.trim(),
|
|
name: form.name.value.trim(),
|
|
};
|
|
const btn = form.querySelector('button[type=submit]');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Registering…';
|
|
|
|
const r = await fetch('/api/devices', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
const data = await r.json().catch(() => ({}));
|
|
btn.disabled = false;
|
|
btn.textContent = 'Add Device';
|
|
|
|
if (r.ok) {
|
|
toast(data.synced ? 'Registered with Apple' : 'Saved locally (ASC not configured)', 'success');
|
|
form.reset();
|
|
load();
|
|
} else {
|
|
toast(data.error || 'Register failed', 'error');
|
|
}
|
|
});
|
|
|
|
load();
|