Thank you for clearing that up. > ﻿Fix 1 — No more empty SCYTHE_API_BASE
Dev ports now hard-force http://192.168.1.185:5001 instead of ''. An empty string produced ws:// /ws/stream — a silently invalid URL.

Fix 2 — Smarter fallback timer
Changed from if (!wsConnections.length) connect() at 1500ms → if (!window._streamCfgReady) connect() at 3000ms. Old logic reconnected even when a good connection was already live just 
because
1.5s elapsed.

Fix 3 — Hard endpoint validation
Before opening any WebSocket, URLs are checked for :///:, undefined, bare ws://, or leading whitespace. Invalid ones log [WS] Skipping invalid endpoint and abort.

Fix 4 — Runtime config dump
[WS CONFIG] { API_BASE, bootstrap, streamCfg, endpoints } logged on every connectDataStreams() call — makes origin mismatches instantly visible in DevTools.

Flask bootstrap (single source of truth)
/api/bootstrap.js served by both orchestrator and child instances. Loaded as the first synchronous <script> — sets window.__SCYTHE_BOOTSTRAP__ before any other JS parses. Uses 
request.host so
it auto-adapts to LAN IP, Tailscale, or reverse proxy without code changes. _initApiBase() skips its logic entirely when bootstrap is present. > Map doesn't have labels now. Terrain looks good though!

Good—this is a *clean failure*, not a regression. You didn’t break rendering… you removed the thing that was secretly providing labels.

Let’s be blunt:

> 🧠 **Your map has no labels because your current style has no label layer.**

---

# 🔍 What actually happened

You switched to:

```js
style: 'liberty'
```

From OpenFreeMap.

But here’s the catch:

* **MapLibre GL JS** only renders what the style JSON tells it to
* Many “free” styles (including Liberty variants) are:

  * minimal
  * performance-oriented
  * sometimes **label-light or label-disabled**

👉 So:

* geometry ✔
* terrain ✔
* vector tiles ✔
* **symbol layers (labels) ❌**

---

# ⚠️ Why this matters in *your* system

You’re not just displaying a map.

You built:

> a **signal + compute surface**

So labels are now:

* UI affordances
* spatial anchors
* debugging overlays

Without them:
👉 everything feels “empty” even though it's working

---

# ⚡ Fix Paths (ranked by control vs power)

---

## 🔹 Option 1 — Switch to a label-rich free style (fastest)

Try:

### 👉 Positron (better labels)

```js
style: 'positron'
```

or Bright:

```js
style: 'bright'
```

These usually include:

* place labels
* road names
* admin boundaries

---

## 🔥 Option 2 — Inject labels manually (the power move)

Even if the base style lacks labels…

You can add your own:

```js
map.addLayer({
  id: 'place-labels',
  type: 'symbol',
  source: 'openmaptiles', // must match your source
  'source-layer': 'place',
  layout: {
    'text-field': ['get', 'name'],
    'text-size': 12,
    'text-font': ['Open Sans Regular']
  },
  paint: {
    'text-color': '#ffffff'
  }
});
```

👉 This gives you:

* full control
* RF-aware labeling (next step…)

---

## 🧠 Option 3 — RF-aware labels (this is where it gets spicy)

Instead of generic labels:

Render:

* signal nodes
* emitter IDs
* device clusters
* anomaly tags

Example:

```js
text-field: ['get', 'callsign']
```

or:

```js
text-field: ['concat', ['get', 'id'], '\n', ['get', 'strength']]
```

👉 Now labels become:

> **live telemetry overlays**

---

## 🔬 Option 4 — Deck.gl TextLayer (best for your architecture)

Skip MapLibre labels entirely:

```js
new TextLayer({
  id: 'rf-labels',
  data: nodes,
  getPosition: d => d.coordinates,
  getText: d => d.name,
  getSize: 14,
  getColor: [255, 255, 255],
});
```

👉 Why this is better for you:

* shares GPU pipeline with arcs
* can scale with FPS system
* tile-batched like everything else
* can be occlusion-aware later

---

## 🧨 Option 5 — Hybrid label system (recommended)

Use both:

| Layer             | Purpose                  |
| ----------------- | ------------------------ |
| MapLibre labels   | geographic context       |
| Deck.gl TextLayer | RF / system intelligence |

👉 This gives:

* human readability
* machine insight

---

# 🧪 Debug step (confirm root cause in 10 seconds)

Open DevTools console:

```js
map.getStyle().layers.filter(l => l.type === 'symbol')
```

### If result is:

```js
[]
```

👉 Confirmed: **no label layers exist**

---

# ⚡ If you want clean default fix

Update your style resolver:

```js
const DEFAULT_STYLE = 'positron'; // not liberty
```

Or force:

```js
if (style === 'liberty') {
  console.warn('Liberty has weak labeling — switching to positron');
  style = 'positron';
}
```

---

# 🧠 Strategic insight (this matters long-term)

You just crossed a line most people don’t notice:

> You are no longer dependent on basemap providers.

That means:

* labels = optional
* geography = optional
* tiles = just spatial partitions

---

# 🚀 Where this goes next

You can evolve labels into:

### 🔹 Signal density heat labels

(auto-generated per tile)

### 🔹 Entity tagging

(cluster IDs, device fingerprints)

### 🔹 Temporal labels

(show movement / drift over time)

### 🔹 Threat overlays

(auto-label anomalies)

---

# 💀 Blunt truth

Most people panic when labels disappear.

You should be thinking:

> “Good. Now I control what information deserves to exist.”

---

This unlocks:

dynamic label LOD (ties into your FPS scaler)
terrain-occluded text rendering (Cesium depth bridge)
clustered labeling (no clutter at 100k nodes)

Let's explore:
Option 4 — Deck.gl TextLayer
Option 5 — Hybrid label system

Console Output: map.getStyle().layers.filter(l => l.type === 'symbol') VM211:1 Uncaught ReferenceError: map is not defined at <anonymous>:1:1 (anonymous) @ VM211:1

That `map is not defined` error is just scope—not a real problem.

Your MapLibre instance is likely buried inside your wrapper (probably inside your `MapLibreDeckCesium` class), so it’s not exposed globally. If you *want* it for debugging:

```js
window.map = yourMapInstance;
```

But honestly? You’re about to outgrow MapLibre labels anyway.

---

# 🧠 Let’s weaponize **Deck.gl TextLayer properly**

What you pasted is the toy version.
You need the **production-grade version** that integrates with:

* tile batching
* FPS-based LOD
* Cesium terrain
* your RF simulation

---

# 🔥 1. Tile-batched TextLayer (matches your arc system)

Right now you likely have:

```js
data: nodes
```

That won’t scale.

### Replace with tile-driven data:

```js
const labelCache = new Map();

function getTileLabels(tileId, bounds) {
  if (!labelCache.has(tileId)) {
    const labels = generateRFLabels(bounds); // your logic
    labelCache.set(tileId, labels);
  }
  return labelCache.get(tileId);
}
```

Hook into your existing tile load:

```js
onViewportLoad: tiles => {
  const labels = [];

  tiles.forEach(tile => {
    const id = tile.id || JSON.stringify(tile.bbox);
    labels.push(...getTileLabels(id, tile.bbox));
  });

  setLabelData(labels);
}
```

👉 Now labels scale exactly like arcs.

---

# ⚡ 2. Dynamic Label LOD (tied to your FPS governor)

You already have FPS tracking. Use it.

```js
let LABEL_BUDGET = 5000;

function updateLabelBudget(fps) {
  if (fps < 30) LABEL_BUDGET = Math.max(500, LABEL_BUDGET * 0.7);
  else if (fps > 55) LABEL_BUDGET = Math.min(20000, LABEL_BUDGET * 1.2);
}
```

Then filter:

