diff --git a/src/features/packets/payload-renderers.tsx b/src/features/packets/payload-renderers.tsx
index e5575f7..b5bcc6f 100644
--- a/src/features/packets/payload-renderers.tsx
+++ b/src/features/packets/payload-renderers.tsx
@@ -301,16 +301,15 @@ function GroupTextPayload({ payload }: PayloadProps) {
{String(decrypted.sender)}
)}
- {decrypted.message != null && (
+ {decrypted.content != null && (
Message
- {String(decrypted.message)}
+ {String(decrypted.content)}
)}
-
- {decrypted.timestamp != null && {formatUnixTs(decrypted.timestamp as number)}}
- {decrypted.flags != null && Flags {String(decrypted.flags)}}
-
+ {decrypted.sentAt != null && (
+ {formatTimestamp(decrypted.sentAt as number)}
+ )}
) : (
diff --git a/src/features/packets/usePackets.ts b/src/features/packets/usePackets.ts
index 79a90f6..5fc99a9 100644
--- a/src/features/packets/usePackets.ts
+++ b/src/features/packets/usePackets.ts
@@ -143,6 +143,7 @@ export function usePackets() {
firstHeardAt: data.observation.heardAt,
lastHeardAt: data.observation.heardAt,
observationCount: data.packet.observationCount,
+ scope: data.packet.scope,
latestObserver: {
id: data.observation.observerId,
displayName: data.observation.observerName,
diff --git a/src/types/ws.ts b/src/types/ws.ts
index 9349243..2212b3a 100644
--- a/src/types/ws.ts
+++ b/src/types/ws.ts
@@ -41,6 +41,7 @@ export interface WsPacketObservation {
routeTypeName: string;
isFirstObservation: boolean;
observationCount: number;
+ scope?: string; // matched transport scope name; omitted when none matched
};
observation: {
observerId: string;
diff --git a/tests/features/packets/payload-renderers.test.tsx b/tests/features/packets/payload-renderers.test.tsx
index 3f8d20c..f5acacb 100644
--- a/tests/features/packets/payload-renderers.test.tsx
+++ b/tests/features/packets/payload-renderers.test.tsx
@@ -1,6 +1,7 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { PayloadBreakdown } from "../../../src/features/packets/payload-renderers";
+import { formatTimestamp } from "../../../src/lib/formatters";
import type { ResolvedHop } from "../../../src/types/api";
const tracePayload = {
@@ -44,3 +45,30 @@ describe("PayloadBreakdown — trace resolvedRoute overlay", () => {
expect(screen.getByText("-")).toBeInTheDocument();
});
});
+
+describe("PayloadBreakdown — GROUP_TEXT decrypted channel message", () => {
+ // Backend GetPacket enrichment nests decrypted:{sender,content,sentAt} (sentAt is epoch ms).
+ // See tower-server db/packets.go + internal/ingest/side_effects.go.
+ // chosen so the ms reading (5:43 p.m.) and the wrong seconds reading (1:13 p.m.) differ
+ const sentAt = 1_700_001_800_000; // epoch ms
+ const payload = {
+ type: "GROUP_TEXT",
+ channelHash: "ab",
+ cipherMac: "00112233",
+ ciphertext: "deadbeef",
+ decrypted: { sender: "Alice", content: "hello mesh", sentAt },
+ };
+
+ it("renders the decrypted sender and message body from content", () => {
+ render();
+ expect(screen.getByText("Alice")).toBeInTheDocument();
+ expect(screen.getByText("hello mesh")).toBeInTheDocument();
+ });
+
+ it("formats sentAt as epoch milliseconds, not seconds", () => {
+ render();
+ expect(screen.getByText(formatTimestamp(sentAt))).toBeInTheDocument();
+ // the seconds interpretation (×1000) would be a far-future date — must not appear
+ expect(screen.queryByText(formatTimestamp(sentAt * 1000))).not.toBeInTheDocument();
+ });
+});