定时任务前端复核修正(第 2 轮):错过已跳过的悬停不再替服务端认定是超出补发宽限,没有记原因时不指向不存在的原因

服务端把两种没有补发的触发都记成 kind=misfire、status=skipped_misfire:一是超出补发宽限,二是在宽限内恢复时
积压的那几次(只发起最近一次,更早的不补)。触发类型「定时」的悬停却一律写「错过后已超出补发宽限」,积压那条的
原因写的是「积压的触发不补,只发起最近一次」,同一行给出两个互相矛盾的原因,而这条明明是在宽限以内被跳过的。

- 触发类型的悬停改成不点明原因:「按计划应在此时触发,错过后没有补发,原因见本条记录」;本条没有记原因时去掉
  「原因见本条记录」。
- 结局悬停的兜底说明(本条没有记原因时用)一起改:错过已跳过不再写「超出补发宽限」,两种原因都适用;发起失败
  不再写「到点」(手动立即运行也会发起失败),也不再写「原因见本条记录」(用到兜底时本条恰好没有原因)。
- check-schedules 的触发历史夹具按服务端的原话各放一条积压不补、超出宽限的跳过,外加错过已跳过、发起失败各一条
  没有记原因的;断言整行连同各处悬停只说本条记的原因,不说另一种,不指向不存在的原因。修复前这几项红 5 项。
