A glyph's UI is an attestation. You publish one, and every page in that namespace redraws it where it stands — no node restart, no rebuild of QNTX, no file on disk.
qntx glyph publish crier --file dist/glyph-module.js --to https://api.q.abcd.nl
qntx glyph list --to https://api.q.abcd.nl
Subject glyph-<name>, predicate glyph:module, the module's source under the
source attribute. Publishing again supersedes: the store answers newest
first, so the last one published is the one served, and the ones before it stay
where they are.
The node serves what is published from /g/<name>.js. That is a root of its
own rather than a path under /api, because a page imports what it renders and
an import is governed by script-src, which names origins and not routes. An
edge can put /g/ on the node while the rest of the page stays where it is,
and the module is then same-origin with the page that imports it.
/g/ is of ANYONE. A glyph module is UI, and UI is not a boundary — every
call it makes is gated against whoever made it. What is served is what was
published; a glyph written and not yet public is an attestation this route does
not read.
Two things:
export const glyphDef = {
symbol: '📯', // what it is drawn as
title: 'CRIER',
label: 'crier',
manifestation: 'panel', // omit for the canvas; 'panel' puts it in the tray
defaultWidth: 960,
defaultHeight: 640,
}
export const render = (glyph, ui) => {
const { element, content } = ui.glyph({
defaults: { x: glyph.x ?? 200, y: glyph.y ?? 200, width: 960, height: 640 },
titleBar: { label: glyphDef.title },
resizable: true,
})
content.textContent = 'hello'
return element
}
ui.glyph() builds the frame: position, drag, resize, the title bar, and the ⬆
that lifts the glyph off the canvas into a window and puts it back. A module
that returns a bare element instead gets that frame wrapped around it.
What else ui carries: log, onCleanup, attestations(query),
pluginFetch, pluginWebSocket, onMeld, loadConfig/saveConfig,
input/button/statusLine, spawnResult. Register every timer, socket and
subscription through onCleanup — it runs when the glyph closes and again
before a republished module draws in its place. Without it, publishing twice
leaves the first one still running underneath the second.
One ES module, no import map, nothing fetched at runtime from another origin.
Bundle whatever it needs into the file. CRIER's build is an example: Svelte
compiled and bundled by bun build into a single dist/glyph-module.js, which
is the file handed to --file.
Nothing about the build is prescribed. The node stores a string and serves it
with Content-Type: text/javascript; what produced that string is yours.
standing-glyph-published matches on the predicate. It is a watcher every
node is born with, held in no store, and no route can delete or disable it.
It runs nothing — its action type is tell, which reaches browsers and no
code anywhere./g/, see an attestation id they are not
holding, and import the new module.--to names the node. Without it, the one this machine runs, on the port
am.toml gives.
The token is --token, then $QNTX_TOKEN, then ~/.qntx/token. A refusal
names which of those it used, so a 403 tells you which credential was refused
rather than that something was.
A glyph is published into the namespace the caller acts in, and served to that namespace alone. Namespaces are their own universes and nothing crosses (ADR-026) — a glyph published in one is not on another's canvas.
The glyph draws its own absence. Where it should be, you get a frame that says what the node answered: published and not yet loaded, published and failed to load with the reason, or nothing published under that name. Nobody should have to open a console to find out why a glyph is missing.
/g/ refuses in ways that survive an edge: 410 when nothing is published
under that name, 422 when the ask is malformed. Neither is in CloudFront's
rewritable set, so a refusal cannot come back as an HTML page that fails as a
parse error naming nothing.
server/glyph_module_handlers.go — what /g/ servesats/watcher/standing.go — the watcher every node is born with