atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
ea51d9fda49d82c38cf3920e34d6be05d8398cae
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git

Blame: src/ui.html

99524cf9f17catagen2026-10-10 10:53 UTC1
<!DOCTYPE html>
99524cf9f17catagen2026-10-10 10:53 UTC2
<html>
99524cf9f17catagen2026-10-10 10:53 UTC3
<head>
99524cf9f17catagen2026-10-10 10:53 UTC4
<meta charset="utf-8">
99524cf9f17catagen2026-10-10 10:53 UTC5
<title>jelqd</title>
185851447cebatagen2026-10-10 12:58 UTC6
<style>
185851447cebatagen2026-10-10 12:58 UTC7
.frame { border: 2px inset; background: #fff; height: 85vh; overflow: scroll; padding: 4px; }
185851447cebatagen2026-10-10 12:58 UTC8
#log { white-space: pre-wrap; margin: 0; }
185851447cebatagen2026-10-10 12:58 UTC9
pre, li, p, td, label { overflow-wrap: anywhere; }
185851447cebatagen2026-10-10 12:58 UTC10
</style>
99524cf9f17catagen2026-10-10 10:53 UTC11
</head>
99524cf9f17catagen2026-10-10 10:53 UTC12
<body>
99524cf9f17catagen2026-10-10 10:53 UTC13
185851447cebatagen2026-10-10 12:58 UTC14
<h1>jelqd</h1>
a8269407e054atagen2026-10-13 12:44 UTC15
<p>A roguelike played over HTTP.</p>
a8269407e054atagen2026-10-13 12:44 UTC16
<p>Pick a resource, pick a method, send it.<br>The log on the right is everything the server has said to you.</p>
a8269407e054atagen2026-10-13 12:44 UTC17
<p>Language <select id="locale"><option>en-GB</option><option>en-US</option><option>en-AU</option></select></p>
185851447cebatagen2026-10-10 12:58 UTC18
185851447cebatagen2026-10-10 12:58 UTC19
<table width="100%" cellspacing="6">
185851447cebatagen2026-10-10 12:58 UTC20
<tr>
185851447cebatagen2026-10-10 12:58 UTC21
<td width="45%" valign="top">
185851447cebatagen2026-10-10 12:58 UTC22
<div class="frame">
185851447cebatagen2026-10-10 12:58 UTC23
431c1bc8ded1atagen2026-10-10 13:20 UTC24
<div id="alerts"></div>
431c1bc8ded1atagen2026-10-10 13:20 UTC25
185851447cebatagen2026-10-10 12:58 UTC26
<form id="form">
185851447cebatagen2026-10-10 12:58 UTC27
<h3>Method</h3>
185851447cebatagen2026-10-10 12:58 UTC28
<div id="methods"></div>
f16ab17c8ff6atagen2026-10-13 11:14 UTC29
<datalist id="excuses"></datalist>
185851447cebatagen2026-10-10 12:58 UTC30
185851447cebatagen2026-10-10 12:58 UTC31
<h3>Request</h3>
185851447cebatagen2026-10-10 12:58 UTC32
<table id="fields" cellpadding="2"></table>
185851447cebatagen2026-10-10 12:58 UTC33
185851447cebatagen2026-10-10 12:58 UTC34
<p><input type="submit" id="send" value="Send" style="font-size: 1.3em"></p>
185851447cebatagen2026-10-10 12:58 UTC35
185851447cebatagen2026-10-10 12:58 UTC36
<h3>Stats</h3>
ea51d9fda49datagen2026-10-14 12:20 UTC37
<table id="progress" border="1" cellspacing="0" cellpadding="3"></table>
ea51d9fda49datagen2026-10-14 12:20 UTC38
<br>
185851447cebatagen2026-10-10 12:58 UTC39
<table id="stats" border="1" cellspacing="0" cellpadding="3"></table>
185851447cebatagen2026-10-10 12:58 UTC40
185851447cebatagen2026-10-10 12:58 UTC41
<h3>Resource</h3>
185851447cebatagen2026-10-10 12:58 UTC42
<div id="targets"></div>
185851447cebatagen2026-10-10 12:58 UTC43
</form>
185851447cebatagen2026-10-10 12:58 UTC44
185851447cebatagen2026-10-10 12:58 UTC45
<p><small>Every request goes through <code>/ui/send</code>, which replays it inside the server, so the page can show redirects and send methods and headers that browsers refuse.</small></p>
185851447cebatagen2026-10-10 12:58 UTC46
</div>
185851447cebatagen2026-10-10 12:58 UTC47
</td>
185851447cebatagen2026-10-10 12:58 UTC48
<td valign="top">
185851447cebatagen2026-10-10 12:58 UTC49
<div class="frame" id="frame"><pre id="log"></pre></div>
185851447cebatagen2026-10-10 12:58 UTC50
</td>
185851447cebatagen2026-10-10 12:58 UTC51
</tr>
185851447cebatagen2026-10-10 12:58 UTC52
</table>
99524cf9f17catagen2026-10-10 10:53 UTC53
99524cf9f17catagen2026-10-10 10:53 UTC54
<script>
185851447cebatagen2026-10-10 12:58 UTC55
const REL = "https://jelq.dev/rels/";
6619c458c23eatagen2026-10-14 08:08 UTC56
const METHODS = ["JELQ", "HOLD", "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", "TRACE"];
0a0bfee8329eatagen2026-10-11 05:11 UTC57
const ALL_LUBES = ["", "application/baby-oil", "application/coconut-oil", "application/lotion", "application/vaseline", "text/plain; charset=spit", "image/jpeg"];
0a0bfee8329eatagen2026-10-11 05:11 UTC58
let stocked = null;
f16ab17c8ff6atagen2026-10-13 11:14 UTC59
let excuses = [];
f16ab17c8ff6atagen2026-10-13 11:14 UTC60
let nonce = null;
14de3e1e8dc2atagen2026-10-11 06:19 UTC61
let inventorySeq = 0;
ea51d9fda49datagen2026-10-14 12:20 UTC62
const PROGRESS = [
4f54028cba69atagen2026-10-14 11:45 UTC63
  { header: "x-turn", label: "Turn" },
4f54028cba69atagen2026-10-14 11:45 UTC64
  { header: "x-floor-turn", label: "Floor turn" },
4f54028cba69atagen2026-10-14 11:45 UTC65
  { header: "x-floor", label: "Floor", better: 1 },
4f54028cba69atagen2026-10-14 11:45 UTC66
  { header: "x-cash", label: "Cash", better: 1 },
ea51d9fda49datagen2026-10-14 12:20 UTC67
];
ea51d9fda49datagen2026-10-14 12:20 UTC68
const STATS = [
4f54028cba69atagen2026-10-14 11:45 UTC69
  { header: "x-self-reported-length", label: "Self-reported length", better: 1, places: 2, measured: { header: "x-length", label: "Length" } },
4f54028cba69atagen2026-10-14 11:45 UTC70
  { header: "x-girth", label: "Girth", better: 1, places: 2 },
4f54028cba69atagen2026-10-14 11:45 UTC71
  { header: "x-foreskin", label: "Foreskin", better: 1, places: 2 },
4f54028cba69atagen2026-10-14 11:45 UTC72
  { header: "x-friction", label: "Friction", better: -1 },
4f54028cba69atagen2026-10-14 11:45 UTC73
  { header: "x-eq", label: "EQ" },
4f54028cba69atagen2026-10-14 11:45 UTC74
  { header: "x-stamina", label: "Stamina", better: 1 },
4f54028cba69atagen2026-10-14 11:45 UTC75
  { header: "x-suspicion", label: "Suspicion", better: -1 },
4f54028cba69atagen2026-10-14 11:45 UTC76
  { header: "x-shame", label: "Shame", better: -1 },
185851447cebatagen2026-10-10 12:58 UTC77
];
beabb5985f4aatagen2026-10-14 11:31 UTC78
const AROUSAL = ["Flaccid", "Cold", "Warm", "Ideal", "Swollen", "Throbbing", "Straining", "Imminent"];
f3a1913c1299atagen2026-10-14 11:42 UTC79
const DANGER_COLOURS = { dangerous: "#b8860b", critical: "red" };
ddcf8b3a91dcatagen2026-10-14 11:03 UTC80
let lastStats = {};
ea51d9fda49datagen2026-10-14 12:20 UTC81
const QUIET = new Set(["link", "set-cookie", "server", "x-powered-by", "cache-control", "content-length", "content-type", "date"].concat(PROGRESS.concat(STATS).flatMap(s => [s.header, s.measured?.header]).filter(Boolean)));
99524cf9f17catagen2026-10-10 10:53 UTC82
185851447cebatagen2026-10-10 12:58 UTC83
const FIELDS = {
185851447cebatagen2026-10-10 12:58 UTC84
  seed: { label: "seed (unranked)", kind: "text" },
58e1fb3943e6atagen2026-10-10 13:30 UTC85
  technique: { label: "technique", kind: "select" },
58e1fb3943e6atagen2026-10-10 13:30 UTC86
  also: { label: "combo with", kind: "select" },
185851447cebatagen2026-10-10 12:58 UTC87
  intensity: { label: "intensity, 1 to 10", kind: "number", value: "5" },
58e1fb3943e6atagen2026-10-10 13:30 UTC88
  lube: { label: "Content-Type", header: "Content-Type", kind: "select" },
185851447cebatagen2026-10-10 12:58 UTC89
  range: { label: "Range", header: "Range", kind: "text", placeholder: "inches=0-2" },
185851447cebatagen2026-10-10 12:58 UTC90
  ifMatch: { label: "If-Match", header: "If-Match", kind: "text" },
185851447cebatagen2026-10-10 12:58 UTC91
  ifNoneMatch: { label: "If-None-Match", header: "If-None-Match", kind: "text" },
185851447cebatagen2026-10-10 12:58 UTC92
  ifModifiedSince: { label: "If-Modified-Since", header: "If-Modified-Since", kind: "text", placeholder: "an HTTP-date" },
185851447cebatagen2026-10-10 12:58 UTC93
  proxy: { label: "Proxy-Authorization", header: "Proxy-Authorization", kind: "text", placeholder: "Ruler 5.4" },
185851447cebatagen2026-10-10 12:58 UTC94
  upgrade: { label: "Upgrade", header: "Upgrade", kind: "select", options: ["", "underhand", "overhand"] },
185851447cebatagen2026-10-10 12:58 UTC95
  authorization: { label: "Authorization", header: "Authorization", kind: "text", placeholder: 'Excuse "homework"' },
185851447cebatagen2026-10-10 12:58 UTC96
  extra: { label: "other headers, one per line", kind: "textarea", placeholder: "Expect: gains" },
185851447cebatagen2026-10-10 12:58 UTC97
};
185851447cebatagen2026-10-10 12:58 UTC98
const BY_TARGET = {
185851447cebatagen2026-10-10 12:58 UTC99
  "runs POST": ["seed"],
185851447cebatagen2026-10-10 12:58 UTC100
  "length GET": ["range"],
185851447cebatagen2026-10-10 12:58 UTC101
  "length HEAD": ["range"],
185851447cebatagen2026-10-10 12:58 UTC102
  "foreskin DELETE": ["ifMatch"],
185851447cebatagen2026-10-10 12:58 UTC103
  "elder GET": ["proxy", "ifModifiedSince"],
185851447cebatagen2026-10-10 12:58 UTC104
  "grip GET": ["upgrade"],
4b690ff501b8atagen2026-10-10 13:27 UTC105
  "diagnosis GET": ["authorization", "ifNoneMatch"],
185851447cebatagen2026-10-10 12:58 UTC106
};
d604371299fbatagen2026-10-10 13:54 UTC107
const GROUPS = [
d604371299fbatagen2026-10-10 13:54 UTC108
  ["play", "Play"],
7d3fb63208bfatagen2026-10-11 04:38 UTC109
  ["shop", "Shop"],
d604371299fbatagen2026-10-10 13:54 UTC110
  ["you", "You"],
d604371299fbatagen2026-10-10 13:54 UTC111
  ["said", "From the last response"],
d604371299fbatagen2026-10-10 13:54 UTC112
  ["reading", "Reading"],
d604371299fbatagen2026-10-10 13:54 UTC113
  ["meta", "Meta"],
d604371299fbatagen2026-10-10 13:54 UTC114
];
7d3fb63208bfatagen2026-10-11 04:38 UTC115
const GROUP_OF = { run: "play", floor: "play", drawer: "play", shop: "play", "shop-item": "shop", kitchen: "play", portal: "play", elder: "play", diagnosis: "play", length: "you", foreskin: "you", grip: "you", inventory: "you", footage: "you", techniques: "reading", thread: "reading" };
d604371299fbatagen2026-10-10 13:54 UTC116
const NAMES = { run: "the run", thread: "the 2009 forum thread" };
185851447cebatagen2026-10-10 12:58 UTC117
const GLOBALS = [
d604371299fbatagen2026-10-10 13:54 UTC118
  { key: "runs", group: "play", name: "start a new run", path: "/runs", methods: ["POST"] },
d604371299fbatagen2026-10-10 13:54 UTC119
  { key: "global", group: "meta", name: "leaderboard", path: "/leaderboard", methods: ["GET", "HEAD", "OPTIONS"] },
d604371299fbatagen2026-10-10 13:54 UTC120
  { key: "global", group: "meta", name: "graveyard", path: "/graveyard", methods: ["GET", "HEAD", "OPTIONS"] },
185851447cebatagen2026-10-10 12:58 UTC121
];
185851447cebatagen2026-10-10 12:58 UTC122
185851447cebatagen2026-10-10 12:58 UTC123
let run = null;
185851447cebatagen2026-10-10 12:58 UTC124
let linked = [];
58e1fb3943e6atagen2026-10-10 13:30 UTC125
let techniques = [];
185851447cebatagen2026-10-10 12:58 UTC126
let offered = [];
0cad5f1c5014atagen2026-10-14 11:36 UTC127
let known = new Set();
0cad5f1c5014atagen2026-10-14 11:36 UTC128
let fresh = new Set();
4b690ff501b8atagen2026-10-10 13:27 UTC129
let challenged = false;
ec6fd4bb5b60atagen2026-10-11 04:34 UTC130
const etags = {};
185851447cebatagen2026-10-10 12:58 UTC131
let target = "/runs";
185851447cebatagen2026-10-10 12:58 UTC132
let method = "POST";
185851447cebatagen2026-10-10 12:58 UTC133
const values = {};
185851447cebatagen2026-10-10 12:58 UTC134
185851447cebatagen2026-10-10 12:58 UTC135
const $ = id => document.getElementById(id);
185851447cebatagen2026-10-10 12:58 UTC136
185851447cebatagen2026-10-10 12:58 UTC137
function short(path) {
185851447cebatagen2026-10-10 12:58 UTC138
  return path.replace(/[0-9a-f]{32}/, id => id.slice(0, 6) + "…");
99524cf9f17catagen2026-10-10 10:53 UTC139
}
99524cf9f17catagen2026-10-10 10:53 UTC140
185851447cebatagen2026-10-10 12:58 UTC141
function el(tag, props, ...children) {
185851447cebatagen2026-10-10 12:58 UTC142
  const node = Object.assign(document.createElement(tag), props || {});
185851447cebatagen2026-10-10 12:58 UTC143
  for (const child of children) node.append(child);
185851447cebatagen2026-10-10 12:58 UTC144
  return node;
99524cf9f17catagen2026-10-10 10:53 UTC145
}
99524cf9f17catagen2026-10-10 10:53 UTC146
58e1fb3943e6atagen2026-10-10 13:30 UTC147
function titled(text) {
58e1fb3943e6atagen2026-10-10 13:30 UTC148
  return text.charAt(0).toUpperCase() + text.slice(1);
58e1fb3943e6atagen2026-10-10 13:30 UTC149
}
58e1fb3943e6atagen2026-10-10 13:30 UTC150
58e1fb3943e6atagen2026-10-10 13:30 UTC151
function technique(name) {
58e1fb3943e6atagen2026-10-10 13:30 UTC152
  return techniques.find(t => t.name === name);
58e1fb3943e6atagen2026-10-10 13:30 UTC153
}
58e1fb3943e6atagen2026-10-10 13:30 UTC154
58e1fb3943e6atagen2026-10-10 13:30 UTC155
function clashes(a, b) {
58e1fb3943e6atagen2026-10-10 13:30 UTC156
  const rules = [a.lubricant, b.lubricant];
58e1fb3943e6atagen2026-10-10 13:30 UTC157
  return rules.includes("required") && rules.includes("forbidden");
58e1fb3943e6atagen2026-10-10 13:30 UTC158
}
58e1fb3943e6atagen2026-10-10 13:30 UTC159
58e1fb3943e6atagen2026-10-10 13:30 UTC160
// The options a field can take right now, or null for free text.
58e1fb3943e6atagen2026-10-10 13:30 UTC161
function options(name) {
58e1fb3943e6atagen2026-10-10 13:30 UTC162
  if (name === "technique") return techniques.map(t => t.name);
58e1fb3943e6atagen2026-10-10 13:30 UTC163
  if (name === "also") {
58e1fb3943e6atagen2026-10-10 13:30 UTC164
    const first = technique(fieldValue("technique"));
e028a4184d00atagen2026-10-14 10:49 UTC165
    if (!first || !first.intensity) return [""];
e028a4184d00atagen2026-10-14 10:49 UTC166
    const partners = techniques.filter(t => t.intensity && t.name !== first.name && !clashes(first, t));
58e1fb3943e6atagen2026-10-10 13:30 UTC167
    return [""].concat(partners.map(t => t.name));
58e1fb3943e6atagen2026-10-10 13:30 UTC168
  }
0a0bfee8329eatagen2026-10-11 05:11 UTC169
  if (name === "lube") {
0a0bfee8329eatagen2026-10-11 05:11 UTC170
    const lubes = stocked === null ? ALL_LUBES : ALL_LUBES.filter(l => l === "" || l.includes("/") && !l.startsWith("application/") || stocked.includes(l.replace("application/", "")));
0a0bfee8329eatagen2026-10-11 05:11 UTC171
    return lubeRule() === "required" ? lubes.filter(Boolean) : lubes;
0a0bfee8329eatagen2026-10-11 05:11 UTC172
  }
58e1fb3943e6atagen2026-10-10 13:30 UTC173
  return FIELDS[name].options || null;
58e1fb3943e6atagen2026-10-10 13:30 UTC174
}
58e1fb3943e6atagen2026-10-10 13:30 UTC175
58e1fb3943e6atagen2026-10-10 13:30 UTC176
function raw(name) {
58e1fb3943e6atagen2026-10-10 13:30 UTC177
  return values[name] ?? FIELDS[name].value ?? "";
58e1fb3943e6atagen2026-10-10 13:30 UTC178
}
58e1fb3943e6atagen2026-10-10 13:30 UTC179
58e1fb3943e6atagen2026-10-10 13:30 UTC180
function fieldValue(name) {
58e1fb3943e6atagen2026-10-10 13:30 UTC181
  const allowed = options(name);
58e1fb3943e6atagen2026-10-10 13:30 UTC182
  const value = raw(name);
58e1fb3943e6atagen2026-10-10 13:30 UTC183
  if (allowed && !allowed.includes(value)) return allowed[0] ?? "";
58e1fb3943e6atagen2026-10-10 13:30 UTC184
  return value;
58e1fb3943e6atagen2026-10-10 13:30 UTC185
}
58e1fb3943e6atagen2026-10-10 13:30 UTC186
58e1fb3943e6atagen2026-10-10 13:30 UTC187
function lubeRule() {
58e1fb3943e6atagen2026-10-10 13:30 UTC188
  const picked = [technique(fieldValue("technique")), technique(fieldValue("also"))].filter(Boolean);
ec6fd4bb5b60atagen2026-10-11 04:34 UTC189
  if (!picked.some(t => t.lubricant)) return "none";
58e1fb3943e6atagen2026-10-10 13:30 UTC190
  for (const rule of ["forbidden", "required"]) {
58e1fb3943e6atagen2026-10-10 13:30 UTC191
    if (picked.some(t => t.lubricant === rule)) return rule;
58e1fb3943e6atagen2026-10-10 13:30 UTC192
  }
58e1fb3943e6atagen2026-10-10 13:30 UTC193
  return "optional";
58e1fb3943e6atagen2026-10-10 13:30 UTC194
}
58e1fb3943e6atagen2026-10-10 13:30 UTC195
58e1fb3943e6atagen2026-10-10 13:30 UTC196
function allowedMethods(t) {
58e1fb3943e6atagen2026-10-10 13:30 UTC197
  return t.key === "run" && !techniques.length ? t.methods.filter(m => m !== "JELQ") : t.methods;
58e1fb3943e6atagen2026-10-10 13:30 UTC198
}
58e1fb3943e6atagen2026-10-10 13:30 UTC199
185851447cebatagen2026-10-10 12:58 UTC200
function targets() {
d604371299fbatagen2026-10-10 13:54 UTC201
  const listed = run ? linked : [];
d604371299fbatagen2026-10-10 13:54 UTC202
  const known = new Set(listed.concat(GLOBALS).map(t => t.path));
d604371299fbatagen2026-10-10 13:54 UTC203
  const fresh = offered.filter((t, i) => !known.has(t.path) && offered.findIndex(o => o.path === t.path) === i);
d604371299fbatagen2026-10-10 13:54 UTC204
  return listed.concat(fresh, GLOBALS);
185851447cebatagen2026-10-10 12:58 UTC205
}
185851447cebatagen2026-10-10 12:58 UTC206
185851447cebatagen2026-10-10 12:58 UTC207
function current() {
185851447cebatagen2026-10-10 12:58 UTC208
  return targets().find(t => t.path === target) || GLOBALS[0];
185851447cebatagen2026-10-10 12:58 UTC209
}
185851447cebatagen2026-10-10 12:58 UTC210
185851447cebatagen2026-10-10 12:58 UTC211
function chooseTarget(t) {
58e1fb3943e6atagen2026-10-10 13:30 UTC212
  const allowed = allowedMethods(t);
185851447cebatagen2026-10-10 12:58 UTC213
  target = t.path;
58e1fb3943e6atagen2026-10-10 13:30 UTC214
  method = allowed.includes("JELQ") ? "JELQ" : allowed.find(m => m !== "HEAD" && m !== "OPTIONS") || allowed[0];
185851447cebatagen2026-10-10 12:58 UTC215
  render();
185851447cebatagen2026-10-10 12:58 UTC216
}
185851447cebatagen2026-10-10 12:58 UTC217
185851447cebatagen2026-10-10 12:58 UTC218
function renderStats(headers) {
f3a1913c1299atagen2026-10-14 11:42 UTC219
  const dangers = Object.fromEntries((headers && headers["x-danger"] || "").split(",").filter(Boolean).map(d => d.trim().split("=")));
ea51d9fda49datagen2026-10-14 12:20 UTC220
  renderTable($("progress"), PROGRESS, headers, dangers);
ea51d9fda49datagen2026-10-14 12:20 UTC221
  renderTable($("stats"), STATS, headers, dangers);
ea51d9fda49datagen2026-10-14 12:20 UTC222
  lastStats = headers || {};
ea51d9fda49datagen2026-10-14 12:20 UTC223
}
ea51d9fda49datagen2026-10-14 12:20 UTC224
ea51d9fda49datagen2026-10-14 12:20 UTC225
function renderTable(table, list, headers, dangers) {
ea51d9fda49datagen2026-10-14 12:20 UTC226
  table.replaceChildren();
ea51d9fda49datagen2026-10-14 12:20 UTC227
  const stats = list.map(s => s.measured && headers && s.measured.header in headers ? { ...s, ...s.measured, unmeasured: s.header } : s);
f39b10e20a0batagen2026-10-10 14:33 UTC228
  for (let i = 0; i < stats.length; i += 2) {
185851447cebatagen2026-10-10 12:58 UTC229
    const row = el("tr");
4f54028cba69atagen2026-10-14 11:45 UTC230
    for (const stat of stats.slice(i, i + 2)) {
4f54028cba69atagen2026-10-14 11:45 UTC231
      const value = headers ? headers[stat.header] : undefined;
624fe9c09c8eatagen2026-10-14 12:11 UTC232
      const done = stat.header === "x-floor-turn" && headers && "x-floor-over" in headers;
624fe9c09c8eatagen2026-10-14 12:11 UTC233
      const cell = el("td", { style: done ? "color: green; font-weight: bold" : "" }, value === undefined ? "—" : value);
4f54028cba69atagen2026-10-14 11:45 UTC234
      const changed = value === undefined ? null : change(stat, value);
08ffcd6cbba5atagen2026-10-14 11:19 UTC235
      if (changed) cell.append(" ", changed);
4f54028cba69atagen2026-10-14 11:45 UTC236
      const measured = stat.unmeasured in lastStats;
4f54028cba69atagen2026-10-14 11:45 UTC237
      const colour = DANGER_COLOURS[dangers[stat.header.slice(2)]] || (measured ? "green" : "");
4f54028cba69atagen2026-10-14 11:45 UTC238
      row.append(el("th", { align: "left", textContent: stat.label, style: colour ? "color: " + colour : "" }), cell);
185851447cebatagen2026-10-10 12:58 UTC239
    }
185851447cebatagen2026-10-10 12:58 UTC240
    table.append(row);
99524cf9f17catagen2026-10-10 10:53 UTC241
  }
185851447cebatagen2026-10-10 12:58 UTC242
}
185851447cebatagen2026-10-10 12:58 UTC243
beabb5985f4aatagen2026-10-14 11:31 UTC244
function fromIdeal(arousal) {
ea51d9fda49datagen2026-10-14 12:20 UTC245
  const rung = AROUSAL.indexOf(arousal);
ea51d9fda49datagen2026-10-14 12:20 UTC246
  return rung < 0 ? AROUSAL.length : Math.abs(rung - AROUSAL.indexOf("Ideal"));
beabb5985f4aatagen2026-10-14 11:31 UTC247
}
beabb5985f4aatagen2026-10-14 11:31 UTC248
4f54028cba69atagen2026-10-14 11:45 UTC249
function change(stat, value) {
4f54028cba69atagen2026-10-14 11:45 UTC250
  const before = lastStats[stat.header];
4f54028cba69atagen2026-10-14 11:45 UTC251
  if (before === undefined || before === value) return null;
4f54028cba69atagen2026-10-14 11:45 UTC252
  if (stat.header === "x-eq") {
beabb5985f4aatagen2026-10-14 11:31 UTC253
    const closer = Math.sign(fromIdeal(before) - fromIdeal(value));
beabb5985f4aatagen2026-10-14 11:31 UTC254
    const was = el("b", { textContent: before, style: closer ? "color: " + (closer > 0 ? "green" : "red") : "" });
beabb5985f4aatagen2026-10-14 11:31 UTC255
    return el("small", {}, "(was ", was, ")");
beabb5985f4aatagen2026-10-14 11:31 UTC256
  }
4f54028cba69atagen2026-10-14 11:45 UTC257
  if (!stat.better) return null;
08ffcd6cbba5atagen2026-10-14 11:19 UTC258
  const delta = parseFloat(value) - parseFloat(before);
08ffcd6cbba5atagen2026-10-14 11:19 UTC259
  if (!(Math.abs(delta) >= 0.005)) return null;
4f54028cba69atagen2026-10-14 11:45 UTC260
  const text = (delta > 0 ? "+" : "") + (stat.places ? delta.toFixed(stat.places) : delta);
4f54028cba69atagen2026-10-14 11:45 UTC261
  return el("b", { textContent: text, style: "color: " + (Math.sign(delta) === stat.better ? "green" : "red") });
08ffcd6cbba5atagen2026-10-14 11:19 UTC262
}
08ffcd6cbba5atagen2026-10-14 11:19 UTC263
431c1bc8ded1atagen2026-10-10 13:20 UTC264
function renderAlerts(list) {
431c1bc8ded1atagen2026-10-10 13:20 UTC265
  const box = $("alerts");
431c1bc8ded1atagen2026-10-10 13:20 UTC266
  box.replaceChildren();
431c1bc8ded1atagen2026-10-10 13:20 UTC267
  const alerts = [];
8599b930cebfatagen2026-10-14 11:33 UTC268
  let door = null;
431c1bc8ded1atagen2026-10-10 13:20 UTC269
  for (const [name, value] of list || []) {
8599b930cebfatagen2026-10-14 11:33 UTC270
    if (name === "via") door = titled(value.split(" ").pop()) + " is at the door.";
ef7d6dee5976atagen2026-10-10 13:23 UTC271
    if (name === "x-observed-by") alerts.push((value === "cat" ? "The cat" : titled(value)) + " is watching.");
431c1bc8ded1atagen2026-10-10 13:20 UTC272
    if (name === "warning") {
431c1bc8ded1atagen2026-10-10 13:20 UTC273
      const text = (value.match(/"([^"]*)"/) || [, value])[1];
aef6f6498ea2atagen2026-10-13 11:23 UTC274
      alerts.push(titled(text) + (/[.!?]$/.test(text) ? "" : "."));
431c1bc8ded1atagen2026-10-10 13:20 UTC275
    }
431c1bc8ded1atagen2026-10-10 13:20 UTC276
  }
8599b930cebfatagen2026-10-14 11:33 UTC277
  if (door && !alerts.some(a => a.startsWith(door))) alerts.unshift(door);
e76ab808e7adatagen2026-10-10 13:46 UTC278
  const stamina = (list || []).find(([name]) => name === "x-stamina");
a6caa2f99786atagen2026-10-12 10:38 UTC279
  if (stamina && stamina[1].startsWith("0/")) alerts.push("Exhausted. HOLD for the refractory period.");
431c1bc8ded1atagen2026-10-10 13:20 UTC280
  for (const alert of alerts) box.append(el("h2", { textContent: alert }));
431c1bc8ded1atagen2026-10-10 13:20 UTC281
}
431c1bc8ded1atagen2026-10-10 13:20 UTC282
185851447cebatagen2026-10-10 12:58 UTC283
function renderTargets() {
185851447cebatagen2026-10-10 12:58 UTC284
  const box = $("targets");
185851447cebatagen2026-10-10 12:58 UTC285
  box.replaceChildren();
d604371299fbatagen2026-10-10 13:54 UTC286
  const all = targets();
d604371299fbatagen2026-10-10 13:54 UTC287
  for (const [group, title] of GROUPS) {
d604371299fbatagen2026-10-10 13:54 UTC288
    const members = all.filter(t => t.group === group);
d604371299fbatagen2026-10-10 13:54 UTC289
    if (!members.length) continue;
d604371299fbatagen2026-10-10 13:54 UTC290
    box.append(el("h4", { textContent: title }));
d604371299fbatagen2026-10-10 13:54 UTC291
    for (const t of members) {
d604371299fbatagen2026-10-10 13:54 UTC292
      const radio = el("input", { type: "radio", name: "target", checked: t.path === target, onchange: () => chooseTarget(t) });
0cad5f1c5014atagen2026-10-14 11:36 UTC293
      const name = fresh.has(t.path) ? el("b", { textContent: t.name }) : t.name;
0cad5f1c5014atagen2026-10-14 11:36 UTC294
      box.append(el("label", {}, radio, " ", name, " ", el("small", { textContent: short(t.path) })), el("br"));
d604371299fbatagen2026-10-10 13:54 UTC295
    }
185851447cebatagen2026-10-10 12:58 UTC296
  }
185851447cebatagen2026-10-10 12:58 UTC297
}
185851447cebatagen2026-10-10 12:58 UTC298
185851447cebatagen2026-10-10 12:58 UTC299
function renderMethods() {
185851447cebatagen2026-10-10 12:58 UTC300
  const box = $("methods");
185851447cebatagen2026-10-10 12:58 UTC301
  box.replaceChildren();
58e1fb3943e6atagen2026-10-10 13:30 UTC302
  const allowed = allowedMethods(current());
58e1fb3943e6atagen2026-10-10 13:30 UTC303
  for (const m of METHODS.filter(m => allowed.includes(m))) {
58e1fb3943e6atagen2026-10-10 13:30 UTC304
    const radio = el("input", { type: "radio", name: "method", checked: m === method, onchange: () => { method = m; render(); } });
185851447cebatagen2026-10-10 12:58 UTC305
    const label = el("label", {}, radio, " ");
185851447cebatagen2026-10-10 12:58 UTC306
    label.append(m === "JELQ" ? el("big", {}, el("b", { textContent: "JELQ" })) : m);
185851447cebatagen2026-10-10 12:58 UTC307
    box.append(label, " ");
185851447cebatagen2026-10-10 12:58 UTC308
  }
185851447cebatagen2026-10-10 12:58 UTC309
}
185851447cebatagen2026-10-10 12:58 UTC310
185851447cebatagen2026-10-10 12:58 UTC311
function fieldNames() {
4b690ff501b8atagen2026-10-10 13:27 UTC312
  const t = current();
58e1fb3943e6atagen2026-10-10 13:30 UTC313
  const key = t.key + " " + method;
58e1fb3943e6atagen2026-10-10 13:30 UTC314
  let names = BY_TARGET[key] || [];
58e1fb3943e6atagen2026-10-10 13:30 UTC315
  if (key === "run JELQ") {
58e1fb3943e6atagen2026-10-10 13:30 UTC316
    const chosen = technique(fieldValue("technique"));
58e1fb3943e6atagen2026-10-10 13:30 UTC317
    names = ["technique"];
58e1fb3943e6atagen2026-10-10 13:30 UTC318
    if (options("also").length > 1) names.push("also");
e028a4184d00atagen2026-10-14 10:49 UTC319
    if (chosen && chosen.intensity) names.push("intensity");
58e1fb3943e6atagen2026-10-10 13:30 UTC320
    if (["required", "optional"].includes(lubeRule())) names.push("lube");
58e1fb3943e6atagen2026-10-10 13:30 UTC321
  }
4b690ff501b8atagen2026-10-10 13:27 UTC322
  const scoped = run && !["runs", "global"].includes(t.key);
4b690ff501b8atagen2026-10-10 13:27 UTC323
  const asked = challenged && scoped && !names.includes("authorization");
4b690ff501b8atagen2026-10-10 13:27 UTC324
  return (asked ? ["authorization"] : []).concat(names);
185851447cebatagen2026-10-10 12:58 UTC325
}
185851447cebatagen2026-10-10 12:58 UTC326
f16ab17c8ff6atagen2026-10-13 11:14 UTC327
function renderExcuses() {
f16ab17c8ff6atagen2026-10-13 11:14 UTC328
  const said = excuses.concat(["the truth"]);
f16ab17c8ff6atagen2026-10-13 11:14 UTC329
  $("excuses").replaceChildren(...said.map(e => el("option", {
f16ab17c8ff6atagen2026-10-13 11:14 UTC330
    value: nonce ? 'Digest username="' + e + '", nonce="' + nonce + '"' : 'Excuse "' + e + '"',
f16ab17c8ff6atagen2026-10-13 11:14 UTC331
  })));
f16ab17c8ff6atagen2026-10-13 11:14 UTC332
}
f16ab17c8ff6atagen2026-10-13 11:14 UTC333
185851447cebatagen2026-10-10 12:58 UTC334
function renderFields() {
185851447cebatagen2026-10-10 12:58 UTC335
  const table = $("fields");
185851447cebatagen2026-10-10 12:58 UTC336
  table.replaceChildren();
185851447cebatagen2026-10-10 12:58 UTC337
  for (const name of fieldNames()) {
185851447cebatagen2026-10-10 12:58 UTC338
    const f = FIELDS[name];
ec6fd4bb5b60atagen2026-10-11 04:34 UTC339
    let value = fieldValue(name);
ec6fd4bb5b60atagen2026-10-11 04:34 UTC340
    if (name === "ifMatch" && !value) value = values.ifMatch = etags[current().path] || "";
58e1fb3943e6atagen2026-10-10 13:30 UTC341
    const choices = options(name);
185851447cebatagen2026-10-10 12:58 UTC342
    let input;
58e1fb3943e6atagen2026-10-10 13:30 UTC343
    if (choices) {
0cad5f1c5014atagen2026-10-14 11:36 UTC344
      const isNew = o => ["technique", "also"].includes(name) && fresh.has("technique " + o);
0cad5f1c5014atagen2026-10-14 11:36 UTC345
      input = el("select", {}, ...choices.map(o => el("option", { value: o, textContent: o || "(none)", selected: o === value, style: isNew(o) ? "font-weight: bold" : "" })));
185851447cebatagen2026-10-10 12:58 UTC346
    } else if (f.kind === "textarea") {
185851447cebatagen2026-10-10 12:58 UTC347
      input = el("textarea", { rows: 3, cols: 40, value, placeholder: f.placeholder || "" });
185851447cebatagen2026-10-10 12:58 UTC348
    } else {
cddf59e37d5fatagen2026-10-13 12:37 UTC349
      const hint = name === "authorization" && nonce ? 'Digest username="helping Nan", nonce="' + nonce + '"' : f.placeholder || "";
cddf59e37d5fatagen2026-10-13 12:37 UTC350
      input = el("input", { type: f.kind, value, placeholder: hint, size: 30 });
185851447cebatagen2026-10-10 12:58 UTC351
    }
58e1fb3943e6atagen2026-10-10 13:30 UTC352
    input.oninput = () => { values[name] = input.value; };
58e1fb3943e6atagen2026-10-10 13:30 UTC353
    input.onchange = () => { values[name] = input.value; if (choices) renderFields(); };
7d3fb63208bfatagen2026-10-11 04:38 UTC354
    if (name === "authorization") input.setAttribute("list", "excuses");
185851447cebatagen2026-10-10 12:58 UTC355
    table.append(el("tr", {}, el("td", { align: "right", valign: "top", textContent: f.label }), el("td", {}, input)));
185851447cebatagen2026-10-10 12:58 UTC356
  }
58e1fb3943e6atagen2026-10-10 13:30 UTC357
  const extra = el("textarea", { rows: 3, cols: 40, value: raw("extra"), placeholder: FIELDS.extra.placeholder });
4b690ff501b8atagen2026-10-10 13:27 UTC358
  extra.oninput = () => { values.extra = extra.value; };
4b690ff501b8atagen2026-10-10 13:27 UTC359
  const more = el("details", { open: Boolean(values.extra) }, el("summary", { textContent: "more headers" }), extra);
4b690ff501b8atagen2026-10-10 13:27 UTC360
  table.append(el("tr", {}, el("td"), el("td", {}, more)));
185851447cebatagen2026-10-10 12:58 UTC361
  $("send").value = method + " " + short(current().path);
185851447cebatagen2026-10-10 12:58 UTC362
}
185851447cebatagen2026-10-10 12:58 UTC363
185851447cebatagen2026-10-10 12:58 UTC364
function render() {
58e1fb3943e6atagen2026-10-10 13:30 UTC365
  if (!allowedMethods(current()).includes(method)) return chooseTarget(current());
185851447cebatagen2026-10-10 12:58 UTC366
  renderTargets();
185851447cebatagen2026-10-10 12:58 UTC367
  renderMethods();
185851447cebatagen2026-10-10 12:58 UTC368
  renderFields();
185851447cebatagen2026-10-10 12:58 UTC369
}
185851447cebatagen2026-10-10 12:58 UTC370
185851447cebatagen2026-10-10 12:58 UTC371
function parseLinks(header) {
58e1fb3943e6atagen2026-10-10 13:30 UTC372
  const resources = [];
58e1fb3943e6atagen2026-10-10 13:30 UTC373
  const moves = [];
185851447cebatagen2026-10-10 12:58 UTC374
  for (const match of header.matchAll(/<([^>]*)>((?:\s*;\s*[a-z-]+="[^"]*")*)/g)) {
185851447cebatagen2026-10-10 12:58 UTC375
    const params = Object.fromEntries([...match[2].matchAll(/([a-z-]+)="([^"]*)"/g)].map(p => [p[1], p[2]]));
185851447cebatagen2026-10-10 12:58 UTC376
    const name = (params.rel || "").replace(REL, "");
58e1fb3943e6atagen2026-10-10 13:30 UTC377
    if (name === "technique") {
e028a4184d00atagen2026-10-14 10:49 UTC378
      moves.push({ name: params.title, lubricant: params.lubricant || null, intensity: params.intensity || null });
58e1fb3943e6atagen2026-10-10 13:30 UTC379
      continue;
58e1fb3943e6atagen2026-10-10 13:30 UTC380
    }
185851447cebatagen2026-10-10 12:58 UTC381
    const methods = (params.allow || "GET").split(",").map(s => s.trim());
d46f9d0b953aatagen2026-10-10 14:41 UTC382
    resources.push({ key: name, group: GROUP_OF[name] || "play", name: NAMES[name] || params.title || name, path: match[1], methods });
185851447cebatagen2026-10-10 12:58 UTC383
  }
58e1fb3943e6atagen2026-10-10 13:30 UTC384
  return [resources, moves];
99524cf9f17catagen2026-10-10 10:53 UTC385
}
99524cf9f17catagen2026-10-10 10:53 UTC386
99524cf9f17catagen2026-10-10 10:53 UTC387
function hrefs(value, found) {
99524cf9f17catagen2026-10-10 10:53 UTC388
  if (value && typeof value === "object") {
185851447cebatagen2026-10-10 12:58 UTC389
    if (typeof value.href === "string") {
185851447cebatagen2026-10-10 12:58 UTC390
      const name = value.title || value.item || value.name || value.href.split("/").pop();
d604371299fbatagen2026-10-10 13:54 UTC391
      found.push({ key: "offered", group: "said", name, path: value.href, methods: [value.method || "GET"] });
185851447cebatagen2026-10-10 12:58 UTC392
    }
99524cf9f17catagen2026-10-10 10:53 UTC393
    for (const key in value) hrefs(value[key], found);
99524cf9f17catagen2026-10-10 10:53 UTC394
  }
99524cf9f17catagen2026-10-10 10:53 UTC395
  return found;
99524cf9f17catagen2026-10-10 10:53 UTC396
}
99524cf9f17catagen2026-10-10 10:53 UTC397
185851447cebatagen2026-10-10 12:58 UTC398
function log(text) {
585e6b7ce086atagen2026-10-10 13:43 UTC399
  $("log").prepend(text + "\n\n");
585e6b7ce086atagen2026-10-10 13:43 UTC400
  $("frame").scrollTop = 0;
99524cf9f17catagen2026-10-10 10:53 UTC401
}
99524cf9f17catagen2026-10-10 10:53 UTC402
185851447cebatagen2026-10-10 12:58 UTC403
function describe(r) {
185851447cebatagen2026-10-10 12:58 UTC404
  const lines = ["< " + r.status + " " + r.reason];
185851447cebatagen2026-10-10 12:58 UTC405
  for (const [name, value] of r.headers) {
185851447cebatagen2026-10-10 12:58 UTC406
    if (!QUIET.has(name)) lines.push("< " + name + ": " + short(value));
185851447cebatagen2026-10-10 12:58 UTC407
  }
185851447cebatagen2026-10-10 12:58 UTC408
  let body = null;
185851447cebatagen2026-10-10 12:58 UTC409
  try { body = JSON.parse(r.body); } catch (e) {}
185851447cebatagen2026-10-10 12:58 UTC410
  if (body && typeof body === "object") {
185851447cebatagen2026-10-10 12:58 UTC411
    const rest = Object.assign({}, body);
185851447cebatagen2026-10-10 12:58 UTC412
    if (rest.detail) lines.push("  " + rest.detail);
185851447cebatagen2026-10-10 12:58 UTC413
    for (const key of ["type", "title", "status", "detail"]) delete rest[key];
185851447cebatagen2026-10-10 12:58 UTC414
    if (Object.keys(rest).length) lines.push(short(JSON.stringify(rest, null, 2)).replace(/^/gm, "  "));
185851447cebatagen2026-10-10 12:58 UTC415
  } else if (r.body) {
e76ab808e7adatagen2026-10-10 13:46 UTC416
    const range = (r.headers.find(h => h[0] === "content-range") || [])[1] || "";
e76ab808e7adatagen2026-10-10 13:46 UTC417
    const partial = r.status === 206 && range.match(/^bytes (\d+)-(\d+)\/(\d+)$/);
185851447cebatagen2026-10-10 12:58 UTC418
    lines.push("  " + r.body);
ec6fd4bb5b60atagen2026-10-11 04:34 UTC419
    if (partial) lines.push("  [bytes " + partial[1] + "-" + partial[2] + " of " + partial[3] + ". Half-mast.]");
185851447cebatagen2026-10-10 12:58 UTC420
  }
185851447cebatagen2026-10-10 12:58 UTC421
  return lines.join("\n");
99524cf9f17catagen2026-10-10 10:53 UTC422
}
99524cf9f17catagen2026-10-10 10:53 UTC423
4f54028cba69atagen2026-10-14 11:45 UTC424
function absorb(r, sentPath) {
185851447cebatagen2026-10-10 12:58 UTC425
  const headers = Object.fromEntries(r.headers.map(([n, v]) => [n.toLowerCase(), v]));
ec6fd4bb5b60atagen2026-10-11 04:34 UTC426
  if (headers.etag) etags[sentPath] = headers.etag;
ec6fd4bb5b60atagen2026-10-11 04:34 UTC427
  const digest = (headers["www-authenticate"] || "").match(/nonce="([^"]*)"/);
f16ab17c8ff6atagen2026-10-13 11:14 UTC428
  if ("www-authenticate" in headers) nonce = digest ? digest[1] : null;
f16ab17c8ff6atagen2026-10-13 11:14 UTC429
  renderExcuses();
185851447cebatagen2026-10-10 12:58 UTC430
  let body = null;
185851447cebatagen2026-10-10 12:58 UTC431
  try { body = JSON.parse(r.body); } catch (e) {}
58e1fb3943e6atagen2026-10-10 13:30 UTC432
  const started = r.status === 201 && (sentPath === "/runs" || sentPath.startsWith("/runs?"));
185851447cebatagen2026-10-10 12:58 UTC433
58e1fb3943e6atagen2026-10-10 13:30 UTC434
  if (started) {
58e1fb3943e6atagen2026-10-10 13:30 UTC435
    run = headers.location;
ddcf8b3a91dcatagen2026-10-14 11:03 UTC436
    lastStats = {};
58e1fb3943e6atagen2026-10-10 13:30 UTC437
    log("--- a new run has started at " + short(run) + " ---");
185851447cebatagen2026-10-10 12:58 UTC438
  }
431c1bc8ded1atagen2026-10-10 13:20 UTC439
  if ("x-eq" in headers) {
431c1bc8ded1atagen2026-10-10 13:20 UTC440
    renderStats(headers);
431c1bc8ded1atagen2026-10-10 13:20 UTC441
    renderAlerts(r.headers);
4b690ff501b8atagen2026-10-10 13:27 UTC442
    challenged = r.status === 401 || "via" in headers;
431c1bc8ded1atagen2026-10-10 13:20 UTC443
  }
58e1fb3943e6atagen2026-10-10 13:30 UTC444
  if (headers.link) [linked, techniques] = parseLinks(headers.link);
0a0bfee8329eatagen2026-10-11 05:11 UTC445
  if (run) {
139164489032atagen2026-10-11 05:46 UTC446
    const active = run;
14de3e1e8dc2atagen2026-10-11 06:19 UTC447
    const seq = ++inventorySeq;
0a0bfee8329eatagen2026-10-11 05:11 UTC448
    fetch("/ui/send", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ method: "GET", path: run + "/inventory", headers: [], body: "" }) })
0a0bfee8329eatagen2026-10-11 05:11 UTC449
      .then(r => r.json())
0a0bfee8329eatagen2026-10-11 05:11 UTC450
      .then(r => {
14de3e1e8dc2atagen2026-10-11 06:19 UTC451
        if (run !== active || seq !== inventorySeq) return;
f16ab17c8ff6atagen2026-10-13 11:14 UTC452
        try {
f16ab17c8ff6atagen2026-10-13 11:14 UTC453
          const held = JSON.parse(r.body);
f16ab17c8ff6atagen2026-10-13 11:14 UTC454
          stocked = (held.items || []).map(i => i.name);
f16ab17c8ff6atagen2026-10-13 11:14 UTC455
          excuses = held.excuses || [];
f16ab17c8ff6atagen2026-10-13 11:14 UTC456
          renderExcuses();
f16ab17c8ff6atagen2026-10-13 11:14 UTC457
          render();
f16ab17c8ff6atagen2026-10-13 11:14 UTC458
        } catch (e) {}
0a0bfee8329eatagen2026-10-11 05:11 UTC459
      });
0a0bfee8329eatagen2026-10-11 05:11 UTC460
  }
185851447cebatagen2026-10-10 12:58 UTC461
  offered = body ? hrefs(body, []) : [];
58e1fb3943e6atagen2026-10-10 13:30 UTC462
  if (headers.location && !started) {
d604371299fbatagen2026-10-10 13:54 UTC463
    offered.unshift({ key: "offered", group: "said", name: short(headers.location).split("/").pop() + " (Location)", path: headers.location, methods: ["GET"] });
185851447cebatagen2026-10-10 12:58 UTC464
  }
4f54028cba69atagen2026-10-14 11:45 UTC465
  if (run && "x-run-over" in headers) {
185851447cebatagen2026-10-10 12:58 UTC466
    log("--- the run has ended ---");
431c1bc8ded1atagen2026-10-10 13:20 UTC467
    renderAlerts(null);
4b690ff501b8atagen2026-10-10 13:27 UTC468
    challenged = false;
185851447cebatagen2026-10-10 12:58 UTC469
    run = null;
185851447cebatagen2026-10-10 12:58 UTC470
    linked = [];
58e1fb3943e6atagen2026-10-10 13:30 UTC471
    techniques = [];
185851447cebatagen2026-10-10 12:58 UTC472
  }
0cad5f1c5014atagen2026-10-14 11:36 UTC473
  const menu = targets().filter(t => t.group !== "said").map(t => t.path).concat(techniques.map(t => "technique " + t.name));
0cad5f1c5014atagen2026-10-14 11:36 UTC474
  fresh = started ? new Set() : new Set(menu.filter(m => !known.has(m)));
0cad5f1c5014atagen2026-10-14 11:36 UTC475
  known = new Set(menu);
185851447cebatagen2026-10-10 12:58 UTC476
  if (started || !targets().some(t => t.path === target)) {
185851447cebatagen2026-10-10 12:58 UTC477
    const home = linked.find(t => t.key === "run");
58e1fb3943e6atagen2026-10-10 13:30 UTC478
    chooseTarget(home || GLOBALS[0]);
185851447cebatagen2026-10-10 12:58 UTC479
  } else {
185851447cebatagen2026-10-10 12:58 UTC480
    render();
99524cf9f17catagen2026-10-10 10:53 UTC481
  }
99524cf9f17catagen2026-10-10 10:53 UTC482
}
99524cf9f17catagen2026-10-10 10:53 UTC483
185851447cebatagen2026-10-10 12:58 UTC484
async function go(event) {
185851447cebatagen2026-10-10 12:58 UTC485
  event.preventDefault();
185851447cebatagen2026-10-10 12:58 UTC486
  const t = current();
185851447cebatagen2026-10-10 12:58 UTC487
  const key = t.key + " " + method;
185851447cebatagen2026-10-10 12:58 UTC488
  const value = name => fieldValue(name).trim();
185851447cebatagen2026-10-10 12:58 UTC489
  const headers = [];
185851447cebatagen2026-10-10 12:58 UTC490
  let path = t.path;
185851447cebatagen2026-10-10 12:58 UTC491
  for (const name of fieldNames()) {
185851447cebatagen2026-10-10 12:58 UTC492
    const f = FIELDS[name];
185851447cebatagen2026-10-10 12:58 UTC493
    if (f.header && value(name)) headers.push([f.header, value(name)]);
185851447cebatagen2026-10-10 12:58 UTC494
  }
185851447cebatagen2026-10-10 12:58 UTC495
  if (fieldNames().includes("upgrade") && value("upgrade")) headers.push(["Connection", "upgrade"]);
58e1fb3943e6atagen2026-10-10 13:30 UTC496
  for (const line of raw("extra").split("\n")) {
185851447cebatagen2026-10-10 12:58 UTC497
    const at = line.indexOf(":");
185851447cebatagen2026-10-10 12:58 UTC498
    if (at > 0) headers.push([line.slice(0, at).trim(), line.slice(at + 1).trim()]);
185851447cebatagen2026-10-10 12:58 UTC499
  }
185851447cebatagen2026-10-10 12:58 UTC500
  if (key === "run JELQ") {
58e1fb3943e6atagen2026-10-10 13:30 UTC501
    const names = fieldNames();
58e1fb3943e6atagen2026-10-10 13:30 UTC502
    const chosen = [value("technique")].concat(names.includes("also") && value("also") ? [value("also")] : []);
58e1fb3943e6atagen2026-10-10 13:30 UTC503
    path += "?technique=" + encodeURIComponent(chosen.join(","));
58e1fb3943e6atagen2026-10-10 13:30 UTC504
    if (names.includes("intensity")) path += "&intensity=" + encodeURIComponent(value("intensity"));
08ffcd6cbba5atagen2026-10-14 11:19 UTC505
    delete values.also;
185851447cebatagen2026-10-10 12:58 UTC506
  }
185851447cebatagen2026-10-10 12:58 UTC507
  if (key === "runs POST" && value("seed")) path += "?seed=" + encodeURIComponent(value("seed"));
99524cf9f17catagen2026-10-10 10:53 UTC508
585e6b7ce086atagen2026-10-10 13:43 UTC509
  const sent = ["> " + method + " " + short(path)].concat(headers.map(([name, v]) => "> " + name + ": " + v)).join("\n");
cddf59e37d5fatagen2026-10-13 12:37 UTC510
  delete values.authorization;
ef7d6dee5976atagen2026-10-10 13:23 UTC511
  headers.push(["Accept-Language", $("locale").value]);
7288fc269bb6atagen2026-10-10 13:36 UTC512
  let r;
7288fc269bb6atagen2026-10-10 13:36 UTC513
  try {
7288fc269bb6atagen2026-10-10 13:36 UTC514
    const response = await fetch("/ui/send", {
7288fc269bb6atagen2026-10-10 13:36 UTC515
      method: "POST",
7288fc269bb6atagen2026-10-10 13:36 UTC516
      headers: { "Content-Type": "application/json" },
6619c458c23eatagen2026-10-14 08:08 UTC517
      body: JSON.stringify({ method, path, headers }),
7288fc269bb6atagen2026-10-10 13:36 UTC518
    });
7288fc269bb6atagen2026-10-10 13:36 UTC519
    r = await response.json();
7288fc269bb6atagen2026-10-10 13:36 UTC520
  } catch (error) {
585e6b7ce086atagen2026-10-10 13:43 UTC521
    log(sent + "\n! The server did not answer. Is jelqd still running? (" + error.message + ")");
7288fc269bb6atagen2026-10-10 13:36 UTC522
    return;
7288fc269bb6atagen2026-10-10 13:36 UTC523
  }
7288fc269bb6atagen2026-10-10 13:36 UTC524
  if (!Array.isArray(r.headers)) {
585e6b7ce086atagen2026-10-10 13:43 UTC525
    log(sent + "\n! The page could not read that reply:\n  " + JSON.stringify(r));
7288fc269bb6atagen2026-10-10 13:36 UTC526
    return;
7288fc269bb6atagen2026-10-10 13:36 UTC527
  }
585e6b7ce086atagen2026-10-10 13:43 UTC528
  log(sent + "\n" + describe(r));
4f54028cba69atagen2026-10-14 11:45 UTC529
  absorb(r, path);
99524cf9f17catagen2026-10-10 10:53 UTC530
}
185851447cebatagen2026-10-10 12:58 UTC531
185851447cebatagen2026-10-10 12:58 UTC532
$("form").onsubmit = go;
ef7d6dee5976atagen2026-10-10 13:23 UTC533
$("locale").value = localStorage.getItem("locale") || "en-GB";
ef7d6dee5976atagen2026-10-10 13:23 UTC534
$("locale").onchange = () => localStorage.setItem("locale", $("locale").value);
185851447cebatagen2026-10-10 12:58 UTC535
renderStats(null);
185851447cebatagen2026-10-10 12:58 UTC536
render();
585e6b7ce086atagen2026-10-10 13:43 UTC537
log("Welcome. Choose \"start a new run\" and send POST to begin. The newest turn is always at the top.");
99524cf9f17catagen2026-10-10 10:53 UTC538
</script>
99524cf9f17catagen2026-10-10 10:53 UTC539
</body>
99524cf9f17catagen2026-10-10 10:53 UTC540
</html>