Rewrites the builder console to browse local Xcode projects instead of accepting source uploads or git URLs. Replaces the devices page with a profiles page that manages ad-hoc provisioning profiles and lists registered bundle IDs per team. Adds multi-account support: ASC API keys are now stored in an asc_keys table keyed by team_id (team_name, key_id, issuer_id, p8_filename). At build time, the worker reads DEVELOPMENT_TEAM from the Xcode project and auto-picks the matching key for fastlane sigh + JWT signing. Legacy single-key settings auto-migrate on first boot. Fixes storefront IPA parser to handle binary plists produced by Xcode. Drops the enrollment bridge, device management routes, and direct ASC API client -- fastlane sigh handles profile lifecycle now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
211 lines
8.1 KiB
JavaScript
211 lines
8.1 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);
|
|
}
|
|
|
|
function formatDate(s) {
|
|
if (!s) return '--';
|
|
return new Date(s).toLocaleDateString();
|
|
}
|
|
|
|
function statusBadge(status) {
|
|
const cls = status === 'valid' ? 'succeeded'
|
|
: status === 'expiring' ? 'pending'
|
|
: status === 'expired' ? 'failed'
|
|
: 'pending';
|
|
return `<span class="badge ${cls}">${status}</span>`;
|
|
}
|
|
|
|
function methodBadge(method) {
|
|
if (method === 'ad-hoc') return '<span class="badge running">ad-hoc</span>';
|
|
if (method === 'development') return '<span class="badge pending">dev</span>';
|
|
if (method === 'app-store') return '<span class="badge succeeded">app-store</span>';
|
|
if (method === 'enterprise') return '<span class="badge succeeded">enterprise</span>';
|
|
return `<span class="badge">${method || 'unknown'}</span>`;
|
|
}
|
|
|
|
// --- Installed profiles ---
|
|
|
|
let installedProfiles = [];
|
|
|
|
async function loadProfiles() {
|
|
const r = await fetch('/api/profiles');
|
|
if (r.status === 401) { location.href = '/login'; return; }
|
|
installedProfiles = await r.json();
|
|
const container = $('#profiles-container');
|
|
|
|
if (!installedProfiles.length) {
|
|
container.innerHTML = '<div class="card"><p style="color:var(--text-muted)">No provisioning profiles installed. Generate one from the bundle IDs above.</p></div>';
|
|
return;
|
|
}
|
|
|
|
// Only show ad-hoc profiles
|
|
const adhoc = installedProfiles.filter(p => p.method === 'ad-hoc');
|
|
if (!adhoc.length) {
|
|
container.innerHTML = '<div class="card"><p style="color:var(--text-muted)">No ad-hoc profiles found. Other profile types are installed but not shown.</p></div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Bundle ID</th>
|
|
<th>Name</th>
|
|
<th>Devices</th>
|
|
<th>Expires</th>
|
|
<th>Status</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${adhoc.map(p => `
|
|
<tr>
|
|
<td class="mono">${esc(p.bundleIdentifier || '--')}</td>
|
|
<td style="font-size:13px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(p.name)}">${esc(p.name || '--')}</td>
|
|
<td class="mono">${p.deviceCount ?? '--'}</td>
|
|
<td class="mono">${esc(formatDate(p.expiresAt))}</td>
|
|
<td>${statusBadge(p.status)}</td>
|
|
<td style="white-space:nowrap">
|
|
${p.bundleIdentifier ? `<button class="btn-sm btn-secondary regen-btn" data-bundle="${esc(p.bundleIdentifier)}" style="margin-right:4px">Regenerate</button>` : ''}
|
|
<button class="btn-sm btn-danger delete-btn" data-uuid="${esc(p.uuid)}">Delete</button>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
|
|
bindProfileActions(container);
|
|
}
|
|
|
|
function bindProfileActions(container) {
|
|
container.querySelectorAll('.delete-btn').forEach((btn) => {
|
|
btn.addEventListener('click', async () => {
|
|
if (!confirm('Delete this profile?')) return;
|
|
btn.disabled = true;
|
|
btn.textContent = '...';
|
|
try {
|
|
const r = await fetch(`/api/profiles/${btn.dataset.uuid}`, { method: 'DELETE' });
|
|
if (!r.ok) throw new Error((await r.json()).error || 'Delete failed');
|
|
toast('Profile deleted', 'success');
|
|
loadProfiles();
|
|
loadBundleIds();
|
|
} catch (err) {
|
|
toast(err.message, 'error');
|
|
btn.disabled = false;
|
|
btn.textContent = 'Delete';
|
|
}
|
|
});
|
|
});
|
|
|
|
container.querySelectorAll('.regen-btn').forEach((btn) => {
|
|
btn.addEventListener('click', () => generateProfile(btn.dataset.bundle, null, btn));
|
|
});
|
|
}
|
|
|
|
async function generateProfile(bundleId, teamId, btn) {
|
|
if (!teamId) {
|
|
// Regenerate button on the installed-profiles table doesn't know the team yet — look it up
|
|
const installed = installedProfiles.find(p => p.bundleIdentifier === bundleId);
|
|
teamId = installed?.teamId;
|
|
}
|
|
if (!teamId) {
|
|
toast('No team ID available for this bundle. Generate from the ASC bundle list above.', 'error');
|
|
return;
|
|
}
|
|
const origText = btn.textContent;
|
|
btn.disabled = true;
|
|
btn.textContent = 'Generating...';
|
|
try {
|
|
const r = await fetch('/api/profiles/generate', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ bundleId, teamId }),
|
|
});
|
|
if (!r.ok) throw new Error((await r.json()).error || 'Generation failed');
|
|
toast(`Profile generated for ${bundleId}`, 'success');
|
|
loadProfiles();
|
|
loadBundleIds();
|
|
} catch (err) {
|
|
toast(err.message, 'error');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.textContent = origText;
|
|
}
|
|
}
|
|
|
|
// --- Bundle IDs from ASC ---
|
|
|
|
async function loadBundleIds() {
|
|
const container = $('#bundle-ids-container');
|
|
try {
|
|
const r = await fetch('/api/bundle-ids');
|
|
if (r.status === 401) { location.href = '/login'; return; }
|
|
const groups = await r.json();
|
|
|
|
if (groups.error) throw new Error(groups.error);
|
|
if (!Array.isArray(groups) || !groups.length) {
|
|
container.innerHTML = '<div class="card"><p style="color:var(--text-muted)">No developer accounts configured. Add one in Settings.</p></div>';
|
|
return;
|
|
}
|
|
|
|
const installedBundles = new Set(
|
|
installedProfiles
|
|
.filter(p => p.method === 'ad-hoc')
|
|
.map(p => p.bundleIdentifier)
|
|
);
|
|
|
|
container.innerHTML = groups.map(g => {
|
|
if (g.error) {
|
|
return `<div class="card" style="margin-bottom:16px">
|
|
<h3 style="margin-bottom:8px">${esc(g.teamName)} <span class="mono" style="color:var(--text-muted);font-size:12px">${esc(g.teamId)}</span></h3>
|
|
<p style="color:var(--danger);font-size:13px">${esc(g.error)}</p>
|
|
</div>`;
|
|
}
|
|
if (!g.bundleIds.length) {
|
|
return `<div class="card" style="margin-bottom:16px">
|
|
<h3 style="margin-bottom:8px">${esc(g.teamName)} <span class="mono" style="color:var(--text-muted);font-size:12px">${esc(g.teamId)}</span></h3>
|
|
<p style="color:var(--text-muted);font-size:13px">No bundle IDs registered.</p>
|
|
</div>`;
|
|
}
|
|
return `<div class="card" style="padding:0;overflow:hidden;margin-bottom:16px">
|
|
<div style="padding:12px 20px;border-bottom:1px solid var(--border);background:var(--bg)">
|
|
<h3 style="font-size:14px">${esc(g.teamName)} <span class="mono" style="color:var(--text-muted);font-size:12px;margin-left:6px">${esc(g.teamId)}</span></h3>
|
|
</div>
|
|
${g.bundleIds.map(b => {
|
|
const hasProfile = installedBundles.has(b.identifier);
|
|
return `<div class="bundle-id-row" style="display:flex;align-items:center;justify-content:space-between;padding:12px 20px;border-bottom:1px solid var(--border)">
|
|
<div>
|
|
<span class="mono" style="font-size:14px">${esc(b.identifier)}</span>
|
|
<span style="color:var(--text-muted);font-size:12px;margin-left:8px">${esc(b.name)}</span>
|
|
<span style="color:var(--text-muted);font-size:11px;margin-left:6px">${esc(b.platform)}</span>
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:8px">
|
|
${hasProfile
|
|
? '<span class="badge succeeded">has profile</span>'
|
|
: `<button class="btn-sm gen-btn" data-bundle="${esc(b.identifier)}" data-team="${esc(g.teamId)}">Generate Ad-Hoc</button>`
|
|
}
|
|
</div>
|
|
</div>`;
|
|
}).join('')}
|
|
</div>`;
|
|
}).join('');
|
|
|
|
container.querySelectorAll('.gen-btn').forEach((btn) => {
|
|
btn.addEventListener('click', () => generateProfile(btn.dataset.bundle, btn.dataset.team, btn));
|
|
});
|
|
|
|
} catch (err) {
|
|
container.innerHTML = `<div class="card"><p style="color:var(--danger)">${esc(err.message)}</p><p style="color:var(--text-muted);font-size:13px;margin-top:8px">Configure at least one Developer Account in Settings to fetch bundle IDs.</p></div>`;
|
|
}
|
|
}
|
|
|
|
// Load both in parallel
|
|
loadProfiles().then(() => loadBundleIds());
|