major: kestrel is now a tak server #6

Merged
keligrubb merged 6 commits from atak-compat into main 2026-02-17 16:41:42 +00:00
12 changed files with 41 additions and 41 deletions
Showing only changes of commit a4996e7c91 - Show all commits

View File

@@ -36,7 +36,7 @@ Camera and geolocation in the browser require a **secure context** (HTTPS) when
npm run dev npm run dev
``` ```
3. On your phone, open **https://192.168.1.123:3000** (same IP you passed above). Accept the browser's untrusted certificate warning once (e.g. Advanced → Proceed). Then log in and use Share live; camera and location will work. 3. On your phone, open **https://192.168.1.123:3000** (same IP you passed above). Accept the browser's "untrusted certificate" warning once (e.g. Advanced → Proceed). Then log in and use Share live; camera and location will work.
Without the certs, `npm run dev` still runs over HTTP as before. Without the certs, `npm run dev` still runs over HTTP as before.
@@ -50,7 +50,7 @@ The **Share live** feature uses WebRTC for real-time video streaming from mobile
- **Mediasoup** server (runs automatically in the Nuxt process) - **Mediasoup** server (runs automatically in the Nuxt process)
- **mediasoup-client** (browser library, included automatically) - **mediasoup-client** (browser library, included automatically)
**Streaming from a phone on your LAN:** The server auto-detects your machine's LAN IP (from network interfaces) and uses it for WebRTC. Open **https://<your-LAN-IP>:3000** on both phone and laptop (same IP as for your dev cert). To override (e.g. Docker or multiple NICs), set `MEDIASOUP_ANNOUNCED_IP`. Ensure firewall allows UDP/TCP ports 4000049999 on the server. **Streaming from a phone on your LAN:** The server auto-detects your machine's LAN IP (from network interfaces) and uses it for WebRTC. Open **https://<your-LAN-IP>:3000** on both phone and laptop (same IP as for your dev cert). To override (e.g. Docker or multiple NICs), set `MEDIASOUP_ANNOUNCED_IP`. Ensure firewall allows UDP/TCP ports 40000-49999 on the server.
See [docs/live-streaming.md](docs/live-streaming.md) for setup and usage. See [docs/live-streaming.md](docs/live-streaming.md) for setup and usage.
@@ -60,12 +60,12 @@ KestrelOS can act as a **TAK Server** so ATAK and iTAK devices connect and share
## Scripts ## Scripts
- `npm run dev` development server - `npm run dev` - development server
- `npm run build` production build - `npm run build` - production build
- `npm run test` run tests - `npm run test` - run tests
- `npm run test:coverage` run tests with coverage (85% threshold) - `npm run test:coverage` - run tests with coverage (85% threshold)
- `npm run test:e2e` Playwright E2E tests - `npm run test:e2e` - Playwright E2E tests
- `npm run lint` ESLint (zero warnings) - `npm run lint` - ESLint (zero warnings)
## Documentation ## Documentation
@@ -76,7 +76,7 @@ Full docs are in the **[docs/](docs/README.md)** directory: [installation](docs/
- **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). Set `COT_TTL_MS=90000`, `COT_REQUIRE_AUTH=true`. 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 terminalcopy 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.
## Docker ## Docker
@@ -106,9 +106,9 @@ Health: `GET /health` (overview), `GET /health/live` (liveness), `GET /health/re
Merges to `main` trigger a semver release. Use one of these prefixes in your PR title to set the version bump: Merges to `main` trigger a semver release. Use one of these prefixes in your PR title to set the version bump:
- `major:` breaking changes - `major:` - breaking changes
- `minor:` new features - `minor:` - new features
- `patch:` bug fixes, docs (default if no prefix) - `patch:` - bug fixes, docs (default if no prefix)
Example: `minor: Add map layer toggle` Example: `minor: Add map layer toggle`

View File

@@ -130,7 +130,7 @@ function getPoiIcon(L, poi) {
}) })
} }
const LIVE_ICON_COLOR = '#22c9c9' /* kestrel-accent JS string for Leaflet SVG */ const LIVE_ICON_COLOR = '#22c9c9' /* kestrel-accent - JS string for Leaflet SVG */
function getLiveSessionIcon(L) { function getLiveSessionIcon(L) {
const html = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${LIVE_ICON_COLOR}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="2" fill="${LIVE_ICON_COLOR}"/></svg>` const html = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${LIVE_ICON_COLOR}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="2" fill="${LIVE_ICON_COLOR}"/></svg>`
return L.divIcon({ return L.divIcon({
@@ -141,7 +141,7 @@ function getLiveSessionIcon(L) {
}) })
} }
const COT_ICON_COLOR = '#f59e0b' /* amber ATAK/CoT devices */ const COT_ICON_COLOR = '#f59e0b' /* amber - ATAK/CoT devices */
function getCotEntityIcon(L) { function getCotEntityIcon(L) {
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>` 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>`
return L.divIcon({ return L.divIcon({

View File

@@ -47,7 +47,7 @@
Wrong host: server sees <strong>{{ failureReason.wrongHost.serverHostname }}</strong> but you opened this page at <strong>{{ failureReason.wrongHost.clientHostname }}</strong>. Use the same URL or set MEDIASOUP_ANNOUNCED_IP. Wrong host: server sees <strong>{{ failureReason.wrongHost.serverHostname }}</strong> but you opened this page at <strong>{{ failureReason.wrongHost.clientHostname }}</strong>. Use the same URL or set MEDIASOUP_ANNOUNCED_IP.
</p> </p>
<ul class="normal-case list-inside list-disc text-left text-kestrel-muted"> <ul class="normal-case list-inside list-disc text-left text-kestrel-muted">
<li><strong>Firewall:</strong> Open UDP/TCP 4000049999 on the server.</li> <li><strong>Firewall:</strong> Open UDP/TCP 40000-49999 on the server.</li>
<li><strong>Wrong host:</strong> Server must see the same address you use.</li> <li><strong>Wrong host:</strong> Server must see the same address you use.</li>
<li><strong>Restrictive NAT / cellular:</strong> TURN may be required.</li> <li><strong>Restrictive NAT / cellular:</strong> TURN may be required.</li>
</ul> </ul>
@@ -66,7 +66,7 @@
Wrong host: server sees <strong>{{ failureReason.wrongHost.serverHostname }}</strong> but you opened at <strong>{{ failureReason.wrongHost.clientHostname }}</strong>. Wrong host: server sees <strong>{{ failureReason.wrongHost.serverHostname }}</strong> but you opened at <strong>{{ failureReason.wrongHost.clientHostname }}</strong>.
</p> </p>
<ul class="normal-case list-inside list-disc text-left text-kestrel-muted"> <ul class="normal-case list-inside list-disc text-left text-kestrel-muted">
<li>Firewall: open ports 4000049999.</li> <li>Firewall: open ports 40000-49999.</li>
<li>Wrong host: use same URL or set MEDIASOUP_ANNOUNCED_IP.</li> <li>Wrong host: use same URL or set MEDIASOUP_ANNOUNCED_IP.</li>
<li>Restrictive NAT: TURN may be required.</li> <li>Restrictive NAT: TURN may be required.</li>
</ul> </ul>

View File

@@ -112,7 +112,7 @@
class="border-b border-kestrel-border" class="border-b border-kestrel-border"
> >
<td class="px-4 py-2 text-kestrel-text"> <td class="px-4 py-2 text-kestrel-text">
{{ p.label || '' }} {{ p.label || '-' }}
</td> </td>
<td class="px-4 py-2 text-kestrel-muted"> <td class="px-4 py-2 text-kestrel-muted">
{{ p.lat }} {{ p.lat }}

View File

@@ -42,7 +42,7 @@
</h3> </h3>
<div class="kestrel-card p-4"> <div class="kestrel-card p-4">
<p class="mb-3 text-sm text-kestrel-text"> <p class="mb-3 text-sm text-kestrel-text">
Scan this QR code with iTAK (or ATAK) to add this KestrelOS server. Youll be prompted for your KestrelOS username and password after scanning. Scan this QR code with iTAK (or ATAK) to add this KestrelOS server. You'll be prompted for your KestrelOS username and password after scanning.
</p> </p>
<div <div
v-if="takQrDataUrl" v-if="takQrDataUrl"

View File

@@ -39,7 +39,7 @@
Wrong host: server sees <strong>{{ webrtcFailureReason.wrongHost.serverHostname }}</strong> but you opened this page at <strong>{{ webrtcFailureReason.wrongHost.clientHostname }}</strong>. Use the same URL on phone and server, or set MEDIASOUP_ANNOUNCED_IP. Wrong host: server sees <strong>{{ webrtcFailureReason.wrongHost.serverHostname }}</strong> but you opened this page at <strong>{{ webrtcFailureReason.wrongHost.clientHostname }}</strong>. Use the same URL on phone and server, or set MEDIASOUP_ANNOUNCED_IP.
</p> </p>
<ul class="mt-2 list-inside list-disc space-y-0.5 text-kestrel-muted"> <ul class="mt-2 list-inside list-disc space-y-0.5 text-kestrel-muted">
<li><strong>Firewall:</strong> Open UDP/TCP ports 4000049999 on the server.</li> <li><strong>Firewall:</strong> Open UDP/TCP ports 40000-49999 on the server.</li>
<li><strong>Wrong host:</strong> Server must see the same address you use (see above or open /api/live/debug-request-host).</li> <li><strong>Wrong host:</strong> Server must see the same address you use (see above or open /api/live/debug-request-host).</li>
<li><strong>Restrictive NAT / cellular:</strong> A TURN server may be required (future enhancement).</li> <li><strong>Restrictive NAT / cellular:</strong> A TURN server may be required (future enhancement).</li>
</ul> </ul>
@@ -68,7 +68,7 @@
v-if="sharing" v-if="sharing"
class="absolute bottom-2 left-2 rounded bg-black/70 px-2 py-1 text-xs text-green-400" class="absolute bottom-2 left-2 rounded bg-black/70 px-2 py-1 text-xs text-green-400"
> >
● Live you appear on the map ● Live - you appear on the map
</div> </div>
</div> </div>
@@ -273,7 +273,7 @@ async function startSharing() {
return return
} }
// 5. Get location (continuous) also requires HTTPS on mobile Safari // 5. Get location (continuous) - also requires HTTPS on mobile Safari
if (!navigator.geolocation) { if (!navigator.geolocation) {
setError('Geolocation not supported in this browser.') setError('Geolocation not supported in this browser.')
cleanup() cleanup()

View File

@@ -4,8 +4,8 @@ Tactical Operations Center (TOC) for OSINT feeds: map view, cameras/devices, liv
## Quick Start ## Quick Start
1. [Installation](installation.md) npm, Docker, or Helm 1. [Installation](installation.md) - npm, Docker, or Helm
2. [Authentication](auth.md) First login (bootstrap admin or OIDC) 2. [Authentication](auth.md) - First login (bootstrap admin or OIDC)
3. [Map and cameras](map-and-cameras.md) Add devices and view streams 3. [Map and cameras](map-and-cameras.md) - Add devices and view streams
4. [ATAK and iTAK](atak-itak.md) Connect TAK clients (port 8089) 4. [ATAK and iTAK](atak-itak.md) - Connect TAK clients (port 8089)
5. [Share live](live-streaming.md) Stream from mobile device (HTTPS required) 5. [Share live](live-streaming.md) - Stream from mobile device (HTTPS required)

View File

@@ -21,11 +21,11 @@ KestrelOS acts as a **TAK Server**. ATAK (Android) and iTAK (iOS) connect on **p
## iTAK (iOS) ## iTAK (iOS)
**Option A QR code (easiest):** **Option A - QR code (easiest):**
1. KestrelOS **Settings****TAK Server** → Scan QR with iTAK 1. KestrelOS **Settings****TAK Server** → Scan QR with iTAK
2. Enter username/password when prompted 2. Enter username/password when prompted
**Option B Manual:** **Option B - Manual:**
1. **Settings****Network** → Add **TAK Server** 1. **Settings****Network** → Add **TAK Server**
2. Set **Host**, **Port** (`8089`), enable SSL if needed 2. Set **Host**, **Port** (`8089`), enable SSL if needed
3. Enable **Use Authentication**, enter username/password 3. Enable **Use Authentication**, enter username/password

View File

@@ -32,8 +32,8 @@ OIDC users don't have a KestrelOS password. To use ATAK/iTAK:
## Roles ## Roles
- **Admin** Manage users, edit POIs, add/edit devices (API) - **Admin** - Manage users, edit POIs, add/edit devices (API)
- **Leader** Edit POIs, add/edit devices (API) - **Leader** - Edit POIs, add/edit devices (API)
- **Member** View map/cameras/POIs, use Share live - **Member** - View map/cameras/POIs, use Share live
Only admins can change roles (Members page). Only admins can change roles (Members page).

View File

@@ -15,7 +15,7 @@ Stream your phone's camera and location to KestrelOS. Appears as a **live sessio
- **HTTPS** (browsers require secure context for camera/geolocation) - **HTTPS** (browsers require secure context for camera/geolocation)
- **Camera and location permissions** - **Camera and location permissions**
- **WebRTC ports:** UDP/TCP `4000049999` open on server - **WebRTC ports:** UDP/TCP `40000-49999` open on server
## Local Development ## Local Development
@@ -40,5 +40,5 @@ npm run dev
|-------|-----| |-------|-----|
| "HTTPS required" | Use `https://` (not `http://`) | | "HTTPS required" | Use `https://` (not `http://`) |
| "Media devices not available" | Ensure HTTPS and browser permissions | | "Media devices not available" | Ensure HTTPS and browser permissions |
| "WebRTC: failed" / "Wrong host" | Set `MEDIASOUP_ANNOUNCED_IP`, open firewall ports `4000049999` | | "WebRTC: failed" / "Wrong host" | Set `MEDIASOUP_ANNOUNCED_IP`, open firewall ports `40000-49999` |
| Stream not visible | Check server reachability and firewall | | Stream not visible | Check server reachability and firewall |

View File

@@ -4,16 +4,16 @@ 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
- **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)
## Cameras ## Cameras
A **camera** is either: A **camera** is either:
1. A **device** Fixed feed with stream URL 1. A **device** - Fixed feed with stream URL
2. A **live session** Mobile device streaming via Share live 2. A **live session** - Mobile device streaming via Share live
View via map markers or **Cameras** page (sidebar). View via map markers or **Cameras** page (sidebar).

View File

@@ -202,14 +202,14 @@ function startCotServers() {
serverState.tlsServer = createTlsServer(tlsOpts, socket => setupSocket(socket, true)) serverState.tlsServer = createTlsServer(tlsOpts, socket => setupSocket(socket, true))
serverState.tlsServer.on('error', err => console.error('[cot] TLS server error:', err?.message)) serverState.tlsServer.on('error', err => console.error('[cot] TLS server error:', err?.message))
serverState.tlsServer.listen(port, '0.0.0.0', () => { serverState.tlsServer.listen(port, '0.0.0.0', () => {
console.log('[cot] CoT server listening on 0.0.0.0:' + port + ' (TLS) use this port in ATAK/iTAK and enable SSL') console.log('[cot] CoT server listening on 0.0.0.0:' + port + ' (TLS) - use this port in ATAK/iTAK and enable SSL')
}) })
} }
else { else {
serverState.tcpServer = createTcpServer(socket => setupSocket(socket, false)) serverState.tcpServer = createTcpServer(socket => setupSocket(socket, false))
serverState.tcpServer.on('error', err => console.error('[cot] TCP server error:', err?.message)) serverState.tcpServer.on('error', err => console.error('[cot] TCP server error:', err?.message))
serverState.tcpServer.listen(port, '0.0.0.0', () => { serverState.tcpServer.listen(port, '0.0.0.0', () => {
console.log('[cot] CoT server listening on 0.0.0.0:' + port + ' (plain TCP) use this port in ATAK/iTAK with SSL disabled') console.log('[cot] CoT server listening on 0.0.0.0:' + port + ' (plain TCP) - use this port in ATAK/iTAK with SSL disabled')
}) })
} }
} }