Compare commits

..
Author SHA1 Message Date
Renovate Bot aa9e371b0f chore(deps): update all non-major dependencies
renovate/stability-days Updates have met minimum release age requirement
PR / lint (pull_request) Successful in 24s
PR / e2e (pull_request) Successful in 38s
PR / test (pull_request) Successful in 32s
PR / docker-build (pull_request) Successful in 44s
2026-09-03 13:02:08 +00:00
CI b8d4ab7b48 release v1.1.12 [skip ci] 2026-08-23 17:25:46 +00:00
Renovate Bot 9c6960d7c7 chore(deps): update all non-major dependencies (#47)
Push / release (push) Successful in 10s
Push / publish (push) Successful in 32s
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@vitest/coverage-v8](https://vitest.dev/guide/coverage) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8)) | [`4.1.10` → `4.1.11`](https://renovatebot.com/diffs/npm/@vitest%2fcoverage-v8/4.1.10/4.1.11) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@vitest%2fcoverage-v8/4.1.11?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vitest%2fcoverage-v8/4.1.10/4.1.11?slim=true) |
| [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) | [`5.10.1` → `5.11.0`](https://renovatebot.com/diffs/npm/fast-xml-parser/5.10.1/5.11.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/fast-xml-parser/5.11.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/fast-xml-parser/5.10.1/5.11.0?slim=true) |
| [happy-dom](https://github.com/capricorn86/happy-dom) | [`20.11.2` → `20.11.6`](https://renovatebot.com/diffs/npm/happy-dom/20.11.2/20.11.6) | ![age](https://developer.mend.io/api/mc/badges/age/npm/happy-dom/20.11.6?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/happy-dom/20.11.2/20.11.6?slim=true) |
| [hls.js](https://github.com/video-dev/hls.js) | [`1.7.0` → `1.7.1`](https://renovatebot.com/diffs/npm/hls.js/1.7.0/1.7.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/hls.js/1.7.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/hls.js/1.7.0/1.7.1?slim=true) |
| [mediasoup](https://mediasoup.org) ([source](https://github.com/versatica/mediasoup)) | [`3.24.2` → `3.26.0`](https://renovatebot.com/diffs/npm/mediasoup/3.24.2/3.26.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/mediasoup/3.26.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/mediasoup/3.24.2/3.26.0?slim=true) |
| [openid-client](https://github.com/panva/openid-client) | [`6.8.5` → `6.8.7`](https://renovatebot.com/diffs/npm/openid-client/6.8.5/6.8.7) | ![age](https://developer.mend.io/api/mc/badges/age/npm/openid-client/6.8.7?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/openid-client/6.8.5/6.8.7?slim=true) |
| [vitest](https://vitest.dev) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest)) | [`4.1.10` → `4.1.11`](https://renovatebot.com/diffs/npm/vitest/4.1.10/4.1.11) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vitest/4.1.11?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vitest/4.1.10/4.1.11?slim=true) |

---

### Release Notes

<details>
<summary>vitest-dev/vitest (@&#8203;vitest/coverage-v8)</summary>

### [`v4.1.11`](https://github.com/vitest-dev/vitest/releases/tag/v4.1.11)

[Compare Source](https://github.com/vitest-dev/vitest/compare/v4.1.10...v4.1.11)

#####    🐞 Bug Fixes

- Revive global concurrency limit for test lifecycle \[backport to v4]  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) and [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;10992](https://github.com/vitest-dev/vitest/issues/10992) [<samp>(5146d)</samp>](https://github.com/vitest-dev/vitest/commit/5146df80b)
- **browser**:
  - Encode iframeId in tester iframe URL \[backport to v4]  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va), **Pduhard** and **Claude Opus 4.8** in [#&#8203;10955](https://github.com/vitest-dev/vitest/issues/10955) [<samp>(10b2c)</samp>](https://github.com/vitest-dev/vitest/commit/10b2cd201)
  - Trigger playwright/chromium gc on lower disk availability \[backport to v4]  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa), **Hiroshi Ogawa** and **OpenCode** in [#&#8203;10951](https://github.com/vitest-dev/vitest/issues/10951) [<samp>(9851d)</samp>](https://github.com/vitest-dev/vitest/commit/9851dbc41)
- **mocker**:
  - Restrict redirect mocks to the fs allowlist \[backport to v4]  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;10974](https://github.com/vitest-dev/vitest/issues/10974) [<samp>(fe5a1)</samp>](https://github.com/vitest-dev/vitest/commit/fe5a11d3c)

#####     [View changes on GitHub](https://github.com/vitest-dev/vitest/compare/v4.1.10...v4.1.11)

</details>

<details>
<summary>NaturalIntelligence/fast-xml-parser (fast-xml-parser)</summary>

### [`v5.11.0`](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.11.0)

[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.10.1...v5.11.0)

#### What's Changed

- add support for endIndex in node metadata (5.x edition) by [@&#8203;Wain-PC](https://github.com/Wain-PC) in [#&#8203;850](https://github.com/NaturalIntelligence/fast-xml-parser/pull/850)
- fix: don't crash on a closing tag with no matching opening tag by [@&#8203;hdimer](https://github.com/hdimer) in [#&#8203;861](https://github.com/NaturalIntelligence/fast-xml-parser/pull/861)

#### New Contributors

- [@&#8203;Wain-PC](https://github.com/Wain-PC) made their first contribution in [#&#8203;850](https://github.com/NaturalIntelligence/fast-xml-parser/pull/850)
- [@&#8203;hdimer](https://github.com/hdimer) made their first contribution in [#&#8203;861](https://github.com/NaturalIntelligence/fast-xml-parser/pull/861)

**Full Changelog**: <https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.10.1...v5.11.0>

</details>

<details>
<summary>capricorn86/happy-dom (happy-dom)</summary>

### [`v20.11.6`](https://github.com/capricorn86/happy-dom/releases/tag/v20.11.6)

[Compare Source](https://github.com/capricorn86/happy-dom/compare/v20.11.5...v20.11.6)

##### :construction\_worker\_man: Patch fixes

- Updates docs for the global-registrator package - By **[@&#8203;capricorn86](https://github.com/capricorn86)** in task [#&#8203;2300](https://github.com/capricorn86/happy-dom/issues/2300)

### [`v20.11.5`](https://github.com/capricorn86/happy-dom/releases/tag/v20.11.5)

[Compare Source](https://github.com/capricorn86/happy-dom/compare/v20.11.4...v20.11.5)

##### :construction\_worker\_man: Patch fixes

- Allow explicit element types for querySelector (e.g. `querySelector<HTMLInputElement>(".my-input")`) - By **[@&#8203;cyphercodes](https://github.com/cyphercodes)**

### [`v20.11.4`](https://github.com/capricorn86/happy-dom/releases/tag/v20.11.4)

[Compare Source](https://github.com/capricorn86/happy-dom/compare/v20.11.3...v20.11.4)

##### :construction\_worker\_man: Patch fixes

- Fixes the CORS check `fetch()` to match origins instead of host and protocol - By **[@&#8203;rexxars](https://github.com/rexxars)** in task [#&#8203;1490](https://github.com/capricorn86/happy-dom/issues/1490)

### [`v20.11.3`](https://github.com/capricorn86/happy-dom/releases/tag/v20.11.3)

[Compare Source](https://github.com/capricorn86/happy-dom/compare/v20.11.2...v20.11.3)

##### :construction\_worker\_man: Patch fixes

- Make document.links return a live HTMLCollection - By **[@&#8203;bangseongbeom](https://github.com/bangseongbeom)** in task [#&#8203;2299](https://github.com/capricorn86/happy-dom/issues/2299)
- Copy labels array to prevent mutation of cached querySelectorAll result - By **[@&#8203;mixelburg](https://github.com/mixelburg)** in task [#&#8203;2226](https://github.com/capricorn86/happy-dom/issues/2226)

</details>

<details>
<summary>video-dev/hls.js (hls.js)</summary>

### [`v1.7.1`](https://github.com/video-dev/hls.js/releases/tag/v1.7.1)

[Compare Source](https://github.com/video-dev/hls.js/compare/v1.7.0...v1.7.1)

### Summary

HLS.js v1.7.1 includes bug fixes and improvements over the previous release.

#### Changes Since The Last Release

- Fix Interstitial snap-out at live edge and `BUFFER_APPEND_NO_PROGRESS` false positives ([#&#8203;7979](https://github.com/video-dev/hls.js/issues/7979)) [@&#8203;robwalch](https://github.com/robwalch)
- Workaround issue where `ManagedMediaSource` does not emit "startstreaming" when seeking ([#&#8203;7984](https://github.com/video-dev/hls.js/issues/7984))
- Fix permanent stall loading fragment-hint parts of encrypted low-latency streams ([#&#8203;7976](https://github.com/video-dev/hls.js/issues/7976)) [@&#8203;zaki699-blip](https://github.com/zaki699-blip)
- Document decode timebase change in MIGRATING ([#&#8203;7986](https://github.com/video-dev/hls.js/issues/7986)) [@&#8203;robwalch](https://github.com/robwalch)

#### Demo Page

<https://26ea065a.hls-js-dev.pages.dev/demo/>

#### API and Breaking Changes

No public exports were removed and no runtime behavior changes are required to upgrade from v1.6 to v1.7. TypeScript consumers might see new compile errors where previously loose types have been narrowed. Each is listed with upgrade guidance in the migration guide:
<https://github.com/video-dev/hls.js/blob/v1.7.0/MIGRATING.md#migrating-from-hlsjs-16-to-17>

Some exported type dependencies ("eventemitter3", "[@&#8203;svta/cml-cmcd](https://github.com/svta/cml-cmcd)", "[@&#8203;svta/cml-utils](https://github.com/svta/cml-utils)", "[@&#8203;svta/cml-structured-field-values](https://github.com/svta/cml-structured-field-values)") have not been bundled with hls.d.ts. Please file an issue if this is blocking you from upgrading.

#### Feedback

Please provide feedback via [Issues in GitHub](https://github.com/video-dev/hls.js/issues/new/choose). For more details on how to contribute to HLS.js, see our [CONTRIBUTING guide](https://github.com/video-dev/hls.js/blob/master/CONTRIBUTING.md).

</details>

<details>
<summary>versatica/mediasoup (mediasoup)</summary>

### [`v3.26.0`](https://github.com/versatica/mediasoup/blob/HEAD/CHANGELOG.md#3260)

[Compare Source](https://github.com/versatica/mediasoup/compare/3.25.0...3.26.0)

- **Breaking change:** Simulcast and SVC: Limit temporal layer to the preferred one ([PR #&#8203;1892](https://github.com/versatica/mediasoup/pull/1892)).

### [`v3.25.0`](https://github.com/versatica/mediasoup/blob/HEAD/CHANGELOG.md#3250)

[Compare Source](https://github.com/versatica/mediasoup/compare/3.24.2...3.25.0)

- Worker: Fix undefined behavior in `RtpStreamRecv::UpdateScore()` when no packets were received ([PR #&#8203;1886](https://github.com/versatica/mediasoup/pull/1886)).
- SCTP: Fix `SackChunk::GetValidatedGapAckBlocks()` returning a bogus gap-ack-block ([PR #&#8203;1891](https://github.com/versatica/mediasoup/pull/1891)).
- Do not make generated RTCP Sender Reports depend on RTP packet arrival time ([issue #&#8203;1881](https://github.com/versatica/mediasoup/issues/1881)):
  - `RemoteClockOffsetEstimator` class ([PR #&#8203;1882](https://github.com/versatica/mediasoup/pull/1882)).
  - Prepare `RtpStream` classes for capture time based RTCP Sender Reports ([PR #&#8203;1883](https://github.com/versatica/mediasoup/pull/1883), [PR #&#8203;1888](https://github.com/versatica/mediasoup/pull/1888)).
  - `RemoteCaptureTimeEstimator` class ([PR #&#8203;1884](https://github.com/versatica/mediasoup/pull/1884)).
  - Estimate the capture instant of each received RTP packet ([PR #&#8203;1885](https://github.com/versatica/mediasoup/pull/1885)).
  - Generate RTCP Sender Reports based on the capture instant of the media rather than on the packet arrival time ([PR #&#8203;1887](https://github.com/versatica/mediasoup/pull/1887)).
  - `SimulcastProducerStreamManager`: Apply new capture time logic and fix 'abs-capture-time' rewriting ([PR #&#8203;1889](https://github.com/versatica/mediasoup/pull/1889)).

</details>

<details>
<summary>panva/openid-client (openid-client)</summary>

### [`v6.8.7`](https://github.com/panva/openid-client/blob/HEAD/CHANGELOG.md#687-2026-08-20)

[Compare Source](https://github.com/panva/openid-client/compare/v6.8.6...v6.8.7)

##### Fixes

- allow destructuring the claims helper ([38bd8c0](https://github.com/panva/openid-client/commit/38bd8c052a7e1e6d0e2beda14d35fb38b9b26d4c)), references [#&#8203;887](https://github.com/panva/openid-client/issues/887)

### [`v6.8.6`](https://github.com/panva/openid-client/blob/HEAD/CHANGELOG.md#686-2026-08-18)

[Compare Source](https://github.com/panva/openid-client/compare/v6.8.5...v6.8.6)

##### Fixes

- avoid undefined user-agent in fetchProtectedResource ([492c3c3](https://github.com/panva/openid-client/commit/492c3c36aad1ac324661b808b32bc17a35d22665)), references [#&#8203;885](https://github.com/panva/openid-client/issues/885)

</details>

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zMy4yIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19-->

Reviewed-on: https://git.keligrubb.com/keligrubb/kestrelos/pulls/47
Co-authored-by: Renovate Bot <renovate@keligrubb.com>
2026-08-23 17:25:33 +00:00
CI 068f82c0ff release v1.1.11 [skip ci] 2026-08-19 02:11:32 +00:00
Keli GrubbandMadison Grubb 1f45cc152a refactor(websocket): remove unused broadcastToSession export (#45)
Push / release (push) Successful in 11s
Push / publish (push) Successful in 32s
## Removed

- `broadcastToSession` from `server/plugins/websocket.js` — an exported but never-imported, never-called dead-code function. Its dependencies (`getSessionConnections`, `addSessionConnection`, `removeSessionConnection`) are retained since they remain actively used for per-session connection tracking within the plugin.

## Notes

- Full repository search confirms zero remaining references to `broadcastToSession`.
- ESLint passes; full test suite passes (50 files, 406 tests).

Closes #40

---------

Co-authored-by: Madison Grubb <madison@elastiflow.com>
Reviewed-on: #45
2026-08-19 02:11:19 +00:00
CI be95b6864b release v1.1.10 [skip ci] 2026-08-14 22:27:50 +00:00
Keli GrubbandMadison Grubb cfac12b699 chore(deps): update dependencies and fix security vulnerabilities (#44)
Push / release (push) Successful in 12s
Push / publish (push) Successful in 37s
## Summary

Updates all out-of-date dependencies and applies `npm audit fix` to resolve 14 security vulnerabilities (3 critical, 8 high).

### Dependency Updates

| Package | Current → Latest |
|---|---|
| `nuxt` | 4.4.8 → 4.5.2 |
| `mediasoup` | 3.20.9 → 3.24.2 |
| `mediasoup-client` | 3.21.0 → 3.22.0 |
| `hls.js` | 1.6.16 → 1.7.0 |
| `vue` | 3.5.38 → 3.5.41 |
| `vue-router` | 5.1.0 → 5.2.0 |
| `@nuxt/icon` | 2.2.3 → 2.5.0 |
| `eslint` | 10.5.0 → 10.8.1 |
| `vitest` / `@vitest/coverage-v8` | 4.1.9 → 4.1.10 |
| `ws` | 8.21.0 → 8.21.3 |
| `openid-client` | 6.8.4 → 6.8.5 |
| `fast-xml-parser` | 5.9.3 → 5.10.1 |
| `happy-dom` | 20.10.6 → 20.11.2 |
| `@playwright/test` | 1.61.1 → 1.62.1 |
| `@nuxt/eslint` | 1.16.0 → 1.17.0 |
| `@nuxt/test-utils` | 4.0.3 → 4.1.0 |
| `@iconify-json/tabler` | 1.2.35 → 1.2.38 |

### Security Audit Fixes (`npm audit fix`)

- **`@nuxt/devtools`** (critical): Unauthenticated DevTools RPC allows arbitrary command execution on the developer's host
- **`tar`** (critical): Multiple process-crash/DoS vulnerabilities via crafted tar archives
- **`esbuild`** (high): Arbitrary file read when running dev server on Windows
- **`brace-expansion`** (high): Multiple DoS vectors (memory exhaustion, process hang)
- **`flatted`** (high): Prototype pollution via `parse()`
- **`svgo`** (high): `removeScripts` plugin leaves executable scripts intact

### Verification

- All 406 tests pass (3 skipped)
- Lint clean (`eslint . --max-warnings 0`)
- `npm audit` reports 0 vulnerabilities

---------

Co-authored-by: Madison Grubb <madison@elastiflow.com>
Reviewed-on: #44
2026-08-14 22:27:36 +00:00
CI 88e520aa59 release v1.1.9 [skip ci] 2026-08-13 17:14:52 +00:00
Renovate Bot 5aa3bd8977 chore(deps): update dependency fast-xml-parser to v5.10.1 [security] (#42)
Push / release (push) Successful in 12s
Push / publish (push) Successful in 46s
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) | [`5.9.3` → `5.10.1`](https://renovatebot.com/diffs/npm/fast-xml-parser/5.9.3/5.10.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/fast-xml-parser/5.10.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/fast-xml-parser/5.9.3/5.10.1?slim=true) |

---

### fast-xml-parser: Repeated DOCTYPE declarations reset entity expansion limits
[GHSA-8r6m-32jq-jx6q](https://github.com/advisories/GHSA-8r6m-32jq-jx6q)

<details>
<summary>More information</summary>

#### Details
##### Impact
`fast-xml-parser` processes multiple "DOCTYPE" declarations within a single XML document. Each declaration passes its entities to `@nodable/entities` through `addInputEntities()`.

`addInputEntities()` resets the entity expansion counters every time it is called. An attacker can therefore insert additional DOCTYPE declarations to repeatedly reset maxTotalExpansions and maxExpandedLength during one parse operation.

This allows a crafted XML document to exceed the configured entity-expansion limits and can cause excessive CPU use, event-loop blocking, memory exhaustion, and process termination.

##### Workarounds
- Manually check if multiple DOCTYPEs are not present in input contents
- Update to v5.10.1
- Keep `processEntity` flag off

#### Severity
- CVSS Score: 8.7 / 10 (High)
- Vector String: `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N`

#### References
- [https://github.com/NaturalIntelligence/fast-xml-parser/security/advisories/GHSA-8r6m-32jq-jx6q](https://github.com/NaturalIntelligence/fast-xml-parser/security/advisories/GHSA-8r6m-32jq-jx6q)
- [https://github.com/NaturalIntelligence/fast-xml-parser/commit/4e546e03987662de5495d050b5fba26bea65383f](https://github.com/NaturalIntelligence/fast-xml-parser/commit/4e546e03987662de5495d050b5fba26bea65383f)
- [https://github.com/NaturalIntelligence/fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser)
- [https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.10.1](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.10.1)

This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-8r6m-32jq-jx6q) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)).
</details>

---

### Release Notes

<details>
<summary>NaturalIntelligence/fast-xml-parser (fast-xml-parser)</summary>

### [`v5.10.1`](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.10.1)

[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.10.0...v5.10.1)

**Full Changelog**: <https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.10.0...v5.10.1>

### [`v5.10.0`](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.10.0)

[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.9.3...v5.10.0)

#### What's Changed

- Bump actions/checkout from 6.0.3 to 7.0.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;849](https://github.com/NaturalIntelligence/fast-xml-parser/pull/849)
- Bump zizmorcore/zizmor-action from 0.5.6 to 0.5.7 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;848](https://github.com/NaturalIntelligence/fast-xml-parser/pull/848)

**Full Changelog**: <https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.9.3...v5.10.0>

</details>

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4yOC4wIiwidXBkYXRlZEluVmVyIjoiNDQuMjguMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIiwic2VjdXJpdHkiXX0=-->

Reviewed-on: #42
Co-authored-by: Renovate Bot <renovate@keligrubb.com>
2026-08-13 17:14:36 +00:00
CI 6fae069dec release v1.1.8 [skip ci] 2026-08-13 14:34:14 +00:00
Renovate Bot 2d32047ae2 chore(deps): update https://git.keligrubb.com/actions/setup-node action to v7 (#38)
Push / release (push) Successful in 11s
Push / publish (push) Successful in 36s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [https://git.keligrubb.com/actions/setup-node](https://git.keligrubb.com/actions/setup-node) | action | major | `v6` → `v7` |

---

### Release Notes

<details>
<summary>actions/setup-node (https://git.keligrubb.com/actions/setup-node)</summary>

### [`v7.0.0`](https://git.keligrubb.com/actions/setup-node/compare/v7...v7)

[Compare Source](https://git.keligrubb.com/actions/setup-node/compare/v7...v7)

### [`v7`](https://git.keligrubb.com/actions/setup-node/compare/v6.5.0...v7)

[Compare Source](https://git.keligrubb.com/actions/setup-node/compare/v6.5.0...v7)

</details>

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNjUuNCIsInVwZGF0ZWRJblZlciI6IjQzLjI2NS40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #38
Co-authored-by: Renovate Bot <renovate@keligrubb.com>
2026-08-13 14:34:00 +00:00
CI 4d835a4f82 release v1.1.7 [skip ci] 2026-08-13 14:24:18 +00:00
Renovate Bot 8c3f878636 fix(deps): update dependency supercluster to v9 (#41)
Push / release (push) Successful in 11s
Push / publish (push) Successful in 55s
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [supercluster](https://github.com/mapbox/supercluster) | [`^8.0.1` → `^9.0.0`](https://renovatebot.com/diffs/npm/supercluster/8.0.1/9.0.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/supercluster/9.0.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/supercluster/8.0.1/9.0.0?slim=true) |

---

### Release Notes

<details>
<summary>mapbox/supercluster (supercluster)</summary>

### [`v9.0.0`](https://github.com/mapbox/supercluster/releases/tag/v9.0.0)

[Compare Source](https://github.com/mapbox/supercluster/compare/v8.0.1...v9.0.0)

- Radically optimize **memory footprint and performance** — on a sample 1M points index, Supercluster now uses 94% less transient allocation, 57% lower peak heap, 65% less retained memory, and runs 23% faster. [#&#8203;258](https://github.com/mapbox/supercluster/issues/258)
- Improve internal coordinate precision by 6 bits (64 times).
- ⚠️ Breaking: hard-cap `maxZoom` at 30 (it wasn't practical to use it on higher zooms anyway).
- Add support for `MultiPoint` features. [#&#8203;263](https://github.com/mapbox/supercluster/issues/263)

</details>

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4yNy4wIiwidXBkYXRlZEluVmVyIjoiNDQuMjcuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Reviewed-on: https://git.keligrubb.com/keligrubb/kestrelos/pulls/41
Co-authored-by: Renovate Bot <renovate@keligrubb.com>
2026-08-13 14:24:05 +00:00
CI cebeee4040 release v1.1.6 [skip ci] 2026-06-24 20:55:04 +00:00
Keli GrubbandMadison Grubb bb01e9a06c Add ADS-B, AIS, and ALPR map layers with live CoT streaming (#36)
Push / release (push) Successful in 13s
Push / publish (push) Successful in 1m4s
## Summary

- **ADS-B & AIS:** OpenSky and AISStream OSINT feeds upsert into the CoT store; tactical tracks still arrive via adsbcot/aiscot on `:8089`. Map clients subscribe via `GET /api/cot/stream` (SSE) with viewport bbox filtering and Air / Surface / Team layer toggles.
- **ALPR (Flock/OSM):** Toggleable license-plate reader layer sourced from OpenStreetMap, with SQLite cache, Overpass fallback, tiled viewport fetching, and clustered markers with direction cones.
- **Map performance:** Ring-based tile selection (fixes zoom-out crash), immutable tile cache, incremental marker sync, split cluster load/query, and padded SSE bbox to reduce reconnect churn.

## Docs

- `docs/tracking.md` — ADS-B/AIS accuracy tiers, freshness, self-hosted receivers, optional OSINT API keys
- `docs/map-and-cameras.md` — ALPR layer and map behavior updates

---------

Co-authored-by: Madison Grubb <madison@elastiflow.com>
Reviewed-on: #36
2026-06-24 20:54:50 +00:00
CI a6b87305a1 release v1.1.5 [skip ci] 2026-06-21 04:26:35 +00:00
Renovate Bot 829db93065 chore(deps): update https://git.keligrubb.com/actions/checkout action to v7 (#35)
Push / release (push) Successful in 14s
Push / publish (push) Failing after 3h2m29s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [https://git.keligrubb.com/actions/checkout](https://git.keligrubb.com/actions/checkout) | action | major | `v6` → `v7` |

---

### Release Notes

<details>
<summary>actions/checkout (https://git.keligrubb.com/actions/checkout)</summary>

### [`v7.0.0`](https://git.keligrubb.com/actions/checkout/blob/HEAD/CHANGELOG.md#v700)

[Compare Source](https://git.keligrubb.com/actions/checkout/compare/v7...v7)

- Block checking out fork PR for pull\_request\_target and workflow\_run by [@&#8203;aiqiaoy](https://github.com/aiqiaoy) in [#&#8203;2454](https://github.com/actions/checkout/pull/2454)
- Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the minor-actions-dependencies group across 1 directory by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2458](https://github.com/actions/checkout/pull/2458)
- Bump flatted from 3.3.1 to 3.4.2 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2460](https://github.com/actions/checkout/pull/2460)
- Bump js-yaml from 4.1.0 to 4.2.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2461](https://github.com/actions/checkout/pull/2461)
- Bump [@&#8203;actions/core](https://github.com/actions/core) and [@&#8203;actions/tool-cache](https://github.com/actions/tool-cache) and Remove uuid by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2459](https://github.com/actions/checkout/pull/2459)
- upgrade module to esm and update dependencies by [@&#8203;aiqiaoy](https://github.com/aiqiaoy) in [#&#8203;2463](https://github.com/actions/checkout/pull/2463)
- Bump the minor-npm-dependencies group across 1 directory with 3 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2462](https://github.com/actions/checkout/pull/2462)

### [`v7`](https://git.keligrubb.com/actions/checkout/blob/HEAD/CHANGELOG.md#v700)

[Compare Source](https://git.keligrubb.com/actions/checkout/compare/v6.0.3...v7)

- Block checking out fork PR for pull\_request\_target and workflow\_run by [@&#8203;aiqiaoy](https://github.com/aiqiaoy) in [#&#8203;2454](https://github.com/actions/checkout/pull/2454)
- Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the minor-actions-dependencies group across 1 directory by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2458](https://github.com/actions/checkout/pull/2458)
- Bump flatted from 3.3.1 to 3.4.2 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2460](https://github.com/actions/checkout/pull/2460)
- Bump js-yaml from 4.1.0 to 4.2.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2461](https://github.com/actions/checkout/pull/2461)
- Bump [@&#8203;actions/core](https://github.com/actions/core) and [@&#8203;actions/tool-cache](https://github.com/actions/tool-cache) and Remove uuid by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2459](https://github.com/actions/checkout/pull/2459)
- upgrade module to esm and update dependencies by [@&#8203;aiqiaoy](https://github.com/aiqiaoy) in [#&#8203;2463](https://github.com/actions/checkout/pull/2463)
- Bump the minor-npm-dependencies group across 1 directory with 3 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2462](https://github.com/actions/checkout/pull/2462)

</details>

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yMzMuNCIsInVwZGF0ZWRJblZlciI6IjQzLjIzMy40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #35
Co-authored-by: Renovate Bot <renovate@keligrubb.com>
Co-committed-by: Renovate Bot <renovate@keligrubb.com>
2026-06-21 04:26:21 +00:00
Renovate Bot afaf305bda chore(deps): update all non-major dependencies (#32)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@vue/test-utils](https://github.com/vuejs/test-utils) | [`2.4.8` → `2.4.10`](https://renovatebot.com/diffs/npm/@vue%2ftest-utils/2.4.8/2.4.10) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@vue%2ftest-utils/2.4.10?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vue%2ftest-utils/2.4.8/2.4.10?slim=true) |
| [mediasoup](https://mediasoup.org) ([source](https://github.com/versatica/mediasoup)) | [`3.19.21` → `3.19.22`](https://renovatebot.com/diffs/npm/mediasoup/3.19.21/3.19.22) | ![age](https://developer.mend.io/api/mc/badges/age/npm/mediasoup/3.19.22?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/mediasoup/3.19.21/3.19.22?slim=true) |
| [mediasoup-client](https://mediasoup.org) ([source](https://github.com/versatica/mediasoup-client)) | [`3.19.0` → `3.20.0`](https://renovatebot.com/diffs/npm/mediasoup-client/3.19.0/3.20.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/mediasoup-client/3.20.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/mediasoup-client/3.19.0/3.20.0?slim=true) |
| [nuxt](https://nuxt.com) ([source](https://github.com/nuxt/nuxt/tree/HEAD/packages/nuxt)) | [`4.4.2` → `4.4.4`](https://renovatebot.com/diffs/npm/nuxt/4.4.2/4.4.4) | ![age](https://developer.mend.io/api/mc/badges/age/npm/nuxt/4.4.4?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/nuxt/4.4.2/4.4.4?slim=true) |
| [openid-client](https://github.com/panva/openid-client) | [`6.8.3` → `6.8.4`](https://renovatebot.com/diffs/npm/openid-client/6.8.3/6.8.4) | ![age](https://developer.mend.io/api/mc/badges/age/npm/openid-client/6.8.4?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/openid-client/6.8.3/6.8.4?slim=true) |

---

### Release Notes

<details>
<summary>vuejs/test-utils (@&#8203;vue/test-utils)</summary>

### [`v2.4.10`](https://github.com/vuejs/test-utils/compare/v2.4.9...v2.4.10)

[Compare Source](https://github.com/vuejs/test-utils/compare/v2.4.9...v2.4.10)

### [`v2.4.9`](https://github.com/vuejs/test-utils/releases/tag/v2.4.9)

[Compare Source](https://github.com/vuejs/test-utils/compare/v2.4.8...v2.4.9)

[compare changes](https://github.com/vuejs/test-utils/compare/v2.4.8...v2.4.9)

##### 🩹 Fixes

- Tolerate duplicate attachTo cleanup ([#&#8203;2830](https://github.com/vuejs/test-utils/pull/2830))

##### 📖 Documentation

- Document release process ([#&#8203;2834](https://github.com/vuejs/test-utils/pull/2834))

##### 🏡 Chore

- Migrate renovate config ([5d37934](https://github.com/vuejs/test-utils/commit/5d37934))

##### 🤖 CI

- Pin github actions to commit hashes ([75dcef3](https://github.com/vuejs/test-utils/commit/75dcef3))

##### ❤️ Contributors

- Cédric Exbrayat ([@&#8203;cexbrayat](https://github.com/cexbrayat))
- Daniel Roe ([@&#8203;danielroe](https://github.com/danielroe))

</details>

<details>
<summary>versatica/mediasoup (mediasoup)</summary>

### [`v3.19.22`](https://github.com/versatica/mediasoup/blob/HEAD/CHANGELOG.md#31922)

[Compare Source](https://github.com/versatica/mediasoup/compare/3.19.21...3.19.22)

- Node: Avoid "worker died" event when the Node application is closed via signal without calling `worker.close()` ([PR #&#8203;1788](https://github.com/versatica/mediasoup/pull/1788)).

</details>

<details>
<summary>versatica/mediasoup-client (mediasoup-client)</summary>

### [`v3.20.0`](https://github.com/versatica/mediasoup-client/compare/3.19.0...3.20.0)

[Compare Source](https://github.com/versatica/mediasoup-client/compare/3.19.0...3.20.0)

</details>

<details>
<summary>nuxt/nuxt (nuxt)</summary>

### [`v4.4.4`](https://github.com/nuxt/nuxt/compare/v4.4.2...v4.4.4)

[Compare Source](https://github.com/nuxt/nuxt/compare/v4.4.2...v4.4.4)

</details>

<details>
<summary>panva/openid-client (openid-client)</summary>

### [`v6.8.4`](https://github.com/panva/openid-client/blob/HEAD/CHANGELOG.md#684-2026-04-27)

[Compare Source](https://github.com/panva/openid-client/compare/v6.8.3...v6.8.4)

##### Fixes

- apply optional non-repudiation on generic grant ID Tokens ([6202888](https://github.com/panva/openid-client/commit/62028884943a1987eb1ddedc9efc01953189a14f))
- filter jwe decryption keys by algorithm ([34e2ffd](https://github.com/panva/openid-client/commit/34e2ffdea5b3b5afffeff55208aaad1d3b42d0fb))
- preserve poll abort signals on requests ([96a2d17](https://github.com/panva/openid-client/commit/96a2d176b982196f432990ef1f64978cb7cb881c))
- retry dpop nonce errors for generic grants ([498c4d9](https://github.com/panva/openid-client/commit/498c4d9c098340ad2f8b9dbb91bfe7f1166a3fd4))

</details>

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNTAuMCIsInVwZGF0ZWRJblZlciI6IjQzLjE1MC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #32
Co-authored-by: Renovate Bot <renovate@keligrubb.com>
Co-committed-by: Renovate Bot <renovate@keligrubb.com>
2026-05-04 15:05:24 +00:00
CI 2a48715ea8 release v1.1.4 [skip ci] 2026-04-29 20:24:17 +00:00
Renovate Bot 9d34f5f7c5 chore(deps): update all non-major dependencies (#31)
Push / release (push) Successful in 30s
Push / publish (push) Failing after 3h9m29s
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@vitest/coverage-v8](https://vitest.dev/guide/coverage) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8)) | [`4.1.4` → `4.1.5`](https://renovatebot.com/diffs/npm/@vitest%2fcoverage-v8/4.1.4/4.1.5) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@vitest%2fcoverage-v8/4.1.5?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vitest%2fcoverage-v8/4.1.4/4.1.5?slim=true) |
| [@vue/test-utils](https://github.com/vuejs/test-utils) | [`2.4.6` → `2.4.8`](https://renovatebot.com/diffs/npm/@vue%2ftest-utils/2.4.6/2.4.8) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@vue%2ftest-utils/2.4.8?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vue%2ftest-utils/2.4.6/2.4.8?slim=true) |
| [eslint](https://eslint.org) ([source](https://github.com/eslint/eslint)) | [`10.2.0` → `10.2.1`](https://renovatebot.com/diffs/npm/eslint/10.2.0/10.2.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/eslint/10.2.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint/10.2.0/10.2.1?slim=true) |
| [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) | [`5.6.0` → `5.7.2`](https://renovatebot.com/diffs/npm/fast-xml-parser/5.6.0/5.7.2) | ![age](https://developer.mend.io/api/mc/badges/age/npm/fast-xml-parser/5.7.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/fast-xml-parser/5.6.0/5.7.2?slim=true) |
| [mediasoup](https://mediasoup.org) ([source](https://github.com/versatica/mediasoup)) | [`3.19.19` → `3.19.21`](https://renovatebot.com/diffs/npm/mediasoup/3.19.19/3.19.21) | ![age](https://developer.mend.io/api/mc/badges/age/npm/mediasoup/3.19.21?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/mediasoup/3.19.19/3.19.21?slim=true) |
| [mediasoup-client](https://mediasoup.org) ([source](https://github.com/versatica/mediasoup-client)) | [`3.18.8` → `3.19.0`](https://renovatebot.com/diffs/npm/mediasoup-client/3.18.8/3.19.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/mediasoup-client/3.19.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/mediasoup-client/3.18.8/3.19.0?slim=true) |
| [vitest](https://vitest.dev) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest)) | [`4.1.4` → `4.1.5`](https://renovatebot.com/diffs/npm/vitest/4.1.4/4.1.5) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vitest/4.1.5?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vitest/4.1.4/4.1.5?slim=true) |
| [vue](https://vuejs.org/) ([source](https://github.com/vuejs/core)) | [`3.5.32` → `3.5.33`](https://renovatebot.com/diffs/npm/vue/3.5.32/3.5.33) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vue/3.5.33?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue/3.5.32/3.5.33?slim=true) |
| [vue-router](https://router.vuejs.org) ([source](https://github.com/vuejs/router)) | [`5.0.4` → `5.0.6`](https://renovatebot.com/diffs/npm/vue-router/5.0.4/5.0.6) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vue-router/5.0.6?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue-router/5.0.4/5.0.6?slim=true) |

---

### Release Notes

<details>
<summary>vitest-dev/vitest (@&#8203;vitest/coverage-v8)</summary>

### [`v4.1.5`](https://github.com/vitest-dev/vitest/releases/tag/v4.1.5)

[Compare Source](https://github.com/vitest-dev/vitest/compare/v4.1.4...v4.1.5)

#####    🚀 Experimental Features

- **coverage**: Istanbul to support `instrumenter` option  -  by [@&#8203;BartWaardenburg](https://github.com/BartWaardenburg) and [@&#8203;AriPerkkio](https://github.com/AriPerkkio) in [#&#8203;10119](https://github.com/vitest-dev/vitest/issues/10119) [<samp>(0e0ff)</samp>](https://github.com/vitest-dev/vitest/commit/0e0ff41c7)

#####    🐞 Bug Fixes

- \--project negation excludes browser instances  -  by [@&#8203;felamaslen](https://github.com/felamaslen) in [#&#8203;10131](https://github.com/vitest-dev/vitest/issues/10131) [<samp>(9423d)</samp>](https://github.com/vitest-dev/vitest/commit/9423dc084)
- Project color label on html reporter  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;10142](https://github.com/vitest-dev/vitest/issues/10142) [<samp>(596f7)</samp>](https://github.com/vitest-dev/vitest/commit/596f73986)
- Fix `vi.defineHelper` called as object method  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;10163](https://github.com/vitest-dev/vitest/issues/10163) [<samp>(122c2)</samp>](https://github.com/vitest-dev/vitest/commit/122c25b5b)
- Alias `agent` reporter to `minimal`  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;10157](https://github.com/vitest-dev/vitest/issues/10157) [<samp>(663b9)</samp>](https://github.com/vitest-dev/vitest/commit/663b99fe3)
- Respect diff config options in soft assertions  -  by [@&#8203;Copilot](https://github.com/Copilot), **sheremet-va** and [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;8696](https://github.com/vitest-dev/vitest/issues/8696) [<samp>(9787d)</samp>](https://github.com/vitest-dev/vitest/commit/9787dedad)
- Respect diff config options in soft assertions "  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;8696](https://github.com/vitest-dev/vitest/issues/8696) [<samp>(7dc6d)</samp>](https://github.com/vitest-dev/vitest/commit/7dc6d54fd)
- **ast-collect**: Recognize \_*vi\_import* prefix in static test discovery  -  by [@&#8203;Yejneshwar](https://github.com/Yejneshwar) in [#&#8203;10129](https://github.com/vitest-dev/vitest/issues/10129) [<samp>(32546)</samp>](https://github.com/vitest-dev/vitest/commit/325463ab2)
- **coverage**: Descriptive error message when reports directory is removed during test run  -  by [@&#8203;DaveT1991](https://github.com/DaveT1991) and [@&#8203;AriPerkkio](https://github.com/AriPerkkio) in [#&#8203;10117](https://github.com/vitest-dev/vitest/issues/10117) [<samp>(14133)</samp>](https://github.com/vitest-dev/vitest/commit/1413382e1)
- **snapshot**: Increase default snapshot max output length  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) and **Codex** in [#&#8203;10150](https://github.com/vitest-dev/vitest/issues/10150) [<samp>(21e66)</samp>](https://github.com/vitest-dev/vitest/commit/21e66ff63)
- **ui**: Fix jsx/tsx syntax highlight  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;10152](https://github.com/vitest-dev/vitest/issues/10152) [<samp>(f1b1f)</samp>](https://github.com/vitest-dev/vitest/commit/f1b1f6c7b)
- **web-worker**: Support MessagePort objects referenced inside postMessage data  -  by [@&#8203;whitphx](https://github.com/whitphx) and **Claude Opus 4.6 (1M context)** in [#&#8203;9927](https://github.com/vitest-dev/vitest/issues/9927) and [#&#8203;10124](https://github.com/vitest-dev/vitest/issues/10124) [<samp>(7ad7d)</samp>](https://github.com/vitest-dev/vitest/commit/7ad7d39af)
- **api**: Make test-specification options writable  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;10154](https://github.com/vitest-dev/vitest/issues/10154) [<samp>(6abd5)</samp>](https://github.com/vitest-dev/vitest/commit/6abd557b7)

#####     [View changes on GitHub](https://github.com/vitest-dev/vitest/compare/v4.1.4...v4.1.5)

</details>

<details>
<summary>vuejs/test-utils (@&#8203;vue/test-utils)</summary>

### [`v2.4.8`](https://github.com/vuejs/test-utils/releases/tag/v2.4.8)

[Compare Source](https://github.com/vuejs/test-utils/compare/v2.4.7...v2.4.8)

[compare changes](https://github.com/vuejs/test-utils/compare/v2.4.7...v2.4.8)

##### 🩹 Fixes

- Correct declaration entrypoint ([#&#8203;2826](https://github.com/vuejs/test-utils/pull/2826))

##### 🤖 CI

- Enable pkg.pr.new ([#&#8203;2827](https://github.com/vuejs/test-utils/pull/2827))

##### ❤️ Contributors

- Cédric Exbrayat ([@&#8203;cexbrayat](https://github.com/cexbrayat))
- Daniel Roe ([@&#8203;danielroe](https://github.com/danielroe))

### [`v2.4.7`](https://github.com/vuejs/test-utils/releases/tag/v2.4.7)

[Compare Source](https://github.com/vuejs/test-utils/compare/v2.4.6...v2.4.7)

[compare changes](https://github.com/vuejs/test-utils/compare/v2.4.6...v2.4.7)

##### 🚀 Enhancements

- Add Chinese docs translation ([#&#8203;2552](https://github.com/vuejs/test-utils/pull/2552))
- SetData()/shallowMount with initialData for components using the Composition API / <script setup> ([#&#8203;2655](https://github.com/vuejs/test-utils/pull/2655))

##### 🩹 Fixes

- Preserve code from keyboard events ([#&#8203;2434](https://github.com/vuejs/test-utils/pull/2434))
- Switch browser and require exports definitions ([#&#8203;2501](https://github.com/vuejs/test-utils/pull/2501))
- Re-add peer dependencies but with wider range ([#&#8203;2511](https://github.com/vuejs/test-utils/pull/2511))
- Resolve warnings in docs:dev ([30b7491](https://github.com/vuejs/test-utils/commit/30b7491))
- Resolve TypeScript type errors in .vitepress/config ([#&#8203;2549](https://github.com/vuejs/test-utils/pull/2549))
- Accept FunctionalComponent<any> as selector ([0bb947f](https://github.com/vuejs/test-utils/commit/0bb947f))
- Text() misses content for array functional component ([#&#8203;2579](https://github.com/vuejs/test-utils/pull/2579))
- Use await in test ([c5482b4](https://github.com/vuejs/test-utils/commit/c5482b4))
- **deps:** Update dependency vue-component-type-helpers to v3 ([#&#8203;2683](https://github.com/vuejs/test-utils/pull/2683))
- Remove wrapper div when unmount ([#&#8203;2700](https://github.com/vuejs/test-utils/pull/2700))
- Make mount options slots compatible with noUncheckedIndexedAccess true ([#&#8203;2713](https://github.com/vuejs/test-utils/pull/2713))
- Add missing peerDependency [@&#8203;vue/compiler-dom](https://github.com/vue/compiler-dom) ([75801ba](https://github.com/vuejs/test-utils/commit/75801ba))
- **docs:** Declare css module for vitepress typecheck ([ddaca97](https://github.com/vuejs/test-utils/commit/ddaca97))

##### 💅 Refactors

- Enforce consistent usage of type imports ([#&#8203;2734](https://github.com/vuejs/test-utils/pull/2734))

##### 📖 Documentation

- Clarify findComponent vs getComponent ([#&#8203;2435](https://github.com/vuejs/test-utils/pull/2435))
- Update fr docs ([67064ef](https://github.com/vuejs/test-utils/commit/67064ef))
- Add note about partial transition stub support ([#&#8203;2431](https://github.com/vuejs/test-utils/pull/2431))
- Fix missing data at passing data section essentials guide ([dda205e](https://github.com/vuejs/test-utils/commit/dda205e))
- Fix missing data at passing data section essentials guide fr ([ae2c72c](https://github.com/vuejs/test-utils/commit/ae2c72c))
- Fix plugin TS declaration example ([#&#8203;2466](https://github.com/vuejs/test-utils/pull/2466))
- Fixed incorrect checkbox value check ([#&#8203;2495](https://github.com/vuejs/test-utils/pull/2495))
- Capital letter in sentence fix ([#&#8203;2499](https://github.com/vuejs/test-utils/pull/2499))
- Import missing DOMWrapper on Implementation of the plugin section ([#&#8203;2519](https://github.com/vuejs/test-utils/pull/2519))
- Add migration step for deprecated ref syntax in findAllComponents ([#&#8203;2498](https://github.com/vuejs/test-utils/pull/2498))
- Correct anchor hash links and fix typo ([#&#8203;2551](https://github.com/vuejs/test-utils/pull/2551))
- Center logo on home ([#&#8203;2559](https://github.com/vuejs/test-utils/pull/2559))
- **zh-cn:** Review a-crash-course ([#&#8203;2563](https://github.com/vuejs/test-utils/pull/2563))
- Use code-group for install commands ([#&#8203;2571](https://github.com/vuejs/test-utils/pull/2571))
- **zh-cn:** Review event-handing.md ([#&#8203;2572](https://github.com/vuejs/test-utils/pull/2572))
- **zh-cn:** Enhance conditional-rendering.md ([#&#8203;2562](https://github.com/vuejs/test-utils/pull/2562))
- **zh-cn:** Review easy-to-test ([#&#8203;2567](https://github.com/vuejs/test-utils/pull/2567))
- **zh-cn:** Review passing-data.md ([#&#8203;2575](https://github.com/vuejs/test-utils/pull/2575))
- **zh-cn:** Review async-suspense.md ([#&#8203;2576](https://github.com/vuejs/test-utils/pull/2576))
- **zh:** 优化 API 文档格式和内容 ([#&#8203;2569](https://github.com/vuejs/test-utils/pull/2569))
- **zh:** 更新 Vitest 模拟日期和计时器的说明 ([#&#8203;2578](https://github.com/vuejs/test-utils/pull/2578))
- **zh-cn:** Review http-requests.md ([#&#8203;2580](https://github.com/vuejs/test-utils/pull/2580))
- **zh-cn:** Review forms ([#&#8203;2582](https://github.com/vuejs/test-utils/pull/2582))
- **zh-cn:** Guide/advanced/slots.md ([#&#8203;2565](https://github.com/vuejs/test-utils/pull/2565))
- **zh:** Review extending-vtu ([#&#8203;2583](https://github.com/vuejs/test-utils/pull/2583))
- **zh:** Review index ([#&#8203;2584](https://github.com/vuejs/test-utils/pull/2584))
- Fix modelValue test example ([85bfdf4](https://github.com/vuejs/test-utils/commit/85bfdf4))
- Removes broken link from plugins.md ([69bc1ce](https://github.com/vuejs/test-utils/commit/69bc1ce))
- **zh:** Review transitions, component-instance, and reusability-composition ([#&#8203;2616](https://github.com/vuejs/test-utils/pull/2616))
- **zh:** Review v-model and vuex ([#&#8203;2617](https://github.com/vuejs/test-utils/pull/2617))
- **zh:** Review all the rest advanced guide ([#&#8203;2619](https://github.com/vuejs/test-utils/pull/2619))
- **zh:** Review migration ([#&#8203;2623](https://github.com/vuejs/test-utils/pull/2623))
- Fix a typo in transitions.md ([#&#8203;2635](https://github.com/vuejs/test-utils/pull/2635))
- Update crash-course to script setup ([c81aa79](https://github.com/vuejs/test-utils/commit/c81aa79))
- Update Essentials section to setup (composition api) ([#&#8203;2647](https://github.com/vuejs/test-utils/pull/2647))
- Typos in examples ([#&#8203;2678](https://github.com/vuejs/test-utils/pull/2678))
- Typo in easy-to-test.md ([#&#8203;2710](https://github.com/vuejs/test-utils/pull/2710))
- Add note about mocking requestAnimationFrame for transitions ([2324c65](https://github.com/vuejs/test-utils/commit/2324c65))
- Updated example TodoApp to script setup ([#&#8203;2727](https://github.com/vuejs/test-utils/pull/2727))
- Remove "Using data" section from "Conditional Rendering" guide and fix passing data test example ([#&#8203;2743](https://github.com/vuejs/test-utils/pull/2743))
- Follow-up fixes for the conditional rendering guide ([#&#8203;2744](https://github.com/vuejs/test-utils/pull/2744))
- Mention shallowMount stub name changes in migration guide ([80e051a](https://github.com/vuejs/test-utils/commit/80e051a))
- Update conditional rendering documentation to clarify isVisible() usage with attachTo ([#&#8203;2799](https://github.com/vuejs/test-utils/pull/2799))
- Restore Options API component for data() mounting example ([#&#8203;2804](https://github.com/vuejs/test-utils/pull/2804))
- Promote Vitest as recommended test runner ([#&#8203;2805](https://github.com/vuejs/test-utils/pull/2805))
- **api:** Note that setValue does not accept objects on `<select>` ([#&#8203;2819](https://github.com/vuejs/test-utils/pull/2819))

##### 🏡 Chore

- Add api/index.md to docs:translation:compare ([6b8681c](https://github.com/vuejs/test-utils/commit/6b8681c))
- Remove unnecessary generic arguments ([cfd70c6](https://github.com/vuejs/test-utils/commit/cfd70c6))
- Ignore TS error in config object ([9d0a618](https://github.com/vuejs/test-utils/commit/9d0a618))
- Simplify eslint packages ([c1d0ffd](https://github.com/vuejs/test-utils/commit/c1d0ffd))
- Use eslint v9 with flat config ([2f19fdf](https://github.com/vuejs/test-utils/commit/2f19fdf))
- Expose Stubs type publicly ([#&#8203;2492](https://github.com/vuejs/test-utils/pull/2492))
- Update documentation file path ([9c96594](https://github.com/vuejs/test-utils/commit/9c96594))
- Use pnpm v10 ([e4c2cb3](https://github.com/vuejs/test-utils/commit/e4c2cb3))
- Pnpm approve build ([81c54e9](https://github.com/vuejs/test-utils/commit/81c54e9))
- Use github issue forms ([#&#8203;2673](https://github.com/vuejs/test-utils/pull/2673))
- Exclude class components from test type-checking ([0899008](https://github.com/vuejs/test-utils/commit/0899008))
- Add explicit coverage include for vitest v4 ([51672b9](https://github.com/vuejs/test-utils/commit/51672b9))
- Update to prettier v3.7 ([fed9e7c](https://github.com/vuejs/test-utils/commit/fed9e7c))
- Migrate to oxfmt ([81c1de9](https://github.com/vuejs/test-utils/commit/81c1de9))
- Migrate to oxlint ([a361908](https://github.com/vuejs/test-utils/commit/a361908))
- Prepare TypeScript 6 migration settings ([55e1262](https://github.com/vuejs/test-utils/commit/55e1262))
- Adjust tsd config for TypeScript 6 ([7d23eb5](https://github.com/vuejs/test-utils/commit/7d23eb5))
- Avoid TypeScript 6 target deprecation warning ([81d063c](https://github.com/vuejs/test-utils/commit/81d063c))

##### 🤖 CI

- Remove node v22 build ([7ebf58d](https://github.com/vuejs/test-utils/commit/7ebf58d))
- Add node v22 build ([57540ee](https://github.com/vuejs/test-utils/commit/57540ee))
- Use "pool: threads" instead of vmThreads ([d0cbb54](https://github.com/vuejs/test-utils/commit/d0cbb54))
- Remove node v18 and add v24 ([fd9cf95](https://github.com/vuejs/test-utils/commit/fd9cf95))
- Add trusted publishing release workflow ([#&#8203;2825](https://github.com/vuejs/test-utils/pull/2825))

##### ❤️ Contributors

- Lachlan Miller ([@&#8203;lmiller1990](https://github.com/lmiller1990))
- cexbrayat ([@&#8203;cexbrayat](https://github.com/cexbrayat))
- Nicolas Bonamy ([@&#8203;nbonamy](https://github.com/nbonamy))
- KatWorkGit ([@&#8203;KatWorkGit](https://github.com/KatWorkGit))
- Wouter Kroes ([@&#8203;wouterkroes](https://github.com/wouterkroes))
- Rama Muhammad Murshal ([@&#8203;ramammurshal](https://github.com/ramammurshal))
- Evan You ([@&#8203;yyx990803](https://github.com/yyx990803))
- Vlad Starkovsky ([@&#8203;starkovsky](https://github.com/starkovsky))
- Joe ([@&#8203;joaoprp](https://github.com/joaoprp))
- Priyadarshi Kumar ([@&#8203;Psingh132](https://github.com/Psingh132))
- Sébastien Ronveaux ([@&#8203;sronveaux](https://github.com/sronveaux))
- Gilliam ([@&#8203;Gi11i4m](https://github.com/Gi11i4m))
- Baranov Dmytro ([@&#8203;dimas7001](https://github.com/dimas7001))
- BrendonHenrique ([@&#8203;BrendonHenrique](https://github.com/BrendonHenrique))
- Lorenz van Herwaarden ([@&#8203;lorenzvanherwaarden](https://github.com/lorenzvanherwaarden))
- wuzhiqing ([@&#8203;DDDDD12138](https://github.com/DDDDD12138))
- 阿菜 Cai ([@&#8203;RSS1102](https://github.com/RSS1102))
- Jinjiang ([@&#8203;Jinjiang](https://github.com/Jinjiang))
- Kylin ([@&#8203;lxKylin](https://github.com/lxKylin))
- Qianhe Chen ([@&#8203;chenqianhe](https://github.com/chenqianhe))
- 时瑶 ([@&#8203;KiritaniAyaka](https://github.com/KiritaniAyaka))
- h7ml ([@&#8203;h7ml](https://github.com/h7ml))
- Nicander ([@&#8203;Nicander93](https://github.com/Nicander93))
- Take-John ([@&#8203;takejohn](https://github.com/takejohn))
- ilyasherstoboev ([@&#8203;ilyasherstoboev](https://github.com/ilyasherstoboev))
- aimerie ([@&#8203;aimerie](https://github.com/aimerie))
- Miguel Rincon ([@&#8203;miguelrincon](https://github.com/miguelrincon))
- bcastlel ([@&#8203;bcastlel](https://github.com/bcastlel))
- Claudiu ([@&#8203;sofuxro](https://github.com/sofuxro))
- Artem Dragunov ([@&#8203;dragunovartem99](https://github.com/dragunovartem99))
- Robin ([@&#8203;OrbisK](https://github.com/OrbisK))
- Koen Mertens ([@&#8203;KCMertens](https://github.com/KCMertens))
- meomking ([@&#8203;CaptainWang98](https://github.com/CaptainWang98))
- Pepijn Olivier ([@&#8203;pepijnolivier](https://github.com/pepijnolivier))
- Tomina ([@&#8203;Thomaash](https://github.com/Thomaash))
- Gareth Jones ([@&#8203;G-Rath](https://github.com/G-Rath))
- Jerry Hogan ([@&#8203;hdJerry](https://github.com/hdJerry))
- Marco Pasqualetti ([@&#8203;marcalexiei](https://github.com/marcalexiei))
- guoxk ([@&#8203;guoxk-me](https://github.com/guoxk-me))
- kimulaco ([@&#8203;kimulaco](https://github.com/kimulaco))
- Erwan IQUEL ([@&#8203;Olympus5](https://github.com/Olympus5))
- Matt Van Horn ([@&#8203;mvanhorn](https://github.com/mvanhorn))
- Daniel Roe ([@&#8203;danielroe](https://github.com/danielroe))

</details>

<details>
<summary>eslint/eslint (eslint)</summary>

### [`v10.2.1`](https://github.com/eslint/eslint/releases/tag/v10.2.1)

[Compare Source](https://github.com/eslint/eslint/compare/v10.2.0...v10.2.1)

#### Bug Fixes

- [`14be92b`](https://github.com/eslint/eslint/commit/14be92b6d1fa0923b8923830f2208e5e2705b002) fix: model generator yield resumption paths in code path analysis ([#&#8203;20665](https://github.com/eslint/eslint/issues/20665)) (sethamus)
- [`84a19d2`](https://github.com/eslint/eslint/commit/84a19d2c32255db6b9cfc08644a607aae6d5cb62) fix: no-async-promise-executor false positives for shadowed Promise ([#&#8203;20740](https://github.com/eslint/eslint/issues/20740)) (xbinaryx)
- [`af764af`](https://github.com/eslint/eslint/commit/af764af0ec38225755fbf8a6f207f0c77b595a8d) fix: clarify language and processor validation errors ([#&#8203;20729](https://github.com/eslint/eslint/issues/20729)) (Pixel998)
- [`e251b89`](https://github.com/eslint/eslint/commit/e251b89a38280973e468a4a9386c138f4f55d10d) fix: update eslint ([#&#8203;20715](https://github.com/eslint/eslint/issues/20715)) (renovate\[bot])

#### Documentation

- [`ca92ca0`](https://github.com/eslint/eslint/commit/ca92ca0fb4599e8de1e2fb914e695fe7397cbe63) docs: reuse markdown-it instance for markdown filter ([#&#8203;20768](https://github.com/eslint/eslint/issues/20768)) (Amaresh  S M)
- [`57d2ee2`](https://github.com/eslint/eslint/commit/57d2ee213305cee0cb55ef08e0480b57396269a9) docs:  Enable Eleventy incremental mode for watch ([#&#8203;20767](https://github.com/eslint/eslint/issues/20767)) (Amaresh  S M)
- [`c1621b9`](https://github.com/eslint/eslint/commit/c1621b915742276e5f4b25efe790ca62296330dc) docs: fix typos in code-path-analyzer.js ([#&#8203;20700](https://github.com/eslint/eslint/issues/20700)) (Ayush Shukla)
- [`1418d52`](https://github.com/eslint/eslint/commit/1418d522d10bde1960f4942afb548bc7160ec49e) docs: Update README (GitHub Actions Bot)
- [`39771e6`](https://github.com/eslint/eslint/commit/39771e6e600f0b0617fdeafff6dd07e4211ffde6) docs: Update README (GitHub Actions Bot)
- [`71e0469`](https://github.com/eslint/eslint/commit/71e04693def2df57268f08f3072a2749df6bf438) docs: fix incomplete JSDoc param description in no-shadow rule ([#&#8203;20728](https://github.com/eslint/eslint/issues/20728)) (kuldeep kumar)
- [`22119ce`](https://github.com/eslint/eslint/commit/22119ceb93e28f62262fc1d98ff1b1442d6e2dbf) docs: clarify scope of for-direction rule with dead code examples ([#&#8203;20723](https://github.com/eslint/eslint/issues/20723)) (Amaresh  S M)
- [`8f3fb77`](https://github.com/eslint/eslint/commit/8f3fb77f122a5641d1833cad5d93f3f54fa3be0b) docs: document `meta.docs.dialects` ([#&#8203;20718](https://github.com/eslint/eslint/issues/20718)) (Pixel998)

#### Chores

- [`7ddfea9`](https://github.com/eslint/eslint/commit/7ddfea9c4f62add1588c5c0b0da568c299246383) chore: update dependency prettier to v3.8.2 ([#&#8203;20770](https://github.com/eslint/eslint/issues/20770)) (renovate\[bot])
- [`fac40e1`](https://github.com/eslint/eslint/commit/fac40e1de2ba7646cc7cd2d3f93fbdd1f8819001) ci: bump pnpm/action-setup from 5.0.0 to 6.0.0 ([#&#8203;20763](https://github.com/eslint/eslint/issues/20763)) (dependabot\[bot])
- [`7246f92`](https://github.com/eslint/eslint/commit/7246f923332522d8b3d46b6ee646fce88535f3fb) test: add tests for SuppressionsService.load() error handling ([#&#8203;20734](https://github.com/eslint/eslint/issues/20734)) (kuldeep kumar)
- [`4f34b1e`](https://github.com/eslint/eslint/commit/4f34b1e592b0f63d766d9903998e8e36eb49d3aa) chore: update pnpm/action-setup action to v5 ([#&#8203;20762](https://github.com/eslint/eslint/issues/20762)) (renovate\[bot])
- [`51080eb`](https://github.com/eslint/eslint/commit/51080eb5c98d619434e4835dbe9f1c6654aca3b8) test: processor service ([#&#8203;20731](https://github.com/eslint/eslint/issues/20731)) (kuldeep kumar)
- [`e7e1889`](https://github.com/eslint/eslint/commit/e7e1889fca9b6044e08f41b38df20a1ce45808c8) chore: remove stale babel-eslint10 fixture and test  ([#&#8203;20727](https://github.com/eslint/eslint/issues/20727)) (kuldeep kumar)
- [`4e1a87c`](https://github.com/eslint/eslint/commit/4e1a87cb8fb90e309524bc36bc5f31b9f9cfaa76) test: remove redundant async/await in flat config array tests ([#&#8203;20722](https://github.com/eslint/eslint/issues/20722)) (Pixel998)
- [`066eabb`](https://github.com/eslint/eslint/commit/066eabb3643b12931f991594969bcc0028f71a5f) test: add rule metadata coverage for `languages` and `docs.dialects` ([#&#8203;20717](https://github.com/eslint/eslint/issues/20717)) (Pixel998)

</details>

<details>
<summary>NaturalIntelligence/fast-xml-parser (fast-xml-parser)</summary>

### [`v5.7.2`](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.7.2): backward compatibility for numerical external entity, fix #&#8203;705, #&#8203;817

[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.7.1...v5.7.2)

- allow numerical external entity for backward compatibility
- fix [#&#8203;705](https://github.com/NaturalIntelligence/fast-xml-parser/issues/705): attributesGroupName working with preserveOrder
- fix [#&#8203;817](https://github.com/NaturalIntelligence/fast-xml-parser/issues/817): stackoverflow when tag expression is very long

### [`v5.7.1`](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.7.1): upgrade @&#8203;nodable/entities and FXB

[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.7.0...v5.7.1)

- Use `@nodable/entities` v2.1.0
  - breaking changes
    - single entity scan. You're not allowed to use entity value to form another entity name.
    - you cant add numeric external entity
    - entity error message when expantion limit is crossed might change
  - typings are updated for new options related to process entity
  - please follow documentation of `@nodable/entities` for more detail.
  - performance
    - if processEntities is false, then there should not be impact on performance.
    - if processEntities is true, but you dont pass entity decoder separately then performance may degrade by approx 8-10%
    - if processEntities is true, and you pass entity decoder separately
      - if no entity then performance should be same as before
      - if there are entities then performance should be increased from past versions
  - ignoreAttributes is not required to be set to set xml version for NCR entity value
- update 'fast-xml-builder' to sanitize malicious CDATA and comment's content

### [`v5.7.0`](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.6.0...v5.7.0)

[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.6.0...v5.7.0)

</details>

<details>
<summary>versatica/mediasoup (mediasoup)</summary>

### [`v3.19.21`](https://github.com/versatica/mediasoup/blob/HEAD/CHANGELOG.md#31921)

[Compare Source](https://github.com/versatica/mediasoup/compare/3.19.20...3.19.21)

- Worker: Fix regression in `DirectTransport` when closing a `DataProducer` or `DataConsumer` ([PR #&#8203;1780](https://github.com/versatica/mediasoup/pull/1780)).

### [`v3.19.20`](https://github.com/versatica/mediasoup/blob/HEAD/CHANGELOG.md#31920)

[Compare Source](https://github.com/versatica/mediasoup/compare/3.19.19...3.19.20)

- Worker: Add `useBuiltInSctpStack` setting (defaults to `false`) to enable mediasoup built-in SCTP stack ([PR #&#8203;1777](https://github.com/versatica/mediasoup/pull/1777)).

</details>

<details>
<summary>versatica/mediasoup-client (mediasoup-client)</summary>

### [`v3.19.0`](https://github.com/versatica/mediasoup-client/compare/3.18.8...3.19.0)

[Compare Source](https://github.com/versatica/mediasoup-client/compare/3.18.8...3.19.0)

</details>

<details>
<summary>vuejs/core (vue)</summary>

### [`v3.5.33`](https://github.com/vuejs/core/blob/HEAD/CHANGELOG.md#3533-2026-04-22)

[Compare Source](https://github.com/vuejs/core/compare/v3.5.32...v3.5.33)

##### Bug Fixes

- **compiler-sfc:** handle nested :deep in selector pseudos ([#&#8203;14725](https://github.com/vuejs/core/issues/14725)) ([bb9d265](https://github.com/vuejs/core/commit/bb9d265d8dcdde2af824fc01b24f9a7b3169f5fa)), closes [#&#8203;14724](https://github.com/vuejs/core/issues/14724)
- **reactivity:** unlink effect scopes on out-of-order off ([#&#8203;14734](https://github.com/vuejs/core/issues/14734)) ([e7659be](https://github.com/vuejs/core/commit/e7659beafc5407e892fa70f3f4ade80263b0905d)), closes [#&#8203;14733](https://github.com/vuejs/core/issues/14733)
- **runtime-dom:** preserve textarea resize dimensions ([#&#8203;14747](https://github.com/vuejs/core/issues/14747)) ([11fb2fd](https://github.com/vuejs/core/commit/11fb2fd4a246e40f6f350701dfea73ec525b4f59)), closes [#&#8203;14741](https://github.com/vuejs/core/issues/14741)
- **teleport:** don't move teleport children if not mounted ([#&#8203;14702](https://github.com/vuejs/core/issues/14702)) ([6a61f44](https://github.com/vuejs/core/commit/6a61f4452ba1a31fc929cadf8abe3337ac4d3a46)), closes [#&#8203;14701](https://github.com/vuejs/core/issues/14701)
- **transition:** preserve placeholder for conditional explicit default slots ([#&#8203;14748](https://github.com/vuejs/core/issues/14748)) ([45990ce](https://github.com/vuejs/core/commit/45990cecf4604b2f39c571ab6aefa49d362af36a)), closes [#&#8203;14727](https://github.com/vuejs/core/issues/14727)

</details>

<details>
<summary>vuejs/router (vue-router)</summary>

### [`v5.0.6`](https://github.com/vuejs/router/releases/tag/v5.0.6)

[Compare Source](https://github.com/vuejs/router/compare/v5.0.5...v5.0.6)

#####    🐞 Bug Fixes

- Missing closing quote in generated import  -  by [@&#8203;zjy040525](https://github.com/zjy040525) and [@&#8203;posva](https://github.com/posva) in [#&#8203;2688](https://github.com/vuejs/router/issues/2688) [<samp>(32f78)</samp>](https://github.com/vuejs/router/commit/32f78c77)

#####     [View changes on GitHub](https://github.com/vuejs/router/compare/v5.0.5...v5.0.6)

### [`v5.0.5`](https://github.com/vuejs/router/releases/tag/v5.0.5)

[Compare Source](https://github.com/vuejs/router/compare/v5.0.4...v5.0.5)

#####    🚀 Features

- Enable standard schema param parsers  -  by [@&#8203;posva](https://github.com/posva) [<samp>(ea8e3)</samp>](https://github.com/vuejs/router/commit/ea8e3e21)
- Normalize param parsers once  -  by [@&#8203;posva](https://github.com/posva) [<samp>(48087)</samp>](https://github.com/vuejs/router/commit/480877cc)

#####    🐞 Bug Fixes

- Track definePage imports per-file to fix named view race condition  -  by [@&#8203;posva](https://github.com/posva) [<samp>(11191)</samp>](https://github.com/vuejs/router/commit/11191bca)
- Avoid double decoding hash on string location  -  by [@&#8203;posva](https://github.com/posva) [<samp>(1578c)</samp>](https://github.com/vuejs/router/commit/1578c9e9)

#####     [View changes on GitHub](https://github.com/vuejs/router/compare/v5.0.4...v5.0.5)

</details>

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMzIuMyIsInVwZGF0ZWRJblZlciI6IjQzLjE1MC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://git.keligrubb.com/keligrubb/kestrelos/pulls/31
Co-authored-by: Renovate Bot <renovate@keligrubb.com>
Co-committed-by: Renovate Bot <renovate@keligrubb.com>
2026-04-29 20:23:47 +00:00
CI 5b4bb6b33a release v1.1.3 [skip ci] 2026-04-19 02:28:28 +00:00
Renovate Bot e16b492257 chore(deps): update dependency mediasoup-client to v3.18.8 (#30)
Push / release (push) Successful in 15s
Push / publish (push) Successful in 1m50s
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [mediasoup-client](https://mediasoup.org) ([source](https://github.com/versatica/mediasoup-client)) | [`3.18.7` → `3.18.8`](https://renovatebot.com/diffs/npm/mediasoup-client/3.18.7/3.18.8) | ![age](https://developer.mend.io/api/mc/badges/age/npm/mediasoup-client/3.18.8?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/mediasoup-client/3.18.7/3.18.8?slim=true) |

---

### Release Notes

<details>
<summary>versatica/mediasoup-client (mediasoup-client)</summary>

### [`v3.18.8`](https://github.com/versatica/mediasoup-client/compare/3.18.7...3.18.8)

[Compare Source](https://github.com/versatica/mediasoup-client/compare/3.18.7...3.18.8)

</details>

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMjkuMCIsInVwZGF0ZWRJblZlciI6IjQzLjEyOS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #30
Co-authored-by: Renovate Bot <renovate@keligrubb.com>
Co-committed-by: Renovate Bot <renovate@keligrubb.com>
2026-04-19 02:28:13 +00:00
CI 265e02119a release v1.1.2 [skip ci] 2026-04-15 04:10:14 +00:00
Renovate Bot 82d454ade4 Update dependency fast-xml-parser to v5.6.0 (#28)
Push / release (push) Successful in 23s
Push / publish (push) Successful in 1m8s
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) | [`5.5.12` → `5.6.0`](https://renovatebot.com/diffs/npm/fast-xml-parser/5.5.12/5.6.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/fast-xml-parser/5.6.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/fast-xml-parser/5.5.12/5.6.0?slim=true) |

---

### Release Notes

<details>
<summary>NaturalIntelligence/fast-xml-parser (fast-xml-parser)</summary>

### [`v5.6.0`](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.6.0): use @&#8203;nodable/entities to replace entities

[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.5.12...v5.6.0)

- No API change
- No change in performance for basic usage
- No typing change
- No config change
- new dependency
- breaking: error messages for entities might have been changed.
-

**Full Changelog**: <https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.5.12...v5.6.0>

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMjAuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEyMC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: #28
Co-authored-by: Renovate Bot <renovate@keligrubb.com>
Co-committed-by: Renovate Bot <renovate@keligrubb.com>
2026-04-15 04:09:54 +00:00
CI a9b300d711 release v1.1.1 [skip ci] 2026-04-15 03:03:37 +00:00
Keli Grubb fded3a04d4 ci: split push release/publish and harden workflows (#27)
Push / release (push) Successful in 47s
Push / publish (push) Successful in 1m0s
### Added
* Separate release from Docker/Helm publish
* enrich releases with PRbodies when available
* tighten release.sh validation and idempotency
* trim PR docker-build metadata for act-runner stability

Reviewed-on: #27
Co-authored-by: keligrubb <keligrubb324@gmail.com>
Co-committed-by: keligrubb <keligrubb324@gmail.com>
2026-04-15 03:03:04 +00:00
CI 0d897f17b5 release v1.1.0 [skip ci] 2026-04-15 02:45:02 +00:00
Renovate Bot 216f6f83fe Update all non-major dependencies (#25)
Push / release-docker-helm (push) Failing after 3m26s
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | Type | Update |
|---|---|---|---|---|---|
| [@iconify-json/tabler](https://icon-sets.iconify.design/tabler/) | [`1.2.31` → `1.2.33`](https://renovatebot.com/diffs/npm/@iconify-json%2ftabler/1.2.31/1.2.33) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@iconify-json%2ftabler/1.2.33?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@iconify-json%2ftabler/1.2.31/1.2.33?slim=true) | devDependencies | patch |
| [@nuxt/test-utils](https://github.com/nuxt/test-utils) | [`4.0.0` → `4.0.2`](https://renovatebot.com/diffs/npm/@nuxt%2ftest-utils/4.0.0/4.0.2) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@nuxt%2ftest-utils/4.0.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nuxt%2ftest-utils/4.0.0/4.0.2?slim=true) | devDependencies | patch |
| [@playwright/test](https://playwright.dev) ([source](https://github.com/microsoft/playwright)) | [`1.58.2` → `1.59.1`](https://renovatebot.com/diffs/npm/@playwright%2ftest/1.58.2/1.59.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@playwright%2ftest/1.59.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@playwright%2ftest/1.58.2/1.59.1?slim=true) | devDependencies | minor |
| [@vitest/coverage-v8](https://vitest.dev/guide/coverage) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8)) | [`4.1.0` → `4.1.4`](https://renovatebot.com/diffs/npm/@vitest%2fcoverage-v8/4.1.0/4.1.4) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@vitest%2fcoverage-v8/4.1.4?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vitest%2fcoverage-v8/4.1.0/4.1.4?slim=true) | devDependencies | patch |
| [eslint](https://eslint.org) ([source](https://github.com/eslint/eslint)) | [`10.0.3` → `10.2.0`](https://renovatebot.com/diffs/npm/eslint/10.0.3/10.2.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/eslint/10.2.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint/10.0.3/10.2.0?slim=true) | devDependencies | minor |
| [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) | [`5.5.3` → `5.5.12`](https://renovatebot.com/diffs/npm/fast-xml-parser/5.5.3/5.5.12) | ![age](https://developer.mend.io/api/mc/badges/age/npm/fast-xml-parser/5.5.12?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/fast-xml-parser/5.5.3/5.5.12?slim=true) | dependencies | patch |
| [happy-dom](https://github.com/capricorn86/happy-dom) | [`20.8.3` → `20.9.0`](https://renovatebot.com/diffs/npm/happy-dom/20.8.3/20.9.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/happy-dom/20.9.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/happy-dom/20.8.3/20.9.0?slim=true) | devDependencies | minor |
| [hls.js](https://github.com/video-dev/hls.js) | [`1.6.15` → `1.6.16`](https://renovatebot.com/diffs/npm/hls.js/1.6.15/1.6.16) | ![age](https://developer.mend.io/api/mc/badges/age/npm/hls.js/1.6.16?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/hls.js/1.6.15/1.6.16?slim=true) | dependencies | patch |
| mcr.microsoft.com/playwright | `v1.58.2-noble` → `v1.59.1-noble` | ![age](https://developer.mend.io/api/mc/badges/age/docker/mcr.microsoft.com%2fplaywright/v1.59.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/docker/mcr.microsoft.com%2fplaywright/v1.58.2/v1.59.1?slim=true) | container | minor |
| [mediasoup](https://mediasoup.org) ([source](https://github.com/versatica/mediasoup)) | [`3.19.17` → `3.19.19`](https://renovatebot.com/diffs/npm/mediasoup/3.19.17/3.19.19) | ![age](https://developer.mend.io/api/mc/badges/age/npm/mediasoup/3.19.19?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/mediasoup/3.19.17/3.19.19?slim=true) | dependencies | patch |
| [openid-client](https://github.com/panva/openid-client) | [`6.8.2` → `6.8.3`](https://renovatebot.com/diffs/npm/openid-client/6.8.2/6.8.3) | ![age](https://developer.mend.io/api/mc/badges/age/npm/openid-client/6.8.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/openid-client/6.8.2/6.8.3?slim=true) | dependencies | patch |
| [vitest](https://vitest.dev) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest)) | [`4.1.0` → `4.1.4`](https://renovatebot.com/diffs/npm/vitest/4.1.0/4.1.4) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vitest/4.1.4?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vitest/4.1.0/4.1.4?slim=true) | devDependencies | patch |
| [vue](https://github.com/vuejs/core/tree/main/packages/vue#readme) ([source](https://github.com/vuejs/core)) | [`3.5.30` → `3.5.32`](https://renovatebot.com/diffs/npm/vue/3.5.30/3.5.32) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vue/3.5.32?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue/3.5.30/3.5.32?slim=true) | dependencies | patch |
| [vue-router](https://router.vuejs.org) ([source](https://github.com/vuejs/router)) | [`5.0.3` → `5.0.4`](https://renovatebot.com/diffs/npm/vue-router/5.0.3/5.0.4) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vue-router/5.0.4?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue-router/5.0.3/5.0.4?slim=true) | dependencies | patch |
| [ws](https://github.com/websockets/ws) | [`8.19.0` → `8.20.0`](https://renovatebot.com/diffs/npm/ws/8.19.0/8.20.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/ws/8.20.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/ws/8.19.0/8.20.0?slim=true) | dependencies | minor |

---

### Release Notes

<details>
<summary>nuxt/test-utils (@&#8203;nuxt/test-utils)</summary>

### [`v4.0.2`](https://github.com/nuxt/test-utils/releases/tag/v4.0.2)

[Compare Source](https://github.com/nuxt/test-utils/compare/v4.0.1...v4.0.2)

#### 👉 Changelog

[compare changes](https://github.com/nuxt/test-utils/compare/v4.0.1...v4.0.2)

##### 🩹 Fixes

- **config:** Respect override dev value ([#&#8203;1602](https://github.com/nuxt/test-utils/pull/1602))

##### 🤖 CI

- Use pnpm publish to resolve workspace dependencies ([#&#8203;1651](https://github.com/nuxt/test-utils/pull/1651))

##### ❤️ Contributors

- Julien Huang ([@&#8203;huang-julien](https://github.com/huang-julien))
- Vasily Kuzin ([@&#8203;ExEr7um](https://github.com/ExEr7um))

### [`v4.0.1`](https://github.com/nuxt/test-utils/releases/tag/v4.0.1)

[Compare Source](https://github.com/nuxt/test-utils/compare/v4.0.0...v4.0.1)

#### 👉 Changelog

[compare changes](https://github.com/nuxt/test-utils/compare/v4.0.0...v4.0.1)

##### 🩹 Fixes

- **config:** Rename deps.optimizer.web to client for vitest4 ([#&#8203;1593](https://github.com/nuxt/test-utils/pull/1593))
- **runtime-utils:** Fix `mockNuxtImport` types when using string target ([#&#8203;1592](https://github.com/nuxt/test-utils/pull/1592))
- **config:** Pass non-project options for non-nuxt simple setup ([#&#8203;1582](https://github.com/nuxt/test-utils/pull/1582))
- **config:** Do not import `defineConfig` from `vite` ([1aa5e8748](https://github.com/nuxt/test-utils/commit/1aa5e8748))
- **runtime:** Handle `ResourceLoader` removal in jsdom v28 ([#&#8203;1611](https://github.com/nuxt/test-utils/pull/1611))
- **config,vitest-environment:** Directly import peerDeps ([#&#8203;1617](https://github.com/nuxt/test-utils/pull/1617))
- **runtime-utils:** Align mount options merge w/ vue-test-utils ([#&#8203;1610](https://github.com/nuxt/test-utils/pull/1610))
- **vitest-environment:** Avoid `vitest/environments` import warning ([#&#8203;1627](https://github.com/nuxt/test-utils/pull/1627))
- **runtime:** Avoid error when vue/test-utils is not installed ([#&#8203;1646](https://github.com/nuxt/test-utils/pull/1646))
- **config:** Prefer project h3 version if present ([#&#8203;1641](https://github.com/nuxt/test-utils/pull/1641))

##### 🏡 Chore

- Bump `vitest-environment-nuxt` versions ([f5ec72127](https://github.com/nuxt/test-utils/commit/f5ec72127))
- Use workspace dependency ([14fb254a7](https://github.com/nuxt/test-utils/commit/14fb254a7))
- Example playwright config improve type annotation for devices ([#&#8203;1581](https://github.com/nuxt/test-utils/pull/1581))
- `pkg-pr-new` prerelease `vitest-environment-nuxt` ([#&#8203;1601](https://github.com/nuxt/test-utils/pull/1601))
- Allow explicit any ([633c93c2a](https://github.com/nuxt/test-utils/commit/633c93c2a))
- Switch unit test target to dir and move type unit tests to test:types ([#&#8203;1618](https://github.com/nuxt/test-utils/pull/1618))
- Update lockfile ([8306abf00](https://github.com/nuxt/test-utils/commit/8306abf00))

#####  Tests

- Add failing test for stubbed global provide ([#&#8203;1314](https://github.com/nuxt/test-utils/pull/1314))
- Update assertions deprecated in vitest 4.1 ([#&#8203;1629](https://github.com/nuxt/test-utils/pull/1629))
- Change example/workspace to use glob based projects setup ([#&#8203;1585](https://github.com/nuxt/test-utils/pull/1585))

##### 🤖 CI

- Pin github actions to full-length commit shas ([2832fd6d5](https://github.com/nuxt/test-utils/commit/2832fd6d5))
- Avoid checkout for reproduction comment ([e4e67ab09](https://github.com/nuxt/test-utils/commit/e4e67ab09))
- Rename workflow ([99318b9fc](https://github.com/nuxt/test-utils/commit/99318b9fc))
- Correctly publish `pkg-pr-new` prerelease ([#&#8203;1598](https://github.com/nuxt/test-utils/pull/1598))

##### ❤️ Contributors

- Daniel Roe ([@&#8203;danielroe](https://github.com/danielroe))
- Yoshihiro Yamaguchi ([@&#8203;yamachi4416](https://github.com/yamachi4416))
- Robin ([@&#8203;OrbisK](https://github.com/OrbisK))
- Paul Melero ([@&#8203;paulmelero](https://github.com/paulmelero))

</details>

<details>
<summary>microsoft/playwright (@&#8203;playwright/test)</summary>

### [`v1.59.1`](https://github.com/microsoft/playwright/releases/tag/v1.59.1)

[Compare Source](https://github.com/microsoft/playwright/compare/v1.59.0...v1.59.1)

##### Bug Fixes

- **\[Windows]** Reverted hiding console window when spawning browser processes, which caused regressions including broken `codegen`, `--ui` and `show` commands ([#&#8203;39990](https://github.com/microsoft/playwright/issues/39990))

### [`v1.59.0`](https://github.com/microsoft/playwright/releases/tag/v1.59.0)

[Compare Source](https://github.com/microsoft/playwright/compare/v1.58.2...v1.59.0)

#### 🎬 Screencast

New [page.screencast](https://playwright.dev/docs/api/class-page#page-screencast) API provides a unified interface for capturing page content with:

- Screencast recordings
- Action annotations
- Visual overlays
- Real-time frame capture
- Agentic video receipts

<center>

<img src="https://raw.githubusercontent.com/microsoft/playwright/main/docs/src/images/release-notes-1.59-screencast-demo.gif" alt="Demo" width="500" height="313" />

</center>

**Screencast recording** — record video with precise start/stop control, as an alternative to the [`recordVideo`](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-video) option:

```js
await page.screencast.start({ path: 'video.webm' });
// ... perform actions ...
await page.screencast.stop();
```

**Action annotations** — enable built-in visual annotations that highlight interacted elements and display action titles during recording:

```js
await page.screencast.showActions({ position: 'top-right' });
```

[screencast.showActions()](https://playwright.dev/docs/api/class-screencast#screencast-show-actions) accepts `position` (`'top-left'`, `'top'`, `'top-right'`, `'bottom-left'`, `'bottom'`, `'bottom-right'`), `duration` (ms per annotation), and `fontSize` (px). Returns a disposable to stop showing actions.

Action annotations can also be enabled in test fixtures via the `video` option:

```js
// playwright.config.ts
export default defineConfig({
  use: {
    video: {
      mode: 'on',
      show: {
        actions: { position: 'top-left' },
        test: { position: 'top-right' },
      },
    },
  },
});
```

**Visual overlays** — add chapter titles and custom HTML overlays on top of the page for richer narration:

```js
await page.screencast.showChapter('Adding TODOs', {
  description: 'Type and press enter for each TODO',
  duration: 1000,
});

await page.screencast.showOverlay('<div style="color: red">Recording</div>');
```

**Real-time frame capture** — stream JPEG-encoded frames for custom processing like thumbnails, live previews, AI vision, and more:

```js
await page.screencast.start({
  onFrame: ({ data }) => sendToVisionModel(data),
  size: { width: 800, height: 600 },
});
```

**Agentic video receipts** — coding agents can produce video evidence of their work. After completing a task, an agent can record a walkthrough video with rich annotations for human review:

```js
await page.screencast.start({ path: 'receipt.webm' });
await page.screencast.showActions({ position: 'top-right' });

await page.screencast.showChapter('Verifying checkout flow', {
  description: 'Added coupon code support per ticket #&#8203;1234',
});

// Agent performs the verification steps...
await page.locator('#coupon').fill('SAVE20');
await page.locator('#apply-coupon').click();
await expect(page.locator('.discount')).toContainText('20%');

await page.screencast.showChapter('Done', {
  description: 'Coupon applied, discount reflected in total',
});

await page.screencast.stop();
```

The resulting video serves as a receipt: chapter titles provide context, action annotations highlight each interaction, and the visual walkthrough is faster to review than text logs.

#### 🔗 Interoperability

New [browser.bind()](https://playwright.dev/docs/api/class-browser#browser-bind) API makes a launched browser available for `playwright-cli`, `@playwright/mcp`, and other clients to connect to.

**Bind a browser** — start a browser and bind it so others can connect:

```js
const { endpoint } = await browser.bind('my-session', {
  workspaceDir: '/my/project',
});
```

**Connect from playwright-cli** — connect to the running browser from your favorite coding agent.

```bash
playwright-cli attach my-session
playwright-cli -s my-session snapshot
```

**Connect from [@&#8203;playwright/mcp](https://github.com/playwright/mcp)** — or point your MCP server to the running browser.

```bash
@&#8203;playwright/mcp --endpoint=my-session
```

**Connect from a Playwright client** — use API to connect to the browser. Multiple clients at a time are supported!

```js
const browser = await chromium.connect(endpoint);
```

Pass `host` and `port` options to bind over WebSocket instead of a named pipe:

```js
const { endpoint } = await browser.bind('my-session', {
  host: 'localhost',
  port: 0,
});
// endpoint is a ws:// URL
```

Call [browser.unbind()](https://playwright.dev/docs/api/class-browser#browser-unbind) to stop accepting new connections.

#### 📊 Observability

Run `playwright-cli show` to open the Dashboard that lists all the bound browsers, their statuses, and allows interacting with them:

- See what your agent is doing on the background browsers
- Click into the sessions for manual interventions
- Open DevTools to inspect pages from the background browsers.

<center>

<img src="https://raw.githubusercontent.com/microsoft/playwright/main/docs/src/images/release-notes-1.59-dashboard.png" alt="Demo" width="1169" height="835" />

</center>
- `playwright-cli` binds all of its browsers automatically, so you can see what your agents are doing.
- Pass `PLAYWRIGHT_DASHBOARD=1` env variable to see all `@playwright/test` browsers in the dashboard.

#### 🐛 CLI debugger for agents

Coding agents can now run `npx playwright test --debug=cli` to attach and debug tests over `playwright-cli` — perfect for automatically fixing tests in agentic workflows:

```bash
$ npx playwright test --debug=cli

### Debugging Instructions
- Run "playwright-cli attach tw-87b59e" to attach to this test

$ playwright-cli attach tw-87b59e

### Session `tw-87b59e` created, attached to `tw-87b59e`.
Run commands with: playwright-cli --session=tw-87b59e <command>

### Paused
- Navigate to "/" at output/tests/example.spec.ts:4

$ playwright-cli --session tw-87b59e step-over

### Page
- Page URL: https://playwright.dev/
- Page Title: Fast and reliable end-to-end testing for modern web apps | Playwright

### Paused
- Expect "toHaveTitle" at output/tests/example.spec.ts:7
```

#### 📋 CLI trace analysis for agents

Coding agents can run `npx playwright trace` to explore [Playwright Trace](https://playwright.dev/docs/trace-viewer) and understand failing or flaky tests from the command line:

```bash
$ npx playwright trace open test-results/example-has-title-chromium/trace.zip
  Title:        example.spec.ts:3 › has title

$ npx playwright trace actions --grep="expect"
     # Time       Action                                                  Duration
  ──── ─────────  ─────────────────────────────────────────────────────── ────────
    9. 0:00.859  Expect "toHaveTitle"                                        5.1s  ✗

$ npx playwright trace action 9
  Expect "toHaveTitle"
  Error: expect(page).toHaveTitle(expected) failed
    Expected pattern: /Wrong Title/
    Received string:  "Fast and reliable end-to-end testing for modern web apps | Playwright"
    Timeout: 5000ms
  Snapshots
    available: before, after
    usage:     npx playwright trace snapshot 9 --name <before|after>

$ npx playwright trace snapshot 9 --name after

### Page
- Page Title: Fast and reliable end-to-end testing for modern web apps | Playwright

$ npx playwright trace close
```

#### ♻️ `await using`

Many APIs now return [async disposables](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncDispose), enabling the `await using` syntax for automatic cleanup:

```js
await using page = await context.newPage();
{
  await using route = await page.route('**/*', route => route.continue());
  await using script = await page.addInitScript('console.log("init script here")');
  await page.goto('https://playwright.dev');
  // do something
}
// route and init script have been removed at this point
```

#### 🔍 Snapshots and Locators

- Method [page.ariaSnapshot()](https://playwright.dev/docs/api/class-page#page-aria-snapshot) to capture the aria snapshot of the page — equivalent to `page.locator('body').ariaSnapshot()`.
- Options `depth` and `mode` in [locator.ariaSnapshot()](https://playwright.dev/docs/api/class-locator#locator-aria-snapshot).
- Method [locator.normalize()](https://playwright.dev/docs/api/class-locator#locator-normalize) converts a locator to follow best practices like test ids and aria roles.
- Method [page.pickLocator()](https://playwright.dev/docs/api/class-page#page-pick-locator) enters an interactive mode where hovering over elements highlights them and shows the corresponding locator. Click an element to get its [Locator](https://playwright.dev/docs/api/class-locator) back. Use [page.cancelPickLocator()](https://playwright.dev/docs/api/class-page#page-cancel-pick-locator) to cancel.

#### New APIs

##### Screencast

- [page.screencast](https://playwright.dev/docs/api/class-page#page-screencast) provides video recording, real-time frame streaming, and overlay management.
- Methods [screencast.start()](https://playwright.dev/docs/api/class-screencast#screencast-start) and [screencast.stop()](https://playwright.dev/docs/api/class-screencast#screencast-stop) for recording and frame capture.
- Methods [screencast.showActions()](https://playwright.dev/docs/api/class-screencast#screencast-show-actions) and [screencast.hideActions()](https://playwright.dev/docs/api/class-screencast#screencast-hide-actions) for action annotations.
- Methods [screencast.showChapter()](https://playwright.dev/docs/api/class-screencast#screencast-show-chapter) and [screencast.showOverlay()](https://playwright.dev/docs/api/class-screencast#screencast-show-overlay) for visual overlays.
- Methods [screencast.showOverlays()](https://playwright.dev/docs/api/class-screencast#screencast-show-overlays) and [screencast.hideOverlays()](https://playwright.dev/docs/api/class-screencast#screencast-hide-overlays) for overlay visibility control.

##### Storage, Console and Errors

- Method [browserContext.setStorageState()](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-storage-state) clears existing cookies, local storage, and IndexedDB for all origins and sets a new storage state — no need to create a new context.
- Methods [page.clearConsoleMessages()](https://playwright.dev/docs/api/class-page#page-clear-console-messages) and [page.clearPageErrors()](https://playwright.dev/docs/api/class-page#page-clear-page-errors) to clear stored messages and errors.
- Option `filter` in [page.consoleMessages()](https://playwright.dev/docs/api/class-page#page-console-messages) and [page.pageErrors()](https://playwright.dev/docs/api/class-page#page-page-errors) controls which messages are returned.
- Method [consoleMessage.timestamp()](https://playwright.dev/docs/api/class-consolemessage#console-message-timestamp).

##### Miscellaneous

- [browserContext.debugger](https://playwright.dev/docs/api/class-browsercontext#browser-context-debugger) provides programmatic control over the Playwright debugger.
- Method [browserContext.isClosed()](https://playwright.dev/docs/api/class-browsercontext#browser-context-is-closed).
- Method [request.existingResponse()](https://playwright.dev/docs/api/class-request#request-existing-response) returns the response without waiting.
- Method [response.httpVersion()](https://playwright.dev/docs/api/class-response#response-http-version) returns the HTTP version used by the response.
- Events [cdpSession.on('event')](https://playwright.dev/docs/api/class-cdpsession#cdp-session-event-event) and [cdpSession.on('close')](https://playwright.dev/docs/api/class-cdpsession#cdp-session-event-close) for CDP sessions.
- Option `live` in [tracing.start()](https://playwright.dev/docs/api/class-tracing#tracing-start) for real-time trace updates.
- Option `artifactsDir` in [browserType.launch()](https://playwright.dev/docs/api/class-browsertype#browser-type-launch) to configure the artifacts directory.

#### 🛠️ Other improvements

- UI Mode has an option to only show tests affected by source changes.
- UI Mode and Trace Viewer have improved action filtering.
- HTML Reporter shows the list of runs from the same worker.
- HTML Reporter allows filtering test steps for quick search.
- New trace mode `'retain-on-failure-and-retries'` records a trace for each test run and retains all traces when an attempt fails — great for comparing a passing trace with a failing one from a flaky test.

#### Known Issues ⚠️⚠️

- `navigator.platform` emulation can cause Ctrl or Meta dispatching errors ([#&#8203;40009](https://github.com/microsoft/playwright/issues/40009)). Pass `PLAYWRIGHT_NO_UA_PLATFORM = '1'` environment variable while we are issuing a patch release. Let us know in the issue how it affected you.

#### Breaking Changes ⚠️

- Removed macOS 14 support for WebKit. We recommend upgrading your macOS version, or keeping an older Playwright version.
- Removed `@playwright/experimental-ct-svelte` package.

#### Browser Versions

- Chromium 147.0.7727.15
- Mozilla Firefox 148.0.2
- WebKit 26.4

This version was also tested against the following stable channels:

- Google Chrome 146
- Microsoft Edge 146

</details>

<details>
<summary>vitest-dev/vitest (@&#8203;vitest/coverage-v8)</summary>

### [`v4.1.4`](https://github.com/vitest-dev/vitest/releases/tag/v4.1.4)

[Compare Source](https://github.com/vitest-dev/vitest/compare/v4.1.3...v4.1.4)

#####    🚀 Experimental Features

- **coverage**:
  - Default to text reporter `skipFull` if agent detected  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;10018](https://github.com/vitest-dev/vitest/issues/10018) [<samp>(53757)</samp>](https://github.com/vitest-dev/vitest/commit/53757804c)
- **experimental**:
  - Expose `assertion` as a public field  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;10095](https://github.com/vitest-dev/vitest/issues/10095) [<samp>(a120e)</samp>](https://github.com/vitest-dev/vitest/commit/a120e3ab8)
  - Support aria snapshot  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa), **Claude Opus 4.6 (1M context)**, [@&#8203;AriPerkkio](https://github.com/AriPerkkio), **Codex** and [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;9668](https://github.com/vitest-dev/vitest/issues/9668) [<samp>(d4fbb)</samp>](https://github.com/vitest-dev/vitest/commit/d4fbb5cc9)
- **reporter**:
  - Add filterMeta option to json reporter  -  by [@&#8203;nami8824](https://github.com/nami8824) and [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;10078](https://github.com/vitest-dev/vitest/issues/10078) [<samp>(b77de)</samp>](https://github.com/vitest-dev/vitest/commit/b77de968e)

#####    🐞 Bug Fixes

- Use "black" foreground for labeled terminal message to ensure contrast  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;10076](https://github.com/vitest-dev/vitest/issues/10076) [<samp>(203f0)</samp>](https://github.com/vitest-dev/vitest/commit/203f07af7)
- Make `expect(..., message)` consistent as error message prefix  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) and **Codex** in [#&#8203;10068](https://github.com/vitest-dev/vitest/issues/10068) [<samp>(a1b5f)</samp>](https://github.com/vitest-dev/vitest/commit/a1b5f0f4f)
- Do not hoist imports whose names match class properties .  -  by [@&#8203;SunsetFi](https://github.com/SunsetFi) in [#&#8203;10093](https://github.com/vitest-dev/vitest/issues/10093) and [#&#8203;10094](https://github.com/vitest-dev/vitest/issues/10094) [<samp>(0fc4b)</samp>](https://github.com/vitest-dev/vitest/commit/0fc4b47e0)
- **browser**: Spread user server options into browser Vite server in project  -  by [@&#8203;GoldStrikeArch](https://github.com/GoldStrikeArch) in [#&#8203;10049](https://github.com/vitest-dev/vitest/issues/10049) [<samp>(65c9d)</samp>](https://github.com/vitest-dev/vitest/commit/65c9d55eb)

#####     [View changes on GitHub](https://github.com/vitest-dev/vitest/compare/v4.1.3...v4.1.4)

### [`v4.1.3`](https://github.com/vitest-dev/vitest/releases/tag/v4.1.3)

[Compare Source](https://github.com/vitest-dev/vitest/compare/v4.1.2...v4.1.3)

#####    🚀 Experimental Features

- Add `experimental.preParse` flag  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;10070](https://github.com/vitest-dev/vitest/issues/10070) [<samp>(78273)</samp>](https://github.com/vitest-dev/vitest/commit/7827363bd)
- Support `browser.locators.exact` option  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;10013](https://github.com/vitest-dev/vitest/issues/10013) [<samp>(48799)</samp>](https://github.com/vitest-dev/vitest/commit/487990a19)
- Add `TestAttachment.bodyEncoding`  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;9969](https://github.com/vitest-dev/vitest/issues/9969) [<samp>(89ca0)</samp>](https://github.com/vitest-dev/vitest/commit/89ca0e254)
- Support custom snapshot matcher  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa), **Claude Sonnet 4.6** and **Codex** in [#&#8203;9973](https://github.com/vitest-dev/vitest/issues/9973) [<samp>(59b0e)</samp>](https://github.com/vitest-dev/vitest/commit/59b0e6411)

#####    🐞 Bug Fixes

- Advance fake timers with `expect.poll` interval  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) and **Claude Sonnet 4.6** in [#&#8203;10022](https://github.com/vitest-dev/vitest/issues/10022) [<samp>(3f5bf)</samp>](https://github.com/vitest-dev/vitest/commit/3f5bfa365)
- Add `@vitest/coverage-v8` and `@vitest/coverage-istanbul` as optional dependency  -  by [@&#8203;alan-agius4](https://github.com/alan-agius4) in [#&#8203;10025](https://github.com/vitest-dev/vitest/issues/10025) [<samp>(146d4)</samp>](https://github.com/vitest-dev/vitest/commit/146d4f0a0)
- Fix `defineHelper` for webkit async stack trace + update playwright 1.59.0  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;10036](https://github.com/vitest-dev/vitest/issues/10036) [<samp>(5a5fa)</samp>](https://github.com/vitest-dev/vitest/commit/5a5fa49fe)
- Fix suite hook throwing errors for unused auto test-scoped fixture  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) and **Claude Sonnet 4.6** in [#&#8203;10035](https://github.com/vitest-dev/vitest/issues/10035) [<samp>(39865)</samp>](https://github.com/vitest-dev/vitest/commit/398657e8d)
- **expect**:
  - Remove `JestExtendError.context` from verbose error reporting  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;9983](https://github.com/vitest-dev/vitest/issues/9983) [<samp>(66751)</samp>](https://github.com/vitest-dev/vitest/commit/66751c9e8)
  - Don't leak "runner" types  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;10004](https://github.com/vitest-dev/vitest/issues/10004) [<samp>(ec204)</samp>](https://github.com/vitest-dev/vitest/commit/ec2045543)
- **snapshot**:
  - Fix flagging obsolete snapshots for snapshot properties mismatch  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) and **Claude Sonnet 4.6** in [#&#8203;9986](https://github.com/vitest-dev/vitest/issues/9986) [<samp>(6b869)</samp>](https://github.com/vitest-dev/vitest/commit/6b869156b)
  - Export custom snapshot matcher helper from `vitest`  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) and **Codex** in [#&#8203;10042](https://github.com/vitest-dev/vitest/issues/10042) [<samp>(691d3)</samp>](https://github.com/vitest-dev/vitest/commit/691d341fd)
- **ui**:
  - Don't leak vite types  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;10005](https://github.com/vitest-dev/vitest/issues/10005) [<samp>(fdff1)</samp>](https://github.com/vitest-dev/vitest/commit/fdff1bf9a)
- **vm**:
  - Fix external module resolve error with deps optimizer query  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) and **Claude Sonnet 4.6** in [#&#8203;10024](https://github.com/vitest-dev/vitest/issues/10024) [<samp>(9dbf4)</samp>](https://github.com/vitest-dev/vitest/commit/9dbf47786)

#####     [View changes on GitHub](https://github.com/vitest-dev/vitest/compare/v4.1.2...v4.1.3)

### [`v4.1.2`](https://github.com/vitest-dev/vitest/releases/tag/v4.1.2)

[Compare Source](https://github.com/vitest-dev/vitest/compare/v4.1.1...v4.1.2)

This release bumps Vitest's `flatted` version and removes version pinning to resolve `flatted`'s CVE related issues ([#&#8203;9975](https://github.com/vitest-dev/vitest/issues/9975)).

#####    🐞 Bug Fixes

- Don't resolve `setupFiles` from parent directory  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;9960](https://github.com/vitest-dev/vitest/issues/9960) [<samp>(7aa93)</samp>](https://github.com/vitest-dev/vitest/commit/7aa937776)
- Ensure sequential mock/unmock resolution  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) and **Claude Opus 4.6** in [#&#8203;9830](https://github.com/vitest-dev/vitest/issues/9830) [<samp>(7c065)</samp>](https://github.com/vitest-dev/vitest/commit/7c06598db)
- **browser**: Take failure screenshot if `toMatchScreenshot` can't capture a stable screenshot  -  by [@&#8203;macarie](https://github.com/macarie) in [#&#8203;9847](https://github.com/vitest-dev/vitest/issues/9847) [<samp>(faace)</samp>](https://github.com/vitest-dev/vitest/commit/faace1fbe)
- **coverage**: Correct `coverageConfigDefaults` values and types  -  by [@&#8203;Arthie](https://github.com/Arthie) in [#&#8203;9940](https://github.com/vitest-dev/vitest/issues/9940) [<samp>(b3c99)</samp>](https://github.com/vitest-dev/vitest/commit/b3c992cb2)
- **pretty-format**: Fix output limit over counting  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;9965](https://github.com/vitest-dev/vitest/issues/9965) [<samp>(d3b7a)</samp>](https://github.com/vitest-dev/vitest/commit/d3b7a40fa)
- Disable colors if agent is detected  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) and [@&#8203;AriPerkkio](https://github.com/AriPerkkio) in [#&#8203;9851](https://github.com/vitest-dev/vitest/issues/9851) [<samp>(6f97b)</samp>](https://github.com/vitest-dev/vitest/commit/6f97b55dd)

#####     [View changes on GitHub](https://github.com/vitest-dev/vitest/compare/v4.1.1...v4.1.2)

### [`v4.1.1`](https://github.com/vitest-dev/vitest/releases/tag/v4.1.1)

[Compare Source](https://github.com/vitest-dev/vitest/compare/v4.1.0...v4.1.1)

#####    🚀 Features

- **experimental**:
  - Expose `matchesTags` to test if the current filter matches tags  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;9913](https://github.com/vitest-dev/vitest/issues/9913) [<samp>(eec53)</samp>](https://github.com/vitest-dev/vitest/commit/eec53d9f5)
  - Introduce `experimental.vcsProvider`  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;9928](https://github.com/vitest-dev/vitest/issues/9928) [<samp>(56115)</samp>](https://github.com/vitest-dev/vitest/commit/561150036)

#####    🐞 Bug Fixes

- Mark `TestProject.testFilesList` internal properly  -  by [@&#8203;sapphi-red](https://github.com/sapphi-red) in [#&#8203;9867](https://github.com/vitest-dev/vitest/issues/9867) [<samp>(54f26)</samp>](https://github.com/vitest-dev/vitest/commit/54f2660f5)
- Detect fixture that returns without calling `use`  -  by [@&#8203;oilater](https://github.com/oilater) in [#&#8203;9831](https://github.com/vitest-dev/vitest/issues/9831) and [#&#8203;9861](https://github.com/vitest-dev/vitest/issues/9861) [<samp>(633ae)</samp>](https://github.com/vitest-dev/vitest/commit/633ae2303)
- Drop vite 8.beta support  -  by [@&#8203;AriPerkkio](https://github.com/AriPerkkio) in [#&#8203;9862](https://github.com/vitest-dev/vitest/issues/9862) [<samp>(b78f5)</samp>](https://github.com/vitest-dev/vitest/commit/b78f5389d)
- Type regression in vi.mocked() static class methods  -  by [@&#8203;purepear](https://github.com/purepear) and [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;9857](https://github.com/vitest-dev/vitest/issues/9857) [<samp>(90926)</samp>](https://github.com/vitest-dev/vitest/commit/90926641b)
- Properly re-evaluate actual modules of mocked external  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;9898](https://github.com/vitest-dev/vitest/issues/9898) [<samp>(ae5ec)</samp>](https://github.com/vitest-dev/vitest/commit/ae5ec03ef)
- Preserve coverage report when html reporter overlaps  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;9889](https://github.com/vitest-dev/vitest/issues/9889) [<samp>(2d81a)</samp>](https://github.com/vitest-dev/vitest/commit/2d81ad897)
- Provide `vi.advanceTimers` to the preview provider  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;9891](https://github.com/vitest-dev/vitest/issues/9891) [<samp>(1bc3e)</samp>](https://github.com/vitest-dev/vitest/commit/1bc3e63be)
- Don't leak event listener in playwright provider  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;9910](https://github.com/vitest-dev/vitest/issues/9910) [<samp>(d9355)</samp>](https://github.com/vitest-dev/vitest/commit/d93550ff7)
- Open browser in `--standalone` mode without running tests  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;9911](https://github.com/vitest-dev/vitest/issues/9911) [<samp>(e78ad)</samp>](https://github.com/vitest-dev/vitest/commit/e78adcf97)
- Guard disposable and optional `body`  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;9912](https://github.com/vitest-dev/vitest/issues/9912) [<samp>(6fdb2)</samp>](https://github.com/vitest-dev/vitest/commit/6fdb2ba61)
- Resolve `retry.condition` RegExp serialization issue  -  by [@&#8203;nstepien](https://github.com/nstepien) and [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;9942](https://github.com/vitest-dev/vitest/issues/9942) [<samp>(7b605)</samp>](https://github.com/vitest-dev/vitest/commit/7b6054328)
- **collect**:
  - Don't treat extra props on `test` return as tests  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;9871](https://github.com/vitest-dev/vitest/issues/9871) [<samp>(141e7)</samp>](https://github.com/vitest-dev/vitest/commit/141e72aa1)
- **coverage**:
  - Simplify provider types  -  by [@&#8203;AriPerkkio](https://github.com/AriPerkkio) in [#&#8203;9931](https://github.com/vitest-dev/vitest/issues/9931) [<samp>(aaf9f)</samp>](https://github.com/vitest-dev/vitest/commit/aaf9f18ae)
  - Load built-in provider without module runner  -  by [@&#8203;AriPerkkio](https://github.com/AriPerkkio) in [#&#8203;9939](https://github.com/vitest-dev/vitest/issues/9939) [<samp>(bf892)</samp>](https://github.com/vitest-dev/vitest/commit/bf8920817)
- **expect**:
  - Soft assertions continue after .resolves/.rejects promise errors  -  by [@&#8203;mixelburg](https://github.com/mixelburg), **Maks Pikov**, **Claude Opus 4.6 (1M context)** and [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;9843](https://github.com/vitest-dev/vitest/issues/9843) [<samp>(6d74b)</samp>](https://github.com/vitest-dev/vitest/commit/6d74b4948)
  - Fix sinon-chai style API  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;9943](https://github.com/vitest-dev/vitest/issues/9943) [<samp>(0f08d)</samp>](https://github.com/vitest-dev/vitest/commit/0f08dda2c)
- **pretty-format**:
  - Limit output for large object  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) and **Claude Opus 4.6 (1M context)** in [#&#8203;9949](https://github.com/vitest-dev/vitest/issues/9949) [<samp>(0d5f9)</samp>](https://github.com/vitest-dev/vitest/commit/0d5f9d6ef)

#####     [View changes on GitHub](https://github.com/vitest-dev/vitest/compare/v4.1.0...v4.1.1)

</details>

<details>
<summary>eslint/eslint (eslint)</summary>

### [`v10.2.0`](https://github.com/eslint/eslint/releases/tag/v10.2.0)

[Compare Source](https://github.com/eslint/eslint/compare/v10.1.0...v10.2.0)

#### Features

- [`586ec2f`](https://github.com/eslint/eslint/commit/586ec2f43092779acc957866db4abe999112d1e1) feat: Add `meta.languages` support to rules ([#&#8203;20571](https://github.com/eslint/eslint/issues/20571)) (Copilot)
- [`14207de`](https://github.com/eslint/eslint/commit/14207dee3939dc87cfa8b2fcfc271fff2cfd6471) feat: add `Temporal` to `no-obj-calls` ([#&#8203;20675](https://github.com/eslint/eslint/issues/20675)) (Pixel998)
- [`bbb2c93`](https://github.com/eslint/eslint/commit/bbb2c93a2b31bd30924f32fe69a9acf41f9dfe35) feat: add Temporal to ES2026 globals ([#&#8203;20672](https://github.com/eslint/eslint/issues/20672)) (Pixel998)

#### Bug Fixes

- [`542cb3e`](https://github.com/eslint/eslint/commit/542cb3e6442a4e6ee3457c799e2a0ee23bef0c6a) fix: update first-party dependencies ([#&#8203;20714](https://github.com/eslint/eslint/issues/20714)) (Francesco Trotta)

#### Documentation

- [`a2af743`](https://github.com/eslint/eslint/commit/a2af743ea60f683d0e0de9d98267c1e7e4f5e412) docs: add `language` to configuration objects ([#&#8203;20712](https://github.com/eslint/eslint/issues/20712)) (Francesco Trotta)
- [`845f23f`](https://github.com/eslint/eslint/commit/845f23f1370892bf07d819497ac518c9e65090d6) docs: Update README (GitHub Actions Bot)
- [`5fbcf59`](https://github.com/eslint/eslint/commit/5fbcf5958b897cc4df5d652924d18428db37f7ee) docs: remove `sourceType` from ts playground link ([#&#8203;20477](https://github.com/eslint/eslint/issues/20477)) (Tanuj Kanti)
- [`8702a47`](https://github.com/eslint/eslint/commit/8702a474659be786b6b1392e5e7c0c56355ae4a4) docs: Update README (GitHub Actions Bot)
- [`ddeaded`](https://github.com/eslint/eslint/commit/ddeaded2ab36951383ff67c60fb64ec68d29a46a) docs: Update README (GitHub Actions Bot)
- [`2b44966`](https://github.com/eslint/eslint/commit/2b4496691266547784a7f7ad1989ce53381bab91) docs: add Major Releases section to Manage Releases ([#&#8203;20269](https://github.com/eslint/eslint/issues/20269)) (Milos Djermanovic)
- [`eab65c7`](https://github.com/eslint/eslint/commit/eab65c700ebb16a6e790910c720450c9908961fd) docs: update `eslint` versions in examples ([#&#8203;20664](https://github.com/eslint/eslint/issues/20664)) (루밀LuMir)
- [`3e4a299`](https://github.com/eslint/eslint/commit/3e4a29903bf31f0998e45ad9128a265bce1edc56) docs: update ESM Dependencies policies with note for own-usage packages ([#&#8203;20660](https://github.com/eslint/eslint/issues/20660)) (Milos Djermanovic)

#### Chores

- [`8120e30`](https://github.com/eslint/eslint/commit/8120e30f833474f47acc061d24d164e9f022264f) refactor: extract no unmodified loop condition ([#&#8203;20679](https://github.com/eslint/eslint/issues/20679)) (kuldeep kumar)
- [`46e8469`](https://github.com/eslint/eslint/commit/46e8469786be1b2bbb522100e1d44624d98d3745) chore: update dependency markdownlint-cli2 to ^0.22.0 ([#&#8203;20697](https://github.com/eslint/eslint/issues/20697)) (renovate\[bot])
- [`01ed3aa`](https://github.com/eslint/eslint/commit/01ed3aa68477f81a7188e1498cf4906e02015b7c) test: add unit tests for unicode utilities ([#&#8203;20622](https://github.com/eslint/eslint/issues/20622)) (Manish chaudhary)
- [`811f493`](https://github.com/eslint/eslint/commit/811f4930f82ee2b6ac8eae75cade9bed63de0781) ci: remove `--legacy-peer-deps` from types integration tests ([#&#8203;20667](https://github.com/eslint/eslint/issues/20667)) (Milos Djermanovic)
- [`6b86fcf`](https://github.com/eslint/eslint/commit/6b86fcfc5c75d6a3b8a2cf7bcdb3ef60635a9a03) chore: update dependency npm-run-all2 to v8 ([#&#8203;20663](https://github.com/eslint/eslint/issues/20663)) (renovate\[bot])
- [`632c4f8`](https://github.com/eslint/eslint/commit/632c4f83bf32b77981c7d395cacddd1bb172ee25) chore: add `prettier` update commit to `.git-blame-ignore-revs` ([#&#8203;20662](https://github.com/eslint/eslint/issues/20662)) (루밀LuMir)
- [`b0b0f21`](https://github.com/eslint/eslint/commit/b0b0f21927e03ba092400e3c70d7058f537765c8) chore: update dependency eslint-plugin-regexp to ^3.1.0 ([#&#8203;20659](https://github.com/eslint/eslint/issues/20659)) (Milos Djermanovic)
- [`228a2dd`](https://github.com/eslint/eslint/commit/228a2dd4b272c17f516ee3541f1dd69eca0a8ab0) chore: update dependency eslint-plugin-eslint-plugin to ^7.3.2 ([#&#8203;20661](https://github.com/eslint/eslint/issues/20661)) (Milos Djermanovic)
- [`3ab4d7e`](https://github.com/eslint/eslint/commit/3ab4d7e244df244102de9d0d250b2ff12456a785) test: Add tests for eslintrc-style keys ([#&#8203;20645](https://github.com/eslint/eslint/issues/20645)) (kuldeep kumar)

### [`v10.1.0`](https://github.com/eslint/eslint/releases/tag/v10.1.0)

[Compare Source](https://github.com/eslint/eslint/compare/v10.0.3...v10.1.0)

#### Features

- [`ff4382b`](https://github.com/eslint/eslint/commit/ff4382be349035acdb170627a2dc92828e134562) feat: apply fix for `no-var` in `TSModuleBlock` ([#&#8203;20638](https://github.com/eslint/eslint/issues/20638)) (Tanuj Kanti)
- [`0916995`](https://github.com/eslint/eslint/commit/0916995b51528872b15ba4fedb24172cf25fcb3f) feat: Implement api support for bulk-suppressions ([#&#8203;20565](https://github.com/eslint/eslint/issues/20565)) (Blake Sager)

#### Bug Fixes

- [`2b8824e`](https://github.com/eslint/eslint/commit/2b8824e6be4223980e929a20025602df20d05ea2) fix: Prevent `no-var` autofix when a variable is used before declaration ([#&#8203;20464](https://github.com/eslint/eslint/issues/20464)) (Amaresh  S M)
- [`e58b4bf`](https://github.com/eslint/eslint/commit/e58b4bff167e79afd067d1b0ee9360bec2d3393e) fix: update eslint ([#&#8203;20597](https://github.com/eslint/eslint/issues/20597)) (renovate\[bot])

#### Documentation

- [`b7b57fe`](https://github.com/eslint/eslint/commit/b7b57fe9942c572ff651230f1f96cefed787ca52) docs: use correct JSDoc link in require-jsdoc.md ([#&#8203;20641](https://github.com/eslint/eslint/issues/20641)) (mkemna-clb)
- [`58e4cfc`](https://github.com/eslint/eslint/commit/58e4cfc7dbf0fe40c73f09bf0ff94ad944d0ba0e) docs: add deprecation notice partial ([#&#8203;20639](https://github.com/eslint/eslint/issues/20639)) (Milos Djermanovic)
- [`7143dbf`](https://github.com/eslint/eslint/commit/7143dbf99df27c61edf1552da981794e99a0b2f2) docs: update v9 migration guide for `@eslint/js` usage ([#&#8203;20540](https://github.com/eslint/eslint/issues/20540)) (fnx)
- [`035fc4f`](https://github.com/eslint/eslint/commit/035fc4fbe506e3e4524882cf50db37a4e430adf4) docs: note that `globalReturn` applies only with `sourceType: "script"` ([#&#8203;20630](https://github.com/eslint/eslint/issues/20630)) (Milos Djermanovic)
- [`e972c88`](https://github.com/eslint/eslint/commit/e972c88ab7474a74191ee99ac2558b00d0427a8a) docs: merge ESLint option descriptions into type definitions ([#&#8203;20608](https://github.com/eslint/eslint/issues/20608)) (Francesco Trotta)
- [`7f10d84`](https://github.com/eslint/eslint/commit/7f10d8440137f0cfd75f18f4746ba6a1c621b953) docs: Update README (GitHub Actions Bot)
- [`aeed007`](https://github.com/eslint/eslint/commit/aeed0078ca2f73d4744cc522102178d45b5be64e) docs: open playground link in new tab ([#&#8203;20602](https://github.com/eslint/eslint/issues/20602)) (Tanuj Kanti)
- [`a0d1a37`](https://github.com/eslint/eslint/commit/a0d1a3772679d3d74bb860fc65b5b58678acd452) docs: Add AI Usage Policy ([#&#8203;20510](https://github.com/eslint/eslint/issues/20510)) (Nicholas C. Zakas)

#### Chores

- [`a9f9cce`](https://github.com/eslint/eslint/commit/a9f9cce82d80b540a0e3549d0e91c16df28740d8) chore: update dependency eslint-plugin-unicorn to ^63.0.0 ([#&#8203;20584](https://github.com/eslint/eslint/issues/20584)) (Milos Djermanovic)
- [`1f42bd7`](https://github.com/eslint/eslint/commit/1f42bd7876ae4192cf7f7f4faf73b4ef3d2563cb) chore: update `prettier` to 3.8.1 ([#&#8203;20651](https://github.com/eslint/eslint/issues/20651)) (루밀LuMir)
- [`c0a6f4a`](https://github.com/eslint/eslint/commit/c0a6f4a2b4169edeca2a81bf7b47783e39ade366) chore: update dependency [@&#8203;eslint/json](https://github.com/eslint/json) to ^1.2.0 ([#&#8203;20652](https://github.com/eslint/eslint/issues/20652)) (renovate\[bot])
- [`cc43f79`](https://github.com/eslint/eslint/commit/cc43f795c42e5ec2f19bb43b1f6d534ef2e469f3) chore: update dependency c8 to v11 ([#&#8203;20650](https://github.com/eslint/eslint/issues/20650)) (renovate\[bot])
- [`2ce4635`](https://github.com/eslint/eslint/commit/2ce4635b036ff2665c7009afddf9c0fb2274dceb) chore: update dependency [@&#8203;eslint/json](https://github.com/eslint/json) to v1 ([#&#8203;20649](https://github.com/eslint/eslint/issues/20649)) (renovate\[bot])
- [`f0406ee`](https://github.com/eslint/eslint/commit/f0406eedcc3dc415babbbf6bbdb5db1eebfd487b) chore: update dependency markdownlint-cli2 to ^0.21.0 ([#&#8203;20646](https://github.com/eslint/eslint/issues/20646)) (renovate\[bot])
- [`dbb4c95`](https://github.com/eslint/eslint/commit/dbb4c9582a00bac604d5c6ac671bb7111468a846) chore: remove trunk ([#&#8203;20478](https://github.com/eslint/eslint/issues/20478)) (sethamus)
- [`c672a2a`](https://github.com/eslint/eslint/commit/c672a2a70579fddf1c6ce33dfa712d705726e1c9) test: fix CLI test for empty output file ([#&#8203;20640](https://github.com/eslint/eslint/issues/20640)) (kuldeep kumar)
- [`c7ada24`](https://github.com/eslint/eslint/commit/c7ada2455680036bbfc42fcb1511ff28afe3c587) ci: bump pnpm/action-setup from 4.3.0 to 4.4.0 ([#&#8203;20636](https://github.com/eslint/eslint/issues/20636)) (dependabot\[bot])
- [`07c4b8b`](https://github.com/eslint/eslint/commit/07c4b8b4a9f49145e60a3448dd57853213ed4de3) test: fix `RuleTester` test without test runners ([#&#8203;20631](https://github.com/eslint/eslint/issues/20631)) (Francesco Trotta)
- [`079bba7`](https://github.com/eslint/eslint/commit/079bba7ff17d0a99fdffe32bf991d005ba797fae) test: Add tests for `isValidWithUnicodeFlag` ([#&#8203;20601](https://github.com/eslint/eslint/issues/20601)) (Manish chaudhary)
- [`5885ae6`](https://github.com/eslint/eslint/commit/5885ae66216bcee9310bbf73786b7d7d5774aeaf) ci: unpin Node.js 25.x in CI ([#&#8203;20615](https://github.com/eslint/eslint/issues/20615)) (Copilot)
- [`f65e5d3`](https://github.com/eslint/eslint/commit/f65e5d3c0df65fdb317ad6d23f7ae113c5f4b6d7) chore: update pnpm/action-setup digest to [`b906aff`](https://github.com/eslint/eslint/commit/b906aff) ([#&#8203;20610](https://github.com/eslint/eslint/issues/20610)) (renovate\[bot])

</details>

<details>
<summary>NaturalIntelligence/fast-xml-parser (fast-xml-parser)</summary>

### [`v5.5.12`](https://github.com/NaturalIntelligence/fast-xml-parser/compare/12b041ddeffa8b37ea44d572284c00a89148f2d3...5d8a8916a9310c4e5fad74040b9d1382c7b54b17)

[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/12b041ddeffa8b37ea44d572284c00a89148f2d3...5d8a8916a9310c4e5fad74040b9d1382c7b54b17)

### [`v5.5.11`](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.5.10...12b041ddeffa8b37ea44d572284c00a89148f2d3)

[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.5.10...12b041ddeffa8b37ea44d572284c00a89148f2d3)

### [`v5.5.10`](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.5.10): performance improvment, increase entity expansion default limit

[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.5.9...v5.5.10)

- increase default entity explansion limit as many projects demand for that

```
maxEntitySize: 10000,
maxExpansionDepth: 10000,
maxTotalExpansions: Infinity,
maxExpandedLength: 100000,
maxEntityCount: 1000,
```

- performance improvement
  - reduce calls to toString
  - early return when entities are not present
  - prepare rawAttrsForMatcher only if user sets `jPath: false`

**Full Changelog**: <https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.5.9...v5.5.10>

### [`v5.5.9`](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.5.9): fix typins and matcher instance in callbacks

[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.5.8...v5.5.9)

combine typings file to avoid configuration changes
pass readonly instance of matcher to the call backs to avoid accidental push/pop call

### [`v5.5.8`](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.5.7...v5.5.8)

[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.5.7...v5.5.8)

### [`v5.5.7`](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.5.7): fix bugs of entity parsing and value parsing

[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.5.6...v5.5.7)

fix: entity expansion limits
update strnum package to 2.2.0

### [`v5.5.6`](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.5.6): fix entity expansion and incorrect replacement and performance

[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.5.5...v5.5.6)

**Full Changelog**: <https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.5.5...v5.5.6>

### [`v5.5.5`](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.5.5): support onDangerousProperty

[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.5.4...v5.5.5)

**Full Changelog**: <https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.5.3...v5.5.5>

### [`v5.5.4`](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.5.3...v5.5.4)

[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.5.3...v5.5.4)

</details>

<details>
<summary>capricorn86/happy-dom (happy-dom)</summary>

### [`v20.9.0`](https://github.com/capricorn86/happy-dom/releases/tag/v20.9.0)

[Compare Source](https://github.com/capricorn86/happy-dom/compare/v20.8.9...v20.9.0)

##### 🎨 Features

- Adds support for event listener properties on Window (e.g. `Window.onkeydown`) - By **[@&#8203;capricorn86](https://github.com/capricorn86)** in task [#&#8203;2131](https://github.com/capricorn86/happy-dom/issues/2131)

### [`v20.8.9`](https://github.com/capricorn86/happy-dom/releases/tag/v20.8.9)

[Compare Source](https://github.com/capricorn86/happy-dom/compare/v20.8.8...v20.8.9)

##### :construction\_worker\_man: Patch fixes

- Fixes issue where cookies from the current origin was being forwarded to the target origin in fetch requests - By **[@&#8203;capricorn86](https://github.com/capricorn86)** in task [#&#8203;2117](https://github.com/capricorn86/happy-dom/issues/2117)
  - A security advisory ([GHSA-w4gp-fjgq-3q4g](https://github.com/capricorn86/happy-dom/security/advisories/GHSA-w4gp-fjgq-3q4g)) was reported for this security vulnerability. Big thanks to [@&#8203;r74tech](https://github.com/r74tech) for reporting this!

### [`v20.8.8`](https://github.com/capricorn86/happy-dom/releases/tag/v20.8.8)

[Compare Source](https://github.com/capricorn86/happy-dom/compare/v20.8.7...v20.8.8)

##### :construction\_worker\_man: Patch fixes

- Fixes issue where export names can be interpolated as executable code in ESM - By **[@&#8203;capricorn86](https://github.com/capricorn86)** in task [#&#8203;2113](https://github.com/capricorn86/happy-dom/issues/2113)
  - A security advisory ([GHSA-6q6h-j7hj-3r64](https://github.com/capricorn86/happy-dom/security/advisories/GHSA-6q6h-j7hj-3r64)) has been reported that shows a security vulnerability where it may be possible to escape the VM context and get access to process level functionality in unsafe environments using CommonJS. Big thanks to [@&#8203;tndud042713](https://github.com/tndud042713) for reporting this!

### [`v20.8.7`](https://github.com/capricorn86/happy-dom/releases/tag/v20.8.7)

[Compare Source](https://github.com/capricorn86/happy-dom/compare/v20.8.6...v20.8.7)

##### :construction\_worker\_man: Patch fixes

- Replace implementing Node.js `Console` with common `IConsole` interface to support latest version of Bun - By **[@&#8203;YevheniiKotyrlo](https://github.com/YevheniiKotyrlo)** in task [#&#8203;1845](https://github.com/capricorn86/happy-dom/issues/1845)

### [`v20.8.6`](https://github.com/capricorn86/happy-dom/releases/tag/v20.8.6)

[Compare Source](https://github.com/capricorn86/happy-dom/compare/v20.8.5...v20.8.6)

##### :construction\_worker\_man: Patch fixes

- `Request.formData()` should honor "Content-Type" header - By **[@&#8203;brianhelba](https://github.com/brianhelba)** in task [#&#8203;2106](https://github.com/capricorn86/happy-dom/issues/2106)

### [`v20.8.5`](https://github.com/capricorn86/happy-dom/releases/tag/v20.8.5)

[Compare Source](https://github.com/capricorn86/happy-dom/compare/v20.8.4...v20.8.5)

##### :construction\_worker\_man: Patch fixes

- Fixes error thrown when modifying DOM structure in `connectedCallback()` - By **[@&#8203;capricorn86](https://github.com/capricorn86)** in task [#&#8203;2110](https://github.com/capricorn86/happy-dom/issues/2110)

### [`v20.8.4`](https://github.com/capricorn86/happy-dom/releases/tag/v20.8.4)

[Compare Source](https://github.com/capricorn86/happy-dom/compare/v20.8.3...v20.8.4)

##### :construction\_worker\_man: Patch fixes

- Replace ConsoleConstructor import with indexed access type - By **[@&#8203;YevheniiKotyrlo](https://github.com/YevheniiKotyrlo)** in task [#&#8203;1845](https://github.com/capricorn86/happy-dom/issues/1845)

</details>

<details>
<summary>video-dev/hls.js (hls.js)</summary>

### [`v1.6.16`](https://github.com/video-dev/hls.js/releases/tag/v1.6.16)

[Compare Source](https://github.com/video-dev/hls.js/compare/v1.6.15...v1.6.16)

### Summary

HLS.js v1.6.16 includes bug fixes and improvements over the last release.

#### Changes Since The Last Release

- Fix Interstitials live start with short sliding window ([#&#8203;7799](https://github.com/video-dev/hls.js/issues/7799))
- Limit buffering while paused outside live sliding window ([#&#8203;7788](https://github.com/video-dev/hls.js/issues/7788))

#### Demo Page

<https://121bff6b.hls-js-dev.pages.dev/demo/>

#### Feedback

Please provide feedback via [Issues in GitHub](https://github.com/video-dev/hls.js/issues/new/choose). For more details on how to contribute to HLS.js, see our [CONTRIBUTING guide](https://github.com/video-dev/hls.js/blob/master/CONTRIBUTING.md).

</details>

<details>
<summary>versatica/mediasoup (mediasoup)</summary>

### [`v3.19.19`](https://github.com/versatica/mediasoup/blob/HEAD/CHANGELOG.md#31919)

[Compare Source](https://github.com/versatica/mediasoup/compare/3.19.18...3.19.19)

- Worker: Ensure 4-byte alignment for network packet receive buffers and test buffers to avoid undefined behavior ([PR #&#8203;1756](https://github.com/versatica/mediasoup/pull/1756).
- Worker: Update liburing from 2.12-1 to 2.14-1 ([PR #&#8203;1761](https://github.com/versatica/mediasoup/pull/1761).

### [`v3.19.18`](https://github.com/versatica/mediasoup/blob/HEAD/CHANGELOG.md#31918)

[Compare Source](https://github.com/versatica/mediasoup/compare/3.19.17...3.19.18)

- Worker: Improve `Utils::Crypto::GetRandomUInt()` ([PR #&#8203;1725](https://github.com/versatica/mediasoup/pull/1725).
- Convert `WORKER_CLOSE` into a notification ([PR #&#8203;1729](https://github.com/versatica/mediasoup/pull/1729).
- Node tests: Replace `sctp` unmaintained library with `werift-sctp` ([PR #&#8203;1732](https://github.com/versatica/mediasoup/pull/1732), thanks to [@&#8203;shinyoshiaki](https://github.com/shinyoshiaki) for his help with `werift-sctp`.
- Worker: Require C++20 ([PR #&#8203;1741](https://github.com/versatica/mediasoup/pull/1741).
- Fix "SCTP failed" if no DataChannel is created on a Transport with `enableSctp: true` ([PR #&#8203;1749](https://github.com/versatica/mediasoup/pull/1749).

</details>

<details>
<summary>panva/openid-client (openid-client)</summary>

### [`v6.8.3`](https://github.com/panva/openid-client/blob/HEAD/CHANGELOG.md#683-2026-04-13)

[Compare Source](https://github.com/panva/openid-client/compare/v6.8.2...v6.8.3)

##### Documentation

- note a workaround for redirect\_uri with query string or bare origin ([e9689de](https://github.com/panva/openid-client/commit/e9689de336fed2c9e6678ac804f1c6b7d9bbd338)), closes [#&#8203;868](https://github.com/panva/openid-client/issues/868)

##### Fixes

- **passport:** delete one-time state on callback ([1e7dd2e](https://github.com/panva/openid-client/commit/1e7dd2eced03e729447bfa38c8c455268748c5c3))

</details>

<details>
<summary>vuejs/core (vue)</summary>

### [`v3.5.32`](https://github.com/vuejs/core/blob/HEAD/CHANGELOG.md#3532-2026-04-03)

[Compare Source](https://github.com/vuejs/core/compare/v3.5.31...v3.5.32)

##### Bug Fixes

- **runtime-core:** prevent currentInstance leak into sibling render during async setup re-entry ([#&#8203;14668](https://github.com/vuejs/core/issues/14668)) ([f166353](https://github.com/vuejs/core/commit/f1663535a163057788d3285dec54a245c3efb3ad)), closes [#&#8203;14667](https://github.com/vuejs/core/issues/14667)
- **teleport:** handle updates before deferred mount ([#&#8203;14642](https://github.com/vuejs/core/issues/14642)) ([32b44f1](https://github.com/vuejs/core/commit/32b44f19f67aa30899817a7e79a4510f3b52970a)), closes [#&#8203;14640](https://github.com/vuejs/core/issues/14640)
- **types:** allow customRef to have different getter/setter types ([#&#8203;14639](https://github.com/vuejs/core/issues/14639)) ([e20ddb0](https://github.com/vuejs/core/commit/e20ddb00188e9935884930046fa572eab7c9dcba))
- **types:** use private branding for shallowReactive ([#&#8203;14641](https://github.com/vuejs/core/issues/14641)) ([302c47a](https://github.com/vuejs/core/commit/302c47a4994bc8b47b8a2af6693d8cb6bbd4b06b)), closes [#&#8203;14638](https://github.com/vuejs/core/issues/14638) [#&#8203;14493](https://github.com/vuejs/core/issues/14493)

##### Reverts

- Revert "fix(server-renderer): cleanup component effect scopes after SSR render" ([#&#8203;14674](https://github.com/vuejs/core/issues/14674)) ([219d83b](https://github.com/vuejs/core/commit/219d83bd305ce6fc052941acaaf02e7bc70616a4)), closes [#&#8203;14674](https://github.com/vuejs/core/issues/14674) [#&#8203;14669](https://github.com/vuejs/core/issues/14669)

### [`v3.5.31`](https://github.com/vuejs/core/blob/HEAD/CHANGELOG.md#3531-2026-03-25)

[Compare Source](https://github.com/vuejs/core/compare/v3.5.30...v3.5.31)

##### Bug Fixes

- **compiler-sfc:** allow Node.js subpath imports patterns in asset urls ([#&#8203;13045](https://github.com/vuejs/core/issues/13045)) ([95c3356](https://github.com/vuejs/core/commit/95c33560c9af369d44a7670d0c3b93fb62323be2)), closes [#&#8203;9919](https://github.com/vuejs/core/issues/9919)
- **compiler-sfc:** support template literal as defineModel name ([#&#8203;14622](https://github.com/vuejs/core/issues/14622)) ([bd7eef0](https://github.com/vuejs/core/commit/bd7eef0161d69bccd48ac303bc6a56ba8d718e2d)), closes [#&#8203;14621](https://github.com/vuejs/core/issues/14621)
- **reactivity:** normalize toRef property keys before dep lookup + improve types ([#&#8203;14625](https://github.com/vuejs/core/issues/14625)) ([1bb28d0](https://github.com/vuejs/core/commit/1bb28d011b31bc75c80d2860bc6484cceec1ff20)), closes [#&#8203;12427](https://github.com/vuejs/core/issues/12427) [#&#8203;12431](https://github.com/vuejs/core/issues/12431)
- **runtime-core:** invalidate detached v-for memo vnodes after unmount ([#&#8203;14624](https://github.com/vuejs/core/issues/14624)) ([560def4](https://github.com/vuejs/core/commit/560def426fc38c1710fda7ddd1ac634d67897960)), closes [#&#8203;12708](https://github.com/vuejs/core/issues/12708) [#&#8203;12710](https://github.com/vuejs/core/issues/12710)
- **runtime-core:** preserve nullish event handlers in mergeProps ([#&#8203;14550](https://github.com/vuejs/core/issues/14550)) ([5725222](https://github.com/vuejs/core/commit/5725222a6bc5d1dd032318349ce0c540b1d63a49))
- **runtime-core:** prevent merging model listener when value is null or undefined ([#&#8203;14629](https://github.com/vuejs/core/issues/14629)) ([b39e032](https://github.com/vuejs/core/commit/b39e0329f67354702f4e417e55c15c61d2439657))
- **runtime-dom:** defer teleport mount/update until suspense resolves ([#&#8203;8619](https://github.com/vuejs/core/issues/8619)) ([88ed045](https://github.com/vuejs/core/commit/88ed04501555b9257df8d7ad86d844c2c2136e50)), closes [#&#8203;8603](https://github.com/vuejs/core/issues/8603)
- **runtime-dom:** handle activeElement check in Shadow DOM for v-model ([#&#8203;14196](https://github.com/vuejs/core/issues/14196)) ([959ded2](https://github.com/vuejs/core/commit/959ded22ab7ea1453f607e0964e1fb6748ece6c7))
- **server-renderer:** cleanup component effect scopes after SSR render ([#&#8203;14548](https://github.com/vuejs/core/issues/14548)) ([862f11e](https://github.com/vuejs/core/commit/862f11ee017d51cb9573a8c0642055b3b17cace8))
- **suspense:** avoid unmount activeBranch twice if wrapped in transition ([#&#8203;9392](https://github.com/vuejs/core/issues/9392)) ([908c6ad](https://github.com/vuejs/core/commit/908c6ad05e1c76ae690d2e50f3bd28278af07e22)), closes [#&#8203;7966](https://github.com/vuejs/core/issues/7966)
- **suspense:** update suspense vnode's el during branch self-update ([#&#8203;12922](https://github.com/vuejs/core/issues/12922)) ([a2c1700](https://github.com/vuejs/core/commit/a2c17004c84e5ce3c4e82e35b806ba381144eed3)), closes [#&#8203;12920](https://github.com/vuejs/core/issues/12920)
- **transition:** skip enter guard while hmr updating ([#&#8203;14611](https://github.com/vuejs/core/issues/14611)) ([be0a2f1](https://github.com/vuejs/core/commit/be0a2f1a7fc3d81d05638798cc628848cfa62cef)), closes [#&#8203;14608](https://github.com/vuejs/core/issues/14608)
- **types:** prevent shallowReactive marker from leaking into value unions ([#&#8203;14493](https://github.com/vuejs/core/issues/14493)) ([3b561db](https://github.com/vuejs/core/commit/3b561db4ab42d06166b002f13c0e97cb2bd4a061)), closes [#&#8203;14490](https://github.com/vuejs/core/issues/14490)

</details>

<details>
<summary>vuejs/router (vue-router)</summary>

### [`v5.0.4`](https://github.com/vuejs/router/releases/tag/v5.0.4)

[Compare Source](https://github.com/vuejs/router/compare/v5.0.3...v5.0.4)

#####    🐞 Bug Fixes

- Avoid iterator helpers for Node 20 compat  -  by [@&#8203;cwandev](https://github.com/cwandev) in [#&#8203;2635](https://github.com/vuejs/router/issues/2635) [<samp>(47130)</samp>](https://github.com/vuejs/router/commit/4713069d)
- Escape backslahes in string literals  -  by [@&#8203;posva](https://github.com/posva) [<samp>(71fdb)</samp>](https://github.com/vuejs/router/commit/71fdbf36)
- Avoid false duplicate route warning for named views  -  by [@&#8203;posva](https://github.com/posva) [<samp>(72012)</samp>](https://github.com/vuejs/router/commit/72012ab0)
- Allow pushing to auto routes  -  by [@&#8203;posva](https://github.com/posva) [<samp>(47f03)</samp>](https://github.com/vuejs/router/commit/47f0334b)
- **loaders**: Restore context in sequential awaits  -  by [@&#8203;posva](https://github.com/posva) [<samp>(fce5d)</samp>](https://github.com/vuejs/router/commit/fce5d1e3)

#####     [View changes on GitHub](https://github.com/vuejs/router/compare/v5.0.3...v5.0.4)

</details>

<details>
<summary>websockets/ws (ws)</summary>

### [`v8.20.0`](https://github.com/websockets/ws/releases/tag/8.20.0)

[Compare Source](https://github.com/websockets/ws/compare/8.19.0...8.20.0)

### Features

- Added exports for the `PerMessageDeflate` class and utilities for the
  `Sec-WebSocket-Extensions` and `Sec-WebSocket-Protocol` headers ([`d3503c1`](https://github.com/websockets/ws/commit/d3503c1f)).

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDIuMTAiLCJ1cGRhdGVkSW5WZXIiOiI0My4xMjAuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Reviewed-on: https://git.keligrubb.com/keligrubb/kestrelos/pulls/25
Co-authored-by: Renovate Bot <renovate@keligrubb.com>
Co-committed-by: Renovate Bot <renovate@keligrubb.com>
2026-04-15 02:42:47 +00:00
CI c023bdccae release v1.0.10 [skip ci] 2026-04-15 02:35:40 +00:00
Keli Grubb 78f3ad8fcc Remove npm overrides for tar (#26)
Push / release-docker-helm (push) Successful in 4m21s
Drop the package.json overrides entry so transitive tar versions follow
what dependencies resolve. Refresh package-lock.json after npm install.

Reviewed-on: #26
Co-authored-by: keligrubb <keligrubb324@gmail.com>
Co-committed-by: keligrubb <keligrubb324@gmail.com>
2026-04-15 02:32:49 +00:00
CI 2e7a52ed15 release v1.0.9 [skip ci] 2026-03-24 14:05:10 +00:00
Renovate Bot 221b3533e5 chore(deps): update https://git.keligrubb.com/actions/setup-helm action to v5 (#23)
Push / release-docker-helm (push) Successful in 5m24s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [https://git.keligrubb.com/actions/setup-helm](https://git.keligrubb.com/actions/setup-helm) | action | major | `v4` → `v5` |

---

### Release Notes

<details>
<summary>actions/setup-helm (https://git.keligrubb.com/actions/setup-helm)</summary>

### [`v5`](https://git.keligrubb.com/actions/setup-helm/compare/v4...v5)

[Compare Source](https://git.keligrubb.com/actions/setup-helm/compare/v4...v5)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My44Ny4xIiwidXBkYXRlZEluVmVyIjoiNDMuODcuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Reviewed-on: #23
Co-authored-by: Renovate Bot <renovate@keligrubb.com>
Co-committed-by: Renovate Bot <renovate@keligrubb.com>
2026-03-24 14:02:21 +00:00
CI 578dccd0cf release v1.0.8 [skip ci] 2026-03-12 19:42:02 +00:00
Keli Grubb 0ecad475ef patch: fix release file (#22)
Push / release-docker-helm (push) Successful in 4m16s
Reviewed-on: #22
2026-03-12 19:39:15 +00:00
Keli Grubb d5789b79a6 patch: swap to stdlib sqlite3 (#21)
Push / release-docker-helm (push) Failing after 2m39s
Reviewed-on: #21
2026-03-12 19:17:26 +00:00
CI 45ce90f3cc release v1.0.7 [skip ci] 2026-03-06 02:30:45 +00:00
Renovate Bot 3a817625c5 chore(deps): update docker/build-push-action action to v7 (#19)
Push / release-docker-helm (push) Successful in 3m33s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [docker/build-push-action](https://github.com/docker/build-push-action) | action | major | `v6` → `v7` |

---

### Release Notes

<details>
<summary>docker/build-push-action (docker/build-push-action)</summary>

### [`v7`](https://github.com/docker/build-push-action/compare/v6...v7)

[Compare Source](https://github.com/docker/build-push-action/compare/v6...v7)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My41Ni4xIiwidXBkYXRlZEluVmVyIjoiNDMuNTYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Reviewed-on: #19
Reviewed-by: Keli Grubb <keligrubb324@gmail.com>
Co-authored-by: Renovate Bot <renovate@keligrubb.com>
Co-committed-by: Renovate Bot <renovate@keligrubb.com>
2026-03-06 02:28:31 +00:00
CI d5abea48b3 release v1.0.6 [skip ci] 2026-03-05 15:37:20 +00:00
Keli GrubbandMadison Grubb 1da69ac272 patch: fix docker login during push stage (#18)
Push / release-docker-helm (push) Successful in 4m24s
Co-authored-by: Madison Grubb <madison@elastiflow.com>
Reviewed-on: #18
2026-03-05 15:34:32 +00:00
CI bd2092a3ea release v1.0.5 [skip ci] 2026-03-05 14:55:05 +00:00
Keli GrubbandMadison Grubb afb6fb8ac7 patch: fix deploy pipeline stages for token registry uploads (#17)
Push / release-docker-helm (push) Failing after 2m56s
Co-authored-by: Madison Grubb <madison@elastiflow.com>
Reviewed-on: #17
2026-03-05 14:52:10 +00:00
CI 414e9f4c33 release v1.0.4 [skip ci] 2026-03-04 20:04:05 +00:00
Keli GrubbandMadison Grubb 10035221fb patch: fix deploy pipeline (#15)
Push / release-docker-helm (push) Failing after 2m15s
Co-authored-by: Madison Grubb <madison@elastiflow.com>
Reviewed-on: #15
2026-03-04 20:01:50 +00:00
Keli GrubbandMadison Grubb 52a6f4368c patch: migrate to gitea actions (#14)
Push / release-docker-helm (push) Failing after 2s
Co-authored-by: Madison Grubb <madison@elastiflow.com>
Reviewed-on: #14
2026-03-04 19:49:16 +00:00
CI e774dbc301 release v1.0.3 [skip ci] 2026-02-23 03:45:37 +00:00
Renovate Bot 5bf582f3ad fix(deps): update dependency vue-router to v5 (#12)
ci/woodpecker/push/push Pipeline was successful
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [vue-router](https://router.vuejs.org) ([source](https://github.com/vuejs/router)) | [`^4.4.0` → `^5.0.0`](https://renovatebot.com/diffs/npm/vue-router/4.6.4/5.0.3) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vue-router/5.0.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue-router/4.6.4/5.0.3?slim=true) |

---

### Release Notes

<details>
<summary>vuejs/router (vue-router)</summary>

### [`v5.0.3`](https://github.com/vuejs/router/releases/tag/v5.0.3)

[Compare Source](https://github.com/vuejs/router/compare/v5.0.2...v5.0.3)

#####    🚨 Breaking Changes

- **experimental**:
  - Make miss() throw internally and return never  -  by [@&#8203;posva](https://github.com/posva) [<samp>(077e1)</samp>](https://github.com/vuejs/router/commit/077e1740)
  - Add reroute() and deprecate NavigationResult  -  by [@&#8203;posva](https://github.com/posva) [<samp>(308db)</samp>](https://github.com/vuejs/router/commit/308db14a)
  - Remove selectNavigationResult  -  by [@&#8203;posva](https://github.com/posva) [<samp>(9e88a)</samp>](https://github.com/vuejs/router/commit/9e88aed4)

#####    🚀 Features

- Support \_parent in nested folders  -  by [@&#8203;posva](https://github.com/posva) [<samp>(0a37f)</samp>](https://github.com/vuejs/router/commit/0a37f474)
- Warn on \_parent conflict  -  by [@&#8203;posva](https://github.com/posva) [<samp>(182fe)</samp>](https://github.com/vuejs/router/commit/182fe03a)
- Set \_parent as non matchable by default  -  by [@&#8203;posva](https://github.com/posva) [<samp>(8f91c)</samp>](https://github.com/vuejs/router/commit/8f91c99f)
- Warn on conflicting components for routes  -  by [@&#8203;posva](https://github.com/posva) [<samp>(34ace)</samp>](https://github.com/vuejs/router/commit/34aceb98)
- Use type module  -  by [@&#8203;posva](https://github.com/posva) [<samp>(dc9ff)</samp>](https://github.com/vuejs/router/commit/dc9ffe81)
- Add deprecation warning for next() callback in navigation guards  -  by [@&#8203;posva](https://github.com/posva) [<samp>(797f5)</samp>](https://github.com/vuejs/router/commit/797f55de)
- Extract alias from definePage  -  by [@&#8203;posva](https://github.com/posva) [<samp>(835df)</samp>](https://github.com/vuejs/router/commit/835df1ff)
- Display aliases in logs  -  by [@&#8203;posva](https://github.com/posva) [<samp>(7aa60)</samp>](https://github.com/vuejs/router/commit/7aa607fc)
- Deprecate new NavigationResult(to) in favor of reroute(to)  -  by [@&#8203;posva](https://github.com/posva) [<samp>(382e3)</samp>](https://github.com/vuejs/router/commit/382e34b4)
- **experimental**:
  - Handle aliasOf in resolvers  -  by [@&#8203;posva](https://github.com/posva) [<samp>(8fe45)</samp>](https://github.com/vuejs/router/commit/8fe453c9)
  - Generate aliases from override in resolver  -  by [@&#8203;posva](https://github.com/posva) [<samp>(a00ac)</samp>](https://github.com/vuejs/router/commit/a00ac355)
  - Warn against non absolute aliases  -  by [@&#8203;posva](https://github.com/posva) [<samp>(476c6)</samp>](https://github.com/vuejs/router/commit/476c6697)

#####    🐞 Bug Fixes

- Avoid non matchable routes in auto-routes ��-  by [@&#8203;posva](https://github.com/posva) [<samp>(48649)</samp>](https://github.com/vuejs/router/commit/48649030)
- Handle quotes in d.ts  -  by [@&#8203;posva](https://github.com/posva) [<samp>(d7764)</samp>](https://github.com/vuejs/router/commit/d7764700)
- Avoid route entry in map for \_parent  -  by [@&#8203;posva](https://github.com/posva) [<samp>(1dfcc)</samp>](https://github.com/vuejs/router/commit/1dfccf82)
- Handle nested groups  -  by [@&#8203;posva](https://github.com/posva) [<samp>(4a4be)</samp>](https://github.com/vuejs/router/commit/4a4bed94)
- Stable route ordering for group folders with same path  -  by [@&#8203;posva](https://github.com/posva) [<samp>(1db94)</samp>](https://github.com/vuejs/router/commit/1db9467c)
- Correct route ordering for group nodes with inflated scores  -  by [@&#8203;posva](https://github.com/posva) [<samp>(515f4)</samp>](https://github.com/vuejs/router/commit/515f4843)
- Cleanup old route overrides  -  by [@&#8203;posva](https://github.com/posva) [<samp>(b28a7)</samp>](https://github.com/vuejs/router/commit/b28a71e2)
- Remove name from \_parent.vue files  -  by [@&#8203;posva](https://github.com/posva) [<samp>(6e8f1)</samp>](https://github.com/vuejs/router/commit/6e8f1a11)
- **ci**:
  - Format sponsor files before change detection  -  by [@&#8203;posva](https://github.com/posva) [<samp>(f68d6)</samp>](https://github.com/vuejs/router/commit/f68d6fad)
  - Use manual git commit in update-sponsors  -  by [@&#8203;posva](https://github.com/posva) [<samp>(8ee99)</samp>](https://github.com/vuejs/router/commit/8ee992cb)
- **experimental**:
  - Resolve TS errors in resolver/router type hierarchy  -  by [@&#8203;posva](https://github.com/posva) [<samp>(a86f1)</samp>](https://github.com/vuejs/router/commit/a86f1f3a)
- **types**:
  - Relax RouteMapGeneric constraint for interface-based RouteNamedMap  -  by [@&#8203;YevheniiKotyrlo](https://github.com/YevheniiKotyrlo) in [#&#8203;2624](https://github.com/vuejs/router/issues/2624) [<samp>(cdf7b)</samp>](https://github.com/vuejs/router/commit/cdf7b442)
- **volar**:
  - Use `ts.getTokenPosOfNode` instead of `node.getStart`  -  by [@&#8203;KazariEX](https://github.com/KazariEX) in [#&#8203;2630](https://github.com/vuejs/router/issues/2630) [<samp>(0b050)</samp>](https://github.com/vuejs/router/commit/0b0504bd)

#####    🏎 Performance

- Avoid merging empty object in record  -  by [@&#8203;posva](https://github.com/posva) [<samp>(4213e)</samp>](https://github.com/vuejs/router/commit/4213eb66)

#####     [View changes on GitHub](https://github.com/vuejs/router/compare/v5.0.2...v5.0.3)

### [`v5.0.2`](https://github.com/vuejs/router/releases/tag/v5.0.2)

[Compare Source](https://github.com/vuejs/router/compare/v5.0.1...v5.0.2)

#####    🐞 Bug Fixes

- Remove devtools from iife build  -  by [@&#8203;posva](https://github.com/posva) [<samp>(58c03)</samp>](https://github.com/vuejs/router/commit/58c033c0)
- Loose version check vue-router  -  by [@&#8203;posva](https://github.com/posva) [<samp>(90e4b)</samp>](https://github.com/vuejs/router/commit/90e4bb80)
- **volar**: Empty options  -  by [@&#8203;posva](https://github.com/posva) [<samp>(02275)</samp>](https://github.com/vuejs/router/commit/022758a7)

#####     [View changes on GitHub](https://github.com/vuejs/router/compare/v5.0.1...v5.0.2)

### [`v5.0.1`](https://github.com/vuejs/router/releases/tag/v5.0.1)

[Compare Source](https://github.com/vuejs/router/compare/v5.0.0...v5.0.1)

#####    🐞 Bug Fixes

- **volar**: Make typed plugin work with vue-tsc  -  by [@&#8203;peter50216](https://github.com/peter50216) in [#&#8203;2607](https://github.com/vuejs/router/issues/2607) [<samp>(7845e)</samp>](https://github.com/vuejs/router/commit/7845e327)

#####     [View changes on GitHub](https://github.com/vuejs/router/compare/v5.0.0...v5.0.1)

### [`v5.0.0`](https://github.com/vuejs/router/releases/tag/v5.0.0)

[Compare Source](https://github.com/vuejs/router/compare/v4.6.4...v5.0.0)

Vue Router 5 is a *boring* release, it merges [unplugin-vue-router](https://uvr.esm.is) into the core package with no breaking changes. The only exception is that the *iife* build no longer includes `@vue/devtools-api` because it has been upgraded to v8 and does not expose an IIFE build itself. You can track that change in [this issue](https://github.com/vuejs/devtools/issues/989). See [the migration guide](https://router.vuejs.org/guide/migration/v4-to-v5.html) for instructions on how to upgrade from unplugin-vue-router to Vue Router 5.

#####    🚀 Features

- **experimental**: Query params are optional by default  -  by [@&#8203;posva](https://github.com/posva) [<samp>(78913)</samp>](https://github.com/vuejs/router/commit/78913551)
- Add volar plugins  -  by [@&#8203;posva](https://github.com/posva) [<samp>(530ac)</samp>](https://github.com/vuejs/router/commit/530ac53e)
- Add data loaders as experimental  -  by [@&#8203;posva](https://github.com/posva) [<samp>(ab895)</samp>](https://github.com/vuejs/router/commit/ab89513d)
- Add route json schema  -  by [@&#8203;posva](https://github.com/posva) [<samp>(20675)</samp>](https://github.com/vuejs/router/commit/2067515a)
- Upgrade devtools-api to v7  -  by [@&#8203;posva](https://github.com/posva) [<samp>(17b84)</samp>](https://github.com/vuejs/router/commit/17b841b8)
- Upgrade devtools to v8  -  by [@&#8203;posva](https://github.com/posva) [<samp>(b8aa2)</samp>](https://github.com/vuejs/router/commit/b8aa2395)
- Runtime error on missing param parsers  -  by [@&#8203;posva](https://github.com/posva) [<samp>(3444b)</samp>](https://github.com/vuejs/router/commit/3444bc94)
- **volar**: Allow rootDir option  -  by [@&#8203;posva](https://github.com/posva) [<samp>(df65a)</samp>](https://github.com/vuejs/router/commit/df65a864)

#####    🐞 Bug Fixes

- Avoid breaking older browsers support  -  by [@&#8203;posva](https://github.com/posva) [<samp>(c7ba4)</samp>](https://github.com/vuejs/router/commit/c7ba4507)
- Trigger navigation guards when keep-alive component is reactivated for different route  -  by [@&#8203;babu-ch](https://github.com/babu-ch) and [@&#8203;posva](https://github.com/posva) in [#&#8203;2604](https://github.com/vuejs/router/issues/2604) [<samp>(c7735)</samp>](https://github.com/vuejs/router/commit/c7735d30)
- Add automatic types for param parsers  -  by [@&#8203;posva](https://github.com/posva) [<samp>(0fb5d)</samp>](https://github.com/vuejs/router/commit/0fb5da34)
- Param parsers when dts is not at root  -  by [@&#8203;posva](https://github.com/posva) [<samp>(16b39)</samp>](https://github.com/vuejs/router/commit/16b39ff7)
- Expose resolveOptions for unplugin  -  by [@&#8203;posva](https://github.com/posva) [<samp>(35543)</samp>](https://github.com/vuejs/router/commit/355431b6)
- Escape tildes in paths  -  by [@&#8203;posva](https://github.com/posva) [<samp>(aac2e)</samp>](https://github.com/vuejs/router/commit/aac2e265)
- **volar**: Upgrade config read  -  by [@&#8203;posva](https://github.com/posva) [<samp>(e3024)</samp>](https://github.com/vuejs/router/commit/e3024d19)

#####     [View changes on GitHub](https://github.com/vuejs/router/compare/v4.6.4...v5.0.0)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4zMS4xIiwidXBkYXRlZEluVmVyIjoiNDMuMzEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Reviewed-on: https://git.keligrubb.com/keligrubb/kestrelos/pulls/12
Co-authored-by: Renovate Bot <renovate@keligrubb.com>
Co-committed-by: Renovate Bot <renovate@keligrubb.com>
2026-02-23 03:44:55 +00:00
CI 7398bb0a16 release v1.0.2 [skip ci] 2026-02-22 20:44:40 +00:00
Renovate Bot 79d36df83c chore(deps): update dependency eslint to v10 (#10)
ci/woodpecker/push/push Pipeline was successful
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [eslint](https://eslint.org) ([source](https://github.com/eslint/eslint)) | [`^9.0.0` → `^10.0.0`](https://renovatebot.com/diffs/npm/eslint/9.39.2/10.0.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/eslint/10.0.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint/9.39.2/10.0.1?slim=true) |

---

### Release Notes

<details>
<summary>eslint/eslint (eslint)</summary>

### [`v10.0.1`](https://github.com/eslint/eslint/releases/tag/v10.0.1)

[Compare Source](https://github.com/eslint/eslint/compare/v10.0.0...v10.0.1)

#### Bug Fixes

- [`c87d5bd`](https://github.com/eslint/eslint/commit/c87d5bded54c5cf491eb04c24c9d09bbbd42c23e) fix: update eslint ([#&#8203;20531](https://github.com/eslint/eslint/issues/20531)) (renovate\[bot])
- [`d841001`](https://github.com/eslint/eslint/commit/d84100115c14691691058f00779c94e74fca946a) fix: update `minimatch` to `10.2.1` to address security vulnerabilities ([#&#8203;20519](https://github.com/eslint/eslint/issues/20519)) (루밀LuMir)
- [`04c2147`](https://github.com/eslint/eslint/commit/04c21475b3004904948f02049f2888b401d82c78) fix: update error message for unused suppressions ([#&#8203;20496](https://github.com/eslint/eslint/issues/20496)) (fnx)
- [`38b089c`](https://github.com/eslint/eslint/commit/38b089c1726feac0e31a31d47941bd99e29ce003) fix: update dependency [@&#8203;eslint/config-array](https://github.com/eslint/config-array) to ^0.23.1 ([#&#8203;20484](https://github.com/eslint/eslint/issues/20484)) (renovate\[bot])

#### Documentation

- [`5b3dbce`](https://github.com/eslint/eslint/commit/5b3dbce50a1404a9f118afe810cefeee79388a2a) docs: add AI acknowledgement section to templates ([#&#8203;20431](https://github.com/eslint/eslint/issues/20431)) (루밀LuMir)
- [`6f23076`](https://github.com/eslint/eslint/commit/6f23076037d5879f20fb3be2ef094293b1e8d38c) docs: toggle nav in no-JS mode ([#&#8203;20476](https://github.com/eslint/eslint/issues/20476)) (Tanuj Kanti)
- [`b69cfb3`](https://github.com/eslint/eslint/commit/b69cfb32a16c5d5e9986390d484fae1d21e406f9) docs: Update README (GitHub Actions Bot)

#### Chores

- [`e5c281f`](https://github.com/eslint/eslint/commit/e5c281ffd038a3a7a3e5364db0b9378e0ad83020) chore: updates for v9.39.3 release (Jenkins)
- [`8c3832a`](https://github.com/eslint/eslint/commit/8c3832adb77cd993b4a24891900d5eeaaf093cdc) chore: update [@&#8203;typescript-eslint/parser](https://github.com/typescript-eslint/parser) to ^8.56.0 ([#&#8203;20514](https://github.com/eslint/eslint/issues/20514)) (Milos Djermanovic)
- [`8330d23`](https://github.com/eslint/eslint/commit/8330d238ae6adb68bb6a1c9381e38cfedd990d94) test: add tests for config-api ([#&#8203;20493](https://github.com/eslint/eslint/issues/20493)) (Milos Djermanovic)
- [`37d6e91`](https://github.com/eslint/eslint/commit/37d6e91e88fa6a2ca6d8726679096acff21ba6cc) chore: remove eslint v10 prereleases from eslint-config-eslint deps ([#&#8203;20494](https://github.com/eslint/eslint/issues/20494)) (Milos Djermanovic)
- [`da7cd0e`](https://github.com/eslint/eslint/commit/da7cd0e79197ad16e17052eef99df141de6dbfb1) refactor: cleanup error message templates ([#&#8203;20479](https://github.com/eslint/eslint/issues/20479)) (Francesco Trotta)
- [`84fb885`](https://github.com/eslint/eslint/commit/84fb885d49ac810e79a9491276b4828b53d913e5) chore: package.json update for [@&#8203;eslint/js](https://github.com/eslint/js) release (Jenkins)
- [`1f66734`](https://github.com/eslint/eslint/commit/1f667344b57c4c09b548d94bcfac1f91b6e5c63d) chore: add `eslint` to `peerDependencies` of `@eslint/js` ([#&#8203;20467](https://github.com/eslint/eslint/issues/20467)) (Milos Djermanovic)

### [`v10.0.0`](https://github.com/eslint/eslint/releases/tag/v10.0.0)

[Compare Source](https://github.com/eslint/eslint/compare/v9.39.3...v10.0.0)

#### Breaking Changes

- [`f9e54f4`](https://github.com/eslint/eslint/commit/f9e54f43a5e497cdfa179338b431093245cb787b) feat!: estimate rule-tester failure location ([#&#8203;20420](https://github.com/eslint/eslint/issues/20420)) (ST-DDT)
- [`a176319`](https://github.com/eslint/eslint/commit/a176319d8ade1a7d9b2d7fb8f038f55a2662325f) feat!: replace `chalk` with `styleText` and add `color` to `ResultsMeta` ([#&#8203;20227](https://github.com/eslint/eslint/issues/20227)) (루밀LuMir)
- [`c7046e6`](https://github.com/eslint/eslint/commit/c7046e6c1e03c4ca0eee4888a1f2eba4c6454f84) feat!: enable JSX reference tracking ([#&#8203;20152](https://github.com/eslint/eslint/issues/20152)) (Pixel998)
- [`fa31a60`](https://github.com/eslint/eslint/commit/fa31a608901684fbcd9906d1907e66561d16e5aa) feat!: add `name` to configs ([#&#8203;20015](https://github.com/eslint/eslint/issues/20015)) (Kirk Waiblinger)
- [`3383e7e`](https://github.com/eslint/eslint/commit/3383e7ec9028166cafc8ea7986c2f7498d0049f0) fix!: remove deprecated `SourceCode` methods ([#&#8203;20137](https://github.com/eslint/eslint/issues/20137)) (Pixel998)
- [`501abd0`](https://github.com/eslint/eslint/commit/501abd0e916a35554c58b7c0365537f1fa3880ce) feat!: update dependency minimatch to v10 ([#&#8203;20246](https://github.com/eslint/eslint/issues/20246)) (renovate\[bot])
- [`ca4d3b4`](https://github.com/eslint/eslint/commit/ca4d3b40085de47561f89656a2207d09946ed45e) fix!: stricter rule tester assertions for valid test cases ([#&#8203;20125](https://github.com/eslint/eslint/issues/20125)) (唯然)
- [`96512a6`](https://github.com/eslint/eslint/commit/96512a66c86402fb0538cdcb6cd30b9073f6bf3b) fix!: Remove deprecated rule context methods ([#&#8203;20086](https://github.com/eslint/eslint/issues/20086)) (Nicholas C. Zakas)
- [`c69fdac`](https://github.com/eslint/eslint/commit/c69fdacdb2e886b9d965568a397aa8220db3fe90) feat!: remove eslintrc support ([#&#8203;20037](https://github.com/eslint/eslint/issues/20037)) (Francesco Trotta)
- [`208b5cc`](https://github.com/eslint/eslint/commit/208b5cc34a8374ff81412b5bec2e0800eebfbd04) feat!: Use `ScopeManager#addGlobals()` ([#&#8203;20132](https://github.com/eslint/eslint/issues/20132)) (Milos Djermanovic)
- [`a2ee188`](https://github.com/eslint/eslint/commit/a2ee188ea7a38a0c6155f3d39e2b00e1d0f36e14) fix!: add `uniqueItems: true` in `no-invalid-regexp` option ([#&#8203;20155](https://github.com/eslint/eslint/issues/20155)) (Tanuj Kanti)
- [`a89059d`](https://github.com/eslint/eslint/commit/a89059dbf2832d417dd493ee81483227ec44e4ab) feat!: Program range span entire source text ([#&#8203;20133](https://github.com/eslint/eslint/issues/20133)) (Pixel998)
- [`39a6424`](https://github.com/eslint/eslint/commit/39a6424373d915fa9de0d7b0caba9a4dc3da9b53) fix!: assert 'text' is a string across all RuleFixer methods ([#&#8203;20082](https://github.com/eslint/eslint/issues/20082)) (Pixel998)
- [`f28fbf8`](https://github.com/eslint/eslint/commit/f28fbf846244e043c92b355b224d121b06140b44) fix!: Deprecate `"always"` and `"as-needed"` options of the `radix` rule ([#&#8203;20223](https://github.com/eslint/eslint/issues/20223)) (Milos Djermanovic)
- [`aa3fb2b`](https://github.com/eslint/eslint/commit/aa3fb2b233e929b37220be940575f42c280e0b98) fix!: tighten `func-names` schema ([#&#8203;20119](https://github.com/eslint/eslint/issues/20119)) (Pixel998)
- [`f6c0ed0`](https://github.com/eslint/eslint/commit/f6c0ed0311dcfee853367d5068c765d066e6b756) feat!: report `eslint-env` comments as errors ([#&#8203;20128](https://github.com/eslint/eslint/issues/20128)) (Francesco Trotta)
- [`4bf739f`](https://github.com/eslint/eslint/commit/4bf739fb533e59f7f0a66b65f7bc80be0f37d8db) fix!: remove deprecated `LintMessage#nodeType` and `TestCaseError#type` ([#&#8203;20096](https://github.com/eslint/eslint/issues/20096)) (Pixel998)
- [`523c076`](https://github.com/eslint/eslint/commit/523c076866400670fb2192a3f55dbf7ad3469247) feat!: drop support for jiti < 2.2.0 ([#&#8203;20016](https://github.com/eslint/eslint/issues/20016)) (michael faith)
- [`454a292`](https://github.com/eslint/eslint/commit/454a292c95f34dad232411ddac06408e6383bb64) feat!: update `eslint:recommended` configuration ([#&#8203;20210](https://github.com/eslint/eslint/issues/20210)) (Pixel998)
- [`4f880ee`](https://github.com/eslint/eslint/commit/4f880ee02992e1bf0e96ebaba679985e2d1295f1) feat!: remove `v10_*` and inactive `unstable_*` flags ([#&#8203;20225](https://github.com/eslint/eslint/issues/20225)) (sethamus)
- [`f18115c`](https://github.com/eslint/eslint/commit/f18115c363a4ac7671a4c7f30ee13d57ebba330f) feat!: `no-shadow-restricted-names` report `globalThis` by default ([#&#8203;20027](https://github.com/eslint/eslint/issues/20027)) (sethamus)
- [`c6358c3`](https://github.com/eslint/eslint/commit/c6358c31fbd3937b92d89be2618ffdf5a774604e) feat!: Require Node.js `^20.19.0 || ^22.13.0 || >=24` ([#&#8203;20160](https://github.com/eslint/eslint/issues/20160)) (Milos Djermanovic)

#### Features

- [`bff9091`](https://github.com/eslint/eslint/commit/bff9091927811497dbf066b0e3b85ecb37d43822) feat: handle `Array.fromAsync` in `array-callback-return` ([#&#8203;20457](https://github.com/eslint/eslint/issues/20457)) (Francesco Trotta)
- [`290c594`](https://github.com/eslint/eslint/commit/290c594bb50c439fb71bc75521ee5360daa8c222) feat: add `self` to `no-implied-eval` rule ([#&#8203;20468](https://github.com/eslint/eslint/issues/20468)) (sethamus)
- [`43677de`](https://github.com/eslint/eslint/commit/43677de07ebd6e14bfac40a46ad749ba783c45f2) feat: fix handling of function and class expression names in `no-shadow` ([#&#8203;20432](https://github.com/eslint/eslint/issues/20432)) (Milos Djermanovic)
- [`f0cafe5`](https://github.com/eslint/eslint/commit/f0cafe5f37e7765e9d8c2751b5f5d33107687009) feat: rule tester add assertion option `requireData` ([#&#8203;20409](https://github.com/eslint/eslint/issues/20409)) (fnx)
- [`f7ab693`](https://github.com/eslint/eslint/commit/f7ab6937e63bc618d326710858f5861a68f80616) feat: output RuleTester test case failure index ([#&#8203;19976](https://github.com/eslint/eslint/issues/19976)) (ST-DDT)
- [`7cbcbf9`](https://github.com/eslint/eslint/commit/7cbcbf9c3c2008deee7d143ae35e668e8ffbccb3) feat: add `countThis` option to `max-params` ([#&#8203;20236](https://github.com/eslint/eslint/issues/20236)) (Gerkin)
- [`f148a5e`](https://github.com/eslint/eslint/commit/f148a5eaa1e89dd80ade62f0a690186b00b9f6e1) feat: add error assertion options ([#&#8203;20247](https://github.com/eslint/eslint/issues/20247)) (ST-DDT)
- [`09e6654`](https://github.com/eslint/eslint/commit/09e66549ecada6dcb8c567a60faf044fce049188) feat: update error loc of `require-yield` and `no-useless-constructor` ([#&#8203;20267](https://github.com/eslint/eslint/issues/20267)) (Tanuj Kanti)

#### Bug Fixes

- [`436b82f`](https://github.com/eslint/eslint/commit/436b82f3c0a8cfa2fdc17d173e95ea11d5d3ee03) fix: update eslint ([#&#8203;20473](https://github.com/eslint/eslint/issues/20473)) (renovate\[bot])
- [`1d29d22`](https://github.com/eslint/eslint/commit/1d29d22fe302443cec2a11da0816397f94af97ec) fix: detect default `this` binding in `Array.fromAsync` callbacks ([#&#8203;20456](https://github.com/eslint/eslint/issues/20456)) (Francesco Trotta)
- [`727451e`](https://github.com/eslint/eslint/commit/727451eff55b35d853e0e443d0de58f4550762bf) fix: fix regression of global mode report range in `strict` rule ([#&#8203;20462](https://github.com/eslint/eslint/issues/20462)) (ntnyq)
- [`e80485f`](https://github.com/eslint/eslint/commit/e80485fcd27196fa0b6f6b5c7ac8cf49ad4b079d) fix: remove fake `FlatESLint` and `LegacyESLint` exports ([#&#8203;20460](https://github.com/eslint/eslint/issues/20460)) (Francesco Trotta)
- [`9eeff3b`](https://github.com/eslint/eslint/commit/9eeff3bc13813a786b8a4c3815def97c0fb646ef) fix: update esquery ([#&#8203;20423](https://github.com/eslint/eslint/issues/20423)) (cryptnix)
- [`b34b938`](https://github.com/eslint/eslint/commit/b34b93852d014ebbcf3538d892b55e0216cdf681) fix: use `Error.prepareStackTrace` to estimate failing test location ([#&#8203;20436](https://github.com/eslint/eslint/issues/20436)) (Francesco Trotta)
- [`51aab53`](https://github.com/eslint/eslint/commit/51aab5393b058f7cbed69041a9069b2bd106aabd) fix: update eslint ([#&#8203;20443](https://github.com/eslint/eslint/issues/20443)) (renovate\[bot])
- [`23490b2`](https://github.com/eslint/eslint/commit/23490b266276792896a0b7b43c49a1ce87bf8568) fix: handle space before colon in `RuleTester` location estimation ([#&#8203;20433](https://github.com/eslint/eslint/issues/20433)) (Francesco Trotta)
- [`f244dbf`](https://github.com/eslint/eslint/commit/f244dbf2191267a4cafd08645243624baf3e8c83) fix: use `MessagePlaceholderData` type from `@eslint/core` ([#&#8203;20348](https://github.com/eslint/eslint/issues/20348)) (루밀LuMir)
- [`d186f8c`](https://github.com/eslint/eslint/commit/d186f8c0747f14890e86a5a39708b052b391ddaf) fix: update eslint ([#&#8203;20427](https://github.com/eslint/eslint/issues/20427)) (renovate\[bot])
- [`2332262`](https://github.com/eslint/eslint/commit/2332262deb4ef3188b210595896bb0ff552a7e66) fix: error location should not modify error message in RuleTester ([#&#8203;20421](https://github.com/eslint/eslint/issues/20421)) (Milos Djermanovic)
- [`ab99b21`](https://github.com/eslint/eslint/commit/ab99b21a6715dee1035d8f4e6d6841853eb5563f) fix: ensure `filename` is passed as third argument to `verifyAndFix()` ([#&#8203;20405](https://github.com/eslint/eslint/issues/20405)) (루밀LuMir)
- [`8a60f3b`](https://github.com/eslint/eslint/commit/8a60f3bc80ad96c65feeb29886342623c630199c) fix: remove `ecmaVersion` and `sourceType` from `ParserOptions` type ([#&#8203;20415](https://github.com/eslint/eslint/issues/20415)) (Pixel998)
- [`eafd727`](https://github.com/eslint/eslint/commit/eafd727a060131f7fc79b2eb5698d8d27683c3a2) fix: remove `TDZ` scope type ([#&#8203;20231](https://github.com/eslint/eslint/issues/20231)) (jaymarvelz)
- [`39d1f51`](https://github.com/eslint/eslint/commit/39d1f51680d4fbade16b4d9c07ad61a87ee3b1ea) fix: correct `Scope` typings ([#&#8203;20404](https://github.com/eslint/eslint/issues/20404)) (sethamus)
- [`2bd0f13`](https://github.com/eslint/eslint/commit/2bd0f13a92fb373827f16210aa4748d4885fddb1) fix: update `verify` and `verifyAndFix` types ([#&#8203;20384](https://github.com/eslint/eslint/issues/20384)) (Francesco Trotta)
- [`ba6ebfa`](https://github.com/eslint/eslint/commit/ba6ebfa78de0b8522cea5ee80179887e92c6c935) fix: correct typings for `loadESLint()` and `shouldUseFlatConfig()` ([#&#8203;20393](https://github.com/eslint/eslint/issues/20393)) (루밀LuMir)
- [`e7673ae`](https://github.com/eslint/eslint/commit/e7673ae096900330599680efe91f8a199a5c2e59) fix: correct RuleTester typings ([#&#8203;20105](https://github.com/eslint/eslint/issues/20105)) (Pixel998)
- [`53e9522`](https://github.com/eslint/eslint/commit/53e95222af8561a8eed282fa9fd44b2f320a3c37) fix: strict removed formatters check ([#&#8203;20241](https://github.com/eslint/eslint/issues/20241)) (ntnyq)
- [`b017f09`](https://github.com/eslint/eslint/commit/b017f094d4e53728f8d335b9cf8b16dc074afda3) fix: correct `no-restricted-import` messages ([#&#8203;20374](https://github.com/eslint/eslint/issues/20374)) (Francesco Trotta)

#### Documentation

- [`e978dda`](https://github.com/eslint/eslint/commit/e978ddaab7e6a3c38b4a2afa721148a6ef38f29a) docs: Update README (GitHub Actions Bot)
- [`4cecf83`](https://github.com/eslint/eslint/commit/4cecf8393ae9af18c4cfd50621115eb23b3d0cb6) docs: Update README (GitHub Actions Bot)
- [`c79f0ab`](https://github.com/eslint/eslint/commit/c79f0ab2e2d242a93b08ff2f6a0712e2ef60b7b8) docs: Update README (GitHub Actions Bot)
- [`773c052`](https://github.com/eslint/eslint/commit/773c0527c72c09fb5e63c2036b5cb9783f1f04d3) docs: Update README (GitHub Actions Bot)
- [`f2962e4`](https://github.com/eslint/eslint/commit/f2962e46a0e8ee8e04d76e9d899f6a7c73a646f1) docs: document `meta.docs.frozen` property ([#&#8203;20475](https://github.com/eslint/eslint/issues/20475)) (Pixel998)
- [`8e94f58`](https://github.com/eslint/eslint/commit/8e94f58bebfd854eed814a39e19dea4e3c3ee4a3) docs: fix broken anchor links from gerund heading updates ([#&#8203;20449](https://github.com/eslint/eslint/issues/20449)) (Copilot)
- [`1495654`](https://github.com/eslint/eslint/commit/14956543d42ab542f72820f38941d0bcc39a1fbb) docs: Update README (GitHub Actions Bot)
- [`0b8ed5c`](https://github.com/eslint/eslint/commit/0b8ed5c0aa4222a9b6b185c605cfedaef4662dcb) docs: document support for `:is` selector alias ([#&#8203;20454](https://github.com/eslint/eslint/issues/20454)) (sethamus)
- [`1c4b33f`](https://github.com/eslint/eslint/commit/1c4b33fe8620dcaafbe6e8f4e9515b624476548c) docs: Document policies about ESM-only dependencies ([#&#8203;20448](https://github.com/eslint/eslint/issues/20448)) (Milos Djermanovic)
- [`3e5d38c`](https://github.com/eslint/eslint/commit/3e5d38cdd5712bef50d440585b0f6669a2e9a9b9) docs: add missing indentation space in rule example ([#&#8203;20446](https://github.com/eslint/eslint/issues/20446)) (fnx)
- [`63a0c7c`](https://github.com/eslint/eslint/commit/63a0c7c84bf5b12357893ea2bf0482aa3c855bac) docs: Update README (GitHub Actions Bot)
- [`65ed0c9`](https://github.com/eslint/eslint/commit/65ed0c94e7cd1e3f882956113228311d8c7b3463) docs: Update README (GitHub Actions Bot)
- [`b0e4717`](https://github.com/eslint/eslint/commit/b0e4717d6619ffd02913cf3633b44d8e6953d938) docs: \[no-await-in-loop] Expand inapplicability ([#&#8203;20363](https://github.com/eslint/eslint/issues/20363)) (Niklas Hambüchen)
- [`fca421f`](https://github.com/eslint/eslint/commit/fca421f6a4eecd52f2a7ae5765bd9008f62f9994) docs: Update README (GitHub Actions Bot)
- [`d925c54`](https://github.com/eslint/eslint/commit/d925c54f045b2230d3404e8aa18f4e2860a35e1d) docs: update config syntax in `no-lone-blocks` ([#&#8203;20413](https://github.com/eslint/eslint/issues/20413)) (Pixel998)
- [`7d5c95f`](https://github.com/eslint/eslint/commit/7d5c95f281cb88868f4e09ca07fbbc6394d78c41) docs: remove redundant `sourceType: "module"` from rule examples ([#&#8203;20412](https://github.com/eslint/eslint/issues/20412)) (Pixel998)
- [`02e7e71`](https://github.com/eslint/eslint/commit/02e7e7126366fc5eeffb713f865d80a759dc14b0) docs: correct `.mts` glob pattern in files with extensions example ([#&#8203;20403](https://github.com/eslint/eslint/issues/20403)) (Ali Essalihi)
- [`264b981`](https://github.com/eslint/eslint/commit/264b981101a3cf0c12eba200ac64e5523186a89f) docs: Update README (GitHub Actions Bot)
- [`5a4324f`](https://github.com/eslint/eslint/commit/5a4324f38e7ce370038351ef7412dcf8548c105e) docs: clarify `"local"` option of `no-unused-vars` ([#&#8203;20385](https://github.com/eslint/eslint/issues/20385)) (Milos Djermanovic)
- [`e593aa0`](https://github.com/eslint/eslint/commit/e593aa0fd29f51edea787815ffc847aa723ef1f8) docs: improve clarity, grammar, and wording in documentation site README ([#&#8203;20370](https://github.com/eslint/eslint/issues/20370)) (Aditya)
- [`3f5062e`](https://github.com/eslint/eslint/commit/3f5062ed5f27eb25414faced2478ae076906874e) docs: Add messages property to rule meta documentation ([#&#8203;20361](https://github.com/eslint/eslint/issues/20361)) (Sabya Sachi)
- [`9e5a5c2`](https://github.com/eslint/eslint/commit/9e5a5c2b6b368cdacd678eabf36b441bd8bb726c) docs: remove `Examples` headings from rule docs ([#&#8203;20364](https://github.com/eslint/eslint/issues/20364)) (Milos Djermanovic)
- [`194f488`](https://github.com/eslint/eslint/commit/194f488a8dc97850485afe704d2a64096582f96d) docs: Update README (GitHub Actions Bot)
- [`0f5a94a`](https://github.com/eslint/eslint/commit/0f5a94a84beee19f376025c74f703f275d52c94b) docs: \[class-methods-use-this] explain purpose of rule ([#&#8203;20008](https://github.com/eslint/eslint/issues/20008)) (Kirk Waiblinger)
- [`df5566f`](https://github.com/eslint/eslint/commit/df5566f826d9f5740546e473aa6876b1f7d2f12c) docs: add Options section to all rule docs ([#&#8203;20296](https://github.com/eslint/eslint/issues/20296)) (sethamus)
- [`adf7a2b`](https://github.com/eslint/eslint/commit/adf7a2b202743a98edc454890574292dd2b34837) docs: no-unsafe-finally note for generator functions ([#&#8203;20330](https://github.com/eslint/eslint/issues/20330)) (Tom Pereira)
- [`ef7028c`](https://github.com/eslint/eslint/commit/ef7028c9688dc931051a4217637eb971efcbd71b) docs: Update README (GitHub Actions Bot)
- [`fbae5d1`](https://github.com/eslint/eslint/commit/fbae5d18854b30ea3b696672c7699cef3ec92140) docs: consistently use "v10.0.0" in migration guide ([#&#8203;20328](https://github.com/eslint/eslint/issues/20328)) (Pixel998)
- [`778aa2d`](https://github.com/eslint/eslint/commit/778aa2d83e1ef1e2bd1577ee976c5a43472a3dbe) docs: ignoring default file patterns ([#&#8203;20312](https://github.com/eslint/eslint/issues/20312)) (Tanuj Kanti)
- [`4b5dbcd`](https://github.com/eslint/eslint/commit/4b5dbcdae52c1c16293dc68028cab18ed2504841) docs: reorder v10 migration guide ([#&#8203;20315](https://github.com/eslint/eslint/issues/20315)) (Milos Djermanovic)
- [`5d84a73`](https://github.com/eslint/eslint/commit/5d84a7371d01ead1b274600c055fe49150d487f1) docs: Update README (GitHub Actions Bot)
- [`37c8863`](https://github.com/eslint/eslint/commit/37c8863088a2d7e845d019f68a329f53a3fe2c35) docs: fix incorrect anchor link in v10 migration guide ([#&#8203;20299](https://github.com/eslint/eslint/issues/20299)) (Pixel998)
- [`077ff02`](https://github.com/eslint/eslint/commit/077ff028b6ce036da091d2f7ed8c606c9d017468) docs: add migrate-to-10.0.0 doc ([#&#8203;20143](https://github.com/eslint/eslint/issues/20143)) (唯然)
- [`3822e1b`](https://github.com/eslint/eslint/commit/3822e1b768bb4a64b72b73b5657737a6ee5c8afe) docs: Update README (GitHub Actions Bot)

#### Build Related

- [`9f08712`](https://github.com/eslint/eslint/commit/9f0871236e90ec78bcdbfa352cc1363b4bae5596) Build: changelog update for 10.0.0-rc.2 (Jenkins)
- [`1e2c449`](https://github.com/eslint/eslint/commit/1e2c449701524b426022fde19144b1d22d8197b0) Build: changelog update for 10.0.0-rc.1 (Jenkins)
- [`c4c72a8`](https://github.com/eslint/eslint/commit/c4c72a8d996dda629e85e78a6ef5417242594b5d) Build: changelog update for 10.0.0-rc.0 (Jenkins)
- [`7e4daf9`](https://github.com/eslint/eslint/commit/7e4daf93d255ed343d68e999aad167bb20e5a96b) Build: changelog update for 10.0.0-beta.0 (Jenkins)
- [`a126a2a`](https://github.com/eslint/eslint/commit/a126a2ab136406017f2dac2d7632114e37e62dc2) build: add .scss files entry to knip ([#&#8203;20389](https://github.com/eslint/eslint/issues/20389)) (Francesco Trotta)
- [`f5c0193`](https://github.com/eslint/eslint/commit/f5c01932f69189b260646d60b28011c55870e65d) Build: changelog update for 10.0.0-alpha.1 (Jenkins)
- [`165326f`](https://github.com/eslint/eslint/commit/165326f0469dd6a9b33598a6fceb66336bb2deb5) Build: changelog update for 10.0.0-alpha.0 (Jenkins)

#### Chores

- [`1ece282`](https://github.com/eslint/eslint/commit/1ece282c2286b5dc187ece2a793dbd8798f20bd7) chore: ignore `/docs/v9.x` in link checker ([#&#8203;20452](https://github.com/eslint/eslint/issues/20452)) (Milos Djermanovic)
- [`034e139`](https://github.com/eslint/eslint/commit/034e1397446205e83eb341354605380195c88633) ci: add type integration test for `@html-eslint/eslint-plugin` ([#&#8203;20345](https://github.com/eslint/eslint/issues/20345)) (sethamus)
- [`f3fbc2f`](https://github.com/eslint/eslint/commit/f3fbc2f60cbe2c718364feb8c3fc0452c0df3c56) chore: set `@eslint/js` version to 10.0.0 to skip releasing it ([#&#8203;20466](https://github.com/eslint/eslint/issues/20466)) (Milos Djermanovic)
- [`afc0681`](https://github.com/eslint/eslint/commit/afc06817bbd0625c7b0a46bdc81c38dab0c99441) chore: remove scopeManager.addGlobals patch for typescript-eslint parser ([#&#8203;20461](https://github.com/eslint/eslint/issues/20461)) (fnx)
- [`3e5a173`](https://github.com/eslint/eslint/commit/3e5a173053fe0bb3d0f29aff12eb2c19ae21aa36) refactor: use types from `@eslint/plugin-kit` ([#&#8203;20435](https://github.com/eslint/eslint/issues/20435)) (Pixel998)
- [`11644b1`](https://github.com/eslint/eslint/commit/11644b1dc2bdf4c4f3a97901932e5f25c9f60775) ci: rename workflows ([#&#8203;20463](https://github.com/eslint/eslint/issues/20463)) (Milos Djermanovic)
- [`2d14173`](https://github.com/eslint/eslint/commit/2d14173729ae75fe562430dd5e37c457f44bc7ac) chore: fix typos in docs and comments ([#&#8203;20458](https://github.com/eslint/eslint/issues/20458)) (o-m12a)
- [`6742f92`](https://github.com/eslint/eslint/commit/6742f927ba6afb1bce6f64b9b072a1a11dbf53c4) test: add endLine/endColumn to invalid test case in no-alert ([#&#8203;20441](https://github.com/eslint/eslint/issues/20441)) (경하)
- [`3e22c82`](https://github.com/eslint/eslint/commit/3e22c82a87f44f7407ff75b17b26f1ceed3edd14) test: add missing location data to no-template-curly-in-string tests ([#&#8203;20440](https://github.com/eslint/eslint/issues/20440)) (Haeun Kim)
- [`b4b3127`](https://github.com/eslint/eslint/commit/b4b3127f8542c599ce2dea804b6582ebc40c993d) chore: package.json update for [@&#8203;eslint/js](https://github.com/eslint/js) release (Jenkins)
- [`f658419`](https://github.com/eslint/eslint/commit/f6584191cb5cabd62f6a197339a91e1f9b3f8432) refactor: remove `raw` parser option from JS language ([#&#8203;20416](https://github.com/eslint/eslint/issues/20416)) (Pixel998)
- [`2c3efb7`](https://github.com/eslint/eslint/commit/2c3efb728b294b74a240ec24c7be8137a31cf5f0) chore: remove `category` from type test fixtures ([#&#8203;20417](https://github.com/eslint/eslint/issues/20417)) (Pixel998)
- [`36193fd`](https://github.com/eslint/eslint/commit/36193fd9ad27764d8e4a24ce7c7bbeeaf5d4a6ba) chore: remove `category` from formatter test fixtures ([#&#8203;20418](https://github.com/eslint/eslint/issues/20418)) (Pixel998)
- [`e8d203b`](https://github.com/eslint/eslint/commit/e8d203b0d9f66e55841863f90d215fd83b7eee0f) chore: add JSX language tag validation to `check-rule-examples` ([#&#8203;20414](https://github.com/eslint/eslint/issues/20414)) (Pixel998)
- [`bc465a1`](https://github.com/eslint/eslint/commit/bc465a1e9d955b6e53a45d1b5da7c632dae77262) chore: pin dependencies ([#&#8203;20397](https://github.com/eslint/eslint/issues/20397)) (renovate\[bot])
- [`703f0f5`](https://github.com/eslint/eslint/commit/703f0f551daea28767e5a68a00e335928919a7ff) test: replace deprecated rules in `linter` tests ([#&#8203;20406](https://github.com/eslint/eslint/issues/20406)) (루밀LuMir)
- [`ba71baa`](https://github.com/eslint/eslint/commit/ba71baa87265888b582f314163df1d727441e2f1) test: enable `strict` mode in type tests ([#&#8203;20398](https://github.com/eslint/eslint/issues/20398)) (루밀LuMir)
- [`f9c4968`](https://github.com/eslint/eslint/commit/f9c49683a6d69ff0b5425803955fc226f7e05d76) refactor: remove `lib/linter/rules.js` ([#&#8203;20399](https://github.com/eslint/eslint/issues/20399)) (Francesco Trotta)
- [`6f1c48e`](https://github.com/eslint/eslint/commit/6f1c48e5e7f8195f7796ea04e756841391ada927) chore: updates for v9.39.2 release (Jenkins)
- [`54bf0a3`](https://github.com/eslint/eslint/commit/54bf0a3646265060f5f22faef71ec840d630c701) ci: create package manager test ([#&#8203;20392](https://github.com/eslint/eslint/issues/20392)) (루밀LuMir)
- [`3115021`](https://github.com/eslint/eslint/commit/3115021439490d1ed12da5804902ebbf8a5e574b) refactor: simplify JSDoc comment detection logic ([#&#8203;20360](https://github.com/eslint/eslint/issues/20360)) (Pixel998)
- [`4345b17`](https://github.com/eslint/eslint/commit/4345b172a81e1394579ec09df51ba460b956c3b5) chore: update `@eslint-community/regexpp` to `4.12.2` ([#&#8203;20366](https://github.com/eslint/eslint/issues/20366)) (루밀LuMir)
- [`772c9ee`](https://github.com/eslint/eslint/commit/772c9ee9b65b6ad0be3e46462a7f93c37578cfa8) chore: update dependency [@&#8203;eslint/eslintrc](https://github.com/eslint/eslintrc) to ^3.3.3 ([#&#8203;20359](https://github.com/eslint/eslint/issues/20359)) (renovate\[bot])
- [`0b14059`](https://github.com/eslint/eslint/commit/0b14059491d830a49b3577931f4f68fbcfce6be5) chore: package.json update for [@&#8203;eslint/js](https://github.com/eslint/js) release (Jenkins)
- [`d6e7bf3`](https://github.com/eslint/eslint/commit/d6e7bf3064be01d159d6856e3718672c6a97a8e1) ci: bump actions/checkout from 5 to 6 ([#&#8203;20350](https://github.com/eslint/eslint/issues/20350)) (dependabot\[bot])
- [`139d456`](https://github.com/eslint/eslint/commit/139d4567d4afe3f1e1cdae21769d5e868f90ef0d) chore: require mandatory headers in rule docs ([#&#8203;20347](https://github.com/eslint/eslint/issues/20347)) (Milos Djermanovic)
- [`3b0289c`](https://github.com/eslint/eslint/commit/3b0289c7b605b2d94fe2d0c347d07eea4b6ba1d4) chore: remove unused `.eslintignore` and test fixtures ([#&#8203;20316](https://github.com/eslint/eslint/issues/20316)) (Pixel998)
- [`a463e7b`](https://github.com/eslint/eslint/commit/a463e7bea0d18af55e5557e33691e4b0685d9523) chore: update dependency js-yaml to v4 \[security] ([#&#8203;20319](https://github.com/eslint/eslint/issues/20319)) (renovate\[bot])
- [`ebfe905`](https://github.com/eslint/eslint/commit/ebfe90533d07a7020a5c63b93763fe537120f61f) chore: remove redundant rules from eslint-config-eslint ([#&#8203;20327](https://github.com/eslint/eslint/issues/20327)) (Milos Djermanovic)
- [`88dfdb2`](https://github.com/eslint/eslint/commit/88dfdb23ee541de4e9c3aa5d8a152c5980f6cc3f) test: add regression tests for message placeholder interpolation ([#&#8203;20318](https://github.com/eslint/eslint/issues/20318)) (fnx)
- [`6ed0f75`](https://github.com/eslint/eslint/commit/6ed0f758ff460b7a182c8d16b0487ae707e43cc9) chore: skip type checking in `eslint-config-eslint` ([#&#8203;20323](https://github.com/eslint/eslint/issues/20323)) (Francesco Trotta)
- [`1e2cad5`](https://github.com/eslint/eslint/commit/1e2cad5f6fa47ed6ed89d2a29798dda926d50990) chore: package.json update for [@&#8203;eslint/js](https://github.com/eslint/js) release (Jenkins)
- [`9da2679`](https://github.com/eslint/eslint/commit/9da26798483270a2c3c490c41cbd8f0c28edf75a) chore: update `@eslint/*` dependencies ([#&#8203;20321](https://github.com/eslint/eslint/issues/20321)) (Milos Djermanovic)
- [`0439794`](https://github.com/eslint/eslint/commit/043979418161e1c17becef31b1dd5c6e1b031e98) refactor: use types from [@&#8203;eslint/core](https://github.com/eslint/core) ([#&#8203;20235](https://github.com/eslint/eslint/issues/20235)) (jaymarvelz)
- [`cb51ec2`](https://github.com/eslint/eslint/commit/cb51ec2d6d3b729bf02a5e6b58b236578c6cce42) test: cleanup `SourceCode#traverse` tests ([#&#8203;20289](https://github.com/eslint/eslint/issues/20289)) (Milos Djermanovic)
- [`897a347`](https://github.com/eslint/eslint/commit/897a3471d6da073c1a179fa84f7a3fe72973ec45) chore: remove restriction for `type` in rule tests ([#&#8203;20305](https://github.com/eslint/eslint/issues/20305)) (Pixel998)
- [`d972098`](https://github.com/eslint/eslint/commit/d9720988579734da7323fbacca4c67058651d6ff) chore: ignore prettier updates in renovate to keep in sync with trunk ([#&#8203;20304](https://github.com/eslint/eslint/issues/20304)) (Pixel998)
- [`a086359`](https://github.com/eslint/eslint/commit/a0863593872fe01b5dd0e04c682450c26ae40ac8) chore: remove redundant `fast-glob` dev-dependency ([#&#8203;20301](https://github.com/eslint/eslint/issues/20301)) (루밀LuMir)
- [`564b302`](https://github.com/eslint/eslint/commit/564b30215c3c1aba47bc29f948f11db5c824cacd) chore: install `prettier` as a dev dependency ([#&#8203;20302](https://github.com/eslint/eslint/issues/20302)) (michael faith)
- [`8257b57`](https://github.com/eslint/eslint/commit/8257b5729d6a26f88b079aa389df4ecea4451a80) refactor: correct regex for `eslint-plugin/report-message-format` ([#&#8203;20300](https://github.com/eslint/eslint/issues/20300)) (루밀LuMir)
- [`e251671`](https://github.com/eslint/eslint/commit/e2516713bc9ae62117da3f490d9cb6a9676f44fe) refactor: extract assertions in RuleTester ([#&#8203;20135](https://github.com/eslint/eslint/issues/20135)) (唯然)
- [`2e7f25e`](https://github.com/eslint/eslint/commit/2e7f25e18908e66d9bd1a4dc016709e39e19a24d) chore: add `legacy-peer-deps` to `.npmrc` ([#&#8203;20281](https://github.com/eslint/eslint/issues/20281)) (Milos Djermanovic)
- [`39c638a`](https://github.com/eslint/eslint/commit/39c638a9aeb7ddc353684d536bbf69d1d39380bd) chore: update eslint-config-eslint dependencies for v10 prereleases ([#&#8203;20278](https://github.com/eslint/eslint/issues/20278)) (Milos Djermanovic)
- [`8533b3f`](https://github.com/eslint/eslint/commit/8533b3fa281e6ecc481083ee83e9c34cae22f31c) chore: update dependency [@&#8203;eslint/json](https://github.com/eslint/json) to ^0.14.0 ([#&#8203;20288](https://github.com/eslint/eslint/issues/20288)) (renovate\[bot])
- [`796ddf6`](https://github.com/eslint/eslint/commit/796ddf6db5c8fe3e098aa3198128f8ce3c58f8e0) chore: update dependency [@&#8203;eslint/js](https://github.com/eslint/js) to ^9.39.1 ([#&#8203;20285](https://github.com/eslint/eslint/issues/20285)) (renovate\[bot])

### [`v9.39.3`](https://github.com/eslint/eslint/releases/tag/v9.39.3)

[Compare Source](https://github.com/eslint/eslint/compare/v9.39.2...v9.39.3)

#### Bug Fixes

- [`791bf8d`](https://github.com/eslint/eslint/commit/791bf8d7e76ce7ab8c88cb8982658823da4eff27) fix: restore TypeScript 4.0 compatibility in types ([#&#8203;20504](https://github.com/eslint/eslint/issues/20504)) (sethamus)

#### Chores

- [`8594a43`](https://github.com/eslint/eslint/commit/8594a436c22a0167fe3c2c4109bbdb04e519a014) chore: upgrade [@&#8203;eslint/js](https://github.com/eslint/js)@&#8203;9.39.3 ([#&#8203;20529](https://github.com/eslint/eslint/issues/20529)) (Milos Djermanovic)
- [`9ceef92`](https://github.com/eslint/eslint/commit/9ceef92fbd3d1298d9a00483f86897834b88acac) chore: package.json update for [@&#8203;eslint/js](https://github.com/eslint/js) release (Jenkins)
- [`af498c6`](https://github.com/eslint/eslint/commit/af498c63b9ca065223a425a85afabdcc8451e69b) chore: ignore `/docs/v9.x` in link checker ([#&#8203;20453](https://github.com/eslint/eslint/issues/20453)) (Milos Djermanovic)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4zMS4xIiwidXBkYXRlZEluVmVyIjoiNDMuMzEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Reviewed-on: https://git.keligrubb.com/keligrubb/kestrelos/pulls/10
Co-authored-by: Renovate Bot <renovate@keligrubb.com>
Co-committed-by: Renovate Bot <renovate@keligrubb.com>
2026-02-22 20:43:48 +00:00
72 changed files with 9701 additions and 6778 deletions
+92
View File
@@ -0,0 +1,92 @@
name: PR
on:
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: https://git.keligrubb.com/actions/checkout@v7
- name: Setup Node.js
uses: https://git.keligrubb.com/actions/setup-node@v7
with:
node-version: "24"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run lint
run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: https://git.keligrubb.com/actions/checkout@v7
- name: Setup Node.js
uses: https://git.keligrubb.com/actions/setup-node@v7
with:
node-version: "24"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test
e2e:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.62.1-noble
steps:
- uses: https://git.keligrubb.com/actions/checkout@v7
- name: Setup Node.js
uses: https://git.keligrubb.com/actions/setup-node@v7
with:
node-version: "24"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Generate dev cert
run: ./scripts/gen-dev-cert.sh
- name: Run e2e tests
run: npm run test:e2e
env:
NODE_TLS_REJECT_UNAUTHORIZED: "0"
docker-build:
runs-on: ubuntu-latest
steps:
- uses: https://git.keligrubb.com/actions/checkout@v7
- name: Set Docker image tag
id: image
run: |
REGISTRY="${GITHUB_SERVER_URL#https://}"
REGISTRY="${REGISTRY#http://}"
echo "tag=${REGISTRY}/${{ github.repository }}:latest" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: https://git.keligrubb.com/actions/docker-setup-buildx-action@v4
- name: Build (dry run)
uses: https://git.keligrubb.com/actions/docker-build-push-action@v7
env:
# Keeps GITHUB_OUTPUT small; Gitea act-runner can choke on multiline
# outputs when PR webhook payloads (e.g. Renovate bodies) are huge.
DOCKER_BUILD_SUMMARY: "false"
DOCKER_BUILD_RECORD_UPLOAD: "false"
with:
context: .
push: false
provenance: false
sbom: false
tags: ${{ steps.image.outputs.tag }}
+90
View File
@@ -0,0 +1,90 @@
name: Push
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: https://git.keligrubb.com/actions/checkout@v7
with:
token: ${{ secrets.KESTRELOS_REPO_TOKEN }}
- name: Get PR description for changelog
env:
GITEA_REPO_TOKEN: ${{ secrets.KESTRELOS_REPO_TOKEN }}
run: |
sudo rm -f /etc/apt/sources.list.d/microsoft*.list /etc/apt/sources.list.d/azure*.list 2>/dev/null || true
sudo apt-get update -qq && sudo apt-get install -y -qq jq
RESP=$(curl -sf -H "Authorization: token $GITEA_REPO_TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/commits/${{ github.sha }}/pull") || true
if [ -n "$RESP" ]; then
echo "$RESP" | jq -r '.body // empty' > .ci_pr_body 2>/dev/null || true
fi
- name: Release (bump, tag, push, create release)
env:
CI_REPO_OWNER: ${{ github.actor }}
CI_REPO_NAME: ${{ github.event.repository.name }}
CI_FORGE_URL: ${{ github.server_url }}
CI_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
GITEA_REPO_TOKEN: ${{ secrets.KESTRELOS_REPO_TOKEN }}
run: |
sudo rm -f /etc/apt/sources.list.d/microsoft*.list /etc/apt/sources.list.d/azure*.list 2>/dev/null || true
sudo apt-get update -qq && sudo apt-get install -y -qq git wget
./scripts/release.sh
publish:
needs: release
runs-on: ubuntu-latest
steps:
- uses: https://git.keligrubb.com/actions/checkout@v7
with:
ref: main
token: ${{ secrets.KESTRELOS_REPO_TOKEN }}
- name: Log in to container registry
uses: https://git.keligrubb.com/actions/docker-login-action@v4
with:
registry: git.keligrubb.com
username: ${{ github.actor }}
password: ${{ secrets.KESTRELOS_REPO_TOKEN }}
- name: Set up Docker Buildx
uses: https://git.keligrubb.com/actions/docker-setup-buildx-action@v4
- name: Build Docker image
uses: https://git.keligrubb.com/actions/docker-build-push-action@v7
with:
context: .
load: true
tags: kestrelos:built
- name: Push Docker image (version + latest)
run: |
VERSION=$(awk '/"version"/ { match($0, /[0-9]+\.[0-9]+\.[0-9]+/); print substr($0, RSTART, RLENGTH); exit }' package.json)
case "$VERSION" in
[0-9]*.[0-9]*.[0-9]*) ;;
*) echo "error: package.json version must be x.y.z (got: $VERSION)"; exit 1 ;;
esac
REGISTRY="git.keligrubb.com"
IMAGE="$REGISTRY/${{ github.repository }}"
for tag in "$VERSION" latest; do
docker tag kestrelos:built "$IMAGE:$tag"
docker push "$IMAGE:$tag"
done
- name: Set up Helm
uses: https://git.keligrubb.com/actions/setup-helm@v5
- name: Package and push Helm chart
env:
GITEA_REPO_TOKEN: ${{ secrets.KESTRELOS_REPO_TOKEN }}
run: |
helm package helm/kestrelos
for f in kestrelos-*.tgz; do
curl -sf -u "${{ github.actor }}:$GITEA_REPO_TOKEN" -X POST --upload-file "$f" \
"${{ github.server_url }}/api/packages/${{ github.actor }}/helm/api/charts"
done
+1 -1
View File
@@ -1 +1 @@
setups.@nuxt/test-utils="4.0.0" setups.@nuxt/test-utils="4.1.0"
-41
View File
@@ -1,41 +0,0 @@
when:
- event: pull_request
steps:
- name: install
image: node:24-slim
depends_on: []
commands:
- npm ci
- name: lint
image: node:24-slim
depends_on: [install]
commands:
- npm run lint
- name: test
image: node:24-slim
depends_on: [install]
commands:
- npm run test
- name: e2e
image: mcr.microsoft.com/playwright:v1.58.2-noble
depends_on: [install]
commands:
- ./scripts/gen-dev-cert.sh
- npm run test:e2e
environment:
NODE_TLS_REJECT_UNAUTHORIZED: "0"
- name: docker-build
image: woodpeckerci/plugin-kaniko
depends_on: []
settings:
repo: ${CI_REPO_OWNER}/${CI_REPO_NAME}
registry: git.keligrubb.com
tags: latest
dry-run: true
single-snapshot: true
cleanup: true
-36
View File
@@ -1,36 +0,0 @@
when:
- event: push
branch: main
steps:
- name: release
image: alpine
commands:
- apk add --no-cache git
- ./scripts/release.sh
environment:
GITEA_REPO_TOKEN:
from_secret: gitea_repo_token
- name: docker
image: woodpeckerci/plugin-kaniko
depends_on: [release]
settings:
repo: ${CI_REPO_OWNER}/${CI_REPO_NAME}
registry: git.keligrubb.com
username: ${CI_REPO_OWNER}
password:
from_secret: gitea_registry_token
single-snapshot: true
cleanup: true
- name: helm
image: alpine/helm
depends_on: [release]
environment:
GITEA_REGISTRY_TOKEN:
from_secret: gitea_registry_token
commands:
- apk add --no-cache curl
- helm package helm/kestrelos
- curl -sf -u $CI_REPO_OWNER:$GITEA_REGISTRY_TOKEN -X POST --upload-file kestrelos-*.tgz https://git.keligrubb.com/api/packages/$CI_REPO_OWNER/helm/api/charts
+970
View File
@@ -1,3 +1,973 @@
## [1.1.12] - 2026-08-23
### Changed
- update all non-major dependencies (#47)
This PR contains the following updates:
| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@vitest/coverage-v8](https://vitest.dev/guide/coverage) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8)) | [`4.1.10` → `4.1.11`](https://renovatebot.com/diffs/npm/@vitest%2fcoverage-v8/4.1.10/4.1.11) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@vitest%2fcoverage-v8/4.1.11?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vitest%2fcoverage-v8/4.1.10/4.1.11?slim=true) |
| [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) | [`5.10.1` → `5.11.0`](https://renovatebot.com/diffs/npm/fast-xml-parser/5.10.1/5.11.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/fast-xml-parser/5.11.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/fast-xml-parser/5.10.1/5.11.0?slim=true) |
| [happy-dom](https://github.com/capricorn86/happy-dom) | [`20.11.2` → `20.11.6`](https://renovatebot.com/diffs/npm/happy-dom/20.11.2/20.11.6) | ![age](https://developer.mend.io/api/mc/badges/age/npm/happy-dom/20.11.6?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/happy-dom/20.11.2/20.11.6?slim=true) |
| [hls.js](https://github.com/video-dev/hls.js) | [`1.7.0` → `1.7.1`](https://renovatebot.com/diffs/npm/hls.js/1.7.0/1.7.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/hls.js/1.7.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/hls.js/1.7.0/1.7.1?slim=true) |
| [mediasoup](https://mediasoup.org) ([source](https://github.com/versatica/mediasoup)) | [`3.24.2` → `3.26.0`](https://renovatebot.com/diffs/npm/mediasoup/3.24.2/3.26.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/mediasoup/3.26.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/mediasoup/3.24.2/3.26.0?slim=true) |
| [openid-client](https://github.com/panva/openid-client) | [`6.8.5` → `6.8.7`](https://renovatebot.com/diffs/npm/openid-client/6.8.5/6.8.7) | ![age](https://developer.mend.io/api/mc/badges/age/npm/openid-client/6.8.7?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/openid-client/6.8.5/6.8.7?slim=true) |
| [vitest](https://vitest.dev) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest)) | [`4.1.10` → `4.1.11`](https://renovatebot.com/diffs/npm/vitest/4.1.10/4.1.11) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vitest/4.1.11?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vitest/4.1.10/4.1.11?slim=true) |
---
### Release Notes
<details>
<summary>vitest-dev/vitest (@&#8203;vitest/coverage-v8)</summary>
### [`v4.1.11`](https://github.com/vitest-dev/vitest/releases/tag/v4.1.11)
[Compare Source](https://github.com/vitest-dev/vitest/compare/v4.1.10...v4.1.11)
#####    🐞 Bug Fixes
- Revive global concurrency limit for test lifecycle \[backport to v4]  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) and [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;10992](https://github.com/vitest-dev/vitest/issues/10992) [<samp>(5146d)</samp>](https://github.com/vitest-dev/vitest/commit/5146df80b)
- **browser**:
- Encode iframeId in tester iframe URL \[backport to v4]  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va), **Pduhard** and **Claude Opus 4.8** in [#&#8203;10955](https://github.com/vitest-dev/vitest/issues/10955) [<samp>(10b2c)</samp>](https://github.com/vitest-dev/vitest/commit/10b2cd201)
- Trigger playwright/chromium gc on lower disk availability \[backport to v4]  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa), **Hiroshi Ogawa** and **OpenCode** in [#&#8203;10951](https://github.com/vitest-dev/vitest/issues/10951) [<samp>(9851d)</samp>](https://github.com/vitest-dev/vitest/commit/9851dbc41)
- **mocker**:
- Restrict redirect mocks to the fs allowlist \[backport to v4]  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;10974](https://github.com/vitest-dev/vitest/issues/10974) [<samp>(fe5a1)</samp>](https://github.com/vitest-dev/vitest/commit/fe5a11d3c)
#####     [View changes on GitHub](https://github.com/vitest-dev/vitest/compare/v4.1.10...v4.1.11)
</details>
<details>
<summary>NaturalIntelligence/fast-xml-parser (fast-xml-parser)</summary>
### [`v5.11.0`](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.11.0)
[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.10.1...v5.11.0)
#### What's Changed
- add support for endIndex in node metadata (5.x edition) by [@&#8203;Wain-PC](https://github.com/Wain-PC) in [#&#8203;850](https://github.com/NaturalIntelligence/fast-xml-parser/pull/850)
- fix: don't crash on a closing tag with no matching opening tag by [@&#8203;hdimer](https://github.com/hdimer) in [#&#8203;861](https://github.com/NaturalIntelligence/fast-xml-parser/pull/861)
#### New Contributors
- [@&#8203;Wain-PC](https://github.com/Wain-PC) made their first contribution in [#&#8203;850](https://github.com/NaturalIntelligence/fast-xml-parser/pull/850)
- [@&#8203;hdimer](https://github.com/hdimer) made their first contribution in [#&#8203;861](https://github.com/NaturalIntelligence/fast-xml-parser/pull/861)
**Full Changelog**: <https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.10.1...v5.11.0>
</details>
<details>
<summary>capricorn86/happy-dom (happy-dom)</summary>
### [`v20.11.6`](https://github.com/capricorn86/happy-dom/releases/tag/v20.11.6)
[Compare Source](https://github.com/capricorn86/happy-dom/compare/v20.11.5...v20.11.6)
##### :construction\_worker\_man: Patch fixes
- Updates docs for the global-registrator package - By **[@&#8203;capricorn86](https://github.com/capricorn86)** in task [#&#8203;2300](https://github.com/capricorn86/happy-dom/issues/2300)
### [`v20.11.5`](https://github.com/capricorn86/happy-dom/releases/tag/v20.11.5)
[Compare Source](https://github.com/capricorn86/happy-dom/compare/v20.11.4...v20.11.5)
##### :construction\_worker\_man: Patch fixes
- Allow explicit element types for querySelector (e.g. `querySelector<HTMLInputElement>(".my-input")`) - By **[@&#8203;cyphercodes](https://github.com/cyphercodes)**
### [`v20.11.4`](https://github.com/capricorn86/happy-dom/releases/tag/v20.11.4)
[Compare Source](https://github.com/capricorn86/happy-dom/compare/v20.11.3...v20.11.4)
##### :construction\_worker\_man: Patch fixes
- Fixes the CORS check `fetch()` to match origins instead of host and protocol - By **[@&#8203;rexxars](https://github.com/rexxars)** in task [#&#8203;1490](https://github.com/capricorn86/happy-dom/issues/1490)
### [`v20.11.3`](https://github.com/capricorn86/happy-dom/releases/tag/v20.11.3)
[Compare Source](https://github.com/capricorn86/happy-dom/compare/v20.11.2...v20.11.3)
##### :construction\_worker\_man: Patch fixes
- Make document.links return a live HTMLCollection - By **[@&#8203;bangseongbeom](https://github.com/bangseongbeom)** in task [#&#8203;2299](https://github.com/capricorn86/happy-dom/issues/2299)
- Copy labels array to prevent mutation of cached querySelectorAll result - By **[@&#8203;mixelburg](https://github.com/mixelburg)** in task [#&#8203;2226](https://github.com/capricorn86/happy-dom/issues/2226)
</details>
<details>
<summary>video-dev/hls.js (hls.js)</summary>
### [`v1.7.1`](https://github.com/video-dev/hls.js/releases/tag/v1.7.1)
[Compare Source](https://github.com/video-dev/hls.js/compare/v1.7.0...v1.7.1)
### Summary
HLS.js v1.7.1 includes bug fixes and improvements over the previous release.
#### Changes Since The Last Release
- Fix Interstitial snap-out at live edge and `BUFFER_APPEND_NO_PROGRESS` false positives ([#&#8203;7979](https://github.com/video-dev/hls.js/issues/7979)) [@&#8203;robwalch](https://github.com/robwalch)
- Workaround issue where `ManagedMediaSource` does not emit "startstreaming" when seeking ([#&#8203;7984](https://github.com/video-dev/hls.js/issues/7984))
- Fix permanent stall loading fragment-hint parts of encrypted low-latency streams ([#&#8203;7976](https://github.com/video-dev/hls.js/issues/7976)) [@&#8203;zaki699-blip](https://github.com/zaki699-blip)
- Document decode timebase change in MIGRATING ([#&#8203;7986](https://github.com/video-dev/hls.js/issues/7986)) [@&#8203;robwalch](https://github.com/robwalch)
#### Demo Page
<https://26ea065a.hls-js-dev.pages.dev/demo/>
#### API and Breaking Changes
No public exports were removed and no runtime behavior changes are required to upgrade from v1.6 to v1.7. TypeScript consumers might see new compile errors where previously loose types have been narrowed. Each is listed with upgrade guidance in the migration guide:
<https://github.com/video-dev/hls.js/blob/v1.7.0/MIGRATING.md#migrating-from-hlsjs-16-to-17>
Some exported type dependencies ("eventemitter3", "[@&#8203;svta/cml-cmcd](https://github.com/svta/cml-cmcd)", "[@&#8203;svta/cml-utils](https://github.com/svta/cml-utils)", "[@&#8203;svta/cml-structured-field-values](https://github.com/svta/cml-structured-field-values)") have not been bundled with hls.d.ts. Please file an issue if this is blocking you from upgrading.
#### Feedback
Please provide feedback via [Issues in GitHub](https://github.com/video-dev/hls.js/issues/new/choose). For more details on how to contribute to HLS.js, see our [CONTRIBUTING guide](https://github.com/video-dev/hls.js/blob/master/CONTRIBUTING.md).
</details>
<details>
<summary>versatica/mediasoup (mediasoup)</summary>
### [`v3.26.0`](https://github.com/versatica/mediasoup/blob/HEAD/CHANGELOG.md#3260)
[Compare Source](https://github.com/versatica/mediasoup/compare/3.25.0...3.26.0)
- **Breaking change:** Simulcast and SVC: Limit temporal layer to the preferred one ([PR #&#8203;1892](https://github.com/versatica/mediasoup/pull/1892)).
### [`v3.25.0`](https://github.com/versatica/mediasoup/blob/HEAD/CHANGELOG.md#3250)
[Compare Source](https://github.com/versatica/mediasoup/compare/3.24.2...3.25.0)
- Worker: Fix undefined behavior in `RtpStreamRecv::UpdateScore()` when no packets were received ([PR #&#8203;1886](https://github.com/versatica/mediasoup/pull/1886)).
- SCTP: Fix `SackChunk::GetValidatedGapAckBlocks()` returning a bogus gap-ack-block ([PR #&#8203;1891](https://github.com/versatica/mediasoup/pull/1891)).
- Do not make generated RTCP Sender Reports depend on RTP packet arrival time ([issue #&#8203;1881](https://github.com/versatica/mediasoup/issues/1881)):
- `RemoteClockOffsetEstimator` class ([PR #&#8203;1882](https://github.com/versatica/mediasoup/pull/1882)).
- Prepare `RtpStream` classes for capture time based RTCP Sender Reports ([PR #&#8203;1883](https://github.com/versatica/mediasoup/pull/1883), [PR #&#8203;1888](https://github.com/versatica/mediasoup/pull/1888)).
- `RemoteCaptureTimeEstimator` class ([PR #&#8203;1884](https://github.com/versatica/mediasoup/pull/1884)).
- Estimate the capture instant of each received RTP packet ([PR #&#8203;1885](https://github.com/versatica/mediasoup/pull/1885)).
- Generate RTCP Sender Reports based on the capture instant of the media rather than on the packet arrival time ([PR #&#8203;1887](https://github.com/versatica/mediasoup/pull/1887)).
- `SimulcastProducerStreamManager`: Apply new capture time logic and fix 'abs-capture-time' rewriting ([PR #&#8203;1889](https://github.com/versatica/mediasoup/pull/1889)).
</details>
<details>
<summary>panva/openid-client (openid-client)</summary>
### [`v6.8.7`](https://github.com/panva/openid-client/blob/HEAD/CHANGELOG.md#687-2026-08-20)
[Compare Source](https://github.com/panva/openid-client/compare/v6.8.6...v6.8.7)
##### Fixes
- allow destructuring the claims helper ([38bd8c0](https://github.com/panva/openid-client/commit/38bd8c052a7e1e6d0e2beda14d35fb38b9b26d4c)), references [#&#8203;887](https://github.com/panva/openid-client/issues/887)
### [`v6.8.6`](https://github.com/panva/openid-client/blob/HEAD/CHANGELOG.md#686-2026-08-18)
[Compare Source](https://github.com/panva/openid-client/compare/v6.8.5...v6.8.6)
##### Fixes
- avoid undefined user-agent in fetchProtectedResource ([492c3c3](https://github.com/panva/openid-client/commit/492c3c36aad1ac324661b808b32bc17a35d22665)), references [#&#8203;885](https://github.com/panva/openid-client/issues/885)
</details>
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zMy4yIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19-->
## [1.1.11] - 2026-08-19
### Changed
- remove unused broadcastToSession export (#45)
## Removed
- `broadcastToSession` from `server/plugins/websocket.js` — an exported but never-imported, never-called dead-code function. Its dependencies (`getSessionConnections`, `addSessionConnection`, `removeSessionConnection`) are retained since they remain actively used for per-session connection tracking within the plugin.
## Notes
- Full repository search confirms zero remaining references to `broadcastToSession`.
- ESLint passes; full test suite passes (50 files, 406 tests).
Closes #40
## [1.1.10] - 2026-08-14
### Changed
- update dependencies and fix security vulnerabilities (#44)
## Summary
Updates all out-of-date dependencies and applies `npm audit fix` to resolve 14 security vulnerabilities (3 critical, 8 high).
### Dependency Updates
| Package | Current → Latest |
|---|---|
| `nuxt` | 4.4.8 → 4.5.2 |
| `mediasoup` | 3.20.9 → 3.24.2 |
| `mediasoup-client` | 3.21.0 → 3.22.0 |
| `hls.js` | 1.6.16 → 1.7.0 |
| `vue` | 3.5.38 → 3.5.41 |
| `vue-router` | 5.1.0 → 5.2.0 |
| `@nuxt/icon` | 2.2.3 → 2.5.0 |
| `eslint` | 10.5.0 → 10.8.1 |
| `vitest` / `@vitest/coverage-v8` | 4.1.9 → 4.1.10 |
| `ws` | 8.21.0 → 8.21.3 |
| `openid-client` | 6.8.4 → 6.8.5 |
| `fast-xml-parser` | 5.9.3 → 5.10.1 |
| `happy-dom` | 20.10.6 → 20.11.2 |
| `@playwright/test` | 1.61.1 → 1.62.1 |
| `@nuxt/eslint` | 1.16.0 → 1.17.0 |
| `@nuxt/test-utils` | 4.0.3 → 4.1.0 |
| `@iconify-json/tabler` | 1.2.35 → 1.2.38 |
### Security Audit Fixes (`npm audit fix`)
- **`@nuxt/devtools`** (critical): Unauthenticated DevTools RPC allows arbitrary command execution on the developer's host
- **`tar`** (critical): Multiple process-crash/DoS vulnerabilities via crafted tar archives
- **`esbuild`** (high): Arbitrary file read when running dev server on Windows
- **`brace-expansion`** (high): Multiple DoS vectors (memory exhaustion, process hang)
- **`flatted`** (high): Prototype pollution via `parse()`
- **`svgo`** (high): `removeScripts` plugin leaves executable scripts intact
### Verification
- All 406 tests pass (3 skipped)
- Lint clean (`eslint . --max-warnings 0`)
- `npm audit` reports 0 vulnerabilities
## [1.1.9] - 2026-08-13
### Changed
- update dependency fast-xml-parser to v5.10.1 [security] (#42)
This PR contains the following updates:
| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) | [`5.9.3` → `5.10.1`](https://renovatebot.com/diffs/npm/fast-xml-parser/5.9.3/5.10.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/fast-xml-parser/5.10.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/fast-xml-parser/5.9.3/5.10.1?slim=true) |
---
### fast-xml-parser: Repeated DOCTYPE declarations reset entity expansion limits
[GHSA-8r6m-32jq-jx6q](https://github.com/advisories/GHSA-8r6m-32jq-jx6q)
<details>
<summary>More information</summary>
#### Details
##### Impact
`fast-xml-parser` processes multiple "DOCTYPE" declarations within a single XML document. Each declaration passes its entities to `@nodable/entities` through `addInputEntities()`.
`addInputEntities()` resets the entity expansion counters every time it is called. An attacker can therefore insert additional DOCTYPE declarations to repeatedly reset maxTotalExpansions and maxExpandedLength during one parse operation.
This allows a crafted XML document to exceed the configured entity-expansion limits and can cause excessive CPU use, event-loop blocking, memory exhaustion, and process termination.
##### Workarounds
- Manually check if multiple DOCTYPEs are not present in input contents
- Update to v5.10.1
- Keep `processEntity` flag off
#### Severity
- CVSS Score: 8.7 / 10 (High)
- Vector String: `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N`
#### References
- [https://github.com/NaturalIntelligence/fast-xml-parser/security/advisories/GHSA-8r6m-32jq-jx6q](https://github.com/NaturalIntelligence/fast-xml-parser/security/advisories/GHSA-8r6m-32jq-jx6q)
- [https://github.com/NaturalIntelligence/fast-xml-parser/commit/4e546e03987662de5495d050b5fba26bea65383f](https://github.com/NaturalIntelligence/fast-xml-parser/commit/4e546e03987662de5495d050b5fba26bea65383f)
- [https://github.com/NaturalIntelligence/fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser)
- [https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.10.1](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.10.1)
This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-8r6m-32jq-jx6q) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)).
</details>
---
### Release Notes
<details>
<summary>NaturalIntelligence/fast-xml-parser (fast-xml-parser)</summary>
### [`v5.10.1`](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.10.1)
[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.10.0...v5.10.1)
**Full Changelog**: <https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.10.0...v5.10.1>
### [`v5.10.0`](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.10.0)
[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.9.3...v5.10.0)
#### What's Changed
- Bump actions/checkout from 6.0.3 to 7.0.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;849](https://github.com/NaturalIntelligence/fast-xml-parser/pull/849)
- Bump zizmorcore/zizmor-action from 0.5.6 to 0.5.7 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;848](https://github.com/NaturalIntelligence/fast-xml-parser/pull/848)
**Full Changelog**: <https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.9.3...v5.10.0>
</details>
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4yOC4wIiwidXBkYXRlZEluVmVyIjoiNDQuMjguMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIiwic2VjdXJpdHkiXX0=-->
## [1.1.8] - 2026-08-13
### Changed
- update https://git.keligrubb.com/actions/setup-node action to v7 (#38)
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| [https://git.keligrubb.com/actions/setup-node](https://git.keligrubb.com/actions/setup-node) | action | major | `v6``v7` |
---
### Release Notes
<details>
<summary>actions/setup-node (https://git.keligrubb.com/actions/setup-node)</summary>
### [`v7.0.0`](https://git.keligrubb.com/actions/setup-node/compare/v7...v7)
[Compare Source](https://git.keligrubb.com/actions/setup-node/compare/v7...v7)
### [`v7`](https://git.keligrubb.com/actions/setup-node/compare/v6.5.0...v7)
[Compare Source](https://git.keligrubb.com/actions/setup-node/compare/v6.5.0...v7)
</details>
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNjUuNCIsInVwZGF0ZWRJblZlciI6IjQzLjI2NS40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
## [1.1.7] - 2026-08-13
### Changed
- update dependency supercluster to v9 (#41)
This PR contains the following updates:
| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [supercluster](https://github.com/mapbox/supercluster) | [`^8.0.1` → `^9.0.0`](https://renovatebot.com/diffs/npm/supercluster/8.0.1/9.0.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/supercluster/9.0.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/supercluster/8.0.1/9.0.0?slim=true) |
---
### Release Notes
<details>
<summary>mapbox/supercluster (supercluster)</summary>
### [`v9.0.0`](https://github.com/mapbox/supercluster/releases/tag/v9.0.0)
[Compare Source](https://github.com/mapbox/supercluster/compare/v8.0.1...v9.0.0)
- Radically optimize **memory footprint and performance** — on a sample 1M points index, Supercluster now uses 94% less transient allocation, 57% lower peak heap, 65% less retained memory, and runs 23% faster. [#&#8203;258](https://github.com/mapbox/supercluster/issues/258)
- Improve internal coordinate precision by 6 bits (64 times).
- ⚠️ Breaking: hard-cap `maxZoom` at 30 (it wasn't practical to use it on higher zooms anyway).
- Add support for `MultiPoint` features. [#&#8203;263](https://github.com/mapbox/supercluster/issues/263)
</details>
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4yNy4wIiwidXBkYXRlZEluVmVyIjoiNDQuMjcuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
## [1.1.6] - 2026-06-24
### Changed
- Add ADS-B, AIS, and ALPR map layers with live CoT streaming (#36)
## Summary
- **ADS-B & AIS:** OpenSky and AISStream OSINT feeds upsert into the CoT store; tactical tracks still arrive via adsbcot/aiscot on `:8089`. Map clients subscribe via `GET /api/cot/stream` (SSE) with viewport bbox filtering and Air / Surface / Team layer toggles.
- **ALPR (Flock/OSM):** Toggleable license-plate reader layer sourced from OpenStreetMap, with SQLite cache, Overpass fallback, tiled viewport fetching, and clustered markers with direction cones.
- **Map performance:** Ring-based tile selection (fixes zoom-out crash), immutable tile cache, incremental marker sync, split cluster load/query, and padded SSE bbox to reduce reconnect churn.
## Docs
- `docs/tracking.md` — ADS-B/AIS accuracy tiers, freshness, self-hosted receivers, optional OSINT API keys
- `docs/map-and-cameras.md` — ALPR layer and map behavior updates
## [1.1.5] - 2026-06-21
### Changed
- update https://git.keligrubb.com/actions/checkout action to v7 (#35)
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| [https://git.keligrubb.com/actions/checkout](https://git.keligrubb.com/actions/checkout) | action | major | `v6``v7` |
---
### Release Notes
<details>
<summary>actions/checkout (https://git.keligrubb.com/actions/checkout)</summary>
### [`v7.0.0`](https://git.keligrubb.com/actions/checkout/blob/HEAD/CHANGELOG.md#v700)
[Compare Source](https://git.keligrubb.com/actions/checkout/compare/v7...v7)
- Block checking out fork PR for pull\_request\_target and workflow\_run by [@&#8203;aiqiaoy](https://github.com/aiqiaoy) in [#&#8203;2454](https://github.com/actions/checkout/pull/2454)
- Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the minor-actions-dependencies group across 1 directory by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2458](https://github.com/actions/checkout/pull/2458)
- Bump flatted from 3.3.1 to 3.4.2 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2460](https://github.com/actions/checkout/pull/2460)
- Bump js-yaml from 4.1.0 to 4.2.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2461](https://github.com/actions/checkout/pull/2461)
- Bump [@&#8203;actions/core](https://github.com/actions/core) and [@&#8203;actions/tool-cache](https://github.com/actions/tool-cache) and Remove uuid by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2459](https://github.com/actions/checkout/pull/2459)
- upgrade module to esm and update dependencies by [@&#8203;aiqiaoy](https://github.com/aiqiaoy) in [#&#8203;2463](https://github.com/actions/checkout/pull/2463)
- Bump the minor-npm-dependencies group across 1 directory with 3 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2462](https://github.com/actions/checkout/pull/2462)
### [`v7`](https://git.keligrubb.com/actions/checkout/blob/HEAD/CHANGELOG.md#v700)
[Compare Source](https://git.keligrubb.com/actions/checkout/compare/v6.0.3...v7)
- Block checking out fork PR for pull\_request\_target and workflow\_run by [@&#8203;aiqiaoy](https://github.com/aiqiaoy) in [#&#8203;2454](https://github.com/actions/checkout/pull/2454)
- Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the minor-actions-dependencies group across 1 directory by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2458](https://github.com/actions/checkout/pull/2458)
- Bump flatted from 3.3.1 to 3.4.2 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2460](https://github.com/actions/checkout/pull/2460)
- Bump js-yaml from 4.1.0 to 4.2.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2461](https://github.com/actions/checkout/pull/2461)
- Bump [@&#8203;actions/core](https://github.com/actions/core) and [@&#8203;actions/tool-cache](https://github.com/actions/tool-cache) and Remove uuid by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2459](https://github.com/actions/checkout/pull/2459)
- upgrade module to esm and update dependencies by [@&#8203;aiqiaoy](https://github.com/aiqiaoy) in [#&#8203;2463](https://github.com/actions/checkout/pull/2463)
- Bump the minor-npm-dependencies group across 1 directory with 3 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2462](https://github.com/actions/checkout/pull/2462)
</details>
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yMzMuNCIsInVwZGF0ZWRJblZlciI6IjQzLjIzMy40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
## [1.1.4] - 2026-04-29
### Changed
- update all non-major dependencies (#31)
This PR contains the following updates:
| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@vitest/coverage-v8](https://vitest.dev/guide/coverage) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8)) | [`4.1.4` → `4.1.5`](https://renovatebot.com/diffs/npm/@vitest%2fcoverage-v8/4.1.4/4.1.5) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@vitest%2fcoverage-v8/4.1.5?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vitest%2fcoverage-v8/4.1.4/4.1.5?slim=true) |
| [@vue/test-utils](https://github.com/vuejs/test-utils) | [`2.4.6` → `2.4.8`](https://renovatebot.com/diffs/npm/@vue%2ftest-utils/2.4.6/2.4.8) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@vue%2ftest-utils/2.4.8?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vue%2ftest-utils/2.4.6/2.4.8?slim=true) |
| [eslint](https://eslint.org) ([source](https://github.com/eslint/eslint)) | [`10.2.0` → `10.2.1`](https://renovatebot.com/diffs/npm/eslint/10.2.0/10.2.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/eslint/10.2.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint/10.2.0/10.2.1?slim=true) |
| [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) | [`5.6.0` → `5.7.2`](https://renovatebot.com/diffs/npm/fast-xml-parser/5.6.0/5.7.2) | ![age](https://developer.mend.io/api/mc/badges/age/npm/fast-xml-parser/5.7.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/fast-xml-parser/5.6.0/5.7.2?slim=true) |
| [mediasoup](https://mediasoup.org) ([source](https://github.com/versatica/mediasoup)) | [`3.19.19` → `3.19.21`](https://renovatebot.com/diffs/npm/mediasoup/3.19.19/3.19.21) | ![age](https://developer.mend.io/api/mc/badges/age/npm/mediasoup/3.19.21?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/mediasoup/3.19.19/3.19.21?slim=true) |
| [mediasoup-client](https://mediasoup.org) ([source](https://github.com/versatica/mediasoup-client)) | [`3.18.8` → `3.19.0`](https://renovatebot.com/diffs/npm/mediasoup-client/3.18.8/3.19.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/mediasoup-client/3.19.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/mediasoup-client/3.18.8/3.19.0?slim=true) |
| [vitest](https://vitest.dev) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest)) | [`4.1.4` → `4.1.5`](https://renovatebot.com/diffs/npm/vitest/4.1.4/4.1.5) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vitest/4.1.5?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vitest/4.1.4/4.1.5?slim=true) |
| [vue](https://vuejs.org/) ([source](https://github.com/vuejs/core)) | [`3.5.32` → `3.5.33`](https://renovatebot.com/diffs/npm/vue/3.5.32/3.5.33) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vue/3.5.33?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue/3.5.32/3.5.33?slim=true) |
| [vue-router](https://router.vuejs.org) ([source](https://github.com/vuejs/router)) | [`5.0.4` → `5.0.6`](https://renovatebot.com/diffs/npm/vue-router/5.0.4/5.0.6) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vue-router/5.0.6?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue-router/5.0.4/5.0.6?slim=true) |
---
### Release Notes
<details>
<summary>vitest-dev/vitest (@&#8203;vitest/coverage-v8)</summary>
### [`v4.1.5`](https://github.com/vitest-dev/vitest/releases/tag/v4.1.5)
[Compare Source](https://github.com/vitest-dev/vitest/compare/v4.1.4...v4.1.5)
#####    🚀 Experimental Features
- **coverage**: Istanbul to support `instrumenter` option  -  by [@&#8203;BartWaardenburg](https://github.com/BartWaardenburg) and [@&#8203;AriPerkkio](https://github.com/AriPerkkio) in [#&#8203;10119](https://github.com/vitest-dev/vitest/issues/10119) [<samp>(0e0ff)</samp>](https://github.com/vitest-dev/vitest/commit/0e0ff41c7)
#####    🐞 Bug Fixes
- \--project negation excludes browser instances  -  by [@&#8203;felamaslen](https://github.com/felamaslen) in [#&#8203;10131](https://github.com/vitest-dev/vitest/issues/10131) [<samp>(9423d)</samp>](https://github.com/vitest-dev/vitest/commit/9423dc084)
- Project color label on html reporter  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;10142](https://github.com/vitest-dev/vitest/issues/10142) [<samp>(596f7)</samp>](https://github.com/vitest-dev/vitest/commit/596f73986)
- Fix `vi.defineHelper` called as object method  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;10163](https://github.com/vitest-dev/vitest/issues/10163) [<samp>(122c2)</samp>](https://github.com/vitest-dev/vitest/commit/122c25b5b)
- Alias `agent` reporter to `minimal`  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;10157](https://github.com/vitest-dev/vitest/issues/10157) [<samp>(663b9)</samp>](https://github.com/vitest-dev/vitest/commit/663b99fe3)
- Respect diff config options in soft assertions  -  by [@&#8203;Copilot](https://github.com/Copilot), **sheremet-va** and [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;8696](https://github.com/vitest-dev/vitest/issues/8696) [<samp>(9787d)</samp>](https://github.com/vitest-dev/vitest/commit/9787dedad)
- Respect diff config options in soft assertions "  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;8696](https://github.com/vitest-dev/vitest/issues/8696) [<samp>(7dc6d)</samp>](https://github.com/vitest-dev/vitest/commit/7dc6d54fd)
- **ast-collect**: Recognize \_*vi\_import* prefix in static test discovery  -  by [@&#8203;Yejneshwar](https://github.com/Yejneshwar) in [#&#8203;10129](https://github.com/vitest-dev/vitest/issues/10129) [<samp>(32546)</samp>](https://github.com/vitest-dev/vitest/commit/325463ab2)
- **coverage**: Descriptive error message when reports directory is removed during test run  -  by [@&#8203;DaveT1991](https://github.com/DaveT1991) and [@&#8203;AriPerkkio](https://github.com/AriPerkkio) in [#&#8203;10117](https://github.com/vitest-dev/vitest/issues/10117) [<samp>(14133)</samp>](https://github.com/vitest-dev/vitest/commit/1413382e1)
- **snapshot**: Increase default snapshot max output length  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) and **Codex** in [#&#8203;10150](https://github.com/vitest-dev/vitest/issues/10150) [<samp>(21e66)</samp>](https://github.com/vitest-dev/vitest/commit/21e66ff63)
- **ui**: Fix jsx/tsx syntax highlight  -  by [@&#8203;hi-ogawa](https://github.com/hi-ogawa) in [#&#8203;10152](https://github.com/vitest-dev/vitest/issues/10152) [<samp>(f1b1f)</samp>](https://github.com/vitest-dev/vitest/commit/f1b1f6c7b)
- **web-worker**: Support MessagePort objects referenced inside postMessage data  -  by [@&#8203;whitphx](https://github.com/whitphx) and **Claude Opus 4.6 (1M context)** in [#&#8203;9927](https://github.com/vitest-dev/vitest/issues/9927) and [#&#8203;10124](https://github.com/vitest-dev/vitest/issues/10124) [<samp>(7ad7d)</samp>](https://github.com/vitest-dev/vitest/commit/7ad7d39af)
- **api**: Make test-specification options writable  -  by [@&#8203;sheremet-va](https://github.com/sheremet-va) in [#&#8203;10154](https://github.com/vitest-dev/vitest/issues/10154) [<samp>(6abd5)</samp>](https://github.com/vitest-dev/vitest/commit/6abd557b7)
#####     [View changes on GitHub](https://github.com/vitest-dev/vitest/compare/v4.1.4...v4.1.5)
</details>
<details>
<summary>vuejs/test-utils (@&#8203;vue/test-utils)</summary>
### [`v2.4.8`](https://github.com/vuejs/test-utils/releases/tag/v2.4.8)
[Compare Source](https://github.com/vuejs/test-utils/compare/v2.4.7...v2.4.8)
[compare changes](https://github.com/vuejs/test-utils/compare/v2.4.7...v2.4.8)
##### 🩹 Fixes
- Correct declaration entrypoint ([#&#8203;2826](https://github.com/vuejs/test-utils/pull/2826))
##### 🤖 CI
- Enable pkg.pr.new ([#&#8203;2827](https://github.com/vuejs/test-utils/pull/2827))
##### ❤️ Contributors
- Cédric Exbrayat ([@&#8203;cexbrayat](https://github.com/cexbrayat))
- Daniel Roe ([@&#8203;danielroe](https://github.com/danielroe))
### [`v2.4.7`](https://github.com/vuejs/test-utils/releases/tag/v2.4.7)
[Compare Source](https://github.com/vuejs/test-utils/compare/v2.4.6...v2.4.7)
[compare changes](https://github.com/vuejs/test-utils/compare/v2.4.6...v2.4.7)
##### 🚀 Enhancements
- Add Chinese docs translation ([#&#8203;2552](https://github.com/vuejs/test-utils/pull/2552))
- SetData()/shallowMount with initialData for components using the Composition API / <script setup> ([#&#8203;2655](https://github.com/vuejs/test-utils/pull/2655))
##### 🩹 Fixes
- Preserve code from keyboard events ([#&#8203;2434](https://github.com/vuejs/test-utils/pull/2434))
- Switch browser and require exports definitions ([#&#8203;2501](https://github.com/vuejs/test-utils/pull/2501))
- Re-add peer dependencies but with wider range ([#&#8203;2511](https://github.com/vuejs/test-utils/pull/2511))
- Resolve warnings in docs:dev ([30b7491](https://github.com/vuejs/test-utils/commit/30b7491))
- Resolve TypeScript type errors in .vitepress/config ([#&#8203;2549](https://github.com/vuejs/test-utils/pull/2549))
- Accept FunctionalComponent<any> as selector ([0bb947f](https://github.com/vuejs/test-utils/commit/0bb947f))
- Text() misses content for array functional component ([#&#8203;2579](https://github.com/vuejs/test-utils/pull/2579))
- Use await in test ([c5482b4](https://github.com/vuejs/test-utils/commit/c5482b4))
- **deps:** Update dependency vue-component-type-helpers to v3 ([#&#8203;2683](https://github.com/vuejs/test-utils/pull/2683))
- Remove wrapper div when unmount ([#&#8203;2700](https://github.com/vuejs/test-utils/pull/2700))
- Make mount options slots compatible with noUncheckedIndexedAccess true ([#&#8203;2713](https://github.com/vuejs/test-utils/pull/2713))
- Add missing peerDependency [@&#8203;vue/compiler-dom](https://github.com/vue/compiler-dom) ([75801ba](https://github.com/vuejs/test-utils/commit/75801ba))
- **docs:** Declare css module for vitepress typecheck ([ddaca97](https://github.com/vuejs/test-utils/commit/ddaca97))
##### 💅 Refactors
- Enforce consistent usage of type imports ([#&#8203;2734](https://github.com/vuejs/test-utils/pull/2734))
##### 📖 Documentation
- Clarify findComponent vs getComponent ([#&#8203;2435](https://github.com/vuejs/test-utils/pull/2435))
- Update fr docs ([67064ef](https://github.com/vuejs/test-utils/commit/67064ef))
- Add note about partial transition stub support ([#&#8203;2431](https://github.com/vuejs/test-utils/pull/2431))
- Fix missing data at passing data section essentials guide ([dda205e](https://github.com/vuejs/test-utils/commit/dda205e))
- Fix missing data at passing data section essentials guide fr ([ae2c72c](https://github.com/vuejs/test-utils/commit/ae2c72c))
- Fix plugin TS declaration example ([#&#8203;2466](https://github.com/vuejs/test-utils/pull/2466))
- Fixed incorrect checkbox value check ([#&#8203;2495](https://github.com/vuejs/test-utils/pull/2495))
- Capital letter in sentence fix ([#&#8203;2499](https://github.com/vuejs/test-utils/pull/2499))
- Import missing DOMWrapper on Implementation of the plugin section ([#&#8203;2519](https://github.com/vuejs/test-utils/pull/2519))
- Add migration step for deprecated ref syntax in findAllComponents ([#&#8203;2498](https://github.com/vuejs/test-utils/pull/2498))
- Correct anchor hash links and fix typo ([#&#8203;2551](https://github.com/vuejs/test-utils/pull/2551))
- Center logo on home ([#&#8203;2559](https://github.com/vuejs/test-utils/pull/2559))
- **zh-cn:** Review a-crash-course ([#&#8203;2563](https://github.com/vuejs/test-utils/pull/2563))
- Use code-group for install commands ([#&#8203;2571](https://github.com/vuejs/test-utils/pull/2571))
- **zh-cn:** Review event-handing.md ([#&#8203;2572](https://github.com/vuejs/test-utils/pull/2572))
- **zh-cn:** Enhance conditional-rendering.md ([#&#8203;2562](https://github.com/vuejs/test-utils/pull/2562))
- **zh-cn:** Review easy-to-test ([#&#8203;2567](https://github.com/vuejs/test-utils/pull/2567))
- **zh-cn:** Review passing-data.md ([#&#8203;2575](https://github.com/vuejs/test-utils/pull/2575))
- **zh-cn:** Review async-suspense.md ([#&#8203;2576](https://github.com/vuejs/test-utils/pull/2576))
- **zh:** 优化 API 文档格式和内容 ([#&#8203;2569](https://github.com/vuejs/test-utils/pull/2569))
- **zh:** 更新 Vitest 模拟日期和计时器的说明 ([#&#8203;2578](https://github.com/vuejs/test-utils/pull/2578))
- **zh-cn:** Review http-requests.md ([#&#8203;2580](https://github.com/vuejs/test-utils/pull/2580))
- **zh-cn:** Review forms ([#&#8203;2582](https://github.com/vuejs/test-utils/pull/2582))
- **zh-cn:** Guide/advanced/slots.md ([#&#8203;2565](https://github.com/vuejs/test-utils/pull/2565))
- **zh:** Review extending-vtu ([#&#8203;2583](https://github.com/vuejs/test-utils/pull/2583))
- **zh:** Review index ([#&#8203;2584](https://github.com/vuejs/test-utils/pull/2584))
- Fix modelValue test example ([85bfdf4](https://github.com/vuejs/test-utils/commit/85bfdf4))
- Removes broken link from plugins.md ([69bc1ce](https://github.com/vuejs/test-utils/commit/69bc1ce))
- **zh:** Review transitions, component-instance, and reusability-composition ([#&#8203;2616](https://github.com/vuejs/test-utils/pull/2616))
- **zh:** Review v-model and vuex ([#&#8203;2617](https://github.com/vuejs/test-utils/pull/2617))
- **zh:** Review all the rest advanced guide ([#&#8203;2619](https://github.com/vuejs/test-utils/pull/2619))
- **zh:** Review migration ([#&#8203;2623](https://github.com/vuejs/test-utils/pull/2623))
- Fix a typo in transitions.md ([#&#8203;2635](https://github.com/vuejs/test-utils/pull/2635))
- Update crash-course to script setup ([c81aa79](https://github.com/vuejs/test-utils/commit/c81aa79))
- Update Essentials section to setup (composition api) ([#&#8203;2647](https://github.com/vuejs/test-utils/pull/2647))
- Typos in examples ([#&#8203;2678](https://github.com/vuejs/test-utils/pull/2678))
- Typo in easy-to-test.md ([#&#8203;2710](https://github.com/vuejs/test-utils/pull/2710))
- Add note about mocking requestAnimationFrame for transitions ([2324c65](https://github.com/vuejs/test-utils/commit/2324c65))
- Updated example TodoApp to script setup ([#&#8203;2727](https://github.com/vuejs/test-utils/pull/2727))
- Remove "Using data" section from "Conditional Rendering" guide and fix passing data test example ([#&#8203;2743](https://github.com/vuejs/test-utils/pull/2743))
- Follow-up fixes for the conditional rendering guide ([#&#8203;2744](https://github.com/vuejs/test-utils/pull/2744))
- Mention shallowMount stub name changes in migration guide ([80e051a](https://github.com/vuejs/test-utils/commit/80e051a))
- Update conditional rendering documentation to clarify isVisible() usage with attachTo ([#&#8203;2799](https://github.com/vuejs/test-utils/pull/2799))
- Restore Options API component for data() mounting example ([#&#8203;2804](https://github.com/vuejs/test-utils/pull/2804))
- Promote Vitest as recommended test runner ([#&#8203;2805](https://github.com/vuejs/test-utils/pull/2805))
- **api:** Note that setValue does not accept objects on `<select>` ([#&#8203;2819](https://github.com/vuejs/test-utils/pull/2819))
##### 🏡 Chore
- Add api/index.md to docs:translation:compare ([6b8681c](https://github.com/vuejs/test-utils/commit/6b8681c))
- Remove unnecessary generic arguments ([cfd70c6](https://github.com/vuejs/test-utils/commit/cfd70c6))
- Ignore TS error in config object ([9d0a618](https://github.com/vuejs/test-utils/commit/9d0a618))
- Simplify eslint packages ([c1d0ffd](https://github.com/vuejs/test-utils/commit/c1d0ffd))
- Use eslint v9 with flat config ([2f19fdf](https://github.com/vuejs/test-utils/commit/2f19fdf))
- Expose Stubs type publicly ([#&#8203;2492](https://github.com/vuejs/test-utils/pull/2492))
- Update documentation file path ([9c96594](https://github.com/vuejs/test-utils/commit/9c96594))
- Use pnpm v10 ([e4c2cb3](https://github.com/vuejs/test-utils/commit/e4c2cb3))
- Pnpm approve build ([81c54e9](https://github.com/vuejs/test-utils/commit/81c54e9))
- Use github issue forms ([#&#8203;2673](https://github.com/vuejs/test-utils/pull/2673))
- Exclude class components from test type-checking ([0899008](https://github.com/vuejs/test-utils/commit/0899008))
- Add explicit coverage include for vitest v4 ([51672b9](https://github.com/vuejs/test-utils/commit/51672b9))
- Update to prettier v3.7 ([fed9e7c](https://github.com/vuejs/test-utils/commit/fed9e7c))
- Migrate to oxfmt ([81c1de9](https://github.com/vuejs/test-utils/commit/81c1de9))
- Migrate to oxlint ([a361908](https://github.com/vuejs/test-utils/commit/a361908))
- Prepare TypeScript 6 migration settings ([55e1262](https://github.com/vuejs/test-utils/commit/55e1262))
- Adjust tsd config for TypeScript 6 ([7d23eb5](https://github.com/vuejs/test-utils/commit/7d23eb5))
- Avoid TypeScript 6 target deprecation warning ([81d063c](https://github.com/vuejs/test-utils/commit/81d063c))
##### 🤖 CI
- Remove node v22 build ([7ebf58d](https://github.com/vuejs/test-utils/commit/7ebf58d))
- Add node v22 build ([57540ee](https://github.com/vuejs/test-utils/commit/57540ee))
- Use "pool: threads" instead of vmThreads ([d0cbb54](https://github.com/vuejs/test-utils/commit/d0cbb54))
- Remove node v18 and add v24 ([fd9cf95](https://github.com/vuejs/test-utils/commit/fd9cf95))
- Add trusted publishing release workflow ([#&#8203;2825](https://github.com/vuejs/test-utils/pull/2825))
##### ❤️ Contributors
- Lachlan Miller ([@&#8203;lmiller1990](https://github.com/lmiller1990))
- cexbrayat ([@&#8203;cexbrayat](https://github.com/cexbrayat))
- Nicolas Bonamy ([@&#8203;nbonamy](https://github.com/nbonamy))
- KatWorkGit ([@&#8203;KatWorkGit](https://github.com/KatWorkGit))
- Wouter Kroes ([@&#8203;wouterkroes](https://github.com/wouterkroes))
- Rama Muhammad Murshal ([@&#8203;ramammurshal](https://github.com/ramammurshal))
- Evan You ([@&#8203;yyx990803](https://github.com/yyx990803))
- Vlad Starkovsky ([@&#8203;starkovsky](https://github.com/starkovsky))
- Joe ([@&#8203;joaoprp](https://github.com/joaoprp))
- Priyadarshi Kumar ([@&#8203;Psingh132](https://github.com/Psingh132))
- Sébastien Ronveaux ([@&#8203;sronveaux](https://github.com/sronveaux))
- Gilliam ([@&#8203;Gi11i4m](https://github.com/Gi11i4m))
- Baranov Dmytro ([@&#8203;dimas7001](https://github.com/dimas7001))
- BrendonHenrique ([@&#8203;BrendonHenrique](https://github.com/BrendonHenrique))
- Lorenz van Herwaarden ([@&#8203;lorenzvanherwaarden](https://github.com/lorenzvanherwaarden))
- wuzhiqing ([@&#8203;DDDDD12138](https://github.com/DDDDD12138))
- 阿菜 Cai ([@&#8203;RSS1102](https://github.com/RSS1102))
- Jinjiang ([@&#8203;Jinjiang](https://github.com/Jinjiang))
- Kylin ([@&#8203;lxKylin](https://github.com/lxKylin))
- Qianhe Chen ([@&#8203;chenqianhe](https://github.com/chenqianhe))
- 时瑶 ([@&#8203;KiritaniAyaka](https://github.com/KiritaniAyaka))
- h7ml ([@&#8203;h7ml](https://github.com/h7ml))
- Nicander ([@&#8203;Nicander93](https://github.com/Nicander93))
- Take-John ([@&#8203;takejohn](https://github.com/takejohn))
- ilyasherstoboev ([@&#8203;ilyasherstoboev](https://github.com/ilyasherstoboev))
- aimerie ([@&#8203;aimerie](https://github.com/aimerie))
- Miguel Rincon ([@&#8203;miguelrincon](https://github.com/miguelrincon))
- bcastlel ([@&#8203;bcastlel](https://github.com/bcastlel))
- Claudiu ([@&#8203;sofuxro](https://github.com/sofuxro))
- Artem Dragunov ([@&#8203;dragunovartem99](https://github.com/dragunovartem99))
- Robin ([@&#8203;OrbisK](https://github.com/OrbisK))
- Koen Mertens ([@&#8203;KCMertens](https://github.com/KCMertens))
- meomking ([@&#8203;CaptainWang98](https://github.com/CaptainWang98))
- Pepijn Olivier ([@&#8203;pepijnolivier](https://github.com/pepijnolivier))
- Tomina ([@&#8203;Thomaash](https://github.com/Thomaash))
- Gareth Jones ([@&#8203;G-Rath](https://github.com/G-Rath))
- Jerry Hogan ([@&#8203;hdJerry](https://github.com/hdJerry))
- Marco Pasqualetti ([@&#8203;marcalexiei](https://github.com/marcalexiei))
- guoxk ([@&#8203;guoxk-me](https://github.com/guoxk-me))
- kimulaco ([@&#8203;kimulaco](https://github.com/kimulaco))
- Erwan IQUEL ([@&#8203;Olympus5](https://github.com/Olympus5))
- Matt Van Horn ([@&#8203;mvanhorn](https://github.com/mvanhorn))
- Daniel Roe ([@&#8203;danielroe](https://github.com/danielroe))
</details>
<details>
<summary>eslint/eslint (eslint)</summary>
### [`v10.2.1`](https://github.com/eslint/eslint/releases/tag/v10.2.1)
[Compare Source](https://github.com/eslint/eslint/compare/v10.2.0...v10.2.1)
#### Bug Fixes
- [`14be92b`](https://github.com/eslint/eslint/commit/14be92b6d1fa0923b8923830f2208e5e2705b002) fix: model generator yield resumption paths in code path analysis ([#&#8203;20665](https://github.com/eslint/eslint/issues/20665)) (sethamus)
- [`84a19d2`](https://github.com/eslint/eslint/commit/84a19d2c32255db6b9cfc08644a607aae6d5cb62) fix: no-async-promise-executor false positives for shadowed Promise ([#&#8203;20740](https://github.com/eslint/eslint/issues/20740)) (xbinaryx)
- [`af764af`](https://github.com/eslint/eslint/commit/af764af0ec38225755fbf8a6f207f0c77b595a8d) fix: clarify language and processor validation errors ([#&#8203;20729](https://github.com/eslint/eslint/issues/20729)) (Pixel998)
- [`e251b89`](https://github.com/eslint/eslint/commit/e251b89a38280973e468a4a9386c138f4f55d10d) fix: update eslint ([#&#8203;20715](https://github.com/eslint/eslint/issues/20715)) (renovate\[bot])
#### Documentation
- [`ca92ca0`](https://github.com/eslint/eslint/commit/ca92ca0fb4599e8de1e2fb914e695fe7397cbe63) docs: reuse markdown-it instance for markdown filter ([#&#8203;20768](https://github.com/eslint/eslint/issues/20768)) (Amaresh S M)
- [`57d2ee2`](https://github.com/eslint/eslint/commit/57d2ee213305cee0cb55ef08e0480b57396269a9) docs: Enable Eleventy incremental mode for watch ([#&#8203;20767](https://github.com/eslint/eslint/issues/20767)) (Amaresh S M)
- [`c1621b9`](https://github.com/eslint/eslint/commit/c1621b915742276e5f4b25efe790ca62296330dc) docs: fix typos in code-path-analyzer.js ([#&#8203;20700](https://github.com/eslint/eslint/issues/20700)) (Ayush Shukla)
- [`1418d52`](https://github.com/eslint/eslint/commit/1418d522d10bde1960f4942afb548bc7160ec49e) docs: Update README (GitHub Actions Bot)
- [`39771e6`](https://github.com/eslint/eslint/commit/39771e6e600f0b0617fdeafff6dd07e4211ffde6) docs: Update README (GitHub Actions Bot)
- [`71e0469`](https://github.com/eslint/eslint/commit/71e04693def2df57268f08f3072a2749df6bf438) docs: fix incomplete JSDoc param description in no-shadow rule ([#&#8203;20728](https://github.com/eslint/eslint/issues/20728)) (kuldeep kumar)
- [`22119ce`](https://github.com/eslint/eslint/commit/22119ceb93e28f62262fc1d98ff1b1442d6e2dbf) docs: clarify scope of for-direction rule with dead code examples ([#&#8203;20723](https://github.com/eslint/eslint/issues/20723)) (Amaresh S M)
- [`8f3fb77`](https://github.com/eslint/eslint/commit/8f3fb77f122a5641d1833cad5d93f3f54fa3be0b) docs: document `meta.docs.dialects` ([#&#8203;20718](https://github.com/eslint/eslint/issues/20718)) (Pixel998)
#### Chores
- [`7ddfea9`](https://github.com/eslint/eslint/commit/7ddfea9c4f62add1588c5c0b0da568c299246383) chore: update dependency prettier to v3.8.2 ([#&#8203;20770](https://github.com/eslint/eslint/issues/20770)) (renovate\[bot])
- [`fac40e1`](https://github.com/eslint/eslint/commit/fac40e1de2ba7646cc7cd2d3f93fbdd1f8819001) ci: bump pnpm/action-setup from 5.0.0 to 6.0.0 ([#&#8203;20763](https://github.com/eslint/eslint/issues/20763)) (dependabot\[bot])
- [`7246f92`](https://github.com/eslint/eslint/commit/7246f923332522d8b3d46b6ee646fce88535f3fb) test: add tests for SuppressionsService.load() error handling ([#&#8203;20734](https://github.com/eslint/eslint/issues/20734)) (kuldeep kumar)
- [`4f34b1e`](https://github.com/eslint/eslint/commit/4f34b1e592b0f63d766d9903998e8e36eb49d3aa) chore: update pnpm/action-setup action to v5 ([#&#8203;20762](https://github.com/eslint/eslint/issues/20762)) (renovate\[bot])
- [`51080eb`](https://github.com/eslint/eslint/commit/51080eb5c98d619434e4835dbe9f1c6654aca3b8) test: processor service ([#&#8203;20731](https://github.com/eslint/eslint/issues/20731)) (kuldeep kumar)
- [`e7e1889`](https://github.com/eslint/eslint/commit/e7e1889fca9b6044e08f41b38df20a1ce45808c8) chore: remove stale babel-eslint10 fixture and test ([#&#8203;20727](https://github.com/eslint/eslint/issues/20727)) (kuldeep kumar)
- [`4e1a87c`](https://github.com/eslint/eslint/commit/4e1a87cb8fb90e309524bc36bc5f31b9f9cfaa76) test: remove redundant async/await in flat config array tests ([#&#8203;20722](https://github.com/eslint/eslint/issues/20722)) (Pixel998)
- [`066eabb`](https://github.com/eslint/eslint/commit/066eabb3643b12931f991594969bcc0028f71a5f) test: add rule metadata coverage for `languages` and `docs.dialects` ([#&#8203;20717](https://github.com/eslint/eslint/issues/20717)) (Pixel998)
</details>
<details>
<summary>NaturalIntelligence/fast-xml-parser (fast-xml-parser)</summary>
### [`v5.7.2`](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.7.2): backward compatibility for numerical external entity, fix #&#8203;705, #&#8203;817
[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.7.1...v5.7.2)
- allow numerical external entity for backward compatibility
- fix [#&#8203;705](https://github.com/NaturalIntelligence/fast-xml-parser/issues/705): attributesGroupName working with preserveOrder
- fix [#&#8203;817](https://github.com/NaturalIntelligence/fast-xml-parser/issues/817): stackoverflow when tag expression is very long
### [`v5.7.1`](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.7.1): upgrade @&#8203;nodable/entities and FXB
[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.7.0...v5.7.1)
- Use `@nodable/entities` v2.1.0
- breaking changes
- single entity scan. You're not allowed to use entity value to form another entity name.
- you cant add numeric external entity
- entity error message when expantion limit is crossed might change
- typings are updated for new options related to process entity
- please follow documentation of `@nodable/entities` for more detail.
- performance
- if processEntities is false, then there should not be impact on performance.
- if processEntities is true, but you dont pass entity decoder separately then performance may degrade by approx 8-10%
- if processEntities is true, and you pass entity decoder separately
- if no entity then performance should be same as before
- if there are entities then performance should be increased from past versions
- ignoreAttributes is not required to be set to set xml version for NCR entity value
- update 'fast-xml-builder' to sanitize malicious CDATA and comment's content
### [`v5.7.0`](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.6.0...v5.7.0)
[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.6.0...v5.7.0)
</details>
<details>
<summary>versatica/mediasoup (mediasoup)</summary>
### [`v3.19.21`](https://github.com/versatica/mediasoup/blob/HEAD/CHANGELOG.md#31921)
[Compare Source](https://github.com/versatica/mediasoup/compare/3.19.20...3.19.21)
- Worker: Fix regression in `DirectTransport` when closing a `DataProducer` or `DataConsumer` ([PR #&#8203;1780](https://github.com/versatica/mediasoup/pull/1780)).
### [`v3.19.20`](https://github.com/versatica/mediasoup/blob/HEAD/CHANGELOG.md#31920)
[Compare Source](https://github.com/versatica/mediasoup/compare/3.19.19...3.19.20)
- Worker: Add `useBuiltInSctpStack` setting (defaults to `false`) to enable mediasoup built-in SCTP stack ([PR #&#8203;1777](https://github.com/versatica/mediasoup/pull/1777)).
</details>
<details>
<summary>versatica/mediasoup-client (mediasoup-client)</summary>
### [`v3.19.0`](https://github.com/versatica/mediasoup-client/compare/3.18.8...3.19.0)
[Compare Source](https://github.com/versatica/mediasoup-client/compare/3.18.8...3.19.0)
</details>
<details>
<summary>vuejs/core (vue)</summary>
### [`v3.5.33`](https://github.com/vuejs/core/blob/HEAD/CHANGELOG.md#3533-2026-04-22)
[Compare Source](https://github.com/vuejs/core/compare/v3.5.32...v3.5.33)
##### Bug Fixes
- **compiler-sfc:** handle nested :deep in selector pseudos ([#&#8203;14725](https://github.com/vuejs/core/issues/14725)) ([bb9d265](https://github.com/vuejs/core/commit/bb9d265d8dcdde2af824fc01b24f9a7b3169f5fa)), closes [#&#8203;14724](https://github.com/vuejs/core/issues/14724)
- **reactivity:** unlink effect scopes on out-of-order off ([#&#8203;14734](https://github.com/vuejs/core/issues/14734)) ([e7659be](https://github.com/vuejs/core/commit/e7659beafc5407e892fa70f3f4ade80263b0905d)), closes [#&#8203;14733](https://github.com/vuejs/core/issues/14733)
- **runtime-dom:** preserve textarea resize dimensions ([#&#8203;14747](https://github.com/vuejs/core/issues/14747)) ([11fb2fd](https://github.com/vuejs/core/commit/11fb2fd4a246e40f6f350701dfea73ec525b4f59)), closes [#&#8203;14741](https://github.com/vuejs/core/issues/14741)
- **teleport:** don't move teleport children if not mounted ([#&#8203;14702](https://github.com/vuejs/core/issues/14702)) ([6a61f44](https://github.com/vuejs/core/commit/6a61f4452ba1a31fc929cadf8abe3337ac4d3a46)), closes [#&#8203;14701](https://github.com/vuejs/core/issues/14701)
- **transition:** preserve placeholder for conditional explicit default slots ([#&#8203;14748](https://github.com/vuejs/core/issues/14748)) ([45990ce](https://github.com/vuejs/core/commit/45990cecf4604b2f39c571ab6aefa49d362af36a)), closes [#&#8203;14727](https://github.com/vuejs/core/issues/14727)
</details>
<details>
<summary>vuejs/router (vue-router)</summary>
### [`v5.0.6`](https://github.com/vuejs/router/releases/tag/v5.0.6)
[Compare Source](https://github.com/vuejs/router/compare/v5.0.5...v5.0.6)
#####    🐞 Bug Fixes
- Missing closing quote in generated import  -  by [@&#8203;zjy040525](https://github.com/zjy040525) and [@&#8203;posva](https://github.com/posva) in [#&#8203;2688](https://github.com/vuejs/router/issues/2688) [<samp>(32f78)</samp>](https://github.com/vuejs/router/commit/32f78c77)
#####     [View changes on GitHub](https://github.com/vuejs/router/compare/v5.0.5...v5.0.6)
### [`v5.0.5`](https://github.com/vuejs/router/releases/tag/v5.0.5)
[Compare Source](https://github.com/vuejs/router/compare/v5.0.4...v5.0.5)
#####    🚀 Features
- Enable standard schema param parsers  -  by [@&#8203;posva](https://github.com/posva) [<samp>(ea8e3)</samp>](https://github.com/vuejs/router/commit/ea8e3e21)
- Normalize param parsers once  -  by [@&#8203;posva](https://github.com/posva) [<samp>(48087)</samp>](https://github.com/vuejs/router/commit/480877cc)
#####    🐞 Bug Fixes
- Track definePage imports per-file to fix named view race condition  -  by [@&#8203;posva](https://github.com/posva) [<samp>(11191)</samp>](https://github.com/vuejs/router/commit/11191bca)
- Avoid double decoding hash on string location  -  by [@&#8203;posva](https://github.com/posva) [<samp>(1578c)</samp>](https://github.com/vuejs/router/commit/1578c9e9)
#####     [View changes on GitHub](https://github.com/vuejs/router/compare/v5.0.4...v5.0.5)
</details>
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMzIuMyIsInVwZGF0ZWRJblZlciI6IjQzLjE1MC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
## [1.1.3] - 2026-04-19
### Changed
- update dependency mediasoup-client to v3.18.8 (#30)
This PR contains the following updates:
| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [mediasoup-client](https://mediasoup.org) ([source](https://github.com/versatica/mediasoup-client)) | [`3.18.7` → `3.18.8`](https://renovatebot.com/diffs/npm/mediasoup-client/3.18.7/3.18.8) | ![age](https://developer.mend.io/api/mc/badges/age/npm/mediasoup-client/3.18.8?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/mediasoup-client/3.18.7/3.18.8?slim=true) |
---
### Release Notes
<details>
<summary>versatica/mediasoup-client (mediasoup-client)</summary>
### [`v3.18.8`](https://github.com/versatica/mediasoup-client/compare/3.18.7...3.18.8)
[Compare Source](https://github.com/versatica/mediasoup-client/compare/3.18.7...3.18.8)
</details>
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMjkuMCIsInVwZGF0ZWRJblZlciI6IjQzLjEyOS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
## [1.1.2] - 2026-04-15
### Changed
- Update dependency fast-xml-parser to v5.6.0 (#28)
This PR contains the following updates:
| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) | [`5.5.12` → `5.6.0`](https://renovatebot.com/diffs/npm/fast-xml-parser/5.5.12/5.6.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/fast-xml-parser/5.6.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/fast-xml-parser/5.5.12/5.6.0?slim=true) |
---
### Release Notes
<details>
<summary>NaturalIntelligence/fast-xml-parser (fast-xml-parser)</summary>
### [`v5.6.0`](https://github.com/NaturalIntelligence/fast-xml-parser/releases/tag/v5.6.0): use @&#8203;nodable/entities to replace entities
[Compare Source](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.5.12...v5.6.0)
- No API change
- No change in performance for basic usage
- No typing change
- No config change
- new dependency
- breaking: error messages for entities might have been changed.
-
**Full Changelog**: <https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.5.12...v5.6.0>
</details>
---
### Configuration
📅 **Schedule**: (UTC)
- Branch creation
- At any time (no schedule defined)
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
**Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
---
This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMjAuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEyMC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
## [1.1.1] - 2026-04-15
### Changed
- split push release/publish and harden workflows (#27)
### Added
* Separate release from Docker/Helm publish
* enrich releases with PRbodies when available
* tighten release.sh validation and idempotency
* trim PR docker-build metadata for act-runner stability
## [1.1.0] - 2026-04-15
### Changed
- Update all non-major dependencies (#25)
## [1.0.10] - 2026-04-15
### Changed
- Remove npm overrides for tar (#26)
## [1.0.9] - 2026-03-24
### Changed
- update https://git.keligrubb.com/actions/setup-helm action to v5 (#23)
## [1.0.8] - 2026-03-12
### Changed
- fix release file (#22)
## [1.0.7] - 2026-03-06
### Changed
- chore(deps): update docker/build-push-action action to v7 (#19)
## [1.0.6] - 2026-03-05
### Changed
- fix docker login during push stage (#18)
## [1.0.5] - 2026-03-05
### Changed
- fix deploy pipeline stages for token registry uploads (#17)
## [1.0.4] - 2026-03-04
### Changed
- fix deploy pipeline (#15)
## [1.0.3] - 2026-02-23
### Changed
- fix(deps): update dependency vue-router to v5 (#12)
## [1.0.2] - 2026-02-22
### Changed
- chore(deps): update dependency eslint to v10 (#10)
## [1.0.1] - 2026-02-22 ## [1.0.1] - 2026-02-22
### Changed ### Changed
- chore: Configure Renovate (#7) - chore: Configure Renovate (#7)
+2 -2
View File
@@ -56,7 +56,7 @@ See [docs/live-streaming.md](docs/live-streaming.md) for setup and usage.
### ATAK / CoT (Cursor on Target) ### ATAK / CoT (Cursor on Target)
KestrelOS can act as a **TAK Server** so ATAK and iTAK devices connect and share positions. No plugins: in ATAK, add a **Server** connection (host = KestrelOS, port **8089** for CoT). Check **Use Authentication** and enter your **KestrelOS username** and **password** (local users use their login password; OIDC users must set an **ATAK password** once under **Account** in the web app). Devices relay CoT to each other (team members see each other on the ATAK map) and appear on the KestrelOS web map; they drop off after ~90 seconds if no updates. Optional: set `COT_TTL_MS`, `COT_REQUIRE_AUTH`; CoT runs on port 8089 (default). KestrelOS can act as a **TAK Server** so ATAK and iTAK devices connect and share positions. No plugins: in ATAK, add a **Server** connection (host = KestrelOS, port **8089** for CoT). Check **Use Authentication** and enter your **KestrelOS username** and **password** (local users use their login password; OIDC users must set an **ATAK password** once under **Account** in the web app). Devices relay CoT to each other (team members see each other on the ATAK map) and appear on the KestrelOS web map; they drop off after ~90 seconds if no updates. CoT runs on port 8089 (default).
## Scripts ## Scripts
@@ -74,7 +74,7 @@ Full docs are in the **[docs/](docs/README.md)** directory: [installation](docs/
## Configuration ## Configuration
- **Devices**: Manage cameras/devices via the API (`/api/devices`); see [Map and cameras](docs/map-and-cameras.md). Each device needs `name`, `device_type`, `lat`, `lng`, `stream_url`, and `source_type` (`mjpeg` or `hls`). - **Devices**: Manage cameras/devices via the API (`/api/devices`); see [Map and cameras](docs/map-and-cameras.md). Each device needs `name`, `device_type`, `lat`, `lng`, `stream_url`, and `source_type` (`mjpeg` or `hls`).
- **Environment**: No required env vars for basic run. For production, set `HOST=0.0.0.0` and expose ports 3000 (web/API) and 8089 (CoT). Set `COT_TTL_MS=90000`, `COT_REQUIRE_AUTH=true`. For TLS use `.dev-certs/` or set `COT_SSL_CERT` and `COT_SSL_KEY`. - **Environment**: No required env vars for basic run. For production, set `HOST=0.0.0.0` and expose ports 3000 (web/API) and 8089 (CoT). For TLS use `.dev-certs/` or set `COT_SSL_CERT` and `COT_SSL_KEY`.
- **Authentication**: The login page always offers password sign-in (local). Optionally set `BOOTSTRAP_EMAIL` and `BOOTSTRAP_PASSWORD` before the first run to create the first admin; otherwise a default admin is created and its credentials are printed in the terminal. To also show an OIDC sign-in button, configure `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, and optionally `OIDC_LABEL`, `OIDC_REDIRECT_URI`. See [docs/auth.md](docs/auth.md) for local login, OIDC config, and sign up. - **Authentication**: The login page always offers password sign-in (local). Optionally set `BOOTSTRAP_EMAIL` and `BOOTSTRAP_PASSWORD` before the first run to create the first admin; otherwise a default admin is created and its credentials are printed in the terminal. To also show an OIDC sign-in button, configure `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, and optionally `OIDC_LABEL`, `OIDC_REDIRECT_URI`. See [docs/auth.md](docs/auth.md) for local login, OIDC config, and sign up.
- **Bootstrap admin** (when using local auth): The server initializes the database and runs bootstrap at startup. On first run (no users in the database), it creates the first admin. If you set `BOOTSTRAP_EMAIL` and `BOOTSTRAP_PASSWORD` before starting, that account is created. If you don't set them, a default admin is created (identifier: `admin`) with a random password and the credentials are printed in the terminal-copy them and sign in at `/login`, then change the password or add users via Members. Use **Members** to change roles (admin, leader, member). Only admins can change roles; admins and leaders can edit POIs. - **Bootstrap admin** (when using local auth): The server initializes the database and runs bootstrap at startup. On first run (no users in the database), it creates the first admin. If you set `BOOTSTRAP_EMAIL` and `BOOTSTRAP_PASSWORD` before starting, that account is created. If you don't set them, a default admin is created (identifier: `admin`) with a random password and the credentials are printed in the terminal-copy them and sign in at `/login`, then change the password or add users via Members. Use **Members** to change roles (admin, leader, member). Only admins can change roles; admins and leaders can edit POIs.
- **Database**: SQLite file at `data/kestrelos.db` (created automatically). Contains users, sessions, and POIs. - **Database**: SQLite file at `data/kestrelos.db` (created automatically). Contains users, sessions, and POIs.
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<NuxtLayout> <NuxtLayout>
<NuxtPage :key="$route.path" /> <NuxtPage />
</NuxtLayout> </NuxtLayout>
</template> </template>
+38 -2
View File
@@ -16,6 +16,9 @@
.kestrel-btn-secondary { @apply rounded border border-kestrel-border px-4 py-2 text-sm text-kestrel-text transition-colors hover:bg-kestrel-border; } .kestrel-btn-secondary { @apply rounded border border-kestrel-border px-4 py-2 text-sm text-kestrel-text transition-colors hover:bg-kestrel-border; }
.kestrel-context-menu-item { @apply block w-full px-3 py-1.5 text-left text-sm text-kestrel-text transition-colors hover:bg-kestrel-border; } .kestrel-context-menu-item { @apply block w-full px-3 py-1.5 text-left text-sm text-kestrel-text transition-colors hover:bg-kestrel-border; }
.kestrel-context-menu-item-danger { @apply block w-full px-3 py-1.5 text-left text-sm text-red-400 transition-colors hover:bg-kestrel-border; } .kestrel-context-menu-item-danger { @apply block w-full px-3 py-1.5 text-left text-sm text-red-400 transition-colors hover:bg-kestrel-border; }
.kestrel-cot-layer-btn { @apply rounded px-1.5 py-0.5 text-kestrel-muted transition-colors hover:text-kestrel-text; }
.kestrel-cot-layer-btn-active { @apply bg-kestrel-border text-kestrel-accent; }
.cot-icon-rotatable { @apply inline-flex origin-center; }
.kestrel-panel-base { @apply flex flex-col border border-kestrel-border bg-kestrel-surface; } .kestrel-panel-base { @apply flex flex-col border border-kestrel-border bg-kestrel-surface; }
.kestrel-panel-inline { @apply rounded-lg shadow-glow; } .kestrel-panel-inline { @apply rounded-lg shadow-glow; }
.kestrel-panel-overlay { @apply absolute right-0 top-0 z-[1000] h-full w-full border-l shadow-glow md:w-[420px] shadow-glow-panel; } .kestrel-panel-overlay { @apply absolute right-0 top-0 z-[1000] h-full w-full border-l shadow-glow md:w-[420px] shadow-glow-panel; }
@@ -84,12 +87,14 @@
} }
.kestrel-map-container .leaflet-control-zoom, .kestrel-map-container .leaflet-control-zoom,
.kestrel-map-container .leaflet-control-locate, .kestrel-map-container .leaflet-control-locate,
.kestrel-map-container .leaflet-control-alpr,
.kestrel-map-container .savetiles.leaflet-bar { .kestrel-map-container .savetiles.leaflet-bar {
@apply rounded-md overflow-hidden font-mono border border-kestrel-glow shadow-glow-sm; @apply rounded-md overflow-hidden font-mono border border-kestrel-glow shadow-glow-sm;
border-color: rgba(34, 201, 201, 0.35) !important; border-color: rgba(34, 201, 201, 0.35) !important;
} }
.kestrel-map-container .leaflet-control-zoom a, .kestrel-map-container .leaflet-control-zoom a,
.kestrel-map-container .leaflet-control-locate, .kestrel-map-container .leaflet-control-locate,
.kestrel-map-container .leaflet-control-alpr,
.kestrel-map-container .savetiles.leaflet-bar a { .kestrel-map-container .savetiles.leaflet-bar a {
@apply w-8 h-8 leading-8 bg-kestrel-surface text-kestrel-text border-none rounded-none text-lg font-semibold no-underline transition-all duration-150; @apply w-8 h-8 leading-8 bg-kestrel-surface text-kestrel-text border-none rounded-none text-lg font-semibold no-underline transition-all duration-150;
width: 32px !important; width: 32px !important;
@@ -105,9 +110,14 @@
} }
.kestrel-map-container .leaflet-control-zoom a:hover, .kestrel-map-container .leaflet-control-zoom a:hover,
.kestrel-map-container .leaflet-control-locate:hover, .kestrel-map-container .leaflet-control-locate:hover,
.kestrel-map-container .leaflet-control-alpr:hover,
.kestrel-map-container .savetiles.leaflet-bar a:hover { .kestrel-map-container .savetiles.leaflet-bar a:hover {
@apply bg-kestrel-surface-hover text-kestrel-accent shadow-glow-md text-shadow-glow-md; @apply bg-kestrel-surface-hover text-kestrel-accent shadow-glow-md text-shadow-glow-md;
} }
.kestrel-map-container .leaflet-control-alpr[aria-pressed="true"] {
color: #ef4444 !important;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.45);
}
.kestrel-map-container .savetiles.leaflet-bar { .kestrel-map-container .savetiles.leaflet-bar {
@apply flex flex-col; @apply flex flex-col;
} }
@@ -119,12 +129,38 @@
padding: 6px 10px !important; padding: 6px 10px !important;
font-size: 11px !important; font-size: 11px !important;
} }
.kestrel-map-container .leaflet-control-locate { .kestrel-map-container .leaflet-control-locate,
.kestrel-map-container .leaflet-control-alpr {
@apply flex items-center justify-center p-0 cursor-pointer; @apply flex items-center justify-center p-0 cursor-pointer;
} }
.kestrel-map-container .leaflet-control-locate svg { .kestrel-map-container .leaflet-control-locate svg,
.kestrel-map-container .leaflet-control-alpr svg {
color: currentColor; color: currentColor;
} }
.kestrel-map-container .alpr-cone {
display: inline-flex;
transform-origin: center center;
}
.kestrel-map-container .alpr-cluster-icon {
background: transparent;
border: none;
}
.kestrel-map-container .alpr-cluster {
@apply flex items-center justify-center rounded-full bg-red-500/90 font-mono text-xs font-semibold text-white;
width: 100%;
height: 100%;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.5);
}
.kestrel-map-container .cot-cluster-icon {
background: transparent;
border: none;
}
.kestrel-map-container .cot-cluster {
@apply flex items-center justify-center rounded-full bg-sky-500/90 font-mono text-xs font-semibold text-white;
width: 100%;
height: 100%;
box-shadow: 0 0 8px rgba(56, 189, 248, 0.5);
}
.kestrel-map-container .live-session-icon { .kestrel-map-container .live-session-icon {
animation: live-pulse 1.5s ease-in-out infinite; animation: live-pulse 1.5s ease-in-out infinite;
} }
+6 -1
View File
@@ -26,6 +26,11 @@
:user="user" :user="user"
@signout="onLogout" @signout="onLogout"
/> />
<span
v-else-if="authPending"
class="inline-block h-8 w-8"
aria-hidden="true"
/>
<NuxtLink <NuxtLink
v-else v-else
to="/login" to="/login"
@@ -75,7 +80,7 @@ watch(sidebarCollapsed, (v) => {
} }
}) })
const { user, refresh } = useUser() const { user, authPending, refresh } = useUser()
watch(isMobile, (mobile) => { watch(isMobile, (mobile) => {
if (mobile) drawerOpen.value = false if (mobile) drawerOpen.value = false
+136 -46
View File
@@ -47,11 +47,41 @@
@submit="onPoiSubmit" @submit="onPoiSubmit"
@confirm-delete="confirmDeletePoi" @confirm-delete="confirmDeletePoi"
/> />
<div
v-if="mapContext"
class="pointer-events-auto absolute right-3 top-3 z-[1000] flex gap-0.5 rounded border border-kestrel-border bg-kestrel-surface/95 p-0.5 text-xs shadow-glow"
data-testid="cot-layer-toggles"
>
<button
v-for="layer in COT_LAYERS"
:key="layer.key"
type="button"
class="kestrel-cot-layer-btn"
:class="{ 'kestrel-cot-layer-btn-active': cotLayers[layer.key] }"
@click="emit('toggleCotLayer', layer.key)"
>
{{ layer.label }}
</button>
</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import 'leaflet/dist/leaflet.css' import 'leaflet/dist/leaflet.css'
import {
createAlprControl,
createAlprLayer,
setAlprControlPressed,
syncAlprLayer,
} from '~/utils/alprMapLayer.js'
import {
createCotLayer,
getCotClusters,
loadCotCluster,
syncCotLayer,
} from '~/utils/cotMapLayer.js'
import { clearFeatureMarkers } from '~/utils/mapMarkerSync.js'
const props = defineProps({ const props = defineProps({
devices: { devices: {
@@ -70,13 +100,25 @@ const props = defineProps({
type: Array, type: Array,
default: () => [], default: () => [],
}, },
cotLayers: {
type: Object,
default: () => ({ air: true, surface: true, ground: true }),
},
alprMarkers: {
type: Array,
default: () => [],
},
showAlpr: {
type: Boolean,
default: false,
},
canEditPois: { canEditPois: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}) })
const emit = defineEmits(['select', 'selectLive', 'refreshPois']) const emit = defineEmits(['select', 'selectLive', 'refreshPois', 'boundsChange', 'toggleAlpr', 'toggleCotLayer'])
const CONTEXT_MENU_EMPTY = Object.freeze({ type: null, poi: null, latlng: null, x: 0, y: 0 }) const CONTEXT_MENU_EMPTY = Object.freeze({ type: null, poi: null, latlng: null, x: 0, y: 0 })
const mapRef = ref(null) const mapRef = ref(null)
const contextMenuRef = ref(null) const contextMenuRef = ref(null)
@@ -85,7 +127,9 @@ const mapContext = ref(null)
const markersRef = ref([]) const markersRef = ref([])
const poiMarkersRef = ref({}) const poiMarkersRef = ref({})
const liveMarkersRef = ref({}) const liveMarkersRef = ref({})
const cotMarkersRef = ref({}) const cotLayerRef = ref(null)
const cotMapView = ref(null)
const alprLayerRef = ref(null)
const contextMenu = ref({ ...CONTEXT_MENU_EMPTY }) const contextMenu = ref({ ...CONTEXT_MENU_EMPTY })
const showPoiModal = ref(false) const showPoiModal = ref(false)
@@ -104,6 +148,11 @@ const DEFAULT_ZOOM = 17
const MARKER_ICON_PATH = '/' const MARKER_ICON_PATH = '/'
const POI_TOOLTIP_CLASS = 'kestrel-poi-tooltip' const POI_TOOLTIP_CLASS = 'kestrel-poi-tooltip'
const POI_ICON_COLORS = { pin: '#22c9c9', flag: '#e53e3e', waypoint: '#a78bfa' } const POI_ICON_COLORS = { pin: '#22c9c9', flag: '#e53e3e', waypoint: '#a78bfa' }
const COT_LAYERS = Object.freeze([
{ key: 'air', label: 'Air' },
{ key: 'surface', label: 'Surface' },
{ key: 'ground', label: 'Team' },
])
const ICON_SIZE = 28 const ICON_SIZE = 28
@@ -141,14 +190,41 @@ function getLiveSessionIcon(L) {
}) })
} }
const COT_ICON_COLOR = '#f59e0b' /* amber - ATAK/CoT devices */ function refreshCotMapView(map) {
function getCotEntityIcon(L) { const bounds = map.getBounds()
const html = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${COT_ICON_COLOR}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="8" r="2.5" fill="${COT_ICON_COLOR}"/></svg>` cotMapView.value = {
return L.divIcon({ south: bounds.getSouth(),
className: 'poi-div-icon cot-entity-icon', west: bounds.getWest(),
html: `<span class="poi-icon-svg">${html}</span>`, north: bounds.getNorth(),
iconSize: [ICON_SIZE, ICON_SIZE], east: bounds.getEast(),
iconAnchor: [ICON_SIZE / 2, ICON_SIZE], zoom: map.getZoom(),
}
}
function renderCotLayer() {
const ctx = mapContext.value
const { L } = leafletRef.value || {}
const layer = cotLayerRef.value
if (!ctx?.map || !L || !layer) return
const view = cotMapView.value
const features = view ? getCotClusters(view) : []
syncCotLayer(L, ctx.map, layer, features)
}
function reloadCotCluster() {
loadCotCluster(props.cotEntities || [])
renderCotLayer()
}
function emitBounds(map) {
refreshCotMapView(map)
const bounds = map.getBounds()
emit('boundsChange', {
south: bounds.getSouth(),
west: bounds.getWest(),
north: bounds.getNorth(),
east: bounds.getEast(),
zoom: map.getZoom(),
}) })
} }
@@ -160,7 +236,7 @@ function createMap(initialCenter) {
? initialCenter ? initialCenter
: DEFAULT_VIEW : DEFAULT_VIEW
const map = L.map(mapRef.value, { zoomControl: false, attributionControl: false }).setView(center, DEFAULT_ZOOM) const map = L.map(mapRef.value, { zoomControl: false, attributionControl: false, minZoom: 1, maxZoom: 19 }).setView(center, DEFAULT_ZOOM)
L.control.zoom({ position: 'topleft' }).addTo(map) L.control.zoom({ position: 'topleft' }).addTo(map)
const locateControl = L.control({ position: 'topleft' }) const locateControl = L.control({ position: 'topleft' })
@@ -186,6 +262,14 @@ function createMap(initialCenter) {
} }
locateControl.addTo(map) locateControl.addTo(map)
const alprControl = createAlprControl(L, {
showAlpr: props.showAlpr,
onToggle: () => emit('toggleAlpr'),
})
alprControl.addTo(map)
const alprLayer = createAlprLayer(L, map)
const cotLayer = createCotLayer(L, map)
const baseLayer = L.tileLayer(TILE_URL, { const baseLayer = L.tileLayer(TILE_URL, {
attribution: ATTRIBUTION, attribution: ATTRIBUTION,
subdomains: TILE_SUBDOMAINS, subdomains: TILE_SUBDOMAINS,
@@ -214,12 +298,28 @@ function createMap(initialCenter) {
contextMenu.value = { type: 'map', latlng: e.latlng, x: pt.x, y: pt.y } contextMenu.value = { type: 'map', latlng: e.latlng, x: pt.x, y: pt.y }
}) })
mapContext.value = { map, layer: baseLayer, control, locateControl } map.on('moveend', () => {
emitBounds(map)
renderCotLayer()
})
map.on('zoomend', () => {
emitBounds(map)
renderCotLayer()
})
mapContext.value = { map, layer: baseLayer, control, locateControl, alprControl }
alprLayerRef.value = alprLayer
cotLayerRef.value = cotLayer
refreshCotMapView(map)
updateMarkers() updateMarkers()
updatePoiMarkers() updatePoiMarkers()
updateLiveMarkers() updateLiveMarkers()
updateCotMarkers() reloadCotCluster()
nextTick(() => map.invalidateSize()) updateAlprLayer()
nextTick(() => {
map.invalidateSize()
emitBounds(map)
})
} }
function updateMarkers() { function updateMarkers() {
@@ -309,37 +409,16 @@ function updateLiveMarkers() {
liveMarkersRef.value = next liveMarkersRef.value = next
} }
function updateCotMarkers() { function updateAlprLayer() {
const ctx = mapContext.value const ctx = mapContext.value
const { L } = leafletRef.value || {} const { L } = leafletRef.value || {}
if (!ctx?.map || !L) return const layer = alprLayerRef.value
if (!ctx?.map || !L || !layer) return
const entities = (props.cotEntities || []).filter( if (!props.showAlpr) {
e => typeof e?.lat === 'number' && typeof e?.lng === 'number' && e?.id, clearFeatureMarkers(layer)
) return
const byId = Object.fromEntries(entities.map(e => [e.id, e])) }
const prev = cotMarkersRef.value syncAlprLayer(L, ctx.map, layer, props.alprMarkers || [])
const icon = getCotEntityIcon(L)
Object.keys(prev).forEach((id) => {
if (!byId[id]) prev[id]?.remove()
})
const next = entities.reduce((acc, entity) => {
const content = `<div class="kestrel-live-popup"><strong>${escapeHtml(entity.label || entity.id)}</strong> <span class="text-kestrel-muted">ATAK</span></div>`
const existing = prev[entity.id]
if (existing) {
existing.setLatLng([entity.lat, entity.lng])
existing.setIcon(icon)
existing.getPopup()?.setContent(content)
return { ...acc, [entity.id]: existing }
}
const marker = L.marker([entity.lat, entity.lng], { icon })
.addTo(ctx.map)
.bindPopup(content, { className: 'kestrel-live-popup-wrap', maxWidth: 360 })
return { ...acc, [entity.id]: marker }
}, {})
cotMarkersRef.value = next
} }
function escapeHtml(text) { function escapeHtml(text) {
@@ -427,17 +506,22 @@ function destroyMap() {
poiMarkersRef.value = {} poiMarkersRef.value = {}
Object.values(liveMarkersRef.value).forEach(m => m?.remove()) Object.values(liveMarkersRef.value).forEach(m => m?.remove())
liveMarkersRef.value = {} liveMarkersRef.value = {}
Object.values(cotMarkersRef.value).forEach(m => m?.remove()) clearFeatureMarkers(cotLayerRef.value)
cotMarkersRef.value = {} clearFeatureMarkers(alprLayerRef.value)
const ctx = mapContext.value const ctx = mapContext.value
if (ctx) { if (ctx) {
if (ctx.control && ctx.map) ctx.map.removeControl(ctx.control) if (ctx.control && ctx.map) ctx.map.removeControl(ctx.control)
if (ctx.locateControl && ctx.map) ctx.map.removeControl(ctx.locateControl) if (ctx.locateControl && ctx.map) ctx.map.removeControl(ctx.locateControl)
if (ctx.alprControl && ctx.map) ctx.map.removeControl(ctx.alprControl)
if (cotLayerRef.value && ctx.map) ctx.map.removeLayer(cotLayerRef.value)
if (alprLayerRef.value && ctx.map) ctx.map.removeLayer(alprLayerRef.value)
if (ctx.layer && ctx.map) ctx.map.removeLayer(ctx.layer) if (ctx.layer && ctx.map) ctx.map.removeLayer(ctx.layer)
if (ctx.map) ctx.map.remove() if (ctx.map) ctx.map.remove()
mapContext.value = null mapContext.value = null
} }
cotLayerRef.value = null
alprLayerRef.value = null
} }
function initMapWithLocation() { function initMapWithLocation() {
@@ -503,5 +587,11 @@ onBeforeUnmount(() => {
watch(() => props.devices, () => updateMarkers(), { deep: true }) watch(() => props.devices, () => updateMarkers(), { deep: true })
watch([() => props.pois, () => props.canEditPois], () => updatePoiMarkers(), { deep: true }) watch([() => props.pois, () => props.canEditPois], () => updatePoiMarkers(), { deep: true })
watch(() => props.liveSessions, () => updateLiveMarkers(), { deep: true }) watch(() => props.liveSessions, () => updateLiveMarkers(), { deep: true })
watch(() => props.cotEntities, () => updateCotMarkers(), { deep: true }) watch(() => props.cotEntities, () => reloadCotCluster())
watch(() => props.alprMarkers, () => updateAlprLayer())
watch(() => props.showAlpr, (enabled) => {
setAlprControlPressed(mapContext.value?.alprControl, enabled)
if (enabled && mapContext.value?.map) emitBounds(mapContext.value.map)
else updateAlprLayer()
})
</script> </script>
+146
View File
@@ -0,0 +1,146 @@
import { createClusterIndex } from '~/utils/mapCluster.js'
import {
MAX_BBOX_DEGREES,
bboxFetchKey,
bboxToTileKey,
tileKey,
tilesNearCenter,
} from '~/utils/alprViewport.js'
const STORAGE_KEY = 'kestrelos:showAlprLayer'
const MIN_FETCH_ZOOM = 10
const MAX_TILE_FETCHES = 16
const MAX_CACHED_TILES = 64
const TILE_RETENTION = 3
const EMPTY_TILES = Object.freeze({})
const mergeFeatures = lists => Object.values(
lists.flat().reduce((acc, feature) => {
const id = feature.id ?? feature.properties?.osmId
return id == null ? acc : { ...acc, [id]: feature }
}, {}),
)
const offsets = radius => Array.from({ length: 2 * radius + 1 }, (_, i) => i - radius)
const retentionKeys = bounds => new Set(
tilesNearCenter(bounds, MAX_TILE_FETCHES).flatMap((tile) => {
const r0 = Math.floor(tile.south / MAX_BBOX_DEGREES)
const c0 = Math.floor(tile.west / MAX_BBOX_DEGREES)
return offsets(TILE_RETENTION).flatMap(dr =>
offsets(TILE_RETENTION).map(dc => tileKey(r0 + dr, c0 + dc)),
)
}),
)
const pruneTileCache = (cache, bounds) => {
const keep = retentionKeys(bounds)
const retained = Object.fromEntries(
Object.entries(cache).filter(([key]) => keep.has(key)),
)
const overflow = Object.keys(retained).length - MAX_CACHED_TILES
if (overflow <= 0) return Object.freeze(retained)
const cr = Math.floor((bounds.south + bounds.north) / 2 / MAX_BBOX_DEGREES)
const cc = Math.floor((bounds.west + bounds.east) / 2 / MAX_BBOX_DEGREES)
const drop = new Set(
Object.keys(retained)
.map((key) => {
const [r, c] = key.split(',').map(Number)
return { key, dist: Math.hypot(r - cr, c - cc) }
})
.sort((a, b) => b.dist - a.dist)
.slice(0, overflow)
.map(({ key }) => key),
)
return Object.freeze(
Object.fromEntries(Object.entries(retained).filter(([key]) => !drop.has(key))),
)
}
const cacheChanged = (before, after) => Object.keys(before).length !== Object.keys(after).length
export function useAlprCameras() {
const showAlpr = useState('showAlprLayer', () => {
if (!import.meta.client) return true
return localStorage.getItem(STORAGE_KEY) !== '0'
})
const view = ref(null)
const tiles = ref(EMPTY_TILES)
const cluster = createClusterIndex({ radius: 50, maxZoom: 17 })
const debounceTimer = ref(null)
const requestId = ref(0)
const lastFetchKey = ref('')
const applyTiles = (next) => {
tiles.value = next
cluster.load(mergeFeatures(Object.values(next)))
}
watch(showAlpr, (enabled) => {
if (import.meta.client) localStorage.setItem(STORAGE_KEY, enabled ? '1' : '0')
if (!enabled) {
applyTiles(EMPTY_TILES)
view.value = null
lastFetchKey.value = ''
}
})
const alprMarkers = computed(() => view.value ? cluster.query(view.value) : [])
const toggleAlpr = () => {
showAlpr.value = !showAlpr.value
}
const onBoundsChange = (bounds) => {
if (!showAlpr.value || !bounds) return
view.value = bounds
const pruned = pruneTileCache(tiles.value, bounds)
if (cacheChanged(tiles.value, pruned)) applyTiles(pruned)
if ((bounds.zoom ?? 14) < MIN_FETCH_ZOOM) return
const key = bboxFetchKey(bounds)
if (key === lastFetchKey.value) return
if (debounceTimer.value) clearTimeout(debounceTimer.value)
debounceTimer.value = setTimeout(() => {
lastFetchKey.value = key
fetchViewport(bounds)
}, 400)
}
const fetchViewport = async (bounds) => {
const id = requestId.value + 1
requestId.value = id
const missing = tilesNearCenter(bounds, MAX_TILE_FETCHES)
.filter(tile => !(bboxToTileKey(tile) in tiles.value))
try {
const fetched = missing.length
? await Promise.all(missing.map(async (tile) => {
const params = new URLSearchParams({
south: String(tile.south),
west: String(tile.west),
north: String(tile.north),
east: String(tile.east),
})
const data = await $fetch(`/api/alpr?${params}`).catch(() => null)
return [bboxToTileKey(tile), data?.features ?? []]
}))
: []
if (id !== requestId.value) return
const merged = Object.freeze({
...tiles.value,
...Object.fromEntries(fetched),
})
const pruned = pruneTileCache(merged, bounds)
applyTiles(pruned)
}
catch { /* keep last good index */ }
}
return Object.freeze({ showAlpr, toggleAlpr, alprMarkers, onBoundsChange })
}
+2 -3
View File
@@ -1,6 +1,6 @@
/** Fetches devices + live sessions; polls when tab visible. */ /** Fetches devices + live sessions; polls when tab visible. */
const POLL_MS = 1500 const POLL_MS = 1500
const EMPTY_RESPONSE = Object.freeze({ devices: [], liveSessions: [], cotEntities: [] }) const EMPTY_RESPONSE = Object.freeze({ devices: [], liveSessions: [] })
export function useCameras(options = {}) { export function useCameras(options = {}) {
const { poll: enablePoll = true } = options const { poll: enablePoll = true } = options
@@ -12,7 +12,6 @@ export function useCameras(options = {}) {
const devices = computed(() => Object.freeze([...(data.value?.devices ?? [])])) const devices = computed(() => Object.freeze([...(data.value?.devices ?? [])]))
const liveSessions = computed(() => Object.freeze([...(data.value?.liveSessions ?? [])])) const liveSessions = computed(() => Object.freeze([...(data.value?.liveSessions ?? [])]))
const cotEntities = computed(() => Object.freeze([...(data.value?.cotEntities ?? [])]))
const cameras = computed(() => Object.freeze([...devices.value, ...liveSessions.value])) const cameras = computed(() => Object.freeze([...devices.value, ...liveSessions.value]))
const pollInterval = ref(null) const pollInterval = ref(null)
@@ -37,5 +36,5 @@ export function useCameras(options = {}) {
}) })
onBeforeUnmount(stopPolling) onBeforeUnmount(stopPolling)
return Object.freeze({ data, devices, liveSessions, cotEntities, cameras, refresh, startPolling, stopPolling }) return Object.freeze({ data, devices, liveSessions, cameras, refresh, startPolling, stopPolling })
} }
+48
View File
@@ -0,0 +1,48 @@
const STORAGE_KEY = 'kestrel-cot-layers'
const DEFAULT_LAYERS = Object.freeze({ air: true, surface: true, ground: true })
function loadLayers() {
if (typeof localStorage === 'undefined') return { ...DEFAULT_LAYERS }
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return { ...DEFAULT_LAYERS }
const parsed = JSON.parse(raw)
return {
air: parsed.air !== false,
surface: parsed.surface !== false,
ground: parsed.ground !== false,
}
}
catch {
return { ...DEFAULT_LAYERS }
}
}
function saveLayers(layers) {
if (typeof localStorage === 'undefined') return
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(layers))
}
catch { /* ignore quota */ }
}
export function useCotLayers() {
const layers = ref(loadLayers())
const layerQuery = computed(() => {
const parts = []
if (layers.value.air) parts.push('air')
if (layers.value.surface) parts.push('surface')
if (layers.value.ground) parts.push('ground')
return parts.length ? parts.join(',') : 'none'
})
function toggleLayer(name) {
if (!(name in DEFAULT_LAYERS)) return
layers.value = { ...layers.value, [name]: !layers.value[name] }
saveLayers(layers.value)
}
return Object.freeze({ layers, layerQuery, toggleLayer })
}
+117
View File
@@ -0,0 +1,117 @@
import { bboxFetchKey } from '~/utils/alprViewport.js'
const DEBOUNCE_MS = 300
const EMPTY_ENTITIES = Object.freeze({})
const expandBounds = (bounds, factor = 0.25) => {
const latPad = (bounds.north - bounds.south) * factor
const lngPad = (bounds.east - bounds.west) * factor
return {
south: Math.max(-90, bounds.south - latPad),
north: Math.min(90, bounds.north + latPad),
west: bounds.west - lngPad,
east: bounds.east + lngPad,
}
}
const entitiesFromList = list => Object.freeze(
Object.fromEntries(
(list ?? []).filter(entity => entity?.id).map(entity => [entity.id, entity]),
),
)
export function useCotStream(boundsRef, layerQueryRef) {
const entities = ref(EMPTY_ENTITIES)
const cotEntities = computed(() => Object.freeze(Object.values(entities.value)))
const eventSource = ref(null)
const debounceTimer = ref(null)
const subscribedKey = ref('')
const streamBounds = ref(null)
const setEntities = (record) => {
entities.value = Object.freeze(record)
}
const parseEvent = (e, fn) => {
try {
fn(JSON.parse(e.data))
}
catch { /* ignore */ }
}
const closeStream = () => {
eventSource.value?.close()
eventSource.value = null
}
const connect = () => {
if (typeof window === 'undefined' || typeof EventSource === 'undefined') return
const bounds = streamBounds.value ?? boundsRef.value
if (!bounds) return
closeStream()
const q = new URLSearchParams({
bbox: `${bounds.west},${bounds.south},${bounds.east},${bounds.north}`,
layers: unref(layerQueryRef) || 'air,surface,ground',
})
const es = new EventSource(`/api/cot/stream?${q}`)
eventSource.value = es
es.addEventListener('snapshot', e => parseEvent(e, ({ entities: list }) => {
setEntities(entitiesFromList(list))
}))
es.addEventListener('update', e => parseEvent(e, ({ entity }) => {
if (!entity?.id) return
setEntities({ ...entities.value, [entity.id]: entity })
}))
es.addEventListener('remove', e => parseEvent(e, ({ id }) => {
if (!id) return
setEntities(Object.fromEntries(
Object.entries(entities.value).filter(([key]) => key !== String(id)),
))
}))
es.onerror = () => {
closeStream()
scheduleConnect()
}
}
const scheduleConnect = () => {
if (debounceTimer.value) clearTimeout(debounceTimer.value)
debounceTimer.value = setTimeout(() => {
debounceTimer.value = null
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return
connect()
}, DEBOUNCE_MS)
}
const maybeReconnect = (bounds) => {
if (!bounds) return
const key = bboxFetchKey(bounds)
if (key === subscribedKey.value) return
subscribedKey.value = key
streamBounds.value = expandBounds(bounds)
scheduleConnect()
}
watch(boundsRef, maybeReconnect, { deep: true })
watch(layerQueryRef, () => {
subscribedKey.value = ''
scheduleConnect()
})
onMounted(() => {
if (typeof document === 'undefined') return
document.addEventListener('visibilitychange', () => {
document.visibilityState === 'visible' ? scheduleConnect() : closeStream()
})
if (document.visibilityState === 'visible') maybeReconnect(boundsRef.value)
})
onBeforeUnmount(() => {
if (debounceTimer.value) clearTimeout(debounceTimer.value)
closeStream()
})
return Object.freeze({ cotEntities })
}
+3 -2
View File
@@ -2,12 +2,13 @@ const EDIT_ROLES = Object.freeze(['admin', 'leader'])
export function useUser() { export function useUser() {
const requestFetch = useRequestFetch() const requestFetch = useRequestFetch()
const { data: user, refresh } = useAsyncData( const { data: user, refresh, status } = useAsyncData(
'user', 'user',
() => (requestFetch ?? $fetch)('/api/me').catch(() => null), () => (requestFetch ?? $fetch)('/api/me').catch(() => null),
{ default: () => null }, { default: () => null },
) )
const authPending = computed(() => status.value === 'pending')
const canEditPois = computed(() => EDIT_ROLES.includes(user.value?.role)) const canEditPois = computed(() => EDIT_ROLES.includes(user.value?.role))
const isAdmin = computed(() => user.value?.role === 'admin') const isAdmin = computed(() => user.value?.role === 'admin')
return Object.freeze({ user, canEditPois, isAdmin, refresh }) return Object.freeze({ user, authPending, canEditPois, isAdmin, refresh })
} }
+1 -1
View File
@@ -3,7 +3,7 @@ const LOGIN_PATH = '/login'
export default defineNuxtRouteMiddleware(async (to) => { export default defineNuxtRouteMiddleware(async (to) => {
if (to.path === LOGIN_PATH) return if (to.path === LOGIN_PATH) return
const { user, refresh } = useUser() const { user, refresh } = useUser()
await refresh() if (!user.value) await refresh()
if (user.value) return if (user.value) return
const redirect = to.fullPath.startsWith('/') ? to.fullPath : `/${to.fullPath}` const redirect = to.fullPath.startsWith('/') ? to.fullPath : `/${to.fullPath}`
return navigateTo({ path: LOGIN_PATH, query: { redirect } }, { replace: true }) return navigateTo({ path: LOGIN_PATH, query: { redirect } }, { replace: true })
+22 -1
View File
@@ -7,11 +7,23 @@
:pois="pois ?? []" :pois="pois ?? []"
:live-sessions="liveSessions ?? []" :live-sessions="liveSessions ?? []"
:cot-entities="cotEntities ?? []" :cot-entities="cotEntities ?? []"
:cot-layers="cotLayers"
:alpr-markers="showAlpr ? (alprMarkers ?? []) : []"
:show-alpr="showAlpr"
:can-edit-pois="canEditPois" :can-edit-pois="canEditPois"
@select="selectedCamera = $event" @select="selectedCamera = $event"
@select-live="onSelectLive($event)" @select-live="onSelectLive($event)"
@refresh-pois="refreshPois" @refresh-pois="refreshPois"
@bounds-change="onMapBoundsChange"
@toggle-alpr="toggleAlpr"
@toggle-cot-layer="toggleLayer"
/> />
<template #fallback>
<div
class="h-full min-h-[300px] bg-kestrel-bg"
aria-hidden="true"
/>
</template>
</ClientOnly> </ClientOnly>
</div> </div>
<CameraViewer <CameraViewer
@@ -23,11 +35,20 @@
</template> </template>
<script setup> <script setup>
const { devices, liveSessions, cotEntities } = useCameras() const { devices, liveSessions } = useCameras()
const { data: pois, refresh: refreshPois } = usePois() const { data: pois, refresh: refreshPois } = usePois()
const { canEditPois } = useUser() const { canEditPois } = useUser()
const { showAlpr, toggleAlpr, alprMarkers, onBoundsChange: onAlprBoundsChange } = useAlprCameras()
const { layers: cotLayers, layerQuery, toggleLayer } = useCotLayers()
const mapBounds = ref(null)
const { cotEntities } = useCotStream(mapBounds, layerQuery)
const selectedCamera = ref(null) const selectedCamera = ref(null)
function onMapBoundsChange(bounds) {
mapBounds.value = bounds
onAlprBoundsChange(bounds)
}
function onSelectLive(session) { function onSelectLive(session) {
selectedCamera.value = (liveSessions.value ?? []).find(s => s.id === session?.id) ?? session selectedCamera.value = (liveSessions.value ?? []).find(s => s.id === session?.id) ?? session
} }
+220
View File
@@ -0,0 +1,220 @@
import { syncFeatureMarkers } from './mapMarkerSync.js'
const ICON_SIZE = 28
const CONE_SIZE = 52
const ALPR_COLOR = '#ef4444'
const DEFAULT_FOV = 60
function escapeHtml(text) {
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
function conePath(fov) {
const half = Math.min(Math.max(fov / 2, 10), 85)
const cx = 26
const cy = 26
const r = 24
const toRad = deg => ((deg - 90) * Math.PI) / 180
const x1 = cx + r * Math.cos(toRad(-half))
const y1 = cy + r * Math.sin(toRad(-half))
const x2 = cx + r * Math.cos(toRad(half))
const y2 = cy + r * Math.sin(toRad(half))
return `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 0 1 ${x2} ${y2} Z`
}
function compassLabel(deg) {
if (!Number.isFinite(deg)) return null
const dirs = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']
const idx = Math.round((((deg % 360) + 360) % 360) / 45) % 8
return dirs[idx]
}
function wikidataLink(label, qid) {
if (!qid) return escapeHtml(label)
const id = escapeHtml(qid)
return `<a href="https://www.wikidata.org/wiki/${id}" target="_blank" rel="noopener">${escapeHtml(label)}</a>`
}
function popupLine(parts) {
const line = parts.filter(Boolean).join(' · ')
return line ? `<div class="text-kestrel-muted text-xs mt-1">${line}</div>` : ''
}
function titleHtml(props) {
if (props.name) return escapeHtml(props.name)
if (props.model) {
const mfr = props.manufacturer ? escapeHtml(props.manufacturer) : null
return mfr ? `${mfr} <span class="text-kestrel-accent">${escapeHtml(props.model)}</span>` : escapeHtml(props.model)
}
const makeModel = [props.manufacturer, props.model].filter(Boolean).join(' ')
if (makeModel) {
if (props.manufacturerWikidata && !props.model) return wikidataLink(makeModel, props.manufacturerWikidata)
return escapeHtml(makeModel)
}
if (props.operator) return wikidataLink(props.operator, props.operatorWikidata)
if (props.ref) return escapeHtml(props.ref)
return 'ALPR camera'
}
function titleText(props) {
if (props.name) return props.name
if (props.model) {
return [props.manufacturer, props.model].filter(Boolean).join(' ')
}
const makeModel = [props.manufacturer, props.model].filter(Boolean).join(' ')
if (makeModel) return makeModel
if (props.operator) return props.operator
if (props.ref) return props.ref
return 'ALPR camera'
}
function modelLineHtml(props) {
if (props.model) {
return `<div class="text-sm mt-0.5"><span class="text-kestrel-muted">Model</span> <strong>${escapeHtml(props.model)}</strong></div>`
}
if (props.modelUnknown) {
return '<div class="text-kestrel-muted text-xs mt-0.5">Model not recorded in OpenStreetMap</div>'
}
return ''
}
/** Human-readable ALPR popup (GeoJSON properties in, HTML out). */
export function formatAlprPopup(props) {
const title = titleHtml(props)
const headline = titleText(props)
const makeModel = [props.manufacturer, props.model].filter(Boolean).join(' ')
const identity = []
if (props.name && makeModel) identity.push(escapeHtml(makeModel))
if (props.operator && headline !== props.operator) {
identity.push(wikidataLink(props.operator, props.operatorWikidata))
}
if (props.ref && headline !== props.ref) identity.push(escapeHtml(props.ref))
if (props.brand) identity.push(escapeHtml(props.brand))
const view = []
if (props.direction != null) {
const deg = Math.round(props.direction)
const compass = compassLabel(props.direction)
view.push(compass ? `Facing ${compass} (${deg}°)` : `Facing ${deg}°`)
}
if (props.fov != null && props.direction != null) view.push(`~${Math.round(props.fov)}° view`)
const note = props.description || props.note
const noteHtml = note ? `<div class="text-kestrel-muted text-xs mt-1">${escapeHtml(note)}</div>` : ''
const identityHtml = identity.length
? `<div class="text-kestrel-muted text-xs mt-0.5">${identity.join(' · ')}</div>`
: ''
const modelHtml = (props.model || props.modelUnknown) ? modelLineHtml(props) : ''
const foot = `<div class="text-kestrel-muted text-xs mt-2"><a href="https://www.openstreetmap.org/node/${props.osmId}" target="_blank" rel="noopener">View on OpenStreetMap</a></div>`
return `<div class="kestrel-live-popup"><strong>${title}</strong> <span class="text-kestrel-muted">License plate reader</span>${modelHtml}${identityHtml}${popupLine(view)}${noteHtml}${foot}</div>`
}
function popupHtml(props) {
return formatAlprPopup(props)
}
function pointIcon(L, direction, fov) {
const hasDirection = Number.isFinite(direction)
if (!hasDirection) {
const html = `<span class="poi-icon-svg"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${ALPR_COLOR}" stroke-width="2"><rect x="3" y="5" width="18" height="12" rx="2"/><circle cx="12" cy="11" r="3"/><path d="M8 21h8"/></svg></span>`
return L.divIcon({
className: 'poi-div-icon alpr-icon',
html,
iconSize: [ICON_SIZE, ICON_SIZE],
iconAnchor: [ICON_SIZE / 2, ICON_SIZE],
})
}
const spread = Number.isFinite(fov) ? fov : DEFAULT_FOV
const html = `<span class="alpr-cone" style="transform:rotate(${direction}deg)"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52" width="${CONE_SIZE}" height="${CONE_SIZE}"><path d="${conePath(spread)}" fill="${ALPR_COLOR}" fill-opacity="0.3" stroke="${ALPR_COLOR}" stroke-width="1.5"/><circle cx="26" cy="26" r="3" fill="${ALPR_COLOR}"/></svg></span>`
return L.divIcon({
className: 'poi-div-icon alpr-icon',
html,
iconSize: [CONE_SIZE, CONE_SIZE],
iconAnchor: [CONE_SIZE / 2, CONE_SIZE / 2],
})
}
function clusterIcon(L, count) {
const size = count < 10 ? 28 : count < 100 ? 34 : 40
return L.divIcon({
className: 'alpr-cluster-icon',
html: `<span class="alpr-cluster">${count}</span>`,
iconSize: [size, size],
iconAnchor: [size / 2, size / 2],
})
}
export function createAlprControl(L, { showAlpr, onToggle }) {
const control = L.control({ position: 'topleft' })
control.onAdd = function () {
const el = document.createElement('button')
el.type = 'button'
el.className = 'leaflet-bar leaflet-control-alpr'
el.title = 'Toggle ALPR cameras (OSM)'
el.setAttribute('aria-label', 'Toggle ALPR cameras')
el.setAttribute('aria-pressed', showAlpr ? 'true' : 'false')
el.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><rect x="3" y="5" width="18" height="12" rx="2"/><circle cx="12" cy="11" r="3"/></svg>'
el.addEventListener('click', onToggle)
control._button = el
return el
}
return control
}
export function setAlprControlPressed(control, pressed) {
control?._button?.setAttribute('aria-pressed', pressed ? 'true' : 'false')
}
function featureKey(feature) {
const props = feature.properties ?? {}
if (props.cluster) return `c:${props.cluster_id}`
const id = props.osmId
return id != null ? `a:${id}` : null
}
function coords(feature) {
const [lng, lat] = feature.geometry.coordinates
return { lat, lng }
}
function attachClusterClick(marker, feature, map) {
marker.on('click', () => {
const { lat, lng } = coords(feature)
const props = feature.properties ?? {}
const zoom = props.expansionZoom ?? map.getZoom() + 2
map.setView([lat, lng], Math.min(zoom, 19), { animate: true })
})
}
export function createAlprLayer(L, map) {
return L.layerGroup().addTo(map)
}
export function syncAlprLayer(L, map, layer, features) {
syncFeatureMarkers(layer, features, {
keyFor: featureKey,
create: (feature) => {
const { lat, lng } = coords(feature)
const props = feature.properties ?? {}
const isCluster = Boolean(props.cluster)
const icon = isCluster ? clusterIcon(L, props.point_count) : pointIcon(L, props.direction, props.fov)
const marker = L.marker([lat, lng], { icon })
if (isCluster) attachClusterClick(marker, feature, map)
else marker.bindPopup(popupHtml(props), { className: 'kestrel-live-popup-wrap', maxWidth: 320 })
return marker
},
update: (marker, feature) => {
const { lat, lng } = coords(feature)
const props = feature.properties ?? {}
const isCluster = Boolean(props.cluster)
marker.setLatLng([lat, lng])
const icon = isCluster ? clusterIcon(L, props.point_count) : pointIcon(L, props.direction, props.fov)
marker.setIcon(icon)
if (!isCluster) marker.setPopupContent(popupHtml(props))
},
})
}
+77
View File
@@ -0,0 +1,77 @@
export const MAX_BBOX_DEGREES = 0.5
export function tileKey(row, col) {
return `${row},${col}`
}
export function bboxToTileKey(bbox) {
const row = Math.floor(bbox.south / MAX_BBOX_DEGREES)
const col = Math.floor(bbox.west / MAX_BBOX_DEGREES)
return tileKey(row, col)
}
function tileBox(row, col, step = MAX_BBOX_DEGREES) {
const south = row * step
const west = col * step
return { south, west, north: south + step, east: west + step }
}
export function bboxFetchKey(bounds) {
const zoom = bounds.zoom ?? 14
const step = zoom >= 14 ? 0.025 : zoom >= 11 ? 0.1 : zoom >= 8 ? 0.25 : 1
const q = v => Math.round(v / step) * step
return [q(bounds.south), q(bounds.west), q(bounds.north), q(bounds.east)].join(',')
}
function ringOffsets(radius) {
if (radius === 0) return [[0, 0]]
return Array.from({ length: 2 * radius + 1 }, (_, i) => i - radius)
.flatMap(dr => Array.from({ length: 2 * radius + 1 }, (_, j) => j - radius)
.filter(dc => Math.abs(dr) === radius || Math.abs(dc) === radius)
.map(dc => [dr, dc]))
}
function collectTiles(state) {
const {
centerRow, centerCol, minRow, maxRow, minCol, maxCol, step, limit, radius, seen, tiles,
} = state
if (tiles.length >= limit) return tiles
const inBounds = (row, col) => row >= minRow && row <= maxRow && col >= minCol && col <= maxCol
const { nextSeen, added } = ringOffsets(radius)
.map(([dr, dc]) => [centerRow + dr, centerCol + dc])
.filter(([row, col]) => inBounds(row, col))
.reduce((acc, [row, col]) => {
const key = `${row},${col}`
if (acc.nextSeen.has(key)) return acc
return {
nextSeen: new Set([...acc.nextSeen, key]),
added: [...acc.added, tileBox(row, col, step)],
}
}, { nextSeen: seen, added: [] })
if (added.length === 0 && radius > 0) return tiles
const nextTiles = [...tiles, ...added].slice(0, limit)
if (nextTiles.length >= limit) return nextTiles
return collectTiles({ ...state, radius: radius + 1, seen: nextSeen, tiles: nextTiles })
}
export function tilesNearCenter(bounds, limit) {
const step = MAX_BBOX_DEGREES
const lat = (bounds.south + bounds.north) / 2
const lng = (bounds.west + bounds.east) / 2
return collectTiles({
centerRow: Math.floor(lat / step),
centerCol: Math.floor(lng / step),
minRow: Math.floor(bounds.south / step),
maxRow: Math.ceil(bounds.north / step) - 1,
minCol: Math.floor(bounds.west / step),
maxCol: Math.ceil(bounds.east / step) - 1,
step,
limit,
radius: 0,
seen: new Set(),
tiles: [],
})
}
+131
View File
@@ -0,0 +1,131 @@
/** Map CoT / ADS-B entity display: icons and popups. */
export const COT_COLORS = {
air: '#60a5fa',
helicopter: '#fbbf24',
surface: '#38bdf8',
ground: '#f59e0b',
}
export function cotCategory(type) {
const t = typeof type === 'string' ? type : ''
if (t.startsWith('a-f-A-')) return 'air'
if (t.startsWith('a-f-S-')) return 'surface'
return 'ground'
}
/** Whether the entity is a helicopter or fixed-wing aircraft. @returns {'helicopter' | 'fixedWing'} */
export function cotAirIconKind(entity) {
const type = entity?.type ?? ''
if (type.endsWith('-C-H') || type.endsWith('-M-H')) return 'helicopter'
return 'fixedWing'
}
function iconWrap(heading, inner) {
const rotate = Number.isFinite(heading) ? ` style="transform:rotate(${heading}deg)"` : ''
return `<span class="poi-icon-svg cot-icon-rotatable"${rotate}>${inner}</span>`
}
const PLANE_SVG = color =>
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="${color}"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>`
const HELI_SVG = color =>
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="1.75" stroke-linecap="round"><circle cx="12" cy="12" r="2.5" fill="${color}"/><path d="M3 8h18M3 12h18"/><path d="M12 8v8"/><path d="M9 16h6"/></svg>`
const SHIP_SVG = color =>
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2"><path d="M2 20c2-4 6-6 10-6s8 2 10 6"/><path d="M12 14V4"/><path d="m8 8 4-4 4 4"/></svg>`
const GROUND_SVG = color =>
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="8" r="2.5" fill="${color}"/></svg>`
export function getCotIconHtml(entity) {
const category = cotCategory(entity?.type)
const heading = Number(entity?.heading)
if (category === 'air') {
const kind = cotAirIconKind(entity)
const color = kind === 'helicopter' ? COT_COLORS.helicopter : COT_COLORS.air
const svg = kind === 'helicopter' ? HELI_SVG(color) : PLANE_SVG(color)
return { html: iconWrap(heading, svg), className: `cot-entity-${kind}` }
}
if (category === 'surface') {
return { html: iconWrap(heading, SHIP_SVG(COT_COLORS.surface)), className: 'cot-entity-surface' }
}
return { html: iconWrap(undefined, GROUND_SVG(COT_COLORS.ground)), className: 'cot-entity-ground' }
}
function msToKnots(ms) {
return Number.isFinite(ms) ? Math.round(ms * 1.94384) : null
}
function metersToFeet(m) {
return Number.isFinite(m) ? Math.round(m * 3.28084) : null
}
function fmtHeading(deg) {
return Number.isFinite(deg) ? `${Math.round(deg)}°` : null
}
function fmtVerticalFpm(ms) {
if (!Number.isFinite(ms) || ms === 0) return null
const fpm = Math.round(ms * 196.85)
return `${fpm > 0 ? '+' : ''}${fpm} fpm`
}
function icaoFromEntity(entity) {
if (entity?.icao) return String(entity.icao).toUpperCase()
if (typeof entity?.id === 'string' && entity.id.startsWith('ICAO.')) {
return entity.id.slice(5).toUpperCase()
}
return null
}
function mmsiFromEntity(entity) {
if (entity?.mmsi) return String(entity.mmsi)
if (typeof entity?.id === 'string' && entity.id.startsWith('MMSI.')) return entity.id.slice(5)
return null
}
function popupLine(escape, parts) {
const line = parts.filter(Boolean).join(' · ')
return line ? `<div class="text-kestrel-muted text-xs mt-1">${line}</div>` : ''
}
/**
* @param {Record<string, unknown>} entity
* @param {(s: string) => string} escape
*/
export function formatCotPopup(entity, escape) {
const category = cotCategory(entity?.type)
const label = escape(entity?.label || entity?.id || 'Unknown')
if (entity?.source === 'adsb' || category === 'air') {
const tag = cotAirIconKind(entity) === 'helicopter' ? 'Helicopter' : 'Aircraft'
const icao = icaoFromEntity(entity)
const meta = [
icao ? `ICAO ${icao}` : null,
entity?.originCountry ? escape(String(entity.originCountry)) : null,
].filter(Boolean).join(' · ')
const alt = metersToFeet(entity?.altitude)
const stats = [
alt != null ? `${alt.toLocaleString()} ft` : null,
entity?.onGround ? 'On ground' : null,
msToKnots(entity?.speed) != null ? `${msToKnots(entity.speed)} kt` : null,
fmtHeading(entity?.heading),
fmtVerticalFpm(entity?.verticalRate),
entity?.squawk ? `Squawk ${escape(String(entity.squawk))}` : null,
]
return `<div class="kestrel-live-popup"><strong>${label}</strong> <span class="text-kestrel-muted">${tag}</span>${meta ? `<div class="text-kestrel-muted text-xs mt-0.5">${meta}</div>` : ''}${popupLine(escape, stats)}</div>`
}
if (entity?.source === 'ais' || category === 'surface') {
const mmsi = mmsiFromEntity(entity)
const meta = mmsi ? `MMSI ${escape(mmsi)}` : ''
const stats = [
Number.isFinite(entity?.speed) ? `${Number(entity.speed).toFixed(1)} kt` : null,
fmtHeading(entity?.heading),
]
return `<div class="kestrel-live-popup"><strong>${label}</strong> <span class="text-kestrel-muted">Vessel</span>${meta ? `<div class="text-kestrel-muted text-xs mt-0.5">${meta}</div>` : ''}${popupLine(escape, stats)}</div>`
}
return `<div class="kestrel-live-popup"><strong>${label}</strong> <span class="text-kestrel-muted">Team</span></div>`
}
+107
View File
@@ -0,0 +1,107 @@
import { createClusterIndex } from './mapCluster.js'
import { syncFeatureMarkers } from './mapMarkerSync.js'
import { cotCategory, formatCotPopup, getCotIconHtml } from './cotDisplay.js'
const ICON_SIZE = 28
const CLUSTER = createClusterIndex({ radius: 50, maxZoom: 14, minPoints: 2 })
function escapeHtml(text) {
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
export function entitiesToFeatures(entities) {
return (entities || [])
.filter(e => typeof e?.lat === 'number' && typeof e?.lng === 'number' && e?.id)
.map(e => ({
type: 'Feature',
geometry: { type: 'Point', coordinates: [e.lng, e.lat] },
properties: { entity: e, cotCategory: cotCategory(e.type) },
}))
}
export function loadCotCluster(entities) {
CLUSTER.load(entitiesToFeatures(entities))
}
export function getCotClusters(view) {
return CLUSTER.query(view)
}
function featureKey(feature) {
const props = feature.properties ?? {}
if (props.cluster) return `c:${props.cluster_id}`
const id = props.entity?.id
return id != null ? `e:${id}` : null
}
function clusterIcon(L, count) {
const size = count < 10 ? 28 : count < 100 ? 34 : 40
return L.divIcon({
className: 'cot-cluster-icon',
html: `<span class="cot-cluster">${count}</span>`,
iconSize: [size, size],
iconAnchor: [size / 2, size / 2],
})
}
function entityIcon(L, entity) {
const { html, className } = getCotIconHtml(entity)
return L.divIcon({
className: `poi-div-icon cot-entity-icon ${className}`,
html,
iconSize: [ICON_SIZE, ICON_SIZE],
iconAnchor: [ICON_SIZE / 2, ICON_SIZE / 2],
})
}
function coords(feature) {
const [lng, lat] = feature.geometry.coordinates
return { lat, lng }
}
function attachClusterClick(marker, feature, map) {
marker.on('click', () => {
const { lat, lng } = coords(feature)
const props = feature.properties ?? {}
const zoom = props.expansionZoom ?? map.getZoom() + 2
map.setView([lat, lng], Math.min(zoom, 19), { animate: true })
})
}
export function createCotLayer(L, map) {
return L.layerGroup().addTo(map)
}
export function syncCotLayer(L, map, layer, features) {
syncFeatureMarkers(layer, features, {
keyFor: featureKey,
create: (feature) => {
const { lat, lng } = coords(feature)
const props = feature.properties ?? {}
const isCluster = Boolean(props.cluster)
const icon = isCluster ? clusterIcon(L, props.point_count) : entityIcon(L, props.entity)
const marker = L.marker([lat, lng], { icon })
if (isCluster) attachClusterClick(marker, feature, map)
else if (props.entity) {
marker.bindPopup(
formatCotPopup(props.entity, escapeHtml),
{ className: 'kestrel-live-popup-wrap', maxWidth: 360 },
)
}
return marker
},
update: (marker, feature) => {
const { lat, lng } = coords(feature)
const props = feature.properties ?? {}
const isCluster = Boolean(props.cluster)
marker.setLatLng([lat, lng])
const icon = isCluster ? clusterIcon(L, props.point_count) : entityIcon(L, props.entity)
marker.setIcon(icon)
if (!isCluster && props.entity) {
marker.setPopupContent(formatCotPopup(props.entity, escapeHtml))
}
},
})
}
+31
View File
@@ -0,0 +1,31 @@
import Supercluster from 'supercluster'
export function createClusterIndex(options = {}) {
const index = new Supercluster(options)
const state = { features: Object.freeze([]) }
return {
load(features) {
const list = Object.freeze([...(features ?? [])])
index.load(list)
state.features = list
},
query(view) {
if (!view || state.features.length === 0) return []
const { west, south, east, north, zoom } = view
return index.getClusters(
[west, south, east, north],
Math.floor(zoom ?? 14),
).map((feature) => {
if (!feature.properties?.cluster) return feature
return {
...feature,
properties: {
...feature.properties,
expansionZoom: index.getClusterExpansionZoom(feature.properties.cluster_id),
},
}
})
},
}
}
+37
View File
@@ -0,0 +1,37 @@
const SYNC_KEY = '_kestrelMarkerSync'
const pointFeatures = (features, keyFor) => (features ?? [])
.filter(f => f?.geometry?.type === 'Point')
.map(f => ({ feature: f, key: keyFor(f) }))
.filter(({ key }) => key != null)
export function syncFeatureMarkers(layer, features, { keyFor, create, update }) {
const prev = layer[SYNC_KEY] ?? new Map()
const next = pointFeatures(features, keyFor).reduce((map, { feature, key }) => {
const existing = prev.get(key)
if (existing) {
update(existing, feature)
return new Map([...map, [key, existing]])
}
const marker = create(feature)
layer.addLayer(marker)
return new Map([...map, [key, marker]])
}, new Map())
Array.from(prev.entries())
.filter(([key]) => !next.has(key))
.forEach(([, marker]) => layer.removeLayer(marker))
layer[SYNC_KEY] = next
}
export function clearFeatureMarkers(layer) {
if (!layer) return
const prev = layer[SYNC_KEY]
if (prev) {
Array.from(prev.values()).forEach(marker => layer.removeLayer(marker))
layer[SYNC_KEY] = new Map()
return
}
layer.clearLayers()
}
+2 -1
View File
@@ -2,6 +2,8 @@
KestrelOS acts as a **TAK Server**. ATAK (Android) and iTAK (iOS) connect on **port 8089** (CoT). Devices relay positions to each other and appear on the KestrelOS map. KestrelOS acts as a **TAK Server**. ATAK (Android) and iTAK (iOS) connect on **port 8089** (CoT). Devices relay positions to each other and appear on the KestrelOS map.
ADS-B and AIS via [adsbcot](https://github.com/snstac/adsbcot) / [aiscot](https://github.com/snstac/aiscot): see [tracking.md](tracking.md).
## Connection ## Connection
**Host:** KestrelOS hostname/IP **Host:** KestrelOS hostname/IP
@@ -59,7 +61,6 @@ OIDC users must set an **ATAK password** first:
| Variable | Default | Description | | Variable | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
| `COT_PORT` | `8089` | CoT server port | | `COT_PORT` | `8089` | CoT server port |
| `COT_TTL_MS` | `90000` | Device timeout (~90s) |
| `COT_REQUIRE_AUTH` | `true` | Require authentication | | `COT_REQUIRE_AUTH` | `true` | Require authentication |
| `COT_SSL_CERT` | `.dev-certs/cert.pem` | TLS cert path | | `COT_SSL_CERT` | `.dev-certs/cert.pem` | TLS cert path |
| `COT_SSL_KEY` | `.dev-certs/key.pem` | TLS key path | | `COT_SSL_KEY` | `.dev-certs/key.pem` | TLS key path |
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

+10
View File
@@ -5,6 +5,7 @@ KestrelOS shows a **map** with devices, POIs, live sessions (Share live), and AT
## Map Layers ## Map Layers
- **Devices** - Fixed feeds (IPTV, ALPR, CCTV, NVR, etc.) added via API - **Devices** - Fixed feeds (IPTV, ALPR, CCTV, NVR, etc.) added via API
- **ALPR (OSM / DeFlock)** - Crowdsourced license-plate cameras from OpenStreetMap; toggle on the map (camera icon control). Reference only, no stream.
- **POIs** - Points of interest (admin/leader can edit) - **POIs** - Points of interest (admin/leader can edit)
- **Live sessions** - Mobile devices streaming via Share live - **Live sessions** - Mobile devices streaming via Share live
- **CoT (ATAK/iTAK)** - Amber markers for connected TAK devices (position only) - **CoT (ATAK/iTAK)** - Amber markers for connected TAK devices (position only)
@@ -47,6 +48,15 @@ Stream URLs must be `http://` or `https://`.
**Cameras endpoint:** `GET /api/cameras` returns devices + live sessions + CoT entities. **Cameras endpoint:** `GET /api/cameras` returns devices + live sessions + CoT entities.
## ALPR layer (DeFlock / OpenStreetMap)
deflock.me has no bulk download API. KestrelOS queries OpenStreetMap via Overpass (`surveillance:type=ALPR`) and returns **GeoJSON FeatureCollection** from `GET /api/alpr`.
- **Map:** ALPR layer is on by default (toggle top-left to hide). Marker popups show OSM identifying tags (manufacturer, model, operator, ref, Wikidata, etc.) when contributors tagged them.
- **Offline:** Run `npm run import:alpr` to preload SQLite; cache serves automatically when Overpass is unreachable.
Attribution: © OpenStreetMap contributors.
## POIs ## POIs
Admins/leaders add/edit from **POI** page (sidebar). POIs appear as map pins (reference only, no stream). Admins/leaders add/edit from **POI** page (sidebar). POIs appear as map pins (reference only, no stream).
+48
View File
@@ -0,0 +1,48 @@
# ADS-B and AIS
Aircraft and vessels use the same **CoT** store as ATAK/iTAK. The map consumes `GET /api/cot/stream` (SSE, viewport bbox). Toggle **Air**, **Surface**, and **Team** on the map.
## Accuracy tiers
1. **Tactical (best):** local SDR/AIS receiver → [adsbcot](https://github.com/snstac/adsbcot) / [aiscot](https://github.com/snstac/aiscot) → KestrelOS CoT `:8089` (sub-second updates).
2. **Vessels (live OSINT):** AISStream WebSocket push as vessels transmit.
3. **Aircraft (awareness OSINT):** OpenSky bbox poll — not a live stream; typical lag ~5s.
For tactical use, run local receivers. Do not rely on OpenSky alone.
## Freshness
Tracks update via SSE `update` events (CoT `:8089`, AISStream) or coalesced `snapshot` after each OpenSky poll. Stale tracks are removed automatically (team ~90s, OSINT ~30s without a new fix).
OSINT feeds run only while a map client is connected (SSE subscriber). Keep the map tab visible for live updates.
## Self-hosted
**ADS-B:** [adsbcot](https://github.com/snstac/adsbcot) → `tls://host:8089`
```ini
[adsbcot]
COT_URL = tls://kestrelos.example.com:8089
FEED_URL = tcp+beast://127.0.0.1:30005
```
**AIS:** [aiscot](https://github.com/snstac/aiscot) → `tls://host:8089`
```ini
[aiscot]
COT_URL = tls://kestrelos.example.com:8089
FEED_URL = tcp://127.0.0.1:10110
```
Use KestrelOS credentials (see [atak-itak.md](atak-itak.md)).
## OSINT APIs (optional)
Set these only if you want viewport OSINT without local receivers:
| Variable | Purpose |
|----------|---------|
| `AISSTREAM_API_KEY` | AISStream WebSocket |
| `OPENSKY_CLIENT_ID` / `OPENSKY_CLIENT_SECRET` | OpenSky OAuth (recommended for production) |
UIDs: `ICAO.*` (ADS-B), `MMSI.*` (AIS). Icons follow CoT type (`a-f-A-*`, `a-f-S-*`, `a-f-G-*`).
+2 -2
View File
@@ -2,5 +2,5 @@ apiVersion: v2
name: kestrelos name: kestrelos
description: KestrelOS TOC for OSINT feeds - map, camera feeds, offline tiles description: KestrelOS TOC for OSINT feeds - map, camera feeds, offline tiles
type: application type: application
version: 1.0.1 version: 1.1.12
appVersion: "1.0.1" appVersion: "1.1.12"
+1 -1
View File
@@ -2,7 +2,7 @@ replicaCount: 1
image: image:
repository: git.keligrubb.com/keligrubb/kestrelos repository: git.keligrubb.com/keligrubb/kestrelos
tag: 1.0.1 tag: 1.1.12
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
service: service:
+3 -1
View File
@@ -32,9 +32,11 @@ export default defineNuxtConfig({
public: { public: {
version: pkg.version ?? '', version: pkg.version ?? '',
}, },
cotTtlMs: 90_000,
cotRequireAuth: true, cotRequireAuth: true,
cotDebug: false, cotDebug: false,
aisstreamApiKey: process.env.AISSTREAM_API_KEY || '',
openskyClientId: process.env.OPENSKY_CLIENT_ID || '',
openskyClientSecret: process.env.OPENSKY_CLIENT_SECRET || '',
}, },
devServer: { devServer: {
host: '0.0.0.0', host: '0.0.0.0',
+5035 -6478
View File
File diff suppressed because it is too large Load Diff
+24 -26
View File
@@ -1,6 +1,6 @@
{ {
"name": "kestrelos", "name": "kestrelos",
"version": "1.0.1", "version": "1.1.12",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -16,38 +16,36 @@
"test:e2e:ui": "playwright test --ui test/e2e", "test:e2e:ui": "playwright test --ui test/e2e",
"test:e2e:debug": "playwright test --debug test/e2e", "test:e2e:debug": "playwright test --debug test/e2e",
"test:e2e:install": "playwright install --with-deps webkit chromium firefox", "test:e2e:install": "playwright install --with-deps webkit chromium firefox",
"lint": "eslint . --max-warnings 0" "lint": "eslint . --max-warnings 0",
"import:alpr": "node scripts/import-alpr.js"
}, },
"dependencies": { "dependencies": {
"@nuxt/icon": "^2.2.1", "@nuxt/icon": "^2.5.0",
"@nuxtjs/tailwindcss": "^6.14.0", "@nuxtjs/tailwindcss": "^6.14.0",
"fast-xml-parser": "^5.3.6", "fast-xml-parser": "^5.10.1",
"hls.js": "^1.5.0", "hls.js": "^1.7.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"leaflet.offline": "^3.2.0", "leaflet.offline": "^3.2.1",
"mediasoup": "^3.19.14", "mediasoup": "^3.24.2",
"mediasoup-client": "^3.18.6", "mediasoup-client": "^3.22.0",
"nuxt": "^4.0.0", "nuxt": "^4.5.2",
"openid-client": "^6.8.2", "openid-client": "^6.8.5",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"sqlite3": "^5.1.7", "supercluster": "^9.0.0",
"vue": "^3.4.0", "vue": "^3.5.41",
"vue-router": "^4.4.0", "vue-router": "^5.2.0",
"ws": "^8.18.0" "ws": "^8.21.3"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/tabler": "^1.2.26", "@iconify-json/tabler": "^1.2.38",
"@nuxt/eslint": "^1.15.0", "@nuxt/eslint": "^1.17.0",
"@nuxt/test-utils": "^4.0.0", "@nuxt/test-utils": "^4.1.0",
"@playwright/test": "^1.58.2", "@playwright/test": "^1.62.1",
"@vitest/coverage-v8": "^4.0.0", "@vitest/coverage-v8": "^4.1.10",
"@vue/test-utils": "^2.4.0", "@vue/test-utils": "^2.4.11",
"eslint": "^9.0.0", "eslint": "^10.8.1",
"happy-dom": "^20.6.1", "happy-dom": "^20.11.2",
"vitest": "^4.0.0" "vitest": "^4.1.10"
},
"overrides": {
"tar": "^7.5.7"
} }
} }
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env node
import { getDb, closeDb } from '../server/utils/db.js'
import { importAllAlprNodes } from '../server/utils/alpr.js'
try {
const db = await getDb()
console.log('[import-alpr] Fetching ALPR nodes from Overpass…')
const count = await importAllAlprNodes(db)
console.log(`[import-alpr] Cached ${count} nodes in SQLite.`)
}
catch (error) {
console.error('[import-alpr] Failed:', error?.message || error)
process.exitCode = 1
}
finally {
closeDb()
}
+42 -9
View File
@@ -3,18 +3,51 @@ set -e
# version # version
msg="${CI_COMMIT_MESSAGE:-}" msg="${CI_COMMIT_MESSAGE:-}"
# optional PR body (written by workflow from Gitea API when this commit is a merged PR)
if [ -f .ci_pr_body ]; then
CI_PR_DESCRIPTION=$(cat .ci_pr_body); rm -f .ci_pr_body
else
CI_PR_DESCRIPTION=""
fi
export CI_PR_DESCRIPTION
bump=patch bump=patch
echo "$msg" | grep -qi minor: && bump=minor # Conventional commits: chore/fix => patch, feat => minor
echo "$msg" | grep -Eqi '(^|[[:space:]])(fix|chore)(\([^)]*\))?:' && bump=patch
echo "$msg" | grep -Eqi '(^|[[:space:]])feat(\([^)]*\))?:' && bump=minor
# Conventional commits breaking change: type!:
echo "$msg" | grep -Eqi '(^|[[:space:]])[a-zA-Z]+(\([^)]*\))?!:' && bump=major
# Explicit bump prefixes still supported (but never downgrade a major bump)
echo "$msg" | grep -qi minor: && [ "$bump" != "major" ] && bump=minor
echo "$msg" | grep -qi major: && bump=major echo "$msg" | grep -qi major: && bump=major
cur=$(awk '/"version"/ { match($0, /[0-9]+\.[0-9]+\.[0-9]+/); print substr($0, RSTART, RLENGTH); exit }' package.json) cur=$(awk '/"version"/ { match($0, /[0-9]+\.[0-9]+\.[0-9]+/); print substr($0, RSTART, RLENGTH); exit }' package.json)
case "$cur" in
[0-9]*.[0-9]*.[0-9]*) ;;
*) echo "error: package.json version must be x.y.z (got: $cur)"; exit 1 ;;
esac
major=$(echo "$cur" | cut -d. -f1); minor=$(echo "$cur" | cut -d. -f2); patch=$(echo "$cur" | cut -d. -f3) major=$(echo "$cur" | cut -d. -f1); minor=$(echo "$cur" | cut -d. -f2); patch=$(echo "$cur" | cut -d. -f3)
case "$bump" in major) major=$((major+1)); minor=0; patch=0 ;; minor) minor=$((minor+1)); patch=0 ;; patch) patch=$((patch+1)) ;; esac case "$bump" in major) major=$((major+1)); minor=0; patch=0 ;; minor) minor=$((minor+1)); patch=0 ;; patch) patch=$((patch+1)) ;; esac
newVersion="$major.$minor.$patch" newVersion="$major.$minor.$patch"
[ -z "$cur" ] && { echo "error: could not read version from package.json"; exit 1; }
# changelog entry (strip prefix from first line) url="https://${CI_REPO_OWNER}:${GITEA_REPO_TOKEN}@${CI_FORGE_URL#https://}/${CI_REPO_OWNER}/${CI_REPO_NAME}.git"
changelogEntry=$(echo "$msg" | head -1 | awk '{sub(/^[mM]ajor:[ \t]*/,""); sub(/^[mM]inor:[ \t]*/,""); sub(/^[pP]atch:[ \t]*/,""); print}') if [ -n "$(git ls-remote "$url" "refs/tags/v$newVersion" 2>/dev/null)" ]; then
exit 0
fi
# changelog entry (strip explicit bump prefixes & any conventional-commit type(scope):); optional PR description enriches it
changelogEntry=$(
echo "$msg" \
| head -1 \
| sed -E 's/^[[:space:]]*[mM]ajor:[[:space:]]*//; s/^[[:space:]]*[mM]inor:[[:space:]]*//; s/^[[:space:]]*[pP]atch:[[:space:]]*//' \
| sed -E 's/^[[:space:]]*[a-zA-Z]+(\([^)]*\))?:[[:space:]]*//'
)
[ -z "$changelogEntry" ] && changelogEntry="Release v$newVersion" [ -z "$changelogEntry" ] && changelogEntry="Release v$newVersion"
if [ -n "$CI_PR_DESCRIPTION" ]; then
changelogFull="- $changelogEntry
$CI_PR_DESCRIPTION"
else
changelogFull="- $changelogEntry"
fi
# bump files # bump files
awk -v v="$newVersion" '/"version"/ { sub(/[0-9]+\.[0-9]+\.[0-9]+/, v) } { print }' package.json > package.json.tmp && mv package.json.tmp package.json awk -v v="$newVersion" '/"version"/ { sub(/[0-9]+\.[0-9]+\.[0-9]+/, v) } { print }' package.json > package.json.tmp && mv package.json.tmp package.json
@@ -24,18 +57,18 @@ awk -v v="$newVersion" '/^ tag:/ { $0 = " tag: " v }; { print }' helm/kestrelo
# changelog # changelog
new="## [$newVersion] - $(date +%Y-%m-%d) new="## [$newVersion] - $(date +%Y-%m-%d)
### Changed ### Changed
- $changelogEntry $changelogFull
" "
{ [ ! -f CHANGELOG.md ] && printf '# Changelog\n\n'; printf '%s' "$new"; [ -f CHANGELOG.md ] && cat CHANGELOG.md; } > CHANGELOG.md.tmp && mv CHANGELOG.md.tmp CHANGELOG.md # Create CHANGELOG.md if missing (first release); otherwise prepend new entry to existing content.
{ [ ! -f CHANGELOG.md ] && printf '# Changelog\n\n'; printf '%s' "$new"; [ -f CHANGELOG.md ] && cat CHANGELOG.md || true; } > CHANGELOG.md.tmp && mv CHANGELOG.md.tmp CHANGELOG.md
# git # git
git config user.email "ci@kestrelos" && git config user.name "CI" git config user.email "ci@kestrelos" && git config user.name "CI"
git add package.json helm/kestrelos/Chart.yaml helm/kestrelos/values.yaml CHANGELOG.md git add package.json helm/kestrelos/Chart.yaml helm/kestrelos/values.yaml CHANGELOG.md
git commit -m "release v$newVersion [skip ci]" git commit -m "release v$newVersion [skip ci]"
url="https://${CI_REPO_OWNER}:${GITEA_REPO_TOKEN}@${CI_FORGE_URL#https://}/${CI_REPO_OWNER}/${CI_REPO_NAME}.git"
git tag "v$newVersion" git tag "v$newVersion"
# artifact for kaniko (tag list) # artifact for docker (tag list)
printf '%s\n%s\n' "$newVersion" "latest" > .tags printf '%s\n%s\n' "$newVersion" "latest" > .tags
retry() { n=0; while ! "$@"; do n=$((n+1)); [ $n -ge 3 ] && return 1; sleep 2; done; } retry() { n=0; while ! "$@"; do n=$((n+1)); [ $n -ge 3 ] && return 1; sleep 2; done; }
retry git push "$url" HEAD:main "v$newVersion" retry git push "$url" HEAD:main "v$newVersion"
@@ -43,7 +76,7 @@ retry git push "$url" HEAD:main "v$newVersion"
# gitea release # gitea release
body="## Changelog body="## Changelog
### Changed ### Changed
- $changelogEntry $changelogFull
## Installation ## Installation
- [Docker image](${CI_FORGE_URL}/${CI_REPO_OWNER}/-/packages/container/${CI_REPO_NAME}) - [Docker image](${CI_FORGE_URL}/${CI_REPO_OWNER}/-/packages/container/${CI_REPO_NAME})
+19
View File
@@ -0,0 +1,19 @@
import { getDb } from '../utils/db.js'
import { requireAuth } from '../utils/authHelpers.js'
import { getAlprCameras, parseBbox } from '../utils/alpr.js'
export default defineEventHandler(async (event) => {
requireAuth(event)
let bbox
try {
bbox = parseBbox(getQuery(event))
}
catch (error) {
throw createError({
statusCode: error?.statusCode || 400,
message: error?.message || 'invalid bbox',
})
}
const db = await getDb()
return getAlprCameras(db, bbox)
})
+2 -6
View File
@@ -1,19 +1,15 @@
import { getDb } from '../utils/db.js' import { getDb } from '../utils/db.js'
import { requireAuth } from '../utils/authHelpers.js' import { requireAuth } from '../utils/authHelpers.js'
import { getActiveSessions } from '../utils/liveSessions.js' import { getActiveSessions } from '../utils/liveSessions.js'
import { getActiveEntities } from '../utils/cotStore.js'
import { rowToDevice, sanitizeDeviceForResponse } from '../utils/deviceUtils.js' import { rowToDevice, sanitizeDeviceForResponse } from '../utils/deviceUtils.js'
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
requireAuth(event) requireAuth(event)
const config = useRuntimeConfig() const [db, sessions] = await Promise.all([
const ttlMs = Number(config.cotTtlMs ?? 90_000) || 90_000
const [db, sessions, cotEntities] = await Promise.all([
getDb(), getDb(),
getActiveSessions(), getActiveSessions(),
getActiveEntities(ttlMs),
]) ])
const rows = await db.all('SELECT id, name, device_type, vendor, lat, lng, stream_url, source_type, config FROM devices ORDER BY id') const rows = await db.all('SELECT id, name, device_type, vendor, lat, lng, stream_url, source_type, config FROM devices ORDER BY id')
const devices = rows.map(rowToDevice).filter(Boolean).map(sanitizeDeviceForResponse) const devices = rows.map(rowToDevice).filter(Boolean).map(sanitizeDeviceForResponse)
return { devices, liveSessions: sessions, cotEntities } return { devices, liveSessions: sessions }
}) })
+45
View File
@@ -0,0 +1,45 @@
import { createEventStream } from 'h3'
import { requireAuth } from '../../utils/authHelpers.js'
import { getActiveEntitiesInBbox } from '../../utils/cotStore.js'
import { registerSubscriber } from '../../utils/cotSubscribers.js'
import { getCotSnapshotOpts } from '../../utils/cotSnapshot.js'
import { COT_SSE_HEARTBEAT_MS } from '../../utils/constants.js'
import { parseBboxParam, parseLayersParam } from '../../utils/cotEntityUtils.js'
import { scheduleTrackingFeedRefresh } from '../../utils/trackingFeed.js'
export default defineEventHandler((event) => {
requireAuth(event)
const query = getQuery(event)
const bbox = parseBboxParam(typeof query.bbox === 'string' ? query.bbox : undefined)
const layers = parseLayersParam(typeof query.layers === 'string' ? query.layers : undefined)
const snapshotOpts = getCotSnapshotOpts()
const stream = createEventStream(event)
const push = (eventName, data) => stream.push({ event: eventName, data })
const sendSnapshot = async () => {
const entities = await getActiveEntitiesInBbox(bbox, { ...snapshotOpts, layers })
await push('snapshot', JSON.stringify({ entities }))
}
const unregister = registerSubscriber({ bbox, layers, push })
let heartbeat
stream.onClosed(async () => {
clearInterval(heartbeat)
unregister()
scheduleTrackingFeedRefresh()
})
void (async () => {
scheduleTrackingFeedRefresh()
await sendSnapshot()
heartbeat = setInterval(() => {
push('heartbeat', '{}').catch(() => {})
}, COT_SSE_HEARTBEAT_MS)
})()
return stream.send()
})
+1 -1
View File
@@ -108,7 +108,7 @@ async function processFrame(socket, rawMessage, payload, authenticated) {
socket.destroy() socket.destroy()
return return
} }
updateFromCot(parsed).catch((err) => { updateFromCot({ ...parsed, type: parsed.eventType }).catch((err) => {
console.error('[cot] Error updating from CoT:', err?.message) console.error('[cot] Error updating from CoT:', err?.message)
}) })
if (authenticated) broadcast(socket, rawMessage) if (authenticated) broadcast(socket, rawMessage)
+32
View File
@@ -0,0 +1,32 @@
import { registerCleanup } from '../utils/shutdown.js'
import { onCotChange, pruneStaleEntities } from '../utils/cotStore.js'
import { getCotSnapshotOpts } from '../utils/cotSnapshot.js'
import {
notifySubscribersForEntity,
notifySubscribersRemove,
} from '../utils/cotSubscribers.js'
import { COT_PRUNE_INTERVAL_MS } from '../utils/constants.js'
let pruneTimer = null
let offChange = () => {}
export default defineNitroPlugin(() => {
offChange = onCotChange((changeEvent, payload) => {
if (changeEvent === 'update' && payload?.entity) {
notifySubscribersForEntity('update', { entity: payload.entity }, payload.entity).catch(() => {})
}
else if (changeEvent === 'remove' && payload?.id) {
notifySubscribersRemove(payload.id).catch(() => {})
}
})
pruneTimer = setInterval(() => {
pruneStaleEntities(getCotSnapshotOpts()).catch(() => {})
}, COT_PRUNE_INTERVAL_MS)
registerCleanup(() => {
offChange()
if (pruneTimer) clearInterval(pruneTimer)
pruneTimer = null
})
})
+7
View File
@@ -0,0 +1,7 @@
import { registerCleanup } from '../utils/shutdown.js'
import { startTrackingFeed, stopTrackingFeed } from '../utils/trackingFeed.js'
export default defineNitroPlugin(() => {
startTrackingFeed()
registerCleanup(stopTrackingFeed)
})
-10
View File
@@ -43,16 +43,6 @@ export function removeSessionConnection(sessionId, ws) {
} }
} }
export function broadcastToSession(sessionId, message) {
const conns = getSessionConnections(sessionId)
const data = JSON.stringify(message)
for (const ws of conns) {
if (ws.readyState === 1) { // OPEN
ws.send(data)
}
}
}
export default defineNitroPlugin((nitroApp) => { export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('ready', async () => { nitroApp.hooks.hook('ready', async () => {
const server = nitroApp.h3App.server || nitroApp.h3App.nodeServer const server = nitroApp.h3App.server || nitroApp.h3App.nodeServer
+263
View File
@@ -0,0 +1,263 @@
import { cameraToFeature, featureCollection } from './alprGeo.js'
const OVERPASS_URL = 'https://overpass.deflock.org/api/interpreter'
const MAX_BBOX_DEGREES = 0.5
const CACHE_TTL_MS = 86_400_000
const TILE_SCALE = 10
function httpError(statusCode, message) {
const err = new Error(message)
err.statusCode = statusCode
return err
}
/**
* @param {unknown} tags
* @returns {Record<string, string>} String OSM tags only
*/
function normalizeTags(tags) {
if (!tags || typeof tags !== 'object') return {}
return Object.fromEntries(
Object.entries(/** @type {Record<string, unknown>} */ (tags))
.filter(([, v]) => typeof v === 'string')
.map(([k, v]) => [k, /** @type {string} */ (v)]),
)
}
/**
* @param {unknown} element
* @returns {{ osmId: number, lat: number, lng: number, manufacturer: string | null, direction: number | null, tags: Record<string, string> } | null} Parsed camera or null
*/
export function parseOverpassElement(element) {
if (!element || typeof element !== 'object') return null
const el = /** @type {Record<string, unknown>} */ (element)
if (el.type !== 'node' || typeof el.id !== 'number') return null
const lat = Number(el.lat)
const lng = Number(el.lon ?? el.lng)
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null
const tags = normalizeTags(el.tags)
const directionRaw = tags['camera:direction'] ?? tags.direction
const direction = directionRaw != null && directionRaw !== '' ? Number(directionRaw) : null
const fovRaw = tags['camera:angle'] ?? tags['surveillance:angle']
const fov = fovRaw != null && fovRaw !== '' ? Number(fovRaw) : null
return {
osmId: el.id,
lat,
lng,
manufacturer: tags.manufacturer ?? tags.brand ?? null,
direction: Number.isFinite(direction) ? direction : null,
fov: Number.isFinite(fov) ? fov : null,
tags,
}
}
export function parseBbox(query) {
const south = Number(query.south)
const west = Number(query.west)
const north = Number(query.north)
const east = Number(query.east)
if (![south, west, north, east].every(Number.isFinite)) {
throw httpError(400, 'south, west, north, east required')
}
if (south >= north || west >= east) {
throw httpError(400, 'invalid bbox')
}
if (north - south > MAX_BBOX_DEGREES || east - west > MAX_BBOX_DEGREES) {
throw httpError(400, `bbox exceeds ${MAX_BBOX_DEGREES}°`)
}
return { south, west, north, east }
}
/** @param {{ south: number, west: number, north: number, east: number }} bbox */
export function tileKeysForBbox(bbox) {
const latMin = Math.floor(bbox.south * TILE_SCALE)
const latMax = Math.floor(bbox.north * TILE_SCALE)
const lngMin = Math.floor(bbox.west * TILE_SCALE)
const lngMax = Math.floor(bbox.east * TILE_SCALE)
const keys = []
for (let lat = latMin; lat <= latMax; lat++) {
for (let lng = lngMin; lng <= lngMax; lng++) {
keys.push(`${lat}:${lng}`)
}
}
return keys
}
export function buildBboxQuery(bbox) {
const { south, west, north, east } = bbox
return `[out:json][timeout:25];node["surveillance:type"="ALPR"](${south},${west},${north},${east});out body;`
}
export function buildGlobalQuery() {
return '[out:json][timeout:300];node["surveillance:type"="ALPR"];out body;'
}
function rowToCamera(row, source) {
const tags = (() => {
try {
return JSON.parse(row.tags)
}
catch {
return {}
}
})()
const fovRaw = tags['camera:angle'] ?? tags['surveillance:angle']
const fov = fovRaw != null && fovRaw !== '' ? Number(fovRaw) : null
return {
osmId: row.osm_id,
lat: row.lat,
lng: row.lng,
manufacturer: row.manufacturer ?? null,
direction: row.direction ?? null,
fov: Number.isFinite(fov) ? fov : null,
tags,
source,
}
}
/** @param {Awaited<ReturnType<typeof import('./db.js').getDb>>} db */
export async function upsertAlprNode(db, camera) {
const now = new Date().toISOString()
await db.run(
`INSERT INTO alpr_nodes (osm_id, lat, lng, manufacturer, direction, tags, fetched_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(osm_id) DO UPDATE SET
lat = excluded.lat,
lng = excluded.lng,
manufacturer = excluded.manufacturer,
direction = excluded.direction,
tags = excluded.tags,
fetched_at = excluded.fetched_at`,
[
camera.osmId,
camera.lat,
camera.lng,
camera.manufacturer,
camera.direction,
JSON.stringify(camera.tags),
now,
],
)
}
/** @param {Awaited<ReturnType<typeof import('./db.js').getDb>>} db */
export async function upsertAlprNodesBatch(db, cameras) {
if (!cameras.length) return
await db.run('BEGIN TRANSACTION')
try {
for (const camera of cameras) {
await upsertAlprNode(db, camera)
}
await db.run('COMMIT')
}
catch (error) {
await db.run('ROLLBACK').catch(() => {})
throw error
}
}
/** @param {Awaited<ReturnType<typeof import('./db.js').getDb>>} db */
export async function markTilesFetched(db, keys) {
if (!keys.length) return
const now = new Date().toISOString()
await db.run('BEGIN TRANSACTION')
try {
for (const key of keys) {
await db.run('INSERT OR REPLACE INTO alpr_tiles (tile_key, fetched_at) VALUES (?, ?)', [key, now])
}
await db.run('COMMIT')
}
catch (error) {
await db.run('ROLLBACK').catch(() => {})
throw error
}
}
/** @param {Awaited<ReturnType<typeof import('./db.js').getDb>>} db */
async function tilesAreFresh(db, keys) {
if (!keys.length) return false
const placeholders = keys.map(() => '?').join(',')
const rows = await db.all(
`SELECT tile_key, fetched_at FROM alpr_tiles WHERE tile_key IN (${placeholders})`,
keys,
)
if (rows.length !== keys.length) return false
const cutoff = Date.now() - CACHE_TTL_MS
return rows.every(row => Date.parse(row.fetched_at) >= cutoff)
}
/** @param {Awaited<ReturnType<typeof import('./db.js').getDb>>} db */
export async function getCachedAlprNodes(db, bbox) {
const rows = await db.all(
`SELECT osm_id, lat, lng, manufacturer, direction, tags
FROM alpr_nodes
WHERE lat >= ? AND lat <= ? AND lng >= ? AND lng <= ?`,
[bbox.south, bbox.north, bbox.west, bbox.east],
)
return rows.map(row => rowToCamera(row, 'cache'))
}
export async function fetchOverpass(query) {
const res = await fetch(OVERPASS_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `data=${encodeURIComponent(query)}`,
signal: AbortSignal.timeout(120_000),
})
if (!res.ok) {
throw httpError(502, `Overpass error: ${res.status}`)
}
const data = await res.json()
const elements = Array.isArray(data?.elements) ? data.elements : []
return elements.map(parseOverpassElement).filter(Boolean)
}
/** @param {Awaited<ReturnType<typeof import('./db.js').getDb>>} db */
async function fetchAndCacheBbox(db, bbox, tileKeys) {
const live = await fetchOverpass(buildBboxQuery(bbox))
await upsertAlprNodesBatch(db, live)
await markTilesFetched(db, tileKeys)
return live.map(c => ({ ...c, source: 'live' }))
}
const refreshInFlight = new Map()
/** @param {Awaited<ReturnType<typeof import('./db.js').getDb>>} db */
function refreshBboxInBackground(db, bbox, tileKeys) {
const key = tileKeys.join('|')
if (refreshInFlight.has(key)) return
const job = fetchAndCacheBbox(db, bbox, tileKeys)
.catch(() => {})
.finally(() => refreshInFlight.delete(key))
refreshInFlight.set(key, job)
}
/** @param {Awaited<ReturnType<typeof import('./db.js').getDb>>} db */
export async function getAlprCameras(db, bbox) {
const tileKeys = tileKeysForBbox(bbox)
const cached = await getCachedAlprNodes(db, bbox)
if (await tilesAreFresh(db, tileKeys)) {
return featureCollection(cached.map(cameraToFeature), 'cache')
}
if (cached.length > 0) {
refreshBboxInBackground(db, bbox, tileKeys)
return featureCollection(cached.map(cameraToFeature), 'cache')
}
try {
const live = await fetchAndCacheBbox(db, bbox, tileKeys)
return featureCollection(live.map(cameraToFeature), 'live')
}
catch {
return featureCollection(cached.map(cameraToFeature), 'cache')
}
}
/** @param {Awaited<ReturnType<typeof import('./db.js').getDb>>} db */
export async function importAllAlprNodes(db) {
const cameras = await fetchOverpass(buildGlobalQuery())
await upsertAlprNodesBatch(db, cameras)
return cameras.length
}
+175
View File
@@ -0,0 +1,175 @@
const ATTRIBUTION = '© OpenStreetMap contributors'
/** Longest match first — Flock, Genetec, Leonardo, etc. */
const KNOWN_ALPR_MODELS = [
'Falcon Flex',
'AutoVu CR-H2',
'AutoVu Sharp',
'UnicamVELOCITY3',
'Falcon',
'Sparrow',
'Raven',
'Condor',
'Wing',
'Pelican',
]
const GENERIC_CAMERA_NAMES = new Set([
'flock alpr camera',
'flock alpr cameera',
'flock camera',
'flock alpr',
'flock',
'flock camera',
'automatic license plate reader (alpr)',
'alpr camera',
'alpr',
])
const SKIP_EXTRA_TAGS = new Set([
'surveillance:type',
'man_made',
'camera:direction',
'direction',
'camera:angle',
'surveillance:angle',
'manufacturer',
'brand',
'model',
'operator',
'name',
'ref',
'surveillance',
'camera:type',
'description',
'note',
'colour',
'color',
'manufacturer:wikidata',
'model:wikidata',
'operator:wikidata',
])
function escapeRegex(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
/** @param {string} value */
export function normalizeModelName(value) {
const trimmed = value.trim()
if (!trimmed) return null
const lower = trimmed.toLowerCase()
for (const model of KNOWN_ALPR_MODELS) {
if (model.toLowerCase() === lower) return model
}
return trimmed.split(/\s+/).map(word =>
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
).join(' ')
}
/** @param {string} text */
function matchKnownModel(text) {
for (const model of KNOWN_ALPR_MODELS) {
const re = new RegExp(`\\b${escapeRegex(model)}\\b`, 'i')
if (re.test(text)) return model
}
const flock = text.match(/\bFlock\s+([a-z][\w -]*)/i)
if (flock) {
const fragment = flock[1].trim()
if (!fragment || /^safety$/i.test(fragment)) return null
return matchKnownModel(fragment)
}
return null
}
/** @param {Record<string, string>} tags */
export function inferModelFromTags(tags) {
const t = tags ?? {}
if (t.model?.trim()) return normalizeModelName(t.model)
for (const key of ['name', 'description', 'note']) {
const value = t[key]
if (!value?.trim()) continue
const model = matchKnownModel(value)
if (model) return model
}
return null
}
/** @param {string | null | undefined} name */
export function isGenericCameraName(name) {
if (!name?.trim()) return false
const normalized = name.trim().toLowerCase()
if (GENERIC_CAMERA_NAMES.has(normalized)) return true
if (/^flock\s+alpr\b/i.test(name) && !matchKnownModel(name)) return true
if (matchKnownModel(name) && /\bflock\b/i.test(name)) return true
return false
}
/** @param {Record<string, string>} tags */
export function displayNameFromTags(tags) {
const name = tags?.name?.trim()
if (!name || isGenericCameraName(name)) return null
if (matchKnownModel(name)) return null
return name
}
/** @param {Record<string, string>} tags */
export function identifyingProperties(tags, manufacturer = null) {
const t = tags ?? {}
const mfr = manufacturer ?? t.manufacturer ?? t.brand ?? null
const model = inferModelFromTags(t)
const name = displayNameFromTags(t)
/** @type {Record<string, string | boolean>} */
const props = {}
if (mfr) props.manufacturer = mfr
if (model) props.model = model
if (mfr && !model) props.modelUnknown = true
if (t.brand && t.brand !== mfr) props.brand = t.brand
if (t.operator) props.operator = t.operator
if (name) props.name = name
if (t.ref) props.ref = t.ref
if (t.surveillance) props.surveillance = t.surveillance
if (t['camera:type']) props.cameraType = t['camera:type']
if (t.description) props.description = t.description
if (t.note) props.note = t.note
const colour = t.colour ?? t.color
if (colour) props.colour = colour
if (t['manufacturer:wikidata']) props.manufacturerWikidata = t['manufacturer:wikidata']
if (t['model:wikidata']) props.modelWikidata = t['model:wikidata']
if (t['operator:wikidata']) props.operatorWikidata = t['operator:wikidata']
const extra = {}
for (const [key, value] of Object.entries(t)) {
if (SKIP_EXTRA_TAGS.has(key) || !value?.trim()) continue
extra[key] = value
}
if (Object.keys(extra).length) props.tags = extra
return props
}
/** @param {{ osmId: number, lat: number, lng: number, manufacturer: string | null, direction: number | null, fov: number | null, tags: Record<string, string> }} camera */
export function cameraToFeature(camera) {
return {
type: 'Feature',
id: camera.osmId,
geometry: { type: 'Point', coordinates: [camera.lng, camera.lat] },
properties: {
osmId: camera.osmId,
direction: camera.direction,
fov: camera.fov,
...identifyingProperties(camera.tags, camera.manufacturer),
},
}
}
/** @param {ReturnType<typeof cameraToFeature>[]} features */
export function featureCollection(features, source) {
return {
type: 'FeatureCollection',
features,
attribution: ATTRIBUTION,
source,
}
}
export { ATTRIBUTION }
+2
View File
@@ -19,6 +19,8 @@ export const SKIP_PATHS = Object.freeze([
]) ])
export const PROTECTED_PATH_PREFIXES = Object.freeze([ export const PROTECTED_PATH_PREFIXES = Object.freeze([
'/api/alpr',
'/api/cot',
'/api/cameras', '/api/cameras',
'/api/devices', '/api/devices',
'/api/live', '/api/live',
+13 -1
View File
@@ -2,10 +2,22 @@
* Application constants with environment variable support. * Application constants with environment variable support.
*/ */
// CoT / tracking (fixed defaults — not env-configurable)
export const COT_TTL_MS = 90_000
/** @deprecated Use COT_TTL_MS */
export const COT_ENTITY_TTL_MS = COT_TTL_MS
export const COT_OSINT_TTL_MS = 30_000
export const COT_PRUNE_INTERVAL_MS = 15_000
export const COT_SSE_HEARTBEAT_MS = 15_000
export const OPENSKY_CACHE_MS = 5_000
export const TRACKING_FEED_DEBOUNCE_MS = 500
export const COT_TAK_FILTER_BBOX = false
export const COT_SSE_MAX_ENTITIES = 2000
export const MAX_OPENSKY_BBOX_DEGREES = 10
// Timeouts (milliseconds) // Timeouts (milliseconds)
export const COT_AUTH_TIMEOUT_MS = Number(process.env.COT_AUTH_TIMEOUT_MS) || 15_000 export const COT_AUTH_TIMEOUT_MS = Number(process.env.COT_AUTH_TIMEOUT_MS) || 15_000
export const LIVE_SESSION_TTL_MS = Number(process.env.LIVE_SESSION_TTL_MS) || 60_000 export const LIVE_SESSION_TTL_MS = Number(process.env.LIVE_SESSION_TTL_MS) || 60_000
export const COT_ENTITY_TTL_MS = Number(process.env.COT_ENTITY_TTL_MS) || 90_000
export const POLL_INTERVAL_MS = Number(process.env.POLL_INTERVAL_MS) || 1500 export const POLL_INTERVAL_MS = Number(process.env.POLL_INTERVAL_MS) || 1500
export const SHUTDOWN_TIMEOUT_MS = Number(process.env.SHUTDOWN_TIMEOUT_MS) || 30_000 export const SHUTDOWN_TIMEOUT_MS = Number(process.env.SHUTDOWN_TIMEOUT_MS) || 30_000
+191
View File
@@ -0,0 +1,191 @@
/**
* CoT entity helpers: filters and OSINT → CoT mapping.
*/
/**
* @param {string} id
* @returns {'adsb' | 'ais' | 'tak'} Inferred track source.
*/
export function inferSourceFromId(id) {
if (typeof id !== 'string') return 'tak'
if (id.startsWith('ICAO.')) return 'adsb'
if (id.startsWith('MMSI.')) return 'ais'
return 'tak'
}
/**
* @param {string} [type]
* @returns {'air' | 'surface' | 'ground'} CoT display category.
*/
export function cotCategoryFromType(type) {
const t = typeof type === 'string' ? type : ''
if (t.startsWith('a-f-A-')) return 'air'
if (t.startsWith('a-f-S-')) return 'surface'
return 'ground'
}
/**
* @param {{ lat: number, lng: number }} point
* @param {{ west: number, south: number, east: number, north: number }} bbox
*/
export function isInBbox(point, bbox) {
if (!point || !bbox) return false
const { lat, lng } = point
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return false
return lat >= bbox.south && lat <= bbox.north && lng >= bbox.west && lng <= bbox.east
}
/**
* @param {Set<string> | string[] | undefined} layers
* @param {{ type?: string, source?: string }} entity
*/
export function matchesLayerFilter(layers, entity) {
if (!layers || layers.size === 0) return true
const category = cotCategoryFromType(entity.type)
if (layers.has('air') && category === 'air') return true
if (layers.has('surface') && category === 'surface') return true
if (layers.has('ground') && category === 'ground') return true
return false
}
/** OpenSky emitter category → MilStd CoT air type. */
function openSkyCategoryToType(category) {
if (category === 8) return 'a-f-A-C-H' // rotorcraft
if (category === 14) return 'a-f-A-C-F' // UAV — plane icon
return 'a-f-A-C-F'
}
/** OpenSky state vector → CoT upsert. */
export function openSkyStateToCot(state) {
if (!Array.isArray(state) || state.length < 11) return null
const icao24 = String(state[0] ?? '').trim().toLowerCase()
if (!icao24) return null
const lat = Number(state[6])
const lng = Number(state[5])
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null
const callsign = typeof state[1] === 'string' ? state[1].trim() : ''
const originCountry = typeof state[2] === 'string' ? state[2].trim() : ''
const category = Number(state[17])
const type = Number.isFinite(category) ? openSkyCategoryToType(category) : 'a-f-A-C-F'
const heading = Number(state[10])
const speed = Number(state[9])
const altitude = Number(state[7])
const verticalRate = Number(state[11])
const onGround = state[8] === true
const squawk = state[14] != null ? String(state[14]).padStart(4, '0') : undefined
return {
id: `ICAO.${icao24}`,
lat,
lng,
label: callsign || icao24.toUpperCase(),
type,
source: 'adsb',
icao: icao24,
originCountry: originCountry || undefined,
heading: Number.isFinite(heading) ? heading : undefined,
speed: Number.isFinite(speed) ? speed : undefined,
altitude: Number.isFinite(altitude) ? altitude : undefined,
verticalRate: Number.isFinite(verticalRate) ? verticalRate : undefined,
onGround: onGround || undefined,
squawk: squawk && squawk !== '0000' ? squawk : undefined,
}
}
/** AISStream position report → CoT upsert. */
export function aisStreamMessageToCot(message) {
if (!message || typeof message !== 'object') return null
const meta = /** @type {Record<string, unknown>} */ (message.MetaData)
const msg = /** @type {Record<string, unknown>} */ (message.Message)
if (!msg || typeof msg !== 'object') return null
const report = /** @type {Record<string, unknown>} */ (
msg.PositionReport ?? msg.StandardClassBPositionReport ?? msg.ExtendedClassBPositionReport
)
if (!report || typeof report !== 'object') return null
const mmsi = Number(meta?.MMSI ?? report.UserID)
if (!Number.isFinite(mmsi)) return null
const lat = Number(report.Latitude)
const lng = Number(report.Longitude)
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null
const shipName = typeof meta?.ShipName === 'string' ? meta.ShipName.trim() : ''
const heading = Number(report.Cog ?? report.TrueHeading)
const speed = Number(report.Sog)
return {
id: `MMSI.${mmsi}`,
lat,
lng,
label: shipName || `MMSI ${mmsi}`,
type: 'a-f-S-C',
source: 'ais',
mmsi: String(mmsi),
heading: Number.isFinite(heading) ? heading : undefined,
speed: Number.isFinite(speed) ? speed : undefined,
}
}
/**
* Union of subscriber bboxes.
* @param {Array<{ west: number, south: number, east: number, north: number } | null | undefined>} boxes
*/
export function unionBboxes(boxes) {
let west = Infinity
let south = Infinity
let east = -Infinity
let north = -Infinity
let has = false
for (const bbox of boxes) {
if (!bbox) continue
has = true
west = Math.min(west, bbox.west)
south = Math.min(south, bbox.south)
east = Math.max(east, bbox.east)
north = Math.max(north, bbox.north)
}
return has ? { west, south, east, north } : null
}
/**
* Shrink bbox to max span (degrees per axis), centered on midpoint.
* @param {{ west: number, south: number, east: number, north: number } | null} bbox
* @param {number} maxDegrees
*/
export function clampBbox(bbox, maxDegrees) {
if (!bbox || !Number.isFinite(maxDegrees) || maxDegrees <= 0) return bbox
const latSpan = bbox.north - bbox.south
const lngSpan = bbox.east - bbox.west
if (latSpan <= maxDegrees && lngSpan <= maxDegrees) return bbox
const latMid = (bbox.north + bbox.south) / 2
const lngMid = (bbox.east + bbox.west) / 2
const half = maxDegrees / 2
return {
south: latMid - half,
north: latMid + half,
west: lngMid - half,
east: lngMid + half,
}
}
/**
* @param {string | undefined} raw
* @returns {Set<string>} Enabled layer names.
*/
export function parseLayersParam(raw) {
if (!raw || typeof raw !== 'string') {
return new Set(['air', 'surface', 'ground'])
}
const parts = raw.split(',').map(s => s.trim().toLowerCase()).filter(Boolean)
if (parts.length === 0 || parts.includes('none')) return new Set()
return new Set(parts)
}
/**
* @param {string | undefined} raw
* @returns {{ west: number, south: number, east: number, north: number } | null} Parsed bbox or null.
*/
export function parseBboxParam(raw) {
if (!raw || typeof raw !== 'string') return null
const parts = raw.split(',').map(s => Number(s.trim()))
if (parts.length !== 4 || parts.some(n => !Number.isFinite(n))) return null
const [west, south, east, north] = parts
if (south > north || west > east) return null
return { west, south, east, north }
}
+15
View File
@@ -0,0 +1,15 @@
import {
COT_OSINT_TTL_MS,
COT_SSE_MAX_ENTITIES,
COT_TAK_FILTER_BBOX,
COT_TTL_MS,
} from './constants.js'
export function getCotSnapshotOpts() {
return {
ttlMs: COT_TTL_MS,
osintTtlMs: COT_OSINT_TTL_MS,
takFilterBbox: COT_TAK_FILTER_BBOX,
maxEntities: COT_SSE_MAX_ENTITIES,
}
}
+171 -27
View File
@@ -1,66 +1,210 @@
/** /**
* In-memory CoT entity store: upsert by id, prune on read by TTL. * In-memory CoT store (TAK, ADS-B, AIS).
* Single source of truth; getActiveEntities returns new objects (no mutation of returned refs).
*/ */
import { acquire } from './asyncLock.js' import { acquire } from './asyncLock.js'
import { COT_ENTITY_TTL_MS } from './constants.js' import { COT_OSINT_TTL_MS, COT_TTL_MS } from './constants.js'
import { inferSourceFromId, isInBbox, matchesLayerFilter } from './cotEntityUtils.js'
const entities = new Map() const entities = new Map()
/** @type {Set<(event: string, payload: unknown) => void>} */
const listeners = new Set()
/**
* @param {(event: string, payload: unknown) => void} fn
* @returns {() => void} Unsubscribe function.
*/
export function onCotChange(fn) {
listeners.add(fn)
return () => listeners.delete(fn)
}
function emitChange(event, payload) {
for (const fn of listeners) {
try {
fn(event, payload)
}
catch {
/* ignore listener errors */
}
}
}
function pickOptionalNumber(value, fallback) {
if (value === undefined || value === null) return fallback
const n = Number(value)
return Number.isFinite(n) ? n : fallback
}
/**
* @param {Record<string, unknown>} entity
*/
function toSnapshot(entity) {
return {
id: entity.id,
lat: entity.lat,
lng: entity.lng,
label: entity.label ?? entity.id,
type: entity.type ?? '',
source: entity.source ?? 'tak',
heading: entity.heading,
speed: entity.speed,
altitude: entity.altitude,
verticalRate: entity.verticalRate,
onGround: entity.onGround,
originCountry: entity.originCountry,
icao: entity.icao,
mmsi: entity.mmsi,
squawk: entity.squawk,
updatedAt: entity.updatedAt,
}
}
/**
* @param {Record<string, unknown>} entity
* @param {{ ttlMs?: number, osintTtlMs?: number }} opts
*/
function entityTtlMs(entity, opts) {
const source = entity.source
if (source === 'adsb' || source === 'ais') {
return opts.osintTtlMs ?? COT_OSINT_TTL_MS
}
return opts.ttlMs ?? COT_TTL_MS
}
function isEntityExpired(entity, now, opts) {
return now - entity.updatedAt > entityTtlMs(entity, opts)
}
/** /**
* Upsert entity by id. Input is not mutated; stored value is a new object. * Upsert entity by id. Input is not mutated; stored value is a new object.
* @param {{ id: string, lat: number, lng: number, label?: string, eventType?: string, type?: string }} parsed * @param {{ id: string, lat: number, lng: number, label?: string, eventType?: string, type?: string, source?: string, heading?: number, speed?: number, altitude?: number }} parsed
* @param {{ silent?: boolean }} [options]
*/ */
export async function updateFromCot(parsed) { export async function updateFromCot(parsed, options = {}) {
if (!parsed || typeof parsed.id !== 'string') return if (!parsed || typeof parsed.id !== 'string') return
const lat = Number(parsed.lat) const lat = Number(parsed.lat)
const lng = Number(parsed.lng) const lng = Number(parsed.lng)
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return if (!Number.isFinite(lat) || !Number.isFinite(lng)) return
let snapshot = null
await acquire(`cot-${parsed.id}`, async () => { await acquire(`cot-${parsed.id}`, async () => {
const now = Date.now() const now = Date.now()
const existing = entities.get(parsed.id) const existing = entities.get(parsed.id)
const label = typeof parsed.label === 'string' ? parsed.label : (existing?.label ?? parsed.id) const label = typeof parsed.label === 'string' ? parsed.label : (existing?.label ?? parsed.id)
const type = typeof parsed.eventType === 'string' ? parsed.eventType : (typeof parsed.type === 'string' ? parsed.type : (existing?.type ?? '')) const type = typeof parsed.eventType === 'string'
? parsed.eventType
: (typeof parsed.type === 'string' ? parsed.type : (existing?.type ?? ''))
const explicitSource = parsed.source
const source = explicitSource === 'adsb' || explicitSource === 'ais' || explicitSource === 'tak'
? explicitSource
: inferSourceFromId(parsed.id)
entities.set(parsed.id, { const stored = {
id: parsed.id, id: parsed.id,
lat, lat,
lng, lng,
label, label,
type, type,
source,
heading: pickOptionalNumber(parsed.heading, existing?.heading),
speed: pickOptionalNumber(parsed.speed, existing?.speed),
altitude: pickOptionalNumber(parsed.altitude, existing?.altitude),
verticalRate: pickOptionalNumber(parsed.verticalRate, existing?.verticalRate),
onGround: typeof parsed.onGround === 'boolean' ? parsed.onGround : existing?.onGround,
originCountry: typeof parsed.originCountry === 'string' ? parsed.originCountry : existing?.originCountry,
icao: typeof parsed.icao === 'string' ? parsed.icao : existing?.icao,
mmsi: typeof parsed.mmsi === 'string' ? parsed.mmsi : existing?.mmsi,
squawk: typeof parsed.squawk === 'string' ? parsed.squawk : existing?.squawk,
updatedAt: now, updatedAt: now,
}) }
entities.set(parsed.id, stored)
snapshot = toSnapshot(stored)
})
if (snapshot && !options.silent) emitChange('update', { entity: snapshot })
}
/**
* @param {number} now
* @param {{ ttlMs?: number, osintTtlMs?: number }} opts
*/
function pruneExpired(now, opts) {
const expired = []
for (const entity of entities.values()) {
if (isEntityExpired(entity, now, opts)) expired.push(entity.id)
}
for (const id of expired) {
entities.delete(id)
emitChange('remove', { id })
}
}
/**
* @param {{ ttlMs?: number, osintTtlMs?: number }} [opts]
*/
export async function pruneStaleEntities(opts = {}) {
const ttlMs = opts.ttlMs ?? COT_TTL_MS
const osintTtlMs = opts.osintTtlMs ?? COT_OSINT_TTL_MS
await acquire('cot-prune', async () => {
pruneExpired(Date.now(), { ttlMs, osintTtlMs })
}) })
} }
/** /**
* Active entities (updated within ttlMs). Prunes expired. Returns new array of new objects. * @param {Record<string, unknown>} entity
* @param {number} [ttlMs] * @param {{ west: number, south: number, east: number, north: number } | null | undefined} bbox
* @returns {Promise<Array<{ id: string, lat: number, lng: number, label: string, type: string, updatedAt: number }>>} Snapshot of active entities. * @param {boolean} takFilterBbox
*/ */
export async function getActiveEntities(ttlMs = COT_ENTITY_TTL_MS) { function passesBboxFilter(entity, bbox, takFilterBbox) {
if (!bbox) return true
const inBox = isInBbox(entity, bbox)
if (entity.source === 'tak' && !takFilterBbox) return true
return inBox
}
/**
* Active entities (updated within ttlMs). Prunes expired. Returns new array of new objects.
* @param {{ ttlMs?: number, osintTtlMs?: number }} [opts]
*/
export async function getActiveEntities(opts = {}) {
const ttlMs = opts.ttlMs ?? COT_TTL_MS
const osintTtlMs = opts.osintTtlMs ?? COT_OSINT_TTL_MS
return acquire('cot-prune', async () => { return acquire('cot-prune', async () => {
const now = Date.now() const now = Date.now()
pruneExpired(now, { ttlMs, osintTtlMs })
return [...entities.values()].map(toSnapshot)
})
}
/**
* Active entities filtered by viewport bbox and layer set.
* @param {{ west: number, south: number, east: number, north: number } | null} bbox
* @param {{ ttlMs?: number, osintTtlMs?: number, layers?: Set<string>, takFilterBbox?: boolean, maxEntities?: number }} [opts]
*/
export async function getActiveEntitiesInBbox(bbox, opts = {}) {
const ttlMs = opts.ttlMs ?? COT_TTL_MS
const osintTtlMs = opts.osintTtlMs ?? COT_OSINT_TTL_MS
const layers = opts.layers
const takFilterBbox = Boolean(opts.takFilterBbox)
const ttlOpts = { ttlMs, osintTtlMs }
return acquire('cot-prune', async () => {
const now = Date.now()
pruneExpired(now, ttlOpts)
const active = [] const active = []
const expired = []
for (const entity of entities.values()) { for (const entity of entities.values()) {
if (now - entity.updatedAt <= ttlMs) { if (isEntityExpired(entity, now, ttlOpts)) continue
active.push({ const snap = toSnapshot(entity)
id: entity.id, if (!passesBboxFilter(snap, bbox, takFilterBbox)) continue
lat: entity.lat, if (!matchesLayerFilter(layers, snap)) continue
lng: entity.lng, active.push(snap)
label: entity.label ?? entity.id, }
type: entity.type ?? '', const maxEntities = opts.maxEntities
updatedAt: entity.updatedAt, if (maxEntities != null && active.length > maxEntities) {
}) active.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))
} active.length = maxEntities
else {
expired.push(entity.id)
}
} }
for (const id of expired) entities.delete(id)
return active return active
}) })
} }
+70
View File
@@ -0,0 +1,70 @@
/** SSE subscriber registry; bbox union drives OSINT feeds. */
import { getActiveEntitiesInBbox } from './cotStore.js'
import { isInBbox, matchesLayerFilter, unionBboxes } from './cotEntityUtils.js'
/** @typedef {{ west: number, south: number, east: number, north: number }} Bbox */
/** @typedef {(event: string, data: string) => Promise<void> | void} PushFn */
/** @type {Map<string, { bbox: Bbox | null, layers: Set<string>, push: PushFn }>} */
const subscribers = new Map()
let nextId = 1
/**
* @param {{ bbox: Bbox | null, layers: Set<string>, push: PushFn }} sub
* @returns {() => void} Unregister function.
*/
export function registerSubscriber(sub) {
const id = String(nextId++)
subscribers.set(id, sub)
return () => subscribers.delete(id)
}
/** @returns {Bbox | null} Union of all subscriber bboxes. */
export function getSubscriberBboxUnion() {
return unionBboxes([...subscribers.values()].map(s => s.bbox))
}
export function getSubscriberCount() {
return subscribers.size
}
export function clearSubscribers() {
subscribers.clear()
}
export async function notifySubscribersForEntity(event, payload, entity) {
const data = JSON.stringify(payload)
const tasks = []
for (const sub of subscribers.values()) {
if (sub.bbox && !isInBbox(entity, sub.bbox)) continue
if (!matchesLayerFilter(sub.layers, entity)) continue
tasks.push(Promise.resolve(sub.push(event, data)))
}
await Promise.all(tasks)
}
export async function notifySubscribersRemove(id) {
const data = JSON.stringify({ id })
await Promise.all(
[...subscribers.values()].map(sub => Promise.resolve(sub.push('remove', data))),
)
}
/**
* Push a filtered snapshot to each active SSE subscriber.
* @param {{ ttlMs?: number, osintTtlMs?: number, takFilterBbox?: boolean, maxEntities?: number }} snapshotOpts
*/
export async function broadcastSubscriberSnapshots(snapshotOpts) {
const tasks = []
for (const sub of subscribers.values()) {
tasks.push((async () => {
const entities = await getActiveEntitiesInBbox(sub.bbox, {
...snapshotOpts,
layers: sub.layers,
})
await sub.push('snapshot', JSON.stringify({ entities }))
})())
}
await Promise.all(tasks)
}
+125 -25
View File
@@ -1,16 +1,14 @@
import { join, dirname } from 'node:path' import { join, dirname } from 'node:path'
import { mkdirSync, existsSync } from 'node:fs' import { mkdirSync, existsSync } from 'node:fs'
import { randomBytes, randomUUID } from 'node:crypto'
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import { promisify } from 'node:util'
import { randomBytes } from 'node:crypto'
import { hashPassword } from './password.js' import { hashPassword } from './password.js'
import { registerCleanup } from './shutdown.js' import { registerCleanup } from './shutdown.js'
// Resolve from project root so bundled server (e.g. .output) finds node_modules/sqlite3
const requireFromRoot = createRequire(join(process.cwd(), 'package.json')) const requireFromRoot = createRequire(join(process.cwd(), 'package.json'))
const sqlite3 = requireFromRoot('sqlite3') const { DatabaseSync } = requireFromRoot('node:sqlite')
const SCHEMA_VERSION = 4 const SCHEMA_VERSION = 6
const DB_BUSY_TIMEOUT_MS = 5000 const DB_BUSY_TIMEOUT_MS = 5000
let dbInstance = null let dbInstance = null
@@ -61,6 +59,20 @@ const SCHEMA = {
source_type TEXT NOT NULL DEFAULT 'mjpeg', source_type TEXT NOT NULL DEFAULT 'mjpeg',
config TEXT config TEXT
)`, )`,
alpr_nodes: `CREATE TABLE IF NOT EXISTS alpr_nodes (
osm_id INTEGER PRIMARY KEY,
lat REAL NOT NULL,
lng REAL NOT NULL,
manufacturer TEXT,
direction INTEGER,
tags TEXT NOT NULL,
fetched_at TEXT NOT NULL
)`,
alpr_nodes_index: 'CREATE INDEX IF NOT EXISTS idx_alpr_lat_lng ON alpr_nodes(lat, lng)',
alpr_tiles: `CREATE TABLE IF NOT EXISTS alpr_tiles (
tile_key TEXT PRIMARY KEY,
fetched_at TEXT NOT NULL
)`,
} }
const getDbPath = () => { const getDbPath = () => {
@@ -120,6 +132,19 @@ const migrateToV4 = async (run, all) => {
await run('ALTER TABLE users ADD COLUMN cot_password_hash TEXT') await run('ALTER TABLE users ADD COLUMN cot_password_hash TEXT')
} }
const migrateToV5 = async (run, all) => {
const tables = await all('SELECT name FROM sqlite_master WHERE type=\'table\' AND name=\'alpr_nodes\'')
if (tables.length > 0) return
await run(SCHEMA.alpr_nodes)
await run(SCHEMA.alpr_nodes_index)
}
const migrateToV6 = async (run, all) => {
const tables = await all('SELECT name FROM sqlite_master WHERE type=\'table\' AND name=\'alpr_tiles\'')
if (tables.length > 0) return
await run(SCHEMA.alpr_tiles)
}
const runMigrations = async (run, all, get) => { const runMigrations = async (run, all, get) => {
const version = await getSchemaVersion(get) const version = await getSchemaVersion(get)
if (version >= SCHEMA_VERSION) return if (version >= SCHEMA_VERSION) return
@@ -135,6 +160,14 @@ const runMigrations = async (run, all, get) => {
await migrateToV4(run, all) await migrateToV4(run, all)
await setSchemaVersion(run, 4) await setSchemaVersion(run, 4)
} }
if (version < 5) {
await migrateToV5(run, all)
await setSchemaVersion(run, 5)
}
if (version < 6) {
await migrateToV6(run, all)
await setSchemaVersion(run, 6)
}
} }
const initDb = async (db, run, all, get) => { const initDb = async (db, run, all, get) => {
@@ -144,7 +177,6 @@ const initDb = async (db, run, all, get) => {
catch { catch {
// WAL not supported (e.g., network filesystem) // WAL not supported (e.g., network filesystem)
} }
db.configure('busyTimeout', DB_BUSY_TIMEOUT_MS)
await run(SCHEMA.schema_version) await run(SCHEMA.schema_version)
await run(SCHEMA.users) await run(SCHEMA.users)
@@ -152,6 +184,9 @@ const initDb = async (db, run, all, get) => {
await run(SCHEMA.sessions) await run(SCHEMA.sessions)
await run(SCHEMA.pois) await run(SCHEMA.pois)
await run(SCHEMA.devices) await run(SCHEMA.devices)
await run(SCHEMA.alpr_nodes)
await run(SCHEMA.alpr_nodes_index)
await run(SCHEMA.alpr_tiles)
if (!testPath) { if (!testPath) {
// Bootstrap admin user on first run // Bootstrap admin user on first run
@@ -168,7 +203,7 @@ const initDb = async (db, run, all, get) => {
await run( await run(
'INSERT INTO users (id, identifier, password_hash, role, created_at, auth_provider, oidc_issuer, oidc_sub) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', 'INSERT INTO users (id, identifier, password_hash, role, created_at, auth_provider, oidc_issuer, oidc_sub) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[crypto.randomUUID(), identifier, hashPassword(plainPassword), 'admin', new Date().toISOString(), 'local', null, null], [randomUUID(), identifier, hashPassword(plainPassword), 'admin', new Date().toISOString(), 'local', null, null],
) )
if (!email || !password) { if (!email || !password) {
@@ -181,37 +216,104 @@ const initDb = async (db, run, all, get) => {
export async function getDb() { export async function getDb() {
if (dbInstance) return dbInstance if (dbInstance) return dbInstance
const db = new sqlite3.Database(getDbPath(), (err) => { const sqliteDb = (() => {
if (err) { try {
return new DatabaseSync(getDbPath(), { timeout: DB_BUSY_TIMEOUT_MS })
}
catch (err) {
console.error('[db] Failed to open database:', err.message) console.error('[db] Failed to open database:', err.message)
throw err throw err
} }
}) })()
const run = promisify(db.run.bind(db)) const run = (sql, params = []) =>
const all = promisify(db.all.bind(db)) new Promise((resolve, reject) => {
const get = promisify(db.get.bind(db)) const stmt = sqliteDb.prepare(sql)
try {
stmt.run(...params)
resolve()
}
catch (error) {
reject(error)
}
finally {
try {
stmt.finalize()
}
catch {
// ignore finalize errors
}
}
})
const all = (sql, params = []) =>
new Promise((resolve, reject) => {
const stmt = sqliteDb.prepare(sql)
try {
const rows = stmt.all(...params)
resolve(rows)
}
catch (error) {
reject(error)
}
finally {
try {
stmt.finalize()
}
catch {
// ignore finalize errors
}
}
})
const get = (sql, params = []) =>
new Promise((resolve, reject) => {
const stmt = sqliteDb.prepare(sql)
try {
const row = stmt.get(...params)
resolve(row)
}
catch (error) {
reject(error)
}
finally {
try {
stmt.finalize()
}
catch {
// ignore finalize errors
}
}
})
const wrappedDb = {
close(callback) {
try {
sqliteDb.close()
if (typeof callback === 'function') callback(null)
}
catch (error) {
if (typeof callback === 'function') callback(error)
else throw error
}
},
}
try { try {
await initDb(db, run, all, get) await initDb(wrappedDb, run, all, get)
} }
catch (error) { catch (error) {
db.close() wrappedDb.close()
console.error('[db] Database initialization failed:', error.message) console.error('[db] Database initialization failed:', error.message)
throw error throw error
} }
dbInstance = { db, run, all, get } dbInstance = { db: wrappedDb, run, all, get }
registerCleanup(async () => { registerCleanup(async () => {
if (dbInstance) { if (dbInstance) {
try { try {
await new Promise((resolve, reject) => { dbInstance.db.close()
dbInstance.db.close((err) => {
if (err) reject(err)
else resolve()
})
})
} }
catch (error) { catch (error) {
console.error('[db] Error closing database during shutdown:', error?.message) console.error('[db] Error closing database during shutdown:', error?.message)
@@ -290,9 +392,7 @@ export async function withTransaction(db, callback) {
export function closeDb() { export function closeDb() {
if (!dbInstance) return if (!dbInstance) return
try { try {
dbInstance.db.close((err) => { dbInstance.db.close()
if (err) console.error('[db] Error closing database:', err.message)
})
} }
catch (error) { catch (error) {
console.error('[db] Error closing database:', error.message) console.error('[db] Error closing database:', error.message)
+280
View File
@@ -0,0 +1,280 @@
/** OSINT feeds (AISStream, OpenSky) → cotStore. */
import WebSocket from 'ws'
import { updateFromCot } from './cotStore.js'
import {
broadcastSubscriberSnapshots,
getSubscriberBboxUnion,
} from './cotSubscribers.js'
import { getCotSnapshotOpts } from './cotSnapshot.js'
import { openSkyStateToCot, aisStreamMessageToCot, clampBbox } from './cotEntityUtils.js'
import { OPENSKY_CACHE_MS, TRACKING_FEED_DEBOUNCE_MS, MAX_OPENSKY_BBOX_DEGREES } from './constants.js'
const COALESCE_MS = 150
const state = {
aisSocket: null,
aisReconnectTimer: null,
aisBackoffMs: 1000,
openSkyTimer: null,
bboxDebounceTimer: null,
coalesceTimer: null,
openSkyToken: null,
openSkyTokenExpiresAt: 0,
/** @type {Map<string, { fetchedAt: number, states: unknown[][] }>} */
openSkyCache: new Map(),
lastAisBbox: null,
stopped: false,
}
function getConfig() {
return useRuntimeConfig()
}
function openskyPollMs() {
return OPENSKY_CACHE_MS
}
async function fetchOpenSkyToken(clientId, clientSecret) {
const now = Date.now()
if (state.openSkyToken && state.openSkyTokenExpiresAt > now + 60_000) {
return state.openSkyToken
}
const body = new URLSearchParams({
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret,
})
const res = await fetch('https://auth.opensky-network.org/auth/realms/opensky-network/protocol/openid-connect/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
})
if (!res.ok) throw new Error(`OpenSky auth failed: ${res.status}`)
const json = await res.json()
state.openSkyToken = json.access_token
state.openSkyTokenExpiresAt = now + (Number(json.expires_in) || 1800) * 1000
return state.openSkyToken
}
function cacheKeyForBbox(bbox) {
const round = n => Math.round(n * 10) / 10
return `${round(bbox.west)},${round(bbox.south)},${round(bbox.east)},${round(bbox.north)}`
}
async function fetchOpenSkyForBbox(bbox) {
const config = getConfig()
const clientId = config.openskyClientId
const clientSecret = config.openskyClientSecret
const cacheMs = openskyPollMs()
const key = cacheKeyForBbox(bbox)
const cached = state.openSkyCache.get(key)
if (cached && Date.now() - cached.fetchedAt < cacheMs) {
return cached.states
}
const params = new URLSearchParams({
lamin: String(bbox.south),
lomin: String(bbox.west),
lamax: String(bbox.north),
lomax: String(bbox.east),
})
const headers = {}
if (clientId && clientSecret) {
const token = await fetchOpenSkyToken(clientId, clientSecret)
headers.Authorization = `Bearer ${token}`
}
const res = await fetch(`https://opensky-network.org/api/states/all?${params}`, { headers })
if (!res.ok) {
console.error('[trackingFeed] OpenSky fetch failed:', res.status)
return []
}
const json = await res.json()
const states = Array.isArray(json?.states) ? json.states : []
state.openSkyCache.set(key, { fetchedAt: Date.now(), states })
return states
}
export function scheduleCoalescedSnapshot() {
if (state.coalesceTimer) clearTimeout(state.coalesceTimer)
state.coalesceTimer = setTimeout(() => {
state.coalesceTimer = null
broadcastSubscriberSnapshots(getCotSnapshotOpts()).catch(() => {})
}, COALESCE_MS)
}
async function ingestOpenSkyBbox(bbox) {
try {
const states = await fetchOpenSkyForBbox(bbox)
for (const row of states) {
const parsed = openSkyStateToCot(row)
if (parsed) await updateFromCot(parsed, { silent: true })
}
if (states.length > 0) scheduleCoalescedSnapshot()
}
catch (err) {
console.error('[trackingFeed] OpenSky error:', err?.message)
}
}
async function pollOpenSky() {
if (state.stopped) return
const union = getSubscriberBboxUnion()
const bbox = clampBbox(union, MAX_OPENSKY_BBOX_DEGREES)
if (!bbox) return
await ingestOpenSkyBbox(bbox)
}
function stopOpenSkyPoll() {
if (state.openSkyTimer) {
clearInterval(state.openSkyTimer)
state.openSkyTimer = null
}
}
/** Tests only. @internal */
export function scheduleOpenSkyPollForTests() {
scheduleOpenSkyPoll()
}
function scheduleOpenSkyPoll() {
if (state.openSkyTimer || state.stopped) return
if (!getSubscriberBboxUnion()) return
const intervalMs = openskyPollMs()
state.openSkyTimer = setInterval(() => {
pollOpenSky().catch((err) => {
console.error('[trackingFeed] OpenSky poll error:', err?.message)
})
}, intervalMs)
}
function aisBboxKey(bbox) {
if (!bbox) return null
return `${bbox.west},${bbox.south},${bbox.east},${bbox.north}`
}
function closeAisSocket() {
if (state.aisSocket) {
try {
state.aisSocket.removeAllListeners()
state.aisSocket.close()
}
catch { /* ignore */ }
state.aisSocket = null
}
}
function scheduleAisReconnect() {
if (state.stopped || state.aisReconnectTimer) return
state.aisReconnectTimer = setTimeout(() => {
state.aisReconnectTimer = null
connectAisStream()
}, state.aisBackoffMs)
state.aisBackoffMs = Math.min(state.aisBackoffMs * 2, 60_000)
}
function subscribeAisBbox(ws, bbox) {
const apiKey = getConfig().aisstreamApiKey
if (!apiKey || !bbox) return
const key = aisBboxKey(bbox)
if (key === state.lastAisBbox) return
state.lastAisBbox = key
ws.send(JSON.stringify({
APIKey: apiKey,
BoundingBoxes: [[[bbox.south, bbox.west], [bbox.north, bbox.east]]],
}))
}
function connectAisStream() {
const apiKey = getConfig().aisstreamApiKey
if (!apiKey || state.stopped) return
closeAisSocket()
const ws = new WebSocket('wss://stream.aisstream.io/v0/stream')
state.aisSocket = ws
ws.on('open', () => {
state.aisBackoffMs = 1000
const union = getSubscriberBboxUnion()
const bbox = clampBbox(union, MAX_OPENSKY_BBOX_DEGREES)
if (bbox) subscribeAisBbox(ws, bbox)
})
ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString())
const parsed = aisStreamMessageToCot(message)
if (parsed) updateFromCot(parsed).catch(() => {})
}
catch {
/* ignore malformed AIS messages */
}
})
ws.on('close', () => {
state.aisSocket = null
if (!state.stopped) scheduleAisReconnect()
})
ws.on('error', (err) => {
console.error('[trackingFeed] AISStream error:', err?.message)
})
}
function refreshFeedBboxes() {
if (state.stopped) return
const union = getSubscriberBboxUnion()
const bbox = clampBbox(union, MAX_OPENSKY_BBOX_DEGREES)
if (!bbox) {
stopOpenSkyPoll()
return
}
if (getConfig().aisstreamApiKey) {
if (state.aisSocket?.readyState === WebSocket.OPEN) {
subscribeAisBbox(state.aisSocket, bbox)
}
else if (!state.aisSocket) {
connectAisStream()
}
}
pollOpenSky().catch(() => {})
scheduleOpenSkyPoll()
}
export function scheduleTrackingFeedRefresh() {
if (state.bboxDebounceTimer) clearTimeout(state.bboxDebounceTimer)
state.bboxDebounceTimer = setTimeout(() => {
state.bboxDebounceTimer = null
refreshFeedBboxes()
}, TRACKING_FEED_DEBOUNCE_MS)
}
export function startTrackingFeed() {
state.stopped = false
}
export function stopTrackingFeed() {
state.stopped = true
if (state.bboxDebounceTimer) clearTimeout(state.bboxDebounceTimer)
if (state.coalesceTimer) clearTimeout(state.coalesceTimer)
state.coalesceTimer = null
stopOpenSkyPoll()
if (state.aisReconnectTimer) clearTimeout(state.aisReconnectTimer)
state.aisReconnectTimer = null
closeAisSocket()
state.openSkyCache.clear()
state.lastAisBbox = null
}
/** Tests only. @internal */
export function resetTrackingFeedForTests() {
stopTrackingFeed()
state.stopped = false
}
/** Tests only. @internal */
export function isOpenSkyPollActive() {
return state.openSkyTimer != null
}
+1 -1
View File
@@ -23,7 +23,7 @@ const ensureDevCerts = () => {
) )
} }
catch (error) { catch (error) {
throw new Error(`Failed to generate dev certificates: ${error.message}`) throw new Error(`Failed to generate dev certificates: ${error.message}`, { cause: error })
} }
} }
+1 -1
View File
@@ -35,7 +35,7 @@ export function ensureDevCerts() {
console.log('[test] Generated .dev-certs/key.pem and .dev-certs/cert.pem') console.log('[test] Generated .dev-certs/key.pem and .dev-certs/cert.pem')
} }
catch (error) { catch (error) {
throw new Error(`Failed to generate dev certificates: ${error.message}`) throw new Error(`Failed to generate dev certificates: ${error.message}`, { cause: error })
} }
} }
+27 -10
View File
@@ -7,6 +7,7 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest' import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { spawn, execSync } from 'node:child_process' import { spawn, execSync } from 'node:child_process'
import { connect } from 'node:tls' import { connect } from 'node:tls'
import { createServer } from 'node:net'
import { existsSync, mkdirSync } from 'node:fs' import { existsSync, mkdirSync } from 'node:fs'
import { join, dirname } from 'node:path' import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
@@ -21,11 +22,21 @@ const devCertsDir = join(projectRoot, '.dev-certs')
const devKey = join(devCertsDir, 'key.pem') const devKey = join(devCertsDir, 'key.pem')
const devCert = join(devCertsDir, 'cert.pem') const devCert = join(devCertsDir, 'cert.pem')
const API_PORT = 3000
const COT_PORT = 8089
const COT_AUTH_USER = 'test' const COT_AUTH_USER = 'test'
const COT_AUTH_PASS = 'test' const COT_AUTH_PASS = 'test'
function getFreePort() {
return new Promise((resolve, reject) => {
const server = createServer()
server.listen(0, () => {
const address = server.address()
const port = typeof address === 'object' && address ? address.port : 0
server.close(() => resolve(port))
})
server.on('error', reject)
})
}
function ensureDevCerts() { function ensureDevCerts() {
if (existsSync(devKey) && existsSync(devCert)) return if (existsSync(devKey) && existsSync(devCert)) return
mkdirSync(devCertsDir, { recursive: true }) mkdirSync(devCertsDir, { recursive: true })
@@ -37,12 +48,12 @@ function ensureDevCerts() {
const FETCH_TIMEOUT_MS = 5000 const FETCH_TIMEOUT_MS = 5000
async function waitForHealth(timeoutMs = 90000) { async function waitForHealth(apiPort, timeoutMs = 90000) {
const start = Date.now() const start = Date.now()
while (Date.now() - start < timeoutMs) { while (Date.now() - start < timeoutMs) {
for (const protocol of ['https', 'http']) { for (const protocol of ['https', 'http']) {
try { try {
const baseURL = `${protocol}://localhost:${API_PORT}` const baseURL = `${protocol}://localhost:${apiPort}`
const ctrl = new AbortController() const ctrl = new AbortController()
const t = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS) const t = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS)
const res = await fetch(`${baseURL}/health`, { method: 'GET', signal: ctrl.signal }) const res = await fetch(`${baseURL}/health`, { method: 'GET', signal: ctrl.signal })
@@ -55,16 +66,20 @@ async function waitForHealth(timeoutMs = 90000) {
} }
await new Promise(r => setTimeout(r, 1000)) await new Promise(r => setTimeout(r, 1000))
} }
throw new Error(`Health not OK on ${API_PORT} within ${timeoutMs}ms`) throw new Error(`Health not OK on ${apiPort} within ${timeoutMs}ms`)
} }
describe('Server and CoT integration', () => { describe('Server and CoT integration', () => {
const testState = { const testState = {
serverProcess: null, serverProcess: null,
apiPort: 0,
cotPort: 0,
} }
beforeAll(async () => { beforeAll(async () => {
ensureDevCerts() ensureDevCerts()
testState.apiPort = await getFreePort()
testState.cotPort = await getFreePort()
const serverPath = join(projectRoot, '.output', 'server', 'index.mjs') const serverPath = join(projectRoot, '.output', 'server', 'index.mjs')
if (!existsSync(serverPath)) { if (!existsSync(serverPath)) {
execSync('npm run build', { cwd: projectRoot, stdio: 'pipe' }) execSync('npm run build', { cwd: projectRoot, stdio: 'pipe' })
@@ -72,6 +87,8 @@ describe('Server and CoT integration', () => {
const dbPath = join(tmpdir(), `kestrelos-it-${process.pid}-${Date.now()}.db`) const dbPath = join(tmpdir(), `kestrelos-it-${process.pid}-${Date.now()}.db`)
const env = { const env = {
...process.env, ...process.env,
PORT: String(testState.apiPort),
COT_PORT: String(testState.cotPort),
DB_PATH: dbPath, DB_PATH: dbPath,
BOOTSTRAP_EMAIL: COT_AUTH_USER, BOOTSTRAP_EMAIL: COT_AUTH_USER,
BOOTSTRAP_PASSWORD: COT_AUTH_PASS, BOOTSTRAP_PASSWORD: COT_AUTH_PASS,
@@ -83,7 +100,7 @@ describe('Server and CoT integration', () => {
}) })
testState.serverProcess.stdout?.on('data', d => process.stdout.write(d)) testState.serverProcess.stdout?.on('data', d => process.stdout.write(d))
testState.serverProcess.stderr?.on('data', d => process.stderr.write(d)) testState.serverProcess.stderr?.on('data', d => process.stderr.write(d))
await waitForHealth(90000) await waitForHealth(testState.apiPort, 90000)
}, 120000) }, 120000)
afterAll(() => { afterAll(() => {
@@ -92,11 +109,11 @@ describe('Server and CoT integration', () => {
} }
}) })
it('serves health on port 3000', async () => { it('serves health on the configured API port', async () => {
const tryProtocols = async (protocols) => { const tryProtocols = async (protocols) => {
if (protocols.length === 0) throw new Error('No protocol succeeded') if (protocols.length === 0) throw new Error('No protocol succeeded')
try { try {
const res = await fetch(`${protocols[0]}://localhost:${API_PORT}/health`, { method: 'GET', headers: { Accept: 'application/json' } }) const res = await fetch(`${protocols[0]}://localhost:${testState.apiPort}/health`, { method: 'GET', headers: { Accept: 'application/json' } })
if (res?.ok) return res if (res?.ok) return res
return tryProtocols(protocols.slice(1)) return tryProtocols(protocols.slice(1))
} }
@@ -112,10 +129,10 @@ describe('Server and CoT integration', () => {
expect(body.endpoints).toHaveProperty('ready', '/health/ready') expect(body.endpoints).toHaveProperty('ready', '/health/ready')
}) })
it('CoT on 8089: TAK client auth with username/password succeeds (socket stays open)', async () => { it('CoT: TAK client auth with username/password succeeds (socket stays open)', async () => {
const payload = buildAuthCotXml({ username: COT_AUTH_USER, password: COT_AUTH_PASS }) const payload = buildAuthCotXml({ username: COT_AUTH_USER, password: COT_AUTH_PASS })
const socket = await new Promise((resolve, reject) => { const socket = await new Promise((resolve, reject) => {
const s = connect(COT_PORT, '127.0.0.1', { rejectUnauthorized: false }, () => { const s = connect(testState.cotPort, '127.0.0.1', { rejectUnauthorized: false }, () => {
s.write(payload, () => resolve(s)) s.write(payload, () => resolve(s))
}) })
s.on('error', reject) s.on('error', reject)
+23 -24
View File
@@ -1,27 +1,24 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { SHUTDOWN_TIMEOUT_MS } from '../../server/utils/constants.js'
import { registerCleanup, graceful, initShutdownHandlers, clearCleanup } from '../../server/utils/shutdown.js' import { registerCleanup, graceful, initShutdownHandlers, clearCleanup } from '../../server/utils/shutdown.js'
describe('shutdown integration', () => { describe('shutdown integration', () => {
const testState = { /** @type {import('vitest').MockInstance} */
originalExit: null, let exitSpy
exitCalls: [], /** @type {typeof process.on} */
originalOn: null, let originalOn
}
beforeEach(() => { beforeEach(() => {
clearCleanup() clearCleanup()
testState.exitCalls = [] exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {})
testState.originalExit = process.exit originalOn = process.on
process.exit = vi.fn((code) => {
testState.exitCalls.push(code)
})
testState.originalOn = process.on
}) })
afterEach(() => { afterEach(() => {
process.exit = testState.originalExit exitSpy.mockRestore()
process.on = testState.originalOn process.on = originalOn
clearCleanup() clearCleanup()
vi.useRealTimers()
}) })
it('initializes signal handlers', () => { it('initializes signal handlers', () => {
@@ -32,7 +29,6 @@ describe('shutdown integration', () => {
initShutdownHandlers() initShutdownHandlers()
expect(process.on).toHaveBeenCalledWith('SIGTERM', expect.any(Function)) expect(process.on).toHaveBeenCalledWith('SIGTERM', expect.any(Function))
expect(process.on).toHaveBeenCalledWith('SIGINT', expect.any(Function)) expect(process.on).toHaveBeenCalledWith('SIGINT', expect.any(Function))
process.on = testState.originalOn
}) })
it('signal handler calls graceful', async () => { it('signal handler calls graceful', async () => {
@@ -43,9 +39,10 @@ describe('shutdown integration', () => {
initShutdownHandlers() initShutdownHandlers()
const sigtermHandler = handlers.SIGTERM const sigtermHandler = handlers.SIGTERM
expect(sigtermHandler).toBeDefined() expect(sigtermHandler).toBeDefined()
await sigtermHandler() sigtermHandler()
expect(testState.exitCalls.length).toBeGreaterThan(0) await vi.waitFor(() => {
process.on = testState.originalOn expect(exitSpy).toHaveBeenCalled()
})
}) })
it('signal handler handles graceful error', async () => { it('signal handler handles graceful error', async () => {
@@ -59,18 +56,20 @@ describe('shutdown integration', () => {
registerCleanup(async () => { registerCleanup(async () => {
throw new Error('Force error') throw new Error('Force error')
}) })
await sigintHandler() sigintHandler()
expect(testState.exitCalls.length).toBeGreaterThan(0) await vi.waitFor(() => {
process.on = testState.originalOn expect(exitSpy).toHaveBeenCalled()
})
}) })
it('covers timeout path in graceful', async () => { it('covers timeout path in graceful', async () => {
vi.useFakeTimers()
registerCleanup(async () => { registerCleanup(async () => {
await new Promise(resolve => setTimeout(resolve, 40000)) await new Promise(resolve => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS + 5_000))
}) })
graceful() graceful()
await new Promise(resolve => setTimeout(resolve, 100)) await vi.advanceTimersByTimeAsync(SHUTDOWN_TIMEOUT_MS + 1)
expect(testState.exitCalls.length).toBeGreaterThan(0) expect(exitSpy).toHaveBeenCalledWith(1)
}) })
it('covers graceful catch block', async () => { it('covers graceful catch block', async () => {
@@ -78,6 +77,6 @@ describe('shutdown integration', () => {
throw new Error('Test error') throw new Error('Test error')
}) })
await graceful() await graceful()
expect(testState.exitCalls.length).toBeGreaterThan(0) expect(exitSpy).toHaveBeenCalled()
}) })
}) })
+8
View File
@@ -75,4 +75,12 @@ describe('KestrelMap', () => {
expect(wrapper.props('pois')).toHaveLength(1) expect(wrapper.props('pois')).toHaveLength(1)
expect(wrapper.props('canEditPois')).toBe(false) expect(wrapper.props('canEditPois')).toBe(false)
}) })
it('includes CoT layer toggles and supercluster layer', async () => {
const componentPath = resolve(__dirname, '../../app/components/KestrelMap.vue')
const source = readFileSync(componentPath, 'utf-8')
expect(source).toContain('cot-layer-toggles')
expect(source).toContain('cotMapLayer')
expect(source).toContain('syncCotLayer')
})
}) })
-14
View File
@@ -15,26 +15,12 @@ describe('useCameras', () => {
setupEndpoints(() => ({ setupEndpoints(() => ({
devices: [{ id: '1', name: 'Test', lat: 37.7, lng: -122.4, streamUrl: '', sourceType: 'mjpeg', device_type: 'feed' }], devices: [{ id: '1', name: 'Test', lat: 37.7, lng: -122.4, streamUrl: '', sourceType: 'mjpeg', device_type: 'feed' }],
liveSessions: [], liveSessions: [],
cotEntities: [],
})) }))
const wrapper = await mountSuspended(Index) const wrapper = await mountSuspended(Index)
await wait() await wait()
expect(wrapper.findComponent({ name: 'KestrelMap' }).exists()).toBe(true) expect(wrapper.findComponent({ name: 'KestrelMap' }).exists()).toBe(true)
}) })
it('exposes cotEntities from API', async () => {
const cotEntities = [{ id: 'cot-1', lat: 38, lng: -123, label: 'ATAK1' }]
setupEndpoints(() => ({
devices: [],
liveSessions: [],
cotEntities,
}))
const wrapper = await mountSuspended(Index)
await wait()
const map = wrapper.findComponent({ name: 'KestrelMap' })
expect(map.props('cotEntities')).toEqual(cotEntities)
})
it('handles API error and falls back to empty devices and liveSessions', async () => { it('handles API error and falls back to empty devices and liveSessions', async () => {
setupEndpoints(() => { setupEndpoints(() => {
throw new Error('network') throw new Error('network')
+38
View File
@@ -0,0 +1,38 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { useCotLayers } from '../../app/composables/useCotLayers.js'
describe('useCotLayers', () => {
beforeEach(() => {
localStorage.clear()
})
afterEach(() => {
localStorage.clear()
})
it('defaults all layers on', () => {
const { layers, layerQuery } = useCotLayers()
expect(layers.value).toEqual({ air: true, surface: true, ground: true })
expect(layerQuery.value).toBe('air,surface,ground')
})
it('toggles layers and persists to localStorage', () => {
const { layers, toggleLayer, layerQuery } = useCotLayers()
toggleLayer('air')
expect(layers.value.air).toBe(false)
expect(layerQuery.value).toBe('surface,ground')
const stored = JSON.parse(localStorage.getItem('kestrel-cot-layers'))
expect(stored.air).toBe(false)
})
})
describe('useCotStream helpers', () => {
it('layer query none when all off', () => {
localStorage.clear()
const { toggleLayer, layerQuery } = useCotLayers()
toggleLayer('air')
toggleLayer('surface')
toggleLayer('ground')
expect(layerQuery.value).toBe('none')
})
})
+154
View File
@@ -0,0 +1,154 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import {
parseOverpassElement,
parseBbox,
tileKeysForBbox,
getAlprCameras,
markTilesFetched,
} from '../../server/utils/alpr.js'
import { cameraToFeature, identifyingProperties, inferModelFromTags, isGenericCameraName } from '../../server/utils/alprGeo.js'
import { getDb, setDbPathForTest, closeDb } from '../../server/utils/db.js'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { randomUUID } from 'node:crypto'
describe('alpr utils', () => {
describe('parseOverpassElement', () => {
it('parses a valid OSM node', () => {
const out = parseOverpassElement({
type: 'node',
id: 123,
lat: 33.75,
lon: -84.39,
tags: { 'manufacturer': 'Flock Safety', 'camera:direction': '90' },
})
expect(out?.osmId).toBe(123)
expect(out?.manufacturer).toBe('Flock Safety')
})
})
describe('cameraToFeature', () => {
it('returns GeoJSON Point feature with identifying tags', () => {
const feature = cameraToFeature({
osmId: 1,
lat: 33.5,
lng: -84.5,
manufacturer: 'Flock Safety',
direction: 90,
fov: 45,
tags: {
'model': 'Falcon',
'operator': 'City PD',
'ref': 'CAM-12',
'operator:wikidata': 'Q123',
'fixme': 'verify mount',
},
})
expect(feature.properties.manufacturer).toBe('Flock Safety')
expect(feature.properties.model).toBe('Falcon')
expect(feature.properties.operator).toBe('City PD')
expect(feature.properties.ref).toBe('CAM-12')
expect(feature.properties.operatorWikidata).toBe('Q123')
expect(feature.properties.tags).toEqual({ fixme: 'verify mount' })
})
})
describe('identifyingProperties', () => {
it('omits brand when same as manufacturer', () => {
const props = identifyingProperties({ manufacturer: 'Flock Safety', brand: 'Flock Safety', model: 'Falcon' })
expect(props.manufacturer).toBe('Flock Safety')
expect(props.model).toBe('Falcon')
expect(props.brand).toBeUndefined()
})
it('infers model from lowercase name tag', () => {
const props = identifyingProperties({
manufacturer: 'Flock Safety',
name: 'falcon',
})
expect(props.model).toBe('Falcon')
expect(props.name).toBeUndefined()
expect(props.modelUnknown).toBeUndefined()
})
it('drops generic camera names and flags unknown model', () => {
const props = identifyingProperties({
manufacturer: 'Flock Safety',
name: 'Flock ALPR camera',
})
expect(props.name).toBeUndefined()
expect(props.model).toBeUndefined()
expect(props.modelUnknown).toBe(true)
})
})
describe('inferModelFromTags', () => {
it('reads model tag and mis-tagged Flock Falcon name', () => {
expect(inferModelFromTags({ model: 'Sparrow' })).toBe('Sparrow')
expect(inferModelFromTags({ name: 'Flock Falcon' })).toBe('Falcon')
expect(isGenericCameraName('Flock ALPR camera')).toBe(true)
expect(isGenericCameraName('Peachtree & 5th')).toBe(false)
})
})
describe('parseBbox', () => {
it('parses valid bbox', () => {
expect(parseBbox({ south: 33.0, west: -85.0, north: 33.4, east: -84.6 })).toEqual({
south: 33.0,
west: -85.0,
north: 33.4,
east: -84.6,
})
})
it('rejects oversized bbox', () => {
expect(() => parseBbox({ south: 0, west: 0, north: 2, east: 1 })).toThrow(/bbox exceeds/)
})
})
describe('cache and fetch', () => {
let dbPath
beforeEach(async () => {
dbPath = join(tmpdir(), `kestrelos-alpr-${randomUUID()}.db`)
setDbPathForTest(dbPath)
await getDb()
})
afterEach(() => {
closeDb()
setDbPathForTest(null)
})
it('returns GeoJSON FeatureCollection from cache', async () => {
const db = await getDb()
const bbox = { south: 33.4, west: -85.0, north: 33.6, east: -84.0 }
await db.run(
`INSERT INTO alpr_nodes (osm_id, lat, lng, manufacturer, direction, tags, fetched_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[99, 33.5, -84.5, 'Acme', 180, '{}', new Date().toISOString()],
)
await markTilesFetched(db, tileKeysForBbox(bbox))
vi.stubGlobal('fetch', vi.fn())
const result = await getAlprCameras(db, bbox)
expect(result.type).toBe('FeatureCollection')
expect(result.features).toHaveLength(1)
expect(result.source).toBe('cache')
vi.unstubAllGlobals()
})
it('falls back to cache when Overpass fails', async () => {
const db = await getDb()
await db.run(
`INSERT INTO alpr_nodes (osm_id, lat, lng, manufacturer, direction, tags, fetched_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[42, 33.5, -84.5, null, null, '{}', new Date().toISOString()],
)
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down')))
const result = await getAlprCameras(db, { south: 33.4, west: -85.0, north: 33.6, east: -84.0 })
expect(result.type).toBe('FeatureCollection')
expect(result.features).toHaveLength(1)
vi.unstubAllGlobals()
})
})
})
+57
View File
@@ -0,0 +1,57 @@
import { describe, it, expect } from 'vitest'
import { formatAlprPopup } from '../../app/utils/alprMapLayer.js'
describe('formatAlprPopup', () => {
it('summarizes manufacturer, model, and operator', () => {
const html = formatAlprPopup({
osmId: 1,
manufacturer: 'Flock Safety',
model: 'Falcon',
operator: 'City PD',
direction: 90,
fov: 60,
})
expect(html).toContain('Flock Safety')
expect(html).toContain('Falcon')
expect(html).toContain('License plate reader')
expect(html).toContain('City PD')
expect(html).toContain('Facing E (90°)')
expect(html).toContain('~60° view')
expect(html).not.toContain('Manufacturer')
expect(html).not.toContain('fixme')
})
it('uses name as title with make/model below', () => {
const html = formatAlprPopup({
osmId: 2,
name: 'Peachtree & 5th',
manufacturer: 'Flock Safety',
model: 'Falcon',
})
expect(html).toContain('<strong>Peachtree &amp; 5th</strong>')
expect(html).toContain('Model</span> <strong>Falcon</strong>')
expect(html).toContain('Flock Safety Falcon')
})
it('links operator wikidata inline', () => {
const html = formatAlprPopup({
osmId: 3,
operator: 'Atlanta Police',
operatorWikidata: 'Q123',
})
expect(html).toContain('<strong>')
expect(html).toContain('href="https://www.wikidata.org/wiki/Q123"')
expect(html).toContain('Atlanta Police')
expect(html).not.toContain('Wikidata')
})
it('shows model unknown when OSM lacks model tag', () => {
const html = formatAlprPopup({
osmId: 4,
manufacturer: 'Flock Safety',
modelUnknown: true,
})
expect(html).toContain('Flock Safety')
expect(html).toContain('Model not recorded in OpenStreetMap')
})
})
+10
View File
@@ -0,0 +1,10 @@
import { describe, it, expect } from 'vitest'
import { tilesNearCenter } from '../../app/utils/alprViewport.js'
describe('tilesNearCenter', () => {
it('returns a single tile for a small viewport', () => {
const tiles = tilesNearCenter({ south: 37.7, west: -122.5, north: 37.8, east: -122.4 }, 16)
expect(tiles).toHaveLength(1)
expect(tiles[0]).toEqual({ south: 37.5, west: -122.5, north: 38, east: -122 })
})
})
+1
View File
@@ -10,6 +10,7 @@ describe('authSkipPaths', () => {
it('does not skip any protected path', () => { it('does not skip any protected path', () => {
const protectedPaths = [ const protectedPaths = [
...PROTECTED_PATH_PREFIXES, ...PROTECTED_PATH_PREFIXES,
'/api/alpr',
'/api/cameras', '/api/cameras',
'/api/devices', '/api/devices',
'/api/devices/any-id', '/api/devices/any-id',
+6
View File
@@ -2,7 +2,10 @@ import { describe, it, expect } from 'vitest'
import { import {
COT_AUTH_TIMEOUT_MS, COT_AUTH_TIMEOUT_MS,
LIVE_SESSION_TTL_MS, LIVE_SESSION_TTL_MS,
COT_TTL_MS,
COT_ENTITY_TTL_MS, COT_ENTITY_TTL_MS,
COT_OSINT_TTL_MS,
COT_PRUNE_INTERVAL_MS,
POLL_INTERVAL_MS, POLL_INTERVAL_MS,
SHUTDOWN_TIMEOUT_MS, SHUTDOWN_TIMEOUT_MS,
COT_PORT, COT_PORT,
@@ -18,7 +21,10 @@ describe('constants', () => {
it('uses default values when env vars not set', () => { it('uses default values when env vars not set', () => {
expect(COT_AUTH_TIMEOUT_MS).toBe(15000) expect(COT_AUTH_TIMEOUT_MS).toBe(15000)
expect(LIVE_SESSION_TTL_MS).toBe(60000) expect(LIVE_SESSION_TTL_MS).toBe(60000)
expect(COT_TTL_MS).toBe(90000)
expect(COT_ENTITY_TTL_MS).toBe(90000) expect(COT_ENTITY_TTL_MS).toBe(90000)
expect(COT_OSINT_TTL_MS).toBe(30000)
expect(COT_PRUNE_INTERVAL_MS).toBe(15000)
expect(POLL_INTERVAL_MS).toBe(1500) expect(POLL_INTERVAL_MS).toBe(1500)
expect(SHUTDOWN_TIMEOUT_MS).toBe(30000) expect(SHUTDOWN_TIMEOUT_MS).toBe(30000)
expect(COT_PORT).toBe(8089) expect(COT_PORT).toBe(8089)
+68
View File
@@ -0,0 +1,68 @@
import { describe, it, expect } from 'vitest'
import {
cotAirIconKind,
cotCategory,
formatCotPopup,
getCotIconHtml,
} from '../../app/utils/cotDisplay.js'
const esc = s => String(s)
describe('cotDisplay', () => {
it('detects helicopter from CoT type', () => {
expect(cotAirIconKind({ type: 'a-f-A-C-H' })).toBe('helicopter')
expect(cotAirIconKind({ type: 'a-f-A-C-F' })).toBe('fixedWing')
})
it('renders distinct air icon kinds', () => {
const plane = getCotIconHtml({ type: 'a-f-A-C-F', heading: 90 })
const heli = getCotIconHtml({ type: 'a-f-A-C-H', heading: 180 })
expect(plane.className).toBe('cot-entity-fixedWing')
expect(heli.className).toBe('cot-entity-helicopter')
expect(plane.html).toContain('rotate(90deg)')
expect(heli.html).toContain('rotate(180deg)')
})
it('formats rich ADS-B popup', () => {
const html = formatCotPopup({
source: 'adsb',
type: 'a-f-A-C-F',
label: 'UAL123',
icao: 'abc123',
originCountry: 'United States',
altitude: 10000,
speed: 200,
heading: 270,
verticalRate: 5,
squawk: '1200',
}, esc)
expect(html).toContain('UAL123')
expect(html).toContain('Aircraft')
expect(html).toContain('ICAO ABC123')
expect(html).toContain('United States')
expect(html).toContain('ft')
expect(html).toContain('kt')
expect(html).toContain('270°')
expect(html).toContain('Squawk 1200')
})
it('formats vessel popup with MMSI', () => {
const html = formatCotPopup({
source: 'ais',
type: 'a-f-S-C',
label: 'TEST SHIP',
id: 'MMSI.366123456',
speed: 12,
heading: 90,
}, esc)
expect(html).toContain('Vessel')
expect(html).toContain('MMSI 366123456')
})
it('formats team popup', () => {
expect(cotCategory('a-f-G-U-C')).toBe('ground')
const html = formatCotPopup({ type: 'a-f-G-U-C', label: 'Alpha 1' }, esc)
expect(html).toContain('Team')
expect(html).toContain('Alpha 1')
})
})
+111
View File
@@ -0,0 +1,111 @@
import { describe, it, expect } from 'vitest'
import {
inferSourceFromId,
cotCategoryFromType,
isInBbox,
matchesLayerFilter,
openSkyStateToCot,
aisStreamMessageToCot,
unionBboxes,
clampBbox,
parseBboxParam,
parseLayersParam,
} from '../../../server/utils/cotEntityUtils.js'
describe('cotEntityUtils', () => {
it('infers source from UID prefix', () => {
expect(inferSourceFromId('ICAO.abc123')).toBe('adsb')
expect(inferSourceFromId('MMSI.366123456')).toBe('ais')
expect(inferSourceFromId('ANDROID-deadbeef')).toBe('tak')
})
it('maps CoT type to category', () => {
expect(cotCategoryFromType('a-f-A-C-F')).toBe('air')
expect(cotCategoryFromType('a-f-S-C')).toBe('surface')
expect(cotCategoryFromType('a-f-G-U-C')).toBe('ground')
})
it('checks bbox membership', () => {
const bbox = { west: -123, south: 37, east: -122, north: 38 }
expect(isInBbox({ lat: 37.5, lng: -122.5 }, bbox)).toBe(true)
expect(isInBbox({ lat: 40, lng: -122.5 }, bbox)).toBe(false)
})
it('filters by layer set', () => {
const airOnly = new Set(['air'])
expect(matchesLayerFilter(airOnly, { type: 'a-f-A-C-F' })).toBe(true)
expect(matchesLayerFilter(airOnly, { type: 'a-f-S-C' })).toBe(false)
})
it('maps OpenSky state vector to CoT', () => {
const state = ['abc123', 'UAL123 ', 'United States', 1, 2, -122.4, 37.7, 10000, false, 200, 90, 5, null, null, 1200, false, 0, 0]
const cot = openSkyStateToCot(state)
expect(cot).toMatchObject({
id: 'ICAO.abc123',
lat: 37.7,
lng: -122.4,
label: 'UAL123',
source: 'adsb',
type: 'a-f-A-C-F',
icao: 'abc123',
originCountry: 'United States',
heading: 90,
speed: 200,
altitude: 10000,
verticalRate: 5,
squawk: '1200',
})
})
it('maps OpenSky rotorcraft to helicopter CoT type', () => {
const state = ['heli01', 'N123HC ', 'United States', 1, 2, -122.4, 37.7, 500, false, 50, 180, 0, null, null, null, false, 0, 8]
const cot = openSkyStateToCot(state)
expect(cot?.type).toBe('a-f-A-C-H')
})
it('maps AISStream message to CoT', () => {
const cot = aisStreamMessageToCot({
MetaData: { MMSI: 366123456, ShipName: 'TEST SHIP' },
Message: {
PositionReport: {
UserID: 366123456,
Latitude: 37.8,
Longitude: -122.3,
Sog: 12.5,
Cog: 180,
},
},
})
expect(cot).toMatchObject({
id: 'MMSI.366123456',
lat: 37.8,
lng: -122.3,
label: 'TEST SHIP',
source: 'ais',
type: 'a-f-S-C',
})
})
it('unions bboxes', () => {
expect(unionBboxes([
{ west: -123, south: 37, east: -122, north: 38 },
{ west: -124, south: 36, east: -121, north: 39 },
])).toEqual({ west: -124, south: 36, east: -121, north: 39 })
})
it('clamps oversized bbox to max span', () => {
const huge = { west: -125, south: 32, east: -115, north: 42 }
const clamped = clampBbox(huge, 10)
expect(clamped.north - clamped.south).toBeCloseTo(10)
expect(clamped.east - clamped.west).toBeCloseTo(10)
expect((clamped.north + clamped.south) / 2).toBeCloseTo(37)
expect((clamped.east + clamped.west) / 2).toBeCloseTo(-120)
})
it('parses bbox and layers query params', () => {
expect(parseBboxParam('-123,37,-122,38')).toEqual({ west: -123, south: 37, east: -122, north: 38 })
expect(parseBboxParam('bad')).toBeNull()
expect(parseLayersParam('air,surface')).toEqual(new Set(['air', 'surface']))
expect(parseLayersParam('none').size).toBe(0)
})
})
+46
View File
@@ -0,0 +1,46 @@
import { describe, it, expect } from 'vitest'
import {
entitiesToFeatures,
getCotClusters,
loadCotCluster,
} from '../../app/utils/cotMapLayer.js'
function makeEntities(n, centerLat = 37.7, centerLng = -122.4) {
return Array.from({ length: n }, (_, i) => ({
id: `ICAO.${i}`,
lat: centerLat + (i % 10) * 0.01,
lng: centerLng + Math.floor(i / 10) * 0.01,
type: 'a-f-A-C-F',
label: `AC${i}`,
}))
}
describe('cotMapLayer', () => {
it('converts entities to GeoJSON points', () => {
const features = entitiesToFeatures([
{ id: 'ICAO.1', lat: 37.7, lng: -122.4, type: 'a-f-A-C-F' },
{ id: 'bad', lat: 'x', lng: 0 },
])
expect(features).toHaveLength(1)
expect(features[0].geometry.coordinates).toEqual([-122.4, 37.7])
expect(features[0].properties.entity.id).toBe('ICAO.1')
})
it('clusters dense tracks at low zoom', () => {
loadCotCluster(makeEntities(50))
const view = { west: -123, south: 37, east: -122, north: 38, zoom: 6 }
const clusters = getCotClusters(view)
const clusterCount = clusters.filter(f => f.properties?.cluster).length
const pointCount = clusters.filter(f => !f.properties?.cluster).length
expect(clusterCount).toBeGreaterThan(0)
expect(clusterCount + pointCount).toBeLessThan(50)
})
it('shows individual markers at high zoom', () => {
loadCotCluster(makeEntities(20))
const view = { west: -123, south: 37, east: -122, north: 38, zoom: 15 }
const clusters = getCotClusters(view)
expect(clusters.every(f => !f.properties?.cluster)).toBe(true)
expect(clusters).toHaveLength(20)
})
})
+13
View File
@@ -44,4 +44,17 @@ describe('cotServer (parse-and-store path)', () => {
expect(active[0].lng).toBe(4) expect(active[0].lng).toBe(4)
expect(active[0].label).toBe('Updated') expect(active[0].label).toBe('Updated')
}) })
it('infers adsb source from ICAO uid when ingesting CoT position', async () => {
await updateFromCot({
id: 'ICAO.abc123',
lat: 37.7,
lng: -122.4,
label: 'N12345',
eventType: 'a-f-A-C-F',
})
const active = await getActiveEntities()
expect(active[0].source).toBe('adsb')
expect(active[0].type).toBe('a-f-A-C-F')
})
}) })
+84 -3
View File
@@ -1,5 +1,12 @@
import { describe, it, expect, beforeEach } from 'vitest' import { describe, it, expect, beforeEach } from 'vitest'
import { updateFromCot, getActiveEntities, clearCotStore } from '../../../server/utils/cotStore.js' import {
updateFromCot,
getActiveEntities,
getActiveEntitiesInBbox,
clearCotStore,
onCotChange,
pruneStaleEntities,
} from '../../../server/utils/cotStore.js'
describe('cotStore', () => { describe('cotStore', () => {
beforeEach(() => { beforeEach(() => {
@@ -14,6 +21,28 @@ describe('cotStore', () => {
expect(active[0].lat).toBe(37.7) expect(active[0].lat).toBe(37.7)
expect(active[0].lng).toBe(-122.4) expect(active[0].lng).toBe(-122.4)
expect(active[0].label).toBe('Alpha') expect(active[0].label).toBe('Alpha')
expect(active[0].source).toBe('tak')
})
it('stores enriched ADS-B fields and infers source', async () => {
await updateFromCot({
id: 'ICAO.abc123',
lat: 37.7,
lng: -122.4,
label: 'UAL1',
type: 'a-f-A-C-F',
heading: 90,
speed: 200,
altitude: 10000,
})
const active = await getActiveEntities()
expect(active[0]).toMatchObject({
source: 'adsb',
heading: 90,
speed: 200,
altitude: 10000,
type: 'a-f-A-C-F',
})
}) })
it('updates same uid', async () => { it('updates same uid', async () => {
@@ -41,10 +70,10 @@ describe('cotStore', () => {
it('prunes expired entities after getActiveEntities', async () => { it('prunes expired entities after getActiveEntities', async () => {
await updateFromCot({ id: 'uid-1', lat: 37, lng: -122 }) await updateFromCot({ id: 'uid-1', lat: 37, lng: -122 })
const active1 = await getActiveEntities(100) const active1 = await getActiveEntities({ ttlMs: 100 })
expect(active1).toHaveLength(1) expect(active1).toHaveLength(1)
await new Promise(r => setTimeout(r, 150)) await new Promise(r => setTimeout(r, 150))
const active2 = await getActiveEntities(100) const active2 = await getActiveEntities({ ttlMs: 100 })
expect(active2).toHaveLength(0) expect(active2).toHaveLength(0)
}) })
@@ -55,4 +84,56 @@ describe('cotStore', () => {
expect(active).toHaveLength(2) expect(active).toHaveLength(2)
expect(active.map(e => e.id).sort()).toEqual(['a', 'b']) expect(active.map(e => e.id).sort()).toEqual(['a', 'b'])
}) })
it('filters OSINT entities by bbox but keeps team globally', async () => {
await updateFromCot({ id: 'ICAO.abc', lat: 37.5, lng: -122.5, type: 'a-f-A-C-F' })
await updateFromCot({ id: 'MMSI.123', lat: 40, lng: -100, type: 'a-f-S-C' })
await updateFromCot({ id: 'ANDROID-1', lat: 50, lng: 10, source: 'tak' })
const bbox = { west: -123, south: 37, east: -122, north: 38 }
const active = await getActiveEntitiesInBbox(bbox, { takFilterBbox: false })
const ids = active.map(e => e.id).sort()
expect(ids).toEqual(['ANDROID-1', 'ICAO.abc'])
})
it('filters team by bbox when COT_TAK_FILTER_BBOX enabled', async () => {
await updateFromCot({ id: 'ANDROID-1', lat: 50, lng: 10, source: 'tak' })
const bbox = { west: -123, south: 37, east: -122, north: 38 }
const active = await getActiveEntitiesInBbox(bbox, { takFilterBbox: true })
expect(active).toHaveLength(0)
})
it('caps bbox query results at maxEntities', async () => {
for (let i = 0; i < 5; i++) {
await updateFromCot({ id: `ICAO.${i}`, lat: 37.5 + i * 0.01, lng: -122.4, type: 'a-f-A-C-F' })
}
const bbox = { west: -123, south: 37, east: -122, north: 38 }
const active = await getActiveEntitiesInBbox(bbox, { maxEntities: 3 })
expect(active).toHaveLength(3)
})
it('skips emit when silent upsert', async () => {
const updates = []
const off = onCotChange((event) => {
if (event === 'update') updates.push(event)
})
await updateFromCot({ id: 'ICAO.silent', lat: 37, lng: -122, type: 'a-f-A-C-F' }, { silent: true })
off()
expect(updates).toHaveLength(0)
const active = await getActiveEntities()
expect(active.some(e => e.id === 'ICAO.silent')).toBe(true)
})
it('pruneStaleEntities uses shorter TTL for OSINT sources', async () => {
await updateFromCot({ id: 'ICAO.old', lat: 37, lng: -122, source: 'adsb', type: 'a-f-A-C-F' })
await updateFromCot({ id: 'ANDROID-1', lat: 37, lng: -122, source: 'tak' })
const removed = []
const off = onCotChange((event, payload) => {
if (event === 'remove') removed.push(payload.id)
})
await new Promise(r => setTimeout(r, 60))
await pruneStaleEntities({ ttlMs: 10_000, osintTtlMs: 50 })
off()
expect(removed).toContain('ICAO.old')
expect(removed).not.toContain('ANDROID-1')
})
}) })
+96
View File
@@ -0,0 +1,96 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import {
registerSubscriber,
getSubscriberBboxUnion,
clearSubscribers,
notifySubscribersForEntity,
notifySubscribersRemove,
broadcastSubscriberSnapshots,
} from '../../../server/utils/cotSubscribers.js'
import { updateFromCot, clearCotStore } from '../../../server/utils/cotStore.js'
describe('cotSubscribers', () => {
beforeEach(() => {
clearSubscribers()
})
it('unions subscriber bboxes', () => {
registerSubscriber({
bbox: { west: -123, south: 37, east: -122, north: 38 },
layers: new Set(['air']),
push: vi.fn(),
})
registerSubscriber({
bbox: { west: -124, south: 36, east: -121, north: 39 },
layers: new Set(['surface']),
push: vi.fn(),
})
expect(getSubscriberBboxUnion()).toEqual({ west: -124, south: 36, east: -121, north: 39 })
})
it('notifies subscribers inside bbox and matching layer', async () => {
const push = vi.fn()
registerSubscriber({
bbox: { west: -123, south: 37, east: -122, north: 38 },
layers: new Set(['air']),
push,
})
await notifySubscribersForEntity('update', { entity: { id: 'ICAO.x' } }, {
id: 'ICAO.x',
lat: 37.5,
lng: -122.5,
type: 'a-f-A-C-F',
})
expect(push).toHaveBeenCalledWith('update', expect.any(String))
})
it('skips subscribers when entity outside bbox', async () => {
const push = vi.fn()
registerSubscriber({
bbox: { west: -123, south: 37, east: -122, north: 38 },
layers: new Set(['air']),
push,
})
await notifySubscribersForEntity('update', { entity: { id: 'ICAO.x' } }, {
id: 'ICAO.x',
lat: 40,
lng: -122.5,
type: 'a-f-A-C-F',
})
expect(push).not.toHaveBeenCalled()
})
it('notifySubscribersRemove pushes to all subscribers', async () => {
const pushA = vi.fn()
const pushB = vi.fn()
registerSubscriber({
bbox: { west: -123, south: 37, east: -122, north: 38 },
layers: new Set(['air']),
push: pushA,
})
registerSubscriber({
bbox: { west: -125, south: 35, east: -120, north: 40 },
layers: new Set(['surface']),
push: pushB,
})
await notifySubscribersRemove('ICAO.removed')
expect(pushA).toHaveBeenCalledWith('remove', JSON.stringify({ id: 'ICAO.removed' }))
expect(pushB).toHaveBeenCalledWith('remove', JSON.stringify({ id: 'ICAO.removed' }))
})
it('broadcastSubscriberSnapshots sends per-subscriber filtered snapshot', async () => {
clearCotStore()
await updateFromCot({ id: 'ICAO.in', lat: 37.5, lng: -122.5, type: 'a-f-A-C-F' })
await updateFromCot({ id: 'ICAO.out', lat: 40, lng: -100, type: 'a-f-A-C-F' })
const push = vi.fn()
registerSubscriber({
bbox: { west: -123, south: 37, east: -122, north: 38 },
layers: new Set(['air']),
push,
})
await broadcastSubscriberSnapshots({ ttlMs: 90_000, osintTtlMs: 30_000, takFilterBbox: false })
expect(push).toHaveBeenCalledWith('snapshot', expect.any(String))
const payload = JSON.parse(push.mock.calls[0][1])
expect(payload.entities.map(e => e.id)).toEqual(['ICAO.in'])
})
})
+55
View File
@@ -0,0 +1,55 @@
import { describe, it, expect, vi } from 'vitest'
import { createClusterIndex } from '../../app/utils/mapCluster.js'
import { clearFeatureMarkers, syncFeatureMarkers } from '../../app/utils/mapMarkerSync.js'
import { bboxFetchKey, tilesNearCenter } from '../../app/utils/alprViewport.js'
describe('mapCluster', () => {
it('loads once and queries by viewport', () => {
const index = createClusterIndex({ radius: 50, maxZoom: 14, minPoints: 2 })
const features = Array.from({ length: 20 }, (_, i) => ({
type: 'Feature',
geometry: { type: 'Point', coordinates: [-122.4 + i * 0.01, 37.7] },
properties: { id: i },
}))
index.load(features)
const zoomedOut = index.query({ west: -123, south: 37, east: -122, north: 38, zoom: 6 })
expect(zoomedOut.some(f => f.properties?.cluster)).toBe(true)
const zoomedIn = index.query({ west: -123, south: 37, east: -122, north: 38, zoom: 15 })
expect(zoomedIn).toHaveLength(20)
})
})
describe('mapMarkerSync', () => {
it('reuses markers by key', () => {
const layer = {
_layers: [],
addLayer(m) { this._layers.push(m) },
removeLayer(m) { this._layers = this._layers.filter(x => x !== m) },
}
const create = vi.fn(f => ({ id: f.properties.id }))
const update = vi.fn()
const opts = { keyFor: f => f.properties.id, create, update }
const pt = (id, lng, lat) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [lng, lat] }, properties: { id } })
syncFeatureMarkers(layer, [pt(1, -122, 37)], opts)
syncFeatureMarkers(layer, [pt(1, -121.9, 37.1)], opts)
expect(create).toHaveBeenCalledTimes(1)
expect(update).toHaveBeenCalledTimes(1)
clearFeatureMarkers(layer)
expect(layer._layers).toHaveLength(0)
})
})
describe('alprViewport', () => {
it('selects nearby tiles without scanning the world', () => {
const world = { south: -85, west: -180, north: 85, east: 180 }
expect(tilesNearCenter(world, 16)).toHaveLength(16)
expect(tilesNearCenter(world, 1)[0]).toEqual({ south: 0, west: 0, north: 0.5, east: 0.5 })
})
it('coarsens fetch keys when zoomed out', () => {
const bounds = { south: 37.01, west: -122.51, north: 37.99, east: -122.01, zoom: 6 }
expect(bboxFetchKey(bounds)).toBe(bboxFetchKey({ ...bounds, south: bounds.south + 0.05 }))
expect(bboxFetchKey({ ...bounds, zoom: 14 })).not.toBe(bboxFetchKey(bounds))
})
})
+29
View File
@@ -0,0 +1,29 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { clearSubscribers, registerSubscriber } from '../../../server/utils/cotSubscribers.js'
import {
isOpenSkyPollActive,
resetTrackingFeedForTests,
scheduleOpenSkyPollForTests,
} from '../../../server/utils/trackingFeed.js'
describe('trackingFeed', () => {
beforeEach(() => {
clearSubscribers()
resetTrackingFeedForTests()
})
it('does not start OpenSky poll without SSE subscribers', () => {
scheduleOpenSkyPollForTests()
expect(isOpenSkyPollActive()).toBe(false)
})
it('starts OpenSky poll when a subscriber is registered', () => {
registerSubscriber({
bbox: { west: -123, south: 37, east: -122, north: 38 },
layers: new Set(['air']),
push: vi.fn(),
})
scheduleOpenSkyPollForTests()
expect(isOpenSkyPollActive()).toBe(true)
})
})