diff --git a/src/TickerQ.Dashboard/wwwroot/src/components/crontickerComponents/CronOccurrenceDialog.vue b/src/TickerQ.Dashboard/wwwroot/src/components/crontickerComponents/CronOccurrenceDialog.vue index 0a181fd6..53a023ac 100644 --- a/src/TickerQ.Dashboard/wwwroot/src/components/crontickerComponents/CronOccurrenceDialog.vue +++ b/src/TickerQ.Dashboard/wwwroot/src/components/crontickerComponents/CronOccurrenceDialog.vue @@ -9,8 +9,7 @@ import { methodName, type TickerNotificationHubType } from '@/hub/tickerNotifica import type { GetCronTickerOccurrenceResponse } from '@/http/services/types/cronTickerOccurrenceService.types' import { ConfirmDialogProps } from '@/components/common/ConfirmDialog.vue' import PaginationFooter from '@/components/PaginationFooter.vue' -import { formatTime } from '@/utilities/dateTimeParser' -import { format } from 'timeago.js' +import { formatTime, formatTimeAgo } from '@/utilities/dateTimeParser' const confirmDialog = useDialog<{ data: string }>().withComponent( () => import('@/components/common/ConfirmDialog.vue'), @@ -100,7 +99,7 @@ const addHubListeners = async () => { ...currentItem, ...val, status: Status[val.status as any], - executedAt: `${format(val.executedAt)} (took ${formatTime(val.elapsedTime as number, true)})`, + executedAt: `${formatTimeAgo(val.executedAt)} (took ${formatTime(val.elapsedTime as number, true)})`, retryIntervals: currentItem.retryIntervals, lockedAt: currentItem.lockedAt, // Preserve existing lockedAt lockHolder: currentItem.lockHolder, diff --git a/src/TickerQ.Dashboard/wwwroot/src/http/services/cronTickerOccurrenceService.ts b/src/TickerQ.Dashboard/wwwroot/src/http/services/cronTickerOccurrenceService.ts index cab9e972..471364d2 100644 --- a/src/TickerQ.Dashboard/wwwroot/src/http/services/cronTickerOccurrenceService.ts +++ b/src/TickerQ.Dashboard/wwwroot/src/http/services/cronTickerOccurrenceService.ts @@ -1,9 +1,8 @@ -import { formatDate, formatTime } from '@/utilities/dateTimeParser'; +import { formatDate, formatTime, formatTimeAgo } from '@/utilities/dateTimeParser'; import { useBaseHttpService } from '../base/baseHttpService'; import { Status } from './types/base/baseHttpResponse.types'; import { GetCronTickerOccurrenceGraphDataRequest, GetCronTickerOccurrenceGraphDataResponse, GetCronTickerOccurrenceRequest, GetCronTickerOccurrenceResponse } from './types/cronTickerOccurrenceService.types'; -import { format} from 'timeago.js'; import { nameof } from '@/utilities/nameof'; import { useTimeZoneStore } from '@/stores/timeZoneStore'; @@ -23,14 +22,12 @@ const getByCronTickerId = () => { } // Safely set status with null check - if (response.status !== undefined && response.status !== null) { + if (response.status != null) { response.status = Status[response.status as any]; } - if (response.executedAt != null || response.executedAt != undefined) { - // Ensure the datetime is treated as UTC by adding 'Z' if missing - const utcExecutedAt = response.executedAt.endsWith('Z') ? response.executedAt : response.executedAt + 'Z'; - response.executedAt = `${format(utcExecutedAt)} (took ${formatTime(response.elapsedTime as number, true)})`; + if (response.executedAt != null) { + response.executedAt = `${formatTimeAgo(response.executedAt)} (took ${formatTime(response.elapsedTime as number, true)})`; } const utcExecutionTime = response.executionTime.endsWith('Z') ? response.executionTime : response.executionTime + 'Z'; @@ -75,17 +72,15 @@ const getByCronTickerIdPaginated = () => { if (!item) return item; // Safely set status with null check and ensure it's always a string - if (item.status !== undefined && item.status !== null) { + if (item.status != null) { const statusValue = Status[item.status as any]; item.status = statusValue !== undefined ? statusValue : String(item.status); } else { item.status = 'Unknown'; } - if (item.executedAt != null || item.executedAt != undefined) { - // Ensure the datetime is treated as UTC by adding 'Z' if missing - const utcExecutedAt = item.executedAt.endsWith('Z') ? item.executedAt : item.executedAt + 'Z'; - item.executedAt = `${format(utcExecutedAt)} (took ${formatTime(item.elapsedTime as number, true)})`; + if (item.executedAt != null) { + item.executedAt = `${formatTimeAgo(item.executedAt)} (took ${formatTime(item.elapsedTime as number, true)})`; } const utcExecutionTime = item.executionTime.endsWith('Z') ? item.executionTime : item.executionTime + 'Z'; diff --git a/src/TickerQ.Dashboard/wwwroot/src/http/services/timeTickerService.ts b/src/TickerQ.Dashboard/wwwroot/src/http/services/timeTickerService.ts index 644a90d3..5a2ab9e6 100644 --- a/src/TickerQ.Dashboard/wwwroot/src/http/services/timeTickerService.ts +++ b/src/TickerQ.Dashboard/wwwroot/src/http/services/timeTickerService.ts @@ -1,5 +1,5 @@ -import { formatDate, formatTime } from '@/utilities/dateTimeParser'; +import { formatDate, formatTime, formatTimeAgo } from '@/utilities/dateTimeParser'; import { useBaseHttpService } from '../base/baseHttpService'; import { Status } from './types/base/baseHttpResponse.types'; import { @@ -11,7 +11,6 @@ import { UpdateTimeTickerRequest } from './types/timeTickerService.types' import { nameof } from '@/utilities/nameof'; -import { format} from 'timeago.js'; import { useFunctionNameStore } from '@/stores/functionNames'; import { useTimeZoneStore } from '@/stores/timeZoneStore'; @@ -36,12 +35,12 @@ const getTimeTickers = () => { // Recursive function to process item and its children const processItem = (item: GetTimeTickerResponse): GetTimeTickerResponse => { // Safely set status with null check - if (item.status !== undefined && item.status !== null) { + if (item.status != null) { item.status = Status[item.status as any]; } - if (item.executedAt != null || item.executedAt != undefined) - item.executedAt = `${format(item.executedAt)} (took ${formatTime(item.elapsedTime as number, true)})`; + if (item.executedAt != null) + item.executedAt = `${formatTimeAgo(item.executedAt)} (took ${formatTime(item.elapsedTime as number, true)})`; item.executionTimeFormatted = formatDate(item.executionTime, true, timeZoneStore.effectiveTimeZone); item.requestType = functionNamesStore.getNamespaceOrNull(item.function) ?? ''; @@ -108,12 +107,12 @@ const getTimeTickersPaginated = () => { if (response && response.items && Array.isArray(response.items)) { response.items = response.items.map((item: GetTimeTickerResponse) => { const processItem = (item: GetTimeTickerResponse): GetTimeTickerResponse => { - if (item.status !== undefined && item.status !== null) { + if (item.status != null) { item.status = Status[item.status as any]; } - if (item.executedAt != null || item.executedAt != undefined) - item.executedAt = `${format(item.executedAt)} (took ${formatTime(item.elapsedTime as number, true)})`; + if (item.executedAt != null) + item.executedAt = `${formatTimeAgo(item.executedAt)} (took ${formatTime(item.elapsedTime as number, true)})`; item.executionTimeFormatted = formatDate(item.executionTime, true, timeZoneStore.effectiveTimeZone); item.requestType = functionNamesStore.getNamespaceOrNull(item.function) ?? ''; diff --git a/src/TickerQ.Dashboard/wwwroot/src/utilities/dateTimeParser.ts b/src/TickerQ.Dashboard/wwwroot/src/utilities/dateTimeParser.ts index 6102b001..917bde4f 100644 --- a/src/TickerQ.Dashboard/wwwroot/src/utilities/dateTimeParser.ts +++ b/src/TickerQ.Dashboard/wwwroot/src/utilities/dateTimeParser.ts @@ -1,3 +1,4 @@ +import { format as timeago } from 'timeago.js'; export function formatDate( utcDateString: string, @@ -126,3 +127,14 @@ export function formatFromUtcToLocal(utcDateString: string): string { return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}` } + +export function formatTimeAgo(date: string | Date): string { + // Front-end often passes dates as strings straight up from JSON payloads. + // All dates on back-end are UTC but dates loaded by EF have DateTimeKind.Unspecified by default, + // which is serialized to JSON without any offset suffix. + // We have to specify them as UTC so that they're not parsed as local time by JS. + if (typeof date === 'string' && !date.endsWith('Z')) { + date = date + 'Z' + } + return timeago(date) +} \ No newline at end of file