fix: bulk challenge enable path (compose YAML, base images, async toggle, tooling)
- fix_dup_volumes.py: 4 canonical templates had TWO volumes: keys inside one service (invalid YAML -> 'mapping key volumes already defined'), which broke every enable for anti-alchemy/burvesigner/gemas-notes/kode-viewer. - fjb: ghcr.io base is not anonymously pullable on this host; swapped to the official httpd:2.4 (its httpd.conf only uses stock modules). Added onlyBuiltDependencies to package.json (pnpm >=10 blocks esbuild's postinstall). - xl + kode-viewer: node:20-slim-bookworm is not a real tag; use node:20-bookworm-slim. gift-voucher: buster -> bookworm. - prebuild_images.py: build each challenge's shared services-<name> image once in parallel (passes a placeholder PASSWORD build-arg, since several Dockerfiles run chpasswd and fail on an empty arg). - set_enabled.py / sync_all_challenges.py: batch registry flip + runtime apply that survives panel restarts and reports per-team results. - Challenge toggle is now async: PATCH returns a job id, the client polls /api/challenges/jobs/<id> so a multi-minute build no longer blocks the panel. Added _SYNC_LOCK to serialize concurrent compose rewrites.
This commit is contained in:
+32
-3
@@ -400,9 +400,38 @@ async function toggleChallenge(name, enabled) {
|
||||
msg.style.color = '#f5c542';
|
||||
try {
|
||||
const d = await api('/api/challenges/' + encodeURIComponent(name), { method: 'PATCH', body: JSON.stringify({ enabled }) });
|
||||
msg.textContent = `✓ ${d.enabled ? 'diaktifkan' : 'dinonaktifkan'} (${(d.applied || []).length} tim)`;
|
||||
msg.style.color = d.enabled ? '#22c55e' : '#e74c3c';
|
||||
setTimeout(() => { msg.textContent = ''; loadChMgr(); }, 4000);
|
||||
if (d.unchanged) {
|
||||
msg.textContent = `✓ ${d.enabled ? 'diaktifkan' : 'dinonaktifkan'} (sudah sesuai)`;
|
||||
msg.style.color = '#22c55e';
|
||||
setTimeout(() => { msg.textContent = ''; loadChMgr(); }, 3000);
|
||||
return;
|
||||
}
|
||||
// Docker build runs on the server; poll the job until it reports per-team results.
|
||||
if (!d.job) throw new Error('server tidak mengembalikan job id');
|
||||
const started = Date.now();
|
||||
const poll = async () => {
|
||||
const j = await api('/api/challenges/jobs/' + encodeURIComponent(d.job));
|
||||
if (j.state === 'running') {
|
||||
const secs = Math.round((Date.now() - started) / 1000);
|
||||
msg.textContent = `⏳ build ${secs}s…`;
|
||||
setTimeout(poll, 3000);
|
||||
return;
|
||||
}
|
||||
if (j.state === 'error') {
|
||||
msg.textContent = '✗ ' + (j.detail || 'gagal');
|
||||
msg.style.color = '#e74c3c';
|
||||
loadChMgr();
|
||||
return;
|
||||
}
|
||||
const okCount = (j.applied || []).filter(t => t.ok).length;
|
||||
const bad = (j.applied || []).filter(t => !t.ok);
|
||||
msg.textContent = bad.length
|
||||
? `⚠ ${okCount}/${(j.applied || []).length} tim OK — ${bad.map(t => `team${t.team}: ${(t.detail || '').split('\n').slice(-1)[0]}`).join('; ').slice(0, 120)}`
|
||||
: `✓ ${d.enabled ? 'diaktifkan' : 'dinonaktifkan'} (${okCount} tim)`;
|
||||
msg.style.color = bad.length ? '#f5c542' : '#22c55e';
|
||||
setTimeout(() => { msg.textContent = ''; loadChMgr(); }, 6000);
|
||||
};
|
||||
poll();
|
||||
} catch (e) {
|
||||
msg.textContent = '✗ ' + e.message;
|
||||
msg.style.color = '#e74c3c';
|
||||
|
||||
Reference in New Issue
Block a user