From 0154ab79dc025d8c74af580d744601a50b0b5f84 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 24 Mar 2026 11:24:25 -0400 Subject: [PATCH 1/4] fix(web): include connect plugin version in callbacks - Purpose: send Connect capability data in account callback payloads so downstream apps can preserve sign-in/sign-out actions during callback resolution. - Before: server callback payloads omitted connectPluginVersion, so sign-in/sign-out callbacks could arrive without any Connect version field and be re-resolved as generic license flows. - Problem: the account app could not reliably keep Connect actions visible after redirect, especially when license mismatch logic also applied. - Now: callback payloads include connectPluginVersion and store coverage asserts the field is preserved in both purchase and account callback payload builders. - How: add connectPluginVersion to buildServerCallbackPayload() and extend the web store tests to cover the callback contract. --- web/__test__/store/server.test.ts | 7 +++++++ web/src/store/server.ts | 1 + 2 files changed, 8 insertions(+) diff --git a/web/__test__/store/server.test.ts b/web/__test__/store/server.test.ts index a66639cc25..bdbecc52eb 100644 --- a/web/__test__/store/server.test.ts +++ b/web/__test__/store/server.test.ts @@ -92,6 +92,7 @@ const getStore = () => { Object.defineProperties(store, { apiVersion: { value: '', writable: true }, array: { value: undefined, writable: true }, + connectPluginVersion: { value: '', writable: true }, registered: { value: undefined, writable: true }, state: { value: undefined, writable: true }, regGen: { value: 0, writable: true }, @@ -181,6 +182,7 @@ const getStore = () => { serverPurchasePayload: { get: () => { const payload = { + connectPluginVersion: store.connectPluginVersion || undefined, description: store.description, deviceCount: store.deviceCount, expireTime: store.expireTime, @@ -209,6 +211,7 @@ const getStore = () => { serverAccountPayload: { get: () => { const payload = { + connectPluginVersion: store.connectPluginVersion || undefined, deviceCount: store.deviceCount, description: store.description, expireTime: store.expireTime, @@ -638,6 +641,7 @@ describe('useServerStore', () => { const store = getStore(); store.setServer({ + connectPluginVersion: '2024.05.06.1049', deviceCount: 6, description: 'Test Server', expireTime: 123, @@ -660,6 +664,7 @@ describe('useServerStore', () => { const payload = store.serverPurchasePayload; + expect(payload.connectPluginVersion).toBe('2024.05.06.1049'); expect(payload.description).toBe('Test Server'); expect(payload.deviceCount).toBe(6); expect(payload.expireTime).toBe(123); @@ -708,6 +713,7 @@ describe('useServerStore', () => { const store = getStore(); store.setServer({ + connectPluginVersion: '2024.05.06.1049', deviceCount: 6, description: 'Test Server', expireTime: 123, @@ -731,6 +737,7 @@ describe('useServerStore', () => { const payload = store.serverAccountPayload; + expect(payload.connectPluginVersion).toBe('2024.05.06.1049'); expect(payload.deviceCount).toBe(6); expect(payload.description).toBe('Test Server'); expect(payload.expireTime).toBe(123); diff --git a/web/src/store/server.ts b/web/src/store/server.ts index c2c31ab2d4..67eec28aad 100644 --- a/web/src/store/server.ts +++ b/web/src/store/server.ts @@ -233,6 +233,7 @@ export const useServerStore = defineStore('server', () => { const buildServerCallbackPayload = (overrides: Partial = {}): ServerData => { const payload: ServerData = { + connectPluginVersion: connectPluginVersion.value, description: description.value, deviceCount: deviceCount.value, expireTime: expireTime.value, From 7fbf50c58793d591cef47dfc83998c0381ad7bac Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 24 Mar 2026 16:29:30 -0400 Subject: [PATCH 2/4] fix(web): include connect state in callback payloads - Purpose: align the callback payload branch with the latest local shared-callbacks ServerData shape for Connect-aware flows. - Before: the PR only threaded connectPluginVersion through callback payloads, so newer shared-callbacks consumers would still miss connectState context. - Problem: Connect callbacks could not carry the full Connect runtime metadata expected by the updated local library, especially on replace/account/purchase flows. - Now: the server store includes both connectPluginVersion and connectState in callback payloads, and keeps the extra typing local so the branch still type-checks before the package release lands. - How: add a local callback payload type alias in the server store, source connectState from the cloud minigraph status, and extend the server-store tests to cover purchase, account, and replace payloads. --- web/__test__/store/server.test.ts | 17 +++++++++++++++++ web/src/store/server.ts | 21 +++++++++++++++------ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/web/__test__/store/server.test.ts b/web/__test__/store/server.test.ts index bdbecc52eb..80e1c0e383 100644 --- a/web/__test__/store/server.test.ts +++ b/web/__test__/store/server.test.ts @@ -92,6 +92,7 @@ const getStore = () => { Object.defineProperties(store, { apiVersion: { value: '', writable: true }, array: { value: undefined, writable: true }, + cloud: { value: undefined, writable: true }, connectPluginVersion: { value: '', writable: true }, registered: { value: undefined, writable: true }, state: { value: undefined, writable: true }, @@ -182,6 +183,7 @@ const getStore = () => { serverPurchasePayload: { get: () => { const payload = { + connectState: store.cloud?.minigraphql?.status, connectPluginVersion: store.connectPluginVersion || undefined, description: store.description, deviceCount: store.deviceCount, @@ -211,6 +213,7 @@ const getStore = () => { serverAccountPayload: { get: () => { const payload = { + connectState: store.cloud?.minigraphql?.status, connectPluginVersion: store.connectPluginVersion || undefined, deviceCount: store.deviceCount, description: store.description, @@ -641,6 +644,9 @@ describe('useServerStore', () => { const store = getStore(); store.setServer({ + cloud: createTestData({ + minigraphql: { status: 'CONNECTED' }, + }) as PartialCloudFragment, connectPluginVersion: '2024.05.06.1049', deviceCount: 6, description: 'Test Server', @@ -664,6 +670,7 @@ describe('useServerStore', () => { const payload = store.serverPurchasePayload; + expect(payload.connectState).toBe('CONNECTED'); expect(payload.connectPluginVersion).toBe('2024.05.06.1049'); expect(payload.description).toBe('Test Server'); expect(payload.deviceCount).toBe(6); @@ -713,6 +720,9 @@ describe('useServerStore', () => { const store = getStore(); store.setServer({ + cloud: createTestData({ + minigraphql: { status: 'CONNECTED' }, + }) as PartialCloudFragment, connectPluginVersion: '2024.05.06.1049', deviceCount: 6, description: 'Test Server', @@ -737,6 +747,7 @@ describe('useServerStore', () => { const payload = store.serverAccountPayload; + expect(payload.connectState).toBe('CONNECTED'); expect(payload.connectPluginVersion).toBe('2024.05.06.1049'); expect(payload.deviceCount).toBe(6); expect(payload.description).toBe('Test Server'); @@ -763,6 +774,10 @@ describe('useServerStore', () => { const store = getStore(); store.setServer({ + cloud: createTestData({ + minigraphql: { status: 'CONNECTED' }, + }) as PartialCloudFragment, + connectPluginVersion: '2024.05.06.1049', flashGuid: '058F-6387-0000-0000F1F1E1C6', guid: '058F-6387-0000-0000F1F1E1C6', keyfile: '/boot/config/Pro.key', @@ -770,6 +785,8 @@ describe('useServerStore', () => { tpmGuid: '01-V35H8S0L1QHK1SBG1XHXJNH7', }); + expect(store.serverReplacePayload.connectState).toBe('CONNECTED'); + expect(store.serverReplacePayload.connectPluginVersion).toBe('2024.05.06.1049'); expect(store.serverReplacePayload.guid).toBe('01-V35H8S0L1QHK1SBG1XHXJNH7'); }); diff --git a/web/src/store/server.ts b/web/src/store/server.ts index 67eec28aad..f6049db640 100644 --- a/web/src/store/server.ts +++ b/web/src/store/server.ts @@ -51,6 +51,12 @@ import { useThemeStore } from '~/store/theme'; import { useUnraidApiStore } from '~/store/unraidApi'; import { getRegistrationDeviceLimit, normalizeRegistrationType } from '~/utils/registration'; +type CallbackConnectState = PartialCloudFragment['minigraphql']['status']; +type CallbackServerData = ServerData & { + connectPluginVersion?: string; + connectState?: CallbackConnectState; +}; + export const useServerStore = defineStore('server', () => { const { t } = useI18n(); const accountStore = useAccountStore(); @@ -231,9 +237,12 @@ export const useServerStore = defineStore('server', () => { }; }); - const buildServerCallbackPayload = (overrides: Partial = {}): ServerData => { - const payload: ServerData = { - connectPluginVersion: connectPluginVersion.value, + const buildServerCallbackPayload = ( + overrides: Partial = {} + ): CallbackServerData => { + const payload: CallbackServerData = { + connectPluginVersion: connectPluginVersion.value || undefined, + connectState: cloud.value?.minigraphql.status, description: description.value, deviceCount: deviceCount.value, expireTime: expireTime.value, @@ -278,12 +287,12 @@ export const useServerStore = defineStore('server', () => { }; }; - const serverPurchasePayload = computed((): ServerData => buildServerCallbackPayload()); + const serverPurchasePayload = computed((): CallbackServerData => buildServerCallbackPayload()); - const serverAccountPayload = computed((): ServerData => buildServerCallbackPayload()); + const serverAccountPayload = computed((): CallbackServerData => buildServerCallbackPayload()); const serverReplacePayload = computed( - (): ServerData => ({ + (): CallbackServerData => ({ ...buildServerCallbackPayload({ guid: replaceFlashGuid.value, }), From 3f2319dac36d5bfa4ab4ddc96a769965c28f6101 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 24 Mar 2026 16:54:08 -0400 Subject: [PATCH 3/4] chore(web): upgrade shared-callbacks to 3.1.0 - Purpose: install the published release on the PR branch and stop carrying a local callback payload typing shim. - Before: the branch depended on , so the web store had to define local callback types to model the new Connect metadata while waiting for the package release. - Problem: that kept the branch temporarily divergent from the real package API and added local maintenance overhead in the callback payload builder. - Now: the web workspace depends on , the lockfile points at the published package, and the server store uses the shared package types directly. - How: bump the dependency and lockfile, import from , remove the local callback type aliases, and normalize the GraphQL Connect status through a typed helper before putting it into . --- pnpm-lock.yaml | 10 +++++----- web/package.json | 2 +- web/src/store/server.ts | 37 ++++++++++++++++++++++--------------- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf2ac6a5ab..2fb68c7751 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1098,8 +1098,8 @@ importers: specifier: 8.21.3 version: 8.21.3(vue@3.5.20(typescript@5.9.2)) '@unraid/shared-callbacks': - specifier: 3.0.0 - version: 3.0.0 + specifier: 3.1.0 + version: 3.1.0 '@unraid/ui': specifier: link:../unraid-ui version: link:../unraid-ui @@ -5114,8 +5114,8 @@ packages: cpu: [x64, arm64] os: [linux, darwin] - '@unraid/shared-callbacks@3.0.0': - resolution: {integrity: sha512-O4AN5nsmnwUQ1utYhG2wS9L2NAFn3eOg5YHKq9h9EUa3n8xQeUOzeM6UV2xBg9YJGuF3wQsaEpfj1GyX/MIAGw==} + '@unraid/shared-callbacks@3.1.0': + resolution: {integrity: sha512-Zvz9nlvLSjbTstplvarbH2rwZrsl4ns7djpDmqPi4xH35PvYeK9ut1A/Hkd2Rbl/+nvAMF77d5ZzZkSTb4IoSg==} '@unraid/tailwind-rem-to-rem@2.0.0': resolution: {integrity: sha512-zccpQx5fvEBkAB0JkRwwtyRrT9l26LsjkozLy44LGv0NdZGaxgscniIqJRM+OQj5pSpsWDzExebAtUKdE98Flg==} @@ -17173,7 +17173,7 @@ snapshots: - encoding - supports-color - '@unraid/shared-callbacks@3.0.0': + '@unraid/shared-callbacks@3.1.0': dependencies: crypto-js: 4.2.0 diff --git a/web/package.json b/web/package.json index 61fe3aade8..5b57c06d40 100644 --- a/web/package.json +++ b/web/package.json @@ -115,7 +115,7 @@ "@jsonforms/vue-vuetify": "3.6.0", "@nuxt/ui": "4.0.0-alpha.0", "@tanstack/vue-table": "8.21.3", - "@unraid/shared-callbacks": "3.0.0", + "@unraid/shared-callbacks": "3.1.0", "@unraid/ui": "link:../unraid-ui", "@vue/apollo-composable": "4.2.2", "@vueuse/components": "13.8.0", diff --git a/web/src/store/server.ts b/web/src/store/server.ts index f6049db640..5bde8e4b53 100644 --- a/web/src/store/server.ts +++ b/web/src/store/server.ts @@ -24,7 +24,7 @@ import dayjs from 'dayjs'; import prerelease from 'semver/functions/prerelease'; import type { ApolloQueryResult } from '@apollo/client/core/index.js'; -import type { ServerActionTypes, ServerData } from '@unraid/shared-callbacks'; +import type { ConnectState, ServerActionTypes, ServerData } from '@unraid/shared-callbacks'; import type { Config, PartialCloudFragment, ServerStateQuery } from '~/composables/gql/graphql'; import type { Error } from '~/store/errors'; import type { Theme } from '~/themes/types'; @@ -51,12 +51,6 @@ import { useThemeStore } from '~/store/theme'; import { useUnraidApiStore } from '~/store/unraidApi'; import { getRegistrationDeviceLimit, normalizeRegistrationType } from '~/utils/registration'; -type CallbackConnectState = PartialCloudFragment['minigraphql']['status']; -type CallbackServerData = ServerData & { - connectPluginVersion?: string; - connectState?: CallbackConnectState; -}; - export const useServerStore = defineStore('server', () => { const { t } = useI18n(); const accountStore = useAccountStore(); @@ -237,12 +231,25 @@ export const useServerStore = defineStore('server', () => { }; }); - const buildServerCallbackPayload = ( - overrides: Partial = {} - ): CallbackServerData => { - const payload: CallbackServerData = { + const getConnectState = (): ConnectState | undefined => { + const connectState = cloud.value?.minigraphql.status; + + switch (connectState) { + case 'PRE_INIT': + case 'CONNECTING': + case 'CONNECTED': + case 'PING_FAILURE': + case 'ERROR_RETRYING': + return connectState; + default: + return undefined; + } + }; + + const buildServerCallbackPayload = (overrides: Partial = {}): ServerData => { + const payload: ServerData = { connectPluginVersion: connectPluginVersion.value || undefined, - connectState: cloud.value?.minigraphql.status, + connectState: getConnectState(), description: description.value, deviceCount: deviceCount.value, expireTime: expireTime.value, @@ -287,12 +294,12 @@ export const useServerStore = defineStore('server', () => { }; }; - const serverPurchasePayload = computed((): CallbackServerData => buildServerCallbackPayload()); + const serverPurchasePayload = computed((): ServerData => buildServerCallbackPayload()); - const serverAccountPayload = computed((): CallbackServerData => buildServerCallbackPayload()); + const serverAccountPayload = computed((): ServerData => buildServerCallbackPayload()); const serverReplacePayload = computed( - (): CallbackServerData => ({ + (): ServerData => ({ ...buildServerCallbackPayload({ guid: replaceFlashGuid.value, }), From 19abdba394390a15f1378e52958f6061c91eae8b Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 24 Mar 2026 16:56:39 -0400 Subject: [PATCH 4/4] test(web): cover callback metadata fallback - Purpose: verify the callback payload fallback path when Connect metadata is absent. - Before: the server store tests covered only the positive case where connectPluginVersion and cloud minigraph status were both present. - Problem: that left the current branch without an assertion for the expected fallback behavior when those fields are missing. - Now: the test suite includes a focused spec that omits both cloud metadata and connectPluginVersion and confirms the payload leaves those fields undefined while preserving the rest of the callback data. - How: add a single serverPurchasePayload test in server.test.ts and re-run the focused Vitest file. --- web/__test__/store/server.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/web/__test__/store/server.test.ts b/web/__test__/store/server.test.ts index 80e1c0e383..9d1127c73c 100644 --- a/web/__test__/store/server.test.ts +++ b/web/__test__/store/server.test.ts @@ -692,6 +692,31 @@ describe('useServerStore', () => { expect(payload.wanFQDN).toBe('test.myunraid.net'); }); + it('should fall back when Connect metadata is unavailable', () => { + const store = getStore(); + + store.setServer({ + deviceCount: 6, + guid: '123456', + keyfile: '/boot/config/Plus.key', + name: 'TestServer', + osVersion: '6.10.3', + registered: true, + state: 'PLUS' as ServerState, + }); + + const payload = store.serverPurchasePayload; + + expect(payload.connectState).toBeUndefined(); + expect(payload.connectPluginVersion).toBeUndefined(); + expect(payload.guid).toBe('123456'); + expect(payload.keyfile).toBe('/boot/config/Plus.key'); + expect(payload.name).toBe('TestServer'); + expect(payload.osVersion).toBe('6.10.3'); + expect(payload.registered).toBe(true); + expect(payload.state).toBe('PLUS'); + }); + it('should include activationCodeData in server callback payloads when present', () => { const store = getStore(); activationCodeStoreMock.activationCode.value = {