diff --git a/.changeset/job-observation-history.md b/.changeset/job-observation-history.md new file mode 100644 index 00000000..f2dc8351 --- /dev/null +++ b/.changeset/job-observation-history.md @@ -0,0 +1,6 @@ +--- +'@inkcre/core': patch +'@inkcre/web': patch +--- + +修复 Job 详情的观测时间范围与成功提交后的弹窗关闭。详情明确展示信息库中的提交事实,历史诊断支持带时钟余量的默认窗口和最长 31 天的绝对范围,并说明部分生命周期覆盖。Core 导出单次查询宽度常量供界面复用。 diff --git a/apps/web/src/components/obsrv/LogsViewer/LogsViewer.md b/apps/web/src/components/obsrv/LogsViewer/LogsViewer.md index 7bbcb1c7..afaa1cda 100644 --- a/apps/web/src/components/obsrv/LogsViewer/LogsViewer.md +++ b/apps/web/src/components/obsrv/LogsViewer/LogsViewer.md @@ -2,6 +2,10 @@ Job 内嵌观测视图,复用 ObservationExplorer 和 Core 的统一查询接口,不直接访问 PostgreSQL 或选择外部供应商。 -`jobId` 同时关联新结构化事件和历史 job.N 日志;`since` 使用 Job 创建时间,旧任务不因默认最近窗口而被遗漏。也保留独立 `traceId` 查询。 +`jobId` 同时关联新结构化事件和历史 job.N 日志;默认窗口从创建前一小时延至结束后一小时,给不同 Peer 的时钟偏差留出余量。进行中的作业以刷新时间估算结束端。超过 31 天时显示末段,可在关联筛选选择更早的绝对窗口。也保留独立 `traceId` 查询。 查询有界,记录过多时提示缩窄范围;不承诺全量历史。`enablePolling` 与 `pollingInterval` 控制刷新。连接或业务范围变化使在途响应失效;刷新失败保留同范围已有记录并提示重试。 + +Job 页面从信息库中的 Job 行展示“提交已保存”、当前状态与创建、开始、结束时间。 +这些信息可由其他 Peer 在提交方退出后读取。观测列表和摘要只消费实际返回的记录, +查询为空时仍显示该 Job 的持久信息。 diff --git a/apps/web/src/components/obsrv/LogsViewer/LogsViewer.ts b/apps/web/src/components/obsrv/LogsViewer/LogsViewer.ts index c0839ee6..49fd25a4 100644 --- a/apps/web/src/components/obsrv/LogsViewer/LogsViewer.ts +++ b/apps/web/src/components/obsrv/LogsViewer/LogsViewer.ts @@ -2,8 +2,8 @@ export const logsViewerProps = { /** Business Job correlation also includes legacy job.N log records. */ jobId: { type: Number, default: undefined }, traceId: { type: String, default: undefined }, - /** Job creation time keeps an old Job's history in its query window. */ - since: { type: String, default: undefined }, + createdAt: { type: String, default: undefined }, + closedAt: { type: String, default: undefined }, pollingInterval: { type: Number, default: 5000 }, enablePolling: { type: Boolean, default: true }, } diff --git a/apps/web/src/components/obsrv/LogsViewer/LogsViewer.vue b/apps/web/src/components/obsrv/LogsViewer/LogsViewer.vue index 26e2cd6c..2e200719 100644 --- a/apps/web/src/components/obsrv/LogsViewer/LogsViewer.vue +++ b/apps/web/src/components/obsrv/LogsViewer/LogsViewer.vue @@ -11,7 +11,8 @@ defineEmits(logsViewerEmits) embedded :job-id="props.jobId" :trace-id="props.traceId" - :since="props.since" + :job-created-at="props.createdAt" + :job-closed-at="props.closedAt" :enable-polling="props.enablePolling" :polling-interval="props.pollingInterval" /> diff --git a/apps/web/src/components/obsrv/ObservationExplorer/ObservationExplorer.md b/apps/web/src/components/obsrv/ObservationExplorer/ObservationExplorer.md index e16daa66..a9516665 100644 --- a/apps/web/src/components/obsrv/ObservationExplorer/ObservationExplorer.md +++ b/apps/web/src/components/obsrv/ObservationExplorer/ObservationExplorer.md @@ -5,5 +5,10 @@ - 查询是有界时间窗口,截断时要求缩窄范围;摘要只统计已返回记录。 - 连接或范围变化使在途结果失效;刷新失败保留同一范围的已有结果并标明陈旧。 - 日志、事件和操作记录共用明细与关联筛选;业务 Job 状态始终来自 Job 页面。 -- `jobId`、`traceId`、`since` 限制嵌入范围;`enablePolling`/`pollingInterval` 控制刷新。 +- `jobId` 和 `traceId` 限制嵌入范围,`jobCreatedAt` 和 `jobClosedAt` 为默认时间窗提供作业生命周期;`enablePolling`/`pollingInterval` 控制刷新。 - `embedded` 收起全局导航标题和关联筛选,保留时间、严重度、错误与后端状态。 + +Job 默认范围在创建至结束的时间段两端各留一小时余量,运行中以刷新时间作为结束端。 +一小时是可调整的初始范围。每次查询最多 31 天,默认超长范围取靠近结束端的末段并提示更早阶段。 +绝对时间输入支持查询任意历史时段,手动窗口覆盖部分生命周期时也显示范围提示。 +记录保留由当前后端配置决定。结束时间更新触发一次刷新,之后仍可手动刷新读取晚到记录。 diff --git a/apps/web/src/components/obsrv/ObservationExplorer/ObservationExplorer.ts b/apps/web/src/components/obsrv/ObservationExplorer/ObservationExplorer.ts index c8db9de6..83e1f6db 100644 --- a/apps/web/src/components/obsrv/ObservationExplorer/ObservationExplorer.ts +++ b/apps/web/src/components/obsrv/ObservationExplorer/ObservationExplorer.ts @@ -1,8 +1,33 @@ +import { OBSERVATION_MAX_WINDOW_MS } from '@inkcre/core' + export interface ObservationExplorerProps { embedded?: boolean jobId?: number traceId?: string - since?: string + jobCreatedAt?: string + jobClosedAt?: string enablePolling?: boolean pollingInterval?: number } + +const hour = 60 * 60 * 1000 + +/** Job state and observation timestamps can come from different Peer clocks. */ +export function defaultObservationWindow( + now: number, + hours: number, + jobCreatedAt?: string, + jobClosedAt?: string +): { since: string; until: string; limited: boolean } { + let until = now + let since = now - hours * hour + if (jobCreatedAt) { + const created = Date.parse(jobCreatedAt) + const end = jobClosedAt ? Date.parse(jobClosedAt) : now + until = Math.max(created, end) + hour + since = Math.min(created, end) - hour + } + const limited = until - since > OBSERVATION_MAX_WINDOW_MS + since = Math.max(since, until - OBSERVATION_MAX_WINDOW_MS) + return { since: new Date(since).toISOString(), until: new Date(until).toISOString(), limited } +} diff --git a/apps/web/src/components/obsrv/ObservationExplorer/ObservationExplorer.vue b/apps/web/src/components/obsrv/ObservationExplorer/ObservationExplorer.vue index 84856de1..837c3a19 100644 --- a/apps/web/src/components/obsrv/ObservationExplorer/ObservationExplorer.vue +++ b/apps/web/src/components/obsrv/ObservationExplorer/ObservationExplorer.vue @@ -6,12 +6,13 @@ import { InkButton, InkDropdown, InkForm, InkInput, InkLoading } from '@inkcre/u import { configStore, createObservabilityClient, + OBSERVATION_MAX_WINDOW_MS, type ObservationPage, type ObservationQuery, type ObservationRecord, type ObservabilityStatus, } from '@inkcre/core' -import type { ObservationExplorerProps } from './ObservationExplorer' +import { defaultObservationWindow, type ObservationExplorerProps } from './ObservationExplorer' const props = withDefaults(defineProps(), { embedded: false, @@ -31,6 +32,7 @@ const chronological = ref(false) const customSince = ref('') const customUntil = ref('') const page = ref(null) +const windowLimited = ref(false) const status = ref(null) const loading = ref(false) const error = ref(false) @@ -67,6 +69,15 @@ const meanDuration = computed(() => : null ) const history = computed(() => status.value?.history) +const partialJobWindow = computed(() => { + if (!page.value || !props.jobCreatedAt) return false + const created = Date.parse(props.jobCreatedAt) + const end = props.jobClosedAt ? Date.parse(props.jobClosedAt) : Date.now() + return ( + Math.min(created, end) < Date.parse(page.value.since) || + Math.max(created, end) >= Date.parse(page.value.until) + ) +}) const windowOptions = computed(() => [ { value: 1, label: t('observability.lastHour') }, { value: 24, label: t('observability.lastDay') }, @@ -109,9 +120,14 @@ async function refresh(): Promise { } } const current = generation - const until = appliedUntil ?? new Date().toISOString() - const since = - appliedSince ?? props.since ?? new Date(Date.now() - appliedHours * 3600000).toISOString() + const defaultWindow = defaultObservationWindow( + Date.now(), + appliedHours, + props.jobCreatedAt, + props.jobClosedAt + ) + const since = appliedSince ?? defaultWindow.since + const until = appliedUntil ?? defaultWindow.until loading.value = true error.value = false statusError.value = false @@ -123,8 +139,10 @@ async function refresh(): Promise { if (current !== generation) return loading.value = false hasLoaded.value = true - if (queryResult.status === 'fulfilled') page.value = queryResult.value - else error.value = true + if (queryResult.status === 'fulfilled') { + page.value = queryResult.value + windowLimited.value = appliedSince === null && defaultWindow.limited + } else error.value = true if (statusResult.status === 'fulfilled') status.value = statusResult.value else statusError.value = true } @@ -139,7 +157,10 @@ function apply(): void { const end = Date.parse(customUntil.value) if ( (customSince.value || customUntil.value) && - (!Number.isFinite(start) || !Number.isFinite(end) || start >= end) + (!Number.isFinite(start) || + !Number.isFinite(end) || + start >= end || + end - start > OBSERVATION_MAX_WINDOW_MS) ) { validationError.value = t('observability.invalidWindow') return @@ -182,6 +203,14 @@ const { pause, resume } = useIntervalFn( () => props.pollingInterval, { immediate: false } ) +watch( + () => props.jobClosedAt, + () => { + generation++ + loading.value = false + void refresh() + } +) watch( () => props.enablePolling, (enabled) => (enabled ? resume() : pause()), @@ -194,7 +223,7 @@ watch( configStore.metaConfig.INKCRE_PEER_ID, props.jobId, props.traceId, - props.since, + props.jobCreatedAt, ], () => { generation++ @@ -265,7 +294,7 @@ onUnmounted(() => {
{
- {{ t('observability.correlate') }} + {{ t(embedded ? 'observability.timeRange' : 'observability.correlate') }}
{

{{ t('observability.absoluteWindow') }}

+

+ {{ t('observability.jobWindow') }} +

{

{{ t('observability.sampleNote') }}

+

+ {{ t('observability.windowLimited') }} +

+

+ {{ t('observability.partialJobWindow') }} +

{{ t('observability.truncated') }}

diff --git a/apps/web/src/locales/messages/en.json b/apps/web/src/locales/messages/en.json index 0809cce5..6038a9b8 100644 --- a/apps/web/src/locales/messages/en.json +++ b/apps/web/src/locales/messages/en.json @@ -284,7 +284,9 @@ "notAvailable": "N/A", "notFound": "Job not found", "sourceNotFound": "Source not found", - "openDiagnostics": "Open diagnostics (search by Job ID)" + "openDiagnostics": "Open diagnostics (search by Job ID)", + "submissionSaved": "Submission saved. The status and times below come from the Job record in the info-base.", + "currentStatus": "Current status" }, "logs": { "loading": "Loading logs…", @@ -503,22 +505,26 @@ "errors": "Error records", "meanDuration": "Mean observed operation duration", "notObserved": "Not observed", - "sampleNote": "These summaries describe returned records only, not deployment-wide error rates, totals or complete performance metrics.", + "sampleNote": "Counts and mean duration are calculated from the returned records.", "truncated": "The result limit was reached. Narrow the time window or add correlation filters. This timeline may be incomplete.", "loadFailed": "Observation query failed. Check the filters, connection and backend configuration, then retry. A failed query is not an empty result.", "keptResults": "Previously returned records remain below and have not been refreshed.", "empty": "No observation records in this window.", - "emptyExplanation": "This does not prove that no work ran. Older Jobs, disabled recording or expired data may have no records.", + "emptyExplanation": "Adjust the time range or filters. Job details show the submission, status and lifecycle times stored in the info-base.", "records": "Observation records", "openJob": "Open Job #{id}", "sameTrace": "Filter this Trace", "samePeer": "Filter this Peer", "sameOperation": "Filter this operation", "attributes": "Record attributes", - "absoluteWindow": "Optional absolute window (local time). Fill both fields to override the time shortcut; clear them to return to the shortcut.", + "absoluteWindow": "Query any historical period, up to 31 days per request. Times use your local timezone. Enter both times to override the default range, or clear them to restore it. Record retention follows the selected backend configuration.", "since": "From", "until": "Until", - "invalidWindow": "Enter both dates, with the end after the start.", - "configuration": "Configuration" + "invalidWindow": "Enter both times, with the end after the start and a range of at most 31 days.", + "configuration": "Configuration", + "timeRange": "Time range", + "jobWindow": "The default range adds one hour before Job creation and after closure. Active Jobs use the refresh time for the end. Choose an absolute range to look earlier or later.", + "windowLimited": "The default range covers the 31 days nearest the end. Use Time range to query earlier stages in separate windows.", + "partialJobWindow": "This query covers part of the Job lifecycle. Adjust the time range to inspect earlier or later stages." } } diff --git a/apps/web/src/locales/messages/zh-CN.json b/apps/web/src/locales/messages/zh-CN.json index d46c6025..6991c17e 100644 --- a/apps/web/src/locales/messages/zh-CN.json +++ b/apps/web/src/locales/messages/zh-CN.json @@ -284,7 +284,9 @@ "notAvailable": "不可用", "notFound": "未找到任务", "sourceNotFound": "未找到数据源", - "openDiagnostics": "打开诊断入口(使用 Job ID 查询)" + "openDiagnostics": "打开诊断入口(使用 Job ID 查询)", + "submissionSaved": "提交已保存。下列状态和时间来自信息库中的 Job 记录。", + "currentStatus": "当前状态" }, "logs": { "loading": "正在读取日志…", @@ -503,22 +505,26 @@ "errors": "错误记录", "meanDuration": "已观测操作平均耗时", "notObserved": "未观测到", - "sampleNote": "仅统计当前返回记录,不代表整个部署的错误率、总量或完整性能指标。", + "sampleNote": "数量与平均耗时依据当前返回记录计算。", "truncated": "结果已达到数量上限。请缩窄时间窗口或增加关联筛选;当前时间线可能不完整。", "loadFailed": "观测查询失败。请检查筛选条件、当前连接及后端配置后重试;失败不是空结果。", "keptResults": "下方保留上次成功返回的记录,尚未刷新。", "empty": "当前范围内没有观测记录。", - "emptyExplanation": "这不能证明业务没有执行。旧任务、关闭采集或已过保留期的数据可能没有记录。", + "emptyExplanation": "可调整时间范围或筛选条件,并在 Job 详情查看信息库中的提交、状态和起止时间。", "records": "观测记录", "openJob": "打开 Job #{id}", "sameTrace": "筛选同一 Trace", "samePeer": "筛选同一 Peer", "sameOperation": "筛选同一操作", "attributes": "记录属性", - "absoluteWindow": "可选的绝对时间范围(本地时间)。填写两个时间会覆盖快捷窗口;清空后恢复快捷窗口。", + "absoluteWindow": "可查询任意历史时间段,每次最长 31 天。时间采用本地时区。填写起止时间后覆盖默认范围,清空可恢复默认范围。记录保留由当前后端配置决定。", "since": "开始时间", "until": "结束时间", - "invalidWindow": "请填写完整时间范围,结束时间必须晚于开始时间。", - "configuration": "配置" + "invalidWindow": "请填写完整时间范围,结束时间须晚于开始时间,每次最多查询 31 天。", + "configuration": "配置", + "timeRange": "时间范围", + "jobWindow": "默认范围在 Job 创建至结束的时间段前后各留 1 小时余量。进行中的 Job 使用刷新时间作为结束端,可选择更早或更晚的绝对范围。", + "windowLimited": "默认范围已收窄为靠近结束端的 31 天。更早的阶段可通过“时间范围”分段查询。", + "partialJobWindow": "当前查询只覆盖 Job 生命周期的一部分,可调整时间范围查看更早或更晚的阶段。" } } diff --git a/apps/web/src/views/jobs/job/job.scss b/apps/web/src/views/jobs/job/job.scss index a5cf9891..258bb6f2 100644 --- a/apps/web/src/views/jobs/job/job.scss +++ b/apps/web/src/views/jobs/job/job.scss @@ -56,6 +56,12 @@ } .metadata { + &__notice { + @include apply-font(body-sm); + color: sys-var(color, text, subtle); + margin: 0; + } + &__header { display: flex; justify-content: space-between; diff --git a/apps/web/src/views/jobs/job/job.vue b/apps/web/src/views/jobs/job/job.vue index 963b08b8..3044ceb7 100644 --- a/apps/web/src/views/jobs/job/job.vue +++ b/apps/web/src/views/jobs/job/job.vue @@ -149,6 +149,8 @@ watch(

{{ t('navigation.job') }} #{{ job.id }}

+ + @@ -161,7 +163,7 @@ watch( >{{ t('job.openDiagnostics') }} - +