From b5c2177edc11324f57e9c22145f8f336cdadacc5 Mon Sep 17 00:00:00 2001 From: blessdyb Date: Sun, 27 Sep 2026 01:57:26 -0700 Subject: [PATCH 1/2] Coverage: what Flowlight can't account for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every other screen answers what happened. This one answers what you would not have been told, which decides how much the others are worth. The app claims it sees every connection, and then honest paragraphs elsewhere list the exceptions — traffic from before capture started, system services content filters are never shown, certificate pinning, QUIC, an agent's MCP server on a pipe — so reading any screen correctly meant having read the documentation first. Three questions per app, kept separate because they fail separately. Whether it was *seen*: the filter sees flows as they open, the sampler reads counters once a second and misses whole connections between readings, which is a gap no percentage can express and so is said in words. Whether it was *named*: the share of that app's bytes, not connections, whose destination had a hostname — one unnamed connection carrying a gigabyte is a bigger hole than a hundred carrying a kilobyte, and counting connections ranks them backwards. And whether it was *readable*: being on the Never decrypted list is a choice, inspection being off is a setting, and only "never reached the proxy" is a gap. What no engine sees at all is on the screen rather than in a footnote, because a coverage figure that counted only what it could see would be a reassurance rather than a fact. Adding a tenth screen also found a crash that had been waiting: the sidebar derived ⌘1…⌘9 from each item's position, so the tenth produced `Character("10")` and trapped on launch. It is ⌘1…⌘9 then ⌘0 now, as browsers number tabs, and nil past that. Co-Authored-By: Claude Opus 5 (1M context) --- Flowlight/Analysis/Coverage.swift | 106 ++++++++++ Flowlight/App/AppNavigation.swift | 16 +- Flowlight/App/FlowlightApp.swift | 3 +- .../Localization/de.lproj/Localizable.strings | 26 +++ .../Localization/en.lproj/Localizable.strings | 26 +++ .../Localization/es.lproj/Localizable.strings | 26 +++ .../Localization/fr.lproj/Localizable.strings | 26 +++ .../Localization/it.lproj/Localizable.strings | 26 +++ .../Localization/ja.lproj/Localizable.strings | 26 +++ .../Localization/ko.lproj/Localizable.strings | 26 +++ .../pt-PT.lproj/Localizable.strings | 26 +++ .../zh-Hans.lproj/Localizable.strings | 26 +++ .../zh-Hant.lproj/Localizable.strings | 26 +++ Flowlight/UI/ContentView.swift | 1 + Flowlight/UI/CoverageView.swift | 185 ++++++++++++++++++ FlowlightTests/CoverageTests.swift | 75 +++++++ docs/docs/index.html | 20 +- docs/llms-full.txt | 25 ++- docs/sitemap.xml | 2 +- site/pages/docs.html | 20 +- 20 files changed, 705 insertions(+), 8 deletions(-) create mode 100644 Flowlight/Analysis/Coverage.swift create mode 100644 Flowlight/UI/CoverageView.swift create mode 100644 FlowlightTests/CoverageTests.swift diff --git a/Flowlight/Analysis/Coverage.swift b/Flowlight/Analysis/Coverage.swift new file mode 100644 index 0000000..bec3684 --- /dev/null +++ b/Flowlight/Analysis/Coverage.swift @@ -0,0 +1,106 @@ +import Foundation + +/// How much of one app's traffic Flowlight can actually account for, and what is missing. +/// +/// The app says it sees every connection, and then the honest paragraphs elsewhere list the exceptions: +/// traffic from before the filter started, system services a content filter never sees, a Mac where another +/// filter owns the only slot, apps that pin their certificates, QUIC the proxy is never offered, an agent's +/// MCP server talking over a pipe. All of that is true and all of it is somewhere else, so reading any screen +/// correctly means having read the documentation first. +/// +/// This turns it into a per-app answer. Three separate questions, because they fail independently: is the +/// traffic *seen* at all, is the destination *named*, and is the content *readable*. An app can be fully seen +/// and entirely unnamed, or named and unreadable, and lumping those together is how a coverage number becomes +/// a reassurance rather than a fact. +struct AppCoverage: Identifiable, Equatable, Sendable { + var bundleID: String + var appName: String + var bytes: Int64 + /// How its flows reach Flowlight. + var capture: Capture + /// The share of this app's bytes whose destination has a hostname rather than a bare address. + var named: Double + /// Whether anything of this app's has been decrypted, and why not when it hasn't. + var inspection: Inspection + var id: String { bundleID } + + enum Capture: Equatable, Sendable { + /// The Network Extension sees flows as they open. + case filter + /// The sampler reads counters once a second: a connection that opens and closes between two readings + /// is never counted, which is a gap no percentage can show. + case sampler + /// The filter is the chosen source but isn't delivering — another content filter holds the slot, or it + /// hasn't been installed — so what is on screen came from the sampler instead. + case fellBack + case demo + } + + enum Inspection: Equatable, Sendable { + /// Not turned on. Nothing is decrypted for anyone. + case off + /// On, and this app's requests are being read. + case reading + /// On, but nothing of this app's has arrived: it was started without the proxy, or it ignores proxy + /// settings, or it pins its certificates and was passed through untouched. + case notRouted + /// Deliberately excluded — Apple services, password managers, anything on the never-inspect list. + case excluded + } + + /// Whether anything here is worth a user's attention. An app that is seen, named and either read or not + /// meant to be read is covered; everything else has a gap worth naming. + var isComplete: Bool { + capture == .filter && named > 0.99 && (inspection == .reading || inspection == .excluded || inspection == .off) + } +} + +enum CoverageReport { + /// Builds one row per app from a report's breakdown, the apps whose traffic has been decrypted, and the + /// state of the two engines. + /// + /// `named` is computed over bytes rather than flows on purpose: one unnamed connection carrying a gigabyte + /// is a bigger hole than a hundred unnamed connections carrying a kilobyte each, and a count would rank + /// them the other way round. + static func build(rows: [BreakdownRow], inspected: Set, excluded: Set, + mode: CaptureMode, fellBack: Bool, inspectionOn: Bool, isDemo: Bool, + limit: Int = 60) -> [AppCoverage] { + var bytes: [String: Int64] = [:] + var namedBytes: [String: Int64] = [:] + var names: [String: String] = [:] + for row in rows where !row.bundleID.isEmpty { + bytes[row.bundleID, default: 0] += row.counters.total + if !row.domain.isEmpty { namedBytes[row.bundleID, default: 0] += row.counters.total } + if names[row.bundleID] == nil || names[row.bundleID]?.isEmpty == true { + names[row.bundleID] = row.appName.isEmpty ? row.bundleID : row.appName + } + } + let capture: AppCoverage.Capture = isDemo ? .demo + : mode == .nettop ? .sampler : (fellBack ? .fellBack : .filter) + + return bytes.map { bundleID, total in + let inspection: AppCoverage.Inspection + if isDemo || !inspectionOn { inspection = .off } + else if inspected.contains(bundleID) { inspection = .reading } + else if excluded.contains(bundleID) { inspection = .excluded } + else { inspection = .notRouted } + return AppCoverage(bundleID: bundleID, appName: names[bundleID] ?? bundleID, bytes: total, + capture: capture, + named: total > 0 ? Double(namedBytes[bundleID] ?? 0) / Double(total) : 0, + inspection: inspection) + } + // Biggest first: a gap matters in proportion to what is going through it. + .sorted { ($0.bytes, $1.appName) > ($1.bytes, $0.appName) } + .prefix(limit) + .map { $0 } + } + + /// The share of all bytes in the report that sit behind a complete row. Deliberately not an average of the + /// per-app percentages: an app moving a gigabyte and an app moving a kilobyte are not half the picture each. + static func overall(_ rows: [AppCoverage]) -> Double { + let total = rows.reduce(Int64(0)) { $0 + $1.bytes } + guard total > 0 else { return 0 } + let complete = rows.filter(\.isComplete).reduce(Int64(0)) { $0 + $1.bytes } + return Double(complete) / Double(total) + } +} diff --git a/Flowlight/App/AppNavigation.swift b/Flowlight/App/AppNavigation.swift index 62af7e2..6e281cf 100644 --- a/Flowlight/App/AppNavigation.swift +++ b/Flowlight/App/AppNavigation.swift @@ -1,7 +1,7 @@ import SwiftUI enum SidebarItem: String, CaseIterable, Identifiable { - case live, agents, reports, alerts, rules, ask, inspect, devices, capture + case live, agents, reports, alerts, rules, ask, inspect, devices, coverage, capture var id: String { rawValue } /// The part of the documentation that explains this screen. /// @@ -18,6 +18,7 @@ enum SidebarItem: String, CaseIterable, Identifiable { case .ask: return "ask" case .inspect: return "inspection" case .devices: return "devices" + case .coverage: return "coverage" case .capture: return "capture" } } @@ -34,6 +35,7 @@ enum SidebarItem: String, CaseIterable, Identifiable { case .ask: return "Ask" case .inspect: return "Inspect" case .devices: return "Devices" + case .coverage: return "Coverage" case .capture: return "Capture" } } @@ -47,6 +49,7 @@ enum SidebarItem: String, CaseIterable, Identifiable { case .rules: return L("Rules") case .ask: return L("Ask") case .devices: return L("Devices") + case .coverage: return L("Coverage") case .inspect: return L("Inspect") case .capture: return L("Capture") } @@ -60,18 +63,25 @@ enum SidebarItem: String, CaseIterable, Identifiable { case .rules: return "hand.raised" case .ask: return "text.bubble" case .devices: return "dot.radiowaves.left.and.right" + case .coverage: return "circle.dashed.inset.filled" case .inspect: return "lock.open.display" case .capture: return "antenna.radiowaves.left.and.right" } } - var shortcut: KeyEquivalent { KeyEquivalent(Character(String((Self.allCases.firstIndex(of: self) ?? 0) + 1))) } + /// ⌘1…⌘9 and then ⌘0, the way every browser numbers its tabs. Nil past the tenth: there is no eleventh + /// digit, and the arithmetic that assumed there was turned `10` into a `Character` and trapped the moment + /// a tenth screen was added. + var shortcut: KeyEquivalent? { + guard let index = Self.allCases.firstIndex(of: self), index < 10 else { return nil } + return KeyEquivalent(Character("\(index == 9 ? 0 : index + 1)")) + } var section: SidebarSection { switch self { case .live, .agents, .reports, .alerts: return .traffic case .ask, .inspect: return .investigate case .rules: return .control - case .devices, .capture: return .sources + case .devices, .coverage, .capture: return .sources } } } diff --git a/Flowlight/App/FlowlightApp.swift b/Flowlight/App/FlowlightApp.swift index 88e1d8e..7cfd786 100644 --- a/Flowlight/App/FlowlightApp.swift +++ b/Flowlight/App/FlowlightApp.swift @@ -85,7 +85,8 @@ struct FlowlightApp: App { } CommandGroup(before: .sidebar) { ForEach(SidebarItem.allCases) { item in - Button(item.title) { nav.selection = item }.keyboardShortcut(item.shortcut, modifiers: .command) + Button(item.title) { nav.selection = item } + .keyboardShortcut(item.shortcut ?? .init(" "), modifiers: item.shortcut == nil ? [] : .command) } Divider() Button(focus.isOn ? L("Turn Focus Off") : L("Turn Focus On")) { focus.isOn.toggle() } diff --git a/Flowlight/Localization/de.lproj/Localizable.strings b/Flowlight/Localization/de.lproj/Localizable.strings index f31a9a1..d95f106 100644 --- a/Flowlight/Localization/de.lproj/Localizable.strings +++ b/Flowlight/Localization/de.lproj/Localizable.strings @@ -70,6 +70,7 @@ "Rules" = "Regeln"; "Ask" = "Fragen"; "Devices" = "Geräte"; +"Coverage" = "Abdeckung"; "Inspect" = "Inspizieren"; "Capture" = "Erfassung"; "Traffic" = "Datenverkehr"; @@ -181,6 +182,7 @@ "The provider returned HTTP %lld. %@" = "Der Anbieter hat HTTP %lld zurückgegeben. %@"; "The provider's answer wasn't in a shape Flowlight could read." = "Die Antwort des Anbieters hatte keine Form, die Flowlight lesen konnte."; "No endpoint is set for %@." = "Für %@ ist kein Endpunkt festgelegt."; +"%@ is not an https:// address. A key and your question would cross the network in the clear, so Flowlight won't send them." = "%@ ist keine https://-Adresse. Ein Schlüssel und Ihre Frage würden im Klartext über das Netzwerk laufen, deshalb sendet Flowlight sie nicht."; "%@ needs an API key. Add one in Settings — it goes to your login Keychain." = "%@ benötigt einen API-Schlüssel. Fügen Sie in den Einstellungen einen hinzu – er wird im Anmeldeschlüsselbund gespeichert."; "The on-device model isn't reached over HTTP." = "Das Modell auf dem Gerät wird nicht über HTTP erreicht."; "I ran out of steps before I could answer that. Try asking something narrower." = "Die Schritte waren aufgebraucht, bevor eine Antwort möglich war. Fragen Sie etwas Engeres."; @@ -675,6 +677,30 @@ "Error" = "Fehler"; "OK" = "OK"; +/* UI/CoverageView.swift */ +"Nothing recorded yet" = "Noch nichts aufgezeichnet"; +"Coverage is worked out from traffic Flowlight has already recorded. Leave it running for a moment." = "Die Abdeckung wird aus dem Datenverkehr berechnet, den Flowlight schon aufgezeichnet hat. Lassen Sie es einen Moment laufen."; +"%@ of traffic fully accounted for" = "%@ des Datenverkehrs vollständig erfasst"; +"How much of this Mac is covered" = "Wie viel von diesem Mac abgedeckt ist"; +"HTTPS inspection is on" = "Die HTTPS-Inspektion ist ein"; +"HTTPS inspection is off" = "Die HTTPS-Inspektion ist aus"; +"What an app sent is readable only where the app was routed through the proxy. Anything else is counted and named, but its contents were never offered to Flowlight." = "Was eine App gesendet hat, ist nur dort lesbar, wo die App über den Proxy geleitet wurde. Alles andere wird gezählt und benannt, aber sein Inhalt wurde Flowlight nie angeboten."; +"Flowlight sees which connections were made and where to, but not what was inside them. Nothing is decrypted until you turn inspection on." = "Flowlight sieht, welche Verbindungen zustande kamen und wohin, aber nicht, was darin war. Es wird nichts entschlüsselt, solange Sie die Inspektion nicht einschalten."; +"What no engine can see" = "Was keine Engine sehen kann"; +"Traffic from before capture started, system services that content filters are never shown, apps that pin their certificates, QUIC the proxy is never offered, and an agent's local MCP server talking over a pipe. None of these appear anywhere in Flowlight, so they are absent from these figures too." = "Datenverkehr von vor dem Start der Erfassung, Systemdienste, die Inhaltsfiltern nie gezeigt werden, Apps, die ihre Zertifikate pinnen, QUIC, das dem Proxy nie angeboten wird, und der lokale MCP-Server eines Agenten, der über eine Pipe spricht. Nichts davon erscheint irgendwo in Flowlight, also fehlt es auch in diesen Zahlen."; +"Sampling with nettop, not the filter" = "Abtastung mit nettop, nicht mit dem Filter"; +"The filter sees connections as they open" = "Der Filter sieht Verbindungen beim Öffnen"; +"Every eligible TCP and UDP flow is attributed as it opens, including connections too short for a sampler to catch." = "Jeder geeignete TCP- und UDP-Flow wird beim Öffnen zugeordnet, auch Verbindungen, die für den Sampler zu kurz sind."; +"The filter isn't answering, so these figures come from the sampler: a connection that opens and closes between two readings is missing entirely, and no percentage below can show it." = "Der Filter antwortet nicht, diese Zahlen stammen also vom Sampler: Eine Verbindung, die sich zwischen zwei Messungen öffnet und wieder schließt, fehlt ganz, und kein Prozentwert unten kann sie zeigen."; +"Byte counts are exact, but a connection that opens and closes between two readings is never recorded — a quick DNS lookup, a fast API call, a script that runs curl and exits. Only the filter sees those." = "Die Byte-Zähler sind genau, aber eine Verbindung, die sich zwischen zwei Messungen öffnet und wieder schließt, wird nie aufgezeichnet – eine schnelle DNS-Abfrage, ein kurzer API-Aufruf, ein Skript, das curl ausführt und endet. Nur der Filter sieht das."; +"By app" = "Nach App"; +"Share of this app's bytes whose destination has a hostname rather than a bare address." = "Anteil der Bytes dieser App, deren Ziel einen Hostnamen hat und nicht bloß eine Adresse."; +"%@ of its bytes went to addresses with no hostname" = "%@ ihrer Bytes gingen an Adressen ohne Hostnamen"; +"nothing of its traffic reached the proxy, so no contents were read" = "nichts von ihrem Datenverkehr erreichte den Proxy, es wurden also keine Inhalte gelesen"; +"it is on the Never decrypted list, so its contents are deliberately not read" = "sie steht auf der Liste „Nie entschlüsselt“, ihre Inhalte werden also absichtlich nicht gelesen"; +"short connections may be missing entirely" = "kurze Verbindungen können ganz fehlen"; +"Seen as it happened, named, and readable." = "Beim Entstehen gesehen, benannt und lesbar."; + /* UI/DevicesView.swift */ "USB" = "USB"; "Not watching" = "Nicht überwacht"; diff --git a/Flowlight/Localization/en.lproj/Localizable.strings b/Flowlight/Localization/en.lproj/Localizable.strings index 4e74a19..58ed206 100644 --- a/Flowlight/Localization/en.lproj/Localizable.strings +++ b/Flowlight/Localization/en.lproj/Localizable.strings @@ -72,6 +72,7 @@ "Rules" = "Rules"; "Ask" = "Ask"; "Devices" = "Devices"; +"Coverage" = "Coverage"; "Inspect" = "Inspect"; "Capture" = "Capture"; "Traffic" = "Traffic"; @@ -183,6 +184,7 @@ "The provider returned HTTP %lld. %@" = "The provider returned HTTP %lld. %@"; "The provider's answer wasn't in a shape Flowlight could read." = "The provider's answer wasn't in a shape Flowlight could read."; "No endpoint is set for %@." = "No endpoint is set for %@."; +"%@ is not an https:// address. A key and your question would cross the network in the clear, so Flowlight won't send them." = "%@ is not an https:// address. A key and your question would cross the network in the clear, so Flowlight won't send them."; "%@ needs an API key. Add one in Settings — it goes to your login Keychain." = "%@ needs an API key. Add one in Settings — it goes to your login Keychain."; "The on-device model isn't reached over HTTP." = "The on-device model isn't reached over HTTP."; "I ran out of steps before I could answer that. Try asking something narrower." = "I ran out of steps before I could answer that. Try asking something narrower."; @@ -677,6 +679,30 @@ "Error" = "Error"; "OK" = "OK"; +/* UI/CoverageView.swift */ +"Nothing recorded yet" = "Nothing recorded yet"; +"Coverage is worked out from traffic Flowlight has already recorded. Leave it running for a moment." = "Coverage is worked out from traffic Flowlight has already recorded. Leave it running for a moment."; +"%@ of traffic fully accounted for" = "%@ of traffic fully accounted for"; +"How much of this Mac is covered" = "How much of this Mac is covered"; +"HTTPS inspection is on" = "HTTPS inspection is on"; +"HTTPS inspection is off" = "HTTPS inspection is off"; +"What an app sent is readable only where the app was routed through the proxy. Anything else is counted and named, but its contents were never offered to Flowlight." = "What an app sent is readable only where the app was routed through the proxy. Anything else is counted and named, but its contents were never offered to Flowlight."; +"Flowlight sees which connections were made and where to, but not what was inside them. Nothing is decrypted until you turn inspection on." = "Flowlight sees which connections were made and where to, but not what was inside them. Nothing is decrypted until you turn inspection on."; +"What no engine can see" = "What no engine can see"; +"Traffic from before capture started, system services that content filters are never shown, apps that pin their certificates, QUIC the proxy is never offered, and an agent's local MCP server talking over a pipe. None of these appear anywhere in Flowlight, so they are absent from these figures too." = "Traffic from before capture started, system services that content filters are never shown, apps that pin their certificates, QUIC the proxy is never offered, and an agent's local MCP server talking over a pipe. None of these appear anywhere in Flowlight, so they are absent from these figures too."; +"Sampling with nettop, not the filter" = "Sampling with nettop, not the filter"; +"The filter sees connections as they open" = "The filter sees connections as they open"; +"Every eligible TCP and UDP flow is attributed as it opens, including connections too short for a sampler to catch." = "Every eligible TCP and UDP flow is attributed as it opens, including connections too short for a sampler to catch."; +"The filter isn't answering, so these figures come from the sampler: a connection that opens and closes between two readings is missing entirely, and no percentage below can show it." = "The filter isn't answering, so these figures come from the sampler: a connection that opens and closes between two readings is missing entirely, and no percentage below can show it."; +"Byte counts are exact, but a connection that opens and closes between two readings is never recorded — a quick DNS lookup, a fast API call, a script that runs curl and exits. Only the filter sees those." = "Byte counts are exact, but a connection that opens and closes between two readings is never recorded — a quick DNS lookup, a fast API call, a script that runs curl and exits. Only the filter sees those."; +"By app" = "By app"; +"Share of this app's bytes whose destination has a hostname rather than a bare address." = "Share of this app's bytes whose destination has a hostname rather than a bare address."; +"%@ of its bytes went to addresses with no hostname" = "%@ of its bytes went to addresses with no hostname"; +"nothing of its traffic reached the proxy, so no contents were read" = "nothing of its traffic reached the proxy, so no contents were read"; +"it is on the Never decrypted list, so its contents are deliberately not read" = "it is on the Never decrypted list, so its contents are deliberately not read"; +"short connections may be missing entirely" = "short connections may be missing entirely"; +"Seen as it happened, named, and readable." = "Seen as it happened, named, and readable."; + /* UI/DevicesView.swift */ "USB" = "USB"; "Not watching" = "Not watching"; diff --git a/Flowlight/Localization/es.lproj/Localizable.strings b/Flowlight/Localization/es.lproj/Localizable.strings index 87282be..a195860 100644 --- a/Flowlight/Localization/es.lproj/Localizable.strings +++ b/Flowlight/Localization/es.lproj/Localizable.strings @@ -70,6 +70,7 @@ "Rules" = "Reglas"; "Ask" = "Preguntar"; "Devices" = "Dispositivos"; +"Coverage" = "Cobertura"; "Inspect" = "Inspeccionar"; "Capture" = "Captura"; "Traffic" = "Tráfico"; @@ -181,6 +182,7 @@ "The provider returned HTTP %lld. %@" = "El proveedor ha devuelto HTTP %lld. %@"; "The provider's answer wasn't in a shape Flowlight could read." = "La respuesta del proveedor no tenía un formato que Flowlight pueda leer."; "No endpoint is set for %@." = "No hay ningún endpoint configurado para %@."; +"%@ is not an https:// address. A key and your question would cross the network in the clear, so Flowlight won't send them." = "%@ no es una dirección https://. Una clave y tu pregunta cruzarían la red en claro, así que Flowlight no las envía."; "%@ needs an API key. Add one in Settings — it goes to your login Keychain." = "%@ necesita una clave de API. Añádela en Ajustes: se guarda en tu llavero de inicio de sesión."; "The on-device model isn't reached over HTTP." = "Al modelo en el dispositivo no se llega por HTTP."; "I ran out of steps before I could answer that. Try asking something narrower." = "Me he quedado sin pasos antes de poder responder. Prueba con una pregunta más concreta."; @@ -675,6 +677,30 @@ "Error" = "Error"; "OK" = "OK"; +/* UI/CoverageView.swift */ +"Nothing recorded yet" = "Aún no se ha registrado nada"; +"Coverage is worked out from traffic Flowlight has already recorded. Leave it running for a moment." = "La cobertura se calcula a partir del tráfico que Flowlight ya ha registrado. Déjalo funcionando un momento."; +"%@ of traffic fully accounted for" = "%@ del tráfico contabilizado por completo"; +"How much of this Mac is covered" = "Cuánto de este Mac está cubierto"; +"HTTPS inspection is on" = "La inspección HTTPS está activada"; +"HTTPS inspection is off" = "La inspección HTTPS está desactivada"; +"What an app sent is readable only where the app was routed through the proxy. Anything else is counted and named, but its contents were never offered to Flowlight." = "Lo que una app envió solo es legible allí donde la app pasó por el proxy. Todo lo demás se cuenta y se nombra, pero su contenido nunca se le ofreció a Flowlight."; +"Flowlight sees which connections were made and where to, but not what was inside them. Nothing is decrypted until you turn inspection on." = "Flowlight ve qué conexiones se hicieron y hacia dónde, pero no lo que había dentro. No se descifra nada hasta que activas la inspección."; +"What no engine can see" = "Lo que ningún motor puede ver"; +"Traffic from before capture started, system services that content filters are never shown, apps that pin their certificates, QUIC the proxy is never offered, and an agent's local MCP server talking over a pipe. None of these appear anywhere in Flowlight, so they are absent from these figures too." = "El tráfico anterior al inicio de la captura, los servicios del sistema que nunca se muestran a los filtros de contenido, las apps que fijan sus certificados, el QUIC que nunca se le ofrece al proxy y el servidor MCP local de un agente que se comunica por una tubería. Nada de esto aparece en ningún sitio de Flowlight, así que también falta en estas cifras."; +"Sampling with nettop, not the filter" = "Muestreando con nettop, no con el filtro"; +"The filter sees connections as they open" = "El filtro ve las conexiones en el momento en que se abren"; +"Every eligible TCP and UDP flow is attributed as it opens, including connections too short for a sampler to catch." = "Cada flujo TCP y UDP admitido se atribuye en el momento en que se abre, incluidas las conexiones demasiado breves para que las capte un muestreador."; +"The filter isn't answering, so these figures come from the sampler: a connection that opens and closes between two readings is missing entirely, and no percentage below can show it." = "El filtro no responde, así que estas cifras vienen del muestreador: una conexión que se abre y se cierra entre dos lecturas falta por completo, y ningún porcentaje de abajo puede mostrarla."; +"Byte counts are exact, but a connection that opens and closes between two readings is never recorded — a quick DNS lookup, a fast API call, a script that runs curl and exits. Only the filter sees those." = "Los recuentos de bytes son exactos, pero una conexión que se abre y se cierra entre dos lecturas no se registra nunca: una consulta DNS rápida, una llamada de API veloz, un script que ejecuta curl y sale. Esas solo las ve el filtro."; +"By app" = "Por app"; +"Share of this app's bytes whose destination has a hostname rather than a bare address." = "Proporción de los bytes de esta app cuyo destino tiene un nombre de host y no una dirección a secas."; +"%@ of its bytes went to addresses with no hostname" = "%@ de sus bytes fueron a direcciones sin nombre de host"; +"nothing of its traffic reached the proxy, so no contents were read" = "nada de su tráfico llegó al proxy, así que no se leyó ningún contenido"; +"it is on the Never decrypted list, so its contents are deliberately not read" = "está en la lista de nunca descifrados, así que su contenido no se lee a propósito"; +"short connections may be missing entirely" = "puede que falten por completo las conexiones breves"; +"Seen as it happened, named, and readable." = "Visto en el momento, con nombre y legible."; + /* UI/DevicesView.swift */ "USB" = "USB"; "Not watching" = "Sin vigilar"; diff --git a/Flowlight/Localization/fr.lproj/Localizable.strings b/Flowlight/Localization/fr.lproj/Localizable.strings index 6e0412d..3e76685 100644 --- a/Flowlight/Localization/fr.lproj/Localizable.strings +++ b/Flowlight/Localization/fr.lproj/Localizable.strings @@ -70,6 +70,7 @@ "Rules" = "Règles"; "Ask" = "Demander"; "Devices" = "Appareils"; +"Coverage" = "Couverture"; "Inspect" = "Inspecter"; "Capture" = "Capture"; "Traffic" = "Trafic"; @@ -181,6 +182,7 @@ "The provider returned HTTP %lld. %@" = "Le fournisseur a renvoyé HTTP %lld. %@"; "The provider's answer wasn't in a shape Flowlight could read." = "La réponse du fournisseur n'avait pas une forme que Flowlight puisse lire."; "No endpoint is set for %@." = "Aucun point de terminaison n'est défini pour %@."; +"%@ is not an https:// address. A key and your question would cross the network in the clear, so Flowlight won't send them." = "%@ n'est pas une adresse https://. Une clé et votre question traverseraient le réseau en clair : Flowlight ne les envoie pas."; "%@ needs an API key. Add one in Settings — it goes to your login Keychain." = "%@ nécessite une clé API. En ajouter une dans Réglages — elle est conservée dans votre trousseau de session."; "The on-device model isn't reached over HTTP." = "Le modèle sur l'appareil n'est pas joint via HTTP."; "I ran out of steps before I could answer that. Try asking something narrower." = "Le nombre d'étapes disponibles a été épuisé avant d'obtenir une réponse. Essayer une question plus précise."; @@ -675,6 +677,30 @@ "Error" = "Erreur"; "OK" = "OK"; +/* UI/CoverageView.swift */ +"Nothing recorded yet" = "Rien d'enregistré pour l'instant"; +"Coverage is worked out from traffic Flowlight has already recorded. Leave it running for a moment." = "La couverture est calculée à partir du trafic que Flowlight a déjà enregistré. Le laisser tourner un moment."; +"%@ of traffic fully accounted for" = "%@ du trafic entièrement pris en compte"; +"How much of this Mac is covered" = "Quelle part de ce Mac est couverte"; +"HTTPS inspection is on" = "L'inspection HTTPS est activée"; +"HTTPS inspection is off" = "L'inspection HTTPS est désactivée"; +"What an app sent is readable only where the app was routed through the proxy. Anything else is counted and named, but its contents were never offered to Flowlight." = "Ce qu'une app a envoyé n'est lisible que là où l'app est passée par le proxy. Tout le reste est compté et nommé, mais son contenu n'a jamais été proposé à Flowlight."; +"Flowlight sees which connections were made and where to, but not what was inside them. Nothing is decrypted until you turn inspection on." = "Flowlight voit quelles connexions ont été établies et vers où, mais pas ce qu'elles contenaient. Rien n'est déchiffré tant que vous n'activez pas l'inspection."; +"What no engine can see" = "Ce qu'aucun moteur ne peut voir"; +"Traffic from before capture started, system services that content filters are never shown, apps that pin their certificates, QUIC the proxy is never offered, and an agent's local MCP server talking over a pipe. None of these appear anywhere in Flowlight, so they are absent from these figures too." = "Le trafic antérieur au démarrage de la capture, les services système qui ne sont jamais présentés aux filtres de contenu, les apps qui épinglent leurs certificats, le QUIC qui n'est jamais proposé au proxy, et le serveur MCP local d'un agent qui communique par un tube. Rien de tout cela n'apparaît où que ce soit dans Flowlight, et manque donc aussi dans ces chiffres."; +"Sampling with nettop, not the filter" = "Échantillonnage avec nettop, pas avec le filtre"; +"The filter sees connections as they open" = "Le filtre voit les connexions à leur ouverture"; +"Every eligible TCP and UDP flow is attributed as it opens, including connections too short for a sampler to catch." = "Chaque flux TCP et UDP éligible est attribué à son ouverture, y compris les connexions trop brèves pour qu'un échantillonneur les saisisse."; +"The filter isn't answering, so these figures come from the sampler: a connection that opens and closes between two readings is missing entirely, and no percentage below can show it." = "Le filtre ne répond pas, ces chiffres viennent donc de l'échantillonneur : une connexion qui s'ouvre et se ferme entre deux relevés manque entièrement, et aucun pourcentage ci-dessous ne peut la montrer."; +"Byte counts are exact, but a connection that opens and closes between two readings is never recorded — a quick DNS lookup, a fast API call, a script that runs curl and exits. Only the filter sees those." = "Les décomptes d'octets sont exacts, mais une connexion qui s'ouvre et se ferme entre deux relevés n'est jamais enregistrée — une recherche DNS rapide, un appel d'API bref, un script qui lance curl et se termine. Seul le filtre les voit."; +"By app" = "Par app"; +"Share of this app's bytes whose destination has a hostname rather than a bare address." = "Part des octets de cette app dont la destination porte un nom d'hôte plutôt qu'une simple adresse."; +"%@ of its bytes went to addresses with no hostname" = "%@ de ses octets sont allés vers des adresses sans nom d'hôte"; +"nothing of its traffic reached the proxy, so no contents were read" = "rien de son trafic n'a atteint le proxy, aucun contenu n'a donc été lu"; +"it is on the Never decrypted list, so its contents are deliberately not read" = "elle est sur la liste Jamais déchiffré, son contenu n'est donc délibérément pas lu"; +"short connections may be missing entirely" = "des connexions brèves peuvent manquer entièrement"; +"Seen as it happened, named, and readable." = "Vu au moment où cela s'est produit, nommé et lisible."; + /* UI/DevicesView.swift */ "USB" = "USB"; "Not watching" = "Non surveillé"; diff --git a/Flowlight/Localization/it.lproj/Localizable.strings b/Flowlight/Localization/it.lproj/Localizable.strings index a4dfb4e..acde3c4 100644 --- a/Flowlight/Localization/it.lproj/Localizable.strings +++ b/Flowlight/Localization/it.lproj/Localizable.strings @@ -70,6 +70,7 @@ "Rules" = "Regole"; "Ask" = "Chiedi"; "Devices" = "Dispositivi"; +"Coverage" = "Copertura"; "Inspect" = "Ispeziona"; "Capture" = "Acquisizione"; "Traffic" = "Traffico"; @@ -181,6 +182,7 @@ "The provider returned HTTP %lld. %@" = "Il provider ha restituito HTTP %lld. %@"; "The provider's answer wasn't in a shape Flowlight could read." = "La risposta del provider non era in un formato leggibile da Flowlight."; "No endpoint is set for %@." = "Nessun endpoint impostato per %@."; +"%@ is not an https:// address. A key and your question would cross the network in the clear, so Flowlight won't send them." = "%@ non è un indirizzo https://. Una chiave e la tua domanda attraverserebbero la rete in chiaro, quindi Flowlight non le invia."; "%@ needs an API key. Add one in Settings — it goes to your login Keychain." = "%@ richiede una chiave API. Aggiungila in Impostazioni: viene salvata nel portachiavi di accesso."; "The on-device model isn't reached over HTTP." = "Il modello sul dispositivo non viene raggiunto via HTTP."; "I ran out of steps before I could answer that. Try asking something narrower." = "Ho esaurito i passaggi disponibili prima di poter rispondere. Prova con una domanda più circoscritta."; @@ -675,6 +677,30 @@ "Error" = "Errore"; "OK" = "OK"; +/* UI/CoverageView.swift */ +"Nothing recorded yet" = "Ancora nulla di registrato"; +"Coverage is worked out from traffic Flowlight has already recorded. Leave it running for a moment." = "La copertura è calcolata sul traffico che Flowlight ha già registrato. Lascialo in esecuzione per un momento."; +"%@ of traffic fully accounted for" = "%@ del traffico interamente contabilizzato"; +"How much of this Mac is covered" = "Quanta parte di questo Mac è coperta"; +"HTTPS inspection is on" = "L'ispezione HTTPS è attiva"; +"HTTPS inspection is off" = "L'ispezione HTTPS è disattivata"; +"What an app sent is readable only where the app was routed through the proxy. Anything else is counted and named, but its contents were never offered to Flowlight." = "Ciò che un'app ha inviato è leggibile solo dove l'app è passata attraverso il proxy. Tutto il resto viene contato e nominato, ma il suo contenuto non è mai stato offerto a Flowlight."; +"Flowlight sees which connections were made and where to, but not what was inside them. Nothing is decrypted until you turn inspection on." = "Flowlight vede quali connessioni sono state fatte e verso dove, ma non che cosa contenessero. Non viene decifrato nulla finché non attivi l'ispezione."; +"What no engine can see" = "Ciò che nessun motore può vedere"; +"Traffic from before capture started, system services that content filters are never shown, apps that pin their certificates, QUIC the proxy is never offered, and an agent's local MCP server talking over a pipe. None of these appear anywhere in Flowlight, so they are absent from these figures too." = "Il traffico precedente all'avvio dell'acquisizione, i servizi di sistema che non vengono mai mostrati ai filtri contenuti, le app che applicano il pinning dei certificati, il QUIC che non viene mai offerto al proxy e il server MCP locale di un agente che comunica tramite pipe. Nulla di questo compare in alcun punto di Flowlight, quindi manca anche da queste cifre."; +"Sampling with nettop, not the filter" = "Campionamento con nettop, non con il filtro"; +"The filter sees connections as they open" = "Il filtro vede le connessioni nel momento in cui si aprono"; +"Every eligible TCP and UDP flow is attributed as it opens, including connections too short for a sampler to catch." = "Ogni flusso TCP e UDP ammesso viene attribuito nel momento in cui si apre, comprese le connessioni troppo brevi perché un campionatore le colga."; +"The filter isn't answering, so these figures come from the sampler: a connection that opens and closes between two readings is missing entirely, and no percentage below can show it." = "Il filtro non risponde, quindi queste cifre vengono dal campionatore: una connessione che si apre e si chiude tra due letture manca del tutto, e nessuna percentuale qui sotto può mostrarla."; +"Byte counts are exact, but a connection that opens and closes between two readings is never recorded — a quick DNS lookup, a fast API call, a script that runs curl and exits. Only the filter sees those." = "I conteggi di byte sono esatti, ma una connessione che si apre e si chiude tra due letture non viene mai registrata: una rapida risoluzione DNS, una chiamata API veloce, uno script che esegue curl ed esce. Quelle le vede solo il filtro."; +"By app" = "Per app"; +"Share of this app's bytes whose destination has a hostname rather than a bare address." = "Quota dei byte di questa app il cui destinatario ha un nome host anziché soltanto un indirizzo."; +"%@ of its bytes went to addresses with no hostname" = "%@ dei suoi byte è andato verso indirizzi senza nome host"; +"nothing of its traffic reached the proxy, so no contents were read" = "nulla del suo traffico ha raggiunto il proxy, quindi non è stato letto alcun contenuto"; +"it is on the Never decrypted list, so its contents are deliberately not read" = "è nell'elenco Mai decifrati, quindi i suoi contenuti non vengono letti di proposito"; +"short connections may be missing entirely" = "le connessioni brevi potrebbero mancare del tutto"; +"Seen as it happened, named, and readable." = "Visto mentre accadeva, con un nome e leggibile."; + /* UI/DevicesView.swift */ "USB" = "USB"; "Not watching" = "Non monitorato"; diff --git a/Flowlight/Localization/ja.lproj/Localizable.strings b/Flowlight/Localization/ja.lproj/Localizable.strings index 883896d..0ea9cd6 100644 --- a/Flowlight/Localization/ja.lproj/Localizable.strings +++ b/Flowlight/Localization/ja.lproj/Localizable.strings @@ -70,6 +70,7 @@ "Rules" = "ルール"; "Ask" = "質問"; "Devices" = "デバイス"; +"Coverage" = "カバー範囲"; "Inspect" = "インスペクト"; "Capture" = "キャプチャ"; "Traffic" = "トラフィック"; @@ -181,6 +182,7 @@ "The provider returned HTTP %lld. %@" = "プロバイダが HTTP %lld を返しました。%@"; "The provider's answer wasn't in a shape Flowlight could read." = "プロバイダの応答は Flowlight が読み取れる形式ではありませんでした。"; "No endpoint is set for %@." = "%@ のエンドポイントが設定されていません。"; +"%@ is not an https:// address. A key and your question would cross the network in the clear, so Flowlight won't send them." = "%@ は https:// のアドレスではありません。キーとあなたの質問が平文でネットワークを流れることになるため、Flowlight は送信しません。"; "%@ needs an API key. Add one in Settings — it goes to your login Keychain." = "%@ には API キーが必要です。設定で追加してください。キーはログインキーチェーンに保存されます。"; "The on-device model isn't reached over HTTP." = "オンデバイスモデルへは HTTP で接続しません。"; "I ran out of steps before I could answer that. Try asking something narrower." = "回答にたどり着く前に手順の上限に達しました。もう少し範囲を絞って質問してください。"; @@ -675,6 +677,30 @@ "Error" = "エラー"; "OK" = "OK"; +/* UI/CoverageView.swift */ +"Nothing recorded yet" = "まだ何も記録されていません"; +"Coverage is worked out from traffic Flowlight has already recorded. Leave it running for a moment." = "カバー範囲は、Flowlight がすでに記録したトラフィックから算出します。しばらく動かしたままにしてください。"; +"%@ of traffic fully accounted for" = "トラフィックの %@ を完全に把握"; +"How much of this Mac is covered" = "この Mac をどこまでカバーできているか"; +"HTTPS inspection is on" = "HTTPS インスペクションはオンです"; +"HTTPS inspection is off" = "HTTPS インスペクションはオフです"; +"What an app sent is readable only where the app was routed through the proxy. Anything else is counted and named, but its contents were never offered to Flowlight." = "アプリが送った内容を読めるのは、そのアプリがプロキシを経由した部分だけです。それ以外は数えられ、名前も付きますが、中身が Flowlight に渡されたことはありません。"; +"Flowlight sees which connections were made and where to, but not what was inside them. Nothing is decrypted until you turn inspection on." = "Flowlight には、どの接続がどこへ向けて行われたかは見えますが、その中身は見えません。インスペクションをオンにするまで、何も復号しません。"; +"What no engine can see" = "どのエンジンにも見えないもの"; +"Traffic from before capture started, system services that content filters are never shown, apps that pin their certificates, QUIC the proxy is never offered, and an agent's local MCP server talking over a pipe. None of these appear anywhere in Flowlight, so they are absent from these figures too." = "キャプチャを始める前のトラフィック、コンテンツフィルタに一度も見せられないシステムサービス、証明書をピン留めしているアプリ、プロキシに一度も渡されない QUIC、そしてパイプで通信するエージェントのローカル MCP サーバ。これらは Flowlight のどこにも現れないため、ここの数値にも入っていません。"; +"Sampling with nettop, not the filter" = "フィルタではなく nettop でサンプリング中"; +"The filter sees connections as they open" = "フィルタは接続が開かれた時点で見ています"; +"Every eligible TCP and UDP flow is attributed as it opens, including connections too short for a sampler to catch." = "対象となる TCP と UDP のフローはすべて、開かれた時点でアプリに結び付けられます。サンプラーでは捉えられないほど短い接続も含みます。"; +"The filter isn't answering, so these figures come from the sampler: a connection that opens and closes between two readings is missing entirely, and no percentage below can show it." = "フィルタが応答しないため、ここの数値はサンプラーによるものです。2 回の読み取りの間に開いて閉じる接続はまるごと欠け、下のどのパーセンテージにも現れません。"; +"Byte counts are exact, but a connection that opens and closes between two readings is never recorded — a quick DNS lookup, a fast API call, a script that runs curl and exits. Only the filter sees those." = "バイト数は正確ですが、2 回の読み取りの間に開いて閉じる接続は記録されません。短い DNS 照会、速い API 呼び出し、curl を実行して終了するスクリプトなどです。それが見えるのはフィルタだけです。"; +"By app" = "アプリ別"; +"Share of this app's bytes whose destination has a hostname rather than a bare address." = "このアプリのバイト数のうち、宛先にホスト名があった(単なるアドレスではなかった)割合です。"; +"%@ of its bytes went to addresses with no hostname" = "バイト数の %@ がホスト名のないアドレスに向かいました"; +"nothing of its traffic reached the proxy, so no contents were read" = "トラフィックがプロキシに届いていないため、中身は読めていません"; +"it is on the Never decrypted list, so its contents are deliberately not read" = "「復号しないホスト」に入っているため、中身はあえて読んでいません"; +"short connections may be missing entirely" = "短い接続がまるごと欠けている可能性があります"; +"Seen as it happened, named, and readable." = "発生した時点で見えており、名前も付き、中身も読めています。"; + /* UI/DevicesView.swift */ "USB" = "USB"; "Not watching" = "監視していません"; diff --git a/Flowlight/Localization/ko.lproj/Localizable.strings b/Flowlight/Localization/ko.lproj/Localizable.strings index 7d72849..3a10d9b 100644 --- a/Flowlight/Localization/ko.lproj/Localizable.strings +++ b/Flowlight/Localization/ko.lproj/Localizable.strings @@ -70,6 +70,7 @@ "Rules" = "규칙"; "Ask" = "질문"; "Devices" = "기기"; +"Coverage" = "포착 범위"; "Inspect" = "검사"; "Capture" = "캡처"; "Traffic" = "트래픽"; @@ -181,6 +182,7 @@ "The provider returned HTTP %lld. %@" = "제공업체 응답: HTTP %lld. %@"; "The provider's answer wasn't in a shape Flowlight could read." = "제공업체의 응답이 Flowlight가 읽을 수 있는 형식이 아닙니다."; "No endpoint is set for %@." = "%@의 엔드포인트가 설정되지 않았습니다."; +"%@ is not an https:// address. A key and your question would cross the network in the clear, so Flowlight won't send them." = "%@는 https:// 주소가 아닙니다. 키와 질문이 평문으로 네트워크를 지나가게 되므로 Flowlight는 보내지 않습니다."; "%@ needs an API key. Add one in Settings — it goes to your login Keychain." = "%@에는 API 키가 필요합니다. 설정에서 추가하세요. 키는 로그인 키체인에 저장됩니다."; "The on-device model isn't reached over HTTP." = "온디바이스 모델은 HTTP로 연결하지 않습니다."; "I ran out of steps before I could answer that. Try asking something narrower." = "답을 내기 전에 단계 한도에 도달했습니다. 좀 더 좁혀서 질문해 보세요."; @@ -675,6 +677,30 @@ "Error" = "오류"; "OK" = "확인"; +/* UI/CoverageView.swift */ +"Nothing recorded yet" = "아직 기록된 것이 없습니다"; +"Coverage is worked out from traffic Flowlight has already recorded. Leave it running for a moment." = "포착 범위는 Flowlight가 이미 기록한 트래픽으로 계산합니다. 잠시 그대로 실행해 두세요."; +"%@ of traffic fully accounted for" = "트래픽의 %@를 완전히 파악"; +"How much of this Mac is covered" = "이 Mac을 어디까지 포착하고 있는지"; +"HTTPS inspection is on" = "HTTPS 검사가 켜져 있습니다"; +"HTTPS inspection is off" = "HTTPS 검사가 꺼져 있습니다"; +"What an app sent is readable only where the app was routed through the proxy. Anything else is counted and named, but its contents were never offered to Flowlight." = "앱이 보낸 내용은 그 앱이 프록시를 거친 부분에서만 읽을 수 있습니다. 그 밖의 것은 집계되고 이름도 붙지만, 내용이 Flowlight에 전달된 적은 없습니다."; +"Flowlight sees which connections were made and where to, but not what was inside them. Nothing is decrypted until you turn inspection on." = "Flowlight는 어떤 연결이 어디로 이뤄졌는지는 보지만, 그 안에 무엇이 있었는지는 보지 못합니다. 검사를 켜기 전까지는 아무것도 복호화하지 않습니다."; +"What no engine can see" = "어떤 엔진도 볼 수 없는 것"; +"Traffic from before capture started, system services that content filters are never shown, apps that pin their certificates, QUIC the proxy is never offered, and an agent's local MCP server talking over a pipe. None of these appear anywhere in Flowlight, so they are absent from these figures too." = "캡처를 시작하기 전의 트래픽, 콘텐츠 필터에는 한 번도 보이지 않는 시스템 서비스, 인증서를 고정하는 앱, 프록시에 한 번도 넘겨지지 않는 QUIC, 그리고 파이프로 통신하는 에이전트의 로컬 MCP 서버입니다. 이런 것들은 Flowlight 어디에도 나타나지 않으므로 이 수치에도 빠져 있습니다."; +"Sampling with nettop, not the filter" = "필터가 아니라 nettop으로 샘플링 중"; +"The filter sees connections as they open" = "필터가 연결이 열리는 순간을 포착합니다"; +"Every eligible TCP and UDP flow is attributed as it opens, including connections too short for a sampler to catch." = "대상이 되는 TCP·UDP 흐름은 열리는 순간 모두 앱에 귀속됩니다. 샘플러가 잡기에는 너무 짧은 연결도 포함합니다."; +"The filter isn't answering, so these figures come from the sampler: a connection that opens and closes between two readings is missing entirely, and no percentage below can show it." = "필터가 응답하지 않아 이 수치는 샘플러에서 나온 것입니다. 두 번의 읽기 사이에 열리고 닫히는 연결은 통째로 빠지며, 아래의 어떤 비율로도 드러나지 않습니다."; +"Byte counts are exact, but a connection that opens and closes between two readings is never recorded — a quick DNS lookup, a fast API call, a script that runs curl and exits. Only the filter sees those." = "바이트 수는 정확하지만, 두 번의 읽기 사이에 열리고 닫히는 연결은 전혀 기록되지 않습니다. 짧은 DNS 조회, 빠른 API 호출, curl을 실행하고 끝나는 스크립트 같은 것입니다. 그런 것은 필터만 봅니다."; +"By app" = "앱별"; +"Share of this app's bytes whose destination has a hostname rather than a bare address." = "이 앱의 바이트 중 목적지에 호스트 이름이 있었던(맨 주소가 아니었던) 비율입니다."; +"%@ of its bytes went to addresses with no hostname" = "바이트의 %@가 호스트 이름이 없는 주소로 갔습니다"; +"nothing of its traffic reached the proxy, so no contents were read" = "트래픽이 프록시에 전혀 닿지 않아 내용을 읽지 못했습니다"; +"it is on the Never decrypted list, so its contents are deliberately not read" = "복호화하지 않을 대상 목록에 있어 내용을 일부러 읽지 않습니다"; +"short connections may be missing entirely" = "짧은 연결이 통째로 빠져 있을 수 있습니다"; +"Seen as it happened, named, and readable." = "일어난 순간에 포착되고, 이름이 붙고, 내용도 읽혔습니다."; + /* UI/DevicesView.swift */ "USB" = "USB"; "Not watching" = "감시하지 않음"; diff --git a/Flowlight/Localization/pt-PT.lproj/Localizable.strings b/Flowlight/Localization/pt-PT.lproj/Localizable.strings index d069ba1..49390e8 100644 --- a/Flowlight/Localization/pt-PT.lproj/Localizable.strings +++ b/Flowlight/Localization/pt-PT.lproj/Localizable.strings @@ -70,6 +70,7 @@ "Rules" = "Regras"; "Ask" = "Perguntar"; "Devices" = "Dispositivos"; +"Coverage" = "Cobertura"; "Inspect" = "Inspecionar"; "Capture" = "Captura"; "Traffic" = "Tráfego"; @@ -181,6 +182,7 @@ "The provider returned HTTP %lld. %@" = "O fornecedor devolveu HTTP %lld. %@"; "The provider's answer wasn't in a shape Flowlight could read." = "A resposta do fornecedor não tinha um formato que o Flowlight conseguisse ler."; "No endpoint is set for %@." = "Não está definido nenhum endpoint para %@."; +"%@ is not an https:// address. A key and your question would cross the network in the clear, so Flowlight won't send them." = "%@ não é um endereço https://. Uma chave e a sua pergunta atravessariam a rede em claro, por isso o Flowlight não as envia."; "%@ needs an API key. Add one in Settings — it goes to your login Keychain." = "%@ precisa de uma chave de API. Adicione uma nas Definições — vai para o porta-chaves de início de sessão."; "The on-device model isn't reached over HTTP." = "O modelo no dispositivo não é alcançado por HTTP."; "I ran out of steps before I could answer that. Try asking something narrower." = "Esgotei os passos antes de conseguir responder. Tente perguntar algo mais restrito."; @@ -675,6 +677,30 @@ "Error" = "Erro"; "OK" = "OK"; +/* UI/CoverageView.swift */ +"Nothing recorded yet" = "Ainda não há nada registado"; +"Coverage is worked out from traffic Flowlight has already recorded. Leave it running for a moment." = "A cobertura é calculada a partir do tráfego que o Flowlight já registou. Deixe-o a correr por um momento."; +"%@ of traffic fully accounted for" = "%@ do tráfego totalmente contabilizado"; +"How much of this Mac is covered" = "Quanto deste Mac está coberto"; +"HTTPS inspection is on" = "A inspeção HTTPS está ativada"; +"HTTPS inspection is off" = "A inspeção HTTPS está desativada"; +"What an app sent is readable only where the app was routed through the proxy. Anything else is counted and named, but its contents were never offered to Flowlight." = "O que uma app enviou só é legível onde a app passou pelo proxy. Todo o resto é contado e nomeado, mas o seu conteúdo nunca foi oferecido ao Flowlight."; +"Flowlight sees which connections were made and where to, but not what was inside them. Nothing is decrypted until you turn inspection on." = "O Flowlight vê que ligações foram feitas e para onde, mas não o que estava dentro delas. Nada é desencriptado enquanto não ativar a inspeção."; +"What no engine can see" = "O que nenhum motor consegue ver"; +"Traffic from before capture started, system services that content filters are never shown, apps that pin their certificates, QUIC the proxy is never offered, and an agent's local MCP server talking over a pipe. None of these appear anywhere in Flowlight, so they are absent from these figures too." = "O tráfego anterior ao início da captura, os serviços do sistema que nunca são mostrados aos filtros de conteúdo, as apps que fixam os seus certificados, o QUIC que nunca é oferecido ao proxy e o servidor MCP local de um agente que comunica por um pipe. Nada disto aparece em qualquer parte do Flowlight, por isso também falta nestes números."; +"Sampling with nettop, not the filter" = "Amostragem com o nettop, não com o filtro"; +"The filter sees connections as they open" = "O filtro vê as ligações à medida que abrem"; +"Every eligible TCP and UDP flow is attributed as it opens, including connections too short for a sampler to catch." = "Todos os fluxos TCP e UDP elegíveis são atribuídos à medida que abrem, incluindo ligações demasiado curtas para um amostrador apanhar."; +"The filter isn't answering, so these figures come from the sampler: a connection that opens and closes between two readings is missing entirely, and no percentage below can show it." = "O filtro não está a responder, por isso estes números vêm do amostrador: uma ligação que abre e fecha entre duas leituras falta por completo, e nenhuma percentagem abaixo a consegue mostrar."; +"Byte counts are exact, but a connection that opens and closes between two readings is never recorded — a quick DNS lookup, a fast API call, a script that runs curl and exits. Only the filter sees those." = "As contagens de bytes são exatas, mas uma ligação que abre e fecha entre duas leituras nunca é registada — uma consulta DNS rápida, uma chamada de API rápida, um script que executa curl e sai. Só o filtro vê essas."; +"By app" = "Por app"; +"Share of this app's bytes whose destination has a hostname rather than a bare address." = "Proporção dos bytes desta app cujo destino tem um nome de anfitrião em vez de um endereço simples."; +"%@ of its bytes went to addresses with no hostname" = "%@ dos seus bytes foram para endereços sem nome de anfitrião"; +"nothing of its traffic reached the proxy, so no contents were read" = "nada do seu tráfego chegou ao proxy, por isso não foi lido nenhum conteúdo"; +"it is on the Never decrypted list, so its contents are deliberately not read" = "está na lista Nunca desencriptado, por isso o seu conteúdo não é lido de propósito"; +"short connections may be missing entirely" = "podem faltar por completo ligações curtas"; +"Seen as it happened, named, and readable." = "Visto no momento em que aconteceu, nomeado e legível."; + /* UI/DevicesView.swift */ "USB" = "USB"; "Not watching" = "Não vigiado"; diff --git a/Flowlight/Localization/zh-Hans.lproj/Localizable.strings b/Flowlight/Localization/zh-Hans.lproj/Localizable.strings index e008391..8495710 100644 --- a/Flowlight/Localization/zh-Hans.lproj/Localizable.strings +++ b/Flowlight/Localization/zh-Hans.lproj/Localizable.strings @@ -70,6 +70,7 @@ "Rules" = "规则"; "Ask" = "提问"; "Devices" = "设备"; +"Coverage" = "覆盖范围"; "Inspect" = "检查"; "Capture" = "捕获"; "Traffic" = "流量"; @@ -181,6 +182,7 @@ "The provider returned HTTP %lld. %@" = "提供方返回了 HTTP %lld。%@"; "The provider's answer wasn't in a shape Flowlight could read." = "提供方的应答不是 Flowlight 能读取的格式。"; "No endpoint is set for %@." = "尚未为 %@ 设置端点。"; +"%@ is not an https:// address. A key and your question would cross the network in the clear, so Flowlight won't send them." = "%@ 不是 https:// 地址。密钥和你的问题会以明文经过网络,因此 Flowlight 不会发送它们。"; "%@ needs an API key. Add one in Settings — it goes to your login Keychain." = "%@ 需要 API 密钥。请在“设置”中添加 —— 它会保存到你的登录钥匙串。"; "The on-device model isn't reached over HTTP." = "设备端模型不通过 HTTP 访问。"; "I ran out of steps before I could answer that. Try asking something narrower." = "在能够作答之前步数已用尽。请把问题问得更具体一些。"; @@ -675,6 +677,30 @@ "Error" = "错误"; "OK" = "好"; +/* UI/CoverageView.swift */ +"Nothing recorded yet" = "尚未记录到任何内容"; +"Coverage is worked out from traffic Flowlight has already recorded. Leave it running for a moment." = "覆盖范围是根据 Flowlight 已经记录的流量算出来的。请让它再运行一会儿。"; +"%@ of traffic fully accounted for" = "%@ 的流量完全有据可查"; +"How much of this Mac is covered" = "这台 Mac 被覆盖了多少"; +"HTTPS inspection is on" = "HTTPS 检查已开启"; +"HTTPS inspection is off" = "HTTPS 检查已关闭"; +"What an app sent is readable only where the app was routed through the proxy. Anything else is counted and named, but its contents were never offered to Flowlight." = "应用发送的内容,只有在该应用经由代理服务器时才可读。其余的都有计数、也有名称,但其内容从未交给 Flowlight。"; +"Flowlight sees which connections were made and where to, but not what was inside them. Nothing is decrypted until you turn inspection on." = "Flowlight 能看到建立了哪些连接、连到哪里,但看不到其中的内容。在你开启检查之前,什么都不会被解密。"; +"What no engine can see" = "任何引擎都看不到的东西"; +"Traffic from before capture started, system services that content filters are never shown, apps that pin their certificates, QUIC the proxy is never offered, and an agent's local MCP server talking over a pipe. None of these appear anywhere in Flowlight, so they are absent from these figures too." = "捕获开始之前的流量、从不会交给内容过滤器的系统服务、固定证书的应用、从不会交给代理服务器的 QUIC,以及某个代理通过管道通信的本地 MCP 服务器。这些在 Flowlight 的任何地方都不会出现,因此也不在这些数字之内。"; +"Sampling with nettop, not the filter" = "正在用 nettop 采样,而不是过滤器"; +"The filter sees connections as they open" = "过滤器在连接建立时就看到它们"; +"Every eligible TCP and UDP flow is attributed as it opens, including connections too short for a sampler to catch." = "每一条符合条件的 TCP 和 UDP 流都在建立时归属到应用,包括短到采样器抓不到的连接。"; +"The filter isn't answering, so these figures come from the sampler: a connection that opens and closes between two readings is missing entirely, and no percentage below can show it." = "过滤器没有响应,因此以下数字来自采样器:在两次读取之间开启又关闭的连接会整条缺失,下面任何百分比都无法显示它。"; +"Byte counts are exact, but a connection that opens and closes between two readings is never recorded — a quick DNS lookup, a fast API call, a script that runs curl and exits. Only the filter sees those." = "字节数是精确的,但在两次读取之间开启又关闭的连接从不会被记录 —— 一次快速的 DNS 查询、一次很快的 API 调用、一个运行 curl 后就退出的脚本。这些只有过滤器能看到。"; +"By app" = "按应用"; +"Share of this app's bytes whose destination has a hostname rather than a bare address." = "该应用的字节中,目的地有主机名(而不是只有一个地址)的比例。"; +"%@ of its bytes went to addresses with no hostname" = "该应用 %@ 的字节发往了没有主机名的地址"; +"nothing of its traffic reached the proxy, so no contents were read" = "它的流量没有任何一部分到达代理服务器,因此没有读取到任何内容"; +"it is on the Never decrypted list, so its contents are deliberately not read" = "它在“从不解密”列表中,因此其内容是故意不去读取的"; +"short connections may be missing entirely" = "短连接可能整条缺失"; +"Seen as it happened, named, and readable." = "在发生时即被看到,有名称,内容也可读。"; + /* UI/DevicesView.swift */ "USB" = "USB"; "Not watching" = "未监视"; diff --git a/Flowlight/Localization/zh-Hant.lproj/Localizable.strings b/Flowlight/Localization/zh-Hant.lproj/Localizable.strings index 47a5bb8..8a92222 100644 --- a/Flowlight/Localization/zh-Hant.lproj/Localizable.strings +++ b/Flowlight/Localization/zh-Hant.lproj/Localizable.strings @@ -70,6 +70,7 @@ "Rules" = "規則"; "Ask" = "提問"; "Devices" = "裝置"; +"Coverage" = "涵蓋範圍"; "Inspect" = "檢查"; "Capture" = "擷取"; "Traffic" = "流量"; @@ -181,6 +182,7 @@ "The provider returned HTTP %lld. %@" = "供應商回傳 HTTP %lld。%@"; "The provider's answer wasn't in a shape Flowlight could read." = "供應商的回應格式不是 Flowlight 讀得懂的。"; "No endpoint is set for %@." = "尚未為 %@ 設定端點。"; +"%@ is not an https:// address. A key and your question would cross the network in the clear, so Flowlight won't send them." = "%@ 不是 https:// 位址。金鑰和你的問題會以明文經過網路,所以 Flowlight 不會送出它們。"; "%@ needs an API key. Add one in Settings — it goes to your login Keychain." = "%@ 需要 API 金鑰。請在「設定」中加入 — 金鑰會存入你的登入鑰匙串。"; "The on-device model isn't reached over HTTP." = "裝置端模型不是透過 HTTP 連線的。"; "I ran out of steps before I could answer that. Try asking something narrower." = "我在能回答之前就用完了可執行的步驟。試著問得更具體一些。"; @@ -675,6 +677,30 @@ "Error" = "錯誤"; "OK" = "好"; +/* UI/CoverageView.swift */ +"Nothing recorded yet" = "還沒有記錄到任何東西"; +"Coverage is worked out from traffic Flowlight has already recorded. Leave it running for a moment." = "涵蓋範圍是從 Flowlight 已經記錄的流量算出來的。請讓它再執行一會兒。"; +"%@ of traffic fully accounted for" = "%@ 的流量完全有據可查"; +"How much of this Mac is covered" = "這台 Mac 涵蓋了多少"; +"HTTPS inspection is on" = "HTTPS 檢查已開啟"; +"HTTPS inspection is off" = "HTTPS 檢查已關閉"; +"What an app sent is readable only where the app was routed through the proxy. Anything else is counted and named, but its contents were never offered to Flowlight." = "App 送出的內容,只有在該 App 經過代理伺服器時才讀得到。其餘的都有計數、也有名稱,但內容從來沒有交給 Flowlight。"; +"Flowlight sees which connections were made and where to, but not what was inside them. Nothing is decrypted until you turn inspection on." = "Flowlight 看得到建立了哪些連線、連到哪裡,但看不到連線裡面的內容。在你開啟檢查之前,不會解密任何東西。"; +"What no engine can see" = "任何引擎都看不到的東西"; +"Traffic from before capture started, system services that content filters are never shown, apps that pin their certificates, QUIC the proxy is never offered, and an agent's local MCP server talking over a pipe. None of these appear anywhere in Flowlight, so they are absent from these figures too." = "擷取開始之前的流量、從來不會交給內容過濾器的系統服務、使用憑證綁定的 App、從來不會交給代理伺服器的 QUIC,以及某個代理透過管道通訊的本機 MCP 伺服器。這些在 Flowlight 的任何地方都不會出現,所以也不在這些數字裡。"; +"Sampling with nettop, not the filter" = "正在用 nettop 取樣,而不是過濾器"; +"The filter sees connections as they open" = "過濾器在連線建立時就看到它們"; +"Every eligible TCP and UDP flow is attributed as it opens, including connections too short for a sampler to catch." = "每一條符合條件的 TCP 和 UDP 流量都在建立時歸給 App,包括短到取樣器抓不到的連線。"; +"The filter isn't answering, so these figures come from the sampler: a connection that opens and closes between two readings is missing entirely, and no percentage below can show it." = "過濾器沒有回應,所以以下數字來自取樣器:在兩次讀取之間開啟又關閉的連線會整條漏掉,下面任何百分比都顯示不出來。"; +"Byte counts are exact, but a connection that opens and closes between two readings is never recorded — a quick DNS lookup, a fast API call, a script that runs curl and exits. Only the filter sees those." = "位元組計數是精確的,但在兩次讀取之間開啟又關閉的連線永遠不會被記錄 — 一次快速的 DNS 查詢、一次很快的 API 呼叫、一個執行 curl 後就結束的指令檔。這些只有過濾器看得到。"; +"By app" = "依 App"; +"Share of this app's bytes whose destination has a hostname rather than a bare address." = "這個 App 的位元組中,目的地有主機名稱(而不是只有一個位址)的比例。"; +"%@ of its bytes went to addresses with no hostname" = "這個 App 有 %@ 的位元組送到了沒有主機名稱的位址"; +"nothing of its traffic reached the proxy, so no contents were read" = "它的流量沒有任何一部分到達代理伺服器,所以沒有讀到任何內容"; +"it is on the Never decrypted list, so its contents are deliberately not read" = "它在「永不解密」清單中,所以刻意不讀取它的內容"; +"short connections may be missing entirely" = "短連線可能整條漏掉"; +"Seen as it happened, named, and readable." = "在發生時就看到、有名稱,內容也讀得到。"; + /* UI/DevicesView.swift */ "USB" = "USB"; "Not watching" = "未監看"; diff --git a/Flowlight/UI/ContentView.swift b/Flowlight/UI/ContentView.swift index 34fc73c..448402c 100644 --- a/Flowlight/UI/ContentView.swift +++ b/Flowlight/UI/ContentView.swift @@ -69,6 +69,7 @@ struct ContentView: View { case .inspect: InspectView() case .ask: AskView() case .devices: DevicesView() + case .coverage: CoverageView() case .capture: CaptureView() } } diff --git a/Flowlight/UI/CoverageView.swift b/Flowlight/UI/CoverageView.swift new file mode 100644 index 0000000..e9b6fbe --- /dev/null +++ b/Flowlight/UI/CoverageView.swift @@ -0,0 +1,185 @@ +import SwiftUI + +/// What Flowlight can and cannot account for, per app. +/// +/// Every other screen answers "what happened". This one answers "what would I not have been told", which is +/// the question a monitoring tool usually leaves to its documentation — and which decides how much the other +/// screens are worth. +struct CoverageView: View { + @EnvironmentObject var monitor: TrafficMonitor + + var body: some View { CoverageContent(inspection: monitor.inspection) } +} + +private struct CoverageContent: View { + @ObservedObject var inspection: InspectionController + @EnvironmentObject var monitor: TrafficMonitor + @EnvironmentObject var nav: AppNavigation + @AppStorage("coverage.window") private var window: AgentWindow = .day + @State private var rows: [AppCoverage] = [] + @State private var loaded = false + + var body: some View { + ScrollView { + LazyVStack(alignment: .leading, spacing: 18) { + summary + if rows.isEmpty && loaded { + ContentUnavailableView { + Label(L("Nothing recorded yet"), systemImage: "chart.bar.doc.horizontal") + } description: { + Text(L("Coverage is worked out from traffic Flowlight has already recorded. Leave it running for a moment.")) + } + } else { + table + } + } + .padding(Measure.gutter) + } + .navigationTitle(L("Coverage")) + .navigationSubtitle(subtitle) + .toolbar { + ToolbarItem(placement: .primaryAction) { + Picker(L("Window"), selection: $window) { + ForEach(AgentWindow.allCases) { Text($0.title).tag($0) } + } + .pickerStyle(.segmented).labelsHidden() + } + } + .task(id: LoadKey(window: window, version: monitor.dataVersion, inspecting: inspection.enabled)) { await load() } + } + + private struct LoadKey: Equatable { var window: AgentWindow; var version: Int; var inspecting: Bool } + + private var subtitle: String { + guard !rows.isEmpty else { return L("Nothing recorded yet") } + return L("%@ of traffic fully accounted for", + CoverageReport.overall(rows).formatted(.percent.precision(.fractionLength(0)))) + } + + /// The two engine-wide facts first, because they explain most of the rows underneath and are fixed in one + /// place rather than per app. + private var summary: some View { + VStack(alignment: .leading, spacing: 8) { + CaptureHeading(L("How much of this Mac is covered")) + VStack(alignment: .leading, spacing: 10) { + CaptureFact(icon: monitor.mode == .networkExtension && !monitor.extensionFellBack + ? "checkmark.seal" : "exclamationmark.triangle", + tint: monitor.mode == .networkExtension && !monitor.extensionFellBack ? .green : .orange, + title: captureTitle, detail: captureDetail) + CaptureFact(icon: inspection.enabled ? "checkmark.seal" : "info.circle", + tint: inspection.enabled ? .green : .secondary, + title: inspection.enabled ? L("HTTPS inspection is on") : L("HTTPS inspection is off"), + detail: inspection.enabled + ? L("What an app sent is readable only where the app was routed through the proxy. Anything else is counted and named, but its contents were never offered to Flowlight.") + : L("Flowlight sees which connections were made and where to, but not what was inside them. Nothing is decrypted until you turn inspection on.")) + CaptureFact(icon: "questionmark.circle", tint: .secondary, + title: L("What no engine can see"), + detail: L("Traffic from before capture started, system services that content filters are never shown, apps that pin their certificates, QUIC the proxy is never offered, and an agent's local MCP server talking over a pipe. None of these appear anywhere in Flowlight, so they are absent from these figures too.")) + } + .padding(12) + .background(.quaternary.opacity(0.35)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + } + + private var captureTitle: String { + switch monitor.mode { + case .networkExtension: + return monitor.extensionFellBack ? L("Sampling with nettop, not the filter") : L("The filter sees connections as they open") + case .nettop: + return L("Sampling with nettop every second") + } + } + + private var captureDetail: String { + switch monitor.mode { + case .networkExtension where !monitor.extensionFellBack: + return L("Every eligible TCP and UDP flow is attributed as it opens, including connections too short for a sampler to catch.") + case .networkExtension: + return L("The filter isn't answering, so these figures come from the sampler: a connection that opens and closes between two readings is missing entirely, and no percentage below can show it.") + case .nettop: + return L("Byte counts are exact, but a connection that opens and closes between two readings is never recorded — a quick DNS lookup, a fast API call, a script that runs curl and exits. Only the filter sees those.") + } + } + + private var table: some View { + VStack(alignment: .leading, spacing: 8) { + CaptureHeading(L("By app")) + VStack(spacing: 0) { + ForEach(Array(rows.enumerated()), id: \.element.id) { index, row in + if index > 0 { Divider().padding(.leading, 12) } + CoverageRow(coverage: row) + } + } + .background(.quaternary.opacity(0.35)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + } + + private func load() async { + let from = Date().addingTimeInterval(-window.interval), to = Date() + let grain = window.granularity + let result = try? await monitor.read { db -> ([BreakdownRow], [HTTPExchange]) in + (try db.breakdown(grain, from: from, to: to), + try db.exchanges(since: from, limit: 4000)) + } + guard let (breakdown, exchanges) = result else { return } + rows = CoverageReport.build(rows: breakdown, + inspected: Set(exchanges.map(\.bundleID).filter { !$0.isEmpty }), + excluded: Set(inspection.neverInspect), + mode: monitor.mode, fellBack: monitor.extensionFellBack, + inspectionOn: inspection.enabled && inspection.running, + isDemo: DemoData.isEnabled) + loaded = true + } +} + +/// One app: what is seen, what is named, what is readable — and the one sentence that says which of those is +/// missing, rather than three badges someone has to interpret. +private struct CoverageRow: View { + var coverage: AppCoverage + @EnvironmentObject var nav: AppNavigation + + var body: some View { + HStack(alignment: .top, spacing: 12) { + Image(systemName: coverage.isComplete ? "checkmark.circle.fill" : "circle.dashed") + .foregroundStyle(coverage.isComplete ? Color.green : .orange) + .frame(width: 20) + VStack(alignment: .leading, spacing: 3) { + HStack(spacing: 8) { + Text(coverage.appName).font(.body.weight(.medium)).lineLimit(1) + Text(ByteFormat.string(coverage.bytes)).font(.caption).foregroundStyle(.secondary) + } + Text(gap).font(.caption).foregroundStyle(.secondary).prose() + .fixedSize(horizontal: false, vertical: true) + } + Spacer(minLength: 8) + Text(coverage.named.formatted(.percent.precision(.fractionLength(0)))) + .font(.caption.monospaced()) + .foregroundStyle(coverage.named > 0.99 ? .secondary : .primary) + .help(L("Share of this app's bytes whose destination has a hostname rather than a bare address.")) + } + .padding(12) + .contentShape(Rectangle()) + } + + /// The sentence. Named gaps first, because an unnamed destination is the one a person can act on. + private var gap: String { + var parts: [String] = [] + if coverage.named < 0.99 { + parts.append(L("%@ of its bytes went to addresses with no hostname", + (1 - coverage.named).formatted(.percent.precision(.fractionLength(0))))) + } + switch coverage.inspection { + case .notRouted: parts.append(L("nothing of its traffic reached the proxy, so no contents were read")) + case .excluded: parts.append(L("it is on the Never decrypted list, so its contents are deliberately not read")) + case .reading, .off: break + } + switch coverage.capture { + case .sampler, .fellBack: parts.append(L("short connections may be missing entirely")) + case .filter, .demo: break + } + guard !parts.isEmpty else { return L("Seen as it happened, named, and readable.") } + return parts.joined(separator: " · ") + } +} diff --git a/FlowlightTests/CoverageTests.swift b/FlowlightTests/CoverageTests.swift new file mode 100644 index 0000000..9a0c303 --- /dev/null +++ b/FlowlightTests/CoverageTests.swift @@ -0,0 +1,75 @@ +import XCTest +@testable import Flowlight + +final class CoverageTests: XCTestCase { + private func row(_ bundleID: String, _ name: String, domain: String, bytes: Int64) -> BreakdownRow { + BreakdownRow(bundleID: bundleID, appName: name, appPath: "", domain: domain, remoteIP: "203.0.113.7", + ports: "443", protocols: "https", + counters: FlowCounters(bytesIn: 0, bytesOut: bytes, flows: 1)) + } + + private func build(_ rows: [BreakdownRow], inspected: Set = [], excluded: Set = [], + mode: CaptureMode = .networkExtension, fellBack: Bool = false, + inspectionOn: Bool = true) -> [AppCoverage] { + CoverageReport.build(rows: rows, inspected: inspected, excluded: excluded, mode: mode, + fellBack: fellBack, inspectionOn: inspectionOn, isDemo: false) + } + + /// Naming is measured in bytes, not connections. One unnamed connection carrying a gigabyte is a bigger + /// hole than a hundred carrying a kilobyte, and counting rows ranks them the other way round. + func testNamingIsWeightedByBytes() { + let rows = [row("a", "App", domain: "example.com", bytes: 100), + row("a", "App", domain: "", bytes: 900)] + let coverage = try? XCTUnwrap(build(rows).first) + XCTAssertEqual(coverage?.named ?? 0, 0.1, accuracy: 0.001) + } + + /// The three questions fail independently: an app can be fully seen and entirely unnamed. A row is only + /// complete when nothing is missing, so "covered" can't quietly mean "mostly". + func testAnUnnamedAppIsNotComplete() { + let seen = build([row("a", "App", domain: "example.com", bytes: 100)], inspected: ["a"]) + XCTAssertTrue(seen[0].isComplete) + let unnamed = build([row("b", "Other", domain: "", bytes: 100)], inspected: ["b"]) + XCTAssertFalse(unnamed[0].isComplete) + } + + /// Inspection has four answers and only one of them is a gap. Being on the never-inspect list is a choice + /// someone made, not a failure, and must not be reported as one. + func testInspectionStatesAreDistinguished() { + let rows = [row("a", "Read", domain: "x.com", bytes: 10), row("b", "Skipped", domain: "x.com", bytes: 10), + row("c", "Missed", domain: "x.com", bytes: 10)] + let coverage = build(rows, inspected: ["a"], excluded: ["b"]) + let byID = Dictionary(uniqueKeysWithValues: coverage.map { ($0.bundleID, $0.inspection) }) + XCTAssertEqual(byID["a"], .reading) + XCTAssertEqual(byID["b"], .excluded) + XCTAssertEqual(byID["c"], .notRouted) + XCTAssertTrue(build(rows, inspectionOn: false).allSatisfy { $0.inspection == .off }) + } + + /// With inspection off, nothing is decrypted for anyone — which is a setting, not a per-app gap, so those + /// rows still count as complete. Otherwise every app on a default install would be flagged. + func testInspectionOffIsNotAPerAppGap() { + XCTAssertTrue(build([row("a", "App", domain: "x.com", bytes: 10)], inspectionOn: false)[0].isComplete) + } + + /// The sampler misses whole connections rather than some bytes of them, so no percentage can express it. + /// Falling back to it counts the same way: what is on screen came from the sampler either way. + func testTheSamplerAndAFallbackBothCountAsSampled() { + XCTAssertEqual(build([row("a", "App", domain: "x.com", bytes: 10)], mode: .nettop)[0].capture, .sampler) + XCTAssertEqual(build([row("a", "App", domain: "x.com", bytes: 10)], fellBack: true)[0].capture, .fellBack) + XCTAssertFalse(build([row("a", "App", domain: "x.com", bytes: 10)], inspected: ["a"], mode: .nettop)[0].isComplete) + } + + /// The overall figure is a share of bytes, not an average of percentages: an app moving a gigabyte and one + /// moving a kilobyte are not half the picture each. + func testOverallIsWeightedByTraffic() { + let rows = [row("big", "Big", domain: "", bytes: 999_000), row("small", "Small", domain: "x.com", bytes: 1_000)] + let coverage = build(rows, inspected: ["big", "small"]) + XCTAssertEqual(CoverageReport.overall(coverage), 0.001, accuracy: 0.0005) + } + + func testEmptyInputIsZeroRatherThanACrash() { + XCTAssertTrue(build([]).isEmpty) + XCTAssertEqual(CoverageReport.overall([]), 0) + } +} diff --git a/docs/docs/index.html b/docs/docs/index.html index 4a98779..9b1cec8 100644 --- a/docs/docs/index.html +++ b/docs/docs/index.html @@ -90,7 +90,7 @@

