mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-09-28 03:31:59 +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
|
# — doing so would pre-advance the counter and cause append() to reject
|
||||||
# the event as a replay, silently dropping the message.
|
# the event as a replay, silently dropping the message.
|
||||||
#
|
#
|
||||||
# The chain payload must match the signed payload exactly. The message
|
# Strip `epoch` — the message was signed without it so including it
|
||||||
# was signed WITHOUT the `epoch` field (compose_encrypted_gate_message
|
# would cause a signature mismatch. `gate_envelope` and `reply_to`
|
||||||
# excludes it from the signing payload), so we must strip it here too —
|
# are kept in the payload for cross-node decryption; signature
|
||||||
# otherwise infonet.append() re-verifies the signature against a payload
|
# verification in build_signature_payload() strips them automatically.
|
||||||
# that includes epoch and gets a mismatch → "invalid signature".
|
|
||||||
chain_payload = {k: v for k, v in gate_payload.items() if k != "epoch"}
|
chain_payload = {k: v for k, v in gate_payload.items() if k != "epoch"}
|
||||||
chain_event_id = ""
|
chain_event_id = ""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ def build_signature_payload(
|
|||||||
payload: dict[str, Any],
|
payload: dict[str, Any],
|
||||||
) -> str:
|
) -> str:
|
||||||
normalized = normalize_payload(event_type, payload)
|
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)
|
payload_json = canonical_json(normalized)
|
||||||
return "|".join(
|
return "|".join(
|
||||||
[PROTOCOL_VERSION, NETWORK_ID, event_type, node_id, str(sequence), payload_json]
|
[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"
|
_GATE_ENVELOPE_DOMAIN = "gate_persona"
|
||||||
|
|
||||||
|
|
||||||
def _gate_envelope_key() -> bytes:
|
def _gate_envelope_key_shared(gate_id: str) -> bytes:
|
||||||
"""Return the 256-bit AES key for gate envelope encryption."""
|
"""Derive a 256-bit AES key from the gate ID via HKDF.
|
||||||
from services.mesh.mesh_secure_storage import _load_domain_key # type: ignore[attr-defined]
|
|
||||||
return _load_domain_key(_GATE_ENVELOPE_DOMAIN)
|
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:
|
def _gate_envelope_encrypt(gate_id: str, plaintext: str) -> str:
|
||||||
"""Encrypt plaintext under the gate domain key. Returns base64."""
|
"""Encrypt plaintext under the shared gate-derived key. Returns base64."""
|
||||||
key = _gate_envelope_key()
|
key = _gate_envelope_key_shared(gate_id)
|
||||||
nonce = _os.urandom(12)
|
nonce = _os.urandom(12)
|
||||||
aad = f"gate_envelope|{gate_id}".encode("utf-8")
|
aad = f"gate_envelope|{gate_id}".encode("utf-8")
|
||||||
ct = _AESGCM(key).encrypt(nonce, plaintext.encode("utf-8"), aad)
|
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:
|
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:
|
try:
|
||||||
raw = base64.b64decode(token)
|
raw = base64.b64decode(token)
|
||||||
if len(raw) < 13:
|
if len(raw) < 13:
|
||||||
return None
|
return None
|
||||||
nonce, ct = raw[:12], raw[12:]
|
nonce, ct = raw[:12], raw[12:]
|
||||||
key = _gate_envelope_key()
|
|
||||||
aad = f"gate_envelope|{gate_id}".encode("utf-8")
|
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:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
# Self-echo plaintext cache: MLS cannot decrypt messages authored by the same
|
# 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)
|
epoch = _safe_int(payload.get("epoch", 0), 0)
|
||||||
if epoch > 0:
|
if epoch > 0:
|
||||||
normalized["epoch"] = epoch
|
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
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3234,7 +3234,7 @@ const MaplibreViewer = ({
|
|||||||
type="symbol"
|
type="symbol"
|
||||||
layout={{
|
layout={{
|
||||||
'icon-image': ['get', 'iconId'],
|
'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-allow-overlap': true,
|
||||||
'icon-rotate': ['get', 'rotation'],
|
'icon-rotate': ['get', 'rotation'],
|
||||||
'icon-rotation-alignment': 'map',
|
'icon-rotation-alignment': 'map',
|
||||||
@@ -3249,7 +3249,7 @@ const MaplibreViewer = ({
|
|||||||
type="symbol"
|
type="symbol"
|
||||||
layout={{
|
layout={{
|
||||||
'icon-image': ['get', 'iconId'],
|
'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-allow-overlap': true,
|
||||||
'icon-rotate': ['get', 'rotation'],
|
'icon-rotate': ['get', 'rotation'],
|
||||||
'icon-rotation-alignment': 'map',
|
'icon-rotation-alignment': 'map',
|
||||||
@@ -3264,7 +3264,7 @@ const MaplibreViewer = ({
|
|||||||
type="symbol"
|
type="symbol"
|
||||||
layout={{
|
layout={{
|
||||||
'icon-image': ['get', 'iconId'],
|
'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-allow-overlap': true,
|
||||||
'icon-rotate': ['get', 'rotation'],
|
'icon-rotate': ['get', 'rotation'],
|
||||||
'icon-rotation-alignment': 'map',
|
'icon-rotation-alignment': 'map',
|
||||||
@@ -3279,7 +3279,7 @@ const MaplibreViewer = ({
|
|||||||
type="symbol"
|
type="symbol"
|
||||||
layout={{
|
layout={{
|
||||||
'icon-image': ['get', 'iconId'],
|
'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-allow-overlap': true,
|
||||||
'icon-rotate': ['get', 'rotation'],
|
'icon-rotate': ['get', 'rotation'],
|
||||||
'icon-rotation-alignment': 'map',
|
'icon-rotation-alignment': 'map',
|
||||||
@@ -3469,7 +3469,7 @@ const MaplibreViewer = ({
|
|||||||
['==', ['get', 'iconId'], 'svgPotusHeli'],
|
['==', ['get', 'iconId'], 'svgPotusHeli'],
|
||||||
]}
|
]}
|
||||||
paint={{
|
paint={{
|
||||||
'circle-radius': 18,
|
'circle-radius': ['interpolate', ['linear'], ['zoom'], 5, 18, 8, 22, 12, 40],
|
||||||
'circle-color': 'transparent',
|
'circle-color': 'transparent',
|
||||||
'circle-stroke-width': 2,
|
'circle-stroke-width': 2,
|
||||||
'circle-stroke-color': 'gold',
|
'circle-stroke-color': 'gold',
|
||||||
@@ -3483,12 +3483,22 @@ const MaplibreViewer = ({
|
|||||||
layout={{
|
layout={{
|
||||||
'icon-image': ['get', 'iconId'],
|
'icon-image': ['get', 'iconId'],
|
||||||
'icon-size': [
|
'icon-size': [
|
||||||
'case',
|
'interpolate', ['linear'], ['zoom'],
|
||||||
['==', ['get', 'iconId'], 'svgPotusPlane'],
|
5, ['case',
|
||||||
1.3,
|
['==', ['get', 'iconId'], 'svgPotusPlane'], 1.3,
|
||||||
['==', ['get', 'iconId'], 'svgPotusHeli'],
|
['==', ['get', 'iconId'], 'svgPotusHeli'], 1.3,
|
||||||
1.3,
|
0.8,
|
||||||
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-allow-overlap': true,
|
||||||
'icon-rotate': ['get', 'rotation'],
|
'icon-rotate': ['get', 'rotation'],
|
||||||
@@ -3504,7 +3514,7 @@ const MaplibreViewer = ({
|
|||||||
type="symbol"
|
type="symbol"
|
||||||
layout={{
|
layout={{
|
||||||
'icon-image': ['get', 'iconId'],
|
'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-allow-overlap': true,
|
||||||
'icon-rotate': ['get', 'rotation'],
|
'icon-rotate': ['get', 'rotation'],
|
||||||
'icon-rotation-alignment': 'map',
|
'icon-rotation-alignment': 'map',
|
||||||
@@ -3545,7 +3555,7 @@ const MaplibreViewer = ({
|
|||||||
|
|
||||||
{/* HTML labels for UAVs (orange names) */}
|
{/* HTML labels for UAVs (orange names) */}
|
||||||
{uavGeoJSON && !selectedEntity && !isMapInteracting && data?.uavs && (
|
{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) */}
|
{/* 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);
|
setActiveTab(tab.key);
|
||||||
if (tab.key === 'dms') setDmView('contacts');
|
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
|
activeTab === tab.key
|
||||||
? 'text-cyan-300 bg-cyan-950/50 font-bold border-b border-cyan-500/50'
|
? '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'
|
: '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}
|
value={meshRegion}
|
||||||
onChange={(e) => setMeshRegion(e.target.value)}
|
onChange={(e) => setMeshRegion(e.target.value)}
|
||||||
title="Meshtastic MQTT root"
|
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' }}
|
style={{ width: '132px' }}
|
||||||
>
|
>
|
||||||
{meshRoots.map((r) => (
|
{meshRoots.map((r) => (
|
||||||
@@ -4548,7 +4548,7 @@ const MeshChat = React.memo(function MeshChat({
|
|||||||
<select
|
<select
|
||||||
value={meshChannel}
|
value={meshChannel}
|
||||||
onChange={(e) => setMeshChannel(e.target.value)}
|
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) => (
|
{meshChannels.map((ch) => (
|
||||||
<option key={ch} value={ch}>
|
<option key={ch} value={ch}>
|
||||||
@@ -4561,7 +4561,7 @@ const MeshChat = React.memo(function MeshChat({
|
|||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setMeshView('channel')}
|
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'
|
meshView === 'channel'
|
||||||
? 'border-green-500/40 text-green-300 bg-green-950/30'
|
? 'border-green-500/40 text-green-300 bg-green-950/30'
|
||||||
: 'border-[var(--border-primary)]/40 text-[var(--text-muted)] hover:text-green-300'
|
: '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>
|
||||||
<button
|
<button
|
||||||
onClick={() => setMeshView('inbox')}
|
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'
|
meshView === 'inbox'
|
||||||
? 'border-amber-500/40 text-amber-300 bg-amber-950/20'
|
? 'border-amber-500/40 text-amber-300 bg-amber-950/20'
|
||||||
: 'border-[var(--border-primary)]/40 text-[var(--text-muted)] hover:text-amber-300'
|
: 'border-[var(--border-primary)]/40 text-[var(--text-muted)] hover:text-amber-300'
|
||||||
@@ -4580,31 +4580,31 @@ const MeshChat = React.memo(function MeshChat({
|
|||||||
INBOX
|
INBOX
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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'}
|
{publicMeshAddress ? `ADDR ${publicMeshAddress.toUpperCase()}` : 'NO PUBLIC MESH ADDRESS'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto styled-scrollbar px-3 py-1.5 border-l-2 border-cyan-800/25">
|
<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 && (
|
{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}
|
No messages from {meshRegion} / {meshChannel}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{meshView === 'inbox' && (
|
{meshView === 'inbox' && (
|
||||||
<>
|
<>
|
||||||
{!publicMeshAddress && (
|
{!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.
|
Create or load a public mesh identity to see direct Meshtastic traffic.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{publicMeshAddress && meshInboxMessages.length === 0 && (
|
{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.
|
No public direct messages addressed to {publicMeshAddress.toUpperCase()} yet.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{meshInboxMessages.map((m, i) => (
|
{meshInboxMessages.map((m, i) => (
|
||||||
<div key={`${m.timestamp}-${i}`} className="py-0.5 leading-[1.65]">
|
<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
|
<button
|
||||||
onClick={(e) => handleSenderClick(m.from, e, 'meshtastic')}
|
onClick={(e) => handleSenderClick(m.from, e, 'meshtastic')}
|
||||||
className="text-amber-300 shrink-0 hover:text-amber-200 hover:underline cursor-pointer"
|
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)}
|
{displayPublicMeshSender(m.from)}
|
||||||
</button>
|
</button>
|
||||||
<div className="flex-1 min-w-0">
|
<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()}
|
TO {publicMeshAddress.toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div className="break-words whitespace-pre-wrap text-amber-100/90">
|
<div className="break-words whitespace-pre-wrap text-amber-100/90">
|
||||||
{m.text}
|
{m.text}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[var(--text-muted)] shrink-0 text-[13px]">
|
<span className="text-[var(--text-muted)] shrink-0 text-[11px]">
|
||||||
{timeAgo(
|
{timeAgo(
|
||||||
typeof m.timestamp === 'number'
|
typeof m.timestamp === 'number'
|
||||||
? m.timestamp
|
? m.timestamp
|
||||||
@@ -4634,7 +4634,7 @@ const MeshChat = React.memo(function MeshChat({
|
|||||||
{meshView === 'channel' &&
|
{meshView === 'channel' &&
|
||||||
filteredMeshMessages.map((m, i) => (
|
filteredMeshMessages.map((m, i) => (
|
||||||
<div key={`${m.timestamp}-${i}`} className="py-0.5 leading-[1.65]">
|
<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
|
<button
|
||||||
onClick={(e) => handleSenderClick(m.from, e, 'meshtastic')}
|
onClick={(e) => handleSenderClick(m.from, e, 'meshtastic')}
|
||||||
className="text-green-400 shrink-0 hover:text-green-300 hover:underline cursor-pointer"
|
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}
|
{m.text}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[var(--text-muted)] shrink-0 text-[13px]">
|
<span className="text-[var(--text-muted)] shrink-0 text-[11px]">
|
||||||
{timeAgo(
|
{timeAgo(
|
||||||
typeof m.timestamp === 'number'
|
typeof m.timestamp === 'number'
|
||||||
? m.timestamp
|
? m.timestamp
|
||||||
|
|||||||
@@ -876,13 +876,13 @@ export default function TopRightControls({
|
|||||||
onClick={closeTerminalLauncher}
|
onClick={closeTerminalLauncher}
|
||||||
className="absolute inset-0 bg-black/70 backdrop-blur-[2px]"
|
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 className="flex items-center justify-between px-4 py-3 border-b border-cyan-900/30">
|
||||||
<div>
|
<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
|
INFONET TERMINAL
|
||||||
</div>
|
</div>
|
||||||
<div className={`mt-1 text-[9px] font-mono ${terminalStatusTone}`}>
|
<div className={`mt-1 text-[11px] font-mono ${terminalStatusTone}`}>
|
||||||
{terminalStatusLabel} • {terminalTransportTier}
|
{terminalStatusLabel} • {terminalTransportTier}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -892,26 +892,26 @@ export default function TopRightControls({
|
|||||||
className="text-[var(--text-muted)] hover:text-cyan-300 transition-colors"
|
className="text-[var(--text-muted)] hover:text-cyan-300 transition-colors"
|
||||||
title="Close terminal launcher"
|
title="Close terminal launcher"
|
||||||
>
|
>
|
||||||
<X size={13} />
|
<X size={16} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-5 py-5 space-y-4">
|
<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
|
{terminalPrivateReady
|
||||||
? 'Enter the Wormhole-facing terminal and sync with the obfuscated Infonet commons?'
|
? 'Enter the Wormhole-facing terminal and sync with the obfuscated Infonet commons?'
|
||||||
: 'The terminal runs through Wormhole for obfuscated gates, inbox, and experimental comms.'}
|
: '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
|
{terminalPrivateReady
|
||||||
? 'Your obfuscated identity is already provisioned. Entering now keeps the obfuscated lane separate from the public node sync path.'
|
? '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.'}
|
: '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>
|
||||||
</div>
|
</div>
|
||||||
{terminalLaunchError && (
|
{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}
|
{terminalLaunchError}
|
||||||
</div>
|
</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>
|
<div className="text-cyan-300 tracking-[0.18em]">BEFORE YOU ENTER:</div>
|
||||||
<ul className="mt-3 space-y-2 list-disc pl-5">
|
<ul className="mt-3 space-y-2 list-disc pl-5">
|
||||||
<li>The terminal is for Wormhole, gates, and experimental mail.</li>
|
<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>
|
<li>Mesh remains the public perimeter. Wormhole is the obfuscated commons.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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="text-amber-300 tracking-[0.18em]">WORMHOLE CLEANUP:</div>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
Closing the Infonet terminal will shut down Wormhole automatically. If you force-close
|
Closing the Infonet terminal will shut down Wormhole automatically. If you force-close
|
||||||
@@ -934,7 +934,7 @@ export default function TopRightControls({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => void activateWormholeAndLaunchTerminal()}
|
onClick={() => void activateWormholeAndLaunchTerminal()}
|
||||||
disabled={terminalLaunchBusy}
|
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
|
{terminalLaunchBusy
|
||||||
? 'ENTERING...'
|
? 'ENTERING...'
|
||||||
@@ -949,7 +949,7 @@ export default function TopRightControls({
|
|||||||
onMeshChatNavigate?.('meshtastic');
|
onMeshChatNavigate?.('meshtastic');
|
||||||
}}
|
}}
|
||||||
disabled={terminalLaunchBusy}
|
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
|
GO TO MESH
|
||||||
</button>
|
</button>
|
||||||
@@ -957,7 +957,7 @@ export default function TopRightControls({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={closeTerminalLauncher}
|
onClick={closeTerminalLauncher}
|
||||||
disabled={terminalLaunchBusy}
|
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
|
CANCEL
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export function TrackedFlightLabels({
|
|||||||
style={{
|
style={{
|
||||||
...LABEL_BASE,
|
...LABEL_BASE,
|
||||||
color: labelColor,
|
color: labelColor,
|
||||||
fontSize: '10px',
|
fontSize: `${Math.max(10, Math.min(16, 10 + (zoom - 5) * 1.2))}px`,
|
||||||
textShadow: LABEL_SHADOW_EXTRA,
|
textShadow: LABEL_SHADOW_EXTRA,
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
@@ -212,9 +212,11 @@ export function TrackedYachtLabels({ ships, inView, interpShip }: TrackedYachtLa
|
|||||||
interface UavLabelsProps {
|
interface UavLabelsProps {
|
||||||
uavs: UAV[];
|
uavs: UAV[];
|
||||||
inView: (lat: number, lng: number) => boolean;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{uavs.map((uav, i) => {
|
{uavs.map((uav, i) => {
|
||||||
@@ -234,7 +236,7 @@ export function UavLabels({ uavs, inView }: UavLabelsProps) {
|
|||||||
style={{
|
style={{
|
||||||
...LABEL_BASE,
|
...LABEL_BASE,
|
||||||
color: '#ff8c00',
|
color: '#ff8c00',
|
||||||
fontSize: '10px',
|
fontSize: labelSize,
|
||||||
textShadow: LABEL_SHADOW_EXTRA,
|
textShadow: LABEL_SHADOW_EXTRA,
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user