betterwithage commited on
Commit
9ad788e
·
verified ·
1 Parent(s): 45f8083

chore(sync): mirror static/3d kit + Dockerfile-COPY'd web/*.html to Space (hf-sync)

Browse files

Automated 3D/web-page asset sync from szl-holdings/a11oy main via hf-sync.
web/*.html set parsed live from the Dockerfile COPY directives.
Added/updated: 53 file(s)
Deleted (gone from GitHub main): (none)

Keeps static/3d/** and every Dockerfile-COPY'd web/<page>.html identical
to GitHub main so an HF factory rebuild never drops a GitHub edit (closes
the chronic hf-module-drift red for the 3D kit + baked web pages).

static/3d/surfaces/counter-uas.js CHANGED
@@ -1,77 +1,593 @@
1
  // SPDX-License-Identifier: Apache-2.0
2
  // © 2026 Lutar, Stephen P. Jr. — SZL Holdings · Doctrine v11
3
  //
4
- // surfaces/counter-uas.js — Counter-UAS surface STUB (Dev0 foundation; Dev4 fills this).
5
  //
6
- // Leader/technique to model: Anduril Lattice + CesiumJS
7
- // Primary live endpoint (doctrine: WIRE TO LIVE DATA, never fabricate): /counter-uas/evaluate
 
8
  //
9
- // CONTRACT every surface module is an ES module default-exporting:
10
- // { id, title, endpoints[], mount(ctx), unmount() }
11
- // ctx (provided by the /holographic shell via szl3d):
12
- // ctx.stage Stage from szl3d_boot.boot() scene, camera, renderer, THREE,
13
- // start, stop, onFrame, setBloom, backend, ...
14
- // ctx.container the surface's DOM panel (for overlays/badges/HUD)
15
- // ctx.live the szl3d_live module (ctx.live.poll(endpoint, ms, onData, {badge}))
16
- // ctx.label the szl3d_label module (ctx.label.chip / .billboard / .legend)
17
- // ctx.THREE the three module
18
- // mount() attaches scene objects + starts polling. unmount() stops polls and removes
19
- // DOM it added (the shell disposes the stage).
20
  //
21
- // This STUB renders an honest "awaiting Dev4" placeholder + a LIVE badge wired to the
22
- // real endpoint, proving the toolkit primitives work end-to-end before the real viz lands.
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  const ID = "counter-uas";
25
- const TITLE = "Counter-UAS";
26
- const ENDPOINT = "/counter-uas/evaluate";
27
- const ACCENT = 0xff6b6b;
28
 
29
- let _stage = null, _handle = null, _overlay = null, _obj = null;
 
 
 
 
 
 
 
30
 
31
- function mount(ctx) {
32
- _stage = ctx.stage;
33
- const THREE = ctx.THREE;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
- const geo = new THREE.IcosahedronGeometry(2.4, 1);
36
- const mat = new THREE.MeshStandardMaterial({
37
- color: ACCENT, emissive: ACCENT, emissiveIntensity: 0.35,
38
- metalness: 0.4, roughness: 0.35, wireframe: true,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  });
40
- _obj = new THREE.Mesh(geo, mat);
41
- _stage.scene.add(_obj);
42
- _stage.onFrame(() => { if (_obj) { _obj.rotation.y += 0.004; _obj.rotation.x += 0.0015; } });
43
 
44
- try {
45
- const bb = ctx.label.billboard(THREE, "STRUCTURAL-ONLY", { text: TITLE, scale: 0.7, position: [0, 3.4, 0] });
46
- _stage.scene.add(bb);
47
- } catch (_) {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
 
 
49
  _overlay = document.createElement("div");
50
  _overlay.className = "szl3d-surface-overlay";
51
- Object.assign(_overlay.style, { position: "absolute", left: "14px", top: "14px", zIndex: "5",
52
- display: "flex", flexDirection: "column", gap: "8px", maxWidth: "min(92%,420px)" });
53
- const h = document.createElement("div");
54
- h.style.cssText = "font:600 13px ui-sans-serif,system-ui;color:#eef3f6;letter-spacing:.4px";
55
- h.textContent = TITLE + " · awaiting Dev4";
56
- const badge = ctx.live.createBadge();
57
- const legend = ctx.label.legend();
58
- legend.style.opacity = "0.85";
59
- _overlay.appendChild(h); _overlay.appendChild(badge.el); _overlay.appendChild(legend);
60
- (ctx.container || document.body).appendChild(_overlay);
61
-
62
- _handle = ctx.live.poll(ENDPOINT, 5000, (json, meta) => {
63
- // Dev4: replace with the real viz update. Honesty label is in meta.label.
64
- if (meta.label && _obj) { /* map live values to geometry here */ }
65
- }, { badge });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
  return { id: ID, started: true };
68
  }
69
 
70
  function unmount() {
71
- try { if (_handle) _handle.stop(); } catch (_) {}
 
 
 
 
72
  try { if (_overlay && _overlay.parentNode) _overlay.parentNode.removeChild(_overlay); } catch (_) {}
73
- try { if (_obj && _stage) _stage.scene.remove(_obj); } catch (_) {}
74
- _handle = null; _overlay = null; _obj = null; _stage = null;
 
 
75
  }
76
 
77
- export default { id: ID, title: TITLE, endpoints: [ENDPOINT], mount, unmount };
 
1
  // SPDX-License-Identifier: Apache-2.0
2
  // © 2026 Lutar, Stephen P. Jr. — SZL Holdings · Doctrine v11
3
  //
4
+ // surfaces/counter-uas.js — COUNTER-UAS / killinchu holographic surface (Dev4).
5
  //
6
+ // Leader/technique modeled on: Anduril Lattice + CesiumJS + Dedrone.
7
+ // 3D globe + track entities + restricted-airspace SDF volumes + radar sweep cone
8
+ // + signed-verdict beam. (We are *modeled on* the leader; we never claim to BE it.)
9
  //
10
+ // VENDORING NOTE (Dev0 contract §6): the contract offered an escape hatch — full
11
+ // CesiumJS@1.123 OR a three.js globe (textured sphere + lat/long plotting). We took
12
+ // the three.js globe: it reuses the already-vendored three.js r170 (0 new MB, 0 CDN),
13
+ // keeps this PR lean, and is sufficient for track plotting + SDF volumes + sweep cone.
14
+ // See VENDOR_MANIFEST.md §Dev4 for the documented decision.
 
 
 
 
 
 
15
  //
16
+ // HONESTY (killinchu charter / JIATF-401 C4 crosswalk): killinchu SENSES & EVIDENCES
17
+ // it does NOT defeat (no jamming, no GNSS spoofing, no takeover, no kinetic). Every
18
+ // visual here is detect / track / classify / evidence + the signed verdict ONLY. There
19
+ // is NO kinetic effect, NO jam beam, NO interceptor-to-target engagement geometry. The
20
+ // "verdict beam" is the SIGNED DECISION locking onto a track — an evidence act, not a
21
+ // weapon. Λ = Conjecture 1 (advisory, NOT proven trust). All labels are read from the
22
+ // live JSON; values trace to real killinchu endpoints via ctx.live.poll, never hardcoded.
23
+ //
24
+ // Live data (same-origin bridge -> killinchu Space, see szl_counter_uas_proxy.py):
25
+ // /api/a11oy/v1/counter-uas/evaluate live Λ decision + REAL ECDSA-P256 DSSE sig
26
+ // /api/a11oy/v1/counter-uas/telemetry friendly fleet + threat tracks (honest data_kind)
27
+ // /api/a11oy/v1/counter-uas/cued-tracks externally-cued threat tracks (cuing_sensor)
28
+ // /api/a11oy/v1/counter-uas/air-picture real cooperative ADS-B (airplanes.live)
29
+ // /api/a11oy/v1/counter-uas/gates 13-axis Λ-gate spec
30
+ // /static/3d/surfaces/data/drones_db.json 53 verified drone fingerprints (vendored)
31
 
32
  const ID = "counter-uas";
33
+ const TITLE = "Counter-UAS · killinchu";
 
 
34
 
35
+ const EP = {
36
+ evaluate: "/api/a11oy/v1/counter-uas/evaluate",
37
+ telemetry: "/api/a11oy/v1/counter-uas/telemetry",
38
+ cued: "/api/a11oy/v1/counter-uas/cued-tracks",
39
+ airpicture: "/api/a11oy/v1/counter-uas/air-picture",
40
+ gates: "/api/a11oy/v1/counter-uas/gates",
41
+ dronesDb: "/static/3d/surfaces/data/drones_db.json",
42
+ };
43
 