Using Flowlight

Settings and data @@ -423,6 +423,24 @@

Capture: engines and sources

needs no filter and works alongside them. See the developer guide for how it's built.

+

Coverage: what Flowlight can't account for

+

Every other screen answers what happened. Coverage answers what you would not have been told, which is what + decides how much the other screens are worth. It asks three questions per app, because they fail independently.

+

Was it seen? The Network Extension sees each flow as it opens. The nettop sampler reads counters once a + second, so a connection that opens and closes between two readings — a DNS lookup, a fast API call, a script that runs curl + and exits — is missing entirely. That is a gap no percentage can express, so it is stated in words rather than scored.

+

Was it named? The share of that app's bytes that went to a destination with a hostname rather than + a bare address. Bytes, not connections: one unnamed connection carrying a gigabyte is a bigger hole than a hundred carrying a + kilobyte each, and counting connections ranks them the other way round.

+

Was it readable? Whether HTTPS inspection decrypted anything of that app's traffic. Four answers, and only + one is a gap: it is being read; it never reached the proxy; it is on the never-inspect list, which is a choice you made; or + inspection is off, which is a setting rather than a per-app failure.

+

What no engine sees at all is listed on the screen rather than left to this page: traffic from before + capture started, system services that content filters are never shown, apps that pin their certificates, QUIC the proxy is + never offered, and an agent's local MCP server talking to it over a pipe. None of that appears anywhere in Flowlight, so it is + absent from these figures too — a coverage number that counted only what it could see would be a reassurance rather than a + fact.

