Skip to content

Merge development into main - #1010

Merged
thiagoralves merged 8 commits into
mainfrom
development
Aug 12, 2026
Merged

Merge development into main#1010
thiagoralves merged 8 commits into
mainfrom
development

Conversation

@thiagoralves

Copy link
Copy Markdown
Contributor

Promotes development to main, paired with openplc-web #671.

What this ships

Commits Change
3 Simulator session lifecycle — #1009
2 Post-purchase license watch — #1008

Simulator fix (#1009) — Stop never actually stopped the emulator: stopSession() deliberately doesn't own it and the button never made the call, so the avr8js loop kept re-scheduling itself and burning a core, unreachable once the button flipped back to "Start". Also routes every emulator stop path through one choke point that closes the session first, fixing two leaks (window reload, and a start that threw after loadAndRun).

License watch (#1008) — rides along; already reviewed, merged and green on development.

Notes

  • main had no unique non-merge content, so this is a clean promotion.
  • No version bump: APP_VERSION and package.json stay at 4.2.11. This promotion therefore does not produce a desktop release — the "Build and Release" workflow fires on a vX.Y.Z tag, and v4.2.11 already exists. Deliberate — requested explicitly.
  • Shared workspace-activity-bar/default.tsx is byte-identical with openplc-web migrate(step-18): Atoms batch 1 — shared identical components #671.

🤖 Generated with Claude Code

marconetsf and others added 8 commits August 12, 2026 15:49
…ence automatically

Buying happens in an external browser tab, and until now nothing brought
the result back: the editor kept showing "Not licensed" until the user
guessed they should click "Check again". The purchase flow ended in a
dead end at its most important moment.

buy() now opens the purchase page AND starts a purchase watch
(deviceLicense.awaitingPurchase). While it runs, the existing refresh()
executes every 20s — one Modbus read frame plus one HTTP round-trip —
and on the first tick after the completion webhook lands, that same
refresh() activates the licence and WRITES the blob to the device, no
click required. The watch ends on the first licensed report (whoever
produced it: the poll, a manual re-check, the connect flow), after a
30-tick / 10-minute budget, on "Stop waiting", or when the board is
switched / disconnected (clearDeviceLicense).

Badge behaviour while waiting:
- reads "Waiting for purchase…" steadily — it outranks the isChecking
  flicker each tick would otherwise cause;
- withdraws "Buy licence" — offering it mid-wait invites a double buy;
- offers "Stop waiting".

Ticks that would overlap a call still in flight (slow device, 30s HTTP
timeout) are skipped instead of stacking a second call on the same link.
The interval calls refresh through a ref: its identity follows the device
port, and an interval keyed on it would reset the tick budget on every
change.

Companion change (autonomy-edge, EDGE-593): the /buy page now polls the
same truth and shows "License issued" only when the webhook actually
landed it.

Verified: 7 new hook tests (watch start, no-URL no-watch, per-tick
refresh, overlap skip, licensed ends it, tick budget, cancel), 6 new
badge tests, 4 new slice tests; device-slice 127/127, badge 26/26,
tsc --noEmit clean, eslint 0 errors (the 4 unbound-method warnings in
use-device-license.ts pre-date this change, same count).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uUH3ZL5ehreMUf2dtanWD
The format check runs prettier --check over src/**; the multiline
className in the waiting-state badge was hand-wrapped differently than
prettier wants it. No behavioural change — the badge suite still passes
26/26.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uUH3ZL5ehreMUf2dtanWD
…very stop path

Two fixes, one ownership rule: the emulator and the session it serves
start and end together.

1. Stop actually stops the emulator (shared surface)

The Stop button ended the debug session, logged "Simulator stopped." and
left the emulator running. `stopSession()` deliberately does not stop it
(a debug session is a consumer of the emulator, not its owner — see its
docstring), and this button, which owns that job, never made the call.
The avr8js loop kept re-scheduling itself and burning a core for the rest
of the session, unreachable from the UI because the button had already
flipped back to "Start".

`workspace-activity-bar/default.tsx` is byte-identical with openplc-web
and carries the same change there; the two must land together for the
Shared Surface Sync gate.

2. Every stop path closes the session (main process)

Stops were scattered, and two of them closed nothing:

- `handleWindowReload` stopped the emulator but left the session open,
  so main went on holding a simulator session the reloaded renderer knew
  nothing about.
- `handleSimulatorLoadFirmware`'s catch did neither. `loadAndRun` marks
  the emulator running before it finishes wiring, so a throw after that
  point leaked a running emulator with no session — and no button to
  reach it, because the renderer never learned it had started.

All six sites now route through one `stopSimulator()` choke point that
closes the session first, then stops the emulator. The load-failure path
is the parity fix for openplc-web, where the worker already cleans up and
reports 'stopped' when `loadAndRun` throws.

Adds `simulator-session.handler.test.ts`: 5 cases over ordering, the
non-simulator link left alone, the reload path and the throw path. The
two covering new behaviour fail without this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review fixes on the post-purchase watch:

- reset the watch on EVERY licence reset path: a real board change and
  project close previously left it polling (and potentially writing a
  licence) against the next board's package id. One resetDeviceLicense
  helper now serves all three reset paths so a fourth cannot drift.
- replace the 30-tick budget with an absolute deadline persisted in the
  store (awaitingPurchaseUntil, PURCHASE_WATCH_WINDOW_MS): a remount
  resumes the SAME window, an overlap-skipped tick costs nothing, and
  the state is inspectable.
- let a check-failed report outrank the "Waiting for purchase..." badge
  label so a dead link cannot hide behind a calm wait for ten minutes;
  the failure label holds steady across poll ticks.
- start the watch only when openExternalLink actually opened the page,
  and fire the first check immediately instead of at t+20s.
- give the watch a single owner (the board screen passes ownsWatch) so
  the useDeviceConnect instance no longer double-polls the same flag.
- product-neutral panel copy: "OpenPLC checks periodically...".
- slice tests arm the watch before asserting the resets, so the two
  reset assertions can actually fail (verified by mutation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uUH3ZL5ehreMUf2dtanWD
…nt claims

Addresses the review on #1009 / #670.

Stop the emulator from a `finally` (shared surface, mirrors openplc-web).
Sequencing `simulator.stop()` after a plain `await debugSession.stopSession()`
left the emulator running whenever the teardown rejected — control jumped to the
catch, which only logs. Reproduced on web by injecting a rejecting
`debugger.disconnect`: emulator still running, debug panel up on frozen values,
`simulatorRunning` stuck true, every retry failing identically. Nothing settled,
which is worse than the bug this branch fixes. The nearest trigger is a throwing
`onDisconnected` subscriber, which this repo's debugger adapter re-invokes inside
its own catch — the second throw escapes.

Clear `debugSessionRidesDeviceRef` on the manual Stop path (shared surface).
`stopSession()` hides the debugger first, so the drop handler's
`isDebuggerVisible` gate returns before it reaches the reset.

Fix the load-firmware test so it exercises the leak its comment describes. It
threw on `fs.readFile`, which runs BEFORE `loadAndRun` — so the emulator was
never marked running and the assertion passed only because `stopSimulator()` is
unconditional. It would have stayed green if the real post-`loadAndRun` leak
regressed. `fs/promises` is now stubbed so the read succeeds and `loadAndRun`
throws, putting the throw where the leak was; the read-failure case is kept as a
separate test, since the catch cannot tell the two apart. Both fail with the
`stopSimulator()` call removed.

303 suites / 6404 tests pass. The modbus-rtu-client flake and the jest worker
teardown warning both reproduce on a clean `development`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…se-poll

feat(licensing): watch for the purchase after buy() and write the licence automatically
…d-session

fix(simulator): stop the emulator on Stop, and close its session on every stop path
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • development

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e955f082-0638-42b8-a336-015e15852850

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thiagoralves
thiagoralves merged commit 3a9ca23 into main Aug 12, 2026
34 of 39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants