Files
wallet/script/webview-inspect.py
T
grenadeandClaude Fable 5.1 2fcc519215
ci / gate (push) Successful in 9m16s
fix(app): a dev CSP so the Vite page renders, one data directory, and a way to verify the webview
Running the shell for the first time showed a blank white window and no
error anywhere: Vite's dev page injects an inline React Refresh preamble,
the production CSP forbids inline scripts, and every component module then
throws on `$RefreshReg$` before React mounts. `tauri.conf.json` now carries
a `devCsp` for the dev server's origin; the production CSP is unchanged.

`WALLET_DATA_DIR` relocates wallets, settings and profile overrides
together, so a dev run lives in a throwaway directory instead of the real
app data; `WALLET_KEYSTORE_DIR` still moves only the wallets, for sharing
them with quantus-cli in place.

Verified end to end against the local dev node by driving the webview
through WebKitGTK's remote inspector (`script/webview-inspect.py`, kept
with the reason in CLAUDE.md): the lock screen lists both dev accounts;
unlocking crystal_alice shows 99,957.786510548 DEV at block 1832, 100 from
final, with the status bar on ws://127.0.0.1:9944 at spec 152; the block
number advances on its own; a settings change lands in settings.json; the
idle rule locks the UI without a click after the configured minute and the
Rust log says so; the Lock button locks; the console shows nothing but
Vite connecting. GNOME refuses D-Bus screenshots and ImageMagick captures
WebKit's GPU surface as flat grey, so the DOM was the evidence.

Refs #25

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW
2026-09-15 23:18:03 +03:00

104 lines
4.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""Drive the running wallet's webview through WebKitGTK's remote inspector.
Start the app with `WEBKIT_INSPECTOR_HTTP_SERVER=127.0.0.1:9223 cargo tauri dev`
(from crates/wallet-app), then:
script/webview-inspect.py eval "document.body.innerText"
script/webview-inspect.py eval "document.querySelector('form').requestSubmit()"
script/webview-inspect.py console 3
Needs `pip install --user websockets`. This is how the shell was verified on a
Wayland desktop where screenshots are refused and ImageMagick cannot see
WebKit's GPU surface: the DOM is the truth, and JavaScript drives the UI.
"""
import asyncio, json, sys
import websockets
WS = "ws://127.0.0.1:9223/socket/1/1/WebPage"
class Inspector:
def __init__(self, ws):
self.ws, self.n, self.target = ws, 0, None
self.events = []
async def raw(self, method, params=None):
self.n += 1
i = self.n
await self.ws.send(json.dumps({"id": i, "method": method, "params": params or {}}))
return i
async def pump(self, timeout=5.0):
"""Read one frame, unwrapping target messages; returns (kind, payload)."""
msg = json.loads(await asyncio.wait_for(self.ws.recv(), timeout=timeout))
if msg.get("method") == "Target.targetCreated":
self.target = msg["params"]["targetInfo"]["targetId"]
return ("target", self.target)
if msg.get("method") == "Target.dispatchMessageFromTarget":
inner = json.loads(msg["params"]["message"])
if "id" in inner:
return ("reply", inner)
return ("event", inner)
if "id" in msg:
return ("outer-reply", msg)
return ("outer-event", msg)
async def ensure_target(self):
if self.target:
return
await self.raw("Target.exists") if False else None
# The inspector announces the page target on connect.
for _ in range(20):
kind, p = await self.pump()
if kind == "target":
return
raise RuntimeError("no target announced")
async def call(self, method, params=None):
await self.ensure_target()
self.n += 1
i = self.n
inner = json.dumps({"id": i, "method": method, "params": params or {}})
await self.ws.send(json.dumps({"id": 10_000 + i, "method": "Target.sendMessageToTarget",
"params": {"targetId": self.target, "message": inner}}))
while True:
kind, p = await self.pump()
if kind == "reply" and p.get("id") == i:
return p
if kind == "event":
self.events.append(p)
async def main():
cmd = sys.argv[1]
async with websockets.connect(WS, max_size=1 << 24) as ws:
insp = Inspector(ws)
if cmd == "eval":
r = await insp.call("Runtime.evaluate", {"expression": sys.argv[2], "returnByValue": True, "awaitPromise": True})
res = r.get("result", {})
if "error" in r:
print("ERROR:", r["error"]); return
v = res.get("result", {})
val = v.get("value", v)
print(json.dumps(val, indent=1) if isinstance(val, (dict, list)) else val)
if res.get("wasThrown"):
print("THROWN")
else:
secs = float(sys.argv[2]) if len(sys.argv) > 2 else 3.0
await insp.call("Console.enable")
await insp.call("Runtime.enable")
loop = asyncio.get_event_loop(); end = loop.time() + secs
while loop.time() < end:
try:
kind, p = await insp.pump(timeout=max(0.1, end - loop.time()))
except asyncio.TimeoutError:
break
if kind == "event" and p.get("method") == "Console.messageAdded":
m = p["params"]["message"]
print(f"[{m.get('level')}] {m.get('text')} ({m.get('url','')}:{m.get('line','')})")
for p in insp.events:
if p.get("method") == "Console.messageAdded":
m = p["params"]["message"]
print(f"[{m.get('level')}] {m.get('text')} ({m.get('url','')}:{m.get('line','')})")
asyncio.run(main())