+

Devices: Bluetooth, USB and external storage

A Mac sends and receives over more than TCP and UDP. The Devices screen covers the rest, and it is off until you turn it on — not because it needs a permission Flowlight doesn't already have, but because watching more of your diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 521af15..db9f82c 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -83,7 +83,7 @@ Documentation Refusing connectionsGuardrails Sources - CaptureDevices + CaptureCoverageDevices Settings and data Focus modeBackgroundExport @@ -498,6 +498,29 @@ open build/Build/Products/Release/Flowlight.app needs no filter and works alongside them. See the developer guide for how it's built. + Coverage: what Flowlight can't account for + + Every other screen answers what happened. Coverage answers what you would not have been told, which is what + decides how much the other screens are worth. It asks three questions per app, because they fail independently. + + Was it seen? The Network Extension sees each flow as it opens. The nettop sampler reads counters once a + second, so a connection that opens and closes between two readings — a DNS lookup, a fast API call, a script that runs curl + and exits — is missing entirely. That is a gap no percentage can express, so it is stated in words rather than scored. + + Was it named? The share of that app's bytes that went to a destination with a hostname rather than + a bare address. Bytes, not connections: one unnamed connection carrying a gigabyte is a bigger hole than a hundred carrying a + kilobyte each, and counting connections ranks them the other way round. + + Was it readable? Whether HTTPS inspection decrypted anything of that app's traffic. Four answers, and only + one is a gap: it is being read; it never reached the proxy; it is on the never-inspect list, which is a choice you made; or + inspection is off, which is a setting rather than a per-app failure. + + What no engine sees at all is listed on the screen rather than left to this page: traffic from before + capture started, system services that content filters are never shown, apps that pin their certificates, QUIC the proxy is + never offered, and an agent's local MCP server talking to it over a pipe. None of that appears anywhere in Flowlight, so it is + absent from these figures too — a coverage number that counted only what it could see would be a reassurance rather than a + fact. + Devices: Bluetooth, USB and external storage A Mac sends and receives over more than TCP and UDP. The Devices screen covers the rest, and it is off diff --git a/docs/sitemap.xml b/docs/sitemap.xml index 357be42..653ff28 100644 --- a/docs/sitemap.xml +++ b/docs/sitemap.xml @@ -1,7 +1,7 @@ https://flowlight.xinbetween.com/about/2026-09-25 - https://flowlight.xinbetween.com/docs/2026-09-26 + https://flowlight.xinbetween.com/docs/2026-09-27 https://flowlight.xinbetween.com/2026-09-27 https://flowlight.xinbetween.com/privacy/2026-09-25 https://flowlight.xinbetween.com/releases/2026-09-27 diff --git a/site/pages/docs.html b/site/pages/docs.html index ad617b0..aa57bd1 100644 --- a/site/pages/docs.html +++ b/site/pages/docs.html @@ -36,7 +36,7 @@

