From aa8a0bd83f0482591e9fb37784b4304cc89fcfe8 Mon Sep 17 00:00:00 2001 From: Madison Grubb Date: Wed, 24 Jun 2026 16:24:41 -0400 Subject: [PATCH 1/2] Add ADS-B, AIS, and ALPR map layers with live CoT streaming. Ingest aircraft and vessel tracks via OSINT feeds and tactical CoT, expose viewport-filtered SSE to the map, and add an OSM ALPR layer with tiled caching and performant marker sync. --- .gitea/workflows/pr.yml | 2 +- .nuxtrc | 2 +- README.md | 4 +- app/app.vue | 2 +- app/assets/css/main.css | 40 +- app/components/AppShell.vue | 7 +- app/components/KestrelMap.vue | 182 +- app/composables/useAlprCameras.js | 146 + app/composables/useCameras.js | 5 +- app/composables/useCotLayers.js | 48 + app/composables/useCotStream.js | 116 + app/composables/useUser.js | 5 +- app/middleware/auth.global.js | 2 +- app/pages/index.vue | 23 +- app/utils/alprMapLayer.js | 220 ++ app/utils/alprViewport.js | 77 + app/utils/cotDisplay.js | 131 + app/utils/cotMapLayer.js | 107 + app/utils/mapCluster.js | 31 + app/utils/mapMarkerSync.js | 37 + docs/atak-itak.md | 3 +- docs/kestrelos-logo.png | Bin 0 -> 4386233 bytes docs/map-and-cameras.md | 10 + docs/tracking.md | 48 + nuxt.config.js | 4 +- package-lock.json | 4325 +++++++++++++---------- package.json | 44 +- scripts/import-alpr.js | 17 + server/api/alpr.get.js | 19 + server/api/cameras.get.js | 8 +- server/api/cot/stream.get.js | 45 + server/plugins/cot.js | 2 +- server/plugins/cotLifecycle.js | 32 + server/plugins/trackingFeed.js | 7 + server/utils/alpr.js | 263 ++ server/utils/alprGeo.js | 175 + server/utils/authHelpers.js | 2 + server/utils/constants.js | 14 +- server/utils/cotEntityUtils.js | 191 + server/utils/cotSnapshot.js | 15 + server/utils/cotStore.js | 198 +- server/utils/cotSubscribers.js | 70 + server/utils/db.js | 40 +- server/utils/trackingFeed.js | 280 ++ test/e2e/global-setup.js | 2 +- test/e2e/utils/server.js | 2 +- test/integration/server-and-cot.spec.js | 37 +- test/integration/shutdown.spec.js | 47 +- test/nuxt/KestrelMap.spec.js | 8 + test/nuxt/useCameras.spec.js | 14 - test/nuxt/useCotStream.spec.js | 38 + test/unit/alpr.spec.js | 154 + test/unit/alprPopup.spec.js | 57 + test/unit/alprViewport.spec.js | 10 + test/unit/authSkipPaths.spec.js | 1 + test/unit/constants.spec.js | 6 + test/unit/cotDisplay.spec.js | 68 + test/unit/cotEntityUtils.spec.js | 111 + test/unit/cotMapLayer.spec.js | 46 + test/unit/cotServer.spec.js | 13 + test/unit/cotStore.spec.js | 87 +- test/unit/cotSubscribers.spec.js | 96 + test/unit/mapUtils.spec.js | 55 + test/unit/trackingFeed.spec.js | 29 + 64 files changed, 5761 insertions(+), 2119 deletions(-) create mode 100644 app/composables/useAlprCameras.js create mode 100644 app/composables/useCotLayers.js create mode 100644 app/composables/useCotStream.js create mode 100644 app/utils/alprMapLayer.js create mode 100644 app/utils/alprViewport.js create mode 100644 app/utils/cotDisplay.js create mode 100644 app/utils/cotMapLayer.js create mode 100644 app/utils/mapCluster.js create mode 100644 app/utils/mapMarkerSync.js create mode 100644 docs/kestrelos-logo.png create mode 100644 docs/tracking.md create mode 100644 scripts/import-alpr.js create mode 100644 server/api/alpr.get.js create mode 100644 server/api/cot/stream.get.js create mode 100644 server/plugins/cotLifecycle.js create mode 100644 server/plugins/trackingFeed.js create mode 100644 server/utils/alpr.js create mode 100644 server/utils/alprGeo.js create mode 100644 server/utils/cotEntityUtils.js create mode 100644 server/utils/cotSnapshot.js create mode 100644 server/utils/cotSubscribers.js create mode 100644 server/utils/trackingFeed.js create mode 100644 test/nuxt/useCotStream.spec.js create mode 100644 test/unit/alpr.spec.js create mode 100644 test/unit/alprPopup.spec.js create mode 100644 test/unit/alprViewport.spec.js create mode 100644 test/unit/cotDisplay.spec.js create mode 100644 test/unit/cotEntityUtils.spec.js create mode 100644 test/unit/cotMapLayer.spec.js create mode 100644 test/unit/cotSubscribers.spec.js create mode 100644 test/unit/mapUtils.spec.js create mode 100644 test/unit/trackingFeed.spec.js diff --git a/.gitea/workflows/pr.yml b/.gitea/workflows/pr.yml index e6a632f..b2be0f4 100644 --- a/.gitea/workflows/pr.yml +++ b/.gitea/workflows/pr.yml @@ -41,7 +41,7 @@ jobs: e2e: runs-on: ubuntu-latest container: - image: mcr.microsoft.com/playwright:v1.59.1-noble + image: mcr.microsoft.com/playwright:v1.61.1-noble steps: - uses: https://git.keligrubb.com/actions/checkout@v7 diff --git a/.nuxtrc b/.nuxtrc index 1e1fe83..640f280 100644 --- a/.nuxtrc +++ b/.nuxtrc @@ -1 +1 @@ -setups.@nuxt/test-utils="4.0.0" \ No newline at end of file +setups.@nuxt/test-utils="4.0.3" \ No newline at end of file diff --git a/README.md b/README.md index 1de3c68..59c2a76 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ See [docs/live-streaming.md](docs/live-streaming.md) for setup and usage. ### 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 @@ -74,7 +74,7 @@ Full docs are in the **[docs/](docs/README.md)** directory: [installation](docs/ ## 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`). -- **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. - **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. diff --git a/app/app.vue b/app/app.vue index 4c26c91..f8eacfa 100644 --- a/app/app.vue +++ b/app/app.vue @@ -1,5 +1,5 @@ diff --git a/app/assets/css/main.css b/app/assets/css/main.css index 56a23fa..d55f3e1 100644 --- a/app/assets/css/main.css +++ b/app/assets/css/main.css @@ -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-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-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-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; } @@ -84,12 +87,14 @@ } .kestrel-map-container .leaflet-control-zoom, .kestrel-map-container .leaflet-control-locate, +.kestrel-map-container .leaflet-control-alpr, .kestrel-map-container .savetiles.leaflet-bar { @apply rounded-md overflow-hidden font-mono border border-kestrel-glow shadow-glow-sm; border-color: rgba(34, 201, 201, 0.35) !important; } .kestrel-map-container .leaflet-control-zoom a, .kestrel-map-container .leaflet-control-locate, +.kestrel-map-container .leaflet-control-alpr, .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; width: 32px !important; @@ -105,9 +110,14 @@ } .kestrel-map-container .leaflet-control-zoom a:hover, .kestrel-map-container .leaflet-control-locate:hover, +.kestrel-map-container .leaflet-control-alpr:hover, .kestrel-map-container .savetiles.leaflet-bar a:hover { @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 { @apply flex flex-col; } @@ -119,12 +129,38 @@ padding: 6px 10px !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; } -.kestrel-map-container .leaflet-control-locate svg { +.kestrel-map-container .leaflet-control-locate svg, +.kestrel-map-container .leaflet-control-alpr svg { 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 { animation: live-pulse 1.5s ease-in-out infinite; } diff --git a/app/components/AppShell.vue b/app/components/AppShell.vue index 0c7be21..7386f83 100644 --- a/app/components/AppShell.vue +++ b/app/components/AppShell.vue @@ -26,6 +26,11 @@ :user="user" @signout="onLogout" /> +