Migrate to Chart.js v3 (#7896)
* wip * wip * wip * wip * wip * wip * wip * 定期的にresync * Update overview.vue * wip * wip
This commit is contained in:
		
							parent
							
								
									e7660bc8db
								
							
						
					
					
						commit
						4e4c559db6
					
				
					 16 changed files with 980 additions and 1103 deletions
				
			
		
							
								
								
									
										11
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										11
									
								
								package.json
									
										
									
									
									
								
							| 
						 | 
					@ -60,6 +60,9 @@
 | 
				
			||||||
		"@types/jsonld": "1.5.6",
 | 
							"@types/jsonld": "1.5.6",
 | 
				
			||||||
		"@types/katex": "0.11.1",
 | 
							"@types/katex": "0.11.1",
 | 
				
			||||||
		"@types/koa": "2.13.4",
 | 
							"@types/koa": "2.13.4",
 | 
				
			||||||
 | 
							"@types/koa__cors": "3.0.3",
 | 
				
			||||||
 | 
							"@types/koa__multer": "2.0.3",
 | 
				
			||||||
 | 
							"@types/koa__router": "8.0.8",
 | 
				
			||||||
		"@types/koa-bodyparser": "4.3.3",
 | 
							"@types/koa-bodyparser": "4.3.3",
 | 
				
			||||||
		"@types/koa-cors": "0.0.2",
 | 
							"@types/koa-cors": "0.0.2",
 | 
				
			||||||
		"@types/koa-favicon": "2.0.21",
 | 
							"@types/koa-favicon": "2.0.21",
 | 
				
			||||||
| 
						 | 
					@ -67,9 +70,6 @@
 | 
				
			||||||
		"@types/koa-mount": "4.0.1",
 | 
							"@types/koa-mount": "4.0.1",
 | 
				
			||||||
		"@types/koa-send": "4.1.3",
 | 
							"@types/koa-send": "4.1.3",
 | 
				
			||||||
		"@types/koa-views": "7.0.0",
 | 
							"@types/koa-views": "7.0.0",
 | 
				
			||||||
		"@types/koa__cors": "3.0.3",
 | 
					 | 
				
			||||||
		"@types/koa__multer": "2.0.3",
 | 
					 | 
				
			||||||
		"@types/koa__router": "8.0.8",
 | 
					 | 
				
			||||||
		"@types/markdown-it": "12.2.3",
 | 
							"@types/markdown-it": "12.2.3",
 | 
				
			||||||
		"@types/matter-js": "0.17.5",
 | 
							"@types/matter-js": "0.17.5",
 | 
				
			||||||
		"@types/mocha": "8.2.3",
 | 
							"@types/mocha": "8.2.3",
 | 
				
			||||||
| 
						 | 
					@ -119,7 +119,9 @@
 | 
				
			||||||
		"cafy": "15.2.1",
 | 
							"cafy": "15.2.1",
 | 
				
			||||||
		"cbor": "8.0.2",
 | 
							"cbor": "8.0.2",
 | 
				
			||||||
		"chalk": "4.1.2",
 | 
							"chalk": "4.1.2",
 | 
				
			||||||
		"chart.js": "2.9.4",
 | 
							"chart.js": "3.5.1",
 | 
				
			||||||
 | 
							"chartjs-adapter-date-fns": "2.0.0",
 | 
				
			||||||
 | 
							"chartjs-plugin-zoom": "1.1.1",
 | 
				
			||||||
		"cli-highlight": "2.1.11",
 | 
							"cli-highlight": "2.1.11",
 | 
				
			||||||
		"compare-versions": "3.6.0",
 | 
							"compare-versions": "3.6.0",
 | 
				
			||||||
		"concurrently": "6.3.0",
 | 
							"concurrently": "6.3.0",
 | 
				
			||||||
| 
						 | 
					@ -127,6 +129,7 @@
 | 
				
			||||||
		"crc-32": "1.2.0",
 | 
							"crc-32": "1.2.0",
 | 
				
			||||||
		"css-loader": "6.4.0",
 | 
							"css-loader": "6.4.0",
 | 
				
			||||||
		"cssnano": "5.0.8",
 | 
							"cssnano": "5.0.8",
 | 
				
			||||||
 | 
							"date-fns": "2.25.0",
 | 
				
			||||||
		"dateformat": "4.5.1",
 | 
							"dateformat": "4.5.1",
 | 
				
			||||||
		"escape-regexp": "0.0.1",
 | 
							"escape-regexp": "0.0.1",
 | 
				
			||||||
		"eslint": "8.0.1",
 | 
							"eslint": "8.0.1",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										628
									
								
								src/client/components/chart.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										628
									
								
								src/client/components/chart.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,628 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<canvas ref="chartEl"></canvas>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import { defineComponent, onMounted, ref, watch, PropType } from 'vue';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						Chart,
 | 
				
			||||||
 | 
						ArcElement,
 | 
				
			||||||
 | 
						LineElement,
 | 
				
			||||||
 | 
						BarElement,
 | 
				
			||||||
 | 
						PointElement,
 | 
				
			||||||
 | 
						BarController,
 | 
				
			||||||
 | 
						LineController,
 | 
				
			||||||
 | 
						CategoryScale,
 | 
				
			||||||
 | 
						LinearScale,
 | 
				
			||||||
 | 
						TimeScale,
 | 
				
			||||||
 | 
						Legend,
 | 
				
			||||||
 | 
						Title,
 | 
				
			||||||
 | 
						Tooltip,
 | 
				
			||||||
 | 
						SubTitle,
 | 
				
			||||||
 | 
						Filler,
 | 
				
			||||||
 | 
					} from 'chart.js';
 | 
				
			||||||
 | 
					import 'chartjs-adapter-date-fns';
 | 
				
			||||||
 | 
					import { enUS } from 'date-fns/locale';
 | 
				
			||||||
 | 
					import zoomPlugin from 'chartjs-plugin-zoom';
 | 
				
			||||||
 | 
					import * as os from '@client/os';
 | 
				
			||||||
 | 
					import { defaultStore } from '@client/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Chart.register(
 | 
				
			||||||
 | 
						ArcElement,
 | 
				
			||||||
 | 
						LineElement,
 | 
				
			||||||
 | 
						BarElement,
 | 
				
			||||||
 | 
						PointElement,
 | 
				
			||||||
 | 
						BarController,
 | 
				
			||||||
 | 
						LineController,
 | 
				
			||||||
 | 
						CategoryScale,
 | 
				
			||||||
 | 
						LinearScale,
 | 
				
			||||||
 | 
						TimeScale,
 | 
				
			||||||
 | 
						Legend,
 | 
				
			||||||
 | 
						Title,
 | 
				
			||||||
 | 
						Tooltip,
 | 
				
			||||||
 | 
						SubTitle,
 | 
				
			||||||
 | 
						Filler,
 | 
				
			||||||
 | 
						zoomPlugin,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
 | 
				
			||||||
 | 
					const negate = arr => arr.map(x => -x);
 | 
				
			||||||
 | 
					const alpha = (hex, a) => {
 | 
				
			||||||
 | 
						const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
 | 
				
			||||||
 | 
						const r = parseInt(result[1], 16);
 | 
				
			||||||
 | 
						const g = parseInt(result[2], 16);
 | 
				
			||||||
 | 
						const b = parseInt(result[3], 16);
 | 
				
			||||||
 | 
						return `rgba(${r}, ${g}, ${b}, ${a})`;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const colors = ['#008FFB', '#00E396', '#FEB019', '#FF4560'];
 | 
				
			||||||
 | 
					const getColor = (i) => {
 | 
				
			||||||
 | 
						return colors[i % colors.length];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default defineComponent({
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							src: {
 | 
				
			||||||
 | 
								type: String,
 | 
				
			||||||
 | 
								required: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							args: {
 | 
				
			||||||
 | 
								type: Object,
 | 
				
			||||||
 | 
								required: false,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							limit: {
 | 
				
			||||||
 | 
								type: Number,
 | 
				
			||||||
 | 
								required: false,
 | 
				
			||||||
 | 
								default: 90
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							span: {
 | 
				
			||||||
 | 
								type: String as PropType<'hour' | 'day'>,
 | 
				
			||||||
 | 
								required: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							detailed: {
 | 
				
			||||||
 | 
								type: Boolean,
 | 
				
			||||||
 | 
								required: false,
 | 
				
			||||||
 | 
								default: false
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						setup(props) {
 | 
				
			||||||
 | 
							const now = new Date();
 | 
				
			||||||
 | 
							let chartInstance: Chart = null;
 | 
				
			||||||
 | 
							let data: {
 | 
				
			||||||
 | 
								series: {
 | 
				
			||||||
 | 
									name: string;
 | 
				
			||||||
 | 
									type: 'line' | 'area';
 | 
				
			||||||
 | 
									color?: string;
 | 
				
			||||||
 | 
									borderDash?: number[];
 | 
				
			||||||
 | 
									hidden?: boolean;
 | 
				
			||||||
 | 
									data: {
 | 
				
			||||||
 | 
										x: number;
 | 
				
			||||||
 | 
										y: number;
 | 
				
			||||||
 | 
									}[];
 | 
				
			||||||
 | 
								}[];
 | 
				
			||||||
 | 
							} = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const chartEl = ref<HTMLCanvasElement>(null);
 | 
				
			||||||
 | 
							const fetching = ref(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const getDate = (ago: number) => {
 | 
				
			||||||
 | 
								const y = now.getFullYear();
 | 
				
			||||||
 | 
								const m = now.getMonth();
 | 
				
			||||||
 | 
								const d = now.getDate();
 | 
				
			||||||
 | 
								const h = now.getHours();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return props.span === 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const format = (arr) => {
 | 
				
			||||||
 | 
								return arr.map((v, i) => ({
 | 
				
			||||||
 | 
									x: getDate(i).getTime(),
 | 
				
			||||||
 | 
									y: v
 | 
				
			||||||
 | 
								}));
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const render = () => {
 | 
				
			||||||
 | 
								if (chartInstance) {
 | 
				
			||||||
 | 
									chartInstance.destroy();
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// フォントカラー
 | 
				
			||||||
 | 
								Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								chartInstance = new Chart(chartEl.value, {
 | 
				
			||||||
 | 
									type: 'line',
 | 
				
			||||||
 | 
									data: {
 | 
				
			||||||
 | 
										labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
 | 
				
			||||||
 | 
										datasets: data.series.map((x, i) => ({
 | 
				
			||||||
 | 
											parsing: false,
 | 
				
			||||||
 | 
											label: x.name,
 | 
				
			||||||
 | 
											data: x.data.slice().reverse(),
 | 
				
			||||||
 | 
											pointRadius: 0,
 | 
				
			||||||
 | 
											tension: 0,
 | 
				
			||||||
 | 
											borderWidth: 2,
 | 
				
			||||||
 | 
											borderColor: x.color ? x.color : getColor(i),
 | 
				
			||||||
 | 
											borderDash: x.borderDash || [],
 | 
				
			||||||
 | 
											borderJoinStyle: 'round',
 | 
				
			||||||
 | 
											backgroundColor: alpha(x.color ? x.color : getColor(i), 0.1),
 | 
				
			||||||
 | 
											fill: x.type === 'area',
 | 
				
			||||||
 | 
											hidden: !!x.hidden,
 | 
				
			||||||
 | 
										})),
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									options: {
 | 
				
			||||||
 | 
										aspectRatio: 2.5,
 | 
				
			||||||
 | 
										layout: {
 | 
				
			||||||
 | 
											padding: {
 | 
				
			||||||
 | 
												left: 16,
 | 
				
			||||||
 | 
												right: 16,
 | 
				
			||||||
 | 
												top: 16,
 | 
				
			||||||
 | 
												bottom: 8,
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										scales: {
 | 
				
			||||||
 | 
											x: {
 | 
				
			||||||
 | 
												type: 'time',
 | 
				
			||||||
 | 
												time: {
 | 
				
			||||||
 | 
													stepSize: 1,
 | 
				
			||||||
 | 
													unit: props.span === 'day' ? 'month' : 'day',
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
												grid: {
 | 
				
			||||||
 | 
													display: props.detailed,
 | 
				
			||||||
 | 
													color: gridColor,
 | 
				
			||||||
 | 
													borderColor: 'rgb(0, 0, 0, 0)',
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
												ticks: {
 | 
				
			||||||
 | 
													display: props.detailed,
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
												adapters: {
 | 
				
			||||||
 | 
													date: {
 | 
				
			||||||
 | 
														locale: enUS,
 | 
				
			||||||
 | 
													},
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
												min: getDate(props.limit).getTime(),
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
											y: {
 | 
				
			||||||
 | 
												position: 'left',
 | 
				
			||||||
 | 
												grid: {
 | 
				
			||||||
 | 
													color: gridColor,
 | 
				
			||||||
 | 
													borderColor: 'rgb(0, 0, 0, 0)',
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
												ticks: {
 | 
				
			||||||
 | 
													display: props.detailed,
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										interaction: {
 | 
				
			||||||
 | 
											intersect: false,
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										plugins: {
 | 
				
			||||||
 | 
											legend: {
 | 
				
			||||||
 | 
												position: 'bottom',
 | 
				
			||||||
 | 
												labels: {
 | 
				
			||||||
 | 
													boxWidth: 16,
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
											tooltip: {
 | 
				
			||||||
 | 
												mode: 'index',
 | 
				
			||||||
 | 
												animation: {
 | 
				
			||||||
 | 
													duration: 0,
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
											zoom: {
 | 
				
			||||||
 | 
												pan: {
 | 
				
			||||||
 | 
													enabled: true,
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
												zoom: {
 | 
				
			||||||
 | 
													wheel: {
 | 
				
			||||||
 | 
														enabled: true,
 | 
				
			||||||
 | 
													},
 | 
				
			||||||
 | 
													pinch: {
 | 
				
			||||||
 | 
														enabled: true,
 | 
				
			||||||
 | 
													},
 | 
				
			||||||
 | 
													drag: {
 | 
				
			||||||
 | 
														enabled: false,
 | 
				
			||||||
 | 
													},
 | 
				
			||||||
 | 
													mode: 'x',
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
												limits: {
 | 
				
			||||||
 | 
													x: {
 | 
				
			||||||
 | 
														min: 'original',
 | 
				
			||||||
 | 
														max: 'original',
 | 
				
			||||||
 | 
													},
 | 
				
			||||||
 | 
													y: {
 | 
				
			||||||
 | 
														min: 'original',
 | 
				
			||||||
 | 
														max: 'original',
 | 
				
			||||||
 | 
													},
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const exportData = () => {
 | 
				
			||||||
 | 
								// TODO
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fetchFederationInstancesChart = async (total: boolean): Promise<typeof data> => {
 | 
				
			||||||
 | 
								const raw = await os.api('charts/federation', { limit: props.limit, span: props.span });
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									series: [{
 | 
				
			||||||
 | 
										name: 'Instances',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(total
 | 
				
			||||||
 | 
											? raw.instance.total
 | 
				
			||||||
 | 
											: sum(raw.instance.inc, negate(raw.instance.dec))
 | 
				
			||||||
 | 
										),
 | 
				
			||||||
 | 
									}],
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fetchNotesChart = async (type: string): Promise<typeof data> => {
 | 
				
			||||||
 | 
								const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									series: [{
 | 
				
			||||||
 | 
										name: 'All',
 | 
				
			||||||
 | 
										type: 'line',
 | 
				
			||||||
 | 
										borderDash: [5, 5],
 | 
				
			||||||
 | 
										data: format(type == 'combined'
 | 
				
			||||||
 | 
											? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
 | 
				
			||||||
 | 
											: sum(raw[type].inc, negate(raw[type].dec))
 | 
				
			||||||
 | 
										),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Renotes',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(type == 'combined'
 | 
				
			||||||
 | 
											? sum(raw.local.diffs.renote, raw.remote.diffs.renote)
 | 
				
			||||||
 | 
											: raw[type].diffs.renote
 | 
				
			||||||
 | 
										),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Replies',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(type == 'combined'
 | 
				
			||||||
 | 
											? sum(raw.local.diffs.reply, raw.remote.diffs.reply)
 | 
				
			||||||
 | 
											: raw[type].diffs.reply
 | 
				
			||||||
 | 
										),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Normal',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(type == 'combined'
 | 
				
			||||||
 | 
											? sum(raw.local.diffs.normal, raw.remote.diffs.normal)
 | 
				
			||||||
 | 
											: raw[type].diffs.normal
 | 
				
			||||||
 | 
										),
 | 
				
			||||||
 | 
									}],
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fetchNotesTotalChart = async (): Promise<typeof data> => {
 | 
				
			||||||
 | 
								const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									series: [{
 | 
				
			||||||
 | 
										name: 'Combined',
 | 
				
			||||||
 | 
										type: 'line',
 | 
				
			||||||
 | 
										data: format(sum(raw.local.total, raw.remote.total)),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Local',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(raw.local.total),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Remote',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(raw.remote.total),
 | 
				
			||||||
 | 
									}],
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fetchUsersChart = async (total: boolean): Promise<typeof data> => {
 | 
				
			||||||
 | 
								const raw = await os.api('charts/users', { limit: props.limit, span: props.span });
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									series: [{
 | 
				
			||||||
 | 
										name: 'Combined',
 | 
				
			||||||
 | 
										type: 'line',
 | 
				
			||||||
 | 
										data: format(total
 | 
				
			||||||
 | 
											? sum(raw.local.total, raw.remote.total)
 | 
				
			||||||
 | 
											: sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
 | 
				
			||||||
 | 
										),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Local',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(total
 | 
				
			||||||
 | 
											? raw.local.total
 | 
				
			||||||
 | 
											: sum(raw.local.inc, negate(raw.local.dec))
 | 
				
			||||||
 | 
										),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Remote',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(total
 | 
				
			||||||
 | 
											? raw.remote.total
 | 
				
			||||||
 | 
											: sum(raw.remote.inc, negate(raw.remote.dec))
 | 
				
			||||||
 | 
										),
 | 
				
			||||||
 | 
									}],
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fetchActiveUsersChart = async (): Promise<typeof data> => {
 | 
				
			||||||
 | 
								const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span });
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									series: [{
 | 
				
			||||||
 | 
										name: 'Combined',
 | 
				
			||||||
 | 
										type: 'line',
 | 
				
			||||||
 | 
										data: format(sum(raw.local.users, raw.remote.users)),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Local',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(raw.local.users),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Remote',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(raw.remote.users),
 | 
				
			||||||
 | 
									}],
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fetchDriveChart = async (): Promise<typeof data> => {
 | 
				
			||||||
 | 
								const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									bytes: true,
 | 
				
			||||||
 | 
									series: [{
 | 
				
			||||||
 | 
										name: 'All',
 | 
				
			||||||
 | 
										type: 'line',
 | 
				
			||||||
 | 
										borderDash: [5, 5],
 | 
				
			||||||
 | 
										data: format(
 | 
				
			||||||
 | 
											sum(
 | 
				
			||||||
 | 
												raw.local.incSize,
 | 
				
			||||||
 | 
												negate(raw.local.decSize),
 | 
				
			||||||
 | 
												raw.remote.incSize,
 | 
				
			||||||
 | 
												negate(raw.remote.decSize)
 | 
				
			||||||
 | 
											)
 | 
				
			||||||
 | 
										),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Local +',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(raw.local.incSize),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Local -',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(negate(raw.local.decSize)),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Remote +',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(raw.remote.incSize),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Remote -',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(negate(raw.remote.decSize)),
 | 
				
			||||||
 | 
									}],
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fetchDriveTotalChart = async (): Promise<typeof data> => {
 | 
				
			||||||
 | 
								const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									bytes: true,
 | 
				
			||||||
 | 
									series: [{
 | 
				
			||||||
 | 
										name: 'Combined',
 | 
				
			||||||
 | 
										type: 'line',
 | 
				
			||||||
 | 
										data: format(sum(raw.local.totalSize, raw.remote.totalSize)),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Local',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(raw.local.totalSize),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Remote',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(raw.remote.totalSize),
 | 
				
			||||||
 | 
									}],
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fetchDriveFilesChart = async (): Promise<typeof data> => {
 | 
				
			||||||
 | 
								const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									series: [{
 | 
				
			||||||
 | 
										name: 'All',
 | 
				
			||||||
 | 
										type: 'line',
 | 
				
			||||||
 | 
										borderDash: [5, 5],
 | 
				
			||||||
 | 
										data: format(
 | 
				
			||||||
 | 
											sum(
 | 
				
			||||||
 | 
												raw.local.incCount,
 | 
				
			||||||
 | 
												negate(raw.local.decCount),
 | 
				
			||||||
 | 
												raw.remote.incCount,
 | 
				
			||||||
 | 
												negate(raw.remote.decCount)
 | 
				
			||||||
 | 
											)
 | 
				
			||||||
 | 
										),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Local +',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(raw.local.incCount),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Local -',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(negate(raw.local.decCount)),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Remote +',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(raw.remote.incCount),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Remote -',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(negate(raw.remote.decCount)),
 | 
				
			||||||
 | 
									}],
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fetchDriveFilesTotalChart = async (): Promise<typeof data> => {
 | 
				
			||||||
 | 
								const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									series: [{
 | 
				
			||||||
 | 
										name: 'Combined',
 | 
				
			||||||
 | 
										type: 'line',
 | 
				
			||||||
 | 
										data: format(sum(raw.local.totalCount, raw.remote.totalCount)),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Local',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(raw.local.totalCount),
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Remote',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										data: format(raw.remote.totalCount),
 | 
				
			||||||
 | 
									}],
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fetchInstanceRequestsChart = async (): Promise<typeof data> => {
 | 
				
			||||||
 | 
								const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									series: [{
 | 
				
			||||||
 | 
										name: 'In',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										color: '#008FFB',
 | 
				
			||||||
 | 
										data: format(raw.requests.received)
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Out (succ)',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										color: '#00E396',
 | 
				
			||||||
 | 
										data: format(raw.requests.succeeded)
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Out (fail)',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										color: '#FEB019',
 | 
				
			||||||
 | 
										data: format(raw.requests.failed)
 | 
				
			||||||
 | 
									}]
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fetchInstanceUsersChart = async (total: boolean): Promise<typeof data> => {
 | 
				
			||||||
 | 
								const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									series: [{
 | 
				
			||||||
 | 
										name: 'Users',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										color: '#008FFB',
 | 
				
			||||||
 | 
										data: format(total
 | 
				
			||||||
 | 
											? raw.users.total
 | 
				
			||||||
 | 
											: sum(raw.users.inc, negate(raw.users.dec))
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
 | 
									}]
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fetchInstanceNotesChart = async (total: boolean): Promise<typeof data> => {
 | 
				
			||||||
 | 
								const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									series: [{
 | 
				
			||||||
 | 
										name: 'Notes',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										color: '#008FFB',
 | 
				
			||||||
 | 
										data: format(total
 | 
				
			||||||
 | 
											? raw.notes.total
 | 
				
			||||||
 | 
											: sum(raw.notes.inc, negate(raw.notes.dec))
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
 | 
									}]
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fetchInstanceFfChart = async (total: boolean): Promise<typeof data> => {
 | 
				
			||||||
 | 
								const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									series: [{
 | 
				
			||||||
 | 
										name: 'Following',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										color: '#008FFB',
 | 
				
			||||||
 | 
										data: format(total
 | 
				
			||||||
 | 
											? raw.following.total
 | 
				
			||||||
 | 
											: sum(raw.following.inc, negate(raw.following.dec))
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										name: 'Followers',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										color: '#00E396',
 | 
				
			||||||
 | 
										data: format(total
 | 
				
			||||||
 | 
											? raw.followers.total
 | 
				
			||||||
 | 
											: sum(raw.followers.inc, negate(raw.followers.dec))
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
 | 
									}]
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof data> => {
 | 
				
			||||||
 | 
								const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									bytes: true,
 | 
				
			||||||
 | 
									series: [{
 | 
				
			||||||
 | 
										name: 'Drive usage',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										color: '#008FFB',
 | 
				
			||||||
 | 
										data: format(total
 | 
				
			||||||
 | 
											? raw.drive.totalUsage
 | 
				
			||||||
 | 
											: sum(raw.drive.incUsage, negate(raw.drive.decUsage))
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
 | 
									}]
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof data> => {
 | 
				
			||||||
 | 
								const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									series: [{
 | 
				
			||||||
 | 
										name: 'Drive files',
 | 
				
			||||||
 | 
										type: 'area',
 | 
				
			||||||
 | 
										color: '#008FFB',
 | 
				
			||||||
 | 
										data: format(total
 | 
				
			||||||
 | 
											? raw.drive.totalFiles
 | 
				
			||||||
 | 
											: sum(raw.drive.incFiles, negate(raw.drive.decFiles))
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
 | 
									}]
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fetchAndRender = async () => {
 | 
				
			||||||
 | 
								const fetchData = () => {
 | 
				
			||||||
 | 
									switch (props.src) {
 | 
				
			||||||
 | 
										case 'federation-instances': return fetchFederationInstancesChart(false);
 | 
				
			||||||
 | 
										case 'federation-instances-total': return fetchFederationInstancesChart(true);
 | 
				
			||||||
 | 
										case 'users': return fetchUsersChart(false);
 | 
				
			||||||
 | 
										case 'users-total': return fetchUsersChart(true);
 | 
				
			||||||
 | 
										case 'active-users': return fetchActiveUsersChart();
 | 
				
			||||||
 | 
										case 'notes': return fetchNotesChart('combined');
 | 
				
			||||||
 | 
										case 'local-notes': return fetchNotesChart('local');
 | 
				
			||||||
 | 
										case 'remote-notes': return fetchNotesChart('remote');
 | 
				
			||||||
 | 
										case 'notes-total': return fetchNotesTotalChart();
 | 
				
			||||||
 | 
										case 'drive': return fetchDriveChart();
 | 
				
			||||||
 | 
										case 'drive-total': return fetchDriveTotalChart();
 | 
				
			||||||
 | 
										case 'drive-files': return fetchDriveFilesChart();
 | 
				
			||||||
 | 
										case 'drive-files-total': return fetchDriveFilesTotalChart();
 | 
				
			||||||
 | 
										
 | 
				
			||||||
 | 
										case 'instances-requests': return fetchInstanceRequestsChart();
 | 
				
			||||||
 | 
										case 'instances-users': return fetchInstanceUsersChart(false);
 | 
				
			||||||
 | 
										case 'instances-users-total': return fetchInstanceUsersChart(true);
 | 
				
			||||||
 | 
										case 'instances-notes': return fetchInstanceNotesChart(false);
 | 
				
			||||||
 | 
										case 'instances-notes-total': return fetchInstanceNotesChart(true);
 | 
				
			||||||
 | 
										case 'instances-ff': return fetchInstanceFfChart(false);
 | 
				
			||||||
 | 
										case 'instances-ff-total': return fetchInstanceFfChart(true);
 | 
				
			||||||
 | 
										case 'instances-drive-usage': return fetchInstanceDriveUsageChart(false);
 | 
				
			||||||
 | 
										case 'instances-drive-usage-total': return fetchInstanceDriveUsageChart(true);
 | 
				
			||||||
 | 
										case 'instances-drive-files': return fetchInstanceDriveFilesChart(false);
 | 
				
			||||||
 | 
										case 'instances-drive-files-total': return fetchInstanceDriveFilesChart(true);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
								fetching.value = true;
 | 
				
			||||||
 | 
								data = await fetchData();
 | 
				
			||||||
 | 
								fetching.value = false;
 | 
				
			||||||
 | 
								render();
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							watch(() => [props.src, props.span], fetchAndRender);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							onMounted(() => {
 | 
				
			||||||
 | 
								fetchAndRender();
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								chartEl,
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -24,35 +24,26 @@
 | 
				
			||||||
				<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
 | 
									<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
 | 
				
			||||||
			</optgroup>
 | 
								</optgroup>
 | 
				
			||||||
		</MkSelect>
 | 
							</MkSelect>
 | 
				
			||||||
		<MkSelect v-model="chartSpan" style="margin: 0;">
 | 
							<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
 | 
				
			||||||
			<option value="hour">{{ $ts.perHour }}</option>
 | 
								<option value="hour">{{ $ts.perHour }}</option>
 | 
				
			||||||
			<option value="day">{{ $ts.perDay }}</option>
 | 
								<option value="day">{{ $ts.perDay }}</option>
 | 
				
			||||||
		</MkSelect>
 | 
							</MkSelect>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<canvas ref="chart"></canvas>
 | 
						<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { defineComponent, markRaw } from 'vue';
 | 
					import { defineComponent, onMounted, ref, watch } from 'vue';
 | 
				
			||||||
import Chart from 'chart.js';
 | 
					import MkSelect from '@client/components/form/select.vue';
 | 
				
			||||||
import MkSelect from './form/select.vue';
 | 
					import MkChart from '@client/components/chart.vue';
 | 
				
			||||||
import number from '@client/filters/number';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
 | 
					 | 
				
			||||||
const negate = arr => arr.map(x => -x);
 | 
					 | 
				
			||||||
const alpha = (hex, a) => {
 | 
					 | 
				
			||||||
	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
 | 
					 | 
				
			||||||
	const r = parseInt(result[1], 16);
 | 
					 | 
				
			||||||
	const g = parseInt(result[2], 16);
 | 
					 | 
				
			||||||
	const b = parseInt(result[3], 16);
 | 
					 | 
				
			||||||
	return `rgba(${r}, ${g}, ${b}, ${a})`;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
import * as os from '@client/os';
 | 
					import * as os from '@client/os';
 | 
				
			||||||
 | 
					import { defaultStore } from '@client/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
	components: {
 | 
						components: {
 | 
				
			||||||
		MkSelect
 | 
							MkSelect,
 | 
				
			||||||
 | 
							MkChart,
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	props: {
 | 
						props: {
 | 
				
			||||||
| 
						 | 
					@ -68,463 +59,15 @@ export default defineComponent({
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data() {
 | 
						setup() {
 | 
				
			||||||
 | 
							const chartSpan = ref<'hour' | 'day'>('hour');
 | 
				
			||||||
 | 
							const chartSrc = ref('notes');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			notesLocalWoW: 0,
 | 
								chartSrc,
 | 
				
			||||||
			notesLocalDoD: 0,
 | 
								chartSpan,
 | 
				
			||||||
			notesRemoteWoW: 0,
 | 
							};
 | 
				
			||||||
			notesRemoteDoD: 0,
 | 
					 | 
				
			||||||
			usersLocalWoW: 0,
 | 
					 | 
				
			||||||
			usersLocalDoD: 0,
 | 
					 | 
				
			||||||
			usersRemoteWoW: 0,
 | 
					 | 
				
			||||||
			usersRemoteDoD: 0,
 | 
					 | 
				
			||||||
			now: null,
 | 
					 | 
				
			||||||
			chart: null,
 | 
					 | 
				
			||||||
			chartInstance: null,
 | 
					 | 
				
			||||||
			chartSrc: 'notes',
 | 
					 | 
				
			||||||
			chartSpan: 'hour',
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					 | 
				
			||||||
	computed: {
 | 
					 | 
				
			||||||
		data(): any {
 | 
					 | 
				
			||||||
			if (this.chart == null) return null;
 | 
					 | 
				
			||||||
			switch (this.chartSrc) {
 | 
					 | 
				
			||||||
				case 'federation-instances': return this.federationInstancesChart(false);
 | 
					 | 
				
			||||||
				case 'federation-instances-total': return this.federationInstancesChart(true);
 | 
					 | 
				
			||||||
				case 'users': return this.usersChart(false);
 | 
					 | 
				
			||||||
				case 'users-total': return this.usersChart(true);
 | 
					 | 
				
			||||||
				case 'active-users': return this.activeUsersChart();
 | 
					 | 
				
			||||||
				case 'notes': return this.notesChart('combined');
 | 
					 | 
				
			||||||
				case 'local-notes': return this.notesChart('local');
 | 
					 | 
				
			||||||
				case 'remote-notes': return this.notesChart('remote');
 | 
					 | 
				
			||||||
				case 'notes-total': return this.notesTotalChart();
 | 
					 | 
				
			||||||
				case 'drive': return this.driveChart();
 | 
					 | 
				
			||||||
				case 'drive-total': return this.driveTotalChart();
 | 
					 | 
				
			||||||
				case 'drive-files': return this.driveFilesChart();
 | 
					 | 
				
			||||||
				case 'drive-files-total': return this.driveFilesTotalChart();
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		stats(): any[] {
 | 
					 | 
				
			||||||
			const stats =
 | 
					 | 
				
			||||||
				this.chartSpan == 'day' ? this.chart.perDay :
 | 
					 | 
				
			||||||
				this.chartSpan == 'hour' ? this.chart.perHour :
 | 
					 | 
				
			||||||
				null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			return stats;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	watch: {
 | 
					 | 
				
			||||||
		chartSrc() {
 | 
					 | 
				
			||||||
			this.renderChart();
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		chartSpan() {
 | 
					 | 
				
			||||||
			this.renderChart();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async created() {
 | 
					 | 
				
			||||||
		this.now = new Date();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		this.fetchChart();
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
					 | 
				
			||||||
		async fetchChart() {
 | 
					 | 
				
			||||||
			const [perHour, perDay] = await Promise.all([Promise.all([
 | 
					 | 
				
			||||||
				os.api('charts/federation', { limit: this.chartLimit, span: 'hour' }),
 | 
					 | 
				
			||||||
				os.api('charts/users', { limit: this.chartLimit, span: 'hour' }),
 | 
					 | 
				
			||||||
				os.api('charts/active-users', { limit: this.chartLimit, span: 'hour' }),
 | 
					 | 
				
			||||||
				os.api('charts/notes', { limit: this.chartLimit, span: 'hour' }),
 | 
					 | 
				
			||||||
				os.api('charts/drive', { limit: this.chartLimit, span: 'hour' }),
 | 
					 | 
				
			||||||
			]), Promise.all([
 | 
					 | 
				
			||||||
				os.api('charts/federation', { limit: this.chartLimit, span: 'day' }),
 | 
					 | 
				
			||||||
				os.api('charts/users', { limit: this.chartLimit, span: 'day' }),
 | 
					 | 
				
			||||||
				os.api('charts/active-users', { limit: this.chartLimit, span: 'day' }),
 | 
					 | 
				
			||||||
				os.api('charts/notes', { limit: this.chartLimit, span: 'day' }),
 | 
					 | 
				
			||||||
				os.api('charts/drive', { limit: this.chartLimit, span: 'day' }),
 | 
					 | 
				
			||||||
			])]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			const chart = {
 | 
					 | 
				
			||||||
				perHour: {
 | 
					 | 
				
			||||||
					federation: perHour[0],
 | 
					 | 
				
			||||||
					users: perHour[1],
 | 
					 | 
				
			||||||
					activeUsers: perHour[2],
 | 
					 | 
				
			||||||
					notes: perHour[3],
 | 
					 | 
				
			||||||
					drive: perHour[4],
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				perDay: {
 | 
					 | 
				
			||||||
					federation: perDay[0],
 | 
					 | 
				
			||||||
					users: perDay[1],
 | 
					 | 
				
			||||||
					activeUsers: perDay[2],
 | 
					 | 
				
			||||||
					notes: perDay[3],
 | 
					 | 
				
			||||||
					drive: perDay[4],
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			this.chart = chart;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			this.renderChart();
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		renderChart() {
 | 
					 | 
				
			||||||
			if (this.chartInstance) {
 | 
					 | 
				
			||||||
				this.chartInstance.destroy();
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// TODO: var(--panel)の色が暗いか明るいかで判定する
 | 
					 | 
				
			||||||
			const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
 | 
					 | 
				
			||||||
			this.chartInstance = markRaw(new Chart(this.$refs.chart, {
 | 
					 | 
				
			||||||
				type: 'line',
 | 
					 | 
				
			||||||
				data: {
 | 
					 | 
				
			||||||
					labels: new Array(this.chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
 | 
					 | 
				
			||||||
					datasets: this.data.series.map(x => ({
 | 
					 | 
				
			||||||
						label: x.name,
 | 
					 | 
				
			||||||
						data: x.data.slice().reverse(),
 | 
					 | 
				
			||||||
						pointRadius: 0,
 | 
					 | 
				
			||||||
						lineTension: 0,
 | 
					 | 
				
			||||||
						borderWidth: 2,
 | 
					 | 
				
			||||||
						borderColor: x.color,
 | 
					 | 
				
			||||||
						borderDash: x.borderDash || [],
 | 
					 | 
				
			||||||
						backgroundColor: alpha(x.color, 0.1),
 | 
					 | 
				
			||||||
						fill: x.fill == null ? true : x.fill,
 | 
					 | 
				
			||||||
						hidden: !!x.hidden
 | 
					 | 
				
			||||||
					}))
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				options: {
 | 
					 | 
				
			||||||
					aspectRatio: 2.5,
 | 
					 | 
				
			||||||
					layout: {
 | 
					 | 
				
			||||||
						padding: {
 | 
					 | 
				
			||||||
							left: 16,
 | 
					 | 
				
			||||||
							right: 16,
 | 
					 | 
				
			||||||
							top: 16,
 | 
					 | 
				
			||||||
							bottom: 8
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
					legend: {
 | 
					 | 
				
			||||||
						position: 'bottom',
 | 
					 | 
				
			||||||
						labels: {
 | 
					 | 
				
			||||||
							boxWidth: 16,
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
					scales: {
 | 
					 | 
				
			||||||
						xAxes: [{
 | 
					 | 
				
			||||||
							type: 'time',
 | 
					 | 
				
			||||||
							time: {
 | 
					 | 
				
			||||||
								stepSize: 1,
 | 
					 | 
				
			||||||
								unit: this.chartSpan == 'day' ? 'month' : 'day',
 | 
					 | 
				
			||||||
							},
 | 
					 | 
				
			||||||
							gridLines: {
 | 
					 | 
				
			||||||
								display: this.detailed,
 | 
					 | 
				
			||||||
								color: gridColor,
 | 
					 | 
				
			||||||
								zeroLineColor: gridColor,
 | 
					 | 
				
			||||||
							},
 | 
					 | 
				
			||||||
							ticks: {
 | 
					 | 
				
			||||||
								display: this.detailed
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
						}],
 | 
					 | 
				
			||||||
						yAxes: [{
 | 
					 | 
				
			||||||
							position: 'left',
 | 
					 | 
				
			||||||
							gridLines: {
 | 
					 | 
				
			||||||
								color: gridColor,
 | 
					 | 
				
			||||||
								zeroLineColor: gridColor,
 | 
					 | 
				
			||||||
							},
 | 
					 | 
				
			||||||
							ticks: {
 | 
					 | 
				
			||||||
								display: this.detailed
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
						}]
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
					tooltips: {
 | 
					 | 
				
			||||||
						intersect: false,
 | 
					 | 
				
			||||||
						mode: 'index',
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}));
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		getDate(ago: number) {
 | 
					 | 
				
			||||||
			const y = this.now.getFullYear();
 | 
					 | 
				
			||||||
			const m = this.now.getMonth();
 | 
					 | 
				
			||||||
			const d = this.now.getDate();
 | 
					 | 
				
			||||||
			const h = this.now.getHours();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		format(arr) {
 | 
					 | 
				
			||||||
			const now = Date.now();
 | 
					 | 
				
			||||||
			return arr.map((v, i) => ({
 | 
					 | 
				
			||||||
				x: new Date(now - ((this.chartSpan == 'day' ? 86400000 :3600000 ) * i)),
 | 
					 | 
				
			||||||
				y: v
 | 
					 | 
				
			||||||
			}));
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		federationInstancesChart(total: boolean): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'Instances',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(total
 | 
					 | 
				
			||||||
						? this.stats.federation.instance.total
 | 
					 | 
				
			||||||
						: sum(this.stats.federation.instance.inc, negate(this.stats.federation.instance.dec))
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		notesChart(type: string): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'All',
 | 
					 | 
				
			||||||
					type: 'line',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					borderDash: [5, 5],
 | 
					 | 
				
			||||||
					fill: false,
 | 
					 | 
				
			||||||
					data: this.format(type == 'combined'
 | 
					 | 
				
			||||||
						? sum(this.stats.notes.local.inc, negate(this.stats.notes.local.dec), this.stats.notes.remote.inc, negate(this.stats.notes.remote.dec))
 | 
					 | 
				
			||||||
						: sum(this.stats.notes[type].inc, negate(this.stats.notes[type].dec))
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Renotes',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#00E396',
 | 
					 | 
				
			||||||
					data: this.format(type == 'combined'
 | 
					 | 
				
			||||||
						? sum(this.stats.notes.local.diffs.renote, this.stats.notes.remote.diffs.renote)
 | 
					 | 
				
			||||||
						: this.stats.notes[type].diffs.renote
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Replies',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#FEB019',
 | 
					 | 
				
			||||||
					data: this.format(type == 'combined'
 | 
					 | 
				
			||||||
						? sum(this.stats.notes.local.diffs.reply, this.stats.notes.remote.diffs.reply)
 | 
					 | 
				
			||||||
						: this.stats.notes[type].diffs.reply
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Normal',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#FF4560',
 | 
					 | 
				
			||||||
					data: this.format(type == 'combined'
 | 
					 | 
				
			||||||
						? sum(this.stats.notes.local.diffs.normal, this.stats.notes.remote.diffs.normal)
 | 
					 | 
				
			||||||
						: this.stats.notes[type].diffs.normal
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		notesTotalChart(): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'Combined',
 | 
					 | 
				
			||||||
					type: 'line',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(sum(this.stats.notes.local.total, this.stats.notes.remote.total))
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Local',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					hidden: true,
 | 
					 | 
				
			||||||
					data: this.format(this.stats.notes.local.total)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Remote',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					hidden: true,
 | 
					 | 
				
			||||||
					data: this.format(this.stats.notes.remote.total)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		usersChart(total: boolean): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'Combined',
 | 
					 | 
				
			||||||
					type: 'line',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(total
 | 
					 | 
				
			||||||
						? sum(this.stats.users.local.total, this.stats.users.remote.total)
 | 
					 | 
				
			||||||
						: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec), this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Local',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					hidden: true,
 | 
					 | 
				
			||||||
					data: this.format(total
 | 
					 | 
				
			||||||
						? this.stats.users.local.total
 | 
					 | 
				
			||||||
						: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec))
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Remote',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					hidden: true,
 | 
					 | 
				
			||||||
					data: this.format(total
 | 
					 | 
				
			||||||
						? this.stats.users.remote.total
 | 
					 | 
				
			||||||
						: sum(this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		activeUsersChart(): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'Combined',
 | 
					 | 
				
			||||||
					type: 'line',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(sum(this.stats.activeUsers.local.count, this.stats.activeUsers.remote.count))
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Local',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					hidden: true,
 | 
					 | 
				
			||||||
					data: this.format(this.stats.activeUsers.local.count)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Remote',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					hidden: true,
 | 
					 | 
				
			||||||
					data: this.format(this.stats.activeUsers.remote.count)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		driveChart(): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				bytes: true,
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'All',
 | 
					 | 
				
			||||||
					type: 'line',
 | 
					 | 
				
			||||||
					color: '#09d8e2',
 | 
					 | 
				
			||||||
					borderDash: [5, 5],
 | 
					 | 
				
			||||||
					fill: false,
 | 
					 | 
				
			||||||
					data: this.format(
 | 
					 | 
				
			||||||
						sum(
 | 
					 | 
				
			||||||
							this.stats.drive.local.incSize,
 | 
					 | 
				
			||||||
							negate(this.stats.drive.local.decSize),
 | 
					 | 
				
			||||||
							this.stats.drive.remote.incSize,
 | 
					 | 
				
			||||||
							negate(this.stats.drive.remote.decSize)
 | 
					 | 
				
			||||||
						)
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Local +',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(this.stats.drive.local.incSize)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Local -',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#FF4560',
 | 
					 | 
				
			||||||
					data: this.format(negate(this.stats.drive.local.decSize))
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Remote +',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#00E396',
 | 
					 | 
				
			||||||
					data: this.format(this.stats.drive.remote.incSize)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Remote -',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#FEB019',
 | 
					 | 
				
			||||||
					data: this.format(negate(this.stats.drive.remote.decSize))
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		driveTotalChart(): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				bytes: true,
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'Combined',
 | 
					 | 
				
			||||||
					type: 'line',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize))
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Local',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					hidden: true,
 | 
					 | 
				
			||||||
					data: this.format(this.stats.drive.local.totalSize)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Remote',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					hidden: true,
 | 
					 | 
				
			||||||
					data: this.format(this.stats.drive.remote.totalSize)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		driveFilesChart(): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'All',
 | 
					 | 
				
			||||||
					type: 'line',
 | 
					 | 
				
			||||||
					color: '#09d8e2',
 | 
					 | 
				
			||||||
					borderDash: [5, 5],
 | 
					 | 
				
			||||||
					fill: false,
 | 
					 | 
				
			||||||
					data: this.format(
 | 
					 | 
				
			||||||
						sum(
 | 
					 | 
				
			||||||
							this.stats.drive.local.incCount,
 | 
					 | 
				
			||||||
							negate(this.stats.drive.local.decCount),
 | 
					 | 
				
			||||||
							this.stats.drive.remote.incCount,
 | 
					 | 
				
			||||||
							negate(this.stats.drive.remote.decCount)
 | 
					 | 
				
			||||||
						)
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Local +',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(this.stats.drive.local.incCount)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Local -',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#FF4560',
 | 
					 | 
				
			||||||
					data: this.format(negate(this.stats.drive.local.decCount))
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Remote +',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#00E396',
 | 
					 | 
				
			||||||
					data: this.format(this.stats.drive.remote.incCount)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Remote -',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#FEB019',
 | 
					 | 
				
			||||||
					data: this.format(negate(this.stats.drive.remote.decCount))
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		driveFilesTotalChart(): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'Combined',
 | 
					 | 
				
			||||||
					type: 'line',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(sum(this.stats.drive.local.totalCount, this.stats.drive.remote.totalCount))
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Local',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					hidden: true,
 | 
					 | 
				
			||||||
					data: this.format(this.stats.drive.local.totalCount)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Remote',
 | 
					 | 
				
			||||||
					type: 'area',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					hidden: true,
 | 
					 | 
				
			||||||
					data: this.format(this.stats.drive.remote.totalCount)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		number
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										47
									
								
								src/client/components/number-diff.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/client/components/number-diff.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,47 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<span class="ceaaebcd" :class="{ isPlus, isMinus, isZero }">
 | 
				
			||||||
 | 
						<slot name="before"></slot>{{ isPlus ? '+' : isMinus ? '-' : '' }}{{ number(value) }}<slot name="after"></slot>
 | 
				
			||||||
 | 
					</span>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import { computed, defineComponent } from 'vue';
 | 
				
			||||||
 | 
					import number from '@client/filters/number';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default defineComponent({
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							value: {
 | 
				
			||||||
 | 
								type: Number,
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						setup(props) {
 | 
				
			||||||
 | 
							const isPlus = computed(() => props.value > 0);
 | 
				
			||||||
 | 
							const isMinus = computed(() => props.value < 0);
 | 
				
			||||||
 | 
							const isZero = computed(() => props.value === 0);
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								isPlus,
 | 
				
			||||||
 | 
								isMinus,
 | 
				
			||||||
 | 
								isZero,
 | 
				
			||||||
 | 
								number,
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
 | 
					.ceaaebcd {
 | 
				
			||||||
 | 
						&.isPlus {
 | 
				
			||||||
 | 
							color: var(--success);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&.isMinus {
 | 
				
			||||||
 | 
							color: var(--error);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&.isZero {
 | 
				
			||||||
 | 
							opacity: 0.5;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -65,17 +65,17 @@
 | 
				
			||||||
			<div class="_debobigegoPanel cmhjzshl">
 | 
								<div class="_debobigegoPanel cmhjzshl">
 | 
				
			||||||
				<div class="selects">
 | 
									<div class="selects">
 | 
				
			||||||
					<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
 | 
										<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
 | 
				
			||||||
						<option value="requests">{{ $ts._instanceCharts.requests }}</option>
 | 
											<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
 | 
				
			||||||
						<option value="users">{{ $ts._instanceCharts.users }}</option>
 | 
											<option value="instance-users">{{ $ts._instanceCharts.users }}</option>
 | 
				
			||||||
						<option value="users-total">{{ $ts._instanceCharts.usersTotal }}</option>
 | 
											<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
 | 
				
			||||||
						<option value="notes">{{ $ts._instanceCharts.notes }}</option>
 | 
											<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
 | 
				
			||||||
						<option value="notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
 | 
											<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
 | 
				
			||||||
						<option value="ff">{{ $ts._instanceCharts.ff }}</option>
 | 
											<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
 | 
				
			||||||
						<option value="ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
 | 
											<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
 | 
				
			||||||
						<option value="drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
 | 
											<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
 | 
				
			||||||
						<option value="drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
 | 
											<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
 | 
				
			||||||
						<option value="drive-files">{{ $ts._instanceCharts.files }}</option>
 | 
											<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
 | 
				
			||||||
						<option value="drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
 | 
											<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
 | 
				
			||||||
					</MkSelect>
 | 
										</MkSelect>
 | 
				
			||||||
					<MkSelect v-model="chartSpan" style="margin: 0;">
 | 
										<MkSelect v-model="chartSpan" style="margin: 0;">
 | 
				
			||||||
						<option value="hour">{{ $ts.perHour }}</option>
 | 
											<option value="hour">{{ $ts.perHour }}</option>
 | 
				
			||||||
| 
						 | 
					@ -83,7 +83,7 @@
 | 
				
			||||||
					</MkSelect>
 | 
										</MkSelect>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
				<div class="chart">
 | 
									<div class="chart">
 | 
				
			||||||
					<canvas :ref="setChart"></canvas>
 | 
										<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
| 
						 | 
					@ -135,7 +135,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { defineAsyncComponent, defineComponent } from 'vue';
 | 
					import { defineAsyncComponent, defineComponent } from 'vue';
 | 
				
			||||||
import Chart from 'chart.js';
 | 
					import MkChart from '@client/components/chart.vue';
 | 
				
			||||||
import FormObjectView from '@client/components/debobigego/object-view.vue';
 | 
					import FormObjectView from '@client/components/debobigego/object-view.vue';
 | 
				
			||||||
import FormTextarea from '@client/components/debobigego/textarea.vue';
 | 
					import FormTextarea from '@client/components/debobigego/textarea.vue';
 | 
				
			||||||
import FormLink from '@client/components/debobigego/link.vue';
 | 
					import FormLink from '@client/components/debobigego/link.vue';
 | 
				
			||||||
| 
						 | 
					@ -151,17 +151,6 @@ import bytes from '@client/filters/bytes';
 | 
				
			||||||
import * as symbols from '@client/symbols';
 | 
					import * as symbols from '@client/symbols';
 | 
				
			||||||
import MkInstanceInfo from '@client/pages/instance/instance.vue';
 | 
					import MkInstanceInfo from '@client/pages/instance/instance.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const chartLimit = 90;
 | 
					 | 
				
			||||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
 | 
					 | 
				
			||||||
const negate = arr => arr.map(x => -x);
 | 
					 | 
				
			||||||
const alpha = hex => {
 | 
					 | 
				
			||||||
	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
 | 
					 | 
				
			||||||
	const r = parseInt(result[1], 16);
 | 
					 | 
				
			||||||
	const g = parseInt(result[2], 16);
 | 
					 | 
				
			||||||
	const b = parseInt(result[3], 16);
 | 
					 | 
				
			||||||
	return `rgba(${r}, ${g}, ${b}, 0.1)`;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
	components: {
 | 
						components: {
 | 
				
			||||||
		FormBase,
 | 
							FormBase,
 | 
				
			||||||
| 
						 | 
					@ -173,6 +162,7 @@ export default defineComponent({
 | 
				
			||||||
		FormKeyValueView,
 | 
							FormKeyValueView,
 | 
				
			||||||
		FormSuspense,
 | 
							FormSuspense,
 | 
				
			||||||
		MkSelect,
 | 
							MkSelect,
 | 
				
			||||||
 | 
							MkChart,
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	props: {
 | 
						props: {
 | 
				
			||||||
| 
						 | 
					@ -199,53 +189,11 @@ export default defineComponent({
 | 
				
			||||||
			dnsPromiseFactory: () => os.api('federation/dns', {
 | 
								dnsPromiseFactory: () => os.api('federation/dns', {
 | 
				
			||||||
				host: this.host
 | 
									host: this.host
 | 
				
			||||||
			}),
 | 
								}),
 | 
				
			||||||
			now: null,
 | 
								chartSrc: 'instance-requests',
 | 
				
			||||||
			canvas: null,
 | 
					 | 
				
			||||||
			chart: null,
 | 
					 | 
				
			||||||
			chartInstance: null,
 | 
					 | 
				
			||||||
			chartSrc: 'requests',
 | 
					 | 
				
			||||||
			chartSpan: 'hour',
 | 
								chartSpan: 'hour',
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	computed: {
 | 
					 | 
				
			||||||
		data(): any {
 | 
					 | 
				
			||||||
			if (this.chart == null) return null;
 | 
					 | 
				
			||||||
			switch (this.chartSrc) {
 | 
					 | 
				
			||||||
				case 'requests': return this.requestsChart();
 | 
					 | 
				
			||||||
				case 'users': return this.usersChart(false);
 | 
					 | 
				
			||||||
				case 'users-total': return this.usersChart(true);
 | 
					 | 
				
			||||||
				case 'notes': return this.notesChart(false);
 | 
					 | 
				
			||||||
				case 'notes-total': return this.notesChart(true);
 | 
					 | 
				
			||||||
				case 'ff': return this.ffChart(false);
 | 
					 | 
				
			||||||
				case 'ff-total': return this.ffChart(true);
 | 
					 | 
				
			||||||
				case 'drive-usage': return this.driveUsageChart(false);
 | 
					 | 
				
			||||||
				case 'drive-usage-total': return this.driveUsageChart(true);
 | 
					 | 
				
			||||||
				case 'drive-files': return this.driveFilesChart(false);
 | 
					 | 
				
			||||||
				case 'drive-files-total': return this.driveFilesChart(true);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		stats(): any[] {
 | 
					 | 
				
			||||||
			const stats =
 | 
					 | 
				
			||||||
				this.chartSpan == 'day' ? this.chart.perDay :
 | 
					 | 
				
			||||||
				this.chartSpan == 'hour' ? this.chart.perHour :
 | 
					 | 
				
			||||||
				null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			return stats;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	watch: {
 | 
					 | 
				
			||||||
		chartSrc() {
 | 
					 | 
				
			||||||
			this.renderChart();
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		chartSpan() {
 | 
					 | 
				
			||||||
			this.renderChart();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	mounted() {
 | 
						mounted() {
 | 
				
			||||||
		this.fetch();
 | 
							this.fetch();
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
| 
						 | 
					@ -258,190 +206,6 @@ export default defineComponent({
 | 
				
			||||||
			this.instance = await os.api('federation/show-instance', {
 | 
								this.instance = await os.api('federation/show-instance', {
 | 
				
			||||||
				host: this.host
 | 
									host: this.host
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					 | 
				
			||||||
			this.now = new Date();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			const [perHour, perDay] = await Promise.all([
 | 
					 | 
				
			||||||
				os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }),
 | 
					 | 
				
			||||||
				os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }),
 | 
					 | 
				
			||||||
			]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			const chart = {
 | 
					 | 
				
			||||||
				perHour: perHour,
 | 
					 | 
				
			||||||
				perDay: perDay
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			this.chart = chart;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			this.renderChart();
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		setChart(el) {
 | 
					 | 
				
			||||||
			this.canvas = el;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		renderChart() {
 | 
					 | 
				
			||||||
			if (this.chartInstance) {
 | 
					 | 
				
			||||||
				this.chartInstance.destroy();
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
 | 
					 | 
				
			||||||
			this.chartInstance = new Chart(this.canvas, {
 | 
					 | 
				
			||||||
				type: 'line',
 | 
					 | 
				
			||||||
				data: {
 | 
					 | 
				
			||||||
					labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
 | 
					 | 
				
			||||||
					datasets: this.data.series.map(x => ({
 | 
					 | 
				
			||||||
						label: x.name,
 | 
					 | 
				
			||||||
						data: x.data.slice().reverse(),
 | 
					 | 
				
			||||||
						pointRadius: 0,
 | 
					 | 
				
			||||||
						lineTension: 0,
 | 
					 | 
				
			||||||
						borderWidth: 2,
 | 
					 | 
				
			||||||
						borderColor: x.color,
 | 
					 | 
				
			||||||
						backgroundColor: alpha(x.color),
 | 
					 | 
				
			||||||
					}))
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				options: {
 | 
					 | 
				
			||||||
					aspectRatio: 2.5,
 | 
					 | 
				
			||||||
					layout: {
 | 
					 | 
				
			||||||
						padding: {
 | 
					 | 
				
			||||||
							left: 16,
 | 
					 | 
				
			||||||
							right: 16,
 | 
					 | 
				
			||||||
							top: 16,
 | 
					 | 
				
			||||||
							bottom: 16
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
					legend: {
 | 
					 | 
				
			||||||
						position: 'bottom',
 | 
					 | 
				
			||||||
						labels: {
 | 
					 | 
				
			||||||
							boxWidth: 16,
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
					scales: {
 | 
					 | 
				
			||||||
						xAxes: [{
 | 
					 | 
				
			||||||
							gridLines: {
 | 
					 | 
				
			||||||
								display: false
 | 
					 | 
				
			||||||
							},
 | 
					 | 
				
			||||||
							ticks: {
 | 
					 | 
				
			||||||
								display: false
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
						}],
 | 
					 | 
				
			||||||
						yAxes: [{
 | 
					 | 
				
			||||||
							position: 'right',
 | 
					 | 
				
			||||||
							ticks: {
 | 
					 | 
				
			||||||
								display: false
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
						}]
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
					tooltips: {
 | 
					 | 
				
			||||||
						intersect: false,
 | 
					 | 
				
			||||||
						mode: 'index',
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		getDate(ago: number) {
 | 
					 | 
				
			||||||
			const y = this.now.getFullYear();
 | 
					 | 
				
			||||||
			const m = this.now.getMonth();
 | 
					 | 
				
			||||||
			const d = this.now.getDate();
 | 
					 | 
				
			||||||
			const h = this.now.getHours();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		format(arr) {
 | 
					 | 
				
			||||||
			return arr;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		requestsChart(): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'In',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(this.stats.requests.received)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Out (succ)',
 | 
					 | 
				
			||||||
					color: '#00E396',
 | 
					 | 
				
			||||||
					data: this.format(this.stats.requests.succeeded)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Out (fail)',
 | 
					 | 
				
			||||||
					color: '#FEB019',
 | 
					 | 
				
			||||||
					data: this.format(this.stats.requests.failed)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		usersChart(total: boolean): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'Users',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(total
 | 
					 | 
				
			||||||
						? this.stats.users.total
 | 
					 | 
				
			||||||
						: sum(this.stats.users.inc, negate(this.stats.users.dec))
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		notesChart(total: boolean): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'Notes',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(total
 | 
					 | 
				
			||||||
						? this.stats.notes.total
 | 
					 | 
				
			||||||
						: sum(this.stats.notes.inc, negate(this.stats.notes.dec))
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		ffChart(total: boolean): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'Following',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(total
 | 
					 | 
				
			||||||
						? this.stats.following.total
 | 
					 | 
				
			||||||
						: sum(this.stats.following.inc, negate(this.stats.following.dec))
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Followers',
 | 
					 | 
				
			||||||
					color: '#00E396',
 | 
					 | 
				
			||||||
					data: this.format(total
 | 
					 | 
				
			||||||
						? this.stats.followers.total
 | 
					 | 
				
			||||||
						: sum(this.stats.followers.inc, negate(this.stats.followers.dec))
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		driveUsageChart(total: boolean): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				bytes: true,
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'Drive usage',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(total
 | 
					 | 
				
			||||||
						? this.stats.drive.totalUsage
 | 
					 | 
				
			||||||
						: sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage))
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		driveFilesChart(total: boolean): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'Drive files',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(total
 | 
					 | 
				
			||||||
						? this.stats.drive.totalFiles
 | 
					 | 
				
			||||||
						: sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles))
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		info() {
 | 
							info() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -78,17 +78,17 @@
 | 
				
			||||||
				<span class="label">{{ $ts.charts }}</span>
 | 
									<span class="label">{{ $ts.charts }}</span>
 | 
				
			||||||
				<div class="selects">
 | 
									<div class="selects">
 | 
				
			||||||
					<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
 | 
										<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
 | 
				
			||||||
						<option value="requests">{{ $ts._instanceCharts.requests }}</option>
 | 
											<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
 | 
				
			||||||
						<option value="users">{{ $ts._instanceCharts.users }}</option>
 | 
											<option value="instance-users">{{ $ts._instanceCharts.users }}</option>
 | 
				
			||||||
						<option value="users-total">{{ $ts._instanceCharts.usersTotal }}</option>
 | 
											<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
 | 
				
			||||||
						<option value="notes">{{ $ts._instanceCharts.notes }}</option>
 | 
											<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
 | 
				
			||||||
						<option value="notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
 | 
											<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
 | 
				
			||||||
						<option value="ff">{{ $ts._instanceCharts.ff }}</option>
 | 
											<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
 | 
				
			||||||
						<option value="ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
 | 
											<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
 | 
				
			||||||
						<option value="drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
 | 
											<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
 | 
				
			||||||
						<option value="drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
 | 
											<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
 | 
				
			||||||
						<option value="drive-files">{{ $ts._instanceCharts.files }}</option>
 | 
											<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
 | 
				
			||||||
						<option value="drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
 | 
											<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
 | 
				
			||||||
					</MkSelect>
 | 
										</MkSelect>
 | 
				
			||||||
					<MkSelect v-model="chartSpan" style="margin: 0;">
 | 
										<MkSelect v-model="chartSpan" style="margin: 0;">
 | 
				
			||||||
						<option value="hour">{{ $ts.perHour }}</option>
 | 
											<option value="hour">{{ $ts.perHour }}</option>
 | 
				
			||||||
| 
						 | 
					@ -97,7 +97,7 @@
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div class="chart">
 | 
								<div class="chart">
 | 
				
			||||||
				<canvas :ref="setChart"></canvas>
 | 
									<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<div class="operations section">
 | 
							<div class="operations section">
 | 
				
			||||||
| 
						 | 
					@ -124,28 +124,17 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { defineComponent, markRaw } from 'vue';
 | 
					import { defineComponent, markRaw } from 'vue';
 | 
				
			||||||
import Chart from 'chart.js';
 | 
					 | 
				
			||||||
import XModalWindow from '@client/components/ui/modal-window.vue';
 | 
					import XModalWindow from '@client/components/ui/modal-window.vue';
 | 
				
			||||||
import MkUsersDialog from '@client/components/users-dialog.vue';
 | 
					import MkUsersDialog from '@client/components/users-dialog.vue';
 | 
				
			||||||
import MkSelect from '@client/components/form/select.vue';
 | 
					import MkSelect from '@client/components/form/select.vue';
 | 
				
			||||||
import MkButton from '@client/components/ui/button.vue';
 | 
					import MkButton from '@client/components/ui/button.vue';
 | 
				
			||||||
import MkSwitch from '@client/components/form/switch.vue';
 | 
					import MkSwitch from '@client/components/form/switch.vue';
 | 
				
			||||||
import MkInfo from '@client/components/ui/info.vue';
 | 
					import MkInfo from '@client/components/ui/info.vue';
 | 
				
			||||||
 | 
					import MkChart from '@client/components/chart.vue';
 | 
				
			||||||
import bytes from '@client/filters/bytes';
 | 
					import bytes from '@client/filters/bytes';
 | 
				
			||||||
import number from '@client/filters/number';
 | 
					import number from '@client/filters/number';
 | 
				
			||||||
import * as os from '@client/os';
 | 
					import * as os from '@client/os';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const chartLimit = 90;
 | 
					 | 
				
			||||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
 | 
					 | 
				
			||||||
const negate = arr => arr.map(x => -x);
 | 
					 | 
				
			||||||
const alpha = hex => {
 | 
					 | 
				
			||||||
	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
 | 
					 | 
				
			||||||
	const r = parseInt(result[1], 16);
 | 
					 | 
				
			||||||
	const g = parseInt(result[2], 16);
 | 
					 | 
				
			||||||
	const b = parseInt(result[3], 16);
 | 
					 | 
				
			||||||
	return `rgba(${r}, ${g}, ${b}, 0.1)`;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
	components: {
 | 
						components: {
 | 
				
			||||||
		XModalWindow,
 | 
							XModalWindow,
 | 
				
			||||||
| 
						 | 
					@ -153,6 +142,7 @@ export default defineComponent({
 | 
				
			||||||
		MkButton,
 | 
							MkButton,
 | 
				
			||||||
		MkSwitch,
 | 
							MkSwitch,
 | 
				
			||||||
		MkInfo,
 | 
							MkInfo,
 | 
				
			||||||
 | 
							MkChart,
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	props: {
 | 
						props: {
 | 
				
			||||||
| 
						 | 
					@ -167,42 +157,12 @@ export default defineComponent({
 | 
				
			||||||
	data() {
 | 
						data() {
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			isSuspended: this.instance.isSuspended,
 | 
								isSuspended: this.instance.isSuspended,
 | 
				
			||||||
			now: null,
 | 
					 | 
				
			||||||
			canvas: null,
 | 
					 | 
				
			||||||
			chart: null,
 | 
					 | 
				
			||||||
			chartInstance: null,
 | 
					 | 
				
			||||||
			chartSrc: 'requests',
 | 
								chartSrc: 'requests',
 | 
				
			||||||
			chartSpan: 'hour',
 | 
								chartSpan: 'hour',
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	computed: {
 | 
						computed: {
 | 
				
			||||||
		data(): any {
 | 
					 | 
				
			||||||
			if (this.chart == null) return null;
 | 
					 | 
				
			||||||
			switch (this.chartSrc) {
 | 
					 | 
				
			||||||
				case 'requests': return this.requestsChart();
 | 
					 | 
				
			||||||
				case 'users': return this.usersChart(false);
 | 
					 | 
				
			||||||
				case 'users-total': return this.usersChart(true);
 | 
					 | 
				
			||||||
				case 'notes': return this.notesChart(false);
 | 
					 | 
				
			||||||
				case 'notes-total': return this.notesChart(true);
 | 
					 | 
				
			||||||
				case 'ff': return this.ffChart(false);
 | 
					 | 
				
			||||||
				case 'ff-total': return this.ffChart(true);
 | 
					 | 
				
			||||||
				case 'drive-usage': return this.driveUsageChart(false);
 | 
					 | 
				
			||||||
				case 'drive-usage-total': return this.driveUsageChart(true);
 | 
					 | 
				
			||||||
				case 'drive-files': return this.driveFilesChart(false);
 | 
					 | 
				
			||||||
				case 'drive-files-total': return this.driveFilesChart(true);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		stats(): any[] {
 | 
					 | 
				
			||||||
			const stats =
 | 
					 | 
				
			||||||
				this.chartSpan == 'day' ? this.chart.perDay :
 | 
					 | 
				
			||||||
				this.chartSpan == 'hour' ? this.chart.perHour :
 | 
					 | 
				
			||||||
				null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			return stats;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		meta() {
 | 
							meta() {
 | 
				
			||||||
			return this.$instance;
 | 
								return this.$instance;
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
| 
						 | 
					@ -219,49 +179,15 @@ export default defineComponent({
 | 
				
			||||||
				isSuspended: this.isSuspended
 | 
									isSuspended: this.isSuspended
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					 | 
				
			||||||
		chartSrc() {
 | 
					 | 
				
			||||||
			this.renderChart();
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		chartSpan() {
 | 
					 | 
				
			||||||
			this.renderChart();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async created() {
 | 
					 | 
				
			||||||
		this.now = new Date();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const [perHour, perDay] = await Promise.all([
 | 
					 | 
				
			||||||
			os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }),
 | 
					 | 
				
			||||||
			os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }),
 | 
					 | 
				
			||||||
		]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const chart = {
 | 
					 | 
				
			||||||
			perHour: perHour,
 | 
					 | 
				
			||||||
			perDay: perDay
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		this.chart = chart;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		this.renderChart();
 | 
					 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		setChart(el) {
 | 
					 | 
				
			||||||
			this.canvas = el;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		changeBlock(e) {
 | 
							changeBlock(e) {
 | 
				
			||||||
			os.api('admin/update-meta', {
 | 
								os.api('admin/update-meta', {
 | 
				
			||||||
				blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
 | 
									blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		setSrc(src) {
 | 
					 | 
				
			||||||
			this.chartSrc = src;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		removeAllFollowing() {
 | 
							removeAllFollowing() {
 | 
				
			||||||
			os.apiWithDialog('admin/federation/remove-all-following', {
 | 
								os.apiWithDialog('admin/federation/remove-all-following', {
 | 
				
			||||||
				host: this.instance.host
 | 
									host: this.instance.host
 | 
				
			||||||
| 
						 | 
					@ -274,170 +200,6 @@ export default defineComponent({
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		renderChart() {
 | 
					 | 
				
			||||||
			if (this.chartInstance) {
 | 
					 | 
				
			||||||
				this.chartInstance.destroy();
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
 | 
					 | 
				
			||||||
			this.chartInstance = markRaw(new Chart(this.canvas, {
 | 
					 | 
				
			||||||
				type: 'line',
 | 
					 | 
				
			||||||
				data: {
 | 
					 | 
				
			||||||
					labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
 | 
					 | 
				
			||||||
					datasets: this.data.series.map(x => ({
 | 
					 | 
				
			||||||
						label: x.name,
 | 
					 | 
				
			||||||
						data: x.data.slice().reverse(),
 | 
					 | 
				
			||||||
						pointRadius: 0,
 | 
					 | 
				
			||||||
						lineTension: 0,
 | 
					 | 
				
			||||||
						borderWidth: 2,
 | 
					 | 
				
			||||||
						borderColor: x.color,
 | 
					 | 
				
			||||||
						backgroundColor: alpha(x.color),
 | 
					 | 
				
			||||||
					}))
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				options: {
 | 
					 | 
				
			||||||
					aspectRatio: 2.5,
 | 
					 | 
				
			||||||
					layout: {
 | 
					 | 
				
			||||||
						padding: {
 | 
					 | 
				
			||||||
							left: 16,
 | 
					 | 
				
			||||||
							right: 16,
 | 
					 | 
				
			||||||
							top: 16,
 | 
					 | 
				
			||||||
							bottom: 0
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
					legend: {
 | 
					 | 
				
			||||||
						position: 'bottom',
 | 
					 | 
				
			||||||
						labels: {
 | 
					 | 
				
			||||||
							boxWidth: 16,
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
					scales: {
 | 
					 | 
				
			||||||
						xAxes: [{
 | 
					 | 
				
			||||||
							gridLines: {
 | 
					 | 
				
			||||||
								display: false
 | 
					 | 
				
			||||||
							},
 | 
					 | 
				
			||||||
							ticks: {
 | 
					 | 
				
			||||||
								display: false
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
						}],
 | 
					 | 
				
			||||||
						yAxes: [{
 | 
					 | 
				
			||||||
							position: 'right',
 | 
					 | 
				
			||||||
							ticks: {
 | 
					 | 
				
			||||||
								display: false
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
						}]
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
					tooltips: {
 | 
					 | 
				
			||||||
						intersect: false,
 | 
					 | 
				
			||||||
						mode: 'index',
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}));
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		getDate(ago: number) {
 | 
					 | 
				
			||||||
			const y = this.now.getFullYear();
 | 
					 | 
				
			||||||
			const m = this.now.getMonth();
 | 
					 | 
				
			||||||
			const d = this.now.getDate();
 | 
					 | 
				
			||||||
			const h = this.now.getHours();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		format(arr) {
 | 
					 | 
				
			||||||
			return arr;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		requestsChart(): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'In',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(this.stats.requests.received)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Out (succ)',
 | 
					 | 
				
			||||||
					color: '#00E396',
 | 
					 | 
				
			||||||
					data: this.format(this.stats.requests.succeeded)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Out (fail)',
 | 
					 | 
				
			||||||
					color: '#FEB019',
 | 
					 | 
				
			||||||
					data: this.format(this.stats.requests.failed)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		usersChart(total: boolean): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'Users',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(total
 | 
					 | 
				
			||||||
						? this.stats.users.total
 | 
					 | 
				
			||||||
						: sum(this.stats.users.inc, negate(this.stats.users.dec))
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		notesChart(total: boolean): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'Notes',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(total
 | 
					 | 
				
			||||||
						? this.stats.notes.total
 | 
					 | 
				
			||||||
						: sum(this.stats.notes.inc, negate(this.stats.notes.dec))
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		ffChart(total: boolean): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'Following',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(total
 | 
					 | 
				
			||||||
						? this.stats.following.total
 | 
					 | 
				
			||||||
						: sum(this.stats.following.inc, negate(this.stats.following.dec))
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					name: 'Followers',
 | 
					 | 
				
			||||||
					color: '#00E396',
 | 
					 | 
				
			||||||
					data: this.format(total
 | 
					 | 
				
			||||||
						? this.stats.followers.total
 | 
					 | 
				
			||||||
						: sum(this.stats.followers.inc, negate(this.stats.followers.dec))
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		driveUsageChart(total: boolean): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				bytes: true,
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'Drive usage',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(total
 | 
					 | 
				
			||||||
						? this.stats.drive.totalUsage
 | 
					 | 
				
			||||||
						: sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage))
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		driveFilesChart(total: boolean): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				series: [{
 | 
					 | 
				
			||||||
					name: 'Drive files',
 | 
					 | 
				
			||||||
					color: '#008FFB',
 | 
					 | 
				
			||||||
					data: this.format(total
 | 
					 | 
				
			||||||
						? this.stats.drive.totalFiles
 | 
					 | 
				
			||||||
						: sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles))
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				}]
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		showFollowing() {
 | 
							showFollowing() {
 | 
				
			||||||
			os.modal(MkUsersDialog, {
 | 
								os.modal(MkUsersDialog, {
 | 
				
			||||||
				title: this.$ts.instanceFollowing,
 | 
									title: this.$ts.instanceFollowing,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -52,7 +52,21 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { defineComponent, markRaw } from 'vue';
 | 
					import { defineComponent, markRaw } from 'vue';
 | 
				
			||||||
import Chart from 'chart.js';
 | 
					import {
 | 
				
			||||||
 | 
					  Chart,
 | 
				
			||||||
 | 
					  ArcElement,
 | 
				
			||||||
 | 
					  LineElement,
 | 
				
			||||||
 | 
					  BarElement,
 | 
				
			||||||
 | 
					  PointElement,
 | 
				
			||||||
 | 
					  BarController,
 | 
				
			||||||
 | 
					  LineController,
 | 
				
			||||||
 | 
					  CategoryScale,
 | 
				
			||||||
 | 
					  LinearScale,
 | 
				
			||||||
 | 
					  Legend,
 | 
				
			||||||
 | 
					  Title,
 | 
				
			||||||
 | 
					  Tooltip,
 | 
				
			||||||
 | 
					  SubTitle
 | 
				
			||||||
 | 
					} from 'chart.js';
 | 
				
			||||||
import MkButton from '@client/components/ui/button.vue';
 | 
					import MkButton from '@client/components/ui/button.vue';
 | 
				
			||||||
import MkSelect from '@client/components/form/select.vue';
 | 
					import MkSelect from '@client/components/form/select.vue';
 | 
				
			||||||
import MkInput from '@client/components/form/input.vue';
 | 
					import MkInput from '@client/components/form/input.vue';
 | 
				
			||||||
| 
						 | 
					@ -64,6 +78,21 @@ import bytes from '@client/filters/bytes';
 | 
				
			||||||
import number from '@client/filters/number';
 | 
					import number from '@client/filters/number';
 | 
				
			||||||
import MkInstanceInfo from './instance.vue';
 | 
					import MkInstanceInfo from './instance.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Chart.register(
 | 
				
			||||||
 | 
					  ArcElement,
 | 
				
			||||||
 | 
					  LineElement,
 | 
				
			||||||
 | 
					  BarElement,
 | 
				
			||||||
 | 
					  PointElement,
 | 
				
			||||||
 | 
					  BarController,
 | 
				
			||||||
 | 
					  LineController,
 | 
				
			||||||
 | 
					  CategoryScale,
 | 
				
			||||||
 | 
					  LinearScale,
 | 
				
			||||||
 | 
					  Legend,
 | 
				
			||||||
 | 
					  Title,
 | 
				
			||||||
 | 
					  Tooltip,
 | 
				
			||||||
 | 
					  SubTitle
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const alpha = (hex, a) => {
 | 
					const alpha = (hex, a) => {
 | 
				
			||||||
	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
 | 
						const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
 | 
				
			||||||
	const r = parseInt(result[1], 16);
 | 
						const r = parseInt(result[1], 16);
 | 
				
			||||||
| 
						 | 
					@ -116,7 +145,7 @@ export default defineComponent({
 | 
				
			||||||
	mounted() {
 | 
						mounted() {
 | 
				
			||||||
		this.fetchJobs();
 | 
							this.fetchJobs();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
 | 
							Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		os.api('admin/server-info', {}).then(res => {
 | 
							os.api('admin/server-info', {}).then(res => {
 | 
				
			||||||
			this.serverInfo = res;
 | 
								this.serverInfo = res;
 | 
				
			||||||
| 
						 | 
					@ -157,7 +186,7 @@ export default defineComponent({
 | 
				
			||||||
					datasets: [{
 | 
										datasets: [{
 | 
				
			||||||
						label: 'CPU',
 | 
											label: 'CPU',
 | 
				
			||||||
						pointRadius: 0,
 | 
											pointRadius: 0,
 | 
				
			||||||
						lineTension: 0,
 | 
											tension: 0,
 | 
				
			||||||
						borderWidth: 2,
 | 
											borderWidth: 2,
 | 
				
			||||||
						borderColor: '#86b300',
 | 
											borderColor: '#86b300',
 | 
				
			||||||
						backgroundColor: alpha('#86b300', 0.1),
 | 
											backgroundColor: alpha('#86b300', 0.1),
 | 
				
			||||||
| 
						 | 
					@ -165,7 +194,7 @@ export default defineComponent({
 | 
				
			||||||
					}, {
 | 
										}, {
 | 
				
			||||||
						label: 'MEM (active)',
 | 
											label: 'MEM (active)',
 | 
				
			||||||
						pointRadius: 0,
 | 
											pointRadius: 0,
 | 
				
			||||||
						lineTension: 0,
 | 
											tension: 0,
 | 
				
			||||||
						borderWidth: 2,
 | 
											borderWidth: 2,
 | 
				
			||||||
						borderColor: '#935dbf',
 | 
											borderColor: '#935dbf',
 | 
				
			||||||
						backgroundColor: alpha('#935dbf', 0.02),
 | 
											backgroundColor: alpha('#935dbf', 0.02),
 | 
				
			||||||
| 
						 | 
					@ -173,7 +202,7 @@ export default defineComponent({
 | 
				
			||||||
					}, {
 | 
										}, {
 | 
				
			||||||
						label: 'MEM (used)',
 | 
											label: 'MEM (used)',
 | 
				
			||||||
						pointRadius: 0,
 | 
											pointRadius: 0,
 | 
				
			||||||
						lineTension: 0,
 | 
											tension: 0,
 | 
				
			||||||
						borderWidth: 2,
 | 
											borderWidth: 2,
 | 
				
			||||||
						borderColor: '#935dbf',
 | 
											borderColor: '#935dbf',
 | 
				
			||||||
						borderDash: [5, 5],
 | 
											borderDash: [5, 5],
 | 
				
			||||||
| 
						 | 
					@ -198,7 +227,7 @@ export default defineComponent({
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
					scales: {
 | 
										scales: {
 | 
				
			||||||
						xAxes: [{
 | 
											x: {
 | 
				
			||||||
							gridLines: {
 | 
												gridLines: {
 | 
				
			||||||
								display: false,
 | 
													display: false,
 | 
				
			||||||
								color: this.gridColor,
 | 
													color: this.gridColor,
 | 
				
			||||||
| 
						 | 
					@ -207,8 +236,8 @@ export default defineComponent({
 | 
				
			||||||
							ticks: {
 | 
												ticks: {
 | 
				
			||||||
								display: false,
 | 
													display: false,
 | 
				
			||||||
							}
 | 
												}
 | 
				
			||||||
						}],
 | 
											},
 | 
				
			||||||
						yAxes: [{
 | 
											y: {
 | 
				
			||||||
							position: 'right',
 | 
												position: 'right',
 | 
				
			||||||
							gridLines: {
 | 
												gridLines: {
 | 
				
			||||||
								display: true,
 | 
													display: true,
 | 
				
			||||||
| 
						 | 
					@ -219,7 +248,7 @@ export default defineComponent({
 | 
				
			||||||
								display: false,
 | 
													display: false,
 | 
				
			||||||
								max: 100
 | 
													max: 100
 | 
				
			||||||
							}
 | 
												}
 | 
				
			||||||
						}]
 | 
											}
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
					tooltips: {
 | 
										tooltips: {
 | 
				
			||||||
						intersect: false,
 | 
											intersect: false,
 | 
				
			||||||
| 
						 | 
					@ -238,7 +267,7 @@ export default defineComponent({
 | 
				
			||||||
					datasets: [{
 | 
										datasets: [{
 | 
				
			||||||
						label: 'In',
 | 
											label: 'In',
 | 
				
			||||||
						pointRadius: 0,
 | 
											pointRadius: 0,
 | 
				
			||||||
						lineTension: 0,
 | 
											tension: 0,
 | 
				
			||||||
						borderWidth: 2,
 | 
											borderWidth: 2,
 | 
				
			||||||
						borderColor: '#94a029',
 | 
											borderColor: '#94a029',
 | 
				
			||||||
						backgroundColor: alpha('#94a029', 0.1),
 | 
											backgroundColor: alpha('#94a029', 0.1),
 | 
				
			||||||
| 
						 | 
					@ -246,7 +275,7 @@ export default defineComponent({
 | 
				
			||||||
					}, {
 | 
										}, {
 | 
				
			||||||
						label: 'Out',
 | 
											label: 'Out',
 | 
				
			||||||
						pointRadius: 0,
 | 
											pointRadius: 0,
 | 
				
			||||||
						lineTension: 0,
 | 
											tension: 0,
 | 
				
			||||||
						borderWidth: 2,
 | 
											borderWidth: 2,
 | 
				
			||||||
						borderColor: '#ff9156',
 | 
											borderColor: '#ff9156',
 | 
				
			||||||
						backgroundColor: alpha('#ff9156', 0.1),
 | 
											backgroundColor: alpha('#ff9156', 0.1),
 | 
				
			||||||
| 
						 | 
					@ -270,7 +299,7 @@ export default defineComponent({
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
					scales: {
 | 
										scales: {
 | 
				
			||||||
						xAxes: [{
 | 
											x: {
 | 
				
			||||||
							gridLines: {
 | 
												gridLines: {
 | 
				
			||||||
								display: false,
 | 
													display: false,
 | 
				
			||||||
								color: this.gridColor,
 | 
													color: this.gridColor,
 | 
				
			||||||
| 
						 | 
					@ -279,8 +308,8 @@ export default defineComponent({
 | 
				
			||||||
							ticks: {
 | 
												ticks: {
 | 
				
			||||||
								display: false
 | 
													display: false
 | 
				
			||||||
							}
 | 
												}
 | 
				
			||||||
						}],
 | 
											},
 | 
				
			||||||
						yAxes: [{
 | 
											y: {
 | 
				
			||||||
							position: 'right',
 | 
												position: 'right',
 | 
				
			||||||
							gridLines: {
 | 
												gridLines: {
 | 
				
			||||||
								display: true,
 | 
													display: true,
 | 
				
			||||||
| 
						 | 
					@ -290,7 +319,7 @@ export default defineComponent({
 | 
				
			||||||
							ticks: {
 | 
												ticks: {
 | 
				
			||||||
								display: false,
 | 
													display: false,
 | 
				
			||||||
							}
 | 
												}
 | 
				
			||||||
						}]
 | 
											}
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
					tooltips: {
 | 
										tooltips: {
 | 
				
			||||||
						intersect: false,
 | 
											intersect: false,
 | 
				
			||||||
| 
						 | 
					@ -309,7 +338,7 @@ export default defineComponent({
 | 
				
			||||||
					datasets: [{
 | 
										datasets: [{
 | 
				
			||||||
						label: 'Read',
 | 
											label: 'Read',
 | 
				
			||||||
						pointRadius: 0,
 | 
											pointRadius: 0,
 | 
				
			||||||
						lineTension: 0,
 | 
											tension: 0,
 | 
				
			||||||
						borderWidth: 2,
 | 
											borderWidth: 2,
 | 
				
			||||||
						borderColor: '#94a029',
 | 
											borderColor: '#94a029',
 | 
				
			||||||
						backgroundColor: alpha('#94a029', 0.1),
 | 
											backgroundColor: alpha('#94a029', 0.1),
 | 
				
			||||||
| 
						 | 
					@ -317,7 +346,7 @@ export default defineComponent({
 | 
				
			||||||
					}, {
 | 
										}, {
 | 
				
			||||||
						label: 'Write',
 | 
											label: 'Write',
 | 
				
			||||||
						pointRadius: 0,
 | 
											pointRadius: 0,
 | 
				
			||||||
						lineTension: 0,
 | 
											tension: 0,
 | 
				
			||||||
						borderWidth: 2,
 | 
											borderWidth: 2,
 | 
				
			||||||
						borderColor: '#ff9156',
 | 
											borderColor: '#ff9156',
 | 
				
			||||||
						backgroundColor: alpha('#ff9156', 0.1),
 | 
											backgroundColor: alpha('#ff9156', 0.1),
 | 
				
			||||||
| 
						 | 
					@ -341,7 +370,7 @@ export default defineComponent({
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
					scales: {
 | 
										scales: {
 | 
				
			||||||
						xAxes: [{
 | 
											x: {
 | 
				
			||||||
							gridLines: {
 | 
												gridLines: {
 | 
				
			||||||
								display: false,
 | 
													display: false,
 | 
				
			||||||
								color: this.gridColor,
 | 
													color: this.gridColor,
 | 
				
			||||||
| 
						 | 
					@ -350,8 +379,8 @@ export default defineComponent({
 | 
				
			||||||
							ticks: {
 | 
												ticks: {
 | 
				
			||||||
								display: false
 | 
													display: false
 | 
				
			||||||
							}
 | 
												}
 | 
				
			||||||
						}],
 | 
											},
 | 
				
			||||||
						yAxes: [{
 | 
											y: {
 | 
				
			||||||
							position: 'right',
 | 
												position: 'right',
 | 
				
			||||||
							gridLines: {
 | 
												gridLines: {
 | 
				
			||||||
								display: true,
 | 
													display: true,
 | 
				
			||||||
| 
						 | 
					@ -361,7 +390,7 @@ export default defineComponent({
 | 
				
			||||||
							ticks: {
 | 
												ticks: {
 | 
				
			||||||
								display: false,
 | 
													display: false,
 | 
				
			||||||
							}
 | 
												}
 | 
				
			||||||
						}]
 | 
											}
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
					tooltips: {
 | 
										tooltips: {
 | 
				
			||||||
						intersect: false,
 | 
											intersect: false,
 | 
				
			||||||
| 
						 | 
					@ -371,18 +400,6 @@ export default defineComponent({
 | 
				
			||||||
			}));
 | 
								}));
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		async showInstanceInfo(q) {
 | 
					 | 
				
			||||||
			let instance = q;
 | 
					 | 
				
			||||||
			if (typeof q === 'string') {
 | 
					 | 
				
			||||||
				instance = await os.api('federation/show-instance', {
 | 
					 | 
				
			||||||
					host: q
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			os.popup(MkInstanceInfo, {
 | 
					 | 
				
			||||||
				instance: instance
 | 
					 | 
				
			||||||
			}, {}, 'closed');
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		fetchJobs() {
 | 
							fetchJobs() {
 | 
				
			||||||
			os.api('admin/queue/deliver-delayed', {}).then(jobs => {
 | 
								os.api('admin/queue/deliver-delayed', {}).then(jobs => {
 | 
				
			||||||
				this.jobs = jobs;
 | 
									this.jobs = jobs;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,61 +1,67 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<FormBase>
 | 
					<div>
 | 
				
			||||||
	<FormSuspense :p="init">
 | 
						<MkHeader :info="header"/>
 | 
				
			||||||
		<FormSuspense :p="fetchStats" v-slot="{ result: stats }">
 | 
					 | 
				
			||||||
			<FormGroup>
 | 
					 | 
				
			||||||
				<FormKeyValueView>
 | 
					 | 
				
			||||||
					<template #key>Users</template>
 | 
					 | 
				
			||||||
					<template #value>{{ number(stats.originalUsersCount) }}</template>
 | 
					 | 
				
			||||||
				</FormKeyValueView>
 | 
					 | 
				
			||||||
				<FormKeyValueView>
 | 
					 | 
				
			||||||
					<template #key>Notes</template>
 | 
					 | 
				
			||||||
					<template #value>{{ number(stats.originalNotesCount) }}</template>
 | 
					 | 
				
			||||||
				</FormKeyValueView>
 | 
					 | 
				
			||||||
			</FormGroup>
 | 
					 | 
				
			||||||
		</FormSuspense>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		<div class="_debobigegoItem">
 | 
						<div class="edbbcaef">
 | 
				
			||||||
			<div class="_debobigegoPanel">
 | 
							<div class="numbers" v-if="stats">
 | 
				
			||||||
				<MkInstanceStats :chart-limit="300" :detailed="true"/>
 | 
								<div class="number _panel">
 | 
				
			||||||
 | 
									<div class="label">Users</div>
 | 
				
			||||||
 | 
									<div class="value _monospace">
 | 
				
			||||||
 | 
										{{ number(stats.originalUsersCount) }}
 | 
				
			||||||
 | 
										<MkNumberDiff v-if="usersComparedToThePrevDay" class="diff" :value="usersComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><template #before>(</template><template #after>)</template></MkNumberDiff>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="number _panel">
 | 
				
			||||||
 | 
									<div class="label">Notes</div>
 | 
				
			||||||
 | 
									<div class="value _monospace">
 | 
				
			||||||
 | 
										{{ number(stats.originalNotesCount) }}
 | 
				
			||||||
 | 
										<MkNumberDiff v-if="notesComparedToThePrevDay" class="diff" :value="notesComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><template #before>(</template><template #after>)</template></MkNumberDiff>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		<XMetrics/>
 | 
							<MkContainer :foldable="true" class="charts">
 | 
				
			||||||
 | 
								<template #header><i class="fas fa-chart-bar"></i>{{ $ts.charts }}</template>
 | 
				
			||||||
 | 
								<div style="padding-top: 12px;">
 | 
				
			||||||
 | 
									<MkInstanceStats :chart-limit="500" :detailed="true"/>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</MkContainer>
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		<FormSuspense :p="fetchServerInfo" v-slot="{ result: serverInfo }">
 | 
								<!--<XMetrics/>-->
 | 
				
			||||||
			<FormGroup>
 | 
					
 | 
				
			||||||
				<FormKeyValueView>
 | 
							<div class="numbers">
 | 
				
			||||||
					<template #key>Node.js</template>
 | 
								<div class="number _panel">
 | 
				
			||||||
					<template #value>{{ serverInfo.node }}</template>
 | 
									<div class="label">Misskey</div>
 | 
				
			||||||
				</FormKeyValueView>
 | 
									<div class="value _monospace">{{ version }}</div>
 | 
				
			||||||
				<FormKeyValueView>
 | 
								</div>
 | 
				
			||||||
					<template #key>PostgreSQL</template>
 | 
								<div class="number _panel" v-if="serverInfo">
 | 
				
			||||||
					<template #value>{{ serverInfo.psql }}</template>
 | 
									<div class="label">Node.js</div>
 | 
				
			||||||
				</FormKeyValueView>
 | 
									<div class="value _monospace">{{ serverInfo.node }}</div>
 | 
				
			||||||
				<FormKeyValueView>
 | 
								</div>
 | 
				
			||||||
					<template #key>Redis</template>
 | 
								<div class="number _panel" v-if="serverInfo">
 | 
				
			||||||
					<template #value>{{ serverInfo.redis }}</template>
 | 
									<div class="label">PostgreSQL</div>
 | 
				
			||||||
				</FormKeyValueView>
 | 
									<div class="value _monospace">{{ serverInfo.psql }}</div>
 | 
				
			||||||
			</FormGroup>
 | 
								</div>
 | 
				
			||||||
		</FormSuspense>
 | 
								<div class="number _panel" v-if="serverInfo">
 | 
				
			||||||
	</FormSuspense>
 | 
									<div class="label">Redis</div>
 | 
				
			||||||
</FormBase>
 | 
									<div class="value _monospace">{{ serverInfo.redis }}</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="number _panel">
 | 
				
			||||||
 | 
									<div class="label">Vue</div>
 | 
				
			||||||
 | 
									<div class="value _monospace">{{ vueVersion }}</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { computed, defineComponent, markRaw } from 'vue';
 | 
					import { computed, defineComponent, version as vueVersion } from 'vue';
 | 
				
			||||||
import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
 | 
					import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
 | 
				
			||||||
import FormInput from '@client/components/debobigego/input.vue';
 | 
					 | 
				
			||||||
import FormButton from '@client/components/debobigego/button.vue';
 | 
					 | 
				
			||||||
import FormBase from '@client/components/debobigego/base.vue';
 | 
					 | 
				
			||||||
import FormGroup from '@client/components/debobigego/group.vue';
 | 
					 | 
				
			||||||
import FormTextarea from '@client/components/debobigego/textarea.vue';
 | 
					 | 
				
			||||||
import FormInfo from '@client/components/debobigego/info.vue';
 | 
					 | 
				
			||||||
import FormSuspense from '@client/components/debobigego/suspense.vue';
 | 
					 | 
				
			||||||
import MkInstanceStats from '@client/components/instance-stats.vue';
 | 
					import MkInstanceStats from '@client/components/instance-stats.vue';
 | 
				
			||||||
import MkButton from '@client/components/ui/button.vue';
 | 
					import MkButton from '@client/components/ui/button.vue';
 | 
				
			||||||
import MkSelect from '@client/components/form/select.vue';
 | 
					import MkSelect from '@client/components/form/select.vue';
 | 
				
			||||||
import MkInput from '@client/components/form/input.vue';
 | 
					import MkNumberDiff from '@client/components/number-diff.vue';
 | 
				
			||||||
import MkContainer from '@client/components/ui/container.vue';
 | 
					import MkContainer from '@client/components/ui/container.vue';
 | 
				
			||||||
import MkFolder from '@client/components/ui/folder.vue';
 | 
					import MkFolder from '@client/components/ui/folder.vue';
 | 
				
			||||||
import { version, url } from '@client/config';
 | 
					import { version, url } from '@client/config';
 | 
				
			||||||
| 
						 | 
					@ -68,12 +74,10 @@ import * as symbols from '@client/symbols';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
	components: {
 | 
						components: {
 | 
				
			||||||
		FormBase,
 | 
							MkNumberDiff,
 | 
				
			||||||
		FormSuspense,
 | 
					 | 
				
			||||||
		FormGroup,
 | 
					 | 
				
			||||||
		FormInfo,
 | 
					 | 
				
			||||||
		FormKeyValueView,
 | 
							FormKeyValueView,
 | 
				
			||||||
		MkInstanceStats,
 | 
							MkInstanceStats,
 | 
				
			||||||
 | 
							MkContainer,
 | 
				
			||||||
		XMetrics,
 | 
							XMetrics,
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -82,17 +86,22 @@ export default defineComponent({
 | 
				
			||||||
	data() {
 | 
						data() {
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			[symbols.PAGE_INFO]: {
 | 
								[symbols.PAGE_INFO]: {
 | 
				
			||||||
				title: this.$ts.overview,
 | 
									title: this.$ts.dashboard,
 | 
				
			||||||
				icon: 'fas fa-tachometer-alt',
 | 
									icon: 'fas fa-tachometer-alt',
 | 
				
			||||||
				bg: 'var(--bg)',
 | 
									bg: 'var(--bg)',
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			page: 'index',
 | 
								header: {
 | 
				
			||||||
 | 
									title: this.$ts.dashboard,
 | 
				
			||||||
 | 
									icon: 'fas fa-tachometer-alt',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
			version,
 | 
								version,
 | 
				
			||||||
 | 
								vueVersion,
 | 
				
			||||||
			url,
 | 
								url,
 | 
				
			||||||
			stats: null,
 | 
								stats: null,
 | 
				
			||||||
			meta: null,
 | 
								meta: null,
 | 
				
			||||||
			fetchStats: () => os.api('stats', {}),
 | 
								serverInfo: null,
 | 
				
			||||||
			fetchServerInfo: () => os.api('admin/server-info', {}),
 | 
								usersComparedToThePrevDay: null,
 | 
				
			||||||
 | 
								notesComparedToThePrevDay: null,
 | 
				
			||||||
			fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
 | 
								fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
 | 
				
			||||||
			fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
 | 
								fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					@ -100,13 +109,29 @@ export default defineComponent({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async mounted() {
 | 
						async mounted() {
 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
							this.$emit('info', this[symbols.PAGE_INFO]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							os.api('meta', { detail: true }).then(meta => {
 | 
				
			||||||
 | 
								this.meta = meta;
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							os.api('stats', {}).then(stats => {
 | 
				
			||||||
 | 
								this.stats = stats;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								os.api('charts/users', { limit: 2, span: 'day' }).then(chart => {
 | 
				
			||||||
 | 
									this.usersComparedToThePrevDay = this.stats.originalUsersCount - chart.local.total[1];
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								os.api('charts/notes', { limit: 2, span: 'day' }).then(chart => {
 | 
				
			||||||
 | 
									this.notesComparedToThePrevDay = this.stats.originalNotesCount - chart.local.total[1];
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							os.api('admin/server-info', {}).then(serverInfo => {
 | 
				
			||||||
 | 
								this.serverInfo = serverInfo;
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		async init() {
 | 
					 | 
				
			||||||
			this.meta = await os.api('meta', { detail: true });
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
		async showInstanceInfo(q) {
 | 
							async showInstanceInfo(q) {
 | 
				
			||||||
			let instance = q;
 | 
								let instance = q;
 | 
				
			||||||
			if (typeof q === 'string') {
 | 
								if (typeof q === 'string') {
 | 
				
			||||||
| 
						 | 
					@ -125,3 +150,36 @@ export default defineComponent({
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
 | 
					.edbbcaef {
 | 
				
			||||||
 | 
						> .numbers {
 | 
				
			||||||
 | 
							display: grid;
 | 
				
			||||||
 | 
							grid-gap: 8px;
 | 
				
			||||||
 | 
							grid-template-columns: repeat(auto-fill,minmax(130px,1fr));
 | 
				
			||||||
 | 
							margin: 16px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> .number {
 | 
				
			||||||
 | 
								padding: 12px 16px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								> .label {
 | 
				
			||||||
 | 
									opacity: 0.7;
 | 
				
			||||||
 | 
									font-size: 0.8em;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								> .value {
 | 
				
			||||||
 | 
									font-weight: bold;
 | 
				
			||||||
 | 
									font-size: 1.2em;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									> .diff {
 | 
				
			||||||
 | 
										font-size: 0.8em;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> .charts {
 | 
				
			||||||
 | 
							margin: var(--margin);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -67,7 +67,7 @@ export default defineComponent({
 | 
				
			||||||
		// TODO: var(--panel)の色が暗いか明るいかで判定する
 | 
							// TODO: var(--panel)の色が暗いか明るいかで判定する
 | 
				
			||||||
		const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
 | 
							const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
 | 
							Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.chart = markRaw(new Chart(this.$refs.chart, {
 | 
							this.chart = markRaw(new Chart(this.$refs.chart, {
 | 
				
			||||||
			type: 'line',
 | 
								type: 'line',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,11 @@
 | 
				
			||||||
import * as tinycolor from 'tinycolor2';
 | 
					import * as tinycolor from 'tinycolor2';
 | 
				
			||||||
import Chart from 'chart.js';
 | 
					 | 
				
			||||||
import { Hpml } from './evaluator';
 | 
					import { Hpml } from './evaluator';
 | 
				
			||||||
import { values, utils } from '@syuilo/aiscript';
 | 
					import { values, utils } from '@syuilo/aiscript';
 | 
				
			||||||
import { Fn, HpmlScope } from '.';
 | 
					import { Fn, HpmlScope } from '.';
 | 
				
			||||||
import { Expr } from './expr';
 | 
					import { Expr } from './expr';
 | 
				
			||||||
import * as seedrandom from 'seedrandom';
 | 
					import * as seedrandom from 'seedrandom';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs
 | 
					// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs
 | 
				
			||||||
Chart.pluginService.register({
 | 
					Chart.pluginService.register({
 | 
				
			||||||
	beforeDraw: (chart, easing) => {
 | 
						beforeDraw: (chart, easing) => {
 | 
				
			||||||
| 
						 | 
					@ -18,6 +18,7 @@ Chart.pluginService.register({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function initAiLib(hpml: Hpml) {
 | 
					export function initAiLib(hpml: Hpml) {
 | 
				
			||||||
	return {
 | 
						return {
 | 
				
			||||||
| 
						 | 
					@ -49,11 +50,12 @@ export function initAiLib(hpml: Hpml) {
 | 
				
			||||||
			]));
 | 
								]));
 | 
				
			||||||
		}),
 | 
							}),
 | 
				
			||||||
		'MkPages:chart': values.FN_NATIVE(([id, opts]) => {
 | 
							'MkPages:chart': values.FN_NATIVE(([id, opts]) => {
 | 
				
			||||||
 | 
								/* TODO
 | 
				
			||||||
			utils.assertString(id);
 | 
								utils.assertString(id);
 | 
				
			||||||
			utils.assertObject(opts);
 | 
								utils.assertObject(opts);
 | 
				
			||||||
			const canvas = hpml.canvases[id.value];
 | 
								const canvas = hpml.canvases[id.value];
 | 
				
			||||||
			const color = getComputedStyle(document.documentElement).getPropertyValue('--accent');
 | 
								const color = getComputedStyle(document.documentElement).getPropertyValue('--accent');
 | 
				
			||||||
			Chart.defaults.global.defaultFontColor = '#555';
 | 
								Chart.defaults.color = '#555';
 | 
				
			||||||
			const chart = new Chart(canvas, {
 | 
								const chart = new Chart(canvas, {
 | 
				
			||||||
				type: opts.value.get('type').value,
 | 
									type: opts.value.get('type').value,
 | 
				
			||||||
				data: {
 | 
									data: {
 | 
				
			||||||
| 
						 | 
					@ -122,6 +124,7 @@ export function initAiLib(hpml: Hpml) {
 | 
				
			||||||
					})
 | 
										})
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
								*/
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@ import procesObjectStorage from './processors/object-storage/index';
 | 
				
			||||||
import { queueLogger } from './logger';
 | 
					import { queueLogger } from './logger';
 | 
				
			||||||
import { DriveFile } from '@/models/entities/drive-file';
 | 
					import { DriveFile } from '@/models/entities/drive-file';
 | 
				
			||||||
import { getJobInfo } from './get-job-info';
 | 
					import { getJobInfo } from './get-job-info';
 | 
				
			||||||
import { dbQueue, deliverQueue, inboxQueue, objectStorageQueue } from './queues';
 | 
					import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue } from './queues';
 | 
				
			||||||
import { ThinUser } from './types';
 | 
					import { ThinUser } from './types';
 | 
				
			||||||
import { IActivity } from '@/remote/activitypub/type';
 | 
					import { IActivity } from '@/remote/activitypub/type';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,11 +22,20 @@ function renderError(e: Error): any {
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const systemLogger = queueLogger.createSubLogger('system');
 | 
				
			||||||
const deliverLogger = queueLogger.createSubLogger('deliver');
 | 
					const deliverLogger = queueLogger.createSubLogger('deliver');
 | 
				
			||||||
const inboxLogger = queueLogger.createSubLogger('inbox');
 | 
					const inboxLogger = queueLogger.createSubLogger('inbox');
 | 
				
			||||||
const dbLogger = queueLogger.createSubLogger('db');
 | 
					const dbLogger = queueLogger.createSubLogger('db');
 | 
				
			||||||
const objectStorageLogger = queueLogger.createSubLogger('objectStorage');
 | 
					const objectStorageLogger = queueLogger.createSubLogger('objectStorage');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					systemQueue
 | 
				
			||||||
 | 
						.on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`))
 | 
				
			||||||
 | 
						.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
 | 
				
			||||||
 | 
						.on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
 | 
				
			||||||
 | 
						.on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
 | 
				
			||||||
 | 
						.on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) }))
 | 
				
			||||||
 | 
						.on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
deliverQueue
 | 
					deliverQueue
 | 
				
			||||||
	.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
 | 
						.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
 | 
				
			||||||
	.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
 | 
						.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
 | 
				
			||||||
| 
						 | 
					@ -220,12 +229,17 @@ export function createCleanRemoteFilesJob() {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function() {
 | 
					export default function() {
 | 
				
			||||||
	if (!envOption.onlyServer) {
 | 
						if (envOption.onlyServer) return;
 | 
				
			||||||
		deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver);
 | 
					
 | 
				
			||||||
		inboxQueue.process(config.inboxJobConcurrency || 16, processInbox);
 | 
						deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver);
 | 
				
			||||||
		processDb(dbQueue);
 | 
						inboxQueue.process(config.inboxJobConcurrency || 16, processInbox);
 | 
				
			||||||
		procesObjectStorage(objectStorageQueue);
 | 
						processDb(dbQueue);
 | 
				
			||||||
	}
 | 
						procesObjectStorage(objectStorageQueue);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						systemQueue.add('resyncCharts', {
 | 
				
			||||||
 | 
						}, {
 | 
				
			||||||
 | 
							repeat: { cron: '0 0 * * *' }
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function destroy() {
 | 
					export function destroy() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/queue/processors/system/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/queue/processors/system/index.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					import * as Bull from 'bull';
 | 
				
			||||||
 | 
					import { resyncCharts } from './resync-charts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const jobs = {
 | 
				
			||||||
 | 
						resyncCharts,
 | 
				
			||||||
 | 
					} as Record<string, Bull.ProcessCallbackFunction<{}> | Bull.ProcessPromiseFunction<{}>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function(dbQueue: Bull.Queue<{}>) {
 | 
				
			||||||
 | 
						for (const [k, v] of Object.entries(jobs)) {
 | 
				
			||||||
 | 
							dbQueue.process(k, v);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										21
									
								
								src/queue/processors/system/resync-charts.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/queue/processors/system/resync-charts.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					import * as Bull from 'bull';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { queueLogger } from '../../logger';
 | 
				
			||||||
 | 
					import { driveChart, notesChart, usersChart } from '@/services/chart/index';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const logger = queueLogger.createSubLogger('resync-charts');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default async function resyncCharts(job: Bull.Job<{}>, done: any): Promise<void> {
 | 
				
			||||||
 | 
						logger.info(`Resync charts...`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO: ユーザーごとのチャートも更新する
 | 
				
			||||||
 | 
						// TODO: インスタンスごとのチャートも更新する
 | 
				
			||||||
 | 
						await Promise.all([
 | 
				
			||||||
 | 
							driveChart.resync(),
 | 
				
			||||||
 | 
							notesChart.resync(),
 | 
				
			||||||
 | 
							usersChart.resync(),
 | 
				
			||||||
 | 
						]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logger.succ(`All charts successfully resynced.`);
 | 
				
			||||||
 | 
						done();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,7 @@ import config from '@/config/index';
 | 
				
			||||||
import { initialize as initializeQueue } from './initialize';
 | 
					import { initialize as initializeQueue } from './initialize';
 | 
				
			||||||
import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData } from './types';
 | 
					import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData } from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const systemQueue = initializeQueue<{}>('system');
 | 
				
			||||||
export const deliverQueue = initializeQueue<DeliverJobData>('deliver', config.deliverJobPerSec || 128);
 | 
					export const deliverQueue = initializeQueue<DeliverJobData>('deliver', config.deliverJobPerSec || 128);
 | 
				
			||||||
export const inboxQueue = initializeQueue<InboxJobData>('inbox', config.inboxJobPerSec || 16);
 | 
					export const inboxQueue = initializeQueue<InboxJobData>('inbox', config.inboxJobPerSec || 16);
 | 
				
			||||||
export const dbQueue = initializeQueue<DbJobData>('db');
 | 
					export const dbQueue = initializeQueue<DbJobData>('db');
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import define from '../../define';
 | 
					import define from '../../define';
 | 
				
			||||||
import { driveChart, notesChart, usersChart, instanceChart } from '@/services/chart/index';
 | 
					import { driveChart, notesChart, usersChart } from '@/services/chart/index';
 | 
				
			||||||
import { insertModerationLog } from '@/services/insert-moderation-log';
 | 
					import { insertModerationLog } from '@/services/insert-moderation-log';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const meta = {
 | 
					export const meta = {
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ export default define(meta, async (ps, me) => {
 | 
				
			||||||
	driveChart.resync();
 | 
						driveChart.resync();
 | 
				
			||||||
	notesChart.resync();
 | 
						notesChart.resync();
 | 
				
			||||||
	usersChart.resync();
 | 
						usersChart.resync();
 | 
				
			||||||
	instanceChart.resync();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// TODO: ユーザーごとのチャートもキューに入れて更新する
 | 
						// TODO: ユーザーごとのチャートもキューに入れて更新する
 | 
				
			||||||
 | 
						// TODO: インスタンスごとのチャートもキューに入れて更新する
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										46
									
								
								yarn.lock
									
										
									
									
									
								
							
							
						
						
									
										46
									
								
								yarn.lock
									
										
									
									
									
								
							| 
						 | 
					@ -2664,28 +2664,22 @@ character-parser@^2.2.0:
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    is-regex "^1.0.3"
 | 
					    is-regex "^1.0.3"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
chart.js@2.9.4:
 | 
					chart.js@3.5.1:
 | 
				
			||||||
  version "2.9.4"
 | 
					  version "3.5.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.4.tgz#0827f9563faffb2dc5c06562f8eb10337d5b9684"
 | 
					  resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.5.1.tgz#73e24d23a4134a70ccdb5e79a917f156b6f3644a"
 | 
				
			||||||
  integrity sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==
 | 
					  integrity sha512-m5kzt72I1WQ9LILwQC4syla/LD/N413RYv2Dx2nnTkRS9iv/ey1xLTt0DnPc/eWV4zI+BgEgDYBIzbQhZHc/PQ==
 | 
				
			||||||
  dependencies:
 | 
					 | 
				
			||||||
    chartjs-color "^2.1.0"
 | 
					 | 
				
			||||||
    moment "^2.10.2"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
chartjs-color-string@^0.6.0:
 | 
					chartjs-adapter-date-fns@2.0.0:
 | 
				
			||||||
  version "0.6.0"
 | 
					  version "2.0.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
 | 
					  resolved "https://registry.yarnpkg.com/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz#5e53b2f660b993698f936f509c86dddf9ed44c6b"
 | 
				
			||||||
  integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
 | 
					  integrity sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==
 | 
				
			||||||
  dependencies:
 | 
					 | 
				
			||||||
    color-name "^1.0.0"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
chartjs-color@^2.1.0:
 | 
					chartjs-plugin-zoom@1.1.1:
 | 
				
			||||||
  version "2.4.1"
 | 
					  version "1.1.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
 | 
					  resolved "https://registry.yarnpkg.com/chartjs-plugin-zoom/-/chartjs-plugin-zoom-1.1.1.tgz#8a28923a17fcb5eb57a0dc94c5113bf402677647"
 | 
				
			||||||
  integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
 | 
					  integrity sha512-1q54WOzK7FtAjkbemQeqvmFUV0btNYIQny2HbQ6Awq9wUtCz7Zmj6vIgp3C1DYMQwN0nqgpC3vnApqiwI7cSdQ==
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    chartjs-color-string "^0.6.0"
 | 
					    hammerjs "^2.0.8"
 | 
				
			||||||
    color-convert "^1.9.3"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
check-more-types@2.24.0, check-more-types@^2.24.0:
 | 
					check-more-types@2.24.0, check-more-types@^2.24.0:
 | 
				
			||||||
  version "2.24.0"
 | 
					  version "2.24.0"
 | 
				
			||||||
| 
						 | 
					@ -2974,7 +2968,7 @@ collection-visit@^1.0.0:
 | 
				
			||||||
    map-visit "^1.0.0"
 | 
					    map-visit "^1.0.0"
 | 
				
			||||||
    object-visit "^1.0.0"
 | 
					    object-visit "^1.0.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
color-convert@^1.3.0, color-convert@^1.9.0, color-convert@^1.9.3:
 | 
					color-convert@^1.3.0, color-convert@^1.9.0:
 | 
				
			||||||
  version "1.9.3"
 | 
					  version "1.9.3"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
 | 
					  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
 | 
				
			||||||
  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
 | 
					  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
 | 
				
			||||||
| 
						 | 
					@ -3616,6 +3610,11 @@ data-urls@^2.0.0:
 | 
				
			||||||
    whatwg-mimetype "^2.3.0"
 | 
					    whatwg-mimetype "^2.3.0"
 | 
				
			||||||
    whatwg-url "^8.0.0"
 | 
					    whatwg-url "^8.0.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					date-fns@2.25.0:
 | 
				
			||||||
 | 
					  version "2.25.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.25.0.tgz#8c5c8f1d958be3809a9a03f4b742eba894fc5680"
 | 
				
			||||||
 | 
					  integrity sha512-ovYRFnTrbGPD4nqaEqescPEv1mNwvt+UTqI3Ay9SzNtey9NZnYu6E2qCcBBgJ6/2VF1zGGygpyTDITqpQQ5e+w==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
date-fns@^2.16.1:
 | 
					date-fns@^2.16.1:
 | 
				
			||||||
  version "2.19.0"
 | 
					  version "2.19.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.19.0.tgz#65193348635a28d5d916c43ec7ce6fbd145059e1"
 | 
					  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.19.0.tgz#65193348635a28d5d916c43ec7ce6fbd145059e1"
 | 
				
			||||||
| 
						 | 
					@ -5300,6 +5299,11 @@ gulplog@^1.0.0:
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    glogg "^1.0.0"
 | 
					    glogg "^1.0.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					hammerjs@^2.0.8:
 | 
				
			||||||
 | 
					  version "2.0.8"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/hammerjs/-/hammerjs-2.0.8.tgz#04ef77862cff2bb79d30f7692095930222bf60f1"
 | 
				
			||||||
 | 
					  integrity sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
har-schema@^2.0.0:
 | 
					har-schema@^2.0.0:
 | 
				
			||||||
  version "2.0.0"
 | 
					  version "2.0.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
 | 
					  resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
 | 
				
			||||||
| 
						 | 
					@ -7383,7 +7387,7 @@ moment-timezone@^0.5.25:
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    moment ">= 2.9.0"
 | 
					    moment ">= 2.9.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"moment@>= 2.9.0", moment@^2.10.2, moment@^2.22.2:
 | 
					"moment@>= 2.9.0", moment@^2.22.2:
 | 
				
			||||||
  version "2.24.0"
 | 
					  version "2.24.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
 | 
					  resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
 | 
				
			||||||
  integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
 | 
					  integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue