Refresh ops center in realtime

This commit is contained in:
kris
2026-04-07 16:04:18 +08:00
parent a42e5b75dc
commit 0b0bc5152f
7 changed files with 320 additions and 5 deletions

View File

@@ -9,8 +9,15 @@ import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.LinkedHashMap;
import java.util.Map;
public class OpsCenterActivity extends BossScreenActivity {
private static final long REALTIME_RELOAD_THROTTLE_MS = 900L;
private LinearLayout contentRoot;
private @Nullable BossRealtimeClient realtimeClient;
private final Map<String, Long> recentRealtimeEventTimestamps = new LinkedHashMap<>();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -20,9 +27,28 @@ public class OpsCenterActivity extends BossScreenActivity {
contentRoot = new LinearLayout(this);
contentRoot.setOrientation(LinearLayout.VERTICAL);
replaceContent(contentRoot);
realtimeClient = new BossRealtimeClient(apiClient, this::handleRealtimeEvent);
reload();
}
@Override
protected void onResume() {
super.onResume();
updateRealtimeSubscription();
}
@Override
protected void onPause() {
stopRealtimeUpdates();
super.onPause();
}
@Override
protected void onDestroy() {
stopRealtimeUpdates();
super.onDestroy();
}
@Override
protected void reload() {
setRefreshing(true);
@@ -42,6 +68,63 @@ public class OpsCenterActivity extends BossScreenActivity {
});
}
private void updateRealtimeSubscription() {
if (apiClient != null && apiClient.hasSessionHints() && realtimeClient != null) {
realtimeClient.start();
return;
}
stopRealtimeUpdates();
}
private void stopRealtimeUpdates() {
if (realtimeClient != null) {
realtimeClient.stop();
}
}
void handleRealtimeEvent(BossRealtimeEvent event) {
if (event == null || event.eventName.isEmpty()) {
return;
}
if (!shouldReloadForRealtimeEvent(event)) {
return;
}
String eventFingerprint = BossRealtimeClient.buildEventFingerprint(event);
if (eventFingerprint.isEmpty()) {
return;
}
long now = System.currentTimeMillis();
if (isDuplicateRealtimeEvent(eventFingerprint, now)) {
return;
}
runOnUiThread(this::reload);
}
private boolean shouldReloadForRealtimeEvent(BossRealtimeEvent event) {
return "app.logs.updated".equals(event.eventName)
|| "project.context_risk.updated".equals(event.eventName);
}
private boolean isDuplicateRealtimeEvent(String eventFingerprint, long now) {
pruneRecentRealtimeEvents(now);
Long previousEventAt = recentRealtimeEventTimestamps.get(eventFingerprint);
if (previousEventAt != null && now - previousEventAt < REALTIME_RELOAD_THROTTLE_MS) {
return true;
}
recentRealtimeEventTimestamps.put(eventFingerprint, now);
return false;
}
private void pruneRecentRealtimeEvents(long now) {
java.util.Iterator<Map.Entry<String, Long>> iterator = recentRealtimeEventTimestamps.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Long> entry = iterator.next();
if (now - entry.getValue() >= REALTIME_RELOAD_THROTTLE_MS) {
iterator.remove();
}
}
}
private void render(JSONObject ops) {
replaceContent(contentRoot);
contentRoot.removeAllViews();

View File

@@ -0,0 +1,101 @@
package com.hyzq.boss;
import static org.junit.Assert.assertEquals;
import android.content.Intent;
import org.json.JSONObject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 34)
public class OpsCenterActivityTest {
@Test
public void appLogsUpdatedEventTriggersReload() throws Exception {
TestOpsCenterActivity activity = Robolectric
.buildActivity(TestOpsCenterActivity.class, new Intent())
.setup()
.resume()
.get();
activity.reloadEnabled = true;
activity.reloadCount = 0;
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
ReflectionHelpers.ClassParameter.from(
BossRealtimeEvent.class,
new BossRealtimeEvent("app.logs.updated", new JSONObject().put("deviceId", "mac-studio"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
assertEquals(1, activity.reloadCount);
}
@Test
public void projectContextRiskUpdatedEventTriggersReload() throws Exception {
TestOpsCenterActivity activity = Robolectric
.buildActivity(TestOpsCenterActivity.class, new Intent())
.setup()
.resume()
.get();
activity.reloadEnabled = true;
activity.reloadCount = 0;
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
ReflectionHelpers.ClassParameter.from(
BossRealtimeEvent.class,
new BossRealtimeEvent("project.context_risk.updated", new JSONObject().put("status", "verified"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
assertEquals(1, activity.reloadCount);
}
@Test
public void unrelatedConversationEventDoesNotTriggerReload() throws Exception {
TestOpsCenterActivity activity = Robolectric
.buildActivity(TestOpsCenterActivity.class, new Intent())
.setup()
.resume()
.get();
activity.reloadEnabled = true;
activity.reloadCount = 0;
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
ReflectionHelpers.ClassParameter.from(
BossRealtimeEvent.class,
new BossRealtimeEvent("conversation.updated", new JSONObject().put("projectId", "project-1"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
assertEquals(0, activity.reloadCount);
}
public static class TestOpsCenterActivity extends OpsCenterActivity {
private boolean reloadEnabled;
private int reloadCount;
@Override
protected void reload() {
if (!reloadEnabled) {
return;
}
reloadCount += 1;
setRefreshing(false);
}
}
}