44
+ // Globe radius (world units) + altitude scale (alt_m -> world units above surface).
45
+ const R = 3.0;
46
+ const ALT_SCALE = 1.0 / 18000; // ~18 km ceiling maps to ~+1 globe radius of lift
47
+ const ALT_MAX_LIFT = 0.9;
48
+
49
+ // Classification → color (benign / unknown / hostile / friendly). NEVER a "defeat" color.
50
+ const CLS = {
51
+ friendly: 0x39d3c4, // teal — own fleet
52
+ benign: 0x2fd07a, // green — cooperative / allied
53
+ unknown: 0xe8c074, // amber — uncharacterized
54
+ suspect: 0xffa54a, // orange — Λ below threshold
55
+ hostile: 0xff6b6b, // red — Λ below floor (still only SENSED, never engaged)
56
+ };
57
+
58
+ let _ctx = null, _stage = null, THREE = null;
59
+ let _group = null, _globe = null, _handles = [], _frameOff = null, _overlay = null;
60
+ let _disposables = [];
61
+
62
+ // Live caches (honest: empty until a real fetch lands).
63
+ let _state = {
64
+ evaluate: null, telemetry: null, cued: null, airpicture: null, gates: null, db: null,
65
+ meta: {}, dbCount: 0,
66
+ };
67
+
68
+ // ── geo helpers ────────────────────────────────────────────────────────────
69
+ function llToVec(lat, lon, lift) {
70
+ const phi = (90 - lat) * Math.PI / 180;
71
+ const theta = (lon + 180) * Math.PI / 180;
72
+ const r = R + (lift || 0);
73
+ return new THREE.Vector3(
74
+ -r * Math.sin(phi) * Math.cos(theta),
75
+ r * Math.cos(phi),
76
+ r * Math.sin(phi) * Math.sin(theta)
77
+ );
78
+ }
79
+ function altLift(alt_m) {
80
+ if (alt_m == null) return 0.02;
81
+ return Math.min(ALT_MAX_LIFT, Math.max(0.0, alt_m * ALT_SCALE));
82
+ }
83
+ function classify(track) {
84
+ // Honest mapping straight off the live fields — no invented severity.
85
+ if (track.threat_category == null && track.role) return "friendly";
86
+ const v = (track.lambda_verdict || "").toUpperCase();
87
+ if (v.indexOf("THREAT") >= 0) return "hostile";
88
+ if (v.indexOf("SUSPECT") >= 0) return "suspect";
89
+ const s = (track.side || "").toLowerCase();
90
+ if (s === "allied") return "benign";
91
+ if (track.threat_category) return "unknown";
92
+ return "unknown";
93
+ }
94
+
95
+ function _track(fn) { _disposables.push(fn); }
96
+
97
+ // ── demo builders (each returns {group, update?} and is added to _group) ─────
98
+ // D1: textured-feel globe (procedural lat/long graticule sphere; no external image,
99
+ // 0 CDN). Sense-only base for the air picture.
100
+ function buildGlobe() {
101
+ const g = new THREE.Group();
102
+ const sphere = new THREE.Mesh(
103
+ new THREE.SphereGeometry(R, 64, 48),
104
+ new THREE.MeshStandardMaterial({ color: 0x0a1626, emissive: 0x06101c,
105
+ emissiveIntensity: 0.5, metalness: 0.1, roughness: 0.9, transparent: true, opacity: 0.96 })
106
+ );
107
+ g.add(sphere);
108
+ _globe = sphere;
109
+ // graticule (lat/long lines) — gives the globe orientation without a texture/CDN
110
+ const grat = new THREE.Group();
111
+ const lineMat = new THREE.LineBasicMaterial({ color: 0x1d3a52, transparent: true, opacity: 0.5 });
112
+ for (let lat = -60; lat <= 60; lat += 30) {
113
+ const pts = [];
114
+ for (let lon = -180; lon <= 180; lon += 6) pts.push(llToVec(lat, lon, 0.005));
115
+ grat.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), lineMat));
116
+ }
117
+ for (let lon = -180; lon < 180; lon += 30) {
118
+ const pts = [];
119
+ for (let lat = -90; lat <= 90; lat += 6) pts.push(llToVec(lat, lon, 0.005));
120
+ grat.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), lineMat));
121
+ }
122
+ g.add(grat);
123
+ // D2: atmospheric SDF-style glow halo (additive shell)
124
+ const halo = new THREE.Mesh(
125
+ new THREE.SphereGeometry(R * 1.06, 48, 32),
126
+ new THREE.MeshBasicMaterial({ color: 0x2a6f9e, transparent: true, opacity: 0.08,
127
+ side: THREE.BackSide, blending: THREE.AdditiveBlending, depthWrite: false })
128
+ );
129
+ g.add(halo);
130
+ _track(() => { sphere.geometry.dispose(); sphere.material.dispose(); halo.geometry.dispose(); halo.material.dispose(); });
131
+ return { group: g };
132
+ }
133
+
134
+ // D3: radar sweep cone — animated sector that rotates around the site (Dedrone/Anduril
135
+ // coverage sweep). This is a SENSOR coverage sweep, not an effector.
136
+ function buildRadarSweep(siteLat, siteLon) {
137
+ const g = new THREE.Group();
138
+ const center = llToVec(siteLat, siteLon, 0.0);
139
+ // D4: sensor coverage cone (dome of regard)
140
+ const coneGeo = new THREE.ConeGeometry(0.9, 1.2, 32, 1, true);
141
+ const coneMat = new THREE.MeshBasicMaterial({ color: 0x39d3c4, transparent: true,
142
+ opacity: 0.07, side: THREE.DoubleSide, depthWrite: false, blending: THREE.AdditiveBlending });
143
+ const cone = new THREE.Mesh(coneGeo, coneMat);
144
+ cone.position.copy(center.clone().multiplyScalar(1.18));
145
+ cone.lookAt(0, 0, 0);
146
+ cone.rotateX(Math.PI);
147
+ g.add(cone);
148
+ // sweep wedge
149
+ const sweepGeo = new THREE.CircleGeometry(0.85, 24, 0, Math.PI / 5);
150
+ const sweepMat = new THREE.MeshBasicMaterial({ color: 0x6fb1ff, transparent: true,
151
+ opacity: 0.22, side: THREE.DoubleSide, depthWrite: false, blending: THREE.AdditiveBlending });
152
+ const sweep = new THREE.Mesh(sweepGeo, sweepMat);
153
+ sweep.position.copy(center.clone().multiplyScalar(1.02));
154
+ sweep.lookAt(center.clone().multiplyScalar(2));
155
+ g.add(sweep);
156
+ _track(() => { coneGeo.dispose(); coneMat.dispose(); sweepGeo.dispose(); sweepMat.dispose(); });
157
+ return { group: g, update: (t) => { sweep.rotation.z = t * 1.4; } };
158
+ }
159
+
160
+ // Build a single track entity: marker + altitude stalk + history trail + label.
161
+ function buildTrackEntity(track, isFriendly) {
162
+ const g = new THREE.Group();
163
+ const cls = isFriendly ? "friendly" : classify(track);
164
+ const color = CLS[cls] || CLS.unknown;
165
+ const lift = altLift(track.alt_m);
166
+ const pos = llToVec(track.lat, track.lon, lift);
167
+
168
+ // marker (octahedron = aircraft track glyph)
169
+ const mGeo = new THREE.OctahedronGeometry(0.055, 0);
170
+ const mMat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 0.85, metalness: 0.3, roughness: 0.4 });
171
+ const marker = new THREE.Mesh(mGeo, mMat);
172
+ marker.position.copy(pos);
173
+ g.add(marker);
174
+
175
+ // D5: altitude stalk to surface (3D altitude encoding)
176
+ const surf = llToVec(track.lat, track.lon, 0.0);
177
+ const stalk = new THREE.Line(
178
+ new THREE.BufferGeometry().setFromPoints([surf, pos]),
179
+ new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.4 })
180
+ );
181
+ g.add(stalk);
182
 
183
+ // D6: classification ring around hostile/suspect (SENSED status, never an engagement)
184
+ let ring = null;
185
+ if (cls === "hostile" || cls === "suspect") {
186
+ const rGeo = new THREE.RingGeometry(0.09, 0.11, 24);
187
+ const rMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.5, side: THREE.DoubleSide, depthWrite: false });
188
+ ring = new THREE.Mesh(rGeo, rMat);
189
+ ring.position.copy(pos);
190
+ ring.lookAt(0, 0, 0);
191
+ g.add(ring);
192
+ _track(() => { rGeo.dispose(); rMat.dispose(); });
193
+ }
194
+
195
+ _track(() => { mGeo.dispose(); mMat.dispose(); stalk.geometry.dispose(); stalk.material.dispose(); });
196
+ return { group: g, marker, ring, cls, track };
197
+ }
198
+
199
+ // ── HUD / overlay ────────────────────────────────────────────────────────────
200
+ function hud(html) {
201
+ const d = document.createElement("div");
202
+ d.innerHTML = html;
203
+ return d;
204
+ }
205
+ function row(label, value, labelToken) {
206
+ const r = document.createElement("div");
207
+ r.style.cssText = "display:flex;align-items:center;gap:8px;justify-content:space-between;font:11px ui-monospace,Menlo,monospace;color:#9fb1bf";
208
+ const l = document.createElement("span"); l.textContent = label; l.style.color = "#cfe0ec";
209
+ const v = document.createElement("span"); v.textContent = value; v.style.color = "#eef3f6"; v.style.fontWeight = "600";
210
+ r.appendChild(l); r.appendChild(v);
211
+ if (labelToken && _ctx) { const c = _ctx.label.chip(labelToken); c.style.marginLeft = "6px"; r.appendChild(c); }
212
+ return r;
213
+ }
214
+
215
+ // ── live scene state ──────────────────────────────────────────────────────────
216
+ let _trackGroup = null; // holds all track entities, rebuilt on telemetry/cued change
217
+ let _verdictBeam = null; // signed-verdict beam (decision lock), not a weapon
218
+ let _dsseLock = null; // DSSE signature lock animation node
219
+ let _fingerprintCallout = null;
220
+ let _restrictedZones = null; // restricted-airspace SDF volumes
221
+ let _ridPanel = null, _gatesPanel = null, _verdictPanel = null, _posturePanel = null, _fpPanel = null;
222
+ let _beamPhase = 0, _lockPhase = 0;
223
+
224
+ function rebuildTracks() {
225
+ if (!_trackGroup) { _trackGroup = new THREE.Group(); _group.add(_trackGroup); }
226
+ // clear old
227
+ while (_trackGroup.children.length) {
228
+ const c = _trackGroup.children.pop();
229
+ _trackGroup.remove(c);
230
+ }
231
+ const entities = [];
232
+ const tele = _state.telemetry;
233
+ const cued = _state.cued;
234
+
235
+ // D7: friendly fleet tracks (own fleet, teal) — telemetry.friendly_drones
236
+ if (tele && Array.isArray(tele.friendly_drones)) {
237
+ tele.friendly_drones.forEach((d) => {
238
+ const e = buildTrackEntity(d, true); _trackGroup.add(e.group); entities.push(e);
239
+ });
240
+ }
241
+ // D8: SENSED threat tracks (telemetry.threat_tracks) — classification color
242
+ if (tele && Array.isArray(tele.threat_tracks)) {
243
+ tele.threat_tracks.forEach((d) => {
244
+ const e = buildTrackEntity(d, false); _trackGroup.add(e.group); entities.push(e);
245
+ });
246
+ }
247
+ // D9: externally-cued tracks (cued-tracks) — carry cuing_sensor (we own no sensor)
248
+ if (cued && Array.isArray(cued.tracks)) {
249
+ cued.tracks.forEach((d) => {
250
+ const e = buildTrackEntity(d, false); _trackGroup.add(e.group); entities.push(e);
251
+ });
252
+ }
253
+ // D10: real cooperative ADS-B air picture (airplanes.live) — small benign dots,
254
+ // capped for perf. Cooperative manned aircraft = green (benign), per AirSight pattern.
255
+ if (_state.airpicture && Array.isArray(_state.airpicture.aircraft)) {
256
+ const ac = _state.airpicture.aircraft.slice(0, 120);
257
+ const geo = new THREE.BufferGeometry();
258
+ const verts = [];
259
+ ac.forEach((a) => {
260
+ if (a.lat == null || a.lon == null) return;
261
+ const p = llToVec(a.lat, a.lon, altLift((a.alt_baro || 0) * 0.3048));
262
+ verts.push(p.x, p.y, p.z);
263
+ });
264
+ geo.setAttribute("position", new THREE.Float32BufferAttribute(verts, 3));
265
+ const pts = new THREE.Points(geo, new THREE.PointsMaterial({ color: CLS.benign, size: 0.04, transparent: true, opacity: 0.7 }));
266
+ _trackGroup.add(pts);
267
+ _track(() => { geo.dispose(); pts.material.dispose(); });
268
+ }
269
+ _trackGroup.userData.entities = entities;
270
+ return entities;
271
+ }
272
+
273
+ // D11: restricted-airspace SDF volumes — translucent glowing boxes/spheres around
274
+ // protected sites. Breach = pulse (SENSED breach, not an engagement).
275
+ function buildRestrictedZones(centerLat, centerLon) {
276
+ const g = new THREE.Group();
277
+ const sites = [
278
+ { lat: centerLat, lon: centerLon, r: 0.42, name: "PROTECTED SITE" },
279
+ { lat: centerLat + 0.02, lon: centerLon + 0.03, r: 0.26, name: "INNER KEEP-OUT" },
280
+ ];
281
+ const meshes = [];
282
+ sites.forEach((s) => {
283
+ const geo = new THREE.SphereGeometry(s.r, 24, 18);
284
+ const mat = new THREE.MeshBasicMaterial({ color: 0xff6b6b, transparent: true, opacity: 0.08,
285
+ side: THREE.DoubleSide, depthWrite: false, blending: THREE.AdditiveBlending });
286
+ const m = new THREE.Mesh(geo, mat);
287
+ m.position.copy(llToVec(s.lat, s.lon, s.r * 0.4));
288
+ g.add(m);
289
+ // wireframe outline (SDF boundary)
290
+ const wf = new THREE.LineSegments(new THREE.WireframeGeometry(geo),
291
+ new THREE.LineBasicMaterial({ color: 0xff8a8a, transparent: true, opacity: 0.35 }));
292
+ wf.position.copy(m.position);
293
+ g.add(wf);
294
+ meshes.push(m);
295
+ _track(() => { geo.dispose(); mat.dispose(); wf.geometry.dispose(); wf.material.dispose(); });
296
  });
297
+ return { group: g, update: (t) => { meshes.forEach((m, i) => { m.material.opacity = 0.06 + 0.05 * (0.5 + 0.5 * Math.sin(t * 1.5 + i)); }); } };
298
+ }
 
