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>
157 lines
4.4 KiB
JavaScript
157 lines
4.4 KiB
JavaScript
const $ = (s) => document.querySelector(s);
|
|
const esc = (s) => { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; };
|
|
|
|
let selectedProject = null;
|
|
|
|
function toast(msg, kind = '') {
|
|
const t = $('#toast');
|
|
t.textContent = msg;
|
|
t.className = 'toast show ' + kind;
|
|
setTimeout(() => t.classList.remove('show'), 3500);
|
|
}
|
|
|
|
// --- Path bar (breadcrumbs) ---
|
|
|
|
function renderPathBar(currentPath) {
|
|
const bar = $('#path-bar');
|
|
const segments = currentPath.split('/').filter(Boolean);
|
|
let html = '';
|
|
for (let i = 0; i < segments.length; i++) {
|
|
const fullPath = '/' + segments.slice(0, i + 1).join('/');
|
|
if (i > 0) html += '<span class="path-separator">/</span>';
|
|
html += `<span class="path-segment" data-path="${esc(fullPath)}">${esc(segments[i])}</span>`;
|
|
}
|
|
bar.innerHTML = html;
|
|
bar.querySelectorAll('.path-segment').forEach((el) => {
|
|
el.addEventListener('click', () => browse(el.dataset.path));
|
|
});
|
|
}
|
|
|
|
// --- File list ---
|
|
|
|
function renderFileList(entries) {
|
|
const list = $('#file-list');
|
|
|
|
if (!entries.length) {
|
|
list.innerHTML = '<p style="color:var(--text-muted);padding:8px 0">No Xcode projects or subdirectories found here.</p>';
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = entries.map((e) => {
|
|
let icon, cls;
|
|
if (e.type === 'xcworkspace') {
|
|
icon = '\u{1F4E6}';
|
|
cls = 'xcode-project';
|
|
} else if (e.type === 'xcodeproj') {
|
|
icon = '\u{1F528}';
|
|
cls = 'xcode-project';
|
|
} else {
|
|
icon = '\u{1F4C1}';
|
|
cls = '';
|
|
}
|
|
return `<div class="file-entry ${cls}" data-path="${esc(e.path)}" data-type="${e.type}">
|
|
<span class="icon">${icon}</span>
|
|
<span class="name">${esc(e.name)}</span>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
list.querySelectorAll('.file-entry').forEach((el) => {
|
|
el.addEventListener('click', () => {
|
|
if (el.dataset.type === 'directory') {
|
|
browse(el.dataset.path);
|
|
} else {
|
|
selectProject(el.dataset.path);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- Browse directory ---
|
|
|
|
async function browse(dirPath) {
|
|
const list = $('#file-list');
|
|
list.innerHTML = '<p style="color:var(--text-muted)">Loading...</p>';
|
|
|
|
const url = dirPath
|
|
? `/api/filesystem/browse?path=${encodeURIComponent(dirPath)}`
|
|
: '/api/filesystem/browse';
|
|
|
|
try {
|
|
const r = await fetch(url);
|
|
if (r.status === 401) { location.href = '/login'; return; }
|
|
const data = await r.json();
|
|
if (!r.ok) throw new Error(data.error);
|
|
|
|
renderPathBar(data.path);
|
|
renderFileList(data.entries);
|
|
} catch (err) {
|
|
list.innerHTML = `<p style="color:var(--danger)">${esc(err.message)}</p>`;
|
|
}
|
|
}
|
|
|
|
// --- Select a project ---
|
|
|
|
async function selectProject(projectPath) {
|
|
selectedProject = projectPath;
|
|
const config = $('#project-config');
|
|
config.style.display = 'block';
|
|
$('#selected-project').textContent = projectPath;
|
|
|
|
const select = $('#scheme-select');
|
|
select.disabled = true;
|
|
select.innerHTML = '<option>Loading schemes...</option>';
|
|
$('#build-btn').disabled = true;
|
|
|
|
document.querySelectorAll('.file-entry').forEach((el) => {
|
|
el.classList.toggle('selected', el.dataset.path === projectPath);
|
|
});
|
|
|
|
try {
|
|
const r = await fetch(`/api/filesystem/schemes?projectPath=${encodeURIComponent(projectPath)}`);
|
|
const data = await r.json();
|
|
if (!r.ok) throw new Error(data.error);
|
|
|
|
if (!data.schemes.length) {
|
|
select.innerHTML = '<option>No schemes found</option>';
|
|
return;
|
|
}
|
|
|
|
select.innerHTML = data.schemes.map((s) =>
|
|
`<option value="${esc(s)}">${esc(s)}</option>`
|
|
).join('');
|
|
select.disabled = false;
|
|
$('#build-btn').disabled = false;
|
|
} catch (err) {
|
|
select.innerHTML = `<option>Error: ${err.message}</option>`;
|
|
toast(err.message, 'error');
|
|
}
|
|
}
|
|
|
|
// --- Build button ---
|
|
|
|
$('#build-btn').addEventListener('click', async () => {
|
|
if (!selectedProject) return;
|
|
const scheme = $('#scheme-select').value;
|
|
const btn = $('#build-btn');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Starting build...';
|
|
|
|
try {
|
|
const r = await fetch('/api/build/local', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ projectPath: selectedProject, scheme }),
|
|
});
|
|
const data = await r.json();
|
|
if (!r.ok) throw new Error(data.error || 'Build failed to start');
|
|
location.href = `/#${data.job_id}`;
|
|
} catch (err) {
|
|
toast(err.message, 'error');
|
|
btn.disabled = false;
|
|
btn.textContent = 'Build';
|
|
}
|
|
});
|
|
|
|
// Start browsing at default path
|
|
browse(null);
|