@mzzsfy/dsh-cron-board 0.3.1 → 0.3.2

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mzzsfy/dsh-cron-board",
3
3
  "description": "轻量级定时任务看板:环境变量集中管理与定时任务调度,执行体支持本地脚本与 dsh 会话任务",
4
- "version": "0.3.1",
4
+ "version": "0.3.2",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "exports": {
package/src/client.js CHANGED
@@ -679,7 +679,7 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
679
679
  h('button', { className: 'cb-button cb-button--danger', onClick: onConfirm }, '删除')))
680
680
  }
681
681
 
682
- function JobsTab({ jobs, status, now, reload, wsModel }) {
682
+ function JobsTab({ jobs, status, now, reload, applyJobPatch, wsModel }) {
683
683
  const [formJob, setFormJob] = useState(null)
684
684
  const [notice, setNotice] = useState(null)
685
685
  const [kindFilter, setKindFilter] = useState('all')
@@ -691,7 +691,13 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
691
691
  setTimeout(reload, 300)
692
692
  }
693
693
  const toggleJob = async (job) => {
694
- await request('PATCH', 'jobs/' + job.id, { enabled: !job.enabled })
694
+ // 乐观翻转:受控开关即时到位;失败回滚并提示,不静默
695
+ applyJobPatch(job.id, { enabled: !job.enabled })
696
+ const outcome = await request('PATCH', 'jobs/' + job.id, { enabled: !job.enabled })
697
+ if (!outcome.ok) {
698
+ applyJobPatch(job.id, { enabled: job.enabled })
699
+ setNotice('切换失败:' + outcome.error)
700
+ }
695
701
  reload()
696
702
  }
697
703
  const deleteJob = async (job) => {
@@ -811,14 +817,20 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
811
817
  h('button', { className: 'cb-button cb-button--danger', onClick: () => doApply(true) }, '覆盖导入')))
812
818
  }
813
819
 
814
- function EnvsTab({ envs, reload }) {
820
+ function EnvsTab({ envs, reload, applyEnvPatch }) {
815
821
  const [formRow, setFormRow] = useState(null)
816
822
  const [importing, setImporting] = useState(false)
817
823
  const [exportText, setExportText] = useState(null)
818
824
  const [notice, setNotice] = useState(null)
819
825
 
820
826
  const toggleEnv = async (row) => {
821
- await request('PATCH', 'envs/' + row.id, { enabled: !row.enabled })
827
+ // 乐观翻转:受控开关即时到位;失败回滚并提示,不静默
828
+ applyEnvPatch(row.id, { enabled: !row.enabled })
829
+ const outcome = await request('PATCH', 'envs/' + row.id, { enabled: !row.enabled })
830
+ if (!outcome.ok) {
831
+ applyEnvPatch(row.id, { enabled: row.enabled })
832
+ setNotice('切换失败:' + outcome.error)
833
+ }
822
834
  reload()
823
835
  }
824
836
  const deleteEnv = async (row) => {
@@ -921,6 +933,13 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
921
933
  const [now, setNow] = useState(Date.now())
922
934
  const [reloadFlag, setReloadFlag] = useState(0)
923
935
  const reload = useCallback(() => setReloadFlag((value) => value + 1), [])
936
+ // 乐观更新:受控开关点击即时到位,PATCH 失败由调用方回滚;reload 拉权威态对账
937
+ const applyJobPatch = useCallback((id, patch) => {
938
+ setJobs((prev) => prev.map((job) => (job.id === id ? { ...job, ...patch } : job)))
939
+ }, [])
940
+ const applyEnvPatch = useCallback((id, patch) => {
941
+ setEnvs((prev) => prev.map((row) => (row.id === id ? { ...row, ...patch } : row)))
942
+ }, [])
924
943
 
925
944
  useEffect(() => {
926
945
  if (!active) return undefined
@@ -949,8 +968,8 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
949
968
  status ? h('span', { className: 'cb-meta' },
950
969
  '运行中 ' + status.scheduler.active + ' · 队列 ' + status.scheduler.queued +
951
970
  (status.nextAt ? ' · 下次 ' + relativeTime(status.nextAt, now) : '')) : null),
952
- tab === 'jobs' ? h(JobsTab, { key: 'jobs', jobs, status, now, reload, wsModel }) : null,
953
- tab === 'envs' ? h(EnvsTab, { key: 'envs', envs, reload }) : null,
971
+ tab === 'jobs' ? h(JobsTab, { key: 'jobs', jobs, status, now, reload, applyJobPatch, wsModel }) : null,
972
+ tab === 'envs' ? h(EnvsTab, { key: 'envs', envs, reload, applyEnvPatch }) : null,
954
973
  tab === 'logs' ? h(LogsTab, { key: 'logs', jobs, reload, reloadFlag }) : null)
955
974
  }
956
975
 
@@ -60,6 +60,24 @@ test('client.js 无顶层词法声明(经典 script 书挡内安全)', () => {
60
60
  assert.ok(trimmed.endsWith('})()'), '必须以 IIFE 书挡结尾')
61
61
  })
62
62
 
63
+ test('启停开关乐观更新契约:即时翻转 + 失败回滚提示,禁止等待往返才反馈', () => {
64
+ // Given 受控 switch(checked 锁定 props)点击后无本地 state 变化不产生视觉反馈
65
+ // When toggleJob/toggleEnv 发起 PATCH
66
+ // Then 先乐观翻转,失败回滚并 notice,最后 reload 对账权威态(顺序锁定)
67
+ for (const fn of ['toggleJob', 'toggleEnv']) {
68
+ const start = source.indexOf('const ' + fn + ' = async')
69
+ assert.ok(start > 0, `缺少 ${fn}`)
70
+ const body = source.slice(start, source.indexOf('reload()', start))
71
+ assert.match(body, /apply(Job|Env)Patch\([^)]*, \{ enabled: !/m, `${fn} 必须先乐观翻转`)
72
+ assert.match(body, /if \(!outcome\.ok\)/m, `${fn} 必须检查 PATCH 结果`)
73
+ assert.match(body, /apply(Job|Env)Patch\([^)]*, \{ enabled: (job|row)\.enabled \}\)/m, `${fn} 失败必须回滚`)
74
+ assert.match(body, /切换失败:/m, `${fn} 失败必须提示,禁止静默`)
75
+ }
76
+ // 两个 Tab 均接收乐观更新通道(Board 下传)
77
+ assert.match(source, /applyJobPatch, wsModel/)
78
+ assert.match(source, /envs, reload, applyEnvPatch/)
79
+ })
80
+
63
81
  test('client.js 主页面双形态挂载契约(默认主界面;设置开关手动移入侧边栏;无自动回退)', () => {
64
82
  // Given 用户要求:默认永远主界面;检测到 better-sidebar 提供设置项,由用户手动移入;
65
83
  // 移入后页签关闭/禁用不再自动返回主界面(挂载权完全交给用户与侧边栏)