Add builder service: scaffold, ASC API, devices UI, fastlane profile manager

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
This commit is contained in:
trey
2026-04-11 13:28:01 -05:00
parent a1e60d390d
commit e9b6936904
19 changed files with 1666 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
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">&times;</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();