feat: unify card runtime and event-driven web ui
Some checks failed
CI / Backend Checks (push) Failing after 36s
CI / Frontend Checks (push) Failing after 40s

This commit is contained in:
kacper 2026-04-06 15:42:53 -04:00
parent 0edf8c3fef
commit 4dfb7ca3cc
105 changed files with 17382 additions and 8505 deletions

View file

@ -0,0 +1,24 @@
export function mount({ root, state, host }) {
const render = (nextState) => {
const title = typeof nextState.title === "string" ? nextState.title : "";
root.innerHTML = `<div data-fixture-title="${title}">${title}</div>`;
};
render(state);
host.setLiveContent({ phase: "mount", title: state.title ?? "" });
host.setRefreshHandler(() => {
root.dataset.refreshed = "1";
});
return {
update({ state: nextState, host: nextHost }) {
render(nextState);
nextHost.setLiveContent({ phase: "update", title: nextState.title ?? "" });
},
destroy() {
root.dataset.destroyed = "1";
root.innerHTML = "";
host.setRefreshHandler(null);
},
};
}

View file

@ -0,0 +1,119 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import subprocess
from pathlib import Path
LIVE_TEMPLATES_DIR = Path.home() / ".nanobot" / "cards" / "templates"
EXAMPLE_TEMPLATES_DIR = (
Path(__file__).resolve().parent.parent / "examples" / "cards" / "templates"
)
LEGACY_MARKERS = (
"__nanobot",
"document.currentScript",
"mountLegacyTemplate",
"legacy-template-module",
)
def iter_template_dirs(root: Path) -> dict[str, Path]:
templates: dict[str, Path] = {}
if not root.exists():
return templates
for child in sorted(root.iterdir()):
if not child.is_dir():
continue
if not (child / "manifest.json").exists():
continue
templates[child.name] = child
return templates
def validate_template_dir(template_dir: Path, failures: list[str]) -> None:
manifest_path = template_dir / "manifest.json"
template_path = template_dir / "template.html"
runtime_path = template_dir / "card.js"
for required in (manifest_path, template_path, runtime_path):
if not required.exists():
failures.append(f"{template_dir}: missing {required.name}")
return
try:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
except Exception as exc:
failures.append(f"{manifest_path}: invalid JSON ({exc})")
return
if not isinstance(manifest, dict):
failures.append(f"{manifest_path}: manifest must be an object")
template_html = template_path.read_text(encoding="utf-8")
if "<script" in template_html.lower():
failures.append(f"{template_path}: inline script tags are not allowed")
runtime_js = runtime_path.read_text(encoding="utf-8")
if "export function mount" not in runtime_js:
failures.append(f"{runtime_path}: missing `export function mount`")
for marker in LEGACY_MARKERS:
if marker in runtime_js:
failures.append(f"{runtime_path}: legacy marker `{marker}` still present")
result = subprocess.run(
["node", "--check", str(runtime_path)],
capture_output=True,
text=True,
)
if result.returncode != 0:
detail = (result.stderr or result.stdout).strip()
failures.append(f"{runtime_path}: node --check failed: {detail}")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--live-root", type=Path, default=LIVE_TEMPLATES_DIR)
parser.add_argument("--example-root", type=Path, default=EXAMPLE_TEMPLATES_DIR)
args = parser.parse_args()
live_root = args.live_root.expanduser()
example_root = args.example_root.expanduser()
failures: list[str] = []
for template_dir in iter_template_dirs(live_root).values():
validate_template_dir(template_dir, failures)
for template_dir in iter_template_dirs(example_root).values():
validate_template_dir(template_dir, failures)
sync_check = subprocess.run(
[
"python3",
str(Path(__file__).resolve().parent / "sync_card_templates.py"),
"--check",
"--source",
str(live_root),
"--dest",
str(example_root),
],
capture_output=True,
text=True,
)
if sync_check.returncode != 0:
detail = (sync_check.stdout + sync_check.stderr).strip()
if detail:
print(detail)
failures.append("template mirror drift detected")
if failures:
for failure in failures:
print(failure)
return 1
print("card runtime ok")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,57 @@
import assert from "node:assert/strict";
import { mount } from "./card_runtime_fixture_card.mjs";
function createFakeRoot() {
return {
dataset: {},
innerHTML: "",
};
}
async function main() {
const root = createFakeRoot();
const liveContentCalls = [];
const refreshHandlers = [];
const host = {
setLiveContent(value) {
liveContentCalls.push(value);
},
setRefreshHandler(handler) {
refreshHandlers.push(handler);
},
};
const mounted = mount({
root,
state: { title: "alpha" },
host,
});
assert.equal(typeof mounted?.update, "function");
assert.equal(typeof mounted?.destroy, "function");
assert.match(root.innerHTML, /alpha/);
assert.deepEqual(liveContentCalls.at(-1), { phase: "mount", title: "alpha" });
assert.equal(typeof refreshHandlers.at(-1), "function");
refreshHandlers.at(-1)?.();
assert.equal(root.dataset.refreshed, "1");
mounted.update({
root,
state: { title: "beta" },
host,
});
assert.match(root.innerHTML, /beta/);
assert.deepEqual(liveContentCalls.at(-1), { phase: "update", title: "beta" });
mounted.destroy();
assert.equal(root.innerHTML, "");
assert.equal(root.dataset.destroyed, "1");
assert.equal(refreshHandlers.at(-1), null);
console.log("card runtime fixture ok");
}
await main();