299
 
300
+ // D12: Λ-gate signed-verdict beam — when /evaluate returns, a beam from the globe
301
+ // core to the decision plane "locks" the verdict. This is the SIGNED DECISION
302
+ // (an evidence act). Color = decision (ALLOW=green / CLASSIFY=amber / HALT=red).
303
+ // It is NOT a weapon: nothing is emitted at a track, nothing is defeated.
304
+ function buildVerdictBeam() {
305
+ const g = new THREE.Group();
306
+ const geo = new THREE.CylinderGeometry(0.02, 0.08, 2.6, 16, 1, true);
307
+ const mat = new THREE.MeshBasicMaterial({ color: CLS.benign, transparent: true, opacity: 0.0,
308
+ side: THREE.DoubleSide, depthWrite: false, blending: THREE.AdditiveBlending });
309
+ const beam = new THREE.Mesh(geo, mat);
310
+ beam.position.set(0, R + 1.3, 0);
311
+ g.add(beam);
312
+ _verdictBeam = beam;
313
+ _track(() => { geo.dispose(); mat.dispose(); });
314
+ return { group: g, update: (t) => {
315
+ if (!_state.evaluate) return;
316
+ const dec = (_state.evaluate.decision || "").toUpperCase();
317
+ const col = dec === "ALLOW" ? CLS.benign : dec === "HALT" ? CLS.hostile : CLS.unknown;
318
+ beam.material.color.setHex(col);
319
+ beam.material.opacity = 0.18 + 0.12 * (0.5 + 0.5 * Math.sin(t * 2.2));
320
+ } };
321
+ }
322
+
323
+ // D13: DSSE-signature lock animation — when the real ECDSA-P256 DSSE signature is
324
+ // present (lambda_receipt.dsse.signed === true), an orbiting ring "locks" closed
325
+ // around the verdict, with the signature truncation shown in the HUD. The lock is
326
+ // the cryptographic seal of the SIGNED VERDICT — the evidence, not an effect.
327
+ function buildDsseLock() {
328
+ const g = new THREE.Group();
329
+ const geo = new THREE.TorusGeometry(0.5, 0.018, 12, 48);
330
+ const mat = new THREE.MeshStandardMaterial({ color: 0x6fb1ff, emissive: 0x6fb1ff, emissiveIntensity: 0.6, metalness: 0.6, roughness: 0.3 });
331
+ const ring = new THREE.Mesh(geo, mat);
332
+ ring.position.set(0, R + 1.3, 0);
333
+ g.add(ring);
334
+ _dsseLock = ring;
335
+ _track(() => { geo.dispose(); mat.dispose(); });
336
+ return { group: g, update: (t) => {
337
+ const signed = !!(_state.evaluate && _state.evaluate.lambda_receipt &&
338
+ _state.evaluate.lambda_receipt.dsse && _state.evaluate.lambda_receipt.dsse.signed);
339
+ ring.rotation.x = Math.PI / 2;
340
+ ring.rotation.z = t * (signed ? 0.9 : 0.15);
341
+ ring.material.color.setHex(signed ? 0x2fd07a : 0x8a97a3); // green when signed, gray when not
342
+ ring.material.emissive.setHex(signed ? 0x2fd07a : 0x8a97a3);
343
+ ring.scale.setScalar(signed ? 1.0 : 0.92 + 0.06 * Math.sin(t * 3));
344
+ } };
345
+ }
346
 
347
+ // ── overlay panels (the HUD reads live values + honest labels) ────────────────
348
+ function buildOverlay() {
349
  _overlay = document.createElement("div");
350
  _overlay.className = "szl3d-surface-overlay";
351
+ Object.assign(_overlay.style, { position: "absolute", left: "14px", top: "14px", right: "14px",
352
+ zIndex: "5", display: "flex", justifyContent: "space-between", gap: "12px",
353
+ pointerEvents: "none", fontFamily: "ui-monospace,Menlo,monospace" });
354
+
355
+ const left = document.createElement("div");
356
+ Object.assign(left.style, { display: "flex", flexDirection: "column", gap: "8px", maxWidth: "min(46%,360px)", pointerEvents: "auto" });
357
+ const right = document.createElement("div");
358
+ Object.assign(right.style, { display: "flex", flexDirection: "column", gap: "8px", maxWidth: "min(46%,360px)", alignItems: "flex-end", pointerEvents: "auto" });
359
+
360
+ const card = (title) => {
361
+ const c = document.createElement("div");
362
+ c.style.cssText = "background:rgba(8,14,20,.82);border:1px solid #1d2a36;border-radius:9px;padding:9px 11px;display:flex;flex-direction:column;gap:5px;backdrop-filter:blur(3px)";
363
+ const h = document.createElement("div");
364
+ h.style.cssText = "font:600 11px ui-sans-serif,system-ui;color:#eef3f6;letter-spacing:.5px;text-transform:uppercase";
365
+ h.textContent = title; c.appendChild(h);
366
+ return c;
367
+ };
368
+
369
+ const head = document.createElement("div");
370
+ head.style.cssText = "font:600 13px ui-sans-serif,system-ui;color:#eef3f6;letter-spacing:.4px";
371
+ head.textContent = TITLE;
372
+ left.appendChild(head);
373
+
374
+ const badge = _ctx.live.createBadge();
375
+ left.appendChild(badge.el);
376
+
377
+ // D14: 13-axis Λ-gate verdict panel
378
+ _verdictPanel = card("Λ-Gate Verdict · senses-and-evidences");
379
+ left.appendChild(_verdictPanel);
380
+
381
+ // D15: DSSE signature panel (real ECDSA-P256)
382
+ _fpPanel = null;
383
+ _ridPanel = card("Remote-ID / RID Validator");
384
+ left.appendChild(_ridPanel);
385
+
386
+ // right column
387
+ _posturePanel = card("JIATF-401 posture");
388
+ right.appendChild(_posturePanel);
389
+ _gatesPanel = card("13-Axis Λ-Gates");
390
+ right.appendChild(_gatesPanel);
391
+ _fingerprintCallout = card("Fingerprint match · 53-DB");
392
+ right.appendChild(_fingerprintCallout);
393
+
394
+ const legend = _ctx.label.legend();
395
+ legend.style.opacity = "0.85"; legend.style.pointerEvents = "none";
396
+ left.appendChild(legend);
397
+
398
+ _overlay.appendChild(left); _overlay.appendChild(right);
399
+ (_ctx.container || document.body).appendChild(_overlay);
400
+ return badge;
401
+ }
402
+
403
+ function clearCard(card) { while (card.children.length > 1) card.removeChild(card.lastChild); }
404
+
405
+ function renderVerdict() {
406
+ const ev = _state.evaluate;
407
+ clearCard(_verdictPanel);
408
+ if (!ev || ev.degraded) {
409
+ _verdictPanel.appendChild(row("decision", ev && ev.degraded ? "DEGRADED" : "—", "STRUCTURAL-ONLY"));
410
+ return;
411
+ }
412
+ const dec = ev.decision || "—";
413
+ const lam = ev.lambda != null ? ev.lambda.toFixed(6) : "—";
414
+ const floor = ev.lambda_floor != null ? ev.lambda_floor : "—";
415
+ _verdictPanel.appendChild(row("decision", dec));
416
+ _verdictPanel.appendChild(row("Λ score", lam + " (floor " + floor + ")"));
417
+ _verdictPanel.appendChild(row("Λ status", "Conjecture 1 · advisory"));
418
+ // D16: real DSSE signature lock-on (truncated sig + keyid)
419
+ const dsse = ev.lambda_receipt && ev.lambda_receipt.dsse;
420
+ if (dsse) {
421
+ const sig = (dsse.signatures && dsse.signatures[0] && dsse.signatures[0].sig) || "";
422
+ const sigShort = sig ? (sig.slice(0, 18) + "…" + sig.slice(-8)) : "—";
423
+ _verdictPanel.appendChild(row("DSSE", dsse.signed ? "SIGNED ✓" : "UNSIGNED", dsse.signed ? "MEASURED" : "STRUCTURAL-ONLY"));
424
+ _verdictPanel.appendChild(row("keyid", dsse.keyid || "—"));
425
+ _verdictPanel.appendChild(row("ECDSA-P256 sig", sigShort));
426
+ }
427
+ // D17: CI signing honesty disclosure (PLACEHOLDER per killinchu honest posture)
428
+ if (ev.signature) {
429
+ const ci = row("CI sign", ev.signature.indexOf("PLACEHOLDER") >= 0 ? "PLACEHOLDER" : "wired", "STRUCTURAL-ONLY");
430
+ _verdictPanel.appendChild(ci);
431
+ }
432
+ }
433
+
434
+ function renderRid() {
435
+ clearCard(_ridPanel);
436
+ const tele = _state.telemetry, cued = _state.cued;
437
+ // RID validator: count tracks broadcasting a Remote-ID vs those without (RID-OFF =
438
+ // spoofable/uncooperative). Honest: a broadcast ID is a CLAIM, not ground truth.
439
+ let withRid = 0, withoutRid = 0;
440
+ const scan = (arr, key) => (arr || []).forEach((t) => { (t[key]) ? withRid++ : withoutRid++; });
441
+ if (tele) { scan(tele.friendly_drones, "remote_id"); scan(tele.threat_tracks, "remote_id"); }
442
+ if (cued) scan(cued.tracks, "remote_id");
443
+ _ridPanel.appendChild(row("RID present", String(withRid)));
444
+ _ridPanel.appendChild(row("RID OFF (uncoop)", String(withoutRid)));
445
+ _ridPanel.appendChild(row("note", "broadcast ID = claim"));
446
+ if (tele && tele.data_kind) _ridPanel.appendChild(row("data_kind", String(tele.data_kind), tele.data_kind === "demo_mock" ? "SAMPLE" : "MEASURED"));
447
+ }
448
+
449
+ function renderPosture() {
450
+ clearCard(_posturePanel);
451
+ _posturePanel.appendChild(row("charter", "SENSE + EVIDENCE"));
452
+ _posturePanel.appendChild(row("defeat", "OUT-OF-SCOPE"));
453
+ _posturePanel.appendChild(row("no jam/spoof", "✓"));
454
+ _posturePanel.appendChild(row("no takeover", "✓"));
455
+ _posturePanel.appendChild(row("no kinetic", "✓"));
456
+ _posturePanel.appendChild(row("human-on-loop", "ROE gate"));
457
+ _posturePanel.appendChild(row("C4 strongest fit", "§6–§7 receipts"));
458
+ }
459
+
460
+ function renderGates() {
461
+ clearCard(_gatesPanel);
462
+ const g = _state.gates;
463
+ if (!g || g.degraded || !Array.isArray(g.gates)) {
464
+ _gatesPanel.appendChild(row("gates", g && g.degraded ? "DEGRADED" : "—", "STRUCTURAL-ONLY"));
465
+ return;
466
+ }
467
+ _gatesPanel.appendChild(row("axes", String(g.count != null ? g.count : g.gates.length)));
468
+ // show first few axis names live
469
+ g.gates.slice(0, 5).forEach((ax) => _gatesPanel.appendChild(row("Λ" + ax.axis, ax.name || "—")));
470
+ _gatesPanel.appendChild(row("status", "Conjecture 1 — OPEN"));
471
+ }
472
+
473
+ function renderFingerprint() {
474
+ clearCard(_fingerprintCallout);
475
+ const db = _state.db;
476
+ if (!db || !Array.isArray(db)) {
477
+ _fingerprintCallout.appendChild(row("DB", "loading…", "STRUCTURAL-ONLY"));
478
+ return;
479
+ }
480
+ _fingerprintCallout.appendChild(row("fingerprints", String(db.length)));
481
+ // D: attempt to match a SENSED threat track type to the DB (classification callout)
482
+ const tele = _state.telemetry;
483
+ let matched = null;
484
+ if (tele && Array.isArray(tele.threat_tracks)) {
485
+ for (const t of tele.threat_tracks) {
486
+ const ty = (t.type || "").toUpperCase();
487
+ matched = db.find((d) => ty && (String(d.model).toUpperCase().indexOf(ty.split("-")[0]) >= 0));
488
+ if (matched) break;
489
+ }
490
+ }
491
+ // fall back to a representative adversary fingerprint to show the callout shape
492
+ const ex = matched || db.find((d) => d.side === "adversary") || db[0];
493
+ if (ex) {
494
+ _fingerprintCallout.appendChild(row("model", String(ex.model)));
495
+ _fingerprintCallout.appendChild(row("mfr", String(ex.manufacturer)));
496
+ _fingerprintCallout.appendChild(row("group", String(ex.group || "—")));
497
+ _fingerprintCallout.appendChild(row("side", String(ex.side || "—")));
498
+ }
499
+ _fingerprintCallout.appendChild(row("source", "drones_db.json"));
500
+ }
501
+
502
+ // ── mount / unmount ───────────────────────────────────────────────────────────
503
+ function mount(ctx) {
504
+ _ctx = ctx; _stage = ctx.stage; THREE = ctx.THREE;
505
+ _group = new THREE.Group();
506
+ _stage.scene.add(_group);
507
+
508
+ // lighting so the globe reads in 3D
509
+ const amb = new THREE.AmbientLight(0x223344, 1.1);
510
+ const key = new THREE.DirectionalLight(0xbfe0ff, 1.3); key.position.set(5, 6, 7);
511
+ _group.add(amb); _group.add(key);
512
+ _track(() => { /* lights freed with group removal */ });
513
+
514
+ // build the static scene demos
515
+ const updaters = [];
516
+ const globe = buildGlobe(); _group.add(globe.group);
517
+
518
+ // protected site (matches killinchu demo fleet vicinity ~ 37.42, -122.17)
519
+ const siteLat = 37.4275, siteLon = -122.1697;
520
+ const sweep = buildRadarSweep(siteLat, siteLon); _group.add(sweep.group); updaters.push(sweep.update);
521
+ const zones = buildRestrictedZones(siteLat, siteLon); _group.add(zones.group); updaters.push(zones.update);
522
+ const beam = buildVerdictBeam(); _group.add(beam.group); updaters.push(beam.update);
523
+ const lock = buildDsseLock(); _group.add(lock.group); updaters.push(lock.update);
524
+
525
+ // floating honesty billboard
526
+ try {
527
+ const bb = ctx.label.billboard(THREE, "STRUCTURAL-ONLY", { text: "senses · evidences · NOT defeats", scale: 0.42, position: [0, R + 2.0, 0] });
528
+ _group.add(bb);
529
+ _track(() => { if (bb.material.map) bb.material.map.dispose(); bb.material.dispose(); });
530
+ } catch (_) {}
531
+
532
+ const badge = buildOverlay();
533
+ renderPosture(); renderVerdict(); renderRid(); renderGates(); renderFingerprint();
534
+
535
+ // per-frame animation (rotate the estate slowly + run demo updaters). The shell's
536
+ // Stage has no offFrame(), so we self-guard: once unmounted, _group is null and the
537
+ // callback no-ops (the shell disposes the renderer/loop on tab switch).
538
+ let _t = 0;
539
+ _frameOff = () => {
540
+ if (!_group) return;
541
+ _t += 0.016;
542
+ _group.rotation.y += 0.0009;
543
+ for (const u of updaters) { try { u(_t); } catch (_) {} }
544
+ // pulse SENSED hostile/suspect rings
545
+ if (_trackGroup && _trackGroup.userData.entities) {
546
+ for (const e of _trackGroup.userData.entities) {
547
+ if (e.ring) { e.ring.material.opacity = 0.35 + 0.3 * (0.5 + 0.5 * Math.sin(_t * 3)); }
548
+ if (e.marker) e.marker.rotation.y += 0.02;
549
+ }
550
+ }
551
+ };
552
+ _stage.onFrame(_frameOff);
553
+
554
+ // ── wire LIVE data (never fabricate; degrade gracefully) ──
555
+ // primary endpoint drives the badge
556
+ _handles.push(ctx.live.poll(EP.evaluate, 6000, (json, meta) => {
557
+ _state.evaluate = json; _state.meta.evaluate = meta; renderVerdict();
558
+ }, { badge }));
559
+ _handles.push(ctx.live.poll(EP.telemetry, 5000, (json, meta) => {
560
+ _state.telemetry = json; _state.meta.telemetry = meta; rebuildTracks(); renderRid(); renderFingerprint();
561
+ }));
562
+ _handles.push(ctx.live.poll(EP.cued, 7000, (json) => {
563
+ _state.cued = json; rebuildTracks(); renderRid();
564
+ }));
565
+ _handles.push(ctx.live.poll(EP.airpicture, 12000, (json) => {
566
+ _state.airpicture = json; rebuildTracks();
567
+ }));
568
+ _handles.push(ctx.live.poll(EP.gates, 30000, (json) => {
569
+ _state.gates = json; renderGates();
570
+ }));
571
+ // 53-fingerprint DB (static vendored; fetched once)
572
+ fetch(EP.dronesDb, { headers: { accept: "application/json" } })
573
+ .then((r) => r.ok ? r.json() : null)
574
+ .then((db) => { if (db) { _state.db = db; _state.dbCount = db.length; renderFingerprint(); } })
575
+ .catch(() => {});
576
 
577
  return { id: ID, started: true };
578
  }