Using Flowlight

Settings and data @@ -369,6 +369,24 @@

Capture: engines and sources

needs no filter and works alongside them. See the developer guide for how it's built.

+

Coverage: what Flowlight can't account for

+

Every other screen answers what happened. Coverage answers what you would not have been told, which is what + decides how much the other screens are worth. It asks three questions per app, because they fail independently.

+

Was it seen? The Network Extension sees each flow as it opens. The nettop sampler reads counters once a + second, so a connection that opens and closes between two readings — a DNS lookup, a fast API call, a script that runs curl + and exits — is missing entirely. That is a gap no percentage can express, so it is stated in words rather than scored.

+

Was it named? The share of that app's bytes that went to a destination with a hostname rather than + a bare address. Bytes, not connections: one unnamed connection carrying a gigabyte is a bigger hole than a hundred carrying a + kilobyte each, and counting connections ranks them the other way round.

+

Was it readable? Whether HTTPS inspection decrypted anything of that app's traffic. Four answers, and only + one is a gap: it is being read; it never reached the proxy; it is on the never-inspect list, which is a choice you made; or + inspection is off, which is a setting rather than a per-app failure.

+

What no engine sees at all is listed on the screen rather than left to this page: traffic from before + capture started, system services that content filters are never shown, apps that pin their certificates, QUIC the proxy is + never offered, and an agent's local MCP server talking to it over a pipe. None of that appears anywhere in Flowlight, so it is + absent from these figures too — a coverage number that counted only what it could see would be a reassurance rather than a + fact.

