feat: Job queue inspector (#15856)
* wip
* wip
* Update job-queue.vue
* wip
* wip
* Update job-queue.vue
* wip
* Update job-queue.vue
* wip
* Update QueueService.ts
* Update QueueService.ts
* Update QueueService.ts
* Update job-queue.vue
* wip
* wip
* wip
* Update job-queue.vue
* wip
* Update MkTl.vue
* wip
* Update index.vue
* wip
* wip
* Update MkTl.vue
* 🎨
* jobs search
* wip
* Update job-queue.vue
* wip
* wip
* Update job-queue.vue
* Update job-queue.vue
* Update job-queue.vue
* Update job-queue.vue
* wip
* Update job-queue.job.vue
* wip
* wip
* wip
* Update MkCode.vue
* wip
* Update job-queue.job.vue
* wip
* Update job-queue.job.vue
* Update misskey-js.api.md
* Update CHANGELOG.md
* Update job-queue.job.vue
			
			
This commit is contained in:
		
							parent
							
								
									978ab2f848
								
							
						
					
					
						commit
						7b38806413
					
				CHANGELOG.mdpnpm-lock.yaml
locales
packages
backend
package.json
src
frontend/src
components
pages/admin
federation-job-queue.chart.chart.vuefederation-job-queue.chart.vuefederation-job-queue.vueindex.vuejob-queue.chart.vuejob-queue.job.vuejob-queue.vue
router.definition.tsmisskey-js
|  | @ -1,6 +1,7 @@ | ||||||
| ## 2025.4.1 | ## 2025.4.1 | ||||||
| 
 | 
 | ||||||
| ### General | ### General | ||||||
|  | - Feat: bull-boardに代わるジョブキューの管理ツールが実装されました | ||||||
| - Enhance: チャットの新規メッセージをプッシュ通知するように | - Enhance: チャットの新規メッセージをプッシュ通知するように | ||||||
| 
 | 
 | ||||||
| ### Client | ### Client | ||||||
|  | @ -18,6 +19,7 @@ | ||||||
| - Fix: アカウントの移行時にアンテナのフィルターのユーザが更新されない問題を修正 #15843 | - Fix: アカウントの移行時にアンテナのフィルターのユーザが更新されない問題を修正 #15843 | ||||||
| 
 | 
 | ||||||
| ### Server | ### Server | ||||||
|  | - Enhance: ジョブキューの成功/失敗したジョブも一定数・一定期間保存するようにし、後から問題を調査することを容易に | ||||||
| - Enhance: フォローしているユーザーならフォロワー限定投稿のノートでもアンテナで検知できるように   | - Enhance: フォローしているユーザーならフォロワー限定投稿のノートでもアンテナで検知できるように   | ||||||
| 	(Cherry-picked from https://github.com/yojo-art/cherrypick/pull/568 and https://github.com/team-shahu/misskey/pull/38) | 	(Cherry-picked from https://github.com/yojo-art/cherrypick/pull/568 and https://github.com/team-shahu/misskey/pull/38) | ||||||
| - Fix: システムアカウントの名前がサーバー名と同期されない問題を修正 | - Fix: システムアカウントの名前がサーバー名と同期されない問題を修正 | ||||||
|  |  | ||||||
|  | @ -5398,6 +5398,10 @@ export interface Locale extends ILocale { | ||||||
|      * デッキへ戻る |      * デッキへ戻る | ||||||
|      */ |      */ | ||||||
|     "goToDeck": string; |     "goToDeck": string; | ||||||
|  |     /** | ||||||
|  |      * 連合ジョブ | ||||||
|  |      */ | ||||||
|  |     "federationJobs": string; | ||||||
|     "_chat": { |     "_chat": { | ||||||
|         /** |         /** | ||||||
|          * まだメッセージはありません |          * まだメッセージはありません | ||||||
|  |  | ||||||
|  | @ -1345,6 +1345,7 @@ embed: "埋め込み" | ||||||
| settingsMigrating: "設定を移行しています。しばらくお待ちください... (後ほど、設定→その他→旧設定情報を移行 で手動で移行することもできます)" | settingsMigrating: "設定を移行しています。しばらくお待ちください... (後ほど、設定→その他→旧設定情報を移行 で手動で移行することもできます)" | ||||||
| readonly: "読み取り専用" | readonly: "読み取り専用" | ||||||
| goToDeck: "デッキへ戻る" | goToDeck: "デッキへ戻る" | ||||||
|  | federationJobs: "連合ジョブ" | ||||||
| 
 | 
 | ||||||
| _chat: | _chat: | ||||||
|   noMessagesYet: "まだメッセージはありません" |   noMessagesYet: "まだメッセージはありません" | ||||||
|  |  | ||||||
|  | @ -93,6 +93,7 @@ | ||||||
| 		"@swc/cli": "0.6.0", | 		"@swc/cli": "0.6.0", | ||||||
| 		"@swc/core": "1.11.18", | 		"@swc/core": "1.11.18", | ||||||
| 		"@twemoji/parser": "15.1.1", | 		"@twemoji/parser": "15.1.1", | ||||||
|  | 		"@types/redis-info": "3.0.3", | ||||||
| 		"accepts": "1.3.8", | 		"accepts": "1.3.8", | ||||||
| 		"ajv": "8.17.1", | 		"ajv": "8.17.1", | ||||||
| 		"archiver": "7.0.1", | 		"archiver": "7.0.1", | ||||||
|  | @ -159,6 +160,7 @@ | ||||||
| 		"random-seed": "0.3.0", | 		"random-seed": "0.3.0", | ||||||
| 		"ratelimiter": "3.4.1", | 		"ratelimiter": "3.4.1", | ||||||
| 		"re2": "1.21.4", | 		"re2": "1.21.4", | ||||||
|  | 		"redis-info": "3.1.0", | ||||||
| 		"redis-lock": "0.1.4", | 		"redis-lock": "0.1.4", | ||||||
| 		"reflect-metadata": "0.2.2", | 		"reflect-metadata": "0.2.2", | ||||||
| 		"rename": "1.0.4", | 		"rename": "1.0.4", | ||||||
|  |  | ||||||
|  | @ -576,7 +576,14 @@ export class NoteCreateService implements OnApplicationShutdown { | ||||||
| 				noteId: note.id, | 				noteId: note.id, | ||||||
| 			}, { | 			}, { | ||||||
| 				delay, | 				delay, | ||||||
| 				removeOnComplete: true, | 				removeOnComplete: { | ||||||
|  | 					age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 					count: 30, | ||||||
|  | 				}, | ||||||
|  | 				removeOnFail: { | ||||||
|  | 					age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 					count: 100, | ||||||
|  | 				}, | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,6 +5,8 @@ | ||||||
| 
 | 
 | ||||||
| import { randomUUID } from 'node:crypto'; | import { randomUUID } from 'node:crypto'; | ||||||
| import { Inject, Injectable } from '@nestjs/common'; | import { Inject, Injectable } from '@nestjs/common'; | ||||||
|  | import { MetricsTime, type JobType } from 'bullmq'; | ||||||
|  | import { parse as parseRedisInfo } from 'redis-info'; | ||||||
| import type { IActivity } from '@/core/activitypub/type.js'; | import type { IActivity } from '@/core/activitypub/type.js'; | ||||||
| import type { MiDriveFile } from '@/models/DriveFile.js'; | import type { MiDriveFile } from '@/models/DriveFile.js'; | ||||||
| import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js'; | import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js'; | ||||||
|  | @ -69,50 +71,58 @@ export class QueueService { | ||||||
| 		this.systemQueue.add('tickCharts', { | 		this.systemQueue.add('tickCharts', { | ||||||
| 		}, { | 		}, { | ||||||
| 			repeat: { pattern: '55 * * * *' }, | 			repeat: { pattern: '55 * * * *' }, | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: 10, | ||||||
|  | 			removeOnFail: 30, | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
| 		this.systemQueue.add('resyncCharts', { | 		this.systemQueue.add('resyncCharts', { | ||||||
| 		}, { | 		}, { | ||||||
| 			repeat: { pattern: '0 0 * * *' }, | 			repeat: { pattern: '0 0 * * *' }, | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: 10, | ||||||
|  | 			removeOnFail: 30, | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
| 		this.systemQueue.add('cleanCharts', { | 		this.systemQueue.add('cleanCharts', { | ||||||
| 		}, { | 		}, { | ||||||
| 			repeat: { pattern: '0 0 * * *' }, | 			repeat: { pattern: '0 0 * * *' }, | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: 10, | ||||||
|  | 			removeOnFail: 30, | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
| 		this.systemQueue.add('aggregateRetention', { | 		this.systemQueue.add('aggregateRetention', { | ||||||
| 		}, { | 		}, { | ||||||
| 			repeat: { pattern: '0 0 * * *' }, | 			repeat: { pattern: '0 0 * * *' }, | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: 10, | ||||||
|  | 			removeOnFail: 30, | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
| 		this.systemQueue.add('clean', { | 		this.systemQueue.add('clean', { | ||||||
| 		}, { | 		}, { | ||||||
| 			repeat: { pattern: '0 0 * * *' }, | 			repeat: { pattern: '0 0 * * *' }, | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: 10, | ||||||
|  | 			removeOnFail: 30, | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
| 		this.systemQueue.add('checkExpiredMutings', { | 		this.systemQueue.add('checkExpiredMutings', { | ||||||
| 		}, { | 		}, { | ||||||
| 			repeat: { pattern: '*/5 * * * *' }, | 			repeat: { pattern: '*/5 * * * *' }, | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: 10, | ||||||
|  | 			removeOnFail: 30, | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
| 		this.systemQueue.add('bakeBufferedReactions', { | 		this.systemQueue.add('bakeBufferedReactions', { | ||||||
| 		}, { | 		}, { | ||||||
| 			repeat: { pattern: '0 0 * * *' }, | 			repeat: { pattern: '0 0 * * *' }, | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: 10, | ||||||
|  | 			removeOnFail: 30, | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
| 		this.systemQueue.add('checkModeratorsActivity', { | 		this.systemQueue.add('checkModeratorsActivity', { | ||||||
| 		}, { | 		}, { | ||||||
| 			// 毎時30分に起動
 | 			// 毎時30分に起動
 | ||||||
| 			repeat: { pattern: '30 * * * *' }, | 			repeat: { pattern: '30 * * * *' }, | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: 10, | ||||||
|  | 			removeOnFail: 30, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -134,13 +144,21 @@ export class QueueService { | ||||||
| 			isSharedInbox, | 			isSharedInbox, | ||||||
| 		}; | 		}; | ||||||
| 
 | 
 | ||||||
| 		return this.deliverQueue.add(to, data, { | 		const label = to.replace('https://', '').replace('/inbox', ''); | ||||||
|  | 
 | ||||||
|  | 		return this.deliverQueue.add(label, data, { | ||||||
| 			attempts: this.config.deliverJobMaxAttempts ?? 12, | 			attempts: this.config.deliverJobMaxAttempts ?? 12, | ||||||
| 			backoff: { | 			backoff: { | ||||||
| 				type: 'custom', | 				type: 'custom', | ||||||
| 			}, | 			}, | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -162,12 +180,18 @@ export class QueueService { | ||||||
| 			backoff: { | 			backoff: { | ||||||
| 				type: 'custom', | 				type: 'custom', | ||||||
| 			}, | 			}, | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}; | 		}; | ||||||
| 
 | 
 | ||||||
| 		await this.deliverQueue.addBulk(Array.from(inboxes.entries(), d => ({ | 		await this.deliverQueue.addBulk(Array.from(inboxes.entries(), d => ({ | ||||||
| 			name: d[0], | 			name: d[0].replace('https://', '').replace('/inbox', ''), | ||||||
| 			data: { | 			data: { | ||||||
| 				user, | 				user, | ||||||
| 				content: contentBody, | 				content: contentBody, | ||||||
|  | @ -188,13 +212,21 @@ export class QueueService { | ||||||
| 			signature, | 			signature, | ||||||
| 		}; | 		}; | ||||||
| 
 | 
 | ||||||
| 		return this.inboxQueue.add('', data, { | 		const label = (activity.id ?? '').replace('https://', '').replace('/activity', ''); | ||||||
|  | 
 | ||||||
|  | 		return this.inboxQueue.add(label, data, { | ||||||
| 			attempts: this.config.inboxJobMaxAttempts ?? 8, | 			attempts: this.config.inboxJobMaxAttempts ?? 8, | ||||||
| 			backoff: { | 			backoff: { | ||||||
| 				type: 'custom', | 				type: 'custom', | ||||||
| 			}, | 			}, | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -203,8 +235,14 @@ export class QueueService { | ||||||
| 		return this.dbQueue.add('deleteDriveFiles', { | 		return this.dbQueue.add('deleteDriveFiles', { | ||||||
| 			user: { id: user.id }, | 			user: { id: user.id }, | ||||||
| 		}, { | 		}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -213,8 +251,14 @@ export class QueueService { | ||||||
| 		return this.dbQueue.add('exportCustomEmojis', { | 		return this.dbQueue.add('exportCustomEmojis', { | ||||||
| 			user: { id: user.id }, | 			user: { id: user.id }, | ||||||
| 		}, { | 		}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -223,8 +267,14 @@ export class QueueService { | ||||||
| 		return this.dbQueue.add('exportNotes', { | 		return this.dbQueue.add('exportNotes', { | ||||||
| 			user: { id: user.id }, | 			user: { id: user.id }, | ||||||
| 		}, { | 		}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -233,8 +283,14 @@ export class QueueService { | ||||||
| 		return this.dbQueue.add('exportClips', { | 		return this.dbQueue.add('exportClips', { | ||||||
| 			user: { id: user.id }, | 			user: { id: user.id }, | ||||||
| 		}, { | 		}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -243,8 +299,14 @@ export class QueueService { | ||||||
| 		return this.dbQueue.add('exportFavorites', { | 		return this.dbQueue.add('exportFavorites', { | ||||||
| 			user: { id: user.id }, | 			user: { id: user.id }, | ||||||
| 		}, { | 		}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -255,8 +317,14 @@ export class QueueService { | ||||||
| 			excludeMuting, | 			excludeMuting, | ||||||
| 			excludeInactive, | 			excludeInactive, | ||||||
| 		}, { | 		}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -265,8 +333,14 @@ export class QueueService { | ||||||
| 		return this.dbQueue.add('exportMuting', { | 		return this.dbQueue.add('exportMuting', { | ||||||
| 			user: { id: user.id }, | 			user: { id: user.id }, | ||||||
| 		}, { | 		}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -275,8 +349,14 @@ export class QueueService { | ||||||
| 		return this.dbQueue.add('exportBlocking', { | 		return this.dbQueue.add('exportBlocking', { | ||||||
| 			user: { id: user.id }, | 			user: { id: user.id }, | ||||||
| 		}, { | 		}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -285,8 +365,14 @@ export class QueueService { | ||||||
| 		return this.dbQueue.add('exportUserLists', { | 		return this.dbQueue.add('exportUserLists', { | ||||||
| 			user: { id: user.id }, | 			user: { id: user.id }, | ||||||
| 		}, { | 		}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -295,8 +381,14 @@ export class QueueService { | ||||||
| 		return this.dbQueue.add('exportAntennas', { | 		return this.dbQueue.add('exportAntennas', { | ||||||
| 			user: { id: user.id }, | 			user: { id: user.id }, | ||||||
| 		}, { | 		}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -307,8 +399,14 @@ export class QueueService { | ||||||
| 			fileId: fileId, | 			fileId: fileId, | ||||||
| 			withReplies, | 			withReplies, | ||||||
| 		}, { | 		}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -324,8 +422,14 @@ export class QueueService { | ||||||
| 			user: { id: user.id }, | 			user: { id: user.id }, | ||||||
| 			fileId: fileId, | 			fileId: fileId, | ||||||
| 		}, { | 		}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -335,8 +439,14 @@ export class QueueService { | ||||||
| 			user: { id: user.id }, | 			user: { id: user.id }, | ||||||
| 			fileId: fileId, | 			fileId: fileId, | ||||||
| 		}, { | 		}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -356,8 +466,14 @@ export class QueueService { | ||||||
| 			name, | 			name, | ||||||
| 			data, | 			data, | ||||||
| 			opts: { | 			opts: { | ||||||
| 				removeOnComplete: true, | 				removeOnComplete: { | ||||||
| 				removeOnFail: true, | 					age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 					count: 30, | ||||||
|  | 				}, | ||||||
|  | 				removeOnFail: { | ||||||
|  | 					age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 					count: 100, | ||||||
|  | 				}, | ||||||
| 			}, | 			}, | ||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
|  | @ -368,8 +484,14 @@ export class QueueService { | ||||||
| 			user: { id: user.id }, | 			user: { id: user.id }, | ||||||
| 			fileId: fileId, | 			fileId: fileId, | ||||||
| 		}, { | 		}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -379,8 +501,14 @@ export class QueueService { | ||||||
| 			user: { id: user.id }, | 			user: { id: user.id }, | ||||||
| 			fileId: fileId, | 			fileId: fileId, | ||||||
| 		}, { | 		}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -390,8 +518,14 @@ export class QueueService { | ||||||
| 			user: { id: user.id }, | 			user: { id: user.id }, | ||||||
| 			antenna, | 			antenna, | ||||||
| 		}, { | 		}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -401,8 +535,14 @@ export class QueueService { | ||||||
| 			user: { id: user.id }, | 			user: { id: user.id }, | ||||||
| 			soft: opts.soft, | 			soft: opts.soft, | ||||||
| 		}, { | 		}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -452,8 +592,14 @@ export class QueueService { | ||||||
| 				withReplies: data.withReplies, | 				withReplies: data.withReplies, | ||||||
| 			}, | 			}, | ||||||
| 			opts: { | 			opts: { | ||||||
| 				removeOnComplete: true, | 				removeOnComplete: { | ||||||
| 				removeOnFail: true, | 					age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 					count: 30, | ||||||
|  | 				}, | ||||||
|  | 				removeOnFail: { | ||||||
|  | 					age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 					count: 100, | ||||||
|  | 				}, | ||||||
| 				...opts, | 				...opts, | ||||||
| 			}, | 			}, | ||||||
| 		}; | 		}; | ||||||
|  | @ -464,16 +610,28 @@ export class QueueService { | ||||||
| 		return this.objectStorageQueue.add('deleteFile', { | 		return this.objectStorageQueue.add('deleteFile', { | ||||||
| 			key: key, | 			key: key, | ||||||
| 		}, { | 		}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@bindThis | 	@bindThis | ||||||
| 	public createCleanRemoteFilesJob() { | 	public createCleanRemoteFilesJob() { | ||||||
| 		return this.objectStorageQueue.add('cleanRemoteFiles', {}, { | 		return this.objectStorageQueue.add('cleanRemoteFiles', {}, { | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -504,8 +662,14 @@ export class QueueService { | ||||||
| 			backoff: { | 			backoff: { | ||||||
| 				type: 'custom', | 				type: 'custom', | ||||||
| 			}, | 			}, | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -535,13 +699,19 @@ export class QueueService { | ||||||
| 			backoff: { | 			backoff: { | ||||||
| 				type: 'custom', | 				type: 'custom', | ||||||
| 			}, | 			}, | ||||||
| 			removeOnComplete: true, | 			removeOnComplete: { | ||||||
| 			removeOnFail: true, | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 30, | ||||||
|  | 			}, | ||||||
|  | 			removeOnFail: { | ||||||
|  | 				age: 3600 * 24 * 7, // keep up to 7 days
 | ||||||
|  | 				count: 100, | ||||||
|  | 			}, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@bindThis | 	@bindThis | ||||||
| 	private getQueue(type: typeof QUEUE_TYPES[number]) { | 	private getQueue(type: typeof QUEUE_TYPES[number]): Bull.Queue { | ||||||
| 		switch (type) { | 		switch (type) { | ||||||
| 			case 'system': return this.systemQueue; | 			case 'system': return this.systemQueue; | ||||||
| 			case 'endedPollNotification': return this.endedPollNotificationQueue; | 			case 'endedPollNotification': return this.endedPollNotificationQueue; | ||||||
|  | @ -557,19 +727,173 @@ export class QueueService { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@bindThis | 	@bindThis | ||||||
| 	public clearQueue(queueType: typeof QUEUE_TYPES[number], state: '*' | 'completed' | 'wait' | 'active' | 'paused' | 'prioritized' | 'delayed' | 'failed') { | 	public async queueClear(queueType: typeof QUEUE_TYPES[number], state: '*' | 'completed' | 'wait' | 'active' | 'paused' | 'prioritized' | 'delayed' | 'failed') { | ||||||
| 		const queue = this.getQueue(queueType); | 		const queue = this.getQueue(queueType); | ||||||
| 
 | 
 | ||||||
| 		if (state === '*') { | 		if (state === '*') { | ||||||
| 			queue.clean(0, 0, 'completed'); | 			await Promise.all([ | ||||||
| 			queue.clean(0, 0, 'wait'); | 				queue.clean(0, 0, 'completed'), | ||||||
| 			queue.clean(0, 0, 'active'); | 				queue.clean(0, 0, 'wait'), | ||||||
| 			queue.clean(0, 0, 'paused'); | 				queue.clean(0, 0, 'active'), | ||||||
| 			queue.clean(0, 0, 'prioritized'); | 				queue.clean(0, 0, 'paused'), | ||||||
| 			queue.clean(0, 0, 'delayed'); | 				queue.clean(0, 0, 'prioritized'), | ||||||
| 			queue.clean(0, 0, 'failed'); | 				queue.clean(0, 0, 'delayed'), | ||||||
|  | 				queue.clean(0, 0, 'failed'), | ||||||
|  | 			]); | ||||||
| 		} else { | 		} else { | ||||||
| 			queue.clean(0, 0, state); | 			await queue.clean(0, 0, state); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	@bindThis | ||||||
|  | 	public async queuePromoteJobs(queueType: typeof QUEUE_TYPES[number]) { | ||||||
|  | 		const queue = this.getQueue(queueType); | ||||||
|  | 		await queue.promoteJobs(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@bindThis | ||||||
|  | 	public async queueRetryJob(queueType: typeof QUEUE_TYPES[number], jobId: string) { | ||||||
|  | 		const queue = this.getQueue(queueType); | ||||||
|  | 		const job: Bull.Job | null = await queue.getJob(jobId); | ||||||
|  | 		if (job) { | ||||||
|  | 			if (job.finishedOn != null) { | ||||||
|  | 				await job.retry(); | ||||||
|  | 			} else { | ||||||
|  | 				await job.promote(); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@bindThis | ||||||
|  | 	public async queueRemoveJob(queueType: typeof QUEUE_TYPES[number], jobId: string) { | ||||||
|  | 		const queue = this.getQueue(queueType); | ||||||
|  | 		const job: Bull.Job | null = await queue.getJob(jobId); | ||||||
|  | 		if (job) { | ||||||
|  | 			await job.remove(); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@bindThis | ||||||
|  | 	private packJobData(job: Bull.Job) { | ||||||
|  | 		// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | ||||||
|  | 		const stacktrace = job.stacktrace ? job.stacktrace.filter(Boolean) : []; | ||||||
|  | 		stacktrace.reverse(); | ||||||
|  | 
 | ||||||
|  | 		return { | ||||||
|  | 			id: job.id, | ||||||
|  | 			name: job.name, | ||||||
|  | 			data: job.data, | ||||||
|  | 			opts: job.opts, | ||||||
|  | 			timestamp: job.timestamp, | ||||||
|  | 			processedOn: job.processedOn, | ||||||
|  | 			processedBy: job.processedBy, | ||||||
|  | 			finishedOn: job.finishedOn, | ||||||
|  | 			progress: job.progress, | ||||||
|  | 			attempts: job.attemptsMade, | ||||||
|  | 			delay: job.delay, | ||||||
|  | 			failedReason: job.failedReason, | ||||||
|  | 			stacktrace: stacktrace, | ||||||
|  | 			returnValue: job.returnvalue, | ||||||
|  | 			isFailed: !!job.failedReason || (Array.isArray(stacktrace) && stacktrace.length > 0), | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@bindThis | ||||||
|  | 	public async queueGetJob(queueType: typeof QUEUE_TYPES[number], jobId: string) { | ||||||
|  | 		const queue = this.getQueue(queueType); | ||||||
|  | 		const job: Bull.Job | null = await queue.getJob(jobId); | ||||||
|  | 		if (job) { | ||||||
|  | 			return this.packJobData(job); | ||||||
|  | 		} else { | ||||||
|  | 			throw new Error(`Job not found: ${jobId}`); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@bindThis | ||||||
|  | 	public async queueGetJobs(queueType: typeof QUEUE_TYPES[number], jobTypes: JobType[], search?: string) { | ||||||
|  | 		const RETURN_LIMIT = 100; | ||||||
|  | 		const queue = this.getQueue(queueType); | ||||||
|  | 		let jobs: Bull.Job[]; | ||||||
|  | 
 | ||||||
|  | 		if (search) { | ||||||
|  | 			jobs = await queue.getJobs(jobTypes, 0, 1000); | ||||||
|  | 
 | ||||||
|  | 			jobs = jobs.filter(job => { | ||||||
|  | 				const jobString = JSON.stringify(job).toLowerCase(); | ||||||
|  | 				return search.toLowerCase().split(' ').every(term => { | ||||||
|  | 					return jobString.includes(term); | ||||||
|  | 				}); | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			jobs = jobs.slice(0, RETURN_LIMIT); | ||||||
|  | 		} else { | ||||||
|  | 			jobs = await queue.getJobs(jobTypes, 0, RETURN_LIMIT); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return jobs.map(job => this.packJobData(job)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@bindThis | ||||||
|  | 	public async queueGetQueues() { | ||||||
|  | 		const fetchings = QUEUE_TYPES.map(async type => { | ||||||
|  | 			const queue = this.getQueue(type); | ||||||
|  | 
 | ||||||
|  | 			const counts = await queue.getJobCounts(); | ||||||
|  | 			const isPaused = await queue.isPaused(); | ||||||
|  | 			const metrics_completed = await queue.getMetrics('completed', 0, MetricsTime.ONE_WEEK); | ||||||
|  | 			const metrics_failed = await queue.getMetrics('failed', 0, MetricsTime.ONE_WEEK); | ||||||
|  | 
 | ||||||
|  | 			return { | ||||||
|  | 				name: type, | ||||||
|  | 				counts: counts, | ||||||
|  | 				isPaused, | ||||||
|  | 				metrics: { | ||||||
|  | 					completed: metrics_completed, | ||||||
|  | 					failed: metrics_failed, | ||||||
|  | 				}, | ||||||
|  | 			}; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		return await Promise.all(fetchings); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@bindThis | ||||||
|  | 	public async queueGetQueue(queueType: typeof QUEUE_TYPES[number]) { | ||||||
|  | 		const queue = this.getQueue(queueType); | ||||||
|  | 		const counts = await queue.getJobCounts(); | ||||||
|  | 		const isPaused = await queue.isPaused(); | ||||||
|  | 		const metrics_completed = await queue.getMetrics('completed', 0, MetricsTime.ONE_WEEK); | ||||||
|  | 		const metrics_failed = await queue.getMetrics('failed', 0, MetricsTime.ONE_WEEK); | ||||||
|  | 		const db = parseRedisInfo(await (await queue.client).info()); | ||||||
|  | 
 | ||||||
|  | 		return { | ||||||
|  | 			name: queueType, | ||||||
|  | 			qualifiedName: queue.qualifiedName, | ||||||
|  | 			counts: counts, | ||||||
|  | 			isPaused, | ||||||
|  | 			metrics: { | ||||||
|  | 				completed: metrics_completed, | ||||||
|  | 				failed: metrics_failed, | ||||||
|  | 			}, | ||||||
|  | 			db: { | ||||||
|  | 				version: db.redis_version, | ||||||
|  | 				mode: db.redis_mode, | ||||||
|  | 				runId: db.run_id, | ||||||
|  | 				processId: db.process_id, | ||||||
|  | 				port: parseInt(db.tcp_port), | ||||||
|  | 				os: db.os, | ||||||
|  | 				uptime: parseInt(db.uptime_in_seconds), | ||||||
|  | 				memory: { | ||||||
|  | 					total: parseInt(db.total_system_memory) || parseInt(db.maxmemory), | ||||||
|  | 					used: parseInt(db.used_memory), | ||||||
|  | 					fragmentationRatio: parseInt(db.mem_fragmentation_ratio), | ||||||
|  | 					peak: parseInt(db.used_memory_peak), | ||||||
|  | 				}, | ||||||
|  | 				clients: { | ||||||
|  | 					connected: parseInt(db.connected_clients), | ||||||
|  | 					blocked: parseInt(db.blocked_clients), | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -44,7 +44,7 @@ import { BakeBufferedReactionsProcessorService } from './processors/BakeBuffered | ||||||
| import { CleanProcessorService } from './processors/CleanProcessorService.js'; | import { CleanProcessorService } from './processors/CleanProcessorService.js'; | ||||||
| import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; | import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; | ||||||
| import { QueueLoggerService } from './QueueLoggerService.js'; | import { QueueLoggerService } from './QueueLoggerService.js'; | ||||||
| import { QUEUE, baseQueueOptions } from './const.js'; | import { QUEUE, baseWorkerOptions } from './const.js'; | ||||||
| 
 | 
 | ||||||
| // ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
 | // ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
 | ||||||
| function httpRelatedBackoff(attemptsMade: number) { | function httpRelatedBackoff(attemptsMade: number) { | ||||||
|  | @ -175,7 +175,7 @@ export class QueueProcessorService implements OnApplicationShutdown { | ||||||
| 					return processer(job); | 					return processer(job); | ||||||
| 				} | 				} | ||||||
| 			}, { | 			}, { | ||||||
| 				...baseQueueOptions(this.config, QUEUE.SYSTEM), | 				...baseWorkerOptions(this.config, QUEUE.SYSTEM), | ||||||
| 				autorun: false, | 				autorun: false, | ||||||
| 			}); | 			}); | ||||||
| 
 | 
 | ||||||
|  | @ -232,7 +232,7 @@ export class QueueProcessorService implements OnApplicationShutdown { | ||||||
| 					return processer(job); | 					return processer(job); | ||||||
| 				} | 				} | ||||||
| 			}, { | 			}, { | ||||||
| 				...baseQueueOptions(this.config, QUEUE.DB), | 				...baseWorkerOptions(this.config, QUEUE.DB), | ||||||
| 				autorun: false, | 				autorun: false, | ||||||
| 			}); | 			}); | ||||||
| 
 | 
 | ||||||
|  | @ -264,7 +264,7 @@ export class QueueProcessorService implements OnApplicationShutdown { | ||||||
| 					return this.deliverProcessorService.process(job); | 					return this.deliverProcessorService.process(job); | ||||||
| 				} | 				} | ||||||
| 			}, { | 			}, { | ||||||
| 				...baseQueueOptions(this.config, QUEUE.DELIVER), | 				...baseWorkerOptions(this.config, QUEUE.DELIVER), | ||||||
| 				autorun: false, | 				autorun: false, | ||||||
| 				concurrency: this.config.deliverJobConcurrency ?? 128, | 				concurrency: this.config.deliverJobConcurrency ?? 128, | ||||||
| 				limiter: { | 				limiter: { | ||||||
|  | @ -304,7 +304,7 @@ export class QueueProcessorService implements OnApplicationShutdown { | ||||||
| 					return this.inboxProcessorService.process(job); | 					return this.inboxProcessorService.process(job); | ||||||
| 				} | 				} | ||||||
| 			}, { | 			}, { | ||||||
| 				...baseQueueOptions(this.config, QUEUE.INBOX), | 				...baseWorkerOptions(this.config, QUEUE.INBOX), | ||||||
| 				autorun: false, | 				autorun: false, | ||||||
| 				concurrency: this.config.inboxJobConcurrency ?? 16, | 				concurrency: this.config.inboxJobConcurrency ?? 16, | ||||||
| 				limiter: { | 				limiter: { | ||||||
|  | @ -344,7 +344,7 @@ export class QueueProcessorService implements OnApplicationShutdown { | ||||||
| 					return this.userWebhookDeliverProcessorService.process(job); | 					return this.userWebhookDeliverProcessorService.process(job); | ||||||
| 				} | 				} | ||||||
| 			}, { | 			}, { | ||||||
| 				...baseQueueOptions(this.config, QUEUE.USER_WEBHOOK_DELIVER), | 				...baseWorkerOptions(this.config, QUEUE.USER_WEBHOOK_DELIVER), | ||||||
| 				autorun: false, | 				autorun: false, | ||||||
| 				concurrency: 64, | 				concurrency: 64, | ||||||
| 				limiter: { | 				limiter: { | ||||||
|  | @ -384,7 +384,7 @@ export class QueueProcessorService implements OnApplicationShutdown { | ||||||
| 					return this.systemWebhookDeliverProcessorService.process(job); | 					return this.systemWebhookDeliverProcessorService.process(job); | ||||||
| 				} | 				} | ||||||
| 			}, { | 			}, { | ||||||
| 				...baseQueueOptions(this.config, QUEUE.SYSTEM_WEBHOOK_DELIVER), | 				...baseWorkerOptions(this.config, QUEUE.SYSTEM_WEBHOOK_DELIVER), | ||||||
| 				autorun: false, | 				autorun: false, | ||||||
| 				concurrency: 16, | 				concurrency: 16, | ||||||
| 				limiter: { | 				limiter: { | ||||||
|  | @ -434,7 +434,7 @@ export class QueueProcessorService implements OnApplicationShutdown { | ||||||
| 					return processer(job); | 					return processer(job); | ||||||
| 				} | 				} | ||||||
| 			}, { | 			}, { | ||||||
| 				...baseQueueOptions(this.config, QUEUE.RELATIONSHIP), | 				...baseWorkerOptions(this.config, QUEUE.RELATIONSHIP), | ||||||
| 				autorun: false, | 				autorun: false, | ||||||
| 				concurrency: this.config.relationshipJobConcurrency ?? 16, | 				concurrency: this.config.relationshipJobConcurrency ?? 16, | ||||||
| 				limiter: { | 				limiter: { | ||||||
|  | @ -479,7 +479,7 @@ export class QueueProcessorService implements OnApplicationShutdown { | ||||||
| 					return processer(job); | 					return processer(job); | ||||||
| 				} | 				} | ||||||
| 			}, { | 			}, { | ||||||
| 				...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE), | 				...baseWorkerOptions(this.config, QUEUE.OBJECT_STORAGE), | ||||||
| 				autorun: false, | 				autorun: false, | ||||||
| 				concurrency: 16, | 				concurrency: 16, | ||||||
| 			}); | 			}); | ||||||
|  | @ -512,7 +512,7 @@ export class QueueProcessorService implements OnApplicationShutdown { | ||||||
| 					return this.endedPollNotificationProcessorService.process(job); | 					return this.endedPollNotificationProcessorService.process(job); | ||||||
| 				} | 				} | ||||||
| 			}, { | 			}, { | ||||||
| 				...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION), | 				...baseWorkerOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION), | ||||||
| 				autorun: false, | 				autorun: false, | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ | ||||||
|  * SPDX-License-Identifier: AGPL-3.0-only |  * SPDX-License-Identifier: AGPL-3.0-only | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
|  | import { MetricsTime } from 'bullmq'; | ||||||
| import { Config } from '@/config.js'; | import { Config } from '@/config.js'; | ||||||
| import type * as Bull from 'bullmq'; | import type * as Bull from 'bullmq'; | ||||||
| 
 | 
 | ||||||
|  | @ -27,3 +28,12 @@ export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof t | ||||||
| 		prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue:${queueName}` : `queue:${queueName}`, | 		prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue:${queueName}` : `queue:${queueName}`, | ||||||
| 	}; | 	}; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export function baseWorkerOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.WorkerOptions { | ||||||
|  | 	return { | ||||||
|  | 		...baseQueueOptions(config, queueName), | ||||||
|  | 		metrics: { | ||||||
|  | 			maxDataPoints: MetricsTime.ONE_WEEK, | ||||||
|  | 		}, | ||||||
|  | 	}; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -67,8 +67,14 @@ export * as 'admin/promo/create' from './endpoints/admin/promo/create.js'; | ||||||
| export * as 'admin/queue/clear' from './endpoints/admin/queue/clear.js'; | export * as 'admin/queue/clear' from './endpoints/admin/queue/clear.js'; | ||||||
| export * as 'admin/queue/deliver-delayed' from './endpoints/admin/queue/deliver-delayed.js'; | export * as 'admin/queue/deliver-delayed' from './endpoints/admin/queue/deliver-delayed.js'; | ||||||
| export * as 'admin/queue/inbox-delayed' from './endpoints/admin/queue/inbox-delayed.js'; | export * as 'admin/queue/inbox-delayed' from './endpoints/admin/queue/inbox-delayed.js'; | ||||||
| export * as 'admin/queue/promote' from './endpoints/admin/queue/promote.js'; | export * as 'admin/queue/retry-job' from './endpoints/admin/queue/retry-job.js'; | ||||||
|  | export * as 'admin/queue/remove-job' from './endpoints/admin/queue/remove-job.js'; | ||||||
|  | export * as 'admin/queue/show-job' from './endpoints/admin/queue/show-job.js'; | ||||||
|  | export * as 'admin/queue/promote-jobs' from './endpoints/admin/queue/promote-jobs.js'; | ||||||
|  | export * as 'admin/queue/jobs' from './endpoints/admin/queue/jobs.js'; | ||||||
| export * as 'admin/queue/stats' from './endpoints/admin/queue/stats.js'; | export * as 'admin/queue/stats' from './endpoints/admin/queue/stats.js'; | ||||||
|  | export * as 'admin/queue/queues' from './endpoints/admin/queue/queues.js'; | ||||||
|  | export * as 'admin/queue/queue-stats' from './endpoints/admin/queue/queue-stats.js'; | ||||||
| export * as 'admin/relays/add' from './endpoints/admin/relays/add.js'; | export * as 'admin/relays/add' from './endpoints/admin/relays/add.js'; | ||||||
| export * as 'admin/relays/list' from './endpoints/admin/relays/list.js'; | export * as 'admin/relays/list' from './endpoints/admin/relays/list.js'; | ||||||
| export * as 'admin/relays/remove' from './endpoints/admin/relays/remove.js'; | export * as 'admin/relays/remove' from './endpoints/admin/relays/remove.js'; | ||||||
|  |  | ||||||
|  | @ -19,10 +19,10 @@ export const meta = { | ||||||
| export const paramDef = { | export const paramDef = { | ||||||
| 	type: 'object', | 	type: 'object', | ||||||
| 	properties: { | 	properties: { | ||||||
| 		type: { type: 'string', enum: QUEUE_TYPES }, | 		queue: { type: 'string', enum: QUEUE_TYPES }, | ||||||
| 		state: { type: 'string', enum: ['*', 'wait', 'delayed'] }, | 		state: { type: 'string', enum: ['*', 'completed', 'wait', 'active', 'paused', 'prioritized', 'delayed', 'failed'] }, | ||||||
| 	}, | 	}, | ||||||
| 	required: ['type', 'state'], | 	required: ['queue', 'state'], | ||||||
| } as const; | } as const; | ||||||
| 
 | 
 | ||||||
| @Injectable() | @Injectable() | ||||||
|  | @ -32,7 +32,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||||
| 		private queueService: QueueService, | 		private queueService: QueueService, | ||||||
| 	) { | 	) { | ||||||
| 		super(meta, paramDef, async (ps, me) => { | 		super(meta, paramDef, async (ps, me) => { | ||||||
| 			this.queueService.clearQueue(ps.type, ps.state); | 			this.queueService.queueClear(ps.queue, ps.state); | ||||||
| 
 | 
 | ||||||
| 			this.moderationLogService.log(me, 'clearQueue'); | 			this.moderationLogService.log(me, 'clearQueue'); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
|  | @ -0,0 +1,38 @@ | ||||||
|  | /* | ||||||
|  |  * SPDX-FileCopyrightText: syuilo and misskey-project | ||||||
|  |  * SPDX-License-Identifier: AGPL-3.0-only | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import { Injectable } from '@nestjs/common'; | ||||||
|  | import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||||
|  | import { ModerationLogService } from '@/core/ModerationLogService.js'; | ||||||
|  | import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	tags: ['admin'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 	requireModerator: true, | ||||||
|  | 	kind: 'read:admin:queue', | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | export const paramDef = { | ||||||
|  | 	type: 'object', | ||||||
|  | 	properties: { | ||||||
|  | 		queue: { type: 'string', enum: QUEUE_TYPES }, | ||||||
|  | 		state: { type: 'array', items: { type: 'string', enum: ['active', 'wait', 'delayed', 'completed', 'failed'] } }, | ||||||
|  | 		search: { type: 'string' }, | ||||||
|  | 	}, | ||||||
|  | 	required: ['queue', 'state'], | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | @Injectable() | ||||||
|  | export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | ||||||
|  | 	constructor( | ||||||
|  | 		private queueService: QueueService, | ||||||
|  | 	) { | ||||||
|  | 		super(meta, paramDef, async (ps, me) => { | ||||||
|  | 			return this.queueService.queueGetJobs(ps.queue, ps.state, ps.search); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,39 @@ | ||||||
|  | /* | ||||||
|  |  * SPDX-FileCopyrightText: syuilo and misskey-project | ||||||
|  |  * SPDX-License-Identifier: AGPL-3.0-only | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import { Injectable } from '@nestjs/common'; | ||||||
|  | import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||||
|  | import { ModerationLogService } from '@/core/ModerationLogService.js'; | ||||||
|  | import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	tags: ['admin'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 	requireModerator: true, | ||||||
|  | 	kind: 'write:admin:queue', | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | export const paramDef = { | ||||||
|  | 	type: 'object', | ||||||
|  | 	properties: { | ||||||
|  | 		queue: { type: 'string', enum: QUEUE_TYPES }, | ||||||
|  | 	}, | ||||||
|  | 	required: ['queue'], | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | @Injectable() | ||||||
|  | export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | ||||||
|  | 	constructor( | ||||||
|  | 		private moderationLogService: ModerationLogService, | ||||||
|  | 		private queueService: QueueService, | ||||||
|  | 	) { | ||||||
|  | 		super(meta, paramDef, async (ps, me) => { | ||||||
|  | 			this.queueService.queuePromoteJobs(ps.queue); | ||||||
|  | 
 | ||||||
|  | 			this.moderationLogService.log(me, 'promoteQueue'); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -1,77 +0,0 @@ | ||||||
| /* |  | ||||||
|  * SPDX-FileCopyrightText: syuilo and misskey-project |  | ||||||
|  * SPDX-License-Identifier: AGPL-3.0-only |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| import { Injectable } from '@nestjs/common'; |  | ||||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; |  | ||||||
| import { ModerationLogService } from '@/core/ModerationLogService.js'; |  | ||||||
| import { QueueService } from '@/core/QueueService.js'; |  | ||||||
| 
 |  | ||||||
| export const meta = { |  | ||||||
| 	tags: ['admin'], |  | ||||||
| 
 |  | ||||||
| 	requireCredential: true, |  | ||||||
| 	requireModerator: true, |  | ||||||
| 	kind: 'write:admin:queue', |  | ||||||
| } as const; |  | ||||||
| 
 |  | ||||||
| export const paramDef = { |  | ||||||
| 	type: 'object', |  | ||||||
| 	properties: { |  | ||||||
| 		type: { type: 'string', enum: ['deliver', 'inbox'] }, |  | ||||||
| 	}, |  | ||||||
| 	required: ['type'], |  | ||||||
| } as const; |  | ||||||
| 
 |  | ||||||
| @Injectable() |  | ||||||
| export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 |  | ||||||
| 	constructor( |  | ||||||
| 		private moderationLogService: ModerationLogService, |  | ||||||
| 		private queueService: QueueService, |  | ||||||
| 	) { |  | ||||||
| 		super(meta, paramDef, async (ps, me) => { |  | ||||||
| 			let delayedQueues; |  | ||||||
| 
 |  | ||||||
| 			switch (ps.type) { |  | ||||||
| 				case 'deliver': |  | ||||||
| 					delayedQueues = await this.queueService.deliverQueue.getDelayed(); |  | ||||||
| 					for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) { |  | ||||||
| 						const queue = delayedQueues[queueIndex]; |  | ||||||
| 						try { |  | ||||||
| 							await queue.promote(); |  | ||||||
| 						} catch (e) { |  | ||||||
| 							if (e instanceof Error) { |  | ||||||
| 								if (e.message.indexOf('not in a delayed state') !== -1) { |  | ||||||
| 									throw e; |  | ||||||
| 								} |  | ||||||
| 							} else { |  | ||||||
| 								throw e; |  | ||||||
| 							} |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 					break; |  | ||||||
| 
 |  | ||||||
| 				case 'inbox': |  | ||||||
| 					delayedQueues = await this.queueService.inboxQueue.getDelayed(); |  | ||||||
| 					for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) { |  | ||||||
| 						const queue = delayedQueues[queueIndex]; |  | ||||||
| 						try { |  | ||||||
| 							await queue.promote(); |  | ||||||
| 						} catch (e) { |  | ||||||
| 							if (e instanceof Error) { |  | ||||||
| 								if (e.message.indexOf('not in a delayed state') !== -1) { |  | ||||||
| 									throw e; |  | ||||||
| 								} |  | ||||||
| 							} else { |  | ||||||
| 								throw e; |  | ||||||
| 							} |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 					break; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			this.moderationLogService.log(me, 'promoteQueue'); |  | ||||||
| 		}); |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,36 @@ | ||||||
|  | /* | ||||||
|  |  * SPDX-FileCopyrightText: syuilo and misskey-project | ||||||
|  |  * SPDX-License-Identifier: AGPL-3.0-only | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import { Injectable } from '@nestjs/common'; | ||||||
|  | import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||||
|  | import { ModerationLogService } from '@/core/ModerationLogService.js'; | ||||||
|  | import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	tags: ['admin'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 	requireModerator: true, | ||||||
|  | 	kind: 'read:admin:queue', | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | export const paramDef = { | ||||||
|  | 	type: 'object', | ||||||
|  | 	properties: { | ||||||
|  | 		queue: { type: 'string', enum: QUEUE_TYPES }, | ||||||
|  | 	}, | ||||||
|  | 	required: ['queue'], | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | @Injectable() | ||||||
|  | export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | ||||||
|  | 	constructor( | ||||||
|  | 		private queueService: QueueService, | ||||||
|  | 	) { | ||||||
|  | 		super(meta, paramDef, async (ps, me) => { | ||||||
|  | 			return this.queueService.queueGetQueue(ps.queue); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,35 @@ | ||||||
|  | /* | ||||||
|  |  * SPDX-FileCopyrightText: syuilo and misskey-project | ||||||
|  |  * SPDX-License-Identifier: AGPL-3.0-only | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import { Injectable } from '@nestjs/common'; | ||||||
|  | import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||||
|  | import { ModerationLogService } from '@/core/ModerationLogService.js'; | ||||||
|  | import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	tags: ['admin'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 	requireModerator: true, | ||||||
|  | 	kind: 'read:admin:queue', | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | export const paramDef = { | ||||||
|  | 	type: 'object', | ||||||
|  | 	properties: { | ||||||
|  | 	}, | ||||||
|  | 	required: [], | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | @Injectable() | ||||||
|  | export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | ||||||
|  | 	constructor( | ||||||
|  | 		private queueService: QueueService, | ||||||
|  | 	) { | ||||||
|  | 		super(meta, paramDef, async (ps, me) => { | ||||||
|  | 			return this.queueService.queueGetQueues(); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,38 @@ | ||||||
|  | /* | ||||||
|  |  * SPDX-FileCopyrightText: syuilo and misskey-project | ||||||
|  |  * SPDX-License-Identifier: AGPL-3.0-only | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import { Injectable } from '@nestjs/common'; | ||||||
|  | import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||||
|  | import { ModerationLogService } from '@/core/ModerationLogService.js'; | ||||||
|  | import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	tags: ['admin'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 	requireModerator: true, | ||||||
|  | 	kind: 'write:admin:queue', | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | export const paramDef = { | ||||||
|  | 	type: 'object', | ||||||
|  | 	properties: { | ||||||
|  | 		queue: { type: 'string', enum: QUEUE_TYPES }, | ||||||
|  | 		jobId: { type: 'string' }, | ||||||
|  | 	}, | ||||||
|  | 	required: ['queue', 'jobId'], | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | @Injectable() | ||||||
|  | export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | ||||||
|  | 	constructor( | ||||||
|  | 		private moderationLogService: ModerationLogService, | ||||||
|  | 		private queueService: QueueService, | ||||||
|  | 	) { | ||||||
|  | 		super(meta, paramDef, async (ps, me) => { | ||||||
|  | 			this.queueService.queueRemoveJob(ps.queue, ps.jobId); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,38 @@ | ||||||
|  | /* | ||||||
|  |  * SPDX-FileCopyrightText: syuilo and misskey-project | ||||||
|  |  * SPDX-License-Identifier: AGPL-3.0-only | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import { Injectable } from '@nestjs/common'; | ||||||
|  | import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||||
|  | import { ModerationLogService } from '@/core/ModerationLogService.js'; | ||||||
|  | import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	tags: ['admin'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 	requireModerator: true, | ||||||
|  | 	kind: 'write:admin:queue', | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | export const paramDef = { | ||||||
|  | 	type: 'object', | ||||||
|  | 	properties: { | ||||||
|  | 		queue: { type: 'string', enum: QUEUE_TYPES }, | ||||||
|  | 		jobId: { type: 'string' }, | ||||||
|  | 	}, | ||||||
|  | 	required: ['queue', 'jobId'], | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | @Injectable() | ||||||
|  | export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | ||||||
|  | 	constructor( | ||||||
|  | 		private moderationLogService: ModerationLogService, | ||||||
|  | 		private queueService: QueueService, | ||||||
|  | 	) { | ||||||
|  | 		super(meta, paramDef, async (ps, me) => { | ||||||
|  | 			this.queueService.queueRetryJob(ps.queue, ps.jobId); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,38 @@ | ||||||
|  | /* | ||||||
|  |  * SPDX-FileCopyrightText: syuilo and misskey-project | ||||||
|  |  * SPDX-License-Identifier: AGPL-3.0-only | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import { Injectable } from '@nestjs/common'; | ||||||
|  | import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||||
|  | import { ModerationLogService } from '@/core/ModerationLogService.js'; | ||||||
|  | import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	tags: ['admin'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 	requireModerator: true, | ||||||
|  | 	kind: 'read:admin:queue', | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | export const paramDef = { | ||||||
|  | 	type: 'object', | ||||||
|  | 	properties: { | ||||||
|  | 		queue: { type: 'string', enum: QUEUE_TYPES }, | ||||||
|  | 		jobId: { type: 'string' }, | ||||||
|  | 	}, | ||||||
|  | 	required: ['queue', 'jobId'], | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | @Injectable() | ||||||
|  | export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | ||||||
|  | 	constructor( | ||||||
|  | 		private moderationLogService: ModerationLogService, | ||||||
|  | 		private queueService: QueueService, | ||||||
|  | 	) { | ||||||
|  | 		super(meta, paramDef, async (ps, me) => { | ||||||
|  | 			return this.queueService.queueGetJob(ps.queue, ps.jobId); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -12,8 +12,8 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||||
| 		<template #fallback> | 		<template #fallback> | ||||||
| 			<MkLoading/> | 			<MkLoading/> | ||||||
| 		</template> | 		</template> | ||||||
| 		<XCode v-if="show && lang" :code="code" :lang="lang"/> | 		<XCode v-if="show && lang" class="_selectable" :code="code" :lang="lang"/> | ||||||
| 		<pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre> | 		<pre v-else-if="show" class="_selectable" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre> | ||||||
| 		<button v-else :class="$style.codePlaceholderRoot" @click="show = true"> | 		<button v-else :class="$style.codePlaceholderRoot" @click="show = true"> | ||||||
| 			<div :class="$style.codePlaceholderContainer"> | 			<div :class="$style.codePlaceholderContainer"> | ||||||
| 				<div><i class="ti ti-code"></i> {{ i18n.ts.code }}</div> | 				<div><i class="ti ti-code"></i> {{ i18n.ts.code }}</div> | ||||||
|  | @ -70,11 +70,9 @@ function copy() { | ||||||
| .codeBlockFallbackRoot { | .codeBlockFallbackRoot { | ||||||
| 	display: block; | 	display: block; | ||||||
| 	overflow-wrap: anywhere; | 	overflow-wrap: anywhere; | ||||||
| 	background: var(--MI_THEME-bg); |  | ||||||
| 	padding: 1em; | 	padding: 1em; | ||||||
| 	margin: .5em 0; | 	margin: 0; | ||||||
| 	overflow: auto; | 	overflow: auto; | ||||||
| 	border-radius: 8px; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .codeBlockFallbackCode { | .codeBlockFallbackCode { | ||||||
|  |  | ||||||
|  | @ -38,15 +38,26 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||||
| 			> | 			> | ||||||
| 				<KeepAlive> | 				<KeepAlive> | ||||||
| 					<div v-show="opened"> | 					<div v-show="opened"> | ||||||
|  | 						<MkStickyContainer> | ||||||
|  | 							<template #header> | ||||||
|  | 								<div v-if="$slots.header" :class="$style.inBodyHeader"> | ||||||
|  | 									<slot name="header"></slot> | ||||||
|  | 								</div> | ||||||
|  | 							</template> | ||||||
|  | 
 | ||||||
| 							<MkSpacer v-if="withSpacer" :marginMin="spacerMin" :marginMax="spacerMax"> | 							<MkSpacer v-if="withSpacer" :marginMin="spacerMin" :marginMax="spacerMax"> | ||||||
| 								<slot></slot> | 								<slot></slot> | ||||||
| 							</MkSpacer> | 							</MkSpacer> | ||||||
| 							<div v-else> | 							<div v-else> | ||||||
| 								<slot></slot> | 								<slot></slot> | ||||||
| 							</div> | 							</div> | ||||||
| 						<div v-if="$slots.footer" :class="$style.footer"> | 
 | ||||||
|  | 							<template #footer> | ||||||
|  | 								<div v-if="$slots.footer" :class="$style.inBodyFooter"> | ||||||
| 									<slot name="footer"></slot> | 									<slot name="footer"></slot> | ||||||
| 								</div> | 								</div> | ||||||
|  | 							</template> | ||||||
|  | 						</MkStickyContainer> | ||||||
| 					</div> | 					</div> | ||||||
| 				</KeepAlive> | 				</KeepAlive> | ||||||
| 			</Transition> | 			</Transition> | ||||||
|  | @ -230,14 +241,21 @@ onMounted(() => { | ||||||
| 
 | 
 | ||||||
| 	&.bgSame { | 	&.bgSame { | ||||||
| 		background: var(--MI_THEME-bg); | 		background: var(--MI_THEME-bg); | ||||||
|  | 
 | ||||||
|  | 		.inBodyHeader { | ||||||
|  | 			background: color(from var(--MI_THEME-bg) srgb r g b / 0.75); | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .footer { | .inBodyHeader { | ||||||
| 	position: sticky !important; | 	background: color(from var(--MI_THEME-panel) srgb r g b / 0.75); | ||||||
| 	z-index: 1; | 	-webkit-backdrop-filter: var(--MI-blur, blur(15px)); | ||||||
| 	bottom: var(--MI-stickyBottom, 0px); | 	backdrop-filter: var(--MI-blur, blur(15px)); | ||||||
| 	left: 0; | 	border-bottom: solid 0.5px var(--MI_THEME-divider); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .inBodyFooter { | ||||||
| 	padding: 12px; | 	padding: 12px; | ||||||
| 	background: color(from var(--MI_THEME-bg) srgb r g b / 0.5); | 	background: color(from var(--MI_THEME-bg) srgb r g b / 0.5); | ||||||
| 	-webkit-backdrop-filter: var(--MI-blur, blur(15px)); | 	-webkit-backdrop-filter: var(--MI-blur, blur(15px)); | ||||||
|  |  | ||||||
|  | @ -0,0 +1,235 @@ | ||||||
|  | <!-- | ||||||
|  | SPDX-FileCopyrightText: syuilo and misskey-project | ||||||
|  | SPDX-License-Identifier: AGPL-3.0-only | ||||||
|  | --> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  | <div :class="$style.tabs"> | ||||||
|  | 	<div :class="$style.tabsInner"> | ||||||
|  | 		<button | ||||||
|  | 			v-for="t in tabs" :ref="(el) => tabRefs[t.key] = (el as HTMLElement)" v-tooltip.noDelay="t.title" | ||||||
|  | 			class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: prefer.s.animation }]" | ||||||
|  | 			@mousedown="(ev) => onTabMousedown(t, ev)" @click="(ev) => onTabClick(t, ev)" | ||||||
|  | 		> | ||||||
|  | 			<div :class="$style.tabInner"> | ||||||
|  | 				<i v-if="t.icon" :class="[$style.tabIcon, t.icon]"></i> | ||||||
|  | 				<div | ||||||
|  | 					v-if="!t.iconOnly || (!prefer.s.animation && t.key === tab)" | ||||||
|  | 					:class="$style.tabTitle" | ||||||
|  | 				> | ||||||
|  | 					{{ t.title }} | ||||||
|  | 				</div> | ||||||
|  | 				<Transition | ||||||
|  | 					v-else mode="in-out" @enter="enter" @afterEnter="afterEnter" @leave="leave" | ||||||
|  | 					@afterLeave="afterLeave" | ||||||
|  | 				> | ||||||
|  | 					<div v-show="t.key === tab" :class="[$style.tabTitle, $style.animate]">{{ t.title }}</div> | ||||||
|  | 				</Transition> | ||||||
|  | 			</div> | ||||||
|  | 		</button> | ||||||
|  | 	</div> | ||||||
|  | 	<div | ||||||
|  | 		ref="tabHighlightEl" | ||||||
|  | 		:class="[$style.tabHighlight, { [$style.animate]: prefer.s.animation }]" | ||||||
|  | 	></div> | ||||||
|  | </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | export type Tab = { | ||||||
|  | 	key: string; | ||||||
|  | 	onClick?: (ev: MouseEvent) => void; | ||||||
|  | } & ( | ||||||
|  | 	| { | ||||||
|  | 		iconOnly?: false; | ||||||
|  | 		title: string; | ||||||
|  | 		icon?: string; | ||||||
|  | 	} | ||||||
|  | 	| { | ||||||
|  | 		iconOnly: true; | ||||||
|  | 		icon: string; | ||||||
|  | 	} | ||||||
|  | ); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import { nextTick, onMounted, onUnmounted, useTemplateRef, watch } from 'vue'; | ||||||
|  | import { prefer } from '@/preferences.js'; | ||||||
|  | 
 | ||||||
|  | const props = withDefaults(defineProps<{ | ||||||
|  | 	tabs?: Tab[]; | ||||||
|  | 	tab?: string; | ||||||
|  | }>(), { | ||||||
|  | 	tabs: () => ([] as Tab[]), | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const emit = defineEmits<{ | ||||||
|  | 	(ev: 'update:tab', key: string); | ||||||
|  | 	(ev: 'tabClick', key: string); | ||||||
|  | }>(); | ||||||
|  | 
 | ||||||
|  | const tabHighlightEl = useTemplateRef('tabHighlightEl'); | ||||||
|  | const tabRefs: Record<string, HTMLElement | null> = {}; | ||||||
|  | 
 | ||||||
|  | function onTabMousedown(tab: Tab, ev: MouseEvent): void { | ||||||
|  | 	// ユーザビリティの観点からmousedown時にはonClickは呼ばない | ||||||
|  | 	if (tab.key) { | ||||||
|  | 		emit('update:tab', tab.key); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function onTabClick(t: Tab, ev: MouseEvent): void { | ||||||
|  | 	emit('tabClick', t.key); | ||||||
|  | 
 | ||||||
|  | 	if (t.onClick) { | ||||||
|  | 		ev.preventDefault(); | ||||||
|  | 		ev.stopPropagation(); | ||||||
|  | 		t.onClick(ev); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if (t.key) { | ||||||
|  | 		emit('update:tab', t.key); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function renderTab() { | ||||||
|  | 	const tabEl = props.tab ? tabRefs[props.tab] : undefined; | ||||||
|  | 	if (tabEl && tabHighlightEl.value && tabHighlightEl.value.parentElement) { | ||||||
|  | 		// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある | ||||||
|  | 		// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4 | ||||||
|  | 		const parentRect = tabHighlightEl.value.parentElement.getBoundingClientRect(); | ||||||
|  | 		const rect = tabEl.getBoundingClientRect(); | ||||||
|  | 		tabHighlightEl.value.style.width = rect.width + 'px'; | ||||||
|  | 		tabHighlightEl.value.style.left = (rect.left - parentRect.left + tabHighlightEl.value.parentElement.scrollLeft) + 'px'; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | let entering = false; | ||||||
|  | 
 | ||||||
|  | async function enter(el: Element) { | ||||||
|  | 	if (!(el instanceof HTMLElement)) return; | ||||||
|  | 	entering = true; | ||||||
|  | 	const elementWidth = el.getBoundingClientRect().width; | ||||||
|  | 	el.style.width = '0'; | ||||||
|  | 	el.style.paddingLeft = '0'; | ||||||
|  | 	el.offsetWidth; // reflow | ||||||
|  | 	el.style.width = `${elementWidth}px`; | ||||||
|  | 	el.style.paddingLeft = ''; | ||||||
|  | 	nextTick(() => { | ||||||
|  | 		entering = false; | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	window.setTimeout(renderTab, 170); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function afterEnter(el: Element) { | ||||||
|  | 	if (!(el instanceof HTMLElement)) return; | ||||||
|  | 	// element.style.width = ''; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function leave(el: Element) { | ||||||
|  | 	if (!(el instanceof HTMLElement)) return; | ||||||
|  | 	const elementWidth = el.getBoundingClientRect().width; | ||||||
|  | 	el.style.width = `${elementWidth}px`; | ||||||
|  | 	el.style.paddingLeft = ''; | ||||||
|  | 	el.offsetWidth; // reflow | ||||||
|  | 	el.style.width = '0'; | ||||||
|  | 	el.style.paddingLeft = '0'; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function afterLeave(el: Element) { | ||||||
|  | 	if (!(el instanceof HTMLElement)) return; | ||||||
|  | 	el.style.width = ''; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | onMounted(() => { | ||||||
|  | 	watch([() => props.tab, () => props.tabs], () => { | ||||||
|  | 		nextTick(() => { | ||||||
|  | 			if (entering) return; | ||||||
|  | 			renderTab(); | ||||||
|  | 		}); | ||||||
|  | 	}, { | ||||||
|  | 		immediate: true, | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | onUnmounted(() => { | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" module> | ||||||
|  | .tabs { | ||||||
|  | 	--height: 40px; | ||||||
|  | 
 | ||||||
|  | 	display: block; | ||||||
|  | 	position: relative; | ||||||
|  | 	margin: 0; | ||||||
|  | 	height: var(--height); | ||||||
|  | 	font-size: 85%; | ||||||
|  | 	overflow-x: auto; | ||||||
|  | 	overflow-y: hidden; | ||||||
|  | 	scrollbar-width: none; | ||||||
|  | 
 | ||||||
|  | 	&::-webkit-scrollbar { | ||||||
|  | 		display: none; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .tabsInner { | ||||||
|  | 	display: inline-block; | ||||||
|  | 	height: var(--height); | ||||||
|  | 	white-space: nowrap; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .tab { | ||||||
|  | 	display: inline-block; | ||||||
|  | 	position: relative; | ||||||
|  | 	padding: 0 10px; | ||||||
|  | 	height: 100%; | ||||||
|  | 	font-weight: normal; | ||||||
|  | 	opacity: 0.7; | ||||||
|  | 
 | ||||||
|  | 	&:hover { | ||||||
|  | 		opacity: 1; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	&.active { | ||||||
|  | 		opacity: 1; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	&.animate { | ||||||
|  | 		transition: opacity 0.2s ease; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .tabInner { | ||||||
|  | 	display: flex; | ||||||
|  | 	align-items: center; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .tabIcon + .tabTitle { | ||||||
|  | 	padding-left: 4px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .tabTitle { | ||||||
|  | 	overflow: hidden; | ||||||
|  | 
 | ||||||
|  | 	&.animate { | ||||||
|  | 		transition: width .15s linear, padding-left .15s linear; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .tabHighlight { | ||||||
|  | 	position: absolute; | ||||||
|  | 	bottom: 0; | ||||||
|  | 	height: 3px; | ||||||
|  | 	background: var(--MI_THEME-accent); | ||||||
|  | 	border-radius: 999px; | ||||||
|  | 	transition: none; | ||||||
|  | 	pointer-events: none; | ||||||
|  | 
 | ||||||
|  | 	&.animate { | ||||||
|  | 		transition: width 0.15s ease, left 0.15s ease; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -0,0 +1,173 @@ | ||||||
|  | <!-- | ||||||
|  | SPDX-FileCopyrightText: syuilo and misskey-project | ||||||
|  | SPDX-License-Identifier: AGPL-3.0-only | ||||||
|  | --> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  | <div :class="$style.items"> | ||||||
|  | 	<template v-for="(item, i) in items" :key="item.id"> | ||||||
|  | 		<div :class="$style.left"> | ||||||
|  | 			<slot v-if="item.type === 'event'" name="left" :event="item.data" :timestamp="item.timestamp" :delta="item.delta"></slot> | ||||||
|  | 		</div> | ||||||
|  | 		<div :class="[$style.center, item.type === 'date' ? $style.date : '']"> | ||||||
|  | 			<div :class="$style.centerLine"></div> | ||||||
|  | 			<div :class="$style.centerPoint"></div> | ||||||
|  | 		</div> | ||||||
|  | 		<div :class="$style.right"> | ||||||
|  | 			<slot v-if="item.type === 'event'" name="right" :event="item.data" :timestamp="item.timestamp" :delta="item.delta"></slot> | ||||||
|  | 			<div v-else :class="$style.dateLabel"><i class="ti ti-chevron-up"></i> {{ item.prevText }}</div> | ||||||
|  | 		</div> | ||||||
|  | 	</template> | ||||||
|  | </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import { computed } from 'vue'; | ||||||
|  | 
 | ||||||
|  | const props = defineProps<{ | ||||||
|  | 	events: { | ||||||
|  | 		id: string; | ||||||
|  | 		timestamp: number; | ||||||
|  | 		data: any; | ||||||
|  | 	}[]; | ||||||
|  | }>(); | ||||||
|  | 
 | ||||||
|  | const events = computed(() => { | ||||||
|  | 	return props.events.toSorted((a, b) => b.timestamp - a.timestamp); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | function getDateText(dateInstance: Date) { | ||||||
|  | 	const year = dateInstance.getFullYear(); | ||||||
|  | 	const month = dateInstance.getMonth() + 1; | ||||||
|  | 	const date = dateInstance.getDate(); | ||||||
|  | 	const hour = dateInstance.getHours(); | ||||||
|  | 	return `${year.toString()}/${month.toString()}/${date.toString()} ${hour.toString().padStart(2, '0')}:00:00`; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const items = computed<({ | ||||||
|  | 	id: string; | ||||||
|  | 	type: 'event'; | ||||||
|  | 	timestamp: number; | ||||||
|  | 	delta: number; | ||||||
|  | 	data: any; | ||||||
|  | } | { | ||||||
|  | 	id: string; | ||||||
|  | 	type: 'date'; | ||||||
|  | 	prev: Date; | ||||||
|  | 	prevText: string; | ||||||
|  | 	next: Date | null; | ||||||
|  | 	nextText: string; | ||||||
|  | })[]>(() => { | ||||||
|  | 		const results = []; | ||||||
|  | 		for (let i = 0; i < events.value.length; i++) { | ||||||
|  | 			const item = events.value[i]; | ||||||
|  | 
 | ||||||
|  | 			const date = new Date(item.timestamp); | ||||||
|  | 			const nextDate = events.value[i + 1] ? new Date(events.value[i + 1].timestamp) : null; | ||||||
|  | 
 | ||||||
|  | 			results.push({ | ||||||
|  | 				id: item.id, | ||||||
|  | 				type: 'event', | ||||||
|  | 				timestamp: item.timestamp, | ||||||
|  | 				delta: i === events.value.length - 1 ? 0 : item.timestamp - events.value[i + 1].timestamp, | ||||||
|  | 				data: item.data, | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			if ( | ||||||
|  | 				i !== events.value.length - 1 && | ||||||
|  | 				nextDate != null && ( | ||||||
|  | 					date.getFullYear() !== nextDate.getFullYear() || | ||||||
|  | 					date.getMonth() !== nextDate.getMonth() || | ||||||
|  | 					date.getDate() !== nextDate.getDate() || | ||||||
|  | 					date.getHours() !== nextDate.getHours() | ||||||
|  | 				) | ||||||
|  | 			) { | ||||||
|  | 				results.push({ | ||||||
|  | 					id: `date-${item.id}`, | ||||||
|  | 					type: 'date', | ||||||
|  | 					prev: date, | ||||||
|  | 					prevText: getDateText(date), | ||||||
|  | 					next: nextDate, | ||||||
|  | 					nextText: getDateText(nextDate), | ||||||
|  | 				}); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return results; | ||||||
|  | 	}); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" module> | ||||||
|  | .root { | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .items { | ||||||
|  | 	display: grid; | ||||||
|  | 	grid-template-columns: max-content 18px 1fr; | ||||||
|  | 	gap: 0 8px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .item { | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .center { | ||||||
|  | 	position: relative; | ||||||
|  | 
 | ||||||
|  | 	&.date { | ||||||
|  | 		.centerPoint::before { | ||||||
|  | 			position: absolute; | ||||||
|  | 			content: ""; | ||||||
|  | 			top: 0; | ||||||
|  | 			left: 0; | ||||||
|  | 			right: 0; | ||||||
|  | 			bottom: 0; | ||||||
|  | 			margin: auto; | ||||||
|  | 			width: 7px; | ||||||
|  | 			height: 7px; | ||||||
|  | 			background: var(--MI_THEME-bg); | ||||||
|  | 			border-radius: 50%; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .centerLine { | ||||||
|  | 	position: absolute; | ||||||
|  | 	top: 0; | ||||||
|  | 	left: 0; | ||||||
|  | 	right: 0; | ||||||
|  | 	margin: auto; | ||||||
|  | 	width: 3px; | ||||||
|  | 	height: 100%; | ||||||
|  | 	background: color-mix(in srgb, var(--MI_THEME-accent), var(--MI_THEME-bg) 75%); | ||||||
|  | } | ||||||
|  | .centerPoint { | ||||||
|  | 	position: absolute; | ||||||
|  | 	top: 0; | ||||||
|  | 	left: 0; | ||||||
|  | 	right: 0; | ||||||
|  | 	bottom: 0; | ||||||
|  | 	margin: auto; | ||||||
|  | 	width: 13px; | ||||||
|  | 	height: 13px; | ||||||
|  | 	background: color-mix(in srgb, var(--MI_THEME-accent), var(--MI_THEME-bg) 75%); | ||||||
|  | 	border-radius: 50%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .left { | ||||||
|  | 	min-width: 0; | ||||||
|  | 	align-self: center; | ||||||
|  | 	justify-self: right; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .right { | ||||||
|  | 	min-width: 0; | ||||||
|  | 	align-self: center; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .dateLabel { | ||||||
|  | 	opacity: 0.7; | ||||||
|  | 	font-size: 90%; | ||||||
|  | 	padding: 4px; | ||||||
|  | 	margin: 8px 0; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -50,8 +50,8 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { markRaw, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'; | import { markRaw, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'; | ||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import XChart from './queue.chart.chart.vue'; | import XChart from './federation-job-queue.chart.chart.vue'; | ||||||
| import type { ApQueueDomain } from '@/pages/admin/queue.vue'; | import type { ApQueueDomain } from '@/pages/admin/federation-job-queue.vue'; | ||||||
| import number from '@/filters/number.js'; | import number from '@/filters/number.js'; | ||||||
| import { misskeyApi } from '@/utility/misskey-api.js'; | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
| import { useStream } from '@/stream.js'; | import { useStream } from '@/stream.js'; | ||||||
|  | @ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { ref, computed } from 'vue'; | import { ref, computed } from 'vue'; | ||||||
| import XQueue from './queue.chart.vue'; | import XQueue from './federation-job-queue.chart.vue'; | ||||||
| import XHeader from './_header_.vue'; | import XHeader from './_header_.vue'; | ||||||
| import type { Ref } from 'vue'; | import type { Ref } from 'vue'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
|  | @ -40,7 +40,7 @@ function clear() { | ||||||
| 	}).then(({ canceled }) => { | 	}).then(({ canceled }) => { | ||||||
| 		if (canceled) return; | 		if (canceled) return; | ||||||
| 
 | 
 | ||||||
| 		os.apiWithDialog('admin/queue/clear', { type: tab.value, state: '*' }); | 		os.apiWithDialog('admin/queue/clear', { queue: tab.value, state: '*' }); | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -52,7 +52,7 @@ function promoteAllQueues() { | ||||||
| 	}).then(({ canceled }) => { | 	}).then(({ canceled }) => { | ||||||
| 		if (canceled) return; | 		if (canceled) return; | ||||||
| 
 | 
 | ||||||
| 		os.apiWithDialog('admin/queue/promote', { type: tab.value }); | 		os.apiWithDialog('admin/queue/promote-jobs', { queue: tab.value }); | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -67,7 +67,7 @@ const headerTabs = computed(() => [{ | ||||||
| }]); | }]); | ||||||
| 
 | 
 | ||||||
| definePage(() => ({ | definePage(() => ({ | ||||||
| 	title: i18n.ts.jobQueue, | 	title: i18n.ts.federationJobs, | ||||||
| 	icon: 'ti ti-clock-play', | 	icon: 'ti ti-clock-play', | ||||||
| })); | })); | ||||||
| </script> | </script> | ||||||
|  | @ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||||
| 			</div> | 			</div> | ||||||
| 		</MkSpacer> | 		</MkSpacer> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div v-if="!(narrow && currentPage?.route.name == null)" class="main"> | 	<div v-if="!(narrow && currentPage?.route.name == null)" class="main _pageContainer" style="height: 100%;"> | ||||||
| 		<NestedRouterView/> | 		<NestedRouterView/> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
|  | @ -138,11 +138,16 @@ const menuDef = computed<SuperMenuDef[]>(() => [{ | ||||||
| 		text: i18n.ts.federation, | 		text: i18n.ts.federation, | ||||||
| 		to: '/admin/federation', | 		to: '/admin/federation', | ||||||
| 		active: currentPage.value?.route.name === 'federation', | 		active: currentPage.value?.route.name === 'federation', | ||||||
|  | 	}, { | ||||||
|  | 		icon: 'ti ti-clock-play', | ||||||
|  | 		text: i18n.ts.federationJobs, | ||||||
|  | 		to: '/admin/federation-job-queue', | ||||||
|  | 		active: currentPage.value?.route.name === 'federationJobQueue', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'ti ti-clock-play', | 		icon: 'ti ti-clock-play', | ||||||
| 		text: i18n.ts.jobQueue, | 		text: i18n.ts.jobQueue, | ||||||
| 		to: '/admin/queue', | 		to: '/admin/job-queue', | ||||||
| 		active: currentPage.value?.route.name === 'queue', | 		active: currentPage.value?.route.name === 'jobQueue', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'ti ti-cloud', | 		icon: 'ti ti-cloud', | ||||||
| 		text: i18n.ts.files, | 		text: i18n.ts.files, | ||||||
|  | @ -329,6 +334,8 @@ defineExpose({ | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .hiyeyicy { | .hiyeyicy { | ||||||
|  | 	height: 100%; | ||||||
|  | 
 | ||||||
| 	&.wide { | 	&.wide { | ||||||
| 		display: flex; | 		display: flex; | ||||||
| 		margin: 0 auto; | 		margin: 0 auto; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,127 @@ | ||||||
|  | <!-- | ||||||
|  | SPDX-FileCopyrightText: syuilo and misskey-project | ||||||
|  | SPDX-License-Identifier: AGPL-3.0-only | ||||||
|  | --> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  | <canvas ref="chartEl"></canvas> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import { onMounted, useTemplateRef, watch } from 'vue'; | ||||||
|  | import { Chart } from 'chart.js'; | ||||||
|  | import { store } from '@/store.js'; | ||||||
|  | import { useChartTooltip } from '@/use/use-chart-tooltip.js'; | ||||||
|  | import { chartVLine } from '@/utility/chart-vline.js'; | ||||||
|  | import { alpha } from '@/utility/color.js'; | ||||||
|  | import { initChart } from '@/utility/init-chart.js'; | ||||||
|  | 
 | ||||||
|  | initChart(); | ||||||
|  | 
 | ||||||
|  | const props = defineProps<{ | ||||||
|  | 	dataSet: { | ||||||
|  | 		completed: number[]; | ||||||
|  | 		failed: number[]; | ||||||
|  | 	}; | ||||||
|  | 	aspectRatio?: number; | ||||||
|  | }>(); | ||||||
|  | 
 | ||||||
|  | const chartEl = useTemplateRef('chartEl'); | ||||||
|  | 
 | ||||||
|  | const { handler: externalTooltipHandler } = useChartTooltip(); | ||||||
|  | 
 | ||||||
|  | let chartInstance: Chart; | ||||||
|  | 
 | ||||||
|  | function setData() { | ||||||
|  | 	if (chartInstance == null) return; | ||||||
|  | 	chartInstance.data.labels = []; | ||||||
|  | 	for (let i = 0; i < Math.max(props.dataSet.completed.length, props.dataSet.failed.length); i++) { | ||||||
|  | 		chartInstance.data.labels.push(''); | ||||||
|  | 	} | ||||||
|  | 	chartInstance.data.datasets[0].data = props.dataSet.completed; | ||||||
|  | 	chartInstance.data.datasets[1].data = props.dataSet.failed; | ||||||
|  | 	chartInstance.update(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | watch(() => props.dataSet, () => { | ||||||
|  | 	setData(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | onMounted(() => { | ||||||
|  | 	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; | ||||||
|  | 
 | ||||||
|  | 	chartInstance = new Chart(chartEl.value, { | ||||||
|  | 		type: 'line', | ||||||
|  | 		data: { | ||||||
|  | 			labels: [], | ||||||
|  | 			datasets: [{ | ||||||
|  | 				label: 'Completed', | ||||||
|  | 				pointRadius: 0, | ||||||
|  | 				tension: 0.3, | ||||||
|  | 				borderWidth: 2, | ||||||
|  | 				borderJoinStyle: 'round', | ||||||
|  | 				borderColor: '#4caf50', | ||||||
|  | 				backgroundColor: alpha('#4caf50', 0.2), | ||||||
|  | 				fill: true, | ||||||
|  | 				data: [], | ||||||
|  | 			}, { | ||||||
|  | 				label: 'Failed', | ||||||
|  | 				pointRadius: 0, | ||||||
|  | 				tension: 0.3, | ||||||
|  | 				borderWidth: 2, | ||||||
|  | 				borderJoinStyle: 'round', | ||||||
|  | 				borderColor: '#ff0000', | ||||||
|  | 				backgroundColor: alpha('#ff0000', 0.2), | ||||||
|  | 				fill: true, | ||||||
|  | 				data: [], | ||||||
|  | 			}], | ||||||
|  | 		}, | ||||||
|  | 		options: { | ||||||
|  | 			aspectRatio: props.aspectRatio ?? 2.5, | ||||||
|  | 			layout: { | ||||||
|  | 				padding: { | ||||||
|  | 					left: 0, | ||||||
|  | 					right: 0, | ||||||
|  | 					top: 0, | ||||||
|  | 					bottom: 0, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			scales: { | ||||||
|  | 				x: { | ||||||
|  | 					grid: { | ||||||
|  | 						display: true, | ||||||
|  | 					}, | ||||||
|  | 					ticks: { | ||||||
|  | 						display: false, | ||||||
|  | 						maxTicksLimit: 10, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				y: { | ||||||
|  | 					min: 0, | ||||||
|  | 					grid: { | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			interaction: { | ||||||
|  | 				intersect: false, | ||||||
|  | 			}, | ||||||
|  | 			plugins: { | ||||||
|  | 				legend: { | ||||||
|  | 					display: false, | ||||||
|  | 				}, | ||||||
|  | 				tooltip: { | ||||||
|  | 					enabled: false, | ||||||
|  | 					mode: 'index', | ||||||
|  | 					animation: { | ||||||
|  | 						duration: 0, | ||||||
|  | 					}, | ||||||
|  | 					external: externalTooltipHandler, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		plugins: [chartVLine(vLineColor)], | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	setData(); | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | @ -0,0 +1,280 @@ | ||||||
|  | <!-- | ||||||
|  | SPDX-FileCopyrightText: syuilo and misskey-project | ||||||
|  | SPDX-License-Identifier: AGPL-3.0-only | ||||||
|  | --> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  | <MkFolder> | ||||||
|  | 	<template #label> | ||||||
|  | 		<span v-if="job.opts.repeat != null" style="margin-right: 1em;"><repeat></span> | ||||||
|  | 		<span v-else style="margin-right: 1em;">#{{ job.id }}</span> | ||||||
|  | 		<span>{{ job.name }}</span> | ||||||
|  | 	</template> | ||||||
|  | 	<template #suffix> | ||||||
|  | 		<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.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-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.finishedOn != null" style="margin-left: 1em; color: var(--MI_THEME-success)"><i class="ti ti-check"></i></span> | ||||||
|  | 		<span v-else-if="job.delay != null && job.delay != 0" style="margin-left: 1em;"><i class="ti ti-clock"></i></span> | ||||||
|  | 		<span v-else-if="job.processedOn != null" style="margin-left: 1em; color: var(--MI_THEME-success)"><i class="ti ti-player-play"></i></span> | ||||||
|  | 	</template> | ||||||
|  | 	<template #header> | ||||||
|  | 		<MkTabs | ||||||
|  | 			v-model:tab="tab" | ||||||
|  | 			:tabs="[{ | ||||||
|  | 					key: 'info', | ||||||
|  | 					title: 'Info', | ||||||
|  | 					icon: 'ti ti-info-circle', | ||||||
|  | 				}, { | ||||||
|  | 					key: 'timeline', | ||||||
|  | 					title: 'Timeline', | ||||||
|  | 					icon: 'ti ti-timeline-event', | ||||||
|  | 				}, { | ||||||
|  | 					key: 'data', | ||||||
|  | 					title: 'Data', | ||||||
|  | 					icon: 'ti ti-package', | ||||||
|  | 				}, ...(canEdit ? [{ | ||||||
|  | 					key: 'dataEdit', | ||||||
|  | 					title: 'Data (edit)', | ||||||
|  | 					icon: 'ti ti-package', | ||||||
|  | 				}] : []), | ||||||
|  | 				...(job.returnValue != null ? [{ | ||||||
|  | 					key: 'result', | ||||||
|  | 					title: 'Result', | ||||||
|  | 					icon: 'ti ti-check', | ||||||
|  | 				}] : []), | ||||||
|  | 				...(job.stacktrace.length > 0 ? [{ | ||||||
|  | 					key: 'error', | ||||||
|  | 					title: 'Error', | ||||||
|  | 					icon: 'ti ti-alert-triangle', | ||||||
|  | 				}] : []), { | ||||||
|  | 					key: 'logs', | ||||||
|  | 					title: 'Logs', | ||||||
|  | 					icon: 'ti ti-logs', | ||||||
|  | 				}]" | ||||||
|  | 		/> | ||||||
|  | 	</template> | ||||||
|  | 	<template #footer> | ||||||
|  | 		<div class="_buttons"> | ||||||
|  | 			<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="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 danger rounded style="margin-left: auto;" @click="removeJob()"><i class="ti ti-trash"></i> Remove</MkButton> | ||||||
|  | 		</div> | ||||||
|  | 	</template> | ||||||
|  | 
 | ||||||
|  | 	<div v-if="tab === 'info'" class="_gaps_s"> | ||||||
|  | 		<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 12px;"> | ||||||
|  | 			<MkKeyValue> | ||||||
|  | 				<template #key>ID</template> | ||||||
|  | 				<template #value>{{ job.id }}</template> | ||||||
|  | 			</MkKeyValue> | ||||||
|  | 			<MkKeyValue> | ||||||
|  | 				<template #key>Created at</template> | ||||||
|  | 				<template #value><MkTime :time="job.timestamp" mode="detail"/></template> | ||||||
|  | 			</MkKeyValue> | ||||||
|  | 			<MkKeyValue v-if="job.processedOn != null"> | ||||||
|  | 				<template #key>Processed at</template> | ||||||
|  | 				<template #value><MkTime :time="job.processedOn" mode="detail"/></template> | ||||||
|  | 			</MkKeyValue> | ||||||
|  | 			<MkKeyValue v-if="job.finishedOn != null"> | ||||||
|  | 				<template #key>Finished at</template> | ||||||
|  | 				<template #value><MkTime :time="job.finishedOn" mode="detail"/></template> | ||||||
|  | 			</MkKeyValue> | ||||||
|  | 			<MkKeyValue v-if="job.processedOn != null && job.finishedOn != null"> | ||||||
|  | 				<template #key>Spent</template> | ||||||
|  | 				<template #value>{{ job.finishedOn - job.processedOn }}ms</template> | ||||||
|  | 			</MkKeyValue> | ||||||
|  | 			<MkKeyValue v-if="job.failedReason != null"> | ||||||
|  | 				<template #key>Failed reason</template> | ||||||
|  | 				<template #value><i style="color: var(--MI_THEME-error)" class="ti ti-alert-triangle"></i> {{ job.failedReason }}</template> | ||||||
|  | 			</MkKeyValue> | ||||||
|  | 			<MkKeyValue v-if="job.opts.attempts != null && job.opts.attempts > 0"> | ||||||
|  | 				<template #key>Attempts</template> | ||||||
|  | 				<template #value>{{ job.attempts }} of {{ job.opts.attempts }}</template> | ||||||
|  | 			</MkKeyValue> | ||||||
|  | 			<MkKeyValue v-if="job.progress != null && job.progress > 0"> | ||||||
|  | 				<template #key>Progress</template> | ||||||
|  | 				<template #value>{{ Math.floor(job.progress * 100) }}%</template> | ||||||
|  | 			</MkKeyValue> | ||||||
|  | 		</div> | ||||||
|  | 		<MkFolder :withSpacer="false"> | ||||||
|  | 			<template #label>Options</template> | ||||||
|  | 			<MkCode :code="JSON5.stringify(job.opts, null, '\t')" lang="js"/> | ||||||
|  | 		</MkFolder> | ||||||
|  | 	</div> | ||||||
|  | 	<div v-else-if="tab === 'timeline'"> | ||||||
|  | 		<MkTl :events="timeline"> | ||||||
|  | 			<template #left="{ event }"> | ||||||
|  | 				<div> | ||||||
|  | 					<template v-if="event.type === 'finished'"> | ||||||
|  | 						<template v-if="job.isFailed"> | ||||||
|  | 							<b>Finished</b> <i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i> | ||||||
|  | 						</template> | ||||||
|  | 						<template v-else> | ||||||
|  | 							<b>Finished</b> <i class="ti ti-check" style="color: var(--MI_THEME-success);"></i> | ||||||
|  | 						</template> | ||||||
|  | 					</template> | ||||||
|  | 					<template v-else-if="event.type === 'processed'"> | ||||||
|  | 						<b>Processed</b> <i class="ti ti-player-play"></i> | ||||||
|  | 					</template> | ||||||
|  | 					<template v-else-if="event.type === 'attempt'"> | ||||||
|  | 						<b>Attempt #{{ event.attempt }}</b> <i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> | ||||||
|  | 					</template> | ||||||
|  | 					<template v-else-if="event.type === 'created'"> | ||||||
|  | 						<b>Created</b> <i class="ti ti-plus"></i> | ||||||
|  | 					</template> | ||||||
|  | 				</div> | ||||||
|  | 			</template> | ||||||
|  | 			<template #right="{ event, timestamp, delta }"> | ||||||
|  | 				<div style="margin: 8px 0;"> | ||||||
|  | 					<template v-if="event.type === 'attempt'"> | ||||||
|  | 						<div>at ?</div> | ||||||
|  | 					</template> | ||||||
|  | 					<template v-else> | ||||||
|  | 						<div>at <MkTime :time="timestamp" mode="detail"/></div> | ||||||
|  | 						<div style="font-size: 90%; opacity: 0.7;">{{ timestamp }} (+{{ msSMH(delta) }})</div> | ||||||
|  | 					</template> | ||||||
|  | 				</div> | ||||||
|  | 			</template> | ||||||
|  | 		</MkTl> | ||||||
|  | 	</div> | ||||||
|  | 	<div v-else-if="tab === 'data'"> | ||||||
|  | 		<MkCode :code="JSON5.stringify(job.data, null, '\t')" lang="js"/> | ||||||
|  | 	</div> | ||||||
|  | 	<div v-else-if="tab === 'dataEdit'" class="_gaps_s"> | ||||||
|  | 		<MkCodeEditor v-model="editData" lang="json5"></MkCodeEditor> | ||||||
|  | 		<MkButton><i class="ti ti-device-floppy"></i> Update</MkButton> | ||||||
|  | 	</div> | ||||||
|  | 	<div v-else-if="tab === 'result'"> | ||||||
|  | 		<MkCode :code="job.returnValue"/> | ||||||
|  | 	</div> | ||||||
|  | 	<div v-else-if="tab === 'error'" class="_gaps_s"> | ||||||
|  | 		<MkCode v-for="log in job.stacktrace" :code="log" lang="stacktrace"/> | ||||||
|  | 	</div> | ||||||
|  | </MkFolder> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import { ref, computed, watch } from 'vue'; | ||||||
|  | import JSON5 from 'json5'; | ||||||
|  | import type { Ref } from 'vue'; | ||||||
|  | import * as os from '@/os.js'; | ||||||
|  | import { i18n } from '@/i18n.js'; | ||||||
|  | import MkButton from '@/components/MkButton.vue'; | ||||||
|  | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
|  | import MkTabs from '@/components/MkTabs.vue'; | ||||||
|  | import MkFolder from '@/components/MkFolder.vue'; | ||||||
|  | import MkCode from '@/components/MkCode.vue'; | ||||||
|  | import MkKeyValue from '@/components/MkKeyValue.vue'; | ||||||
|  | import MkCodeEditor from '@/components/MkCodeEditor.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'; | ||||||
|  | 
 | ||||||
|  | function msSMH(v: number | null) { | ||||||
|  | 	if (v == null) return 'N/A'; | ||||||
|  | 	if (v === 0) return '0'; | ||||||
|  | 	const suffixes = ['ms', 's', 'm', 'h']; | ||||||
|  | 	const isMinus = v < 0; | ||||||
|  | 	if (isMinus) v = -v; | ||||||
|  | 	const i = Math.floor(Math.log(v) / Math.log(1000)); | ||||||
|  | 	const value = v / Math.pow(1000, i); | ||||||
|  | 	const suffix = suffixes[i]; | ||||||
|  | 	return `${isMinus ? '-' : ''}${value.toFixed(1)}${suffix}`; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const props = defineProps<{ | ||||||
|  | 	job: any; | ||||||
|  | 	queueType: string; | ||||||
|  | }>(); | ||||||
|  | 
 | ||||||
|  | const emit = defineEmits<{ | ||||||
|  | 	(ev: 'needRefresh'): void, | ||||||
|  | }>(); | ||||||
|  | 
 | ||||||
|  | const tab = ref('info'); | ||||||
|  | const editData = ref(JSON5.stringify(props.job.data, null, '\t')); | ||||||
|  | const canEdit = true; | ||||||
|  | const timeline = computed(() => { | ||||||
|  | 	const events = [{ | ||||||
|  | 		id: 'created', | ||||||
|  | 		timestamp: props.job.timestamp, | ||||||
|  | 		data: { | ||||||
|  | 			type: 'created', | ||||||
|  | 		}, | ||||||
|  | 	}]; | ||||||
|  | 	if (props.job.attempts > 1) { | ||||||
|  | 		for (let i = 1; i < props.job.attempts; i++) { | ||||||
|  | 			events.push({ | ||||||
|  | 				id: `attempt-${i}`, | ||||||
|  | 				timestamp: props.job.timestamp + i, | ||||||
|  | 				data: { | ||||||
|  | 					type: 'attempt', | ||||||
|  | 					attempt: i, | ||||||
|  | 				}, | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if (props.job.processedOn != null) { | ||||||
|  | 		events.push({ | ||||||
|  | 			id: 'processed', | ||||||
|  | 			timestamp: props.job.processedOn, | ||||||
|  | 			data: { | ||||||
|  | 				type: 'processed', | ||||||
|  | 			}, | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 	if (props.job.finishedOn != null) { | ||||||
|  | 		events.push({ | ||||||
|  | 			id: 'finished', | ||||||
|  | 			timestamp: props.job.finishedOn, | ||||||
|  | 			data: { | ||||||
|  | 				type: 'finished', | ||||||
|  | 			}, | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 	return events; | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | async function promoteJob() { | ||||||
|  | 	const { canceled } = await os.confirm({ | ||||||
|  | 		type: 'warning', | ||||||
|  | 		title: i18n.ts.areYouSure, | ||||||
|  | 	}); | ||||||
|  | 	if (canceled) return; | ||||||
|  | 
 | ||||||
|  | 	os.apiWithDialog('admin/queue/retry-job', { queue: props.queueType, jobId: props.job.id }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function removeJob() { | ||||||
|  | 	const { canceled } = await os.confirm({ | ||||||
|  | 		type: 'warning', | ||||||
|  | 		title: i18n.ts.areYouSure, | ||||||
|  | 	}); | ||||||
|  | 	if (canceled) return; | ||||||
|  | 
 | ||||||
|  | 	os.apiWithDialog('admin/queue/remove-job', { queue: props.queueType, jobId: props.job.id }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function moveJob() { | ||||||
|  | 	// TODO | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function refresh() { | ||||||
|  | 	emit('needRefresh'); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function copyRaw() { | ||||||
|  | 	const raw = JSON.stringify(props.job, null, '\t'); | ||||||
|  | 	copyToClipboard(raw); | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" module> | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
|  | @ -0,0 +1,370 @@ | ||||||
|  | <!-- | ||||||
|  | SPDX-FileCopyrightText: syuilo and misskey-project | ||||||
|  | SPDX-License-Identifier: AGPL-3.0-only | ||||||
|  | --> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  | <PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"> | ||||||
|  | 	<MkSpacer> | ||||||
|  | 		<div v-if="tab === '-'" class="_gaps"> | ||||||
|  | 			<div :class="$style.queues"> | ||||||
|  | 				<div v-for="q in queueInfos" :key="q.name" :class="$style.queue" @click="tab = q.name"> | ||||||
|  | 					<div style="display: flex; align-items: center; font-weight: bold;"><i class="ti ti-http-que" style="margin-right: 0.5em;"></i>{{ q.name }}<i v-if="!q.isPaused" style="color: var(--MI_THEME-success); margin-left: auto;" class="ti ti-player-play"></i></div> | ||||||
|  | 					<div :class="$style.queueCounts"> | ||||||
|  | 						<MkKeyValue> | ||||||
|  | 							<template #key>Active</template> | ||||||
|  | 							<template #value>{{ kmg(q.counts.active, 2) }}</template> | ||||||
|  | 						</MkKeyValue> | ||||||
|  | 						<MkKeyValue> | ||||||
|  | 							<template #key>Delayed</template> | ||||||
|  | 							<template #value>{{ kmg(q.counts.delayed, 2) }}</template> | ||||||
|  | 						</MkKeyValue> | ||||||
|  | 						<MkKeyValue> | ||||||
|  | 							<template #key>Waiting</template> | ||||||
|  | 							<template #value>{{ kmg(q.counts.waiting, 2) }}</template> | ||||||
|  | 						</MkKeyValue> | ||||||
|  | 					</div> | ||||||
|  | 					<XChart :dataSet="{ completed: q.metrics.completed.data, failed: q.metrics.failed.data }"/> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 		<div v-else-if="queueInfo" class="_gaps"> | ||||||
|  | 			<MkFolder :defaultOpen="true"> | ||||||
|  | 				<template #label>Overview: {{ tab }}</template> | ||||||
|  | 				<template #icon><i class="ti ti-http-que"></i></template> | ||||||
|  | 				<template #suffix>#{{ queueInfo.db.processId }}:{{ queueInfo.db.port }} / {{ queueInfo.db.runId }}</template> | ||||||
|  | 				<template #caption>{{ queueInfo.qualifiedName }}</template> | ||||||
|  | 				<template #footer> | ||||||
|  | 					<div class="_buttons"> | ||||||
|  | 						<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 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 rounded danger @click="clearQueue"><i class="ti ti-trash"></i> Empty queue</MkButton> | ||||||
|  | 					</div> | ||||||
|  | 				</template> | ||||||
|  | 
 | ||||||
|  | 				<div class="_gaps"> | ||||||
|  | 					<XChart :dataSet="{ completed: queueInfo.metrics.completed.data, failed: queueInfo.metrics.failed.data }" :aspectRatio="5"/> | ||||||
|  | 					<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px;"> | ||||||
|  | 						<MkKeyValue> | ||||||
|  | 							<template #key>Active</template> | ||||||
|  | 							<template #value>{{ kmg(queueInfo.counts.active, 2) }}</template> | ||||||
|  | 						</MkKeyValue> | ||||||
|  | 						<MkKeyValue> | ||||||
|  | 							<template #key>Delayed</template> | ||||||
|  | 							<template #value>{{ kmg(queueInfo.counts.delayed, 2) }}</template> | ||||||
|  | 						</MkKeyValue> | ||||||
|  | 						<MkKeyValue> | ||||||
|  | 							<template #key>Waiting</template> | ||||||
|  | 							<template #value>{{ kmg(queueInfo.counts.waiting, 2) }}</template> | ||||||
|  | 						</MkKeyValue> | ||||||
|  | 					</div> | ||||||
|  | 					<hr> | ||||||
|  | 					<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px;"> | ||||||
|  | 						<MkKeyValue> | ||||||
|  | 							<template #key>Clients: Connected</template> | ||||||
|  | 							<template #value>{{ queueInfo.db.clients.connected }}</template> | ||||||
|  | 						</MkKeyValue> | ||||||
|  | 						<MkKeyValue> | ||||||
|  | 							<template #key>Clients: Blocked</template> | ||||||
|  | 							<template #value>{{ queueInfo.db.clients.blocked }}</template> | ||||||
|  | 						</MkKeyValue> | ||||||
|  | 						<MkKeyValue> | ||||||
|  | 							<template #key>Memory: Peak</template> | ||||||
|  | 							<template #value>{{ bytes(queueInfo.db.memory.peak, 1) }}</template> | ||||||
|  | 						</MkKeyValue> | ||||||
|  | 						<MkKeyValue> | ||||||
|  | 							<template #key>Memory: Total</template> | ||||||
|  | 							<template #value>{{ bytes(queueInfo.db.memory.total, 1) }}</template> | ||||||
|  | 						</MkKeyValue> | ||||||
|  | 						<MkKeyValue> | ||||||
|  | 							<template #key>Memory: Used</template> | ||||||
|  | 							<template #value>{{ bytes(queueInfo.db.memory.used, 1) }}</template> | ||||||
|  | 						</MkKeyValue> | ||||||
|  | 						<MkKeyValue> | ||||||
|  | 							<template #key>Uptime</template> | ||||||
|  | 							<template #value>{{ queueInfo.db.uptime }}</template> | ||||||
|  | 						</MkKeyValue> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 			</MkFolder> | ||||||
|  | 
 | ||||||
|  | 			<MkFolder :defaultOpen="true" :withSpacer="false"> | ||||||
|  | 				<template #label>Jobs: {{ tab }}</template> | ||||||
|  | 				<template #icon><i class="ti ti-list-check"></i></template> | ||||||
|  | 				<template #suffix><A:{{ kmg(queueInfo.counts.active, 2) }}> <D:{{ kmg(queueInfo.counts.delayed, 2) }}> <W:{{ kmg(queueInfo.counts.waiting, 2) }}></template> | ||||||
|  | 				<template #header> | ||||||
|  | 					<MkTabs | ||||||
|  | 						v-model:tab="jobState" | ||||||
|  | 						:class="$style.jobsTabs" :tabs="[{ | ||||||
|  | 							key: 'all', | ||||||
|  | 							title: 'All', | ||||||
|  | 							icon: 'ti ti-code-asterisk', | ||||||
|  | 						}, { | ||||||
|  | 							key: 'latest', | ||||||
|  | 							title: 'Latest', | ||||||
|  | 							icon: 'ti ti-logs', | ||||||
|  | 						}, { | ||||||
|  | 							key: 'completed', | ||||||
|  | 							title: 'Completed', | ||||||
|  | 							icon: 'ti ti-check', | ||||||
|  | 						}, { | ||||||
|  | 							key: 'failed', | ||||||
|  | 							title: 'Failed', | ||||||
|  | 							icon: 'ti ti-circle-x', | ||||||
|  | 						}, { | ||||||
|  | 							key: 'active', | ||||||
|  | 							title: 'Active', | ||||||
|  | 							icon: 'ti ti-player-play', | ||||||
|  | 						}, { | ||||||
|  | 							key: 'delayed', | ||||||
|  | 							title: 'Delayed', | ||||||
|  | 							icon: 'ti ti-clock', | ||||||
|  | 						}, { | ||||||
|  | 							key: 'wait', | ||||||
|  | 							title: 'Waiting', | ||||||
|  | 							icon: 'ti ti-hourglass-high', | ||||||
|  | 						}, { | ||||||
|  | 							key: 'paused', | ||||||
|  | 							title: 'Paused', | ||||||
|  | 							icon: 'ti ti-player-pause', | ||||||
|  | 						}]" | ||||||
|  | 					/> | ||||||
|  | 				</template> | ||||||
|  | 				<template #footer> | ||||||
|  | 					<div class="_buttons"> | ||||||
|  | 						<MkButton rounded @click="fetchJobs()"><i class="ti ti-reload"></i> Refresh view</MkButton> | ||||||
|  | 						<MkButton rounded danger style="margin-left: auto;" @click="removeJobs"><i class="ti ti-trash"></i> Remove jobs</MkButton> | ||||||
|  | 					</div> | ||||||
|  | 				</template> | ||||||
|  | 
 | ||||||
|  | 				<MkSpacer> | ||||||
|  | 					<MkInput | ||||||
|  | 						v-model="searchQuery" | ||||||
|  | 						:placeholder="i18n.ts.search" | ||||||
|  | 						type="search" | ||||||
|  | 						style="margin-bottom: 16px;" | ||||||
|  | 					> | ||||||
|  | 						<template #prefix><i class="ti ti-search"></i></template> | ||||||
|  | 					</MkInput> | ||||||
|  | 
 | ||||||
|  | 					<MkLoading v-if="jobsFetching"/> | ||||||
|  | 					<MkTl | ||||||
|  | 						v-else | ||||||
|  | 						:events="jobs.map((job) => ({ | ||||||
|  | 							id: job.id, | ||||||
|  | 							timestamp: job.finishedOn ?? job.processedOn ?? job.timestamp, | ||||||
|  | 							data: job, | ||||||
|  | 						}))" | ||||||
|  | 						class="_monospace" | ||||||
|  | 					> | ||||||
|  | 						<template #right="{ event: job }"> | ||||||
|  | 							<XJob :job="job" :queueType="tab" style="margin: 4px 0;" @needRefresh="refreshJob(job.id)"/> | ||||||
|  | 						</template> | ||||||
|  | 					</MkTl> | ||||||
|  | 				</MkSpacer> | ||||||
|  | 			</MkFolder> | ||||||
|  | 		</div> | ||||||
|  | 	</MkSpacer> | ||||||
|  | </PageWithHeader> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import { ref, computed, watch } from 'vue'; | ||||||
|  | import JSON5 from 'json5'; | ||||||
|  | import { debounce } from 'throttle-debounce'; | ||||||
|  | import { useInterval } from '@@/js/use-interval.js'; | ||||||
|  | import XChart from './job-queue.chart.vue'; | ||||||
|  | import XJob from './job-queue.job.vue'; | ||||||
|  | import type { Ref } from 'vue'; | ||||||
|  | import * as os from '@/os.js'; | ||||||
|  | import { i18n } from '@/i18n.js'; | ||||||
|  | import { definePage } from '@/page.js'; | ||||||
|  | import MkButton from '@/components/MkButton.vue'; | ||||||
|  | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
|  | import MkTabs from '@/components/MkTabs.vue'; | ||||||
|  | import MkFolder from '@/components/MkFolder.vue'; | ||||||
|  | import MkCode from '@/components/MkCode.vue'; | ||||||
|  | import MkKeyValue from '@/components/MkKeyValue.vue'; | ||||||
|  | import MkTl from '@/components/MkTl.vue'; | ||||||
|  | import kmg from '@/filters/kmg.js'; | ||||||
|  | import MkInput from '@/components/MkInput.vue'; | ||||||
|  | import bytes from '@/filters/bytes.js'; | ||||||
|  | import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; | ||||||
|  | 
 | ||||||
|  | const QUEUE_TYPES = [ | ||||||
|  | 	'system', | ||||||
|  | 	'endedPollNotification', | ||||||
|  | 	'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 queueInfos = ref([]); | ||||||
|  | const queueInfo = ref(); | ||||||
|  | const searchQuery = ref(''); | ||||||
|  | 
 | ||||||
|  | async function fetchQueues() { | ||||||
|  | 	if (tab.value !== '-') return; | ||||||
|  | 	queueInfos.value = await misskeyApi('admin/queue/queues'); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function fetchCurrentQueue() { | ||||||
|  | 	if (tab.value === '-') return; | ||||||
|  | 	queueInfo.value = await misskeyApi('admin/queue/queue-stats', { queue: tab.value }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function fetchJobs() { | ||||||
|  | 	jobsFetching.value = true; | ||||||
|  | 	const state = jobState.value; | ||||||
|  | 	jobs.value = await misskeyApi('admin/queue/jobs', { | ||||||
|  | 		queue: tab.value, | ||||||
|  | 		state: state === 'all' ? ['completed', 'failed', 'active', 'delayed', 'wait'] : state === 'latest' ? ['completed', 'failed'] : [state], | ||||||
|  | 		search: searchQuery.value.trim() === '' ? undefined : searchQuery.value, | ||||||
|  | 	}).then(res => { | ||||||
|  | 		if (state === 'all') { | ||||||
|  | 			res.sort((a, b) => (a.processedOn ?? a.timestamp) > (b.processedOn ?? b.timestamp) ? -1 : 1); | ||||||
|  | 		} else if (state === 'latest') { | ||||||
|  | 			res.sort((a, b) => a.processedOn > b.processedOn ? -1 : 1); | ||||||
|  | 		} else if (state === 'delayed') { | ||||||
|  | 			res.sort((a, b) => (a.processedOn ?? a.timestamp) > (b.processedOn ?? b.timestamp) ? -1 : 1); | ||||||
|  | 		} | ||||||
|  | 		return res; | ||||||
|  | 	}); | ||||||
|  | 	jobsFetching.value = false; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | watch([tab], async () => { | ||||||
|  | 	if (tab.value === '-') { | ||||||
|  | 		fetchQueues(); | ||||||
|  | 	} else { | ||||||
|  | 		fetchCurrentQueue(); | ||||||
|  | 		fetchJobs(); | ||||||
|  | 	} | ||||||
|  | }, { immediate: true }); | ||||||
|  | 
 | ||||||
|  | watch([jobState], () => { | ||||||
|  | 	fetchJobs(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const search = debounce(1000, () => { | ||||||
|  | 	fetchJobs(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | watch([searchQuery], () => { | ||||||
|  | 	search(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | useInterval(() => { | ||||||
|  | 	if (tab.value === '-') { | ||||||
|  | 		fetchQueues(); | ||||||
|  | 	} else { | ||||||
|  | 		fetchCurrentQueue(); | ||||||
|  | 	} | ||||||
|  | }, 1000 * 10, { | ||||||
|  | 	immediate: false, | ||||||
|  | 	afterMounted: true, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | async function clearQueue() { | ||||||
|  | 	const { canceled } = await os.confirm({ | ||||||
|  | 		type: 'warning', | ||||||
|  | 		title: i18n.ts.areYouSure, | ||||||
|  | 	}); | ||||||
|  | 	if (canceled) return; | ||||||
|  | 
 | ||||||
|  | 	os.apiWithDialog('admin/queue/clear', { queue: tab.value, state: '*' }); | ||||||
|  | 
 | ||||||
|  | 	fetchCurrentQueue(); | ||||||
|  | 	fetchJobs(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function promoteAllJobs() { | ||||||
|  | 	const { canceled } = await os.confirm({ | ||||||
|  | 		type: 'warning', | ||||||
|  | 		title: i18n.ts.areYouSure, | ||||||
|  | 	}); | ||||||
|  | 	if (canceled) return; | ||||||
|  | 
 | ||||||
|  | 	os.apiWithDialog('admin/queue/promote-jobs', { queue: tab.value }); | ||||||
|  | 
 | ||||||
|  | 	fetchCurrentQueue(); | ||||||
|  | 	fetchJobs(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function removeJobs() { | ||||||
|  | 	const { canceled } = await os.confirm({ | ||||||
|  | 		type: 'warning', | ||||||
|  | 		title: i18n.ts.areYouSure, | ||||||
|  | 	}); | ||||||
|  | 	if (canceled) return; | ||||||
|  | 
 | ||||||
|  | 	os.apiWithDialog('admin/queue/clear', { queue: tab.value, state: jobState.value }); | ||||||
|  | 
 | ||||||
|  | 	fetchCurrentQueue(); | ||||||
|  | 	fetchJobs(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function refreshJob(jobId: string) { | ||||||
|  | 	const newJob = await misskeyApi('admin/queue/show-job', { queue: tab.value, jobId }); | ||||||
|  | 	const index = jobs.value.findIndex((job) => job.id === jobId); | ||||||
|  | 	if (index !== -1) { | ||||||
|  | 		jobs.value[index] = newJob; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const headerActions = computed(() => []); | ||||||
|  | 
 | ||||||
|  | const headerTabs = computed(() => | ||||||
|  | 	[{ | ||||||
|  | 		key: '-', | ||||||
|  | 		title: i18n.ts.overview, | ||||||
|  | 		icon: 'ti ti-dashboard', | ||||||
|  | 	}].concat(QUEUE_TYPES.map((t) => ({ | ||||||
|  | 		key: t, | ||||||
|  | 		title: t, | ||||||
|  | 	}))), | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | definePage(() => ({ | ||||||
|  | 	title: i18n.ts.jobQueue, | ||||||
|  | 	icon: 'ti ti-clock-play', | ||||||
|  | 	needWideArea: true, | ||||||
|  | })); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" module> | ||||||
|  | .queues { | ||||||
|  | 	display: grid; | ||||||
|  | 	grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); | ||||||
|  | 	gap: 14px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .queue { | ||||||
|  | 	padding: 14px 18px; | ||||||
|  | 	background-color: var(--MI_THEME-panel); | ||||||
|  | 	border-radius: 8px; | ||||||
|  | 	cursor: pointer; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .queueCounts { | ||||||
|  | 	display: grid; | ||||||
|  | 	grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); | ||||||
|  | 	gap: 8px; | ||||||
|  | 	font-size: 85%; | ||||||
|  | 	margin: 6px 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .jobsTabs { | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -392,9 +392,13 @@ export const ROUTE_DEF = [{ | ||||||
| 		name: 'avatarDecorations', | 		name: 'avatarDecorations', | ||||||
| 		component: page(() => import('@/pages/avatar-decorations.vue')), | 		component: page(() => import('@/pages/avatar-decorations.vue')), | ||||||
| 	}, { | 	}, { | ||||||
| 		path: '/queue', | 		path: '/federation-job-queue', | ||||||
| 		name: 'queue', | 		name: 'federationJobQueue', | ||||||
| 		component: page(() => import('@/pages/admin/queue.vue')), | 		component: page(() => import('@/pages/admin/federation-job-queue.vue')), | ||||||
|  | 	}, { | ||||||
|  | 		path: '/job-queue', | ||||||
|  | 		name: 'jobQueue', | ||||||
|  | 		component: page(() => import('@/pages/admin/job-queue.vue')), | ||||||
| 	}, { | 	}, { | ||||||
| 		path: '/files', | 		path: '/files', | ||||||
| 		name: 'files', | 		name: 'files', | ||||||
|  |  | ||||||
|  | @ -267,7 +267,22 @@ type AdminQueueDeliverDelayedResponse = operations['admin___queue___deliver-dela | ||||||
| type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed']['responses']['200']['content']['application/json']; | type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed']['responses']['200']['content']['application/json']; | ||||||
| 
 | 
 | ||||||
| // @public (undocumented) | // @public (undocumented) | ||||||
| type AdminQueuePromoteRequest = operations['admin___queue___promote']['requestBody']['content']['application/json']; | type AdminQueueJobsRequest = operations['admin___queue___jobs']['requestBody']['content']['application/json']; | ||||||
|  | 
 | ||||||
|  | // @public (undocumented) | ||||||
|  | type AdminQueuePromoteJobsRequest = operations['admin___queue___promote-jobs']['requestBody']['content']['application/json']; | ||||||
|  | 
 | ||||||
|  | // @public (undocumented) | ||||||
|  | type AdminQueueQueueStatsRequest = operations['admin___queue___queue-stats']['requestBody']['content']['application/json']; | ||||||
|  | 
 | ||||||
|  | // @public (undocumented) | ||||||
|  | type AdminQueueRemoveJobRequest = operations['admin___queue___remove-job']['requestBody']['content']['application/json']; | ||||||
|  | 
 | ||||||
|  | // @public (undocumented) | ||||||
|  | type AdminQueueRetryJobRequest = operations['admin___queue___retry-job']['requestBody']['content']['application/json']; | ||||||
|  | 
 | ||||||
|  | // @public (undocumented) | ||||||
|  | type AdminQueueShowJobRequest = operations['admin___queue___show-job']['requestBody']['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']; | ||||||
|  | @ -1531,7 +1546,12 @@ declare namespace entities { | ||||||
|         AdminQueueClearRequest, |         AdminQueueClearRequest, | ||||||
|         AdminQueueDeliverDelayedResponse, |         AdminQueueDeliverDelayedResponse, | ||||||
|         AdminQueueInboxDelayedResponse, |         AdminQueueInboxDelayedResponse, | ||||||
|         AdminQueuePromoteRequest, |         AdminQueueJobsRequest, | ||||||
|  |         AdminQueuePromoteJobsRequest, | ||||||
|  |         AdminQueueQueueStatsRequest, | ||||||
|  |         AdminQueueRemoveJobRequest, | ||||||
|  |         AdminQueueRetryJobRequest, | ||||||
|  |         AdminQueueShowJobRequest, | ||||||
|         AdminQueueStatsResponse, |         AdminQueueStatsResponse, | ||||||
|         AdminRelaysAddRequest, |         AdminRelaysAddRequest, | ||||||
|         AdminRelaysAddResponse, |         AdminRelaysAddResponse, | ||||||
|  |  | ||||||
|  | @ -636,12 +636,78 @@ declare module '../api.js' { | ||||||
|       credential?: string | null, |       credential?: string | null, | ||||||
|     ): Promise<SwitchCaseResponseType<E, P>>; |     ): Promise<SwitchCaseResponseType<E, P>>; | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * No description provided. | ||||||
|  |      *  | ||||||
|  |      * **Credential required**: *Yes* / **Permission**: *read:admin:queue* | ||||||
|  |      */ | ||||||
|  |     request<E extends 'admin/queue/jobs', P extends Endpoints[E]['req']>( | ||||||
|  |       endpoint: E, | ||||||
|  |       params: P, | ||||||
|  |       credential?: string | null, | ||||||
|  |     ): Promise<SwitchCaseResponseType<E, P>>; | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * No description provided. |      * No description provided. | ||||||
|      *  |      *  | ||||||
|      * **Credential required**: *Yes* / **Permission**: *write:admin:queue* |      * **Credential required**: *Yes* / **Permission**: *write:admin:queue* | ||||||
|      */ |      */ | ||||||
|     request<E extends 'admin/queue/promote', P extends Endpoints[E]['req']>( |     request<E extends 'admin/queue/promote-jobs', P extends Endpoints[E]['req']>( | ||||||
|  |       endpoint: E, | ||||||
|  |       params: P, | ||||||
|  |       credential?: string | null, | ||||||
|  |     ): Promise<SwitchCaseResponseType<E, P>>; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * No description provided. | ||||||
|  |      *  | ||||||
|  |      * **Credential required**: *Yes* / **Permission**: *read:admin:queue* | ||||||
|  |      */ | ||||||
|  |     request<E extends 'admin/queue/queue-stats', P extends Endpoints[E]['req']>( | ||||||
|  |       endpoint: E, | ||||||
|  |       params: P, | ||||||
|  |       credential?: string | null, | ||||||
|  |     ): Promise<SwitchCaseResponseType<E, P>>; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * No description provided. | ||||||
|  |      *  | ||||||
|  |      * **Credential required**: *Yes* / **Permission**: *read:admin:queue* | ||||||
|  |      */ | ||||||
|  |     request<E extends 'admin/queue/queues', P extends Endpoints[E]['req']>( | ||||||
|  |       endpoint: E, | ||||||
|  |       params: P, | ||||||
|  |       credential?: string | null, | ||||||
|  |     ): Promise<SwitchCaseResponseType<E, P>>; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * No description provided. | ||||||
|  |      *  | ||||||
|  |      * **Credential required**: *Yes* / **Permission**: *write:admin:queue* | ||||||
|  |      */ | ||||||
|  |     request<E extends 'admin/queue/remove-job', P extends Endpoints[E]['req']>( | ||||||
|  |       endpoint: E, | ||||||
|  |       params: P, | ||||||
|  |       credential?: string | null, | ||||||
|  |     ): Promise<SwitchCaseResponseType<E, P>>; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * No description provided. | ||||||
|  |      *  | ||||||
|  |      * **Credential required**: *Yes* / **Permission**: *write:admin:queue* | ||||||
|  |      */ | ||||||
|  |     request<E extends 'admin/queue/retry-job', P extends Endpoints[E]['req']>( | ||||||
|  |       endpoint: E, | ||||||
|  |       params: P, | ||||||
|  |       credential?: string | null, | ||||||
|  |     ): Promise<SwitchCaseResponseType<E, P>>; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * No description provided. | ||||||
|  |      *  | ||||||
|  |      * **Credential required**: *Yes* / **Permission**: *read:admin:queue* | ||||||
|  |      */ | ||||||
|  |     request<E extends 'admin/queue/show-job', P extends Endpoints[E]['req']>( | ||||||
|       endpoint: E, |       endpoint: E, | ||||||
|       params: P, |       params: P, | ||||||
|       credential?: string | null, |       credential?: string | null, | ||||||
|  |  | ||||||
|  | @ -78,7 +78,12 @@ import type { | ||||||
| 	AdminQueueClearRequest, | 	AdminQueueClearRequest, | ||||||
| 	AdminQueueDeliverDelayedResponse, | 	AdminQueueDeliverDelayedResponse, | ||||||
| 	AdminQueueInboxDelayedResponse, | 	AdminQueueInboxDelayedResponse, | ||||||
| 	AdminQueuePromoteRequest, | 	AdminQueueJobsRequest, | ||||||
|  | 	AdminQueuePromoteJobsRequest, | ||||||
|  | 	AdminQueueQueueStatsRequest, | ||||||
|  | 	AdminQueueRemoveJobRequest, | ||||||
|  | 	AdminQueueRetryJobRequest, | ||||||
|  | 	AdminQueueShowJobRequest, | ||||||
| 	AdminQueueStatsResponse, | 	AdminQueueStatsResponse, | ||||||
| 	AdminRelaysAddRequest, | 	AdminRelaysAddRequest, | ||||||
| 	AdminRelaysAddResponse, | 	AdminRelaysAddResponse, | ||||||
|  | @ -694,7 +699,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/promote': { req: AdminQueuePromoteRequest; res: EmptyResponse }; | 	'admin/queue/jobs': { req: AdminQueueJobsRequest; res: EmptyResponse }; | ||||||
|  | 	'admin/queue/promote-jobs': { req: AdminQueuePromoteJobsRequest; res: EmptyResponse }; | ||||||
|  | 	'admin/queue/queue-stats': { req: AdminQueueQueueStatsRequest; res: EmptyResponse }; | ||||||
|  | 	'admin/queue/queues': { req: EmptyRequest; res: EmptyResponse }; | ||||||
|  | 	'admin/queue/remove-job': { req: AdminQueueRemoveJobRequest; res: EmptyResponse }; | ||||||
|  | 	'admin/queue/retry-job': { req: AdminQueueRetryJobRequest; res: EmptyResponse }; | ||||||
|  | 	'admin/queue/show-job': { req: AdminQueueShowJobRequest; res: EmptyResponse }; | ||||||
| 	'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 }; | ||||||
|  |  | ||||||
|  | @ -81,7 +81,12 @@ export type AdminPromoCreateRequest = operations['admin___promo___create']['requ | ||||||
| export type AdminQueueClearRequest = operations['admin___queue___clear']['requestBody']['content']['application/json']; | export type AdminQueueClearRequest = operations['admin___queue___clear']['requestBody']['content']['application/json']; | ||||||
| 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 AdminQueuePromoteRequest = operations['admin___queue___promote']['requestBody']['content']['application/json']; | export type AdminQueueJobsRequest = operations['admin___queue___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 AdminQueueRemoveJobRequest = operations['admin___queue___remove-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 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']; | ||||||
|  |  | ||||||
|  | @ -531,14 +531,68 @@ export type paths = { | ||||||
|      */ |      */ | ||||||
|     post: operations['admin___queue___inbox-delayed']; |     post: operations['admin___queue___inbox-delayed']; | ||||||
|   }; |   }; | ||||||
|   '/admin/queue/promote': { |   '/admin/queue/jobs': { | ||||||
|     /** |     /** | ||||||
|      * admin/queue/promote |      * admin/queue/jobs | ||||||
|  |      * @description No description provided. | ||||||
|  |      * | ||||||
|  |      * **Credential required**: *Yes* / **Permission**: *read:admin:queue* | ||||||
|  |      */ | ||||||
|  |     post: operations['admin___queue___jobs']; | ||||||
|  |   }; | ||||||
|  |   '/admin/queue/promote-jobs': { | ||||||
|  |     /** | ||||||
|  |      * admin/queue/promote-jobs | ||||||
|      * @description No description provided. |      * @description No description provided. | ||||||
|      * |      * | ||||||
|      * **Credential required**: *Yes* / **Permission**: *write:admin:queue* |      * **Credential required**: *Yes* / **Permission**: *write:admin:queue* | ||||||
|      */ |      */ | ||||||
|     post: operations['admin___queue___promote']; |     post: operations['admin___queue___promote-jobs']; | ||||||
|  |   }; | ||||||
|  |   '/admin/queue/queue-stats': { | ||||||
|  |     /** | ||||||
|  |      * admin/queue/queue-stats | ||||||
|  |      * @description No description provided. | ||||||
|  |      * | ||||||
|  |      * **Credential required**: *Yes* / **Permission**: *read:admin:queue* | ||||||
|  |      */ | ||||||
|  |     post: operations['admin___queue___queue-stats']; | ||||||
|  |   }; | ||||||
|  |   '/admin/queue/queues': { | ||||||
|  |     /** | ||||||
|  |      * admin/queue/queues | ||||||
|  |      * @description No description provided. | ||||||
|  |      * | ||||||
|  |      * **Credential required**: *Yes* / **Permission**: *read:admin:queue* | ||||||
|  |      */ | ||||||
|  |     post: operations['admin___queue___queues']; | ||||||
|  |   }; | ||||||
|  |   '/admin/queue/remove-job': { | ||||||
|  |     /** | ||||||
|  |      * admin/queue/remove-job | ||||||
|  |      * @description No description provided. | ||||||
|  |      * | ||||||
|  |      * **Credential required**: *Yes* / **Permission**: *write:admin:queue* | ||||||
|  |      */ | ||||||
|  |     post: operations['admin___queue___remove-job']; | ||||||
|  |   }; | ||||||
|  |   '/admin/queue/retry-job': { | ||||||
|  |     /** | ||||||
|  |      * admin/queue/retry-job | ||||||
|  |      * @description No description provided. | ||||||
|  |      * | ||||||
|  |      * **Credential required**: *Yes* / **Permission**: *write:admin:queue* | ||||||
|  |      */ | ||||||
|  |     post: operations['admin___queue___retry-job']; | ||||||
|  |   }; | ||||||
|  |   '/admin/queue/show-job': { | ||||||
|  |     /** | ||||||
|  |      * admin/queue/show-job | ||||||
|  |      * @description No description provided. | ||||||
|  |      * | ||||||
|  |      * **Credential required**: *Yes* / **Permission**: *read:admin:queue* | ||||||
|  |      */ | ||||||
|  |     post: operations['admin___queue___show-job']; | ||||||
|   }; |   }; | ||||||
|   '/admin/queue/stats': { |   '/admin/queue/stats': { | ||||||
|     /** |     /** | ||||||
|  | @ -8809,9 +8863,9 @@ export type operations = { | ||||||
|       content: { |       content: { | ||||||
|         'application/json': { |         'application/json': { | ||||||
|           /** @enum {string} */ |           /** @enum {string} */ | ||||||
|           type: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; |           queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; | ||||||
|           /** @enum {string} */ |           /** @enum {string} */ | ||||||
|           state: '*' | 'wait' | 'delayed'; |           state: '*' | 'completed' | 'wait' | 'active' | 'paused' | 'prioritized' | 'delayed' | 'failed'; | ||||||
|         }; |         }; | ||||||
|       }; |       }; | ||||||
|     }; |     }; | ||||||
|  | @ -8945,17 +8999,326 @@ export type operations = { | ||||||
|     }; |     }; | ||||||
|   }; |   }; | ||||||
|   /** |   /** | ||||||
|    * admin/queue/promote |    * admin/queue/jobs | ||||||
|    * @description No description provided. |    * @description No description provided. | ||||||
|    * |    * | ||||||
|    * **Credential required**: *Yes* / **Permission**: *write:admin:queue* |    * **Credential required**: *Yes* / **Permission**: *read:admin:queue* | ||||||
|    */ |    */ | ||||||
|   admin___queue___promote: { |   admin___queue___jobs: { | ||||||
|     requestBody: { |     requestBody: { | ||||||
|       content: { |       content: { | ||||||
|         'application/json': { |         'application/json': { | ||||||
|           /** @enum {string} */ |           /** @enum {string} */ | ||||||
|           type: 'deliver' | 'inbox'; |           queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; | ||||||
|  |           state: ('active' | 'wait' | 'delayed' | 'completed' | 'failed')[]; | ||||||
|  |           search?: string; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|  |     responses: { | ||||||
|  |       /** @description OK (without any results) */ | ||||||
|  |       204: { | ||||||
|  |         content: never; | ||||||
|  |       }; | ||||||
|  |       /** @description Client error */ | ||||||
|  |       400: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description Authentication error */ | ||||||
|  |       401: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description Forbidden error */ | ||||||
|  |       403: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description I'm Ai */ | ||||||
|  |       418: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description Internal server error */ | ||||||
|  |       500: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
|  |   /** | ||||||
|  |    * admin/queue/promote-jobs | ||||||
|  |    * @description No description provided. | ||||||
|  |    * | ||||||
|  |    * **Credential required**: *Yes* / **Permission**: *write:admin:queue* | ||||||
|  |    */ | ||||||
|  |   'admin___queue___promote-jobs': { | ||||||
|  |     requestBody: { | ||||||
|  |       content: { | ||||||
|  |         'application/json': { | ||||||
|  |           /** @enum {string} */ | ||||||
|  |           queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|  |     responses: { | ||||||
|  |       /** @description OK (without any results) */ | ||||||
|  |       204: { | ||||||
|  |         content: never; | ||||||
|  |       }; | ||||||
|  |       /** @description Client error */ | ||||||
|  |       400: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description Authentication error */ | ||||||
|  |       401: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description Forbidden error */ | ||||||
|  |       403: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description I'm Ai */ | ||||||
|  |       418: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description Internal server error */ | ||||||
|  |       500: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
|  |   /** | ||||||
|  |    * admin/queue/queue-stats | ||||||
|  |    * @description No description provided. | ||||||
|  |    * | ||||||
|  |    * **Credential required**: *Yes* / **Permission**: *read:admin:queue* | ||||||
|  |    */ | ||||||
|  |   'admin___queue___queue-stats': { | ||||||
|  |     requestBody: { | ||||||
|  |       content: { | ||||||
|  |         'application/json': { | ||||||
|  |           /** @enum {string} */ | ||||||
|  |           queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|  |     responses: { | ||||||
|  |       /** @description OK (without any results) */ | ||||||
|  |       204: { | ||||||
|  |         content: never; | ||||||
|  |       }; | ||||||
|  |       /** @description Client error */ | ||||||
|  |       400: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description Authentication error */ | ||||||
|  |       401: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description Forbidden error */ | ||||||
|  |       403: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description I'm Ai */ | ||||||
|  |       418: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description Internal server error */ | ||||||
|  |       500: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
|  |   /** | ||||||
|  |    * admin/queue/queues | ||||||
|  |    * @description No description provided. | ||||||
|  |    * | ||||||
|  |    * **Credential required**: *Yes* / **Permission**: *read:admin:queue* | ||||||
|  |    */ | ||||||
|  |   admin___queue___queues: { | ||||||
|  |     responses: { | ||||||
|  |       /** @description OK (without any results) */ | ||||||
|  |       204: { | ||||||
|  |         content: never; | ||||||
|  |       }; | ||||||
|  |       /** @description Client error */ | ||||||
|  |       400: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description Authentication error */ | ||||||
|  |       401: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description Forbidden error */ | ||||||
|  |       403: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description I'm Ai */ | ||||||
|  |       418: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description Internal server error */ | ||||||
|  |       500: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
|  |   /** | ||||||
|  |    * admin/queue/remove-job | ||||||
|  |    * @description No description provided. | ||||||
|  |    * | ||||||
|  |    * **Credential required**: *Yes* / **Permission**: *write:admin:queue* | ||||||
|  |    */ | ||||||
|  |   'admin___queue___remove-job': { | ||||||
|  |     requestBody: { | ||||||
|  |       content: { | ||||||
|  |         'application/json': { | ||||||
|  |           /** @enum {string} */ | ||||||
|  |           queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; | ||||||
|  |           jobId: string; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|  |     responses: { | ||||||
|  |       /** @description OK (without any results) */ | ||||||
|  |       204: { | ||||||
|  |         content: never; | ||||||
|  |       }; | ||||||
|  |       /** @description Client error */ | ||||||
|  |       400: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description Authentication error */ | ||||||
|  |       401: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description Forbidden error */ | ||||||
|  |       403: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description I'm Ai */ | ||||||
|  |       418: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description Internal server error */ | ||||||
|  |       500: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
|  |   /** | ||||||
|  |    * admin/queue/retry-job | ||||||
|  |    * @description No description provided. | ||||||
|  |    * | ||||||
|  |    * **Credential required**: *Yes* / **Permission**: *write:admin:queue* | ||||||
|  |    */ | ||||||
|  |   'admin___queue___retry-job': { | ||||||
|  |     requestBody: { | ||||||
|  |       content: { | ||||||
|  |         'application/json': { | ||||||
|  |           /** @enum {string} */ | ||||||
|  |           queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; | ||||||
|  |           jobId: string; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|  |     responses: { | ||||||
|  |       /** @description OK (without any results) */ | ||||||
|  |       204: { | ||||||
|  |         content: never; | ||||||
|  |       }; | ||||||
|  |       /** @description Client error */ | ||||||
|  |       400: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description Authentication error */ | ||||||
|  |       401: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description Forbidden error */ | ||||||
|  |       403: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description I'm Ai */ | ||||||
|  |       418: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       /** @description Internal server error */ | ||||||
|  |       500: { | ||||||
|  |         content: { | ||||||
|  |           'application/json': components['schemas']['Error']; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
|  |   /** | ||||||
|  |    * admin/queue/show-job | ||||||
|  |    * @description No description provided. | ||||||
|  |    * | ||||||
|  |    * **Credential required**: *Yes* / **Permission**: *read:admin:queue* | ||||||
|  |    */ | ||||||
|  |   'admin___queue___show-job': { | ||||||
|  |     requestBody: { | ||||||
|  |       content: { | ||||||
|  |         'application/json': { | ||||||
|  |           /** @enum {string} */ | ||||||
|  |           queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; | ||||||
|  |           jobId: string; | ||||||
|         }; |         }; | ||||||
|       }; |       }; | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  | @ -170,6 +170,9 @@ importers: | ||||||
|       '@twemoji/parser': |       '@twemoji/parser': | ||||||
|         specifier: 15.1.1 |         specifier: 15.1.1 | ||||||
|         version: 15.1.1 |         version: 15.1.1 | ||||||
|  |       '@types/redis-info': | ||||||
|  |         specifier: 3.0.3 | ||||||
|  |         version: 3.0.3 | ||||||
|       accepts: |       accepts: | ||||||
|         specifier: 1.3.8 |         specifier: 1.3.8 | ||||||
|         version: 1.3.8 |         version: 1.3.8 | ||||||
|  | @ -368,6 +371,9 @@ importers: | ||||||
|       re2: |       re2: | ||||||
|         specifier: 1.21.4 |         specifier: 1.21.4 | ||||||
|         version: 1.21.4(patch_hash=018babd22b7ce951bcd10d6246f1e541a7ac7ba212f7fa8985e774ece67d08e1) |         version: 1.21.4(patch_hash=018babd22b7ce951bcd10d6246f1e541a7ac7ba212f7fa8985e774ece67d08e1) | ||||||
|  |       redis-info: | ||||||
|  |         specifier: 3.1.0 | ||||||
|  |         version: 3.1.0 | ||||||
|       redis-lock: |       redis-lock: | ||||||
|         specifier: 0.1.4 |         specifier: 0.1.4 | ||||||
|         version: 0.1.4 |         version: 0.1.4 | ||||||
|  | @ -4462,6 +4468,9 @@ packages: | ||||||
|   '@types/readdir-glob@1.1.1': |   '@types/readdir-glob@1.1.1': | ||||||
|     resolution: {integrity: sha512-ImM6TmoF8bgOwvehGviEj3tRdRBbQujr1N+0ypaln/GWjaerOB26jb93vsRHmdMtvVQZQebOlqt2HROark87mQ==} |     resolution: {integrity: sha512-ImM6TmoF8bgOwvehGviEj3tRdRBbQujr1N+0ypaln/GWjaerOB26jb93vsRHmdMtvVQZQebOlqt2HROark87mQ==} | ||||||
| 
 | 
 | ||||||
|  |   '@types/redis-info@3.0.3': | ||||||
|  |     resolution: {integrity: sha512-VIkNy6JbYI/RLdbPHdm9JQvv6RVld2uE2/6Hdid38Qdq+zvDli2FTpImI8pC5zwp8xS8qVqfzlfyAub8xZEd5g==} | ||||||
|  | 
 | ||||||
|   '@types/rename@1.0.7': |   '@types/rename@1.0.7': | ||||||
|     resolution: {integrity: sha512-E9qapfghUGfBMi3jNhsmCKPIp3f2zvNKpaX1BDGLGJNjzpgsZ/RTx7NaNksFjGoJ+r9NvWF1NSM5vVecnNjVmw==} |     resolution: {integrity: sha512-E9qapfghUGfBMi3jNhsmCKPIp3f2zvNKpaX1BDGLGJNjzpgsZ/RTx7NaNksFjGoJ+r9NvWF1NSM5vVecnNjVmw==} | ||||||
| 
 | 
 | ||||||
|  | @ -9429,6 +9438,9 @@ packages: | ||||||
|     resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} |     resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} | ||||||
|     engines: {node: '>=4'} |     engines: {node: '>=4'} | ||||||
| 
 | 
 | ||||||
|  |   redis-info@3.1.0: | ||||||
|  |     resolution: {integrity: sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==} | ||||||
|  | 
 | ||||||
|   redis-lock@0.1.4: |   redis-lock@0.1.4: | ||||||
|     resolution: {integrity: sha512-7/+zu86XVQfJVx1nHTzux5reglDiyUCDwmW7TSlvVezfhH2YLc/Rc8NE0ejQG+8/0lwKzm29/u/4+ogKeLosiA==} |     resolution: {integrity: sha512-7/+zu86XVQfJVx1nHTzux5reglDiyUCDwmW7TSlvVezfhH2YLc/Rc8NE0ejQG+8/0lwKzm29/u/4+ogKeLosiA==} | ||||||
|     engines: {node: '>=0.6'} |     engines: {node: '>=0.6'} | ||||||
|  | @ -14937,6 +14949,8 @@ snapshots: | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@types/node': 22.14.0 |       '@types/node': 22.14.0 | ||||||
| 
 | 
 | ||||||
|  |   '@types/redis-info@3.0.3': {} | ||||||
|  | 
 | ||||||
|   '@types/rename@1.0.7': {} |   '@types/rename@1.0.7': {} | ||||||
| 
 | 
 | ||||||
|   '@types/resolve@1.20.3': {} |   '@types/resolve@1.20.3': {} | ||||||
|  | @ -21162,6 +21176,10 @@ snapshots: | ||||||
| 
 | 
 | ||||||
|   redis-errors@1.2.0: {} |   redis-errors@1.2.0: {} | ||||||
| 
 | 
 | ||||||
|  |   redis-info@3.1.0: | ||||||
|  |     dependencies: | ||||||
|  |       lodash: 4.17.21 | ||||||
|  | 
 | ||||||
|   redis-lock@0.1.4: {} |   redis-lock@0.1.4: {} | ||||||
| 
 | 
 | ||||||
|   redis-parser@3.0.0: |   redis-parser@3.0.0: | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue