Browsers speak HTTP over TCP and cannot open coap:// URLs, which made the server awkward to try without the CLI client. The bridge accepts an ordinary HTTP request, reissues it as CoAP over UDP, and maps the response code back onto an HTTP status (the raw code is also returned in X-Coap-Code). It serves a test console at / with a name field and a button per route, so GET, POST, 4.04 and 4.05 can all be exercised by clicking. The CoAP request logic moves from src/bin/client.rs into src/lib.rs so the client and the bridge share one implementation. The bridge is a development convenience: a small HTTP/1.1 subset with no keep-alive, chunked bodies or TLS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
135 lines
5.1 KiB
HTML
135 lines
5.1 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>CoAP Hello — Test Console</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: light dark;
|
|
--bg: #fbfbfa; --fg: #1a1a19; --muted: #6b6b68;
|
|
--card: #ffffff; --border: #e4e4e1; --accent: #b05730;
|
|
--ok: #2f7d4f; --err: #b3402f; --code-bg: #f4f4f2;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--bg: #1a1a19; --fg: #edecea; --muted: #9a9a96;
|
|
--card: #232322; --border: #353533; --accent: #e08a5f;
|
|
--ok: #6bbd8a; --err: #e8806c; --code-bg: #2b2b29;
|
|
}
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0; padding: 2rem 1.25rem; background: var(--bg); color: var(--fg);
|
|
font: 15px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
}
|
|
main { max-width: 640px; margin: 0 auto; }
|
|
h1 { font-size: 1.4rem; margin: 0 0 .35rem; letter-spacing: -.01em; }
|
|
.sub { color: var(--muted); margin: 0 0 1.75rem; font-size: .9rem; }
|
|
.card {
|
|
background: var(--card); border: 1px solid var(--border);
|
|
border-radius: 10px; padding: 1.25rem; margin-bottom: 1rem;
|
|
}
|
|
label { display: block; font-weight: 600; font-size: .82rem; margin-bottom: .4rem; }
|
|
input {
|
|
width: 100%; padding: .6rem .7rem; font: inherit; border-radius: 7px;
|
|
border: 1px solid var(--border); background: var(--bg); color: var(--fg);
|
|
}
|
|
input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
.row { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .85rem; }
|
|
button {
|
|
font: inherit; font-weight: 600; padding: .55rem 1rem; border-radius: 7px;
|
|
border: 1px solid var(--border); background: var(--bg); color: var(--fg); cursor: pointer;
|
|
}
|
|
button:hover { border-color: var(--accent); color: var(--accent); }
|
|
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
button.primary:hover { opacity: .88; color: #fff; }
|
|
pre {
|
|
background: var(--code-bg); border: 1px solid var(--border); border-radius: 7px;
|
|
padding: .8rem; overflow-x: auto; margin: 0;
|
|
font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
}
|
|
.status { font-weight: 600; }
|
|
.status.ok { color: var(--ok); }
|
|
.status.err { color: var(--err); }
|
|
h2 { font-size: .82rem; text-transform: uppercase; letter-spacing: .06em;
|
|
color: var(--muted); margin: 0 0 .7rem; }
|
|
.hint { color: var(--muted); font-size: .82rem; margin: .8rem 0 0; }
|
|
code { background: var(--code-bg); padding: .1em .35em; border-radius: 4px;
|
|
font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>CoAP Hello — Test Console</h1>
|
|
<p class="sub">This page talks HTTP to a local bridge, which reissues each
|
|
call as CoAP over UDP and shows you the raw response code.</p>
|
|
|
|
<div class="card">
|
|
<label for="name">Name</label>
|
|
<input id="name" value="Imran" autocomplete="off" placeholder="leave blank for "World"">
|
|
<div class="row">
|
|
<button class="primary" data-act="get">GET /hello</button>
|
|
<button data-act="post">POST /hello</button>
|
|
<button data-act="json">POST (JSON body)</button>
|
|
<button data-act="health">GET /health</button>
|
|
<button data-act="notfound">GET /nope</button>
|
|
</div>
|
|
<p class="hint">Or hit an endpoint directly:
|
|
<a href="/hello?name=Imran">/hello?name=Imran</a> ·
|
|
<a href="/health">/health</a></p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Response</h2>
|
|
<p id="status" class="status">—</p>
|
|
<pre id="out">Pick an action above.</pre>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
const $ = (id) => document.getElementById(id);
|
|
|
|
async function call(method, path, body) {
|
|
$('status').textContent = '…';
|
|
$('status').className = 'status';
|
|
$('out').textContent = '';
|
|
try {
|
|
const res = await fetch(path, {
|
|
method,
|
|
body,
|
|
headers: body ? { 'Content-Type': 'text/plain' } : undefined,
|
|
});
|
|
const text = await res.text();
|
|
const coap = res.headers.get('X-Coap-Code') || '—';
|
|
$('status').textContent = `CoAP ${coap} · HTTP ${res.status}`;
|
|
$('status').className = 'status ' + (res.ok ? 'ok' : 'err');
|
|
$('out').textContent = `${method} ${path}\n${body ? 'body: ' + body + '\n' : ''}\n${text || '(empty)'}`;
|
|
} catch (err) {
|
|
$('status').textContent = 'Request failed';
|
|
$('status').className = 'status err';
|
|
$('out').textContent = String(err) + '\n\nIs the bridge still running?';
|
|
}
|
|
}
|
|
|
|
document.querySelectorAll('button[data-act]').forEach((btn) => {
|
|
btn.addEventListener('click', () => {
|
|
const name = $('name').value.trim();
|
|
switch (btn.dataset.act) {
|
|
case 'get':
|
|
return call('GET', '/hello' + (name ? '?name=' + encodeURIComponent(name) : ''));
|
|
case 'post':
|
|
return call('POST', '/hello', name || '');
|
|
case 'json':
|
|
return call('POST', '/hello', JSON.stringify({ name }));
|
|
case 'health':
|
|
return call('GET', '/health');
|
|
case 'notfound':
|
|
return call('GET', '/nope');
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|