diff --git a/src/backend/scrobblers/AbstractScrobbleClient.ts b/src/backend/scrobblers/AbstractScrobbleClient.ts index 276d2aba..a8bab9f6 100644 --- a/src/backend/scrobblers/AbstractScrobbleClient.ts +++ b/src/backend/scrobblers/AbstractScrobbleClient.ts @@ -287,7 +287,7 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i 'Dead', (): Promise => { if(this.isReady()) { - return this.processDeadLetterQueue().then(() => null).catch((e) => { + return this.processDeadLetterQueue(undefined, 'Reprocessing bulk dead Plays by system').then(() => null).catch((e) => { this.warnings = e; this.logger.error(e); }) diff --git a/src/backend/server/api.ts b/src/backend/server/api.ts index b5693cf3..3bd1502c 100644 --- a/src/backend/server/api.ts +++ b/src/backend/server/api.ts @@ -767,7 +767,7 @@ export const setupApi = (args: ApiArgs, opts: ApiOptions = {}) => { res.status(200).send('OK'); - await ((client as AbstractScrobbleClient).processDeadLetterQueue(1000)); + await ((client as AbstractScrobbleClient).processDeadLetterQueue(1000, 'Reprocessing bulk dead Plays initiated by user')); }); router.put('/api/dead/:id', {middleware: [clientMiddleFunc(true)], hidden: true}, async (req, res, next) => { diff --git a/src/backend/sources/AbstractSource.ts b/src/backend/sources/AbstractSource.ts index 7367eced..874094b2 100644 --- a/src/backend/sources/AbstractSource.ts +++ b/src/backend/sources/AbstractSource.ts @@ -210,7 +210,7 @@ export default abstract class AbstractSource extends AbstractComponent implement 'Dead', (): Promise => { if(this.isReady()) { - return this.processDeadLetterQueue().then(() => null).catch((e) => { + return this.processDeadLetterQueue(undefined, 'Reprocessing bulk dead Plays by system').then(() => null).catch((e) => { this.warnings = e; this.logger.error(e); }) diff --git a/src/client/components/ActivityTimeline.tsx b/src/client/components/ActivityTimeline.tsx index f1edba7a..907e9bd5 100644 --- a/src/client/components/ActivityTimeline.tsx +++ b/src/client/components/ActivityTimeline.tsx @@ -1,5 +1,6 @@ import type { Collapsible } from '@chakra-ui/react'; -import { Card, Text, Icon, SkeletonCircle, SkeletonText, Span, Tabs, Timeline} from '@chakra-ui/react'; +import { Card, Text, Icon, SkeletonCircle, SkeletonText, Span, Tabs, Timeline, Tag, HStack, Stack, Separator} from '@chakra-ui/react'; +import { HiCheck } from "react-icons/hi" import dayjs from "dayjs"; import React from "react"; import { BiWrench } from "react-icons/bi"; @@ -15,7 +16,7 @@ import { activityTransformHasIssue, timelineIconProps, TimelineItemSummaryText } import { ChakraCodeBlockShort } from "./CodeBlock"; import { ErrorAlert } from "./ErrorAlert"; import { MSErrorBoundary } from "./ErrorBoundary"; -import { CheckIcon, ExclamationTriangleIcon, TimelineIndicatorIconQueued } from "./icons/ChakraIcons"; +import { CheckIcon, ExclamationTriangleIcon, TimelineIndicatorIconQueued, XIcon } from "./icons/ChakraIcons"; import { MSCollapsible } from "./MSCollapsible"; import { PlayData } from "./PlayData"; import { ScrobbleActionResult } from "./ScrobbleActionResult"; @@ -274,6 +275,20 @@ const ScrobbleResponseItem = (props: Pick ( + Attempt: {props.count} +) + +const BoolChip = (props: { text: string, check: boolean }) => ( + {props.text} + + {props.check ? : } + +) +const DupeChip = (props: { check: boolean }) => ; +const TransformChip = (props: { check: boolean }) => +const CacheChip = (props: { check: boolean }) => + const QueueTimelineItem = (props: {queueState: PlayEventQueueStateChange, collapsibleOpen: boolean}) => { const { queueState: { @@ -281,7 +296,13 @@ const QueueTimelineItem = (props: {queueState: PlayEventQueueStateChange queueStatus, queueName, error, - retries + retries, + context: { + dupeCheck, + transform, + useCache, + reason + } = {} }, createdAt, } = {}, @@ -292,6 +313,28 @@ const QueueTimelineItem = (props: {queueState: PlayEventQueueStateChange text: React.JSX.Element, title: React.JSX.Element; + const contextHints: React.JSX.Element[] = []; + const tags: React.JSX.Element[] = []; + + if(retries !== undefined && retries > 0) { + tags.push(); + } + if(dupeCheck !== undefined) { + tags.push() + } + if(transform !== undefined) { + tags.push() + } + if(useCache !== undefined) { + tags.push() + } + if(tags.length > 0) { + contextHints.push({tags}) + } + if(reason !== undefined) { + contextHints.push(Reason - {reason}); + } + switch(queueStatus) { case QUEUE_STATUS_QUEUED: indicator = ; @@ -315,7 +358,9 @@ const QueueTimelineItem = (props: {queueState: PlayEventQueueStateChange disableUntil="md" timeline unmountOnExit> + + ) } else { @@ -334,6 +379,7 @@ const QueueTimelineItem = (props: {queueState: PlayEventQueueStateChange {title} + {contextHints.length > 0 ? }>{contextHints} : undefined} ); diff --git a/src/core/Api.ts b/src/core/Api.ts index d133ad5c..35676a3f 100644 --- a/src/core/Api.ts +++ b/src/core/Api.ts @@ -1,6 +1,6 @@ import type { PickKeys } from "ts-essentials" import type { CompareOpKey, ComponentMinimalSelect } from "../backend/common/database/drizzle/drizzleTypes.ts" -import type { ClientType, ComponentAuthType, MonitoringStatus } from "./Atomic.ts" +import type { ClientType, ComponentAuthType, MonitoringStatus, QueueContext } from "./Atomic.ts" import type { SourceType } from "./Atomic.ts" import type { ComponentType, DateLike, ErrorLike, JsonPlayObject, PlayState, QueueName, Replace, SOURCE_SOT_TYPES, SourcePlayerJson } from "./Atomic.ts" import type { Dayjs } from "dayjs" @@ -36,6 +36,7 @@ export interface QueueStateApi { error?: ErrorLike updatedAt: string createdAt: string + context?: QueueContext } export interface PlayApiCommonDetailed extends PlayApiCommon { diff --git a/src/core/tests/utils/apiFixtures.ts b/src/core/tests/utils/apiFixtures.ts index cd6c8e66..ef9429c5 100644 --- a/src/core/tests/utils/apiFixtures.ts +++ b/src/core/tests/utils/apiFixtures.ts @@ -14,6 +14,7 @@ import { CALCULATED_PLAYER_STATUSES } from '../../Atomic.ts'; import { REPORTED_PLAYER_STATUSES } from '../../Atomic.ts'; import { generateArray } from "../../DataUtils.ts"; import type {ErrorIsh} from "../../ErrorUtils.ts"; +import { serializeError } from "serialize-error"; export const generatePlayApiCommon = (commonData: Partial & {play?: JsonPlayObject | PlayObject } = {}, ...playOpts: Parameters): PlayApiCommon => { let play: JsonPlayObject | PlayObject; @@ -102,7 +103,13 @@ export const generatePlayApiCommonDetailed = (opts: { input: inputRes, queueStates: [queueRes], error, - events: [] + events: [ + {eventName: 'queueStateChange', playId: 1, data: {queueName: 'ingress', queueStatus: 'queued', retries: 2, + error: serializeError(generateFakeError()), + context: {reason: 'Bulk scrobble for dead Plays by system', dupeCheck: false, transform: true}}}, + {eventName: 'playStateChange', playId: 1, data: {state: 'discarded', reason: 'Monitoring was off'}}, + {eventName: 'queueStateChange', playId: 1, data: {queueName: 'ingress', retries: 1, queueStatus: 'failed', error: serializeError(generateFakeError())}}, + ] } }