Merge branch 'feat/bundle-routes' into int/wave4

This commit is contained in:
Jason Hoku
2026-11-04 18:39:41 -08:00
17 changed files with 565 additions and 93 deletions
+7 -1
View File
@@ -77,7 +77,7 @@ jobs:
import os, re, sys
sys.path.insert(0, os.getcwd())
import html_generator
page = html_generator.get_html_template("ci", {"items": []}, "1")
page = html_generator.get_html_template("ci", {"items": []}, "1", inline_assets=True)
bodies = re.findall(r"<script(?![^>]*\bsrc=)[^>]*>(.*?)</script>", page, re.S)
assert bodies, "no inline <script> found in the generated page"
out = os.environ.get("RUNNER_TEMP", ".")
@@ -85,6 +85,12 @@ jobs:
with open(os.path.join(out, f"bundle_{n}.js"), "w", encoding="utf-8") as fh:
fh.write(body)
print(f"wrote {len(bodies)} script bodies to {out}")
# globals script + bundle as ONE script: catches a resource redeclaring
# TARGET_NODE_ID / USCG_STANDALONE / fullManifest
with open(os.path.join(out, "bundle_all.js"), "w", encoding="utf-8") as fh:
fh.write("\n;\n".join(bodies))
with open(os.path.join(out, "bundle_route.js"), "w", encoding="utf-8") as fh:
fh.write(html_generator.get_dashboard_bundle("js").text)
PY
for f in "${RUNNER_TEMP:-.}"/bundle_*.js; do
node --check "$f" || { echo "FAIL $f"; exit 1; }
+1 -1
View File
@@ -19,7 +19,7 @@ Read `ProjectStructure.md` in this directory for the full reference (file struct
1. **Python Backend** (runs inside ComfyUI server) — nodes, orchestration, generation, API endpoints
2. **JS Frontend Bridge** (`web/`) — registers nodes in ComfyUI browser tab, served at `/extensions/`
3. **Dashboard SPA** (`resources/`) — standalone app inlined into iframe by `html_generator.py`, zero external requests
3. **Dashboard SPA** (`resources/`) — standalone app; `html_generator.py` builds a small shell (iframe `srcdoc` / `/config_tester/dashboard`) that loads two cached bundle routes (`/config_tester/dashboard_bundle_js` / `_css`, `?v=<hash>`); `inline_assets=True` / `USCG_INLINE_DASHBOARD_ASSETS=1` inlines them again
## Critical Files & Their Roles
+1 -1
View File
@@ -94,7 +94,7 @@ for f in resources/*.js; do
done
```
The Dashboard files in `resources/` are inlined into ONE script (see `js_files` in `html_generator.py`), so a duplicate top-level name only shows up in the assembled page. The `js-syntax` job therefore also builds the page with `html_generator.get_html_template()` and syntax-checks every inline script body (the snippet is in `.github/workflows/tests.yml`).
The Dashboard files in `resources/` share ONE script scope (see `JS_FILES` in `html_generator.py`; the page loads them as one bundle after a small globals script), so a duplicate top-level name only shows up in the assembled page. The `js-syntax` job therefore also builds the page with `html_generator.get_html_template(..., inline_assets=True)` and syntax-checks every inline script body, plus the globals script + bundle joined as one script (the snippet is in `.github/workflows/tests.yml`).
---
+8 -5
View File
@@ -244,6 +244,8 @@ All routes registered via `PromptServer.instance.routes`. One row per route this
| POST | `/config_tester/save_manifest` | `__init__.py` | `{session_name, manifest}` | Full merge save (keeps server-side `meta` and `comparisons`) |
| POST | `/config_tester/get_session_html` | `__init__.py` | `{session_name, node_id}` | Generate the dashboard HTML on demand (empty manifest for an unknown session, not a 404) |
| GET | `/config_tester/dashboard` | `__init__.py` | `?session=` (optional) | Dashboard SPA as a top-level page (standalone mode, `USCG_STANDALONE`, own websocket via `resources/logic_standalone.js`). A lazy shell (no manifest inlined); gzip + content ETag (304 on `If-None-Match`) |
| GET | `/config_tester/dashboard_bundle_js` | `__init__.py` | `?v=<hash>` (compared with the bundle hash, never used as a path) | The dashboard's concatenated `resources/logic_*.js` (`html_generator.get_dashboard_bundle('js')`). Fixed path, no path parameters. `text/javascript`, weak ETag (304 on `If-None-Match`), precomputed gzip, `Cache-Control: public, max-age=31536000, immutable` when `v` matches the current hash (`no-cache` otherwise; a stale `v` still gets the current bundle) |
| GET | `/config_tester/dashboard_bundle_css` | `__init__.py` | `?v=<hash>` | Same for `resources/report.css` (`text/css`) |
| GET | `/config_tester/manifest` | `__init__.py` | `?session=` | A session's manifest as compact JSON for the SPA (lazy loading). Takes a session NAME, never a path. Weak `ETag` (mtime+size) + `If-None-Match` -> 304, gzip when accepted, `Cache-Control: no-cache`; 400 empty name, 403 outside benchmarks, 404 unknown, 503 + `Retry-After` for a torn read (`save_manifest` is not atomic; 3 retries first). Logic in `run_api.load_manifest_for_fetch` |
| POST | `/config_tester/run` | `__init__.py` | `{configs_json or config_name, session_name?, overwrite_existing?, force? (default true), front?, client_id?}` | Headless run: builds the Generator+Dashboard prompt and enqueues it in-process like `/prompt`. Helpers in `run_api.py` |
| GET | `/config_tester/run_status` | `__init__.py` | `?prompt_id=` | `pending`, `running`, `success` or `error` + session / dashboard_url / item_count |
@@ -331,10 +333,11 @@ All routes registered via `PromptServer.instance.routes`. One row per route this
* **`html_generator.py`** (57 lines)
* `get_html_template(title, manifest_data, node_id, standalone=False, inline_manifest=False)` — reads all `/resources/` files, inlines them into `template.html`. By default the page is a **lazy shell**: `fullManifest` is empty and flagged `__uscg_lazy__`, `manifest_data` is ignored, and the SPA fetches `GET /config_tester/manifest?session=<title>` itself (`inline_manifest=True` restores the old inlined data; used by tests)
* **JS Load Order:** `logic_state.js` → `logic_utils.js` → `logic_ratings.js` → `logic_wfparams.js` → `logic_dragdrop.js` → `logic_ui.js` → `logic_upscale_modal.js` → `logic_virtual.js` → `logic_pipeline.js` → `logic_compare.js` → `logic_events.js` → `logic_init.js` → `logic_standalone.js` (all files share ONE inlined script: top-level names must be unique across them)
* Replaces placeholders: `__CSS_CONTENT__`, `__JS_CONTENT__`, `__TITLE__`, `__NODE_ID__`
* Zero external HTTP requests for code (JS/CSS inlined); the manifest comes from `/config_tester/manifest`
* `get_html_template(title, manifest_data, node_id, standalone=False, inline_manifest=False, inline_assets=None)` — builds the dashboard page from `template.html`. By default the page is a **lazy shell**: `fullManifest` is empty and flagged `__uscg_lazy__`, `manifest_data` is ignored, and the SPA fetches `GET /config_tester/manifest?session=<title>` itself (`inline_manifest=True` restores the old inlined data; used by tests). The JS/CSS are NOT inlined by default: the shell carries a small per-page globals script (`TARGET_NODE_ID`, `USCG_STANDALONE`, `fullManifest`) followed by `<script src="/config_tester/dashboard_bundle_js?v=<hash>">` and a `<link>` to `dashboard_bundle_css` (~70 KB shell instead of ~527 KB). `inline_assets=True` (implied by `inline_manifest=True`, or env `USCG_INLINE_DASHBOARD_ASSETS=1`) inlines the same bytes instead (self-contained page for tests / offline export)
* `get_dashboard_bundle(kind)` (`'js'`/`'css'`) returns a cached `Bundle` (text, body, precomputed gzip, content hash), rebuilt under a lock when a resource file's `(mtime_ns, size)` changes, so editing `resources/` still hot-reloads; `bundle_url(kind)`; `JS_FILES`/`CSS_FILES`/`BUNDLE_ROUTES`/`BUNDLE_CONTENT_TYPES`. `html_generator.py` must keep NO relative imports (the e2e conftest loads it by path). HTTP shaping lives in `run_api.bundle_response`
* **JS Load Order:** `logic_state.js` → `logic_utils.js` → `logic_ratings.js` → `logic_wfparams.js` → `logic_dragdrop.js` → `logic_ui.js` → `logic_upscale_modal.js` → `logic_virtual.js` → `logic_pipeline.js` → `logic_compare.js` → `logic_events.js` → `logic_init.js` → `logic_standalone.js` (all files share ONE script scope, together with the shell's globals script: top-level names must be unique across them)
* Replaces placeholders: `__CSS_TAG__`, `__JS_TAG__` (substituted LAST, so resource text is never placeholder-substituted), `__TITLE__`, `__NODE_ID__`, `__STANDALONE_FLAG__`
* Two cached same-origin requests for code (the bundle routes; immutable after the first load, so a repeat load costs only the shell's 304); the manifest comes from `/config_tester/manifest`. The `/*__JSON_START__*/ ... /*__JSON_END__*/` markers in the globals script are regex-parsed by `web/dashboard.js` and `dashboard_node.py`: keep them exactly
---
@@ -834,7 +837,7 @@ __init__.py
3. **onNodeCreated Must Be Synchronous** — In `conf-builder-main.js` line 51, `onNodeCreated` MUST remain synchronous. Async initialization runs inside a fire-and-forget `(async () => { ... })()` IIFE. Making `onNodeCreated` itself async breaks widget registration.
4. **Resources Are Inlined, Data Is Not** — `html_generator.py` reads all `/resources/` files and bakes them into one HTML string (~500 KB; the same string rides ComfyUI's `executed` websocket message and history, so it must not grow with the session). The manifest is NOT in it: the page is a lazy shell and `loadSession()` fetches `/config_tester/manifest` (compact, gzip, ETag). Changes to dashboard JS/CSS: F5 on `/config_tester/dashboard` or the node's RELOAD button rebuild the page from `resources/` on every request; a node that is already showing a page keeps the old JS until RELOAD. The Generator's `executed` message carries a shell, so `web/dashboard.js` turns it into a `reload_manifest` postMessage (conditional re-fetch) instead of parsing data out of the HTML.
4. **Resources Are Bundled, Data Is Not** — `html_generator.py` concatenates the `/resources/` JS and CSS into two cached bundles served by `/config_tester/dashboard_bundle_js` / `_css` (content-hash `?v=`, immutable caching); the page is a ~70 KB shell that references them (the shell rides ComfyUI's `executed` websocket message and history, so it must not grow with the session; `inline_assets=True` / `USCG_INLINE_DASHBOARD_ASSETS=1` inlines everything again). The manifest is NOT in it: the page is a lazy shell and `loadSession()` fetches `/config_tester/manifest` (compact, gzip, ETag). Changes to dashboard JS/CSS: F5 on `/config_tester/dashboard` or the node's RELOAD button rebuild the shell on every request and the bundle when a resource file changed (new `?v=` hash, so the browser fetches it once); a node that is already showing a page keeps the old JS until RELOAD. The Generator's `executed` message carries a shell, so `web/dashboard.js` turns it into a `reload_manifest` postMessage (conditional re-fetch) instead of parsing data out of the HTML.
5. **Manifest Merge on Save** — `manifest_utils.py` `save_manifest()` calls `merge_manifest_user_changes()` which reloads from disk before saving. This preserves user favorites/rejected/notes that changed concurrently while generation was running. Also, `flush_batch_with_vae()` (image_generation.py line 657) does its own disk sync.
+5 -2
View File
@@ -1025,7 +1025,7 @@ http://<comfyui-host>:8188/config_tester/dashboard?session=<session_name>
- It opens its own ComfyUI websocket, so new images and the ETA bar update live while a run is going. After your phone sleeps, it reloads the session when you come back, without duplicating cards. Reverse proxies that do not upgrade websockets (some Tailscale Serve / nginx setups) lose live updates; the **R** key and the resume reload still work.
- Touch screens get 44px buttons, and the lightbox gets a bottom bar (Prev / Favorite / Reject / Revise / Next). The in-node view is unchanged apart from the extra header button.
- **GENERATE NEW** in the lightbox queues through the run API below, into the session you are viewing. "Pack workflow" in export needs the ComfyUI tab and is disabled here.
- **Lazy loading:** the page itself is only the dashboard code (~500 KB, ~115 KB gzipped); the image data comes from `GET /config_tester/manifest?session=<name>` (compact JSON, gzip, `ETag`). A 1000-image session is about 160 KB over the wire instead of 3.7 MB of page plus 3.5 MB of `manifest.json`, and a phone that wakes up and finds nothing new pays one 304. Both the page and the manifest answer `If-None-Match`, so a reload with nothing changed is nearly free. `--enable-compress-response-body` is not needed.
- **Lazy loading:** the page itself is a small shell (~70 KB, ~12 KB gzipped); the dashboard code is two cached files, `/config_tester/dashboard_bundle_js` (~409 KB, ~99 KB gzipped) and `_css` (~49 KB, ~9 KB gzipped), referenced by content hash with `Cache-Control: immutable`, so the browser downloads them once and every later load (and every node iframe) pays only the shell's 304. Editing a file in `resources/` changes the hash, so a reload picks it up. The image data comes from `GET /config_tester/manifest?session=<name>` (compact JSON, gzip, `ETag`). A 1000-image session is about 160 KB over the wire instead of 3.7 MB of page plus 3.5 MB of `manifest.json`, and a phone that wakes up and finds nothing new pays one 304. The page, the bundles and the manifest all answer `If-None-Match`. `--enable-compress-response-body` is not needed. Set `USCG_INLINE_DASHBOARD_ASSETS=1` to inline everything into the page again (one self-contained document).
- Like ComfyUI itself, the page has no authentication. Only expose it on a network you trust.
---
@@ -1563,6 +1563,8 @@ Bot pattern: `POST /run`, post `dashboard_url` to the channel, poll `run_status`
|---|---|---|
| POST | `/configbuilder/preview` | Body `{state}` (the Builder state); returns `{configs_json}` from the same function the node uses (what the JSON Preview panel shows) |
| GET | `/config_tester/dashboard` | The Dashboard as a standalone page, optional `?session=` |
| GET | `/config_tester/dashboard_bundle_js` | The dashboard's JavaScript bundle (`?v=<hash>`, cached immutable, gzip) |
| GET | `/config_tester/dashboard_bundle_css` | The dashboard's stylesheet bundle (`?v=<hash>`, cached immutable, gzip) |
| GET | `/config_tester/manifest` | A session's manifest as compact JSON `?session=<name>` (gzip, ETag / 304); what the Dashboard fetches |
| POST | `/config_tester/save_comparisons` | Append / remove Pairwise Review votes `{session_name, append, remove}` |
| POST | `/config_tester/clear_comparisons` | Delete every vote of a session `{session_name}` |
@@ -1751,7 +1753,7 @@ Tests live in `tests/` and must not contain network or process-spawning code (no
2. **One config-to-JSON implementation**: `UltimateConfigBuilder.state_to_configs_json` in `config_builder_node.py` is the single source of truth. The Builder preview POSTs the state to `/configbuilder/preview`, which runs the same function; there is no JS converter any more.
3. **Dashboard runs in an iframe** — The dashboard HTML is set via `iframe.srcdoc`. All JS/CSS is inlined (no external code requests); the manifest is fetched from `/config_tester/manifest` (the page is a lazy shell with no item data). Communication between the parent ComfyUI page and the iframe uses `postMessage()`. The `dashboard.js` file bridges server events to iframe messages.
3. **Dashboard runs in an iframe** — The dashboard HTML is set via `iframe.srcdoc`. The JS/CSS come from the two cached bundle routes (same origin, absolute `/config_tester/dashboard_bundle_*` paths); the manifest is fetched from `/config_tester/manifest` (the page is a lazy shell with no item data). Communication between the parent ComfyUI page and the iframe uses `postMessage()`. The `dashboard.js` file bridges server events to iframe messages.
4. **Dashboard auto-loads on node creation** — `web/dashboard.js` calls `forceLoadSession()` with a 500ms delay when a dashboard node is created. The `get_session_html` API endpoint returns a full HTML template with empty manifest for non-existent sessions (NOT a 404).
@@ -1832,6 +1834,7 @@ After modifying backend Python files, restart ComfyUI. After modifying `web/` JS
* 📐 **Latent input size:** a connected latent's image size was always cells x 8; it is now cells x the loaded model's latent ratio (16 for Flux.2/MOSS-Image/Krea 2).
### Update 11/4/26: Standalone dashboard, pairwise review, image metrics, preview pass, workflow sweeps
* ⚡ **Dashboard loads from cached bundles:** the dashboard page shrank from ~527 KB to ~70 KB. Its JavaScript and CSS are served once as content-hashed, immutable files (`/config_tester/dashboard_bundle_js` and `_css`), so a repeat load or a second dashboard costs only a small 304, and each run's result message (which carries the page) is ~450 KB lighter. Edit-and-reload of `resources/` still works. `USCG_INLINE_DASHBOARD_ASSETS=1` restores the single-file page.
* 🧩 **Regional prompts (`COUPLE`):** `base COUPLE left | right` (or `COUPLE(rows)`, 2-4 regions, per-region `:w` weight) gives each part of the image its own prompt using native masked conditioning. Triggers, model prefix/suffix and HiRes "Append to end" text go into the base; works with HiRes, tiled HiRes and Florence2 crops. The conditioning cache on disk now keeps every entry (AND prompts included), so older cache files are re-encoded once. See [Prompt Combinators & Regional Prompts (COUPLE)](#prompt-combinators--regional-prompts-couple).
* 📱 **Standalone Dashboard + Run API:** open the dashboard as a normal web page at `/config_tester/dashboard?session=<name>` (phone, second monitor) with live updates over its own websocket. `POST /config_tester/run` and `GET /config_tester/run_status` start and track headless runs (Discord bot, curl) without any workflow JSON.
* ⚖️ **Pairwise Review:** blind A/B compare mode (`C` key) with matched, same-settings, one-axis and random pairing, Bradley-Terry ratings (Sort by Rating) and a per-setting scoreboard with 95% intervals. Votes are stored in the session manifest under `comparisons`.
+1 -1
View File
@@ -168,7 +168,7 @@ Biggest architectural leverage: eliminates the #1 developer friction point.
STARTED (11/4/26): `/config_tester/dashboard` serves the SPA as a standalone page with its own websocket.
DONE (11/4/26) Milestone 1: the page is a lazy shell (`html_generator`, `__uscg_lazy__`); the SPA fetches `GET /config_tester/manifest?session=` (compact JSON, gzip, ETag/304; the route is named `manifest`, not `get_manifest`). The node's `executed` message no longer carries the manifest (`web/dashboard.js` sends `reload_manifest`). Milestone 3 comes free for F5 / RELOAD (the page is rebuilt from `resources/` on every request); a node already showing a page keeps its old JS until RELOAD.
Milestone 2 is blocked as written: ComfyUI's frontend imports every `web/**/*.js` as an extension module, so the `logic_*.js` files in `WEB_DIRECTORY` would run inside the ComfyUI page. Sketch of the cheap alternative: fixed path-less routes `GET /config_tester/dashboard_bundle_js?v=<hash>` / `..._css?v=<hash>` serving the concatenated files with ETag, `Cache-Control: public, max-age=31536000, immutable` and gzip, referenced from the shell with `<script src>` / `<link>` (the shell drops to ~70 KB and repeat loads to ~0 KB). Needs: rework `test_concatenated_script_parses_as_one_script`, the e2e fake origin serving the bundle, a script-order check.
DONE (11/4/26) Milestone 2 via the cheap alternative (static `web/` serving is blocked: ComfyUI's frontend imports every `web/**/*.js` as an extension module): fixed path-less routes `GET /config_tester/dashboard_bundle_js?v=<hash>` / `..._css?v=<hash>` serve the concatenated `resources/` files (`html_generator.get_dashboard_bundle`, rebuilt on `(mtime, size)` change so Milestone 3 hot reload still works) with a weak ETag, `Cache-Control: public, max-age=31536000, immutable`, precomputed gzip; the shell references them with `<script src>` / `<link>` and shrank from ~527 KB to ~70 KB, repeat loads cost only the shell's 304. The node iframe uses them too (every run's `executed` message and history entry is ~450 KB lighter). `inline_assets=True` / env `USCG_INLINE_DASHBOARD_ASSETS=1` keeps a self-contained page (`inline_manifest=True` implies it). Tests: `tests/test_dashboard_bundle.py`, `tests/e2e/test_dashboard_bundle.py`.
#### **28. Onboarding Wizard & Tutorial**
* **Milestone 1:** First-run wizard in Builder UI — walks through: create session → pick model → add LoRA → run → view dashboard (5 interactive steps)
+29 -2
View File
@@ -13,14 +13,14 @@ import time
import inspect
from .sampler_node import SamplerGridTester
from .dashboard_node import SamplerConfigDashboardViewer
from .html_generator import get_html_template
from .html_generator import get_html_template, get_dashboard_bundle, BUNDLE_CONTENT_TYPES
from .config_builder_node import UltimateConfigBuilder
from .json_text_node import SmartJSONTextNode
from .metadata_packer import pack_metadata_into_image
from .directory_scanner import scan_directory_for_images
from .run_api import (
RunTracker, sanitize_session_name_strict, load_manifest_for_fetch, normalize_configs_payload, apply_run_overrides,
build_run_prompt, resolve_saved_config, saved_config_to_configs_obj, summarize_history, find_in_queue,
build_run_prompt, resolve_saved_config, saved_config_to_configs_obj, summarize_history, find_in_queue, bundle_response,
)
from .workflow_sweep_collect import USCGSweepCollect, USCGSweepCollectVideo
import sys
@@ -649,6 +649,33 @@ def _dashboard_response(request, html):
return resp
def _bundle_http(request, kind):
"""Serve the cached dashboard JS/CSS bundle. `kind` is a literal per route; the request only
contributes ?v= (compared, never looked up) and the caching/encoding headers."""
try:
bundle = get_dashboard_bundle(kind)
status, body, headers = bundle_response(
bundle, request.query.get("v", ""), request.headers.get("If-None-Match"),
request.headers.get("Accept-Encoding", ""))
if body is None:
return web.Response(status=status, headers=headers)
return web.Response(status=status, body=body, content_type=BUNDLE_CONTENT_TYPES[kind],
charset="utf-8", headers=headers)
except Exception as e:
print(f"[GridTester] /dashboard_bundle_{kind} failed: {e}")
return web.Response(status=500, text="bundle unavailable")
@server.PromptServer.instance.routes.get("/config_tester/dashboard_bundle_js")
async def dashboard_bundle_js(request):
return _bundle_http(request, "js")
@server.PromptServer.instance.routes.get("/config_tester/dashboard_bundle_css")
async def dashboard_bundle_css(request):
return _bundle_http(request, "css")
@server.PromptServer.instance.routes.get("/config_tester/dashboard")
async def standalone_dashboard(request):
"""
+3 -1
View File
@@ -86,7 +86,9 @@ def _install_stubs():
_subs = {
"sampler_node": {"SamplerGridTester": type("SamplerGridTester", (), {})},
"dashboard_node": {"SamplerConfigDashboardViewer": type("SamplerConfigDashboardViewer", (), {})},
"html_generator": {"get_html_template": lambda *a, **kw: ""},
"html_generator": {"get_html_template": lambda *a, **kw: "",
"get_dashboard_bundle": lambda *a, **kw: None,
"BUNDLE_CONTENT_TYPES": {}},
"json_text_node": {"SmartJSONTextNode": type("SmartJSONTextNode", (), {})},
"metadata_packer": {"pack_metadata_into_image": lambda *a, **kw: None},
"directory_scanner": {
+120 -46
View File
@@ -1,7 +1,103 @@
import os
import json
import gzip
import hashlib
import threading
from typing import NamedTuple
def get_html_template(title, manifest_data, node_id, standalone=False, inline_manifest=False):
RESOURCE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources")
# JS load order (dependencies first!). logic_standalone.js must stay LAST.
JS_FILES = (
"logic_state.js", # Global variables
"logic_utils.js", # API & Helper functions
"logic_ratings.js", # Pairwise review: pure logic (normalisers, Bradley-Terry, pairing, scoreboard)
"logic_wfparams.js", # Workflow Sweep: wf_params helpers (pure; must load before logic_ui/logic_pipeline)
"logic_dragdrop.js", # Revise-modal image drag/send helpers (issue #22; pure, node-testable)
"logic_ui.js", # DOM Creation, Modals, JSON Bars
"logic_upscale_modal.js", # Upscale Modal (preset management, async upscale)
"logic_virtual.js", # The Virtual Scroll Engine & Viewport
"logic_pipeline.js", # Sorting & Filtering Logic
"logic_compare.js", # Pairwise review: compare overlay, votes, scoreboard modal
"logic_events.js", # Event Listeners (Message, Keyboard)
"logic_init.js", # Initialization
"logic_standalone.js" # Standalone-page mode (own websocket, run API); must stay LAST
)
CSS_FILES = ("report.css",)
# Fixed bundle routes (no path parameters; the ?v= query is only compared, never used for lookup).
BUNDLE_ROUTES = {"js": "/config_tester/dashboard_bundle_js", "css": "/config_tester/dashboard_bundle_css"}
BUNDLE_CONTENT_TYPES = {"js": "text/javascript", "css": "text/css"}
class Bundle(NamedTuple):
kind: str
sig: tuple
text: str
body: bytes
gzip: bytes
hash: str
_bundle_cache = {}
_bundle_lock = threading.Lock()
def _files_for(kind):
if kind == "js":
return JS_FILES
if kind == "css":
return CSS_FILES
raise KeyError(kind)
def _signature(names):
"""(name, mtime_ns, size) per resource file: cheap change detection for hot reload."""
sig = [("dir", RESOURCE_DIR, 0)]
for n in names:
st = os.stat(os.path.join(RESOURCE_DIR, n))
sig.append((n, st.st_mtime_ns, st.st_size))
return tuple(sig)
def _build_text(kind):
names = _files_for(kind)
if kind == "css":
with open(os.path.join(RESOURCE_DIR, names[0]), "r", encoding="utf-8") as f:
return f.read()
text = ""
for n in names:
with open(os.path.join(RESOURCE_DIR, n), "r", encoding="utf-8") as f:
text += f"\n/* --- {n} --- */\n"
text += f.read() + "\n"
return text
def get_dashboard_bundle(kind):
"""The page-independent JS or CSS bundle, rebuilt only when a resource file changed.
Thread-safe (called from the aiohttp loop and from the execution thread). Raises KeyError for
an unknown kind and OSError when a resource file is missing.
"""
names = _files_for(kind)
with _bundle_lock:
sig = _signature(names)
cached = _bundle_cache.get(kind)
if cached is not None and cached.sig == sig:
return cached
text = _build_text(kind)
body = text.encode("utf-8")
bundle = Bundle(kind, sig, text, body, gzip.compress(body, 9, mtime=0),
hashlib.sha1(body).hexdigest()[:20])
_bundle_cache[kind] = bundle
return bundle
def bundle_url(kind):
return f"{BUNDLE_ROUTES[kind]}?v={get_dashboard_bundle(kind).hash}"
def get_html_template(title, manifest_data, node_id, standalone=False, inline_manifest=False, inline_assets=None):
"""Build the dashboard page.
By default the page is a SHELL: no item data is inlined (an empty `fullManifest` flagged
@@ -9,6 +105,10 @@ def get_html_template(title, manifest_data, node_id, standalone=False, inline_ma
keeps the HTML (and ComfyUI's `executed` websocket message / history entry, which carries the
whole string) ~500 KB instead of growing with the session. `manifest_data` is only used when
`inline_manifest=True` (old behaviour; offline export / tests).
The JS/CSS are NOT inlined by default: the page references two cached bundle routes
(`bundle_url`), which cuts the shell to ~70 KB. `inline_assets=True` (implied by
`inline_manifest=True`, or env USCG_INLINE_DASHBOARD_ASSETS=1) inlines the same bytes instead.
"""
# 1. Normalize Data
if not inline_manifest:
@@ -24,57 +124,31 @@ def get_html_template(title, manifest_data, node_id, standalone=False, inline_ma
# JSON.parse (web/dashboard.js's incremental refresh) still reads the original text.
json_str = json_str.replace("</", "<\\/")
# 2. Resolve File Paths
current_dir = os.path.dirname(__file__)
resource_dir = os.path.join(current_dir, "resources")
# Define JS Load Order (Dependencies first!)
js_files = [
"logic_state.js", # Global variables
"logic_utils.js", # API & Helper functions
"logic_ratings.js", # Pairwise review: pure logic (normalisers, Bradley-Terry, pairing, scoreboard)
"logic_wfparams.js", # Workflow Sweep: wf_params helpers (pure; must load before logic_ui/logic_pipeline)
"logic_dragdrop.js", # Revise-modal image drag/send helpers (issue #22; pure, node-testable)
"logic_ui.js", # DOM Creation, Modals, JSON Bars
"logic_upscale_modal.js", # Upscale Modal (preset management, async upscale)
"logic_virtual.js", # The Virtual Scroll Engine & Viewport
"logic_pipeline.js", # Sorting & Filtering Logic
"logic_compare.js", # Pairwise review: compare overlay, votes, scoreboard modal
"logic_events.js", # Event Listeners (Message, Keyboard)
"logic_init.js", # Initialization
"logic_standalone.js" # Standalone-page mode (own websocket, run API); must stay LAST
]
# 2. Load template + assets
if inline_assets is None:
inline_assets = bool(inline_manifest)
if os.environ.get("USCG_INLINE_DASHBOARD_ASSETS") == "1":
inline_assets = True
try:
# Load HTML
with open(os.path.join(resource_dir, "template.html"), "r", encoding="utf-8") as f:
with open(os.path.join(RESOURCE_DIR, "template.html"), "r", encoding="utf-8") as f:
html_template = f.read()
# Load CSS
with open(os.path.join(resource_dir, "report.css"), "r", encoding="utf-8") as f:
css_content = f.read()
# Load and Concatenate JS
js_content = ""
for js_file in js_files:
file_path = os.path.join(resource_dir, js_file)
with open(file_path, "r", encoding="utf-8") as f:
js_content += f"\n/* --- {js_file} --- */\n"
js_content += f.read() + "\n"
except FileNotFoundError as e:
js_bundle = get_dashboard_bundle("js")
css_bundle = get_dashboard_bundle("css")
except (FileNotFoundError, OSError) as e:
return f"<h1>Error: Resource files not found.</h1><p>{e}</p>"
if inline_assets:
css_tag = f"<style>\n/* INJECTED CSS */\n{css_bundle.text}\n</style>"
js_tag = f"<script>\n/* INJECTED LOGIC */\n{js_bundle.text}\n</script>"
else:
css_tag = f'<link rel="stylesheet" href="{bundle_url("css")}">'
js_tag = f'<script src="{bundle_url("js")}"></script>'
# 3. Inject Data
wrapped_json = f"/*__JSON_START__*/\nlet fullManifest = {json_str};\n/*__JSON_END__*/"
# 4. Final Replacements
final_html = html_template \
.replace("__TITLE__", str(title)) \
.replace("__NODE_ID__", str(node_id)) \
.replace("__STANDALONE_FLAG__", "true" if standalone else "false") \
.replace("let fullManifest = __JSON_DATA__;", wrapped_json) \
.replace("__CSS_CONTENT__", css_content) \
.replace("__JS_CONTENT__", js_content)
# 4. Final Replacements (asset tags LAST so resource text is never placeholder-substituted)
final_html = html_template .replace("__TITLE__", str(title)) .replace("__NODE_ID__", str(node_id)) .replace("__STANDALONE_FLAG__", "true" if standalone else "false") .replace("let fullManifest = __JSON_DATA__;", wrapped_json) .replace("__CSS_TAG__", css_tag) .replace("__JS_TAG__", js_tag)
return final_html
return final_html
+3 -9
View File
@@ -4,10 +4,7 @@
<head>
<title>USCG - __TITLE__</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style>
/* INJECTED CSS */
__CSS_CONTENT__
</style>
__CSS_TAG__
</head>
<body>
@@ -930,16 +927,13 @@
</div>
<script>
// INJECTED DATA
// INJECTED DATA (per page)
const TARGET_NODE_ID = "__NODE_ID__";
// true when served by /config_tester/dashboard, or whenever this page is not inside a frame
const USCG_STANDALONE = __STANDALONE_FLAG__ || (window.parent === window);
let fullManifest = __JSON_DATA__;
// INJECTED LOGIC
__JS_CONTENT__
</script>
__JS_TAG__
<style>
/* Default behavior: The button is visible */
.gh-hide-on-mobile {
+21
View File
@@ -33,6 +33,27 @@ def _etag_matches(if_none_match, etag):
return False
def bundle_response(bundle, v, if_none_match, accept_encoding):
"""HTTP shaping for the dashboard JS/CSS bundle routes: (status, body_or_None, headers).
`v` (the ?v= query) is only COMPARED with the bundle hash, never used to look anything up: a
stale or bogus value still gets the current bundle, just with a no-cache policy.
"""
etag = 'W/"%s"' % bundle.hash
headers = {
"ETag": etag,
"Vary": "Accept-Encoding",
"X-Content-Type-Options": "nosniff",
"Cache-Control": "public, max-age=31536000, immutable" if v == bundle.hash else "no-cache",
}
if _etag_matches(if_none_match, etag):
return 304, None, headers
if "gzip" in str(accept_encoding or "").lower():
headers["Content-Encoding"] = "gzip"
return 200, bundle.gzip, headers
return 200, bundle.body, headers
def load_manifest_for_fetch(benchmarks_base, raw_session, if_none_match=None):
"""Backend of GET /config_tester/manifest?session=<name> (the dashboard SPA's data fetch).
+2
View File
@@ -141,6 +141,8 @@ _GEN_ORCH_DEPS = {
},
"html_generator": {
"get_html_template": lambda *a, **kw: "",
"get_dashboard_bundle": lambda *a, **kw: None,
"BUNDLE_CONTENT_TYPES": {},
},
"conditioning_cache": {
"ConditioningCache": type("ConditioningCache", (), {}),
+44 -7
View File
@@ -17,17 +17,38 @@ SESSION = "smoke"
N_ITEMS = 24
def load_real_html_generator():
spec = importlib.util.spec_from_file_location(
"uscg_real_html_generator", os.path.join(_NODE_ROOT, "html_generator.py"))
_REAL_HG = None
_REAL_RUN_API = None
def _load_by_path(name, filename):
spec = importlib.util.spec_from_file_location(name, os.path.join(_NODE_ROOT, filename))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def build_dashboard_page(manifest, node_id="7", title=SESSION):
"""The exact HTML string the Dashboard node shows in its iframe srcdoc (a lazy shell: no items inlined)."""
return load_real_html_generator().get_html_template(title, manifest, node_id)
def load_real_html_generator():
"""Cached: the module keeps the bundle cache, so every page and every bundle response share it."""
global _REAL_HG
if _REAL_HG is None:
_REAL_HG = _load_by_path("uscg_real_html_generator", "html_generator.py")
return _REAL_HG
def load_real_run_api():
global _REAL_RUN_API
if _REAL_RUN_API is None:
_REAL_RUN_API = _load_by_path("uscg_real_run_api", "run_api.py")
return _REAL_RUN_API
def build_dashboard_page(manifest, node_id="7", title=SESSION, **kw):
"""The exact HTML string the Dashboard node shows in its iframe srcdoc (a lazy shell: no items inlined).
Bundled by default (the JS/CSS come from the fake origin's bundle routes); pass inline_assets=True
for the self-contained page."""
return load_real_html_generator().get_html_template(title, manifest, node_id, **kw)
def make_item(i, model, sampler, scheduler, cfg, steps, lora):
@@ -110,6 +131,8 @@ class Dashboard:
self.known_sessions = None # None: every session name exists (like the old /view); else a set
self.hold_manifest = False # True: park manifest requests until release_manifest()
self.held = []
self.bundle_requests = [] # (kind, v) per GET /config_tester/dashboard_bundle_{js,css}
self.bundle_status = None # e.g. 500: make the bundle routes fail
def release_manifest(self):
self.hold_manifest = False
@@ -155,6 +178,18 @@ class Dashboard:
if path == "/":
return route.fulfill(status=200, content_type="text/html; charset=utf-8",
body=build_dashboard_page(self.manifest, title=self.title))
if path in ("/config_tester/dashboard_bundle_js", "/config_tester/dashboard_bundle_css"):
kind = "js" if path.endswith("_js") else "css"
from urllib.parse import parse_qs
v = (parse_qs(query).get("v") or [""])[0]
self.bundle_requests.append((kind, v))
if self.bundle_status:
return route.fulfill(status=self.bundle_status, content_type="text/plain", body="bundle unavailable")
hg = load_real_html_generator()
status, body, headers = load_real_run_api().bundle_response(
hg.get_dashboard_bundle(kind), v, None, "")
return route.fulfill(status=status, content_type=hg.BUNDLE_CONTENT_TYPES[kind] + "; charset=utf-8",
body=body, headers=headers)
if path == "/config_tester/manifest":
if self.hold_manifest:
self.held.append(route)
@@ -209,7 +244,9 @@ def dashboard(page, manifest_meta):
@pytest.fixture
def dashboard_kit():
"""Raw pieces for tests that need a custom page around the dashboard (e.g. an iframe wrapper)."""
return {"ORIGIN": ORIGIN, "Dashboard": Dashboard, "make_manifest": make_manifest}
return {"ORIGIN": ORIGIN, "Dashboard": Dashboard, "make_manifest": make_manifest,
"build_dashboard_page": build_dashboard_page, "load_real_html_generator": load_real_html_generator,
"N_ITEMS": N_ITEMS}
@pytest.fixture
+87
View File
@@ -0,0 +1,87 @@
"""Dashboard JS/CSS served as bundle routes (Roadmap 27, milestone 2).
The fake origin (conftest.py `Dashboard.handle`) serves the REAL bundles through `run_api.bundle_response`.
Repeat-load caching is not testable here (page.route disables the HTTP cache): the live recipe covers it.
"""
import os
import pytest
if os.environ.get("USCG_REQUIRE_E2E") == "1":
import playwright.sync_api # noqa: F401
else:
pytest.importorskip("playwright.sync_api")
from playwright.sync_api import expect # noqa: E402
pytestmark = pytest.mark.e2e
ORIGIN = "http://uscg.test"
N_ITEMS = 24
def _setup(page, d):
page.on("pageerror", lambda e: d.pageerrors.append(str(e)))
page.route("**/*", d.handle)
page.set_viewport_size({"width": 1600, "height": 1200})
def test_page_loads_one_js_and_one_css_bundle(page, dashboard_kit):
Dashboard, make_manifest = dashboard_kit["Dashboard"], dashboard_kit["make_manifest"]
d = Dashboard(page, make_manifest())
_setup(page, d)
page.goto(ORIGIN + "/")
page.wait_for_function(f"typeof processedData!=='undefined' && processedData.length==={N_ITEMS}")
expect(page.locator("#grid .card:visible")).to_have_count(N_ITEMS)
hg = dashboard_kit["load_real_html_generator"]()
assert sorted(d.bundle_requests) == sorted([("js", hg.get_dashboard_bundle("js").hash),
("css", hg.get_dashboard_bundle("css").hash)])
assert page.evaluate("typeof uscgBootLoad") == "function"
# a value that only report.css defines (:root --accent) is applied
assert page.evaluate("getComputedStyle(document.documentElement).getPropertyValue('--accent').trim()") == "#00d1b2"
assert page.evaluate("getComputedStyle(document.body).overflow") == "hidden"
assert d.pageerrors == []
def test_srcdoc_iframe_loads_the_bundle_from_the_parent_origin(page, dashboard_kit):
Dashboard, make_manifest = dashboard_kit["Dashboard"], dashboard_kit["make_manifest"]
d = Dashboard(page, make_manifest())
_setup(page, d)
shell = dashboard_kit["build_dashboard_page"](d.manifest)
wrap = '<!doctype html><body><iframe id="f" style="width:1400px;height:900px"></iframe></body>'
page.route(ORIGIN + "/wrap", lambda route: route.fulfill(status=200, content_type="text/html", body=wrap))
page.goto(ORIGIN + "/wrap")
page.evaluate("(h) => { document.getElementById('f').srcdoc = h; }", shell)
page.locator("#f").wait_for()
page.wait_for_function("document.getElementById('f').contentWindow.processedData && "
f"document.getElementById('f').contentWindow.processedData.length==={N_ITEMS}")
inner = page.frames[1]
assert inner.evaluate("USCG_STANDALONE") is False
assert inner.evaluate("processedData.length") == N_ITEMS
assert {k for k, _ in d.bundle_requests} == {"js", "css"}
assert d.pageerrors == []
def test_inline_assets_page_makes_no_bundle_requests(page, dashboard_kit):
Dashboard, make_manifest = dashboard_kit["Dashboard"], dashboard_kit["make_manifest"]
d = Dashboard(page, make_manifest())
_setup(page, d)
page.route(ORIGIN + "/inline", lambda route: route.fulfill(
status=200, content_type="text/html; charset=utf-8",
body=dashboard_kit["build_dashboard_page"](d.manifest, inline_assets=True)))
page.goto(ORIGIN + "/inline")
page.wait_for_function(f"typeof processedData!=='undefined' && processedData.length==={N_ITEMS}")
assert d.bundle_requests == []
assert d.pageerrors == []
def test_failing_bundle_route_does_not_hang_the_page(page, dashboard_kit):
Dashboard, make_manifest = dashboard_kit["Dashboard"], dashboard_kit["make_manifest"]
d = Dashboard(page, make_manifest())
d.bundle_status = 500
_setup(page, d)
page.goto(ORIGIN + "/")
page.wait_for_timeout(500)
assert page.evaluate("typeof uscgBootLoad") == "undefined"
assert page.evaluate("document.readyState") in ("interactive", "complete")
assert sorted(k for k, _ in d.bundle_requests) == ["css", "js"]
+212
View File
@@ -0,0 +1,212 @@
"""Dashboard JS/CSS bundle routes (Roadmap 27, milestone 2): html_generator bundles + run_api.bundle_response."""
import gzip
import importlib.util
import os
import re
import shutil
import subprocess
import tempfile
import pytest
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_PKG = os.path.basename(_ROOT)
NODE = shutil.which("node")
def _load(name, filename):
spec = importlib.util.spec_from_file_location(f"{_PKG}.{name}_bundle_real", os.path.join(_ROOT, filename))
mod = importlib.util.module_from_spec(spec)
mod.__package__ = _PKG
spec.loader.exec_module(mod)
return mod
hg = _load("html_generator", "html_generator.py")
run_api = _load("run_api", "run_api.py")
EXPECTED_JS = (
"logic_state.js", "logic_utils.js", "logic_ratings.js", "logic_wfparams.js", "logic_dragdrop.js",
"logic_ui.js", "logic_upscale_modal.js", "logic_virtual.js", "logic_pipeline.js", "logic_compare.js",
"logic_events.js", "logic_init.js", "logic_standalone.js",
)
TOKENS = ("__CSS_TAG__", "__JS_TAG__", "__CSS_CONTENT__", "__JS_CONTENT__", "__TITLE__", "__NODE_ID__",
"__STANDALONE_FLAG__")
def _shell(title="t", node_id="1", **kw):
return hg.get_html_template(title, {"items": []}, node_id, **kw)
def _read(name):
with open(os.path.join(hg.RESOURCE_DIR, name), "r", encoding="utf-8") as f:
return f.read()
@pytest.fixture(autouse=True)
def _no_env(monkeypatch):
monkeypatch.delenv("USCG_INLINE_DASHBOARD_ASSETS", raising=False)
def test_js_files_exact_order():
assert hg.JS_FILES == EXPECTED_JS
assert hg.JS_FILES[-1] == "logic_standalone.js"
assert hg.CSS_FILES == ("report.css",)
def test_markers_and_each_file_once_in_order():
text = hg.get_dashboard_bundle("js").text
last = -1
for f in EXPECTED_JS:
marker = f"/* --- {f} --- */"
assert text.count(marker) == 1
pos = text.index(marker)
assert pos > last
last = pos
assert text.count(_read(f)) == 1
assert hg.get_dashboard_bundle("css").text == _read("report.css")
def test_bundle_is_page_independent():
b1 = hg.get_dashboard_bundle("js")
_shell("a", "9", standalone=True)
_shell("b", "x", inline_manifest=True)
assert hg.get_dashboard_bundle("js") is b1
for kind in ("js", "css"):
for tok in TOKENS:
assert tok not in hg.get_dashboard_bundle(kind).text
def test_inline_and_bundled_modes_carry_the_same_code():
page = _shell(inline_assets=True)
js = hg.get_dashboard_bundle("js").text
css = hg.get_dashboard_bundle("css").text
assert "<script>\n/* INJECTED LOGIC */\n" + js + "\n</script>" in page
assert "<style>\n/* INJECTED CSS */\n" + css + "\n</style>" in page
def test_shell_references_bundles_with_hash():
page = _shell(standalone=True)
jh = hg.get_dashboard_bundle("js").hash
ch = hg.get_dashboard_bundle("css").hash
tag = f'<script src="/config_tester/dashboard_bundle_js?v={jh}"></script>'
assert page.count(tag) == 1
assert f'<link rel="stylesheet" href="/config_tester/dashboard_bundle_css?v={ch}">' in page
assert "/* --- logic_" not in page
size = len(page.encode("utf-8"))
print(f"measured shell size: {size} bytes")
assert size < 100 * 1024
assert page.index("const TARGET_NODE_ID") < page.index(tag) < page.index('id="upscale-modal-overlay"')
for tok in TOKENS:
assert tok not in page
def test_inline_manifest_implies_inline_assets_and_overrides():
m = {"items": []}
implied = hg.get_html_template("t", m, "1", inline_manifest=True)
assert "/* INJECTED LOGIC */" in implied and "dashboard_bundle_js" not in implied
assert "/* INJECTED LOGIC */" in hg.get_html_template("t", m, "1", inline_assets=True)
off = hg.get_html_template("t", m, "1", inline_manifest=True, inline_assets=False)
assert "/* INJECTED LOGIC */" not in off and "dashboard_bundle_js" in off
def test_env_forces_inline(monkeypatch):
monkeypatch.setenv("USCG_INLINE_DASHBOARD_ASSETS", "1")
page = _shell()
assert "/* INJECTED LOGIC */" in page and "dashboard_bundle_js" not in page
def test_hash_follows_resource_changes(tmp_path, monkeypatch):
dst = tmp_path / "resources"
shutil.copytree(hg.RESOURCE_DIR, dst)
monkeypatch.setattr(hg, "RESOURCE_DIR", str(dst))
hg._bundle_cache.clear()
try:
b1 = hg.get_dashboard_bundle("js")
assert hg.get_dashboard_bundle("js") is b1 # unchanged signature: cached object
url1 = hg.bundle_url("js")
init = dst / "logic_init.js"
st = init.stat()
os.utime(init, ns=(st.st_atime_ns, st.st_mtime_ns + 5_000_000_000)) # mtime only
b2 = hg.get_dashboard_bundle("js")
assert b2 is not b1 and b2.hash == b1.hash
with open(init, "ab") as f:
f.write(b"\n// hot reload edit\n")
b3 = hg.get_dashboard_bundle("js")
assert b3.hash != b1.hash
assert hg.bundle_url("js") != url1 and b3.hash in hg.bundle_url("js")
finally:
hg._bundle_cache.clear()
def test_gzip_round_trip_and_deterministic():
for kind in ("js", "css"):
b = hg.get_dashboard_bundle(kind)
assert gzip.decompress(b.gzip) == b.body
assert b.gzip == gzip.compress(b.body, 9, mtime=0)
assert b.body == b.text.encode("utf-8")
assert len(b.gzip) < len(b.body)
def test_unknown_kind_raises():
with pytest.raises(KeyError):
hg.get_dashboard_bundle("../x")
def test_bundle_response_headers():
b = hg.get_dashboard_bundle("js")
st, body, h = run_api.bundle_response(b, b.hash, None, "gzip, br")
assert st == 200 and body == b.gzip
assert h["Content-Encoding"] == "gzip"
assert h["Cache-Control"] == "public, max-age=31536000, immutable"
assert h["ETag"] == f'W/"{b.hash}"'
assert h["Vary"] == "Accept-Encoding" and h["X-Content-Type-Options"] == "nosniff"
st, body, h = run_api.bundle_response(b, b.hash, None, "")
assert st == 200 and body == b.body and "Content-Encoding" not in h
assert h["Vary"] == "Accept-Encoding" and h["X-Content-Type-Options"] == "nosniff"
for v in ("bogus", "", None):
_, _, h = run_api.bundle_response(b, v, None, "gzip")
assert h["Cache-Control"] == "no-cache"
for inm in (f'W/"{b.hash}"', f'"{b.hash}"', f'"zzz", W/"{b.hash}"'):
st, body, h = run_api.bundle_response(b, b.hash, inm, "gzip")
assert st == 304 and body is None and h["ETag"] == f'W/"{b.hash}"'
st, _, _ = run_api.bundle_response(b, b.hash, 'W/"other"', "gzip")
assert st == 200
@pytest.mark.parametrize("v", ["../../etc/passwd", "logic_ui.js", "..\\..\\windows\\win.ini"])
def test_v_is_never_a_path(v):
b = hg.get_dashboard_bundle("js")
st, body, h = run_api.bundle_response(b, v, None, "")
assert st == 200 and body == b.body and h["Cache-Control"] == "no-cache"
def test_routes_are_fixed_paths():
with open(os.path.join(_ROOT, "__init__.py"), encoding="utf-8") as f:
src = f.read()
assert '"/config_tester/dashboard_bundle_js"' in src
assert '"/config_tester/dashboard_bundle_css"' in src
for path in re.findall(r'routes\.(?:get|post|put|delete)\(\s*"([^"]+)"', src):
assert "{" not in path, path
m = re.search(r"def _bundle_http\(.*?(?=\n@server)", src, re.S)
assert m
handlers = [m.group(0)]
for fn in ("dashboard_bundle_js", "dashboard_bundle_css"):
handlers.append(re.search(r"async def %s\(.*?(?=\n\n\n|\Z)" % fn, src, re.S).group(0))
for body in handlers:
for banned in ("match_info", "open(", "FileResponse", "enable_compression"):
assert banned not in body, banned
@pytest.mark.skipif(NODE is None, reason="node not installed")
def test_bundle_parses_and_composes_with_globals():
page = _shell(standalone=True)
globals_js = re.search(r"<script>\s*// INJECTED DATA \(per page\)([\s\S]*?)</script>", page).group(1)
js = hg.get_dashboard_bundle("js").text
with tempfile.TemporaryDirectory() as d:
for name, code in (("bundle.js", js), ("all.js", globals_js + "\n" + js)):
path = os.path.join(d, name)
with open(path, "w", encoding="utf-8") as f:
f.write(code)
out = subprocess.run([NODE, "--check", path], capture_output=True, timeout=60)
assert out.returncode == 0, out.stderr.decode()
+1 -1
View File
@@ -165,7 +165,7 @@ def test_events_documented():
def test_js_files_order():
src = _read("html_generator.py")
m = re.search(r"js_files\s*=\s*\[(.*?)\n\s*\]", src, re.S)
m = re.search(r"(?:JS_FILES|js_files)\s*=\s*[\[(](.*?)\n\s*[\])]", src, re.S)
assert m
files = re.findall(r'^\s*"([\w\-]+\.js)"', m.group(1), re.M)
assert files and files[-1] == "logic_standalone.js"
+20 -16
View File
@@ -75,9 +75,24 @@ def test_escaped_manifest_still_parses_with_dashboard_js_extraction():
@pytest.mark.parametrize("standalone", [True, False])
def test_concatenated_script_parses_as_one_script(standalone):
page = _page(standalone=standalone)
scripts = re.findall(r"<script>([\s\S]*?)</script>", page)
scripts = re.findall(r"<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)</script>", page)
assert scripts, "no inline script found"
js = max(scripts, key=len)
js = "\n;\n".join(scripts)
with tempfile.TemporaryDirectory() as d:
path = os.path.join(d, "page.js")
with open(path, "w", encoding="utf-8") as f:
f.write(js)
out = subprocess.run([NODE, "--check", path], capture_output=True, timeout=60)
assert out.returncode == 0, out.stderr.decode()
@pytest.mark.skipif(NODE is None, reason="node not installed")
def test_shell_script_parses_as_one_script():
"""The shell's globals script + the bundle route's JS share one global lexical scope."""
page = _shell(standalone=True)
scripts = re.findall(r"<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)</script>", page)
assert scripts
js = "\n;\n".join(scripts) + "\n;\n" + html_generator.get_dashboard_bundle("js").text
with tempfile.TemporaryDirectory() as d:
path = os.path.join(d, "page.js")
with open(path, "w", encoding="utf-8") as f:
@@ -106,7 +121,7 @@ def test_shell_inlines_no_item_data():
def test_shell_size_does_not_grow_with_the_session():
big = [{"id": i, "note": "x" * 2500, "positive": "p" * 300} for i in range(1000)]
assert len(_shell(big).encode("utf-8")) < 560 * 1024
assert len(_shell(big).encode("utf-8")) < 100 * 1024
assert len(_shell(big)) == len(_shell([]))
@@ -115,7 +130,8 @@ def test_shell_placeholders_all_replaced():
assert 'const TARGET_NODE_ID = "42";' in page
assert "const USCG_STANDALONE = true" in page
assert 'id="session-input" class="session-name-input" type="text" value="my-sess"' in page
for token in ("__TITLE__", "__NODE_ID__", "__STANDALONE_FLAG__", "__CSS_CONTENT__", "__JS_CONTENT__"):
for token in ("__TITLE__", "__NODE_ID__", "__STANDALONE_FLAG__", "__CSS_CONTENT__", "__JS_CONTENT__",
"__CSS_TAG__", "__JS_TAG__"):
assert token not in page
@@ -133,18 +149,6 @@ def test_shell_parses_with_dashboard_js_extraction_as_lazy():
assert out.stdout.decode() == '[true,0,"abc"]'
@pytest.mark.skipif(NODE is None, reason="node not installed")
def test_shell_script_parses_as_one_script():
scripts = re.findall(r"<script>([\s\S]*?)</script>", _shell(standalone=True))
js = max(scripts, key=len)
with tempfile.TemporaryDirectory() as d:
path = os.path.join(d, "page.js")
with open(path, "w", encoding="utf-8") as f:
f.write(js)
out = subprocess.run([NODE, "--check", path], capture_output=True, timeout=60)
assert out.returncode == 0, out.stderr.decode()
def test_inline_manifest_flag_keeps_the_old_behaviour():
page = _page()
assert "__uscg_lazy__" not in page.split("/*__JSON_START__*/")[1].split("/*__JSON_END__*/")[0]