diff --git a/WorkbenchReplacement/MainView.swift b/WorkbenchReplacement/MainView.swift index 3d9e335..5765ed8 100644 --- a/WorkbenchReplacement/MainView.swift +++ b/WorkbenchReplacement/MainView.swift @@ -2573,9 +2573,9 @@ struct WorkbenchAdministrationView: View { ScrollView(.vertical) { VStack(alignment: .leading, spacing: 14) { - Label("Server overview", systemImage: "server.rack") + Label("Server overview · Live graphs", systemImage: "chart.xyaxis.line") .font(.title3.bold()) - WorkbenchServerOverview(rows: filteredRows) + WorkbenchServerOverview(rows: filteredRows, samples: diagnosticSamples) if page == .serverStatus { Divider() HStack { @@ -4214,22 +4214,68 @@ private struct WorkbenchInlinePerformanceDashboard: View { // the counter ScrollView, so scrolling hundreds of counters cannot hide charts. private struct WorkbenchServerOverview: View { let rows: [[String]] - private let names = ["Uptime", "Threads_connected", "Threads_running", "Queries", "Slow_queries", "Bytes_received", "Bytes_sent", "Connections"] + let samples: [DiagnosticSample] + + private let names = [ + "Uptime", "Threads_connected", "Threads_running", "Queries", + "Slow_queries", "Bytes_received", "Bytes_sent", "Connections" + ] + + private struct OverviewPoint: Identifiable { + let id: Int + let time: Date + let value: Double + } + + private func points(for name: String) -> [OverviewPoint] { + samples.enumerated().compactMap { index, sample in + guard let row = sample.rows.first(where: { + $0.count > 1 && $0[0].caseInsensitiveCompare(name) == .orderedSame + }), let value = Double(row[1]), value.isFinite else { return nil } + return OverviewPoint(id: index, time: sample.at, value: value) + } + } + var body: some View { - LazyVGrid(columns: [GridItem(.adaptive(minimum: 175), spacing: 12)], spacing: 12) { + LazyVGrid(columns: [GridItem(.adaptive(minimum: 260), spacing: 14)], spacing: 14) { ForEach(names, id: \.self) { name in - let raw = rows.first(where: { $0.count >= 2 && $0[0].caseInsensitiveCompare(name) == .orderedSame })?.last ?? "—" + let raw = rows.first(where: { + $0.count >= 2 && $0[0].caseInsensitiveCompare(name) == .orderedSame + })?.last ?? "—" let tint = WorkbenchCounterCharts.tint(for: name) - VStack(alignment: .leading, spacing: 8) { - Text(name.replacingOccurrences(of: "_", with: " ")) - .font(.caption).foregroundStyle(.secondary) - Text(WorkbenchCounterCharts.displayValue(raw, for: name)) - .font(.title3.bold().monospacedDigit()) - .foregroundStyle(tint) + let series = points(for: name) + VStack(alignment: .leading, spacing: 10) { + HStack(alignment: .firstTextBaseline) { + Text(name.replacingOccurrences(of: "_", with: " ")) + .font(.subheadline.weight(.semibold)) + Spacer(minLength: 8) + Text(WorkbenchCounterCharts.displayValue(raw, for: name)) + .font(.headline.monospacedDigit()) + .foregroundStyle(tint) + } + Chart(series) { point in + AreaMark(x: .value("Time", point.time), y: .value("Value", point.value)) + .foregroundStyle(tint.opacity(0.12)) + LineMark(x: .value("Time", point.time), y: .value("Value", point.value)) + .foregroundStyle(tint) + .lineStyle(StrokeStyle(lineWidth: 2.5)) + PointMark(x: .value("Time", point.time), y: .value("Value", point.value)) + .foregroundStyle(tint) + } + .chartYAxis { AxisMarks(position: .leading) } + .frame(height: 135) + .overlay { + if series.isEmpty { + Text("Waiting for live samples…") + .font(.caption) + .foregroundStyle(.secondary) + } + } } - .frame(maxWidth: .infinity, alignment: .leading) .padding(14) - .background(tint.opacity(0.07), in: RoundedRectangle(cornerRadius: 12)) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(nsColor: .controlBackgroundColor), in: RoundedRectangle(cornerRadius: 12)) + .overlay(RoundedRectangle(cornerRadius: 12).stroke(tint.opacity(0.20))) } } } @@ -4407,7 +4453,7 @@ private struct WorkbenchLiveMonitorScreen: View { if page == .serverStatus { Text("Server overview").font(.headline) ScrollView(.vertical) { - WorkbenchServerOverview(rows: rows) + WorkbenchServerOverview(rows: rows, samples: samples) .frame(maxWidth: .infinity, alignment: .leading) } .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/docs/PHASE50-OVERVIEW-GRAPHS.md b/docs/PHASE50-OVERVIEW-GRAPHS.md new file mode 100644 index 0000000..9b9a699 --- /dev/null +++ b/docs/PHASE50-OVERVIEW-GRAPHS.md @@ -0,0 +1,7 @@ +# Phase 50 — Server overview line graphs + +The eight Server Overview statistic cards are replaced with actual Swift Charts time-series graphs. Each graph retains the current numeric value in its heading and plots up to 60 sampled values over time. This applies to Dashboard and Server Status. The six existing live rate charts remain unchanged above the overview graphs. The overview also appears in the other monitoring screens where it was previously used. + +Charts show raw counter trends (cumulative values for Queries, Bytes, Connections, etc.); the six charts above show per-second rates where applicable. Graphs populate as five-second samples arrive. + +Validation: `swiftc -frontend -parse WorkbenchReplacement/MainView.swift` passed in Linux. Native macOS compilation and UI testing have not been performed.