fix(ui-web): resolve WS race condition causing DISCONNECTED on modern skin
Problem: when skin layouts are lazy-loaded via defineAsyncComponent, the
WebSocket 'open' event can fire before the layout mounts and registers its
listener. The layout then shows DISCONNECTED until manual refresh because
getData() is never triggered.
Root cause: UIClient connects in main.ts constructor (fire-and-forget),
but the skin layout mounts asynchronously after the chunk loads. If the
WS handshake completes during chunk loading, the 'open' event is dispatched
to an EventTarget with zero listeners.
Fix: expose connection state and fetch eagerly when already connected.
- ui-common/WebSocketClient: add `get connected(): boolean` getter
(returns readyState === OPEN)
- ui-web/UIClient: add `isConnected(): boolean` (delegates to client.connected)
- ui-web/useLayoutData: in onMounted, call getData() immediately if
isConnected() is true. Otherwise the normal 'open' handler will
trigger getData() when the connection establishes — no premature
fetch, no error toast, no duplicate calls.
- ui-web/tests: add isConnected to MockUIClient (defaults to false);
add 2 explicit tests for both branches of the mount-time check