Extract amduatd UI module
This commit is contained in:
parent
507007e865
commit
d07dae5252
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
||||||
|
|
|
||||||
445
src/amduatd.c
445
src/amduatd.c
|
|
@ -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,13 +3719,13 @@ 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,
|
||||||
const uint8_t *body,
|
const uint8_t *body,
|
||||||
size_t body_len,
|
size_t body_len,
|
||||||
bool head_only) {
|
bool head_only) {
|
||||||
char hdr[512];
|
char hdr[512];
|
||||||
int n = snprintf(hdr,
|
int n = snprintf(hdr,
|
||||||
sizeof(hdr),
|
sizeof(hdr),
|
||||||
|
|
@ -4070,11 +3750,11 @@ 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,
|
||||||
bool head_only) {
|
bool head_only) {
|
||||||
const uint8_t *body = (const uint8_t *)(text != NULL ? text : "");
|
const uint8_t *body = (const uint8_t *)(text != NULL ? text : "");
|
||||||
size_t len = text != NULL ? strlen(text) : 0;
|
size_t len = text != NULL ? strlen(text) : 0;
|
||||||
return amduatd_http_send_status(
|
return amduatd_http_send_status(
|
||||||
|
|
@ -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,9 +9525,20 @@ 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;
|
||||||
goto conn_cleanup;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strcmp(req.method, "GET") == 0 && strcmp(no_query, "/v1/meta") == 0) {
|
if (strcmp(req.method, "GET") == 0 && strcmp(no_query, "/v1/meta") == 0) {
|
||||||
|
|
@ -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
461
src/amduatd_ui.c
Normal 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
60
src/amduatd_ui.h
Normal 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
|
||||||
Loading…
Reference in a new issue