@ddtcorex/dsh-maestro-config 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,1463 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Maestro Settings — DSH-native redesign.
|
|
4
|
+
* Reuses DeepSeek Harness design tokens & primitive geometry maximally:
|
|
5
|
+
* - --dsw-alias-* color family (no hard-coded hex except QR tile #fff)
|
|
6
|
+
* - Button variants primary/ghost/outline (h36 capsule / h28 small) — same as @deepseek-ai/dsh-client-ui-primitives/Button
|
|
7
|
+
* - Input atom (h32, radius 8, bg-layer-1, focus border brand) — same as primitives/Input
|
|
8
|
+
* - DisclosureRow (24px row, 14px glyph, chevron hover) — same as primitives/DisclosureRow
|
|
9
|
+
* - Panel chroma: inner cards use bg-layer-2 / border-l2 / radius 12 / shadow lv3 where needed
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createElement as h, useEffect, useRef, useState } from 'react'
|
|
13
|
+
import QRCode from 'qrcode'
|
|
14
|
+
import { MAESTRO_ENDPOINTS } from './api.js'
|
|
15
|
+
import { generateWebhookSecret, gitlabWebhookUrl } from './webhook-secret.js'
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// DSH tokens — single source, no custom hex (except QR quiet zone #fff)
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const t = {
|
|
21
|
+
bgLayer1: 'var(--dsw-alias-bg-layer-1)',
|
|
22
|
+
bgLayer2: 'var(--dsw-alias-bg-layer-2)',
|
|
23
|
+
bgLayer3: 'var(--dsw-alias-bg-layer-3)',
|
|
24
|
+
borderL2: 'var(--dsw-alias-border-l2)',
|
|
25
|
+
labelPrimary: 'var(--dsw-alias-label-primary)',
|
|
26
|
+
labelSecondary: 'var(--dsw-alias-label-secondary)',
|
|
27
|
+
labelTertiary: 'var(--dsw-alias-label-tertiary)',
|
|
28
|
+
labelDimmed: 'var(--dsw-alias-label-dimmed)',
|
|
29
|
+
labelFg: 'var(--dsw-alias-label-primary-foreground)',
|
|
30
|
+
primaryFill: 'var(--dsw-alias-button-primary-fill)',
|
|
31
|
+
primaryHover: 'var(--dsw-alias-button-primary-hover)',
|
|
32
|
+
interactiveHover: 'var(--dsw-alias-interactive-bg-hover)',
|
|
33
|
+
interactiveActive: 'var(--dsw-alias-interactive-bg-active)',
|
|
34
|
+
stateError: 'var(--dsw-alias-state-error-primary)',
|
|
35
|
+
brand: 'var(--dsw-alias-brand-primary)',
|
|
36
|
+
shadowLv3: 'var(--dsw-shadow-lv3)',
|
|
37
|
+
scrollbarL2: 'var(--dsw-alias-scrollbar-bg-l2)',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Lightweight DSH primitive mirrors (geometry + tokens identical to host)
|
|
42
|
+
// Usage is identical to @deepseek-ai/dsh-client-ui-primitives at runtime.
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
type ButtonVariant = 'primary' | 'ghost' | 'outline'
|
|
45
|
+
function Button({
|
|
46
|
+
variant = 'ghost',
|
|
47
|
+
size = 'md',
|
|
48
|
+
icon,
|
|
49
|
+
children,
|
|
50
|
+
style,
|
|
51
|
+
...rest
|
|
52
|
+
}: {
|
|
53
|
+
variant?: ButtonVariant
|
|
54
|
+
size?: 'md' | 'sm'
|
|
55
|
+
icon?: unknown
|
|
56
|
+
children?: unknown
|
|
57
|
+
style?: Record<string, unknown>
|
|
58
|
+
} & Record<string, unknown>) {
|
|
59
|
+
const base: Record<string, unknown> = {
|
|
60
|
+
display: 'inline-flex',
|
|
61
|
+
alignItems: 'center',
|
|
62
|
+
justifyContent: 'center',
|
|
63
|
+
gap: 4,
|
|
64
|
+
border: 'none',
|
|
65
|
+
borderRadius: size === 'sm' ? 16 : 18,
|
|
66
|
+
cursor: 'pointer',
|
|
67
|
+
fontSize: size === 'sm' ? 13 : 14,
|
|
68
|
+
lineHeight: size === 'sm' ? '18px' : '22px',
|
|
69
|
+
padding: size === 'sm' ? '0 12px' : '0 14px',
|
|
70
|
+
height: size === 'sm' ? 32 : 36,
|
|
71
|
+
color: t.labelPrimary,
|
|
72
|
+
background: 'transparent',
|
|
73
|
+
fontFamily: 'inherit',
|
|
74
|
+
}
|
|
75
|
+
if (variant === 'primary') {
|
|
76
|
+
base.background = t.primaryFill
|
|
77
|
+
base.color = t.labelFg
|
|
78
|
+
}
|
|
79
|
+
if (variant === 'outline') {
|
|
80
|
+
base.border = `1px solid ${t.borderL2}`
|
|
81
|
+
base.background = 'transparent'
|
|
82
|
+
}
|
|
83
|
+
const merged = { ...base, ...(style as object) } as Record<string, string>
|
|
84
|
+
return h(
|
|
85
|
+
'button',
|
|
86
|
+
{
|
|
87
|
+
type: 'button',
|
|
88
|
+
style: merged,
|
|
89
|
+
onMouseEnter: (e: any) => {
|
|
90
|
+
if ((rest as any).disabled) return
|
|
91
|
+
if (variant === 'primary') (e.currentTarget as HTMLElement).style.background = t.primaryHover as string
|
|
92
|
+
else (e.currentTarget as HTMLElement).style.background = t.interactiveHover as string
|
|
93
|
+
},
|
|
94
|
+
onMouseLeave: (e: any) => {
|
|
95
|
+
if (variant === 'primary') (e.currentTarget as HTMLElement).style.background = t.primaryFill as string
|
|
96
|
+
else (e.currentTarget as HTMLElement).style.background = variant === 'outline' ? 'transparent' : 'transparent'
|
|
97
|
+
},
|
|
98
|
+
...(rest as any),
|
|
99
|
+
},
|
|
100
|
+
icon ? h('span', { style: { display: 'inline-flex', width: 16, height: 16, alignItems: 'center', justifyContent: 'center' } }, icon as any) : null,
|
|
101
|
+
children as any,
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function InputWrap({
|
|
106
|
+
icon,
|
|
107
|
+
children,
|
|
108
|
+
style,
|
|
109
|
+
focused,
|
|
110
|
+
}: {
|
|
111
|
+
icon?: unknown
|
|
112
|
+
children: unknown
|
|
113
|
+
style?: Record<string, unknown>
|
|
114
|
+
focused?: boolean
|
|
115
|
+
}) {
|
|
116
|
+
return h(
|
|
117
|
+
'span',
|
|
118
|
+
{
|
|
119
|
+
style: {
|
|
120
|
+
display: 'inline-flex',
|
|
121
|
+
alignItems: 'center',
|
|
122
|
+
gap: 8,
|
|
123
|
+
height: 36,
|
|
124
|
+
padding: '0 12px',
|
|
125
|
+
border: `1px solid ${focused ? t.brand : t.borderL2}`,
|
|
126
|
+
borderRadius: 10,
|
|
127
|
+
background: t.bgLayer1,
|
|
128
|
+
flex: 1,
|
|
129
|
+
minWidth: 0,
|
|
130
|
+
boxSizing: 'border-box' as const,
|
|
131
|
+
...(style as object),
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
icon
|
|
135
|
+
? h('span', { style: { display: 'inline-flex', width: 16, height: 16, color: t.labelTertiary } }, icon as any)
|
|
136
|
+
: null,
|
|
137
|
+
children as any,
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function FieldInput(props: React.InputHTMLAttributes<HTMLInputElement> & { icon?: unknown }) {
|
|
142
|
+
const [focused, setFocused] = useState(false)
|
|
143
|
+
const { icon, style, ...rest } = props as any
|
|
144
|
+
return h(
|
|
145
|
+
InputWrap as any,
|
|
146
|
+
{ icon, focused, style: { ...(style as object), flex: '1 1 auto' } },
|
|
147
|
+
h('input', {
|
|
148
|
+
...(rest as any),
|
|
149
|
+
onFocus: (e: any) => {
|
|
150
|
+
setFocused(true)
|
|
151
|
+
;(rest as any).onFocus?.(e)
|
|
152
|
+
},
|
|
153
|
+
onBlur: (e: any) => {
|
|
154
|
+
setFocused(false)
|
|
155
|
+
;(rest as any).onBlur?.(e)
|
|
156
|
+
},
|
|
157
|
+
style: {
|
|
158
|
+
flex: 1,
|
|
159
|
+
minWidth: 0,
|
|
160
|
+
border: 'none',
|
|
161
|
+
outline: 'none',
|
|
162
|
+
background: 'transparent',
|
|
163
|
+
fontSize: 14,
|
|
164
|
+
lineHeight: '22px',
|
|
165
|
+
color: t.labelPrimary,
|
|
166
|
+
fontFamily: 'inherit',
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function TextareaField(props: React.TextareaHTMLAttributes<HTMLTextAreaElement>) {
|
|
173
|
+
const [focused, setFocused] = useState(false)
|
|
174
|
+
return h('textarea', {
|
|
175
|
+
...(props as any),
|
|
176
|
+
onFocus: (e: any) => {
|
|
177
|
+
setFocused(true)
|
|
178
|
+
;(props as any).onFocus?.(e)
|
|
179
|
+
},
|
|
180
|
+
onBlur: (e: any) => {
|
|
181
|
+
setFocused(false)
|
|
182
|
+
;(props as any).onBlur?.(e)
|
|
183
|
+
},
|
|
184
|
+
style: {
|
|
185
|
+
width: '100%',
|
|
186
|
+
minHeight: 96,
|
|
187
|
+
padding: '8px 10px',
|
|
188
|
+
border: `1px solid ${focused ? t.brand : t.borderL2}`,
|
|
189
|
+
borderRadius: 8,
|
|
190
|
+
background: t.bgLayer1,
|
|
191
|
+
color: t.labelPrimary,
|
|
192
|
+
fontFamily: 'inherit',
|
|
193
|
+
fontSize: 13,
|
|
194
|
+
lineHeight: '18px',
|
|
195
|
+
resize: 'vertical' as const,
|
|
196
|
+
boxSizing: 'border-box' as const,
|
|
197
|
+
outline: 'none',
|
|
198
|
+
...(props as any).style,
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// DSH DisclosureRow — 24px row, 14px glyph, hover chevron swap, same semantics as host
|
|
204
|
+
function DisclosureRow({
|
|
205
|
+
icon,
|
|
206
|
+
title,
|
|
207
|
+
caption,
|
|
208
|
+
open,
|
|
209
|
+
expandable,
|
|
210
|
+
onToggle,
|
|
211
|
+
children,
|
|
212
|
+
}: {
|
|
213
|
+
icon: unknown
|
|
214
|
+
title: string
|
|
215
|
+
caption?: string
|
|
216
|
+
open: boolean
|
|
217
|
+
expandable: boolean
|
|
218
|
+
onToggle: () => void
|
|
219
|
+
children?: unknown
|
|
220
|
+
}) {
|
|
221
|
+
const rowExpands = expandable
|
|
222
|
+
return h(
|
|
223
|
+
'div',
|
|
224
|
+
{ style: { display: 'flex', flexDirection: 'column', width: '100%', minWidth: 0 } },
|
|
225
|
+
h(
|
|
226
|
+
'div',
|
|
227
|
+
{
|
|
228
|
+
role: rowExpands ? 'button' : undefined,
|
|
229
|
+
tabIndex: rowExpands ? 0 : undefined,
|
|
230
|
+
'aria-expanded': rowExpands ? open : undefined,
|
|
231
|
+
onClick: rowExpands ? onToggle : undefined,
|
|
232
|
+
onKeyDown: rowExpands
|
|
233
|
+
? (e: any) => {
|
|
234
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
235
|
+
e.preventDefault()
|
|
236
|
+
onToggle()
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
: undefined,
|
|
240
|
+
style: {
|
|
241
|
+
display: 'flex',
|
|
242
|
+
alignItems: 'center',
|
|
243
|
+
gap: 8,
|
|
244
|
+
minHeight: 40,
|
|
245
|
+
padding: '8px 10px',
|
|
246
|
+
borderRadius: 12,
|
|
247
|
+
cursor: rowExpands ? 'pointer' : 'default',
|
|
248
|
+
background: open ? 'var(--dsw-specific-sidebar-nav-item-active)' : 'transparent',
|
|
249
|
+
border: `1px solid ${open ? t.borderL2 : 'transparent'}`,
|
|
250
|
+
boxSizing: 'border-box' as const,
|
|
251
|
+
},
|
|
252
|
+
onMouseEnter: (e: any) => {
|
|
253
|
+
if (!open) (e.currentTarget as HTMLElement).style.background = 'var(--dsw-specific-sidebar-nav-item-hover)'
|
|
254
|
+
},
|
|
255
|
+
onMouseLeave: (e: any) => {
|
|
256
|
+
if (!open) (e.currentTarget as HTMLElement).style.background = 'transparent'
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
expandable
|
|
260
|
+
? h(
|
|
261
|
+
'button',
|
|
262
|
+
{
|
|
263
|
+
type: 'button',
|
|
264
|
+
'aria-expanded': open,
|
|
265
|
+
onClick: (e: any) => {
|
|
266
|
+
e.stopPropagation()
|
|
267
|
+
onToggle()
|
|
268
|
+
},
|
|
269
|
+
style: {
|
|
270
|
+
flex: 'none',
|
|
271
|
+
width: 20,
|
|
272
|
+
height: 20,
|
|
273
|
+
display: 'inline-flex',
|
|
274
|
+
alignItems: 'center',
|
|
275
|
+
justifyContent: 'center',
|
|
276
|
+
border: 'none',
|
|
277
|
+
background: 'none',
|
|
278
|
+
cursor: 'pointer',
|
|
279
|
+
color: t.labelTertiary,
|
|
280
|
+
padding: 0,
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
open
|
|
284
|
+
? h('span', { style: { fontSize: 12, lineHeight: 1 } }, '▾')
|
|
285
|
+
: h('span', { style: { fontSize: 12, lineHeight: 1 } }, '▸'),
|
|
286
|
+
)
|
|
287
|
+
: h('span', { style: { width: 20, display: 'inline-flex', justifyContent: 'center', color: t.labelTertiary, flex: 'none' } }, icon as any),
|
|
288
|
+
h('span', { style: { flex: 1, minWidth: 0 } },
|
|
289
|
+
h('span', { style: { display: 'block', fontSize: 14, lineHeight: '20px', fontWeight: 500, color: t.labelPrimary } }, title),
|
|
290
|
+
caption ? h('span', { style: { display: 'block', fontSize: 12, lineHeight: '16px', color: t.labelSecondary, marginTop: 1 } }, caption) : null,
|
|
291
|
+
),
|
|
292
|
+
!open && expandable
|
|
293
|
+
? h('span', { style: { color: t.labelTertiary, fontSize: 12 } }, '›')
|
|
294
|
+
: null,
|
|
295
|
+
),
|
|
296
|
+
open ? h('div', { style: { padding: '10px 0 14px 30px', display: 'flex', flexDirection: 'column', gap: 10 } }, children as any) : null,
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// Shared field helpers — DSH-native label / caption rhythm
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
const fieldLabelStyle: Record<string, string> = {
|
|
304
|
+
display: 'flex',
|
|
305
|
+
flexDirection: 'column',
|
|
306
|
+
gap: '6px',
|
|
307
|
+
fontSize: '12px',
|
|
308
|
+
lineHeight: '16px',
|
|
309
|
+
fontWeight: '500',
|
|
310
|
+
color: t.labelSecondary as string,
|
|
311
|
+
margin: '12px 0 0',
|
|
312
|
+
}
|
|
313
|
+
const captionStyle: Record<string, string> = {
|
|
314
|
+
fontSize: '12px',
|
|
315
|
+
lineHeight: '16px',
|
|
316
|
+
color: t.labelSecondary as string,
|
|
317
|
+
margin: '4px 0',
|
|
318
|
+
}
|
|
319
|
+
const cardInsetStyle: Record<string, string> = {
|
|
320
|
+
display: 'flex',
|
|
321
|
+
flexDirection: 'column',
|
|
322
|
+
gap: '8px',
|
|
323
|
+
padding: '12px',
|
|
324
|
+
borderRadius: '12px',
|
|
325
|
+
border: `1px solid ${t.borderL2}`,
|
|
326
|
+
background: t.bgLayer1 as string,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// DSH General row — title + desc left, control right, 16px 0, border-bottom
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
const rowStyle: Record<string, string> = {
|
|
333
|
+
display: 'flex',
|
|
334
|
+
alignItems: 'center',
|
|
335
|
+
gap: '8px',
|
|
336
|
+
padding: '16px 0',
|
|
337
|
+
borderBottom: `1px solid ${t.borderL2}`,
|
|
338
|
+
minWidth: '0',
|
|
339
|
+
}
|
|
340
|
+
const rowTextStyle: Record<string, string> = {
|
|
341
|
+
flex: '1',
|
|
342
|
+
minWidth: '0',
|
|
343
|
+
display: 'flex',
|
|
344
|
+
flexDirection: 'column',
|
|
345
|
+
gap: '4px',
|
|
346
|
+
paddingRight: '48px',
|
|
347
|
+
}
|
|
348
|
+
const rowTitleStyle: Record<string, string> = {
|
|
349
|
+
fontSize: '14px',
|
|
350
|
+
fontWeight: '400',
|
|
351
|
+
lineHeight: '22px',
|
|
352
|
+
color: t.labelPrimary as string,
|
|
353
|
+
}
|
|
354
|
+
const rowDescStyle: Record<string, string> = {
|
|
355
|
+
fontSize: '12px',
|
|
356
|
+
fontWeight: '400',
|
|
357
|
+
lineHeight: '18px',
|
|
358
|
+
color: t.labelTertiary as string,
|
|
359
|
+
}
|
|
360
|
+
// Pill selector — same as LanguageRow/EnterBehaviorRow: h36 r18 bg-module-platform
|
|
361
|
+
const pillSelectorStyle: Record<string, string> = {
|
|
362
|
+
display: 'inline-flex',
|
|
363
|
+
alignItems: 'center',
|
|
364
|
+
gap: '12px',
|
|
365
|
+
height: '36px',
|
|
366
|
+
padding: '0 14px',
|
|
367
|
+
border: 'none',
|
|
368
|
+
borderRadius: '18px',
|
|
369
|
+
background: 'var(--dsw-alias-bg-module-platform, #F5F6F7)',
|
|
370
|
+
font: 'inherit',
|
|
371
|
+
fontSize: '14px',
|
|
372
|
+
lineHeight: '22px',
|
|
373
|
+
color: t.labelPrimary as string,
|
|
374
|
+
cursor: 'pointer',
|
|
375
|
+
whiteSpace: 'nowrap',
|
|
376
|
+
}
|
|
377
|
+
function SettingRow({ title, description, control }: { title: string; description?: string; control: unknown }) {
|
|
378
|
+
return h(
|
|
379
|
+
'div',
|
|
380
|
+
{ 'data-maestro-row': '', style: rowStyle },
|
|
381
|
+
h('div', { 'data-maestro-row-text': '', style: rowTextStyle }, h('div', { style: rowTitleStyle }, title), description ? h('div', { style: rowDescStyle }, description) : null),
|
|
382
|
+
h('div', { 'data-maestro-control': '', style: { flex: 'none', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 8, minHeight: '36px' } }, control as any),
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
function ToggleRow({ title, description, checked, onChange }: { title: string; description?: string; checked?: boolean; onChange: (v: boolean) => void }) {
|
|
386
|
+
return h(
|
|
387
|
+
'label',
|
|
388
|
+
{ 'data-maestro-row': '', style: { ...rowStyle, cursor: 'pointer', alignItems: 'flex-start' } },
|
|
389
|
+
h('input', { type: 'checkbox', checked: checked === true, onChange: (e: any) => onChange(e.target.checked), style: { width: 16, height: 16, accentColor: t.primaryFill as string, marginTop: 4, flex: 'none' } }),
|
|
390
|
+
h('div', { 'data-maestro-row-text': '', style: { ...rowTextStyle, paddingRight: '0' } }, h('div', { style: rowTitleStyle }, title), description ? h('div', { style: rowDescStyle }, description) : null),
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
// QR — same as before (light tile + quiet zone) — QR itself is #fff
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
function QrImage({ url, size = 104 }: { url: string; size?: number }) {
|
|
398
|
+
const [dataUrl, setDataUrl] = useState<string | null>(null)
|
|
399
|
+
useEffect(() => {
|
|
400
|
+
let live = true
|
|
401
|
+
QRCode.toDataURL(url, { margin: 0, width: size * 2 })
|
|
402
|
+
.then((d) => {
|
|
403
|
+
if (live) setDataUrl(d)
|
|
404
|
+
})
|
|
405
|
+
.catch(() => {})
|
|
406
|
+
return () => {
|
|
407
|
+
live = false
|
|
408
|
+
}
|
|
409
|
+
}, [url, size])
|
|
410
|
+
return h(
|
|
411
|
+
'div',
|
|
412
|
+
{
|
|
413
|
+
style: {
|
|
414
|
+
background: '#ffffff',
|
|
415
|
+
borderRadius: 10,
|
|
416
|
+
boxSizing: 'border-box',
|
|
417
|
+
width: size + 20,
|
|
418
|
+
height: size + 20,
|
|
419
|
+
display: 'flex',
|
|
420
|
+
alignItems: 'center',
|
|
421
|
+
justifyContent: 'center',
|
|
422
|
+
lineHeight: 0,
|
|
423
|
+
flex: 'none',
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
dataUrl === null
|
|
427
|
+
? h('div', { style: { width: size, height: size, background: 'var(--dsw-alias-bg-skeleton)', borderRadius: 4 } })
|
|
428
|
+
: h('img', { src: dataUrl, alt: url, width: size, height: size, style: { display: 'block' } }),
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
// ReviewModelSelector — styled with DSH button + menu tokens
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
function ReviewModelSelector({
|
|
436
|
+
value,
|
|
437
|
+
catalog,
|
|
438
|
+
fallbackValue,
|
|
439
|
+
fallbackLabel,
|
|
440
|
+
onChange,
|
|
441
|
+
label,
|
|
442
|
+
}: {
|
|
443
|
+
value: { provider: string; model: string; reasoningEffort?: string } | null
|
|
444
|
+
catalog: any
|
|
445
|
+
fallbackValue: any
|
|
446
|
+
fallbackLabel: string
|
|
447
|
+
onChange: (v: any) => void
|
|
448
|
+
label: string | null
|
|
449
|
+
}) {
|
|
450
|
+
const groups = catalog?.groups ?? []
|
|
451
|
+
const providers: string[] = groups.map((g: any) => g.provider)
|
|
452
|
+
const selectedProvider = value?.provider ?? ''
|
|
453
|
+
const providerGroup = groups.find((g: any) => g.provider === selectedProvider)
|
|
454
|
+
const selectedEffort = value?.reasoningEffort ?? ''
|
|
455
|
+
const [open, setOpen] = useState(false)
|
|
456
|
+
const [pane, setPane] = useState<'root' | 'model' | 'effort'>('root')
|
|
457
|
+
const rootRef = useRef<HTMLDivElement | null>(null)
|
|
458
|
+
useEffect(() => {
|
|
459
|
+
if (!open) return
|
|
460
|
+
const onDown = (e: MouseEvent) => {
|
|
461
|
+
if (rootRef.current && !rootRef.current.contains(e.target as Node)) {
|
|
462
|
+
setOpen(false)
|
|
463
|
+
setPane('root')
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const onKey = (e: KeyboardEvent) => {
|
|
467
|
+
if ((e as any).key === 'Escape') {
|
|
468
|
+
setOpen(false)
|
|
469
|
+
setPane('root')
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
document.addEventListener('mousedown', onDown as any)
|
|
473
|
+
document.addEventListener('keydown', onKey as any)
|
|
474
|
+
return () => {
|
|
475
|
+
document.removeEventListener('mousedown', onDown as any)
|
|
476
|
+
document.removeEventListener('keydown', onKey as any)
|
|
477
|
+
}
|
|
478
|
+
}, [open])
|
|
479
|
+
const getModelId = (m: any) => (typeof m === 'string' ? m : m.id)
|
|
480
|
+
const getModelName = (m: any) => (typeof m === 'string' ? m : (m.name ?? m.id))
|
|
481
|
+
const selectedModelInfo = (() => {
|
|
482
|
+
if (!selectedProvider || !value?.model) return null
|
|
483
|
+
const raw = (providerGroup?.models ?? []).find((mm: any) => getModelId(mm) === value.model)
|
|
484
|
+
if (raw === undefined) return null
|
|
485
|
+
if (typeof raw === 'string') return { id: raw, supportsReasoning: false, reasoningEfforts: [] as string[] }
|
|
486
|
+
return raw
|
|
487
|
+
})()
|
|
488
|
+
const supportsReasoning = (() => {
|
|
489
|
+
if (!selectedModelInfo) return false
|
|
490
|
+
if (typeof (selectedModelInfo as any).supportsReasoning === 'boolean') return (selectedModelInfo as any).supportsReasoning
|
|
491
|
+
const efforts = (selectedModelInfo as any).reasoningEfforts ?? (selectedModelInfo as any).reasoning?.efforts?.map((e: any) => e.id) ?? []
|
|
492
|
+
return efforts.filter((e: string) => e !== 'off').length > 0
|
|
493
|
+
})()
|
|
494
|
+
const availableEfforts: string[] = (() => {
|
|
495
|
+
if (!supportsReasoning) return []
|
|
496
|
+
const efforts = (selectedModelInfo as any)?.reasoningEfforts ?? (selectedModelInfo as any)?.reasoning?.efforts?.map((e: any) => e.id) ?? []
|
|
497
|
+
const filtered = efforts.filter((e: string) => e !== 'off' && e !== '')
|
|
498
|
+
if (filtered.length > 0) return filtered
|
|
499
|
+
return ['low', 'medium', 'high']
|
|
500
|
+
})()
|
|
501
|
+
const warning =
|
|
502
|
+
selectedEffort !== '' && !supportsReasoning && selectedModelInfo !== null
|
|
503
|
+
? `⚠️ This model does not support reasoning effort "${selectedEffort}" — reviews will fail.`
|
|
504
|
+
: null
|
|
505
|
+
const update = (field: string, newVal: string) => {
|
|
506
|
+
if (newVal === '' && field === 'provider') {
|
|
507
|
+
onChange(null)
|
|
508
|
+
setOpen(false)
|
|
509
|
+
setPane('root')
|
|
510
|
+
return
|
|
511
|
+
}
|
|
512
|
+
const next: any = { provider: value?.provider ?? '', model: value?.model ?? '', ...(value?.reasoningEffort ? { reasoningEffort: value.reasoningEffort } : {}) }
|
|
513
|
+
if (field === 'provider') {
|
|
514
|
+
const g = groups.find((x: any) => x.provider === newVal)
|
|
515
|
+
const first = g?.models[0]
|
|
516
|
+
next.provider = newVal
|
|
517
|
+
next.model = first !== undefined ? getModelId(first) : ''
|
|
518
|
+
} else if (field === 'model') next.model = newVal
|
|
519
|
+
else if (field === 'reasoningEffort') {
|
|
520
|
+
if (newVal === '') delete next.reasoningEffort
|
|
521
|
+
else next.reasoningEffort = newVal
|
|
522
|
+
}
|
|
523
|
+
if (!next.provider || !next.model) onChange(null)
|
|
524
|
+
else onChange(next)
|
|
525
|
+
}
|
|
526
|
+
const effectiveFallback = fallbackValue !== undefined ? fallbackValue : (catalog?.current ?? null)
|
|
527
|
+
const triggerLabel = value
|
|
528
|
+
? `${value.provider} / ${value.model}${value.reasoningEffort ? ` · ${value.reasoningEffort}` : ''}`
|
|
529
|
+
: effectiveFallback
|
|
530
|
+
? `${fallbackLabel} · ${effectiveFallback.provider}/${effectiveFallback.model}${effectiveFallback.reasoningEffort ? ` · ${effectiveFallback.reasoningEffort}` : ''}`
|
|
531
|
+
: fallbackLabel
|
|
532
|
+
const menuStyle: Record<string, string> = {
|
|
533
|
+
position: 'absolute',
|
|
534
|
+
top: 'calc(100% + 8px)',
|
|
535
|
+
left: '0',
|
|
536
|
+
minWidth: '300px',
|
|
537
|
+
maxWidth: '360px',
|
|
538
|
+
background: t.bgLayer2 as string,
|
|
539
|
+
border: `1px solid ${t.borderL2}`,
|
|
540
|
+
borderRadius: '12px',
|
|
541
|
+
boxShadow: t.shadowLv3 as string,
|
|
542
|
+
zIndex: '20',
|
|
543
|
+
padding: '6px',
|
|
544
|
+
}
|
|
545
|
+
const rowStyle: Record<string, any> = {
|
|
546
|
+
width: '100%',
|
|
547
|
+
display: 'flex',
|
|
548
|
+
alignItems: 'center',
|
|
549
|
+
justifyContent: 'space-between',
|
|
550
|
+
gap: 12,
|
|
551
|
+
padding: '9px 10px',
|
|
552
|
+
borderRadius: 8,
|
|
553
|
+
border: 'none',
|
|
554
|
+
background: 'transparent',
|
|
555
|
+
color: t.labelPrimary,
|
|
556
|
+
fontFamily: 'inherit',
|
|
557
|
+
fontSize: 13,
|
|
558
|
+
cursor: 'pointer',
|
|
559
|
+
textAlign: 'left',
|
|
560
|
+
}
|
|
561
|
+
const check = (active: boolean) =>
|
|
562
|
+
active
|
|
563
|
+
? h('svg', { width: 16, height: 16, viewBox: '0 0 16 16', style: { flex: 'none' } }, h('path', { d: 'M3.5 8.2l2.8 2.8L12.5 4.8', fill: 'none', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' }))
|
|
564
|
+
: h('span', { style: { width: 16, flex: 'none' } })
|
|
565
|
+
const chevronRight = h('svg', { width: 14, height: 14, viewBox: '0 0 14 14', style: { flex: 'none', opacity: 0.6 } }, h('path', { d: 'M5 3.5L8.5 7L5 10.5', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round' }))
|
|
566
|
+
const effortLabel = selectedEffort === '' ? 'Default effort' : selectedEffort
|
|
567
|
+
const modelLabel = selectedProvider === '' ? 'Select model' : (value?.model ?? 'Select model')
|
|
568
|
+
return h(
|
|
569
|
+
'div',
|
|
570
|
+
{ ref: rootRef as any, 'data-maestro-trigger-wrap': '', style: { position: 'relative', display: 'inline-block', maxWidth: '100%' } },
|
|
571
|
+
label ? h('span', { style: fieldLabelStyle }, label) : null,
|
|
572
|
+
h(
|
|
573
|
+
'button',
|
|
574
|
+
{
|
|
575
|
+
type: 'button',
|
|
576
|
+
style: {
|
|
577
|
+
height: 36,
|
|
578
|
+
padding: '0 14px 0 16px',
|
|
579
|
+
borderRadius: 18,
|
|
580
|
+
border: 'none',
|
|
581
|
+
background: 'var(--dsw-alias-bg-module-platform, #F5F6F7)' as string,
|
|
582
|
+
color: t.labelPrimary as string,
|
|
583
|
+
fontFamily: 'inherit',
|
|
584
|
+
fontSize: 13,
|
|
585
|
+
display: 'inline-flex',
|
|
586
|
+
alignItems: 'center',
|
|
587
|
+
gap: 8,
|
|
588
|
+
cursor: 'pointer',
|
|
589
|
+
maxWidth: 320,
|
|
590
|
+
whiteSpace: 'nowrap',
|
|
591
|
+
},
|
|
592
|
+
onClick: () => {
|
|
593
|
+
setOpen((v) => !v)
|
|
594
|
+
setPane('root')
|
|
595
|
+
},
|
|
596
|
+
'aria-expanded': open,
|
|
597
|
+
'aria-haspopup': 'menu',
|
|
598
|
+
title: triggerLabel,
|
|
599
|
+
},
|
|
600
|
+
h('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, triggerLabel),
|
|
601
|
+
h('svg', { width: 14, height: 14, viewBox: '0 0 14 14', style: { flex: 'none', opacity: 0.7 } }, h('path', { d: 'M3.5 5L7 8.5L10.5 5', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round' })),
|
|
602
|
+
),
|
|
603
|
+
open
|
|
604
|
+
? h(
|
|
605
|
+
'div',
|
|
606
|
+
{ 'data-maestro-menu': '', style: menuStyle as any, role: 'menu' },
|
|
607
|
+
pane === 'root'
|
|
608
|
+
? h(
|
|
609
|
+
'div',
|
|
610
|
+
null,
|
|
611
|
+
h(
|
|
612
|
+
'button',
|
|
613
|
+
{ type: 'button', style: { ...rowStyle, background: !value ? t.bgLayer1 : 'transparent' } as any, onClick: () => { onChange(null); setOpen(false) } },
|
|
614
|
+
h('span', null, fallbackLabel),
|
|
615
|
+
check(!value),
|
|
616
|
+
),
|
|
617
|
+
h('div', { style: { height: 1, background: t.borderL2 as string, margin: '6px 2px' } }),
|
|
618
|
+
h(
|
|
619
|
+
'button',
|
|
620
|
+
{ type: 'button', style: rowStyle as any, onClick: () => setPane('model') },
|
|
621
|
+
h('span', null, 'Model'),
|
|
622
|
+
h('span', { style: { display: 'flex', alignItems: 'center', gap: 8, color: t.labelSecondary as string, maxWidth: 160, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, h('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis' } }, modelLabel), chevronRight),
|
|
623
|
+
),
|
|
624
|
+
h(
|
|
625
|
+
'button',
|
|
626
|
+
{ type: 'button', style: rowStyle as any, onClick: () => setPane('effort') },
|
|
627
|
+
h('span', null, 'Effort'),
|
|
628
|
+
h('span', { style: { display: 'flex', alignItems: 'center', gap: 8, color: t.labelSecondary as string } }, effortLabel, chevronRight),
|
|
629
|
+
),
|
|
630
|
+
value ? h('p', { style: { ...captionStyle, margin: '8px 4px 2px' } }, `Selected: ${value.provider} / ${value.model}${value.reasoningEffort ? ` (${value.reasoningEffort})` : ''}`) : null,
|
|
631
|
+
!value && effectiveFallback ? h('p', { style: { ...captionStyle, margin: '8px 4px 2px' } }, `${fallbackLabel === 'Use Global' ? 'Using Global' : 'Using DSH default'}: ${effectiveFallback.provider} / ${effectiveFallback.model}${effectiveFallback.reasoningEffort ? ` (${effectiveFallback.reasoningEffort})` : ''}`) : null,
|
|
632
|
+
warning ? h('p', { style: { ...captionStyle, margin: '4px 4px 2px', color: t.stateError as string } }, warning) : null,
|
|
633
|
+
)
|
|
634
|
+
: null,
|
|
635
|
+
pane === 'model'
|
|
636
|
+
? h(
|
|
637
|
+
'div',
|
|
638
|
+
null,
|
|
639
|
+
h('button', { type: 'button', style: { ...rowStyle, color: t.labelSecondary } as any, onClick: () => setPane('root') }, h('span', null, '← Back'), h('span', { style: { fontSize: 12 } }, 'Model')),
|
|
640
|
+
h(
|
|
641
|
+
'div',
|
|
642
|
+
{ style: { maxHeight: 260, overflowY: 'auto', marginTop: 4 } },
|
|
643
|
+
providers.length === 0
|
|
644
|
+
? h('p', { style: captionStyle }, 'No providers')
|
|
645
|
+
: providers.map((p: string) => {
|
|
646
|
+
const g = groups.find((x: any) => x.provider === p)
|
|
647
|
+
const ms: any[] = g?.models ?? []
|
|
648
|
+
return h(
|
|
649
|
+
'div',
|
|
650
|
+
{ key: p, style: { marginBottom: 8 } },
|
|
651
|
+
h('div', { style: { fontSize: 11, fontWeight: 600, color: t.labelSecondary as string, padding: '6px 10px 2px', textTransform: 'uppercase', letterSpacing: 0.4, display: 'flex', alignItems: 'center', gap: 6 } }, h('span', { style: { width: 6, height: 6, borderRadius: 3, background: t.borderL2 as string, flex: 'none' } }), (g as any)?.name ?? p),
|
|
652
|
+
ms.length === 0
|
|
653
|
+
? h('p', { style: { ...captionStyle, padding: '2px 10px 2px 28px' } }, 'No models')
|
|
654
|
+
: h(
|
|
655
|
+
'div',
|
|
656
|
+
{ style: { marginLeft: 12, borderLeft: `1px solid ${t.borderL2}`, paddingLeft: 6, display: 'flex', flexDirection: 'column', gap: 2 } },
|
|
657
|
+
ms.map((m: any) => {
|
|
658
|
+
const mid = getModelId(m)
|
|
659
|
+
const mname = getModelName(m)
|
|
660
|
+
const active = value?.provider === p && value?.model === mid
|
|
661
|
+
return h(
|
|
662
|
+
'button',
|
|
663
|
+
{
|
|
664
|
+
key: mid,
|
|
665
|
+
type: 'button',
|
|
666
|
+
style: { ...rowStyle, paddingLeft: 10, background: active ? (t.bgLayer1 as string) : 'transparent' } as any,
|
|
667
|
+
onClick: () => {
|
|
668
|
+
update('model', mid)
|
|
669
|
+
if (value?.provider !== p) update('provider', p)
|
|
670
|
+
else {
|
|
671
|
+
const next: any = { provider: p, model: mid, ...(selectedEffort ? { reasoningEffort: selectedEffort } : {}) }
|
|
672
|
+
onChange(next)
|
|
673
|
+
setPane('root')
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
h('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis' } }, mname),
|
|
678
|
+
check(active),
|
|
679
|
+
)
|
|
680
|
+
}),
|
|
681
|
+
),
|
|
682
|
+
)
|
|
683
|
+
}),
|
|
684
|
+
),
|
|
685
|
+
)
|
|
686
|
+
: null,
|
|
687
|
+
pane === 'effort'
|
|
688
|
+
? h(
|
|
689
|
+
'div',
|
|
690
|
+
null,
|
|
691
|
+
h('button', { type: 'button', style: { ...rowStyle, color: t.labelSecondary } as any, onClick: () => setPane('root') }, h('span', null, '← Back'), h('span', { style: { fontSize: 12 } }, 'Effort')),
|
|
692
|
+
selectedModelInfo === null
|
|
693
|
+
? h('p', { style: { ...captionStyle, margin: '8px 4px 2px' } }, 'Select a model first to configure effort.')
|
|
694
|
+
: !supportsReasoning
|
|
695
|
+
? h(
|
|
696
|
+
'div',
|
|
697
|
+
null,
|
|
698
|
+
h('p', { style: { ...captionStyle, margin: '8px 4px 6px' } }, 'This model does not support reasoning effort — using provider default'),
|
|
699
|
+
h(
|
|
700
|
+
'div',
|
|
701
|
+
{ style: { marginTop: 4 } },
|
|
702
|
+
([{ id: '', label: 'Default effort' }] as any).map((e: any) =>
|
|
703
|
+
h(
|
|
704
|
+
'button',
|
|
705
|
+
{ key: e.id || 'default', type: 'button', style: { ...rowStyle, background: selectedEffort === e.id ? (t.bgLayer1 as string) : 'transparent' } as any, onClick: () => { update('reasoningEffort', e.id); setPane('root') } },
|
|
706
|
+
h('span', null, e.label),
|
|
707
|
+
check(selectedEffort === e.id),
|
|
708
|
+
),
|
|
709
|
+
),
|
|
710
|
+
),
|
|
711
|
+
warning ? h('p', { style: { ...captionStyle, margin: '8px 4px 2px', color: t.stateError as string } }, warning) : null,
|
|
712
|
+
)
|
|
713
|
+
: h(
|
|
714
|
+
'div',
|
|
715
|
+
null,
|
|
716
|
+
h(
|
|
717
|
+
'div',
|
|
718
|
+
{ style: { marginTop: 4 } },
|
|
719
|
+
([{ id: '', label: 'Default effort' }, ...availableEfforts.map((id) => ({ id, label: id }))] as any).map((e: any) =>
|
|
720
|
+
h(
|
|
721
|
+
'button',
|
|
722
|
+
{ key: e.id || 'default', type: 'button', style: { ...rowStyle, background: selectedEffort === e.id ? (t.bgLayer1 as string) : 'transparent' } as any, onClick: () => { update('reasoningEffort', e.id); setPane('root') } },
|
|
723
|
+
h('span', null, e.label),
|
|
724
|
+
check(selectedEffort === e.id),
|
|
725
|
+
),
|
|
726
|
+
),
|
|
727
|
+
),
|
|
728
|
+
warning ? h('p', { style: { ...captionStyle, margin: '8px 4px 2px', color: t.stateError as string } }, warning) : null,
|
|
729
|
+
),
|
|
730
|
+
)
|
|
731
|
+
: null,
|
|
732
|
+
)
|
|
733
|
+
: null,
|
|
734
|
+
)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function ProjectMappingsEditor({ mappings, onChange, catalog, globalReviewModel }: { mappings: any[]; onChange: (v: any[]) => void; catalog: any; globalReviewModel: any }) {
|
|
738
|
+
const rows = mappings as any[]
|
|
739
|
+
const updateRow = (index: number, field: string, value: any) => {
|
|
740
|
+
const next = rows.map((row: any, i: number) => (i === index ? { ...row, [field]: value } : row))
|
|
741
|
+
onChange(next.filter((r) => (r.projectPath ?? '') !== '' || (r.localRepoPath ?? '') !== ''))
|
|
742
|
+
}
|
|
743
|
+
const removeRow = (index: number) => onChange(rows.filter((_, i) => i !== index))
|
|
744
|
+
const addRow = () => onChange([...rows, { projectPath: '', localRepoPath: '', reviewProfile: 'magento2' }])
|
|
745
|
+
return h(
|
|
746
|
+
'div',
|
|
747
|
+
{ 'data-maestro-projects': '', style: { display: 'flex', flexDirection: 'column', gap: 12, paddingTop: 8 } },
|
|
748
|
+
// Header — count + primary Add
|
|
749
|
+
h(
|
|
750
|
+
'div',
|
|
751
|
+
{ style: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, padding: '4px 0 4px' } },
|
|
752
|
+
h(
|
|
753
|
+
'div',
|
|
754
|
+
{ style: { flex: 1, minWidth: 0 } },
|
|
755
|
+
h('div', { style: { fontSize: 14, fontWeight: 600, color: t.labelPrimary as string, lineHeight: '20px' } }, `Projects — ${rows.length} tracked`),
|
|
756
|
+
h('div', { style: { fontSize: 12, color: t.labelSecondary as string, lineHeight: '16px', marginTop: 2 } }, 'GitLab path → local checkout → profile → model override'),
|
|
757
|
+
),
|
|
758
|
+
h(Button as any, { variant: 'primary', size: 'md', onClick: addRow }, '+ Add project'),
|
|
759
|
+
),
|
|
760
|
+
rows.length === 0
|
|
761
|
+
? h(
|
|
762
|
+
'div',
|
|
763
|
+
{
|
|
764
|
+
'data-maestro-project-empty': '',
|
|
765
|
+
style: {
|
|
766
|
+
display: 'flex',
|
|
767
|
+
flexDirection: 'column',
|
|
768
|
+
alignItems: 'center',
|
|
769
|
+
gap: 10,
|
|
770
|
+
padding: '20px 16px',
|
|
771
|
+
borderRadius: 12,
|
|
772
|
+
border: `1px dashed ${t.borderL2}`,
|
|
773
|
+
background: 'transparent',
|
|
774
|
+
textAlign: 'center' as const,
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
h('div', { style: { fontSize: 13, color: t.labelSecondary as string, lineHeight: '18px' } }, 'No projects yet — add your first mapping'),
|
|
778
|
+
h(Button as any, { variant: 'outline', size: 'md', onClick: addRow }, '+ Add project'),
|
|
779
|
+
)
|
|
780
|
+
: h(
|
|
781
|
+
'div',
|
|
782
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 12 } },
|
|
783
|
+
...rows.map((row: any, i: number) =>
|
|
784
|
+
h(
|
|
785
|
+
'div',
|
|
786
|
+
{
|
|
787
|
+
key: i,
|
|
788
|
+
'data-maestro-project-card': '',
|
|
789
|
+
style: {
|
|
790
|
+
display: 'flex',
|
|
791
|
+
flexDirection: 'column',
|
|
792
|
+
gap: 12,
|
|
793
|
+
padding: 12,
|
|
794
|
+
borderRadius: 12,
|
|
795
|
+
border: `1px solid ${t.borderL2}`,
|
|
796
|
+
background: t.bgLayer1 as string,
|
|
797
|
+
boxSizing: 'border-box' as const,
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
// Card header: index + path + remove
|
|
801
|
+
h(
|
|
802
|
+
'div',
|
|
803
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 } },
|
|
804
|
+
h(
|
|
805
|
+
'div',
|
|
806
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 8, minWidth: 0, flex: 1 } },
|
|
807
|
+
h(
|
|
808
|
+
'span',
|
|
809
|
+
{
|
|
810
|
+
style: {
|
|
811
|
+
flex: 'none',
|
|
812
|
+
width: 24,
|
|
813
|
+
height: 24,
|
|
814
|
+
borderRadius: 12,
|
|
815
|
+
background: 'var(--dsw-alias-bg-module-platform, #F5F6F7)',
|
|
816
|
+
display: 'inline-flex',
|
|
817
|
+
alignItems: 'center',
|
|
818
|
+
justifyContent: 'center',
|
|
819
|
+
fontSize: 11,
|
|
820
|
+
fontWeight: 600,
|
|
821
|
+
color: t.labelSecondary as string,
|
|
822
|
+
},
|
|
823
|
+
},
|
|
824
|
+
String(i + 1),
|
|
825
|
+
),
|
|
826
|
+
h(
|
|
827
|
+
'span',
|
|
828
|
+
{
|
|
829
|
+
style: {
|
|
830
|
+
fontSize: 12,
|
|
831
|
+
fontFamily: 'ui-monospace, monospace',
|
|
832
|
+
color: row.projectPath ? (t.labelPrimary as string) : (t.labelTertiary as string),
|
|
833
|
+
overflow: 'hidden',
|
|
834
|
+
textOverflow: 'ellipsis',
|
|
835
|
+
whiteSpace: 'nowrap',
|
|
836
|
+
minWidth: 0,
|
|
837
|
+
},
|
|
838
|
+
},
|
|
839
|
+
row.projectPath || 'Untitled project',
|
|
840
|
+
),
|
|
841
|
+
),
|
|
842
|
+
h(
|
|
843
|
+
Button as any,
|
|
844
|
+
{ variant: 'outline', size: 'sm', onClick: () => removeRow(i), 'aria-label': `Remove project ${i + 1}`, title: 'Remove mapping' },
|
|
845
|
+
'✕',
|
|
846
|
+
),
|
|
847
|
+
),
|
|
848
|
+
// Grid 2-col for paths
|
|
849
|
+
h(
|
|
850
|
+
'div',
|
|
851
|
+
{ 'data-maestro-project-grid': '', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 } },
|
|
852
|
+
h(
|
|
853
|
+
'label',
|
|
854
|
+
{ style: fieldLabelStyle, 'data-maestro-field': '' },
|
|
855
|
+
'GitLab path',
|
|
856
|
+
h(FieldInput as any, {
|
|
857
|
+
placeholder: 'group/project',
|
|
858
|
+
value: row.projectPath,
|
|
859
|
+
onChange: (e: any) => updateRow(i, 'projectPath', e.target.value),
|
|
860
|
+
'aria-label': `GitLab path ${i + 1}`,
|
|
861
|
+
style: { width: '100%' } as any,
|
|
862
|
+
}),
|
|
863
|
+
),
|
|
864
|
+
h(
|
|
865
|
+
'label',
|
|
866
|
+
{ style: fieldLabelStyle, 'data-maestro-field': '' },
|
|
867
|
+
'Local checkout',
|
|
868
|
+
h(FieldInput as any, {
|
|
869
|
+
placeholder: '/path/to/local/clone',
|
|
870
|
+
value: row.localRepoPath,
|
|
871
|
+
onChange: (e: any) => updateRow(i, 'localRepoPath', e.target.value),
|
|
872
|
+
'aria-label': `Local checkout ${i + 1}`,
|
|
873
|
+
style: { width: '100%' } as any,
|
|
874
|
+
}),
|
|
875
|
+
),
|
|
876
|
+
),
|
|
877
|
+
// Row for profile + model — unified label+control, gap 12, pill h36
|
|
878
|
+
h(
|
|
879
|
+
'div',
|
|
880
|
+
{ 'data-maestro-project-profile-row': '', style: { display: 'flex', gap: 12, flexWrap: 'wrap' as const, alignItems: 'flex-start' } },
|
|
881
|
+
h(
|
|
882
|
+
'label',
|
|
883
|
+
{ style: { ...fieldLabelStyle, flex: '1 1 160px', minWidth: 0 } },
|
|
884
|
+
'Review profile',
|
|
885
|
+
h(
|
|
886
|
+
'select',
|
|
887
|
+
{
|
|
888
|
+
value: row.reviewProfile ?? 'magento2',
|
|
889
|
+
onChange: (e: any) => updateRow(i, 'reviewProfile', e.target.value),
|
|
890
|
+
'aria-label': `Review profile ${i + 1}`,
|
|
891
|
+
style: {
|
|
892
|
+
height: 36,
|
|
893
|
+
width: '100%',
|
|
894
|
+
padding: '0 14px',
|
|
895
|
+
border: 'none',
|
|
896
|
+
borderRadius: 18,
|
|
897
|
+
background: 'var(--dsw-alias-bg-module-platform, #F5F6F7)' as string,
|
|
898
|
+
color: t.labelPrimary as string,
|
|
899
|
+
font: 'inherit',
|
|
900
|
+
fontSize: 13,
|
|
901
|
+
},
|
|
902
|
+
},
|
|
903
|
+
h('option', { value: 'magento2' }, 'Magento 2'),
|
|
904
|
+
h('option', { value: 'generic' }, 'Generic'),
|
|
905
|
+
),
|
|
906
|
+
),
|
|
907
|
+
h(
|
|
908
|
+
'label',
|
|
909
|
+
{ style: { ...fieldLabelStyle, flex: '1 1 200px', minWidth: 0 } },
|
|
910
|
+
'Model override',
|
|
911
|
+
h(ReviewModelSelector as any, {
|
|
912
|
+
value: row.reviewModel ?? null,
|
|
913
|
+
catalog,
|
|
914
|
+
fallbackValue: globalReviewModel ?? catalog?.current ?? null,
|
|
915
|
+
fallbackLabel: globalReviewModel ? 'Use Global' : 'Use DSH default',
|
|
916
|
+
onChange: (v: any) => updateRow(i, 'reviewModel', v),
|
|
917
|
+
label: null,
|
|
918
|
+
}),
|
|
919
|
+
),
|
|
920
|
+
),
|
|
921
|
+
),
|
|
922
|
+
),
|
|
923
|
+
),
|
|
924
|
+
)
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function SecretInput({ label, placeholder, hasSaved, onSave }: { label: string; placeholder: string; hasSaved?: boolean; onSave: (v: string) => void }) {
|
|
928
|
+
const [draft, setDraft] = useState('')
|
|
929
|
+
const clear = () => {
|
|
930
|
+
setDraft('')
|
|
931
|
+
onSave('')
|
|
932
|
+
}
|
|
933
|
+
return h(
|
|
934
|
+
'div',
|
|
935
|
+
null,
|
|
936
|
+
h('label', { style: fieldLabelStyle }, label),
|
|
937
|
+
h(
|
|
938
|
+
'div',
|
|
939
|
+
{ style: { display: 'flex', gap: 8 } },
|
|
940
|
+
h(FieldInput as any, {
|
|
941
|
+
placeholder: hasSaved === true ? 'saved — leave blank to keep' : placeholder,
|
|
942
|
+
type: 'password',
|
|
943
|
+
autoComplete: 'off',
|
|
944
|
+
value: draft,
|
|
945
|
+
onChange: (e: any) => setDraft(e.target.value),
|
|
946
|
+
onBlur: () => {
|
|
947
|
+
if (draft !== '') onSave(draft)
|
|
948
|
+
},
|
|
949
|
+
style: { flex: 1 } as any,
|
|
950
|
+
}),
|
|
951
|
+
hasSaved === true ? h(Button as any, { variant: 'outline', size: 'sm', onClick: clear }, 'Clear') : null,
|
|
952
|
+
),
|
|
953
|
+
)
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function ToggleField({ label, caption, checked, onChange }: { label: string; caption?: string; checked?: boolean; onChange: (v: boolean) => void }) {
|
|
957
|
+
return h(
|
|
958
|
+
'label',
|
|
959
|
+
{ style: { display: 'flex', alignItems: 'flex-start', gap: 10, margin: '8px 0', cursor: 'pointer' } },
|
|
960
|
+
h('input', { type: 'checkbox', checked: checked === true, onChange: (e: any) => onChange(e.target.checked), style: { marginTop: 4, width: 16, height: 16, accentColor: t.primaryFill as string } }),
|
|
961
|
+
h('span', null, h('div', { style: { fontSize: 13, color: t.labelPrimary as string, lineHeight: '18px' } }, label), caption ? h('div', { style: captionStyle }, caption) : null),
|
|
962
|
+
)
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function LanAccess({ proxyStatus, lanPin }: { proxyStatus: any; lanPin: any }) {
|
|
966
|
+
const urls: string[] = proxyStatus?.lanUrls ?? []
|
|
967
|
+
const [selected, setSelected] = useState(0)
|
|
968
|
+
const index = Math.min(selected, Math.max(urls.length - 1, 0))
|
|
969
|
+
if (!proxyStatus?.running) {
|
|
970
|
+
return h('p', { style: { color: t.stateError as string, fontSize: 12, margin: '8px 0 0' } }, proxyStatus?.errorMessage ?? 'Proxy not running')
|
|
971
|
+
}
|
|
972
|
+
return h(
|
|
973
|
+
'div',
|
|
974
|
+
null,
|
|
975
|
+
h('p', { style: captionStyle }, lanPin?.enabled === true ? 'Open this DSH UI from any device on your network — visitors enter the LAN PIN below.' : 'Open this DSH UI from any device on your network — no PIN needed.'),
|
|
976
|
+
urls.length > 0
|
|
977
|
+
? h(
|
|
978
|
+
'div',
|
|
979
|
+
{ style: { display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 12 } },
|
|
980
|
+
...urls.map((url, i) =>
|
|
981
|
+
h(Button as any, { key: url, variant: i === index ? 'primary' : 'outline', size: 'sm', onClick: () => setSelected(i) }, url.replace(/^http:\/\//, '')),
|
|
982
|
+
),
|
|
983
|
+
)
|
|
984
|
+
: null,
|
|
985
|
+
urls.length > 0
|
|
986
|
+
? h(
|
|
987
|
+
'div',
|
|
988
|
+
{ 'data-maestro-qr-row': '', style: { display: 'flex', gap: 14, alignItems: 'center', flexWrap: 'wrap' as const } },
|
|
989
|
+
h(QrImage as any, { url: urls[index], size: 116 }),
|
|
990
|
+
h('div', null, h('div', { style: { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: 12, color: t.labelPrimary as string, wordBreak: 'break-all' } }, urls[index]), h('p', { style: captionStyle }, 'Scan with a phone on the same network.')),
|
|
991
|
+
)
|
|
992
|
+
: null,
|
|
993
|
+
lanPin !== null ? h(LanPinRow as any, { lanPin }) : null,
|
|
994
|
+
)
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function LanPinRow({ lanPin }: { lanPin: any }) {
|
|
998
|
+
return h(
|
|
999
|
+
'div',
|
|
1000
|
+
{ style: { marginTop: 12 } },
|
|
1001
|
+
h(
|
|
1002
|
+
'label',
|
|
1003
|
+
{ style: { display: 'flex', gap: 8, alignItems: 'center', cursor: 'pointer' } },
|
|
1004
|
+
h('input', { type: 'checkbox', checked: lanPin.enabled, onChange: (e: any) => lanPin.onToggle(e.target.checked), style: { width: 15, height: 15, accentColor: t.primaryFill as string } }),
|
|
1005
|
+
h('span', { style: { ...fieldLabelStyle, margin: 0 } }, 'Require a PIN on the LAN'),
|
|
1006
|
+
),
|
|
1007
|
+
lanPin.enabled
|
|
1008
|
+
? h(
|
|
1009
|
+
'div',
|
|
1010
|
+
{ style: { display: 'flex', gap: 8, alignItems: 'center', marginTop: 8, flexWrap: 'wrap' as const } },
|
|
1011
|
+
h('span', { style: { ...fieldLabelStyle, margin: 0 } }, 'LAN PIN'),
|
|
1012
|
+
h('code', { style: { fontFamily: 'ui-monospace, monospace', fontSize: 15, letterSpacing: 2, color: t.labelPrimary as string } }, lanPin.show ? (lanPin.pin ?? '••••••••') : '••••••••'),
|
|
1013
|
+
lanPin.show ? h(Button as any, { variant: 'outline', size: 'sm', onClick: lanPin.onHide }, 'Hide') : h(Button as any, { variant: 'outline', size: 'sm', onClick: lanPin.onShow }, 'Show'),
|
|
1014
|
+
h(Button as any, { variant: 'outline', size: 'sm', onClick: lanPin.onRotate }, 'Rotate'),
|
|
1015
|
+
)
|
|
1016
|
+
: null,
|
|
1017
|
+
)
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function PublicAccess({ status, pin, showPin, onRevealPin, onHidePin, onRotatePin }: { status: any; pin: string | null; showPin: boolean; onRevealPin: () => void; onHidePin: () => void; onRotatePin: () => void }) {
|
|
1021
|
+
return h(
|
|
1022
|
+
'div',
|
|
1023
|
+
null,
|
|
1024
|
+
status?.running && status?.publicUrl
|
|
1025
|
+
? h(
|
|
1026
|
+
'div',
|
|
1027
|
+
null,
|
|
1028
|
+
h(
|
|
1029
|
+
'div',
|
|
1030
|
+
{ 'data-maestro-qr-row': '', style: { display: 'flex', gap: 14, alignItems: 'center', marginBottom: 12, flexWrap: 'wrap' as const } },
|
|
1031
|
+
h(QrImage as any, { url: status.publicUrl, size: 116 }),
|
|
1032
|
+
h('div', null, h('div', { style: { fontFamily: 'ui-monospace, monospace', fontSize: 12, color: t.labelPrimary as string, wordBreak: 'break-all' } }, status.publicUrl), h('p', { style: captionStyle }, 'Works from anywhere; visitors enter the PIN below.')),
|
|
1033
|
+
),
|
|
1034
|
+
)
|
|
1035
|
+
: h('p', { style: captionStyle }, 'Start the tunnel to get a public address.'),
|
|
1036
|
+
h(
|
|
1037
|
+
'div',
|
|
1038
|
+
{ style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' as const } },
|
|
1039
|
+
h('span', { style: { ...fieldLabelStyle, margin: 0 } }, 'Access PIN'),
|
|
1040
|
+
h('code', { style: { fontFamily: 'ui-monospace, monospace', fontSize: 15, letterSpacing: 2, color: t.labelPrimary as string } }, showPin && pin !== null ? pin : '••••••••'),
|
|
1041
|
+
showPin ? h(Button as any, { variant: 'outline', size: 'sm', onClick: onHidePin }, 'Hide') : h(Button as any, { variant: 'outline', size: 'sm', onClick: onRevealPin }, 'Show'),
|
|
1042
|
+
h(Button as any, { variant: 'outline', size: 'sm', onClick: onRotatePin }, 'Rotate'),
|
|
1043
|
+
),
|
|
1044
|
+
h('p', { style: captionStyle }, 'Stays the same across tunnel and DSH restarts; use Rotate when you need a new PIN.'),
|
|
1045
|
+
)
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function NamedTunnelSetupNote() {
|
|
1049
|
+
return h(
|
|
1050
|
+
'div',
|
|
1051
|
+
{ style: { ...cardInsetStyle, marginTop: '12px' } },
|
|
1052
|
+
h('p', { style: { ...captionStyle, marginBottom: 4, fontWeight: 500, color: t.labelPrimary as string } }, 'Named tunnel — one-time manual setup (requires your own Cloudflare account):'),
|
|
1053
|
+
h('ol', { style: { margin: '4px 0', paddingLeft: 20, fontSize: 12, color: t.labelSecondary as string, lineHeight: '18px' } }, h('li', null, 'cloudflared tunnel login'), h('li', null, 'cloudflared tunnel create dsh-maestro-webhook'), h('li', null, 'cloudflared tunnel route dns dsh-maestro-webhook <your-hostname>'), h('li', null, 'Paste the printed Tunnel ID, credentials file path and hostname below.')),
|
|
1054
|
+
)
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// ---------------------------------------------------------------------------
|
|
1058
|
+
// Main — DSH-native grouped settings (DisclosureRow per domain)
|
|
1059
|
+
// ---------------------------------------------------------------------------
|
|
1060
|
+
export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; configRpcCall?: any }) {
|
|
1061
|
+
const [status, setStatus] = useState<any>(null)
|
|
1062
|
+
const [proxyStatus, setProxyStatus] = useState<any>(null)
|
|
1063
|
+
const [config, setConfig] = useState<any>({ tunnelMode: 'quick', projectMappings: [] })
|
|
1064
|
+
const [catalog, setCatalog] = useState<any>(null)
|
|
1065
|
+
const [busy, setBusy] = useState(false)
|
|
1066
|
+
const [error, setError] = useState<string | null>(null)
|
|
1067
|
+
const [pin, setPin] = useState<string | null>(null)
|
|
1068
|
+
const [showPin, setShowPin] = useState(false)
|
|
1069
|
+
const [lanPinEnabled, setLanPinEnabled] = useState(false)
|
|
1070
|
+
const [lanPin, setLanPin] = useState<string | null>(null)
|
|
1071
|
+
const [showLanPin, setShowLanPin] = useState(false)
|
|
1072
|
+
const [guard, setGuard] = useState<any>({})
|
|
1073
|
+
const [patternsText, setPatternsText] = useState('')
|
|
1074
|
+
const [placeholdersText, setPlaceholdersText] = useState('')
|
|
1075
|
+
const [supervisorCfg, setSupervisorCfg] = useState<any>({})
|
|
1076
|
+
const [notifierCfg, setNotifierCfg] = useState<any>({})
|
|
1077
|
+
const [activeTab, setActiveTab] = useState('tunnel')
|
|
1078
|
+
// Mobile: inject responsive overrides once (mirrors dsh-maestro-mobile settings-sheet pill pattern + market catsWrap)
|
|
1079
|
+
useEffect(() => {
|
|
1080
|
+
const css = `
|
|
1081
|
+
/* Maestro nested tabs — market-like pill bar + mobile fixes */
|
|
1082
|
+
[data-maestro-tabs] { display:flex; gap:6px; overflow-x:auto; overflow-y:hidden; scrollbar-width:none; -webkit-overflow-scrolling:touch; overscroll-behavior-x:contain; touch-action:pan-x; padding-bottom:6px; margin-bottom:10px; border-bottom:1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.08)); }
|
|
1083
|
+
[data-maestro-tabs]::-webkit-scrollbar { display:none; width:0; height:0; }
|
|
1084
|
+
[data-maestro-tab] { flex:none; height:32px; min-width:fit-content; padding:0 14px; border-radius:999px; border:1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.12)); background:transparent; color:var(--dsw-alias-label-primary); font-size:13px; line-height:20px; white-space:nowrap; display:inline-flex; align-items:center; justify-content:center; cursor:pointer; font-family:inherit; -webkit-tap-highlight-color:transparent; }
|
|
1085
|
+
[data-maestro-tab][data-active="true"] { background:var(--dsw-specific-sidebar-nav-item-active, #EBEEF2); border-color:var(--dsw-specific-sidebar-nav-item-active, #EBEEF2); color:var(--dsw-alias-label-primary); }
|
|
1086
|
+
[data-maestro-tab]:hover { background:var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,.06)); }
|
|
1087
|
+
[data-maestro-tab][data-active="true"]:hover { background:var(--dsw-specific-sidebar-nav-item-active, #EBEEF2); }
|
|
1088
|
+
[data-maestro-tab]:focus-visible { outline:2px solid var(--dsw-alias-state-business-primary, #4f6ef7); outline-offset:1px; }
|
|
1089
|
+
[data-maestro-panel] { width:100%; min-width:0; box-sizing:border-box; }
|
|
1090
|
+
/* label → input dính nhau fix: flex column gap + full width */
|
|
1091
|
+
[data-maestro-panel] label { gap:6px !important; }
|
|
1092
|
+
[data-maestro-row]:last-child { border-bottom:none !important; }
|
|
1093
|
+
[data-maestro-panel] label > span { width:100% !important; box-sizing:border-box !important; }
|
|
1094
|
+
@media (max-width: 640px) {
|
|
1095
|
+
[data-maestro-settings-card] { max-width:100% !important; gap:6px !important; padding:0 2px !important; }
|
|
1096
|
+
[data-maestro-tabs] { gap:6px !important; padding:0 0 6px !important; margin:0 -2px 10px !important; }
|
|
1097
|
+
[data-maestro-tab] { height:32px !important; padding:0 12px !important; font-size:13px !important; }
|
|
1098
|
+
[data-maestro-mapping-row] { flex-direction:column !important; align-items:stretch !important; gap:8px !important; }
|
|
1099
|
+
[data-maestro-mapping-row] > * { flex:1 1 100% !important; width:100% !important; max-width:100% !important; }
|
|
1100
|
+
[data-maestro-qr-row] { flex-direction:column !important; align-items:flex-start !important; }
|
|
1101
|
+
[data-maestro-trigger-wrap] { max-width:100% !important; }
|
|
1102
|
+
[data-maestro-menu] { min-width:0 !important; max-width:calc(100vw - 32px) !important; left:0 !important; right:auto !important; }
|
|
1103
|
+
[data-maestro-panel] label { gap:8px !important; }
|
|
1104
|
+
div[data-maestro-row] { flex-direction:column !important; align-items:stretch !important; padding:12px 0 !important; }
|
|
1105
|
+
[data-maestro-row-text] { padding-right:0 !important; }
|
|
1106
|
+
[data-maestro-control] { width:100% !important; justify-content:flex-start !important; }
|
|
1107
|
+
[data-maestro-control] > span { width:100% !important; }
|
|
1108
|
+
[data-maestro-control] select { width:100% !important; }
|
|
1109
|
+
[data-maestro-project-grid] { grid-template-columns:1fr !important; }
|
|
1110
|
+
[data-maestro-project-card] { padding:10px !important; }
|
|
1111
|
+
[data-maestro-project-profile-row] { flex-direction:column !important; align-items:stretch !important; }
|
|
1112
|
+
[data-maestro-project-profile-row] > label { flex:1 1 100% !important; width:100% !important; }
|
|
1113
|
+
}
|
|
1114
|
+
@media (max-width: 390px) {
|
|
1115
|
+
[data-maestro-tab] { height:30px !important; padding:0 10px !important; font-size:12px !important; }
|
|
1116
|
+
}
|
|
1117
|
+
`
|
|
1118
|
+
const tag = document.createElement('style')
|
|
1119
|
+
tag.dataset.plugin = '@ddtcorex/dsh-maestro-config'
|
|
1120
|
+
tag.dataset.pluginCss = 'maestro/mobile-tabs.css'
|
|
1121
|
+
tag.textContent = css
|
|
1122
|
+
document.head.appendChild(tag)
|
|
1123
|
+
return () => tag.remove()
|
|
1124
|
+
}, [])
|
|
1125
|
+
|
|
1126
|
+
const call = async (endpoint: string, payload?: unknown) => {
|
|
1127
|
+
const res = await rpcCall(endpoint, payload)
|
|
1128
|
+
if (!res?.ok) throw new Error(res?.error?.message ?? 'RPC failed')
|
|
1129
|
+
return res.value
|
|
1130
|
+
}
|
|
1131
|
+
const unwrap = (res: any) => {
|
|
1132
|
+
if (res && typeof res === 'object' && 'ok' in res) {
|
|
1133
|
+
if (res.ok) return res.value
|
|
1134
|
+
throw new Error(res.error?.message ?? 'RPC failed')
|
|
1135
|
+
}
|
|
1136
|
+
return res
|
|
1137
|
+
}
|
|
1138
|
+
const cfgGet = async (domain: string) => {
|
|
1139
|
+
if (!configRpcCall) throw new Error('config RPC not available')
|
|
1140
|
+
const res = await configRpcCall('get', { domain })
|
|
1141
|
+
return unwrap(res)
|
|
1142
|
+
}
|
|
1143
|
+
const cfgSet = async (domain: string, patch: object) => {
|
|
1144
|
+
if (!configRpcCall) throw new Error('config RPC not available')
|
|
1145
|
+
const res = await configRpcCall('set', { domain, patch })
|
|
1146
|
+
return unwrap(res)
|
|
1147
|
+
}
|
|
1148
|
+
const saveGuard = async (patch: any) => {
|
|
1149
|
+
setError(null)
|
|
1150
|
+
const next = { ...guard, ...patch }
|
|
1151
|
+
if (patch.gitProtection && guard.gitProtection) next.gitProtection = { ...guard.gitProtection, ...patch.gitProtection }
|
|
1152
|
+
setGuard(next)
|
|
1153
|
+
try {
|
|
1154
|
+
await cfgSet('guard', patch)
|
|
1155
|
+
} catch (e: any) {
|
|
1156
|
+
setError(e.message ?? String(e))
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
const commitBlacklistPatterns = async (text: string) => {
|
|
1160
|
+
const patterns = text.split('\n').map((s) => s.trim()).filter(Boolean)
|
|
1161
|
+
setError(null)
|
|
1162
|
+
try {
|
|
1163
|
+
await cfgSet('guardBlacklist', { patterns })
|
|
1164
|
+
} catch (e: any) {
|
|
1165
|
+
setError(e.message ?? String(e))
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
const commitPlaceholders = async () => {
|
|
1169
|
+
setError(null)
|
|
1170
|
+
let obj: any = {}
|
|
1171
|
+
try {
|
|
1172
|
+
obj = placeholdersText.trim() ? JSON.parse(placeholdersText) : {}
|
|
1173
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) throw new Error('placeholders must be JSON object')
|
|
1174
|
+
} catch (e: any) {
|
|
1175
|
+
setError(`placeholders JSON invalid: ${e.message ?? String(e)}`)
|
|
1176
|
+
return
|
|
1177
|
+
}
|
|
1178
|
+
try {
|
|
1179
|
+
await cfgSet('guardBlacklist', { placeholders: obj })
|
|
1180
|
+
} catch (e: any) {
|
|
1181
|
+
setError(e.message ?? String(e))
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const saveSupervisorCfg = async (patch: any) => {
|
|
1185
|
+
setError(null)
|
|
1186
|
+
setSupervisorCfg((prev: any) => ({ ...prev, ...patch }))
|
|
1187
|
+
try {
|
|
1188
|
+
await cfgSet('supervisor', patch)
|
|
1189
|
+
} catch (e: any) {
|
|
1190
|
+
setError(e.message ?? String(e))
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
const saveNotifierCfg = async (patch: any) => {
|
|
1194
|
+
setError(null)
|
|
1195
|
+
setNotifierCfg((prev: any) => {
|
|
1196
|
+
const next = { ...prev }
|
|
1197
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
1198
|
+
if (k === 'telegram' && typeof v === 'object' && v !== null) (next as any).telegram = { ...((prev as any).telegram ?? {}), ...(v as any) }
|
|
1199
|
+
else (next as any)[k] = v
|
|
1200
|
+
}
|
|
1201
|
+
return next
|
|
1202
|
+
})
|
|
1203
|
+
try {
|
|
1204
|
+
await cfgSet('notifier', patch)
|
|
1205
|
+
} catch (e: any) {
|
|
1206
|
+
setError(e.message ?? String(e))
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
const refresh = async () => {
|
|
1210
|
+
try {
|
|
1211
|
+
setStatus(await call(MAESTRO_ENDPOINTS.status, {}))
|
|
1212
|
+
} catch {}
|
|
1213
|
+
try {
|
|
1214
|
+
setProxyStatus(await call(MAESTRO_ENDPOINTS.proxyStatus, {}))
|
|
1215
|
+
} catch {}
|
|
1216
|
+
}
|
|
1217
|
+
useEffect(() => {
|
|
1218
|
+
call(MAESTRO_ENDPOINTS.getConfig, {})
|
|
1219
|
+
.then((saved) => setConfig((prev: any) => ({ ...prev, ...saved })))
|
|
1220
|
+
.catch(() => {})
|
|
1221
|
+
if (configRpcCall) {
|
|
1222
|
+
configRpcCall('get', { domain: 'supervisor' })
|
|
1223
|
+
.then((res: any) => {
|
|
1224
|
+
if (res?.ok && res.value?.model) setConfig((prev: any) => ({ ...prev, supervisorModel: res.value.model }))
|
|
1225
|
+
})
|
|
1226
|
+
.catch(() => {})
|
|
1227
|
+
Promise.all([cfgGet('guard').catch(() => ({})), cfgGet('guardBlacklist').catch(() => ({ patterns: [], placeholders: {} })), cfgGet('supervisor').catch(() => ({})), cfgGet('notifier').catch(() => ({}))])
|
|
1228
|
+
.then(([g, bl, sup, not]) => {
|
|
1229
|
+
setGuard(g ?? {})
|
|
1230
|
+
const pats = Array.isArray((bl as any)?.patterns) ? (bl as any).patterns : []
|
|
1231
|
+
const ph = (bl as any)?.placeholders && typeof (bl as any).placeholders === 'object' ? (bl as any).placeholders : {}
|
|
1232
|
+
setPatternsText(pats.join('\n'))
|
|
1233
|
+
setPlaceholdersText(JSON.stringify(ph, null, 2))
|
|
1234
|
+
setSupervisorCfg(sup ?? {})
|
|
1235
|
+
setNotifierCfg(not ?? {})
|
|
1236
|
+
})
|
|
1237
|
+
.catch(() => {})
|
|
1238
|
+
}
|
|
1239
|
+
call(MAESTRO_ENDPOINTS.lanPinStatus, {})
|
|
1240
|
+
.then((value: any) => {
|
|
1241
|
+
setLanPinEnabled(value.enabled)
|
|
1242
|
+
if (value.enabled) setLanPin(value.pin ?? null)
|
|
1243
|
+
})
|
|
1244
|
+
.catch(() => {})
|
|
1245
|
+
call(MAESTRO_ENDPOINTS.modelsList, {})
|
|
1246
|
+
.then((value) => setCatalog(value))
|
|
1247
|
+
.catch(() => {})
|
|
1248
|
+
}, [])
|
|
1249
|
+
useEffect(() => {
|
|
1250
|
+
refresh()
|
|
1251
|
+
const t = setInterval(refresh, 3000)
|
|
1252
|
+
return () => clearInterval(t)
|
|
1253
|
+
}, [])
|
|
1254
|
+
const revealPin = async () => {
|
|
1255
|
+
if (pin === null) {
|
|
1256
|
+
try {
|
|
1257
|
+
setPin((await call(MAESTRO_ENDPOINTS.getPin, {})).pin)
|
|
1258
|
+
} catch (err: any) {
|
|
1259
|
+
setError(err.message)
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
setShowPin(true)
|
|
1263
|
+
}
|
|
1264
|
+
const rotatePin = async () => {
|
|
1265
|
+
setError(null)
|
|
1266
|
+
try {
|
|
1267
|
+
const fresh = (await call(MAESTRO_ENDPOINTS.rotatePin, {})).pin
|
|
1268
|
+
setPin(fresh)
|
|
1269
|
+
setShowPin(true)
|
|
1270
|
+
} catch (err: any) {
|
|
1271
|
+
setError(err.message)
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
const toggleLanPin = async (enabled: boolean) => {
|
|
1275
|
+
setError(null)
|
|
1276
|
+
const previous = lanPinEnabled
|
|
1277
|
+
setLanPinEnabled(enabled)
|
|
1278
|
+
try {
|
|
1279
|
+
await call(MAESTRO_ENDPOINTS.lanPinSetEnabled, { enabled })
|
|
1280
|
+
if (enabled) {
|
|
1281
|
+
const value = await call(MAESTRO_ENDPOINTS.lanPinStatus, {})
|
|
1282
|
+
setLanPin(value.pin ?? null)
|
|
1283
|
+
setShowLanPin(true)
|
|
1284
|
+
} else {
|
|
1285
|
+
setLanPin(null)
|
|
1286
|
+
setShowLanPin(false)
|
|
1287
|
+
}
|
|
1288
|
+
} catch (err: any) {
|
|
1289
|
+
setLanPinEnabled(previous)
|
|
1290
|
+
setError(err.message)
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
const revealLanPin = async () => {
|
|
1294
|
+
if (lanPin === null) {
|
|
1295
|
+
try {
|
|
1296
|
+
setLanPin((await call(MAESTRO_ENDPOINTS.lanPinStatus, {})).pin ?? null)
|
|
1297
|
+
} catch (err: any) {
|
|
1298
|
+
setError(err.message)
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
setShowLanPin(true)
|
|
1302
|
+
}
|
|
1303
|
+
const rotateLanPin = async () => {
|
|
1304
|
+
setError(null)
|
|
1305
|
+
try {
|
|
1306
|
+
const fresh = (await call(MAESTRO_ENDPOINTS.lanPinRotate, {})).pin
|
|
1307
|
+
setLanPin(fresh)
|
|
1308
|
+
setShowLanPin(true)
|
|
1309
|
+
} catch (err: any) {
|
|
1310
|
+
setError(err.message)
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
const startTunnel = async () => {
|
|
1314
|
+
setBusy(true)
|
|
1315
|
+
setError(null)
|
|
1316
|
+
try {
|
|
1317
|
+
setStatus(await call(MAESTRO_ENDPOINTS.tunnelStart, {}))
|
|
1318
|
+
} catch (err: any) {
|
|
1319
|
+
setError(err.message)
|
|
1320
|
+
} finally {
|
|
1321
|
+
setBusy(false)
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
const stopTunnel = async () => {
|
|
1325
|
+
setBusy(true)
|
|
1326
|
+
setError(null)
|
|
1327
|
+
try {
|
|
1328
|
+
setStatus(await call(MAESTRO_ENDPOINTS.tunnelStop, {}))
|
|
1329
|
+
} catch (err: any) {
|
|
1330
|
+
setError(err.message)
|
|
1331
|
+
} finally {
|
|
1332
|
+
setBusy(false)
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
const saveField = async (field: string, value: unknown) => {
|
|
1336
|
+
setError(null)
|
|
1337
|
+
setConfig((prev: any) => ({ ...prev, [field]: value }))
|
|
1338
|
+
if (field === 'supervisorModel' && configRpcCall) {
|
|
1339
|
+
try {
|
|
1340
|
+
const res = await configRpcCall('set', { domain: 'supervisor', patch: { model: value } })
|
|
1341
|
+
if (res?.ok) return
|
|
1342
|
+
} catch {}
|
|
1343
|
+
}
|
|
1344
|
+
try {
|
|
1345
|
+
await call(MAESTRO_ENDPOINTS.saveConfig, { [field]: value })
|
|
1346
|
+
} catch (err: any) {
|
|
1347
|
+
setError(err.message)
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// Nested tabs like plugin marketplace — pill bar + single panel (market catsWrap catsRow pattern)
|
|
1352
|
+
const TABS = [
|
|
1353
|
+
{ id: 'tunnel', label: 'Tunnel' },
|
|
1354
|
+
{ id: 'notify', label: 'Notify' },
|
|
1355
|
+
{ id: 'gitlab', label: 'GitLab' },
|
|
1356
|
+
{ id: 'review', label: 'Review' },
|
|
1357
|
+
{ id: 'guard', label: 'Guard' },
|
|
1358
|
+
{ id: 'blacklist', label: 'Blacklist' },
|
|
1359
|
+
{ id: 'supervisor', label: 'Supervisor' },
|
|
1360
|
+
{ id: 'notifier', label: 'Notifier' },
|
|
1361
|
+
] as const
|
|
1362
|
+
|
|
1363
|
+
const tabContents: Record<string, unknown> = {
|
|
1364
|
+
tunnel: h(
|
|
1365
|
+
'div',
|
|
1366
|
+
{ style: { display: 'flex', flexDirection: 'column' } },
|
|
1367
|
+
h(SettingRow as any, { title: 'Tunnel mode', description: 'Quick = ephemeral URL, Named = stable URL with Cloudflare setup.', control: h('select', { value: config.tunnelMode, onChange: (e: any) => saveField('tunnelMode', e.target.value), style: { height: 36, padding: '0 12px', border: `1px solid ${t.borderL2}`, borderRadius: 18, background: 'var(--dsw-alias-bg-module-platform, #F5F6F7)' as string, color: t.labelPrimary as string, font: 'inherit', fontSize: 13 } }, h('option', { value: 'quick' }, 'Quick'), h('option', { value: 'named' }, 'Named')) }),
|
|
1368
|
+
config.tunnelMode === 'named' ? h(NamedTunnelSetupNote as any, null) : null,
|
|
1369
|
+
config.tunnelMode === 'named'
|
|
1370
|
+
? h('div', { style: { display: 'flex', flexDirection: 'column' } },
|
|
1371
|
+
h(SettingRow as any, { title: 'Tunnel ID', description: 'Cloudflare tunnel ID.', control: h(FieldInput as any, { placeholder: 'Tunnel ID', value: config.tunnelId ?? '', onChange: (e: any) => saveField('tunnelId', e.target.value), style: { width: 260 } as any }) }),
|
|
1372
|
+
h(SettingRow as any, { title: 'Credentials file', description: 'Path to tunnel credentials JSON.', control: h(FieldInput as any, { placeholder: '~/.cloudflared/<id>.json', value: config.tunnelCredentialsFile ?? '', onChange: (e: any) => saveField('tunnelCredentialsFile', e.target.value), style: { width: 260 } as any }) }),
|
|
1373
|
+
h(SettingRow as any, { title: 'Hostname', description: 'Public hostname for the tunnel.', control: h(FieldInput as any, { placeholder: 'dsh.example.com', value: config.tunnelHostname ?? '', onChange: (e: any) => saveField('tunnelHostname', e.target.value), style: { width: 260 } as any }) }),
|
|
1374
|
+
)
|
|
1375
|
+
: null,
|
|
1376
|
+
h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' as const, padding: '12px 0', borderBottom: `1px solid ${t.borderL2}` } }, status?.running ? h(Button as any, { variant: 'outline', size: 'md', disabled: busy, onClick: stopTunnel }, 'Stop tunnel') : h(Button as any, { variant: 'primary', size: 'md', disabled: busy, onClick: startTunnel }, 'Start tunnel')),
|
|
1377
|
+
h('div', { style: { ...cardInsetStyle, marginTop: '12px' } }, h('div', { style: { fontSize: 13, fontWeight: 600, color: t.labelPrimary as string } }, 'Remote access — LAN'), h(LanAccess as any, { proxyStatus, lanPin: lanPinEnabled === null ? null : { enabled: lanPinEnabled, pin: lanPin, show: showLanPin, onShow: revealLanPin, onHide: () => setShowLanPin(false), onRotate: rotateLanPin, onToggle: toggleLanPin } })),
|
|
1378
|
+
h('div', { style: { ...cardInsetStyle, marginTop: '12px' } }, h('div', { style: { fontSize: 13, fontWeight: 600, color: t.labelPrimary as string } }, 'Public access'), h(PublicAccess as any, { status, pin, showPin, onRevealPin: revealPin, onHidePin: () => setShowPin(false), onRotatePin: rotatePin })),
|
|
1379
|
+
),
|
|
1380
|
+
notify: h(
|
|
1381
|
+
'div',
|
|
1382
|
+
{ style: { display: 'flex', flexDirection: 'column' } },
|
|
1383
|
+
h('div', { style: { padding: '12px 0', borderBottom: `1px solid ${t.borderL2}` } }, h('p', { style: captionStyle }, 'Sends one protected startup update with the current public-access PIN to a single chat. Leave either credential blank to disable.')),
|
|
1384
|
+
h(SettingRow as any, { title: 'Bot token', description: 'Telegram bot token from @BotFather.', control: h('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } }, h(FieldInput as any, { placeholder: '123456:ABC-DEF...', type: 'password', autoComplete: 'off', value: config.hasTelegramBotToken ? '••••••••' : '', onChange: (e: any) => saveField('telegramBotToken', e.target.value), style: { width: 220 } as any }), config.hasTelegramBotToken ? h(Button as any, { variant: 'outline', size: 'md', onClick: () => saveField('telegramBotToken', '') }, 'Clear') : null) }),
|
|
1385
|
+
h(SettingRow as any, { title: 'Chat ID', description: 'Target chat, e.g. -1001234567890.', control: h(FieldInput as any, { placeholder: '-1001234567890', value: config.telegramChatId ?? '', onChange: (e: any) => saveField('telegramChatId', e.target.value), style: { width: 220 } as any }) }),
|
|
1386
|
+
h(ToggleRow as any, { title: 'Also notify about reviews', description: 'One message per review run with its outcome.', checked: config.telegramReviewNotifications === true, onChange: (v: boolean) => saveField('telegramReviewNotifications', v) }),
|
|
1387
|
+
),
|
|
1388
|
+
gitlab: h(
|
|
1389
|
+
'div',
|
|
1390
|
+
{ style: { display: 'flex', flexDirection: 'column' } },
|
|
1391
|
+
h(SettingRow as any, { title: 'GitLab base URL', description: 'e.g. https://gitlab.example.com', control: h(FieldInput as any, { placeholder: 'https://gitlab.example.com', value: config.gitlabBaseUrl ?? '', onChange: (e: any) => saveField('gitlabBaseUrl', e.target.value), style: { width: 260 } as any }) }),
|
|
1392
|
+
h(SettingRow as any, { title: 'GitLab token', description: 'Personal access token with api scope.', control: h('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } }, h(FieldInput as any, { type: 'password', autoComplete: 'off', value: config.hasGitlabToken ? '••••••••' : '', placeholder: 'GitLab token', onChange: (e: any) => saveField('gitlabToken', e.target.value), style: { width: 200 } as any }), config.hasGitlabToken ? h(Button as any, { variant: 'outline', size: 'md', onClick: () => saveField('gitlabToken', '') }, 'Clear') : null) }),
|
|
1393
|
+
h(SettingRow as any, { title: 'Bot username', description: 'Username of the bot that posts reviews.', control: h(FieldInput as any, { placeholder: 'maestro-bot', value: config.botUsername ?? '', onChange: (e: any) => saveField('botUsername', e.target.value), style: { width: 220 } as any }) }),
|
|
1394
|
+
h(SettingRow as any, { title: 'Webhook secret', description: 'Secret token for GitLab webhooks.', control: h('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } }, h(FieldInput as any, { type: 'password', autoComplete: 'off', value: config.hasWebhookSecret ? '••••••••' : '', placeholder: 'Webhook secret', onChange: (e: any) => saveField('webhookSecret', e.target.value), style: { width: 200 } as any }), h(Button as any, { variant: 'outline', size: 'md', onClick: () => saveField('webhookSecret', generateWebhookSecret()) }, 'Generate')) }),
|
|
1395
|
+
h('div', { style: { padding: '16px 0', display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
1396
|
+
h('p', { style: captionStyle }, 'In GitLab: Settings → Webhooks, Secret token = this value, enable Merge request events. Webhook URL:'),
|
|
1397
|
+
h('div', { style: { fontFamily: 'ui-monospace, monospace', fontSize: 12, color: t.labelPrimary as string, wordBreak: 'break-all', padding: '10px 12px', borderRadius: 8, background: t.bgLayer3 as string, border: `1px solid ${t.borderL2}`, overflowWrap:'anywhere' as any } }, gitlabWebhookUrl(config.tunnelHostname)),
|
|
1398
|
+
),
|
|
1399
|
+
),
|
|
1400
|
+
review: h(
|
|
1401
|
+
'div',
|
|
1402
|
+
{ style: { display: 'flex', flexDirection: 'column' } },
|
|
1403
|
+
h(ToggleRow as any, { title: 'Re-review on push', description: 'When new commits are pushed, trigger an automatic re-review.', checked: config.autoRereviewOnPush === true, onChange: (v: boolean) => saveField('autoRereviewOnPush', v) }),
|
|
1404
|
+
h(SettingRow as any, { title: 'Global review model', description: 'Model for automated reviews. Empty = DSH default.', control: h(ReviewModelSelector as any, { value: config.reviewModel ?? null, catalog, fallbackValue: catalog?.current ?? null, fallbackLabel: 'Use DSH default', onChange: (v: any) => saveField('reviewModel', v), label: null }) }),
|
|
1405
|
+
h(SettingRow as any, { title: 'Supervisor model', description: 'Model for auto-fixing DSH Web crashes.', control: h(ReviewModelSelector as any, { value: config.supervisorModel ?? null, catalog, fallbackValue: catalog?.current ?? null, fallbackLabel: 'Use DSH default', onChange: (v: any) => saveField('supervisorModel', v), label: null }) }),
|
|
1406
|
+
h(ProjectMappingsEditor as any, { mappings: config.projectMappings ?? [], onChange: (mappings: any) => saveField('projectMappings', mappings), catalog, globalReviewModel: config.reviewModel ?? null }),
|
|
1407
|
+
),
|
|
1408
|
+
guard: h(
|
|
1409
|
+
'div',
|
|
1410
|
+
{ style: { display: 'flex', flexDirection: 'column' } },
|
|
1411
|
+
h(ToggleRow as any, { title: 'Block publish commands', description: 'Prevent publish-related commands when enabled.', checked: guard.publishBlocked === true, onChange: (v: boolean) => saveGuard({ publishBlocked: v }) }),
|
|
1412
|
+
h(ToggleRow as any, { title: 'Protect git branches', description: 'Block direct pushes to protected branches.', checked: guard.gitProtection?.enabled === true, onChange: (v: boolean) => saveGuard({ gitProtection: { enabled: v, branches: guard.gitProtection?.branches ?? ['master', 'main'] } }) }),
|
|
1413
|
+
h(SettingRow as any, { title: 'Protected branches', description: 'Comma-separated list, e.g. master, main.', control: h(FieldInput as any, { value: (guard.gitProtection?.branches ?? ['master', 'main']).join(', '), placeholder: 'master, main', onChange: (e: any) => saveGuard({ gitProtection: { enabled: guard.gitProtection?.enabled ?? true, branches: e.target.value.split(',').map((s: any) => s.trim()).filter(Boolean) } }), style: { width: 260 } as any }) }),
|
|
1414
|
+
h(ToggleRow as any, { title: 'Contain working directory', description: 'Restrict file operations to the session working directory.', checked: guard.cwdContainment === true, onChange: (v: boolean) => saveGuard({ cwdContainment: v }) }),
|
|
1415
|
+
h(SettingRow as any, { title: 'Credential file paths', description: 'Comma-separated paths to credential files.', control: h(FieldInput as any, { value: (guard.credentialPaths ?? []).join(', '), placeholder: '~/.config/credentials.yaml', onChange: (e: any) => saveGuard({ credentialPaths: e.target.value.split(',').map((s: any) => s.trim()).filter(Boolean) }), style: { width: 260 } as any }) }),
|
|
1416
|
+
),
|
|
1417
|
+
blacklist: h(
|
|
1418
|
+
'div',
|
|
1419
|
+
{ style: { display: 'flex', flexDirection: 'column' } },
|
|
1420
|
+
h('div', { style: { ...rowStyle, flexDirection:'column', alignItems:'stretch', gap: 8 } as any },
|
|
1421
|
+
h('div', { style: rowTitleStyle }, 'Blacklist patterns'),
|
|
1422
|
+
h('div', { style: rowDescStyle }, 'One pattern per line. Matching files are blocked from commit.'),
|
|
1423
|
+
h(TextareaField as any, { value: patternsText, placeholder: 'example-project\nacme-shop', onChange: (e: any) => setPatternsText(e.target.value), onBlur: (e: any) => commitBlacklistPatterns(e.target.value) }),
|
|
1424
|
+
),
|
|
1425
|
+
h('div', { style: { ...rowStyle, flexDirection:'column', alignItems:'stretch', gap: 8, borderBottom:'none' } as any },
|
|
1426
|
+
h('div', { style: rowTitleStyle }, 'Placeholder mappings'),
|
|
1427
|
+
h('div', { style: rowDescStyle }, 'JSON object mapping blocked patterns to placeholder suggestions.'),
|
|
1428
|
+
h(TextareaField as any, { value: placeholdersText, placeholder: '{"example-project":"my-project"}', onChange: (e: any) => setPlaceholdersText(e.target.value), onBlur: () => commitPlaceholders(), style: { minHeight: 80 } as any }),
|
|
1429
|
+
h('div', { style: { marginTop: 8 } }, h(Button as any, { variant: 'outline', size: 'sm', onClick: () => { commitBlacklistPatterns(patternsText); commitPlaceholders() } }, 'Save Blacklist')),
|
|
1430
|
+
),
|
|
1431
|
+
),
|
|
1432
|
+
supervisor: h(
|
|
1433
|
+
'div',
|
|
1434
|
+
{ style: { display: 'flex', flexDirection: 'column' } },
|
|
1435
|
+
h(SettingRow as any, { title: 'Check interval', description: 'Milliseconds between supervisor checks. Default 5000.', control: h(FieldInput as any, { type: 'number', value: supervisorCfg.intervalMs ?? '', placeholder: '5000', onChange: (e: any) => { const v = e.target.value === '' ? undefined : Number(e.target.value); saveSupervisorCfg({ intervalMs: v }) }, style: { width: 160 } as any }) }),
|
|
1436
|
+
h(SettingRow as any, { title: 'Down threshold', description: 'Consecutive failures before marking a session as down.', control: h(FieldInput as any, { type: 'number', value: supervisorCfg.downThreshold ?? '', placeholder: '3', onChange: (e: any) => { const v = e.target.value === '' ? undefined : Number(e.target.value); saveSupervisorCfg({ downThreshold: v }) }, style: { width: 160 } as any }) }),
|
|
1437
|
+
h(ToggleRow as any, { title: 'Auto-resume sessions', description: 'Automatically resume sessions marked as down.', checked: supervisorCfg.autoResumeEnabled === true, onChange: (v: boolean) => saveSupervisorCfg({ autoResumeEnabled: v }) }),
|
|
1438
|
+
),
|
|
1439
|
+
notifier: h(
|
|
1440
|
+
'div',
|
|
1441
|
+
{ style: { display: 'flex', flexDirection: 'column' } },
|
|
1442
|
+
h(SettingRow as any, { title: 'Bot token', description: 'Telegram bot token from @BotFather.', control: h(FieldInput as any, { type: 'password', autoComplete: 'off', value: notifierCfg.telegram?.botToken ?? '', placeholder: '123456:ABC-DEF...', onChange: (e: any) => saveNotifierCfg({ telegram: { botToken: e.target.value } }), style: { width: 260 } as any }) }),
|
|
1443
|
+
h(SettingRow as any, { title: 'Chat ID', description: 'Target chat, e.g. -1001234567890.', control: h(FieldInput as any, { value: notifierCfg.telegram?.chatId ?? '', placeholder: '-1001234567890', onChange: (e: any) => saveNotifierCfg({ telegram: { chatId: e.target.value } }), style: { width: 260 } as any }) }),
|
|
1444
|
+
h(ToggleRow as any, { title: 'Review notifications', description: 'Also notify about finished reviews.', checked: notifierCfg.telegram?.reviewNotifications === true || notifierCfg.policy?.reviewNotifications === true, onChange: (v: boolean) => saveNotifierCfg({ telegram: { reviewNotifications: v } }) }),
|
|
1445
|
+
),
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
return h(
|
|
1449
|
+
'div',
|
|
1450
|
+
{ 'data-maestro-settings-card': '', style: { display: 'flex', flexDirection: 'column', gap: 8, width: '100%', maxWidth: 640, minWidth:0, boxSizing:'border-box' as any } },
|
|
1451
|
+
h(
|
|
1452
|
+
'div',
|
|
1453
|
+
{ style: { padding: '2px 2px 8px' } },
|
|
1454
|
+
h('div', { style: { fontSize: 15, fontWeight: 600, color: t.labelPrimary as string, lineHeight: '22px' } }, 'Maestro'),
|
|
1455
|
+
h('div', { style: { fontSize: 12, color: t.labelSecondary as string, lineHeight: '16px', marginTop: 2 } }, 'Tunnel, access, review & guard — all via the shared Maestro store. Uses the same tokens and primitives as DSH settings.'),
|
|
1456
|
+
),
|
|
1457
|
+
h('div', { 'data-maestro-tabs': '', role:'tablist', 'aria-label':'Maestro settings sections' },
|
|
1458
|
+
...TABS.map(tab => h('button', { key: tab.id, 'data-maestro-tab':'', 'data-active': String(activeTab===tab.id), role:'tab', 'aria-selected': activeTab===tab.id, onClick: () => setActiveTab(tab.id) }, tab.label))
|
|
1459
|
+
),
|
|
1460
|
+
h('div', { 'data-maestro-panel': activeTab, style: { display:'flex', flexDirection:'column', gap:10, minWidth:0 } }, tabContents[activeTab] as any),
|
|
1461
|
+
error ? h('p', { style: { color: t.stateError as string, fontSize: 12, margin: '8px 0 0', padding: '8px 10px', borderRadius: 8, background: 'color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent)', border: `1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 30%, transparent)` } }, error) : null,
|
|
1462
|
+
)
|
|
1463
|
+
}
|