View file

@ -5,6 +5,19 @@ status=0
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
python_files=(
"app.py"
"app_dependencies.py"
"card_store.py"
"inbox_service.py"
"message_pipeline.py"
"nanobot_api_client.py"
"route_helpers.py"
"rtc_manager.py"
"session_store.py"
"tool_job_service.py"
"ui_event_bus.py"
"web_runtime.py"
"workbench_store.py"
"routes"
)
run_check() {
@ -33,7 +46,7 @@ run_check \
uv run --with "deptry>=0.24.0,<1.0.0" \
deptry . \
--requirements-files requirements.txt \
--known-first-party app,supertonic_gateway,voice_rtc,wisper \
--known-first-party app,card_store,supertonic_gateway,sync_card_templates,voice_rtc,wisper \
--per-rule-ignores DEP002=uvicorn \
--extend-exclude ".*/frontend/.*" \
--extend-exclude ".*/\\.venv/.*" \
@ -42,5 +55,14 @@ run_check \
"Vulture" \
uv run --with "vulture>=2.15,<3.0.0" \
vulture "${python_files[@]}" --min-confidence 80
run_check \
"Card Runtime" \
python3 scripts/check_card_runtime.py
run_check \
"Card Runtime Fixture" \
node scripts/check_card_runtime_fixture.mjs
run_check \
"Backend Tests" \
.venv/bin/python -m unittest discover -s tests -p "test_*.py"
exit "${status}"

View file

@ -0,0 +1,139 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import filecmp
import os
import shutil
from pathlib import Path
LIVE_TEMPLATES_DIR = Path.home() / ".nanobot" / "cards" / "templates"
EXAMPLE_TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "examples" / "cards" / "templates"
def iter_template_dirs(root: Path) -> dict[str, Path]:
templates: dict[str, Path] = {}
if not root.exists():
return templates
for child in sorted(root.iterdir()):
if not child.is_dir():
continue
if not (child / "manifest.json").exists():
continue
templates[child.name] = child
return templates
def sync_directory(source: Path, dest: Path) -> None:
dest.mkdir(parents=True, exist_ok=True)
source_entries = {path.name: path for path in source.iterdir()}
dest_entries = {path.name: path for path in dest.iterdir()} if dest.exists() else {}
for name, source_path in source_entries.items():
dest_path = dest / name
if source_path.is_dir():
sync_directory(source_path, dest_path)
continue
dest_path.parent.mkdir(parents=True, exist_ok=True)
if not dest_path.exists() or not filecmp.cmp(source_path, dest_path, shallow=False):
shutil.copy2(source_path, dest_path)
for name, dest_path in dest_entries.items():
if name in source_entries:
continue
if dest_path.is_dir():
shutil.rmtree(dest_path)
else:
dest_path.unlink(missing_ok=True)
def compare_directory(source: Path, dest: Path, drifts: list[str], *, prefix: str) -> None:
source_entries = {path.name: path for path in source.iterdir()} if source.exists() else {}
dest_entries = {path.name: path for path in dest.iterdir()} if dest.exists() else {}
for name, source_path in source_entries.items():
rel = f"{prefix}/{name}" if prefix else name
dest_path = dest / name
if name not in dest_entries:
drifts.append(f"missing mirror: {rel}")
continue
if source_path.is_dir():
if not dest_path.is_dir():
drifts.append(f"type mismatch: {rel}")
continue
compare_directory(source_path, dest_path, drifts, prefix=rel)
continue
if dest_path.is_dir():
drifts.append(f"type mismatch: {rel}")
continue
if not filecmp.cmp(source_path, dest_path, shallow=False):
drifts.append(f"content drift: {rel}")
for name in dest_entries:
if name not in source_entries:
rel = f"{prefix}/{name}" if prefix else name
drifts.append(f"stale mirror: {rel}")
def run_check(source_root: Path, dest_root: Path) -> int:
drifts: list[str] = []
source_templates = iter_template_dirs(source_root)
dest_templates = iter_template_dirs(dest_root)
for name, source_dir in source_templates.items():
dest_dir = dest_root / name
if name not in dest_templates:
drifts.append(f"missing mirror: {name}")
continue
compare_directory(source_dir, dest_dir, drifts, prefix=name)
for name in dest_templates:
if name not in source_templates:
drifts.append(f"stale mirror: {name}")
if drifts:
for drift in drifts:
print(drift)
return 1
print("template sync ok")
return 0
def run_sync(source_root: Path, dest_root: Path) -> int:
source_templates = iter_template_dirs(source_root)
dest_root.mkdir(parents=True, exist_ok=True)
for name, source_dir in source_templates.items():
sync_directory(source_dir, dest_root / name)
for child in list(dest_root.iterdir()):
if child.name in source_templates:
continue
if child.is_dir():
shutil.rmtree(child)
else:
child.unlink(missing_ok=True)
print(f"synced {len(source_templates)} template directories")
return 0
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--check", action="store_true")
parser.add_argument("--source", type=Path, default=LIVE_TEMPLATES_DIR)
parser.add_argument("--dest", type=Path, default=EXAMPLE_TEMPLATES_DIR)
args = parser.parse_args()
source_root = args.source.expanduser()
dest_root = args.dest.expanduser()
if args.check:
return run_check(source_root, dest_root)
return run_sync(source_root, dest_root)
if __name__ == "__main__":
raise SystemExit(main())