From 84ffe92263e05ddb2a095a33a49de520396cd127 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 20 Mar 2026 10:46:15 +0100 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9C=A8=20Add=20hex=20view=20for=20messag?= =?UTF-8?q?e=20body?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a hex dump viewer to the message body view alongside the existing JSON/XML formatted view. Features: - Virtualized rendering for large payloads (only visible rows in DOM) - Responsive layout: bytes per row adapts to container width (8-48) - 8-byte lane grouping with dotted vertical separators - Selection sync between hex bytes and ASCII text panels - Null bytes (0x00) and non-printable chars styled in gray - Encoding label (US-ASCII) shown in toolbar - Copy to clipboard (selection or first 64KB hex dump) - Raw bytes stored alongside formatted text in MessageStore --- .../src/components/messages/BodyView.vue | 53 ++- .../src/components/messages/HexView.vue | 415 ++++++++++++++++++ src/Frontend/src/stores/MessageStore.ts | 8 +- .../recoverability-available.ts | 17 +- 4 files changed, 487 insertions(+), 6 deletions(-) create mode 100644 src/Frontend/src/components/messages/HexView.vue diff --git a/src/Frontend/src/components/messages/BodyView.vue b/src/Frontend/src/components/messages/BodyView.vue index ceb1e8d8bb..61055e476a 100644 --- a/src/Frontend/src/components/messages/BodyView.vue +++ b/src/Frontend/src/components/messages/BodyView.vue @@ -1,13 +1,17 @@ @@ -38,4 +50,39 @@ const body = computed(() => bodyState.value.data.value); .gap { margin-top: 5px; } + +.view-mode-toggle { + display: flex; + gap: 0; + margin-bottom: 5px; +} + +.toggle-btn { + padding: 4px 14px; + border: 1px solid #ccc; + background: #f3f3f3; + color: #333; + font-size: 13px; + cursor: pointer; + transition: background-color 0.15s, border-color 0.15s; +} + +.toggle-btn:first-child { + border-radius: 4px 0 0 4px; +} + +.toggle-btn:last-child { + border-radius: 0 4px 4px 0; + border-left: none; +} + +.toggle-btn:hover { + background: #e6e6e6; +} + +.toggle-btn.active { + background: var(--sp-blue, #00a3c4); + color: white; + border-color: var(--sp-blue, #00a3c4); +} diff --git a/src/Frontend/src/components/messages/HexView.vue b/src/Frontend/src/components/messages/HexView.vue new file mode 100644 index 0000000000..17e1203282 --- /dev/null +++ b/src/Frontend/src/components/messages/HexView.vue @@ -0,0 +1,415 @@ + + + + + diff --git a/src/Frontend/src/stores/MessageStore.ts b/src/Frontend/src/stores/MessageStore.ts index 447b9d3e43..134c0fa759 100644 --- a/src/Frontend/src/stores/MessageStore.ts +++ b/src/Frontend/src/stores/MessageStore.ts @@ -64,7 +64,7 @@ interface Model { export const useMessageStore = defineStore("MessageStore", () => { const headers = ref>({ data: [] }); - const body = ref>({ data: {} }); + const body = ref>({ data: {} }); const state = reactive>({ data: { failure_metadata: {}, failure_status: {}, dialog_status: {}, invoked_saga: {} } }); const edit_and_retry_config = ref({ enabled: false, locked_headers: [], sensitive_headers: [] }); const conversationData = ref>({ data: [] }); @@ -227,7 +227,11 @@ export const useMessageStore = defineStore("MessageStore", () => { const contentType = response.headers.get("content-type"); body.value.data.content_type = contentType ?? "text/plain"; - body.value.data.value = await response.text(); + + const arrayBuffer = await response.arrayBuffer(); + body.value.data.rawBytes = new Uint8Array(arrayBuffer); + const charset = contentType?.match(/charset=([^\s;]+)/i)?.[1] ?? "utf-8"; + body.value.data.value = new TextDecoder(charset).decode(arrayBuffer); if (contentType === "application/json") { body.value.data.value = stringify(parse(body.value.data.value), null, 2) ?? body.value.data.value; diff --git a/src/Frontend/test/mocks/scenarios/recoverability/recoverability-available.ts b/src/Frontend/test/mocks/scenarios/recoverability/recoverability-available.ts index a442c83331..9079e4c122 100644 --- a/src/Frontend/test/mocks/scenarios/recoverability/recoverability-available.ts +++ b/src/Frontend/test/mocks/scenarios/recoverability/recoverability-available.ts @@ -12,6 +12,7 @@ * 2. Navigate to Failed Messages view * 3. Recoverability capability card should show "Available" status * 4. Failed message recovery features should work + * 5. Click a failed message → Body tab → toggle "Hex" to see hex view */ import { createScenario } from "../scenario-helper"; import * as precondition from "../../../preconditions"; @@ -19,4 +20,18 @@ import * as precondition from "../../../preconditions"; const { worker, runScenario } = createScenario(); export { worker }; -export const setupComplete = runScenario(precondition.scenarioRecoverabilityAvailable); +export const setupComplete = runScenario(async ({ driver }) => { + await driver.setUp(precondition.scenarioRecoverabilityAvailable); + + // Add a failed message with body so the Body tab (and Hex view) can be tested + // Note: withGroupId and withMessageId must match because the mock's body_url + // uses the groupId but the body endpoint handler uses the messageId + await driver.setUp( + precondition.hasFailedMessage({ + withGroupId: "hex-test-1", + withMessageId: "hex-test-1", + withContentType: "application/json", + withBody: { orderId: 12345, customerName: "Alice", amount: 99.95, shipped: false, notes: "express delivery" }, + }) + ); +}); From a011879790d520f7e2f5e59dd04b5f150070957a Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 20 Mar 2026 10:48:26 +0100 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=90=9B=20Fix=20ASCII=20lane=20separat?= =?UTF-8?q?or=20adding=20visible=20space=20in=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the 0.5ch-wide inline-block separator between 8-char groups in the ASCII column with a thin dotted left-border on the first char of each lane. This prevents misleading spaces in the text (e.g. "orderI d" instead of "orderId") while keeping the visual grouping. --- .../src/components/messages/HexView.vue | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/Frontend/src/components/messages/HexView.vue b/src/Frontend/src/components/messages/HexView.vue index 17e1203282..55e616d2dd 100644 --- a/src/Frontend/src/components/messages/HexView.vue +++ b/src/Frontend/src/components/messages/HexView.vue @@ -29,8 +29,8 @@ const bytesPerRow = computed(() => { // Try multiples of LANE_SIZE from large to small for (const bpr of [48, 40, 32, 24, 16]) { const lanes = bpr / LANE_SIZE; - // offset: 10ch, hex: bpr*3ch + (lanes-1)*1.5ch, sep: 3ch, ascii: bpr*1ch + (lanes-1)*0.5ch, margin: 2ch - const needed = (10 + bpr * 3 + (lanes - 1) * 1.5 + 3 + bpr + (lanes - 1) * 0.5 + 2) * ch; + // offset: 10ch, hex: bpr*3ch + (lanes-1)*1.5ch, sep: 3ch, ascii: bpr*1ch, margin: 2ch + const needed = (10 + bpr * 3 + (lanes - 1) * 1.5 + 3 + bpr + 2) * ch; if (availablePx >= needed) return bpr; } return 8; @@ -258,16 +258,15 @@ const copyText = computed(() => { - + {{ char.display }} @@ -391,10 +390,9 @@ const copyText = computed(() => { background-color: #e8f0fe; } -/* Lane separator in ASCII area */ -.ascii-lane-sep { - display: inline-block; - width: 0.5ch; +/* Lane boundary marker in ASCII area — thin border, no added width */ +.ascii-char.lane-start { + border-left: 1px dotted #d0d0d0; } /* Null bytes (0x00): dim gray */ From a821e29cff1523de0a27261b05e6ad82f63237a2 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 20 Mar 2026 12:36:22 +0100 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=90=9B=20Auto-switch=20to=20hex=20vie?= =?UTF-8?q?w=20when=20message=20body=20parsing=20fails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap JSON/XML formatting in try/catch so parse failures are surfaced via a `parse_failed` flag instead of crashing. When detected, the UI automatically switches to hex view and shows a warning banner. --- .../src/components/messages/BodyView.vue | 6 ++++++ src/Frontend/src/stores/MessageStore.ts | 16 ++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Frontend/src/components/messages/BodyView.vue b/src/Frontend/src/components/messages/BodyView.vue index 61055e476a..28d49e1660 100644 --- a/src/Frontend/src/components/messages/BodyView.vue +++ b/src/Frontend/src/components/messages/BodyView.vue @@ -23,6 +23,11 @@ watch( const contentType = computed(() => parseContentType(bodyState.value.data.content_type)); const body = computed(() => bodyState.value.data.value); const rawBytes = computed(() => bodyState.value.data.rawBytes); +const parseFailed = computed(() => bodyState.value.data.parse_failed); + +watch(parseFailed, (failed) => { + if (failed) viewMode.value = "hex"; +}, { immediate: true });