Extract amduatd UI module

This commit is contained in:
Carl Niklas Rydberg 2026-01-23 23:30:29 +01:00
parent 507007e865
commit d07dae5252
5 changed files with 578 additions and 409 deletions

View file

@ -5,6 +5,8 @@ set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF) set(CMAKE_C_EXTENSIONS OFF)
option(AMDUATD_ENABLE_UI "Build amduatd embedded UI" ON)
add_subdirectory(vendor/amduat) add_subdirectory(vendor/amduat)
add_library(amduat_federation add_library(amduat_federation
@ -22,13 +24,22 @@ target_link_libraries(amduat_federation
PRIVATE amduat_asl amduat_enc amduat_util amduat_fed PRIVATE amduat_asl amduat_enc amduat_util amduat_fed
) )
add_executable(amduatd src/amduatd.c) set(amduatd_sources src/amduatd.c)
if(AMDUATD_ENABLE_UI)
list(APPEND amduatd_sources src/amduatd_ui.c)
endif()
add_executable(amduatd ${amduatd_sources})
target_include_directories(amduatd target_include_directories(amduatd
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/src/internal PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/src/internal
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include
) )
target_compile_definitions(amduatd
PRIVATE AMDUATD_ENABLE_UI=$<BOOL:${AMDUATD_ENABLE_UI}>
)
target_link_libraries(amduatd target_link_libraries(amduatd
PRIVATE amduat_tgk amduat_pel amduat_format amduat_asl_store_fs amduat_asl PRIVATE amduat_tgk amduat_pel amduat_format amduat_asl_store_fs amduat_asl
amduat_enc amduat_hash_asl1 amduat_util amduat_federation amduat_enc amduat_hash_asl1 amduat_util amduat_federation

View file

@ -9,6 +9,14 @@ cmake -S . -B build
cmake --build build -j cmake --build build -j
``` ```
To build without the embedded UI:
```sh
cmake -S . -B build -DAMDUATD_ENABLE_UI=OFF
```
When the UI is enabled (default), `/v1/ui` serves the same embedded HTML as before.
## Core dependency ## Core dependency
This repo vendors the core implementation as a git submodule at `vendor/amduat`. This repo vendors the core implementation as a git submodule at `vendor/amduat`.

View file

@ -33,6 +33,7 @@
#include "amduat/util/hex.h" #include "amduat/util/hex.h"
#include "amduat/util/log.h" #include "amduat/util/log.h"
#include "amduat/hash/asl1.h" #include "amduat/hash/asl1.h"
#include "amduatd_ui.h"
#include <errno.h> #include <errno.h>
#include <signal.h> #include <signal.h>
@ -71,6 +72,12 @@ static bool amduatd_send_json_error(int fd,
const char *reason, const char *reason,
const char *msg); const char *msg);
#if AMDUATD_ENABLE_UI
bool amduatd_seed_ui_html(amduat_asl_store_t *store,
const amduat_asl_store_fs_config_t *cfg,
amduat_reference_t *out_ref);
#endif
static uint64_t amduatd_now_ms(void) { static uint64_t amduatd_now_ms(void) {
struct timespec ts; struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) {
@ -1088,319 +1095,6 @@ static const char k_amduatd_contract_v1_json[] =
"}" "}"
"}\n"; "}\n";
static const char k_amduatd_ui_html[] =
"<!doctype html>\n"
"<html lang=\"en\">\n"
"<head>\n"
" <meta charset=\"utf-8\" />\n"
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n"
" <title>amduatd — Concept editor</title>\n"
" <style>\n"
" :root{\n"
" --bg:#0b1220;--card:#111a2e;--text:#eaf0ff;--muted:#b7c3e6;--border:rgba(255,255,255,.10);\n"
" --shadow:0 10px 30px rgba(0,0,0,.35);--radius:18px;--max:980px;--pad:clamp(16px,3.5vw,28px);\n"
" }\n"
" *{box-sizing:border-box;}\n"
" html,body{min-height:100%;}\n"
" html{background:var(--bg);}\n"
" body{margin:0;min-height:100vh;font-family:\"Avenir Next\",\"Avenir\",\"Trebuchet MS\",\"Segoe UI\",sans-serif;color:var(--text);line-height:1.55;"
" background:radial-gradient(900px 400px at 15% 10%,rgba(95,145,255,.35),transparent 60%),"
" radial-gradient(800px 450px at 85% 20%,rgba(255,140,92,.25),transparent 60%),"
" radial-gradient(700px 500px at 50% 95%,rgba(56,220,181,.18),transparent 60%),var(--bg);}\n"
" a{color:inherit;text-decoration:none;}\n"
" a:hover{text-decoration:underline;text-underline-offset:4px;}\n"
" .wrap{max-width:var(--max);margin:0 auto;padding:26px var(--pad) 70px;min-height:100vh;}\n"
" header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 0 22px;}\n"
" .brand{display:flex;align-items:center;gap:10px;font-weight:700;letter-spacing:.2px;}\n"
" .logo{width:38px;height:38px;border-radius:12px;border:1px solid var(--border);"
" background:linear-gradient(135deg,rgba(95,145,255,.9),rgba(56,220,181,.8));box-shadow:var(--shadow);}\n"
" nav{display:flex;gap:14px;flex-wrap:wrap;color:var(--muted);font-size:14px;}\n"
" nav a{padding:6px 10px;border-radius:10px;}\n"
" nav a:hover{background:rgba(255,255,255,.06);text-decoration:none;}\n"
" .hero{border:1px solid var(--border);background:rgba(17,26,46,.72);border-radius:var(--radius);box-shadow:var(--shadow);"
" padding:clamp(22px,4.5vw,42px);backdrop-filter:blur(10px);}\n"
" h1{margin:0 0 10px;font-size:clamp(28px,3.6vw,40px);line-height:1.1;letter-spacing:-0.6px;}\n"
" .lead{margin:0 0 18px;color:var(--muted);font-size:clamp(14px,2vw,17px);max-width:70ch;}\n"
" .cta-row{display:flex;flex-wrap:wrap;gap:12px;margin-top:10px;}\n"
" .grid{display:grid;grid-template-columns:repeat(12,1fr);gap:14px;margin-top:16px;}\n"
" .card{grid-column:span 12;border:1px solid var(--border);background:rgba(17,26,46,.62);border-radius:16px;padding:16px;"
" box-shadow:0 8px 22px rgba(0,0,0,.25);backdrop-filter:blur(10px);}\n"
" .card h2{margin:2px 0 6px;font-size:16px;letter-spacing:.1px;}\n"
" .muted{color:var(--muted);font-size:13px;}\n"
" .span-7{grid-column:span 12;}\n"
" .span-5{grid-column:span 12;}\n"
" .span-6{grid-column:span 12;}\n"
" .stack{display:grid;gap:14px;}\n"
" @media (min-width: 980px){.span-7{grid-column:span 7;}.span-5{grid-column:span 5;}.span-6{grid-column:span 6;}}\n"
" textarea,input,select{width:100%;box-sizing:border-box;border-radius:12px;padding:10px;border:1px solid var(--border);"
" background:rgba(0,0,0,.12);color:var(--text);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;"
" font-size:12.5px;}\n"
" textarea{min-height:420px;resize:vertical;}\n"
" .btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;padding:10px 14px;border-radius:12px;border:1px solid var(--border);"
" background:rgba(255,255,255,.06);color:var(--text);font-weight:600;font-size:14px;cursor:pointer;}\n"
" .btn:hover{background:rgba(255,255,255,.10);}\n"
" .btn.primary{background:linear-gradient(135deg,rgba(95,145,255,.95),rgba(56,220,181,.85));border-color:rgba(255,255,255,.18);}\n"
" .btn.primary:hover{filter:brightness(1.05);}\n"
" .row{display:flex;gap:10px;flex-wrap:wrap;align-items:center;}\n"
" .row > *{flex:1 1 auto;}\n"
" .row .btn{flex:0 0 auto;}\n"
" pre{white-space:pre-wrap;word-break:break-word;margin:0;padding:10px;border-radius:12px;border:1px solid var(--border);"
" background:rgba(0,0,0,.2);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;"
" font-size:12.5px;min-height:120px;color:var(--text);}\n"
" footer{margin-top:26px;color:var(--muted);font-size:13px;display:flex;gap:10px;justify-content:space-between;flex-wrap:wrap;}\n"
" .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}\n"
" </style>\n"
"</head>\n"
"<body>\n"
" <div class=\"wrap\">\n"
" <header>\n"
" <div class=\"brand\" aria-label=\"Site brand\">\n"
" <div class=\"logo\" aria-hidden=\"true\"></div>\n"
" <span>amduatd</span>\n"
" </div>\n"
" <nav aria-label=\"Primary\">\n"
" <a href=\"#editor\">Editor</a>\n"
" <a href=\"#runner\">Run</a>\n"
" <a href=\"#relations\">Relations</a>\n"
" <a href=\"#about\">About</a>\n"
" </nav>\n"
" </header>\n"
" <main>\n"
" <section class=\"hero\" aria-labelledby=\"title\">\n"
" <h1 id=\"title\">Concept editor + PEL runner</h1>\n"
" <p class=\"lead\">Load shows the latest materialized bytes; Save uploads a new artifact and publishes a new version. Use the runner to execute PEL programs against stored artifacts.</p>\n"
" <div class=\"cta-row\">\n"
" <a class=\"btn primary\" href=\"#editor\" role=\"button\">Open editor</a>\n"
" <a class=\"btn\" href=\"#runner\" role=\"button\">Run program</a>\n"
" </div>\n"
" </section>\n"
" <section class=\"grid\" style=\"margin-top:16px;\">\n"
" <div class=\"card span-7\" id=\"editor\">\n"
" <div class=\"row\">\n"
" <input id=\"conceptName\" placeholder=\"concept name (e.g. hello)\" />\n"
" <button class=\"btn\" id=\"btnConceptCreate\" type=\"button\">Create</button>\n"
" <button class=\"btn\" id=\"btnLoad\" type=\"button\">Load</button>\n"
" <button class=\"btn\" id=\"btnSave\" type=\"button\">Save</button>\n"
" </div>\n"
" <div class=\"row\" style=\"margin-top:10px;\">\n"
" <select id=\"mode\">\n"
" <option value=\"text\">bytes: text (utf-8)</option>\n"
" <option value=\"base64\">bytes: base64</option>\n"
" <option value=\"hex\">bytes: hex</option>\n"
" <option value=\"pel_program\">PEL program: JSON</option>\n"
" </select>\n"
" <input id=\"typeTag\" placeholder=\"X-Amduat-Type-Tag (optional)\" />\n"
" <input id=\"latestRef\" placeholder=\"latest_ref\" readonly />\n"
" </div>\n"
" <textarea id=\"editor\" spellcheck=\"false\" placeholder=\"(bytes or PEL program authoring JSON)\"></textarea>\n"
" <div class=\"row\" style=\"margin-top:10px;\">\n"
" <button class=\"btn\" id=\"btnProgramTemplate\" type=\"button\">Insert identity program</button>\n"
" </div>\n"
" <div class=\"row\" style=\"margin-top:10px;\">\n"
" <input id=\"publishRef\" placeholder=\"publish existing ref\" />\n"
" <button class=\"btn\" id=\"btnPublishRef\" type=\"button\">Publish ref</button>\n"
" </div>\n"
" </div>\n"
"\n"
" <div class=\"stack span-5\">\n"
" <div class=\"card\" id=\"runner\">\n"
" <div class=\"muted\" style=\"margin-bottom:8px;\">Upload bytes (sets program_ref)</div>\n"
" <div class=\"row\">\n"
" <input id=\"uploadFile\" type=\"file\" />\n"
" <button class=\"btn\" id=\"btnUpload\" type=\"button\">Upload</button>\n"
" </div>\n"
" <hr style=\"border:none;border-top:1px solid rgba(255,255,255,.10);margin:14px 0;\" />\n"
" <div class=\"muted\" style=\"margin-bottom:8px;\">Run</div>\n"
" <input id=\"programRef\" placeholder=\"program_ref (hex ref or concept name)\" />\n"
" <div class=\"muted\" style=\"margin:10px 0 8px;\">input_refs (comma-separated hex refs or names)</div>\n"
" <input id=\"inputRefs\" placeholder=\"in0,in1,...\" />\n"
" <div class=\"muted\" style=\"margin:10px 0 8px;\">params_ref (optional)</div>\n"
" <input id=\"paramsRef\" placeholder=\"params\" />\n"
" <div class=\"muted\" style=\"margin:10px 0 8px;\">scheme_ref (optional, default dag)</div>\n"
" <input id=\"schemeRef\" placeholder=\"dag\" />\n"
" <div class=\"row\" style=\"margin-top:12px;\">\n"
" <button class=\"btn primary\" id=\"btnRun\" type=\"button\">Run</button>\n"
" <a class=\"muted\" href=\"/v1/contract\">/v1/contract</a>\n"
" <a class=\"muted\" href=\"/v1/meta\">/v1/meta</a>\n"
" </div>\n"
" <div class=\"muted\" style=\"margin:14px 0 8px;\">Response</div>\n"
" <pre id=\"out\"></pre>\n"
" </div>\n"
" <div class=\"card\" id=\"relations\">\n"
" <div class=\"muted\" style=\"margin-bottom:8px;\">Relations</div>\n"
" <div class=\"row\" style=\"margin-top:10px;\">\n"
" <button class=\"btn\" id=\"btnRelations\" type=\"button\">Refresh</button>\n"
" </div>\n"
" <pre id=\"relationsOut\"></pre>\n"
" </div>\n"
" </div>\n"
" </section>\n"
" <section id=\"about\" class=\"grid\" style=\"margin-top:16px;\">\n"
" <article class=\"card span-6\">\n"
" <h2>About</h2>\n"
" <p class=\"muted\">amduatd is a local-first mapping surface over a single ASL store root. This UI is a lightweight editor and runner for concepts and PEL programs.</p>\n"
" </article>\n"
" <article class=\"card span-6\">\n"
" <h2>Links</h2>\n"
" <p class=\"muted\"><a href=\"/v1/contract\">/v1/contract</a> • <a href=\"/v1/meta\">/v1/meta</a> • <a href=\"/v1/relations\">/v1/relations</a></p>\n"
" </article>\n"
" </section>\n"
" </main>\n"
" <footer>\n"
" <span>© 2025 Niklas Rydberg.</span>\n"
" <span><a href=\"#title\">Back to top</a></span>\n"
" </footer>\n"
" </div>\n"
"\n"
" <script>\n"
" const el = (id) => document.getElementById(id);\n"
" const out = (v) => { el('out').textContent = typeof v === 'string' ? v : JSON.stringify(v, null, 2); };\n"
" const td = new TextDecoder('utf-8');\n"
" const te = new TextEncoder();\n"
" const toHex = (u8) => Array.from(u8).map(b => b.toString(16).padStart(2,'0')).join('');\n"
" const fromHex = (s) => { const t=(s||'').trim(); if(t.length%2) throw new Error('hex length must be even'); const o=new Uint8Array(t.length/2); for(let i=0;i<o.length;i++){o[i]=parseInt(t.slice(i*2,i*2+2),16);} return o; };\n"
" const toB64 = (u8) => { let bin=''; for(let i=0;i<u8.length;i++) bin += String.fromCharCode(u8[i]); return btoa(bin); };\n"
" const fromB64 = (s) => { const bin=atob((s||'').trim()); const o=new Uint8Array(bin.length); for(let i=0;i<bin.length;i++) o[i]=bin.charCodeAt(i); return o; };\n"
"\n"
" async function ensureConcept(name){\n"
" const resp = await fetch('/v1/concepts',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name})});\n"
" if(resp.status === 409) return; // already exists\n"
" if(!resp.ok) throw new Error(await resp.text());\n"
" }\n"
"\n"
" async function loadConcept(){\n"
" const name = el('conceptName').value.trim();\n"
" if(!name){ out('missing concept name'); return; }\n"
" const resp = await fetch(`/v1/concepts/${encodeURIComponent(name)}`);\n"
" const text = await resp.text();\n"
" if(!resp.ok){ out(text); return; }\n"
" const j = JSON.parse(text);\n"
" el('latestRef').value = j.latest_ref || '';\n"
" el('programRef').value = name;\n"
" if(!j.latest_ref){ el('editor').value=''; out(text); return; }\n"
" const mode = el('mode').value;\n"
" const infoResp = await fetch(`/v1/artifacts/${j.latest_ref}?format=info`);\n"
" if(infoResp.ok){ const info = JSON.parse(await infoResp.text()); el('typeTag').value = info.has_type_tag ? info.type_tag : ''; }\n"
" if(mode === 'pel_program'){\n"
" if(!el('editor').value.trim()){\n"
" el('editor').value = JSON.stringify({\n"
" nodes:[{id:1,op:{name:'pel.bytes.concat',version:1},inputs:[{external:{input_index:0}}],params_hex:''}],\n"
" roots:[{node_id:1,output_index:0}]\n"
" }, null, 2);\n"
" }\n"
" out(text);\n"
" return;\n"
" }\n"
" const aResp = await fetch(`/v1/artifacts/${j.latest_ref}`);\n"
" if(!aResp.ok){ out(await aResp.text()); return; }\n"
" const u8 = new Uint8Array(await aResp.arrayBuffer());\n"
" if(mode==='hex') el('editor').value = toHex(u8);\n"
" else if(mode==='base64') el('editor').value = toB64(u8);\n"
" else el('editor').value = td.decode(u8);\n"
" out(text);\n"
" }\n"
"\n"
" async function saveConcept(){\n"
" const name = el('conceptName').value.trim();\n"
" if(!name){ out('missing concept name'); return; }\n"
" await ensureConcept(name);\n"
" const mode = el('mode').value;\n"
" if(mode === 'pel_program'){\n"
" const body = JSON.parse(el('editor').value || '{}');\n"
" const mkResp = await fetch('/v1/pel/programs',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});\n"
" const mkText = await mkResp.text();\n"
" if(!mkResp.ok){ out(mkText); return; }\n"
" const mk = JSON.parse(mkText);\n"
" const pref = mk.program_ref;\n"
" const pubResp = await fetch(`/v1/concepts/${encodeURIComponent(name)}/publish`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ref:pref})});\n"
" const pubText = await pubResp.text();\n"
" out(pubText);\n"
" if(pubResp.ok){ el('typeTag').value = '0x00000101'; await loadConcept(); }\n"
" return;\n"
" }\n"
" let u8;\n"
" if(mode==='hex') u8 = fromHex(el('editor').value);\n"
" else if(mode==='base64') u8 = fromB64(el('editor').value);\n"
" else u8 = te.encode(el('editor').value);\n"
" const headers = {'Content-Type':'application/octet-stream'};\n"
" const typeTag = el('typeTag').value.trim();\n"
" if(typeTag) headers['X-Amduat-Type-Tag'] = typeTag;\n"
" const putResp = await fetch('/v1/artifacts',{method:'POST',headers,body:u8});\n"
" const putText = await putResp.text();\n"
" if(!putResp.ok){ out(putText); return; }\n"
" const put = JSON.parse(putText);\n"
" const pubResp = await fetch(`/v1/concepts/${encodeURIComponent(name)}/publish`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ref:put.ref})});\n"
" const pubText = await pubResp.text();\n"
" out(pubText);\n"
" if(pubResp.ok) await loadConcept();\n"
" }\n"
"\n"
" el('btnConceptCreate').addEventListener('click', async () => { try{ await ensureConcept(el('conceptName').value.trim()); out('{\"ok\":true}\\n'); }catch(e){ out(String(e)); } });\n"
" el('btnLoad').addEventListener('click', () => loadConcept().catch(e => out(String(e))));\n"
" el('btnSave').addEventListener('click', () => saveConcept().catch(e => out(String(e))));\n"
" el('mode').addEventListener('change', () => {\n"
" if(el('mode').value === 'pel_program'){\n"
" el('typeTag').value = '0x00000101';\n"
" }\n"
" loadConcept().catch(() => {});\n"
" });\n"
"\n"
" el('btnProgramTemplate').addEventListener('click', () => {\n"
" el('mode').value = 'pel_program';\n"
" el('typeTag').value = '0x00000101';\n"
" el('editor').value = JSON.stringify({\n"
" nodes:[{id:1,op:{name:'pel.bytes.concat',version:1},inputs:[{external:{input_index:0}}],params_hex:''}],\n"
" roots:[{node_id:1,output_index:0}]\n"
" }, null, 2);\n"
" });\n"
"\n"
" el('btnPublishRef').addEventListener('click', async () => {\n"
" try{\n"
" const name = el('conceptName').value.trim();\n"
" const ref = el('publishRef').value.trim();\n"
" if(!name||!ref){ out('missing name/ref'); return; }\n"
" await ensureConcept(name);\n"
" const resp = await fetch(`/v1/concepts/${encodeURIComponent(name)}/publish`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ref})});\n"
" out(await resp.text());\n"
" }catch(e){ out(String(e)); }\n"
" });\n"
"\n"
" el('btnUpload').addEventListener('click', async () => {\n"
" try {\n"
" const file = el('uploadFile').files && el('uploadFile').files[0];\n"
" if (!file) { out('no file selected'); return; }\n"
" const resp = await fetch('/v1/artifacts', { method:'POST', headers:{'Content-Type':'application/octet-stream'}, body:file });\n"
" const text = await resp.text();\n"
" out(text);\n"
" if (resp.ok) { const j = JSON.parse(text); if (j && j.ref) el('programRef').value = j.ref; }\n"
" } catch (e) { out(String(e)); }\n"
" });\n"
"\n"
" el('btnRun').addEventListener('click', async () => {\n"
" try {\n"
" const program_ref = el('programRef').value.trim();\n"
" const input_refs = (el('inputRefs').value || '').split(',').map(s => s.trim()).filter(Boolean);\n"
" const params_ref = el('paramsRef').value.trim();\n"
" const scheme_ref = el('schemeRef').value.trim();\n"
" const body = { program_ref, input_refs };\n"
" if (params_ref) body.params_ref = params_ref;\n"
" if (scheme_ref) body.scheme_ref = scheme_ref;\n"
" const resp = await fetch('/v1/pel/run', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) });\n"
" out(await resp.text());\n"
" } catch (e) { out(String(e)); }\n"
" });\n"
"\n"
" async function loadRelations(){\n"
" const resp = await fetch('/v1/relations');\n"
" const text = await resp.text();\n"
" el('relationsOut').textContent = text;\n"
" }\n"
" el('btnRelations').addEventListener('click', () => loadRelations().catch(e => out(String(e))));\n"
" loadRelations().catch(() => {});\n"
" </script>\n"
"</body>\n"
"</html>\n";
static volatile sig_atomic_t amduatd_should_exit = 0; static volatile sig_atomic_t amduatd_should_exit = 0;
static void amduatd_on_signal(int signo) { static void amduatd_on_signal(int signo) {
@ -3821,20 +3515,6 @@ static bool amduatd_read_line(int fd, char *buf, size_t cap, size_t *out_len) {
return true; return true;
} }
typedef struct {
char method[8];
char path[1024];
char content_type[128];
char accept[128];
char x_type_tag[64];
char x_capability[2048];
size_t content_length;
bool has_actor;
amduat_octets_t actor;
bool has_uid;
uid_t uid;
} amduatd_http_req_t;
static void amduatd_http_req_init(amduatd_http_req_t *req) { static void amduatd_http_req_init(amduatd_http_req_t *req) {
memset(req, 0, sizeof(*req)); memset(req, 0, sizeof(*req));
} }
@ -4039,7 +3719,7 @@ static bool amduatd_http_parse_request(int fd, amduatd_http_req_t *out_req) {
return true; return true;
} }
static bool amduatd_http_send_status(int fd, bool amduatd_http_send_status(int fd,
int code, int code,
const char *reason, const char *reason,
const char *content_type, const char *content_type,
@ -4070,7 +3750,7 @@ static bool amduatd_http_send_status(int fd,
return true; return true;
} }
static bool amduatd_http_send_text(int fd, bool amduatd_http_send_text(int fd,
int code, int code,
const char *reason, const char *reason,
const char *text, const char *text,
@ -5676,33 +5356,6 @@ seed_cleanup:
return ok; return ok;
} }
static bool amduatd_seed_ui_html(amduat_asl_store_t *store,
const amduat_asl_store_fs_config_t *cfg,
amduat_reference_t *out_ref) {
amduat_artifact_t artifact;
amduat_asl_store_error_t err;
if (out_ref != NULL) {
memset(out_ref, 0, sizeof(*out_ref));
}
if (store == NULL || cfg == NULL || out_ref == NULL) {
return false;
}
artifact = amduat_artifact(amduat_octets(k_amduatd_ui_html,
strlen(k_amduatd_ui_html)));
(void)amduat_asl_ref_derive(artifact,
cfg->config.encoding_profile_id,
cfg->config.hash_id,
out_ref,
NULL);
err = amduat_asl_store_put(store, artifact, out_ref);
if (err != AMDUAT_ASL_STORE_OK) {
return false;
}
return true;
}
static bool amduatd_handle_get_artifact(int fd, static bool amduatd_handle_get_artifact(int fd,
amduat_asl_store_t *store, amduat_asl_store_t *store,
const amduatd_http_req_t *req, const amduatd_http_req_t *req,
@ -8170,45 +7823,6 @@ pel_programs_cleanup:
return ok; return ok;
} }
static bool amduatd_handle_get_ui(int fd,
amduat_asl_store_t *store,
amduat_reference_t ui_ref) {
amduat_artifact_t artifact;
amduat_asl_store_error_t err;
if (store == NULL || ui_ref.hash_id == 0 || ui_ref.digest.data == NULL ||
ui_ref.digest.len == 0) {
return amduatd_http_send_text(fd, 500, "Internal Server Error",
"ui not available\n", false);
}
memset(&artifact, 0, sizeof(artifact));
err = amduat_asl_store_get(store, ui_ref, &artifact);
if (err == AMDUAT_ASL_STORE_ERR_NOT_FOUND) {
return amduatd_http_send_text(fd, 404, "Not Found", "not found\n", false);
}
if (err != AMDUAT_ASL_STORE_OK) {
amduat_asl_artifact_free(&artifact);
return amduatd_http_send_text(fd, 500, "Internal Server Error",
"store error\n", false);
}
if (artifact.bytes.len != 0 && artifact.bytes.data == NULL) {
amduat_asl_artifact_free(&artifact);
return amduatd_http_send_text(fd, 500, "Internal Server Error",
"store error\n", false);
}
{
bool ok = amduatd_http_send_status(fd,
200,
"OK",
"text/html; charset=utf-8",
artifact.bytes.data,
artifact.bytes.len,
false);
amduat_asl_artifact_free(&artifact);
return ok;
}
}
static bool amduatd_handle_post_concepts(int fd, static bool amduatd_handle_post_concepts(int fd,
amduat_asl_store_t *store, amduat_asl_store_t *store,
const amduat_asl_store_fs_config_t *cfg, const amduat_asl_store_fs_config_t *cfg,
@ -9911,10 +9525,21 @@ static bool amduatd_handle_conn(int fd,
goto conn_cleanup; goto conn_cleanup;
} }
if (strcmp(req.method, "GET") == 0 && strcmp(no_query, "/v1/ui") == 0) { {
ok = amduatd_handle_get_ui(fd, store, ui_ref); amduatd_ctx_t ui_ctx;
amduatd_http_resp_t ui_resp;
ui_ctx.store = store;
ui_ctx.ui_ref = ui_ref;
ui_resp.fd = fd;
ui_resp.ok = false;
if (amduatd_ui_can_handle(&req)) {
if (amduatd_ui_handle(&ui_ctx, &req, &ui_resp)) {
ok = ui_resp.ok;
goto conn_cleanup; goto conn_cleanup;
} }
}
}
if (strcmp(req.method, "GET") == 0 && strcmp(no_query, "/v1/meta") == 0) { if (strcmp(req.method, "GET") == 0 && strcmp(no_query, "/v1/meta") == 0) {
ok = amduatd_handle_meta(fd, cfg, api_contract_ref, false); ok = amduatd_handle_meta(fd, cfg, api_contract_ref, false);
@ -10190,18 +9815,22 @@ int main(int argc, char **argv) {
fprintf(stderr, "error: failed to seed api contract\n"); fprintf(stderr, "error: failed to seed api contract\n");
return 8; return 8;
} }
#if AMDUATD_ENABLE_UI
if (!amduatd_seed_ui_html(&store, &cfg, &ui_ref)) { if (!amduatd_seed_ui_html(&store, &cfg, &ui_ref)) {
fprintf(stderr, "error: failed to seed ui html\n"); fprintf(stderr, "error: failed to seed ui html\n");
return 8; return 8;
} }
#endif
if (!amduatd_concepts_init(&concepts, &store, &cfg, &dcfg, root)) { if (!amduatd_concepts_init(&concepts, &store, &cfg, &dcfg, root)) {
fprintf(stderr, "error: failed to init concept edges\n"); fprintf(stderr, "error: failed to init concept edges\n");
return 8; return 8;
} }
#if AMDUATD_ENABLE_UI
if (!amduatd_seed_ms_ui_state(&store, &cfg, &concepts, &dcfg)) { if (!amduatd_seed_ms_ui_state(&store, &cfg, &concepts, &dcfg)) {
fprintf(stderr, "error: failed to seed ms ui state\n"); fprintf(stderr, "error: failed to seed ms ui state\n");
return 8; return 8;
} }
#endif
amduat_fed_transport_stub_init(&fed_stub); amduat_fed_transport_stub_init(&fed_stub);
memset(&fed_cfg, 0, sizeof(fed_cfg)); memset(&fed_cfg, 0, sizeof(fed_cfg));

461
src/amduatd_ui.c Normal file
View file

@ -0,0 +1,461 @@
#include "amduatd_ui.h"
#include "amduat/asl/artifact_io.h"
#include "amduat/asl/asl_store_fs.h"
#include "amduat/asl/asl_store_fs_meta.h"
#include "amduat/asl/ref_derive.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
bool amduatd_http_send_text(int fd,
int code,
const char *reason,
const char *text,
bool head_only);
bool amduatd_http_send_status(int fd,
int code,
const char *reason,
const char *content_type,
const uint8_t *body,
size_t body_len,
bool head_only);
static const char k_amduatd_ui_html[] =
"<!doctype html>\n"
"<html lang=\"en\">\n"
"<head>\n"
" <meta charset=\"utf-8\" />\n"
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n"
" <title>amduatd — Concept editor</title>\n"
" <style>\n"
" :root{\n"
" --bg:#0b1220;--card:#111a2e;--text:#eaf0ff;--muted:#b7c3e6;--border:rgba(255,255,255,.10);\n"
" --shadow:0 10px 30px rgba(0,0,0,.35);--radius:18px;--max:980px;--pad:clamp(16px,3.5vw,28px);\n"
" }\n"
" *{box-sizing:border-box;}\n"
" html,body{min-height:100%;}\n"
" html{background:var(--bg);}\n"
" body{margin:0;min-height:100vh;font-family:\"Avenir Next\",\"Avenir\",\"Trebuchet MS\",\"Segoe UI\",sans-serif;color:var(--text);line-height:1.55;"
" background:radial-gradient(900px 400px at 15% 10%,rgba(95,145,255,.35),transparent 60%),"
" radial-gradient(800px 450px at 85% 20%,rgba(255,140,92,.25),transparent 60%),"
" radial-gradient(700px 500px at 50% 95%,rgba(56,220,181,.18),transparent 60%),var(--bg);}\n"
" a{color:inherit;text-decoration:none;}\n"
" a:hover{text-decoration:underline;text-underline-offset:4px;}\n"
" .wrap{max-width:var(--max);margin:0 auto;padding:26px var(--pad) 70px;min-height:100vh;}\n"
" header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 0 22px;}\n"
" .brand{display:flex;align-items:center;gap:10px;font-weight:700;letter-spacing:.2px;}\n"
" .logo{width:38px;height:38px;border-radius:12px;border:1px solid var(--border);"
" background:linear-gradient(135deg,rgba(95,145,255,.9),rgba(56,220,181,.8));box-shadow:var(--shadow);}\n"
" nav{display:flex;gap:14px;flex-wrap:wrap;color:var(--muted);font-size:14px;}\n"
" nav a{padding:6px 10px;border-radius:10px;}\n"
" nav a:hover{background:rgba(255,255,255,.06);text-decoration:none;}\n"
" .hero{border:1px solid var(--border);background:rgba(17,26,46,.72);border-radius:var(--radius);box-shadow:var(--shadow);"
" padding:clamp(22px,4.5vw,42px);backdrop-filter:blur(10px);}\n"
" h1{margin:0 0 10px;font-size:clamp(28px,3.6vw,40px);line-height:1.1;letter-spacing:-0.6px;}\n"
" .lead{margin:0 0 18px;color:var(--muted);font-size:clamp(14px,2vw,17px);max-width:70ch;}\n"
" .cta-row{display:flex;flex-wrap:wrap;gap:12px;margin-top:10px;}\n"
" .grid{display:grid;grid-template-columns:repeat(12,1fr);gap:14px;margin-top:16px;}\n"
" .card{grid-column:span 12;border:1px solid var(--border);background:rgba(17,26,46,.62);border-radius:16px;padding:16px;"
" box-shadow:0 8px 22px rgba(0,0,0,.25);backdrop-filter:blur(10px);}\n"
" .card h2{margin:2px 0 6px;font-size:16px;letter-spacing:.1px;}\n"
" .muted{color:var(--muted);font-size:13px;}\n"
" .span-7{grid-column:span 12;}\n"
" .span-5{grid-column:span 12;}\n"
" .span-6{grid-column:span 12;}\n"
" .stack{display:grid;gap:14px;}\n"
" @media (min-width: 980px){.span-7{grid-column:span 7;}.span-5{grid-column:span 5;}.span-6{grid-column:span 6;}}\n"
" textarea,input,select{width:100%;box-sizing:border-box;border-radius:12px;padding:10px;border:1px solid var(--border);"
" background:rgba(0,0,0,.12);color:var(--text);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;"
" font-size:12.5px;}\n"
" textarea{min-height:420px;resize:vertical;}\n"
" .btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;padding:10px 14px;border-radius:12px;border:1px solid var(--border);"
" background:rgba(255,255,255,.06);color:var(--text);font-weight:600;font-size:14px;cursor:pointer;}\n"
" .btn:hover{background:rgba(255,255,255,.10);}\n"
" .btn.primary{background:linear-gradient(135deg,rgba(95,145,255,.95),rgba(56,220,181,.85));border-color:rgba(255,255,255,.18);}\n"
" .btn.primary:hover{filter:brightness(1.05);}\n"
" .row{display:flex;gap:10px;flex-wrap:wrap;align-items:center;}\n"
" .row > *{flex:1 1 auto;}\n"
" .row .btn{flex:0 0 auto;}\n"
" pre{white-space:pre-wrap;word-break:break-word;margin:0;padding:10px;border-radius:12px;border:1px solid var(--border);"
" background:rgba(0,0,0,.2);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;"
" font-size:12.5px;min-height:120px;color:var(--text);}\n"
" footer{margin-top:26px;color:var(--muted);font-size:13px;display:flex;gap:10px;justify-content:space-between;flex-wrap:wrap;}\n"
" .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}\n"
" </style>\n"
"</head>\n"
"<body>\n"
" <div class=\"wrap\">\n"
" <header>\n"
" <div class=\"brand\" aria-label=\"Site brand\">\n"
" <div class=\"logo\" aria-hidden=\"true\"></div>\n"
" <span>amduatd</span>\n"
" </div>\n"
" <nav aria-label=\"Primary\">\n"
" <a href=\"#editor\">Editor</a>\n"
" <a href=\"#runner\">Run</a>\n"
" <a href=\"#relations\">Relations</a>\n"
" <a href=\"#about\">About</a>\n"
" </nav>\n"
" </header>\n"
" <main>\n"
" <section class=\"hero\" aria-labelledby=\"title\">\n"
" <h1 id=\"title\">Concept editor + PEL runner</h1>\n"
" <p class=\"lead\">Load shows the latest materialized bytes; Save uploads a new artifact and publishes a new version. Use the runner to execute PEL programs against stored artifacts.</p>\n"
" <div class=\"cta-row\">\n"
" <a class=\"btn primary\" href=\"#editor\" role=\"button\">Open editor</a>\n"
" <a class=\"btn\" href=\"#runner\" role=\"button\">Run program</a>\n"
" </div>\n"
" </section>\n"
" <section class=\"grid\" style=\"margin-top:16px;\">\n"
" <div class=\"card span-7\" id=\"editor\">\n"
" <div class=\"row\">\n"
" <input id=\"conceptName\" placeholder=\"concept name (e.g. hello)\" />\n"
" <button class=\"btn\" id=\"btnConceptCreate\" type=\"button\">Create</button>\n"
" <button class=\"btn\" id=\"btnLoad\" type=\"button\">Load</button>\n"
" <button class=\"btn\" id=\"btnSave\" type=\"button\">Save</button>\n"
" </div>\n"
" <div class=\"row\" style=\"margin-top:10px;\">\n"
" <select id=\"mode\">\n"
" <option value=\"text\">bytes: text (utf-8)</option>\n"
" <option value=\"base64\">bytes: base64</option>\n"
" <option value=\"hex\">bytes: hex</option>\n"
" <option value=\"pel_program\">PEL program: JSON</option>\n"
" </select>\n"
" <input id=\"typeTag\" placeholder=\"X-Amduat-Type-Tag (optional)\" />\n"
" <input id=\"latestRef\" placeholder=\"latest_ref\" readonly />\n"
" </div>\n"
" <textarea id=\"editor\" spellcheck=\"false\" placeholder=\"(bytes or PEL program authoring JSON)\"></textarea>\n"
" <div class=\"row\" style=\"margin-top:10px;\">\n"
" <button class=\"btn\" id=\"btnProgramTemplate\" type=\"button\">Insert identity program</button>\n"
" </div>\n"
" <div class=\"row\" style=\"margin-top:10px;\">\n"
" <input id=\"publishRef\" placeholder=\"publish existing ref\" />\n"
" <button class=\"btn\" id=\"btnPublishRef\" type=\"button\">Publish ref</button>\n"
" </div>\n"
" </div>\n"
"\n"
" <div class=\"stack span-5\">\n"
" <div class=\"card\" id=\"runner\">\n"
" <div class=\"muted\" style=\"margin-bottom:8px;\">Upload bytes (sets program_ref)</div>\n"
" <div class=\"row\">\n"
" <input id=\"uploadFile\" type=\"file\" />\n"
" <button class=\"btn\" id=\"btnUpload\" type=\"button\">Upload</button>\n"
" </div>\n"
" <hr style=\"border:none;border-top:1px solid rgba(255,255,255,.10);margin:14px 0;\" />\n"
" <div class=\"muted\" style=\"margin-bottom:8px;\">Run</div>\n"
" <input id=\"programRef\" placeholder=\"program_ref (hex ref or concept name)\" />\n"
" <div class=\"muted\" style=\"margin:10px 0 8px;\">input_refs (comma-separated hex refs or names)</div>\n"
" <input id=\"inputRefs\" placeholder=\"in0,in1,...\" />\n"
" <div class=\"muted\" style=\"margin:10px 0 8px;\">params_ref (optional)</div>\n"
" <input id=\"paramsRef\" placeholder=\"params\" />\n"
" <div class=\"muted\" style=\"margin:10px 0 8px;\">scheme_ref (optional, default dag)</div>\n"
" <input id=\"schemeRef\" placeholder=\"dag\" />\n"
" <div class=\"row\" style=\"margin-top:12px;\">\n"
" <button class=\"btn primary\" id=\"btnRun\" type=\"button\">Run</button>\n"
" <a class=\"muted\" href=\"/v1/contract\">/v1/contract</a>\n"
" <a class=\"muted\" href=\"/v1/meta\">/v1/meta</a>\n"
" </div>\n"
" <div class=\"muted\" style=\"margin:14px 0 8px;\">Response</div>\n"
" <pre id=\"out\"></pre>\n"
" </div>\n"
" <div class=\"card\" id=\"relations\">\n"
" <div class=\"muted\" style=\"margin-bottom:8px;\">Relations</div>\n"
" <div class=\"row\" style=\"margin-top:10px;\">\n"
" <button class=\"btn\" id=\"btnRelations\" type=\"button\">Refresh</button>\n"
" </div>\n"
" <pre id=\"relationsOut\"></pre>\n"
" </div>\n"
" </div>\n"
" </section>\n"
" <section id=\"about\" class=\"grid\" style=\"margin-top:16px;\">\n"
" <article class=\"card span-6\">\n"
" <h2>About</h2>\n"
" <p class=\"muted\">amduatd is a local-first mapping surface over a single ASL store root. This UI is a lightweight editor and runner for concepts and PEL programs.</p>\n"
" </article>\n"
" <article class=\"card span-6\">\n"
" <h2>Links</h2>\n"
" <p class=\"muted\"><a href=\"/v1/contract\">/v1/contract</a> • <a href=\"/v1/meta\">/v1/meta</a> • <a href=\"/v1/relations\">/v1/relations</a></p>\n"
" </article>\n"
" </section>\n"
" </main>\n"
" <footer>\n"
" <span>© 2025 Niklas Rydberg.</span>\n"
" <span><a href=\"#title\">Back to top</a></span>\n"
" </footer>\n"
" </div>\n"
"\n"
" <script>\n"
" const el = (id) => document.getElementById(id);\n"
" const out = (v) => { el('out').textContent = typeof v === 'string' ? v : JSON.stringify(v, null, 2); };\n"
" const td = new TextDecoder('utf-8');\n"
" const te = new TextEncoder();\n"
" const toHex = (u8) => Array.from(u8).map(b => b.toString(16).padStart(2,'0')).join('');\n"
" const fromHex = (s) => { const t=(s||'').trim(); if(t.length%2) throw new Error('hex length must be even'); const o=new Uint8Array(t.length/2); for(let i=0;i<o.length;i++){o[i]=parseInt(t.slice(i*2,i*2+2),16);} return o; };\n"
" const toB64 = (u8) => { let bin=''; for(let i=0;i<u8.length;i++) bin += String.fromCharCode(u8[i]); return btoa(bin); };\n"
" const fromB64 = (s) => { const bin=atob((s||'').trim()); const o=new Uint8Array(bin.length); for(let i=0;i<bin.length;i++) o[i]=bin.charCodeAt(i); return o; };\n"
"\n"
" async function ensureConcept(name){\n"
" const resp = await fetch('/v1/concepts',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name})});\n"
" if(resp.status === 409) return; // already exists\n"
" if(!resp.ok) throw new Error(await resp.text());\n"
" }\n"
"\n"
" async function loadConcept(){\n"
" const name = el('conceptName').value.trim();\n"
" if(!name){ out('missing concept name'); return; }\n"
" const resp = await fetch(`/v1/concepts/${encodeURIComponent(name)}`);\n"
" const text = await resp.text();\n"
" if(!resp.ok){ out(text); return; }\n"
" const j = JSON.parse(text);\n"
" el('latestRef').value = j.latest_ref || '';\n"
" el('programRef').value = name;\n"
" if(!j.latest_ref){ el('editor').value=''; out(text); return; }\n"
" const mode = el('mode').value;\n"
" const infoResp = await fetch(`/v1/artifacts/${j.latest_ref}?format=info`);\n"
" if(infoResp.ok){ const info = JSON.parse(await infoResp.text()); el('typeTag').value = info.has_type_tag ? info.type_tag : ''; }\n"
" if(mode === 'pel_program'){\n"
" if(!el('editor').value.trim()){\n"
" el('editor').value = JSON.stringify({\n"
" nodes:[{id:1,op:{name:'pel.bytes.concat',version:1},inputs:[{external:{input_index:0}}],params_hex:''}],\n"
" roots:[{node_id:1,output_index:0}]\n"
" }, null, 2);\n"
" }\n"
" out(text);\n"
" return;\n"
" }\n"
" const aResp = await fetch(`/v1/artifacts/${j.latest_ref}`);\n"
" if(!aResp.ok){ out(await aResp.text()); return; }\n"
" const u8 = new Uint8Array(await aResp.arrayBuffer());\n"
" if(mode==='hex') el('editor').value = toHex(u8);\n"
" else if(mode==='base64') el('editor').value = toB64(u8);\n"
" else el('editor').value = td.decode(u8);\n"
" out(text);\n"
" }\n"
"\n"
" async function saveConcept(){\n"
" const name = el('conceptName').value.trim();\n"
" if(!name){ out('missing concept name'); return; }\n"
" await ensureConcept(name);\n"
" const mode = el('mode').value;\n"
" if(mode === 'pel_program'){\n"
" const body = JSON.parse(el('editor').value || '{}');\n"
" const mkResp = await fetch('/v1/pel/programs',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});\n"
" const mkText = await mkResp.text();\n"
" if(!mkResp.ok){ out(mkText); return; }\n"
" const mk = JSON.parse(mkText);\n"
" const pref = mk.program_ref;\n"
" const pubResp = await fetch(`/v1/concepts/${encodeURIComponent(name)}/publish`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ref:pref})});\n"
" const pubText = await pubResp.text();\n"
" out(pubText);\n"
" if(pubResp.ok){ el('typeTag').value = '0x00000101'; await loadConcept(); }\n"
" return;\n"
" }\n"
" let u8;\n"
" if(mode==='hex') u8 = fromHex(el('editor').value);\n"
" else if(mode==='base64') u8 = fromB64(el('editor').value);\n"
" else u8 = te.encode(el('editor').value);\n"
" const headers = {'Content-Type':'application/octet-stream'};\n"
" const typeTag = el('typeTag').value.trim();\n"
" if(typeTag) headers['X-Amduat-Type-Tag'] = typeTag;\n"
" const putResp = await fetch('/v1/artifacts',{method:'POST',headers,body:u8});\n"
" const putText = await putResp.text();\n"
" if(!putResp.ok){ out(putText); return; }\n"
" const put = JSON.parse(putText);\n"
" const pubResp = await fetch(`/v1/concepts/${encodeURIComponent(name)}/publish`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ref:put.ref})});\n"
" const pubText = await pubResp.text();\n"
" out(pubText);\n"
" if(pubResp.ok) await loadConcept();\n"
" }\n"
"\n"
" el('btnConceptCreate').addEventListener('click', async () => { try{ await ensureConcept(el('conceptName').value.trim()); out('{\"ok\":true}\\n'); }catch(e){ out(String(e)); } });\n"
" el('btnLoad').addEventListener('click', () => loadConcept().catch(e => out(String(e))));\n"
" el('btnSave').addEventListener('click', () => saveConcept().catch(e => out(String(e))));\n"
" el('mode').addEventListener('change', () => {\n"
" if(el('mode').value === 'pel_program'){\n"
" el('typeTag').value = '0x00000101';\n"
" }\n"
" loadConcept().catch(() => {});\n"
" });\n"
"\n"
" el('btnProgramTemplate').addEventListener('click', () => {\n"
" el('mode').value = 'pel_program';\n"
" el('typeTag').value = '0x00000101';\n"
" el('editor').value = JSON.stringify({\n"
" nodes:[{id:1,op:{name:'pel.bytes.concat',version:1},inputs:[{external:{input_index:0}}],params_hex:''}],\n"
" roots:[{node_id:1,output_index:0}]\n"
" }, null, 2);\n"
" });\n"
"\n"
" el('btnPublishRef').addEventListener('click', async () => {\n"
" try{\n"
" const name = el('conceptName').value.trim();\n"
" const ref = el('publishRef').value.trim();\n"
" if(!name||!ref){ out('missing name/ref'); return; }\n"
" await ensureConcept(name);\n"
" const resp = await fetch(`/v1/concepts/${encodeURIComponent(name)}/publish`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ref})});\n"
" out(await resp.text());\n"
" }catch(e){ out(String(e)); }\n"
" });\n"
"\n"
" el('btnUpload').addEventListener('click', async () => {\n"
" try {\n"
" const file = el('uploadFile').files && el('uploadFile').files[0];\n"
" if (!file) { out('no file selected'); return; }\n"
" const resp = await fetch('/v1/artifacts', { method:'POST', headers:{'Content-Type':'application/octet-stream'}, body:file });\n"
" const text = await resp.text();\n"
" out(text);\n"
" if (resp.ok) { const j = JSON.parse(text); if (j && j.ref) el('programRef').value = j.ref; }\n"
" } catch (e) { out(String(e)); }\n"
" });\n"
"\n"
" el('btnRun').addEventListener('click', async () => {\n"
" try {\n"
" const program_ref = el('programRef').value.trim();\n"
" const input_refs = (el('inputRefs').value || '').split(',').map(s => s.trim()).filter(Boolean);\n"
" const params_ref = el('paramsRef').value.trim();\n"
" const scheme_ref = el('schemeRef').value.trim();\n"
" const body = { program_ref, input_refs };\n"
" if (params_ref) body.params_ref = params_ref;\n"
" if (scheme_ref) body.scheme_ref = scheme_ref;\n"
" const resp = await fetch('/v1/pel/run', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) });\n"
" out(await resp.text());\n"
" } catch (e) { out(String(e)); }\n"
" });\n"
"\n"
" async function loadRelations(){\n"
" const resp = await fetch('/v1/relations');\n"
" const text = await resp.text();\n"
" el('relationsOut').textContent = text;\n"
" }\n"
" el('btnRelations').addEventListener('click', () => loadRelations().catch(e => out(String(e))));\n"
" loadRelations().catch(() => {});\n"
" </script>\n"
"</body>\n"
"</html>\n";
static void amduatd_ui_path_without_query(const char *path,
char *out,
size_t cap) {
const char *q = NULL;
size_t n = 0;
if (out == NULL || cap == 0) {
return;
}
out[0] = '\0';
if (path == NULL) {
return;
}
q = strchr(path, '?');
n = q != NULL ? (size_t)(q - path) : strlen(path);
if (n >= cap) {
n = cap - 1;
}
memcpy(out, path, n);
out[n] = '\0';
}
bool amduatd_ui_can_handle(const amduatd_http_req_t *req) {
char no_query[1024];
if (req == NULL) {
return false;
}
if (strcmp(req->method, "GET") != 0) {
return false;
}
amduatd_ui_path_without_query(req->path, no_query, sizeof(no_query));
return strcmp(no_query, "/v1/ui") == 0;
}
bool amduatd_ui_handle(amduatd_ctx_t *ctx,
const amduatd_http_req_t *req,
amduatd_http_resp_t *resp) {
amduat_artifact_t artifact;
amduat_asl_store_error_t err;
if (ctx == NULL || req == NULL || resp == NULL) {
return false;
}
if (!amduatd_ui_can_handle(req)) {
return false;
}
if (ctx->store == NULL || ctx->ui_ref.hash_id == 0 ||
ctx->ui_ref.digest.data == NULL || ctx->ui_ref.digest.len == 0) {
resp->ok = amduatd_http_send_text(resp->fd,
500,
"Internal Server Error",
"ui not available\n",
false);
return true;
}
memset(&artifact, 0, sizeof(artifact));
err = amduat_asl_store_get(ctx->store, ctx->ui_ref, &artifact);
if (err == AMDUAT_ASL_STORE_ERR_NOT_FOUND) {
resp->ok = amduatd_http_send_text(resp->fd,
404,
"Not Found",
"not found\n",
false);
return true;
}
if (err != AMDUAT_ASL_STORE_OK) {
amduat_asl_artifact_free(&artifact);
resp->ok = amduatd_http_send_text(resp->fd,
500,
"Internal Server Error",
"store error\n",
false);
return true;
}
if (artifact.bytes.len != 0 && artifact.bytes.data == NULL) {
amduat_asl_artifact_free(&artifact);
resp->ok = amduatd_http_send_text(resp->fd,
500,
"Internal Server Error",
"store error\n",
false);
return true;
}
resp->ok = amduatd_http_send_status(resp->fd,
200,
"OK",
"text/html; charset=utf-8",
artifact.bytes.data,
artifact.bytes.len,
false);
amduat_asl_artifact_free(&artifact);
return true;
}
bool amduatd_seed_ui_html(amduat_asl_store_t *store,
const amduat_asl_store_fs_config_t *cfg,
amduat_reference_t *out_ref) {
amduat_artifact_t artifact;
amduat_asl_store_error_t err;
if (out_ref != NULL) {
memset(out_ref, 0, sizeof(*out_ref));
}
if (store == NULL || cfg == NULL || out_ref == NULL) {
return false;
}
artifact = amduat_artifact(amduat_octets(k_amduatd_ui_html,
strlen(k_amduatd_ui_html)));
(void)amduat_asl_ref_derive(artifact,
cfg->config.encoding_profile_id,
cfg->config.hash_id,
out_ref,
NULL);
err = amduat_asl_store_put(store, artifact, out_ref);
if (err != AMDUAT_ASL_STORE_OK) {
return false;
}
return true;
}

60
src/amduatd_ui.h Normal file
View file

@ -0,0 +1,60 @@
#ifndef AMDUATD_UI_H
#define AMDUATD_UI_H
#include "amduat/asl/store.h"
#include <stdbool.h>
#include <stddef.h>
#include <sys/types.h>
#ifndef AMDUATD_ENABLE_UI
#define AMDUATD_ENABLE_UI 1
#endif
typedef struct {
char method[8];
char path[1024];
char content_type[128];
char accept[128];
char x_type_tag[64];
char x_capability[2048];
size_t content_length;
bool has_actor;
amduat_octets_t actor;
bool has_uid;
uid_t uid;
} amduatd_http_req_t;
typedef struct {
int fd;
bool ok;
} amduatd_http_resp_t;
typedef struct {
amduat_asl_store_t *store;
amduat_reference_t ui_ref;
} amduatd_ctx_t;
#if AMDUATD_ENABLE_UI
bool amduatd_ui_can_handle(const amduatd_http_req_t *req);
bool amduatd_ui_handle(amduatd_ctx_t *ctx,
const amduatd_http_req_t *req,
amduatd_http_resp_t *resp);
#else
static inline bool amduatd_ui_can_handle(const amduatd_http_req_t *req) {
(void)req;
return false;
}
static inline bool amduatd_ui_handle(amduatd_ctx_t *ctx,
const amduatd_http_req_t *req,
amduatd_http_resp_t *resp) {
(void)ctx;
(void)req;
(void)resp;
return false;
}
#endif
#endif