Observability: DEBUG debug-level default, greppable decision tags, log export + UDP LAN sink

Debugging the integration build on real iOS 26.5 devices over the air.

- BitLogger DEBUG default minimumLevel now .debug (was .info) so a
  tap-launched sideload surfaces .debug decision logs without the
  BITCHAT_LOG_LEVEL env var; env still wins; release unchanged (.info,
  emits nothing). Extracted defaultMinimumLevel + a test asserting it.
- Greppable bracket decision tags at .info (visible even at .info) via
  SecureLogger, ID-prefixes only, sanitized:
  [ROUTE] source-route vs flood origination (+reason) + fragment
  targeted-resync; [GW] uplink accept/reject, downlink rebroadcast/drop,
  loop skips, drain-timer; [WIFI] offer/accept/decline, AWDL connect,
  transfer start/complete+bytes, fallback+reason; [PREKEY] seal choice,
  consume, unknown-prekey open fail, bundle ingest, re-gossip;
  [SYNC] one concise summary per cycle + per-request served counts (no
  per-packet spam); [PING]/[TRACE] RTT + computed path.
- New OSLog categories: mesh, gateway, transport.
- Refactored BLESourceRouteOriginationPolicy.route -> decide returning a
  Decision enum (flood(reason)/route(hops)) so the flood reason is
  greppable and unit-tested.
- DEBUG-only in-memory ring buffer (LogExportBuffer, ~2000 lines/512KB,
  thread-safe, off main thread) + App Info "Export Logs" share sheet
  (iOS UIActivityViewController / macOS NSSavePanel), VoiceOver labels.
- DEBUG-only UDP LAN log sink (LogNetworkSink): opt-in, off by default,
  fire-and-forget, never blocks/throws; each line prefixed with the
  device nickname for demux; configured via App Info host:port fields.
  Same sanitized formatted line feeds buffer, sink, and export.

All new code is #if DEBUG; release emits nothing and ships no export/sink UI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-07-06 23:04:33 +02:00
co-authored by Claude Fable 5
parent e0e90af9fd
commit 02aa1d67db
16 changed files with 530 additions and 63 deletions
@@ -32,13 +32,13 @@ struct BLESourceRouteOriginationPolicyTests {
)
}
private func route(
private func decide(
packet: BitchatPacket,
isRecipientConnected: Bool = false,
shouldAttemptRoute: Bool = true,
computedRoute: [Data]? = nil
) -> [Data]? {
BLESourceRouteOriginationPolicy.route(
) -> BLESourceRouteOriginationPolicy.Decision {
BLESourceRouteOriginationPolicy.decide(
for: packet,
to: recipient,
localPeerIDData: localPeerIDData,
@@ -49,38 +49,38 @@ struct BLESourceRouteOriginationPolicyTests {
}
@Test func routesWhenAllGatesPass() {
#expect(route(packet: makePacket()) == [hop])
#expect(decide(packet: makePacket()) == .route([hop]))
}
@Test func relayedPacketNeverGetsRoute() {
let relayed = makePacket(senderID: Data(hexString: "aabbccddeeff0011"))
#expect(route(packet: relayed) == nil)
#expect(decide(packet: relayed) == .flood(.relayedNotOriginator))
}
@Test func broadcastRecipientNeverGetsRoute() {
let broadcast = makePacket(recipientID: Data(repeating: 0xFF, count: 8))
#expect(route(packet: broadcast) == nil)
#expect(decide(packet: broadcast) == .flood(.broadcast))
let noRecipient = makePacket(recipientID: nil)
#expect(route(packet: noRecipient) == nil)
#expect(decide(packet: noRecipient) == .flood(.broadcast))
}
@Test func linkLocalTTLNeverGetsRoute() {
// TTL 0/1 packets (e.g. REQUEST_SYNC) cannot traverse hops.
#expect(route(packet: makePacket(ttl: 0)) == nil)
#expect(route(packet: makePacket(ttl: 1)) == nil)
#expect(decide(packet: makePacket(ttl: 0)) == .flood(.noTTLHeadroom))
#expect(decide(packet: makePacket(ttl: 1)) == .flood(.noTTLHeadroom))
}
@Test func directlyConnectedRecipientNeverGetsRoute() {
#expect(route(packet: makePacket(), isRecipientConnected: true) == nil)
#expect(decide(packet: makePacket(), isRecipientConnected: true) == .flood(.recipientDirect))
}
@Test func suppressedRecipientFallsBackToFlood() {
#expect(route(packet: makePacket(), shouldAttemptRoute: false) == nil)
#expect(decide(packet: makePacket(), shouldAttemptRoute: false) == .flood(.routeSuppressed))
}
@Test func missingOrEmptyRouteFallsBackToFlood() {
var sawComputeRoute = false
let result = BLESourceRouteOriginationPolicy.route(
let result = BLESourceRouteOriginationPolicy.decide(
for: makePacket(),
to: recipient,
localPeerIDData: localPeerIDData,
@@ -91,8 +91,8 @@ struct BLESourceRouteOriginationPolicyTests {
return nil
}
)
#expect(result == nil)
#expect(result == .flood(.noPath))
#expect(sawComputeRoute)
#expect(route(packet: makePacket(), computedRoute: []) == nil)
#expect(decide(packet: makePacket(), computedRoute: []) == .flood(.noPath))
}
}