Fix: log viewer wipes itself due to auto-reload on deploy done

Two bugs causing 'can't see why deploy failed':
- showLog() called window.location.reload() on the SSE 'done' event,
  wiping the log panel before the user could read it.
- For already-finished deploys, SSE would immediately fire 'done' and
  reload, showing logs for < 1 second.

Fix:
- showLog() now fetches the deploy via REST first. If done, it renders
  the stored log directly (no SSE). If still running, it streams via
  SSE and closes without reloading when done.
- Added onerror fallback: re-fetches the log via REST if SSE drops.
- Status badge (green/red) updates inline instead of triggering reload.
- Page now auto-opens the latest deploy log on load so the failure
  reason is visible immediately without any clicking.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
This commit is contained in:
Claude 2026-03-19 08:57:01 +00:00
parent 0180f37c31
commit d322cc3ce1
No known key found for this signature in database

View file

@ -213,6 +213,8 @@ pub async fn app_detail(
.await .await
.unwrap_or_default(); .unwrap_or_default();
let latest_deploy_id = deploys.first().map(|d| d.id.as_str()).unwrap_or("");
let mut deploy_rows = String::new(); let mut deploy_rows = String::new();
for d in &deploys { for d in &deploys {
let sha_short = d.sha.as_deref() let sha_short = d.sha.as_deref()
@ -268,7 +270,7 @@ pub async fn app_detail(
<tbody>{deploy_rows}</tbody> <tbody>{deploy_rows}</tbody>
</table> </table>
<div id="log-panel" style="display:none;margin-top:16px"> <div id="log-panel" style="display:none;margin-top:16px">
<h2 style="margin-bottom:8px">Build Log</h2> <h2 id="log-title" style="margin-bottom:8px">Build Log</h2>
<pre id="log-out"></pre> <pre id="log-out"></pre>
</div> </div>
</div> </div>
@ -299,20 +301,64 @@ pub async fn app_detail(
<script> <script>
const APP_ID = '{app_id}'; const APP_ID = '{app_id}';
// Auto-open the latest deploy log on page load.
window.addEventListener('DOMContentLoaded', () => {{
const latest = '{latest_deploy_id}';
if (latest) showLog(latest);
}});
async function deploy() {{ async function deploy() {{
const r = await fetch('/api/apps/' + APP_ID + '/deploy', {{method:'POST'}}); const r = await fetch('/api/apps/' + APP_ID + '/deploy', {{method:'POST'}});
if (r.ok) {{ const d = await r.json(); showLog(d.id); }} if (r.ok) {{ const d = await r.json(); showLog(d.id); }}
else alert('Deploy failed: ' + await r.text()); else alert('Deploy failed: ' + await r.text());
}} }}
function showLog(deployId) {{ async function showLog(deployId) {{
const panel = document.getElementById('log-panel'); const panel = document.getElementById('log-panel');
const out = document.getElementById('log-out'); const out = document.getElementById('log-out');
const title = document.getElementById('log-title');
panel.style.display = 'block'; panel.style.display = 'block';
out.textContent = ''; out.textContent = 'Loading';
panel.scrollIntoView({{behavior:'smooth'}}); panel.scrollIntoView({{behavior:'smooth'}});
// Fetch current deploy state first.
const r = await fetch('/api/deploys/' + deployId);
if (!r.ok) {{ out.textContent = 'Could not load deploy ' + deployId; return; }}
const deploy = await r.json();
title.textContent = 'Build Log ' + deploy.status;
title.style.color = deploy.status === 'success' ? '#4ade80'
: deploy.status === 'failed' ? '#f87171' : '#fb923c';
// Already finished — just render the stored log, no SSE needed.
if (deploy.status === 'success' || deploy.status === 'failed') {{
out.textContent = deploy.log || '(no output captured)';
out.scrollTop = out.scrollHeight;
return;
}}
// Still running — stream updates via SSE.
out.textContent = '';
const es = new EventSource('/api/deploys/' + deployId + '/logs'); const es = new EventSource('/api/deploys/' + deployId + '/logs');
es.onmessage = e => {{ out.textContent += e.data; out.scrollTop = out.scrollHeight; }}; es.onmessage = e => {{
es.addEventListener('done', () => {{ es.close(); window.location.reload(); }}); out.textContent += e.data;
out.scrollTop = out.scrollHeight;
}};
es.addEventListener('done', e => {{
es.close();
title.textContent = 'Build Log ' + e.data;
title.style.color = e.data === 'success' ? '#4ade80' : '#f87171';
}});
es.onerror = () => {{
es.close();
// Fallback: re-fetch the finished log.
fetch('/api/deploys/' + deployId)
.then(r => r.json())
.then(d => {{
out.textContent = d.log || out.textContent;
title.textContent = 'Build Log ' + d.status;
}});
}};
}} }}
async function setEnv() {{ async function setEnv() {{
const key = document.getElementById('ev-key').value.trim(); const key = document.getElementById('ev-key').value.trim();
@ -340,6 +386,7 @@ pub async fn app_detail(
secret = app.webhook_secret, secret = app.webhook_secret,
deploy_rows = deploy_rows, deploy_rows = deploy_rows,
env_rows = env_rows, env_rows = env_rows,
latest_deploy_id = latest_deploy_id,
); );
Ok(Html(page(&app.name, &body))) Ok(Html(page(&app.name, &body)))