From 77c98359524f75e4b41cc7791d08992234fd2c0a Mon Sep 17 00:00:00 2001 From: Etan Joseph Heyman Date: Fri, 19 Jun 2026 18:53:28 +0300 Subject: [PATCH 1/5] feat: V1 side-by-side signal bars --- .../Sources/BrainBar/BrainBarWindowRootView.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift b/brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift index 990ddce3..8f7ee036 100644 --- a/brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift +++ b/brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift @@ -783,14 +783,14 @@ private struct BrainBarSignalCoveragePanel: View { @ViewBuilder private var signalBars: some View { - if compact { - VStack(spacing: 8) { + ViewThatFits(in: .horizontal) { + HStack(alignment: .top, spacing: compact ? 8 : 10) { ForEach(signals) { signal in signalRow(for: signal) } } - } else { - HStack(alignment: .top, spacing: 10) { + + VStack(spacing: 8) { ForEach(signals) { signal in signalRow(for: signal) } @@ -814,8 +814,10 @@ private struct BrainBarSignalCoveragePanel: View { } .buttonStyle(.plain) .help("Show Vector backlog details") + .frame(minWidth: compact ? 150 : 170, maxWidth: .infinity, alignment: .leading) } else { BrainBarSignalCoverageRow(signal: signal, compact: compact, isSelected: false) + .frame(minWidth: compact ? 150 : 170, maxWidth: .infinity, alignment: .leading) } } } @@ -916,6 +918,7 @@ private struct BrainBarVectorSignalDetail: View { } .padding(.vertical, compact ? 12 : 14) .padding(.horizontal, compact ? 12 : 16) + .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: BrainBarDesignTokens.Radius.md, style: .continuous) .fill(signal.accentColor.opacity(0.08)) From 3cc585c4a7ab44227d0872a019f49accd2875c64 Mon Sep 17 00:00:00 2001 From: Etan Joseph Heyman Date: Fri, 19 Jun 2026 19:36:21 +0300 Subject: [PATCH 2/5] Iterate BrainBar V1 under-hood panel --- .../BrainBar/BrainBarWindowRootView.swift | 163 ++++++++++++++---- .../Sources/BrainBar/BrainDatabase.swift | 71 ++++++++ .../BrainBar/Dashboard/PipelineState.swift | 18 +- .../Dashboard/SparklineRenderer.swift | 82 ++++++--- .../Tests/BrainBarTests/DashboardTests.swift | 50 ++++++ 5 files changed, 323 insertions(+), 61 deletions(-) diff --git a/brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift b/brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift index 8f7ee036..04baafb0 100644 --- a/brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift +++ b/brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift @@ -715,7 +715,9 @@ private struct BrainBarSignalCoveragePanel: View { backlogCount: stats.vectorBacklogCount, coveragePercent: stats.vectorCoveragePercent, accentColor: amber, - showsDetail: true + showsDetail: true, + vectorNetDrainRatePerHour: stats.vectorNetDrainRatePerHour, + vectorBacklogETAHours: stats.vectorBacklogETAHours ), BrainBarSignalCoverage( name: "FTS5", @@ -724,7 +726,9 @@ private struct BrainBarSignalCoveragePanel: View { backlogCount: stats.ftsBacklogCount, coveragePercent: stats.ftsCoveragePercent, accentColor: green, - showsDetail: false + showsDetail: false, + vectorNetDrainRatePerHour: nil, + vectorBacklogETAHours: nil ), BrainBarSignalCoverage( name: "Trigram", @@ -733,7 +737,9 @@ private struct BrainBarSignalCoveragePanel: View { backlogCount: stats.trigramBacklogCount, coveragePercent: stats.trigramCoveragePercent, accentColor: green, - showsDetail: false + showsDetail: false, + vectorNetDrainRatePerHour: nil, + vectorBacklogETAHours: nil ), ] } @@ -745,11 +751,6 @@ private struct BrainBarSignalCoveragePanel: View { if isExpanded { signalBars .transition(.opacity.combined(with: .move(edge: .top))) - - if isVectorDetailExpanded, let vector = signals.first(where: { $0.showsDetail }) { - BrainBarVectorSignalDetail(signal: vector, compact: compact) - .transition(.opacity.combined(with: .move(edge: .top))) - } } } .frame(maxWidth: .infinity, alignment: .leading) @@ -786,39 +787,45 @@ private struct BrainBarSignalCoveragePanel: View { ViewThatFits(in: .horizontal) { HStack(alignment: .top, spacing: compact ? 8 : 10) { ForEach(signals) { signal in - signalRow(for: signal) + signalColumn(for: signal) } } VStack(spacing: 8) { ForEach(signals) { signal in - signalRow(for: signal) + signalColumn(for: signal) } } } } @ViewBuilder - private func signalRow(for signal: BrainBarSignalCoverage) -> some View { - if signal.showsDetail { - Button { - withAnimation(.easeInOut(duration: 0.18)) { - isVectorDetailExpanded.toggle() + private func signalColumn(for signal: BrainBarSignalCoverage) -> some View { + VStack(alignment: .leading, spacing: compact ? 8 : 10) { + if signal.showsDetail { + Button { + withAnimation(.easeInOut(duration: 0.18)) { + isVectorDetailExpanded.toggle() + } + } label: { + BrainBarSignalCoverageRow( + signal: signal, + compact: compact, + isSelected: isVectorDetailExpanded + ) } - } label: { - BrainBarSignalCoverageRow( - signal: signal, - compact: compact, - isSelected: isVectorDetailExpanded - ) + .buttonStyle(.plain) + .help("Show Vector backlog details") + + if isVectorDetailExpanded { + BrainBarVectorSignalDetail(signal: signal, compact: compact) + .transition(.opacity.combined(with: .move(edge: .top))) + } + } else { + BrainBarSignalCoverageRow(signal: signal, compact: compact, isSelected: false) } - .buttonStyle(.plain) - .help("Show Vector backlog details") - .frame(minWidth: compact ? 150 : 170, maxWidth: .infinity, alignment: .leading) - } else { - BrainBarSignalCoverageRow(signal: signal, compact: compact, isSelected: false) - .frame(minWidth: compact ? 150 : 170, maxWidth: .infinity, alignment: .leading) } + .frame(minWidth: compact ? 150 : 170, maxWidth: .infinity, alignment: .topLeading) } } @@ -830,6 +837,8 @@ private struct BrainBarSignalCoverage: Identifiable { let coveragePercent: Double let accentColor: Color let showsDetail: Bool + let vectorNetDrainRatePerHour: Double? + let vectorBacklogETAHours: Double? var id: String { name } @@ -896,11 +905,6 @@ private struct BrainBarVectorSignalDetail: View { let signal: BrainBarSignalCoverage let compact: Bool - // TODO: Replace these rollout estimates with measured vector drain fields - // once DashboardStats carries vector indexing history. - private let estimatedDrainRatePerHour = 3_300 - private let estimatedETAHours = 13 - var body: some View { Group { if compact { @@ -932,21 +936,21 @@ private struct BrainBarVectorSignalDetail: View { private var metrics: some View { ViewThatFits(in: .horizontal) { HStack(spacing: compact ? 14 : 22) { - BrainBarSignalDetailMetric(label: "drain", value: "~\(formatted(estimatedDrainRatePerHour))/hr", tint: signal.accentColor) - BrainBarSignalDetailMetric(label: "ETA", value: "~\(estimatedETAHours)h", tint: signal.accentColor) + BrainBarSignalDetailMetric(label: "drain", value: drainText, tint: signal.accentColor) + BrainBarSignalDetailMetric(label: "ETA", value: etaText, tint: signal.accentColor) BrainBarSignalDetailMetric(label: "backlog", value: signal.backlogText, tint: Color.brainBarTextPrimary) } VStack(alignment: .leading, spacing: 10) { - BrainBarSignalDetailMetric(label: "drain", value: "~\(formatted(estimatedDrainRatePerHour))/hr", tint: signal.accentColor) - BrainBarSignalDetailMetric(label: "ETA", value: "~\(estimatedETAHours)h", tint: signal.accentColor) + BrainBarSignalDetailMetric(label: "drain", value: drainText, tint: signal.accentColor) + BrainBarSignalDetailMetric(label: "ETA", value: etaText, tint: signal.accentColor) BrainBarSignalDetailMetric(label: "backlog", value: signal.backlogText, tint: Color.brainBarTextPrimary) } } } private var trend: some View { - Label("falling", systemImage: "arrow.down.right") + Label(isFalling ? "falling" : "waiting", systemImage: isFalling ? "arrow.down.right" : "clock") .font(.system(size: 11, weight: .bold)) .foregroundStyle(signal.accentColor) .padding(.vertical, 5) @@ -956,6 +960,27 @@ private struct BrainBarVectorSignalDetail: View { .help("Vector backlog trend") } + private var isFalling: Bool { + (signal.vectorNetDrainRatePerHour ?? 0) > 0 + } + + private var drainText: String { + guard let rate = signal.vectorNetDrainRatePerHour, rate > 0 else { return "n/a" } + return "~\(formatted(Int(rate.rounded())))/hr" + } + + private var etaText: String { + guard let hours = signal.vectorBacklogETAHours, hours.isFinite, hours > 0 else { return "n/a" } + if hours < 1 { + return "~\(max(1, Int((hours * 60).rounded())))m" + } + if hours < 10 { + let rounded = (hours * 10).rounded() / 10 + return rounded == rounded.rounded() ? "~\(Int(rounded))h" : String(format: "~%.1fh", rounded) + } + return "~\(Int(hours.rounded()))h" + } + private func formatted(_ value: Int) -> String { NumberFormatter.localizedString(from: NSNumber(value: value), number: .decimal) } @@ -1010,14 +1035,29 @@ private struct BrainBarFlowLaneCard: View { BrainBarHeroSparkline( label: lane.sparklineLabel, values: lane.values, + secondaryValues: lane.secondaryValues, + primarySeriesLabel: lane.primarySeriesLabel, + secondarySeriesLabel: lane.secondarySeriesLabel, latestBucketName: lane.latestBucketName, accentColor: lane.accentColor, + secondaryAccentColor: lane.secondaryAccentColor, activityWindowMinutes: lane.activityWindowMinutes, fetchedAt: fetchedAt, pulseRevision: pulseRevision ) .frame(height: chartHeight) + if let primarySeriesLabel = lane.primarySeriesLabel, + let secondarySeriesLabel = lane.secondarySeriesLabel, + let secondaryAccentColor = lane.secondaryAccentColor { + BrainBarSeriesLegend( + primaryLabel: primarySeriesLabel, + primaryColor: lane.accentColor, + secondaryLabel: secondarySeriesLabel, + secondaryColor: secondaryAccentColor + ) + } + ViewThatFits(in: .horizontal) { HStack(spacing: 12) { BrainBarLaneMetric(label: "Rate", value: lane.rateText) @@ -1153,6 +1193,47 @@ private struct BrainBarQueueRail: View { } } +private struct BrainBarSeriesLegend: View { + let primaryLabel: String + let primaryColor: NSColor + let secondaryLabel: String + let secondaryColor: NSColor + + var body: some View { + HStack(spacing: 12) { + legendItem(label: primaryLabel, color: primaryColor, dashed: false) + legendItem(label: secondaryLabel, color: secondaryColor, dashed: true) + Spacer(minLength: 0) + } + .accessibilityElement(children: .combine) + .accessibilityLabel("\(primaryLabel) and \(secondaryLabel) write series") + } + + private func legendItem(label: String, color: NSColor, dashed: Bool) -> some View { + HStack(spacing: 6) { + Capsule() + .fill(Color.brainBar(nsColor: color).opacity(dashed ? 0.55 : 0.9)) + .frame(width: dashed ? 14 : 18, height: 3) + .overlay { + if dashed { + HStack(spacing: 2) { + ForEach(0..<3, id: \.self) { _ in + Capsule() + .fill(Color.brainBarGlassSecondary) + .frame(width: 2, height: 3) + } + } + } + } + Text(label) + .font(.system(size: 10, weight: .semibold)) + .foregroundStyle(Color.brainBarTextMuted) + .lineLimit(1) + } + .fixedSize(horizontal: true, vertical: false) + } +} + struct BrainBarDashboardLayout { let chartColumns: Int let overviewMetricColumns: Int @@ -1533,8 +1614,12 @@ private struct BrainBarFlowStatusPill: View { private struct BrainBarHeroSparkline: View { let label: String let values: [Int] + let secondaryValues: [Int] + let primarySeriesLabel: String? + let secondarySeriesLabel: String? let latestBucketName: String let accentColor: NSColor + let secondaryAccentColor: NSColor? let activityWindowMinutes: Int let fetchedAt: Date let pulseRevision: Int @@ -1550,11 +1635,15 @@ private struct BrainBarHeroSparkline: View { presentation: SparklineChartPresentation( label: label, values: values, + secondaryValues: secondaryValues, + primarySeriesLabel: primarySeriesLabel, + secondarySeriesLabel: secondarySeriesLabel, activityWindowMinutes: activityWindowMinutes, latestBucketName: latestBucketName, fetchedAt: fetchedAt ), accentColor: accentColor, + secondaryAccentColor: secondaryAccentColor, compact: SparklineRenderer.isCompact(size: renderSize) ) .id(pulseRevision) diff --git a/brain-bar/Sources/BrainBar/BrainDatabase.swift b/brain-bar/Sources/BrainBar/BrainDatabase.swift index a6ca02e3..a21eed53 100644 --- a/brain-bar/Sources/BrainBar/BrainDatabase.swift +++ b/brain-bar/Sources/BrainBar/BrainDatabase.swift @@ -29,6 +29,8 @@ final class BrainDatabase: @unchecked Sendable { static let maximumTrigramMaintenanceBatchSize = 10_000 private static let defaultPendingStoreMaxLines = 10_000 private static let pendingStoreMaxLinesEnv = "BRAINBAR_PENDING_STORES_MAX_LINES" + private static let agentWriteSourceWhereClause = "COALESCE(LOWER(TRIM(source)), '') IN ('mcp', 'brain_store', 'manual', 'quick-capture')" + private static let watcherWriteSourceWhereClause = "COALESCE(LOWER(TRIM(source)), '') IN ('realtime_watcher', 'realtime', 'claude_code')" private static let lexicalDefenseReplacements: [String: [String]] = [ "hershkovitz": ["Hershkovits"], "hershkovits": ["Hershkovitz"] @@ -95,6 +97,8 @@ final class BrainDatabase: @unchecked Sendable { let enrichmentRatePerMinute: Double let databaseSizeBytes: Int64 let recentActivityBuckets: [Int] + let recentAgentWriteBuckets: [Int] + let recentWatcherWriteBuckets: [Int] let recentEnrichmentBuckets: [Int] let recentWriteFiveMinuteCount: Int let recentEnrichmentFiveMinuteCount: Int @@ -120,6 +124,8 @@ final class BrainDatabase: @unchecked Sendable { enrichmentRatePerMinute: Double, databaseSizeBytes: Int64, recentActivityBuckets: [Int], + recentAgentWriteBuckets: [Int]? = nil, + recentWatcherWriteBuckets: [Int]? = nil, recentEnrichmentBuckets: [Int], recentWriteFiveMinuteCount: Int = 0, recentEnrichmentFiveMinuteCount: Int = 0, @@ -144,6 +150,8 @@ final class BrainDatabase: @unchecked Sendable { self.enrichmentRatePerMinute = enrichmentRatePerMinute self.databaseSizeBytes = databaseSizeBytes self.recentActivityBuckets = recentActivityBuckets + self.recentAgentWriteBuckets = recentAgentWriteBuckets ?? recentActivityBuckets + self.recentWatcherWriteBuckets = recentWatcherWriteBuckets ?? Array(repeating: 0, count: recentActivityBuckets.count) self.recentEnrichmentBuckets = recentEnrichmentBuckets self.recentWriteFiveMinuteCount = recentWriteFiveMinuteCount self.recentEnrichmentFiveMinuteCount = recentEnrichmentFiveMinuteCount @@ -166,6 +174,14 @@ final class BrainDatabase: @unchecked Sendable { recentActivityBuckets.reduce(0, +) } + var recentAgentWriteCount: Int { + recentAgentWriteBuckets.reduce(0, +) + } + + var recentWatcherWriteCount: Int { + recentWatcherWriteBuckets.reduce(0, +) + } + var recentEnrichmentCount: Int { recentEnrichmentBuckets.reduce(0, +) } @@ -175,6 +191,28 @@ final class BrainDatabase: @unchecked Sendable { return Double(recentWriteCount) / Double(activityWindowMinutes) } + var recentWriteRatePerHour: Double { + writeRatePerMinute * 60 + } + + var recentEnrichmentRatePerHour: Double { + guard activityWindowMinutes > 0 else { return 0 } + return (Double(recentEnrichmentCount) / Double(activityWindowMinutes)) * 60 + } + + var vectorNetDrainRatePerHour: Double { + // Vector rows do not currently carry per-vector timestamps. Until + // they do, use the real dashboard window: recent enrichment + // completions minus recent writes, which is the net backlog drain + // signal available in DashboardStats. + max(recentEnrichmentRatePerHour - recentWriteRatePerHour, 0) + } + + var vectorBacklogETAHours: Double? { + guard vectorBacklogCount > 0, vectorNetDrainRatePerHour > 0 else { return nil } + return Double(vectorBacklogCount) / vectorNetDrainRatePerHour + } + func eventIsLive(_ date: Date?, now: Date = Date()) -> Bool { guard let date else { return false } let liveWindowSeconds = max(Double(liveWindowMinutes) * 60, 0) @@ -227,6 +265,8 @@ final class BrainDatabase: @unchecked Sendable { enrichmentRatePerMinute: enrichmentRatePerMinute, databaseSizeBytes: databaseSizeBytes, recentActivityBuckets: recentActivityBuckets, + recentAgentWriteBuckets: recentAgentWriteBuckets, + recentWatcherWriteBuckets: recentWatcherWriteBuckets, recentEnrichmentBuckets: recentEnrichmentBuckets, recentWriteFiveMinuteCount: recentWriteFiveMinuteCount, recentEnrichmentFiveMinuteCount: recentEnrichmentFiveMinuteCount, @@ -1645,6 +1685,18 @@ final class BrainDatabase: @unchecked Sendable { bucketCount: bucketCount, now: now ) + let recentAgentWriteBuckets = try recentWriteBuckets( + whereClause: Self.agentWriteSourceWhereClause, + activityWindowMinutes: activityWindowMinutes, + bucketCount: bucketCount, + now: now + ) + let recentWatcherWriteBuckets = try recentWriteBuckets( + whereClause: Self.watcherWriteSourceWhereClause, + activityWindowMinutes: activityWindowMinutes, + bucketCount: bucketCount, + now: now + ) let recentEnrichmentBuckets = try recentEnrichmentBuckets( activityWindowMinutes: activityWindowMinutes, bucketCount: bucketCount, @@ -1661,6 +1713,8 @@ final class BrainDatabase: @unchecked Sendable { enrichmentRatePerMinute: enrichmentRatePerMinute, databaseSizeBytes: databaseSizeBytes(), recentActivityBuckets: recentActivityBuckets, + recentAgentWriteBuckets: recentAgentWriteBuckets, + recentWatcherWriteBuckets: recentWatcherWriteBuckets, recentEnrichmentBuckets: recentEnrichmentBuckets, recentWriteFiveMinuteCount: recentWriteFiveMinuteCount, recentEnrichmentFiveMinuteCount: recentEnrichmentFiveMinuteCount, @@ -1849,6 +1903,23 @@ final class BrainDatabase: @unchecked Sendable { ) } + private func recentWriteBuckets( + whereClause: String, + activityWindowMinutes: Int, + bucketCount: Int, + now: Date + ) throws -> [Int] { + guard activityWindowMinutes > 0 else { return Array(repeating: 0, count: bucketCount) } + + return try indexedTimestampBuckets( + column: "created_at", + whereClause: whereClause, + activityWindowMinutes: activityWindowMinutes, + bucketCount: bucketCount, + now: now + ) + } + private func recentEnrichmentBuckets(activityWindowMinutes: Int, bucketCount: Int, now: Date) throws -> [Int] { guard activityWindowMinutes > 0 else { return Array(repeating: 0, count: bucketCount) } diff --git a/brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift b/brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift index ada42f28..ab932ca3 100644 --- a/brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift +++ b/brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift @@ -194,6 +194,10 @@ struct DashboardFlowLane: Sendable, Equatable { let sparklineLabel: String let latestBucketName: String let accentColor: NSColor + let primarySeriesLabel: String? + let secondaryValues: [Int] + let secondarySeriesLabel: String? + let secondaryAccentColor: NSColor? } struct DashboardQueueSummary: Sendable, Equatable { @@ -322,10 +326,14 @@ struct DashboardFlowSummary: Sendable, Equatable { activityWindowMinutes: stats.activityWindowMinutes, now: now ), - values: stats.recentActivityBuckets, + values: stats.recentAgentWriteBuckets, sparklineLabel: "Writes over \(windowLabel)", latestBucketName: "latest write bucket", - accentColor: ingressColor + accentColor: ingressColor, + primarySeriesLabel: "Agent", + secondaryValues: stats.recentWatcherWriteBuckets, + secondarySeriesLabel: "Watcher", + secondaryAccentColor: BrainBarStateTheme.active.theme.color ), queue: DashboardQueueSummary( status: queueStatus, @@ -378,7 +386,11 @@ struct DashboardFlowSummary: Sendable, Equatable { values: stats.recentEnrichmentBuckets, sparklineLabel: "Enrichment completions over \(windowLabel)", latestBucketName: "latest enrichment bucket", - accentColor: enrichmentColor + accentColor: enrichmentColor, + primarySeriesLabel: nil, + secondaryValues: [], + secondarySeriesLabel: nil, + secondaryAccentColor: nil ) ) } diff --git a/brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift b/brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift index b32ff71d..93e8d763 100644 --- a/brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift +++ b/brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift @@ -14,6 +14,9 @@ struct SparklineChartPoint: Identifiable, Equatable, Sendable { struct SparklineChartPresentation: Equatable, Sendable { let label: String let values: [Int] + let secondaryValues: [Int] + let primarySeriesLabel: String? + let secondarySeriesLabel: String? let activityWindowMinutes: Int let latestBucketName: String let fetchedAt: Date @@ -21,12 +24,18 @@ struct SparklineChartPresentation: Equatable, Sendable { init( label: String, values: [Int], + secondaryValues: [Int] = [], + primarySeriesLabel: String? = nil, + secondarySeriesLabel: String? = nil, activityWindowMinutes: Int = 30, latestBucketName: String = "latest bucket count", fetchedAt: Date = Date() ) { self.label = label self.values = values + self.secondaryValues = secondaryValues + self.primarySeriesLabel = primarySeriesLabel + self.secondarySeriesLabel = secondarySeriesLabel self.activityWindowMinutes = activityWindowMinutes self.latestBucketName = latestBucketName self.fetchedAt = fetchedAt @@ -42,16 +51,30 @@ struct SparklineChartPresentation: Equatable, Sendable { points.last } + var secondaryPoints: [SparklineChartPoint] { + secondaryValues.enumerated().map { index, value in + SparklineChartPoint(bucket: index, value: value, timestamp: bucketMidpoint(for: index)) + } + } + + var hasSecondarySeries: Bool { + !secondaryValues.isEmpty + } + var accessibilityLabel: String { label } var accessibilityValue: String { - "\(latestBucketName) \(values.last ?? 0), \(trendDescription)" + var components = ["\(latestBucketName) \(values.last ?? 0)", trendDescription] + if let secondarySeriesLabel, hasSecondarySeries { + components.append("\(secondarySeriesLabel) latest bucket \(secondaryValues.last ?? 0)") + } + return components.joined(separator: ", ") } var maxValue: Int { - max(values.max() ?? 0, 1) + max(values.max() ?? 0, secondaryValues.max() ?? 0, 1) } var xAxisDomainStart: Date { @@ -140,6 +163,7 @@ struct SparklineChartPresentation: Equatable, Sendable { struct SparklineChart: View { let presentation: SparklineChartPresentation let accentColor: NSColor + let secondaryAccentColor: NSColor? let compact: Bool @State private var hoveredBucket: Int? @State private var hoverLocation: CGPoint? @@ -147,39 +171,55 @@ struct SparklineChart: View { init( presentation: SparklineChartPresentation, accentColor: NSColor, + secondaryAccentColor: NSColor? = nil, compact: Bool = false ) { self.presentation = presentation self.accentColor = accentColor + self.secondaryAccentColor = secondaryAccentColor self.compact = compact } var body: some View { VStack(spacing: 2) { - Chart(presentation.points) { point in - if !compact { - AreaMark( + Chart { + ForEach(presentation.points) { point in + if !compact { + AreaMark( + x: .value("Time", point.timestamp), + y: .value("Count", point.value) + ) + .foregroundStyle(Color.brainBar(nsColor: accentColor).opacity(0.10)) + } + + LineMark( x: .value("Time", point.timestamp), y: .value("Count", point.value) ) - .foregroundStyle(Color.brainBar(nsColor: accentColor).opacity(0.10)) + .interpolationMethod(.linear) + .foregroundStyle(Color.brainBar(nsColor: accentColor).opacity(0.85)) + .lineStyle(StrokeStyle(lineWidth: compact ? 1.6 : 2, lineCap: .round, lineJoin: .round)) + + if point == presentation.latestPoint { + PointMark( + x: .value("Time", point.timestamp), + y: .value("Count", point.value) + ) + .foregroundStyle(Color.brainBar(nsColor: accentColor)) + .symbolSize(compact ? 18 : 42) + } } - LineMark( - x: .value("Time", point.timestamp), - y: .value("Count", point.value) - ) - .interpolationMethod(.linear) - .foregroundStyle(Color.brainBar(nsColor: accentColor).opacity(0.85)) - .lineStyle(StrokeStyle(lineWidth: compact ? 1.6 : 2, lineCap: .round, lineJoin: .round)) - - if point == presentation.latestPoint { - PointMark( - x: .value("Time", point.timestamp), - y: .value("Count", point.value) - ) - .foregroundStyle(Color.brainBar(nsColor: accentColor)) - .symbolSize(compact ? 18 : 42) + if presentation.hasSecondarySeries { + ForEach(presentation.secondaryPoints) { point in + LineMark( + x: .value("Time", point.timestamp), + y: .value("Count", point.value) + ) + .interpolationMethod(.linear) + .foregroundStyle(Color.brainBar(nsColor: secondaryAccentColor ?? .systemPurple).opacity(0.88)) + .lineStyle(StrokeStyle(lineWidth: compact ? 1.4 : 2, lineCap: .round, lineJoin: .round, dash: compact ? [] : [4, 3])) + } } } .chartXAxis(.hidden) diff --git a/brain-bar/Tests/BrainBarTests/DashboardTests.swift b/brain-bar/Tests/BrainBarTests/DashboardTests.swift index 3f375a5d..a1c16e1c 100644 --- a/brain-bar/Tests/BrainBarTests/DashboardTests.swift +++ b/brain-bar/Tests/BrainBarTests/DashboardTests.swift @@ -242,6 +242,56 @@ final class DashboardTests: XCTestCase { XCTAssertEqual(stats.recentEnrichmentCount, 4) } + func testDashboardStatsSplitsAgentAndWatcherWriteBuckets() throws { + let fixtures: [(id: String, source: String, offset: TimeInterval)] = [ + ("agent-27m", "mcp", -27 * 60), + ("agent-4m", "mcp", -4 * 60), + ("watcher-52m", "realtime_watcher", -52 * 60), + ("watcher-27m", "realtime_watcher", -27 * 60), + ("watcher-4m", "realtime", -4 * 60), + ] + for fixture in fixtures { + _ = try db.store( + content: "Write source split fixture \(fixture.id)", + tags: ["dashboard"], + importance: 5, + source: fixture.source, + chunkID: fixture.id + ) + db.exec(""" + UPDATE chunks + SET created_at = datetime('now', '\(Int(fixture.offset)) seconds') + WHERE id = '\(fixture.id)' + """) + } + + let stats = try db.dashboardStats(activityWindowMinutes: 60, bucketCount: 12) + + XCTAssertEqual(stats.recentAgentWriteBuckets, [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]) + XCTAssertEqual(stats.recentWatcherWriteBuckets, [0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]) + XCTAssertEqual(stats.recentActivityBuckets, [0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2]) + } + + func testDashboardStatsComputesVectorETAFromBacklogAndNetDrain() { + let stats = DashboardStats( + chunkCount: 10_105, + enrichedChunkCount: 0, + pendingEnrichmentCount: 0, + enrichmentPercent: 0, + enrichmentRatePerMinute: 0, + databaseSizeBytes: 4_096, + recentActivityBuckets: [10], + recentEnrichmentBuckets: [65], + activityWindowMinutes: 1, + bucketCount: 1, + signalEligibleChunkCount: 10_105, + vectorIndexedChunkCount: 0 + ) + + XCTAssertEqual(stats.vectorNetDrainRatePerHour, 3_300, accuracy: 0.001) + XCTAssertEqual(try XCTUnwrap(stats.vectorBacklogETAHours), 10_105.0 / 3_300.0, accuracy: 0.001) + } + func testDashboardStatsSamplesPendingStoreQueueBeforeReadTransaction() throws { let source = try brainBarSourceFile("Sources/BrainBar/BrainDatabase.swift") let methodRange = try XCTUnwrap(source.range(of: "func dashboardStats(")) From ebb296189c1b38f4373015cf207006f26fb1416c Mon Sep 17 00:00:00 2001 From: Etan Joseph Heyman Date: Fri, 19 Jun 2026 19:50:33 +0300 Subject: [PATCH 3/5] Complete BrainBar writes graph series --- .../BrainBar/BrainBarWindowRootView.swift | 102 ++++++++++++---- .../Sources/BrainBar/BrainDatabase.swift | 20 ++- .../BrainBar/Dashboard/PipelineState.swift | 23 +++- .../Dashboard/SparklineRenderer.swift | 114 ++++++++++++++++-- .../Tests/BrainBarTests/DashboardTests.swift | 36 +++++- 5 files changed, 254 insertions(+), 41 deletions(-) diff --git a/brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift b/brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift index 04baafb0..a62055b5 100644 --- a/brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift +++ b/brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift @@ -1038,9 +1038,12 @@ private struct BrainBarFlowLaneCard: View { secondaryValues: lane.secondaryValues, primarySeriesLabel: lane.primarySeriesLabel, secondarySeriesLabel: lane.secondarySeriesLabel, + tertiaryValues: lane.tertiaryValues, + tertiarySeriesLabel: lane.tertiarySeriesLabel, latestBucketName: lane.latestBucketName, accentColor: lane.accentColor, secondaryAccentColor: lane.secondaryAccentColor, + tertiaryAccentColor: lane.tertiaryAccentColor, activityWindowMinutes: lane.activityWindowMinutes, fetchedAt: fetchedAt, pulseRevision: pulseRevision @@ -1054,7 +1057,9 @@ private struct BrainBarFlowLaneCard: View { primaryLabel: primarySeriesLabel, primaryColor: lane.accentColor, secondaryLabel: secondarySeriesLabel, - secondaryColor: secondaryAccentColor + secondaryColor: secondaryAccentColor, + tertiaryLabel: lane.tertiarySeriesLabel, + tertiaryColor: lane.tertiaryAccentColor ) } @@ -1198,33 +1203,43 @@ private struct BrainBarSeriesLegend: View { let primaryColor: NSColor let secondaryLabel: String let secondaryColor: NSColor + let tertiaryLabel: String? + let tertiaryColor: NSColor? var body: some View { - HStack(spacing: 12) { - legendItem(label: primaryLabel, color: primaryColor, dashed: false) - legendItem(label: secondaryLabel, color: secondaryColor, dashed: true) - Spacer(minLength: 0) + ViewThatFits(in: .horizontal) { + HStack(spacing: 12) { + legendItems + Spacer(minLength: 0) + } + VStack(alignment: .leading, spacing: 6) { + legendItems + } } .accessibilityElement(children: .combine) - .accessibilityLabel("\(primaryLabel) and \(secondaryLabel) write series") + .accessibilityLabel(accessibilityLabel) } - private func legendItem(label: String, color: NSColor, dashed: Bool) -> some View { + @ViewBuilder + private var legendItems: some View { + legendItem(label: primaryLabel, color: primaryColor, style: .solid) + legendItem(label: secondaryLabel, color: secondaryColor, style: .dash) + if let tertiaryLabel, let tertiaryColor { + legendItem(label: tertiaryLabel, color: tertiaryColor, style: .dotDash) + } + } + + private var accessibilityLabel: String { + var labels = [primaryLabel, secondaryLabel] + if let tertiaryLabel { + labels.append(tertiaryLabel) + } + return "\(labels.joined(separator: ", ")) write series" + } + + private func legendItem(label: String, color: NSColor, style: LegendSwatchStyle) -> some View { HStack(spacing: 6) { - Capsule() - .fill(Color.brainBar(nsColor: color).opacity(dashed ? 0.55 : 0.9)) - .frame(width: dashed ? 14 : 18, height: 3) - .overlay { - if dashed { - HStack(spacing: 2) { - ForEach(0..<3, id: \.self) { _ in - Capsule() - .fill(Color.brainBarGlassSecondary) - .frame(width: 2, height: 3) - } - } - } - } + LegendSwatch(color: color, style: style) Text(label) .font(.system(size: 10, weight: .semibold)) .foregroundStyle(Color.brainBarTextMuted) @@ -1232,6 +1247,45 @@ private struct BrainBarSeriesLegend: View { } .fixedSize(horizontal: true, vertical: false) } + + private enum LegendSwatchStyle { + case solid + case dash + case dotDash + } + + private struct LegendSwatch: View { + let color: NSColor + let style: LegendSwatchStyle + + var body: some View { + HStack(spacing: 2) { + switch style { + case .solid: + Capsule() + .fill(Color.brainBar(nsColor: color)) + .frame(width: 18, height: 3) + case .dash: + ForEach(0..<3, id: \.self) { _ in + Capsule() + .fill(Color.brainBar(nsColor: color)) + .frame(width: 4, height: 3) + } + case .dotDash: + Capsule() + .fill(Color.brainBar(nsColor: color)) + .frame(width: 8, height: 3) + Capsule() + .fill(Color.brainBar(nsColor: color)) + .frame(width: 3, height: 3) + Capsule() + .fill(Color.brainBar(nsColor: color)) + .frame(width: 3, height: 3) + } + } + .frame(width: 18, height: 6, alignment: .leading) + } + } } struct BrainBarDashboardLayout { @@ -1617,9 +1671,12 @@ private struct BrainBarHeroSparkline: View { let secondaryValues: [Int] let primarySeriesLabel: String? let secondarySeriesLabel: String? + let tertiaryValues: [Int] + let tertiarySeriesLabel: String? let latestBucketName: String let accentColor: NSColor let secondaryAccentColor: NSColor? + let tertiaryAccentColor: NSColor? let activityWindowMinutes: Int let fetchedAt: Date let pulseRevision: Int @@ -1636,14 +1693,17 @@ private struct BrainBarHeroSparkline: View { label: label, values: values, secondaryValues: secondaryValues, + tertiaryValues: tertiaryValues, primarySeriesLabel: primarySeriesLabel, secondarySeriesLabel: secondarySeriesLabel, + tertiarySeriesLabel: tertiarySeriesLabel, activityWindowMinutes: activityWindowMinutes, latestBucketName: latestBucketName, fetchedAt: fetchedAt ), accentColor: accentColor, secondaryAccentColor: secondaryAccentColor, + tertiaryAccentColor: tertiaryAccentColor, compact: SparklineRenderer.isCompact(size: renderSize) ) .id(pulseRevision) diff --git a/brain-bar/Sources/BrainBar/BrainDatabase.swift b/brain-bar/Sources/BrainBar/BrainDatabase.swift index a21eed53..f5f20564 100644 --- a/brain-bar/Sources/BrainBar/BrainDatabase.swift +++ b/brain-bar/Sources/BrainBar/BrainDatabase.swift @@ -29,8 +29,9 @@ final class BrainDatabase: @unchecked Sendable { static let maximumTrigramMaintenanceBatchSize = 10_000 private static let defaultPendingStoreMaxLines = 10_000 private static let pendingStoreMaxLinesEnv = "BRAINBAR_PENDING_STORES_MAX_LINES" - private static let agentWriteSourceWhereClause = "COALESCE(LOWER(TRIM(source)), '') IN ('mcp', 'brain_store', 'manual', 'quick-capture')" - private static let watcherWriteSourceWhereClause = "COALESCE(LOWER(TRIM(source)), '') IN ('realtime_watcher', 'realtime', 'claude_code')" + private static let agentWriteSourceWhereClause = "COALESCE(LOWER(TRIM(source)), '') IN ('mcp', 'manual', 'precompact-hook')" + private static let watcherWriteSourceWhereClause = "COALESCE(LOWER(TRIM(source)), '') IN ('realtime_watcher', 'realtime')" + private static let digestWriteSourceWhereClause = "COALESCE(LOWER(TRIM(source)), '') = 'digest'" private static let lexicalDefenseReplacements: [String: [String]] = [ "hershkovitz": ["Hershkovits"], "hershkovits": ["Hershkovitz"] @@ -99,6 +100,7 @@ final class BrainDatabase: @unchecked Sendable { let recentActivityBuckets: [Int] let recentAgentWriteBuckets: [Int] let recentWatcherWriteBuckets: [Int] + let recentDigestWriteBuckets: [Int] let recentEnrichmentBuckets: [Int] let recentWriteFiveMinuteCount: Int let recentEnrichmentFiveMinuteCount: Int @@ -126,6 +128,7 @@ final class BrainDatabase: @unchecked Sendable { recentActivityBuckets: [Int], recentAgentWriteBuckets: [Int]? = nil, recentWatcherWriteBuckets: [Int]? = nil, + recentDigestWriteBuckets: [Int]? = nil, recentEnrichmentBuckets: [Int], recentWriteFiveMinuteCount: Int = 0, recentEnrichmentFiveMinuteCount: Int = 0, @@ -152,6 +155,7 @@ final class BrainDatabase: @unchecked Sendable { self.recentActivityBuckets = recentActivityBuckets self.recentAgentWriteBuckets = recentAgentWriteBuckets ?? recentActivityBuckets self.recentWatcherWriteBuckets = recentWatcherWriteBuckets ?? Array(repeating: 0, count: recentActivityBuckets.count) + self.recentDigestWriteBuckets = recentDigestWriteBuckets ?? Array(repeating: 0, count: recentActivityBuckets.count) self.recentEnrichmentBuckets = recentEnrichmentBuckets self.recentWriteFiveMinuteCount = recentWriteFiveMinuteCount self.recentEnrichmentFiveMinuteCount = recentEnrichmentFiveMinuteCount @@ -182,6 +186,10 @@ final class BrainDatabase: @unchecked Sendable { recentWatcherWriteBuckets.reduce(0, +) } + var recentDigestWriteCount: Int { + recentDigestWriteBuckets.reduce(0, +) + } + var recentEnrichmentCount: Int { recentEnrichmentBuckets.reduce(0, +) } @@ -267,6 +275,7 @@ final class BrainDatabase: @unchecked Sendable { recentActivityBuckets: recentActivityBuckets, recentAgentWriteBuckets: recentAgentWriteBuckets, recentWatcherWriteBuckets: recentWatcherWriteBuckets, + recentDigestWriteBuckets: recentDigestWriteBuckets, recentEnrichmentBuckets: recentEnrichmentBuckets, recentWriteFiveMinuteCount: recentWriteFiveMinuteCount, recentEnrichmentFiveMinuteCount: recentEnrichmentFiveMinuteCount, @@ -1697,6 +1706,12 @@ final class BrainDatabase: @unchecked Sendable { bucketCount: bucketCount, now: now ) + let recentDigestWriteBuckets = try recentWriteBuckets( + whereClause: Self.digestWriteSourceWhereClause, + activityWindowMinutes: activityWindowMinutes, + bucketCount: bucketCount, + now: now + ) let recentEnrichmentBuckets = try recentEnrichmentBuckets( activityWindowMinutes: activityWindowMinutes, bucketCount: bucketCount, @@ -1715,6 +1730,7 @@ final class BrainDatabase: @unchecked Sendable { recentActivityBuckets: recentActivityBuckets, recentAgentWriteBuckets: recentAgentWriteBuckets, recentWatcherWriteBuckets: recentWatcherWriteBuckets, + recentDigestWriteBuckets: recentDigestWriteBuckets, recentEnrichmentBuckets: recentEnrichmentBuckets, recentWriteFiveMinuteCount: recentWriteFiveMinuteCount, recentEnrichmentFiveMinuteCount: recentEnrichmentFiveMinuteCount, diff --git a/brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift b/brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift index ab932ca3..3fd06f76 100644 --- a/brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift +++ b/brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift @@ -198,6 +198,9 @@ struct DashboardFlowLane: Sendable, Equatable { let secondaryValues: [Int] let secondarySeriesLabel: String? let secondaryAccentColor: NSColor? + let tertiaryValues: [Int] + let tertiarySeriesLabel: String? + let tertiaryAccentColor: NSColor? } struct DashboardQueueSummary: Sendable, Equatable { @@ -229,7 +232,9 @@ struct DashboardFlowSummary: Sendable, Equatable { static func derive(daemon: DaemonHealthSnapshot?, stats: DashboardStats, now: Date = Date()) -> DashboardFlowSummary { let windowLabel = DashboardMetricFormatter.windowLabel(minutes: stats.activityWindowMinutes) - let ingressColor = BrainBarStateTheme.loading.theme.color + let agentStoresColor = NSColor.systemGreen + let jsonlWatcherColor = NSColor.systemOrange + let digestColor = NSColor.systemPurple let enrichmentColor = BrainBarStateTheme.active.theme.color let writesLive = stats.eventIsLive(stats.lastWriteAt, now: now) @@ -329,11 +334,14 @@ struct DashboardFlowSummary: Sendable, Equatable { values: stats.recentAgentWriteBuckets, sparklineLabel: "Writes over \(windowLabel)", latestBucketName: "latest write bucket", - accentColor: ingressColor, - primarySeriesLabel: "Agent", + accentColor: agentStoresColor, + primarySeriesLabel: "Agent stores", secondaryValues: stats.recentWatcherWriteBuckets, - secondarySeriesLabel: "Watcher", - secondaryAccentColor: BrainBarStateTheme.active.theme.color + secondarySeriesLabel: "JSONL watcher", + secondaryAccentColor: jsonlWatcherColor, + tertiaryValues: stats.recentDigestWriteBuckets, + tertiarySeriesLabel: "Digest", + tertiaryAccentColor: digestColor ), queue: DashboardQueueSummary( status: queueStatus, @@ -390,7 +398,10 @@ struct DashboardFlowSummary: Sendable, Equatable { primarySeriesLabel: nil, secondaryValues: [], secondarySeriesLabel: nil, - secondaryAccentColor: nil + secondaryAccentColor: nil, + tertiaryValues: [], + tertiarySeriesLabel: nil, + tertiaryAccentColor: nil ) ) } diff --git a/brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift b/brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift index 93e8d763..8d854eeb 100644 --- a/brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift +++ b/brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift @@ -15,8 +15,10 @@ struct SparklineChartPresentation: Equatable, Sendable { let label: String let values: [Int] let secondaryValues: [Int] + let tertiaryValues: [Int] let primarySeriesLabel: String? let secondarySeriesLabel: String? + let tertiarySeriesLabel: String? let activityWindowMinutes: Int let latestBucketName: String let fetchedAt: Date @@ -25,8 +27,10 @@ struct SparklineChartPresentation: Equatable, Sendable { label: String, values: [Int], secondaryValues: [Int] = [], + tertiaryValues: [Int] = [], primarySeriesLabel: String? = nil, secondarySeriesLabel: String? = nil, + tertiarySeriesLabel: String? = nil, activityWindowMinutes: Int = 30, latestBucketName: String = "latest bucket count", fetchedAt: Date = Date() @@ -34,8 +38,10 @@ struct SparklineChartPresentation: Equatable, Sendable { self.label = label self.values = values self.secondaryValues = secondaryValues + self.tertiaryValues = tertiaryValues self.primarySeriesLabel = primarySeriesLabel self.secondarySeriesLabel = secondarySeriesLabel + self.tertiarySeriesLabel = tertiarySeriesLabel self.activityWindowMinutes = activityWindowMinutes self.latestBucketName = latestBucketName self.fetchedAt = fetchedAt @@ -57,24 +63,56 @@ struct SparklineChartPresentation: Equatable, Sendable { } } + var tertiaryPoints: [SparklineChartPoint] { + tertiaryValues.enumerated().map { index, value in + SparklineChartPoint(bucket: index, value: value, timestamp: bucketMidpoint(for: index)) + } + } + var hasSecondarySeries: Bool { !secondaryValues.isEmpty } + var hasTertiarySeries: Bool { + !tertiaryValues.isEmpty + } + + var hasMultipleSeries: Bool { + hasSecondarySeries || hasTertiarySeries + } + + var primaryLegendLabel: String { + primarySeriesLabel ?? "Primary" + } + + var secondaryLegendLabel: String { + secondarySeriesLabel ?? "Secondary" + } + + var tertiaryLegendLabel: String { + tertiarySeriesLabel ?? "Tertiary" + } + var accessibilityLabel: String { label } var accessibilityValue: String { var components = ["\(latestBucketName) \(values.last ?? 0)", trendDescription] + if let primarySeriesLabel { + components.append("\(primarySeriesLabel) latest bucket \(values.last ?? 0)") + } if let secondarySeriesLabel, hasSecondarySeries { components.append("\(secondarySeriesLabel) latest bucket \(secondaryValues.last ?? 0)") } + if let tertiarySeriesLabel, hasTertiarySeries { + components.append("\(tertiarySeriesLabel) latest bucket \(tertiaryValues.last ?? 0)") + } return components.joined(separator: ", ") } var maxValue: Int { - max(values.max() ?? 0, secondaryValues.max() ?? 0, 1) + max(values.max() ?? 0, secondaryValues.max() ?? 0, tertiaryValues.max() ?? 0, 1) } var xAxisDomainStart: Date { @@ -110,8 +148,21 @@ struct SparklineChartPresentation: Equatable, Sendable { func tooltipText(forBucket bucket: Int) -> String { let clampedBucket = min(max(bucket, 0), max(values.count - 1, 0)) - let value = values.indices.contains(clampedBucket) ? values[clampedBucket] : 0 - return "\(bucketLabel(for: clampedBucket)) (\(relativeBucketLabel(for: clampedBucket))): \(value)" + let primaryValue = values.indices.contains(clampedBucket) ? values[clampedBucket] : 0 + guard hasMultipleSeries else { + return "\(bucketLabel(for: clampedBucket)) (\(relativeBucketLabel(for: clampedBucket))): \(primaryValue)" + } + + var seriesComponents = ["\(primarySeriesLabel ?? "Primary") \(primaryValue)"] + if let secondarySeriesLabel, hasSecondarySeries { + let value = secondaryValues.indices.contains(clampedBucket) ? secondaryValues[clampedBucket] : 0 + seriesComponents.append("\(secondarySeriesLabel) \(value)") + } + if let tertiarySeriesLabel, hasTertiarySeries { + let value = tertiaryValues.indices.contains(clampedBucket) ? tertiaryValues[clampedBucket] : 0 + seriesComponents.append("\(tertiarySeriesLabel) \(value)") + } + return "\(bucketLabel(for: clampedBucket)) (\(relativeBucketLabel(for: clampedBucket))): \(seriesComponents.joined(separator: ", "))" } private func bucketRange(for bucket: Int) -> (start: Date, end: Date) { @@ -164,6 +215,7 @@ struct SparklineChart: View { let presentation: SparklineChartPresentation let accentColor: NSColor let secondaryAccentColor: NSColor? + let tertiaryAccentColor: NSColor? let compact: Bool @State private var hoveredBucket: Int? @State private var hoverLocation: CGPoint? @@ -172,11 +224,13 @@ struct SparklineChart: View { presentation: SparklineChartPresentation, accentColor: NSColor, secondaryAccentColor: NSColor? = nil, + tertiaryAccentColor: NSColor? = nil, compact: Bool = false ) { self.presentation = presentation self.accentColor = accentColor self.secondaryAccentColor = secondaryAccentColor + self.tertiaryAccentColor = tertiaryAccentColor self.compact = compact } @@ -184,7 +238,7 @@ struct SparklineChart: View { VStack(spacing: 2) { Chart { ForEach(presentation.points) { point in - if !compact { + if !compact && !presentation.hasMultipleSeries { AreaMark( x: .value("Time", point.timestamp), y: .value("Count", point.value) @@ -194,10 +248,11 @@ struct SparklineChart: View { LineMark( x: .value("Time", point.timestamp), - y: .value("Count", point.value) + y: .value("Count", point.value), + series: .value("Series", presentation.primaryLegendLabel) ) .interpolationMethod(.linear) - .foregroundStyle(Color.brainBar(nsColor: accentColor).opacity(0.85)) + .foregroundStyle(by: .value("Series", presentation.primaryLegendLabel)) .lineStyle(StrokeStyle(lineWidth: compact ? 1.6 : 2, lineCap: .round, lineJoin: .round)) if point == presentation.latestPoint { @@ -214,16 +269,32 @@ struct SparklineChart: View { ForEach(presentation.secondaryPoints) { point in LineMark( x: .value("Time", point.timestamp), - y: .value("Count", point.value) + y: .value("Count", point.value), + series: .value("Series", presentation.secondaryLegendLabel) ) .interpolationMethod(.linear) - .foregroundStyle(Color.brainBar(nsColor: secondaryAccentColor ?? .systemPurple).opacity(0.88)) + .foregroundStyle(by: .value("Series", presentation.secondaryLegendLabel)) .lineStyle(StrokeStyle(lineWidth: compact ? 1.4 : 2, lineCap: .round, lineJoin: .round, dash: compact ? [] : [4, 3])) } } + + if presentation.hasTertiarySeries { + ForEach(presentation.tertiaryPoints) { point in + LineMark( + x: .value("Time", point.timestamp), + y: .value("Count", point.value), + series: .value("Series", presentation.tertiaryLegendLabel) + ) + .interpolationMethod(.linear) + .foregroundStyle(by: .value("Series", presentation.tertiaryLegendLabel)) + .lineStyle(StrokeStyle(lineWidth: compact ? 1.4 : 2, lineCap: .round, lineJoin: .round, dash: compact ? [] : [2, 3])) + } + } } .chartXAxis(.hidden) .chartYAxis(.hidden) + .chartLegend(.hidden) + .chartForegroundStyleScale(chartForegroundStyleScale) .chartXScale(domain: presentation.xAxisDomainStart...presentation.xAxisDomainEnd) .chartYScale(domain: 0...presentation.maxValue) .chartPlotStyle { plotArea in @@ -323,6 +394,33 @@ struct SparklineChart: View { return Array(Set([0, last / 2, last])).sorted() } + private var chartForegroundStyleScale: KeyValuePairs { + let primaryColor = Color.brainBar(nsColor: accentColor) + let secondaryColor = Color.brainBar(nsColor: secondaryAccentColor ?? .systemOrange) + let tertiaryColor = Color.brainBar(nsColor: tertiaryAccentColor ?? .systemPurple) + + if presentation.hasSecondarySeries && presentation.hasTertiarySeries { + return [ + presentation.primaryLegendLabel: primaryColor, + presentation.secondaryLegendLabel: secondaryColor, + presentation.tertiaryLegendLabel: tertiaryColor, + ] + } + if presentation.hasSecondarySeries { + return [ + presentation.primaryLegendLabel: primaryColor, + presentation.secondaryLegendLabel: secondaryColor, + ] + } + if presentation.hasTertiarySeries { + return [ + presentation.primaryLegendLabel: primaryColor, + presentation.tertiaryLegendLabel: tertiaryColor, + ] + } + return [presentation.primaryLegendLabel: primaryColor] + } + @ViewBuilder private func sparklineTooltip(forBucket bucket: Int) -> some View { Text(presentation.tooltipText(forBucket: bucket)) diff --git a/brain-bar/Tests/BrainBarTests/DashboardTests.swift b/brain-bar/Tests/BrainBarTests/DashboardTests.swift index a1c16e1c..9a8c8aa5 100644 --- a/brain-bar/Tests/BrainBarTests/DashboardTests.swift +++ b/brain-bar/Tests/BrainBarTests/DashboardTests.swift @@ -242,13 +242,17 @@ final class DashboardTests: XCTestCase { XCTAssertEqual(stats.recentEnrichmentCount, 4) } - func testDashboardStatsSplitsAgentAndWatcherWriteBuckets() throws { + func testDashboardStatsSplitsAgentWatcherAndDigestWriteBuckets() throws { let fixtures: [(id: String, source: String, offset: TimeInterval)] = [ ("agent-27m", "mcp", -27 * 60), - ("agent-4m", "mcp", -4 * 60), + ("agent-manual-4m", "manual", -4 * 60), + ("agent-precompact-4m", "precompact-hook", -4 * 60), + ("quick-capture-4m", "quick-capture", -4 * 60), ("watcher-52m", "realtime_watcher", -52 * 60), ("watcher-27m", "realtime_watcher", -27 * 60), ("watcher-4m", "realtime", -4 * 60), + ("claude-code-4m", "claude_code", -4 * 60), + ("digest-4m", "digest", -4 * 60), ] for fixture in fixtures { _ = try db.store( @@ -267,9 +271,33 @@ final class DashboardTests: XCTestCase { let stats = try db.dashboardStats(activityWindowMinutes: 60, bucketCount: 12) - XCTAssertEqual(stats.recentAgentWriteBuckets, [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]) + XCTAssertEqual(stats.recentAgentWriteBuckets, [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2]) XCTAssertEqual(stats.recentWatcherWriteBuckets, [0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]) - XCTAssertEqual(stats.recentActivityBuckets, [0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2]) + XCTAssertEqual(stats.recentDigestWriteBuckets, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]) + XCTAssertEqual(stats.recentActivityBuckets, [0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 6]) + } + + func testDashboardFlowLabelsWriteSeriesBySourcePath() { + let stats = DashboardStats( + chunkCount: 9, + enrichedChunkCount: 0, + pendingEnrichmentCount: 0, + enrichmentPercent: 0, + enrichmentRatePerMinute: 0, + databaseSizeBytes: 0, + recentActivityBuckets: [1, 2, 3], + recentAgentWriteBuckets: [1, 0, 1], + recentWatcherWriteBuckets: [0, 2, 1], + recentDigestWriteBuckets: [0, 0, 1], + recentEnrichmentBuckets: [0, 0, 0], + activityWindowMinutes: 15 + ) + + let summary = DashboardFlowSummary.derive(daemon: nil, stats: stats, now: Date(timeIntervalSince1970: 1_764_236_400)) + + XCTAssertEqual(summary.ingress.primarySeriesLabel, "Agent stores") + XCTAssertEqual(summary.ingress.secondarySeriesLabel, "JSONL watcher") + XCTAssertEqual(summary.ingress.tertiarySeriesLabel, "Digest") } func testDashboardStatsComputesVectorETAFromBacklogAndNetDrain() { From f479e97b6a499fe0943340f187a62fae91c59613 Mon Sep 17 00:00:00 2001 From: Etan Joseph Heyman Date: Fri, 19 Jun 2026 20:15:40 +0300 Subject: [PATCH 4/5] Polish BrainBar V1 dashboard design --- .../BrainBar/BrainBarWindowRootView.swift | 253 +++++++----- .../BrainBar/Dashboard/PipelineState.swift | 6 +- .../Dashboard/SparklineRenderer.swift | 369 ++++++++++++------ brain-bar/Sources/BrainBar/DesignTokens.swift | 20 + .../Tests/BrainBarTests/DashboardTests.swift | 59 +++ 5 files changed, 495 insertions(+), 212 deletions(-) diff --git a/brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift b/brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift index a62055b5..0c7a4c87 100644 --- a/brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift +++ b/brain-bar/Sources/BrainBar/BrainBarWindowRootView.swift @@ -488,24 +488,23 @@ private struct BrainBarDashboardView: View { VStack(alignment: .leading, spacing: layout.gridSpacing) { BrainBarSectionLabel("Pipeline") + writesCard(layout: layout) + + signalCoveragePanel(layout: layout) + .padding(.top, max(0, 20 - layout.gridSpacing)) + if layout.chartColumns == 2 { HStack(alignment: .top, spacing: layout.gridSpacing) { - VStack(alignment: .leading, spacing: 12) { - writesCard(layout: layout) - signalCoveragePanel(layout: layout) - } - .frame(maxWidth: .infinity, alignment: .topLeading) enrichmentsCard(layout: layout) + queueRail(layout: layout) } } else { VStack(spacing: layout.gridSpacing) { - writesCard(layout: layout) - signalCoveragePanel(layout: layout) enrichmentsCard(layout: layout) + queueRail(layout: layout) } } - queueRail(layout: layout) agentPresenceStrip(layout: layout) } .padding(layout.cardPadding) @@ -692,7 +691,11 @@ private struct BrainBarTrigramProgress: View { .foregroundStyle(.secondary) .monospacedDigit() } - ProgressView(value: min(stats.trigramCoveragePercent, 100), total: 100) + BrainBarAnimatedCoverageBar( + percent: min(stats.trigramCoveragePercent, 100), + accentColor: .brainBarSignalTrigram + ) + .frame(height: 6) } } } @@ -702,19 +705,18 @@ private struct BrainBarSignalCoveragePanel: View { let compact: Bool @Binding var isExpanded: Bool @Binding var isVectorDetailExpanded: Bool + @Environment(\.accessibilityReduceMotion) private var reduceMotion + @State private var revealedSignalIDs: Set = [] private var signals: [BrainBarSignalCoverage] { - let green = BrainBarStateTheme.active.theme.swiftUIColor - let amber = BrainBarStateTheme.degraded.theme.swiftUIColor - - return [ + [ BrainBarSignalCoverage( name: "Vector", indexedCount: stats.vectorIndexedChunkCount, totalCount: stats.signalEligibleChunkCount, backlogCount: stats.vectorBacklogCount, coveragePercent: stats.vectorCoveragePercent, - accentColor: amber, + accentColor: .brainBarSignalVector, showsDetail: true, vectorNetDrainRatePerHour: stats.vectorNetDrainRatePerHour, vectorBacklogETAHours: stats.vectorBacklogETAHours @@ -725,7 +727,7 @@ private struct BrainBarSignalCoveragePanel: View { totalCount: stats.signalEligibleChunkCount, backlogCount: stats.ftsBacklogCount, coveragePercent: stats.ftsCoveragePercent, - accentColor: green, + accentColor: .brainBarSignalFTS5, showsDetail: false, vectorNetDrainRatePerHour: nil, vectorBacklogETAHours: nil @@ -736,7 +738,7 @@ private struct BrainBarSignalCoveragePanel: View { totalCount: stats.signalEligibleChunkCount, backlogCount: stats.trigramBacklogCount, coveragePercent: stats.trigramCoveragePercent, - accentColor: green, + accentColor: .brainBarSignalTrigram, showsDetail: false, vectorNetDrainRatePerHour: nil, vectorBacklogETAHours: nil @@ -746,6 +748,10 @@ private struct BrainBarSignalCoveragePanel: View { var body: some View { VStack(alignment: .leading, spacing: compact ? 10 : 12) { + Text("SIGNAL COVERAGE") + .font(.system(size: 11, weight: .semibold)) + .tracking(0.88) + .foregroundStyle(Color.brainBarTextSecondary.opacity(0.60)) disclosureButton if isExpanded { @@ -754,11 +760,17 @@ private struct BrainBarSignalCoveragePanel: View { } } .frame(maxWidth: .infinity, alignment: .leading) + .onAppear { + updateRevealedSignals(animated: false) + } + .onChange(of: isExpanded) { _, _ in + updateRevealedSignals(animated: true) + } } private var disclosureButton: some View { Button { - withAnimation(.easeInOut(duration: 0.18)) { + withAnimation(reduceMotion ? nil : .easeInOut(duration: 0.25)) { isExpanded.toggle() if !isExpanded { isVectorDetailExpanded = false @@ -766,9 +778,11 @@ private struct BrainBarSignalCoveragePanel: View { } } label: { HStack(spacing: 7) { - Image(systemName: isExpanded ? "chevron.down" : "chevron.right") + Image(systemName: "chevron.right") .font(.system(size: 9, weight: .bold)) .frame(width: 12, height: 12) + .rotationEffect(.degrees(isExpanded ? 90 : 0)) + .animation(reduceMotion ? nil : .easeInOut(duration: 0.25), value: isExpanded) Text("see under the hood") .font(.system(size: 11, weight: .semibold)) @@ -804,7 +818,7 @@ private struct BrainBarSignalCoveragePanel: View { VStack(alignment: .leading, spacing: compact ? 8 : 10) { if signal.showsDetail { Button { - withAnimation(.easeInOut(duration: 0.18)) { + withAnimation(reduceMotion ? nil : .easeInOut(duration: 0.25)) { isVectorDetailExpanded.toggle() } } label: { @@ -826,6 +840,27 @@ private struct BrainBarSignalCoveragePanel: View { } } .frame(minWidth: compact ? 150 : 170, maxWidth: .infinity, alignment: .topLeading) + .opacity(isExpanded ? (revealedSignalIDs.contains(signal.id) ? 1 : 0) : 1) + .offset(y: isExpanded && !revealedSignalIDs.contains(signal.id) ? 8 : 0) + } + + private func updateRevealedSignals(animated: Bool) { + guard isExpanded else { + revealedSignalIDs.removeAll() + return + } + if reduceMotion || !animated { + revealedSignalIDs = Set(signals.map(\.id)) + return + } + revealedSignalIDs.removeAll() + for (index, signal) in signals.enumerated() { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.06) { + withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) { + _ = revealedSignalIDs.insert(signal.id) + } + } + } } } @@ -859,44 +894,77 @@ private struct BrainBarSignalCoverageRow: View { let signal: BrainBarSignalCoverage let compact: Bool let isSelected: Bool + @Environment(\.accessibilityReduceMotion) private var reduceMotion var body: some View { VStack(alignment: .leading, spacing: compact ? 7 : 8) { HStack(alignment: .firstTextBaseline, spacing: 8) { Text(signal.name) - .font(.system(size: compact ? 12 : 13, weight: .semibold)) + .font(.system(size: 14, weight: .semibold)) .foregroundStyle(Color.brainBarTextPrimary) Spacer(minLength: 8) Text(signal.percentText) - .font(.system(size: compact ? 13 : 15, weight: .bold, design: .rounded)) + .font(.system(size: compact ? 18 : 20, weight: .bold, design: .rounded)) .foregroundStyle(signal.accentColor) .monospacedDigit() } - GeometryReader { proxy in - ZStack(alignment: .leading) { - Capsule() - .fill(signal.accentColor.opacity(0.16)) - Capsule() - .fill( - LinearGradient( - colors: [signal.accentColor.opacity(0.68), signal.accentColor], - startPoint: .leading, - endPoint: .trailing - ) - ) - .frame(width: proxy.size.width * signal.clampedCoveragePercent / 100) - } - } + BrainBarAnimatedCoverageBar(percent: signal.clampedCoveragePercent, accentColor: signal.accentColor) .frame(height: 6) } .frame(maxWidth: .infinity, alignment: .leading) - .padding(.vertical, compact ? 10 : 11) - .padding(.horizontal, compact ? 11 : 12) - .background(BrainBarDashboardCardStyle(emphasized: isSelected)) + .padding(14) + .background(BrainBarDashboardCardStyle(emphasized: isSelected, cornerRadius: 14)) .overlay { - RoundedRectangle(cornerRadius: BrainBarDesignTokens.Radius.md, style: .continuous) - .stroke(signal.accentColor.opacity(isSelected ? 0.48 : 0.2), lineWidth: isSelected ? 1.2 : 1) + RoundedRectangle(cornerRadius: 14, style: .continuous) + .stroke(signal.accentColor.opacity(isSelected ? 0.70 : 0.2), lineWidth: isSelected ? 1.5 : 1) + } + .shadow(color: isSelected ? signal.accentColor.opacity(0.18) : .clear, radius: 12, y: 2) + .scaleEffect(isSelected ? 0.98 : 1) + .animation(reduceMotion ? nil : .spring(response: 0.4, dampingFraction: 0.85), value: signal.clampedCoveragePercent) + } +} + +private struct BrainBarAnimatedCoverageBar: View { + let percent: Double + let accentColor: Color + @Environment(\.accessibilityReduceMotion) private var reduceMotion + @State private var displayedPercent: Double = 0 + + var body: some View { + GeometryReader { proxy in + ZStack(alignment: .leading) { + Capsule() + .fill(accentColor.opacity(0.16)) + Capsule() + .fill( + LinearGradient( + colors: [accentColor, accentColor.opacity(0.70)], + startPoint: .leading, + endPoint: .trailing + ) + ) + .frame(width: proxy.size.width * min(max(displayedPercent, 0), 100) / 100) + } + } + .onAppear { + if reduceMotion { + displayedPercent = percent + } else { + displayedPercent = 0 + withAnimation(.easeOut(duration: 0.45)) { + displayedPercent = percent + } + } + } + .onChange(of: percent) { _, newValue in + if reduceMotion { + displayedPercent = newValue + } else { + withAnimation(.spring(response: 0.4, dampingFraction: 0.85)) { + displayedPercent = newValue + } + } } } } @@ -1000,8 +1068,9 @@ private struct BrainBarSignalDetailMetric: View { .lineLimit(1) Text(label) - .font(.system(size: 9, weight: .semibold)) - .foregroundStyle(Color.brainBarTextMuted) + .font(.system(size: 10, weight: .semibold)) + .tracking(0.60) + .foregroundStyle(Color.brainBarTextSecondary.opacity(0.50)) .textCase(.uppercase) } } @@ -1019,8 +1088,10 @@ private struct BrainBarFlowLaneCard: View { VStack(alignment: .leading, spacing: compact ? 10 : 12) { HStack(alignment: .top) { VStack(alignment: .leading, spacing: 4) { - Text(lane.name) - .font(.system(size: emphasize ? (compact ? 16 : 18) : (compact ? 15 : 16), weight: .semibold)) + Text(lane.name.uppercased()) + .font(.system(size: 11, weight: .semibold)) + .tracking(0.88) + .foregroundStyle(Color.brainBarTextSecondary.opacity(0.60)) Text(lane.windowLabel) .font(.system(size: 11, weight: .medium)) .foregroundStyle(.secondary) @@ -1056,10 +1127,13 @@ private struct BrainBarFlowLaneCard: View { BrainBarSeriesLegend( primaryLabel: primarySeriesLabel, primaryColor: lane.accentColor, + primaryIsActive: lane.values.reduce(0, +) > 0, secondaryLabel: secondarySeriesLabel, secondaryColor: secondaryAccentColor, + secondaryIsActive: lane.secondaryValues.reduce(0, +) > 0, tertiaryLabel: lane.tertiarySeriesLabel, - tertiaryColor: lane.tertiaryAccentColor + tertiaryColor: lane.tertiaryAccentColor, + tertiaryIsActive: lane.tertiaryValues.reduce(0, +) > 0 ) } @@ -1201,14 +1275,17 @@ private struct BrainBarQueueRail: View { private struct BrainBarSeriesLegend: View { let primaryLabel: String let primaryColor: NSColor + let primaryIsActive: Bool let secondaryLabel: String let secondaryColor: NSColor + let secondaryIsActive: Bool let tertiaryLabel: String? let tertiaryColor: NSColor? + let tertiaryIsActive: Bool var body: some View { ViewThatFits(in: .horizontal) { - HStack(spacing: 12) { + HStack(spacing: 16) { legendItems Spacer(minLength: 0) } @@ -1222,10 +1299,10 @@ private struct BrainBarSeriesLegend: View { @ViewBuilder private var legendItems: some View { - legendItem(label: primaryLabel, color: primaryColor, style: .solid) - legendItem(label: secondaryLabel, color: secondaryColor, style: .dash) + legendItem(label: primaryLabel, color: primaryColor, isActive: primaryIsActive) + legendItem(label: secondaryLabel, color: secondaryColor, isActive: secondaryIsActive) if let tertiaryLabel, let tertiaryColor { - legendItem(label: tertiaryLabel, color: tertiaryColor, style: .dotDash) + legendItem(label: tertiaryLabel, color: tertiaryColor, isActive: tertiaryIsActive) } } @@ -1237,55 +1314,18 @@ private struct BrainBarSeriesLegend: View { return "\(labels.joined(separator: ", ")) write series" } - private func legendItem(label: String, color: NSColor, style: LegendSwatchStyle) -> some View { + private func legendItem(label: String, color: NSColor, isActive: Bool) -> some View { HStack(spacing: 6) { - LegendSwatch(color: color, style: style) + RoundedRectangle(cornerRadius: 3, style: .continuous) + .fill(Color.brainBar(nsColor: color).opacity(isActive ? 1 : 0.35)) + .frame(width: 10, height: 10) Text(label) - .font(.system(size: 10, weight: .semibold)) - .foregroundStyle(Color.brainBarTextMuted) + .font(.system(size: 11, weight: .medium)) + .foregroundStyle(Color.brainBarTextSecondary.opacity(isActive ? 0.70 : 0.35)) .lineLimit(1) } .fixedSize(horizontal: true, vertical: false) } - - private enum LegendSwatchStyle { - case solid - case dash - case dotDash - } - - private struct LegendSwatch: View { - let color: NSColor - let style: LegendSwatchStyle - - var body: some View { - HStack(spacing: 2) { - switch style { - case .solid: - Capsule() - .fill(Color.brainBar(nsColor: color)) - .frame(width: 18, height: 3) - case .dash: - ForEach(0..<3, id: \.self) { _ in - Capsule() - .fill(Color.brainBar(nsColor: color)) - .frame(width: 4, height: 3) - } - case .dotDash: - Capsule() - .fill(Color.brainBar(nsColor: color)) - .frame(width: 8, height: 3) - Capsule() - .fill(Color.brainBar(nsColor: color)) - .frame(width: 3, height: 3) - Capsule() - .fill(Color.brainBar(nsColor: color)) - .frame(width: 3, height: 3) - } - } - .frame(width: 18, height: 6, alignment: .leading) - } - } } struct BrainBarDashboardLayout { @@ -1433,13 +1473,16 @@ private struct BrainBarLaneMetric: View { var body: some View { VStack(alignment: .leading, spacing: 2) { - Text(label) - .font(.system(size: 10, weight: .medium)) - .foregroundStyle(.secondary) Text(value) - .font(.system(size: 12, weight: .semibold)) + .font(.system(size: 16, weight: .bold, design: .rounded)) .lineLimit(1) .minimumScaleFactor(0.8) + .monospacedDigit() + Text(label) + .font(.system(size: 10, weight: .semibold)) + .tracking(0.60) + .foregroundStyle(Color.brainBarTextSecondary.opacity(0.50)) + .textCase(.uppercase) } } } @@ -1498,7 +1541,9 @@ private struct BrainBarOverviewStat: View { VStack(alignment: .leading, spacing: isHero ? 8 : 4) { Text(label) .font(.system(size: BrainBarDesignTokens.TypeScale.label, weight: .semibold)) - .foregroundStyle(Color.brainBarTextMuted) + .tracking(0.66) + .foregroundStyle(Color.brainBarTextSecondary.opacity(0.50)) + .textCase(.uppercase) Text(value) .font(.system(size: isHero ? BrainBarDesignTokens.TypeScale.hero : BrainBarDesignTokens.TypeScale.title, weight: .semibold, design: .rounded)) .lineLimit(1) @@ -1619,9 +1664,10 @@ private struct BrainBarDiagnosticTile: View { private struct BrainBarDashboardCardStyle: View { var emphasized = false + var cornerRadius: CGFloat = 18 var body: some View { - RoundedRectangle(cornerRadius: 18, style: .continuous) + RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) .fill( LinearGradient( colors: emphasized @@ -1638,9 +1684,16 @@ private struct BrainBarDashboardCardStyle: View { ) ) .overlay( - RoundedRectangle(cornerRadius: 18, style: .continuous) + RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) .stroke(Color.brainBarBorderSoft, lineWidth: 1) ) + .overlay(alignment: .top) { + Rectangle() + .fill(Color.brainBarWhite.opacity(0.06)) + .frame(height: 1) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) + } + .shadow(color: Color.brainBarBlack.opacity(0.30), radius: 8, y: 2) } } diff --git a/brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift b/brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift index 3fd06f76..ad0ab8f8 100644 --- a/brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift +++ b/brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift @@ -232,9 +232,9 @@ struct DashboardFlowSummary: Sendable, Equatable { static func derive(daemon: DaemonHealthSnapshot?, stats: DashboardStats, now: Date = Date()) -> DashboardFlowSummary { let windowLabel = DashboardMetricFormatter.windowLabel(minutes: stats.activityWindowMinutes) - let agentStoresColor = NSColor.systemGreen - let jsonlWatcherColor = NSColor.systemOrange - let digestColor = NSColor.systemPurple + let agentStoresColor = BrainBarDesignTokens.Colors.seriesAgent + let jsonlWatcherColor = BrainBarDesignTokens.Colors.seriesWatcher + let digestColor = BrainBarDesignTokens.Colors.seriesDigest let enrichmentColor = BrainBarStateTheme.active.theme.color let writesLive = stats.eventIsLive(stats.lastWriteAt, now: now) diff --git a/brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift b/brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift index 8d854eeb..a85b8bfd 100644 --- a/brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift +++ b/brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift @@ -11,6 +11,17 @@ struct SparklineChartPoint: Identifiable, Equatable, Sendable { var id: Int { bucket } } +struct SparklineLegendEntry: Equatable, Sendable { + let label: String + let isActive: Bool +} + +enum SparklineSeriesRole: CaseIterable, Equatable, Sendable { + case primary + case secondary + case tertiary +} + struct SparklineChartPresentation: Equatable, Sendable { let label: String let values: [Int] @@ -81,6 +92,32 @@ struct SparklineChartPresentation: Equatable, Sendable { hasSecondarySeries || hasTertiarySeries } + var legendEntries: [SparklineLegendEntry] { + var entries = [ + SparklineLegendEntry(label: primaryLegendLabel, isActive: isSeriesActive(.primary)), + ] + if hasSecondarySeries { + entries.append(SparklineLegendEntry(label: secondaryLegendLabel, isActive: isSeriesActive(.secondary))) + } + if hasTertiarySeries { + entries.append(SparklineLegendEntry(label: tertiaryLegendLabel, isActive: isSeriesActive(.tertiary))) + } + return entries + } + + var visibleSeriesLabels: [String] { + SparklineSeriesRole.allCases.compactMap { role in + guard shouldPlotSeries(role) else { return nil } + return label(for: role) + } + } + + var showsListeningForWritesCaption: Bool { + hasMultipleSeries && + label.localizedCaseInsensitiveContains("writes") && + !legendEntries.contains(where: \.isActive) + } + var primaryLegendLabel: String { primarySeriesLabel ?? "Primary" } @@ -115,6 +152,53 @@ struct SparklineChartPresentation: Equatable, Sendable { max(values.max() ?? 0, secondaryValues.max() ?? 0, tertiaryValues.max() ?? 0, 1) } + func points(for role: SparklineSeriesRole) -> [SparklineChartPoint] { + switch role { + case .primary: + points + case .secondary: + secondaryPoints + case .tertiary: + tertiaryPoints + } + } + + func label(for role: SparklineSeriesRole) -> String? { + switch role { + case .primary: + primaryLegendLabel + case .secondary: + hasSecondarySeries ? secondaryLegendLabel : nil + case .tertiary: + hasTertiarySeries ? tertiaryLegendLabel : nil + } + } + + func isSeriesActive(_ role: SparklineSeriesRole) -> Bool { + switch role { + case .primary: + values.reduce(0, +) > 0 + case .secondary: + secondaryValues.reduce(0, +) > 0 + case .tertiary: + tertiaryValues.reduce(0, +) > 0 + } + } + + func shouldPlotSeries(_ role: SparklineSeriesRole) -> Bool { + guard label(for: role) != nil else { return false } + if hasMultipleSeries { + return isSeriesActive(role) + } + return !points(for: role).isEmpty + } + + func shouldEmphasizeSparsePoints(_ role: SparklineSeriesRole) -> Bool { + guard shouldPlotSeries(role) else { return false } + let total = points(for: role).reduce(0) { $0 + $1.value } + return (1...2).contains(total) + } + var xAxisDomainStart: Date { fetchedAt.addingTimeInterval(-Double(max(activityWindowMinutes * 60, 1))) } @@ -217,8 +301,10 @@ struct SparklineChart: View { let secondaryAccentColor: NSColor? let tertiaryAccentColor: NSColor? let compact: Bool + @Environment(\.accessibilityReduceMotion) private var reduceMotion @State private var hoveredBucket: Int? @State private var hoverLocation: CGPoint? + @State private var lineRevealProgress: CGFloat = 1 init( presentation: SparklineChartPresentation, @@ -236,112 +322,14 @@ struct SparklineChart: View { var body: some View { VStack(spacing: 2) { - Chart { - ForEach(presentation.points) { point in - if !compact && !presentation.hasMultipleSeries { - AreaMark( - x: .value("Time", point.timestamp), - y: .value("Count", point.value) - ) - .foregroundStyle(Color.brainBar(nsColor: accentColor).opacity(0.10)) - } - - LineMark( - x: .value("Time", point.timestamp), - y: .value("Count", point.value), - series: .value("Series", presentation.primaryLegendLabel) - ) - .interpolationMethod(.linear) - .foregroundStyle(by: .value("Series", presentation.primaryLegendLabel)) - .lineStyle(StrokeStyle(lineWidth: compact ? 1.6 : 2, lineCap: .round, lineJoin: .round)) - - if point == presentation.latestPoint { - PointMark( - x: .value("Time", point.timestamp), - y: .value("Count", point.value) - ) - .foregroundStyle(Color.brainBar(nsColor: accentColor)) - .symbolSize(compact ? 18 : 42) - } - } - - if presentation.hasSecondarySeries { - ForEach(presentation.secondaryPoints) { point in - LineMark( - x: .value("Time", point.timestamp), - y: .value("Count", point.value), - series: .value("Series", presentation.secondaryLegendLabel) - ) - .interpolationMethod(.linear) - .foregroundStyle(by: .value("Series", presentation.secondaryLegendLabel)) - .lineStyle(StrokeStyle(lineWidth: compact ? 1.4 : 2, lineCap: .round, lineJoin: .round, dash: compact ? [] : [4, 3])) - } - } - - if presentation.hasTertiarySeries { - ForEach(presentation.tertiaryPoints) { point in - LineMark( - x: .value("Time", point.timestamp), - y: .value("Count", point.value), - series: .value("Series", presentation.tertiaryLegendLabel) - ) - .interpolationMethod(.linear) - .foregroundStyle(by: .value("Series", presentation.tertiaryLegendLabel)) - .lineStyle(StrokeStyle(lineWidth: compact ? 1.4 : 2, lineCap: .round, lineJoin: .round, dash: compact ? [] : [2, 3])) - } - } - } - .chartXAxis(.hidden) - .chartYAxis(.hidden) - .chartLegend(.hidden) - .chartForegroundStyleScale(chartForegroundStyleScale) - .chartXScale(domain: presentation.xAxisDomainStart...presentation.xAxisDomainEnd) - .chartYScale(domain: 0...presentation.maxValue) - .chartPlotStyle { plotArea in - plotArea - .background(Color.brainBarClear) - .padding(compact ? 2 : 10) - } - .chartOverlay { chartProxy in - GeometryReader { geometry in - if let plotAnchor = chartProxy.plotFrame { - let plotFrame = geometry[plotAnchor] - - Rectangle() - .fill(.clear) - .contentShape(Rectangle()) - .onContinuousHover { phase in - switch phase { - case .active(let location): - hoveredBucket = nearestBucket( - to: location, - plotFrame: plotFrame, - chartProxy: chartProxy - ) - hoverLocation = location - case .ended: - hoveredBucket = nil - hoverLocation = nil - } - } - - if let hoveredBucket, - let hoverLocation, - !compact { - let tooltipSize = SparklineTooltipPlacement.tooltipSize(in: geometry.size) - sparklineTooltip(forBucket: hoveredBucket) - .frame(width: tooltipSize.width, alignment: .leading) - .position( - SparklineTooltipPlacement.position( - near: hoverLocation, - in: geometry.size, - tooltipSize: tooltipSize - ) - ) - .allowsHitTesting(false) - } - } - } + if presentation.showsListeningForWritesCaption { + Text("Listening for writes...") + .font(.system(size: 11, weight: .medium)) + .foregroundStyle(Color.brainBarTextSecondary.opacity(0.60)) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .transition(.opacity) + } else { + chartBody } if !compact { @@ -363,6 +351,117 @@ struct SparklineChart: View { .accessibilityElement(children: .combine) .accessibilityLabel(Text(presentation.accessibilityLabel)) .accessibilityValue(Text(presentation.accessibilityValue)) + .onAppear { + if reduceMotion { + lineRevealProgress = 1 + } else { + lineRevealProgress = 0 + withAnimation(.easeOut(duration: 0.6)) { + lineRevealProgress = 1 + } + } + } + .animation(reduceMotion ? nil : .easeInOut(duration: 0.25), value: presentation) + } + + private var chartBody: some View { + GeometryReader { geometry in + let plotFrame = plotFrame(in: geometry.size) + + ZStack(alignment: .topLeading) { + ForEach(SparklineSeriesRole.allCases, id: \.self) { role in + if presentation.shouldPlotSeries(role) { + SparklineSeriesPathShape( + points: presentation.points(for: role), + maxValue: presentation.maxValue, + plotFrame: plotFrame + ) + .stroke(color(for: role), style: lineStyle(for: role)) + + ForEach(visiblePointMarkers(for: role)) { point in + Circle() + .fill(color(for: role)) + .frame(width: compact ? 4.4 : 9, height: compact ? 4.4 : 9) + .position( + self.point( + for: point, + bucketCount: presentation.points(for: role).count, + in: plotFrame + ) + ) + } + } + } + + Rectangle() + .fill(.clear) + .contentShape(Rectangle()) + .onContinuousHover { phase in + switch phase { + case .active(let location): + hoveredBucket = nearestBucket(to: location, plotFrame: plotFrame) + hoverLocation = location + case .ended: + hoveredBucket = nil + hoverLocation = nil + } + } + + if let hoveredBucket, + let hoverLocation, + !compact { + let tooltipSize = SparklineTooltipPlacement.tooltipSize(in: geometry.size) + sparklineTooltip(forBucket: hoveredBucket) + .frame(width: tooltipSize.width, alignment: .leading) + .position( + SparklineTooltipPlacement.position( + near: hoverLocation, + in: geometry.size, + tooltipSize: tooltipSize + ) + ) + .allowsHitTesting(false) + } + } + } + } + + private func visiblePointMarkers(for role: SparklineSeriesRole) -> [SparklineChartPoint] { + let points = presentation.points(for: role) + guard let last = points.last else { return [] } + if presentation.shouldEmphasizeSparsePoints(role) { + return points.filter { $0.value > 0 } + } + return [last] + } + + private func point(for point: SparklineChartPoint, bucketCount: Int, in plotFrame: CGRect) -> CGPoint { + let x: CGFloat + if bucketCount <= 1 { + x = plotFrame.midX + } else { + x = plotFrame.minX + CGFloat(point.bucket) * (plotFrame.width / CGFloat(bucketCount - 1)) + } + let normalizedValue = CGFloat(point.value) / CGFloat(max(presentation.maxValue, 1)) + return CGPoint(x: x, y: plotFrame.maxY - (normalizedValue * plotFrame.height)) + } + + private func plotFrame(in size: CGSize) -> CGRect { + let inset: CGFloat = compact ? 2 : 10 + return CGRect( + x: inset, + y: inset, + width: max(size.width - inset * 2, 1), + height: max(size.height - inset * 2, 1) + ) + } + + private func nearestBucket(to location: CGPoint, plotFrame: CGRect) -> Int? { + guard !presentation.points.isEmpty, plotFrame.contains(location) else { return nil } + let bucketCount = max(presentation.points.count, 1) + if bucketCount == 1 { return 0 } + let normalizedX = min(max((location.x - plotFrame.minX) / max(plotFrame.width, 1), 0), 1) + return Int((normalizedX * CGFloat(bucketCount - 1)).rounded()) } private func nearestBucket( @@ -395,9 +494,9 @@ struct SparklineChart: View { } private var chartForegroundStyleScale: KeyValuePairs { - let primaryColor = Color.brainBar(nsColor: accentColor) - let secondaryColor = Color.brainBar(nsColor: secondaryAccentColor ?? .systemOrange) - let tertiaryColor = Color.brainBar(nsColor: tertiaryAccentColor ?? .systemPurple) + let primaryColor = color(for: .primary) + let secondaryColor = color(for: .secondary) + let tertiaryColor = color(for: .tertiary) if presentation.hasSecondarySeries && presentation.hasTertiarySeries { return [ @@ -421,6 +520,28 @@ struct SparklineChart: View { return [presentation.primaryLegendLabel: primaryColor] } + private func color(for role: SparklineSeriesRole) -> Color { + switch role { + case .primary: + Color.brainBar(nsColor: accentColor) + case .secondary: + Color.brainBar(nsColor: secondaryAccentColor ?? BrainBarDesignTokens.Colors.seriesWatcher) + case .tertiary: + Color.brainBar(nsColor: tertiaryAccentColor ?? BrainBarDesignTokens.Colors.seriesDigest) + } + } + + private func lineStyle(for role: SparklineSeriesRole) -> StrokeStyle { + switch role { + case .primary: + StrokeStyle(lineWidth: compact ? 1.6 : 2.0, lineCap: .round, lineJoin: .round) + case .secondary: + StrokeStyle(lineWidth: compact ? 1.4 : 1.75, lineCap: .round, lineJoin: .round) + case .tertiary: + StrokeStyle(lineWidth: compact ? 1.25 : 1.5, lineCap: .round, lineJoin: .round) + } + } + @ViewBuilder private func sparklineTooltip(forBucket bucket: Int) -> some View { Text(presentation.tooltipText(forBucket: bucket)) @@ -440,6 +561,36 @@ struct SparklineChart: View { } } +private struct SparklineSeriesPathShape: Shape { + let points: [SparklineChartPoint] + let maxValue: Int + let plotFrame: CGRect + + func path(in rect: CGRect) -> Path { + var path = Path() + for (index, point) in points.enumerated() { + let renderedPoint = renderedPoint(for: point, bucketCount: points.count) + if index == 0 { + path.move(to: renderedPoint) + } else { + path.addLine(to: renderedPoint) + } + } + return path + } + + private func renderedPoint(for point: SparklineChartPoint, bucketCount: Int) -> CGPoint { + let x: CGFloat + if bucketCount <= 1 { + x = plotFrame.midX + } else { + x = plotFrame.minX + CGFloat(point.bucket) * (plotFrame.width / CGFloat(bucketCount - 1)) + } + let normalizedValue = CGFloat(point.value) / CGFloat(max(maxValue, 1)) + return CGPoint(x: x, y: plotFrame.maxY - (normalizedValue * plotFrame.height)) + } +} + enum SparklineTooltipPlacement { private static let margin: CGFloat = 8 private static let preferredWidth: CGFloat = 260 diff --git a/brain-bar/Sources/BrainBar/DesignTokens.swift b/brain-bar/Sources/BrainBar/DesignTokens.swift index ffe2c545..1bb4d873 100644 --- a/brain-bar/Sources/BrainBar/DesignTokens.swift +++ b/brain-bar/Sources/BrainBar/DesignTokens.swift @@ -28,6 +28,17 @@ enum BrainBarDesignTokens { static let accentDeep = NSColor.brainBarHex(0x3F6FE0) static let accentViolet = NSColor.brainBarHex(0xA98BFF) + static let signalVector = NSColor.brainBarHex(0xF5A524) + static let signalFTS5 = NSColor.brainBarHex(0x34D399) + static let signalTrigram = NSColor.brainBarHex(0xA78BFA) + + static let seriesAgent = NSColor.brainBarHex(0x22D3EE) + static let seriesWatcher = NSColor.brainBarHex(0xFB7185) + static let seriesDigest = NSColor.brainBarHex(0xA3E635) + static let seriesAgentDimmed = NSColor.brainBarHex(0x22D3EE, alpha: 0.35) + static let seriesWatcherDimmed = NSColor.brainBarHex(0xFB7185, alpha: 0.35) + static let seriesDigestDimmed = NSColor.brainBarHex(0xA3E635, alpha: 0.35) + static let white = NSColor.white static let black = NSColor.black } @@ -160,6 +171,15 @@ extension Color { static let brainBarAccentBright = Color(nsColor: BrainBarDesignTokens.Colors.accentBright) static let brainBarAccentDeep = Color(nsColor: BrainBarDesignTokens.Colors.accentDeep) static let brainBarAccentViolet = Color(nsColor: BrainBarDesignTokens.Colors.accentViolet) + static let brainBarSignalVector = Color(nsColor: BrainBarDesignTokens.Colors.signalVector) + static let brainBarSignalFTS5 = Color(nsColor: BrainBarDesignTokens.Colors.signalFTS5) + static let brainBarSignalTrigram = Color(nsColor: BrainBarDesignTokens.Colors.signalTrigram) + static let brainBarSeriesAgent = Color(nsColor: BrainBarDesignTokens.Colors.seriesAgent) + static let brainBarSeriesWatcher = Color(nsColor: BrainBarDesignTokens.Colors.seriesWatcher) + static let brainBarSeriesDigest = Color(nsColor: BrainBarDesignTokens.Colors.seriesDigest) + static let brainBarSeriesAgentDimmed = Color(nsColor: BrainBarDesignTokens.Colors.seriesAgentDimmed) + static let brainBarSeriesWatcherDimmed = Color(nsColor: BrainBarDesignTokens.Colors.seriesWatcherDimmed) + static let brainBarSeriesDigestDimmed = Color(nsColor: BrainBarDesignTokens.Colors.seriesDigestDimmed) static let brainBarWhite = Color(nsColor: BrainBarDesignTokens.Colors.white) static let brainBarBlack = Color(nsColor: BrainBarDesignTokens.Colors.black) static let brainBarClear = Color.clear diff --git a/brain-bar/Tests/BrainBarTests/DashboardTests.swift b/brain-bar/Tests/BrainBarTests/DashboardTests.swift index 9a8c8aa5..7bd5de8f 100644 --- a/brain-bar/Tests/BrainBarTests/DashboardTests.swift +++ b/brain-bar/Tests/BrainBarTests/DashboardTests.swift @@ -300,6 +300,29 @@ final class DashboardTests: XCTestCase { XCTAssertEqual(summary.ingress.tertiarySeriesLabel, "Digest") } + func testDashboardFlowUsesDistinctV1WriteSeriesPalette() { + let stats = DashboardStats( + chunkCount: 9, + enrichedChunkCount: 0, + pendingEnrichmentCount: 0, + enrichmentPercent: 0, + enrichmentRatePerMinute: 0, + databaseSizeBytes: 0, + recentActivityBuckets: [1, 2, 3], + recentAgentWriteBuckets: [1, 0, 1], + recentWatcherWriteBuckets: [0, 2, 1], + recentDigestWriteBuckets: [0, 0, 1], + recentEnrichmentBuckets: [0, 0, 0], + activityWindowMinutes: 15 + ) + + let summary = DashboardFlowSummary.derive(daemon: nil, stats: stats, now: Date(timeIntervalSince1970: 1_764_236_400)) + + XCTAssertEqual(summary.ingress.accentColor, BrainBarDesignTokens.Colors.seriesAgent) + XCTAssertEqual(summary.ingress.secondaryAccentColor, BrainBarDesignTokens.Colors.seriesWatcher) + XCTAssertEqual(summary.ingress.tertiaryAccentColor, BrainBarDesignTokens.Colors.seriesDigest) + } + func testDashboardStatsComputesVectorETAFromBacklogAndNetDrain() { let stats = DashboardStats( chunkCount: 10_105, @@ -760,6 +783,42 @@ final class DashboardTests: XCTestCase { XCTAssertEqual(presentation.xAxisDomainEnd, now) } + func testSparklinePresentationOmitsZeroWriteSourcesButKeepsDimmedLegendEntries() { + let presentation = SparklineChartPresentation( + label: "Writes over 30m", + values: [0, 1, 0], + secondaryValues: [0, 0, 0], + tertiaryValues: [0, 0, 2], + primarySeriesLabel: "Agent stores", + secondarySeriesLabel: "JSONL watcher", + tertiarySeriesLabel: "Digest", + activityWindowMinutes: 30, + fetchedAt: Date(timeIntervalSince1970: 1_764_236_400) + ) + + XCTAssertEqual(presentation.visibleSeriesLabels, ["Agent stores", "Digest"]) + XCTAssertEqual(presentation.legendEntries.map(\.label), ["Agent stores", "JSONL watcher", "Digest"]) + XCTAssertEqual(presentation.legendEntries.map(\.isActive), [true, false, true]) + } + + func testSparklinePresentationShowsListeningCaptionForColdWriteStart() { + let presentation = SparklineChartPresentation( + label: "Writes over 30m", + values: [0, 0, 0], + secondaryValues: [0, 0, 0], + tertiaryValues: [0, 0, 0], + primarySeriesLabel: "Agent stores", + secondarySeriesLabel: "JSONL watcher", + tertiarySeriesLabel: "Digest", + activityWindowMinutes: 30, + fetchedAt: Date(timeIntervalSince1970: 1_764_236_400) + ) + + XCTAssertTrue(presentation.showsListeningForWritesCaption) + XCTAssertEqual(presentation.visibleSeriesLabels, []) + XCTAssertTrue(presentation.legendEntries.allSatisfy { !$0.isActive }) + } + func testSparklineTooltipPlacementClampsHorizontally() { let container = CGSize(width: 160, height: 100) let tooltip = SparklineTooltipPlacement.tooltipSize(in: container) From f1902dbf3ec5356c93e1ee4f05a014c13bf78e82 Mon Sep 17 00:00:00 2001 From: Etan Joseph Heyman Date: Fri, 19 Jun 2026 20:26:46 +0300 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20V1=20designed=20dashboard=20?= =?UTF-8?q?=E2=80=94=20palette,=20motion,=202-path=20writes=20taxonomy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/BrainBar/BrainDatabase.swift | 18 +------------ .../BrainBar/Dashboard/PipelineState.swift | 7 +++-- .../Dashboard/SparklineRenderer.swift | 2 +- brain-bar/Sources/BrainBar/DesignTokens.swift | 4 --- .../Tests/BrainBarTests/DashboardTests.swift | 26 ++++++++----------- 5 files changed, 16 insertions(+), 41 deletions(-) diff --git a/brain-bar/Sources/BrainBar/BrainDatabase.swift b/brain-bar/Sources/BrainBar/BrainDatabase.swift index f5f20564..ba765614 100644 --- a/brain-bar/Sources/BrainBar/BrainDatabase.swift +++ b/brain-bar/Sources/BrainBar/BrainDatabase.swift @@ -29,9 +29,8 @@ final class BrainDatabase: @unchecked Sendable { static let maximumTrigramMaintenanceBatchSize = 10_000 private static let defaultPendingStoreMaxLines = 10_000 private static let pendingStoreMaxLinesEnv = "BRAINBAR_PENDING_STORES_MAX_LINES" - private static let agentWriteSourceWhereClause = "COALESCE(LOWER(TRIM(source)), '') IN ('mcp', 'manual', 'precompact-hook')" + private static let agentWriteSourceWhereClause = "COALESCE(LOWER(TRIM(source)), '') IN ('mcp', 'manual', 'digest', 'precompact-hook', 'brain_store')" private static let watcherWriteSourceWhereClause = "COALESCE(LOWER(TRIM(source)), '') IN ('realtime_watcher', 'realtime')" - private static let digestWriteSourceWhereClause = "COALESCE(LOWER(TRIM(source)), '') = 'digest'" private static let lexicalDefenseReplacements: [String: [String]] = [ "hershkovitz": ["Hershkovits"], "hershkovits": ["Hershkovitz"] @@ -100,7 +99,6 @@ final class BrainDatabase: @unchecked Sendable { let recentActivityBuckets: [Int] let recentAgentWriteBuckets: [Int] let recentWatcherWriteBuckets: [Int] - let recentDigestWriteBuckets: [Int] let recentEnrichmentBuckets: [Int] let recentWriteFiveMinuteCount: Int let recentEnrichmentFiveMinuteCount: Int @@ -128,7 +126,6 @@ final class BrainDatabase: @unchecked Sendable { recentActivityBuckets: [Int], recentAgentWriteBuckets: [Int]? = nil, recentWatcherWriteBuckets: [Int]? = nil, - recentDigestWriteBuckets: [Int]? = nil, recentEnrichmentBuckets: [Int], recentWriteFiveMinuteCount: Int = 0, recentEnrichmentFiveMinuteCount: Int = 0, @@ -155,7 +152,6 @@ final class BrainDatabase: @unchecked Sendable { self.recentActivityBuckets = recentActivityBuckets self.recentAgentWriteBuckets = recentAgentWriteBuckets ?? recentActivityBuckets self.recentWatcherWriteBuckets = recentWatcherWriteBuckets ?? Array(repeating: 0, count: recentActivityBuckets.count) - self.recentDigestWriteBuckets = recentDigestWriteBuckets ?? Array(repeating: 0, count: recentActivityBuckets.count) self.recentEnrichmentBuckets = recentEnrichmentBuckets self.recentWriteFiveMinuteCount = recentWriteFiveMinuteCount self.recentEnrichmentFiveMinuteCount = recentEnrichmentFiveMinuteCount @@ -186,10 +182,6 @@ final class BrainDatabase: @unchecked Sendable { recentWatcherWriteBuckets.reduce(0, +) } - var recentDigestWriteCount: Int { - recentDigestWriteBuckets.reduce(0, +) - } - var recentEnrichmentCount: Int { recentEnrichmentBuckets.reduce(0, +) } @@ -275,7 +267,6 @@ final class BrainDatabase: @unchecked Sendable { recentActivityBuckets: recentActivityBuckets, recentAgentWriteBuckets: recentAgentWriteBuckets, recentWatcherWriteBuckets: recentWatcherWriteBuckets, - recentDigestWriteBuckets: recentDigestWriteBuckets, recentEnrichmentBuckets: recentEnrichmentBuckets, recentWriteFiveMinuteCount: recentWriteFiveMinuteCount, recentEnrichmentFiveMinuteCount: recentEnrichmentFiveMinuteCount, @@ -1706,12 +1697,6 @@ final class BrainDatabase: @unchecked Sendable { bucketCount: bucketCount, now: now ) - let recentDigestWriteBuckets = try recentWriteBuckets( - whereClause: Self.digestWriteSourceWhereClause, - activityWindowMinutes: activityWindowMinutes, - bucketCount: bucketCount, - now: now - ) let recentEnrichmentBuckets = try recentEnrichmentBuckets( activityWindowMinutes: activityWindowMinutes, bucketCount: bucketCount, @@ -1730,7 +1715,6 @@ final class BrainDatabase: @unchecked Sendable { recentActivityBuckets: recentActivityBuckets, recentAgentWriteBuckets: recentAgentWriteBuckets, recentWatcherWriteBuckets: recentWatcherWriteBuckets, - recentDigestWriteBuckets: recentDigestWriteBuckets, recentEnrichmentBuckets: recentEnrichmentBuckets, recentWriteFiveMinuteCount: recentWriteFiveMinuteCount, recentEnrichmentFiveMinuteCount: recentEnrichmentFiveMinuteCount, diff --git a/brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift b/brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift index ad0ab8f8..0ff30ac8 100644 --- a/brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift +++ b/brain-bar/Sources/BrainBar/Dashboard/PipelineState.swift @@ -234,7 +234,6 @@ struct DashboardFlowSummary: Sendable, Equatable { let windowLabel = DashboardMetricFormatter.windowLabel(minutes: stats.activityWindowMinutes) let agentStoresColor = BrainBarDesignTokens.Colors.seriesAgent let jsonlWatcherColor = BrainBarDesignTokens.Colors.seriesWatcher - let digestColor = BrainBarDesignTokens.Colors.seriesDigest let enrichmentColor = BrainBarStateTheme.active.theme.color let writesLive = stats.eventIsLive(stats.lastWriteAt, now: now) @@ -339,9 +338,9 @@ struct DashboardFlowSummary: Sendable, Equatable { secondaryValues: stats.recentWatcherWriteBuckets, secondarySeriesLabel: "JSONL watcher", secondaryAccentColor: jsonlWatcherColor, - tertiaryValues: stats.recentDigestWriteBuckets, - tertiarySeriesLabel: "Digest", - tertiaryAccentColor: digestColor + tertiaryValues: [], + tertiarySeriesLabel: nil, + tertiaryAccentColor: nil ), queue: DashboardQueueSummary( status: queueStatus, diff --git a/brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift b/brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift index a85b8bfd..463c38e0 100644 --- a/brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift +++ b/brain-bar/Sources/BrainBar/Dashboard/SparklineRenderer.swift @@ -527,7 +527,7 @@ struct SparklineChart: View { case .secondary: Color.brainBar(nsColor: secondaryAccentColor ?? BrainBarDesignTokens.Colors.seriesWatcher) case .tertiary: - Color.brainBar(nsColor: tertiaryAccentColor ?? BrainBarDesignTokens.Colors.seriesDigest) + Color.brainBar(nsColor: tertiaryAccentColor ?? BrainBarDesignTokens.Colors.signalTrigram) } } diff --git a/brain-bar/Sources/BrainBar/DesignTokens.swift b/brain-bar/Sources/BrainBar/DesignTokens.swift index 1bb4d873..615839ed 100644 --- a/brain-bar/Sources/BrainBar/DesignTokens.swift +++ b/brain-bar/Sources/BrainBar/DesignTokens.swift @@ -34,10 +34,8 @@ enum BrainBarDesignTokens { static let seriesAgent = NSColor.brainBarHex(0x22D3EE) static let seriesWatcher = NSColor.brainBarHex(0xFB7185) - static let seriesDigest = NSColor.brainBarHex(0xA3E635) static let seriesAgentDimmed = NSColor.brainBarHex(0x22D3EE, alpha: 0.35) static let seriesWatcherDimmed = NSColor.brainBarHex(0xFB7185, alpha: 0.35) - static let seriesDigestDimmed = NSColor.brainBarHex(0xA3E635, alpha: 0.35) static let white = NSColor.white static let black = NSColor.black @@ -176,10 +174,8 @@ extension Color { static let brainBarSignalTrigram = Color(nsColor: BrainBarDesignTokens.Colors.signalTrigram) static let brainBarSeriesAgent = Color(nsColor: BrainBarDesignTokens.Colors.seriesAgent) static let brainBarSeriesWatcher = Color(nsColor: BrainBarDesignTokens.Colors.seriesWatcher) - static let brainBarSeriesDigest = Color(nsColor: BrainBarDesignTokens.Colors.seriesDigest) static let brainBarSeriesAgentDimmed = Color(nsColor: BrainBarDesignTokens.Colors.seriesAgentDimmed) static let brainBarSeriesWatcherDimmed = Color(nsColor: BrainBarDesignTokens.Colors.seriesWatcherDimmed) - static let brainBarSeriesDigestDimmed = Color(nsColor: BrainBarDesignTokens.Colors.seriesDigestDimmed) static let brainBarWhite = Color(nsColor: BrainBarDesignTokens.Colors.white) static let brainBarBlack = Color(nsColor: BrainBarDesignTokens.Colors.black) static let brainBarClear = Color.clear diff --git a/brain-bar/Tests/BrainBarTests/DashboardTests.swift b/brain-bar/Tests/BrainBarTests/DashboardTests.swift index 7bd5de8f..2452e493 100644 --- a/brain-bar/Tests/BrainBarTests/DashboardTests.swift +++ b/brain-bar/Tests/BrainBarTests/DashboardTests.swift @@ -242,7 +242,7 @@ final class DashboardTests: XCTestCase { XCTAssertEqual(stats.recentEnrichmentCount, 4) } - func testDashboardStatsSplitsAgentWatcherAndDigestWriteBuckets() throws { + func testDashboardStatsSplitsLiveWritesIntoAgentStoresAndJSONLWatcherPaths() throws { let fixtures: [(id: String, source: String, offset: TimeInterval)] = [ ("agent-27m", "mcp", -27 * 60), ("agent-manual-4m", "manual", -4 * 60), @@ -253,6 +253,8 @@ final class DashboardTests: XCTestCase { ("watcher-4m", "realtime", -4 * 60), ("claude-code-4m", "claude_code", -4 * 60), ("digest-4m", "digest", -4 * 60), + ("youtube-4m", "youtube", -4 * 60), + ("whatsapp-4m", "whatsapp", -4 * 60), ] for fixture in fixtures { _ = try db.store( @@ -271,10 +273,9 @@ final class DashboardTests: XCTestCase { let stats = try db.dashboardStats(activityWindowMinutes: 60, bucketCount: 12) - XCTAssertEqual(stats.recentAgentWriteBuckets, [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2]) + XCTAssertEqual(stats.recentAgentWriteBuckets, [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 3]) XCTAssertEqual(stats.recentWatcherWriteBuckets, [0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]) - XCTAssertEqual(stats.recentDigestWriteBuckets, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]) - XCTAssertEqual(stats.recentActivityBuckets, [0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 6]) + XCTAssertEqual(stats.recentActivityBuckets, [0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 8]) } func testDashboardFlowLabelsWriteSeriesBySourcePath() { @@ -288,7 +289,6 @@ final class DashboardTests: XCTestCase { recentActivityBuckets: [1, 2, 3], recentAgentWriteBuckets: [1, 0, 1], recentWatcherWriteBuckets: [0, 2, 1], - recentDigestWriteBuckets: [0, 0, 1], recentEnrichmentBuckets: [0, 0, 0], activityWindowMinutes: 15 ) @@ -297,7 +297,8 @@ final class DashboardTests: XCTestCase { XCTAssertEqual(summary.ingress.primarySeriesLabel, "Agent stores") XCTAssertEqual(summary.ingress.secondarySeriesLabel, "JSONL watcher") - XCTAssertEqual(summary.ingress.tertiarySeriesLabel, "Digest") + XCTAssertNil(summary.ingress.tertiarySeriesLabel) + XCTAssertTrue(summary.ingress.tertiaryValues.isEmpty) } func testDashboardFlowUsesDistinctV1WriteSeriesPalette() { @@ -311,7 +312,6 @@ final class DashboardTests: XCTestCase { recentActivityBuckets: [1, 2, 3], recentAgentWriteBuckets: [1, 0, 1], recentWatcherWriteBuckets: [0, 2, 1], - recentDigestWriteBuckets: [0, 0, 1], recentEnrichmentBuckets: [0, 0, 0], activityWindowMinutes: 15 ) @@ -320,7 +320,7 @@ final class DashboardTests: XCTestCase { XCTAssertEqual(summary.ingress.accentColor, BrainBarDesignTokens.Colors.seriesAgent) XCTAssertEqual(summary.ingress.secondaryAccentColor, BrainBarDesignTokens.Colors.seriesWatcher) - XCTAssertEqual(summary.ingress.tertiaryAccentColor, BrainBarDesignTokens.Colors.seriesDigest) + XCTAssertNil(summary.ingress.tertiaryAccentColor) } func testDashboardStatsComputesVectorETAFromBacklogAndNetDrain() { @@ -788,17 +788,15 @@ final class DashboardTests: XCTestCase { label: "Writes over 30m", values: [0, 1, 0], secondaryValues: [0, 0, 0], - tertiaryValues: [0, 0, 2], primarySeriesLabel: "Agent stores", secondarySeriesLabel: "JSONL watcher", - tertiarySeriesLabel: "Digest", activityWindowMinutes: 30, fetchedAt: Date(timeIntervalSince1970: 1_764_236_400) ) - XCTAssertEqual(presentation.visibleSeriesLabels, ["Agent stores", "Digest"]) - XCTAssertEqual(presentation.legendEntries.map(\.label), ["Agent stores", "JSONL watcher", "Digest"]) - XCTAssertEqual(presentation.legendEntries.map(\.isActive), [true, false, true]) + XCTAssertEqual(presentation.visibleSeriesLabels, ["Agent stores"]) + XCTAssertEqual(presentation.legendEntries.map(\.label), ["Agent stores", "JSONL watcher"]) + XCTAssertEqual(presentation.legendEntries.map(\.isActive), [true, false]) } func testSparklinePresentationShowsListeningCaptionForColdWriteStart() { @@ -806,10 +804,8 @@ final class DashboardTests: XCTestCase { label: "Writes over 30m", values: [0, 0, 0], secondaryValues: [0, 0, 0], - tertiaryValues: [0, 0, 0], primarySeriesLabel: "Agent stores", secondarySeriesLabel: "JSONL watcher", - tertiarySeriesLabel: "Digest", activityWindowMinutes: 30, fetchedAt: Date(timeIntervalSince1970: 1_764_236_400) )