2323#
2424# Parameter fields: name, doc, required (default False), example (pre-filled
2525# in the form), enum (renders a dropdown; "dynamic:<key>" pulls the list
26- # injected by build_docs_spec).
26+ # injected by build_docs_spec), header (renders a checkbox that sends the
27+ # named request header instead of a query parameter — see FORCE_REFRESH_PARAM
28+ # below for why force_refresh needs this).
2729# ---------------------------------------------------------------------------
2830
31+ #: Shared definition for every endpoint that honours a cache refresh signal.
32+ #: Rendered as a checkbox (see paramRow's "header" branch in DOCS_HTML) that
33+ #: sets the ``X-Force-Refresh`` request header rather than a ``force_refresh``
34+ #: query parameter. That distinction matters: appending ``&force_refresh=true``
35+ #: to the URL changes nginx's cache key, so the refreshed response is written
36+ #: into a *different* edge-cache slot and the canonical URL people actually
37+ #: call is never healed. The header leaves the URL untouched, so one ticked
38+ #: "Run" refreshes the v3-cached edge slot and the VFBquery-level cache
39+ #: together, at the URL everyone else hits next — see ha_api._FORCE_REFRESH_HEADER
40+ #: and CACHING.md for the server-side half of this.
41+ FORCE_REFRESH_PARAM = {
42+ "name" : "force_refresh" ,
43+ "header" : "X-Force-Refresh" ,
44+ "doc" : ("Force a cache clear: bypasses the edge cache and makes VFBquery "
45+ "recompute now. Refreshes the canonical URL directly, so this "
46+ "call and the very next one — even unticked — both return fresh "
47+ "data." ),
48+ }
49+
2950ENDPOINT_GROUPS = [
3051 {
3152 "group" : "Term information" ,
4263 {"name" : "id" , "required" : True ,
4364 "doc" : "Ontology class or individual short_form" ,
4465 "example" : "FBbt_00003748" },
45- {"name" : "force_refresh" ,
46- "doc" : "true bypasses the result cache" ,
47- "example" : "" },
66+ FORCE_REFRESH_PARAM ,
4867 ],
4968 },
5069 {
6988 {"name" : "include_graph" ,
7089 "doc" : "true adds a graph rendering of the rows" ,
7190 "example" : "" },
72- {"name" : "force_refresh" ,
73- "doc" : "true bypasses the result cache" , "example" : "" },
91+ FORCE_REFRESH_PARAM ,
7492 ],
7593 },
7694 {
203221 "example" : "" },
204222 {"name" : "include_graph" ,
205223 "doc" : "true adds a graph rendering" , "example" : "" },
224+ FORCE_REFRESH_PARAM ,
206225 ],
207226 },
208227 ],
@@ -512,7 +531,13 @@ def build_docs_spec(version, query_types=None, catmaid_commands=None):
512531function paramRow(param, kind){
513532 const vocab = SPEC.vocabularies || {};
514533 let input;
515- if (param.enum && String(param.enum).startsWith("dynamic:")){
534+ if (param.header){
535+ // A checkbox that sends a request header instead of a query parameter —
536+ // see FORCE_REFRESH_PARAM in the Python source for why force_refresh
537+ // needs this rather than the plain text input below.
538+ input = el("input", {type: "checkbox", "data-name": param.name,
539+ "data-kind": "header", "data-header-name": param.header});
540+ } else if (param.enum && String(param.enum).startsWith("dynamic:")){
516541 const key = param.enum.split(":")[1];
517542 let values = [];
518543 if (key === "query_types") values = vocab.query_types || [];
@@ -537,7 +562,14 @@ def build_docs_spec(version, query_types=None, catmaid_commands=None):
537562function buildUrl(root, endpoint){
538563 let path = endpoint.path;
539564 const query = new URLSearchParams();
565+ const headers = {};
540566 for (const input of root.querySelectorAll("[data-name]")){
567+ if (input.dataset.kind === "header"){
568+ // Ticked -> the named header, at the unmodified URL below, never a
569+ // query parameter — that's the whole point (see FORCE_REFRESH_PARAM).
570+ if (input.checked) headers[input.dataset.headerName] = "true";
571+ continue;
572+ }
541573 const value = input.value.trim();
542574 if (input.dataset.kind === "path"){
543575 if (!value) return {error: input.dataset.name + " is required"};
@@ -552,7 +584,7 @@ def build_docs_spec(version, query_types=None, catmaid_commands=None):
552584 }
553585 }
554586 const qs = query.toString();
555- return {url: path + (qs ? "?" + qs : "")};
587+ return {url: path + (qs ? "?" + qs : ""), headers: headers };
556588}
557589
558590async function run(root, endpoint){
@@ -561,13 +593,22 @@ def build_docs_spec(version, query_types=None, catmaid_commands=None):
561593 const out = root.querySelector("pre.result");
562594 const built = buildUrl(root, endpoint);
563595 if (built.error){ meta.innerHTML = '<span class="bad">' + built.error + "</span>"; return; }
564- urlBox.textContent = new URL(built.url, window.location.href).href;
596+ // A bypass checkbox is a one-shot action, not a sticky setting: reset it to
597+ // off as soon as this run has captured whether it was ticked, so a bypass
598+ // never carries over into someone re-running the same card a second time
599+ // (e.g. to try a different id) without deliberately ticking it again.
600+ for (const box of root.querySelectorAll('[data-kind="header"]')) box.checked = false;
601+ const headerNames = Object.keys(built.headers || {});
602+ urlBox.textContent = new URL(built.url, window.location.href).href +
603+ (headerNames.length ? " (+ header " + headerNames.map(
604+ (h) => h + ": " + built.headers[h]).join(", ") + ")" : "");
565605 meta.textContent = "running…";
566606 out.hidden = false;
567607 out.textContent = "";
568608 const started = performance.now();
569609 try {
570- const resp = await fetch(built.url, {headers: {Accept: "application/json"}});
610+ const resp = await fetch(built.url, {headers: Object.assign(
611+ {Accept: "application/json"}, built.headers || {})});
571612 const ms = Math.round(performance.now() - started);
572613 const text = await resp.text();
573614 let shown = text;
0 commit comments