Files
tomas.kracmarandClaude Sonnet 5 77e3eeaf7b Release 1.9.0
- Sidebar-navigation redesign: NavigationSplitView with Routes / Schedule /
  Activity Log replaces the 2x2 panel-card grid. Primary actions moved to
  the toolbar (dry-run/write, sync status, Sync Now); Export/Import/Reveal
  Log/Cleanup/Refresh/Recheck Permission moved to a toolbar overflow menu.
- Routes are now a collapsed-by-default list -- click a row to expand its
  editor instead of always showing every field for every route.
- Schedule view leads with live auto-sync status; the manual launchd
  schedule is demoted to an explicit optional section underneath.
- Activity Log is readable rows (status icon + text) with a search filter
  and a Clear button, instead of a monospaced text dump.
- Menu bar dropdown got icons and real Cmd+,/Cmd+Q shortcuts.
- Explored as a design mockup first (published as an Artifact, light+dark),
  direction confirmed before writing any SwiftUI.

Fixed along the way: routes were being silently dropped on most launches.
reloadCalendars() pruned routes against a freshly-created EKEventStore's
calendar fetch right after a permission grant, which can under-report
calendars for a moment (especially remote/Exchange accounts) before they
finish loading -- wrongly concluding a route's calendar was gone. Only
survived because of a routes.v1 legacy backup key that itself never got
updated after its first write, so any real route edit made in an affected
session would have been silently reverted on the next launch. Fixed by
skipping the prune specifically on the post-grant reload
(reloadCalendars(pruneRoutes: false)) and keeping the legacy backup current
on every save instead of frozen.

All unit tests pass. Verified on a real install: routes stopped
disappearing across repeated launches (checked via --status and reading
the UserDefaults plist directly).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 09:37:21 +02:00

108 lines
4.0 KiB
Swift

import SwiftUI
/// Activity log viewer — extracted from ContentView. Renders `logText`
/// (plain newline-separated lines, no timestamps — those only exist in the
/// persistent file log written by AppLogStore) as readable rows with a
/// status icon per line instead of a raw monospaced dump, plus a search
/// filter. "Clear" only empties this in-memory view; the file log on disk
/// (Reveal Log File, in the toolbar's overflow menu) is untouched.
struct LogSectionView: View {
@Binding var logText: String
@State private var searchText = ""
private enum LineKind {
case ok, error, warning, info
var symbol: String {
switch self {
case .ok: return "checkmark.circle.fill"
case .error: return "xmark.circle.fill"
case .warning: return "exclamationmark.triangle.fill"
case .info: return "circle.fill"
}
}
var color: Color {
switch self {
case .ok: return .green
case .error: return .red
case .warning: return .orange
case .info: return .secondary
}
}
}
private struct Line: Identifiable {
let id: Int
let text: String
let kind: LineKind
}
private var lines: [Line] {
logText.split(separator: "\n", omittingEmptySubsequences: false)
.enumerated()
.map { idx, raw in
let text = String(raw)
let kind: LineKind
if text.hasPrefix("✓") { kind = .ok }
else if text.hasPrefix("✗") { kind = .error }
else if text.contains("SKIP") || text.contains("WARN") { kind = .warning }
else { kind = .info }
return Line(id: idx, text: text, kind: kind)
}
.filter { !$0.text.isEmpty }
.filter { searchText.isEmpty || $0.text.localizedCaseInsensitiveContains(searchText) }
}
var body: some View {
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 10) {
Text("Activity Log")
.font(.title2.weight(.bold))
Spacer()
HStack(spacing: 6) {
Image(systemName: "magnifyingglass")
.foregroundStyle(.secondary)
TextField("Search log", text: $searchText)
.textFieldStyle(.plain)
}
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 5)
.frame(width: 180)
.background(
RoundedRectangle(cornerRadius: 7, style: .continuous)
.fill(Color.primary.opacity(0.06))
)
Button("Clear") { logText = "" }
.buttonStyle(.bordered)
.disabled(logText.isEmpty)
}
ScrollView {
LazyVStack(alignment: .leading, spacing: 0) {
ForEach(lines) { line in
HStack(alignment: .top, spacing: 10) {
Image(systemName: line.kind.symbol)
.font(.system(size: 10))
.foregroundStyle(line.kind.color)
.padding(.top, 3)
Text(line.text)
.font(.system(.body, design: .monospaced))
.textSelection(.enabled)
Spacer(minLength: 0)
}
.padding(.vertical, 5)
Divider().opacity(0.4)
}
}
}
.frame(maxHeight: .infinity)
.overlay(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.stroke(Color.secondary.opacity(0.22))
)
}
.padding(20)
}
}