+

Devices: Bluetooth, USB and external storage

A Mac sends and receives over more than TCP and UDP. The Devices screen covers the rest, and it is off until you turn it on — not because it needs a permission Flowlight doesn't already have, but because watching more of your From ea14698f7b9563dcbedd09cbafcaff037fc4f649 Mon Sep 17 00:00:00 2001 From: blessdyb Date: Sun, 27 Sep 2026 01:57:49 -0700 Subject: [PATCH 2/2] Flowlight 0.9.0 Version, notes and rebuilt site, on the release branch until the tag has published. Co-Authored-By: Claude Opus 5 (1M context) --- docs/404.html | 2 +- docs/about/index.html | 2 +- docs/de/about/index.html | 2 +- docs/de/docs/index.html | 4 ++-- docs/de/index.html | 6 +++--- docs/de/privacy/index.html | 2 +- docs/docs/index.html | 4 ++-- docs/es/about/index.html | 2 +- docs/es/docs/index.html | 4 ++-- docs/es/index.html | 6 +++--- docs/es/privacy/index.html | 2 +- docs/fr/about/index.html | 2 +- docs/fr/docs/index.html | 4 ++-- docs/fr/index.html | 6 +++--- docs/fr/privacy/index.html | 2 +- docs/index.html | 6 +++--- docs/it/about/index.html | 2 +- docs/it/docs/index.html | 4 ++-- docs/it/index.html | 6 +++--- docs/it/privacy/index.html | 2 +- docs/ja/about/index.html | 2 +- docs/ja/docs/index.html | 4 ++-- docs/ja/index.html | 6 +++--- docs/ja/privacy/index.html | 2 +- docs/ko/about/index.html | 2 +- docs/ko/docs/index.html | 4 ++-- docs/ko/index.html | 6 +++--- docs/ko/privacy/index.html | 2 +- docs/llms-full.txt | 28 +++++++++++++++++++++++++--- docs/llms.txt | 2 +- docs/privacy/index.html | 2 +- docs/pt-PT/about/index.html | 2 +- docs/pt-PT/docs/index.html | 4 ++-- docs/pt-PT/index.html | 6 +++--- docs/pt-PT/privacy/index.html | 2 +- docs/releases/index.html | 24 ++++++++++++++++++++++-- docs/zh-Hans/about/index.html | 2 +- docs/zh-Hans/docs/index.html | 4 ++-- docs/zh-Hans/index.html | 6 +++--- docs/zh-Hans/privacy/index.html | 2 +- docs/zh-Hant/about/index.html | 2 +- docs/zh-Hant/docs/index.html | 4 ++-- docs/zh-Hant/index.html | 6 +++--- docs/zh-Hant/privacy/index.html | 2 +- project.yml | 2 +- site/pages/releases.html | 22 +++++++++++++++++++++- 46 files changed, 141 insertions(+), 79 deletions(-) diff --git a/docs/404.html b/docs/404.html index 0c439bd..9c3b48a 100644 --- a/docs/404.html +++ b/docs/404.html @@ -81,7 +81,7 @@

