diff --git a/crates/isokit-cli/src/serve.rs b/crates/isokit-cli/src/serve.rs index a69496a..408daf1 100644 --- a/crates/isokit-cli/src/serve.rs +++ b/crates/isokit-cli/src/serve.rs @@ -13,7 +13,7 @@ use tiny_http::{Header, Method, Request, Response, Server}; const VIEW_PAGE: &str = r#" isokit preview - +
+ @@ -51,6 +52,7 @@ document.documentElement.setAttribute("data-theme",t);})(); + diff --git a/studio/studio.css b/studio/studio.css index 4de5eae..fa8c93d 100644 --- a/studio/studio.css +++ b/studio/studio.css @@ -52,6 +52,8 @@ input[type=checkbox] { accent-color: var(--accent); } .st-hint { position: absolute; top: 52px; left: 50%; transform: translateX(-50%); background: var(--panel); border: 1px solid var(--accent); border-radius: 16px; padding: 4px 14px; z-index: 5; pointer-events: none; } .st-status { display: flex; align-items: center; gap: 14px; padding: 4px 10px; background: var(--panel); border-top: 1px solid var(--stroke); color: var(--mute); font-variant-numeric: tabular-nums; flex: 0 0 auto; } .st-status select { padding: 1px 4px; } +.st-ver { color: var(--mute); font-size: 11px; text-decoration: none; white-space: nowrap; } +.st-ver:hover { color: var(--fg); text-decoration: underline; } .st-link { background: none; border: 0; color: var(--danger); padding: 0; text-decoration: underline; } .st-right h3 { margin: 0; font-size: 15px; display: flex; align-items: center; gap: 6px; } diff --git a/studio/studio.js b/studio/studio.js index 3b8d895..a246075 100644 --- a/studio/studio.js +++ b/studio/studio.js @@ -356,12 +356,28 @@ if (B && B.preview) $("preview").addEventListener("click", function (e) { e.preventDefault(); B.preview(); }); if (B && B.saveTitle) $("save").title = B.saveTitle; + // engine version from /api/meta; a web build adds its commit and date + // (, tools/build_web.py) + function showVersion() { + var el = $("ver"), m = document.querySelector('meta[name="isokit-build"]'); + var b = m ? m.getAttribute("content").split(" ") : []; + var text = "isokit " + (S.meta.engine || "?"), tip = "isokit engine " + (S.meta.engine || "?"); + if (b[0]) { + text += " · " + b[0].slice(0, 7) + (b[2] ? "+" : ""); + tip += ", build " + b[0] + (b[1] ? " (" + b[1] + ")" : "") + (b[2] ? ", with uncommitted changes" : ""); + el.href = "https://git.b4l.co.th/B4L/isokit/commit/" + b[0]; + } + el.textContent = text; + el.title = tip; + } + // ------------------------------------------------------------ start Promise.all([api("/api/meta"), api("/api/doc")]).then(function (rs) { S.meta = rs[0]; takeState(rs[1]); document.title = S.meta.title + " · isokit Studio"; $("title").textContent = S.meta.title; if (S.meta.load_error) toast(S.meta.load_error, true); + showVersion(); renderPalette(); renderLists(); mountView(); renderRight(); poll(); }); diff --git a/tools/build_web.py b/tools/build_web.py index 46e7dfb..1055df1 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -81,6 +81,24 @@ def demo_files(): return out +def build_info(): + """' [dirty]' for the version text in the Studio's + status bar; GITHUB_SHA (CI) wins over git. Empty when git is missing.""" + def git(*args): + r = subprocess.run(["git", *args], capture_output=True, text=True, cwd=ROOT) + return r.stdout.strip() if r.returncode == 0 else "" + try: + sha = os.environ.get("GITHUB_SHA") or git("rev-parse", "HEAD") + if not sha: + return "" + parts = [sha, git("log", "-1", "--format=%cs", sha) or "-"] + if git("status", "--porcelain", "--untracked-files=no"): + parts.append("dirty") + return " ".join(parts) + except OSError: + return "" + + def replace_once(s, old, new): n = s.count(old) assert n == 1, f"studio/index.html: expected {old!r} once, found {n}" @@ -116,7 +134,7 @@ def main(): print(" (wasm-opt not found: the .wasm stays unoptimised by binaryen, about 15% larger)") for src, dst in [("viewer/isokit-view.js", "viewer"), ("viewer/isokit-view.css", "viewer"), - ("studio/studio.js", "studio"), ("studio/studio.css", "studio"), + ("studio/studio.js", "studio"), ("studio/studio.css", "studio"), ("studio/favicon.svg", "studio"), ("web/web.js", "web"), ("web/web.css", "web")]: (out / "assets" / dst).mkdir(parents=True, exist_ok=True) shutil.copy2(ROOT / src, out / "assets" / dst / Path(src).name) @@ -129,8 +147,11 @@ def main(): v = "v=" + h.hexdigest()[:10] html = (ROOT / "studio" / "index.html").read_text(encoding="utf-8") + build = build_info() html = replace_once(html, 'isokit Studio', - 'isokit Studio\n') + 'isokit Studio\n' + + (f'\n' if build else "")) + html = replace_once(html, 'href="/assets/studio/favicon.svg"', f'href="assets/studio/favicon.svg?{v}"') html = replace_once(html, 'href="/assets/viewer/isokit-view.css"', f'href="assets/viewer/isokit-view.css?{v}"') html = replace_once(html, '', '') html = replace_once(html, '', @@ -148,7 +169,7 @@ def main(): if p.is_file(): total += p.stat().st_size print(f" {p.relative_to(out).as_posix():40} {p.stat().st_size / 1024:8.1f} kB") - print(f"{out} ({total / 1024 / 1024:.1f} MB, {v})") + print(f"{out} ({total / 1024 / 1024:.1f} MB, {v}, build {build or 'unknown'})") if __name__ == "__main__": diff --git a/tools/check_studio.py b/tools/check_studio.py index aced5aa..716dab8 100644 --- a/tools/check_studio.py +++ b/tools/check_studio.py @@ -48,6 +48,10 @@ with sync_playwright() as p: n0 = len(doc()["objects"]) check(pg.locator("#objects li").count() == n0, f"object list shows {n0} objects") check(pg.locator(".st-tile").count() >= 20, "palette lists the prefabs") + ver = pg.locator("#ver").inner_text() + check(ver == "isokit " + get("/api/meta")["engine"], "status bar shows the engine version: " + ver) + icon = urllib.request.urlopen(BASE + "/assets/studio/favicon.svg") + check(icon.headers.get_content_type() == "image/svg+xml" and b" getComputedStyle(document.querySelector('.iso-svg .iso-t')).fill") not in ("", "rgb(0, 0, 0)"), "theme CSS styles the scene") check(pg.evaluate("() => isokit.webmcp") == 15, "15 MCP tools registered with WebMCP") + ver = pg.locator("#ver").inner_text() + check(ver.startswith("isokit " + pg.evaluate("() => isokit.engine") + " · "), "status bar shows version and build: " + ver) + icon = pg.evaluate("""async () => { const l = document.querySelector('link[rel=icon]'); + const r = await fetch(l.href); return r.ok && (await r.text()).includes('