app
Adom Footprint
Public Made by Adomby adom
KiCad footprint creator with layer HUD viewer and delivery to KiCad/Fusion 360
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
//! adom-footprint live app server — the AI-drivable surface (app-creator §7/§7b/§8).
//! Same shell as adom-symbol: GET / (UI), GET /state (poller), POST /load,
//! POST /eval + GET /eval/pending + POST /eval/:id/result + GET /eval/:id,
//! POST/GET /console, POST /shutdown, GET /favicon.svg. No gallia, no Node.
use anyhow::Result;
use serde_json::{json, Value};
use std::collections::{HashMap, VecDeque};
use std::io::Read;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use tiny_http::{Header, Method, Response, Server};
#[derive(Default, Clone)]
pub struct AppState {
pub footprint_name: String,
pub svg: String,
pub pad_count: usize,
pub description: String,
pub tags: String,
/// Raw `.kicad_mod` retained for "Send to" delivery (empty if loaded via SVG).
pub kicad_mod: String,
/// Library folder name (the chip's `<MPN>/` dir) — what the left library rail
/// highlights as "current". Empty when not loaded from a chip-fetcher library.
pub mpn: String,
}
static STATE: Mutex<Option<AppState>> = Mutex::new(None);
/// Root of the chip-fetcher library (a folder of `<MPN>/` dirs) — powers the left
/// library rail. Set from `serve --file <…/MPN/MPN.kicad_mod>`, else auto-detected.
static LIBRARY_ROOT: Mutex<Option<String>> = Mutex::new(None);
/// On-disk path of the currently-loaded `.kicad_mod` — what `/save` writes back to
/// (so text-placement edits persist per-footprint). None for an in-memory push.
static LOADED_PATH: Mutex<Option<String>> = Mutex::new(None);
static EVAL_PENDING: Mutex<VecDeque<(u64, String)>> = Mutex::new(VecDeque::new());
static EVAL_RESULTS: Mutex<Option<HashMap<u64, Value>>> = Mutex::new(None);
static CONSOLE: Mutex<Option<Vec<String>>> = Mutex::new(None);
static EVAL_ID: AtomicU64 = AtomicU64::new(1);
const APP_HTML: &str = include_str!("app.html");
const AV_CSS: &str = include_str!("viewer_core.css");
const AV_JS: &str = include_str!("viewer_core.js");
/// The app HTML with the shared viewer core (window.AV + av- CSS) injected — the
/// SAME core the static embed uses, so the two viewers can't drift.
fn app_html() -> String {
APP_HTML
.replace("/*__AV_CSS__*/", AV_CSS)
.replace("/*__AV_JS__*/", AV_JS)
}
const FAVICON: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><rect x="6.5" y="9" width="3" height="6" rx="0.6"/><rect x="14.5" y="9" width="3" height="6" rx="0.6"/><path d="M6 6.2h1.2"/></svg>"##;
pub fn set_state(s: AppState) {
*STATE.lock().unwrap() = Some(s);
}
pub fn set_library_root(p: String) {
*LIBRARY_ROOT.lock().unwrap() = Some(p);
}
pub fn set_loaded_path(p: Option<String>) {
*LOADED_PATH.lock().unwrap() = p;
}
/// The library root: the explicitly-set one (from `serve --file`), else the first
/// chip-fetcher library we can find on disk — so the rail + `?mpn=` work even when
/// the app was launched without a file.
fn library_root() -> Option<String> {
if let Some(r) = LIBRARY_ROOT.lock().unwrap().clone() {
return Some(r);
}
let home = std::env::var("HOME").unwrap_or_default();
for c in [
format!("{home}/project/chip-fetcher/library"),
"chip-fetcher/library".to_string(),
"/home/adom/project/chip-fetcher/library".to_string(),
] {
if std::path::Path::new(&c).is_dir() {
return Some(c);
}
}
None
}
/// Pick the footprint file to load for a chip: prefer the canonical `<mpn>.kicad_mod`
/// (what adom-lbr exports from), then the Fusion/Altium variants, then any `.kicad_mod`.
fn footprint_file_for(dir: &std::path::Path, mpn: &str) -> Option<std::path::PathBuf> {
for cand in [
format!("{mpn}.kicad_mod"),
format!("{mpn}.fusion.kicad_mod"),
format!("{mpn}.altium.kicad_mod"),
format!("{mpn}.manufacturer.kicad_mod"),
] {
let p = dir.join(&cand);
if p.is_file() {
return Some(p);
}
}
std::fs::read_dir(dir).ok()?.flatten().map(|e| e.path()).find(|p| {
p.extension().map(|x| x == "kicad_mod").unwrap_or(false)
})
}
/// Every chip in the library (a `<MPN>/` dir with at least one `.kicad_mod`),
/// sorted, with the currently-loaded one flagged. Powers the left library rail.
fn list_chips() -> Vec<(String, bool)> {
let root = match library_root() {
Some(r) => r,
None => return vec![],
};
let cur = STATE.lock().unwrap().as_ref().map(|s| s.mpn.clone()).unwrap_or_default();
let mut out = Vec::new();
if let Ok(rd) = std::fs::read_dir(&root) {
for e in rd.flatten() {
let p = e.path();
if !p.is_dir() {
continue;
}
let name = e.file_name().to_string_lossy().to_string();
if footprint_file_for(&p, &name).is_some() {
let is_cur = name == cur;
out.push((name, is_cur));
}
}
}
out.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));
out
}
/// Load a chip's footprint from the library into app state (rendered with the
/// solder-paste group baked, like /load). Returns false if not found.
fn load_chip(mpn: &str) -> bool {
let root = match library_root() {
Some(r) => r,
None => return false,
};
let dir = std::path::Path::new(&root).join(mpn);
let file = match footprint_file_for(&dir, mpn) {
Some(f) => f,
None => return false,
};
let content = match std::fs::read_to_string(&file) {
Ok(c) => c,
Err(_) => return false,
};
let name = crate::first_footprint_name(&content).unwrap_or_else(|| mpn.to_string());
match crate::render::render_footprint_svg_with_paste(&content, crate::bbox_from_mod(&content)) {
Ok(svg) => {
*LOADED_PATH.lock().unwrap() = Some(file.to_string_lossy().to_string());
set_state(AppState {
footprint_name: name,
svg,
pad_count: content.matches("(pad ").count(),
description: String::new(),
tags: String::new(),
kicad_mod: content,
mpn: mpn.to_string(),
});
true
}
Err(_) => false,
}
}
/// The 3D preview PNG for the current chip (chip-thumbnailer output), if any.
fn chip_3d_png() -> Option<Vec<u8>> {
let root = library_root()?;
let mpn = STATE.lock().unwrap().as_ref().map(|s| s.mpn.clone()).unwrap_or_default();
if mpn.is_empty() {
return None;
}
let dir = std::path::Path::new(&root).join(&mpn);
for cand in [
format!("{mpn}-3d-iso-named.png"),
format!("{mpn}-3d-iso.png"),
format!("{mpn}-3d-iso-lg.png"),
] {
if let Ok(b) = std::fs::read(dir.join(&cand)) {
return Some(b);
}
}
None
}
/// Chip metadata for the right-panel "Chip" details — so the user can see WHAT
/// part they're working on, not just an anonymous pad pattern. Merged from the
/// library's `<mpn>-footprint.extracted.json` and `info.json`.
fn chip_details(mpn: &str) -> Value {
let root = match library_root() {
Some(r) => r,
None => return Value::Null,
};
if mpn.is_empty() {
return Value::Null;
}
let dir = std::path::Path::new(&root).join(mpn);
let read_json = |name: String| -> Value {
std::fs::read_to_string(dir.join(name))
.ok()
.and_then(|s| serde_json::from_str::<Value>(&s).ok())
.unwrap_or(Value::Null)
};
let ext = read_json(format!("{mpn}-footprint.extracted.json"));
let info = read_json("info.json".to_string());
// Prefer the extraction, fall back to info.json.
let pick = |k: &str| -> Value {
let a = ext.get(k).cloned().unwrap_or(Value::Null);
if a.is_null() { info.get(k).cloned().unwrap_or(Value::Null) } else { a }
};
let has3d = dir.join(format!("{mpn}-3d-iso-named.png")).is_file()
|| dir.join(format!("{mpn}-3d-iso.png")).is_file();
json!({
"mpn": mpn,
"manufacturer": pick("manufacturer"),
"package": ext.get("package").cloned().unwrap_or(Value::Null),
"bodyDimensions": ext.get("bodyDimensions").cloned().unwrap_or(Value::Null),
"padCount": ext.get("padCount").cloned().unwrap_or(Value::Null),
"family": info.get("family").cloned().unwrap_or(Value::Null),
"description": pick("description"),
"datasheet": info.get("datasheet_url").cloned().unwrap_or(Value::Null),
"has3d": has3d,
})
}
fn state_json() -> String {
match &*STATE.lock().unwrap() {
Some(s) => {
let aps = crate::paste::paste_apertures(&s.kicad_mod);
let dot_count = crate::paste::paste_overlay_svg(&aps, 0.0, 0.0).2;
let (minx, miny, maxx, maxy) = crate::bbox_from_mod(&s.kicad_mod);
json!({
"footprintName": s.footprint_name, "svg": s.svg, "padCount": s.pad_count,
"description": s.description, "tags": s.tags, "hasMod": !s.kicad_mod.is_empty(),
"mpn": s.mpn,
"pasteApertures": aps.len(),
"pasteDots": dot_count,
// For the >NAME/>VALUE editor: current text positions + the
// keep-out bbox (pads ∪ rects) the Auto-layout button clears.
"bbox": { "minx": minx, "miny": miny, "maxx": maxx, "maxy": maxy },
"text": crate::fp_text_state(&s.kicad_mod),
// For the right-panel "Chip" details (what part is this?).
"chip": chip_details(&s.mpn),
// Whether the placement editor can persist to disk (loaded from a file).
"canSave": LOADED_PATH.lock().unwrap().is_some() && !s.kicad_mod.is_empty(),
})
.to_string()
}
None => "{}".to_string(),
}
}
pub fn run(port: u16) -> Result<()> {
*EVAL_RESULTS.lock().unwrap() = Some(HashMap::new());
*CONSOLE.lock().unwrap() = Some(Vec::new());
let server = Server::http(format!("0.0.0.0:{port}")).map_err(|e| anyhow::anyhow!("bind {port}: {e}"))?;
eprintln!("[adom-footprint] serving on http://127.0.0.1:{port}");
register_port(port, "adom-footprint", "PCB footprint viewer live app (AI-drivable)");
for mut req in server.incoming_requests() {
let url = req.url().split('?').next().unwrap_or("/").to_string();
let method = req.method().clone();
let mut body = String::new();
if matches!(method, Method::Post) {
let _ = req.as_reader().read_to_string(&mut body);
}
let resp = route(&method, &url, &body);
let _ = req.respond(resp);
if url == "/shutdown" && matches!(method, Method::Post) {
break;
}
}
unregister_port(port);
Ok(())
}
/// Best-effort HD port registration (app-creator §6a / adom-port-hints).
/// Raw TCP so we add no HTTP-client dependency; never blocks serving, never
/// fails the app if HD is absent (outside Hydrogen Desktop).
fn register_port(port: u16, name: &str, reason: &str) {
let body = format!(
"{{\"port\":{port},\"action\":\"register\",\"name\":\"{name}\",\"reason\":\"{reason}\",\"expose\":\"internal\"}}"
);
std::thread::spawn(move || hd_post(&body));
}
fn unregister_port(port: u16) {
hd_post(&format!("{{\"port\":{port},\"action\":\"unregister\"}}"));
}
fn hd_post(body: &str) {
use std::io::Write;
use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;
let addr = match ("host.docker.internal", 47084u16).to_socket_addrs().ok().and_then(|mut a| a.next()) {
Some(a) => a,
None => return,
};
if let Ok(mut s) = TcpStream::connect_timeout(&addr, Duration::from_millis(800)) {
let _ = s.set_write_timeout(Some(Duration::from_millis(800)));
let req = format!(
"POST /port-forward HTTP/1.1\r\nHost: host.docker.internal\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
let _ = s.write_all(req.as_bytes());
}
}
fn route(method: &Method, url: &str, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
match (method, url) {
(Method::Get, "/") | (Method::Get, "/index.html") => html(&app_html()),
// The standalone interactive embed for the currently-loaded footprint —
// opened by the app's "Embed" button (new tab), the same lean viewer the
// `embed` CLI writes to a file.
(Method::Get, "/embed") => {
let (name, km) = {
let g = STATE.lock().unwrap();
match g.as_ref() {
Some(s) if !s.kicad_mod.is_empty() => (s.footprint_name.clone(), s.kicad_mod.clone()),
_ => (String::new(), String::new()),
}
};
if km.is_empty() {
html("<!doctype html><meta charset=utf-8><body style=\"background:#0d1117;color:#8b949e;font-family:sans-serif;padding:24px\">No footprint loaded to embed.</body>")
} else {
match crate::build_embed_html(&km, &name) {
Ok(h) => html(&h),
Err(e) => { let _ = e; html("<!doctype html><body>embed render failed</body>") }
}
}
}
(Method::Get, "/favicon.svg") => svg(FAVICON),
(Method::Get, "/state") => json_resp(&state_json()),
// Left library rail: list every chip in the chip-fetcher library, mirror its
// project groupings, and load a chosen chip's footprint.
(Method::Get, "/api/chips") => {
let arr: Vec<Value> = list_chips().into_iter().map(|(m, c)| json!({ "mpn": m, "current": c })).collect();
let cur = STATE.lock().unwrap().as_ref().map(|s| s.mpn.clone()).unwrap_or_default();
json_resp(&json!({ "chips": arr, "current": cur }).to_string())
}
(Method::Get, "/api/projects") => {
let home = std::env::var("HOME").unwrap_or_default();
let path = std::path::Path::new(&home).join(".config/adom-chip-fetcher/projects.json");
json_resp(&std::fs::read_to_string(&path).unwrap_or_else(|_| "{\"projects\":{}}".to_string()))
}
// The current chip's 3D preview PNG (serves state.mpn; the client cache-busts
// with a ?mpn= query, which we ignore — the path is what's routed).
(Method::Get, "/api/chip3d") => match chip_3d_png() {
Some(b) => png(b),
None => not_found(),
},
(Method::Post, "/load-chip") => {
let mpn = serde_json::from_str::<Value>(body).ok()
.and_then(|v| v.get("mpn").and_then(|x| x.as_str()).map(String::from))
.unwrap_or_default();
if mpn.is_empty() {
json_err(400, "mpn required")
} else if load_chip(&mpn) {
json_resp(&json!({ "ok": true, "mpn": mpn }).to_string())
} else {
json_err(404, "chip not found in library")
}
}
(Method::Post, "/load") => {
// An in-memory push has no source file → can't /save back to disk.
*LOADED_PATH.lock().unwrap() = None;
if let Ok(v) = serde_json::from_str::<Value>(body) {
// Preferred: push raw .kicad_mod; the server renders it via
// service-kicad (mirrors adom-step's /api/load). This is how
// chip-fetcher drives the app — push a file, it renders.
if let Some(km) = v.get("kicadMod").and_then(|x| x.as_str()) {
let name = v
.get("footprintName")
.and_then(|x| x.as_str())
.map(String::from)
.or_else(|| crate::first_footprint_name(km))
.unwrap_or_else(|| "footprint".to_string());
// Bake the paste group so the live Layers panel's InstaPCB
// toggle can show/hide it — but the client defaults it OFF
// (app.html: paste-dots is opt-in), so the initial view is clean.
match crate::render::render_footprint_svg_with_paste(km, crate::bbox_from_mod(km)) {
Ok(svg) => set_state(AppState {
footprint_name: name,
svg,
pad_count: km.matches("(pad ").count(),
description: v.get("description").and_then(|x| x.as_str()).unwrap_or("").to_string(),
tags: v.get("tags").and_then(|x| x.as_str()).unwrap_or("").to_string(),
kicad_mod: km.to_string(),
mpn: v.get("mpn").and_then(|x| x.as_str()).unwrap_or("").to_string(),
}),
Err(e) => return json_err(422, &e.to_string()),
}
} else {
set_state(AppState {
footprint_name: v.get("footprintName").and_then(|x| x.as_str()).unwrap_or("").to_string(),
svg: v.get("svg").and_then(|x| x.as_str()).unwrap_or("").to_string(),
pad_count: v.get("padCount").and_then(|x| x.as_u64()).unwrap_or(0) as usize,
description: v.get("description").and_then(|x| x.as_str()).unwrap_or("").to_string(),
tags: v.get("tags").and_then(|x| x.as_str()).unwrap_or("").to_string(),
kicad_mod: v.get("kicadMod").and_then(|x| x.as_str()).unwrap_or("").to_string(),
mpn: v.get("mpn").and_then(|x| x.as_str()).unwrap_or("").to_string(),
});
}
}
json_resp("{\"ok\":true}")
}
(Method::Post, "/eval") => {
let code = serde_json::from_str::<Value>(body)
.ok()
.and_then(|v| v.get("code").and_then(|c| c.as_str()).map(String::from))
.unwrap_or_default();
let id = EVAL_ID.fetch_add(1, Ordering::SeqCst);
EVAL_PENDING.lock().unwrap().push_back((id, code));
json_resp(&json!({ "id": id.to_string() }).to_string())
}
(Method::Get, "/eval/pending") => match EVAL_PENDING.lock().unwrap().pop_front() {
Some((id, code)) => json_resp(&json!({ "id": id.to_string(), "code": code }).to_string()),
None => json_resp("{}"),
},
(Method::Get, "/console") => {
let log = CONSOLE.lock().unwrap().clone().unwrap_or_default();
json_resp(&json!({ "lines": log }).to_string())
}
(Method::Post, "/console") => {
if let Ok(v) = serde_json::from_str::<Value>(body) {
let line = format!(
"[{}] {}",
v.get("level").and_then(|l| l.as_str()).unwrap_or("log"),
v.get("msg").and_then(|m| m.as_str()).unwrap_or("")
);
if let Some(buf) = CONSOLE.lock().unwrap().as_mut() {
buf.push(line);
let len = buf.len();
if len > 500 {
buf.drain(0..len - 500);
}
}
}
json_resp("{\"ok\":true}")
}
(Method::Post, "/deliver") => {
let v = serde_json::from_str::<Value>(body).unwrap_or(json!({}));
let target = v.get("target").and_then(|x| x.as_str()).unwrap_or("kicad").to_string();
let (name, km) = match &*STATE.lock().unwrap() {
Some(s) => (s.footprint_name.clone(), s.kicad_mod.clone()),
None => (String::new(), String::new()),
};
json_resp(&crate::deliver::deliver(&name, &km, &target).to_string())
}
// Bake new >NAME/>VALUE positions into the loaded .kicad_mod so delivery
// and export carry the placement the user set in the editor.
(Method::Post, "/text") => {
let v = serde_json::from_str::<Value>(body).unwrap_or(json!({}));
let num = |k: &str, sub: &str| v.get(k).and_then(|o| o.get(sub)).and_then(|n| n.as_f64());
match (num("reference", "x"), num("reference", "y"), num("value", "x"), num("value", "y")) {
(Some(rx), Some(ry), Some(vx), Some(vy)) => {
let mut st = STATE.lock().unwrap();
match st.as_mut() {
Some(s) if !s.kicad_mod.is_empty() => {
s.kicad_mod = crate::rewrite_fp_text(&s.kicad_mod, rx, ry, vx, vy);
// Optional font-size normalization (Auto-layout scales tiny parts).
if let Some(rs) = num("reference", "size") {
s.kicad_mod = crate::set_fp_text_size(&s.kicad_mod, "reference", rs);
}
if let Some(vs) = num("value", "size") {
s.kicad_mod = crate::set_fp_text_size(&s.kicad_mod, "value", vs);
}
json_resp(&json!({ "ok": true, "text": crate::fp_text_state(&s.kicad_mod) }).to_string())
}
_ => json_err(422, "no .kicad_mod loaded to edit"),
}
}
_ => json_err(400, "need reference.{x,y} and value.{x,y}"),
}
}
// Persist the current (text-edited) .kicad_mod back to its library file, so
// placement changes stick per-footprint. Backs the original up once to
// `<file>.orig` before the first overwrite.
(Method::Post, "/save") => {
let path = LOADED_PATH.lock().unwrap().clone();
let km = STATE.lock().unwrap().as_ref().map(|s| s.kicad_mod.clone()).unwrap_or_default();
match path {
Some(p) if !km.is_empty() => {
let orig = format!("{p}.orig");
if !std::path::Path::new(&orig).exists() {
if let Ok(existing) = std::fs::read(&p) {
let _ = std::fs::write(&orig, existing);
}
}
match std::fs::write(&p, &km) {
Ok(_) => json_resp(&json!({ "ok": true, "path": p }).to_string()),
Err(e) => json_err(500, &e.to_string()),
}
}
_ => json_err(422, "no file to save (footprint was pushed, not loaded from disk)"),
}
}
(Method::Post, "/shutdown") => json_resp("{\"ok\":true}"),
_ if url.starts_with("/eval/") => {
let rest = &url["/eval/".len()..];
if let Some(id_str) = rest.strip_suffix("/result") {
if matches!(method, Method::Post) {
if let (Ok(id), Ok(v)) = (id_str.parse::<u64>(), serde_json::from_str::<Value>(body)) {
if let Some(map) = EVAL_RESULTS.lock().unwrap().as_mut() {
map.insert(id, v);
}
}
return json_resp("{\"ok\":true}");
}
}
if let Ok(id) = rest.parse::<u64>() {
let map = EVAL_RESULTS.lock().unwrap();
if let Some(r) = map.as_ref().and_then(|m| m.get(&id)) {
return json_resp(&json!({ "done": true, "result": r }).to_string());
}
return json_resp("{\"done\":false}");
}
not_found()
}
_ => not_found(),
}
}
fn hdr(ct: &str) -> Header {
Header::from_bytes(&b"Content-Type"[..], ct.as_bytes()).unwrap()
}
fn html(s: &str) -> Response<std::io::Cursor<Vec<u8>>> {
Response::from_string(s).with_header(hdr("text/html; charset=utf-8"))
}
fn svg(s: &str) -> Response<std::io::Cursor<Vec<u8>>> {
Response::from_string(s).with_header(hdr("image/svg+xml"))
}
fn json_resp(s: &str) -> Response<std::io::Cursor<Vec<u8>>> {
Response::from_string(s).with_header(hdr("application/json"))
}
fn png(data: Vec<u8>) -> Response<std::io::Cursor<Vec<u8>>> {
Response::from_data(data).with_header(hdr("image/png"))
}
fn not_found() -> Response<std::io::Cursor<Vec<u8>>> {
Response::from_string("{\"error\":\"not found\"}").with_status_code(404).with_header(hdr("application/json"))
}
fn json_err(code: u16, msg: &str) -> Response<std::io::Cursor<Vec<u8>>> {
let body = json!({ "ok": false, "error": msg }).to_string();
Response::from_string(body).with_status_code(code).with_header(hdr("application/json"))
}