mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-04-23 19:16:06 +02:00
fix: cross-node gate decryption, UI text scaling, aircraft zoom
- Derive gate envelope AES key from gate ID via HKDF so all nodes sharing a gate can decrypt each other's messages (was node-local) - Preserve gate_envelope/reply_to in chain payload normalization - Bump Wormhole modal text from 9-10px to 12-13px - Add aircraft icon zoom interpolation (0.8→2.0 across zoom 5-12) - Reduce Mesh Chat panel text sizes for tighter layout
This commit is contained in:
+4
-5
@@ -3825,11 +3825,10 @@ def _submit_gate_message_envelope(request: Request, gate_id: str, body: dict[str
|
||||
# — doing so would pre-advance the counter and cause append() to reject
|
||||
# the event as a replay, silently dropping the message.
|
||||
#
|
||||
# The chain payload must match the signed payload exactly. The message
|
||||
# was signed WITHOUT the `epoch` field (compose_encrypted_gate_message
|
||||
# excludes it from the signing payload), so we must strip it here too —
|
||||
# otherwise infonet.append() re-verifies the signature against a payload
|
||||
# that includes epoch and gets a mismatch → "invalid signature".
|
||||
# Strip `epoch` — the message was signed without it so including it
|
||||
# would cause a signature mismatch. `gate_envelope` and `reply_to`
|
||||
# are kept in the payload for cross-node decryption; signature
|
||||
# verification in build_signature_payload() strips them automatically.
|
||||
chain_payload = {k: v for k, v in gate_payload.items() if k != "epoch"}
|
||||
chain_event_id = ""
|
||||
try:
|
||||
|
||||
@@ -83,6 +83,11 @@ def build_signature_payload(
|
||||
payload: dict[str, Any],
|
||||
) -> str:
|
||||
normalized = normalize_payload(event_type, payload)
|
||||
# gate_envelope and reply_to ride alongside the signed payload — they are
|
||||
# added after the message is signed so must be excluded from verification.
|
||||
if event_type == "gate_message":
|
||||
for _unsig in ("gate_envelope", "reply_to"):
|
||||
normalized.pop(_unsig, None)
|
||||
payload_json = canonical_json(normalized)
|
||||
return "|".join(
|
||||
[PROTOCOL_VERSION, NETWORK_ID, event_type, node_id, str(sequence), payload_json]
|
||||
|
||||
@@ -58,15 +58,36 @@ MLS_GATE_FORMAT = "mls1"
|
||||
_GATE_ENVELOPE_DOMAIN = "gate_persona"
|
||||
|
||||
|
||||
def _gate_envelope_key() -> bytes:
|
||||
"""Return the 256-bit AES key for gate envelope encryption."""
|
||||
from services.mesh.mesh_secure_storage import _load_domain_key # type: ignore[attr-defined]
|
||||
return _load_domain_key(_GATE_ENVELOPE_DOMAIN)
|
||||
def _gate_envelope_key_shared(gate_id: str) -> bytes:
|
||||
"""Derive a 256-bit AES key from the gate ID via HKDF.
|
||||
|
||||
Every node that knows the gate name derives the same key, enabling
|
||||
cross-node decryption of gate_envelope payloads.
|
||||
"""
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
|
||||
ikm = gate_id.strip().lower().encode("utf-8")
|
||||
return HKDF(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=b"shadowbroker-gate-envelope-v1",
|
||||
info=b"gate_envelope_aes256gcm",
|
||||
).derive(ikm)
|
||||
|
||||
|
||||
def _gate_envelope_key_legacy() -> bytes | None:
|
||||
"""Return the old node-local domain key, or None if unavailable."""
|
||||
try:
|
||||
from services.mesh.mesh_secure_storage import _load_domain_key # type: ignore[attr-defined]
|
||||
return _load_domain_key(_GATE_ENVELOPE_DOMAIN)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _gate_envelope_encrypt(gate_id: str, plaintext: str) -> str:
|
||||
"""Encrypt plaintext under the gate domain key. Returns base64."""
|
||||
key = _gate_envelope_key()
|
||||
"""Encrypt plaintext under the shared gate-derived key. Returns base64."""
|
||||
key = _gate_envelope_key_shared(gate_id)
|
||||
nonce = _os.urandom(12)
|
||||
aad = f"gate_envelope|{gate_id}".encode("utf-8")
|
||||
ct = _AESGCM(key).encrypt(nonce, plaintext.encode("utf-8"), aad)
|
||||
@@ -74,15 +95,24 @@ def _gate_envelope_encrypt(gate_id: str, plaintext: str) -> str:
|
||||
|
||||
|
||||
def _gate_envelope_decrypt(gate_id: str, token: str) -> str | None:
|
||||
"""Decrypt a gate envelope token. Returns plaintext or None on failure."""
|
||||
"""Decrypt a gate envelope token. Tries the shared key first, then
|
||||
falls back to the legacy node-local key for old messages."""
|
||||
try:
|
||||
raw = base64.b64decode(token)
|
||||
if len(raw) < 13:
|
||||
return None
|
||||
nonce, ct = raw[:12], raw[12:]
|
||||
key = _gate_envelope_key()
|
||||
aad = f"gate_envelope|{gate_id}".encode("utf-8")
|
||||
return _AESGCM(key).decrypt(nonce, ct, aad).decode("utf-8")
|
||||
# Try shared (cross-node) key first
|
||||
try:
|
||||
return _AESGCM(_gate_envelope_key_shared(gate_id)).decrypt(nonce, ct, aad).decode("utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
# Fall back to legacy node-local key for pre-migration messages
|
||||
legacy_key = _gate_envelope_key_legacy()
|
||||
if legacy_key:
|
||||
return _AESGCM(legacy_key).decrypt(nonce, ct, aad).decode("utf-8")
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
# Self-echo plaintext cache: MLS cannot decrypt messages authored by the same
|
||||
|
||||
@@ -49,6 +49,15 @@ def normalize_gate_message_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
epoch = _safe_int(payload.get("epoch", 0), 0)
|
||||
if epoch > 0:
|
||||
normalized["epoch"] = epoch
|
||||
# gate_envelope carries cross-node decryptable ciphertext — preserve it
|
||||
# on-chain so receiving nodes can decrypt without MLS key exchange.
|
||||
gate_envelope = str(payload.get("gate_envelope", "") or "").strip()
|
||||
if gate_envelope:
|
||||
normalized["gate_envelope"] = gate_envelope
|
||||
# reply_to is a display-only parent message reference.
|
||||
reply_to = str(payload.get("reply_to", "") or "").strip()
|
||||
if reply_to:
|
||||
normalized["reply_to"] = reply_to
|
||||
return normalized
|
||||
|
||||
|
||||
|
||||
@@ -3234,7 +3234,7 @@ const MaplibreViewer = ({
|
||||
type="symbol"
|
||||
layout={{
|
||||
'icon-image': ['get', 'iconId'],
|
||||
'icon-size': 0.8,
|
||||
'icon-size': ['interpolate', ['linear'], ['zoom'], 5, 0.8, 8, 1.0, 12, 2.0],
|
||||
'icon-allow-overlap': true,
|
||||
'icon-rotate': ['get', 'rotation'],
|
||||
'icon-rotation-alignment': 'map',
|
||||
@@ -3249,7 +3249,7 @@ const MaplibreViewer = ({
|
||||
type="symbol"
|
||||
layout={{
|
||||
'icon-image': ['get', 'iconId'],
|
||||
'icon-size': 0.8,
|
||||
'icon-size': ['interpolate', ['linear'], ['zoom'], 5, 0.8, 8, 1.0, 12, 2.0],
|
||||
'icon-allow-overlap': true,
|
||||
'icon-rotate': ['get', 'rotation'],
|
||||
'icon-rotation-alignment': 'map',
|
||||
@@ -3264,7 +3264,7 @@ const MaplibreViewer = ({
|
||||
type="symbol"
|
||||
layout={{
|
||||
'icon-image': ['get', 'iconId'],
|
||||
'icon-size': 0.8,
|
||||
'icon-size': ['interpolate', ['linear'], ['zoom'], 5, 0.8, 8, 1.0, 12, 2.0],
|
||||
'icon-allow-overlap': true,
|
||||
'icon-rotate': ['get', 'rotation'],
|
||||
'icon-rotation-alignment': 'map',
|
||||
@@ -3279,7 +3279,7 @@ const MaplibreViewer = ({
|
||||
type="symbol"
|
||||
layout={{
|
||||
'icon-image': ['get', 'iconId'],
|
||||
'icon-size': 0.8,
|
||||
'icon-size': ['interpolate', ['linear'], ['zoom'], 5, 0.8, 8, 1.0, 12, 2.0],
|
||||
'icon-allow-overlap': true,
|
||||
'icon-rotate': ['get', 'rotation'],
|
||||
'icon-rotation-alignment': 'map',
|
||||
@@ -3469,7 +3469,7 @@ const MaplibreViewer = ({
|
||||
['==', ['get', 'iconId'], 'svgPotusHeli'],
|
||||
]}
|
||||
paint={{
|
||||
'circle-radius': 18,
|
||||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 5, 18, 8, 22, 12, 40],
|
||||
'circle-color': 'transparent',
|
||||
'circle-stroke-width': 2,
|
||||
'circle-stroke-color': 'gold',
|
||||
@@ -3483,12 +3483,22 @@ const MaplibreViewer = ({
|
||||
layout={{
|
||||
'icon-image': ['get', 'iconId'],
|
||||
'icon-size': [
|
||||
'case',
|
||||
['==', ['get', 'iconId'], 'svgPotusPlane'],
|
||||
1.3,
|
||||
['==', ['get', 'iconId'], 'svgPotusHeli'],
|
||||
1.3,
|
||||
0.8,
|
||||
'interpolate', ['linear'], ['zoom'],
|
||||
5, ['case',
|
||||
['==', ['get', 'iconId'], 'svgPotusPlane'], 1.3,
|
||||
['==', ['get', 'iconId'], 'svgPotusHeli'], 1.3,
|
||||
0.8,
|
||||
],
|
||||
8, ['case',
|
||||
['==', ['get', 'iconId'], 'svgPotusPlane'], 1.6,
|
||||
['==', ['get', 'iconId'], 'svgPotusHeli'], 1.6,
|
||||
1.0,
|
||||
],
|
||||
12, ['case',
|
||||
['==', ['get', 'iconId'], 'svgPotusPlane'], 2.6,
|
||||
['==', ['get', 'iconId'], 'svgPotusHeli'], 2.6,
|
||||
2.0,
|
||||
],
|
||||
],
|
||||
'icon-allow-overlap': true,
|
||||
'icon-rotate': ['get', 'rotation'],
|
||||
@@ -3504,7 +3514,7 @@ const MaplibreViewer = ({
|
||||
type="symbol"
|
||||
layout={{
|
||||
'icon-image': ['get', 'iconId'],
|
||||
'icon-size': 0.8,
|
||||
'icon-size': ['interpolate', ['linear'], ['zoom'], 5, 0.8, 8, 1.0, 12, 2.0],
|
||||
'icon-allow-overlap': true,
|
||||
'icon-rotate': ['get', 'rotation'],
|
||||
'icon-rotation-alignment': 'map',
|
||||
@@ -3545,7 +3555,7 @@ const MaplibreViewer = ({
|
||||
|
||||
{/* HTML labels for UAVs (orange names) */}
|
||||
{uavGeoJSON && !selectedEntity && !isMapInteracting && data?.uavs && (
|
||||
<UavLabels uavs={data.uavs} inView={inView} />
|
||||
<UavLabels uavs={data.uavs} inView={inView} zoom={zoom} />
|
||||
)}
|
||||
|
||||
{/* HTML labels for earthquakes (yellow) - only show when zoomed in (~2000 miles = zoom ~5) */}
|
||||
|
||||
@@ -3944,7 +3944,7 @@ const MeshChat = React.memo(function MeshChat({
|
||||
setActiveTab(tab.key);
|
||||
if (tab.key === 'dms') setDmView('contacts');
|
||||
}}
|
||||
className={`flex-1 flex items-center justify-center gap-1 py-1.5 text-sm font-mono tracking-wider transition-colors ${
|
||||
className={`flex-1 flex items-center justify-center gap-1 py-1.5 text-[12px] font-mono tracking-wider transition-colors ${
|
||||
activeTab === tab.key
|
||||
? 'text-cyan-300 bg-cyan-950/50 font-bold border-b border-cyan-500/50'
|
||||
: 'text-[var(--text-muted)] hover:text-cyan-600 border-b border-cyan-900/20'
|
||||
@@ -4536,7 +4536,7 @@ const MeshChat = React.memo(function MeshChat({
|
||||
value={meshRegion}
|
||||
onChange={(e) => setMeshRegion(e.target.value)}
|
||||
title="Meshtastic MQTT root"
|
||||
className="bg-[var(--bg-secondary)]/50 border border-[var(--border-primary)] text-sm font-mono text-cyan-300 px-2 py-1 outline-none focus:border-cyan-700/50"
|
||||
className="bg-[var(--bg-secondary)]/50 border border-[var(--border-primary)] text-[12px] font-mono text-cyan-300 px-2 py-1 outline-none focus:border-cyan-700/50"
|
||||
style={{ width: '132px' }}
|
||||
>
|
||||
{meshRoots.map((r) => (
|
||||
@@ -4548,7 +4548,7 @@ const MeshChat = React.memo(function MeshChat({
|
||||
<select
|
||||
value={meshChannel}
|
||||
onChange={(e) => setMeshChannel(e.target.value)}
|
||||
className="flex-1 bg-[var(--bg-secondary)]/50 border border-[var(--border-primary)] text-sm font-mono text-green-400 px-2 py-1 outline-none focus:border-cyan-700/50"
|
||||
className="flex-1 bg-[var(--bg-secondary)]/50 border border-[var(--border-primary)] text-[12px] font-mono text-green-400 px-2 py-1 outline-none focus:border-cyan-700/50"
|
||||
>
|
||||
{meshChannels.map((ch) => (
|
||||
<option key={ch} value={ch}>
|
||||
@@ -4561,7 +4561,7 @@ const MeshChat = React.memo(function MeshChat({
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setMeshView('channel')}
|
||||
className={`px-2 py-0.5 text-[13px] font-mono tracking-wider border transition-colors ${
|
||||
className={`px-2 py-0.5 text-[11px] font-mono tracking-wider border transition-colors ${
|
||||
meshView === 'channel'
|
||||
? 'border-green-500/40 text-green-300 bg-green-950/30'
|
||||
: 'border-[var(--border-primary)]/40 text-[var(--text-muted)] hover:text-green-300'
|
||||
@@ -4571,7 +4571,7 @@ const MeshChat = React.memo(function MeshChat({
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMeshView('inbox')}
|
||||
className={`px-2 py-0.5 text-[13px] font-mono tracking-wider border transition-colors ${
|
||||
className={`px-2 py-0.5 text-[11px] font-mono tracking-wider border transition-colors ${
|
||||
meshView === 'inbox'
|
||||
? 'border-amber-500/40 text-amber-300 bg-amber-950/20'
|
||||
: 'border-[var(--border-primary)]/40 text-[var(--text-muted)] hover:text-amber-300'
|
||||
@@ -4580,31 +4580,31 @@ const MeshChat = React.memo(function MeshChat({
|
||||
INBOX
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-[12px] font-mono text-[var(--text-muted)] truncate">
|
||||
<div className="text-[10px] font-mono text-[var(--text-muted)] truncate">
|
||||
{publicMeshAddress ? `ADDR ${publicMeshAddress.toUpperCase()}` : 'NO PUBLIC MESH ADDRESS'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto styled-scrollbar px-3 py-1.5 border-l-2 border-cyan-800/25">
|
||||
{meshView === 'channel' && filteredMeshMessages.length === 0 && (
|
||||
<div className="text-sm font-mono text-[var(--text-muted)] text-center py-4 leading-[1.65]">
|
||||
<div className="text-[12px] font-mono text-[var(--text-muted)] text-center py-4 leading-[1.65]">
|
||||
No messages from {meshRegion} / {meshChannel}
|
||||
</div>
|
||||
)}
|
||||
{meshView === 'inbox' && (
|
||||
<>
|
||||
{!publicMeshAddress && (
|
||||
<div className="text-sm font-mono text-[var(--text-muted)] text-center py-4 leading-[1.65]">
|
||||
<div className="text-[12px] font-mono text-[var(--text-muted)] text-center py-4 leading-[1.65]">
|
||||
Create or load a public mesh identity to see direct Meshtastic traffic.
|
||||
</div>
|
||||
)}
|
||||
{publicMeshAddress && meshInboxMessages.length === 0 && (
|
||||
<div className="text-sm font-mono text-[var(--text-muted)] text-center py-4 leading-[1.65]">
|
||||
<div className="text-[12px] font-mono text-[var(--text-muted)] text-center py-4 leading-[1.65]">
|
||||
No public direct messages addressed to {publicMeshAddress.toUpperCase()} yet.
|
||||
</div>
|
||||
)}
|
||||
{meshInboxMessages.map((m, i) => (
|
||||
<div key={`${m.timestamp}-${i}`} className="py-0.5 leading-[1.65]">
|
||||
<div className="flex items-start gap-1.5 text-sm font-mono">
|
||||
<div className="flex items-start gap-1.5 text-[12px] font-mono">
|
||||
<button
|
||||
onClick={(e) => handleSenderClick(m.from, e, 'meshtastic')}
|
||||
className="text-amber-300 shrink-0 hover:text-amber-200 hover:underline cursor-pointer"
|
||||
@@ -4612,14 +4612,14 @@ const MeshChat = React.memo(function MeshChat({
|
||||
{displayPublicMeshSender(m.from)}
|
||||
</button>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-[12px] text-amber-200/70 mb-0.5">
|
||||
<div className="text-[10px] text-amber-200/70 mb-0.5">
|
||||
TO {publicMeshAddress.toUpperCase()}
|
||||
</div>
|
||||
<div className="break-words whitespace-pre-wrap text-amber-100/90">
|
||||
{m.text}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[var(--text-muted)] shrink-0 text-[13px]">
|
||||
<span className="text-[var(--text-muted)] shrink-0 text-[11px]">
|
||||
{timeAgo(
|
||||
typeof m.timestamp === 'number'
|
||||
? m.timestamp
|
||||
@@ -4634,7 +4634,7 @@ const MeshChat = React.memo(function MeshChat({
|
||||
{meshView === 'channel' &&
|
||||
filteredMeshMessages.map((m, i) => (
|
||||
<div key={`${m.timestamp}-${i}`} className="py-0.5 leading-[1.65]">
|
||||
<div className="flex gap-1.5 text-sm font-mono">
|
||||
<div className="flex gap-1.5 text-[12px] font-mono">
|
||||
<button
|
||||
onClick={(e) => handleSenderClick(m.from, e, 'meshtastic')}
|
||||
className="text-green-400 shrink-0 hover:text-green-300 hover:underline cursor-pointer"
|
||||
@@ -4646,7 +4646,7 @@ const MeshChat = React.memo(function MeshChat({
|
||||
>
|
||||
{m.text}
|
||||
</span>
|
||||
<span className="text-[var(--text-muted)] shrink-0 text-[13px]">
|
||||
<span className="text-[var(--text-muted)] shrink-0 text-[11px]">
|
||||
{timeAgo(
|
||||
typeof m.timestamp === 'number'
|
||||
? m.timestamp
|
||||
|
||||
@@ -876,13 +876,13 @@ export default function TopRightControls({
|
||||
onClick={closeTerminalLauncher}
|
||||
className="absolute inset-0 bg-black/70 backdrop-blur-[2px]"
|
||||
/>
|
||||
<div className="relative z-[1201] w-full max-w-[560px] border border-cyan-700/40 bg-[var(--bg-primary)]/96 backdrop-blur-sm shadow-[0_0_32px_rgba(0,255,255,0.12)]">
|
||||
<div className="relative z-[1201] w-full max-w-[640px] border border-cyan-700/40 bg-[var(--bg-primary)]/96 backdrop-blur-sm shadow-[0_0_32px_rgba(0,255,255,0.12)]">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-cyan-900/30">
|
||||
<div>
|
||||
<div className="text-[10px] font-mono tracking-[0.24em] text-cyan-300">
|
||||
<div className="text-[13px] font-mono tracking-[0.24em] text-cyan-300">
|
||||
INFONET TERMINAL
|
||||
</div>
|
||||
<div className={`mt-1 text-[9px] font-mono ${terminalStatusTone}`}>
|
||||
<div className={`mt-1 text-[11px] font-mono ${terminalStatusTone}`}>
|
||||
{terminalStatusLabel} • {terminalTransportTier}
|
||||
</div>
|
||||
</div>
|
||||
@@ -892,26 +892,26 @@ export default function TopRightControls({
|
||||
className="text-[var(--text-muted)] hover:text-cyan-300 transition-colors"
|
||||
title="Close terminal launcher"
|
||||
>
|
||||
<X size={13} />
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-5 py-5 space-y-4">
|
||||
<div className="border border-cyan-500/20 bg-cyan-950/10 px-4 py-4 text-[10px] font-mono text-cyan-100 leading-[1.8]">
|
||||
<div className="border border-cyan-500/20 bg-cyan-950/10 px-4 py-4 text-[13px] font-mono text-cyan-100 leading-[1.8]">
|
||||
{terminalPrivateReady
|
||||
? 'Enter the Wormhole-facing terminal and sync with the obfuscated Infonet commons?'
|
||||
: 'The terminal runs through Wormhole for obfuscated gates, inbox, and experimental comms.'}
|
||||
<div className="mt-2 text-[9px] text-cyan-200/70 normal-case tracking-normal">
|
||||
<div className="mt-2 text-[12px] text-cyan-200/70 normal-case tracking-normal">
|
||||
{terminalPrivateReady
|
||||
? 'Your obfuscated identity is already provisioned. Entering now keeps the obfuscated lane separate from the public node sync path.'
|
||||
: 'This turns Wormhole on and opens the obfuscated lane. If you already have a Wormhole identity, it reuses it. If you do not, it bootstraps one once and then keeps using it.'}
|
||||
</div>
|
||||
</div>
|
||||
{terminalLaunchError && (
|
||||
<div className="border border-amber-500/40 bg-amber-950/20 px-4 py-3 text-[9px] font-mono text-amber-200 leading-[1.7]">
|
||||
<div className="border border-amber-500/40 bg-amber-950/20 px-4 py-3 text-[12px] font-mono text-amber-200 leading-[1.7]">
|
||||
{terminalLaunchError}
|
||||
</div>
|
||||
)}
|
||||
<div className="border border-cyan-500/20 bg-black/30 px-4 py-4 text-[9px] font-mono text-slate-200 leading-[1.85]">
|
||||
<div className="border border-cyan-500/20 bg-black/30 px-4 py-4 text-[12px] font-mono text-slate-200 leading-[1.85]">
|
||||
<div className="text-cyan-300 tracking-[0.18em]">BEFORE YOU ENTER:</div>
|
||||
<ul className="mt-3 space-y-2 list-disc pl-5">
|
||||
<li>The terminal is for Wormhole, gates, and experimental mail.</li>
|
||||
@@ -919,7 +919,7 @@ export default function TopRightControls({
|
||||
<li>Mesh remains the public perimeter. Wormhole is the obfuscated commons.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="border border-amber-500/20 bg-amber-950/10 px-4 py-3 text-[9px] font-mono text-amber-200/80 leading-[1.85]">
|
||||
<div className="border border-amber-500/20 bg-amber-950/10 px-4 py-3 text-[12px] font-mono text-amber-200/80 leading-[1.85]">
|
||||
<div className="text-amber-300 tracking-[0.18em]">WORMHOLE CLEANUP:</div>
|
||||
<div className="mt-2">
|
||||
Closing the Infonet terminal will shut down Wormhole automatically. If you force-close
|
||||
@@ -934,7 +934,7 @@ export default function TopRightControls({
|
||||
type="button"
|
||||
onClick={() => void activateWormholeAndLaunchTerminal()}
|
||||
disabled={terminalLaunchBusy}
|
||||
className="px-4 py-3 border border-cyan-500/40 bg-cyan-950/20 hover:bg-cyan-950/35 disabled:opacity-50 text-[11px] font-mono text-cyan-300 tracking-[0.16em]"
|
||||
className="px-4 py-3 border border-cyan-500/40 bg-cyan-950/20 hover:bg-cyan-950/35 disabled:opacity-50 text-[13px] font-mono text-cyan-300 tracking-[0.16em]"
|
||||
>
|
||||
{terminalLaunchBusy
|
||||
? 'ENTERING...'
|
||||
@@ -949,7 +949,7 @@ export default function TopRightControls({
|
||||
onMeshChatNavigate?.('meshtastic');
|
||||
}}
|
||||
disabled={terminalLaunchBusy}
|
||||
className="px-4 py-3 border border-[var(--border-primary)] hover:border-cyan-500/40 disabled:opacity-50 text-[11px] font-mono text-[var(--text-muted)] tracking-[0.16em]"
|
||||
className="px-4 py-3 border border-[var(--border-primary)] hover:border-cyan-500/40 disabled:opacity-50 text-[13px] font-mono text-[var(--text-muted)] tracking-[0.16em]"
|
||||
>
|
||||
GO TO MESH
|
||||
</button>
|
||||
@@ -957,7 +957,7 @@ export default function TopRightControls({
|
||||
type="button"
|
||||
onClick={closeTerminalLauncher}
|
||||
disabled={terminalLaunchBusy}
|
||||
className="px-4 py-3 border border-[var(--border-primary)] hover:border-cyan-500/40 disabled:opacity-50 text-[11px] font-mono text-[var(--text-muted)] tracking-[0.16em]"
|
||||
className="px-4 py-3 border border-[var(--border-primary)] hover:border-cyan-500/40 disabled:opacity-50 text-[13px] font-mono text-[var(--text-muted)] tracking-[0.16em]"
|
||||
>
|
||||
CANCEL
|
||||
</button>
|
||||
|
||||
@@ -106,7 +106,7 @@ export function TrackedFlightLabels({
|
||||
style={{
|
||||
...LABEL_BASE,
|
||||
color: labelColor,
|
||||
fontSize: '10px',
|
||||
fontSize: `${Math.max(10, Math.min(16, 10 + (zoom - 5) * 1.2))}px`,
|
||||
textShadow: LABEL_SHADOW_EXTRA,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
@@ -212,9 +212,11 @@ export function TrackedYachtLabels({ ships, inView, interpShip }: TrackedYachtLa
|
||||
interface UavLabelsProps {
|
||||
uavs: UAV[];
|
||||
inView: (lat: number, lng: number) => boolean;
|
||||
zoom?: number;
|
||||
}
|
||||
|
||||
export function UavLabels({ uavs, inView }: UavLabelsProps) {
|
||||
export function UavLabels({ uavs, inView, zoom = 5 }: UavLabelsProps) {
|
||||
const labelSize = `${Math.max(10, Math.min(16, 10 + (zoom - 5) * 1.2))}px`;
|
||||
return (
|
||||
<>
|
||||
{uavs.map((uav, i) => {
|
||||
@@ -234,7 +236,7 @@ export function UavLabels({ uavs, inView }: UavLabelsProps) {
|
||||
style={{
|
||||
...LABEL_BASE,
|
||||
color: '#ff8c00',
|
||||
fontSize: '10px',
|
||||
fontSize: labelSize,
|
||||
textShadow: LABEL_SHADOW_EXTRA,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user