Skip to content

Commit f0288f9

Browse files
Add admin runtime configuration dashboard (#333)
* Add admin runtime configuration dashboard * Address runtime configuration review feedback * Mark ESPO runtime config as restart required * Fix runtime config review nits for configured state and dotenv reads. Treat env/database numeric settings as configured even when falsy, cache parsed .env keys, and skip env-lock checks when the DB overlay is disabled. Co-authored-by: Cursor <cursoragent@cursor.com> * Group dashboard runtime configuration settings * Refine runtime secret masks * Narrow dashboard runtime configuration scope * Address runtime config review followups * Reject blank dashboard runtime overrides * Harden runtime config cache handling --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d60be69 commit f0288f9

25 files changed

Lines changed: 2492 additions & 31 deletions

File tree

.env.example

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ WEB_HOST_BIND=127.0.0.1
6969
# Required for protected non-dashboard API routes. Webhook routes use this only
7070
# when WEBHOOK_SHARED_SECRET is unset.
7171
API_SHARED_SECRET=
72+
# Env-only key used to encrypt admin-dashboard-managed secret config values.
73+
CONFIG_SECRET_KEY=
7274
# Optional: separate secret for external /webhooks/* callers. When unset or
7375
# blank, webhook routes fall back to API_SHARED_SECRET for compatibility.
7476
WEBHOOK_SHARED_SECRET=
@@ -239,7 +241,3 @@ MIGADU_MAILBOX_DOMAIN=508.dev
239241
# EspoCRM (required for worker integration)
240242
ESPO_API_KEY=your_key_here
241243
ESPO_BASE_URL=https://espo.url/
242-
243-
# Kimai time tracking (optional until Kimai integrations are used)
244-
KIMAI_BASE_URL=https://kimai.example.com
245-
KIMAI_API_TOKEN=your_kimai_api_token_here
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { cleanup, fireEvent, render, screen, within } from "@testing-library/react"
2+
import { afterEach, describe, expect, it, vi } from "vitest"
3+
4+
import { type ConfigurationItem, ConfigurationView } from "./main"
5+
6+
afterEach(() => cleanup())
7+
8+
function configItem(overrides: Partial<ConfigurationItem>): ConfigurationItem {
9+
return {
10+
key: "TEST_SETTING",
11+
label: "Test setting",
12+
category: "Onboarding",
13+
description: "Test setting description.",
14+
value_type: "string",
15+
is_secret: false,
16+
env_locked: false,
17+
source: "default",
18+
configured: false,
19+
restart_required: false,
20+
value: "",
21+
masked_value: null,
22+
secret_encryption_configured: null,
23+
...overrides,
24+
}
25+
}
26+
27+
const items: ConfigurationItem[] = [
28+
configItem({
29+
key: "DOCUSEAL_BASE_URL",
30+
label: "DocuSeal base URL",
31+
description: "DocuSeal API endpoint used for agreement workflows.",
32+
value_type: "url",
33+
configured: true,
34+
value: "https://docuseal.example.com",
35+
}),
36+
configItem({
37+
key: "DOCUSEAL_API_KEY",
38+
label: "DocuSeal API key",
39+
description: "DocuSeal API key for agreement workflows.",
40+
is_secret: true,
41+
configured: true,
42+
masked_value: "doc...key",
43+
secret_encryption_configured: true,
44+
}),
45+
configItem({
46+
key: "DOCUSEAL_MEMBER_AGREEMENT_TEMPLATE_ID",
47+
label: "DocuSeal member agreement template",
48+
description: "Template ID used to filter/sign member agreements.",
49+
value_type: "int",
50+
configured: true,
51+
value: 123,
52+
}),
53+
configItem({
54+
key: "OPENAI_API_KEY",
55+
label: "OpenAI API key",
56+
category: "AI",
57+
description: "Primary OpenAI-compatible API key.",
58+
is_secret: true,
59+
configured: true,
60+
masked_value: "sec...lue",
61+
secret_encryption_configured: false,
62+
}),
63+
]
64+
65+
describe("ConfigurationView", () => {
66+
it("groups settings and keeps tuning rows behind advanced disclosure", () => {
67+
render(
68+
<ConfigurationView
69+
items={items}
70+
loading={{}}
71+
canWrite
72+
onRefresh={vi.fn()}
73+
onSave={vi.fn()}
74+
onClear={vi.fn()}
75+
/>,
76+
)
77+
78+
expect(screen.getByRole("heading", { name: "Onboarding" })).toBeVisible()
79+
expect(screen.getByRole("heading", { name: "AI Providers" })).toBeVisible()
80+
const onboardingTable = screen.getByRole("table", {
81+
name: "Onboarding configuration settings",
82+
})
83+
expect(within(onboardingTable).getByText("DocuSeal base URL")).toBeVisible()
84+
expect(within(onboardingTable).getByText("DocuSeal API key")).toBeVisible()
85+
expect(screen.getByText("DocuSeal member agreement template")).not.toBeVisible()
86+
87+
fireEvent.click(screen.getByText("Advanced"))
88+
89+
expect(screen.getByText("DocuSeal member agreement template")).toBeVisible()
90+
})
91+
92+
it("filters to a selected group and disables impossible secret saves", () => {
93+
const onSave = vi.fn()
94+
render(
95+
<ConfigurationView
96+
items={items}
97+
loading={{}}
98+
canWrite
99+
onRefresh={vi.fn()}
100+
onSave={onSave}
101+
onClear={vi.fn()}
102+
/>,
103+
)
104+
105+
fireEvent.click(screen.getByRole("button", { name: /AI Providers/ }))
106+
107+
expect(screen.queryByRole("heading", { name: "Onboarding" })).not.toBeInTheDocument()
108+
expect(screen.getByRole("heading", { name: "AI Providers" })).toBeVisible()
109+
expect(screen.getByText("OpenAI API key")).toBeVisible()
110+
expect(screen.getByText("sec...lue")).toBeVisible()
111+
expect(screen.getByText("Encryption key missing")).toBeVisible()
112+
expect(screen.getByRole("button", { name: "Save" })).toBeDisabled()
113+
})
114+
115+
it("disables save for empty non-secret drafts so clear remains explicit", () => {
116+
render(
117+
<ConfigurationView
118+
items={items.slice(0, 3)}
119+
loading={{}}
120+
canWrite
121+
onRefresh={vi.fn()}
122+
onSave={vi.fn()}
123+
onClear={vi.fn()}
124+
/>,
125+
)
126+
127+
fireEvent.change(screen.getByLabelText("DocuSeal base URL value"), {
128+
target: { value: "" },
129+
})
130+
const primaryTable = screen.getByRole("table", {
131+
name: "Onboarding configuration settings",
132+
})
133+
expect(within(primaryTable).getAllByRole("button", { name: "Save" })[0]).toBeDisabled()
134+
135+
fireEvent.click(screen.getByText("Advanced"))
136+
fireEvent.change(screen.getByLabelText("DocuSeal member agreement template value"), {
137+
target: { value: "" },
138+
})
139+
140+
const advancedTable = screen.getByRole("table", {
141+
name: "Onboarding advanced configuration settings",
142+
})
143+
expect(within(advancedTable).getByRole("button", { name: "Save" })).toBeDisabled()
144+
})
145+
})

0 commit comments

Comments
 (0)