2026-05-21 09:50:51 +07:00
|
|
|
/**
|
2026-05-25 07:16:08 +07:00
|
|
|
* Velxio Desktop SPA hooks - mounted from main.tsx when VITE_DESKTOP is set.
|
2026-05-21 09:50:51 +07:00
|
|
|
*
|
2026-05-25 07:16:08 +07:00
|
|
|
* Responsibilities since v0.3.0:
|
2026-05-21 09:50:51 +07:00
|
|
|
*
|
2026-05-25 07:16:08 +07:00
|
|
|
* 1. Decide on first paint whether to mount the welcome / lockout
|
|
|
|
|
* overlay or let the editor open directly. Driven by
|
|
|
|
|
* `license_gate_info()` (returns {state, grandfather_days_remaining}).
|
|
|
|
|
* 2. Listen for `velxio://license-required` from the Tauri shell -
|
|
|
|
|
* fires when the sidecar refuses to start (gate closed) OR
|
|
|
|
|
* exits with code 78 mid-session. Mounts the LockoutOverlay.
|
|
|
|
|
* 3. Mount the persistent side panels (grace banner + ESP32 prompt).
|
2026-05-21 09:50:51 +07:00
|
|
|
*
|
2026-05-25 07:16:08 +07:00
|
|
|
* Variant decision (no key paths):
|
2026-05-21 09:50:51 +07:00
|
|
|
*
|
2026-05-25 07:16:08 +07:00
|
|
|
* gate.state === 'grandfather' -> soft welcome with "use anyway for N days"
|
|
|
|
|
* gate.state === null -> lockout (no_credential variant)
|
|
|
|
|
* gate.state === 'valid'|'soft'|'hard' -> normal app, banner handles grace
|
|
|
|
|
*
|
|
|
|
|
* Tampered tokens currently surface via the sidecar exit-78 path
|
|
|
|
|
* because the shell's runtime_state_for_sidecar() maps `Tampered` to
|
|
|
|
|
* None. We could distinguish the two by passing a reason in the event
|
|
|
|
|
* payload (Phase 4 polish).
|
2026-05-21 09:50:51 +07:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { createRoot, type Root } from 'react-dom/client';
|
2026-05-25 07:16:08 +07:00
|
|
|
import { createElement, createElement as h, Fragment } from 'react';
|
2026-05-21 09:50:51 +07:00
|
|
|
import { DesktopWelcomePage } from './DesktopWelcomePage';
|
|
|
|
|
import { Esp32QemuPrompt } from './Esp32QemuPrompt';
|
feat: STM32 (Blue Pill / Black Pill) QEMU emulation + Pro board gating
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.
Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:06:14 +07:00
|
|
|
import { Stm32QemuPrompt } from './Stm32QemuPrompt';
|
2026-05-21 09:50:51 +07:00
|
|
|
import { GraceBanner } from './GraceBanner';
|
2026-05-25 07:16:08 +07:00
|
|
|
import { LockoutOverlay, type LockoutReason } from './LockoutOverlay';
|
2026-05-27 01:46:39 +07:00
|
|
|
import { UpdateAvailableToast } from './UpdateAvailableToast';
|
2026-05-25 07:16:08 +07:00
|
|
|
import {
|
|
|
|
|
getGateInfo,
|
|
|
|
|
invoke,
|
|
|
|
|
isTauri,
|
|
|
|
|
listen,
|
|
|
|
|
type GateInfo,
|
|
|
|
|
type ValidationResult,
|
|
|
|
|
} from './tauriBridge';
|
2026-05-23 04:08:46 +07:00
|
|
|
import { installDesktopMenuListener } from './menu';
|
|
|
|
|
import { dlog } from './log';
|
2026-05-21 09:50:51 +07:00
|
|
|
import './desktop.css';
|
|
|
|
|
|
|
|
|
|
let mounted = false;
|
2026-05-25 07:16:08 +07:00
|
|
|
let overlayRoot: Root | null = null;
|
|
|
|
|
let overlayHost: HTMLElement | null = null;
|
2026-05-21 09:50:51 +07:00
|
|
|
let sidePanelRoot: Root | null = null;
|
|
|
|
|
|
2026-05-25 07:16:08 +07:00
|
|
|
function unmountOverlay(): void {
|
|
|
|
|
if (overlayRoot) {
|
|
|
|
|
try { overlayRoot.unmount(); } catch { /* noop */ }
|
|
|
|
|
overlayRoot = null;
|
2026-05-21 09:50:51 +07:00
|
|
|
}
|
2026-05-25 07:16:08 +07:00
|
|
|
if (overlayHost) {
|
|
|
|
|
overlayHost.remove();
|
|
|
|
|
overlayHost = null;
|
2026-05-21 09:50:51 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 07:16:08 +07:00
|
|
|
function ensureOverlayHost(): HTMLElement {
|
|
|
|
|
if (overlayHost) return overlayHost;
|
|
|
|
|
overlayHost = document.createElement('div');
|
|
|
|
|
overlayHost.id = 'velxio-desktop-overlay-root';
|
|
|
|
|
document.body.appendChild(overlayHost);
|
|
|
|
|
return overlayHost;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mountWelcome(grandfatherDaysRemaining: number | null): void {
|
|
|
|
|
unmountOverlay();
|
|
|
|
|
const host = ensureOverlayHost();
|
|
|
|
|
overlayRoot = createRoot(host);
|
|
|
|
|
overlayRoot.render(
|
2026-05-21 09:50:51 +07:00
|
|
|
createElement(DesktopWelcomePage, {
|
2026-05-25 07:16:08 +07:00
|
|
|
onAuthorised: () => unmountOverlay(),
|
|
|
|
|
grandfatherDaysRemaining,
|
2026-05-21 09:50:51 +07:00
|
|
|
}),
|
|
|
|
|
);
|
2026-05-25 07:16:08 +07:00
|
|
|
dlog('mountWelcome', { grandfatherDaysRemaining });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mountLockout(reason: LockoutReason): void {
|
|
|
|
|
unmountOverlay();
|
|
|
|
|
const host = ensureOverlayHost();
|
|
|
|
|
overlayRoot = createRoot(host);
|
|
|
|
|
overlayRoot.render(createElement(LockoutOverlay, { reason }));
|
|
|
|
|
dlog('mountLockout', { reason });
|
2026-05-21 09:50:51 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mountSidePanels(): void {
|
|
|
|
|
if (sidePanelRoot) return;
|
|
|
|
|
const host = document.createElement('div');
|
|
|
|
|
host.id = 'velxio-desktop-side-panels';
|
|
|
|
|
document.body.appendChild(host);
|
|
|
|
|
sidePanelRoot = createRoot(host);
|
|
|
|
|
sidePanelRoot.render(
|
2026-05-27 01:46:39 +07:00
|
|
|
h(
|
|
|
|
|
Fragment,
|
|
|
|
|
null,
|
|
|
|
|
h(GraceBanner, null),
|
|
|
|
|
h(Esp32QemuPrompt, null),
|
feat: STM32 (Blue Pill / Black Pill) QEMU emulation + Pro board gating
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.
Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:06:14 +07:00
|
|
|
h(Stm32QemuPrompt, null),
|
2026-05-27 01:46:39 +07:00
|
|
|
// v0.4.0 auto-update toast (~30s after mount). Lives below the
|
|
|
|
|
// grace banner z-index so a lockout / hard-grace doesn't get
|
|
|
|
|
// covered by a "new version available" pitch.
|
|
|
|
|
h(UpdateAvailableToast, null),
|
|
|
|
|
),
|
2026-05-21 09:50:51 +07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-05-25 07:16:08 +07:00
|
|
|
* Decide initial state on first paint. Three outcomes:
|
feat(desktop): skip welcome screen, robust openExternal, in-app nav
Three coordinated changes that fix the "Waiting for browser…" hang
and unblock first-launch UX on the Tauri desktop build:
1. desktop/index.ts — DON'T mountWelcome unconditionally on first
launch. Before, an empty keychain (no key yet) forced the
welcome / sign-in screen on top of the editor, gating 100% of
the app behind an account. Now the editor opens directly:
compile + run + sim + save .vlx all work for free (they're
upstream OSS features), and the license check still runs in
the background just to populate state for the GraceBanner
(which shows for invalid keys — locked, tampered, in
soft/hard grace). Pro-only features (ESP32 QEMU download,
agent IA) prompt for license at use time, where it actually
matters. Matches the "try before you buy" expectation a
desktop install creates.
2. desktop/tauriBridge.ts — rewrite `openExternal` to try every
known IPC path in cascade order and log via the desktop debug
file which one worked. The previous implementation invoked
`plugin:shell|open` with `{ path: url }`, which silently
failed (no ACL match + wrong arg shape) and fell back to
`window.open`, which inside a Tauri webview is a no-op for
external URLs — the browser never opened. New cascade:
plugin:opener|open_url (paired with tauri-plugin-opener which
ships in this revision), then plugin:shell|open with both
`{ path, with: null }` and `{ url }` shapes, then the
window.__TAURI__.shell / opener high-level wrappers that
specific Tauri 2.x flag combos expose. Each attempt logged
via the dlog helper so the next operator can see exactly
which path was used (or that all failed) without devtools.
3. desktop/menu.ts — new `navigate-route` action type. Routes
bundled in the SPA (DocsPage, ExamplesPage, AboutPage) that
used to open velxio.dev in the system browser now navigate
in-window via history.pushState + popstate (mirrors the
locale-switch handler). Respects the current locale prefix
so `/examples` from `/es/editor` lands at `/es/examples`
instead of jumping back to English.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 03:57:46 +07:00
|
|
|
*
|
2026-05-25 07:16:08 +07:00
|
|
|
* 1. State is valid / soft_grace / hard_grace -> editor opens, no
|
|
|
|
|
* overlay (the GraceBanner side panel handles in-app messaging
|
|
|
|
|
* for soft/hard grace).
|
|
|
|
|
* 2. State is grandfather -> soft welcome with grandfather days
|
|
|
|
|
* remaining + a "continue without signing in" escape.
|
|
|
|
|
* 3. State is null -> hard lockout (no_credential variant).
|
feat(desktop): skip welcome screen, robust openExternal, in-app nav
Three coordinated changes that fix the "Waiting for browser…" hang
and unblock first-launch UX on the Tauri desktop build:
1. desktop/index.ts — DON'T mountWelcome unconditionally on first
launch. Before, an empty keychain (no key yet) forced the
welcome / sign-in screen on top of the editor, gating 100% of
the app behind an account. Now the editor opens directly:
compile + run + sim + save .vlx all work for free (they're
upstream OSS features), and the license check still runs in
the background just to populate state for the GraceBanner
(which shows for invalid keys — locked, tampered, in
soft/hard grace). Pro-only features (ESP32 QEMU download,
agent IA) prompt for license at use time, where it actually
matters. Matches the "try before you buy" expectation a
desktop install creates.
2. desktop/tauriBridge.ts — rewrite `openExternal` to try every
known IPC path in cascade order and log via the desktop debug
file which one worked. The previous implementation invoked
`plugin:shell|open` with `{ path: url }`, which silently
failed (no ACL match + wrong arg shape) and fell back to
`window.open`, which inside a Tauri webview is a no-op for
external URLs — the browser never opened. New cascade:
plugin:opener|open_url (paired with tauri-plugin-opener which
ships in this revision), then plugin:shell|open with both
`{ path, with: null }` and `{ url }` shapes, then the
window.__TAURI__.shell / opener high-level wrappers that
specific Tauri 2.x flag combos expose. Each attempt logged
via the dlog helper so the next operator can see exactly
which path was used (or that all failed) without devtools.
3. desktop/menu.ts — new `navigate-route` action type. Routes
bundled in the SPA (DocsPage, ExamplesPage, AboutPage) that
used to open velxio.dev in the system browser now navigate
in-window via history.pushState + popstate (mirrors the
locale-switch handler). Respects the current locale prefix
so `/examples` from `/es/editor` lands at `/es/examples`
instead of jumping back to English.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 03:57:46 +07:00
|
|
|
*
|
2026-05-25 07:16:08 +07:00
|
|
|
* Tampered tokens currently hit case 3 with a generic reason; we
|
|
|
|
|
* could refine by passing a richer payload from the shell later.
|
|
|
|
|
*/
|
|
|
|
|
async function evaluateAndMount(gate: GateInfo): Promise<void> {
|
|
|
|
|
switch (gate.state) {
|
|
|
|
|
case 'valid':
|
|
|
|
|
case 'soft_grace':
|
|
|
|
|
case 'hard_grace':
|
|
|
|
|
dlog('evaluateAndMount: state=valid/grace - no overlay', { state: gate.state });
|
|
|
|
|
return;
|
|
|
|
|
case 'grandfather':
|
|
|
|
|
mountWelcome(gate.grandfather_days_remaining);
|
|
|
|
|
return;
|
|
|
|
|
case null:
|
|
|
|
|
default:
|
|
|
|
|
mountLockout('no_credential');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Background: validate the cached key. Logs the result for the
|
|
|
|
|
* desktop-debug.log file. Does not gate the editor - the shell
|
|
|
|
|
* already did that at startup. This is mostly an observability hook
|
|
|
|
|
* so we can debug a "valid in shell, invalid in API" mismatch.
|
2026-05-21 09:50:51 +07:00
|
|
|
*/
|
2026-05-25 07:16:08 +07:00
|
|
|
async function validateInBackground(): Promise<void> {
|
feat(desktop): skip welcome screen, robust openExternal, in-app nav
Three coordinated changes that fix the "Waiting for browser…" hang
and unblock first-launch UX on the Tauri desktop build:
1. desktop/index.ts — DON'T mountWelcome unconditionally on first
launch. Before, an empty keychain (no key yet) forced the
welcome / sign-in screen on top of the editor, gating 100% of
the app behind an account. Now the editor opens directly:
compile + run + sim + save .vlx all work for free (they're
upstream OSS features), and the license check still runs in
the background just to populate state for the GraceBanner
(which shows for invalid keys — locked, tampered, in
soft/hard grace). Pro-only features (ESP32 QEMU download,
agent IA) prompt for license at use time, where it actually
matters. Matches the "try before you buy" expectation a
desktop install creates.
2. desktop/tauriBridge.ts — rewrite `openExternal` to try every
known IPC path in cascade order and log via the desktop debug
file which one worked. The previous implementation invoked
`plugin:shell|open` with `{ path: url }`, which silently
failed (no ACL match + wrong arg shape) and fell back to
`window.open`, which inside a Tauri webview is a no-op for
external URLs — the browser never opened. New cascade:
plugin:opener|open_url (paired with tauri-plugin-opener which
ships in this revision), then plugin:shell|open with both
`{ path, with: null }` and `{ url }` shapes, then the
window.__TAURI__.shell / opener high-level wrappers that
specific Tauri 2.x flag combos expose. Each attempt logged
via the dlog helper so the next operator can see exactly
which path was used (or that all failed) without devtools.
3. desktop/menu.ts — new `navigate-route` action type. Routes
bundled in the SPA (DocsPage, ExamplesPage, AboutPage) that
used to open velxio.dev in the system browser now navigate
in-window via history.pushState + popstate (mirrors the
locale-switch handler). Respects the current locale prefix
so `/examples` from `/es/editor` lands at `/es/examples`
instead of jumping back to English.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 03:57:46 +07:00
|
|
|
if (!isTauri()) return;
|
2026-05-21 09:50:51 +07:00
|
|
|
try {
|
|
|
|
|
const key = await invoke<string | null>('license_get_key');
|
|
|
|
|
if (!key) {
|
2026-05-25 07:16:08 +07:00
|
|
|
dlog('validateInBackground: no key (grandfather or anonymous mode)');
|
2026-05-21 09:50:51 +07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const result = await invoke<ValidationResult>('license_validate', { key });
|
2026-05-25 07:16:08 +07:00
|
|
|
dlog('validateInBackground: validated', {
|
feat(desktop): skip welcome screen, robust openExternal, in-app nav
Three coordinated changes that fix the "Waiting for browser…" hang
and unblock first-launch UX on the Tauri desktop build:
1. desktop/index.ts — DON'T mountWelcome unconditionally on first
launch. Before, an empty keychain (no key yet) forced the
welcome / sign-in screen on top of the editor, gating 100% of
the app behind an account. Now the editor opens directly:
compile + run + sim + save .vlx all work for free (they're
upstream OSS features), and the license check still runs in
the background just to populate state for the GraceBanner
(which shows for invalid keys — locked, tampered, in
soft/hard grace). Pro-only features (ESP32 QEMU download,
agent IA) prompt for license at use time, where it actually
matters. Matches the "try before you buy" expectation a
desktop install creates.
2. desktop/tauriBridge.ts — rewrite `openExternal` to try every
known IPC path in cascade order and log via the desktop debug
file which one worked. The previous implementation invoked
`plugin:shell|open` with `{ path: url }`, which silently
failed (no ACL match + wrong arg shape) and fell back to
`window.open`, which inside a Tauri webview is a no-op for
external URLs — the browser never opened. New cascade:
plugin:opener|open_url (paired with tauri-plugin-opener which
ships in this revision), then plugin:shell|open with both
`{ path, with: null }` and `{ url }` shapes, then the
window.__TAURI__.shell / opener high-level wrappers that
specific Tauri 2.x flag combos expose. Each attempt logged
via the dlog helper so the next operator can see exactly
which path was used (or that all failed) without devtools.
3. desktop/menu.ts — new `navigate-route` action type. Routes
bundled in the SPA (DocsPage, ExamplesPage, AboutPage) that
used to open velxio.dev in the system browser now navigate
in-window via history.pushState + popstate (mirrors the
locale-switch handler). Respects the current locale prefix
so `/examples` from `/es/editor` lands at `/es/examples`
instead of jumping back to English.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 03:57:46 +07:00
|
|
|
valid: result.valid,
|
|
|
|
|
plan: result.plan,
|
|
|
|
|
reason_code: result.reason_code,
|
|
|
|
|
});
|
2026-05-21 09:50:51 +07:00
|
|
|
} catch (err) {
|
2026-05-25 07:16:08 +07:00
|
|
|
dlog('validateInBackground: failed', { err: String(err) });
|
2026-05-21 09:50:51 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 07:16:08 +07:00
|
|
|
/**
|
|
|
|
|
* Install the `velxio://license-required` listener. The shell fires
|
|
|
|
|
* this in two cases:
|
|
|
|
|
*
|
|
|
|
|
* - At startup, when `runtime_state_for_sidecar()` returns None
|
|
|
|
|
* (no key + grandfather expired, or tampered token). Sidecar
|
|
|
|
|
* wasn't spawned; we mount the lockout.
|
|
|
|
|
* - Mid-session, when the sidecar exits with code 78 (the gate
|
|
|
|
|
* refused mid-run). Same treatment.
|
|
|
|
|
*
|
|
|
|
|
* Payload is either the string "locked" (startup case) or a number
|
|
|
|
|
* (the sidecar's exit code, currently always 78). Either way we
|
|
|
|
|
* mount the no_credential lockout - the user resolves it with
|
|
|
|
|
* sign-in or paste-key, then restartApp() reboots the shell.
|
|
|
|
|
*/
|
|
|
|
|
function installLicenseRequiredListener(): void {
|
|
|
|
|
void listen('velxio://license-required', (event) => {
|
|
|
|
|
dlog('license-required event', { payload: event.payload });
|
|
|
|
|
mountLockout('no_credential');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Foreground polling for gate transitions the shell can't push us.
|
|
|
|
|
*
|
|
|
|
|
* The shell's checkin_loop emits `velxio://license-status` every 6h
|
|
|
|
|
* after a network refresh - but the JWT exp is a fixed timestamp, so
|
|
|
|
|
* the soft -> hard -> locked transitions happen purely on wall-clock
|
|
|
|
|
* elapse. The shell doesn't notice them until the next checkin.
|
|
|
|
|
*
|
|
|
|
|
* We poll `license_gate_info` every 10 min while the document is
|
|
|
|
|
* visible. When the state slips from valid/grace into a locked state
|
|
|
|
|
* (gate.state === null), we mount the lockout overlay AND emit a
|
|
|
|
|
* console log for the debug file. Same trigger as the startup
|
|
|
|
|
* evaluation - the user can sign in / paste-key from the same UI.
|
|
|
|
|
*
|
|
|
|
|
* Polling is suspended while the document is hidden (minimised,
|
|
|
|
|
* background tab in dev) so a backgrounded laptop doesn't burn
|
|
|
|
|
* cycles on a fixed-output computation.
|
|
|
|
|
*/
|
|
|
|
|
let lastGateState: GateInfo['state'] = 'valid';
|
|
|
|
|
|
|
|
|
|
function installGatePoller(): void {
|
|
|
|
|
const POLL_MS = 10 * 60 * 1000;
|
|
|
|
|
const tick = async () => {
|
|
|
|
|
if (document.visibilityState !== 'visible') return;
|
|
|
|
|
try {
|
|
|
|
|
const gate = await getGateInfo();
|
|
|
|
|
// Only react on a TRANSITION INTO a locked state - flicking
|
|
|
|
|
// the overlay on every poll would re-mount during the user
|
|
|
|
|
// typing in the paste field. The lockout-overlay sticks
|
|
|
|
|
// around until restartApp() resolves it.
|
|
|
|
|
if (gate.state === null && lastGateState !== null) {
|
|
|
|
|
dlog('gate transitioned to locked while running', { from: lastGateState });
|
|
|
|
|
mountLockout('expired');
|
|
|
|
|
}
|
|
|
|
|
lastGateState = gate.state;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
dlog('gate poller: getGateInfo failed', { err: String(err) });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
window.setInterval(tick, POLL_MS);
|
|
|
|
|
document.addEventListener('visibilitychange', () => {
|
|
|
|
|
if (document.visibilityState === 'visible') void tick();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-21 09:50:51 +07:00
|
|
|
export const mountDesktop = (): void => {
|
|
|
|
|
if (mounted) return;
|
|
|
|
|
mounted = true;
|
2026-05-25 07:16:08 +07:00
|
|
|
dlog('mountDesktop - Tauri shell active');
|
2026-05-23 04:08:46 +07:00
|
|
|
|
|
|
|
|
// Native menubar (Velxio / File / Edit / View / Help) sends events
|
|
|
|
|
// here. Hook the listener before any UI is mounted so the first
|
|
|
|
|
// user click is never dropped.
|
|
|
|
|
void installDesktopMenuListener();
|
2026-05-21 09:50:51 +07:00
|
|
|
|
|
|
|
|
mountSidePanels();
|
2026-05-25 07:16:08 +07:00
|
|
|
installLicenseRequiredListener();
|
|
|
|
|
installGatePoller();
|
|
|
|
|
|
|
|
|
|
// First-paint gate evaluation. If the shell is pre-0.3.0 the
|
|
|
|
|
// command isn't registered and getGateInfo() returns the legacy
|
|
|
|
|
// "always valid" stub so the editor opens normally.
|
|
|
|
|
void (async () => {
|
|
|
|
|
const gate = await getGateInfo();
|
|
|
|
|
dlog('initial gate', gate);
|
|
|
|
|
lastGateState = gate.state;
|
|
|
|
|
await evaluateAndMount(gate);
|
|
|
|
|
void validateInBackground();
|
|
|
|
|
})();
|
2026-05-21 09:50:51 +07:00
|
|
|
};
|