From 44862f65d2388e19b70a03409f1c16195e8f9342 Mon Sep 17 00:00:00 2001 From: Sergey Savelyev Date: Sat, 3 Jan 2026 18:24:40 -0800 Subject: [PATCH] Transfer Panel Details Without an Extra Layer of JSON Encoding (#14) **Rationale:** This made it harder to snoop on the traffic in the network monitor because it was encoded as a string. **Changes:** - Backend now accepts & provides the panel data as a JSON object rather than as a string - Backend now supports compression - Minor improvements to error handling - Some panel structures were getting saved in the JSON when they weren't supposed to be (now this no longer happens) Reviewed-on: https://gitea.sergeysav.com/sergeysav/telemetry_visualization/pulls/14 Co-authored-by: Sergey Savelyev Co-committed-by: Sergey Savelyev --- api/src/client/mod.rs | 1 - frontend/src/components/CommandInput.vue | 1 - .../CommandParameterListConfigurator.vue | 15 ++++ frontend/src/components/CommandSender.vue | 7 +- frontend/src/components/TelemetryLine.vue | 10 ++- frontend/src/composables/command.ts | 16 +++-- frontend/src/composables/json.ts | 20 +++--- frontend/src/composables/telemetry.ts | 21 ++++-- frontend/src/panels/panel.ts | 7 +- frontend/src/views/PanelEditorView.vue | 72 ++++++++++++++++--- frontend/src/views/PanelView.vue | 8 +-- server/src/http/api/panels.rs | 2 +- server/src/http/mod.rs | 3 +- server/src/panels/mod.rs | 4 +- server/src/panels/panel.rs | 5 +- 15 files changed, 149 insertions(+), 43 deletions(-) diff --git a/api/src/client/mod.rs b/api/src/client/mod.rs index e4ac66b..ae89d0d 100644 --- a/api/src/client/mod.rs +++ b/api/src/client/mod.rs @@ -193,7 +193,6 @@ impl Client { break; } } - println!("Exited Loop"); }); Ok(outer_rx) diff --git a/frontend/src/components/CommandInput.vue b/frontend/src/components/CommandInput.vue index 11d6644..3abb66d 100644 --- a/frontend/src/components/CommandInput.vue +++ b/frontend/src/components/CommandInput.vue @@ -49,7 +49,6 @@ onMounted(() => { model.value = 0.0; } } else if (is_boolean.value) { - debugger; model.value = false; } } diff --git a/frontend/src/components/CommandParameterListConfigurator.vue b/frontend/src/components/CommandParameterListConfigurator.vue index f5a76c0..e97f907 100644 --- a/frontend/src/components/CommandParameterListConfigurator.vue +++ b/frontend/src/components/CommandParameterListConfigurator.vue @@ -76,6 +76,21 @@ watch([command_info], ([cmd_info]) => { value: default_value, }; } + // Perform some cleanup to remove data that shouldn't be there + switch (model_param_value.type) { + case 'constant': + model_param_value = { + type: model_param_value.type, + value: model_param_value.value, + }; + break; + case 'input': + model_param_value = { + type: model_param_value.type, + id: model_param_value.id, + }; + break; + } model_value[param.name] = model_param_value; } model.value = model_value; diff --git a/frontend/src/components/CommandSender.vue b/frontend/src/components/CommandSender.vue index 42096af..d4ac9a6 100644 --- a/frontend/src/components/CommandSender.vue +++ b/frontend/src/components/CommandSender.vue @@ -4,7 +4,7 @@ import { ref } from 'vue'; import CommandParameter from '@/components/CommandParameter.vue'; import FlexDivider from '@/components/FlexDivider.vue'; import type { DynamicDataType } from '@/composables/dynamic.ts'; -import { toJsonString } from '@/composables/json.ts'; +import { parseJsonString, toJsonString } from '@/composables/json.ts'; const props = defineProps<{ command: CommandDefinition | null; @@ -30,10 +30,11 @@ async function sendCommand() { }, body: toJsonString(params), }); + const text_response = await response.text(); if (response.ok) { - result.value = await response.json(); + result.value = parseJsonString(text_response); } else { - result.value = await response.text(); + result.value = text_response; } busy.value = false; } diff --git a/frontend/src/components/TelemetryLine.vue b/frontend/src/components/TelemetryLine.vue index ef2fda7..f265df0 100644 --- a/frontend/src/components/TelemetryLine.vue +++ b/frontend/src/components/TelemetryLine.vue @@ -28,6 +28,7 @@ import { isBooleanType, isNumericType, } from '@/composables/dynamic.ts'; +import { parseJsonString } from '@/composables/json.ts'; const props = defineProps<{ data: string; @@ -138,7 +139,14 @@ watch( const res = await fetch( `/api/tlm/history/${uuid}?from=${min_x.toISOString()}&to=${max_x.toISOString()}&resolution=${data_min_sep.value}`, ); - const response = (await res.json()) as TelemetryDataItem[]; + const text_response = await res.text(); + if (!res.ok) { + console.error(text_response); + return; + } + const response = parseJsonString( + text_response, + ) as TelemetryDataItem[]; for (const data_item of response) { const val_t = Date.parse(data_item.timestamp); const raw_item_val = data_item.value[ diff --git a/frontend/src/composables/command.ts b/frontend/src/composables/command.ts index 06167f5..d4afd83 100644 --- a/frontend/src/composables/command.ts +++ b/frontend/src/composables/command.ts @@ -1,6 +1,7 @@ import { ref, toValue, watchEffect } from 'vue'; import { type MaybeRefOrGetter } from 'vue'; import type { AnyTypeId } from '@/composables/dynamic.ts'; +import { parseJsonString } from '@/composables/json.ts'; export interface CommandParameterDefinition { name: string; @@ -20,8 +21,14 @@ export function useAllCommands() { watchEffect(async () => { try { const res = await fetch(`/api/cmd`); - data.value = await res.json(); - error.value = null; + const text_response = await res.text(); + if (res.ok) { + data.value = parseJsonString(text_response); + error.value = null; + } else { + data.value = null; + error.value = text_response; + } } catch (e) { data.value = null; error.value = e; @@ -41,12 +48,13 @@ export function useCommand(name: MaybeRefOrGetter) { try { const res = await fetch(`/api/cmd/${name_value}`); + const text_response = await res.text(); if (res.ok) { - data.value = await res.json(); + data.value = parseJsonString(text_response); error.value = null; } else { data.value = null; - error.value = await res.text(); + error.value = text_response; } } catch (e) { data.value = null; diff --git a/frontend/src/composables/json.ts b/frontend/src/composables/json.ts index 0e63f14..0717126 100644 --- a/frontend/src/composables/json.ts +++ b/frontend/src/composables/json.ts @@ -1,12 +1,16 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function toJsonString(data: any): string { - return JSON.stringify(data, (_key, value) => { - if (typeof value == 'bigint') { - // @ts-expect-error TS2339 - return JSON.rawJSON(value.toString()); - } - return value; - }); +export function toJsonString(data: any, pretty: boolean = false): string { + return JSON.stringify( + data, + (_key, value) => { + if (typeof value == 'bigint') { + // @ts-expect-error TS2339 + return JSON.rawJSON(value.toString()); + } + return value; + }, + pretty ? 4 : undefined, + ); } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/frontend/src/composables/telemetry.ts b/frontend/src/composables/telemetry.ts index fa56db8..9ea8cd0 100644 --- a/frontend/src/composables/telemetry.ts +++ b/frontend/src/composables/telemetry.ts @@ -1,6 +1,7 @@ import { ref, toValue, watchEffect } from 'vue'; import { type MaybeRefOrGetter } from 'vue'; import type { AnyTypeId } from '@/composables/dynamic.ts'; +import { parseJsonString } from '@/composables/json.ts'; export interface TelemetryDefinition { uuid: string; @@ -16,8 +17,14 @@ export function useAllTelemetry() { watchEffect(async () => { try { const res = await fetch(`/api/tlm/info`); - data.value = await res.json(); - error.value = null; + const text_response = await res.text(); + if (res.ok) { + data.value = parseJsonString(text_response); + error.value = null; + } else { + data.value = null; + error.value = text_response; + } } catch (e) { data.value = null; error.value = e; @@ -37,8 +44,14 @@ export function useTelemetry(name: MaybeRefOrGetter) { try { const res = await fetch(`/api/tlm/info/${name_value}`); - data.value = await res.json(); - error.value = null; + const text_response = await res.text(); + if (res.ok) { + data.value = parseJsonString(text_response); + error.value = null; + } else { + data.value = null; + error.value = text_response; + } } catch (e) { data.value = null; error.value = e; diff --git a/frontend/src/panels/panel.ts b/frontend/src/panels/panel.ts index 45479c0..c95d8f0 100644 --- a/frontend/src/panels/panel.ts +++ b/frontend/src/panels/panel.ts @@ -1,5 +1,6 @@ import type { RouteLocationRaw } from 'vue-router'; import { ref, type Ref, watchEffect } from 'vue'; +import { parseJsonString } from '@/composables/json.ts'; export enum PanelHeirarchyType { LEAF = 'leaf', @@ -61,7 +62,11 @@ export function usePanelHeirarchy(): Ref { watchEffect(async () => { try { const res = await fetch(`/api/panel`); - const data = await res.json(); + if (!res.ok) { + console.error('Failed to fetch panels'); + return; + } + const data = parseJsonString(await res.text()); const server_panels: PanelHeirarchyFolder = { name: 'Server Panels', diff --git a/frontend/src/views/PanelEditorView.vue b/frontend/src/views/PanelEditorView.vue index 5840d01..f3e9e0b 100644 --- a/frontend/src/views/PanelEditorView.vue +++ b/frontend/src/views/PanelEditorView.vue @@ -1,7 +1,7 @@