(self, parsed)
| 1152 | return |
| 1153 | try: |
| 1154 | step = int(step_raw) |
| 1155 | except Exception: |
| 1156 | self._send_html(400, "invalid step") |
| 1157 | return |
| 1158 | if step <= 0: |
| 1159 | self._send_html(400, "step must be >= 1") |
| 1160 | return |
| 1161 | |
| 1162 | rid_html = _html_escape(rid) |
| 1163 | step_html = _html_escape(str(step)) |
| 1164 | |
| 1165 | html = """<!doctype html> |
| 1166 | <html><head><meta charset='utf-8'/> |
| 1167 | <meta name='viewport' content='width=device-width, initial-scale=1'/> |
| 1168 | <title>Step Log @@RID@@ step @@STEP@@</title> |
| 1169 | <style> |
| 1170 | body{font-family:ui-sans-serif,system-ui,Segoe UI,Roboto,Helvetica,Arial;max-width:1400px;margin:16px auto;padding:0 12px;} |
| 1171 | code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;} |
| 1172 | #log{white-space:pre;background:#0b1020;color:#e5e7eb;padding:12px;border-radius:8px;height:78vh;overflow:auto;font-size:12px;line-height:1.35;tab-size:4;} |
| 1173 | .small{color:#6b7280;font-size:12px;} |
| 1174 | .btn{padding:4px 8px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;cursor:pointer;} |
| 1175 | .row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;} |
| 1176 | </style> |
| 1177 | </head><body> |
| 1178 | <a href='/run?id=@@RID@@'>back</a> |
| 1179 | <h2>step_@@STEP@@.log tail</h2> |
| 1180 | <div class='row'> |
| 1181 | <div class='small'>run_id: <code>@@RID@@</code></div> |
| 1182 | <div class='small'>step: <code>@@STEP@@</code></div> |
| 1183 | <button class='btn' id='btnFollow'>follow: on</button> |
| 1184 | <button class='btn' id='btnReload'>reload tail</button> |
| 1185 | <div class='small' id='status'></div> |
| 1186 | </div> |
| 1187 | <div id='log'></div> |
| 1188 | <script> |
| 1189 | (function(){ |
| 1190 | const runId = "@@RID@@"; |
| 1191 | const step = "@@STEP@@"; |
| 1192 | const logEl = document.getElementById('log'); |
| 1193 | const statusEl = document.getElementById('status'); |
| 1194 | const btnFollow = document.getElementById('btnFollow'); |
| 1195 | const btnReload = document.getElementById('btnReload'); |
| 1196 | |
| 1197 | let follow = true; |
| 1198 | let loading = false; |
| 1199 | let loadedStart = null; |
| 1200 | let loadedEnd = null; |
| 1201 | |
| 1202 | const MAX_BYTES = 65536; |
| 1203 | const POLL_MS = 800; |
| 1204 | |
| 1205 | function setStatus(s){ statusEl.textContent = s; } |
| 1206 | function nearBottom(){ return (logEl.scrollHeight - (logEl.scrollTop + logEl.clientHeight)) < 40; } |
| 1207 | function scrollToBottom(){ logEl.scrollTop = logEl.scrollHeight; } |
| 1208 | |
| 1209 | async function fetchChunk(params){ |
| 1210 | const url = new URL('/api/log_chunk', window.location.origin); |
| 1211 | url.searchParams.set('id', runId); |
no test coverage detected