fix(frontend): ジョブキューインスペクタの型エラー解消 (#16020)

* fix(frontend): ジョブキューインスペクタの型エラー解消

* fix

* fix

* fix

* fix
This commit is contained in:
かっこかり 2025-05-22 12:06:07 +09:00 committed by GitHub
parent c7318f5803
commit 000ed1f51f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 501 additions and 95 deletions

View File

@ -39,6 +39,7 @@ import type {
} from './QueueModule.js'; } from './QueueModule.js';
import type httpSignature from '@peertube/http-signature'; import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
import type { Packed } from '@/misc/json-schema.js';
export const QUEUE_TYPES = [ export const QUEUE_TYPES = [
'system', 'system',
@ -774,13 +775,13 @@ export class QueueService {
} }
@bindThis @bindThis
private packJobData(job: Bull.Job) { private packJobData(job: Bull.Job): Packed<'QueueJob'> {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const stacktrace = job.stacktrace ? job.stacktrace.filter(Boolean) : []; const stacktrace = job.stacktrace ? job.stacktrace.filter(Boolean) : [];
stacktrace.reverse(); stacktrace.reverse();
return { return {
id: job.id, id: job.id!,
name: job.name, name: job.name,
data: job.data, data: job.data,
opts: job.opts, opts: job.opts,

View File

@ -31,7 +31,11 @@ import { packedChannelSchema } from '@/models/json-schema/channel.js';
import { packedAntennaSchema } from '@/models/json-schema/antenna.js'; import { packedAntennaSchema } from '@/models/json-schema/antenna.js';
import { packedClipSchema } from '@/models/json-schema/clip.js'; import { packedClipSchema } from '@/models/json-schema/clip.js';
import { packedFederationInstanceSchema } from '@/models/json-schema/federation-instance.js'; import { packedFederationInstanceSchema } from '@/models/json-schema/federation-instance.js';
import { packedQueueCountSchema } from '@/models/json-schema/queue.js'; import {
packedQueueCountSchema,
packedQueueMetricsSchema,
packedQueueJobSchema,
} from '@/models/json-schema/queue.js';
import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js'; import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js';
import { import {
packedEmojiDetailedAdminSchema, packedEmojiDetailedAdminSchema,
@ -100,6 +104,8 @@ export const refs = {
PageBlock: packedPageBlockSchema, PageBlock: packedPageBlockSchema,
Channel: packedChannelSchema, Channel: packedChannelSchema,
QueueCount: packedQueueCountSchema, QueueCount: packedQueueCountSchema,
QueueMetrics: packedQueueMetricsSchema,
QueueJob: packedQueueJobSchema,
Antenna: packedAntennaSchema, Antenna: packedAntennaSchema,
Clip: packedClipSchema, Clip: packedClipSchema,
FederationInstance: packedFederationInstanceSchema, FederationInstance: packedFederationInstanceSchema,

View File

@ -28,3 +28,110 @@ export const packedQueueCountSchema = {
}, },
}, },
} as const; } as const;
// Bull.Metrics
export const packedQueueMetricsSchema = {
type: 'object',
properties: {
meta: {
type: 'object',
optional: false, nullable: false,
properties: {
count: {
type: 'number',
optional: false, nullable: false,
},
prevTS: {
type: 'number',
optional: false, nullable: false,
},
prevCount: {
type: 'number',
optional: false, nullable: false,
},
},
},
data: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'number',
optional: false, nullable: false,
},
},
count: {
type: 'number',
optional: false, nullable: false,
},
},
} as const;
export const packedQueueJobSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false, nullable: false,
},
name: {
type: 'string',
optional: false, nullable: false,
},
data: {
type: 'object',
optional: false, nullable: false,
},
opts: {
type: 'object',
optional: false, nullable: false,
},
timestamp: {
type: 'number',
optional: false, nullable: false,
},
processedOn: {
type: 'number',
optional: true, nullable: false,
},
processedBy: {
type: 'string',
optional: true, nullable: false,
},
finishedOn: {
type: 'number',
optional: true, nullable: false,
},
progress: {
type: 'object',
optional: false, nullable: false,
},
attempts: {
type: 'number',
optional: false, nullable: false,
},
delay: {
type: 'number',
optional: false, nullable: false,
},
failedReason: {
type: 'string',
optional: false, nullable: false,
},
stacktrace: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
returnValue: {
type: 'object',
optional: false, nullable: false,
},
isFailed: {
type: 'boolean',
optional: false, nullable: false,
},
},
} as const;

View File

