Files
lambda_local_runner/docs/index.html

891 lines
27 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AWS Lambda — Study notes &amp; sandbox</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0a0e17;
color: #e8eaf0;
font-family: 'Inter', sans-serif;
line-height: 1.6;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
header {
padding: 16px 24px;
border-bottom: 1px solid #1e2a4a;
display: flex;
align-items: baseline;
gap: 16px;
flex-shrink: 0;
}
header h1 {
font-family: 'JetBrains Mono', monospace;
font-size: 22px;
font-weight: 600;
letter-spacing: 3px;
color: #0066ff;
}
header .subtitle {
font-size: 13px;
color: #4a5568;
letter-spacing: 1px;
text-transform: uppercase;
}
.layout {
display: flex;
flex: 1;
min-height: 0;
}
nav {
display: flex;
flex-direction: column;
gap: 0;
width: 220px;
flex-shrink: 0;
background: #121829;
border-right: 1px solid #1e2a4a;
padding: 8px 0;
overflow-y: auto;
scrollbar-width: none;
}
nav::-webkit-scrollbar { display: none; }
nav a {
padding: 10px 20px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: #8892a8;
text-decoration: none;
border-left: 2px solid transparent;
transition: all 0.15s;
cursor: pointer;
}
nav a:hover { color: #e8eaf0; background: #1a2340; }
nav a.active { color: #0066ff; border-left-color: #0066ff; background: #0d1a33; }
nav .nav-group {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
color: #4a5568;
letter-spacing: 1.5px;
text-transform: uppercase;
padding: 14px 20px 6px;
pointer-events: none;
}
main {
flex: 1;
overflow: auto;
padding: 32px 48px;
}
.graph-section {
display: none;
animation: fadeIn 0.2s ease;
}
.graph-section.active { display: block; }
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.graph-section h2 {
font-family: 'JetBrains Mono', monospace;
font-size: 15px;
font-weight: 500;
color: #8892a8;
margin-bottom: 8px;
letter-spacing: 1px;
}
.graph-section p.lead {
font-size: 13px;
color: #4a5568;
margin-bottom: 24px;
max-width: 800px;
}
.graph-container {
background: #0a0e17;
border: 1px solid #1e2a4a;
padding: 24px;
overflow: auto;
}
.graph-container img {
max-width: 100%;
height: auto;
}
.legend {
display: flex;
gap: 24px;
margin-top: 16px;
font-size: 11px;
font-family: 'JetBrains Mono', monospace;
color: #4a5568;
}
.legend span::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
margin-right: 6px;
border-radius: 50%;
}
.legend .live::before { background: #00c853; }
.legend .mock::before { background: #ffc107; }
.legend .mcp::before { background: #0066ff; }
.legend .ops::before { background: #ff3d00; }
.graph-container a { display: block; }
/* Tree (repo structure) */
.tree-container {
background: #0a0e17;
border: 1px solid #1e2a4a;
padding: 24px;
overflow: auto;
}
.repo-tree {
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
line-height: 1.7;
color: #8892a8;
}
.t-root { color: #0066ff; font-weight: 600; font-size: 15px; }
.t-dir { color: #e8eaf0; font-weight: 500; }
.t-comment { color: #4a5568; }
/* Prose sections */
.graph-section h3 {
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
font-weight: 500;
color: #e8eaf0;
letter-spacing: 1px;
margin: 32px 0 10px;
text-transform: uppercase;
}
.graph-section h3:first-child { margin-top: 0; }
.prose { max-width: 820px; }
.prose p {
font-size: 14px;
color: #b4bccf;
margin-bottom: 14px;
line-height: 1.7;
}
.prose p b { color: #e8eaf0; font-weight: 600; }
.prose code {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: #7ab0ff;
background: #121829;
padding: 1px 5px;
border-radius: 3px;
}
.prose a { color: #0066ff; text-decoration: none; }
.prose a:hover { text-decoration: underline; }
.prose ul, .prose ol {
margin: 8px 0 16px 22px;
font-size: 14px;
color: #b4bccf;
line-height: 1.7;
}
.prose ul li, .prose ol li { margin-bottom: 8px; }
.prose ul li b, .prose ol li b { color: #e8eaf0; font-weight: 600; }
/* Pre / code blocks */
.prose pre {
background: #121829;
border: 1px solid #1e2a4a;
padding: 14px 16px;
border-radius: 4px;
overflow-x: auto;
margin: 12px 0 18px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: #b4bccf;
line-height: 1.6;
}
.prose pre code { background: transparent; padding: 0; color: inherit; }
/* Tables */
.cmp-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
margin: 8px 0 20px;
border: 1px solid #1e2a4a;
}
.cmp-table th {
text-align: left;
background: #121829;
color: #8892a8;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
letter-spacing: 1px;
padding: 10px 14px;
border-bottom: 1px solid #1e2a4a;
}
.cmp-table td {
padding: 10px 14px;
color: #b4bccf;
border-bottom: 1px solid #1e2a4a;
vertical-align: top;
}
.cmp-table td.num {
font-family: 'JetBrains Mono', monospace;
color: #7ab0ff;
white-space: nowrap;
}
.cmp-table td.warn { color: #ffc107; }
.cmp-table td.bad { color: #ff3d00; }
.cmp-table td.ok { color: #00c853; }
.cmp-table tr:last-child td { border-bottom: none; }
/* Callouts */
.callout {
border-left: 3px solid #0066ff;
background: #0d1a33;
padding: 12px 16px;
margin: 16px 0;
font-size: 13px;
color: #b4bccf;
border-radius: 0 4px 4px 0;
}
.callout.warn { border-left-color: #ffc107; background: #2a1f0a; }
.callout.bad { border-left-color: #ff3d00; background: #2a0f0a; }
.callout.ok { border-left-color: #00c853; background: #0a2410; }
.callout b { color: #e8eaf0; }
.placeholder {
color: #4a5568;
font-style: italic;
font-size: 13px;
border: 1px dashed #1e2a4a;
padding: 32px;
text-align: center;
border-radius: 4px;
}
/* Mobile menu toggle */
.menu-toggle {
display: none;
background: transparent;
border: 1px solid #1e2a4a;
color: #e8eaf0;
padding: 6px 10px;
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
cursor: pointer;
line-height: 1;
margin-left: auto;
}
.menu-toggle:hover { background: #1a2340; }
.nav-backdrop {
display: none;
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 10;
}
.layout.nav-open .nav-backdrop { display: block; }
@media (max-width: 720px) {
header { padding: 10px 12px; gap: 8px; }
header h1 { font-size: 16px; letter-spacing: 1px; }
header .subtitle { display: none; }
.menu-toggle { display: inline-block; }
.layout { position: relative; }
nav {
position: absolute;
left: 0; top: 0; bottom: 0;
width: 240px;
z-index: 20;
transform: translateX(-100%);
transition: transform 0.2s ease;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.5);
}
.layout.nav-open nav { transform: translateX(0); }
main { padding: 16px; }
.graph-section h2 { font-size: 13px; }
.prose p, .prose ul, .prose ol { font-size: 13px; }
.cmp-table { font-size: 12px; }
.cmp-table th, .cmp-table td { padding: 6px 8px; }
}
/* ── Lambda Tester ─────────────────────────────────────── */
.tester-form {
display: grid;
grid-template-columns: max-content 1fr;
column-gap: 14px;
row-gap: 10px;
align-items: start;
max-width: 820px;
margin-bottom: 20px;
}
.tester-form label {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: #8892a8;
letter-spacing: 1px;
text-transform: uppercase;
padding-top: 8px;
}
.tester-form select,
.tester-form input,
.tester-form textarea {
background: #121829;
border: 1px solid #1e2a4a;
color: #e8eaf0;
padding: 6px 10px;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
border-radius: 3px;
width: 100%;
}
.tester-form textarea { resize: vertical; min-height: 64px; }
.tester-form select:focus,
.tester-form input:focus,
.tester-form textarea:focus { outline: none; border-color: #0066ff; }
.tester-form .row-buttons {
display: flex;
gap: 8px;
grid-column: 2;
}
.tester-btn {
background: #1a2340;
border: 1px solid #1e2a4a;
color: #b4bccf;
padding: 7px 14px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
border-radius: 3px;
cursor: pointer;
letter-spacing: 0.5px;
}
.tester-btn:hover { background: #243054; color: #e8eaf0; }
.tester-btn.primary { background: #0066ff; color: #fff; border-color: #0066ff; }
.tester-btn.primary:hover { background: #0050cc; }
.tester-btn:disabled { opacity: 0.5; cursor: wait; }
/* CloudWatch REPORT-line block */
.tester-report {
background: #121829;
border: 1px solid #1e2a4a;
padding: 14px 16px;
border-radius: 4px;
margin: 16px 0;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
line-height: 1.7;
white-space: pre;
overflow-x: auto;
color: #b4bccf;
}
.tester-report .k { color: #8892a8; }
.tester-report .v { color: #7ab0ff; }
.tester-report .cold { color: #ffc107; font-weight: 600; }
.tester-report .warm { color: #00c853; font-weight: 600; }
.tester-report .err { color: #ff3d00; font-weight: 600; }
.tester-section details {
background: #0d1325;
border: 1px solid #1e2a4a;
border-radius: 4px;
margin-bottom: 10px;
}
.tester-section details > summary {
cursor: pointer;
padding: 10px 14px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: #8892a8;
letter-spacing: 0.5px;
user-select: none;
}
.tester-section details[open] > summary { border-bottom: 1px solid #1e2a4a; color: #e8eaf0; }
.tester-section details pre {
padding: 12px 16px;
margin: 0;
overflow-x: auto;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: #b4bccf;
background: transparent;
border: none;
max-height: 360px;
}
.tester-section details .empty {
padding: 14px 16px;
color: #4a5568;
font-style: italic;
font-size: 12px;
}
.tester-history { width: 100%; max-width: none; }
.tester-history th.num,
.tester-history td.num {
font-family: 'JetBrains Mono', monospace;
color: #7ab0ff;
text-align: right;
white-space: nowrap;
}
.tester-history tr { cursor: pointer; }
.tester-history tr:hover td { background: #0d1a33; }
.tester-history tr.selected td { background: #0d1a33; color: #e8eaf0; }
.tester-history .badge {
display: inline-block;
padding: 1px 8px;
border-radius: 3px;
font-size: 10px;
font-family: 'JetBrains Mono', monospace;
letter-spacing: 0.5px;
}
.tester-history .badge.cold { background: #2a1f0a; color: #ffc107; }
.tester-history .badge.warm { background: #0a2410; color: #00c853; }
.tester-history .badge.err { background: #2a0f0a; color: #ff3d00; }
</style>
</head>
<body>
<header>
<h1>AWS LAMBDA</h1>
<span class="subtitle">Study notes &amp; sandbox — built from the interview exercise</span>
<button class="menu-toggle" onclick="toggleNav()" aria-label="Toggle navigation"></button>
</header>
<div class="layout">
<div class="nav-backdrop" onclick="toggleNav()"></div>
<nav>
<a class="active">TESTER</a>
<a href="http://docs.eth.local.ar">DOCS</a>
</nav>
<main>
<!-- ===================================================================== -->
<!-- LAMBDA TESTER -->
<!-- ===================================================================== -->
<section id="tester" class="graph-section tester-section active">
<h2>Lambda Tester</h2>
<p class="lead">Local invoker for any <code>handler(event, context)</code> in the project root. Captures the AWS-equivalent <code>REPORT</code> metrics, stdout, structured JSON log lines, and CloudWatch EMF metric records — the latter two will populate automatically once the corresponding function-side improvements land.</p>
<div class="tester-form">
<label>Function</label>
<select id="tester-function" aria-label="Function to invoke"></select>
<label>Memory (MB)</label>
<select id="tester-memory" aria-label="Configured memory size">
<option>128</option><option>256</option><option selected>512</option>
<option>1024</option><option>2048</option><option>3008</option><option>5120</option>
</select>
<label>Timeout (s)</label>
<input id="tester-timeout" type="number" value="30" min="1" max="900">
<label>Event JSON</label>
<div>
<select id="tester-event-sample" style="margin-bottom: 6px; max-width: 320px;"
aria-label="Load a saved sample event">
<option value="">— sample event —</option>
</select>
<textarea id="tester-event" rows="4" spellcheck="false">{}</textarea>
</div>
<span></span>
<div class="row-buttons">
<button class="tester-btn primary" id="tester-invoke">Invoke</button>
<button class="tester-btn" id="tester-reset" title="Clear module cache → next invocation cold-starts">Force cold start</button>
<button class="tester-btn" id="tester-refresh" title="Re-scan /app for handler files">Refresh functions</button>
</div>
</div>
<div id="tester-report" class="tester-report">Hit <span class="v">Invoke</span> to run the selected function.</div>
<details open>
<summary>Result (JSON)</summary>
<pre id="tester-result"><span class="placeholder" style="border: none; padding: 0; display: block;">No invocation yet.</span></pre>
</details>
<details>
<summary>stdout / stderr</summary>
<pre id="tester-stdout"><span class="empty">No output yet.</span></pre>
</details>
<details>
<summary>Structured logs <span style="color:#4a5568">(JSON-per-line on stdout — currently empty, will populate after improvement #2)</span></summary>
<pre id="tester-logs"><span class="empty">No structured logs detected.</span></pre>
</details>
<details>
<summary>EMF metrics <span style="color:#4a5568">(CloudWatch embedded-metric-format on stdout — currently empty, will populate after improvement #3)</span></summary>
<pre id="tester-emf"><span class="empty">No EMF metrics detected.</span></pre>
</details>
<h3>History</h3>
<p class="lead" style="margin-bottom:8px">Click a row to view its full record. Cleared on page reload (FastAPI keeps the last 200 invocations server-side regardless).</p>
<table class="cmp-table tester-history">
<thead>
<tr>
<th>#</th><th>Time</th><th>Function</th><th>Start</th>
<th class="num">Init (ms)</th><th class="num">Duration (ms)</th>
<th class="num">Max RSS (MB)</th><th>Status</th>
</tr>
</thead>
<tbody id="tester-history-body">
<tr><td colspan="8" style="color:#4a5568; text-align:center; font-style:italic;">No invocations yet.</td></tr>
</tbody>
</table>
<h3>Scripts</h3>
<p class="lead" style="margin-bottom:8px">Support scripts bundled with the selected function — any <code>.py</code> besides <code>handler.py</code>.</p>
<div class="tester-form">
<label>Script</label>
<select id="script-name" aria-label="Support script to run">
<option disabled selected>(select a function first)</option>
</select>
<label>Args</label>
<input id="script-args" type="text" placeholder="space-separated e.g. /mnt/documents">
<span></span>
<div class="row-buttons">
<button class="tester-btn primary" id="script-run">Run</button>
</div>
</div>
<div id="script-output" class="tester-report" style="display:none"></div>
</section>
</main>
</div>
<script>
function toggleNav() {
document.querySelector('.layout').classList.toggle('nav-open');
}
/* ── Lambda Tester ─────────────────────────────────────── */
const RUNNER_BASE = '/runner';
const $ = (id) => document.getElementById(id);
async function api(path, opts) {
const resp = await fetch(RUNNER_BASE + path, opts);
if (!resp.ok && resp.status !== 422) {
const body = await resp.text().catch(() => '');
throw new Error(`HTTP ${resp.status}: ${body || resp.statusText}`);
}
return resp.json();
}
let _functionMeta = {}; // name -> { events: [filename, ...] }
async function loadFunctions() {
const sel = $('tester-function');
sel.innerHTML = '<option disabled selected>loading…</option>';
try {
const { functions } = await api('/functions');
sel.innerHTML = '';
_functionMeta = {};
if (!functions.length) {
sel.innerHTML = '<option disabled selected>(no function folders found)</option>';
renderEvents();
return;
}
for (const fn of functions) {
// Backwards-compat: server may return either string names or {name, events} objects.
const name = typeof fn === 'string' ? fn : fn.name;
const events = (typeof fn === 'object' && fn.events) ? fn.events : [];
_functionMeta[name] = { events };
const opt = document.createElement('option');
opt.value = name; opt.textContent = name;
sel.appendChild(opt);
}
renderEvents();
loadScripts();
} catch (e) {
sel.innerHTML = `<option disabled selected>error: ${e.message}</option>`;
}
}
function renderEvents() {
const sel = $('tester-event-sample');
if (!sel) return;
const fn = $('tester-function').value;
const events = (_functionMeta[fn] && _functionMeta[fn].events) || [];
sel.innerHTML = '<option value="">— sample event —</option>';
for (const ev of events) {
const opt = document.createElement('option');
opt.value = ev; opt.textContent = ev;
sel.appendChild(opt);
}
}
async function loadEvent(filename) {
if (!filename) return;
const fn = $('tester-function').value;
try {
const body = await api(`/functions/${encodeURIComponent(fn)}/events/${encodeURIComponent(filename)}`);
$('tester-event').value = JSON.stringify(body, null, 2);
} catch (e) {
$('tester-report').innerHTML = `<span class="err">[ERROR]</span> ${e.message}`;
}
}
function renderReport(rec) {
const m = rec.metrics;
const cold = m.cold_start;
const isError = !!rec.error;
const tag = isError
? '<span class="err">[ERROR]</span>'
: cold ? '<span class="cold">[COLD]</span>' : '<span class="warm">[WARM]</span>';
const initLine = m.init_duration_ms != null
? `<span class="k">Init Duration:</span> <span class="v">${m.init_duration_ms.toFixed(2)} ms</span>\n`
: '';
$('tester-report').innerHTML = (
`${tag} <span class="k">REPORT RequestId:</span> <span class="v">${rec.invocation_id}</span>\n` +
`<span class="k">Function:</span> <span class="v">${rec.function}</span>\n` +
`<span class="k">Duration:</span> <span class="v">${m.duration_ms.toFixed(2)} ms</span>\n` +
`<span class="k">Billed Duration:</span> <span class="v">${m.billed_duration_ms} ms</span>\n` +
`<span class="k">Memory Size:</span> <span class="v">${m.memory_size_mb} MB</span> ` +
`<span class="k">Max Memory Used:</span> <span class="v">${m.max_memory_used_mb.toFixed(2)} MB</span>\n` +
initLine
);
}
function renderResult(rec) {
const pre = $('tester-result');
if (rec.error) {
pre.textContent = `Error: ${rec.error.type}: ${rec.error.message}\n\n${rec.error.traceback || ''}`;
pre.style.color = '#ff8a65';
} else {
pre.style.color = '';
pre.textContent = JSON.stringify(rec.result, null, 2);
}
}
function renderStdout(rec) {
const pre = $('tester-stdout');
const text = (rec.stdout || '') + (rec.stderr ? '\n--- stderr ---\n' + rec.stderr : '');
if (!text.trim()) {
pre.innerHTML = '<span class="empty">No stdout/stderr captured.</span>';
} else {
pre.textContent = text;
}
}
function renderLogs(rec) {
const pre = $('tester-logs');
if (!rec.structured_logs?.length) {
pre.innerHTML = '<span class="empty">No structured (JSON-per-line) logs detected on stdout.</span>';
} else {
pre.textContent = rec.structured_logs.map(l => JSON.stringify(l, null, 2)).join('\n');
}
}
function renderEmf(rec) {
const pre = $('tester-emf');
if (!rec.emf_metrics?.length) {
pre.innerHTML = '<span class="empty">No EMF metric records detected on stdout.</span>';
} else {
pre.textContent = rec.emf_metrics.map(l => JSON.stringify(l, null, 2)).join('\n');
}
}
let _history = []; // newest first; kept in sync with /invocations
async function loadHistory() {
try {
const { invocations } = await api('/invocations');
_history = invocations;
renderHistory();
} catch (e) { /* ignore on first load if backend is still booting */ }
}
function renderHistory() {
const tbody = $('tester-history-body');
if (!_history.length) {
tbody.innerHTML = '<tr><td colspan="8" style="color:#4a5568; text-align:center; font-style:italic;">No invocations yet.</td></tr>';
return;
}
tbody.innerHTML = '';
_history.forEach((h, idx) => {
const tr = document.createElement('tr');
tr.dataset.id = h.invocation_id;
const ts = new Date(h.timestamp * 1000).toLocaleTimeString();
const startBadge = h.cold_start
? '<span class="badge cold">cold</span>'
: '<span class="badge warm">warm</span>';
const statusBadge = h.ok
? '<span class="badge warm">ok</span>'
: '<span class="badge err">err</span>';
tr.innerHTML = `
<td class="num">${_history.length - idx}</td>
<td>${ts}</td>
<td>${h.function}</td>
<td>${startBadge}</td>
<td class="num">${h.init_duration_ms != null ? h.init_duration_ms.toFixed(2) : '—'}</td>
<td class="num">${h.duration_ms.toFixed(2)}</td>
<td class="num">${h.max_memory_used_mb.toFixed(2)}</td>
<td>${statusBadge}</td>`;
tr.addEventListener('click', () => loadHistoryDetail(h.invocation_id, tr));
tbody.appendChild(tr);
});
}
async function loadHistoryDetail(id, row) {
try {
const rec = await api('/invocations/' + id);
document.querySelectorAll('.tester-history tr.selected').forEach(r => r.classList.remove('selected'));
if (row) row.classList.add('selected');
renderReport(rec);
renderResult(rec);
renderStdout(rec);
renderLogs(rec);
renderEmf(rec);
} catch (e) {
$('tester-report').innerHTML = `<span class="err">[ERROR]</span> ${e.message}`;
}
}
async function invoke() {
const btn = $('tester-invoke');
btn.disabled = true;
$('tester-report').innerHTML = '<span class="k">…running…</span>';
try {
const fn = $('tester-function').value;
if (!fn) throw new Error('No function selected.');
let event;
try { event = JSON.parse($('tester-event').value || '{}'); }
catch (e) { throw new Error('Event JSON invalid: ' + e.message); }
const body = {
event,
memory_mb: parseInt($('tester-memory').value, 10),
timeout_ms: parseInt($('tester-timeout').value, 10) * 1000,
};
const rec = await api('/invoke/' + encodeURIComponent(fn), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
renderReport(rec);
renderResult(rec);
renderStdout(rec);
renderLogs(rec);
renderEmf(rec);
loadHistory();
} catch (e) {
$('tester-report').innerHTML = `<span class="err">[ERROR]</span> ${e.message}`;
} finally {
btn.disabled = false;
}
}
async function resetCold() {
const btn = $('tester-reset');
btn.disabled = true;
try {
const { cleared } = await api('/reset', { method: 'POST' });
$('tester-report').innerHTML = `<span class="k">Module cache cleared. Next invocation will be COLD.</span>\n<span class="k">Was loaded:</span> <span class="v">${cleared.join(', ') || '(none)'}</span>`;
} catch (e) {
$('tester-report').innerHTML = `<span class="err">[ERROR]</span> ${e.message}`;
} finally {
btn.disabled = false;
}
}
async function loadScripts() {
const fn = $('tester-function').value;
const sel = $('script-name');
if (!sel) return;
sel.innerHTML = '<option disabled selected>loading…</option>';
if (!fn) { sel.innerHTML = '<option disabled selected>(select a function first)</option>'; return; }
try {
const { scripts } = await api(`/functions/${encodeURIComponent(fn)}/scripts`);
sel.innerHTML = '';
if (!scripts.length) {
sel.innerHTML = '<option disabled selected>(no support scripts)</option>';
} else {
for (const s of scripts) {
const opt = document.createElement('option');
opt.value = s; opt.textContent = s;
sel.appendChild(opt);
}
}
} catch (e) {
sel.innerHTML = `<option disabled selected>error: ${e.message}</option>`;
}
}
async function runScript() {
const btn = $('script-run');
btn.disabled = true;
const out = $('script-output');
out.style.display = 'block';
out.innerHTML = '<span class="k">…running…</span>';
try {
const fn = $('tester-function').value;
if (!fn) throw new Error('No function selected.');
const script = $('script-name').value;
if (!script || script.startsWith('(')) throw new Error('No script selected.');
const argsStr = ($('script-args').value || '').trim();
const args = argsStr ? argsStr.split(/\s+/) : [];
const rec = await api(`/scripts/${encodeURIComponent(fn)}/${encodeURIComponent(script)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ args }),
});
const ok = rec.returncode === 0;
const badge = ok ? '<span class="warm">[ok]</span>' : `<span class="err">[exit ${rec.returncode}]</span>`;
const dur = `<span class="k">duration:</span> <span class="v">${rec.duration_ms.toFixed(2)} ms</span>`;
let content = `${badge} ${dur}\n`;
if (rec.stdout) content += '\n' + rec.stdout;
if (rec.stderr) content += '\n<span class="k">--- stderr ---</span>\n' + rec.stderr;
out.innerHTML = content;
} catch (e) {
out.innerHTML = `<span class="err">[ERROR]</span> ${e.message}`;
} finally {
btn.disabled = false;
}
}
document.addEventListener('DOMContentLoaded', () => {
$('tester-invoke').addEventListener('click', invoke);
$('tester-reset').addEventListener('click', resetCold);
$('tester-refresh').addEventListener('click', loadFunctions);
$('tester-function').addEventListener('change', () => { renderEvents(); loadScripts(); });
$('tester-event-sample').addEventListener('change', (e) => loadEvent(e.target.value));
$('script-run').addEventListener('click', runScript);
loadFunctions();
loadHistory();
});
</script>
</body>
</html>