579
 
580
  function unmount() {
581
+ for (const h of _handles) { try { h.stop(); } catch (_) {} }
582
+ _handles = [];
583
+ _frameOff = null;
584
+ for (const d of _disposables) { try { d(); } catch (_) {} }
585
+ _disposables = [];
586
  try { if (_overlay && _overlay.parentNode) _overlay.parentNode.removeChild(_overlay); } catch (_) {}
587
+ try { if (_group && _stage) _stage.scene.remove(_group); } catch (_) {}
588
+ _overlay = null; _group = null; _globe = null; _trackGroup = null;
589
+ _verdictBeam = null; _dsseLock = null; _stage = null; THREE = null; _ctx = null;
590
+ _state = { evaluate: null, telemetry: null, cued: null, airpicture: null, gates: null, db: null, meta: {}, dbCount: 0 };
591
  }
592
 
593
+ export default { id: ID, title: TITLE, endpoints: [EP.evaluate, EP.telemetry, EP.cued, EP.airpicture, EP.gates], mount, unmount };
static/3d/surfaces/data/drones_db.json ADDED
@@ -0,0 +1,1152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "mq9",
4
+ "model": "MQ-9 Reaper",
5
+ "manufacturer": "General Atomics",
6
+ "country": "USA",
7
+ "side": "allied",
8
+ "role": "strike/ISR",
9
+ "group": "Group 5",
10
+ "telemetry": [
11
+ "ADS-B",
12
+ "Link-16",
13
+ "SATCOM"
14
+ ],
15
+ "specs": {
16
+ "wingspan_m": 20.1,
17
+ "mtow_kg": 4760,
18
+ "speed_kmh": 482,
19
+ "ceiling_m": 15240,
20
+ "endurance_hr": 27,
21
+ "range_km": 1852,
22
+ "unit_cost_usd": 56500000
23
+ },
24
+ "notes": "MALE hunter-killer; AGM-114 Hellfire + GBU; AFSOC inv ~50 (Jan 2025).",
25
+ "source": "https://www.af.mil/About-Us/Fact-Sheets/Display/Article/104470/mq-9-reaper/"
26
+ },
27
+ {
28
+ "id": "rq4",
29
+ "model": "RQ-4 Global Hawk",
30
+ "manufacturer": "Northrop Grumman",
31
+ "country": "USA",
32
+ "side": "allied",
33
+ "role": "ISR",
34
+ "group": "Group 5",
35
+ "telemetry": [
36
+ "SATCOM",
37
+ "SIGINT",
38
+ "IMINT",
39
+ "MTI"
40
+ ],
41
+ "specs": {
42
+ "wingspan_m": 39.9,
43
+ "mtow_kg": 14628,
44
+ "speed_kmh": 629,
45
+ "ceiling_m": 18288,
46
+ "endurance_hr": 34,
47
+ "range_km": 22800
48
+ },
49
+ "notes": "HALE unarmed ISR; Blocks 20/30/40; SAR + SIGINT.",
50
+ "source": "https://www.af.mil/About-Us/Fact-Sheets/Display/Article/104516/rq-4-global-hawk/"
51
+ },
52
+ {
53
+ "id": "mq4c",
54
+ "model": "MQ-4C Triton",
55
+ "manufacturer": "Northrop Grumman",
56
+ "country": "USA",
57
+ "side": "allied",
58
+ "role": "maritime ISR",
59
+ "group": "Group 5",
60
+ "telemetry": [
61
+ "SATCOM",
62
+ "AESA radar",
63
+ "SIGINT"
64
+ ],
65
+ "specs": {
66
+ "wingspan_m": 39.9,
67
+ "mtow_kg": 14628,
68
+ "speed_kmh": 610,
69
+ "ceiling_m": 17000,
70
+ "endurance_hr": 30,
71
+ "range_km": 15000
72
+ },
73
+ "notes": "Navy maritime variant of Global Hawk; 360\u00b0 maritime surveillance.",
74
+ "source": "https://en.wikipedia.org/wiki/Northrop_Grumman_MQ-4C_Triton"
75
+ },
76
+ {
77
+ "id": "rq170",
78
+ "model": "RQ-170 Sentinel",
79
+ "manufacturer": "Lockheed Martin",
80
+ "country": "USA",
81
+ "side": "allied",
82
+ "role": "stealth ISR",
83
+ "group": "Group 5",
84
+ "telemetry": [
85
+ "LOS datalink (classified)"
86
+ ],
87
+ "specs": {
88
+ "wingspan_m": 20.0,
89
+ "ceiling_m": 15000,
90
+ "role_note": "low-observable flying wing"
91
+ },
92
+ "notes": "'Beast of Kandahar' stealth ISR; one captured by Iran 2011.",
93
+ "source": "https://en.wikipedia.org/wiki/Lockheed_Martin_RQ-170_Sentinel"
94
+ },
95
+ {
96
+ "id": "mq1c",
97
+ "model": "MQ-1C Gray Eagle",
98
+ "manufacturer": "General Atomics",
99
+ "country": "USA",
100
+ "side": "allied",
101
+ "role": "strike/ISR",
102
+ "group": "Group 4",
103
+ "telemetry": [
104
+ "ADS-B",
105
+ "TCDL",
106
+ "SATCOM"
107
+ ],
108
+ "specs": {
109
+ "wingspan_m": 17.0,
110
+ "mtow_kg": 1633,
111
+ "speed_kmh": 309,
112
+ "ceiling_m": 8839,
113
+ "endurance_hr": 25,
114
+ "range_km": 370
115
+ },
116
+ "notes": "US Army ERMP; AGM-114; ~204 produced; IOC 2009.",
117
+ "source": "https://www.globalmilitary.net/compare/aircraft/mq-1c-gray-eagle-vs-rq-4-global-hawk/"
118
+ },
119
+ {
120
+ "id": "mq1",
121
+ "model": "MQ-1 Predator",
122
+ "manufacturer": "General Atomics",
123
+ "country": "USA",
124
+ "side": "allied",
125
+ "role": "strike/ISR",
126
+ "group": "Group 4",
127
+ "telemetry": [
128
+ "Ku SATCOM",
129
+ "C-band LOS"
130
+ ],
131
+ "specs": {
132
+ "wingspan_m": 16.8,
133
+ "mtow_kg": 1020,
134
+ "speed_kmh": 217,
135
+ "ceiling_m": 7620,
136
+ "endurance_hr": 24,
137
+ "range_km": 1100
138
+ },
139
+ "notes": "Retired USAF 2018; first armed UAV (Hellfire). Predecessor to Reaper.",
140
+ "source": "https://en.wikipedia.org/wiki/General_Atomics_MQ-1_Predator"
141
+ },
142
+ {
143
+ "id": "mq8",
144
+ "model": "MQ-8 Fire Scout",
145
+ "manufacturer": "Northrop Grumman",
146
+ "country": "USA",
147
+ "side": "allied",
148
+ "role": "VTOL ISR",
149
+ "group": "Group 4",
150
+ "telemetry": [
151
+ "TCDL",
152
+ "Link-16"
153
+ ],
154
+ "specs": {
155
+ "rotor_m": 8.4,
156
+ "mtow_kg": 1430,
157
+ "speed_kmh": 213,
158
+ "ceiling_m": 6100,
159
+ "endurance_hr": 8
160
+ },
161
+ "notes": "US Navy VTOL ISR (MQ-8B/C); shipboard.",
162
+ "source": "https://en.wikipedia.org/wiki/Northrop_Grumman_MQ-8_Fire_Scout"
163
+ },
164
+ {
165
+ "id": "rq7",
166
+ "model": "RQ-7B Shadow",
167
+ "manufacturer": "AAI / Textron",
168
+ "country": "USA",
169
+ "side": "allied",
170
+ "role": "tactical ISR",
171
+ "group": "Group 3",
172
+ "telemetry": [
173
+ "C-band LOS",
174
+ "EO/IR"
175
+ ],
176
+ "specs": {
177
+ "wingspan_m": 4.3,
178
+ "mtow_kg": 208,
179
+ "speed_kmh": 204,
180
+ "ceiling_m": 4600,
181
+ "endurance_hr": 9,
182
+ "range_km": 125
183
+ },
184
+ "notes": "US Army brigade-level tactical ISR; catapult launched.",
185
+ "source": "https://en.wikipedia.org/wiki/AAI_RQ-7_Shadow"
186
+ },
187
+ {
188
+ "id": "rq21",
189
+ "model": "RQ-21 Blackjack",
190
+ "manufacturer": "Boeing / Insitu",
191
+ "country": "USA",
192
+ "side": "allied",
193
+ "role": "tactical ISR",
194
+ "group": "Group 3",
195
+ "telemetry": [
196
+ "LOS datalink",
197
+ "EO/IR"
198
+ ],
199
+ "specs": {
200
+ "wingspan_m": 4.9,
201
+ "mtow_kg": 61,
202
+ "speed_kmh": 167,
203
+ "ceiling_m": 4900,
204
+ "endurance_hr": 16
205
+ },
206
+ "notes": "USMC/Navy expeditionary ISR; runway-independent (catapult/Skyhook).",
207
+ "source": "https://en.wikipedia.org/wiki/Boeing_Insitu_RQ-21_Blackjack"
208
+ },
209
+ {
210
+ "id": "scaneagle",
211
+ "model": "ScanEagle",
212
+ "manufacturer": "Boeing / Insitu",
213
+ "country": "USA",
214
+ "side": "allied",
215
+ "role": "ISR",
216
+ "group": "Group 2",
217
+ "telemetry": [
218
+ "LOS datalink",
219
+ "EO/IR"
220
+ ],
221
+ "specs": {
222
+ "wingspan_m": 3.1,
223
+ "mtow_kg": 22,
224
+ "speed_kmh": 148,
225
+ "ceiling_m": 5950,
226
+ "endurance_hr": 24
227
+ },
228
+ "notes": "Long-endurance small ISR; widely exported to allies.",
229
+ "source": "https://en.wikipedia.org/wiki/Boeing_Insitu_ScanEagle"
230
+ },
231
+ {
232
+ "id": "vbat",
233
+ "model": "V-BAT",
234
+ "manufacturer": "Shield AI",
235
+ "country": "USA",
236
+ "side": "allied",
237
+ "role": "ISR",
238
+ "group": "Group 3",
239
+ "telemetry": [
240
+ "LOS datalink",
241
+ "EO/IR",
242
+ "Hivemind autonomy"
243
+ ],
244
+ "specs": {
245
+ "height_m": 2.7,
246
+ "mtow_kg": 57,
247
+ "speed_kmh": 167,
248
+ "ceiling_m": 6100,
249
+ "endurance_hr": 10
250
+ },
251
+ "notes": "VTOL tail-sitter; AI autonomy (Hivemind) for GPS-denied ops.",
252
+ "source": "https://en.wikipedia.org/wiki/UAS_groups_of_the_United_States_military"
253
+ },
254
+ {
255
+ "id": "rq11",
256
+ "model": "RQ-11B Raven",
257
+ "manufacturer": "AeroVironment",
258
+ "country": "USA",
259
+ "side": "allied",
260
+ "role": "ISR",
261
+ "group": "Group 1",
262
+ "telemetry": [
263
+ "analog/digital LOS",
264
+ "EO/IR"
265
+ ],
266
+ "specs": {
267
+ "wingspan_m": 1.4,
268
+ "mtow_kg": 1.9,
269
+ "speed_kmh": 81,
270
+ "ceiling_m": 4267,
271
+ "endurance_hr": 1.5,
272
+ "range_km": 10
273
+ },
274
+ "notes": "Most-produced US military UAS; hand-launched.",
275
+ "source": "https://en.wikipedia.org/wiki/AeroVironment_RQ-11_Raven"
276
+ },
277
+ {
278
+ "id": "rq20",
279
+ "model": "RQ-20 Puma",
280
+ "manufacturer": "AeroVironment",
281
+ "country": "USA",
282
+ "side": "allied",
283
+ "role": "ISR",
284
+ "group": "Group 1",
285
+ "telemetry": [
286
+ "digital LOS",
287
+ "EO/IR"
288
+ ],
289
+ "specs": {
290
+ "wingspan_m": 2.8,
291
+ "mtow_kg": 6.3,
292
+ "speed_kmh": 83,
293
+ "ceiling_m": 3000,
294
+ "endurance_hr": 2.5
295
+ },
296
+ "notes": "All-environment hand-launched ISR (land/maritime).",
297
+ "source": "https://en.wikipedia.org/wiki/AeroVironment_RQ-20_Puma"
298
+ },
299
+ {
300
+ "id": "wasp",
301
+ "model": "Wasp AE",
302
+ "manufacturer": "AeroVironment",
303
+ "country": "USA",
304
+ "side": "allied",
305
+ "role": "ISR",
306
+ "group": "Group 1",
307
+ "telemetry": [
308
+ "digital LOS",
309
+ "EO/IR"
310
+ ],
311
+ "specs": {
312
+ "wingspan_m": 1.0,
313
+ "mtow_kg": 1.3,
314
+ "speed_kmh": 74,
315
+ "endurance_hr": 0.83
316
+ },
317
+ "notes": "Micro hand-launched ISR; squad-level.",
318
+ "source": "https://en.wikipedia.org/wiki/AeroVironment_Wasp_III"
319
+ },
320
+ {
321
+ "id": "sb300",
322
+ "model": "Switchblade 300 (Blk 20)",
323
+ "manufacturer": "AeroVironment",
324
+ "country": "USA",
325
+ "side": "allied",
326
+ "role": "loitering munition",
327
+ "group": "Group 1",
328
+ "telemetry": [
329
+ "encrypted RF",
330
+ "GPS",
331
+ "EO/IR"
332
+ ],
333
+ "specs": {
334
+ "length_cm": 49.5,
335
+ "mass_kg": 3.27,
336
+ "speed_kmh": 161,
337
+ "endurance_min": 20,
338
+ "range_km": 30,
339
+ "warhead": "frag/EFP"
340
+ },
341
+ "notes": ">700 sent to Ukraine; backpackable precision strike.",
342
+ "source": "https://www.avinc.com/solution/switchblade-300-block-20/"
343
+ },
344
+ {
345
+ "id": "sb600",
346
+ "model": "Switchblade 600",
347
+ "manufacturer": "AeroVironment",
348
+ "country": "USA",
349
+ "side": "allied",
350
+ "role": "loitering munition",
351
+ "group": "Group 2",
352
+ "telemetry": [
353
+ "encrypted RF",
354
+ "SAASM GPS",
355
+ "EO/IR",
356
+ "DDL"
357
+ ],
358
+ "specs": {
359
+ "length_cm": 130,
360
+ "mass_kg": 29.5,
361
+ "speed_kmh": 185,
362
+ "endurance_min": 40,
363
+ "range_km": 90,
364
+ "warhead": "anti-armor (Javelin-derived)"
365
+ },
366
+ "notes": "Anti-armor loitering munition; 90+ km w/ relay.",
367
+ "source": "https://www.avinc.com/solution/switchblade-600/"
368
+ },
369
+ {
370
+ "id": "phoenixghost",
371
+ "model": "Phoenix Ghost",
372
+ "manufacturer": "AEVEX Aerospace",
373
+ "country": "USA",
374
+ "side": "allied",
375
+ "role": "loitering munition",
376
+ "group": "Group 2",
377
+ "telemetry": [
378
+ "encrypted RF",
379
+ "EO/IR"
380
+ ],
381
+ "specs": {
382
+ "endurance_min": 360,
383
+ "warhead": "anti-personnel/light-armor"
384
+ },
385
+ "notes": "Developed with US for Ukraine; long-endurance loiter.",
386
+ "source": "https://en.wikipedia.org/wiki/Phoenix_Ghost"
387
+ },
388
+ {
389
+ "id": "altius600",
390
+ "model": "ALTIUS-600",
391
+ "manufacturer": "Anduril (Area-I)",
392
+ "country": "USA",
393
+ "side": "allied",
394
+ "role": "loitering ISR/strike",
395
+ "group": "Group 2",
396
+ "telemetry": [
397
+ "mesh datalink",
398
+ "EO/IR"
399
+ ],
400
+ "specs": {
401
+ "mass_kg": 12.7,
402
+ "endurance_hr": 4,
403
+ "range_km": 440
404
+ },
405
+ "notes": "Air-launched effect; tube-launched swarm-capable.",
406
+ "source": "https://en.wikipedia.org/wiki/Anduril_Industries"
407
+ },
408
+ {
409
+ "id": "altius700",
410
+ "model": "ALTIUS-700",
411
+ "manufacturer": "Anduril (Area-I)",
412
+ "country": "USA",
413
+ "side": "allied",
414
+ "role": "loitering strike",
415
+ "group": "Group 3",
416
+ "telemetry": [
417
+ "mesh datalink",
418
+ "EO/IR"
419
+ ],
420
+ "specs": {
421
+ "mass_kg": 50,
422
+ "endurance_hr": 4,
423
+ "warhead": "modular"
424
+ },
425
+ "notes": "Larger ALTIUS; heavier payload / standoff strike.",
426
+ "source": "https://en.wikipedia.org/wiki/Anduril_Industries"
427
+ },
428
+ {
429
+ "id": "tb2",
430
+ "model": "Bayraktar TB2",
431
+ "manufacturer": "Baykar",
432
+ "country": "Turkey",
433
+ "side": "dual-use",
434
+ "role": "strike/ISR",
435
+ "group": "Group 4",
436
+ "telemetry": [
437
+ "LOS datalink",
438
+ "EO/IR",
439
+ "laser designator"
440
+ ],
441
+ "specs": {
442
+ "wingspan_m": 12.0,
443
+ "length_m": 6.5,
444
+ "mtow_kg": 1200,
445
+ "speed_kmh": 222,
446
+ "ceiling_m": 8239,
447
+ "range_km": 300,
448
+ "unit_cost_usd": 5000000
449
+ },
450
+ "notes": "Used by allies AND adversaries; MAM-L/MAM-C munitions.",
451
+ "source": "https://www.globalmilitary.net/compare/aircraft/bayraktar-tb2-vs-shahed-136/"
452
+ },
453
+ {
454
+ "id": "tb2akinci",
455
+ "model": "Bayraktar Akinci",
456
+ "manufacturer": "Baykar",
457
+ "country": "Turkey",
458
+ "side": "dual-use",
459
+ "role": "strike/ISR",
460
+ "group": "Group 5",
461
+ "telemetry": [
462
+ "SATCOM",
463
+ "AESA radar",
464
+ "EO/IR"
465
+ ],
466
+ "specs": {
467
+ "wingspan_m": 20.0,
468
+ "mtow_kg": 5500,
469
+ "speed_kmh": 361,
470
+ "ceiling_m": 12190,
471
+ "endurance_hr": 24
472
+ },
473
+ "notes": "HALE strike; cruise-missile capable.",
474
+ "source": "https://en.wikipedia.org/wiki/Baykar_Bayraktar_Ak%C4%B1nc%C4%B1"
475
+ },
476
+ {
477
+ "id": "heron",
478
+ "model": "IAI Heron",
479
+ "manufacturer": "Israel Aerospace Industries",
480
+ "country": "Israel",
481
+ "side": "allied",
482
+ "role": "ISR",
483
+ "group": "Group 4",
484
+ "telemetry": [
485
+ "SATCOM",
486
+ "EO/IR",
487
+ "SAR"
488
+ ],
489
+ "specs": {
490
+ "wingspan_m": 16.6,
491
+ "mtow_kg": 1150,
492
+ "speed_kmh": 207,
493
+ "ceiling_m": 10000,
494
+ "endurance_hr": 52
495
+ },
496
+ "notes": "MALE ISR; widely exported to allied nations.",
497
+ "source": "https://en.wikipedia.org/wiki/IAI_Heron"
498
+ },
499
+ {
500
+ "id": "hermes900",
501
+ "model": "Elbit Hermes 900",
502
+ "manufacturer": "Elbit Systems",
503
+ "country": "Israel",
504
+ "side": "allied",
505
+ "role": "ISR/strike",
506
+ "group": "Group 4",
507
+ "telemetry": [
508
+ "SATCOM",
509
+ "EO/IR",
510
+ "SAR/GMTI"
511
+ ],
512
+ "specs": {
513
+ "wingspan_m": 15.0,
514
+ "mtow_kg": 1180,
515
+ "speed_kmh": 220,
516
+ "ceiling_m": 9100,
517
+ "endurance_hr": 36
518
+ },
519
+ "notes": "MALE multi-payload ISR; allied export.",
520
+ "source": "https://en.wikipedia.org/wiki/Elbit_Hermes_900"
521
+ },
522
+ {
523
+ "id": "harop",
524
+ "model": "IAI Harop",
525
+ "manufacturer": "IAI",
526
+ "country": "Israel",
527
+ "side": "allied",
528
+ "role": "loitering munition",
529
+ "group": "Group 3",
530
+ "telemetry": [
531
+ "man-in-loop datalink",
532
+ "EO/IR"
533
+ ],
534
+ "specs": {
535
+ "wingspan_m": 3.0,
536
+ "speed_kmh": 417,
537
+ "endurance_hr": 6,
538
+ "range_km": 1000,
539
+ "warhead": "23 kg"
540
+ },
541
+ "notes": "Anti-radiation loitering munition; SEAD.",
542
+ "source": "https://en.wikipedia.org/wiki/IAI_Harop"
543
+ },
544
+ {
545
+ "id": "shahed136",
546
+ "model": "Shahed-136 / Geran-2",
547
+ "manufacturer": "HESA (Iran)",
548
+ "country": "Iran",
549
+ "side": "adversary",
550
+ "role": "loitering munition",
551
+ "group": "Group 3",
552
+ "telemetry": [
553
+ "INS",
554
+ "consumer GPS",
555
+ "RF link ~150km"
556
+ ],
557
+ "specs": {
558
+ "length_m": 3.5,
559
+ "wingspan_m": 2.5,
560
+ "mass_kg": 200,
561
+ "speed_kmh": 185,
562
+ "ceiling_m": 4000,
563
+ "range_km": 2500,
564
+ "warhead": "30-50 kg HE-frag"
565
+ },
566
+ "notes": "Russian-operated as Geran-2; salvo/swarm launch; MD-550 50hp pusher.",
567
+ "source": "https://armyrecognition.com/military-products/army/unmanned-systems/unmanned-aerial-vehicles/shahed-136-loitering-munition-kamikaze-suicide-drone-technical-data"
568
+ },
569
+ {
570
+ "id": "shahed131",
571
+ "model": "Shahed-131 / Geran-1",
572
+ "manufacturer": "HESA (Iran)",
573
+ "country": "Iran",
574
+ "side": "adversary",
575
+ "role": "loitering munition",
576
+ "group": "Group 2",
577
+ "telemetry": [
578
+ "INS",
579
+ "consumer GPS"
580
+ ],
581
+ "specs": {
582
+ "range_km": 900,
583
+ "warhead": "15 kg",
584
+ "speed_kmh": 150
585
+ },
586
+ "notes": "Smaller predecessor to Shahed-136; upward-only fin tips.",
587
+ "source": "https://en.wikipedia.org/wiki/Shahed_drones"
588
+ },
589
+ {
590
+ "id": "shahed129",
591
+ "model": "Shahed-129",
592
+ "manufacturer": "HESA (Iran)",
593
+ "country": "Iran",
594
+ "side": "adversary",
595
+ "role": "strike/ISR",
596
+ "group": "Group 4",
597
+ "telemetry": [
598
+ "LOS datalink",
599
+ "EO/IR"
600
+ ],
601
+ "specs": {
602
+ "wingspan_m": 16.0,
603
+ "endurance_hr": 24,
604
+ "range_km": 1700,
605
+ "warhead": "Sadid bombs"
606
+ },
607
+ "notes": "Iranian MALE Reaper-analog.",
608
+ "source": "https://en.wikipedia.org/wiki/HESA_Shahed_129"
609
+ },
610
+ {
611
+ "id": "mohajer6",
612
+ "model": "Mohajer-6",
613
+ "manufacturer": "HESA / Qods (Iran)",
614
+ "country": "Iran",
615
+ "side": "adversary",
616
+ "role": "strike/ISR",
617
+ "group": "Group 3",
618
+ "telemetry": [
619
+ "LOS datalink",
620
+ "EO/IR"
621
+ ],
622
+ "specs": {
623
+ "wingspan_m": 10.0,
624
+ "mtow_kg": 670,
625
+ "endurance_hr": 12,
626
+ "range_km": 200,
627
+ "warhead": "Qaem bombs"
628
+ },
629
+ "notes": "Iranian tactical strike/ISR; supplied to Russia.",
630
+ "source": "https://en.wikipedia.org/wiki/Mohajer-6"
631
+ },
632
+ {
633
+ "id": "lancet3",
634
+ "model": "Lancet-3 (Izdeliye-52)",
635
+ "manufacturer": "ZALA / Kalashnikov (Russia)",
636
+ "country": "Russia",
637
+ "side": "adversary",
638
+ "role": "loitering munition",
639
+ "group": "Group 1",
640
+ "telemetry": [
641
+ "RF datalink",
642
+ "TV terminal guidance"
643
+ ],
644
+ "specs": {
645
+ "mass_kg": 12,
646
+ "speed_kmh": 110,
647
+ "endurance_min": 40,
648
+ "range_km": 40,
649
+ "warhead": "3 kg HE/HE-frag"
650
+ },
651
+ "notes": "Catapult-launched; EO + TV terminal; anti-armor/artillery.",
652
+ "source": "https://armyrecognition.com/military-products/army/unmanned-systems/unmanned-aerial-vehicles/lancet-3-loitering-munition-kamikaze-drone-russia-data-fact-sheet"
653
+ },
654
+ {
655
+ "id": "lancet1",
656
+ "model": "Lancet-1",
657
+ "manufacturer": "ZALA (Russia)",
658
+ "country": "Russia",
659
+ "side": "adversary",
660
+ "role": "loitering munition",
661
+ "group": "Group 1",
662
+ "telemetry": [
663
+ "RF datalink",
664
+ "TV terminal"
665
+ ],
666
+ "specs": {
667
+ "mass_kg": 5,
668
+ "warhead": "1 kg",
669
+ "range_km": 40
670
+ },
671
+ "notes": "Smaller Lancet variant.",
672
+ "source": "https://armyrecognition.com/military-products/army/unmanned-systems/unmanned-aerial-vehicles/lancet-3-loitering-munition-kamikaze-drone-russia-data-fact-sheet"
673
+ },
674
+ {
675
+ "id": "orlan10",
676
+ "model": "Orlan-10",
677
+ "manufacturer": "STC (Russia)",
678
+ "country": "Russia",
679
+ "side": "adversary",
680
+ "role": "ISR/EW",
681
+ "group": "Group 2",
682
+ "telemetry": [
683
+ "RF datalink",
684
+ "EO/IR",
685
+ "Leer-3 EW"
686
+ ],
687
+ "specs": {
688
+ "wingspan_m": 3.1,
689
+ "mtow_kg": 18,
690
+ "speed_kmh": 150,
691
+ "ceiling_m": 5000,
692
+ "endurance_hr": 16,
693
+ "range_km": 120
694
+ },
695
+ "notes": "Russia's workhorse ISR/artillery-spotting UAV; EW variant.",
696
+ "source": "https://en.wikipedia.org/wiki/Orlan-10"
697
+ },
698
+ {
699
+ "id": "forpost",
700
+ "model": "Forpost-R",
701
+ "manufacturer": "UZGA (Russia)",
702
+ "country": "Russia",
703
+ "side": "adversary",
704
+ "role": "strike/ISR",
705
+ "group": "Group 4",
706
+ "telemetry": [
707
+ "LOS datalink",
708
+ "EO/IR"
709
+ ],
710
+ "specs": {
711
+ "wingspan_m": 8.5,
712
+ "mtow_kg": 500,
713
+ "endurance_hr": 18,
714
+ "range_km": 250
715
+ },
716
+ "notes": "Russian licensed IAI Searcher derivative; now strike-capable.",
717
+ "source": "https://en.wikipedia.org/wiki/IAI_Searcher"
718
+ },
719
+ {
720
+ "id": "wingloong2",
721
+ "model": "Wing Loong II",
722
+ "manufacturer": "CAIG / CASC",
723
+ "country": "China",
724
+ "side": "adversary",
725
+ "role": "strike/ISR",
726
+ "group": "Group 5",
727
+ "telemetry": [
728
+ "SATCOM >1000km",
729
+ "EO/IR",
730
+ "SAR"
731
+ ],
732
+ "specs": {
733
+ "length_m": 11.0,
734
+ "wingspan_m": 20.5,
735
+ "mtow_kg": 4200,
736
+ "speed_kmh": 370,
737
+ "ceiling_m": 9900,
738
+ "endurance_hr": 32,
739
+ "range_km": 1000
740
+ },
741
+ "notes": "Chinese MALE; up to 480 kg ordnance; widely exported.",
742
+ "source": "https://en.wikipedia.org/wiki/CAIG_Wing_Loong_II"
743
+ },
744
+ {
745
+ "id": "wingloong1",
746
+ "model": "Wing Loong I",
747
+ "manufacturer": "CAIG / CASC",
748
+ "country": "China",
749
+ "side": "adversary",
750
+ "role": "strike/ISR",
751
+ "group": "Group 4",
752
+ "telemetry": [
753
+ "LOS datalink",
754
+ "EO/IR"
755
+ ],
756
+ "specs": {
757
+ "wingspan_m": 14.0,
758
+ "mtow_kg": 1100,
759
+ "endurance_hr": 20,
760
+ "range_km": 4000
761
+ },
762
+ "notes": "Predator-class Chinese export drone.",
763
+ "source": "https://en.wikipedia.org/wiki/Chengdu_Pterodactyl_I"
764
+ },
765
+ {
766
+ "id": "ch4",
767
+ "model": "CH-4 Rainbow",
768
+ "manufacturer": "CASC",
769
+ "country": "China",
770
+ "side": "adversary",
771
+ "role": "strike/ISR",
772
+ "group": "Group 5",
773
+ "telemetry": [
774
+ "SATCOM",
775
+ "EO/IR"
776
+ ],
777
+ "specs": {
778
+ "wingspan_m": 18.0,
779
+ "mtow_kg": 1330,
780
+ "speed_kmh": 235,
781
+ "ceiling_m": 8000,
782
+ "endurance_hr": 40,
783
+ "range_km": 3500
784
+ },
785
+ "notes": "Reaper-class Chinese export; AR-1 missiles.",
786
+ "source": "https://en.wikipedia.org/wiki/CASC_Rainbow"
787
+ },
788
+ {
789
+ "id": "ch5",
790
+ "model": "CH-5 Rainbow",
791
+ "manufacturer": "CASC",
792
+ "country": "China",
793
+ "side": "adversary",
794
+ "role": "strike/ISR",
795
+ "group": "Group 5",
796
+ "telemetry": [
797
+ "SATCOM",
798
+ "EO/IR",
799
+ "SAR"
800
+ ],
801
+ "specs": {
802
+ "wingspan_m": 21.0,
803
+ "mtow_kg": 3300,
804
+ "endurance_hr": 60,
805
+ "range_km": 10000
806
+ },
807
+ "notes": "Large Chinese MALE; 1 t payload.",
808
+ "source": "https://en.wikipedia.org/wiki/CASC_Rainbow"
809
+ },
810
+ {
811
+ "id": "gj2",
812
+ "model": "GJ-2 (Wing Loong II PLA)",
813
+ "manufacturer": "CAIG",
814
+ "country": "China",
815
+ "side": "adversary",
816
+ "role": "strike/ISR",
817
+ "group": "Group 5",
818
+ "telemetry": [
819
+ "SATCOM",
820
+ "EO/IR"
821
+ ],
822
+ "specs": {
823
+ "wingspan_m": 20.5,
824
+ "mtow_kg": 4200,
825
+ "endurance_hr": 20
826
+ },
827
+ "notes": "PLA-operated armed Wing Loong II designation.",
828
+ "source": "https://en.wikipedia.org/wiki/CAIG_Wing_Loong_II"
829
+ },
830
+ {
831
+ "id": "wz7",
832
+ "model": "WZ-7 Soaring Dragon",
833
+ "manufacturer": "Guizhou",
834
+ "country": "China",
835
+ "side": "adversary",
836
+ "role": "HALE ISR",
837
+ "group": "Group 5",
838
+ "telemetry": [
839
+ "SATCOM",
840
+ "SAR",
841
+ "SIGINT"
842
+ ],
843
+ "specs": {
844
+ "wingspan_m": 25.0,
845
+ "ceiling_m": 18000,
846
+ "endurance_hr": 10
847
+ },
848
+ "notes": "Chinese HALE ISR; joined-wing config; Global Hawk analog.",
849
+ "source": "https://en.wikipedia.org/wiki/Guizhou_Soar_Dragon"
850
+ },
851
+ {
852
+ "id": "djimavic3",
853
+ "model": "DJI Mavic 3",
854
+ "manufacturer": "DJI",
855
+ "country": "China",
856
+ "side": "dual-use",
857
+ "role": "commercial ISR / improvised",
858
+ "group": "Group 1",
859
+ "telemetry": [
860
+ "OcuSync RF",
861
+ "DJI AeroScope",
862
+ "Remote ID"
863
+ ],
864
+ "specs": {
865
+ "mass_kg": 0.9,
866
+ "speed_kmh": 75,
867
+ "ceiling_m": 6000,
868
+ "endurance_min": 46,
869
+ "range_km": 15
870
+ },
871
+ "notes": "Dominant small-quad used by BOTH sides in Ukraine; munition drops.",
872
+ "source": "https://en.wikipedia.org/wiki/DJI_Mavic"
873
+ },
874
+ {
875
+ "id": "djimatrice300",
876
+ "model": "DJI Matrice 300 RTK",
877
+ "manufacturer": "DJI",
878
+ "country": "China",
879
+ "side": "dual-use",
880
+ "role": "commercial ISR",
881
+ "group": "Group 1",
882
+ "telemetry": [
883
+ "OcuSync RF",
884
+ "Remote ID"
885
+ ],
886
+ "specs": {
887
+ "mass_kg": 6.3,
888
+ "speed_kmh": 82,
889
+ "endurance_min": 55,
890
+ "range_km": 15
891
+ },
892
+ "notes": "Heavy-lift commercial quad; ISR + payload.",
893
+ "source": "https://en.wikipedia.org/wiki/DJI"
894
+ },
895
+ {
896
+ "id": "djimini",
897
+ "model": "DJI Mini 4 Pro",
898
+ "manufacturer": "DJI",
899
+ "country": "China",
900
+ "side": "dual-use",
901
+ "role": "commercial micro ISR",
902
+ "group": "Group 1",
903
+ "telemetry": [
904
+ "OcuSync RF",
905
+ "Remote ID"
906
+ ],
907
+ "specs": {
908
+ "mass_kg": 0.249,
909
+ "endurance_min": 34,
910
+ "range_km": 10
911
+ },
912
+ "notes": "Sub-250g micro; ubiquitous recon quad.",
913
+ "source": "https://en.wikipedia.org/wiki/DJI_Mini"
914
+ },
915
+ {
916
+ "id": "fpv7in",
917
+ "model": "FPV 7\" racing quad (kamikaze)",
918
+ "manufacturer": "various",
919
+ "country": "multi",
920
+ "side": "dual-use",
921
+ "role": "FPV strike",
922
+ "group": "Group 1",
923
+ "telemetry": [
924
+ "analog/digital VTX",
925
+ "ELRS RF"
926
+ ],
927
+ "specs": {
928
+ "mass_kg": 1.0,
929
+ "speed_kmh": 150,
930
+ "range_km": 10,
931
+ "warhead": "RPG/shaped charge"
932
+ },
933
+ "notes": "Mass-produced FPV kamikaze; defining threat of 2024-2026 attrition warfare.",
934
+ "source": "https://en.wikipedia.org/wiki/First-person_view_(radio_control)"
935
+ },
936
+ {
937
+ "id": "autel",
938
+ "model": "Autel EVO Max 4T",
939
+ "manufacturer": "Autel Robotics",
940
+ "country": "China",
941
+ "side": "dual-use",
942
+ "role": "commercial ISR",
943
+ "group": "Group 1",
944
+ "telemetry": [
945
+ "Autel SkyLink",
946
+ "Remote ID"
947
+ ],
948
+ "specs": {
949
+ "mass_kg": 1.6,
950
+ "endurance_min": 42,
951
+ "range_km": 20
952
+ },
953
+ "notes": "DJI alternative; thermal + visual ISR quad.",
954
+ "source": "https://en.wikipedia.org/wiki/Autel_Robotics"
955
+ },
956
+ {
957
+ "id": "leonidas",
958
+ "model": "Epirus Leonidas (HPM)",
959
+ "manufacturer": "Epirus",
960
+ "country": "USA",
961
+ "side": "counter-uas",
962
+ "role": "directed-energy C-UAS",
963
+ "group": "C-UAS",
964
+ "telemetry": [
965
+ "radar cue",
966
+ "Lattice C2"
967
+ ],
968
+ "specs": {
969
+ "effect": "high-power microwave",
970
+ "target": "swarms",
971
+ "mode": "area electronics kill"
972
+ },
973
+ "notes": "Solid-state HPM; defeats swarms; demoed w/ Anduril Lattice for USMC.",
974
+ "source": "https://www.epirusinc.com/electronic-warfare"
975
+ },
976
+ {
977
+ "id": "lattice",
978
+ "model": "Anduril Lattice",
979
+ "manufacturer": "Anduril",
980
+ "country": "USA",
981
+ "side": "counter-uas",
982
+ "role": "C2 / sensor fusion",
983
+ "group": "C-UAS",
984
+ "telemetry": [
985
+ "multi-sensor fusion",
986
+ "autonomy"
987
+ ],
988
+ "specs": {
989
+ "effect": "detect-track-classify-defeat C2",
990
+ "target": "all groups"
991
+ },
992
+ "notes": "Open C2 mesh tying sensors to effectors for counter-UAS.",
993
+ "source": "https://www.defenseone.com/business/2023/07/defense-startups-team-defeat-swarm-drones/388909/"
994
+ },
995
+ {
996
+ "id": "dronegun",
997
+ "model": "DroneShield DroneGun Mk4",
998
+ "manufacturer": "DroneShield",
999
+ "country": "Australia",
1000
+ "side": "counter-uas",
1001
+ "role": "RF jammer",
1002
+ "group": "C-UAS",
1003
+ "telemetry": [
1004
+ "RF detection"
1005
+ ],
1006
+ "specs": {
1007
+ "effect": "RF/GNSS jam",
1008
+ "range_m": 1000,
1009
+ "target": "Group 1-2"
1010
+ },
1011
+ "notes": "Handheld RF defeat; integrated w/ Epirus.",
1012
+ "source": "https://uasmagazine.com/articles/droneshield-and-epirus-complete-integration"
1013
+ },
1014
+ {
1015
+ "id": "skywiper",
1016
+ "model": "SkyWiper EDM4S",
1017
+ "manufacturer": "NT Service",
1018
+ "country": "Lithuania",
1019
+ "side": "counter-uas",
1020
+ "role": "RF jammer",
1021
+ "group": "C-UAS",
1022
+ "telemetry": [
1023
+ "RF detection"
1024
+ ],
1025
+ "specs": {
1026
+ "effect": "GNSS+control jam",
1027
+ "mass_kg": 5.5,
1028
+ "target": "Group 1"
1029
+ },
1030
+ "notes": "Handheld RF jammer used in Ukraine.",
1031
+ "source": "https://en.wikipedia.org/wiki/Counter-UAS"
1032
+ },
1033
+ {
1034
+ "id": "coyote",
1035
+ "model": "Raytheon Coyote Block 2",
1036
+ "manufacturer": "Raytheon (RTX)",
1037
+ "country": "USA",
1038
+ "side": "counter-uas",
1039
+ "role": "interceptor effector",
1040
+ "group": "C-UAS",
1041
+ "telemetry": [
1042
+ "Ku radar (KuRFS) cue"
1043
+ ],
1044
+ "specs": {
1045
+ "effect": "kinetic interceptor",
1046
+ "target": "Group 1-3"
1047
+ },
1048
+ "notes": "Tube-launched interceptor; LIDS/FS-LIDS effector.",
1049
+ "source": "https://en.wikipedia.org/wiki/Raytheon_Coyote"
1050
+ },
1051
+ {
1052
+ "id": "kargu",
1053
+ "model": "STM Kargu-2",
1054
+ "manufacturer": "STM",
1055
+ "country": "Turkey",
1056
+ "side": "dual-use",
1057
+ "role": "loitering munition (swarm)",
1058
+ "group": "Group 1",
1059
+ "telemetry": [
1060
+ "RF datalink",
1061
+ "EO",
1062
+ "onboard AI"
1063
+ ],
1064
+ "specs": {
1065
+ "mass_kg": 7,
1066
+ "endurance_min": 30,
1067
+ "warhead": "frag/shaped"
1068
+ },
1069
+ "notes": "Rotary loitering munition; cited in UN report on autonomous engagement (Libya 2020).",
1070
+ "source": "https://en.wikipedia.org/wiki/STM_Kargu"
1071
+ },
1072
+ {
1073
+ "id": "switchblade_blackwing",
1074
+ "model": "Blackwing",
1075
+ "manufacturer": "AeroVironment",
1076
+ "country": "USA",
1077
+ "side": "allied",
1078
+ "role": "ISR (Switchblade family)",
1079
+ "group": "Group 1",
1080
+ "telemetry": [
1081
+ "encrypted RF",
1082
+ "EO/IR"
1083
+ ],
1084
+ "specs": {
1085
+ "mass_kg": 1.8,
1086
+ "endurance_min": 60,
1087
+ "range_km": 8
1088
+ },
1089
+ "notes": "Unarmed ISR variant of Switchblade 300; submarine-launchable.",
1090
+ "source": "https://en.wikipedia.org/wiki/AeroVironment_Switchblade"
1091
+ },
1092
+ {
1093
+ "id": "bayraktar_tb3",
1094
+ "model": "Bayraktar TB3",
1095
+ "manufacturer": "Baykar",
1096
+ "country": "Turkey",
1097
+ "side": "dual-use",
1098
+ "role": "strike/ISR (carrier)",
1099
+ "group": "Group 4",
1100
+ "telemetry": [
1101
+ "SATCOM",
1102
+ "EO/IR"
1103
+ ],
1104
+ "specs": {
1105
+ "wingspan_m": 14.0,
1106
+ "mtow_kg": 1450,
1107
+ "endurance_hr": 24
1108
+ },
1109
+ "notes": "Folding-wing carrier-capable TB2 successor.",
1110
+ "source": "https://en.wikipedia.org/wiki/Baykar_Bayraktar_TB3"
1111
+ },
1112
+ {
1113
+ "id": "geran3",
1114
+ "model": "Geran-3 (jet Shahed)",
1115
+ "manufacturer": "Russia (Shahed deriv.)",
1116
+ "country": "Russia",
1117
+ "side": "adversary",
1118
+ "role": "loitering munition",
1119
+ "group": "Group 3",
1120
+ "telemetry": [
1121
+ "INS",
1122
+ "GNSS"
1123
+ ],
1124
+ "specs": {
1125
+ "speed_kmh": 500,
1126
+ "warhead": "~50 kg",
1127
+ "note": "turbojet variant"
1128
+ },
1129
+ "notes": "Jet-powered Shahed-238 derivative; higher terminal speed.",
1130
+ "source": "https://en.wikipedia.org/wiki/Shahed_drones"
1131
+ },
1132
+ {
1133
+ "id": "zala421",
1134
+ "model": "ZALA 421-16E",
1135
+ "manufacturer": "ZALA Aero (Russia)",
1136
+ "country": "Russia",
1137
+ "side": "adversary",
1138
+ "role": "ISR",
1139
+ "group": "Group 2",
1140
+ "telemetry": [
1141
+ "RF datalink",
1142
+ "EO/IR"
1143
+ ],
1144
+ "specs": {
1145
+ "wingspan_m": 2.8,
1146
+ "endurance_hr": 4,
1147
+ "range_km": 50
1148
+ },
1149
+ "notes": "Russian ISR UAV; pairs with Lancet targeting.",
1150
+ "source": "https://en.wikipedia.org/wiki/ZALA_Aero"
1151
+ }
1152
+ ]
static/3d/vendor/VENDOR_MANIFEST.md CHANGED
@@ -99,7 +99,7 @@ sha256 to this table, and extend the `_ALLOW` map in `szl3d_holographic.py`). Do
99
  | Lib | Owner (dev) | Pinned version | Upstream build to vendor | Target path |