This commit is contained in:
ThearchyHelios
2026-10-05 11:05:42 +08:00
parent defd118ea6
commit 4b972b4f43
3 changed files with 50 additions and 15 deletions
+11 -4
View File
@@ -2792,19 +2792,26 @@ export const SCHEDULE_TEXT = {
manual: '手动「立即运行」',
misfire: '错过触发时间后,在补发宽限内恢复时补发的一次',
} as Record<import('../types').ScheduleFireKind, string>,
/** 错过且超出补发宽限、没有补发的(服务端记 kind=misfire、status=skipped_misfire):类型按「定时」写 */
fireKindMissedHint: '按计划应在此时触发,错过后已超出补发宽限,没有补发',
/**
* 错过、没有补发的(服务端记 kind=misfire、status=skipped_misfire):类型按「定时」写。没有补发的原因有两种(超出
* 补发宽限;宽限内恢复时积压的触发不补,只发起最近一次),这里不替服务端认定是哪一种,本条记了原因时指向它
*/
fireKindMissedHint: (hasReason: boolean) => `按计划应在此时触发,错过后没有补发${hasReason ? ',原因见本条记录' : ''}`,
fireStatus: {
started: '已发起',
skipped_overlap: '上次未结束已跳过',
skipped_misfire: '错过已跳过',
failed_to_start: '发起失败',
} as Record<import('../types').ScheduleFireStatus, string>,
/**
* 结局的一般说明,本条没有记原因时悬停用:只说这种结局是什么,不指向不存在的原因。错过已跳过的两种原因都适用,
* 发起失败也可能是手动立即运行,不写「到点」
*/
fireStatusHint: {
started: '已发起运行',
skipped_overlap: '上一次运行还没有结束,按重叠策略跳过了这一次',
skipped_misfire: '服务停机等原因错过了触发时间,且超出补发宽限,这一次没有发起',
failed_to_start: '到点发起时被拒绝,原因见本条记录',
skipped_misfire: '服务停机等原因错过了触发时间,这一次没有发起',
failed_to_start: '未能发起运行,服务端未提供原因',
} as Record<import('../types').ScheduleFireStatus, string>,
event: { finished: '运行结束', failed: '运行失败', awaiting_approval: '等待审批' } as Record<import('../types').ScheduleEvent, string>,
noticeStatus: { ok: '已发送', failed: '发送失败' } as Record<'ok' | 'failed', string>,
+4 -3
View File
@@ -111,14 +111,15 @@ export function FireStatusChip({ fire }: { fire: ScheduleFire }) {
}
/**
* 触发类型:定时 / 手动 / 补发。服务端把错过的触发都记成 kind=misfire,宽限内补发了的是 started,超出宽限没补发的
* 是 skipped_misfire:后者没有「补发」这回事,按「定时」写,结局由旁边的「错过已跳过」说
* 触发类型:定时 / 手动 / 补发。服务端把错过的触发都记成 kind=misfire:宽限内补发的那一次照常有结局,没有补发的
* 是 skipped_misfire。后者没有「补发」这回事,按「定时」写,结局由旁边的「错过已跳过」说。没有补发有两种原因(超出
* 补发宽限;宽限内恢复时积压的那几次不补),悬停不替服务端认定是哪一种,否则会和本条记的原因自相矛盾
*/
export function FireKindChip({ fire }: { fire: ScheduleFire }) {
const missed = fire.kind === 'misfire' && fire.status === 'skipped_misfire'
const kind = missed ? 'scheduled' : fire.kind
return (
<span className="chip shrink-0" title={missed ? T.fireKindMissedHint : T.fireKindHint[kind]} data-fire-kind-chip={fire.kind}>
<span className="chip shrink-0" title={missed ? T.fireKindMissedHint(!!fire.reason) : T.fireKindHint[kind]} data-fire-kind-chip={fire.kind}>
{T.fireKind[kind] ?? kind}
</span>
)
+35 -8
View File
@@ -6,7 +6,7 @@
// 记录页看不出哪些运行来自定时任务、390px 窄屏被表单撑出横向滚动。
// 还有几处时序和口径:晚到的刷新把刚切换的开关盖回去、先开触发历史再开设置时带着旧版本撞假的 409、保存途中又改了
// 别的字段时替换过的地址明文留在框里、服务端会拒的组合(未发布 + 正式运行、必填没填、已发布版本里没有的字段)
// 照样放行、测试消息的结果把服务端的整句又套一层。
// 照样放行、测试消息的结果把服务端的整句又套一层、错过已跳过的那条悬停另说一个原因(和本条记的原因对不上)。
//
// 浏览器的本机时区故意设成 America/Los_Angeles,和定时任务的时区(Asia/Shanghai、America/New_York)都不同:
// 两者相同时「按定时任务时区写」和「按本机写」在页面上看不出区别,时间相关的断言一律比对按定时任务时区写出的原文。
@@ -371,18 +371,28 @@ const S2 = {
/**
* 触发历史:25 条,新的在前,按 20 条一页给。计划时间都是北京时间周一 09:00(往前数第 i+1 周)。
* 几种类型、结局、通知结果各放一条;错过且超出宽限的照服务端的记法是 kind=misfire、status=skipped_misfire
* 几种类型、结局、通知结果各放一条。错过、没有补发的,服务端一律记成 kind=misfire、status=skipped_misfire,
* 原因却有两种,各放一条、原因照服务端的原话写:f03 是停机一周多、在宽限内恢复时积压的那一次(最近一次就是补发的
* f02),f05 是超出补发宽限的。f06、f07 没有记原因(约定里 reason 可以是 null),看兜底的说明
*/
function makeFires() {
const out = []
const local = (at) => {
const w = wall(Date.parse(at), 'Asia/Shanghai')
return `${w.y}-${pad2(w.mo)}-${pad2(w.d)} ${pad2(w.h)}:${pad2(w.mi)}`
}
const missed = { kind: 'misfire', status: 'skipped_misfire', run_id: null, run_status: null }
for (let i = 0; i < 25; i++) {
const at = iso(nextMonday9(-(i + 1)))
let f = fire(`f${String(i).padStart(2, '0')}`, { fire_at: at, created_at: at })
if (i === 0) f = { ...f, kind: 'manual', notified: [{ event: 'finished', status: 'ok', at }, { event: 'failed', status: 'failed', at }] }
if (i === 1) f = { ...f, status: 'skipped_overlap', run_id: null, run_status: null, reason: '上一次运行(#9f8e7d)尚未结束' }
if (i === 2) f = { ...f, kind: 'misfire', run_status: 'failed' }
if (i === 3) f = { ...f, kind: 'misfire', status: 'skipped_misfire', run_id: null, run_status: null, reason: '服务停机,错过触发时间 3 小时,超出补发宽限 1 小时' }
if (i === 3) f = { ...f, ...missed, reason: `服务未运行,错过了 ${local(at)} 的触发;积压的触发不补,只发起最近一次` }
if (i === 4) f = { ...f, status: 'failed_to_start', run_id: null, run_status: null, reason: '工作流「门店周报」还没有发布:正式运行需要已发布的版本' }
if (i === 5) f = { ...f, ...missed, reason: `服务未运行,错过了 ${local(at)} 的触发,超出补发宽限(1 小时),没有发起` }
if (i === 6) f = { ...f, ...missed, reason: null }
if (i === 7) f = { ...f, status: 'failed_to_start', run_id: null, run_status: null, reason: null }
out.push(f)
}
return out
@@ -1270,11 +1280,28 @@ await section('详情:触发历史分页', async () => {
check('上次未结束已跳过:写原因,没有运行链接', t1.includes('上次未结束已跳过') && t1.includes('#9f8e7d') && await row('f01').locator('a[href^="/runs/"]').count() === 0,
t1.replace(/\s+/g, ' '))
check('补发写「补发」,运行状态照实写', (await row('f02').innerText()).includes('补发') && (await row('f02').innerText()).includes('失败'))
check('错过已跳过写原因', (await row('f03').innerText()).includes('错过已跳过') && (await row('f03').innerText()).includes('超出补发宽限'))
const k3 = row('f03').locator('[data-fire-kind-chip]')
const k3title = (await k3.getAttribute('title')) ?? ''
check('错过且超出宽限、没有补发的:类型不写「补发」,悬停也不说补发了一次', (await k3.innerText()).trim() === '定时'
&& !k3title.includes('补发的一次') && k3title.includes('没有补发'), `${await k3.innerText()} | ${k3title}`)
// 一行里给人看的字:正文连同各处悬停。错过、没有补发的原因只能是本条记的那一种,页面不能另替它认定一种
const said = async (id) => [await row(id).innerText(),
...await row(id).locator('[title]').evaluateAll((els) => els.map((e) => e.getAttribute('title')))].join(' | ').replace(/\s+/g, ' ')
for (const [id, label, own, other] of [['f03', '宽限内积压的', '积压的触发不补', '超出补发宽限'], ['f05', '超出宽限的', '超出补发宽限', '积压']]) {
const k = row(id).locator('[data-fire-kind-chip]')
const kt = (await k.getAttribute('title')) ?? ''
const t = await row(id).innerText()
check(`错过已跳过(${label}):写本条的原因`, t.includes('错过已跳过') && t.includes(own), t.replace(/\s+/g, ' '))
check(`错过已跳过(${label}):类型写「定时」,悬停说没有补发、指向本条的原因,不说补发了一次`, (await k.innerText()).trim() === '定时'
&& !kt.includes('补发的一次') && kt.includes('没有补发') && kt.includes('原因见本条记录'), `${await k.innerText()} | ${kt}`)
const all = await said(id)
check(`错过已跳过(${label}):整行连同悬停都不说另一种原因(${other})`, !all.includes(other), all)
}
const s6 = await said('f06')
check('错过已跳过、没有记原因:不写原因行,悬停不替它认定原因、不指向不存在的原因', s6.includes('错过已跳过')
&& !s6.includes('原因:') && !s6.includes('超出补发宽限') && !s6.includes('积压') && !s6.includes('原因见'), s6)
const st6 = (await row('f06').locator('[data-fire-status-chip]').getAttribute('title')) ?? ''
check('……结局的悬停写这种结局的一般说明', st6.includes('错过') && st6.includes('没有发起'), st6)
const s7 = await said('f07')
const st7 = (await row('f07').locator('[data-fire-status-chip]').getAttribute('title')) ?? ''
check('发起失败、没有记原因:悬停说清没有原因,不指向不存在的原因', s7.includes('发起失败') && !s7.includes('原因:')
&& !s7.includes('原因见') && st7.includes('未能发起运行') && st7.includes('未提供原因'), s7)
check('发起失败写原因', (await row('f04').innerText()).includes('发起失败') && (await row('f04').innerText()).includes('还没有发布'))
const when = row('f05').locator('time')
check('计划时间悬停看时区', ((await when.getAttribute('title')) ?? '').includes('Asia/Shanghai'), (await when.getAttribute('title')) ?? '')