P4.2: HTML index at / — the install-page seed
GET / now serves a dense server-rendered console: each app with its channels' current releases and every release's artifacts as direct /download/<sha256> links. Read-only off the per-request db.json reload; text escaped for HTML. A browser hitting the server root sees the store instead of a 404.
This commit is contained in:
@@ -45,7 +45,7 @@ emit_human :: (s: string, json_mode: bool) {
|
||||
if json_mode { eputs(s); } else { out(s); }
|
||||
}
|
||||
|
||||
HELP :: "dist — application distribution CLI\n\nUsage:\n dist <group> <command> [flags]\n\nGroups & commands:\n ci\n ci publish publish a release from CI\n --manifest <path> publish manifest (dist.json) to read\n --local-store <dir> local artifact store + db.json directory\n release\n release promote point a channel at a release\n --app <slug> app the channel belongs to\n --channel <name> channel to move\n --release <id> release id to promote\n --local-store <dir> local artifact store + db.json directory\n release rollback move a channel back to its previous release\n --app <slug> app the channel belongs to\n --channel <name> channel to roll back\n --local-store <dir> local artifact store + db.json directory\n server\n server run serve the store read-only over HTTP (0.0.0.0)\n --local-store <dir> local artifact store + db.json directory\n --port <n> TCP port (default 8787)\n routes: /healthz, /api/apps, /api/apps/<slug>, /download/<sha256>\n\nGlobal flags:\n --json emit machine-readable JSON on stdout; human text to stderr\n -h, --help show this help and exit\n\nExit codes:\n 0 success\n 1 command failed (publish/promote/rollback aborted or server could not bind)\n 64 usage error (no command, or an unknown/missing command or flag)\n";
|
||||
HELP :: "dist — application distribution CLI\n\nUsage:\n dist <group> <command> [flags]\n\nGroups & commands:\n ci\n ci publish publish a release from CI\n --manifest <path> publish manifest (dist.json) to read\n --local-store <dir> local artifact store + db.json directory\n release\n release promote point a channel at a release\n --app <slug> app the channel belongs to\n --channel <name> channel to move\n --release <id> release id to promote\n --local-store <dir> local artifact store + db.json directory\n release rollback move a channel back to its previous release\n --app <slug> app the channel belongs to\n --channel <name> channel to roll back\n --local-store <dir> local artifact store + db.json directory\n server\n server run serve the store read-only over HTTP (0.0.0.0)\n --local-store <dir> local artifact store + db.json directory\n --port <n> TCP port (default 8787)\n routes: / (HTML index), /healthz, /api/apps, /api/apps/<slug>, /download/<sha256>\n\nGlobal flags:\n --json emit machine-readable JSON on stdout; human text to stderr\n -h, --help show this help and exit\n\nExit codes:\n 0 success\n 1 command failed (publish/promote/rollback aborted or server could not bind)\n 64 usage error (no command, or an unknown/missing command or flag)\n";
|
||||
|
||||
// True if `name` appears as a token in `args`.
|
||||
has_flag :: (args: []string, name: string) -> bool {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// Serves the state the CLI publishes — db.json metadata and the
|
||||
// content-addressed objects — over HTTP (src/server/http.sx):
|
||||
//
|
||||
// GET / HTML index: apps, channels, releases, links
|
||||
// GET /healthz {"status":"ok"} — no store access
|
||||
// GET /api/apps {"apps":[...]} every app in the store
|
||||
// GET /api/apps/<slug> {"app":..,"releases":[..],"channels":[..]}
|
||||
@@ -180,6 +181,103 @@ respond_render :: (client: s32, buf: string, n: s64, overflowed: bool) {
|
||||
http.respond(client, 200, "application/json", "", string.{ ptr = buf.ptr, len = n });
|
||||
}
|
||||
|
||||
// ── HTML index (the install-page seed, subplan 04 Slice 5) ───────────
|
||||
|
||||
// Escape &, <, >, " for HTML text/attribute positions. Returns `s`
|
||||
// unchanged (no copy) when nothing needs escaping.
|
||||
html_escape :: (s: string) -> string {
|
||||
acc := "";
|
||||
start := 0;
|
||||
i := 0;
|
||||
while i < s.len {
|
||||
c := s[i];
|
||||
rep := "";
|
||||
if c == 38 { rep = "&"; } // &
|
||||
if c == 60 { rep = "<"; } // <
|
||||
if c == 62 { rep = ">"; } // >
|
||||
if c == 34 { rep = """; } // "
|
||||
if rep.len > 0 {
|
||||
if i > start { acc = concat(acc, string.{ ptr = @s[start], len = i - start }); }
|
||||
acc = concat(acc, rep);
|
||||
start = i + 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if start == 0 { return s; }
|
||||
if start < s.len { acc = concat(acc, string.{ ptr = @s[start], len = s.len - start }); }
|
||||
return acc;
|
||||
}
|
||||
|
||||
// First 12 hex chars of a digest, for display next to the full-link.
|
||||
short_sha :: (sha: string) -> string {
|
||||
if sha.len < 12 { return sha; }
|
||||
return string.{ ptr = sha.ptr, len = 12 };
|
||||
}
|
||||
|
||||
INDEX_HEAD :: "<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>dist</title><style>body{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#101216;color:#d6dae1;margin:2rem auto;max-width:60rem;padding:0 1rem}h1{font-size:1.1rem;letter-spacing:.04em}h2{font-size:1rem;margin-top:2rem;border-bottom:1px solid #2a2f3a;padding-bottom:.3rem}table{border-collapse:collapse;width:100%;font-size:.85rem}td,th{text-align:left;padding:.3rem .8rem .3rem 0;border-bottom:1px solid #1d212a;vertical-align:top}th{color:#8b93a3;font-weight:normal}a{color:#7ab7ff;text-decoration:none}a:hover{text-decoration:underline}small,.dim{color:#737b8c}</style></head><body><h1>dist — release console</h1>";
|
||||
|
||||
INDEX_FOOT :: "<p class=\"dim\"><small>API: <a href=\"/api/apps\">/api/apps</a> · <a href=\"/healthz\">/healthz</a></small></p></body></html>";
|
||||
|
||||
// Server-rendered index: every app with its channels and its releases'
|
||||
// artifacts as direct /download links. Dense and read-only, per the
|
||||
// product shape — the full admin UI is subplan 06.
|
||||
render_index :: (repo: *Repo) -> string {
|
||||
page := INDEX_HEAD;
|
||||
if repo.apps.len == 0 {
|
||||
page = concat(page, "<p class=\"dim\">nothing published yet</p>");
|
||||
}
|
||||
a := 0;
|
||||
while a < repo.apps.len {
|
||||
app := repo.apps.items[a];
|
||||
page = concat(page, concat("<h2>", concat(html_escape(app.slug), concat(" <small>", concat(html_escape(app.display_name), "</small></h2>")))));
|
||||
|
||||
// Channels: name -> current release pointer.
|
||||
page = concat(page, "<table><tr><th>channel</th><th>current release</th></tr>");
|
||||
c := 0;
|
||||
while c < repo.channels.len {
|
||||
ch := repo.channels.items[c];
|
||||
if ch.app_id == app.id {
|
||||
page = concat(page, concat("<tr><td>", concat(html_escape(ch.name), concat("</td><td>", concat(html_escape(ch.current_release_id), "</td></tr>")))));
|
||||
}
|
||||
c += 1;
|
||||
}
|
||||
page = concat(page, "</table>");
|
||||
|
||||
// Releases with their artifacts as download links.
|
||||
page = concat(page, "<table><tr><th>release</th><th>platform</th><th>artifact</th><th>size</th><th>sha256</th></tr>");
|
||||
r := 0;
|
||||
while r < repo.releases.len {
|
||||
rel := repo.releases.items[r];
|
||||
if rel.app_id == app.id {
|
||||
k := 0;
|
||||
while k < repo.artifacts.len {
|
||||
art := repo.artifacts.items[k];
|
||||
if art.release_id == rel.id {
|
||||
row := concat("<tr><td>", concat(html_escape(rel.version), concat(" <small>", concat(html_escape(rel.id), "</small></td><td>"))));
|
||||
row = concat(row, concat(db.platform_str(art.platform), "</td><td>"));
|
||||
row = concat(row, concat("<a href=\"/download/", concat(art.sha256, concat("\">", concat(html_escape(art.filename), "</a></td><td>")))));
|
||||
row = concat(row, concat(int_to_string(art.size_bytes), "</td><td><small>"));
|
||||
row = concat(row, concat(short_sha(art.sha256), "…</small></td></tr>"));
|
||||
page = concat(page, row);
|
||||
}
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
r += 1;
|
||||
}
|
||||
page = concat(page, "</table>");
|
||||
a += 1;
|
||||
}
|
||||
return concat(page, INDEX_FOOT);
|
||||
}
|
||||
|
||||
handle_index :: (client: s32, store_dir: string) {
|
||||
rq := load_or_503(client, store_dir);
|
||||
if rq == null { return; }
|
||||
repo := rq!;
|
||||
http.respond(client, 200, "text/html; charset=utf-8", "", render_index(@repo));
|
||||
}
|
||||
|
||||
// ── routes ────────────────────────────────────────────────────────────
|
||||
|
||||
handle_apps_index :: (client: s32, store_dir: string) {
|
||||
@@ -235,6 +333,10 @@ route :: (client: s32, store_dir: string, method: string, path: string) -> s64 {
|
||||
"every distd route is GET for now (writes go through the dist CLI)");
|
||||
return 405;
|
||||
}
|
||||
if path == "/" {
|
||||
handle_index(client, store_dir);
|
||||
return 200;
|
||||
}
|
||||
if path == "/healthz" {
|
||||
http.respond(client, 200, "application/json", "", "{\"status\":\"ok\"}");
|
||||
return 200;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// `build/dist server run` on a test port in the background, waits for
|
||||
// /healthz, and asserts every route against curl:
|
||||
//
|
||||
// * / → HTML index naming the app, with a
|
||||
// /download/<sha> link
|
||||
// * /healthz → {"status":"ok"}
|
||||
// * /api/apps → the published app is listed
|
||||
// * /api/apps/<slug> → app + its releases + channels
|
||||
@@ -48,6 +50,24 @@ write_file :: (path: string, body: string) {
|
||||
process.run(cmd);
|
||||
}
|
||||
|
||||
// True iff `needle` occurs in `hay` (plain scan; bodies are small).
|
||||
contains :: (hay: string, needle: string) -> bool {
|
||||
if needle.len == 0 { return true; }
|
||||
if needle.len > hay.len { return false; }
|
||||
i := 0;
|
||||
while i + needle.len <= hay.len {
|
||||
j := 0;
|
||||
ok := true;
|
||||
while j < needle.len {
|
||||
if hay[i + j] != needle[j] { ok = false; break; }
|
||||
j += 1;
|
||||
}
|
||||
if ok { return true; }
|
||||
i += 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// GET `path` and return the body (curl; 2s timeout so a dead server fails
|
||||
// the test instead of hanging it).
|
||||
fetch :: (path: string) -> string {
|
||||
@@ -113,6 +133,12 @@ main :: () -> s32 {
|
||||
print(" server up\n");
|
||||
|
||||
// ── routes ────────────────────────────────────────────────────────
|
||||
process.assert(fetch_code("/") == "200", "/ serves the HTML index");
|
||||
idx := fetch("/");
|
||||
process.assert(contains(idx, "<title>dist</title>"), "index is the dist HTML page");
|
||||
process.assert(contains(idx, "acme-app"), "index names the published app");
|
||||
process.assert(contains(idx, "/download/"), "index links artifact downloads");
|
||||
|
||||
hz := parse_body(fetch("/healthz"), "/healthz", xx arena);
|
||||
process.assert(get_str(hz, "status") == "ok", "/healthz status ok");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user