100
  |---|---|---|---|---|
101
  | ~~deck.gl~~ | ~~Dev1 (energy), Dev9~~ | `deck.gl@9.0.38` | **DONE — vendored by Dev1** (see "VENDORED NOW (Dev1 energy PR)" above) | `/static/3d/vendor/deck.gl/dist.min.js` |
102
- | CesiumJS | Dev4 (counter-uas) | `cesium@1.123` | `unpkg.com/cesium@1.123/Build/Cesium/Cesium.js` + `Build/Cesium/Workers`, `Assets`, `Widgets` dirs | `/static/3d/vendor/cesium/` |
103
  | 3d-force-graph | Dev2, Dev5 | (reuse) `static-vendor/3d-force-graph.min.js` | already vendored — served at `/vendor/3d-force-graph.min.js` | (reuse) |
104
 
105
  When a dev vendors one of these:
@@ -109,6 +109,38 @@ When a dev vendors one of these:
109
  subtrees like Cesium's `Workers/`, extend `_serve_3d` to allow that prefix).
110
  4. Re-run the selftest harness — the no-CDN grep must stay green.
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  ## License notice
113
 
114
  three.js r170 is MIT (Copyright © 2010-2024 three.js authors). The license text is
 
99
  | Lib | Owner (dev) | Pinned version | Upstream build to vendor | Target path |
