diff --git a/Flowlight/Analysis/InstalledApps.swift b/Flowlight/Analysis/InstalledApps.swift
index c4ed9c92..4fc96db3 100644
--- a/Flowlight/Analysis/InstalledApps.swift
+++ b/Flowlight/Analysis/InstalledApps.swift
@@ -17,8 +17,13 @@ enum InstalledApps {
var bluetoothPurpose: String?
}
+ struct ScanResult: Sendable {
+ var apps: [App]
+ var complete: Bool
+ }
+
private static let lock = NSLock()
- private static var cached: [App]?
+ private static var cached: ScanResult?
/// Where applications live. The system folder is included because Safari and Mail are perfectly reasonable
/// things to focus on, and they aren't in /Applications.
@@ -29,15 +34,22 @@ enum InstalledApps {
/// Scanned once and kept. A Mac doesn't gain applications while a popover is open, and re-reading several
/// hundred Info.plists on every keystroke would be felt.
- static func all() -> [App] {
+ static func all() -> [App] { scan().apps }
+
+ static func scan() -> ScanResult {
lock.lock()
if let cached { lock.unlock(); return cached }
lock.unlock()
var found: [String: App] = [:]
+ var complete = true
let manager = FileManager.default
for directory in searchPaths {
- guard let entries = try? manager.contentsOfDirectory(atPath: directory) else { continue }
+ guard manager.fileExists(atPath: directory) else { continue }
+ guard let entries = try? manager.contentsOfDirectory(atPath: directory) else {
+ complete = false
+ continue
+ }
for entry in entries where entry.hasSuffix(".app") {
let path = directory + "/" + entry
guard let bundle = Bundle(path: path), let id = bundle.bundleIdentifier, !id.isEmpty else { continue }
@@ -49,9 +61,14 @@ enum InstalledApps {
found[id] = App(name: name, bundleID: id, path: path, bluetoothPurpose: purpose)
}
}
- let sorted = found.values.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
- lock.lock(); cached = sorted; lock.unlock()
- return sorted
+ let result = ScanResult(
+ apps: found.values.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending },
+ complete: complete && !found.isEmpty
+ )
+ if result.complete {
+ lock.lock(); cached = result; lock.unlock()
+ }
+ return result
}
/// The applications built to use Bluetooth, as they say so themselves in their Info.plist.
diff --git a/Flowlight/Analysis/RulePackStore.swift b/Flowlight/Analysis/RulePackStore.swift
index cd95e6c0..acede341 100644
--- a/Flowlight/Analysis/RulePackStore.swift
+++ b/Flowlight/Analysis/RulePackStore.swift
@@ -31,6 +31,10 @@ final class RulePackStore: ObservableObject {
var enabledCount: Int { configurations.filter(\.enabled).count }
var hasEnabledPacks: Bool { enabledCount > 0 }
+ var failureMessage: String? {
+ if case .failed(let message) = status { return message }
+ return nil
+ }
func configuration(for packID: String) -> RulePackConfiguration {
configurations.first { $0.packID == packID } ?? RulePackConfiguration(packID: packID)
@@ -72,8 +76,11 @@ final class RulePackStore: ObservableObject {
}
}
- func save(_ configuration: RulePackConfiguration) {
- guard definitions.contains(where: { $0.id == configuration.packID }) else { return }
+ func save(_ configuration: RulePackConfiguration, completion: ((Bool) -> Void)? = nil) {
+ guard definitions.contains(where: { $0.id == configuration.packID }) else {
+ completion?(false)
+ return
+ }
var cleaned = configuration
if cleaned.enabled, let definition = definitions.first(where: { $0.id == cleaned.packID }) {
cleaned.consentedVersion = definition.manifest.version
@@ -83,15 +90,18 @@ final class RulePackStore: ObservableObject {
$0.trimmingCharacters(in: .whitespacesAndNewlines)
}.filter { !$0.isEmpty })).sorted()
if cleaned.scope == .selectedApps && cleaned.apps.isEmpty { cleaned.enabled = false }
- persist({ try $0.saveRulePackConfiguration(cleaned) }) { [weak self] in
- guard let self else { return }
+ persist({ try $0.saveRulePackConfiguration(cleaned) }, then: { [weak self] in
+ guard let self else { completion?(false); return }
if let index = self.configurations.firstIndex(where: { $0.packID == cleaned.packID }) {
self.configurations[index] = cleaned
} else {
self.configurations.append(cleaned)
}
self.publish()
- }
+ completion?(true)
+ }, onFailure: {
+ completion?(false)
+ })
}
func setEnabled(_ packID: String, _ enabled: Bool) {
@@ -171,19 +181,28 @@ final class RulePackStore: ObservableObject {
}
private func persist(_ write: @escaping (TrafficDatabase) throws -> Void,
- then commit: @escaping @MainActor () -> Void) {
+ then commit: @escaping @MainActor () -> Void,
+ onFailure: (@MainActor () -> Void)? = nil) {
guard let db else {
status = .failed("Rule pack storage is unavailable.")
+ onFailure?()
return
}
db.queue.async { [weak self, weak db] in
- guard let db else { return }
+ guard let db else {
+ Task { @MainActor in
+ self?.status = .failed("Rule pack storage is unavailable.")
+ onFailure?()
+ }
+ return
+ }
do {
try write(db)
Task { @MainActor in commit() }
} catch {
Task { @MainActor in
self?.status = .failed("Could not save the rule pack change: \(error.localizedDescription)")
+ onFailure?()
}
}
}
diff --git a/Flowlight/UI/RulePackEditorDraft.swift b/Flowlight/UI/RulePackEditorDraft.swift
new file mode 100644
index 00000000..92873225
--- /dev/null
+++ b/Flowlight/UI/RulePackEditorDraft.swift
@@ -0,0 +1,60 @@
+import Foundation
+
+/// The managed-pack editor is a draft until an explicit Save succeeds. Keeping these transitions outside SwiftUI
+/// makes keyboard actions and sheet dismissal incapable of changing enforcement by accident.
+struct RulePackEditorDraft {
+ enum Intent {
+ case review
+ case activate
+ }
+
+ enum AppSubmission: Equatable {
+ case loading
+ case ignored
+ case added(String)
+ }
+
+ enum SaveAction: Equatable {
+ case commit(RulePackConfiguration)
+ case confirmBroadening
+ }
+
+ let originalConfiguration: RulePackConfiguration
+ var configuration: RulePackConfiguration
+ var typedApp = ""
+
+ init(saved: RulePackConfiguration, intent: Intent) {
+ originalConfiguration = saved
+ configuration = saved
+ if intent == .activate { configuration.enabled = true }
+ }
+
+ var canSave: Bool {
+ configuration.scope != .selectedApps || !configuration.apps.isEmpty
+ }
+
+ mutating func submitApp(installed: [InstalledApps.App], appsLoaded: Bool = true) -> AppSubmission {
+ addApp(typedApp, installed: installed, appsLoaded: appsLoaded)
+ }
+
+ mutating func addApp(_ value: String, installed: [InstalledApps.App], appsLoaded: Bool = true) -> AppSubmission {
+ guard appsLoaded else { return .loading }
+ guard let target = InstalledApps.target(forTyped: value, apps: installed) else { return .ignored }
+ if !configuration.apps.contains(where: { $0.caseInsensitiveCompare(target.value) == .orderedSame }) {
+ configuration.apps.append(target.value)
+ configuration.apps.sort()
+ }
+ typedApp = ""
+ return .added(target.value)
+ }
+
+ func requestSave() -> SaveAction? {
+ guard canSave else { return nil }
+ if originalConfiguration.enabled,
+ originalConfiguration.scope == .selectedApps,
+ configuration.scope == .allApps {
+ return .confirmBroadening
+ }
+ return .commit(configuration)
+ }
+}
diff --git a/Flowlight/UI/RulePacksView.swift b/Flowlight/UI/RulePacksView.swift
index 08995851..26d92597 100644
--- a/Flowlight/UI/RulePacksView.swift
+++ b/Flowlight/UI/RulePacksView.swift
@@ -2,25 +2,53 @@ import SwiftUI
struct RulePacksView: View {
@EnvironmentObject var monitor: TrafficMonitor
- @State private var editor: RulePackDefinition?
+
+ var body: some View {
+ RulePacksContent(packs: monitor.rulePacks)
+ }
+}
+
+private struct RulePackEditorRequest: Identifiable {
+ let pack: RulePackDefinition
+ let intent: RulePackEditorDraft.Intent
+ var id: String { pack.id }
+}
+
+struct RulePackSaveTracker {
+ private(set) var packIDs: Set = []
+
+ func contains(_ packID: String) -> Bool { packIDs.contains(packID) }
+ mutating func begin(_ packID: String) { packIDs.insert(packID) }
+ mutating func finish(_ packID: String) { packIDs.remove(packID) }
+}
+
+private struct RulePacksContent: View {
+ @EnvironmentObject var monitor: TrafficMonitor
+ @ObservedObject var packs: RulePackStore
+ @State private var editor: RulePackEditorRequest?
+ @State private var saves = RulePackSaveTracker()
+ @State private var saveErrors: [String: String] = [:]
var body: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 10) {
explanation
- if monitor.rulePacks.deliveryStats.droppedHits > 0 {
+ if packs.deliveryStats.droppedHits > 0 {
Label(L("%lld managed-pack decisions were omitted from activity because the extension's bounded queue was full.",
- monitor.rulePacks.deliveryStats.droppedHits), systemImage: "exclamationmark.triangle")
+ packs.deliveryStats.droppedHits), systemImage: "exclamationmark.triangle")
.font(.caption).foregroundStyle(FL.warning)
.padding(10).background(FL.warning.opacity(0.1), in: RoundedRectangle(cornerRadius: 8))
}
- ForEach(monitor.rulePacks.definitions) { pack in
+ ForEach(packs.definitions) { pack in
card(pack)
}
}
.padding(Measure.gutter)
}
- .sheet(item: $editor) { pack in RulePackEditor(pack: pack) }
+ .sheet(item: $editor) { request in
+ RulePackEditor(pack: request.pack, saved: packs.configuration(for: request.pack.id),
+ intent: request.intent, packs: packs, rules: monitor.rules)
+ }
}
private var explanation: some View {
@@ -33,9 +61,11 @@ struct RulePacksView: View {
}
private func card(_ pack: RulePackDefinition) -> some View {
- let configuration = monitor.rulePacks.configuration(for: pack.id)
- let events = monitor.rulePacks.events(for: pack.id)
+ let configuration = packs.configuration(for: pack.id)
+ let events = packs.events(for: pack.id)
let blocks = events.reduce(0) { $0 + $1.count }
+ let exceptions = packs.overrides(for: pack.id)
+ let isSaving = saves.contains(pack.id)
return VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .top, spacing: 12) {
Image(systemName: symbol(pack)).font(.title3).foregroundStyle(configuration.enabled ? FL.critical : .secondary)
@@ -47,20 +77,38 @@ struct RulePacksView: View {
}
Spacer(minLength: 8)
Toggle("", isOn: Binding(get: { configuration.enabled }, set: { enabled in
- if enabled { editor = pack }
- else { var changed = configuration; changed.enabled = false; monitor.rulePacks.save(changed) }
- })).toggleStyle(.switch).controlSize(.small).labelsHidden()
+ if enabled {
+ saveErrors[pack.id] = nil
+ editor = RulePackEditorRequest(pack: pack, intent: .activate)
+ } else {
+ var changed = configuration
+ changed.enabled = false
+ saves.begin(pack.id)
+ saveErrors[pack.id] = nil
+ packs.save(changed) { success in
+ saves.finish(pack.id)
+ if !success { saveErrors[pack.id] = packs.failureMessage ?? L("Failed: %@", L("Save")) }
+ }
+ }
+ })).toggleStyle(.switch).controlSize(.small).labelsHidden().disabled(isSaving)
}
HStack(spacing: 8) {
pill(status(configuration), tint: statusTint(configuration))
pill(configuration.scope == .allApps ? L("Every app") : L("%lld selected apps", configuration.apps.count))
pill(L("%lld destinations", pack.entries.count))
if blocks > 0 { pill(L("%lld blocked", blocks), tint: FL.critical) }
- if !monitor.rulePacks.overrides(for: pack.id).isEmpty {
- pill(L("%lld exceptions", monitor.rulePacks.overrides(for: pack.id).count), tint: FL.good)
+ if !exceptions.isEmpty {
+ pill(L("%lld exceptions", exceptions.count), tint: FL.good)
}
Spacer()
- Button(L("Review…")) { editor = pack }
+ Button(L("Review…")) {
+ saveErrors[pack.id] = nil
+ editor = RulePackEditorRequest(pack: pack, intent: .review)
+ }.disabled(isSaving)
+ }
+ if let saveError = saveErrors[pack.id] {
+ Label(saveError, systemImage: "exclamationmark.triangle")
+ .font(.caption).foregroundStyle(FL.critical)
}
Text(L(pack.manifest.caveatKey)).font(.caption).foregroundStyle(.tertiary)
.fixedSize(horizontal: false, vertical: true)
@@ -73,13 +121,13 @@ struct RulePacksView: View {
private func status(_ configuration: RulePackConfiguration) -> String {
if !configuration.enabled {
- switch monitor.rulePacks.status {
+ switch packs.status {
case .applying: return L("Applying…")
case .failed: return L("Needs review")
default: return L("Off")
}
}
- switch monitor.rulePacks.status {
+ switch packs.status {
case .off, .ready, .applying: return monitor.canBlock ? L("Applying…") : L("Watching only")
case .enforcing: return monitor.canBlock ? L("Blocking") : L("Watching only")
case .watchingOnly: return L("Watching only")
@@ -90,12 +138,12 @@ struct RulePacksView: View {
private func statusTint(_ configuration: RulePackConfiguration) -> Color? {
if !configuration.enabled {
- switch monitor.rulePacks.status {
+ switch packs.status {
case .applying, .failed: return FL.warning
default: return nil
}
}
- return monitor.canBlock && monitor.rulePacks.status == .enforcing ? FL.critical : FL.warning
+ return monitor.canBlock && packs.status == .enforcing ? FL.critical : FL.warning
}
private func symbol(_ pack: RulePackDefinition) -> String {
@@ -118,19 +166,25 @@ private struct RulePackEditor: View {
let pack: RulePackDefinition
@EnvironmentObject var monitor: TrafficMonitor
@Environment(\.dismiss) private var dismiss
- @State private var configuration: RulePackConfiguration
- @State private var originalConfiguration: RulePackConfiguration
- @State private var app = ""
+ @ObservedObject var packs: RulePackStore
+ @ObservedObject var rules: RuleStore
+ @State private var draft: RulePackEditorDraft
@State private var installed: [InstalledApps.App] = []
+ @State private var appsLoaded = false
+ @State private var appsScanned = false
@State private var preview: RuleSimulation.Result?
@State private var previewing = false
+ @State private var previewError: String?
+ @State private var saveError: String?
@State private var confirmBroadening = false
+ @State private var saving = false
- init(pack: RulePackDefinition) {
+ init(pack: RulePackDefinition, saved: RulePackConfiguration,
+ intent: RulePackEditorDraft.Intent, packs: RulePackStore, rules: RuleStore) {
self.pack = pack
- let initial = RulePackConfiguration(packID: pack.id)
- _configuration = State(initialValue: initial)
- _originalConfiguration = State(initialValue: initial)
+ self.packs = packs
+ self.rules = rules
+ _draft = State(initialValue: RulePackEditorDraft(saved: saved, intent: intent))
}
var body: some View {
@@ -149,28 +203,32 @@ private struct RulePackEditor: View {
exceptions
}.padding(Measure.gutter)
}
+ .disabled(saving)
+ if let saveError {
+ Label(saveError, systemImage: "exclamationmark.triangle")
+ .font(.caption).foregroundStyle(FL.critical)
+ .padding(.horizontal, Measure.gutter).padding(.bottom, 8)
+ }
Divider()
HStack {
Button(L("Test against recent traffic")) { Task { await runPreview() } }
+ .disabled(previewing || saving)
if previewing { ProgressView().controlSize(.small) }
Spacer()
- Button(L("Cancel")) { dismiss() }
+ Button(L("Cancel")) { dismiss() }.disabled(saving)
Button(L("Save")) { save() }
- .keyboardShortcut(.defaultAction)
- .disabled(configuration.scope == .selectedApps && configuration.apps.isEmpty)
+ .disabled(!draft.canSave || saving)
}.padding(Measure.gutter)
}
.frame(width: Measure.prose)
.frame(minHeight: 620)
- .onAppear {
- let saved = monitor.rulePacks.configuration(for: pack.id)
- configuration = saved
- if !saved.enabled { configuration.enabled = true }
- originalConfiguration = saved
- Task { installed = await Task.detached(priority: .userInitiated) { InstalledApps.all() }.value }
- }
+ .interactiveDismissDisabled(saving)
+ .onChange(of: draft.configuration) { _, _ in invalidatePreview() }
+ .onChange(of: packs.version) { _, _ in invalidatePreview() }
+ .onChange(of: rules.version) { _, _ in invalidatePreview() }
+ .onAppear { loadApps() }
.confirmationDialog(L("Apply this pack to every app?"), isPresented: $confirmBroadening) {
- Button(L("Apply to every app"), role: .destructive) { commit() }
+ Button(L("Apply to every app"), role: .destructive) { commit(draft.configuration) }
Button(L("Cancel"), role: .cancel) {}
} message: {
Text(L("This broadens blocking from selected apps to every app on this Mac. Essential system traffic remains allowed."))
@@ -180,16 +238,23 @@ private struct RulePackEditor: View {
private var scope: some View {
VStack(alignment: .leading, spacing: 8) {
Text(L("Scope")).font(.headline)
- Picker("", selection: $configuration.scope) {
+ Picker("", selection: $draft.configuration.scope) {
Text(L("Every app")).tag(RulePackConfiguration.Scope.allApps)
Text(L("Only selected apps")).tag(RulePackConfiguration.Scope.selectedApps)
}.pickerStyle(.segmented).labelsHidden()
- if configuration.scope == .selectedApps {
+ if draft.configuration.scope == .selectedApps {
HStack {
- TextField(L("App name, bundle identifier, or process"), text: $app)
- Button(L("Add")) { addApp() }.disabled(app.trimmingCharacters(in: .whitespaces).isEmpty)
+ TextField(L("App name, bundle identifier, or process"), text: $draft.typedApp)
+ .onSubmit { if appsLoaded { addApp() } }
+ if !appsScanned {
+ ProgressView().controlSize(.small)
+ } else if !appsLoaded {
+ Button(L("Refresh")) { loadApps() }.controlSize(.small)
+ }
+ Button(L("Add")) { addApp() }
+ .disabled(!appsLoaded || draft.typedApp.trimmingCharacters(in: .whitespaces).isEmpty)
}
- if !app.isEmpty, !appSuggestions.isEmpty {
+ if !draft.typedApp.isEmpty, !appSuggestions.isEmpty {
VStack(alignment: .leading, spacing: 0) {
ForEach(appSuggestions) { suggestion in
Button {
@@ -210,8 +275,8 @@ private struct RulePackEditor: View {
}
}
FlowLayout(spacing: 6) {
- ForEach(configuration.apps, id: \.self) { id in
- Button { configuration.apps.removeAll { $0 == id } } label: {
+ ForEach(draft.configuration.apps, id: \.self) { id in
+ Button { draft.configuration.apps.removeAll { $0 == id } } label: {
Label(id, systemImage: "xmark.circle.fill").font(.caption)
}.buttonStyle(.borderless)
}
@@ -227,7 +292,11 @@ private struct RulePackEditor: View {
}
@ViewBuilder private var previewSection: some View {
- if let preview {
+ if let previewError {
+ Label(previewError, systemImage: "exclamationmark.triangle")
+ .font(.caption).foregroundStyle(FL.critical)
+ .padding(10).background(FL.critical.opacity(0.1), in: RoundedRectangle(cornerRadius: 8))
+ } else if let preview {
VStack(alignment: .leading, spacing: 5) {
Text(L("Recent-traffic impact")).font(.headline)
if preview.connectionsConsidered == 0 {
@@ -258,7 +327,7 @@ private struct RulePackEditor: View {
}
@ViewBuilder private var exceptions: some View {
- let found = monitor.rulePacks.overrides(for: pack.id)
+ let found = packs.overrides(for: pack.id)
if !found.isEmpty {
DisclosureGroup(L("%lld exceptions", found.count)) {
ForEach(found) { exception in
@@ -267,7 +336,7 @@ private struct RulePackEditor: View {
Text("→").foregroundStyle(.tertiary)
Text(exception.host).font(.caption.monospaced())
Spacer()
- Button(L("Delete"), role: .destructive) { monitor.rulePacks.deleteOverride(exception) }
+ Button(L("Delete"), role: .destructive) { packs.deleteOverride(exception) }
}.padding(.vertical, 2)
}
}
@@ -275,45 +344,82 @@ private struct RulePackEditor: View {
}
private var appSuggestions: [InstalledApps.App] {
- Array(InstalledApps.search(app, in: installed).filter { candidate in
- !configuration.apps.contains { $0.caseInsensitiveCompare(candidate.bundleID) == .orderedSame }
+ Array(InstalledApps.search(draft.typedApp, in: installed).filter { candidate in
+ !draft.configuration.apps.contains { $0.caseInsensitiveCompare(candidate.bundleID) == .orderedSame }
}.prefix(5))
}
- private func addApp(_ value: String? = nil) {
- guard let target = InstalledApps.target(forTyped: value ?? app, apps: installed) else { return }
- if !configuration.apps.contains(where: { $0.caseInsensitiveCompare(target.value) == .orderedSame }) {
- configuration.apps.append(target.value); configuration.apps.sort()
+ private func loadApps() {
+ appsScanned = false
+ appsLoaded = false
+ Task {
+ let result = await Task.detached(priority: .userInitiated) { InstalledApps.scan() }.value
+ installed = result.apps
+ appsLoaded = result.complete
+ appsScanned = true
}
- app = ""
+ }
+
+ private func addApp(_ value: String? = nil) {
+ if let value { _ = draft.addApp(value, installed: installed, appsLoaded: appsLoaded) }
+ else { _ = draft.submitApp(installed: installed, appsLoaded: appsLoaded) }
}
private func save() {
- if originalConfiguration.enabled,
- originalConfiguration.scope == .selectedApps,
- configuration.scope == .allApps {
- confirmBroadening = true
- } else {
- commit()
+ saveError = nil
+ switch draft.requestSave() {
+ case .commit(let configuration): commit(configuration)
+ case .confirmBroadening: confirmBroadening = true
+ case nil: break
}
}
- private func commit() {
- monitor.rulePacks.save(configuration)
- dismiss()
+ private func commit(_ configuration: RulePackConfiguration? = nil) {
+ saving = true
+ saveError = nil
+ let value = configuration ?? draft.configuration
+ packs.save(value) { success in
+ saving = false
+ if success {
+ dismiss()
+ } else {
+ saveError = packs.failureMessage ?? L("Failed: %@", L("Save"))
+ }
+ }
+ }
+
+ private func invalidatePreview() {
+ preview = nil
+ previewError = nil
}
private func runPreview() async {
+ let candidate = draft.configuration
+ let packVersion = packs.version
+ let ruleVersion = rules.version
previewing = true
+ preview = nil
+ previewError = nil
defer { previewing = false }
- let input = try? await monitor.read {
- (try $0.flowsForSimulation(since: Date().addingTimeInterval(-7 * 86400)), try $0.loadPolicies())
+ do {
+ let input = try await monitor.read {
+ (try $0.flowsForSimulation(since: Date().addingTimeInterval(-7 * 86400)),
+ try $0.loadPolicies())
+ }
+ guard candidate == draft.configuration,
+ packVersion == packs.version,
+ ruleVersion == rules.version else { return }
+ preview = RulePackSimulation.run(candidate: candidate, definitions: packs.definitions,
+ configurations: packs.configurations,
+ overrides: packs.overrides, rules: rules.rules,
+ policies: input.1, paused: rules.isPaused,
+ flows: input.0, session: RuleStore.session)
+ } catch {
+ guard candidate == draft.configuration,
+ packVersion == packs.version,
+ ruleVersion == rules.version else { return }
+ previewError = L("Failed: %@", L("Test against recent traffic"))
}
- preview = RulePackSimulation.run(candidate: configuration, definitions: monitor.rulePacks.definitions,
- configurations: monitor.rulePacks.configurations,
- overrides: monitor.rulePacks.overrides, rules: monitor.rules.rules,
- policies: input?.1 ?? [:], paused: monitor.rules.isPaused,
- flows: input?.0 ?? [], session: RuleStore.session)
}
}
diff --git a/FlowlightTests/RulePackTests.swift b/FlowlightTests/RulePackTests.swift
index 640a898e..cfaa4bee 100644
--- a/FlowlightTests/RulePackTests.swift
+++ b/FlowlightTests/RulePackTests.swift
@@ -224,6 +224,57 @@ final class RulePackTests: XCTestCase {
XCTAssertEqual(publications[0]?.configurations, saved)
}
+ @MainActor
+ func testStoreDisablesEmptySelectedAppConfiguration() async throws {
+ let url = FileManager.default.temporaryDirectory.appendingPathComponent("flowlight-pack-empty-scope-\(UUID().uuidString).sqlite")
+ let db = try TrafficDatabase(url: url)
+ let store = RulePackStore()
+ store.attach(db: db)
+ try await waitUntil { store.configurations.count == store.definitions.count }
+
+ var candidate = store.configuration(for: "official.advertising")
+ candidate.enabled = true
+ candidate.scope = .selectedApps
+ candidate.apps = [" ", "\n"]
+ var completed = false
+ store.save(candidate) { success in
+ XCTAssertTrue(success)
+ completed = true
+ }
+
+ try await waitUntil { completed }
+ let committed = store.configuration(for: candidate.packID)
+ XCTAssertFalse(committed.enabled)
+ XCTAssertEqual(committed.scope, .selectedApps)
+ XCTAssertTrue(committed.apps.isEmpty)
+ let persisted = try db.sync { try $0.loadRulePackConfigurations() }
+ XCTAssertEqual(persisted, [committed])
+ }
+
+ @MainActor
+ func testStoreSaveCompletionSeesCommittedConfiguration() async throws {
+ let url = FileManager.default.temporaryDirectory.appendingPathComponent("flowlight-pack-completion-\(UUID().uuidString).sqlite")
+ let db = try TrafficDatabase(url: url)
+ let store = RulePackStore()
+ store.attach(db: db)
+ try await waitUntil { store.configurations.count == store.definitions.count }
+
+ var candidate = store.configuration(for: "official.advertising")
+ candidate.enabled = true
+ candidate.scope = .selectedApps
+ candidate.apps = ["com.apple.curl"]
+ var completed = false
+ store.save(candidate) { success in
+ XCTAssertTrue(success)
+ XCTAssertEqual(store.configuration(for: candidate.packID).scope, .selectedApps)
+ XCTAssertEqual(store.configuration(for: candidate.packID).apps, ["com.apple.curl"])
+ XCTAssertNotNil(store.snapshot)
+ completed = true
+ }
+
+ try await waitUntil { completed }
+ }
+
@MainActor
func testStorePublishesAfterTransientFailureRecovers() async throws {
let url = FileManager.default.temporaryDirectory.appendingPathComponent("flowlight-pack-recovery-\(UUID().uuidString).sqlite")
@@ -306,6 +357,87 @@ final class RulePackTests: XCTestCase {
XCTAssertNil(store.snapshot)
}
+ func testEditorReviewPreservesOffConfiguration() {
+ let saved = RulePackConfiguration(packID: "official.advertising")
+ let draft = RulePackEditorDraft(saved: saved, intent: .review)
+
+ XCTAssertFalse(draft.configuration.enabled)
+ XCTAssertEqual(draft.configuration.scope, .allApps)
+ XCTAssertEqual(draft.requestSave(), .commit(saved))
+ }
+
+ func testEditorActivationAndCancelNeverMutateSavedConfiguration() {
+ let saved = RulePackConfiguration(packID: "official.advertising")
+ var draft = RulePackEditorDraft(saved: saved, intent: .activate)
+
+ XCTAssertTrue(draft.configuration.enabled)
+ draft.configuration.scope = .selectedApps
+ draft.typedApp = "com.apple.curl"
+ XCTAssertEqual(draft.submitApp(installed: []), .added("com.apple.curl"))
+ XCTAssertEqual(draft.configuration.apps, ["com.apple.curl"])
+ XCTAssertEqual(saved, RulePackConfiguration(packID: "official.advertising"))
+ }
+
+ func testEditorReturnAddsAppWithoutSaving() {
+ let saved = RulePackConfiguration(packID: "official.advertising")
+ var draft = RulePackEditorDraft(saved: saved, intent: .activate)
+ draft.configuration.scope = .selectedApps
+ draft.typedApp = "com.apple.curl"
+
+ XCTAssertEqual(draft.submitApp(installed: []), .added("com.apple.curl"))
+ XCTAssertEqual(draft.configuration.apps, ["com.apple.curl"])
+ XCTAssertEqual(draft.typedApp, "")
+
+ draft.typedApp = "com.example.second"
+ XCTAssertEqual(draft.submitApp(installed: []), .added("com.example.second"))
+ XCTAssertEqual(draft.configuration.apps, ["com.apple.curl", "com.example.second"])
+ XCTAssertEqual(draft.requestSave(), .commit(draft.configuration),
+ "Return only edits the draft; explicit Save decides whether to commit")
+ }
+
+ func testConcurrentCardSavesStayTrackedIndependently() {
+ var saves = RulePackSaveTracker()
+ saves.begin("official.advertising")
+ saves.begin("official.tracking")
+
+ saves.finish("official.advertising")
+
+ XCTAssertFalse(saves.contains("official.advertising"))
+ XCTAssertTrue(saves.contains("official.tracking"))
+ }
+
+ func testEditorWaitsForInstalledAppsBeforeResolvingDisplayName() {
+ let safari = InstalledApps.App(name: "Safari", bundleID: "com.apple.Safari",
+ path: "/Applications/Safari.app")
+ var draft = RulePackEditorDraft(saved: RulePackConfiguration(packID: "official.advertising"),
+ intent: .activate)
+ draft.configuration.scope = .selectedApps
+ draft.typedApp = "Safari"
+
+ XCTAssertEqual(draft.submitApp(installed: [], appsLoaded: false), .loading)
+ XCTAssertEqual(draft.typedApp, "Safari")
+ XCTAssertTrue(draft.configuration.apps.isEmpty)
+
+ XCTAssertEqual(draft.submitApp(installed: [safari], appsLoaded: true), .added("com.apple.Safari"))
+ XCTAssertEqual(draft.configuration.apps, ["com.apple.Safari"])
+ XCTAssertEqual(draft.typedApp, "")
+ }
+
+ func testEditorExplicitSaveAndBroadeningDecision() {
+ var selected = RulePackConfiguration(packID: "official.advertising", enabled: true,
+ scope: .selectedApps, apps: ["com.apple.curl"])
+ var draft = RulePackEditorDraft(saved: selected, intent: .review)
+ XCTAssertEqual(draft.requestSave(), .commit(selected))
+
+ draft.configuration.scope = .allApps
+ XCTAssertEqual(draft.requestSave(), .confirmBroadening)
+
+ selected.apps = []
+ let invalid = RulePackEditorDraft(saved: selected, intent: .review)
+ XCTAssertNil(invalid.requestSave())
+ XCTAssertFalse(invalid.canSave)
+ }
+
func testActivityKeepsPackProvenanceSeparateAndReceiptAfterPrune() throws {
let url = FileManager.default.temporaryDirectory.appendingPathComponent("flowlight-pack-provenance-\(UUID().uuidString).sqlite")
let db = try TrafficDatabase(url: url)
diff --git a/docs/404.html b/docs/404.html
index ed5c704e..f58b62be 100644
--- a/docs/404.html
+++ b/docs/404.html
@@ -86,7 +86,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.14.5 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.15.1 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/about/index.html b/docs/about/index.html
index a38fa366..66e89b79 100644
--- a/docs/about/index.html
+++ b/docs/about/index.html
@@ -153,7 +153,7 @@ Thanks
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.14.5 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.15.1 · 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 0f988be2..2ff7ac90 100644
--- a/docs/de/about/index.html
+++ b/docs/de/about/index.html
@@ -152,7 +152,7 @@ Dank
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.14.5 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.15.1 · 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 4461004f..da5346ff 100644
--- a/docs/de/docs/index.html
+++ b/docs/de/docs/index.html
@@ -76,7 +76,7 @@
Dokumentation
Flowlight benutzen
-
Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.14.5, macOS 15 oder neuer.
+
Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.15.1, macOS 15 oder neuer.
@@ -753,7 +753,7 @@ Grenzen
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.14.5 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.15.1 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/de/index.html b/docs/de/index.html
index c92f92ee..533c8140 100644
--- a/docs/de/index.html
+++ b/docs/de/index.html
@@ -48,7 +48,7 @@
-
+
Zum Inhalt springen
@@ -90,7 +90,7 @@ Sieh, was deine Apps im Netz tun.brew install --cask xinbetween/tap/flowlightKopieren
- v0.14.5 macOS 15+ Universal GPL-3.0 Keine Telemetrie
+ v0.15.1 macOS 15+ Universal GPL-3.0 Keine Telemetrie
@@ -571,7 +571,7 @@ Wisse, was deinen Mac verlässt.
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.14.5 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.15.1 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/de/privacy/index.html b/docs/de/privacy/index.html
index 5bd44a43..b0d8e609 100644
--- a/docs/de/privacy/index.html
+++ b/docs/de/privacy/index.html
@@ -188,7 +188,7 @@
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.14.5 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.15.1 · 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 356b430d..3bcc4a83 100644
--- a/docs/de/threat-model/index.html
+++ b/docs/de/threat-model/index.html
@@ -228,7 +228,7 @@ Eine Schwachstelle melden
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.14.5 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.15.1 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/docs/index.html b/docs/docs/index.html
index c311e756..f841d88a 100644
--- a/docs/docs/index.html
+++ b/docs/docs/index.html
@@ -76,7 +76,7 @@
Documentation
Using Flowlight
-
Everything from the first launch to tuning the agent rules. Flowlight 0.14.5, macOS 15 or later.
+
Everything from the first launch to tuning the agent rules. Flowlight 0.15.1, macOS 15 or later.
@@ -787,7 +787,7 @@ Limitations
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.14.5 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.15.1 · 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 f92509b7..c860c5eb 100644
--- a/docs/es/about/index.html
+++ b/docs/es/about/index.html
@@ -152,7 +152,7 @@ Agradecimientos
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.14.5 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.15.1 · 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 5e60ca7a..b93b3f5b 100644
--- a/docs/es/docs/index.html
+++ b/docs/es/docs/index.html
@@ -76,7 +76,7 @@
Documentación
Usar Flowlight
-
Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.14.5, macOS 15 o posterior.
+
Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.15.1, macOS 15 o posterior.
@@ -747,7 +747,7 @@ Limitaciones
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.14.5 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.15.1 · 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 018fc274..74ba3f12 100644
--- a/docs/es/index.html
+++ b/docs/es/index.html
@@ -48,7 +48,7 @@
-
+
Ir al contenido
@@ -90,7 +90,7 @@ Observa la actividad de red de tus apps.
brew install --cask xinbetween/tap/flowlightCopiar
- v0.14.5 macOS 15+ Universal GPL-3.0 Sin telemetría
+ v0.15.1 macOS 15+ Universal GPL-3.0 Sin telemetría
@@ -571,7 +571,7 @@ Ten claro qué sale de tu Mac.
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.14.5 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.15.1 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/es/privacy/index.html b/docs/es/privacy/index.html
index 323e53c3..dd16995b 100644
--- a/docs/es/privacy/index.html
+++ b/docs/es/privacy/index.html
@@ -188,7 +188,7 @@
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.14.5 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.15.1 · 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 f9a4a936..4f8bbb36 100644
--- a/docs/es/threat-model/index.html
+++ b/docs/es/threat-model/index.html
@@ -223,7 +223,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.14.5 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.15.1 · 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 6b50674b..bc3091bf 100644
--- a/docs/fr/about/index.html
+++ b/docs/fr/about/index.html
@@ -152,7 +152,7 @@ Remerciements
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.14.5 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.15.1 · 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 4d7507e3..1e615d9f 100644
--- a/docs/fr/docs/index.html
+++ b/docs/fr/docs/index.html
@@ -76,7 +76,7 @@
Documentation
Utiliser Flowlight
-
Tout, du premier lancement au réglage des règles des agents. Flowlight 0.14.5, macOS 15 ou version ultérieure.
+
Tout, du premier lancement au réglage des règles des agents. Flowlight 0.15.1, macOS 15 ou version ultérieure.
@@ -753,7 +753,7 @@ Limites
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.14.5 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.15.1 · 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 1f984ac0..2b1cba85 100644
--- a/docs/fr/index.html
+++ b/docs/fr/index.html
@@ -48,7 +48,7 @@
-
+
Aller au contenu
@@ -90,7 +90,7 @@ Voyez l’activité réseau de vos apps.
brew install --cask xinbetween/tap/flowlightCopier
- v0.14.5 macOS 15+ Universel GPL-3.0 Sans télémétrie
+ v0.15.1 macOS 15+ Universel GPL-3.0 Sans télémétrie
@@ -571,7 +571,7 @@ Sachez ce qui quitte votre Mac.
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.14.5 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.15.1 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/fr/privacy/index.html b/docs/fr/privacy/index.html
index bca75118..f7248635 100644
--- a/docs/fr/privacy/index.html
+++ b/docs/fr/privacy/index.html
@@ -188,7 +188,7 @@
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.14.5 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.15.1 · 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 a2e218d0..376378f9 100644
--- a/docs/fr/threat-model/index.html
+++ b/docs/fr/threat-model/index.html
@@ -226,7 +226,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.14.5 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.15.1 · 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 e2a4cbe9..a66a9a77 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -48,7 +48,7 @@
-
+
Skip to content
@@ -90,7 +90,7 @@ See your apps' network activity.brew install --cask xinbetween/tap/flowlightCopy
- v0.14.5 macOS 15+ Universal GPL-3.0 No telemetry
+ v0.15.1 macOS 15+ Universal GPL-3.0 No telemetry
@@ -592,7 +592,7 @@ Know what leaves your Mac.
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.14.5 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.15.1 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/it/about/index.html b/docs/it/about/index.html
index 8dea6237..c3ef1346 100644
--- a/docs/it/about/index.html
+++ b/docs/it/about/index.html
@@ -152,7 +152,7 @@ Ringraziamenti
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.14.5 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.15.1 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/it/docs/index.html b/docs/it/docs/index.html
index f6be250c..46707524 100644
--- a/docs/it/docs/index.html
+++ b/docs/it/docs/index.html
@@ -76,7 +76,7 @@
Documentazione
Usare Flowlight
-
Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.14.5, macOS 15 o successivo.
+
Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.15.1, macOS 15 o successivo.
@@ -751,7 +751,7 @@ Limitazioni
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.14.5 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.15.1 · 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 a4a7539a..e04dd8e7 100644
--- a/docs/it/index.html
+++ b/docs/it/index.html
@@ -48,7 +48,7 @@
-
+
Vai al contenuto
@@ -90,7 +90,7 @@ Vedi la rete delle tue app.Ca
Metti una stella su GitHub
brew install --cask xinbetween/tap/flowlightCopia
- v0.14.5 macOS 15+ Universale GPL-3.0 Nessuna telemetria
+ v0.15.1 macOS 15+ Universale GPL-3.0 Nessuna telemetria
@@ -571,7 +571,7 @@ Sappi che cosa esce dal tuo Mac.
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.14.5 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.15.1 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/it/privacy/index.html b/docs/it/privacy/index.html
index 359df4d7..0e41922a 100644
--- a/docs/it/privacy/index.html
+++ b/docs/it/privacy/index.html
@@ -188,7 +188,7 @@
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.14.5 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.15.1 · 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 23d6da4a..cd636f1a 100644
--- a/docs/it/threat-model/index.html
+++ b/docs/it/threat-model/index.html
@@ -224,7 +224,7 @@ Segnalare una vulnerabilità
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.14.5 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.15.1 · 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 5af756e8..254e1a95 100644
--- a/docs/ja/about/index.html
+++ b/docs/ja/about/index.html
@@ -153,7 +153,7 @@ 謝辞
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.14.5 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.15.1 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/docs/index.html b/docs/ja/docs/index.html
index 4c1f69cb..9e64ff3d 100644
--- a/docs/ja/docs/index.html
+++ b/docs/ja/docs/index.html
@@ -76,7 +76,7 @@
ドキュメント
Flowlight の使い方
-
初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.14.5、macOS 15 以降。
+
初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.15.1、macOS 15 以降。
@@ -701,7 +701,7 @@ 制限事項
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.14.5 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.15.1 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/index.html b/docs/ja/index.html
index 6551bfcf..f804e52d 100644
--- a/docs/ja/index.html
+++ b/docs/ja/index.html
@@ -48,7 +48,7 @@
-
+
本文へスキップ
@@ -90,7 +90,7 @@ アプリの通信が見える。brew install --cask xinbetween/tap/flowlightコピー
- v0.14.5 macOS 15+ ユニバーサル GPL-3.0 テレメトリなし
+ v0.15.1 macOS 15+ ユニバーサル GPL-3.0 テレメトリなし
@@ -570,7 +570,7 @@ Mac から何が出ていくのかを知る。
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.14.5 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.15.1 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/privacy/index.html b/docs/ja/privacy/index.html
index e7b1924c..ca6c1cd0 100644
--- a/docs/ja/privacy/index.html
+++ b/docs/ja/privacy/index.html
@@ -186,7 +186,7 @@
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.14.5 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.15.1 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/threat-model/index.html b/docs/ja/threat-model/index.html
index 04db3de1..49c128a7 100644
--- a/docs/ja/threat-model/index.html
+++ b/docs/ja/threat-model/index.html
@@ -225,7 +225,7 @@ 脆弱性を報告する
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.14.5 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.15.1 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ko/about/index.html b/docs/ko/about/index.html
index 30cbf96e..bdb31f5e 100644
--- a/docs/ko/about/index.html
+++ b/docs/ko/about/index.html
@@ -153,7 +153,7 @@ 감사
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.14.5 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.15.1 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/docs/index.html b/docs/ko/docs/index.html
index c0cd067a..8abc02f2 100644
--- a/docs/ko/docs/index.html
+++ b/docs/ko/docs/index.html
@@ -76,7 +76,7 @@
문서
Flowlight 사용하기
-
첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.14.5, macOS 15 이상.
+
첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.15.1, macOS 15 이상.
@@ -743,7 +743,7 @@ 한계
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.14.5 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.15.1 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/index.html b/docs/ko/index.html
index 43e312df..03b89a4d 100644
--- a/docs/ko/index.html
+++ b/docs/ko/index.html
@@ -48,7 +48,7 @@
-
+
본문으로 이동
@@ -90,7 +90,7 @@ 앱이 무엇을 하는지 봅니다.
brew install --cask xinbetween/tap/flowlight복사
- v0.14.5 macOS 15+ 유니버설 GPL-3.0 텔레메트리 없음
+ v0.15.1 macOS 15+ 유니버설 GPL-3.0 텔레메트리 없음
@@ -571,7 +571,7 @@ 내 Mac에서 무엇이 나가는지 아세요.
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.14.5 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.15.1 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/privacy/index.html b/docs/ko/privacy/index.html
index e864c83f..09d889c6 100644
--- a/docs/ko/privacy/index.html
+++ b/docs/ko/privacy/index.html
@@ -189,7 +189,7 @@
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.14.5 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.15.1 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/threat-model/index.html b/docs/ko/threat-model/index.html
index 886bf819..a4e8f2d9 100644
--- a/docs/ko/threat-model/index.html
+++ b/docs/ko/threat-model/index.html
@@ -223,7 +223,7 @@ 취약점 신고하기
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.14.5 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.15.1 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/llms-full.txt b/docs/llms-full.txt
index 93d664d8..5cc393a3 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.14.5, macOS 15 or later.
+ Everything from the first launch to tuning the agent rules. Flowlight 0.15.1, macOS 15 or later.
On this page
@@ -887,7 +887,7 @@ Understand your AI agents.
brew install --cask xinbetween/tap/flowlightCopy
- v0.14.5macOS 15+UniversalGPL-3.0No telemetry
+ v0.15.1macOS 15+UniversalGPL-3.0No telemetry
connectionslive
@@ -1617,8 +1617,39 @@ Releases
Downloads, checksums and full notes for each version are on GitHub Releases.
+ 0.15.1
+October 18, 2026Latest
+
+ Keep managed-pack changes safely in review. Opening or cancelling an off pack no longer
+ activates it, and configuration changes persist only after an explicit Save succeeds.
+
+ Add selected apps without closing the editor. Pressing Return in the app field now adds
+ the typed app or process to the draft instead of triggering the sheet's Save action.
+
+ Show the configuration that actually committed. Pack cards now observe managed-pack state
+ directly, so scope, status, exceptions and activity refresh as soon as an asynchronous save completes.
+
+ 0.15.0
+October 17, 2026
+
+ Block known ad and tracker destinations across apps. Three official managed packs cover
+ advertising, cross-site tracking, and privacy/telemetry hosts. They ship disabled, require explicit review,
+ and can apply to every app or only selected apps.
+
+ Keep blocking explainable and reversible. Preview seven days of retained traffic before
+ enabling a pack, review its built-in destinations and provenance, inspect aggregated activity, add scoped
+ per-app exceptions, or use an ordinary allow rule as the universal escape hatch.
+
+ Fail open with atomic, version-bound enforcement. Flowlight validates and transfers one
+ complete pack generation to the Network Extension, binds consent to the exact reviewed content, preserves
+ ordinary-rule and essential-system precedence, and never treats a partial or stale update as active.
+
+ Set honest network-only expectations. Hostname blocking cannot remove first-party ads,
+ page gaps, browser overlays, scripted popups or native app windows; those need the later browser extension.
+ ECH, QUIC, encrypted DNS, direct IPs, shared CDNs and unavailable hostnames can also limit coverage.
+
0.14.5
-October 17, 2026Latest
+October 17, 2026
Review plugin actions in Flowlight's existing editors. Plugin findings can now suggest guardrails,
network rules, mock responses, request rewrites and response transforms as typed, inert actions. Inspect opens the
diff --git a/docs/llms.txt b/docs/llms.txt
index 86ab145d..b6693510 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.14.5.
+> 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.15.1.
- [Download Flowlight.dmg](https://github.com/xinbetween/flowlight/releases/latest/download/Flowlight.dmg)
- [Source code](https://github.com/xinbetween/flowlight)
diff --git a/docs/plugins/index.html b/docs/plugins/index.html
index f79a6096..0ba7b180 100644
--- a/docs/plugins/index.html
+++ b/docs/plugins/index.html
@@ -211,7 +211,7 @@ Review checklist
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.14.5 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.15.1 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/privacy/index.html b/docs/privacy/index.html
index a45c0bdd..a9063d7a 100644
--- a/docs/privacy/index.html
+++ b/docs/privacy/index.html
@@ -195,7 +195,7 @@
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.14.5 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.15.1 · 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 51095b6b..01d336ad 100644
--- a/docs/pt-PT/about/index.html
+++ b/docs/pt-PT/about/index.html
@@ -152,7 +152,7 @@ Agradecimentos
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.14.5 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.15.1 · 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 e786828c..8696733e 100644
--- a/docs/pt-PT/docs/index.html
+++ b/docs/pt-PT/docs/index.html
@@ -76,7 +76,7 @@
Documentação
Usar o Flowlight
-
Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.14.5, macOS 15 ou posterior.
+
Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.15.1, macOS 15 ou posterior.
@@ -748,7 +748,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.14.5 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.15.1 · 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 5d03fbd0..70178b72 100644
--- a/docs/pt-PT/index.html
+++ b/docs/pt-PT/index.html
@@ -48,7 +48,7 @@
-
+
Ir para o conteúdo
@@ -90,7 +90,7 @@ Veja a atividade de rede das suas apps.
brew install --cask xinbetween/tap/flowlightCopiar
- v0.14.5 macOS 15+ Universal GPL-3.0 Sem telemetria
+ v0.15.1 macOS 15+ Universal GPL-3.0 Sem telemetria
@@ -571,7 +571,7 @@ Saiba o que sai do seu Mac.
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.14.5 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.15.1 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/pt-PT/privacy/index.html b/docs/pt-PT/privacy/index.html
index dcc3a722..c8f83100 100644
--- a/docs/pt-PT/privacy/index.html
+++ b/docs/pt-PT/privacy/index.html
@@ -188,7 +188,7 @@
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.14.5 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.15.1 · 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 40df0438..e98cb423 100644
--- a/docs/pt-PT/threat-model/index.html
+++ b/docs/pt-PT/threat-model/index.html
@@ -225,7 +225,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.14.5 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.15.1 · 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 204293f7..f81c084d 100644
--- a/docs/releases/index.html
+++ b/docs/releases/index.html
@@ -28,7 +28,7 @@
-
+
Skip to content
@@ -61,7 +61,37 @@ What's new
- 0.14.5 October 17, 2026 Latest
+ 0.15.1 October 18, 2026 Latest
+
+ Keep managed-pack changes safely in review. Opening or cancelling an off pack no longer
+ activates it, and configuration changes persist only after an explicit Save succeeds.
+ Add selected apps without closing the editor. Pressing Return in the app field now adds
+ the typed app or process to the draft instead of triggering the sheet's Save action.
+ Show the configuration that actually committed. Pack cards now observe managed-pack state
+ directly, so scope, status, exceptions and activity refresh as soon as an asynchronous save completes.
+
+
+
+
+
+
+ Block known ad and tracker destinations across apps. Three official managed packs cover
+ advertising, cross-site tracking, and privacy/telemetry hosts. They ship disabled, require explicit review,
+ and can apply to every app or only selected apps.
+ Keep blocking explainable and reversible. Preview seven days of retained traffic before
+ enabling a pack, review its built-in destinations and provenance, inspect aggregated activity, add scoped
+ per-app exceptions, or use an ordinary allow rule as the universal escape hatch.
+ Fail open with atomic, version-bound enforcement. Flowlight validates and transfers one
+ complete pack generation to the Network Extension, binds consent to the exact reviewed content, preserves
+ ordinary-rule and essential-system precedence, and never treats a partial or stale update as active.
+ Set honest network-only expectations. Hostname blocking cannot remove first-party ads,
+ page gaps, browser overlays, scripted popups or native app windows; those need the later browser extension.
+ ECH, QUIC, encrypted DNS, direct IPs, shared CDNs and unavailable hostnames can also limit coverage.
+
+
+
+
+
Review plugin actions in Flowlight's existing editors. Plugin findings can now suggest guardrails,
network rules, mock responses, request rewrites and response transforms as typed, inert actions. Inspect opens the
@@ -1215,7 +1245,7 @@ What's new
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.14.5 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.15.1 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/sitemap.xml b/docs/sitemap.xml
index 5b51fb07..9253a5e4 100644
--- a/docs/sitemap.xml
+++ b/docs/sitemap.xml
@@ -5,7 +5,7 @@
https://flowlight.xinbetween.com/ 2026-10-17
https://flowlight.xinbetween.com/plugins/ 2026-10-13
https://flowlight.xinbetween.com/privacy/ 2026-10-07
- https://flowlight.xinbetween.com/releases/ 2026-10-17
+ https://flowlight.xinbetween.com/releases/ 2026-10-18
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
diff --git a/docs/threat-model/index.html b/docs/threat-model/index.html
index 60ff492b..1d879978 100644
--- a/docs/threat-model/index.html
+++ b/docs/threat-model/index.html
@@ -224,7 +224,7 @@ Reporting a vulnerability
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.14.5 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.15.1 · 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 8a2688b7..60f968de 100644
--- a/docs/zh-Hans/about/index.html
+++ b/docs/zh-Hans/about/index.html
@@ -152,7 +152,7 @@ 致谢
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.14.5 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.15.1 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/docs/index.html b/docs/zh-Hans/docs/index.html
index 3db9cbfa..684152b1 100644
--- a/docs/zh-Hans/docs/index.html
+++ b/docs/zh-Hans/docs/index.html
@@ -76,7 +76,7 @@
文档
使用 Flowlight
-
从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.14.5,macOS 15 或更高版本。
+
从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.15.1,macOS 15 或更高版本。
@@ -704,7 +704,7 @@ 限制
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.14.5 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.15.1 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/index.html b/docs/zh-Hans/index.html
index 7b1e660f..fd06a1b1 100644
--- a/docs/zh-Hans/index.html
+++ b/docs/zh-Hans/index.html
@@ -48,7 +48,7 @@
-
+
跳到正文
@@ -90,7 +90,7 @@ 看清应用的网络活动。
brew install --cask xinbetween/tap/flowlight拷贝
- v0.14.5 macOS 15+ 通用架构 GPL-3.0 无遥测
+ v0.15.1 macOS 15+ 通用架构 GPL-3.0 无遥测
@@ -568,7 +568,7 @@ 知道什么离开了你的 Mac。
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.14.5 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.15.1 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/privacy/index.html b/docs/zh-Hans/privacy/index.html
index 5978d342..090e8ea1 100644
--- a/docs/zh-Hans/privacy/index.html
+++ b/docs/zh-Hans/privacy/index.html
@@ -186,7 +186,7 @@
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.14.5 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.15.1 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/threat-model/index.html b/docs/zh-Hans/threat-model/index.html
index 58c4c32d..679dd69f 100644
--- a/docs/zh-Hans/threat-model/index.html
+++ b/docs/zh-Hans/threat-model/index.html
@@ -212,7 +212,7 @@ 报告漏洞
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.14.5 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.15.1 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hant/about/index.html b/docs/zh-Hant/about/index.html
index 77e80c69..da99e59f 100644
--- a/docs/zh-Hant/about/index.html
+++ b/docs/zh-Hant/about/index.html
@@ -152,7 +152,7 @@ 致謝
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.14.5 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.15.1 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/docs/index.html b/docs/zh-Hant/docs/index.html
index bdd338f5..b207822a 100644
--- a/docs/zh-Hant/docs/index.html
+++ b/docs/zh-Hant/docs/index.html
@@ -76,7 +76,7 @@
說明文件
使用 Flowlight
-
從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.14.5,macOS 15 或以上版本。
+
從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.15.1,macOS 15 或以上版本。
@@ -710,7 +710,7 @@ 限制
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.14.5 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.15.1 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/index.html b/docs/zh-Hant/index.html
index b06cdaf4..dee17a05 100644
--- a/docs/zh-Hant/index.html
+++ b/docs/zh-Hant/index.html
@@ -48,7 +48,7 @@
-
+
跳至內容
@@ -90,7 +90,7 @@ 看見每個 App 的網路活動。brew install --cask xinbetween/tap/flowlight拷貝
- v0.14.5 macOS 15+ 通用架構 GPL-3.0 無遙測
+ v0.15.1 macOS 15+ 通用架構 GPL-3.0 無遙測
@@ -569,7 +569,7 @@ 知道有什麼離開了你的 Mac。
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.14.5 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.15.1 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/privacy/index.html b/docs/zh-Hant/privacy/index.html
index 97a5c639..4d878aab 100644
--- a/docs/zh-Hant/privacy/index.html
+++ b/docs/zh-Hant/privacy/index.html
@@ -186,7 +186,7 @@
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.14.5 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.15.1 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/threat-model/index.html b/docs/zh-Hant/threat-model/index.html
index d440a13b..7865721a 100644
--- a/docs/zh-Hant/threat-model/index.html
+++ b/docs/zh-Hant/threat-model/index.html
@@ -213,7 +213,7 @@ 回報漏洞
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.14.5 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.15.1 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/project.yml b/project.yml
index a20d9f87..0c724f03 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.14.5"
- CURRENT_PROJECT_VERSION: "35"
+ MARKETING_VERSION: "0.15.1"
+ CURRENT_PROJECT_VERSION: "37"
targets:
Flowlight:
type: application
diff --git a/site/pages/releases.html b/site/pages/releases.html
index 337d1de7..6cc47e07 100644
--- a/site/pages/releases.html
+++ b/site/pages/releases.html
@@ -13,7 +13,37 @@ What's new
- 0.14.5 October 17, 2026 Latest
+ 0.15.1 October 18, 2026 Latest
+
+ Keep managed-pack changes safely in review. Opening or cancelling an off pack no longer
+ activates it, and configuration changes persist only after an explicit Save succeeds.
+ Add selected apps without closing the editor. Pressing Return in the app field now adds
+ the typed app or process to the draft instead of triggering the sheet's Save action.
+ Show the configuration that actually committed. Pack cards now observe managed-pack state
+ directly, so scope, status, exceptions and activity refresh as soon as an asynchronous save completes.
+
+
+
+
+
+
+ Block known ad and tracker destinations across apps. Three official managed packs cover
+ advertising, cross-site tracking, and privacy/telemetry hosts. They ship disabled, require explicit review,
+ and can apply to every app or only selected apps.
+ Keep blocking explainable and reversible. Preview seven days of retained traffic before
+ enabling a pack, review its built-in destinations and provenance, inspect aggregated activity, add scoped
+ per-app exceptions, or use an ordinary allow rule as the universal escape hatch.
+ Fail open with atomic, version-bound enforcement. Flowlight validates and transfers one
+ complete pack generation to the Network Extension, binds consent to the exact reviewed content, preserves
+ ordinary-rule and essential-system precedence, and never treats a partial or stale update as active.
+ Set honest network-only expectations. Hostname blocking cannot remove first-party ads,
+ page gaps, browser overlays, scripted popups or native app windows; those need the later browser extension.
+ ECH, QUIC, encrypted DNS, direct IPs, shared CDNs and unavailable hostnames can also limit coverage.
+
+
+
+
+
Review plugin actions in Flowlight's existing editors. Plugin findings can now suggest guardrails,
network rules, mock responses, request rewrites and response transforms as typed, inert actions. Inspect opens the