@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = { export const meta = {
@ -14,6 +13,15 @@ export const meta = {
requireCredential: true, requireCredential: true,
requireModerator: true, requireModerator: true,
kind: 'read:admin:queue', kind: 'read:admin:queue',
res: {
type: 'array',
optional: false, nullable: false,
items: {
optional: false, nullable: false,
ref: 'QueueJob',
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = { export const meta = {
@ -14,6 +13,118 @@ export const meta = {
requireCredential: true, requireCredential: true,
requireModerator: true, requireModerator: true,
kind: 'read:admin:queue', kind: 'read:admin:queue',
res: {
type: 'object',
optional: false, nullable: false,
properties: {
name: {
type: 'string',
optional: false, nullable: false,
enum: QUEUE_TYPES,
},
qualifiedName: {
type: 'string',
optional: false, nullable: false,
},
counts: {
type: 'object',
optional: false, nullable: false,
additionalProperties: {
type: 'number',
},
},
isPaused: {
type: 'boolean',
optional: false, nullable: false,
},
metrics: {
type: 'object',
optional: false, nullable: false,
properties: {
completed: {
optional: false, nullable: false,
ref: 'QueueMetrics',
},
failed: {
optional: false, nullable: false,
ref: 'QueueMetrics',
},
},
},
db: {
type: 'object',
optional: false, nullable: false,
properties: {
version: {
type: 'string',
optional: false, nullable: false,
},
mode: {
type: 'string',
optional: false, nullable: false,
enum: ['cluster', 'standalone', 'sentinel'],
},
runId: {
type: 'string',
optional: false, nullable: false,
},
processId: {
type: 'string',
optional: false, nullable: false,
},
port: {
type: 'number',
optional: false, nullable: false,
},
os: {
type: 'string',
optional: false, nullable: false,
},
uptime: {
type: 'number',
optional: false, nullable: false,
},
memory: {
type: 'object',
optional: false, nullable: false,
properties: {
total: {
type: 'number',
optional: false, nullable: false,
},
used: {
type: 'number',
optional: false, nullable: false,
},
fragmentationRatio: {
type: 'number',
optional: false, nullable: false,
},
peak: {
type: 'number',
optional: false, nullable: false,
},
},
},
clients: {
type: 'object',
optional: false, nullable: false,
properties: {
blocked: {
type: 'number',
optional: false, nullable: false,
},
connected: {
type: 'number',
optional: false, nullable: false,
},
},
},
},
}
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = { export const meta = {
@ -14,6 +13,47 @@ export const meta = {
requireCredential: true, requireCredential: true,
requireModerator: true, requireModerator: true,
kind: 'read:admin:queue', kind: 'read:admin:queue',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
name: {
type: 'string',
optional: false, nullable: false,
enum: QUEUE_TYPES,
},
counts: {
type: 'object',
optional: false, nullable: false,
additionalProperties: {
type: 'number',
},
},
isPaused: {
type: 'boolean',
optional: false, nullable: false,
},
metrics: {
type: 'object',
optional: false, nullable: false,
properties: {
completed: {
optional: false, nullable: false,
ref: 'QueueMetrics',
},
failed: {
optional: false, nullable: false,
ref: 'QueueMetrics',
},
},
},
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = { export const meta = {
@ -14,6 +13,11 @@ export const meta = {
requireCredential: true, requireCredential: true,
requireModerator: true, requireModerator: true,
kind: 'read:admin:queue', kind: 'read:admin:queue',
res: {
optional: false, nullable: false,
ref: 'QueueJob',
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -28,7 +32,6 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
private moderationLogService: ModerationLogService,
private queueService: QueueService, private queueService: QueueService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {

View File

@ -21,15 +21,19 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts">
export type TlEvent<E = any> = {
id: string;
timestamp: number;
data: E;
};
</script>
<script lang="ts" setup generic="T extends unknown">
import { computed } from 'vue'; import { computed } from 'vue';
const props = defineProps<{ const props = defineProps<{
events: { events: TlEvent<T>[];
id: string;
timestamp: number;
data: any;
}[];
}>(); }>();
const events = computed(() => { const events = computed(() => {
@ -44,12 +48,12 @@ function getDateText(dateInstance: Date) {
return `${year.toString()}/${month.toString()}/${date.toString()} ${hour.toString().padStart(2, '0')}:00:00`; return `${year.toString()}/${month.toString()}/${date.toString()} ${hour.toString().padStart(2, '0')}:00:00`;
} }
const items = computed<({ type TlItem<T> = ({
id: string; id: string;
type: 'event'; type: 'event';
timestamp: number; timestamp: number;
delta: number; delta: number
data: any; data: T;
} | { } | {
id: string; id: string;
type: 'date'; type: 'date';
@ -57,8 +61,10 @@ const items = computed<({
prevText: string; prevText: string;
next: Date | null; next: Date | null;
nextText: string; nextText: string;
})[]>(() => { });
const results = [];
const items = computed<TlItem<T>[]>(() => {
const results: TlItem<T>[] = [];
for (let i = 0; i < events.value.length; i++) { for (let i = 0; i < events.value.length; i++) {
const item = events.value[i]; const item = events.value[i];
@ -97,19 +103,12 @@ const items = computed<({
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.root {
}
.items { .items {
display: grid; display: grid;
grid-template-columns: max-content 18px 1fr; grid-template-columns: max-content 18px 1fr;
gap: 0 8px; gap: 0 8px;
} }
.item {
}
.center { .center {
position: relative; position: relative;
@ -140,6 +139,7 @@ const items = computed<({
height: 100%; height: 100%;
background: color-mix(in srgb, var(--MI_THEME-accent), var(--MI_THEME-bg) 75%); background: color-mix(in srgb, var(--MI_THEME-accent), var(--MI_THEME-bg) 75%);
} }
.centerPoint { .centerPoint {
position: absolute; position: absolute;
top: 0; top: 0;

View File

@ -48,6 +48,8 @@ watch(() => props.dataSet, () => {
}); });
onMounted(() => { onMounted(() => {
if (chartEl.value == null) return;
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
chartInstance = new Chart(chartEl.value, { chartInstance = new Chart(chartEl.value, {

View File

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<template #suffix> <template #suffix>
<MkTime :time="job.finishedOn ?? job.processedOn ?? job.timestamp" mode="relative"/> <MkTime :time="job.finishedOn ?? job.processedOn ?? job.timestamp" mode="relative"/>
<span v-if="job.progress != null && job.progress > 0" style="margin-left: 1em;">{{ Math.floor(job.progress * 100) }}%</span> <span v-if="job.progress != null && typeof job.progress === 'number' && job.progress > 0" style="margin-left: 1em;">{{ Math.floor(job.progress * 100) }}%</span>
<span v-if="job.opts.attempts != null && job.opts.attempts > 0 && job.attempts > 1" style="margin-left: 1em; color: var(--MI_THEME-warn); font-variant-numeric: diagonal-fractions;">{{ job.attempts }}/{{ job.opts.attempts }}</span> <span v-if="job.opts.attempts != null && job.opts.attempts > 0 && job.attempts > 1" style="margin-left: 1em; color: var(--MI_THEME-warn); font-variant-numeric: diagonal-fractions;">{{ job.attempts }}/{{ job.opts.attempts }}</span>
<span v-if="job.isFailed && job.finishedOn != null" style="margin-left: 1em; color: var(--MI_THEME-error)"><i class="ti ti-circle-x"></i></span> <span v-if="job.isFailed && job.finishedOn != null" style="margin-left: 1em; color: var(--MI_THEME-error)"><i class="ti ti-circle-x"></i></span>
<span v-else-if="job.isFailed" style="margin-left: 1em; color: var(--MI_THEME-warn)"><i class="ti ti-alert-triangle"></i></span> <span v-else-if="job.isFailed" style="margin-left: 1em; color: var(--MI_THEME-warn)"><i class="ti ti-alert-triangle"></i></span>
@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton rounded @click="copyRaw()"><i class="ti ti-copy"></i> Copy raw</MkButton> <MkButton rounded @click="copyRaw()"><i class="ti ti-copy"></i> Copy raw</MkButton>
<MkButton rounded @click="refresh()"><i class="ti ti-reload"></i> Refresh view</MkButton> <MkButton rounded @click="refresh()"><i class="ti ti-reload"></i> Refresh view</MkButton>
<MkButton rounded @click="promoteJob()"><i class="ti ti-player-track-next"></i> Promote</MkButton> <MkButton rounded @click="promoteJob()"><i class="ti ti-player-track-next"></i> Promote</MkButton>
<MkButton rounded @click="moveJob"><i class="ti ti-arrow-right"></i> Move to</MkButton> <!-- <MkButton rounded @click="moveJob"><i class="ti ti-arrow-right"></i> Move to</MkButton> -->
<MkButton danger rounded style="margin-left: auto;" @click="removeJob()"><i class="ti ti-trash"></i> Remove</MkButton> <MkButton danger rounded style="margin-left: auto;" @click="removeJob()"><i class="ti ti-trash"></i> Remove</MkButton>
</div> </div>
</template> </template>
@ -96,7 +96,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #key>Attempts</template> <template #key>Attempts</template>
<template #value>{{ job.attempts }} of {{ job.opts.attempts }}</template> <template #value>{{ job.attempts }} of {{ job.opts.attempts }}</template>
</MkKeyValue> </MkKeyValue>
<MkKeyValue v-if="job.progress != null && job.progress > 0"> <MkKeyValue v-if="job.progress != null && typeof job.progress === 'number' && job.progress > 0">
<template #key>Progress</template> <template #key>Progress</template>
<template #value>{{ Math.floor(job.progress * 100) }}%</template> <template #value>{{ Math.floor(job.progress * 100) }}%</template>
</MkKeyValue> </MkKeyValue>
@ -150,7 +150,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton><i class="ti ti-device-floppy"></i> Update</MkButton> <MkButton><i class="ti ti-device-floppy"></i> Update</MkButton>
</div> </div>
<div v-else-if="tab === 'result'"> <div v-else-if="tab === 'result'">
<MkCode :code="job.returnValue"/> <MkCode :code="String(job.returnValue)"/>
</div> </div>
<div v-else-if="tab === 'error'" class="_gaps_s"> <div v-else-if="tab === 'error'" class="_gaps_s">
<MkCode v-for="log in job.stacktrace" :code="log" lang="stacktrace"/> <MkCode v-for="log in job.stacktrace" :code="log" lang="stacktrace"/>
@ -159,22 +159,20 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, watch } from 'vue'; import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import JSON5 from 'json5'; import JSON5 from 'json5';
import type { Ref } from 'vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import MkTabs from '@/components/MkTabs.vue'; import MkTabs from '@/components/MkTabs.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkCode from '@/components/MkCode.vue'; import MkCode from '@/components/MkCode.vue';
import MkKeyValue from '@/components/MkKeyValue.vue'; import MkKeyValue from '@/components/MkKeyValue.vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkTl from '@/components/MkTl.vue'; import MkTl from '@/components/MkTl.vue';
import kmg from '@/filters/kmg.js';
import bytes from '@/filters/bytes.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import type { TlEvent } from '@/components/MkTl.vue';
function msSMH(v: number | null) { function msSMH(v: number | null) {
if (v == null) return 'N/A'; if (v == null) return 'N/A';
@ -189,25 +187,34 @@ function msSMH(v: number | null) {
} }
const props = defineProps<{ const props = defineProps<{
job: any; job: Misskey.entities.QueueJob;
queueType: string; queueType: typeof Misskey.queueTypes[number];
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'needRefresh'): void, (ev: 'needRefresh'): void;
}>(); }>();
const tab = ref('info'); const tab = ref('info');
const editData = ref(JSON5.stringify(props.job.data, null, '\t')); const editData = ref(JSON5.stringify(props.job.data, null, '\t'));
const canEdit = true; const canEdit = true;
type TlType = TlEvent<{
type: 'created' | 'processed' | 'finished';
} | {
type: 'attempt';
attempt: number;
}>;
const timeline = computed(() => { const timeline = computed(() => {
const events = [{ const events: TlType[] = [{
id: 'created', id: 'created',
timestamp: props.job.timestamp, timestamp: props.job.timestamp,
data: { data: {
type: 'created', type: 'created',
}, },
}]; }];
if (props.job.attempts > 1) { if (props.job.attempts > 1) {
for (let i = 1; i < props.job.attempts; i++) { for (let i = 1; i < props.job.attempts; i++) {
events.push({ events.push({
@ -261,9 +268,10 @@ async function removeJob() {
os.apiWithDialog('admin/queue/remove-job', { queue: props.queueType, jobId: props.job.id }); os.apiWithDialog('admin/queue/remove-job', { queue: props.queueType, jobId: props.job.id });
} }
function moveJob() { // TODO
// TODO // function moveJob() {
} //
// }
function refresh() { function refresh() {
emit('needRefresh'); emit('needRefresh');

View File

@ -37,9 +37,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #footer> <template #footer>
<div class="_buttons"> <div class="_buttons">
<MkButton rounded @click="promoteAllJobs"><i class="ti ti-player-track-next"></i> Promote all jobs</MkButton> <MkButton rounded @click="promoteAllJobs"><i class="ti ti-player-track-next"></i> Promote all jobs</MkButton>
<MkButton rounded @click="createJob"><i class="ti ti-plus"></i> Add job</MkButton> <!-- <MkButton rounded @click="createJob"><i class="ti ti-plus"></i> Add job</MkButton> -->
<MkButton v-if="queueInfo.isPaused" rounded @click="resumeQueue"><i class="ti ti-player-play"></i> Resume queue</MkButton> <!-- <MkButton v-if="queueInfo.isPaused" rounded @click="resumeQueue"><i class="ti ti-player-play"></i> Resume queue</MkButton> -->
<MkButton v-else rounded danger @click="pauseQueue"><i class="ti ti-player-pause"></i> Pause queue</MkButton> <!-- <MkButton v-else rounded danger @click="pauseQueue"><i class="ti ti-player-pause"></i> Pause queue</MkButton> -->
<MkButton rounded danger @click="clearQueue"><i class="ti ti-trash"></i> Empty queue</MkButton> <MkButton rounded danger @click="clearQueue"><i class="ti ti-trash"></i> Empty queue</MkButton>
</div> </div>
</template> </template>
@ -172,12 +172,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, watch } from 'vue'; import { ref, computed, watch } from 'vue';
import JSON5 from 'json5'; import * as Misskey from 'misskey-js';
import { debounce } from 'throttle-debounce'; import { debounce } from 'throttle-debounce';
import { useInterval } from '@@/js/use-interval.js'; import { useInterval } from '@@/js/use-interval.js';
import XChart from './job-queue.chart.vue'; import XChart from './job-queue.chart.vue';
import XJob from './job-queue.job.vue'; import XJob from './job-queue.job.vue';
import type { Ref } from 'vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js'; import { definePage } from '@/page.js';
@ -185,32 +184,18 @@ import MkButton from '@/components/MkButton.vue';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import MkTabs from '@/components/MkTabs.vue'; import MkTabs from '@/components/MkTabs.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkCode from '@/components/MkCode.vue';
import MkKeyValue from '@/components/MkKeyValue.vue'; import MkKeyValue from '@/components/MkKeyValue.vue';
import MkTl from '@/components/MkTl.vue'; import MkTl from '@/components/MkTl.vue';
import kmg from '@/filters/kmg.js'; import kmg from '@/filters/kmg.js';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import bytes from '@/filters/bytes.js'; import bytes from '@/filters/bytes.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
const QUEUE_TYPES = [ const tab = ref<typeof Misskey.queueTypes[number] | '-'>('-');
'system', const jobState = ref<'all' | 'latest' | 'completed' | 'failed' | 'active' | 'delayed' | 'wait' | 'paused'>('all');
'endedPollNotification', const jobs = ref<Misskey.entities.QueueJob[]>([]);
'deliver',
'inbox',
'db',
'relationship',
'objectStorage',
'userWebhookDeliver',
'systemWebhookDeliver',
] as const;
const tab: Ref<typeof QUEUE_TYPES[number] | '-'> = ref('-');
const jobState = ref('all');
const jobs = ref([]);
const jobsFetching = ref(true); const jobsFetching = ref(true);
const queueInfos = ref([]); const queueInfos = ref<Misskey.entities.AdminQueueQueuesResponse>([]);
const queueInfo = ref(); const queueInfo = ref<Misskey.entities.AdminQueueQueueStatsResponse | null>(null);
const searchQuery = ref(''); const searchQuery = ref('');
async function fetchQueues() { async function fetchQueues() {
@ -230,11 +215,11 @@ async function fetchJobs() {
queue: tab.value, queue: tab.value,
state: state === 'all' ? ['completed', 'failed', 'active', 'delayed', 'wait'] : state === 'latest' ? ['completed', 'failed'] : [state], state: state === 'all' ? ['completed', 'failed', 'active', 'delayed', 'wait'] : state === 'latest' ? ['completed', 'failed'] : [state],
search: searchQuery.value.trim() === '' ? undefined : searchQuery.value, search: searchQuery.value.trim() === '' ? undefined : searchQuery.value,
}).then(res => { }).then((res: Misskey.entities.AdminQueueJobsResponse) => {
if (state === 'all') { if (state === 'all') {
res.sort((a, b) => (a.processedOn ?? a.timestamp) > (b.processedOn ?? b.timestamp) ? -1 : 1); res.sort((a, b) => (a.processedOn ?? a.timestamp) > (b.processedOn ?? b.timestamp) ? -1 : 1);
} else if (state === 'latest') { } else if (state === 'latest') {
res.sort((a, b) => a.processedOn > b.processedOn ? -1 : 1); res.sort((a, b) => a.processedOn! > b.processedOn! ? -1 : 1);
} else if (state === 'delayed') { } else if (state === 'delayed') {
res.sort((a, b) => (a.processedOn ?? a.timestamp) > (b.processedOn ?? b.timestamp) ? -1 : 1); res.sort((a, b) => (a.processedOn ?? a.timestamp) > (b.processedOn ?? b.timestamp) ? -1 : 1);
} }
@ -276,6 +261,8 @@ useInterval(() => {
}); });
async function clearQueue() { async function clearQueue() {
if (tab.value === '-') return;
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
title: i18n.ts.areYouSure, title: i18n.ts.areYouSure,
@ -289,6 +276,8 @@ async function clearQueue() {
} }
async function promoteAllJobs() { async function promoteAllJobs() {
if (tab.value === '-') return;
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
title: i18n.ts.areYouSure, title: i18n.ts.areYouSure,
@ -302,13 +291,15 @@ async function promoteAllJobs() {
} }
async function removeJobs() { async function removeJobs() {
if (tab.value === '-' || jobState.value === 'latest') return;
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
title: i18n.ts.areYouSure, title: i18n.ts.areYouSure,
}); });
if (canceled) return; if (canceled) return;
os.apiWithDialog('admin/queue/clear', { queue: tab.value, state: jobState.value }); os.apiWithDialog('admin/queue/clear', { queue: tab.value, state: jobState.value === 'all' ? '*' : jobState.value });
fetchCurrentQueue(); fetchCurrentQueue();
fetchJobs(); fetchJobs();
@ -324,16 +315,18 @@ async function refreshJob(jobId: string) {
const headerActions = computed(() => []); const headerActions = computed(() => []);
const headerTabs = computed(() => const headerTabs = computed<{
[{ key: string;
key: '-', title: string;
title: i18n.ts.overview, icon?: string;
icon: 'ti ti-dashboard', }[]>(() => [{
}].concat(QUEUE_TYPES.map((t) => ({ key: '-',
key: t, title: i18n.ts.jobQueue,
title: t, icon: 'ti ti-list-check',
}))), }, ...Misskey.queueTypes.map((q) => ({
); key: q,
title: q,
}))]);
definePage(() => ({ definePage(() => ({
title: i18n.ts.jobQueue, title: i18n.ts.jobQueue,

View File

@ -275,12 +275,21 @@ type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed'
// @public (undocumented) // @public (undocumented)
type AdminQueueJobsRequest = operations['admin___queue___jobs']['requestBody']['content']['application/json']; type AdminQueueJobsRequest = operations['admin___queue___jobs']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminQueueJobsResponse = operations['admin___queue___jobs']['responses']['200']['content']['application/json'];
// @public (undocumented) // @public (undocumented)
type AdminQueuePromoteJobsRequest = operations['admin___queue___promote-jobs']['requestBody']['content']['application/json']; type AdminQueuePromoteJobsRequest = operations['admin___queue___promote-jobs']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminQueueQueuesResponse = operations['admin___queue___queues']['responses']['200']['content']['application/json'];
// @public (undocumented) // @public (undocumented)
type AdminQueueQueueStatsRequest = operations['admin___queue___queue-stats']['requestBody']['content']['application/json']; type AdminQueueQueueStatsRequest = operations['admin___queue___queue-stats']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminQueueQueueStatsResponse = operations['admin___queue___queue-stats']['responses']['200']['content']['application/json'];
// @public (undocumented) // @public (undocumented)
type AdminQueueRemoveJobRequest = operations['admin___queue___remove-job']['requestBody']['content']['application/json']; type AdminQueueRemoveJobRequest = operations['admin___queue___remove-job']['requestBody']['content']['application/json'];
@ -290,6 +299,9 @@ type AdminQueueRetryJobRequest = operations['admin___queue___retry-job']['reques
// @public (undocumented) // @public (undocumented)
type AdminQueueShowJobRequest = operations['admin___queue___show-job']['requestBody']['content']['application/json']; type AdminQueueShowJobRequest = operations['admin___queue___show-job']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminQueueShowJobResponse = operations['admin___queue___show-job']['responses']['200']['content']['application/json'];
// @public (undocumented) // @public (undocumented)
type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json']; type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json'];
@ -1532,11 +1544,15 @@ declare namespace entities {
AdminQueueDeliverDelayedResponse, AdminQueueDeliverDelayedResponse,
AdminQueueInboxDelayedResponse, AdminQueueInboxDelayedResponse,
AdminQueueJobsRequest, AdminQueueJobsRequest,
AdminQueueJobsResponse,
AdminQueuePromoteJobsRequest, AdminQueuePromoteJobsRequest,
AdminQueueQueueStatsRequest, AdminQueueQueueStatsRequest,
AdminQueueQueueStatsResponse,
AdminQueueQueuesResponse,
AdminQueueRemoveJobRequest, AdminQueueRemoveJobRequest,
AdminQueueRetryJobRequest, AdminQueueRetryJobRequest,
AdminQueueShowJobRequest, AdminQueueShowJobRequest,
AdminQueueShowJobResponse,
AdminQueueStatsResponse, AdminQueueStatsResponse,
AdminRelaysAddRequest, AdminRelaysAddRequest,
AdminRelaysAddResponse, AdminRelaysAddResponse,
@ -2117,6 +2133,8 @@ declare namespace entities {
PageBlock, PageBlock,
Channel, Channel,
QueueCount, QueueCount,
QueueMetrics,
QueueJob,
Antenna, Antenna,
Clip, Clip,
FederationInstance, FederationInstance,
@ -3205,6 +3223,12 @@ type PureRenote = Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text
// @public (undocumented) // @public (undocumented)
type QueueCount = components['schemas']['QueueCount']; type QueueCount = components['schemas']['QueueCount'];
// @public (undocumented)
type QueueJob = components['schemas']['QueueJob'];
// @public (undocumented)
type QueueMetrics = components['schemas']['QueueMetrics'];
// @public (undocumented) // @public (undocumented)
type QueueStats = { type QueueStats = {
deliver: { deliver: {
@ -3224,6 +3248,9 @@ type QueueStats = {
// @public (undocumented) // @public (undocumented)
type QueueStatsLog = QueueStats[]; type QueueStatsLog = QueueStats[];
// @public (undocumented)
export const queueTypes: readonly ["system", "endedPollNotification", "deliver", "inbox", "db", "relationship", "objectStorage", "userWebhookDeliver", "systemWebhookDeliver"];
// @public (undocumented) // @public (undocumented)
type RenoteMuteCreateRequest = operations['renote-mute___create']['requestBody']['content']['application/json']; type RenoteMuteCreateRequest = operations['renote-mute___create']['requestBody']['content']['application/json'];

View File

@ -79,11 +79,15 @@ import type {
AdminQueueDeliverDelayedResponse, AdminQueueDeliverDelayedResponse,
AdminQueueInboxDelayedResponse, AdminQueueInboxDelayedResponse,
AdminQueueJobsRequest, AdminQueueJobsRequest,
AdminQueueJobsResponse,
AdminQueuePromoteJobsRequest, AdminQueuePromoteJobsRequest,
AdminQueueQueueStatsRequest, AdminQueueQueueStatsRequest,
AdminQueueQueueStatsResponse,
AdminQueueQueuesResponse,
AdminQueueRemoveJobRequest, AdminQueueRemoveJobRequest,
AdminQueueRetryJobRequest, AdminQueueRetryJobRequest,
AdminQueueShowJobRequest, AdminQueueShowJobRequest,
AdminQueueShowJobResponse,
AdminQueueStatsResponse, AdminQueueStatsResponse,
AdminRelaysAddRequest, AdminRelaysAddRequest,
AdminRelaysAddResponse, AdminRelaysAddResponse,
@ -694,13 +698,13 @@ export type Endpoints = {
'admin/queue/clear': { req: AdminQueueClearRequest; res: EmptyResponse }; 'admin/queue/clear': { req: AdminQueueClearRequest; res: EmptyResponse };
'admin/queue/deliver-delayed': { req: EmptyRequest; res: AdminQueueDeliverDelayedResponse }; 'admin/queue/deliver-delayed': { req: EmptyRequest; res: AdminQueueDeliverDelayedResponse };
'admin/queue/inbox-delayed': { req: EmptyRequest; res: AdminQueueInboxDelayedResponse }; 'admin/queue/inbox-delayed': { req: EmptyRequest; res: AdminQueueInboxDelayedResponse };
'admin/queue/jobs': { req: AdminQueueJobsRequest; res: EmptyResponse }; 'admin/queue/jobs': { req: AdminQueueJobsRequest; res: AdminQueueJobsResponse };
'admin/queue/promote-jobs': { req: AdminQueuePromoteJobsRequest; res: EmptyResponse }; 'admin/queue/promote-jobs': { req: AdminQueuePromoteJobsRequest; res: EmptyResponse };
'admin/queue/queue-stats': { req: AdminQueueQueueStatsRequest; res: EmptyResponse }; 'admin/queue/queue-stats': { req: AdminQueueQueueStatsRequest; res: AdminQueueQueueStatsResponse };
'admin/queue/queues': { req: EmptyRequest; res: EmptyResponse }; 'admin/queue/queues': { req: EmptyRequest; res: AdminQueueQueuesResponse };
'admin/queue/remove-job': { req: AdminQueueRemoveJobRequest; res: EmptyResponse }; 'admin/queue/remove-job': { req: AdminQueueRemoveJobRequest; res: EmptyResponse };
'admin/queue/retry-job': { req: AdminQueueRetryJobRequest; res: EmptyResponse }; 'admin/queue/retry-job': { req: AdminQueueRetryJobRequest; res: EmptyResponse };
'admin/queue/show-job': { req: AdminQueueShowJobRequest; res: EmptyResponse }; 'admin/queue/show-job': { req: AdminQueueShowJobRequest; res: AdminQueueShowJobResponse };
'admin/queue/stats': { req: EmptyRequest; res: AdminQueueStatsResponse }; 'admin/queue/stats': { req: EmptyRequest; res: AdminQueueStatsResponse };
'admin/relays/add': { req: AdminRelaysAddRequest; res: AdminRelaysAddResponse }; 'admin/relays/add': { req: AdminRelaysAddRequest; res: AdminRelaysAddResponse };
'admin/relays/list': { req: EmptyRequest; res: AdminRelaysListResponse }; 'admin/relays/list': { req: EmptyRequest; res: AdminRelaysListResponse };

View File

@ -82,11 +82,15 @@ export type AdminQueueClearRequest = operations['admin___queue___clear']['reques
export type AdminQueueDeliverDelayedResponse = operations['admin___queue___deliver-delayed']['responses']['200']['content']['application/json']; export type AdminQueueDeliverDelayedResponse = operations['admin___queue___deliver-delayed']['responses']['200']['content']['application/json'];
export type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed']['responses']['200']['content']['application/json']; export type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed']['responses']['200']['content']['application/json'];
export type AdminQueueJobsRequest = operations['admin___queue___jobs']['requestBody']['content']['application/json']; export type AdminQueueJobsRequest = operations['admin___queue___jobs']['requestBody']['content']['application/json'];
export type AdminQueueJobsResponse = operations['admin___queue___jobs']['responses']['200']['content']['application/json'];
export type AdminQueuePromoteJobsRequest = operations['admin___queue___promote-jobs']['requestBody']['content']['application/json']; export type AdminQueuePromoteJobsRequest = operations['admin___queue___promote-jobs']['requestBody']['content']['application/json'];
export type AdminQueueQueueStatsRequest = operations['admin___queue___queue-stats']['requestBody']['content']['application/json']; export type AdminQueueQueueStatsRequest = operations['admin___queue___queue-stats']['requestBody']['content']['application/json'];
export type AdminQueueQueueStatsResponse = operations['admin___queue___queue-stats']['responses']['200']['content']['application/json'];
export type AdminQueueQueuesResponse = operations['admin___queue___queues']['responses']['200']['content']['application/json'];
export type AdminQueueRemoveJobRequest = operations['admin___queue___remove-job']['requestBody']['content']['application/json']; export type AdminQueueRemoveJobRequest = operations['admin___queue___remove-job']['requestBody']['content']['application/json'];
export type AdminQueueRetryJobRequest = operations['admin___queue___retry-job']['requestBody']['content']['application/json']; export type AdminQueueRetryJobRequest = operations['admin___queue___retry-job']['requestBody']['content']['application/json'];
export type AdminQueueShowJobRequest = operations['admin___queue___show-job']['requestBody']['content']['application/json']; export type AdminQueueShowJobRequest = operations['admin___queue___show-job']['requestBody']['content']['application/json'];
export type AdminQueueShowJobResponse = operations['admin___queue___show-job']['responses']['200']['content']['application/json'];
export type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json']; export type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json'];
export type AdminRelaysAddRequest = operations['admin___relays___add']['requestBody']['content']['application/json']; export type AdminRelaysAddRequest = operations['admin___relays___add']['requestBody']['content']['application/json'];
export type AdminRelaysAddResponse = operations['admin___relays___add']['responses']['200']['content']['application/json']; export type AdminRelaysAddResponse = operations['admin___relays___add']['responses']['200']['content']['application/json'];

View File

@ -29,6 +29,8 @@ export type Page = components['schemas']['Page'];
export type PageBlock = components['schemas']['PageBlock']; export type PageBlock = components['schemas']['PageBlock'];
export type Channel = components['schemas']['Channel']; export type Channel = components['schemas']['Channel'];
export type QueueCount = components['schemas']['QueueCount']; export type QueueCount = components['schemas']['QueueCount'];
export type QueueMetrics = components['schemas']['QueueMetrics'];
export type QueueJob = components['schemas']['QueueJob'];
export type Antenna = components['schemas']['Antenna']; export type Antenna = components['schemas']['Antenna'];
export type Clip = components['schemas']['Clip']; export type Clip = components['schemas']['Clip'];
export type FederationInstance = components['schemas']['FederationInstance']; export type FederationInstance = components['schemas']['FederationInstance'];

View File

@ -4946,6 +4946,32 @@ export type components = {
failed: number; failed: number;
delayed: number; delayed: number;
}; };
QueueMetrics: {
meta: {
count: number;
prevTS: number;
prevCount: number;
};
data: number[];
count: number;
};
QueueJob: {
id: string;
name: string;
data: Record<string, never>;
opts: Record<string, never>;
timestamp: number;
processedOn?: number;
processedBy?: string;
finishedOn?: number;
progress: Record<string, never>;
attempts: number;
delay: number;
failedReason: string;
stacktrace: string[];
returnValue: Record<string, never>;
isFailed: boolean;
};
Antenna: { Antenna: {
/** Format: id */ /** Format: id */
id: string; id: string;
@ -9049,9 +9075,11 @@ export type operations = {
}; };
}; };
responses: { responses: {
/** @description OK (without any results) */ /** @description OK (with results) */
204: { 200: {
content: never; content: {
'application/json': components['schemas']['QueueJob'][];
};
}; };
/** @description Client error */ /** @description Client error */
400: { 400: {
@ -9153,9 +9181,43 @@ export type operations = {
}; };
}; };
responses: { responses: {
/** @description OK (without any results) */ /** @description OK (with results) */
204: { 200: {
content: never; content: {
'application/json': {
/** @enum {string} */
name: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver';
qualifiedName: string;
counts: {
[key: string]: number;
};
isPaused: boolean;
metrics: {
completed: components['schemas']['QueueMetrics'];
failed: components['schemas']['QueueMetrics'];
};
db: {
version: string;
/** @enum {string} */
mode: 'cluster' | 'standalone' | 'sentinel';
runId: string;
processId: string;
port: number;
os: string;
uptime: number;
memory: {
total: number;
used: number;
fragmentationRatio: number;
peak: number;
};
clients: {
blocked: number;
connected: number;
};
};
};
};
}; };
/** @description Client error */ /** @description Client error */
400: { 400: {
@ -9197,9 +9259,22 @@ export type operations = {
*/ */
admin___queue___queues: { admin___queue___queues: {
responses: { responses: {
/** @description OK (without any results) */ /** @description OK (with results) */
204: { 200: {
content: never; content: {
'application/json': ({
/** @enum {string} */
name: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver';
counts: {
[key: string]: number;
};
isPaused: boolean;
metrics: {
completed: components['schemas']['QueueMetrics'];
failed: components['schemas']['QueueMetrics'];
};
})[];
};
}; };
/** @description Client error */ /** @description Client error */
400: { 400: {
@ -9356,9 +9431,11 @@ export type operations = {
}; };
}; };
responses: { responses: {
/** @description OK (without any results) */ /** @description OK (with results) */
204: { 200: {
content: never; content: {
'application/json': components['schemas']['QueueJob'];
};
}; };
/** @description Client error */ /** @description Client error */
400: { 400: {

View File

@ -169,6 +169,18 @@ export const moderationLogTypes = [
'deleteChatRoom', 'deleteChatRoom',
] as const; ] as const;
export const queueTypes = [
'system',
'endedPollNotification',
'deliver',
'inbox',
'db',
'relationship',
'objectStorage',
'userWebhookDeliver',
'systemWebhookDeliver',
] as const;
// See: packages/backend/src/core/ReversiService.ts@L410 // See: packages/backend/src/core/ReversiService.ts@L410
export const reversiUpdateKeys = [ export const reversiUpdateKeys = [
'map', 'map',

View File

@ -13,6 +13,7 @@ export const mutedNoteReasons = consts.mutedNoteReasons;
export const followingVisibilities = consts.followingVisibilities; export const followingVisibilities = consts.followingVisibilities;
export const followersVisibilities = consts.followersVisibilities; export const followersVisibilities = consts.followersVisibilities;
export const moderationLogTypes = consts.moderationLogTypes; export const moderationLogTypes = consts.moderationLogTypes;
export const queueTypes = consts.queueTypes;
export const reversiUpdateKeys = consts.reversiUpdateKeys; export const reversiUpdateKeys = consts.reversiUpdateKeys;
// api extractor not supported yet // api extractor not supported yet