100
  |---|---|---|---|---|
101
  | ~~deck.gl~~ | ~~Dev1 (energy), Dev9~~ | `deck.gl@9.0.38` | **DONE — vendored by Dev1** (see "VENDORED NOW (Dev1 energy PR)" above) | `/static/3d/vendor/deck.gl/dist.min.js` |
102
+ | ~~CesiumJS~~ | ~~Dev4 (counter-uas)~~ | ~~`cesium@1.123`~~ | **NOT vendored — Dev4 took the three.js-globe escape hatch (see §Dev4 below).** If a future surface needs the full Cesium globe (3D Terrain / 3D Tiles), vendor `cesium@1.123` `Build/Cesium/{Cesium.js,Workers,Assets,Widgets}` under `/static/3d/vendor/cesium/` and extend `_serve_3d` to allow that prefix. | `/static/3d/vendor/cesium/` |
103
  | 3d-force-graph | Dev2, Dev5 | (reuse) `static-vendor/3d-force-graph.min.js` | already vendored — served at `/vendor/3d-force-graph.min.js` | (reuse) |
104
 
105
  When a dev vendors one of these:
 
109
  subtrees like Cesium's `Workers/`, extend `_serve_3d` to allow that prefix).
110
  4. Re-run the selftest harness — the no-CDN grep must stay green.
