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:
MythEclipse
2026-09-25 17:01:42 +08:00
parent d4c741926d
commit 6d1ede8c2b
13 changed files with 483 additions and 14 deletions
+32 -3
View File
@@ -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';