That page isn't here

Try the home page,

diff --git a/docs/about/index.html b/docs/about/index.html index d8f93f8..eb4bd98 100644 --- a/docs/about/index.html +++ b/docs/about/index.html @@ -139,7 +139,7 @@

Thanks

diff --git a/docs/de/about/index.html b/docs/de/about/index.html index c80f599..cee2ddb 100644 --- a/docs/de/about/index.html +++ b/docs/de/about/index.html @@ -138,7 +138,7 @@

Dank

diff --git a/docs/de/docs/index.html b/docs/de/docs/index.html index 353820e..2b2e512 100644 --- a/docs/de/docs/index.html +++ b/docs/de/docs/index.html @@ -62,7 +62,7 @@

Dokumentation

Flowlight benutzen

-

Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.8.2, macOS 15 oder neuer.

+

Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.9.0, macOS 15 oder neuer.

diff --git a/docs/de/index.html b/docs/de/index.html index 5db4dbc..b97de69 100644 --- a/docs/de/index.html +++ b/docs/de/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

Sieh, was deine Apps im Netz tun.
brew install --cask xinbetween/tap/flowlight

-

v0.8.2macOS 15+UniversalGPL-3.0Keine Telemetrie

+

v0.9.0macOS 15+UniversalGPL-3.0Keine Telemetrie