111
 
112
+ ## §Dev4 — Counter-UAS / killinchu surface: vendoring decisions
113
+
114
+ The Dev0 contract (§6) offered Dev4 an explicit escape hatch: vendor full CesiumJS@1.123
115
+ **OR** implement a three.js globe (textured sphere + lat/long track plotting). **Dev4 chose
116
+ the three.js globe.** Rationale:
117
+
118
+ - **0 new MB, 0 new CDN risk.** Reuses the already-vendored three.js r170 ESM build above.
119
+ Full Cesium is ~3 MB JS + a `Workers/`+`Assets/`+`Widgets/` subtree (hundreds of files)
120
+ and a separate `CESIUM_BASE_URL` serving contract — heavy for one surface PR.
121
+ - **Sufficient for the technique.** The surface needs a globe + lat/long track entities +
122
+ restricted-airspace SDF volumes + a radar sweep cone + a signed-verdict beam. A procedural
123
+ graticule sphere with `llToVec(lat,lon,alt)` plotting covers all of these without Cesium's
124
+ terrain/imagery tile pipeline. (If a later surface needs real 3D Terrain or 3D Tiles, the
125
+ Cesium TODO row above is preserved for that work.)
126
+ - **No new `_serve_3d` allow-prefix needed** — everything is served by the existing
127
+ `/static/3d/{path}` route + the r170 importmap.
128
+
129
+ ### Data vendored by Dev4 (real, not fabricated)
130
+
131
+ | Path (under `/static/3d/`) | What | Source | Notes |
132
+ |---|---|---|---|
133
+ | `surfaces/data/drones_db.json` | 53 verified drone fingerprints | killinchu repo `drones_db.json` (verified count = 53) | killinchu does not expose this as a JSON HTTP route (its root path serves the Cesium SPA); the surface loads the repo's own DB same-origin. Vendored verbatim, not fabricated. |
134
+
135
+ ### Live-data bridge (server-side, not a vendored lib)
136
+
137
+ The Counter-UAS surface wires to REAL killinchu live data via a same-origin proxy
138
+ (`szl_counter_uas_proxy.py`, registered in `serve.py`) under `/api/a11oy/v1/counter-uas/*`
139
+ → the killinchu Space (`https://szlholdings-killinchu.hf.space`). This is a server-side
140
+ forward (no browser CORS, no CDN script). It degrades gracefully (`{"degraded":true}`) so
141
+ `szl3d_live` renders the honest DEGRADED state. killinchu **senses and evidences; it does
142
+ not defeat** — the surface shows detect/track/classify/evidence + the signed verdict only.
143
+
144
  ## License notice
145
 
146
  three.js r170 is MIT (Copyright © 2010-2024 three.js authors). The license text is