Builder v2: local project browser + multi-team ASC keys
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>
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
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;
|
||||
@@ -6,46 +10,147 @@ function toast(msg, kind = '') {
|
||||
setTimeout(() => t.classList.remove('show'), 3500);
|
||||
}
|
||||
|
||||
$('#upload-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const fd = new FormData(e.target);
|
||||
const btn = e.target.querySelector('button[type=submit]');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Uploading…';
|
||||
// --- 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('/api/build/upload', { method: 'POST', body: fd });
|
||||
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 || 'Upload failed');
|
||||
location.href = `/builds#${data.job_id}`;
|
||||
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 = 'Queue Build';
|
||||
btn.textContent = 'Build';
|
||||
}
|
||||
});
|
||||
|
||||
$('#git-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const body = {
|
||||
url: e.target.url.value.trim(),
|
||||
branch: e.target.branch.value.trim() || null,
|
||||
scheme: e.target.scheme.value.trim() || null,
|
||||
};
|
||||
const btn = e.target.querySelector('button[type=submit]');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Cloning…';
|
||||
try {
|
||||
const r = await fetch('/api/build/git', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await r.json();
|
||||
if (!r.ok) throw new Error(data.error || 'Clone failed');
|
||||
location.href = `/builds#${data.job_id}`;
|
||||
} catch (err) {
|
||||
toast(err.message, 'error');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Queue Build';
|
||||
}
|
||||
});
|
||||
// Start browsing at default path
|
||||
browse(null);
|
||||
|
||||
Reference in New Issue
Block a user