@@ -613,7 +613,7 @@

Wisse, was deinen Mac verlässt.

diff --git a/docs/de/privacy/index.html b/docs/de/privacy/index.html index dd60cb4..6194654 100644 --- a/docs/de/privacy/index.html +++ b/docs/de/privacy/index.html @@ -168,7 +168,7 @@

Kontakt

diff --git a/docs/docs/index.html b/docs/docs/index.html index 9b1cec8..e15da4c 100644 --- a/docs/docs/index.html +++ b/docs/docs/index.html @@ -62,7 +62,7 @@

Documentation

Using Flowlight

-

Everything from the first launch to tuning the agent rules. Flowlight 0.8.2, macOS 15 or later.

+

Everything from the first launch to tuning the agent rules. Flowlight 0.9.0, macOS 15 or later.

diff --git a/docs/es/about/index.html b/docs/es/about/index.html index 82ac38e..e78f628 100644 --- a/docs/es/about/index.html +++ b/docs/es/about/index.html @@ -138,7 +138,7 @@

Agradecimientos

diff --git a/docs/es/docs/index.html b/docs/es/docs/index.html index cbbc663..441435c 100644 --- a/docs/es/docs/index.html +++ b/docs/es/docs/index.html @@ -62,7 +62,7 @@

Documentación

Usar Flowlight

-

Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.8.2, macOS 15 o posterior.

+

Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.9.0, macOS 15 o posterior.

diff --git a/docs/es/index.html b/docs/es/index.html index cd6d881..b54de1b 100644 --- a/docs/es/index.html +++ b/docs/es/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

Observa la actividad de red de tus apps.

brew install --cask xinbetween/tap/flowlight

-

v0.8.2macOS 15+UniversalGPL-3.0Sin telemetría

+

v0.9.0macOS 15+UniversalGPL-3.0Sin telemetría

@@ -613,7 +613,7 @@

Ten claro qué sale de tu Mac.

diff --git a/docs/es/privacy/index.html b/docs/es/privacy/index.html index 7276c6e..abf6f68 100644 --- a/docs/es/privacy/index.html +++ b/docs/es/privacy/index.html @@ -168,7 +168,7 @@

Contacto

diff --git a/docs/fr/about/index.html b/docs/fr/about/index.html index 28d1cf1..8ab2426 100644 --- a/docs/fr/about/index.html +++ b/docs/fr/about/index.html @@ -138,7 +138,7 @@

Remerciements

diff --git a/docs/fr/docs/index.html b/docs/fr/docs/index.html index 9a4d3ca..1893c88 100644 --- a/docs/fr/docs/index.html +++ b/docs/fr/docs/index.html @@ -62,7 +62,7 @@

Documentation

Utiliser Flowlight

-

Tout, du premier lancement au réglage des règles des agents. Flowlight 0.8.2, macOS 15 ou version ultérieure.

+

Tout, du premier lancement au réglage des règles des agents. Flowlight 0.9.0, macOS 15 ou version ultérieure.

diff --git a/docs/fr/index.html b/docs/fr/index.html index d4a0d67..519cf11 100644 --- a/docs/fr/index.html +++ b/docs/fr/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

Voyez l’activité réseau de vos apps.

brew install --cask xinbetween/tap/flowlight

-

v0.8.2macOS 15+UniverselGPL-3.0Sans télémétrie

+

v0.9.0macOS 15+UniverselGPL-3.0Sans télémétrie

@@ -613,7 +613,7 @@

Sachez ce qui quitte votre Mac.

diff --git a/docs/fr/privacy/index.html b/docs/fr/privacy/index.html index 8618645..c1378e0 100644 --- a/docs/fr/privacy/index.html +++ b/docs/fr/privacy/index.html @@ -168,7 +168,7 @@

Contact

diff --git a/docs/index.html b/docs/index.html index 83f0483..09bc631 100644 --- a/docs/index.html +++ b/docs/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

See your apps' network activity.
brew install --cask xinbetween/tap/flowlight

-

v0.8.2macOS 15+UniversalGPL-3.0No telemetry

+

v0.9.0macOS 15+UniversalGPL-3.0No telemetry

@@ -620,7 +620,7 @@

Know what leaves your Mac.

diff --git a/docs/it/about/index.html b/docs/it/about/index.html index 47a856b..ddba4b2 100644 --- a/docs/it/about/index.html +++ b/docs/it/about/index.html @@ -138,7 +138,7 @@

Ringraziamenti

diff --git a/docs/it/docs/index.html b/docs/it/docs/index.html index 21b7666..22c419e 100644 --- a/docs/it/docs/index.html +++ b/docs/it/docs/index.html @@ -62,7 +62,7 @@

Documentazione

Usare Flowlight

-

Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.8.2, macOS 15 o successivo.

+

Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.9.0, macOS 15 o successivo.

diff --git a/docs/it/index.html b/docs/it/index.html index 2e4a2b8..4295761 100644 --- a/docs/it/index.html +++ b/docs/it/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

Vedi la rete delle tue app.
Ca Metti una stella su GitHub

brew install --cask xinbetween/tap/flowlight

-

v0.8.2macOS 15+UniversaleGPL-3.0Nessuna telemetria

+

v0.9.0macOS 15+UniversaleGPL-3.0Nessuna telemetria

@@ -613,7 +613,7 @@

Sappi che cosa esce dal tuo Mac.

diff --git a/docs/it/privacy/index.html b/docs/it/privacy/index.html index f777b6d..3b1d8cd 100644 --- a/docs/it/privacy/index.html +++ b/docs/it/privacy/index.html @@ -168,7 +168,7 @@

Contatti

diff --git a/docs/ja/about/index.html b/docs/ja/about/index.html index 4128921..cf87744 100644 --- a/docs/ja/about/index.html +++ b/docs/ja/about/index.html @@ -139,7 +139,7 @@

謝辞

diff --git a/docs/ja/docs/index.html b/docs/ja/docs/index.html index 66e4ee3..5f77bbd 100644 --- a/docs/ja/docs/index.html +++ b/docs/ja/docs/index.html @@ -62,7 +62,7 @@

ドキュメント

Flowlight の使い方

-

初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.8.2、macOS 15 以降。

+

初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.9.0、macOS 15 以降。

diff --git a/docs/ja/index.html b/docs/ja/index.html index 0062be1..d5b19fa 100644 --- a/docs/ja/index.html +++ b/docs/ja/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

