Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
03d2ffe
feat(apple): add realtime protocol client to SitrepKit
QuintinShaw Jul 19, 2026
6124df8
test(apple): cover realtime state convergence
QuintinShaw Jul 19, 2026
7e960b1
feat(apple): synchronize foreground state over websocket
QuintinShaw Jul 19, 2026
88e79ea
feat(daemon): implement realtime protocol wire types
QuintinShaw Jul 19, 2026
b938c73
feat(daemon): add crash-safe outbox for reliable realtime events
QuintinShaw Jul 19, 2026
941828a
feat(daemon): batch ephemeral metric frames
QuintinShaw Jul 19, 2026
8846b81
fix(apple): target /v3 realtime endpoint
QuintinShaw Jul 19, 2026
26859ff
feat(daemon): add reliable realtime uplink client
QuintinShaw Jul 19, 2026
6a396cb
feat(daemon): integrate realtime uplink behind an opt-in flag
QuintinShaw Jul 19, 2026
04b9a7c
fix(daemon): target /v3 realtime endpoint
QuintinShaw Jul 19, 2026
e7523fc
feat(server): add hibernatable realtime space hub
QuintinShaw Jul 19, 2026
a844770
feat(server): route /v3/realtime upgrades
QuintinShaw Jul 19, 2026
a1788fa
test(server): cover realtime recovery and deduplication
QuintinShaw Jul 19, 2026
fdbae77
perf(server): sample realtime observability
QuintinShaw Jul 19, 2026
44a78ea
docs(server): document realtime cost model
QuintinShaw Jul 19, 2026
951b7e8
fix(apple): harden realtime client fault handling
QuintinShaw Jul 19, 2026
b9e2174
fix(daemon): validate metric_id grammar in metric samples
QuintinShaw Jul 19, 2026
12e98bd
feat(daemon): bound the realtime outbox with HTTP fallback
QuintinShaw Jul 19, 2026
cf4d47c
fix(apple): keep live delta state authoritative over http refresh
QuintinShaw Jul 19, 2026
b0a7bbf
test(apple): pin fixture rejection reasons and add realtime regressions
QuintinShaw Jul 19, 2026
de08949
fix(daemon): stop reconnecting after fatal handshake rejection
QuintinShaw Jul 19, 2026
53d59dd
fix(daemon): reject acks carrying foreign device pairs
QuintinShaw Jul 19, 2026
40ed17a
test(daemon): pin throttle state persistence across reconnect
QuintinShaw Jul 19, 2026
f342ec7
fix(server): reject duplicate protocol_versions and topics
QuintinShaw Jul 19, 2026
04464af
fix(server): degrade unexpected realtime errors to internal_error
QuintinShaw Jul 19, 2026
05fc784
feat(server): sync interest state to sources on connect
QuintinShaw Jul 19, 2026
4dd70c8
fix(server): share metric staleness baseline across connections
QuintinShaw Jul 19, 2026
4ec4a0c
fix(server): harden v3 automation control plane
QuintinShaw Jul 19, 2026
d13eee3
docs(server): record intentional protocol omissions
QuintinShaw Jul 19, 2026
a92d9c9
fix(apple): scope connection resources to their run generation
QuintinShaw Jul 19, 2026
fdac903
fix(apple): decide http overwrites on the actor's authoritative phase
QuintinShaw Jul 19, 2026
b85a6da
fix(server): keep server-chosen automation ids idempotent
QuintinShaw Jul 19, 2026
0c82e0c
Merge branch 'feat/realtime-daemon' into integration/realtime
QuintinShaw Jul 19, 2026
4e88fe1
Merge branch 'feat/realtime-apple' into integration/realtime
QuintinShaw Jul 19, 2026
15aaabf
test(daemon): add opt-in E2E source lifecycle test against a real server
QuintinShaw Jul 19, 2026
86b115d
test(integration): add node viewer E2E smoke scripts
QuintinShaw Jul 19, 2026
5c8c88c
docs(protocol): record realtime cross-line integration verification
QuintinShaw Jul 19, 2026
6677acb
fix(server): log superseded connections as security events
QuintinShaw Jul 19, 2026
2e5cad7
fix(daemon): serialize realtime sends to preserve device_seq wire order
QuintinShaw Jul 19, 2026
ee87232
merge: server superseded security logging fix
QuintinShaw Jul 19, 2026
fb5424d
merge: daemon device_seq wire-order fix
QuintinShaw Jul 19, 2026
595452c
feat(daemon): wire realtime outbox row cap to config
QuintinShaw Jul 19, 2026
a0dc30d
feat(server): add realtime_enabled kill switch to /v2/snapshot
QuintinShaw Jul 19, 2026
569eb58
fix(daemon): stop forking reliable events to /v2 on outbox-full
QuintinShaw Jul 19, 2026
0a0bb07
fix(server): configure explicit observability log sampling
QuintinShaw Jul 19, 2026
e890d50
feat(apple): decode realtime_enabled and add capability gate logic
QuintinShaw Jul 19, 2026
2c48258
fix(apple): gate realtime auto-connect on server capability (P0)
QuintinShaw Jul 19, 2026
bd4d46a
fix(server): version-gate SpaceHub schema DDL against hibernation wakes
QuintinShaw Jul 19, 2026
1b5234e
fix(server): cap metricsCache cardinality with LRU eviction
QuintinShaw Jul 19, 2026
a734dd7
fix(server): log superseded events before the wire reply, not after
QuintinShaw Jul 19, 2026
cddea7a
merge: server pre-merge review fixes (capability flag, observability,…
QuintinShaw Jul 19, 2026
e138d94
merge: daemon outbox backpressure fix (no /v2 fork)
QuintinShaw Jul 19, 2026
31dd499
merge: apple realtime capability gate
QuintinShaw Jul 19, 2026
27474ac
fix(server): strictly parse REALTIME_ENABLED instead of Boolean()
QuintinShaw Jul 19, 2026
80b01f5
fix(server): don't swallow real SQL errors as fresh-DO in schemaVersion
QuintinShaw Jul 19, 2026
f781e5c
fix(daemon): wrap validation failures in a retryable-vs-permanent sen…
QuintinShaw Jul 19, 2026
1efdac6
fix(daemon): serialize reliable retries against fresh Offer()s
QuintinShaw Jul 19, 2026
af85251
merge: server strict flag parsing + schemaVersion error handling
QuintinShaw Jul 19, 2026
971e1ef
merge: daemon rtRetry FIFO serialization + transient error retry
QuintinShaw Jul 19, 2026
e1b87b1
fix(server): gate /v3/realtime and /v3/automations on REALTIME_ENABLED
QuintinShaw Jul 19, 2026
fd3bb11
test(server): cover the REALTIME_ENABLED gate on /v3/realtime and /v3…
QuintinShaw Jul 19, 2026
077c926
fix(daemon): persist overflowed reliable events to a durable outbox_o…
QuintinShaw Jul 19, 2026
16c942b
fix(daemon): replace in-memory reliable-event retry with durable-over…
QuintinShaw Jul 19, 2026
1114ff9
fix(daemon): obey server's realtime capability, not just the local flag
QuintinShaw Jul 19, 2026
bd5908e
merge: server gate /v3 routes on REALTIME_ENABLED
QuintinShaw Jul 19, 2026
5a4d551
merge: daemon durable overflow + obey server realtime capability
QuintinShaw Jul 19, 2026
54f13f1
fix(daemon): serialize legacy-mode outbox drain on the shared realtim…
QuintinShaw Jul 19, 2026
f8d635b
test(daemon): regress duplicate-delivery bug in shared-Client legacy …
QuintinShaw Jul 19, 2026
90c9562
fix(daemon): stop rtclient.Client from residual-retrying overflowed e…
QuintinShaw Jul 19, 2026
f3c9523
test(daemon): cover overflow promotion across a genuine process restart
QuintinShaw Jul 19, 2026
edad342
test(daemon): regress Enqueue's retry loop against real SQLite lock c…
QuintinShaw Jul 19, 2026
9478f2c
merge: daemon serialize legacy drain on shared Client + overflow retr…
QuintinShaw Jul 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 87 additions & 6 deletions apple/SitrepApp/Sitrep/MainTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,16 @@ struct MainTabView: View {
var body: some View {
configuredTabs
.safeAreaInset(edge: .top, spacing: 0) {
// Honesty first: a monitoring tool must SAY when it can't see.
if let err = model.lastError {
ErrorBanner(text: err)
VStack(spacing: 0) {
// Honesty first: a monitoring tool must SAY when it can't see.
if let err = model.lastError {
ErrorBanner(text: err)
} else if let status = SyncStatusStrip.label(for: model.connectionPhase) {
SyncStatusStrip(color: status.color, text: status.text)
}
if model.supersededNotice {
SupersededBanner(isPresented: $model.supersededNotice)
}
}
}
}
Expand Down Expand Up @@ -66,12 +73,22 @@ struct MainTabView: View {
.onReceive(NotificationCenter.default.publisher(for: .sitrepOpenEvents)) { _ in
selection = 2
}
// Coming back to foreground: refresh immediately and revive the
// poll loop if anything killed it while suspended.
// Coming back to foreground: refresh immediately, (re)connect the
// realtime channel, and revive the HTTP fallback poll if anything
// killed it while suspended. While the WebSocket is live, refresh()
// only updates REST-only state (presence) — the reliable collections
// stay owned by deltas. Leaving the foreground closes the realtime
// connection — the interest lease simply lapses server-side rather
// than being explicitly released.
.onChange(of: scenePhase) { _, phase in
if phase == .active {
switch phase {
case .active:
model.ensurePolling()
Task { await model.refresh() }
case .background:
model.leaveBackground()
default:
break
}
}
}
Expand All @@ -94,6 +111,70 @@ struct ErrorBanner: View {
}
}

/// A subtle, transient strip about THIS PHONE's realtime connection to the
/// server — distinct from `PresencePill`/`PresenceRow`, which report
/// whether the Mac/Agent is reporting in. Reuses the same dot-plus-caption
/// language rather than a new visual system; shows nothing once the
/// connection is `.live` (the common case), so it stays out of the way.
struct SyncStatusStrip: View {
let color: Color
let text: String

/// nil means "healthy, show nothing" — only `.live` and `.idle` (not yet
/// started) count as nothing-to-say; every other phase is transient and
/// worth a one-line, low-emphasis mention.
static func label(for phase: RealtimeClient.Phase) -> (color: Color, text: String)? {
switch phase {
case .idle, .live: nil
case .connecting, .handshaking: (.gray, "正在连接实时同步…")
case .subscribed: (.gray, "正在同步…")
case .failed: (.orange, "实时同步中断,已切换到低频刷新")
}
}

var body: some View {
HStack(spacing: 6) {
Circle().fill(color).frame(width: 6, height: 6)
Text(text).font(.caption2.weight(.medium)).foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 4)
.background(.background.secondary)
.transition(.move(edge: .top).combined(with: .opacity))
.animation(.default, value: text)
}
}

/// One-shot, non-disruptive notice for SPEC.md §9.4 `superseded`: this
/// device's realtime connection was replaced by another connection using
/// the same credential — most often the same phone's own connection racing
/// itself across a fast background/foreground cycle, but possibly a sign
/// the credential is in use elsewhere. Tap to dismiss; not repeated.
struct SupersededBanner: View {
@Binding var isPresented: Bool

var body: some View {
Button {
withAnimation { isPresented = false }
} label: {
Label("这台设备的实时连接被另一个连接取代,可能是该凭据在别处也在使用", systemImage: "info.circle")
.font(.caption.weight(.medium))
.lineLimit(2)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 12)
.padding(.vertical, 7)
}
.buttonStyle(.plain)
.background(.blue.opacity(0.12))
.foregroundStyle(.blue)
.transition(.move(edge: .top).combined(with: .opacity))
.task {
try? await Task.sleep(for: .seconds(6))
withAnimation { isPresented = false }
}
}
}

/// "Is my Mac alive" — the first question a monitoring tool must answer.
/// Green: agent heartbeat or ingest within a minute. Orange: minutes quiet.
/// Red: nothing for 10+ minutes.
Expand Down
Loading
Loading