@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.
@@ -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
- function SecretInput({ label, placeholder, hasSaved, onSave }: { label: string; placeholder: string; hasSaved?: boolean; onSave: (v: string) => void }) {
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
- const clear = () => {
953
- setDraft('')
954
- onSave('')
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
- 'div',
958
- null,
959
- h('label', { style: fieldLabelStyle }, label),
960
- h(
961
- 'div',
962
- { style: { display: 'flex', gap: 8 } },
963
- h(FieldInput as any, {
964
- placeholder: hasSaved === true ? 'saved leave blank to keep' : placeholder,
965
- type: 'password',
966
- autoComplete: 'off',
967
- value: draft,
968
- onChange: (e: any) => setDraft(e.target.value),
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
- function PublicAccess({ status, pin, showPin, onRevealPin, onHidePin, onRotatePin }: { status: any; pin: string | null; showPin: boolean; onRevealPin: () => void; onHidePin: () => void; onRotatePin: () => void }) {
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
- setConfig((prev: any) => ({ ...prev, [field]: value }))
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
- await call(MAESTRO_ENDPOINTS.saveConfig, { [field]: value })
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(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) }),
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(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')) }),
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(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: 220 } as any }), notifierCfg.telegram?.botToken ? h(Button as any, { variant: 'outline', size: 'md', onClick: () => saveNotifierCfg({ telegram: { botToken: '' } }) }, 'Clear') : null) }),
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"}