diff --git a/src/i18n/generated/da-v2.json b/src/i18n/generated/da-v2.json
index ed3e36df..86299f02 100644
--- a/src/i18n/generated/da-v2.json
+++ b/src/i18n/generated/da-v2.json
@@ -2541,6 +2541,7 @@
"zip": "Postnummer"
},
"configuration": {
+ "secret_key_not_set": "The secret key is not set, please set it before enabling the integration.",
"backups": {
"active_jobs": "Active jobs",
"actions": "Actions",
diff --git a/src/i18n/generated/de-v2.json b/src/i18n/generated/de-v2.json
index bda5fdd5..3fa11d7c 100644
--- a/src/i18n/generated/de-v2.json
+++ b/src/i18n/generated/de-v2.json
@@ -2651,6 +2651,7 @@
"zip": "Postleitzahl"
},
"configuration": {
+ "secret_key_not_set": "The secret key is not set, please set it before enabling the integration.",
"backups": {
"active_jobs": "Active jobs",
"actions": "Actions",
diff --git a/src/i18n/generated/en-v2.json b/src/i18n/generated/en-v2.json
index cd53a3e0..ed3dca1c 100644
--- a/src/i18n/generated/en-v2.json
+++ b/src/i18n/generated/en-v2.json
@@ -2372,6 +2372,7 @@
"zip": "@:{'words.generated.zip'} @:{'words.generated.code'}"
},
"configuration": {
+ "secret_key_not_set": "The secret key is not set, please set it before enabling the integration.",
"backups": {
"active_jobs": "Active jobs",
"actions": "Actions",
diff --git a/src/i18n/generated/global-v2.json b/src/i18n/generated/global-v2.json
index 5221818c..b41ede5c 100644
--- a/src/i18n/generated/global-v2.json
+++ b/src/i18n/generated/global-v2.json
@@ -1255,6 +1255,7 @@
"workspace_id": "Workspace ID",
"workspace_id_desc": "The canonical Bird workspace identifier. Existing workplaceId values remain readable during migration."
},
+ "secret_key_not_set": "@:{'templates.generated.compat.configuration.secret_key_not_set'}",
"backups": {
"active_jobs": "@:{'templates.generated.compat.configuration.backups.active_jobs'}",
"actions": "@:{'templates.generated.compat.configuration.backups.actions'}",
diff --git a/src/i18n/generated/no-v2.json b/src/i18n/generated/no-v2.json
index f0e3ede2..99f1f5fe 100644
--- a/src/i18n/generated/no-v2.json
+++ b/src/i18n/generated/no-v2.json
@@ -2654,6 +2654,7 @@
"zip": "@:{'words.generated.postnummer'}"
},
"configuration": {
+ "secret_key_not_set": "The secret key is not set, please set it before enabling the integration.",
"backups": {
"active_jobs": "Active jobs",
"actions": "Actions",
diff --git a/src/i18n/generated/sv-v2.json b/src/i18n/generated/sv-v2.json
index b96bbd56..5ef4b21e 100644
--- a/src/i18n/generated/sv-v2.json
+++ b/src/i18n/generated/sv-v2.json
@@ -2704,6 +2704,7 @@
"zip": "Postnummer"
},
"configuration": {
+ "secret_key_not_set": "The secret key is not set, please set it before enabling the integration.",
"backups": {
"active_jobs": "Active jobs",
"actions": "Actions",
diff --git a/src/i18n/source/da/phrases/compat/configuration/index.json b/src/i18n/source/da/phrases/compat/configuration/index.json
index f7748adb..a3fb628a 100644
--- a/src/i18n/source/da/phrases/compat/configuration/index.json
+++ b/src/i18n/source/da/phrases/compat/configuration/index.json
@@ -1,6 +1,7 @@
{
"compat": {
"configuration": {
+ "secret_key_not_set": "The secret key is not set, please set it before enabling the integration.",
"backups": {
"active_jobs": "Active jobs",
"actions": "Actions",
diff --git a/src/i18n/source/de/phrases/compat/configuration/index.json b/src/i18n/source/de/phrases/compat/configuration/index.json
index 6d4d699c..0f6ad148 100644
--- a/src/i18n/source/de/phrases/compat/configuration/index.json
+++ b/src/i18n/source/de/phrases/compat/configuration/index.json
@@ -1,6 +1,7 @@
{
"compat": {
"configuration": {
+ "secret_key_not_set": "The secret key is not set, please set it before enabling the integration.",
"backups": {
"active_jobs": "Active jobs",
"actions": "Actions",
diff --git a/src/i18n/source/en/phrases/compat/configuration/index.json b/src/i18n/source/en/phrases/compat/configuration/index.json
index 863cbeb1..75b617a7 100644
--- a/src/i18n/source/en/phrases/compat/configuration/index.json
+++ b/src/i18n/source/en/phrases/compat/configuration/index.json
@@ -1,6 +1,7 @@
{
"compat": {
"configuration": {
+ "secret_key_not_set": "The secret key is not set, please set it before enabling the integration.",
"backups": {
"active_jobs": "Active jobs",
"actions": "Actions",
diff --git a/src/i18n/source/global/shared/configuration/index.json b/src/i18n/source/global/shared/configuration/index.json
index 080a7955..4012fa13 100644
--- a/src/i18n/source/global/shared/configuration/index.json
+++ b/src/i18n/source/global/shared/configuration/index.json
@@ -1,5 +1,6 @@
{
"configuration": {
+ "secret_key_not_set": "@:{'phrases.compat.configuration.secret_key_not_set'}",
"backups": {
"active_jobs": "@:{'phrases.compat.configuration.backups.active_jobs'}",
"actions": "@:{'phrases.compat.configuration.backups.actions'}",
diff --git a/src/i18n/source/no/phrases/compat/configuration/index.json b/src/i18n/source/no/phrases/compat/configuration/index.json
index 9614a2a3..20ad1e83 100644
--- a/src/i18n/source/no/phrases/compat/configuration/index.json
+++ b/src/i18n/source/no/phrases/compat/configuration/index.json
@@ -1,6 +1,7 @@
{
"compat": {
"configuration": {
+ "secret_key_not_set": "The secret key is not set, please set it before enabling the integration.",
"backups": {
"active_jobs": "Active jobs",
"actions": "Actions",
diff --git a/src/i18n/source/sv/phrases/compat/configuration/index.json b/src/i18n/source/sv/phrases/compat/configuration/index.json
index 80a76509..9c88ce7b 100644
--- a/src/i18n/source/sv/phrases/compat/configuration/index.json
+++ b/src/i18n/source/sv/phrases/compat/configuration/index.json
@@ -1,6 +1,7 @@
{
"compat": {
"configuration": {
+ "secret_key_not_set": "The secret key is not set, please set it before enabling the integration.",
"backups": {
"active_jobs": "Active jobs",
"actions": "Actions",
diff --git a/src/views/dashboards/superUserDashboard/configuration/ConfigurationXLVask.vue b/src/views/dashboards/superUserDashboard/configuration/ConfigurationXLVask.vue
index cf013989..bbf1cb69 100644
--- a/src/views/dashboards/superUserDashboard/configuration/ConfigurationXLVask.vue
+++ b/src/views/dashboards/superUserDashboard/configuration/ConfigurationXLVask.vue
@@ -85,20 +85,46 @@ const onClickTestConnection = async () => {
// --- MiniMax re-authenticate / remove ---------------------------------------
const minimaxApiKeyIsSet = ref(false);
+const minimaxEnabled = ref(false);
const reauthenticatingMiniMax = ref(false);
const removingMiniMax = ref(false);
+const togglingMiniMaxEnabled = ref(false);
-const refreshMiniMaxStatus = async () => {
+/**
+ * The backend returns the get-config response as an array (one entry per
+ * module variable). Extract the single entry we asked for, then trust the
+ * explicit `isSet` flag instead of the redacted `value`.
+ */
+const extractConfigEntry = (response) => {
+ const payload = response?.data?.data;
+ if (Array.isArray(payload)) {
+ return payload[0] ?? null;
+ }
+ return payload ?? null;
+};
+
+const refreshMiniMaxApiKeyStatus = async () => {
try {
const response = await SessionUser.superUser.modules.minimax.config.keys.api_key.get();
- const value = response?.data?.data?.value ?? response?.data?.data ?? '';
- minimaxApiKeyIsSet.value = typeof value === 'string' && value !== '';
+ const entry = extractConfigEntry(response);
+ minimaxApiKeyIsSet.value = entry?.isSet === true;
} catch (error) {
// If the endpoint is unreachable or the key isn't set yet, treat as not-set.
minimaxApiKeyIsSet.value = false;
}
};
+const refreshMiniMaxEnabled = async () => {
+ try {
+ const response = await SessionUser.superUser.modules.minimax.config.enabled.get();
+ const entry = extractConfigEntry(response);
+ const raw = entry?.value;
+ minimaxEnabled.value = raw === true || raw === 'true' || raw === '1' || raw === 1;
+ } catch (error) {
+ minimaxEnabled.value = false;
+ }
+};
+
const onClickReauthenticateMiniMax = async () => {
const { value: apiKey } = await Swal.fire({
title: t('configuration.xlvask.minimax_reauth_title'),
@@ -121,7 +147,10 @@ const onClickReauthenticateMiniMax = async () => {
reauthenticatingMiniMax.value = true;
try {
await SessionUser.superUser.modules.minimax.config.keys.api_key.set(apiKey);
- minimaxApiKeyIsSet.value = true;
+ // Re-fetch from the backend so the UI matches actual persistence (and so a
+ // silent failure surfaces as "still not set" instead of a misleading green
+ // checkmark).
+ await refreshMiniMaxApiKeyStatus();
Swal.fire({
icon: 'success',
title: t('configuration.xlvask.minimax_reauth_success'),
@@ -152,7 +181,7 @@ const onClickRemoveMiniMax = async () => {
removingMiniMax.value = true;
try {
await SessionUser.superUser.modules.minimax.config.keys.api_key.set('');
- minimaxApiKeyIsSet.value = false;
+ await refreshMiniMaxApiKeyStatus();
Swal.fire({
icon: 'success',
title: t('configuration.xlvask.minimax_remove_success'),
@@ -169,9 +198,37 @@ const onClickRemoveMiniMax = async () => {
}
};
+/**
+ * The ConfigurationSecretKey inline-edit flow calls onSave and resolves on
+ * success. Refresh the parent state from the API so the "Hidden" view replaces
+ * the warning as soon as the request actually persists.
+ */
+const onMiniMaxApiKeySaved = async () => {
+ await refreshMiniMaxApiKeyStatus();
+};
+
+/**
+ * The MiniMax "Enable" toggle calls on-switch with the next boolean value.
+ * Re-fetch after the save so the UI reflects persisted state (the inline
+ * `set()` call does not refresh the UI on its own).
+ */
+const onMiniMaxEnabledSwitch = async (nextValue) => {
+ togglingMiniMaxEnabled.value = true;
+ try {
+ await SessionUser.superUser.modules.minimax.config.enabled.set(nextValue);
+ await refreshMiniMaxEnabled();
+ } catch (error) {
+ // Roll back the optimistic UI flip on failure.
+ minimaxEnabled.value = !nextValue;
+ throw error;
+ } finally {
+ togglingMiniMaxEnabled.value = false;
+ }
+};
+
const load = async () => {
await getModuleConfig();
- await refreshMiniMaxStatus();
+ await Promise.all([refreshMiniMaxApiKeyStatus(), refreshMiniMaxEnabled()]);
};
load();
@@ -294,8 +351,9 @@ load();
:title="$t('configuration.xlvask.minimax_enable')"
:description="$t('configuration.xlvask.minimax_enable_desc')"
icon="fas fa-robot"
- :value="SessionUser.superUser.modules.minimax.config.enabled.get ? true : false"
- :on-switch="SessionUser.superUser.modules.minimax.config.enabled.set"
+ :value="minimaxEnabled"
+ :on-switch="onMiniMaxEnabledSwitch"
+ :disabled="togglingMiniMaxEnabled"
/>
diff --git a/tests/unit/configuration-secret-key.spec.js b/tests/unit/configuration-secret-key.spec.js
new file mode 100644
index 00000000..fd6e8bcd
--- /dev/null
+++ b/tests/unit/configuration-secret-key.spec.js
@@ -0,0 +1,91 @@
+// @vitest-environment jsdom
+
+import { flushPromises, mount } from "@vue/test-utils";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+vi.mock("vue-i18n", () => ({
+ useI18n: () => ({
+ t: (key, fallback) => (typeof fallback === "string" ? fallback : key),
+ }),
+}));
+
+const parseError = vi.fn();
+const getError = vi.fn();
+vi.mock("@/components/request/HandleGlobalError.vue", () => ({
+ parseError: (...args) => parseError(...args),
+ getError: (...args) => getError(...args),
+}));
+
+import ConfigurationSecretKey from "@/components/displays/superuser/configuration/ConfigurationSecretKey.vue";
+
+const findSaveButton = (wrapper) => wrapper.findAll("a.button").find((b) => b.find("i.fa-save").exists());
+const findEditButton = (wrapper) => wrapper.findAll("a.button").find((b) => b.find("i.fa-edit").exists());
+
+const mountKey = (props = {}) =>
+ mount(ConfigurationSecretKey, {
+ props: {
+ title: "API key",
+ description: "desc",
+ ...props,
+ },
+ });
+
+describe("ConfigurationSecretKey", () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("shows the warning when not set, hidden view when set", () => {
+ const unset = mountKey({ isSet: false });
+ expect(unset.find(".message.is-warning").exists()).toBe(true);
+ const setWrapper = mountKey({ isSet: true });
+ expect(setWrapper.find(".message.is-warning").exists()).toBe(false);
+ expect(setWrapper.find('input[value="Hidden"]').exists()).toBe(true);
+ });
+
+ it("emits 'saved' after a successful onSave and exits edit mode", async () => {
+ const onSave = vi.fn().mockResolvedValue({ ok: true });
+ const wrapper = mountKey({ onSave, isSet: false });
+ // The input is disabled until the user enters edit mode — click Edit first,
+ // then type, then Save. This matches the production UX for ConfigurationXLVask.
+ await findEditButton(wrapper).trigger("click");
+ await wrapper.find('input[type="text"]').setValue("super-secret");
+ await findSaveButton(wrapper).trigger("click");
+ await flushPromises();
+ expect(onSave).toHaveBeenCalledWith("super-secret");
+ expect(wrapper.emitted("saved")).toBeTruthy();
+ expect(wrapper.emitted("saved").length).toBe(1);
+ expect(wrapper.emitted("saved")[0][0]).toEqual(expect.objectContaining({ value: "super-secret" }));
+ // Edit mode exited — the Save button is gone, the Edit button is back.
+ expect(findSaveButton(wrapper)).toBeUndefined();
+ expect(findEditButton(wrapper).exists()).toBe(true);
+ // Whether the "Hidden" view shows after a save depends on the parent updating
+ // isSet; that's the caller's responsibility and is exercised in the
+ // ConfigurationXLVask end-to-end coverage.
+ });
+
+ it("stays in edit mode and surfaces the error when onSave rejects", async () => {
+ const failure = new Error("boom");
+ const onSave = vi.fn().mockRejectedValue(failure);
+ getError.mockReturnValue("boom");
+ const wrapper = mountKey({ onSave, isSet: false });
+ await findEditButton(wrapper).trigger("click");
+ await wrapper.find('input[type="text"]').setValue("super-secret");
+ await findSaveButton(wrapper).trigger("click");
+ await flushPromises();
+ expect(onSave).toHaveBeenCalledWith("super-secret");
+ expect(wrapper.emitted("saved")).toBeFalsy();
+ expect(parseError).toHaveBeenCalledWith(failure, expect.any(String));
+ // Edit mode preserved so the user can retry.
+ expect(findSaveButton(wrapper).exists()).toBe(true);
+ expect(wrapper.find(".help.is-danger").exists()).toBe(true);
+ });
+
+ it("renders localized warning message instead of hardcoded English", () => {
+ // useI18n mock returns the fallback for any unknown key, so the warning body
+ // should reflect whatever the caller passed via the i18n key fallback path.
+ const wrapper = mountKey({ isSet: false });
+ const body = wrapper.find(".message.is-warning .message-body").text();
+ expect(body).toContain("The secret key is not set");
+ });
+});