A request inspector where API calls stream in with method, path, status and timing bars, and one is opened to show its headers.
╭─ Network ───────────────────────────────────────────────── 12 requests · 3 errors · ● rec ───────╮ │ Filter: /v2/ All XHR JS Img Doc │ │ │ │ Method Path Status Time Size Waterfall │ │──────────────────────────────────────────────────────────────────────────────────────────────────│ │ GET /v2/search?q=invoice 200 312 ms 18 KB █ │ │ POST /v2/uploads 201 742 ms 96 B ██ │ │ GET /v2/tasks/90412/history 404 22 ms 88 B █ │ │ DELETE /v2/sessions/old 204 35 ms 0 B █ │ │▌GET░░░░░/v2/reports/weekly░░░░░░░░░░░░500░░░░░ 1.20 s░ 141 B░░░░░░░░░░░░░░░░░░████░░░░░░░░░░░░░░│ │ GET /v2/notifications 200 57 ms 2.1 KB █ │ │ PUT /v2/settings/theme 200 49 ms 220 B █ │ │ GET /v2/billing/usage 429 12 ms 64 B █ │ │ │ │─ Headers ─ Response ─ Timing ────────────────────────────────────────────────────────────────────│ │ │ │ Request URL https://api.example.dev/v2/reports/weekly DNS ▆ 4 ms │ │ Method GET Connect ▆ 18 ms │ │ Status 500 Internal Server Error TLS ▆ 22 ms │ │ x-request-id req_8f2c41d9a0 Waiting ▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆ 1120 ms │ │ content-type application/json Download ▆ 40 ms │ │ retry-after - │ │ │ │ │ │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
Paste two lines into any page, play it in a terminal, or put a GIF in a README. The code follows the look you pick above.
<script type="module" src="https://www.ascii-magic.com/ascii/ascii.js"></script>
<ascii-art piece="api-request-inspector"></ascii-art>// copy api-request-inspector.js and kit.js into your project, then:
import { useEffect, useRef } from "react";
import create from "./api-request-inspector.js";
export function Piece() {
const ref = useRef(null);
useEffect(() => {
const draw = create(), t0 = performance.now();
let raf;
const tick = () => {
const f = draw((performance.now() - t0) / 1000);
ref.current.textContent = f.text ?? f;
raf = requestAnimationFrame(tick);
};
tick();
return () => cancelAnimationFrame(raf);
}, []);
return <pre ref={ref} style={{ lineHeight: 1, fontFamily: "Menlo, Consolas, monospace" }} />;
}npx --yes https://www.ascii-magic.com/ascii/cli.tgz api-request-inspector<!-- Download GIF below, commit it, then: -->
<p align="center">
<img src="assets/api-request-inspector.gif" alt="API Request Inspector, animated ASCII art" width="720">
</p>Pause on the frame you like, pick a look, then download it at an exact size.
MIT licensed. It imports one small helper file, kit.js.
/* api-request-inspector: a live network panel where API calls stream in with method, status, latency and a waterfall. */
import { Grid, palette, rng } from "../lib/kit.js";
export const meta = {
slug: "api-request-inspector",
name: "API Request Inspector",
category: "screens",
note: "A request inspector where API calls stream in with method, path, status and timing bars, and one is opened to show its headers.",
tags: ["api", "network", "http", "developer", "inspector", "dashboard"],
cols: 100, rows: 26, fps: 24,
palette: palette(["#e6edf3", "#8b949e", "#7ee787", "#79c0ff", "#ffa657", "#ff7b72", "#d2a8ff", "#484f58", "#30363d", "#f2cc60"]),
};
const REQ = [
["GET", "/v2/projects?limit=20", 200, 84, "1.2 KB"], ["POST", "/v2/auth/refresh", 200, 41, "312 B"],
["GET", "/v2/projects/7781/members", 200, 126, "4.8 KB"], ["PATCH", "/v2/tasks/90412", 204, 63, "0 B"],
["GET", "/v2/search?q=invoice", 200, 312, "18 KB"], ["POST", "/v2/uploads", 201, 742, "96 B"],
["GET", "/v2/tasks/90412/history", 404, 22, "88 B"], ["DELETE", "/v2/sessions/old", 204, 35, "0 B"],
["GET", "/v2/reports/weekly", 500, 1204, "141 B"], ["GET", "/v2/notifications", 200, 57, "2.1 KB"],
["PUT", "/v2/settings/theme", 200, 49, "220 B"], ["GET", "/v2/billing/usage", 429, 12, "64 B"],
];
const MC = { GET: "3", POST: "2", PATCH: "4", PUT: "4", DELETE: "5" };
const GAP = 0.55, PERIOD = REQ.length * GAP + 4.5;
export default function create() {
const g = new Grid(meta.cols, meta.rows);
const W = meta.cols, H = meta.rows;
const r = rng(4);
const starts = REQ.map((_, i) => i * GAP + r() * 0.2);
return (t) => {
g.clear();
const lt = t % PERIOD;
g.box(0, 0, W, H, "round", "7");
g.text(2, 0, " Network ", "0");
const vis = REQ.map((q, i) => [q, i]).filter(([, i]) => starts[i] <= lt);
const errs = vis.filter(([q]) => q[2] >= 400).length;
g.text(W - 40, 0, ` ${vis.length} requests · ${errs} errors · ● rec `, errs ? "4" : "1");
g.text(2, 1, "Filter: ", "1"); g.text(10, 1, "/v2/", "0"); g.text(16, 1, "All XHR JS Img Doc", "7");
const hdr = [["Method", 2], ["Path", 10], ["Status", 40], ["Time", 48], ["Size", 56], ["Waterfall", 66]];
hdr.forEach(([n, x]) => g.text(x, 3, n, "1"));
g.hline(1, W - 2, 4, "─", "8");
const openI = 8, openT = starts[openI] + (1204 / 1000) + 0.8;
const panel = lt >= openT && lt < PERIOD - 0.3;
const maxRows = panel ? 8 : 19;
const first = Math.max(0, vis.length - maxRows);
const span = 9;
vis.slice(first).forEach(([q, i], k) => {
const y = 5 + k;
const [m, p, st, ms, sz] = q;
const age = lt - starts[i];
const pending = age < ms / 1000;
const sel = panel && i === openI;
if (sel) { g.fill(1, y, W - 2, 1, "░", "8"); g.set(1, y, "▌", "3"); }
g.text(2, y, m, MC[m]);
g.text(10, y, p, pending ? "1" : "0");
if (pending) g.text(40, y, "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"[Math.floor(lt * 12) % 10] + " ...", "7");
else {
g.text(40, y, String(st), st >= 500 ? "5" : st >= 400 ? "4" : "2");
g.text(48, y, (ms >= 1000 ? (ms / 1000).toFixed(2) + " s" : ms + " ms").padStart(7), ms > 500 ? "4" : "1");
g.text(56, y, sz.padStart(7), "1");
}
// waterfall: bar placed by start time within the last `span` seconds
const x0 = 66 + Math.round(((starts[i] - Math.max(0, lt - span)) / span) * (W - 70));
const dur = Math.min(age, ms / 1000);
const len = Math.max(1, Math.round((dur / span) * (W - 70)));
for (let b = 0; b < len; b++) {
const xx = x0 + b;
if (xx >= 66 && xx < W - 2) g.set(xx, y, "█", st >= 500 ? "5" : st >= 400 ? "4" : b < 1 ? "1" : "3");
}
});
if (panel) {
const [m, p, st, ms] = REQ[openI];
const py = 14;
g.hline(1, W - 2, py, "─", "8");
g.text(2, py, " Headers ", "0"); g.text(12, py, " Response ", "7"); g.text(23, py, " Timing ", "7");
const L = [
["Request URL", "https://api.example.dev" + p, "0"], ["Method", m, MC[m]], ["Status", st + " Internal Server Error", "5"],
["x-request-id", "req_8f2c41d9a0", "1"], ["content-type", "application/json", "1"], ["retry-after", "-", "7"],
];
L.forEach(([k, v, c], i) => { g.text(4, py + 2 + i, k.padEnd(14), "6"); g.text(19, py + 2 + i, v, c); });
const T = [["DNS", 4], ["Connect", 18], ["TLS", 22], ["Waiting", 1120], ["Download", 40]];
T.forEach(([k, v], i) => {
const y = py + 2 + i;
g.text(62, y, k.padEnd(9), "1");
const bl = Math.max(1, Math.round((v / ms) * 16));
for (let b = 0; b < bl; b++) g.set(72 + b, y, "▆", i === 3 ? "4" : "3");
g.text(W - 9, y, (v + " ms").padStart(7), "1");
});
}
return g.frame();
};
}
/YOUR OWN
The pieces here are hand-built animations. To turn your own photo, logo or video into animated ASCII, use the ASCII Magic editor.