アプリの通信が見える。
brew install --cask xinbetween/tap/flowlight

-

v0.8.2macOS 15+ユニバーサルGPL-3.0テレメトリなし

+

v0.9.0macOS 15+ユニバーサルGPL-3.0テレメトリなし

@@ -612,7 +612,7 @@

Mac から何が出ていくのかを知る。

diff --git a/docs/ja/privacy/index.html b/docs/ja/privacy/index.html index eefdc42..9d32622 100644 --- a/docs/ja/privacy/index.html +++ b/docs/ja/privacy/index.html @@ -167,7 +167,7 @@

連絡先

diff --git a/docs/ko/about/index.html b/docs/ko/about/index.html index a9df8e2..d02f5bd 100644 --- a/docs/ko/about/index.html +++ b/docs/ko/about/index.html @@ -139,7 +139,7 @@

감사

diff --git a/docs/ko/docs/index.html b/docs/ko/docs/index.html index 136324e..01786b7 100644 --- a/docs/ko/docs/index.html +++ b/docs/ko/docs/index.html @@ -62,7 +62,7 @@

문서

Flowlight 사용하기

-

첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.8.2, macOS 15 이상.

+

첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.9.0, macOS 15 이상.

diff --git a/docs/ko/index.html b/docs/ko/index.html index bed74bf..8e60960 100644 --- a/docs/ko/index.html +++ b/docs/ko/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

앱이 무엇을 하는지 봅니다.

brew install --cask xinbetween/tap/flowlight

-

v0.8.2macOS 15+유니버설GPL-3.0텔레메트리 없음

+

v0.9.0macOS 15+유니버설GPL-3.0텔레메트리 없음

@@ -613,7 +613,7 @@

내 Mac에서 무엇이 나가는지 아세요.

diff --git a/docs/ko/privacy/index.html b/docs/ko/privacy/index.html index 915aab6..f450f04 100644 --- a/docs/ko/privacy/index.html +++ b/docs/ko/privacy/index.html @@ -170,7 +170,7 @@

문의

diff --git a/docs/llms-full.txt b/docs/llms-full.txt index db9f82c..ee52c41 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -62,7 +62,7 @@ Documentation Using Flowlight - Everything from the first launch to tuning the agent rules. Flowlight 0.8.2, macOS 15 or later. + Everything from the first launch to tuning the agent rules. Flowlight 0.9.0, macOS 15 or later. On this page @@ -721,7 +721,7 @@ Understand your AI agents. brew install --cask xinbetween/tap/flowlightCopy - v0.8.2macOS 15+UniversalGPL-3.0No telemetry + v0.9.0macOS 15+UniversalGPL-3.0No telemetry connectionslive @@ -1279,9 +1279,31 @@ Releases Downloads, checksums and full notes for each version are on GitHub Releases. - 0.8.2 + 0.9.0 September 27, 2026Latest + A new screen: Coverage. Every other screen says what happened. This one says what + you would not have been told — per app, and separately for each of the three ways it can go wrong. + Was the connection seen at all: the filter sees flows as they open, while the sampler reads + counters once a second and misses whole connections between readings. Was the destination + named, measured in bytes rather than connections, because one unnamed connection carrying a + gigabyte is a bigger hole than a hundred carrying a kilobyte. And was it readable — with + being on the Never decrypted list counted as the choice it is, not as a failure. + + What no engine sees is on the screen, not in a footnote: traffic from before + capture started, system services content filters are never shown, apps that pin their certificates, + QUIC, and an agent's local MCP server talking over a pipe. A coverage figure that counted only what + it could see would be a reassurance rather than a fact. + + A crash that was waiting for a tenth screen. The sidebar derived ⌘1 to ⌘9 from + each item's position, so adding a tenth produced an impossible keyboard shortcut and trapped on + launch. It runs ⌘1–⌘9 then ⌘0 now, the way browsers number tabs. + + Downloads and checksums → + + 0.8.2 +September 27, 2026 + An update is verified before it is installed. The checksum comparison treated "no checksum published", "no line for this disk image" and "the file didn't parse" as a match — every way of not knowing was read as knowing it was right. It now requires a published checksum and diff --git a/docs/llms.txt b/docs/llms.txt index cd01d65..9b4705e 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -1,6 +1,6 @@ # Flowlight -> Free, open-source (GPL-3.0) application-aware network monitor for macOS, with focused visibility into AI agents. It attributes observed TCP and UDP activity to the application that made it and records the destination, protocol and byte counts, keeping local history from second to year. Recognized AI agents are listed by name along with the tools and MCP servers they start, under per-agent allowlists. It can also refuse, once asked: a rule blocks an application, a destination or a URL for as long as you specify, and a guardrail withholds a tool from an agent before its model is offered it. Runs on macOS 15 or later; capture is by a sampler or a Network Extension. Current version: 0.8.2. +> Free, open-source (GPL-3.0) application-aware network monitor for macOS, with focused visibility into AI agents. It attributes observed TCP and UDP activity to the application that made it and records the destination, protocol and byte counts, keeping local history from second to year. Recognized AI agents are listed by name along with the tools and MCP servers they start, under per-agent allowlists. It can also refuse, once asked: a rule blocks an application, a destination or a URL for as long as you specify, and a guardrail withholds a tool from an agent before its model is offered it. Runs on macOS 15 or later; capture is by a sampler or a Network Extension. Current version: 0.9.0. - [Download Flowlight.dmg](https://github.com/xinbetween/flowlight/releases/latest/download/Flowlight.dmg) - [Source code](https://github.com/xinbetween/flowlight) diff --git a/docs/privacy/index.html b/docs/privacy/index.html index 1224dfe..c326a98 100644 --- a/docs/privacy/index.html +++ b/docs/privacy/index.html @@ -169,7 +169,7 @@

Contact

diff --git a/docs/pt-PT/about/index.html b/docs/pt-PT/about/index.html index 9025e5d..238ba81 100644 --- a/docs/pt-PT/about/index.html +++ b/docs/pt-PT/about/index.html @@ -138,7 +138,7 @@

Agradecimentos

diff --git a/docs/pt-PT/docs/index.html b/docs/pt-PT/docs/index.html index 09c41f6..e0b9aa9 100644 --- a/docs/pt-PT/docs/index.html +++ b/docs/pt-PT/docs/index.html @@ -62,7 +62,7 @@

Documentação

Usar o Flowlight

-

Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.8.2, macOS 15 ou posterior.

+

Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.9.0, macOS 15 ou posterior.

diff --git a/docs/pt-PT/index.html b/docs/pt-PT/index.html index 5973523..bad2918 100644 --- a/docs/pt-PT/index.html +++ b/docs/pt-PT/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

Veja a atividade de rede das suas apps.

brew install --cask xinbetween/tap/flowlight

-

v0.8.2macOS 15+UniversalGPL-3.0Sem telemetria

+

v0.9.0macOS 15+UniversalGPL-3.0Sem telemetria

@@ -613,7 +613,7 @@

Saiba o que sai do seu Mac.

diff --git a/docs/pt-PT/privacy/index.html b/docs/pt-PT/privacy/index.html index 2dc6d65..0ffc77c 100644 --- a/docs/pt-PT/privacy/index.html +++ b/docs/pt-PT/privacy/index.html @@ -168,7 +168,7 @@

Contacto

diff --git a/docs/releases/index.html b/docs/releases/index.html index e6406f5..e9068ba 100644 --- a/docs/releases/index.html +++ b/docs/releases/index.html @@ -56,7 +56,27 @@

What's new

-

0.8.2

Latest
+

0.9.0

Latest
+
    +
  • A new screen: Coverage. Every other screen says what happened. This one says what + you would not have been told — per app, and separately for each of the three ways it can go wrong. + Was the connection seen at all: the filter sees flows as they open, while the sampler reads + counters once a second and misses whole connections between readings. Was the destination + named, measured in bytes rather than connections, because one unnamed connection carrying a + gigabyte is a bigger hole than a hundred carrying a kilobyte. And was it readable — with + being on the Never decrypted list counted as the choice it is, not as a failure.
  • +
  • What no engine sees is on the screen, not in a footnote: traffic from before + capture started, system services content filters are never shown, apps that pin their certificates, + QUIC, and an agent's local MCP server talking over a pipe. A coverage figure that counted only what + it could see would be a reassurance rather than a fact.
  • +
  • A crash that was waiting for a tenth screen. The sidebar derived ⌘1 to ⌘9 from + each item's position, so adding a tenth produced an impossible keyboard shortcut and trapped on + launch. It runs ⌘1–⌘9 then ⌘0 now, the way browsers number tabs.
  • +
+

Downloads and checksums →

+
+
+

0.8.2

  • An update is verified before it is installed. The checksum comparison treated "no checksum published", "no line for this disk image" and "the file didn't parse" as a match — @@ -757,7 +777,7 @@

    What's new

diff --git a/docs/zh-Hans/about/index.html b/docs/zh-Hans/about/index.html index fee8244..d4f53d1 100644 --- a/docs/zh-Hans/about/index.html +++ b/docs/zh-Hans/about/index.html @@ -138,7 +138,7 @@

致谢

diff --git a/docs/zh-Hans/docs/index.html b/docs/zh-Hans/docs/index.html index 99f743d..10a2e2e 100644 --- a/docs/zh-Hans/docs/index.html +++ b/docs/zh-Hans/docs/index.html @@ -62,7 +62,7 @@

文档

使用 Flowlight

-

从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.8.2,macOS 15 或更高版本。

+

从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.9.0,macOS 15 或更高版本。

diff --git a/docs/zh-Hans/index.html b/docs/zh-Hans/index.html index 1e2404c..2856ab6 100644 --- a/docs/zh-Hans/index.html +++ b/docs/zh-Hans/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

看清应用的网络活动。

brew install --cask xinbetween/tap/flowlight

-

v0.8.2macOS 15+通用架构GPL-3.0无遥测

+

v0.9.0macOS 15+通用架构GPL-3.0无遥测

@@ -611,7 +611,7 @@

知道什么离开了你的 Mac。

diff --git a/docs/zh-Hans/privacy/index.html b/docs/zh-Hans/privacy/index.html index 5a60c90..bf721c6 100644 --- a/docs/zh-Hans/privacy/index.html +++ b/docs/zh-Hans/privacy/index.html @@ -168,7 +168,7 @@

联系方式

diff --git a/docs/zh-Hant/about/index.html b/docs/zh-Hant/about/index.html index 7ed8441..210bdf5 100644 --- a/docs/zh-Hant/about/index.html +++ b/docs/zh-Hant/about/index.html @@ -138,7 +138,7 @@

致謝

diff --git a/docs/zh-Hant/docs/index.html b/docs/zh-Hant/docs/index.html index c74c06c..01b86e1 100644 --- a/docs/zh-Hant/docs/index.html +++ b/docs/zh-Hant/docs/index.html @@ -62,7 +62,7 @@

說明文件

使用 Flowlight

-

從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.8.2,macOS 15 或以上版本。

+

從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.9.0,macOS 15 或以上版本。

diff --git a/docs/zh-Hant/index.html b/docs/zh-Hant/index.html index aa97154..af8af18 100644 --- a/docs/zh-Hant/index.html +++ b/docs/zh-Hant/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

看見每個 App 的網路活動。
brew install --cask xinbetween/tap/flowlight

-

v0.8.2macOS 15+通用架構GPL-3.0無遙測

+

v0.9.0macOS 15+通用架構GPL-3.0無遙測

@@ -612,7 +612,7 @@

知道有什麼離開了你的 Mac。

diff --git a/docs/zh-Hant/privacy/index.html b/docs/zh-Hant/privacy/index.html index bde22cc..ec21e5f 100644 --- a/docs/zh-Hant/privacy/index.html +++ b/docs/zh-Hant/privacy/index.html @@ -168,7 +168,7 @@

聯絡

diff --git a/project.yml b/project.yml index c27d634..3efc99c 100644 --- a/project.yml +++ b/project.yml @@ -11,7 +11,7 @@ settings: DEVELOPMENT_TEAM: "" CODE_SIGN_STYLE: Automatic ENABLE_HARDENED_RUNTIME: YES - MARKETING_VERSION: "0.8.2" + MARKETING_VERSION: "0.9.0" CURRENT_PROJECT_VERSION: "21" targets: Flowlight: diff --git a/site/pages/releases.html b/site/pages/releases.html index 037b59d..edfc3f4 100644 --- a/site/pages/releases.html +++ b/site/pages/releases.html @@ -13,7 +13,27 @@

What's new

-

0.8.2

Latest
+

0.9.0

Latest
+
    +
  • A new screen: Coverage. Every other screen says what happened. This one says what + you would not have been told — per app, and separately for each of the three ways it can go wrong. + Was the connection seen at all: the filter sees flows as they open, while the sampler reads + counters once a second and misses whole connections between readings. Was the destination + named, measured in bytes rather than connections, because one unnamed connection carrying a + gigabyte is a bigger hole than a hundred carrying a kilobyte. And was it readable — with + being on the Never decrypted list counted as the choice it is, not as a failure.
  • +
  • What no engine sees is on the screen, not in a footnote: traffic from before + capture started, system services content filters are never shown, apps that pin their certificates, + QUIC, and an agent's local MCP server talking over a pipe. A coverage figure that counted only what + it could see would be a reassurance rather than a fact.
  • +
  • A crash that was waiting for a tenth screen. The sidebar derived ⌘1 to ⌘9 from + each item's position, so adding a tenth produced an impossible keyboard shortcut and trapped on + launch. It runs ⌘1–⌘9 then ⌘0 now, the way browsers number tabs.
  • +
+

Downloads and checksums →

+
+
+

0.8.2

  • An update is verified before it is installed. The checksum comparison treated "no checksum published", "no line for this disk image" and "the file didn't parse" as a match —