diff --git a/Flowlight/App/FlowlightApp.swift b/Flowlight/App/FlowlightApp.swift
index 3782434f..4d146b04 100644
--- a/Flowlight/App/FlowlightApp.swift
+++ b/Flowlight/App/FlowlightApp.swift
@@ -9,6 +9,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
// with its own database, and it must never strip the enforcement the real running app owns.
guard !FlowlightApp.runningTests, !DemoData.isEnabled else { return }
SettingsEnforcer.shared.relaxAll()
+ UpdateLaunchPolicy.markCleanExit()
}
}
diff --git a/Flowlight/App/UpdateChecker.swift b/Flowlight/App/UpdateChecker.swift
index 55983e15..fe3c15b9 100644
--- a/Flowlight/App/UpdateChecker.swift
+++ b/Flowlight/App/UpdateChecker.swift
@@ -70,6 +70,27 @@ enum VersionCompare {
}
}
+/// The one-shot clean-exit marker that decides whether the next automatic launch check should promote a discovered
+/// update. Kept separate from networking so the lifecycle decision is testable without contacting GitHub.
+enum UpdateLaunchPolicy {
+ static let cleanRelaunchKey = "updates.pendingCleanRelaunchCheck"
+
+ static func markCleanExit(defaults: UserDefaults = .standard) {
+ defaults.set(true, forKey: cleanRelaunchKey)
+ }
+
+ /// Reads and clears the marker before any network work, so one clean exit produces at most one promoted check.
+ static func consumeCleanRelaunch(defaults: UserDefaults = .standard) -> Bool {
+ let pending = defaults.bool(forKey: cleanRelaunchKey)
+ defaults.removeObject(forKey: cleanRelaunchKey)
+ return pending
+ }
+
+ static func shouldCheckAtLaunch(cleanRelaunch: Bool, lastCheck: Date?, now: Date = Date()) -> Bool {
+ cleanRelaunch || lastCheck.map { now.timeIntervalSince($0) >= 12 * 3600 } ?? true
+ }
+}
+
/// Checks GitHub Releases for a newer Flowlight and fetches the disk image on request.
/// It makes one anonymous HTTPS request per check (no cookies, no identifiers beyond what any
/// request carries: IP address and a `Flowlight/` user agent), and it can be turned off.
@@ -99,6 +120,8 @@ final class UpdateChecker: ObservableObject {
let repository: String
private var timer: Timer?
private var downloadTask: Task?
+ /// Fixture seam for release metadata only; downloads always use the private ephemeral session below.
+ private let releaseTransport: (@Sendable (URL) async throws -> (Data, URLResponse))?
private lazy var session: URLSession = {
let config = URLSessionConfiguration.ephemeral
@@ -108,9 +131,11 @@ final class UpdateChecker: ObservableObject {
return URLSession(configuration: config)
}()
- init(bundle: Bundle = .main) {
+ init(bundle: Bundle = .main,
+ releaseTransport: (@Sendable (URL) async throws -> (Data, URLResponse))? = nil) {
currentVersion = bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0"
repository = bundle.object(forInfoDictionaryKey: "FLUpdateRepository") as? String ?? "xinbetween/flowlight"
+ self.releaseTransport = releaseTransport
UserDefaults.standard.register(defaults: [Keys.automatic: true])
}
@@ -133,12 +158,16 @@ final class UpdateChecker: ObservableObject {
func start() {
guard !DemoData.isEnabled else { return }
+ // Consume before checking the preference: a clean exit belongs to this launch, not a later launch after
+ // automatic updates happen to be turned back on.
+ let cleanRelaunch = UpdateLaunchPolicy.consumeCleanRelaunch()
schedule()
guard automatic else { return }
- // Shortly after launch, unless we checked within the last 12 hours.
- if let lastCheck, Date().timeIntervalSince(lastCheck) < 12 * 3600 { return }
+ // A clean quit followed by relaunch gets one fresh, promoted check. Ordinary launches stay quiet when a
+ // recent successful check exists; both wait for the primary UI and capture setup to settle first.
+ guard UpdateLaunchPolicy.shouldCheckAtLaunch(cleanRelaunch: cleanRelaunch, lastCheck: lastCheck) else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 8) { [weak self] in
- Task { await self?.check(userInitiated: false) }
+ Task { await self?.check(userInitiated: false, promoteIfAvailable: cleanRelaunch) }
}
}
@@ -150,7 +179,7 @@ final class UpdateChecker: ObservableObject {
}
}
- func check(userInitiated: Bool) async {
+ func check(userInitiated: Bool, promoteIfAvailable: Bool = false) async {
switch state {
case .downloading, .ready, .installing:
if userInitiated { showWindow = true }
@@ -160,7 +189,12 @@ final class UpdateChecker: ObservableObject {
state = .checking
do {
let url = URL(string: "https://api.github.com/repos/\(repository)/releases/latest")!
- let (data, response) = try await session.data(from: url)
+ let (data, response): (Data, URLResponse)
+ if let releaseTransport {
+ (data, response) = try await releaseTransport(url)
+ } else {
+ (data, response) = try await session.data(from: url)
+ }
let code = (response as? HTTPURLResponse)?.statusCode ?? 0
guard code == 200 else { throw code == 404 ? UpdateError.noRelease : UpdateError.badResponse(code) }
let release = try AppRelease.parse(data)
@@ -174,7 +208,9 @@ final class UpdateChecker: ObservableObject {
// A background check doesn't interrupt what you're doing: it notifies, and the sidebar shows a
// badge. The notification is posted once per version.
let defaults = UserDefaults.standard
- if defaults.string(forKey: Keys.notified) != pending.version {
+ if promoteIfAvailable {
+ showWindow = true
+ } else if defaults.string(forKey: Keys.notified) != pending.version {
defaults.set(pending.version, forKey: Keys.notified)
Notifier.postUpdate(version: pending.version, summary: ReleaseNotes.headline(pending.notes))
}
diff --git a/Flowlight/Inspection/MockRule.swift b/Flowlight/Inspection/MockRule.swift
index ac38686c..b510e79c 100644
--- a/Flowlight/Inspection/MockRule.swift
+++ b/Flowlight/Inspection/MockRule.swift
@@ -1,5 +1,41 @@
import Foundation
+/// How a saved mock body is represented in preferences. Text is emitted as UTF-8; hex is a lossless editable
+/// representation of arbitrary captured bytes.
+enum MockBodyEncoding: String, Codable, Equatable, Sendable {
+ case text
+ case hex
+}
+
+/// Captured body data shown while authoring a mock. This is draft-only data: it is never part of rule matching and
+/// request values are never written into the mock-rule preferences.
+struct MockBodyDraft: Equatable, Sendable {
+ var text: String
+ var encoding: MockBodyEncoding
+ var byteCount: Int
+ var unavailableReason: String?
+
+ static func captured(_ data: Data?, truncated: Bool, side: String) -> MockBodyDraft {
+ if truncated {
+ return MockBodyDraft(text: "", encoding: .text, byteCount: 0,
+ unavailableReason: L("The captured %@ body was truncated, so Flowlight cannot show it as a complete value.", side))
+ }
+ guard let data else {
+ return MockBodyDraft(text: "", encoding: .text, byteCount: 0,
+ unavailableReason: L("The captured %@ body is unavailable.", side))
+ }
+ switch BodyContent.classify(data) {
+ case .binary:
+ return MockBodyDraft(text: MockRule.hex(data), encoding: .hex, byteCount: data.count, unavailableReason: nil)
+ case .empty:
+ return MockBodyDraft(text: "", encoding: .text, byteCount: 0, unavailableReason: nil)
+ default:
+ return MockBodyDraft(text: String(decoding: data, as: UTF8.self), encoding: .text,
+ byteCount: data.count, unavailableReason: nil)
+ }
+ }
+}
+
/// A canned answer for one endpoint: "reply to `POST api.example.com/v1/items` with a 503, after eight seconds".
///
/// It exists to watch an agent cope with an API that fails, stalls or answers with something odd, without breaking
@@ -21,6 +57,8 @@ struct MockRule: Codable, Equatable, Identifiable, Sendable {
var status = 500
var headers: [HTTPHeader] = []
var body = ""
+ /// Old rules decode as text. Hex lets a mock replay a captured binary response without corrupting it as UTF-8.
+ var bodyEncoding: MockBodyEncoding = .text
/// Seconds to wait before answering, so "the API stalls" is testable. The recorded exchange shows the wait as
/// its duration.
var delay: Double = 0
@@ -49,11 +87,11 @@ struct MockRule: Codable, Equatable, Identifiable, Sendable {
}
init(id: UUID = UUID(), enabled: Bool = true, name: String = "", host: String = "", path: String = "*",
- method: String = "", status: Int = 500, headers: [HTTPHeader] = [], body: String = "", delay: Double = 0,
- blocked: Bool = false) {
+ method: String = "", status: Int = 500, headers: [HTTPHeader] = [], body: String = "",
+ bodyEncoding: MockBodyEncoding = .text, delay: Double = 0, blocked: Bool = false) {
self.id = id; self.enabled = enabled; self.name = name; self.host = host; self.path = path
- self.method = method; self.status = status; self.headers = headers; self.body = body; self.delay = delay
- self.blocked = blocked
+ self.method = method; self.status = status; self.headers = headers; self.body = body
+ self.bodyEncoding = bodyEncoding; self.delay = delay; self.blocked = blocked
}
}
@@ -71,11 +109,36 @@ extension MockRule {
status = try c.decodeIfPresent(Int.self, forKey: .status) ?? 500
headers = try c.decodeIfPresent([HTTPHeader].self, forKey: .headers) ?? []
body = try c.decodeIfPresent(String.self, forKey: .body) ?? ""
+ bodyEncoding = try c.decodeIfPresent(MockBodyEncoding.self, forKey: .bodyEncoding) ?? .text
delay = try c.decodeIfPresent(Double.self, forKey: .delay) ?? 0
blocked = try c.decodeIfPresent(Bool.self, forKey: .blocked) ?? false
}
}
+/// A selected recorded exchange plus the safe, temporary context needed to author a static mock.
+struct MockRuleDraft: Identifiable, Equatable, Sendable {
+ var id: UUID { rule.id }
+ var rule: MockRule
+ var requestHeaders: [HTTPHeader]
+ var requestBody: MockBodyDraft
+ var responseBodyNote: String?
+
+ init(exchange: HTTPExchange, requestBody: Data?, responseBody: Data?) {
+ var rule = MockRule(mocking: exchange)
+ rule.status = exchange.status ?? rule.status
+ rule.headers = exchange.responseHeaders
+ let capturedResponse = MockBodyDraft.captured(responseBody, truncated: exchange.responseTruncated, side: L("response"))
+ if capturedResponse.unavailableReason == nil {
+ rule.body = capturedResponse.text
+ rule.bodyEncoding = capturedResponse.encoding
+ }
+ self.rule = rule
+ requestHeaders = exchange.requestHeaders
+ self.requestBody = MockBodyDraft.captured(requestBody, truncated: exchange.requestTruncated, side: L("request"))
+ responseBodyNote = capturedResponse.unavailableReason
+ }
+}
+
// MARK: Matching
/// Whether a request is answered by a rule, and which one. Plain functions over plain data, like `AgentPolicy`:
@@ -105,6 +168,34 @@ enum MockRules {
// MARK: The answer
extension MockRule {
+ /// The bytes Flowlight writes for the configured body. Hex ignores whitespace so a wrapped editor remains easy
+ /// to read, but every remaining character must form a complete hexadecimal byte.
+ static func bodyData(_ body: String, encoding: MockBodyEncoding) -> Data? {
+ switch encoding {
+ case .text:
+ return Data(body.utf8)
+ case .hex:
+ let digits = body.filter { !$0.isWhitespace }
+ guard digits.count.isMultiple(of: 2) else { return nil }
+ var data = Data()
+ var index = digits.startIndex
+ while index < digits.endIndex {
+ let next = digits.index(index, offsetBy: 2)
+ guard let byte = UInt8(digits[index.. String {
+ data.map { String(format: "%02X", $0) }.joined()
+ }
+
+ var bodyData: Data? { Self.bodyData(body, encoding: bodyEncoding) }
+ var hasValidBody: Bool { bodyData != nil }
+
/// The canned response as it goes on the wire.
///
/// Flowlight writes the framing headers itself: a mock whose `Content-Length` disagreed with its body would
@@ -112,7 +203,7 @@ extension MockRule {
/// so there's no real response behind this one to keep a reused connection in step with.
func responseBytes() -> Data {
let bodyAllowed = status != 204 && status != 304 && !(100..<200).contains(status)
- let payload = bodyAllowed ? Data(body.utf8) : Data()
+ let payload = bodyAllowed ? (bodyData ?? Data()) : Data()
var text = "HTTP/1.1 \(status) \(Self.reason(status))\r\n"
let written = Set(["content-length", "connection", "transfer-encoding"])
for header in headers {
@@ -121,7 +212,12 @@ extension MockRule {
text += "\(name): \(Self.headerSafe(header.value))\r\n"
}
if bodyAllowed, !headers.contains(where: { $0.name.caseInsensitiveCompare("Content-Type") == .orderedSame }) {
- text += "Content-Type: \(body.hasPrefix("{") || body.hasPrefix("[") ? "application/json" : "text/plain; charset=utf-8")\r\n"
+ let type: String
+ switch bodyEncoding {
+ case .hex: type = "application/octet-stream"
+ case .text: type = body.hasPrefix("{") || body.hasPrefix("[") ? "application/json" : "text/plain; charset=utf-8"
+ }
+ text += "Content-Type: \(type)\r\n"
}
if bodyAllowed { text += "Content-Length: \(payload.count)\r\n" }
// Named in the response as well as in Flowlight, so an app or a log elsewhere on the Mac can also tell that
diff --git a/Flowlight/UI/InspectView.swift b/Flowlight/UI/InspectView.swift
index 008d275e..dcd53e7b 100644
--- a/Flowlight/UI/InspectView.swift
+++ b/Flowlight/UI/InspectView.swift
@@ -27,8 +27,8 @@ private struct InspectContent: View {
@State private var assessments: [Int64: RiskAssessment] = [:]
@State private var window: AgentWindow = .day
@State private var showSetup = false
- /// A rule prefilled from a recorded exchange, waiting in the editor.
- @State private var mockDraft: MockRule?
+ /// A rule plus safe, temporary captured context waiting in the mock editor.
+ @State private var mockDraft: MockRuleDraft?
/// A response-transform rule is created only after its selected exchange's stored response has been read.
@State private var responseTransformDraft: ResponseTransformDraft?
/// Setup was opened to look at the mock rules, so it opens on them.
@@ -98,7 +98,7 @@ private struct InspectContent: View {
if !LocalRiskSettings.isActive { potentialHarmOnly = false }
}
.sheet(item: $mockDraft) { draft in
- MockRuleEditor(rule: draft, isNew: true) { inspection.mockRules.append($0) }
+ MockRuleEditor(rule: draft.rule, isNew: true, draft: draft) { inspection.mockRules.append($0) }
}
.sheet(item: $responseTransformDraft) { draft in
ResponseTransformRuleEditor(rule: draft.rule, isNew: true, draft: draft) { inspection.responseTransformRules.append($0) }
@@ -120,6 +120,18 @@ private struct InspectContent: View {
!DemoData.isEnabled && exchange.note == nil && exchange.mockRule == nil && exchange.status != nil && !exchange.responseTruncated
}
+ private func prepareMock(from exchange: HTTPExchange) {
+ Task {
+ let bodies: (request: Data, response: Data)?
+ if let id = exchange.id {
+ bodies = try? await monitor.read { try $0.exchangeBodies(id: id) }
+ } else {
+ bodies = nil
+ }
+ mockDraft = MockRuleDraft(exchange: exchange, requestBody: bodies?.request, responseBody: bodies?.response)
+ }
+ }
+
private func prepareResponseTransform(from exchange: HTTPExchange) {
Task {
let body: Data?
@@ -294,7 +306,7 @@ private struct InspectContent: View {
}
.contextMenu(forSelectionType: HTTPExchange.ID.self) { ids in
if !DemoData.isEnabled, let id = ids.first, let e = exchanges.first(where: { $0.id == id }), e.note == nil {
- Button(L("Mock This Endpoint…")) { mockDraft = MockRule(mocking: e) }
+ Button(L("Mock This Endpoint…")) { prepareMock(from: e) }
if canModifyResponse(e) {
Button(L("Modify This Response…")) { prepareResponseTransform(from: e) }
}
@@ -316,7 +328,7 @@ private struct InspectContent: View {
@ViewBuilder private var detail: some View {
if let selected = exchanges.first(where: { $0.id == selection }) {
ExchangeDetail(exchange: selected, assessment: selected.id.flatMap { assessments[$0] }, cause: selected.id.flatMap { links[$0] },
- results: results, highlight: search, mockThis: DemoData.isEnabled ? nil : { mockDraft = MockRule(mocking: selected) },
+ results: results, highlight: search, mockThis: DemoData.isEnabled ? nil : { prepareMock(from: selected) },
modifyResponse: canModifyResponse(selected) ? { prepareResponseTransform(from: selected) } : nil)
.id(selected.id)
} else {
diff --git a/Flowlight/UI/MockRulesView.swift b/Flowlight/UI/MockRulesView.swift
index 8e6f25f2..9cb2d246 100644
--- a/Flowlight/UI/MockRulesView.swift
+++ b/Flowlight/UI/MockRulesView.swift
@@ -95,7 +95,8 @@ struct MockRulesSection: View {
}
}
-/// One rule, edited in a sheet. Everything a canned answer needs and nothing else.
+/// One rule, edited in a sheet. Captured request information is reference material only; the matcher remains host,
+/// path and method, while the response is the durable mock setting.
struct MockRuleEditor: View {
private enum Tab: Hashable { case request, response }
@@ -104,10 +105,24 @@ struct MockRuleEditor: View {
var save: (MockRule) -> Void
@Environment(\.dismiss) private var dismiss
@State private var tab: Tab = .request
- @State private var headerText = ""
+ @State private var requestHeaders: [MockHeaderRow]
+ @State private var responseHeaders: [MockHeaderRow]
+ @State private var requestBody: MockBodyDraft
+ private var responseBodyNote: String?
@State private var statusText = ""
@State private var delayText = ""
+ init(rule: MockRule, isNew: Bool, draft: MockRuleDraft? = nil, save: @escaping (MockRule) -> Void) {
+ _rule = State(initialValue: draft?.rule ?? rule)
+ self.isNew = isNew
+ self.save = save
+ _requestHeaders = State(initialValue: (draft?.requestHeaders ?? []).map(MockHeaderRow.init))
+ _responseHeaders = State(initialValue: (draft?.rule.headers ?? rule.headers).map(MockHeaderRow.init))
+ _requestBody = State(initialValue: draft?.requestBody ?? MockBodyDraft(text: "", encoding: .text, byteCount: 0,
+ unavailableReason: L("No captured request body.")))
+ responseBodyNote = draft?.responseBodyNote
+ }
+
var body: some View {
VStack(alignment: .leading, spacing: 14) {
Text(isNew ? L("New mock response") : L("Edit mock response")).font(.title3.bold())
@@ -126,11 +141,10 @@ struct MockRuleEditor: View {
.pickerStyle(.segmented)
.labelsHidden()
- if tab == .request {
- requestFields
- } else {
- responseFields
+ ScrollView {
+ if tab == .request { requestFields } else { responseFields }
}
+ .frame(maxHeight: 410)
if rule.delay > 0 {
Label(L("The request waits %@ before it's answered. Clients with their own timeout will give up first, which is usually the point.",
@@ -143,81 +157,84 @@ struct MockRuleEditor: View {
Spacer()
Button(isNew ? L("Add Rule") : L("Save")) { save(cleaned()); dismiss() }
.keyboardShortcut(.defaultAction)
- .disabled(rule.host.trimmingCharacters(in: .whitespaces).isEmpty)
+ .disabled(rule.host.trimmingCharacters(in: .whitespaces).isEmpty || !rule.hasValidBody)
}
}
.padding(20)
- .frame(width: 560)
+ .frame(width: 660)
.onAppear {
- headerText = MockRule.headerText(rule.headers)
statusText = String(rule.status)
delayText = rule.delay == 0 ? "" : String(rule.delay)
}
}
private var requestFields: some View {
- Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 10, verticalSpacing: 8) {
- GridRow {
- Text(L("Host")).gridColumnAlignment(.trailing).foregroundStyle(.secondary)
- VStack(alignment: .leading, spacing: 2) {
- TextField(L("api.example.com"), text: $rule.host)
- Text(L("Exactly that host. Write *.example.com to cover the domain and its subdomains."))
- .font(.caption).foregroundStyle(.secondary)
+ VStack(alignment: .leading, spacing: 14) {
+ Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 10, verticalSpacing: 8) {
+ GridRow {
+ Text(L("Host")).gridColumnAlignment(.trailing).foregroundStyle(.secondary)
+ VStack(alignment: .leading, spacing: 2) {
+ TextField(L("api.example.com"), text: $rule.host)
+ Text(L("Exactly that host. Write *.example.com to cover the domain and its subdomains."))
+ .font(.caption).foregroundStyle(.secondary)
+ }
}
- }
- GridRow {
- Text(L("Path")).gridColumnAlignment(.trailing).foregroundStyle(.secondary)
- VStack(alignment: .leading, spacing: 2) {
- TextField(L("/v1/*"), text: $rule.path)
- Text(L("A glob: * matches any run of characters. The query string is ignored unless the pattern contains a ?."))
- .font(.caption).foregroundStyle(.secondary)
+ GridRow {
+ Text(L("Path")).gridColumnAlignment(.trailing).foregroundStyle(.secondary)
+ VStack(alignment: .leading, spacing: 2) {
+ TextField(L("/v1/*"), text: $rule.path)
+ Text(L("A glob: * matches any run of characters. The query string is ignored unless the pattern contains a ?."))
+ .font(.caption).foregroundStyle(.secondary)
+ }
}
- }
- GridRow {
- Text(L("Method")).gridColumnAlignment(.trailing).foregroundStyle(.secondary)
- Picker("", selection: Binding(get: { rule.method.isEmpty ? "ANY" : rule.method.uppercased() },
- set: { rule.method = $0 == "ANY" ? "" : $0 })) {
- ForEach(MockRule.methods, id: \.self) { Text($0).tag($0) }
+ GridRow {
+ Text(L("Method")).gridColumnAlignment(.trailing).foregroundStyle(.secondary)
+ Picker("", selection: Binding(get: { rule.method.isEmpty ? "ANY" : rule.method.uppercased() },
+ set: { rule.method = $0 == "ANY" ? "" : $0 })) {
+ ForEach(MockRule.methods, id: \.self) { Text($0).tag($0) }
+ }
+ .labelsHidden().frame(width: 130)
}
- .labelsHidden().frame(width: 130)
}
+ Divider()
+ Label(L("Captured request details are reference only. They do not change what this mock matches and are not saved with the rule."),
+ systemImage: "info.circle")
+ .font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true)
+ MockHeaderRows(rows: $requestHeaders, emptyMessage: L("No captured request headers."))
+ BodyEditor(title: L("Request payload"), text: $requestBody.text, encoding: $requestBody.encoding,
+ unavailableReason: requestBody.unavailableReason, editable: false)
}
}
private var responseFields: some View {
- Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 10, verticalSpacing: 8) {
- GridRow {
- Text(L("Status")).gridColumnAlignment(.trailing).foregroundStyle(.secondary)
- HStack(spacing: 8) {
- TextField("500", text: $statusText).frame(width: 70)
- .onChange(of: statusText) { _, new in
- if let code = Int(new.filter(\.isNumber)), (100...599).contains(code) { rule.status = code }
- }
- Text(MockRule.reason(rule.status)).font(.caption).foregroundStyle(.secondary)
- Spacer()
- Text(L("Delay")).foregroundStyle(.secondary)
- TextField("0", text: $delayText).frame(width: 60)
- .onChange(of: delayText) { _, new in rule.delay = min(300, max(0, Double(new) ?? 0)) }
- Text(L("seconds")).font(.caption).foregroundStyle(.secondary)
- }
- }
- GridRow(alignment: .top) {
- Text(L("Headers")).gridColumnAlignment(.trailing).foregroundStyle(.secondary)
- VStack(alignment: .leading, spacing: 2) {
- TextEditor(text: $headerText)
- .font(.caption.monospaced()).frame(height: 54)
- .border(.quaternary)
- .onChange(of: headerText) { _, new in rule.headers = MockRule.parseHeaders(new) }
- Text(L("One Name: value per line. Content-Length and Connection are written by Flowlight."))
- .font(.caption).foregroundStyle(.secondary)
+ VStack(alignment: .leading, spacing: 14) {
+ Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 10, verticalSpacing: 8) {
+ GridRow {
+ Text(L("Status")).gridColumnAlignment(.trailing).foregroundStyle(.secondary)
+ HStack(spacing: 8) {
+ TextField("500", text: $statusText).frame(width: 70)
+ .onChange(of: statusText) { _, new in
+ if let code = Int(new.filter(\.isNumber)), (100...599).contains(code) { rule.status = code }
+ }
+ Text(MockRule.reason(rule.status)).font(.caption).foregroundStyle(.secondary)
+ Spacer()
+ Text(L("Delay")).foregroundStyle(.secondary)
+ TextField("0", text: $delayText).frame(width: 60)
+ .onChange(of: delayText) { _, new in rule.delay = min(300, max(0, Double(new) ?? 0)) }
+ Text(L("seconds")).font(.caption).foregroundStyle(.secondary)
+ }
}
}
- GridRow(alignment: .top) {
- Text(L("Body")).gridColumnAlignment(.trailing).foregroundStyle(.secondary)
- TextEditor(text: $rule.body)
- .font(.caption.monospaced()).frame(height: 120)
- .border(.quaternary)
+ MockHeaderRows(rows: $responseHeaders, emptyMessage: L("No response headers yet."))
+ BodyEditor(title: L("Response body"), text: $rule.body, encoding: $rule.bodyEncoding,
+ unavailableReason: responseBodyNote, editable: true)
+ if !rule.hasValidBody {
+ Label(L("Binary mock bodies must contain complete hexadecimal byte pairs."), systemImage: "exclamationmark.triangle")
+ .font(.caption).foregroundStyle(FL.warning)
}
+ Label(L("Saving keeps this response as a mock rule until you remove it, even if captured traffic is later cleared."),
+ systemImage: "archivebox")
+ .font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true)
}
}
@@ -229,11 +246,100 @@ struct MockRuleEditor: View {
copy.path = copy.path.trimmingCharacters(in: .whitespaces)
if copy.path.isEmpty { copy.path = "*" }
copy.method = copy.method.trimmingCharacters(in: .whitespaces).uppercased()
- copy.headers = MockRule.parseHeaders(headerText)
+ copy.headers = responseHeaders.compactMap { row in
+ let name = MockRule.headerSafe(row.name)
+ guard !name.isEmpty else { return nil }
+ return HTTPHeader(name: name, value: MockRule.headerSafe(row.value))
+ }
return copy
}
}
+private struct MockHeaderRow: Identifiable, Equatable {
+ var id = UUID()
+ var name = ""
+ var value = ""
+
+ init() {}
+ init(_ header: HTTPHeader) { name = header.name; value = header.value }
+}
+
+private struct MockHeaderRows: View {
+ @Binding var rows: [MockHeaderRow]
+ var emptyMessage: String
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 7) {
+ HStack {
+ Text(L("Headers")).font(.caption.bold()).foregroundStyle(.secondary)
+ Spacer()
+ Button { rows.append(MockHeaderRow()) } label: { Label(L("Add Header"), systemImage: "plus") }
+ .controlSize(.small)
+ }
+ if rows.isEmpty {
+ Text(emptyMessage).font(.caption).foregroundStyle(.tertiary)
+ }
+ ForEach($rows) { $row in
+ HStack(alignment: .top, spacing: 6) {
+ TextField(L("Name"), text: $row.name).frame(width: 150)
+ AdaptiveMockText(text: $row.value).frame(maxWidth: .infinity)
+ Button { rows.removeAll { $0.id == row.id } } label: { Image(systemName: "minus.circle.fill") }
+ .buttonStyle(.borderless).foregroundStyle(.secondary)
+ .accessibilityLabel(L("Remove header"))
+ }
+ }
+ Text(L("Content-Length, Connection and Transfer-Encoding are written by Flowlight."))
+ .font(.caption).foregroundStyle(.secondary)
+ }
+ }
+}
+
+private struct BodyEditor: View {
+ var title: String
+ @Binding var text: String
+ @Binding var encoding: MockBodyEncoding
+ var unavailableReason: String?
+ var editable: Bool
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 5) {
+ HStack {
+ Text(title).font(.caption.bold()).foregroundStyle(.secondary)
+ if encoding == .hex { Label(L("Binary · hexadecimal"), systemImage: "doc.text") .font(.caption).foregroundStyle(.secondary) }
+ }
+ if let unavailableReason {
+ Label(unavailableReason, systemImage: "info.circle")
+ .font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true)
+ } else {
+ AdaptiveMockText(text: $text, forceMultiline: encoding == .hex)
+ .disabled(!editable)
+ if encoding == .hex {
+ Text(L("Each pair of hexadecimal characters is one byte. Flowlight writes the original bytes."))
+ .font(.caption).foregroundStyle(.secondary)
+ }
+ }
+ }
+ }
+}
+
+private struct AdaptiveMockText: View {
+ @Binding var text: String
+ var forceMultiline = false
+ private var multiline: Bool { forceMultiline || text.count > 80 || text.contains(where: \.isNewline) }
+
+ var body: some View {
+ Group {
+ if multiline {
+ TextEditor(text: $text)
+ .font(.caption.monospaced()).frame(minHeight: forceMultiline ? 100 : 54, maxHeight: forceMultiline ? 180 : 100)
+ .border(.quaternary)
+ } else {
+ TextField("", text: $text).font(.caption.monospaced())
+ }
+ }
+ }
+}
+
enum DelayFormat {
/// "8 s", "0.5 s" — short enough to sit inside a sentence.
static func seconds(_ value: Double) -> String {
diff --git a/FlowlightTests/MockResponseTests.swift b/FlowlightTests/MockResponseTests.swift
index 80a2953a..38ec4e72 100644
--- a/FlowlightTests/MockResponseTests.swift
+++ b/FlowlightTests/MockResponseTests.swift
@@ -87,10 +87,45 @@ final class MockRuleMatchingTests: XCTestCase {
XCTAssertTrue(rule.enabled)
XCTAssertEqual(rule.path, "*")
XCTAssertEqual(rule.status, 500)
+ XCTAssertEqual(rule.bodyEncoding, .text, "old rules remain UTF-8 text")
XCTAssertEqual(rule.delay, 0)
let roundTrip = try JSONDecoder().decode([MockRule].self, from: JSONEncoder().encode([rule]))
XCTAssertEqual(roundTrip, [rule])
}
+
+ func testDraftUsesStoredRedactedContextAndRepresentsBinaryAsHex() {
+ let exchange = HTTPExchange(
+ id: 1, started: Date(), duration: 0, scheme: "https", host: "api.example.com", port: 443, method: "POST",
+ path: "/v1/items?token=ignored", status: 201,
+ requestHeaders: [HTTPHeader(name: "Authorization", value: "Bearer ••• not kept (12 characters)"),
+ HTTPHeader(name: "Accept", value: "application/json")], requestBody: Data(), requestSize: 2,
+ requestTruncated: false, responseHeaders: [HTTPHeader(name: "Content-Type", value: "application/octet-stream")],
+ responseBody: Data(), responseSize: 3, responseTruncated: false, contentType: "", pid: 1, bundleID: "b", appName: "a",
+ agent: nil, agentName: nil, mcpServer: nil, toolCalls: []
+ )
+ let draft = MockRuleDraft(exchange: exchange, requestBody: Data(#"{"ok":true}"#.utf8), responseBody: Data([0, 0xFE, 0xFF]))
+ XCTAssertEqual(draft.rule.status, 201)
+ XCTAssertEqual(draft.rule.path, "/v1/items")
+ XCTAssertEqual(draft.requestHeaders, exchange.requestHeaders)
+ XCTAssertEqual(draft.requestBody.text, #"{"ok":true}"#)
+ XCTAssertEqual(draft.rule.bodyEncoding, .hex)
+ XCTAssertEqual(draft.rule.body, "00FEFF")
+ XCTAssertEqual(draft.rule.bodyData, Data([0, 0xFE, 0xFF]))
+ }
+
+ func testTruncatedCapturedBodiesAreNotUsedAsMockResponses() {
+ var exchange = HTTPExchange(
+ id: 1, started: Date(), duration: 0, scheme: "https", host: "api.example.com", port: 443, method: "POST",
+ path: "/v1/items", status: 200, requestHeaders: [], requestBody: Data(), requestSize: 0, requestTruncated: true,
+ responseHeaders: [], responseBody: Data(), responseSize: 2, responseTruncated: true, contentType: "", pid: 1,
+ bundleID: "b", appName: "a", agent: nil, agentName: nil, mcpServer: nil, toolCalls: []
+ )
+ let draft = MockRuleDraft(exchange: exchange, requestBody: Data("partial".utf8), responseBody: Data("no".utf8))
+ XCTAssertNotNil(draft.requestBody.unavailableReason)
+ XCTAssertNotNil(draft.responseBodyNote)
+ XCTAssertEqual(draft.rule.body, #"{"error": "mocked by Flowlight"}"#)
+ exchange.responseTruncated = false
+ }
}
final class MockResponseBytesTests: XCTestCase {
@@ -132,6 +167,18 @@ final class MockResponseBytesTests: XCTestCase {
XCTAssertEqual(headers, [HTTPHeader(name: "Retry-After", value: "30"), HTTPHeader(name: "X-Empty", value: "")])
XCTAssertEqual(MockRule.headerText(headers), "Retry-After: 30\nX-Empty: ")
}
+
+ func testHexBodyRoundTripsAndWritesBinaryLength() {
+ let rule = MockRule(host: "a.test", status: 200, body: "00 FE\nFF", bodyEncoding: .hex)
+ XCTAssertEqual(rule.bodyData, Data([0, 0xFE, 0xFF]))
+ let bytes = rule.responseBytes()
+ XCTAssertTrue(bytes.starts(with: Data("HTTP/1.1 200 OK\r\n".utf8)))
+ XCTAssertTrue(String(decoding: bytes, as: UTF8.self).contains("Content-Type: application/octet-stream\r\n"))
+ XCTAssertTrue(String(decoding: bytes, as: UTF8.self).contains("Content-Length: 3\r\n"))
+ XCTAssertEqual(bytes.suffix(3), Data([0, 0xFE, 0xFF]))
+ XCTAssertNil(MockRule.bodyData("0FG", encoding: .hex))
+ XCTAssertNil(MockRule.bodyData("0", encoding: .hex))
+ }
}
final class MockGateTests: XCTestCase {
diff --git a/FlowlightTests/UpdateTests.swift b/FlowlightTests/UpdateTests.swift
index befb4300..59f75d74 100644
--- a/FlowlightTests/UpdateTests.swift
+++ b/FlowlightTests/UpdateTests.swift
@@ -1,7 +1,58 @@
+import Foundation
import XCTest
@testable import Flowlight
final class UpdateTests: XCTestCase {
+ private var skippedVersion: Any?
+ private var notifiedVersion: Any?
+ private var lastCheck: Any?
+
+ override func setUp() {
+ super.setUp()
+ let defaults = UserDefaults.standard
+ skippedVersion = defaults.object(forKey: UpdateChecker.Keys.skipped)
+ notifiedVersion = defaults.object(forKey: UpdateChecker.Keys.notified)
+ lastCheck = defaults.object(forKey: UpdateChecker.Keys.lastCheck)
+ defaults.removeObject(forKey: UpdateChecker.Keys.skipped)
+ defaults.removeObject(forKey: UpdateChecker.Keys.notified)
+ defaults.removeObject(forKey: UpdateChecker.Keys.lastCheck)
+ }
+
+ override func tearDown() {
+ let defaults = UserDefaults.standard
+ restore(skippedVersion, forKey: UpdateChecker.Keys.skipped, defaults: defaults)
+ restore(notifiedVersion, forKey: UpdateChecker.Keys.notified, defaults: defaults)
+ restore(lastCheck, forKey: UpdateChecker.Keys.lastCheck, defaults: defaults)
+ super.tearDown()
+ }
+
+ private func restore(_ value: Any?, forKey key: String, defaults: UserDefaults) {
+ defaults.removeObject(forKey: key)
+ if let value { defaults.set(value, forKey: key) }
+ }
+
+ @MainActor
+ private func fixtureChecker(version: String) -> (checker: UpdateChecker, version: String) {
+ let json = """
+ {"tag_name":"v\(version)","name":"Flowlight \(version)","body":"Fixture release","html_url":"https://example.invalid/releases/v\(version)","draft":false,"prerelease":false,"assets":[]}
+ """
+ let data = Data(json.utf8)
+ let response = HTTPURLResponse(url: URL(string: "https://api.github.com/repos/xinbetween/flowlight/releases/latest")!,
+ statusCode: 200, httpVersion: nil, headerFields: nil)!
+ return (UpdateChecker(releaseTransport: { _ in (data, response) }), version)
+ }
+
+ @MainActor
+ private func currentVersion() -> String {
+ UpdateChecker().currentVersion
+ }
+
+ @MainActor
+ private func nextMajorVersion() -> String {
+ let major = Int(currentVersion().split(separator: ".").first ?? "0") ?? 0
+ return "\(major + 1).0.0"
+ }
+
func testVersionComparison() {
XCTAssertTrue(VersionCompare.isNewer("0.1.2", than: "0.1.1"))
XCTAssertTrue(VersionCompare.isNewer("0.10.0", than: "0.9.9"), "numeric, not lexical")
@@ -22,6 +73,72 @@ final class UpdateTests: XCTestCase {
XCTAssertEqual(VersionCompare.checksum(for: "Flowlight.dmg", in: "ABCD *Flowlight.dmg"), "abcd", "binary-mode marker")
}
+ func testCleanRelaunchPolicyConsumesItsOneShotMarker() {
+ let suite = "update-policy-\(UUID().uuidString)"
+ let defaults = UserDefaults(suiteName: suite)!
+ defer { defaults.removePersistentDomain(forName: suite) }
+ XCTAssertFalse(UpdateLaunchPolicy.consumeCleanRelaunch(defaults: defaults))
+ UpdateLaunchPolicy.markCleanExit(defaults: defaults)
+ XCTAssertTrue(UpdateLaunchPolicy.consumeCleanRelaunch(defaults: defaults))
+ XCTAssertFalse(UpdateLaunchPolicy.consumeCleanRelaunch(defaults: defaults), "a clean exit promotes only one next launch")
+ }
+
+ func testCleanRelaunchBypassesTheNormalLaunchThrottle() {
+ let now = Date()
+ XCTAssertFalse(UpdateLaunchPolicy.shouldCheckAtLaunch(cleanRelaunch: false, lastCheck: now, now: now))
+ XCTAssertTrue(UpdateLaunchPolicy.shouldCheckAtLaunch(cleanRelaunch: true, lastCheck: now, now: now))
+ XCTAssertTrue(UpdateLaunchPolicy.shouldCheckAtLaunch(cleanRelaunch: false, lastCheck: now.addingTimeInterval(-12 * 3600), now: now))
+ }
+
+ @MainActor
+ func testCleanRelaunchPromotionOpensWindowForNewerRelease() async {
+ let release = await fixtureChecker(version: nextMajorVersion())
+
+ await release.checker.check(userInitiated: false, promoteIfAvailable: true)
+
+ guard case .available(let available) = release.checker.state else {
+ return XCTFail("newer release should be available")
+ }
+ XCTAssertEqual(available.version, release.version)
+ XCTAssertTrue(release.checker.showWindow)
+ }
+
+ @MainActor
+ func testCleanRelaunchPromotionSkipsSkippedRelease() async {
+ let release = await fixtureChecker(version: nextMajorVersion())
+ UserDefaults.standard.set(release.version, forKey: UpdateChecker.Keys.skipped)
+
+ await release.checker.check(userInitiated: false, promoteIfAvailable: true)
+
+ guard case .available = release.checker.state else {
+ return XCTFail("skipped newer release should still be available")
+ }
+ XCTAssertNil(release.checker.pendingUpdate)
+ XCTAssertFalse(release.checker.showWindow)
+ }
+
+ @MainActor
+ func testCleanRelaunchPromotionDoesNotOpenForCurrentRelease() async {
+ let checker = await fixtureChecker(version: currentVersion())
+
+ await checker.checker.check(userInitiated: false, promoteIfAvailable: true)
+
+ XCTAssertEqual(checker.checker.state, .upToDate)
+ XCTAssertFalse(checker.checker.showWindow)
+ }
+
+ @MainActor
+ func testCleanRelaunchPromotionDoesNotOpenWhenReleaseLookupFails() async {
+ let checker = UpdateChecker { _ in throw URLError(.notConnectedToInternet) }
+
+ await checker.check(userInitiated: false, promoteIfAvailable: true)
+
+ guard case .failed = checker.state else {
+ return XCTFail("a failed release lookup should report failure")
+ }
+ XCTAssertFalse(checker.showWindow)
+ }
+
func testReleaseNotesAreReadable() {
let notes = "**Early preview.** Hi\n\n### What's new\n- One\n- Two\n\n### Checksums (SHA-256)\n```\nabc Flowlight.dmg\n```"
let text = ReleaseNotes.readable(notes)
diff --git a/README.md b/README.md
index 8ab101e6..f23fa7e3 100644
--- a/README.md
+++ b/README.md
@@ -110,9 +110,12 @@ on your Mac. For supported HTTP traffic routed through the proxy, it records hea
### Mock responses
Answer a chosen host, path and method with your own status, headers, body and delay, to see how an agent copes when
-an API fails or returns something odd. The request is never sent. Mocked exchanges are recorded and clearly marked,
-so a session read later still shows what was real. Needs HTTPS inspection, since only decrypted traffic can be
-answered — and a mock changes the reply, not whether the connection can be made.
+an API fails or returns something odd. **Mock This…** pre-fills retained, redacted headers and payloads: request data
+is reference-only, while the response can be edited as text/JSON or lossless hexadecimal for binary bytes. Saving
+persists only the matcher and authored response rule; it remains until removed even if captured traffic is cleared.
+The request is never sent. Mocked exchanges are recorded and clearly marked, so a session read later still shows what
+was real. Needs HTTPS inspection, since only decrypted traffic can be answered — and a mock changes the reply, not
+whether the connection can be made.
### Reports at any zoom
- Second, minute, hour, day, week, month and year views. Click a bar to zoom in.
@@ -308,8 +311,10 @@ same local database.
Releases are signed and notarized, so it opens straight away.
Prefer an installer? Every [release](https://github.com/xinbetween/flowlight/releases/latest) also has a `.pkg`.
2. Launch Flowlight. Traffic appears within a second, and the ↓↑ rates live in your menu bar.
- Flowlight checks for new releases daily. It downloads and verifies an update, then asks before it quits to install
- it and reopen (Flowlight › Check for Updates…). Updating by hand? Quit Flowlight before dragging the new version in.
+ After a clean quit and relaunch, Flowlight checks GitHub for a new release and opens its update window when one is
+ available; it also checks daily. The check is optional in Settings, and any download is verified before Flowlight
+ asks to quit and install it (Flowlight › Check for Updates…). Updating by hand? Quit Flowlight before dragging the
+ new version in.
3. On first launch, **Name Your Traffic** offers the one-time setup that lets Flowlight read hostnames (it asks
for your password once). Network-owner lookups are on by default and can be turned off there or in Capture.
diff --git a/docs/404.html b/docs/404.html
index 3e1bef60..0e233050 100644
--- a/docs/404.html
+++ b/docs/404.html
@@ -13,17 +13,22 @@
+
+
+
+
+
-
+
Skip to content
@@ -80,7 +85,7 @@ That page isn't here Try the home page ,
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.13.7 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.13.8 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index 1d42e16e..2100844c 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -181,6 +181,10 @@ agent can be shown an API that fails, stalls or replies with something odd. Rule
server had sent it, so pairing, parsing and storage are unchanged. Flowlight writes `Content-Length`,
`Connection: close` and `X-Flowlight-Mock` itself — a mock whose framing disagreed with its body would hang the
client rather than test it — and strips CR/LF from rule-supplied headers so a rule can't forge a second response.
+- **Draft versus rule data.** **Mock This…** reads only retained body blobs and already-redacted stored headers into a
+ temporary editor draft. Request headers and payload are reference-only and never become match criteria or durable
+ preferences. The saved response is deliberate rule data: text is encoded as UTF-8, binary bytes as validated hex,
+ and the rule remains in `inspection.mockRules` until removed even after captured traffic is cleared.
- **A mocked exchange says so**: the rule's name goes in `HTTPExchange.mockRule` (column `mock_rule`, an in-place
`ALTER`), not in `note`, which means "nothing was inspected". Inspect badges the row and leads the detail pane
with the fact that the request was never sent, and the active rule count sits in the status bar — a mock left on
diff --git a/docs/about/index.html b/docs/about/index.html
index 7fda3e67..ade50096 100644
--- a/docs/about/index.html
+++ b/docs/about/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Skip to content
@@ -138,7 +152,7 @@ Thanks
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.13.7 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.13.8 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/de/about/index.html b/docs/de/about/index.html
index 00423f97..75760268 100644
--- a/docs/de/about/index.html
+++ b/docs/de/about/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Zum Inhalt springen
@@ -137,7 +151,7 @@ Dank
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.13.7 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.13.8 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/de/docs/index.html b/docs/de/docs/index.html
index 4957f7cb..cc9e2ce8 100644
--- a/docs/de/docs/index.html
+++ b/docs/de/docs/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Zum Inhalt springen
@@ -61,7 +75,7 @@
Dokumentation
Flowlight benutzen
-
Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.13.7, macOS 15 oder neuer.
+
Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.13.8, macOS 15 oder neuer.
@@ -738,7 +752,7 @@ Grenzen
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.13.7 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.13.8 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/de/index.html b/docs/de/index.html
index a9f96672..2b65dacc 100644
--- a/docs/de/index.html
+++ b/docs/de/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Zum Inhalt springen
@@ -75,7 +89,7 @@ Sieh, was deine Apps im Netz tun.brew install --cask xinbetween/tap/flowlightKopieren
- v0.13.7 macOS 15+ Universal GPL-3.0 Keine Telemetrie
+ v0.13.8 macOS 15+ Universal GPL-3.0 Keine Telemetrie
@@ -525,66 +539,6 @@ Wisse, was deinen Mac verlässt.
-
diff --git a/docs/de/privacy/index.html b/docs/de/privacy/index.html
index 2e39a522..8b517d68 100644
--- a/docs/de/privacy/index.html
+++ b/docs/de/privacy/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Zum Inhalt springen
@@ -173,7 +187,7 @@
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.13.7 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.13.8 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/de/threat-model/index.html b/docs/de/threat-model/index.html
index 44dd57a3..ce583894 100644
--- a/docs/de/threat-model/index.html
+++ b/docs/de/threat-model/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Zum Inhalt springen
@@ -213,7 +227,7 @@ Eine Schwachstelle melden
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.13.7 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.13.8 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/docs/index.html b/docs/docs/index.html
index 2e7affc2..ef2849d7 100644
--- a/docs/docs/index.html
+++ b/docs/docs/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Skip to content
@@ -61,7 +75,7 @@
Documentation
Using Flowlight
-
Everything from the first launch to tuning the agent rules. Flowlight 0.13.7, macOS 15 or later.
+
Everything from the first launch to tuning the agent rules. Flowlight 0.13.8, macOS 15 or later.
@@ -382,6 +396,10 @@ Mock responses
Method matches one verb, or any.
Rules are ordered and the first enabled match answers. Each switches on and off on its own.
+ Mock This… starts from the headers and bodies that Flowlight has already retained. Captured request details are
+ reference only: they do not add matching conditions and are not saved with the rule. Text and JSON can be edited directly;
+ binary bodies are shown as hexadecimal bytes. Saving keeps the response as a mock rule until you remove it, even if you later
+ clear captured traffic.
A mocked exchange is recorded like any other and marked Mocked in the list, with the detail pane saying it was
answered by Flowlight rather than by the host, and naming the rule. You can always tell later what was real.
Two things worth knowing. Only traffic Flowlight decrypts can be mocked, so the endpoint has to be going through HTTPS
@@ -623,9 +641,10 @@
Your data
Capture › Clear all data deletes it.
Updates
- When a new version appears, Flowlight tells you with a notification and a badge in the sidebar rather than interrupting what you're
- doing; clicking either opens the update window. A check you start yourself opens it straight away.
- Flowlight checks GitHub for a new release on launch and once a day, and Check for Updates… is in the app
+
After you quit Flowlight cleanly and launch it again, a newly found version opens the update window. At other times,
+ Flowlight uses a notification and sidebar badge rather than interrupting what you're doing; clicking either opens the same
+ window. A check you start yourself opens it straight away.
+ Flowlight checks GitHub after a clean relaunch and once a day, and Check for Updates… is in the app
and menu bar menus. Download fetches the disk image to Downloads and checks it against the release's published SHA-256. Nothing is installed
until you choose: Quit & Install closes Flowlight, replaces it with the new version and reopens it, keeping your history and
settings. Install Later keeps you working. If Flowlight was installed with the .pkg, macOS asks for an administrator
@@ -739,7 +758,7 @@
Limitations
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.13.7 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.13.8 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/es/about/index.html b/docs/es/about/index.html
index 8195547a..63139031 100644
--- a/docs/es/about/index.html
+++ b/docs/es/about/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Ir al contenido
@@ -137,7 +151,7 @@ Agradecimientos
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.13.7 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.13.8 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/es/docs/index.html b/docs/es/docs/index.html
index 2a337947..f48981b5 100644
--- a/docs/es/docs/index.html
+++ b/docs/es/docs/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Ir al contenido
@@ -61,7 +75,7 @@
Documentación
Usar Flowlight
-
Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.13.7, macOS 15 o posterior.
+
Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.13.8, macOS 15 o posterior.
@@ -732,7 +746,7 @@ Limitaciones
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.13.7 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.13.8 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/es/index.html b/docs/es/index.html
index 3259cb99..d393c11a 100644
--- a/docs/es/index.html
+++ b/docs/es/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Ir al contenido
@@ -75,7 +89,7 @@ Observa la actividad de red de tus apps.
brew install --cask xinbetween/tap/flowlightCopiar
- v0.13.7 macOS 15+ Universal GPL-3.0 Sin telemetría
+ v0.13.8 macOS 15+ Universal GPL-3.0 Sin telemetría
@@ -525,66 +539,6 @@ Ten claro qué sale de tu Mac.
-
diff --git a/docs/es/privacy/index.html b/docs/es/privacy/index.html
index cc9d9ea5..4bac13d9 100644
--- a/docs/es/privacy/index.html
+++ b/docs/es/privacy/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Ir al contenido
@@ -173,7 +187,7 @@
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.13.7 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.13.8 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/es/threat-model/index.html b/docs/es/threat-model/index.html
index 87ba3513..e7eb23b4 100644
--- a/docs/es/threat-model/index.html
+++ b/docs/es/threat-model/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Ir al contenido
@@ -208,7 +222,7 @@ Informar de una vulnerabilidad
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.13.7 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.13.8 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/fr/about/index.html b/docs/fr/about/index.html
index bfa0e093..7c618fed 100644
--- a/docs/fr/about/index.html
+++ b/docs/fr/about/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Aller au contenu
@@ -137,7 +151,7 @@ Remerciements
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.13.7 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.13.8 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/fr/docs/index.html b/docs/fr/docs/index.html
index d5e0d0d0..9038ec9b 100644
--- a/docs/fr/docs/index.html
+++ b/docs/fr/docs/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Aller au contenu
@@ -61,7 +75,7 @@
Documentation
Utiliser Flowlight
-
Tout, du premier lancement au réglage des règles des agents. Flowlight 0.13.7, macOS 15 ou version ultérieure.
+
Tout, du premier lancement au réglage des règles des agents. Flowlight 0.13.8, macOS 15 ou version ultérieure.
@@ -738,7 +752,7 @@ Limites
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.13.7 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.13.8 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/fr/index.html b/docs/fr/index.html
index 66427d67..5f638136 100644
--- a/docs/fr/index.html
+++ b/docs/fr/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Aller au contenu
@@ -75,7 +89,7 @@ Voyez l’activité réseau de vos apps.
brew install --cask xinbetween/tap/flowlightCopier
- v0.13.7 macOS 15+ Universel GPL-3.0 Sans télémétrie
+ v0.13.8 macOS 15+ Universel GPL-3.0 Sans télémétrie
@@ -525,66 +539,6 @@ Sachez ce qui quitte votre Mac.
-
diff --git a/docs/fr/privacy/index.html b/docs/fr/privacy/index.html
index 36d13ed0..880e71b9 100644
--- a/docs/fr/privacy/index.html
+++ b/docs/fr/privacy/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Aller au contenu
@@ -173,7 +187,7 @@
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.13.7 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.13.8 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/fr/threat-model/index.html b/docs/fr/threat-model/index.html
index b447a865..7edc025f 100644
--- a/docs/fr/threat-model/index.html
+++ b/docs/fr/threat-model/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Aller au contenu
@@ -211,7 +225,7 @@ Signaler une vulnérabilité
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.13.7 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.13.8 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/index.html b/docs/index.html
index f701fdce..bfb0ce6c 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Skip to content
@@ -75,7 +89,7 @@ See your apps' network activity.brew install --cask xinbetween/tap/flowlightCopy
- v0.13.7 macOS 15+ Universal GPL-3.0 No telemetry
+ v0.13.8 macOS 15+ Universal GPL-3.0 No telemetry
@@ -533,66 +547,6 @@ Know what leaves your Mac.
-
diff --git a/docs/it/docs/index.html b/docs/it/docs/index.html
index 74709706..70fa40ac 100644
--- a/docs/it/docs/index.html
+++ b/docs/it/docs/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Vai al contenuto
@@ -61,7 +75,7 @@
Documentazione
Usare Flowlight
-
Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.13.7, macOS 15 o successivo.
+
Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.13.8, macOS 15 o successivo.
@@ -736,7 +750,7 @@ Limitazioni
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.13.7 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.13.8 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/it/index.html b/docs/it/index.html
index 892ca771..f586401d 100644
--- a/docs/it/index.html
+++ b/docs/it/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Vai al contenuto
@@ -75,7 +89,7 @@ Vedi la rete delle tue app.Ca
Metti una stella su GitHub
brew install --cask xinbetween/tap/flowlightCopia
- v0.13.7 macOS 15+ Universale GPL-3.0 Nessuna telemetria
+ v0.13.8 macOS 15+ Universale GPL-3.0 Nessuna telemetria
@@ -525,66 +539,6 @@ Sappi che cosa esce dal tuo Mac.
-
diff --git a/docs/it/privacy/index.html b/docs/it/privacy/index.html
index bf5e4df8..f9fda606 100644
--- a/docs/it/privacy/index.html
+++ b/docs/it/privacy/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Vai al contenuto
@@ -173,7 +187,7 @@
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.13.7 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.13.8 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/it/threat-model/index.html b/docs/it/threat-model/index.html
index 184c0129..34fe6e9c 100644
--- a/docs/it/threat-model/index.html
+++ b/docs/it/threat-model/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Vai al contenuto
@@ -209,7 +223,7 @@ Segnalare una vulnerabilità
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.13.7 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.13.8 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/ja/about/index.html b/docs/ja/about/index.html
index a50f4119..43a22d26 100644
--- a/docs/ja/about/index.html
+++ b/docs/ja/about/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
本文へスキップ
@@ -138,7 +152,7 @@ 謝辞
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.13.7 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.13.8 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/docs/index.html b/docs/ja/docs/index.html
index 50693943..20ee938c 100644
--- a/docs/ja/docs/index.html
+++ b/docs/ja/docs/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
本文へスキップ
@@ -61,7 +75,7 @@
ドキュメント
Flowlight の使い方
-
初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.13.7、macOS 15 以降。
+
初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.13.8、macOS 15 以降。
@@ -686,7 +700,7 @@ 制限事項
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.13.7 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.13.8 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/index.html b/docs/ja/index.html
index 4cb93a38..37741384 100644
--- a/docs/ja/index.html
+++ b/docs/ja/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
本文へスキップ
@@ -75,7 +89,7 @@ アプリの通信が見える。brew install --cask xinbetween/tap/flowlightコピー
- v0.13.7 macOS 15+ ユニバーサル GPL-3.0 テレメトリなし
+ v0.13.8 macOS 15+ ユニバーサル GPL-3.0 テレメトリなし
@@ -524,66 +538,6 @@ Mac から何が出ていくのかを知る。
-
diff --git a/docs/ja/privacy/index.html b/docs/ja/privacy/index.html
index a2b8612d..9d86a958 100644
--- a/docs/ja/privacy/index.html
+++ b/docs/ja/privacy/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
本文へスキップ
@@ -171,7 +185,7 @@
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.13.7 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.13.8 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/threat-model/index.html b/docs/ja/threat-model/index.html
index dd64e164..d6e9b8ff 100644
--- a/docs/ja/threat-model/index.html
+++ b/docs/ja/threat-model/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
本文へスキップ
@@ -210,7 +224,7 @@ 脆弱性を報告する
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.13.7 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.13.8 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ko/about/index.html b/docs/ko/about/index.html
index 55443380..618450de 100644
--- a/docs/ko/about/index.html
+++ b/docs/ko/about/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
본문으로 이동
@@ -138,7 +152,7 @@ 감사
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.13.7 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.13.8 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/docs/index.html b/docs/ko/docs/index.html
index d3cffe2b..64e9f0d3 100644
--- a/docs/ko/docs/index.html
+++ b/docs/ko/docs/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
본문으로 이동
@@ -61,7 +75,7 @@
문서
Flowlight 사용하기
-
첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.13.7, macOS 15 이상.
+
첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.13.8, macOS 15 이상.
@@ -728,7 +742,7 @@ 한계
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.13.7 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.13.8 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/index.html b/docs/ko/index.html
index ec3fa804..c8533a55 100644
--- a/docs/ko/index.html
+++ b/docs/ko/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
본문으로 이동
@@ -75,7 +89,7 @@ 앱이 무엇을 하는지 봅니다.
brew install --cask xinbetween/tap/flowlight복사
- v0.13.7 macOS 15+ 유니버설 GPL-3.0 텔레메트리 없음
+ v0.13.8 macOS 15+ 유니버설 GPL-3.0 텔레메트리 없음
@@ -525,66 +539,6 @@ 내 Mac에서 무엇이 나가는지 아세요.
-
diff --git a/docs/ko/privacy/index.html b/docs/ko/privacy/index.html
index 1e5e69ee..c938d219 100644
--- a/docs/ko/privacy/index.html
+++ b/docs/ko/privacy/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
본문으로 이동
@@ -174,7 +188,7 @@
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.13.7 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.13.8 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/threat-model/index.html b/docs/ko/threat-model/index.html
index 3b5a3564..e2116e5a 100644
--- a/docs/ko/threat-model/index.html
+++ b/docs/ko/threat-model/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
본문으로 이동
@@ -208,7 +222,7 @@ 취약점 신고하기
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.13.7 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.13.8 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/llms-full.txt b/docs/llms-full.txt
index 6399d570..79d4b82f 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.13.7, macOS 15 or later.
+ Everything from the first launch to tuning the agent rules. Flowlight 0.13.8, macOS 15 or later.
On this page
@@ -439,6 +439,11 @@ open build/Build/Products/Release/Flowlight.app
Rules are ordered and the first enabled match answers. Each switches on and off on its own.
+ Mock This… starts from the headers and bodies that Flowlight has already retained. Captured request details are
+ reference only: they do not add matching conditions and are not saved with the rule. Text and JSON can be edited directly;
+ binary bodies are shown as hexadecimal bytes. Saving keeps the response as a mock rule until you remove it, even if you later
+ clear captured traffic.
+
A mocked exchange is recorded like any other and marked Mocked in the list, with the detail pane saying it was
answered by Flowlight rather than by the host, and naming the rule. You can always tell later what was real.
@@ -740,10 +745,11 @@ open build/Build/Products/Release/Flowlight.app
Updates
- When a new version appears, Flowlight tells you with a notification and a badge in the sidebar rather than interrupting what you're
- doing; clicking either opens the update window. A check you start yourself opens it straight away.
+ After you quit Flowlight cleanly and launch it again, a newly found version opens the update window. At other times,
+ Flowlight uses a notification and sidebar badge rather than interrupting what you're doing; clicking either opens the same
+ window. A check you start yourself opens it straight away.
- Flowlight checks GitHub for a new release on launch and once a day, and Check for Updates… is in the app
+ Flowlight checks GitHub after a clean relaunch and once a day, and Check for Updates… is in the app
and menu bar menus. Download fetches the disk image to Downloads and checks it against the release's published SHA-256. Nothing is installed
until you choose: Quit & Install closes Flowlight, replaces it with the new version and reopens it, keeping your history and
settings. Install Later keeps you working. If Flowlight was installed with the .pkg, macOS asks for an administrator
@@ -847,7 +853,7 @@ Understand your AI agents.
brew install --cask xinbetween/tap/flowlightCopy
- v0.13.7macOS 15+UniversalGPL-3.0No telemetry
+ v0.13.8macOS 15+UniversalGPL-3.0No telemetry
connectionslive
@@ -1361,7 +1367,7 @@ Privacy
RequestSent toWhat it containsWhen
- Update checkapi.github.comA standard HTTPS request for the latest release; GitHub sees your IP address and the Flowlight/ user agentOn launch and daily, from 0.1.2. Off switch in Settings › General
+ Update checkapi.github.comA standard HTTPS request for the latest release; GitHub sees your IP address and the Flowlight/ user agentAfter a clean quit and relaunch, and daily. Off switch in Settings › General
Update downloadgithub.comThe disk image and its checksum fileOnly when you click Download & Open
@@ -1413,8 +1419,25 @@ Releases
Downloads, checksums and full notes for each version are on GitHub Releases.
+ 0.13.8
+October 7, 2026Latest
+
+ Faithful static mocks from inspected traffic. Mock This Endpoint now prepopulates
+ retained, redacted request and response headers as editable key/value rows. Response text and JSON are
+ ready to edit; binary payloads are shown as validated hexadecimal and replayed byte-for-byte. Captured
+ request details remain reference-only, so matching still depends only on host, path and method and a
+ matching mock never reaches the origin.
+
+ Cleaner upgrade prompts. After a clean quit and relaunch, Flowlight checks for a
+ newer non-skipped version and opens its existing Software Update window when one is available. Automatic
+ updates remain optional, background failures stay quiet, and every download is still verified before use.
+
+ More discoverable, lighter site. The website now publishes fuller social and structured
+ metadata, validates canonical and alternate-language pages more rigorously, and keeps its home-page
+ connection log static instead of running a continuous animation.
+
0.13.7
-October 6, 2026Latest
+October 6, 2026
Current-Xcode signed packaging. Flowlight now packages the manually Developer-ID-signed
app directly from its successful archive. This avoids Xcode 26’s unsupported export path for macOS archives
diff --git a/docs/llms.txt b/docs/llms.txt
index c2d9dc8e..ec3705bf 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.13.7.
+> 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.13.8.
- [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 9db96f0e..03069c2b 100644
--- a/docs/privacy/index.html
+++ b/docs/privacy/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Skip to content
@@ -114,7 +128,7 @@ HTTPS inspection
Network requests the app makes
Request Sent to What it contains When
- Update check api.github.comA standard HTTPS request for the latest release; GitHub sees your IP address and the Flowlight/<version> user agent On launch and daily, from 0.1.2. Off switch in Settings › General
+ Update check api.github.comA standard HTTPS request for the latest release; GitHub sees your IP address and the Flowlight/<version> user agent After a clean quit and relaunch, and daily. Off switch in Settings › General
Update download github.comThe disk image and its checksum file Only when you click Download & Open
Reverse DNS Your configured DNS resolver A lookup of an IP address's registered name, when no hostname was seen As needed, in the sampler capture mode, which is the default until the Network Extension is running. No off switch: without it the sampler cannot name most connections. The Network Extension names them from the traffic itself and does not do this
Network owner lookup Team Cymru, over DNS Public IP addresses with no known hostname. Private and reserved addresses never leave your Mac On by default. Off switch in Capture › Identify network owners
@@ -180,7 +194,7 @@
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.13.7 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.13.8 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/pt-PT/about/index.html b/docs/pt-PT/about/index.html
index ab369dd6..b3f5f3b5 100644
--- a/docs/pt-PT/about/index.html
+++ b/docs/pt-PT/about/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Ir para o conteúdo
@@ -137,7 +151,7 @@ Agradecimentos
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.13.7 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.13.8 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/pt-PT/docs/index.html b/docs/pt-PT/docs/index.html
index d2a307f2..35bc47bb 100644
--- a/docs/pt-PT/docs/index.html
+++ b/docs/pt-PT/docs/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Ir para o conteúdo
@@ -61,7 +75,7 @@
Documentação
Usar o Flowlight
-
Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.13.7, macOS 15 ou posterior.
+
Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.13.8, macOS 15 ou posterior.
@@ -733,7 +747,7 @@ Limitações
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.13.7 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.13.8 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/pt-PT/index.html b/docs/pt-PT/index.html
index 4eeb99bd..b7214728 100644
--- a/docs/pt-PT/index.html
+++ b/docs/pt-PT/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Ir para o conteúdo
@@ -75,7 +89,7 @@ Veja a atividade de rede das suas apps.
brew install --cask xinbetween/tap/flowlightCopiar
- v0.13.7 macOS 15+ Universal GPL-3.0 Sem telemetria
+ v0.13.8 macOS 15+ Universal GPL-3.0 Sem telemetria
@@ -525,66 +539,6 @@ Saiba o que sai do seu Mac.
-
diff --git a/docs/pt-PT/privacy/index.html b/docs/pt-PT/privacy/index.html
index 8475c275..7a0cd557 100644
--- a/docs/pt-PT/privacy/index.html
+++ b/docs/pt-PT/privacy/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Ir para o conteúdo
@@ -173,7 +187,7 @@
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.13.7 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.13.8 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/pt-PT/threat-model/index.html b/docs/pt-PT/threat-model/index.html
index 354da6cd..85992fc1 100644
--- a/docs/pt-PT/threat-model/index.html
+++ b/docs/pt-PT/threat-model/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Ir para o conteúdo
@@ -210,7 +224,7 @@ Comunicar uma vulnerabilidade
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.13.7 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.13.8 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/releases/index.html b/docs/releases/index.html
index 2ea5f3bb..cdb5c8a7 100644
--- a/docs/releases/index.html
+++ b/docs/releases/index.html
@@ -13,17 +13,22 @@
+
+
+
+
+
-
+
Skip to content
@@ -55,7 +60,24 @@ What's new
- 0.13.7 October 6, 2026 Latest
+ 0.13.8 October 7, 2026 Latest
+
+ Faithful static mocks from inspected traffic. Mock This Endpoint now prepopulates
+ retained, redacted request and response headers as editable key/value rows. Response text and JSON are
+ ready to edit; binary payloads are shown as validated hexadecimal and replayed byte-for-byte. Captured
+ request details remain reference-only, so matching still depends only on host, path and method and a
+ matching mock never reaches the origin.
+ Cleaner upgrade prompts. After a clean quit and relaunch, Flowlight checks for a
+ newer non-skipped version and opens its existing Software Update window when one is available. Automatic
+ updates remain optional, background failures stay quiet, and every download is still verified before use.
+ More discoverable, lighter site. The website now publishes fuller social and structured
+ metadata, validates canonical and alternate-language pages more rigorously, and keeps its home-page
+ connection log static instead of running a continuous animation.
+
+
+
+
+
Current-Xcode signed packaging. Flowlight now packages the manually Developer-ID-signed
app directly from its successful archive. This avoids Xcode 26’s unsupported export path for macOS archives
@@ -1101,7 +1123,7 @@ What's new
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.13.7 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.13.8 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/sitemap.xml b/docs/sitemap.xml
index 6e2d61f1..35764afc 100644
--- a/docs/sitemap.xml
+++ b/docs/sitemap.xml
@@ -1,54 +1,54 @@
https://flowlight.xinbetween.com/about/ 2026-09-25
- https://flowlight.xinbetween.com/docs/ 2026-10-03
- https://flowlight.xinbetween.com/ 2026-10-01
- https://flowlight.xinbetween.com/privacy/ 2026-10-03
- https://flowlight.xinbetween.com/releases/ 2026-10-06
+ https://flowlight.xinbetween.com/docs/ 2026-10-07
+ https://flowlight.xinbetween.com/ 2026-10-07
+ https://flowlight.xinbetween.com/privacy/ 2026-10-07
+ https://flowlight.xinbetween.com/releases/ 2026-10-07
https://flowlight.xinbetween.com/threat-model/ 2026-09-27
https://flowlight.xinbetween.com/de/about/ 2026-09-26
https://flowlight.xinbetween.com/de/docs/ 2026-09-28
- https://flowlight.xinbetween.com/de/ 2026-10-01
+ https://flowlight.xinbetween.com/de/ 2026-10-07
https://flowlight.xinbetween.com/de/privacy/ 2026-09-27
https://flowlight.xinbetween.com/de/threat-model/ 2026-09-27
https://flowlight.xinbetween.com/es/about/ 2026-09-26
https://flowlight.xinbetween.com/es/docs/ 2026-09-28
- https://flowlight.xinbetween.com/es/ 2026-10-01
+ https://flowlight.xinbetween.com/es/ 2026-10-07
https://flowlight.xinbetween.com/es/privacy/ 2026-09-27
https://flowlight.xinbetween.com/es/threat-model/ 2026-09-27
https://flowlight.xinbetween.com/fr/about/ 2026-09-26
https://flowlight.xinbetween.com/fr/docs/ 2026-09-28
- https://flowlight.xinbetween.com/fr/ 2026-10-01
+ https://flowlight.xinbetween.com/fr/ 2026-10-07
https://flowlight.xinbetween.com/fr/privacy/ 2026-09-27
https://flowlight.xinbetween.com/fr/threat-model/ 2026-09-27
https://flowlight.xinbetween.com/it/about/ 2026-09-26
https://flowlight.xinbetween.com/it/docs/ 2026-09-28
- https://flowlight.xinbetween.com/it/ 2026-10-01
+ https://flowlight.xinbetween.com/it/ 2026-10-07
https://flowlight.xinbetween.com/it/privacy/ 2026-09-27
https://flowlight.xinbetween.com/it/threat-model/ 2026-09-27
https://flowlight.xinbetween.com/pt-PT/about/ 2026-09-26
https://flowlight.xinbetween.com/pt-PT/docs/ 2026-09-28
- https://flowlight.xinbetween.com/pt-PT/ 2026-10-01
+ https://flowlight.xinbetween.com/pt-PT/ 2026-10-07
https://flowlight.xinbetween.com/pt-PT/privacy/ 2026-09-27
https://flowlight.xinbetween.com/pt-PT/threat-model/ 2026-09-27
https://flowlight.xinbetween.com/ja/about/ 2026-09-26
https://flowlight.xinbetween.com/ja/docs/ 2026-09-28
- https://flowlight.xinbetween.com/ja/ 2026-10-01
+ https://flowlight.xinbetween.com/ja/ 2026-10-07
https://flowlight.xinbetween.com/ja/privacy/ 2026-09-27
https://flowlight.xinbetween.com/ja/threat-model/ 2026-09-27
https://flowlight.xinbetween.com/zh-Hans/about/ 2026-09-26
https://flowlight.xinbetween.com/zh-Hans/docs/ 2026-09-28
- https://flowlight.xinbetween.com/zh-Hans/ 2026-10-01
+ https://flowlight.xinbetween.com/zh-Hans/ 2026-10-07
https://flowlight.xinbetween.com/zh-Hans/privacy/ 2026-09-27
https://flowlight.xinbetween.com/zh-Hans/threat-model/ 2026-09-27
https://flowlight.xinbetween.com/zh-Hant/about/ 2026-09-26
https://flowlight.xinbetween.com/zh-Hant/docs/ 2026-09-28
- https://flowlight.xinbetween.com/zh-Hant/ 2026-10-01
+ https://flowlight.xinbetween.com/zh-Hant/ 2026-10-07
https://flowlight.xinbetween.com/zh-Hant/privacy/ 2026-09-27
https://flowlight.xinbetween.com/zh-Hant/threat-model/ 2026-09-27
https://flowlight.xinbetween.com/ko/about/ 2026-09-26
https://flowlight.xinbetween.com/ko/docs/ 2026-09-28
- https://flowlight.xinbetween.com/ko/ 2026-10-01
+ https://flowlight.xinbetween.com/ko/ 2026-10-07
https://flowlight.xinbetween.com/ko/privacy/ 2026-09-27
https://flowlight.xinbetween.com/ko/threat-model/ 2026-09-27
diff --git a/docs/threat-model/index.html b/docs/threat-model/index.html
index 51e06d81..cd76a943 100644
--- a/docs/threat-model/index.html
+++ b/docs/threat-model/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Skip to content
@@ -209,7 +223,7 @@ Reporting a vulnerability
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.13.7 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.13.8 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/zh-Hans/about/index.html b/docs/zh-Hans/about/index.html
index 4d0510f6..13caf191 100644
--- a/docs/zh-Hans/about/index.html
+++ b/docs/zh-Hans/about/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
跳到正文
@@ -137,7 +151,7 @@ 致谢
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.13.7 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.13.8 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/docs/index.html b/docs/zh-Hans/docs/index.html
index b0d85e12..376f29e6 100644
--- a/docs/zh-Hans/docs/index.html
+++ b/docs/zh-Hans/docs/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
跳到正文
@@ -61,7 +75,7 @@
文档
使用 Flowlight
-
从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.13.7,macOS 15 或更高版本。
+
从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.13.8,macOS 15 或更高版本。
@@ -689,7 +703,7 @@ 限制
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.13.7 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.13.8 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/index.html b/docs/zh-Hans/index.html
index 6d33063e..28bf9028 100644
--- a/docs/zh-Hans/index.html
+++ b/docs/zh-Hans/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
跳到正文
@@ -75,7 +89,7 @@ 看清应用的网络活动。
brew install --cask xinbetween/tap/flowlight拷贝
- v0.13.7 macOS 15+ 通用架构 GPL-3.0 无遥测
+ v0.13.8 macOS 15+ 通用架构 GPL-3.0 无遥测
@@ -522,66 +536,6 @@ 知道什么离开了你的 Mac。
-
diff --git a/docs/zh-Hans/privacy/index.html b/docs/zh-Hans/privacy/index.html
index 1b89b6a3..9e0734ce 100644
--- a/docs/zh-Hans/privacy/index.html
+++ b/docs/zh-Hans/privacy/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
跳到正文
@@ -171,7 +185,7 @@
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.13.7 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.13.8 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/threat-model/index.html b/docs/zh-Hans/threat-model/index.html
index 726cd668..6437e86b 100644
--- a/docs/zh-Hans/threat-model/index.html
+++ b/docs/zh-Hans/threat-model/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
跳到正文
@@ -197,7 +211,7 @@ 报告漏洞
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.13.7 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.13.8 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hant/about/index.html b/docs/zh-Hant/about/index.html
index 00ae3f8e..37694afc 100644
--- a/docs/zh-Hant/about/index.html
+++ b/docs/zh-Hant/about/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
跳至內容
@@ -137,7 +151,7 @@ 致謝
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.13.7 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.13.8 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/docs/index.html b/docs/zh-Hant/docs/index.html
index 5ab21edf..c4dbabdb 100644
--- a/docs/zh-Hant/docs/index.html
+++ b/docs/zh-Hant/docs/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
跳至內容
@@ -61,7 +75,7 @@
說明文件
使用 Flowlight
-
從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.13.7,macOS 15 或以上版本。
+
從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.13.8,macOS 15 或以上版本。
@@ -695,7 +709,7 @@ 限制
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.13.7 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.13.8 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/index.html b/docs/zh-Hant/index.html
index ea517749..e39e05fd 100644
--- a/docs/zh-Hant/index.html
+++ b/docs/zh-Hant/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
跳至內容
@@ -75,7 +89,7 @@ 看見每個 App 的網路活動。brew install --cask xinbetween/tap/flowlight拷貝
- v0.13.7 macOS 15+ 通用架構 GPL-3.0 無遙測
+ v0.13.8 macOS 15+ 通用架構 GPL-3.0 無遙測
@@ -523,66 +537,6 @@ 知道有什麼離開了你的 Mac。
-
diff --git a/docs/zh-Hant/privacy/index.html b/docs/zh-Hant/privacy/index.html
index fd5e538f..075a1a1c 100644
--- a/docs/zh-Hant/privacy/index.html
+++ b/docs/zh-Hant/privacy/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
跳至內容
@@ -171,7 +185,7 @@
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.13.7 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.13.8 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/threat-model/index.html b/docs/zh-Hant/threat-model/index.html
index 2a81809e..4405d0fe 100644
--- a/docs/zh-Hant/threat-model/index.html
+++ b/docs/zh-Hant/threat-model/index.html
@@ -20,21 +20,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
跳至內容
@@ -198,7 +212,7 @@ 回報漏洞
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.13.7 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.13.8 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/project.yml b/project.yml
index bff9edf0..69778fa9 100644
--- a/project.yml
+++ b/project.yml
@@ -11,8 +11,8 @@ settings:
DEVELOPMENT_TEAM: ""
CODE_SIGN_STYLE: Automatic
ENABLE_HARDENED_RUNTIME: YES
- MARKETING_VERSION: "0.13.7"
- CURRENT_PROJECT_VERSION: "28"
+ MARKETING_VERSION: "0.13.8"
+ CURRENT_PROJECT_VERSION: "29"
targets:
Flowlight:
type: application
diff --git a/scripts/build_site.py b/scripts/build_site.py
index cf4803a1..11bab4ac 100755
--- a/scripts/build_site.py
+++ b/scripts/build_site.py
@@ -196,7 +196,12 @@ def structured_data(meta, body, lang="en", docs_url=f"{DOMAIN}/docs/#install"):
publisher = {"@type": "Organization", "@id": f"{DOMAIN}/#publisher", "name": "xinbetween",
"url": f"{DOMAIN}/", "logo": f"{DOMAIN}/assets/icon.png",
"sameAs": [f"https://github.com/{REPO}"]}
- graph = [site, publisher]
+ page = {"@type": "WebPage", "@id": f"{DOMAIN}{path}#webpage", "url": f"{DOMAIN}{path}",
+ "name": title, "description": desc, "inLanguage": lang, "isPartOf": {"@id": site["@id"]}}
+ if meta.get("source"):
+ page["dateModified"] = last_changed(meta["source"])
+
+ graph = [site, publisher, page]
if path == localized("/", lang):
graph.append({
@@ -241,12 +246,13 @@ def plain(markup):
return html.unescape(re.sub(r"\s+", " ", text)).strip()
def document(meta, body, root, css_href, inline_css=None, index=True, lang="en", locale="en", alt="",
- docs_url=f"{DOMAIN}/docs/#install"):
+ alternate_locales=(), docs_url=f"{DOMAIN}/docs/#install"):
title, desc, path = meta["title"], meta["description"], meta["path"]
style = f"" if inline_css else f' '
# A page that isn't a destination shouldn't be one in search results either.
robots = "" if index else ' \n'
canonical = f' \n' if index else ""
+ locale_alternates = "".join(f' \n' for value in alternate_locales)
return f"""
@@ -257,14 +263,19 @@ def document(meta, body, root, css_href, inline_css=None, index=True, lang="en",
{robots}{canonical}{alt}
-
+{locale_alternates}
+
+
+
+
+
@@ -362,9 +373,11 @@ def where(stem, lang):
full = fill(header + body + footer, ctx)
dest = OUT / path.strip("/") / "index.html" if path != "/" else OUT / "index.html"
dest.parent.mkdir(parents=True, exist_ok=True)
+ locales = [table[l].get("lang.locale", l) for l in order if l != lang and (stem, l) in sources]
dest.write_text(document(meta, full, root, f"{root}assets/site.css?v={css_version}",
lang=table[lang].get("lang.html", lang),
locale=table[lang].get("lang.locale", lang), alt=alt,
+ alternate_locales=locales,
docs_url=f"{DOMAIN}{where('docs', lang) if 'docs' in english else '/docs/'}#install"))
pages.append((meta, body, lang))
if path == "/":
diff --git a/scripts/check_site.py b/scripts/check_site.py
index 08f809e7..953aadb0 100644
--- a/scripts/check_site.py
+++ b/scripts/check_site.py
@@ -8,7 +8,7 @@
python3 scripts/check_site.py # after scripts/build_site.py
"""
-import re, sys, pathlib, collections
+import re, sys, pathlib, collections, json
ROOT = pathlib.Path(__file__).resolve().parent.parent
OUT = ROOT / "docs"
@@ -32,6 +32,11 @@
SIDEBAR = re.compile(r"enum SidebarItem[^{]*\{\s*case ([\w, ]+)")
ROW = re.compile(r"(?:]*>)?([^<]+)(?: )? \u2318(.) ")
H1 = re.compile(r"]")
+TITLE = re.compile(r"([^<]+) ", re.I)
+META = re.compile(r' (.*?)', re.S | re.I)
def version_key(text):
@@ -201,6 +206,76 @@ def entries(text):
if count != 1:
found.append(f"{page.relative_to(ROOT)}: {count} elements, expected 1")
+ # 13. SEO metadata is generated centrally. Keeping its structural contract here means a template change cannot
+ # quietly publish pages that look fine in a browser but have no usable search/social identity.
+ expected_social = {"og:type", "og:site_name", "og:locale", "og:title", "og:description", "og:url", "og:image",
+ "og:image:type", "og:image:width", "og:image:height", "og:image:alt", "twitter:card",
+ "twitter:title", "twitter:description", "twitter:image", "twitter:image:alt"}
+ indexed = [page for page in PAGES if page.name != "404.html"]
+ canonicals = {}
+ for page in PAGES:
+ text = page.read_text()
+ if not TITLE.search(text) or not TITLE.search(text).group(1).strip():
+ found.append(f"{page.relative_to(ROOT)}: missing non-empty ")
+ metadata = dict(META.findall(text))
+ if not metadata.get("description", "").strip():
+ found.append(f"{page.relative_to(ROOT)}: missing non-empty meta description")
+ is_indexed = page in indexed
+ canonical = CANONICAL.findall(text)
+ if is_indexed:
+ expected = "https://flowlight.xinbetween.com/" + ("" if page == OUT / "index.html" else str(page.relative_to(OUT).parent) + "/")
+ if canonical != [expected]:
+ found.append(f"{page.relative_to(ROOT)}: canonical must be exactly {expected}")
+ canonicals[page] = expected
+ missing = sorted(expected_social - set(metadata))
+ if missing:
+ found.append(f"{page.relative_to(ROOT)}: missing social metadata {', '.join(missing)}")
+ elif metadata["og:url"] != expected or metadata["og:title"] != TITLE.search(text).group(1) or metadata["og:description"] != metadata["description"]:
+ found.append(f"{page.relative_to(ROOT)}: Open Graph URL/title/description disagree with page metadata")
+ elif canonical:
+ found.append(f"{page.relative_to(ROOT)}: noindex page must not have a canonical URL")
+
+ for raw in JSON_LD.findall(text):
+ try:
+ graph = json.loads(raw).get("@graph", [])
+ except json.JSONDecodeError as error:
+ found.append(f"{page.relative_to(ROOT)}: invalid JSON-LD: {error.msg}")
+ continue
+ kinds = {node.get("@type") for node in graph}
+ if is_indexed and "WebPage" not in kinds:
+ found.append(f"{page.relative_to(ROOT)}: JSON-LD has no WebPage node")
+ is_home = page.name == "index.html" and (page.parent == OUT or page.parent.name in LANGS)
+ if is_home and "SoftwareApplication" not in kinds:
+ found.append(f"{page.relative_to(ROOT)}: home JSON-LD has no SoftwareApplication node")
+ if not is_home and is_indexed and "BreadcrumbList" not in kinds:
+ found.append(f"{page.relative_to(ROOT)}: page JSON-LD has no BreadcrumbList node")
+
+ if is_indexed:
+ alternates = dict(ALTERNATE.findall(text))
+ if alternates and "x-default" not in alternates:
+ found.append(f"{page.relative_to(ROOT)}: hreflang cluster has no x-default")
+ for lang, href in alternates.items():
+ if lang == "x-default":
+ continue
+ target = next((candidate for candidate, url in canonicals.items() if url == href), None)
+ if target is not None and lang not in dict(ALTERNATE.findall(target.read_text())):
+ found.append(f"{page.relative_to(ROOT)}: hreflang {lang} is not reciprocal")
+
+ # 14. The sitemap names exactly the pages allowed to be indexed, and robots advertises that sitemap.
+ sitemap = (OUT / "sitemap.xml").read_text()
+ sitemap_urls = set(re.findall(r"([^<]+) ", sitemap))
+ if sitemap_urls != set(canonicals.values()):
+ found.append("docs/sitemap.xml: URLs do not exactly match canonical indexable pages")
+ robots = (OUT / "robots.txt").read_text()
+ if "Sitemap: https://flowlight.xinbetween.com/sitemap.xml" not in robots:
+ found.append("docs/robots.txt: missing or incorrect sitemap declaration")
+
+ # 15. The home is intentionally static/lightweight. A loop here has previously caused real-machine jank.
+ for source in sorted((ROOT / "site/pages").glob("**/index.html")):
+ source_text = source.read_text()
+ if re.search(r"\b(setInterval|setTimeout|requestAnimationFrame)\s*\(", source_text):
+ found.append(f"{source.relative_to(ROOT)}: home page has recurring JavaScript animation")
+
return found
diff --git a/site/pages/de/index.html b/site/pages/de/index.html
index 49de61c1..2589a279 100644
--- a/site/pages/de/index.html
+++ b/site/pages/de/index.html
@@ -471,63 +471,3 @@ Wisse, was deinen Mac verlässt.
-
diff --git a/site/pages/docs.html b/site/pages/docs.html
index 072078d8..a7d2a7c6 100644
--- a/site/pages/docs.html
+++ b/site/pages/docs.html
@@ -329,6 +329,10 @@ Mock responses
Method matches one verb, or any.
Rules are ordered and the first enabled match answers. Each switches on and off on its own.
+ Mock This… starts from the headers and bodies that Flowlight has already retained. Captured request details are
+ reference only: they do not add matching conditions and are not saved with the rule. Text and JSON can be edited directly;
+ binary bodies are shown as hexadecimal bytes. Saving keeps the response as a mock rule until you remove it, even if you later
+ clear captured traffic.
A mocked exchange is recorded like any other and marked Mocked in the list, with the detail pane saying it was
answered by Flowlight rather than by the host, and naming the rule. You can always tell later what was real.
Two things worth knowing. Only traffic Flowlight decrypts can be mocked, so the endpoint has to be going through HTTPS
@@ -570,9 +574,10 @@
Your data
Capture › Clear all data deletes it.
Updates
- When a new version appears, Flowlight tells you with a notification and a badge in the sidebar rather than interrupting what you're
- doing; clicking either opens the update window. A check you start yourself opens it straight away.
- Flowlight checks GitHub for a new release on launch and once a day, and Check for Updates… is in the app
+
After you quit Flowlight cleanly and launch it again, a newly found version opens the update window. At other times,
+ Flowlight uses a notification and sidebar badge rather than interrupting what you're doing; clicking either opens the same
+ window. A check you start yourself opens it straight away.
+ Flowlight checks GitHub after a clean relaunch and once a day, and Check for Updates… is in the app
and menu bar menus. Download fetches the disk image to Downloads and checks it against the release's published SHA-256. Nothing is installed
until you choose: Quit & Install closes Flowlight, replaces it with the new version and reopens it, keeping your history and
settings. Install Later keeps you working. If Flowlight was installed with the .pkg, macOS asks for an administrator
diff --git a/site/pages/es/index.html b/site/pages/es/index.html
index da622d2f..d94b5d77 100644
--- a/site/pages/es/index.html
+++ b/site/pages/es/index.html
@@ -471,63 +471,3 @@
Ten claro qué sale de tu Mac.
-
diff --git a/site/pages/fr/index.html b/site/pages/fr/index.html
index 05271966..08a51af2 100644
--- a/site/pages/fr/index.html
+++ b/site/pages/fr/index.html
@@ -471,63 +471,3 @@ Sachez ce qui quitte votre Mac.
-
diff --git a/site/pages/index.html b/site/pages/index.html
index a19bd606..894c7c22 100644
--- a/site/pages/index.html
+++ b/site/pages/index.html
@@ -480,63 +480,3 @@ Know what leaves your Mac.
-
diff --git a/site/pages/it/index.html b/site/pages/it/index.html
index 4ee06877..8c5c8701 100644
--- a/site/pages/it/index.html
+++ b/site/pages/it/index.html
@@ -471,63 +471,3 @@ Sappi che cosa esce dal tuo Mac.
-
diff --git a/site/pages/ja/index.html b/site/pages/ja/index.html
index c575159c..291252b4 100644
--- a/site/pages/ja/index.html
+++ b/site/pages/ja/index.html
@@ -470,63 +470,3 @@ Mac から何が出ていくのかを知る。
-
diff --git a/site/pages/ko/index.html b/site/pages/ko/index.html
index 2d7076fe..061471f2 100644
--- a/site/pages/ko/index.html
+++ b/site/pages/ko/index.html
@@ -471,63 +471,3 @@ 내 Mac에서 무엇이 나가는지 아세요.
-
diff --git a/site/pages/privacy.html b/site/pages/privacy.html
index 2a570b1d..3dbedb11 100644
--- a/site/pages/privacy.html
+++ b/site/pages/privacy.html
@@ -61,7 +61,7 @@ HTTPS inspection
Network requests the app makes
Request Sent to What it contains When
- Update check api.github.comA standard HTTPS request for the latest release; GitHub sees your IP address and the Flowlight/<version> user agent On launch and daily, from 0.1.2. Off switch in Settings › General
+ Update check api.github.comA standard HTTPS request for the latest release; GitHub sees your IP address and the Flowlight/<version> user agent After a clean quit and relaunch, and daily. Off switch in Settings › General
Update download github.comThe disk image and its checksum file Only when you click Download & Open
Reverse DNS Your configured DNS resolver A lookup of an IP address's registered name, when no hostname was seen As needed, in the sampler capture mode, which is the default until the Network Extension is running. No off switch: without it the sampler cannot name most connections. The Network Extension names them from the traffic itself and does not do this
Network owner lookup Team Cymru, over DNS Public IP addresses with no known hostname. Private and reserved addresses never leave your Mac On by default. Off switch in Capture › Identify network owners
diff --git a/site/pages/pt-PT/index.html b/site/pages/pt-PT/index.html
index e010b622..fc4372b8 100644
--- a/site/pages/pt-PT/index.html
+++ b/site/pages/pt-PT/index.html
@@ -471,63 +471,3 @@ Saiba o que sai do seu Mac.
-
diff --git a/site/pages/releases.html b/site/pages/releases.html
index c6f985a6..87298bff 100644
--- a/site/pages/releases.html
+++ b/site/pages/releases.html
@@ -13,7 +13,24 @@ What's new
- 0.13.7 October 6, 2026 Latest
+ 0.13.8 October 7, 2026 Latest
+
+ Faithful static mocks from inspected traffic. Mock This Endpoint now prepopulates
+ retained, redacted request and response headers as editable key/value rows. Response text and JSON are
+ ready to edit; binary payloads are shown as validated hexadecimal and replayed byte-for-byte. Captured
+ request details remain reference-only, so matching still depends only on host, path and method and a
+ matching mock never reaches the origin.
+ Cleaner upgrade prompts. After a clean quit and relaunch, Flowlight checks for a
+ newer non-skipped version and opens its existing Software Update window when one is available. Automatic
+ updates remain optional, background failures stay quiet, and every download is still verified before use.
+ More discoverable, lighter site. The website now publishes fuller social and structured
+ metadata, validates canonical and alternate-language pages more rigorously, and keeps its home-page
+ connection log static instead of running a continuous animation.
+
+
+
+
+
Current-Xcode signed packaging. Flowlight now packages the manually Developer-ID-signed
app directly from its successful archive. This avoids Xcode 26’s unsupported export path for macOS archives
diff --git a/site/pages/zh-Hans/index.html b/site/pages/zh-Hans/index.html
index 1542ca86..d761ca61 100644
--- a/site/pages/zh-Hans/index.html
+++ b/site/pages/zh-Hans/index.html
@@ -468,63 +468,3 @@ 知道什么离开了你的 Mac。
-
diff --git a/site/pages/zh-Hant/index.html b/site/pages/zh-Hant/index.html
index 5410179b..6b373ea0 100644
--- a/site/pages/zh-Hant/index.html
+++ b/site/pages/zh-Hant/index.html
@@ -469,63 +469,3 @@ 知道有什麼離開了你的 Mac。
-