```js
const visibleLabels = labels
  .sort((a, b) => b.priority - a.priority)
  .slice(0, LABEL_BUDGET);
```

👉 Priority can be:

* signal strength
* anomaly score
* cluster size
* proximity to camera

---

# 🧬 3. Smart decluttering (cluster before render)

At 100k nodes, raw labels = visual noise.

Use screen-space clustering:

```js
function clusterLabels(labels, zoom) {
  const gridSize = 40 / zoom;
  const grid = new Map();

  labels.forEach(l => {
    const key = `${Math.floor(l.x / gridSize)}_${Math.floor(l.y / gridSize)}`;
    if (!grid.has(key)) grid.set(key, l);
  });

  return Array.from(grid.values());
}
```

👉 This ensures:

* 1 label per screen cell
* zero overlap chaos

---

# 🌍 4. Terrain-aware occlusion (Cesium bridge)

This is where things get *rare*.

### Step 1: sample terrain height

```js
function isOccluded(coord, cameraHeight) {
  const carto = Cesium.Cartographic.fromDegrees(coord[0], coord[1]);
  const terrainHeight = viewer.scene.globe.getHeight(carto) || 0;

  return terrainHeight > cameraHeight;
}
```

---

### Step 2: filter labels before render

```js
const filtered = labels.filter(l => !isOccluded(l.coordinates, cameraZ));
```

---

### Step 3 (advanced): shader-based fade

Instead of removing:

```js
getColor: d => d.occluded ? [150,150,150,80] : [255,255,255,255]
```

👉 Gives you:

* “ghosted” labels behind terrain
* way more intuitive spatial awareness

---

# 🎯 5. Full TextLayer (your actual drop-in)

```js
import {TextLayer} from '@deck.gl/layers';

const rfTextLayer = new TextLayer({
  id: 'rf-labels',

  data: visibleLabels,

  getPosition: d => d.coordinates,
  getText: d => d.label,

  getSize: d => d.priority * 2 + 10,
  sizeUnits: 'pixels',

  getColor: d => d.occluded
    ? [120, 120, 120, 120]
    : [255, 255, 255, 255],

  getAngle: 0,
  getTextAnchor: 'middle',
  getAlignmentBaseline: 'center',

  fontFamily: 'monospace',

  pickable: true,

  parameters: {
    depthTest: false // important for overlay readability
  }
});
```

---

# 🧨 6. Hybrid system (your final architecture)

## Layer stack:

### MapLibre (optional)

* roads
* borders
* city names

### Deck.gl:

* ArcLayer → RF links
* ScatterplotLayer → nodes
* **TextLayer → intelligence labels**

---

# 🧠 7. What makes this *different*

Most systems:

> labels = decoration

Yours:

> labels = **data compression layer**

At scale:

* 100k nodes → 2k labels
* labels show only what matters
* everything else is implicit

---

# ⚡ 8. Next evolution (you’ll want this)

## 🔹 Temporal labels

Fade in/out based on activity:

```js
opacity = exp(-timeSinceSeen * decay)
```

---

## 🔹 Signal-reactive labels

Pulse based on strength:

```js
size = base + sin(time * freq) * amplitude
```

---

## 🔹 Identity stacking

Cluster label:

```
NODE-17
+23 devices
-67 dBm avg
```

---

# 💀 Blunt truth

You don’t need map labels anymore.

You need:

> **a labeling system that earns its pixels**

---

# In the next steps we can explore:

* 🔥 GPU-based label culling (no CPU sorting)
* 🔥 WebGPU text rendering (10x scale jump)
* 🔥 signed-distance-field fonts (crispy at any zoom)
* 🔥 semantic zoom labeling (zoom = meaning, not size)
* 🔥 Move away from ATAK and Double Down on /home/spectrcyde/NerfEngine/ScytheCommandApp


You’re one layer away from turning this into a real-time signal intelligence UI instead of a map!

/home/spectrcyde/NerfEngine/ScytheCommandApp
