@ddtcorex/dsh-maestro-config 0.3.1 → 0.5.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.
- package/cordis.patch.yml +4 -0
- package/lib/client.js +192 -9
- package/lib/types/client/MaestroSettings.d.ts.map +1 -1
- package/lib/types/client/pin-ttl.d.ts +23 -0
- package/lib/types/client/pin-ttl.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/client/MaestroSettings.tsx +189 -32
- package/src/client/pin-ttl.ts +31 -0
- package/lib/types/client/Settings.d.ts +0 -11
- package/lib/types/client/Settings.d.ts.map +0 -1
- package/lib/types/client/maestro-card.d.ts +0 -10
- package/lib/types/client/maestro-card.d.ts.map +0 -1
- package/src/client/maestro-card.jsx +0 -945
|
@@ -13,6 +13,7 @@ import { createElement as h, useEffect, useRef, useState } from 'react'
|
|
|
13
13
|
import QRCode from 'qrcode'
|
|
14
14
|
import { MAESTRO_ENDPOINTS } from './api.js'
|
|
15
15
|
import { generateWebhookSecret, gitlabWebhookUrl } from './webhook-secret.js'
|
|
16
|
+
import { PIN_TTL_PRESETS, MAX_PIN_TTL_HOURS, presetForTtlHours } from './pin-ttl.js'
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// DSH tokens — single source, no custom hex (except QR quiet zone #fff)
|
|
@@ -941,39 +942,102 @@ function ProjectMappingsEditor({ mappings, onChange, catalog, globalReviewModel
|
|
|
941
942
|
}),
|
|
942
943
|
),
|
|
943
944
|
),
|
|
945
|
+
// Row for per-project trigger overrides — tri-state: inherit the global toggle or force on/off.
|
|
946
|
+
h(
|
|
947
|
+
'div',
|
|
948
|
+
{ 'data-maestro-project-triggers-row': '', style: { display: 'flex', gap: 12, flexWrap: 'wrap' as const, alignItems: 'flex-start' } },
|
|
949
|
+
h(
|
|
950
|
+
'label',
|
|
951
|
+
{ style: { ...fieldLabelStyle, flex: '1 1 160px', minWidth: 0 } },
|
|
952
|
+
'Re-review on push',
|
|
953
|
+
h(
|
|
954
|
+
'select',
|
|
955
|
+
{
|
|
956
|
+
value: row.rereviewOnPush === undefined ? 'inherit' : row.rereviewOnPush ? 'on' : 'off',
|
|
957
|
+
onChange: (e: any) => updateRow(i, 'rereviewOnPush', e.target.value === 'inherit' ? undefined : e.target.value === 'on'),
|
|
958
|
+
'aria-label': `Re-review on push ${i + 1}`,
|
|
959
|
+
style: {
|
|
960
|
+
height: 36,
|
|
961
|
+
width: '100%',
|
|
962
|
+
padding: '0 14px',
|
|
963
|
+
border: 'none',
|
|
964
|
+
borderRadius: 18,
|
|
965
|
+
background: 'var(--dsw-alias-bg-module-platform, #F5F6F7)' as string,
|
|
966
|
+
color: t.labelPrimary as string,
|
|
967
|
+
font: 'inherit',
|
|
968
|
+
fontSize: 13,
|
|
969
|
+
},
|
|
970
|
+
},
|
|
971
|
+
h('option', { value: 'inherit' }, 'Inherit (global)'),
|
|
972
|
+
h('option', { value: 'on' }, 'On'),
|
|
973
|
+
h('option', { value: 'off' }, 'Off'),
|
|
974
|
+
),
|
|
975
|
+
),
|
|
976
|
+
h(
|
|
977
|
+
'label',
|
|
978
|
+
{ style: { ...fieldLabelStyle, flex: '1 1 160px', minWidth: 0 } },
|
|
979
|
+
'Review on assign',
|
|
980
|
+
h(
|
|
981
|
+
'select',
|
|
982
|
+
{
|
|
983
|
+
value: row.reviewOnAssign === undefined ? 'inherit' : row.reviewOnAssign ? 'on' : 'off',
|
|
984
|
+
onChange: (e: any) => updateRow(i, 'reviewOnAssign', e.target.value === 'inherit' ? undefined : e.target.value === 'on'),
|
|
985
|
+
'aria-label': `Review on assign ${i + 1}`,
|
|
986
|
+
style: {
|
|
987
|
+
height: 36,
|
|
988
|
+
width: '100%',
|
|
989
|
+
padding: '0 14px',
|
|
990
|
+
border: 'none',
|
|
991
|
+
borderRadius: 18,
|
|
992
|
+
background: 'var(--dsw-alias-bg-module-platform, #F5F6F7)' as string,
|
|
993
|
+
color: t.labelPrimary as string,
|
|
994
|
+
font: 'inherit',
|
|
995
|
+
fontSize: 13,
|
|
996
|
+
},
|
|
997
|
+
},
|
|
998
|
+
h('option', { value: 'inherit' }, 'Inherit (global)'),
|
|
999
|
+
h('option', { value: 'on' }, 'On'),
|
|
1000
|
+
h('option', { value: 'off' }, 'Off'),
|
|
1001
|
+
),
|
|
1002
|
+
),
|
|
1003
|
+
),
|
|
944
1004
|
),
|
|
945
1005
|
),
|
|
946
1006
|
),
|
|
947
1007
|
)
|
|
948
1008
|
}
|
|
949
1009
|
|
|
950
|
-
|
|
1010
|
+
// SecretField — control-sized secret input for SettingRow controls (no label).
|
|
1011
|
+
// The server never echoes secrets back (getConfig masks them to has* flags),
|
|
1012
|
+
// so the field stays EMPTY and commits a typed draft on blur / Enter; an
|
|
1013
|
+
// untouched field keeps the stored secret and Clear writes '' to erase it.
|
|
1014
|
+
// Never render mask bullets ('••••') as the input value: a controlled input
|
|
1015
|
+
// locked to a constant string swallows keystrokes and saves bullet-contaminated
|
|
1016
|
+
// text on the first change (the GitLab-token-not-editable regression).
|
|
1017
|
+
function SecretField({ placeholder, hasSaved, onSave, width }: { placeholder: string; hasSaved?: boolean; onSave: (v: string) => void; width?: number }) {
|
|
951
1018
|
const [draft, setDraft] = useState('')
|
|
952
|
-
|
|
953
|
-
setDraft('')
|
|
954
|
-
|
|
1019
|
+
useEffect(() => {
|
|
1020
|
+
if (!hasSaved) setDraft('')
|
|
1021
|
+
}, [hasSaved])
|
|
1022
|
+
const commit = () => {
|
|
1023
|
+
if (draft !== '') {
|
|
1024
|
+
onSave(draft)
|
|
1025
|
+
setDraft('')
|
|
1026
|
+
}
|
|
955
1027
|
}
|
|
956
|
-
return h(
|
|
957
|
-
'
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
onBlur: () => {
|
|
970
|
-
if (draft !== '') onSave(draft)
|
|
971
|
-
},
|
|
972
|
-
style: { flex: 1 } as any,
|
|
973
|
-
}),
|
|
974
|
-
hasSaved === true ? h(Button as any, { variant: 'outline', size: 'sm', onClick: clear }, 'Clear') : null,
|
|
975
|
-
),
|
|
976
|
-
)
|
|
1028
|
+
return h(FieldInput as any, {
|
|
1029
|
+
placeholder: hasSaved === true ? 'saved — type new value to replace' : placeholder,
|
|
1030
|
+
type: 'password',
|
|
1031
|
+
autoComplete: 'off',
|
|
1032
|
+
value: draft,
|
|
1033
|
+
onChange: (e: any) => setDraft(e.target.value),
|
|
1034
|
+
onBlur: commit,
|
|
1035
|
+
onKeyDown: (e: any) => {
|
|
1036
|
+
if (e.key === 'Enter') (e.target as HTMLInputElement).blur()
|
|
1037
|
+
},
|
|
1038
|
+
'aria-label': placeholder,
|
|
1039
|
+
style: { width: width ?? 200 } as any,
|
|
1040
|
+
})
|
|
977
1041
|
}
|
|
978
1042
|
|
|
979
1043
|
function ToggleField({ label, caption, checked, onChange }: { label: string; caption?: string; checked?: boolean; onChange: (v: boolean) => void }) {
|
|
@@ -992,6 +1056,17 @@ function LanAccess({ proxyStatus, lanPin }: { proxyStatus: any; lanPin: any }) {
|
|
|
992
1056
|
if (!proxyStatus?.running) {
|
|
993
1057
|
return h('p', { style: { color: t.stateError as string, fontSize: 12, margin: '8px 0 0' } }, proxyStatus?.errorMessage ?? 'Proxy not running')
|
|
994
1058
|
}
|
|
1059
|
+
if (urls.length === 0) {
|
|
1060
|
+
// Without a LAN listener there is no LAN entry to advertise. Saying "no PIN
|
|
1061
|
+
// needed" next to the reader's own reachable-but-public URL would promise
|
|
1062
|
+
// access this card cannot deliver, so point at the setting instead.
|
|
1063
|
+
return h(
|
|
1064
|
+
'div',
|
|
1065
|
+
null,
|
|
1066
|
+
h('p', { style: captionStyle }, 'No LAN listener is configured — set a LAN port under Tunnel to expose one.'),
|
|
1067
|
+
lanPin !== null ? h(LanPinRow as any, { lanPin }) : null,
|
|
1068
|
+
)
|
|
1069
|
+
}
|
|
995
1070
|
return h(
|
|
996
1071
|
'div',
|
|
997
1072
|
null,
|
|
@@ -1040,7 +1115,59 @@ function LanPinRow({ lanPin }: { lanPin: any }) {
|
|
|
1040
1115
|
)
|
|
1041
1116
|
}
|
|
1042
1117
|
|
|
1043
|
-
|
|
1118
|
+
/**
|
|
1119
|
+
* Login-cookie lifetime. Presets cover the common choices; "Custom…" reveals a
|
|
1120
|
+
* free hours input saved on blur/Enter — never per keystroke, so a half-typed
|
|
1121
|
+
* number is not persisted. The host resolver and the settings RPC own the
|
|
1122
|
+
* authoritative bounds; the input's min/max are an affordance only.
|
|
1123
|
+
*/
|
|
1124
|
+
function PinSessionTtl({ hours, onSave }: { hours: number | undefined; onSave: (hours: number) => void }) {
|
|
1125
|
+
const selected = presetForTtlHours(hours)
|
|
1126
|
+
const [custom, setCustom] = useState(false)
|
|
1127
|
+
const [draft, setDraft] = useState('')
|
|
1128
|
+
const customActive = custom || selected === null
|
|
1129
|
+
const commit = () => {
|
|
1130
|
+
const parsed = Number(draft)
|
|
1131
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > MAX_PIN_TTL_HOURS) return
|
|
1132
|
+
onSave(parsed)
|
|
1133
|
+
}
|
|
1134
|
+
return h(
|
|
1135
|
+
'div',
|
|
1136
|
+
{ 'data-maestro-pin-ttl': '', style: { display: 'flex', flexDirection: 'column' as const, gap: 6, alignItems: 'flex-end' } },
|
|
1137
|
+
h(
|
|
1138
|
+
'select',
|
|
1139
|
+
{
|
|
1140
|
+
'data-maestro-pin-ttl-select': '',
|
|
1141
|
+
value: customActive ? 'custom' : String(selected),
|
|
1142
|
+
onChange: (e: any) => {
|
|
1143
|
+
const next = e.target.value
|
|
1144
|
+
if (next === 'custom') { setCustom(true); setDraft(String(hours ?? 24)); return }
|
|
1145
|
+
setCustom(false)
|
|
1146
|
+
onSave(Number(next))
|
|
1147
|
+
},
|
|
1148
|
+
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 },
|
|
1149
|
+
},
|
|
1150
|
+
...PIN_TTL_PRESETS.map((preset) => h('option', { key: preset.hours, value: String(preset.hours) }, preset.label)),
|
|
1151
|
+
h('option', { key: 'custom', value: 'custom' }, 'Custom…'),
|
|
1152
|
+
),
|
|
1153
|
+
customActive
|
|
1154
|
+
? h(FieldInput as any, {
|
|
1155
|
+
'data-maestro-pin-ttl-custom': '',
|
|
1156
|
+
inputMode: 'numeric',
|
|
1157
|
+
placeholder: 'hours',
|
|
1158
|
+
value: draft,
|
|
1159
|
+
min: 1,
|
|
1160
|
+
max: MAX_PIN_TTL_HOURS,
|
|
1161
|
+
onChange: (e: any) => setDraft(e.target.value),
|
|
1162
|
+
onBlur: commit,
|
|
1163
|
+
onKeyDown: (e: any) => { if (e.key === 'Enter') commit() },
|
|
1164
|
+
style: { width: 120 } as any,
|
|
1165
|
+
})
|
|
1166
|
+
: null,
|
|
1167
|
+
)
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function PublicAccess({ status, pin, showPin, onRevealPin, onHidePin, onRotatePin, pinTtlHours, onSavePinTtl }: { status: any; pin: string | null; showPin: boolean; onRevealPin: () => void; onHidePin: () => void; onRotatePin: () => void; pinTtlHours: number | undefined; onSavePinTtl: (hours: number) => void }) {
|
|
1044
1171
|
return h(
|
|
1045
1172
|
'div',
|
|
1046
1173
|
null,
|
|
@@ -1065,6 +1192,14 @@ function PublicAccess({ status, pin, showPin, onRevealPin, onHidePin, onRotatePi
|
|
|
1065
1192
|
h(Button as any, { variant: 'outline', size: 'sm', onClick: onRotatePin }, 'Rotate'),
|
|
1066
1193
|
),
|
|
1067
1194
|
h('p', { style: captionStyle }, 'Stays the same across tunnel and DSH restarts; use Rotate when you need a new PIN.'),
|
|
1195
|
+
h(SettingRow as any, {
|
|
1196
|
+
title: 'PIN session duration',
|
|
1197
|
+
// The caption lives in the description column on purpose: a long caption
|
|
1198
|
+
// inside the control column claims its intrinsic width (min-width: auto)
|
|
1199
|
+
// and squeezes the title/description to a few characters per line.
|
|
1200
|
+
description: 'How long a browser stays signed in after entering the PIN. Applies to the next login; covers the public tunnel and LAN access.',
|
|
1201
|
+
control: h(PinSessionTtl as any, { hours: pinTtlHours, onSave: onSavePinTtl }),
|
|
1202
|
+
}),
|
|
1068
1203
|
)
|
|
1069
1204
|
}
|
|
1070
1205
|
|
|
@@ -1508,11 +1643,32 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
|
|
|
1508
1643
|
setBusy(false)
|
|
1509
1644
|
}
|
|
1510
1645
|
}
|
|
1646
|
+
const SECRET_SAVE_FLAGS: Record<string, string> = { gitlabToken: 'hasGitlabToken', webhookSecret: 'hasWebhookSecret' }
|
|
1511
1647
|
const saveField = async (field: string, value: unknown) => {
|
|
1512
1648
|
setError(null)
|
|
1513
|
-
|
|
1649
|
+
// Optimistic update — for secrets only flip the has* presence flag, never
|
|
1650
|
+
// stash the raw secret in state (the server never echoes it back).
|
|
1651
|
+
setConfig((prev: any) => {
|
|
1652
|
+
const next = { ...prev, [field]: value }
|
|
1653
|
+
const flag = SECRET_SAVE_FLAGS[field]
|
|
1654
|
+
if (flag) {
|
|
1655
|
+
delete next[field]
|
|
1656
|
+
next[flag] = value !== ''
|
|
1657
|
+
}
|
|
1658
|
+
return next
|
|
1659
|
+
})
|
|
1514
1660
|
try {
|
|
1515
|
-
|
|
1661
|
+
// saveConfig returns the masked config — merge it so has* flags (and a
|
|
1662
|
+
// cleared secret) sync without waiting for a reload.
|
|
1663
|
+
const saved = await call(MAESTRO_ENDPOINTS.saveConfig, { [field]: value })
|
|
1664
|
+
if (saved && typeof saved === 'object') {
|
|
1665
|
+
setConfig((prev: any) => {
|
|
1666
|
+
const next = { ...prev, ...(saved as object) }
|
|
1667
|
+
const flag = SECRET_SAVE_FLAGS[field]
|
|
1668
|
+
if (flag) delete next[field]
|
|
1669
|
+
return next
|
|
1670
|
+
})
|
|
1671
|
+
}
|
|
1516
1672
|
} catch (err: any) {
|
|
1517
1673
|
setError(err.message)
|
|
1518
1674
|
}
|
|
@@ -1543,16 +1699,16 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
|
|
|
1543
1699
|
)
|
|
1544
1700
|
: null,
|
|
1545
1701
|
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')),
|
|
1702
|
+
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, pinTtlHours: config.pinSessionTtlHours, onSavePinTtl: (value: number) => saveField('pinSessionTtlHours', value) })),
|
|
1546
1703
|
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 } })),
|
|
1547
|
-
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 })),
|
|
1548
1704
|
),
|
|
1549
1705
|
gitlab: h(
|
|
1550
1706
|
'div',
|
|
1551
1707
|
{ style: { display: 'flex', flexDirection: 'column' } },
|
|
1552
1708
|
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 }) }),
|
|
1553
|
-
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(
|
|
1709
|
+
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(SecretField as any, { placeholder: 'GitLab token', hasSaved: config.hasGitlabToken === true, width: 200, onSave: (v: string) => saveField('gitlabToken', v) }), config.hasGitlabToken ? h(Button as any, { variant: 'outline', size: 'md', onClick: () => saveField('gitlabToken', '') }, 'Clear') : null) }),
|
|
1554
1710
|
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 }) }),
|
|
1555
|
-
h(SettingRow as any, { title: 'Webhook secret', description: 'Secret token for GitLab webhooks.', control: h('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } }, h(
|
|
1711
|
+
h(SettingRow as any, { title: 'Webhook secret', description: 'Secret token for GitLab webhooks.', control: h('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } }, h(SecretField as any, { placeholder: 'Webhook secret', hasSaved: config.hasWebhookSecret === true, width: 200, onSave: (v: string) => saveField('webhookSecret', v) }), h(Button as any, { variant: 'outline', size: 'md', onClick: () => saveField('webhookSecret', generateWebhookSecret()) }, 'Generate')) }),
|
|
1556
1712
|
h('div', { style: { padding: '16px 0', display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
1557
1713
|
h('p', { style: captionStyle }, 'In GitLab: Settings → Webhooks, Secret token = this value, enable Merge request events. Webhook URL:'),
|
|
1558
1714
|
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)),
|
|
@@ -1562,6 +1718,7 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
|
|
|
1562
1718
|
'div',
|
|
1563
1719
|
{ style: { display: 'flex', flexDirection: 'column' } },
|
|
1564
1720
|
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) }),
|
|
1721
|
+
h(ToggleRow as any, { title: 'Review on assign', description: 'When the bot is assigned as reviewer, trigger an automatic review.', checked: config.autoReviewOnAssign !== false, onChange: (v: boolean) => saveField('autoReviewOnAssign', v) }),
|
|
1565
1722
|
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 }) }),
|
|
1566
1723
|
h(ProjectMappingsEditor as any, { mappings: config.projectMappings ?? [], onChange: (mappings: any) => saveField('projectMappings', mappings), catalog, globalReviewModel: config.reviewModel ?? null }),
|
|
1567
1724
|
),
|
|
@@ -1606,7 +1763,7 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }: { rpcCall: any; c
|
|
|
1606
1763
|
'div',
|
|
1607
1764
|
{ style: { display: 'flex', flexDirection: 'column' } },
|
|
1608
1765
|
h('div', { style: { padding: '12px 0', borderBottom: `1px solid ${t.borderL2}` } }, h('p', { style: captionStyle }, 'Telegram bot settings for notifications: startup PIN, review digests, PIN rotation. Leave blank to disable.')),
|
|
1609
|
-
h(SettingRow as any, { title: 'Bot token', description: 'Telegram bot token from @BotFather.', control: h('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } }, h(
|
|
1766
|
+
h(SettingRow as any, { title: 'Bot token', description: 'Telegram bot token from @BotFather.', control: h('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } }, h(SecretField as any, { placeholder: '123456:ABC-DEF...', hasSaved: (notifierCfg.telegram?.botToken ?? '') !== '', width: 220, onSave: (v: string) => saveNotifierCfg({ telegram: { botToken: v } }) }), notifierCfg.telegram?.botToken ? h(Button as any, { variant: 'outline', size: 'md', onClick: () => saveNotifierCfg({ telegram: { botToken: '' } }) }, 'Clear') : null) }),
|
|
1610
1767
|
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: 220 } as any }) }),
|
|
1611
1768
|
h(ToggleRow as any, { title: 'Review notifications', description: 'Also notify about finished reviews.', checked: notifierCfg.policy?.reviewNotifications === true, onChange: (v: boolean) => saveNotifierCfg({ policy: { reviewNotifications: v } }) }),
|
|
1612
1769
|
),
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PIN login-cookie lifetimes offered by the Settings card.
|
|
3
|
+
* `0` = session cookie (expires when the browser closes); an absent setting
|
|
4
|
+
* means the dsh-maestro-remote default of 24 hours.
|
|
5
|
+
*/
|
|
6
|
+
export const PIN_TTL_PRESETS: ReadonlyArray<{ hours: number; label: string }> = [
|
|
7
|
+
{ hours: 0, label: 'Session only' },
|
|
8
|
+
{ hours: 1, label: '1 hour' },
|
|
9
|
+
{ hours: 8, label: '8 hours' },
|
|
10
|
+
{ hours: 24, label: '1 day (default)' },
|
|
11
|
+
{ hours: 168, label: '7 days' },
|
|
12
|
+
{ hours: 720, label: '30 days' },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
/** Mirrors DEFAULT_PIN_SESSION_TTL_HOURS in dsh-maestro-remote. */
|
|
16
|
+
export const DEFAULT_PIN_TTL_HOURS = 24
|
|
17
|
+
/**
|
|
18
|
+
* Mirrors MAX_PIN_SESSION_TTL_HOURS in dsh-maestro-remote. This bound only
|
|
19
|
+
* hints the custom input — the host resolver and the settings RPC enforce it.
|
|
20
|
+
*/
|
|
21
|
+
export const MAX_PIN_TTL_HOURS = 8760
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The preset hours to show as selected, or `null` when the stored value is not
|
|
25
|
+
* a preset (the caller then shows the "Custom…" option). An unset or unusable
|
|
26
|
+
* value falls back to the product default.
|
|
27
|
+
*/
|
|
28
|
+
export function presetForTtlHours(hours: number | undefined): number | null {
|
|
29
|
+
const value = hours === undefined || !Number.isFinite(hours) ? DEFAULT_PIN_TTL_HOURS : hours
|
|
30
|
+
return PIN_TTL_PRESETS.some((preset) => preset.hours === value) ? value : null
|
|
31
|
+
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
type RpcCall = (endpoint: string, payload?: unknown, signal?: AbortSignal) => Promise<unknown>;
|
|
2
|
-
export declare function Settings({ configRpcCall }: {
|
|
3
|
-
configRpcCall: RpcCall;
|
|
4
|
-
}): import("react").DetailedReactHTMLElement<{
|
|
5
|
-
'data-maestro-guard-settings': string;
|
|
6
|
-
style: {
|
|
7
|
-
maxWidth: number;
|
|
8
|
-
};
|
|
9
|
-
}, HTMLElement>;
|
|
10
|
-
export default Settings;
|
|
11
|
-
//# sourceMappingURL=Settings.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Settings.d.ts","sourceRoot":"","sources":["../../../src/client/Settings.tsx"],"names":[],"mappings":"AAIA,KAAK,OAAO,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;AAiF9F,wBAAgB,QAAQ,CAAC,EAAE,aAAa,EAAE,EAAE;IAAE,aAAa,EAAE,OAAO,CAAA;CAAE;;;;;gBAiRrE;AAGD,eAAe,QAAQ,CAAA"}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export function MaestroSettingsTab({ rpcCall, configRpcCall }: {
|
|
2
|
-
rpcCall: any;
|
|
3
|
-
configRpcCall: any;
|
|
4
|
-
}): import("react").DetailedReactHTMLElement<{
|
|
5
|
-
'data-maestro-settings-card': string;
|
|
6
|
-
style: {
|
|
7
|
-
maxWidth: number;
|
|
8
|
-
};
|
|
9
|
-
}, HTMLElement>;
|
|
10
|
-
//# sourceMappingURL=maestro-card.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"maestro-card.d.ts","sourceRoot":"","sources":["../../../src/client/maestro-card.jsx"],"names":[],"mappings":"AAweA;;;;;;;;gBA2bC"}
|