Merge branch 'develop' into swn
This commit is contained in:
		
							
								
								
									
										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>
 | 
			
		||||
@@ -33,7 +33,7 @@
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
 | 
			
		||||
import MkButton from '../ui/button.vue';
 | 
			
		||||
import MkButton from '@client/components/ui/button.vue';
 | 
			
		||||
import { debounce } from 'throttle-debounce';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="vblkjoeq">
 | 
			
		||||
	<div class="label" @click="focus"><slot name="label"></slot></div>
 | 
			
		||||
	<div class="input" :class="{ inline, disabled, focused }">
 | 
			
		||||
	<div class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick" ref="container">
 | 
			
		||||
		<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
 | 
			
		||||
		<select ref="inputEl"
 | 
			
		||||
		<select class="select" ref="inputEl"
 | 
			
		||||
			v-model="v"
 | 
			
		||||
			:disabled="disabled"
 | 
			
		||||
			:required="required"
 | 
			
		||||
@@ -25,7 +25,8 @@
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
 | 
			
		||||
import MkButton from '../ui/button.vue';
 | 
			
		||||
import MkButton from '@client/components/ui/button.vue';
 | 
			
		||||
import * as os from '@client/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
@@ -81,6 +82,7 @@ export default defineComponent({
 | 
			
		||||
		const inputEl = ref(null);
 | 
			
		||||
		const prefixEl = ref(null);
 | 
			
		||||
		const suffixEl = ref(null);
 | 
			
		||||
		const container = ref(null);
 | 
			
		||||
 | 
			
		||||
		const focus = () => inputEl.value.focus();
 | 
			
		||||
		const onInput = (ev) => {
 | 
			
		||||
@@ -132,6 +134,47 @@ export default defineComponent({
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const onClick = (ev: MouseEvent) => {
 | 
			
		||||
			focused.value = true;
 | 
			
		||||
 | 
			
		||||
			const menu = [];
 | 
			
		||||
			let options = context.slots.default();
 | 
			
		||||
 | 
			
		||||
			for (const optionOrOptgroup of options) {
 | 
			
		||||
				if (optionOrOptgroup.type === 'optgroup') {
 | 
			
		||||
					const optgroup = optionOrOptgroup;
 | 
			
		||||
					menu.push({
 | 
			
		||||
						type: 'label',
 | 
			
		||||
						text: optgroup.props.label,
 | 
			
		||||
					});
 | 
			
		||||
					for (const option of optgroup.children) {
 | 
			
		||||
						menu.push({
 | 
			
		||||
							text: option.children,
 | 
			
		||||
							active: v.value === option.props.value,
 | 
			
		||||
							action: () => {
 | 
			
		||||
								v.value = option.props.value;
 | 
			
		||||
							},
 | 
			
		||||
						});
 | 
			
		||||
					}
 | 
			
		||||
				} else {
 | 
			
		||||
					const option = optionOrOptgroup;
 | 
			
		||||
					menu.push({
 | 
			
		||||
						text: option.children,
 | 
			
		||||
						active: v.value === option.props.value,
 | 
			
		||||
						action: () => {
 | 
			
		||||
							v.value = option.props.value;
 | 
			
		||||
						},
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			os.popupMenu(menu, container.value, {
 | 
			
		||||
				width: container.value.offsetWidth,
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				focused.value = false;
 | 
			
		||||
			});
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			v,
 | 
			
		||||
			focused,
 | 
			
		||||
@@ -141,8 +184,10 @@ export default defineComponent({
 | 
			
		||||
			inputEl,
 | 
			
		||||
			prefixEl,
 | 
			
		||||
			suffixEl,
 | 
			
		||||
			container,
 | 
			
		||||
			focus,
 | 
			
		||||
			onInput,
 | 
			
		||||
			onClick,
 | 
			
		||||
			updated,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
@@ -174,8 +219,15 @@ export default defineComponent({
 | 
			
		||||
	> .input {
 | 
			
		||||
		$height: 42px;
 | 
			
		||||
		position: relative;
 | 
			
		||||
		cursor: pointer;
 | 
			
		||||
 | 
			
		||||
		> select {
 | 
			
		||||
		&:hover {
 | 
			
		||||
			> .select {
 | 
			
		||||
				border-color: var(--inputBorderHover);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .select {
 | 
			
		||||
			appearance: none;
 | 
			
		||||
			-webkit-appearance: none;
 | 
			
		||||
			display: block;
 | 
			
		||||
@@ -195,10 +247,7 @@ export default defineComponent({
 | 
			
		||||
			box-sizing: border-box;
 | 
			
		||||
			cursor: pointer;
 | 
			
		||||
			transition: border-color 0.1s ease-out;
 | 
			
		||||
 | 
			
		||||
			&:hover {
 | 
			
		||||
				border-color: var(--inputBorderHover);
 | 
			
		||||
			}
 | 
			
		||||
			pointer-events: none;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .prefix,
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
 | 
			
		||||
import MkButton from '../ui/button.vue';
 | 
			
		||||
import MkButton from '@client/components/ui/button.vue';
 | 
			
		||||
import { debounce } from 'throttle-debounce';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
 
 | 
			
		||||
@@ -27,8 +27,7 @@ export default defineComponent({
 | 
			
		||||
			default: false
 | 
			
		||||
		},
 | 
			
		||||
		customEmojis: {
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: () => []
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
		isReaction: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
@@ -58,10 +57,7 @@ export default defineComponent({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		ce() {
 | 
			
		||||
			let ce = [];
 | 
			
		||||
			if (this.customEmojis) ce = ce.concat(this.customEmojis);
 | 
			
		||||
			if (this.$instance && this.$instance.emojis) ce = ce.concat(this.$instance.emojis);
 | 
			
		||||
			return ce;
 | 
			
		||||
			return this.customEmojis || this.$instance?.emojis || [];
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -24,35 +24,26 @@
 | 
			
		||||
				<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
 | 
			
		||||
			</optgroup>
 | 
			
		||||
		</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="day">{{ $ts.perDay }}</option>
 | 
			
		||||
		</MkSelect>
 | 
			
		||||
	</div>
 | 
			
		||||
	<canvas ref="chart"></canvas>
 | 
			
		||||
	<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, markRaw } from 'vue';
 | 
			
		||||
import Chart from 'chart.js';
 | 
			
		||||
import MkSelect from './form/select.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 { defineComponent, onMounted, ref, watch } from 'vue';
 | 
			
		||||
import MkSelect from '@client/components/form/select.vue';
 | 
			
		||||
import MkChart from '@client/components/chart.vue';
 | 
			
		||||
import * as os from '@client/os';
 | 
			
		||||
import { defaultStore } from '@client/store';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkSelect
 | 
			
		||||
		MkSelect,
 | 
			
		||||
		MkChart,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
@@ -68,463 +59,15 @@ export default defineComponent({
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
	setup() {
 | 
			
		||||
		const chartSpan = ref<'hour' | 'day'>('hour');
 | 
			
		||||
		const chartSrc = ref('notes');
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			notesLocalWoW: 0,
 | 
			
		||||
			notesLocalDoD: 0,
 | 
			
		||||
			notesRemoteWoW: 0,
 | 
			
		||||
			notesRemoteDoD: 0,
 | 
			
		||||
			usersLocalWoW: 0,
 | 
			
		||||
			usersLocalDoD: 0,
 | 
			
		||||
			usersRemoteWoW: 0,
 | 
			
		||||
			usersRemoteDoD: 0,
 | 
			
		||||
			now: null,
 | 
			
		||||
			chart: null,
 | 
			
		||||
			chartInstance: null,
 | 
			
		||||
			chartSrc: 'notes',
 | 
			
		||||
			chartSpan: 'hour',
 | 
			
		||||
		}
 | 
			
		||||
			chartSrc,
 | 
			
		||||
			chartSpan,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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>
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="rrevdjwt" :class="{ center: align === 'center' }"
 | 
			
		||||
	:style="{ width: width ? width + 'px' : null }"
 | 
			
		||||
	ref="items"
 | 
			
		||||
	@contextmenu.self="e => e.preventDefault()"
 | 
			
		||||
	v-hotkey="keymap"
 | 
			
		||||
@@ -59,6 +60,10 @@ export default defineComponent({
 | 
			
		||||
			type: String,
 | 
			
		||||
			requried: false
 | 
			
		||||
		},
 | 
			
		||||
		width: {
 | 
			
		||||
			type: Number,
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	emits: ['close'],
 | 
			
		||||
	data() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
<MkPopup ref="popup" :src="src" @closed="$emit('closed')">
 | 
			
		||||
	<MkMenu :items="items" :align="align" @close="$refs.popup.close()" class="_popup _shadow"/>
 | 
			
		||||
	<MkMenu :items="items" :align="align" :width="width" @close="$refs.popup.close()" class="_popup _shadow"/>
 | 
			
		||||
</MkPopup>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@@ -24,6 +24,10 @@ export default defineComponent({
 | 
			
		||||
			type: String,
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
		width: {
 | 
			
		||||
			type: Number,
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
		viaKeyboard: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false
 | 
			
		||||
 
 | 
			
		||||
@@ -372,12 +372,17 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) {
 | 
			
		||||
export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: {
 | 
			
		||||
	align?: string;
 | 
			
		||||
	width?: number;
 | 
			
		||||
	viaKeyboard?: boolean;
 | 
			
		||||
}) {
 | 
			
		||||
	return new Promise((resolve, reject) => {
 | 
			
		||||
		let dispose;
 | 
			
		||||
		popup(import('@client/components/ui/popup-menu.vue'), {
 | 
			
		||||
			items,
 | 
			
		||||
			src,
 | 
			
		||||
			width: options?.width,
 | 
			
		||||
			align: options?.align,
 | 
			
		||||
			viaKeyboard: options?.viaKeyboard
 | 
			
		||||
		}, {
 | 
			
		||||
 
 | 
			
		||||
@@ -65,17 +65,17 @@
 | 
			
		||||
			<div class="_debobigegoPanel cmhjzshl">
 | 
			
		||||
				<div class="selects">
 | 
			
		||||
					<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
 | 
			
		||||
						<option value="requests">{{ $ts._instanceCharts.requests }}</option>
 | 
			
		||||
						<option value="users">{{ $ts._instanceCharts.users }}</option>
 | 
			
		||||
						<option value="users-total">{{ $ts._instanceCharts.usersTotal }}</option>
 | 
			
		||||
						<option value="notes">{{ $ts._instanceCharts.notes }}</option>
 | 
			
		||||
						<option value="notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
 | 
			
		||||
						<option value="ff">{{ $ts._instanceCharts.ff }}</option>
 | 
			
		||||
						<option value="ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
 | 
			
		||||
						<option value="drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
 | 
			
		||||
						<option value="drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
 | 
			
		||||
						<option value="drive-files">{{ $ts._instanceCharts.files }}</option>
 | 
			
		||||
						<option value="drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
 | 
			
		||||
						<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
 | 
			
		||||
						<option value="instance-users">{{ $ts._instanceCharts.users }}</option>
 | 
			
		||||
						<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
 | 
			
		||||
						<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
 | 
			
		||||
						<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
 | 
			
		||||
						<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
 | 
			
		||||
						<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
 | 
			
		||||
						<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
 | 
			
		||||
						<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
 | 
			
		||||
						<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
 | 
			
		||||
						<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
 | 
			
		||||
					</MkSelect>
 | 
			
		||||
					<MkSelect v-model="chartSpan" style="margin: 0;">
 | 
			
		||||
						<option value="hour">{{ $ts.perHour }}</option>
 | 
			
		||||
@@ -83,7 +83,7 @@
 | 
			
		||||
					</MkSelect>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="chart">
 | 
			
		||||
					<canvas :ref="setChart"></canvas>
 | 
			
		||||
					<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
@@ -135,7 +135,7 @@
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
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 FormTextarea from '@client/components/debobigego/textarea.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 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({
 | 
			
		||||
	components: {
 | 
			
		||||
		FormBase,
 | 
			
		||||
@@ -173,6 +162,7 @@ export default defineComponent({
 | 
			
		||||
		FormKeyValueView,
 | 
			
		||||
		FormSuspense,
 | 
			
		||||
		MkSelect,
 | 
			
		||||
		MkChart,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
@@ -199,53 +189,11 @@ export default defineComponent({
 | 
			
		||||
			dnsPromiseFactory: () => os.api('federation/dns', {
 | 
			
		||||
				host: this.host
 | 
			
		||||
			}),
 | 
			
		||||
			now: null,
 | 
			
		||||
			canvas: null,
 | 
			
		||||
			chart: null,
 | 
			
		||||
			chartInstance: null,
 | 
			
		||||
			chartSrc: 'requests',
 | 
			
		||||
			chartSrc: 'instance-requests',
 | 
			
		||||
			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() {
 | 
			
		||||
		this.fetch();
 | 
			
		||||
	},
 | 
			
		||||
@@ -258,190 +206,6 @@ export default defineComponent({
 | 
			
		||||
			this.instance = await os.api('federation/show-instance', {
 | 
			
		||||
				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() {
 | 
			
		||||
 
 | 
			
		||||
@@ -78,17 +78,17 @@
 | 
			
		||||
				<span class="label">{{ $ts.charts }}</span>
 | 
			
		||||
				<div class="selects">
 | 
			
		||||
					<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
 | 
			
		||||
						<option value="requests">{{ $ts._instanceCharts.requests }}</option>
 | 
			
		||||
						<option value="users">{{ $ts._instanceCharts.users }}</option>
 | 
			
		||||
						<option value="users-total">{{ $ts._instanceCharts.usersTotal }}</option>
 | 
			
		||||
						<option value="notes">{{ $ts._instanceCharts.notes }}</option>
 | 
			
		||||
						<option value="notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
 | 
			
		||||
						<option value="ff">{{ $ts._instanceCharts.ff }}</option>
 | 
			
		||||
						<option value="ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
 | 
			
		||||
						<option value="drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
 | 
			
		||||
						<option value="drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
 | 
			
		||||
						<option value="drive-files">{{ $ts._instanceCharts.files }}</option>
 | 
			
		||||
						<option value="drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
 | 
			
		||||
						<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
 | 
			
		||||
						<option value="instance-users">{{ $ts._instanceCharts.users }}</option>
 | 
			
		||||
						<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
 | 
			
		||||
						<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
 | 
			
		||||
						<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
 | 
			
		||||
						<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
 | 
			
		||||
						<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
 | 
			
		||||
						<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
 | 
			
		||||
						<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
 | 
			
		||||
						<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
 | 
			
		||||
						<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
 | 
			
		||||
					</MkSelect>
 | 
			
		||||
					<MkSelect v-model="chartSpan" style="margin: 0;">
 | 
			
		||||
						<option value="hour">{{ $ts.perHour }}</option>
 | 
			
		||||
@@ -97,7 +97,7 @@
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="chart">
 | 
			
		||||
				<canvas :ref="setChart"></canvas>
 | 
			
		||||
				<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="operations section">
 | 
			
		||||
@@ -124,28 +124,17 @@
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, markRaw } from 'vue';
 | 
			
		||||
import Chart from 'chart.js';
 | 
			
		||||
import XModalWindow from '@client/components/ui/modal-window.vue';
 | 
			
		||||
import MkUsersDialog from '@client/components/users-dialog.vue';
 | 
			
		||||
import MkSelect from '@client/components/form/select.vue';
 | 
			
		||||
import MkButton from '@client/components/ui/button.vue';
 | 
			
		||||
import MkSwitch from '@client/components/form/switch.vue';
 | 
			
		||||
import MkInfo from '@client/components/ui/info.vue';
 | 
			
		||||
import MkChart from '@client/components/chart.vue';
 | 
			
		||||
import bytes from '@client/filters/bytes';
 | 
			
		||||
import number from '@client/filters/number';
 | 
			
		||||
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({
 | 
			
		||||
	components: {
 | 
			
		||||
		XModalWindow,
 | 
			
		||||
@@ -153,6 +142,7 @@ export default defineComponent({
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkSwitch,
 | 
			
		||||
		MkInfo,
 | 
			
		||||
		MkChart,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
@@ -167,42 +157,12 @@ export default defineComponent({
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			isSuspended: this.instance.isSuspended,
 | 
			
		||||
			now: null,
 | 
			
		||||
			canvas: null,
 | 
			
		||||
			chart: null,
 | 
			
		||||
			chartInstance: null,
 | 
			
		||||
			chartSrc: 'requests',
 | 
			
		||||
			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;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		meta() {
 | 
			
		||||
			return this.$instance;
 | 
			
		||||
		},
 | 
			
		||||
@@ -219,49 +179,15 @@ export default defineComponent({
 | 
			
		||||
				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: {
 | 
			
		||||
		setChart(el) {
 | 
			
		||||
			this.canvas = el;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		changeBlock(e) {
 | 
			
		||||
			os.api('admin/update-meta', {
 | 
			
		||||
				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() {
 | 
			
		||||
			os.apiWithDialog('admin/federation/remove-all-following', {
 | 
			
		||||
				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() {
 | 
			
		||||
			os.modal(MkUsersDialog, {
 | 
			
		||||
				title: this.$ts.instanceFollowing,
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,21 @@
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
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 MkSelect from '@client/components/form/select.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 MkInstanceInfo from './instance.vue';
 | 
			
		||||
 | 
			
		||||
Chart.register(
 | 
			
		||||
  ArcElement,
 | 
			
		||||
  LineElement,
 | 
			
		||||
  BarElement,
 | 
			
		||||
  PointElement,
 | 
			
		||||
  BarController,
 | 
			
		||||
  LineController,
 | 
			
		||||
  CategoryScale,
 | 
			
		||||
  LinearScale,
 | 
			
		||||
  Legend,
 | 
			
		||||
  Title,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  SubTitle
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
@@ -116,7 +145,7 @@ export default defineComponent({
 | 
			
		||||
	mounted() {
 | 
			
		||||
		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 => {
 | 
			
		||||
			this.serverInfo = res;
 | 
			
		||||
@@ -157,7 +186,7 @@ export default defineComponent({
 | 
			
		||||
					datasets: [{
 | 
			
		||||
						label: 'CPU',
 | 
			
		||||
						pointRadius: 0,
 | 
			
		||||
						lineTension: 0,
 | 
			
		||||
						tension: 0,
 | 
			
		||||
						borderWidth: 2,
 | 
			
		||||
						borderColor: '#86b300',
 | 
			
		||||
						backgroundColor: alpha('#86b300', 0.1),
 | 
			
		||||
@@ -165,7 +194,7 @@ export default defineComponent({
 | 
			
		||||
					}, {
 | 
			
		||||
						label: 'MEM (active)',
 | 
			
		||||
						pointRadius: 0,
 | 
			
		||||
						lineTension: 0,
 | 
			
		||||
						tension: 0,
 | 
			
		||||
						borderWidth: 2,
 | 
			
		||||
						borderColor: '#935dbf',
 | 
			
		||||
						backgroundColor: alpha('#935dbf', 0.02),
 | 
			
		||||
@@ -173,7 +202,7 @@ export default defineComponent({
 | 
			
		||||
					}, {
 | 
			
		||||
						label: 'MEM (used)',
 | 
			
		||||
						pointRadius: 0,
 | 
			
		||||
						lineTension: 0,
 | 
			
		||||
						tension: 0,
 | 
			
		||||
						borderWidth: 2,
 | 
			
		||||
						borderColor: '#935dbf',
 | 
			
		||||
						borderDash: [5, 5],
 | 
			
		||||
@@ -198,7 +227,7 @@ export default defineComponent({
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					scales: {
 | 
			
		||||
						xAxes: [{
 | 
			
		||||
						x: {
 | 
			
		||||
							gridLines: {
 | 
			
		||||
								display: false,
 | 
			
		||||
								color: this.gridColor,
 | 
			
		||||
@@ -207,8 +236,8 @@ export default defineComponent({
 | 
			
		||||
							ticks: {
 | 
			
		||||
								display: false,
 | 
			
		||||
							}
 | 
			
		||||
						}],
 | 
			
		||||
						yAxes: [{
 | 
			
		||||
						},
 | 
			
		||||
						y: {
 | 
			
		||||
							position: 'right',
 | 
			
		||||
							gridLines: {
 | 
			
		||||
								display: true,
 | 
			
		||||
@@ -219,7 +248,7 @@ export default defineComponent({
 | 
			
		||||
								display: false,
 | 
			
		||||
								max: 100
 | 
			
		||||
							}
 | 
			
		||||
						}]
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					tooltips: {
 | 
			
		||||
						intersect: false,
 | 
			
		||||
@@ -238,7 +267,7 @@ export default defineComponent({
 | 
			
		||||
					datasets: [{
 | 
			
		||||
						label: 'In',
 | 
			
		||||
						pointRadius: 0,
 | 
			
		||||
						lineTension: 0,
 | 
			
		||||
						tension: 0,
 | 
			
		||||
						borderWidth: 2,
 | 
			
		||||
						borderColor: '#94a029',
 | 
			
		||||
						backgroundColor: alpha('#94a029', 0.1),
 | 
			
		||||
@@ -246,7 +275,7 @@ export default defineComponent({
 | 
			
		||||
					}, {
 | 
			
		||||
						label: 'Out',
 | 
			
		||||
						pointRadius: 0,
 | 
			
		||||
						lineTension: 0,
 | 
			
		||||
						tension: 0,
 | 
			
		||||
						borderWidth: 2,
 | 
			
		||||
						borderColor: '#ff9156',
 | 
			
		||||
						backgroundColor: alpha('#ff9156', 0.1),
 | 
			
		||||
@@ -270,7 +299,7 @@ export default defineComponent({
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					scales: {
 | 
			
		||||
						xAxes: [{
 | 
			
		||||
						x: {
 | 
			
		||||
							gridLines: {
 | 
			
		||||
								display: false,
 | 
			
		||||
								color: this.gridColor,
 | 
			
		||||
@@ -279,8 +308,8 @@ export default defineComponent({
 | 
			
		||||
							ticks: {
 | 
			
		||||
								display: false
 | 
			
		||||
							}
 | 
			
		||||
						}],
 | 
			
		||||
						yAxes: [{
 | 
			
		||||
						},
 | 
			
		||||
						y: {
 | 
			
		||||
							position: 'right',
 | 
			
		||||
							gridLines: {
 | 
			
		||||
								display: true,
 | 
			
		||||
@@ -290,7 +319,7 @@ export default defineComponent({
 | 
			
		||||
							ticks: {
 | 
			
		||||
								display: false,
 | 
			
		||||
							}
 | 
			
		||||
						}]
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					tooltips: {
 | 
			
		||||
						intersect: false,
 | 
			
		||||
@@ -309,7 +338,7 @@ export default defineComponent({
 | 
			
		||||
					datasets: [{
 | 
			
		||||
						label: 'Read',
 | 
			
		||||
						pointRadius: 0,
 | 
			
		||||
						lineTension: 0,
 | 
			
		||||
						tension: 0,
 | 
			
		||||
						borderWidth: 2,
 | 
			
		||||
						borderColor: '#94a029',
 | 
			
		||||
						backgroundColor: alpha('#94a029', 0.1),
 | 
			
		||||
@@ -317,7 +346,7 @@ export default defineComponent({
 | 
			
		||||
					}, {
 | 
			
		||||
						label: 'Write',
 | 
			
		||||
						pointRadius: 0,
 | 
			
		||||
						lineTension: 0,
 | 
			
		||||
						tension: 0,
 | 
			
		||||
						borderWidth: 2,
 | 
			
		||||
						borderColor: '#ff9156',
 | 
			
		||||
						backgroundColor: alpha('#ff9156', 0.1),
 | 
			
		||||
@@ -341,7 +370,7 @@ export default defineComponent({
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					scales: {
 | 
			
		||||
						xAxes: [{
 | 
			
		||||
						x: {
 | 
			
		||||
							gridLines: {
 | 
			
		||||
								display: false,
 | 
			
		||||
								color: this.gridColor,
 | 
			
		||||
@@ -350,8 +379,8 @@ export default defineComponent({
 | 
			
		||||
							ticks: {
 | 
			
		||||
								display: false
 | 
			
		||||
							}
 | 
			
		||||
						}],
 | 
			
		||||
						yAxes: [{
 | 
			
		||||
						},
 | 
			
		||||
						y: {
 | 
			
		||||
							position: 'right',
 | 
			
		||||
							gridLines: {
 | 
			
		||||
								display: true,
 | 
			
		||||
@@ -361,7 +390,7 @@ export default defineComponent({
 | 
			
		||||
							ticks: {
 | 
			
		||||
								display: false,
 | 
			
		||||
							}
 | 
			
		||||
						}]
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					tooltips: {
 | 
			
		||||
						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() {
 | 
			
		||||
			os.api('admin/queue/deliver-delayed', {}).then(jobs => {
 | 
			
		||||
				this.jobs = jobs;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,61 +1,67 @@
 | 
			
		||||
<template>
 | 
			
		||||
<FormBase>
 | 
			
		||||
	<FormSuspense :p="init">
 | 
			
		||||
		<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="_debobigegoPanel">
 | 
			
		||||
				<MkInstanceStats :chart-limit="300" :detailed="true"/>
 | 
			
		||||
<div>
 | 
			
		||||
	<MkHeader :info="header"/>
 | 
			
		||||
 | 
			
		||||
	<div class="edbbcaef">
 | 
			
		||||
		<div class="numbers" v-if="stats">
 | 
			
		||||
			<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>
 | 
			
		||||
 | 
			
		||||
		<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>
 | 
			
		||||
		
 | 
			
		||||
			<!--<XMetrics/>-->
 | 
			
		||||
 | 
			
		||||
		<FormSuspense :p="fetchServerInfo" v-slot="{ result: serverInfo }">
 | 
			
		||||
			<FormGroup>
 | 
			
		||||
				<FormKeyValueView>
 | 
			
		||||
					<template #key>Node.js</template>
 | 
			
		||||
					<template #value>{{ serverInfo.node }}</template>
 | 
			
		||||
				</FormKeyValueView>
 | 
			
		||||
				<FormKeyValueView>
 | 
			
		||||
					<template #key>PostgreSQL</template>
 | 
			
		||||
					<template #value>{{ serverInfo.psql }}</template>
 | 
			
		||||
				</FormKeyValueView>
 | 
			
		||||
				<FormKeyValueView>
 | 
			
		||||
					<template #key>Redis</template>
 | 
			
		||||
					<template #value>{{ serverInfo.redis }}</template>
 | 
			
		||||
				</FormKeyValueView>
 | 
			
		||||
			</FormGroup>
 | 
			
		||||
		</FormSuspense>
 | 
			
		||||
	</FormSuspense>
 | 
			
		||||
</FormBase>
 | 
			
		||||
		<div class="numbers">
 | 
			
		||||
			<div class="number _panel">
 | 
			
		||||
				<div class="label">Misskey</div>
 | 
			
		||||
				<div class="value _monospace">{{ version }}</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="number _panel" v-if="serverInfo">
 | 
			
		||||
				<div class="label">Node.js</div>
 | 
			
		||||
				<div class="value _monospace">{{ serverInfo.node }}</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="number _panel" v-if="serverInfo">
 | 
			
		||||
				<div class="label">PostgreSQL</div>
 | 
			
		||||
				<div class="value _monospace">{{ serverInfo.psql }}</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="number _panel" v-if="serverInfo">
 | 
			
		||||
				<div class="label">Redis</div>
 | 
			
		||||
				<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>
 | 
			
		||||
 | 
			
		||||
<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 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 MkButton from '@client/components/ui/button.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 MkFolder from '@client/components/ui/folder.vue';
 | 
			
		||||
import { version, url } from '@client/config';
 | 
			
		||||
@@ -68,12 +74,10 @@ import * as symbols from '@client/symbols';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		FormBase,
 | 
			
		||||
		FormSuspense,
 | 
			
		||||
		FormGroup,
 | 
			
		||||
		FormInfo,
 | 
			
		||||
		MkNumberDiff,
 | 
			
		||||
		FormKeyValueView,
 | 
			
		||||
		MkInstanceStats,
 | 
			
		||||
		MkContainer,
 | 
			
		||||
		XMetrics,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
@@ -82,17 +86,22 @@ export default defineComponent({
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			[symbols.PAGE_INFO]: {
 | 
			
		||||
				title: this.$ts.overview,
 | 
			
		||||
				title: this.$ts.dashboard,
 | 
			
		||||
				icon: 'fas fa-tachometer-alt',
 | 
			
		||||
				bg: 'var(--bg)',
 | 
			
		||||
			},
 | 
			
		||||
			page: 'index',
 | 
			
		||||
			header: {
 | 
			
		||||
				title: this.$ts.dashboard,
 | 
			
		||||
				icon: 'fas fa-tachometer-alt',
 | 
			
		||||
			},
 | 
			
		||||
			version,
 | 
			
		||||
			vueVersion,
 | 
			
		||||
			url,
 | 
			
		||||
			stats: null,
 | 
			
		||||
			meta: null,
 | 
			
		||||
			fetchStats: () => os.api('stats', {}),
 | 
			
		||||
			fetchServerInfo: () => os.api('admin/server-info', {}),
 | 
			
		||||
			serverInfo: null,
 | 
			
		||||
			usersComparedToThePrevDay: null,
 | 
			
		||||
			notesComparedToThePrevDay: null,
 | 
			
		||||
			fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
 | 
			
		||||
			fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
 | 
			
		||||
		}
 | 
			
		||||
@@ -100,13 +109,29 @@ export default defineComponent({
 | 
			
		||||
 | 
			
		||||
	async mounted() {
 | 
			
		||||
		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: {
 | 
			
		||||
		async init() {
 | 
			
		||||
			this.meta = await os.api('meta', { detail: true });
 | 
			
		||||
		},
 | 
			
		||||
	
 | 
			
		||||
		async showInstanceInfo(q) {
 | 
			
		||||
			let instance = q;
 | 
			
		||||
			if (typeof q === 'string') {
 | 
			
		||||
@@ -125,3 +150,36 @@ export default defineComponent({
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</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)の色が暗いか明るいかで判定する
 | 
			
		||||
		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, {
 | 
			
		||||
			type: 'line',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
import * as tinycolor from 'tinycolor2';
 | 
			
		||||
import Chart from 'chart.js';
 | 
			
		||||
import { Hpml } from './evaluator';
 | 
			
		||||
import { values, utils } from '@syuilo/aiscript';
 | 
			
		||||
import { Fn, HpmlScope } from '.';
 | 
			
		||||
import { Expr } from './expr';
 | 
			
		||||
import * as seedrandom from 'seedrandom';
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs
 | 
			
		||||
Chart.pluginService.register({
 | 
			
		||||
	beforeDraw: (chart, easing) => {
 | 
			
		||||
@@ -18,6 +18,7 @@ Chart.pluginService.register({
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
export function initAiLib(hpml: Hpml) {
 | 
			
		||||
	return {
 | 
			
		||||
@@ -49,11 +50,12 @@ export function initAiLib(hpml: Hpml) {
 | 
			
		||||
			]));
 | 
			
		||||
		}),
 | 
			
		||||
		'MkPages:chart': values.FN_NATIVE(([id, opts]) => {
 | 
			
		||||
			/* TODO
 | 
			
		||||
			utils.assertString(id);
 | 
			
		||||
			utils.assertObject(opts);
 | 
			
		||||
			const canvas = hpml.canvases[id.value];
 | 
			
		||||
			const color = getComputedStyle(document.documentElement).getPropertyValue('--accent');
 | 
			
		||||
			Chart.defaults.global.defaultFontColor = '#555';
 | 
			
		||||
			Chart.defaults.color = '#555';
 | 
			
		||||
			const chart = new Chart(canvas, {
 | 
			
		||||
				type: opts.value.get('type').value,
 | 
			
		||||
				data: {
 | 
			
		||||
@@ -122,6 +124,7 @@ export function initAiLib(hpml: Hpml) {
 | 
			
		||||
					})
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
			*/
 | 
			
		||||
		})
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,7 @@ export const builtinThemes = [
 | 
			
		||||
	require('@client/themes/d-astro.json5'),
 | 
			
		||||
	require('@client/themes/d-future.json5'),
 | 
			
		||||
	require('@client/themes/d-botanical.json5'),
 | 
			
		||||
	require('@client/themes/d-pumpkin.json5'),
 | 
			
		||||
	require('@client/themes/d-black.json5'),
 | 
			
		||||
] as Theme[];
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
	id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea',
 | 
			
		||||
	base: 'dark',
 | 
			
		||||
	name: 'Mi Astro',
 | 
			
		||||
	name: 'Mi Astro Dark',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	props: {
 | 
			
		||||
		bg: '#232125',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
	id: '32a637ef-b47a-4775-bb7b-bacbb823f865',
 | 
			
		||||
 | 
			
		||||
	name: 'Mi Future',
 | 
			
		||||
	name: 'Mi Future Dark',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
 | 
			
		||||
	base: 'dark',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
	id: 'c503d768-7c70-4db2-a4e6-08264304bc8d',
 | 
			
		||||
 | 
			
		||||
	name: 'Mi Persimmon',
 | 
			
		||||
	name: 'Mi Persimmon Dark',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
 | 
			
		||||
	base: 'dark',
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										88
									
								
								src/client/themes/d-pumpkin.json5
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/client/themes/d-pumpkin.json5
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
			
		||||
{
 | 
			
		||||
	id: '0b64fef3-02c7-20b5-dd87-b3f77e2b4301',
 | 
			
		||||
 | 
			
		||||
	name: 'Mi Pumpkin Dark',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
 | 
			
		||||
	base: 'dark',
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		X2: ':darken<2<@panel',
 | 
			
		||||
		X3: 'rgba(255, 255, 255, 0.05)',
 | 
			
		||||
		X4: 'rgba(255, 255, 255, 0.1)',
 | 
			
		||||
		X5: 'rgba(255, 255, 255, 0.05)',
 | 
			
		||||
		X6: 'rgba(255, 255, 255, 0.15)',
 | 
			
		||||
		X7: 'rgba(255, 255, 255, 0.05)',
 | 
			
		||||
		X8: ':lighten<5<@accent',
 | 
			
		||||
		X9: ':darken<5<@accent',
 | 
			
		||||
		bg: 'rgb(37, 32, 47)',
 | 
			
		||||
		fg: '#e0d5c0',
 | 
			
		||||
		X10: ':alpha<0.4<@accent',
 | 
			
		||||
		X11: 'rgba(0, 0, 0, 0.3)',
 | 
			
		||||
		X12: 'rgba(255, 255, 255, 0.1)',
 | 
			
		||||
		X13: 'rgba(255, 255, 255, 0.15)',
 | 
			
		||||
		X14: ':alpha<0.5<@navBg',
 | 
			
		||||
		X15: ':alpha<0<@panel',
 | 
			
		||||
		X16: ':alpha<0.7<@panel',
 | 
			
		||||
		X17: ':alpha<0.8<@bg',
 | 
			
		||||
		cwBg: '#687390',
 | 
			
		||||
		cwFg: '#393f4f',
 | 
			
		||||
		link: 'rgb(172, 193, 68)',
 | 
			
		||||
		warn: '#ecb637',
 | 
			
		||||
		badge: '#31b1ce',
 | 
			
		||||
		error: '#ec4137',
 | 
			
		||||
		focus: ':alpha<0.3<@accent',
 | 
			
		||||
		navBg: '@panel',
 | 
			
		||||
		navFg: '@fg',
 | 
			
		||||
		panel: ':lighten<3<@bg',
 | 
			
		||||
		popup: ':lighten<3<@panel',
 | 
			
		||||
		accent: 'rgb(242, 133, 36)',
 | 
			
		||||
		header: ':alpha<0.7<@panel',
 | 
			
		||||
		infoBg: '#253142',
 | 
			
		||||
		infoFg: '#fff',
 | 
			
		||||
		renote: 'rgb(110, 179, 72)',
 | 
			
		||||
		shadow: 'rgba(0, 0, 0, 0.3)',
 | 
			
		||||
		divider: 'rgba(255, 255, 255, 0.1)',
 | 
			
		||||
		hashtag: 'rgb(188, 90, 255)',
 | 
			
		||||
		mention: 'rgb(72, 179, 139)',
 | 
			
		||||
		modalBg: 'rgba(0, 0, 0, 0.5)',
 | 
			
		||||
		success: '#86b300',
 | 
			
		||||
		buttonBg: 'rgba(255, 255, 255, 0.05)',
 | 
			
		||||
		switchBg: 'rgba(255, 255, 255, 0.15)',
 | 
			
		||||
		acrylicBg: ':alpha<0.5<@bg',
 | 
			
		||||
		cwHoverBg: '#707b97',
 | 
			
		||||
		indicator: '@accent',
 | 
			
		||||
		mentionMe: '@accent',
 | 
			
		||||
		messageBg: '@bg',
 | 
			
		||||
		navActive: '@accent',
 | 
			
		||||
		accentedBg: ':alpha<0.15<@accent',
 | 
			
		||||
		fgOnAccent: '#000',
 | 
			
		||||
		infoWarnBg: '#42321c',
 | 
			
		||||
		infoWarnFg: '#ffbd3e',
 | 
			
		||||
		navHoverFg: ':lighten<17<@fg',
 | 
			
		||||
		dateLabelFg: '@fg',
 | 
			
		||||
		inputBorder: 'rgba(255, 255, 255, 0.1)',
 | 
			
		||||
		panelBorder: '" solid 1px var(--divider)',
 | 
			
		||||
		accentDarken: ':darken<10<@accent',
 | 
			
		||||
		acrylicPanel: ':alpha<0.5<@panel',
 | 
			
		||||
		navIndicator: '@indicator',
 | 
			
		||||
		accentLighten: ':lighten<10<@accent',
 | 
			
		||||
		buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
 | 
			
		||||
		driveFolderBg: ':alpha<0.3<@accent',
 | 
			
		||||
		fgHighlighted: ':lighten<3<@fg',
 | 
			
		||||
		fgTransparent: ':alpha<0.5<@fg',
 | 
			
		||||
		panelHeaderBg: ':lighten<3<@panel',
 | 
			
		||||
		panelHeaderFg: '@fg',
 | 
			
		||||
		buttonGradateA: '@accent',
 | 
			
		||||
		buttonGradateB: ':hue<20<@accent',
 | 
			
		||||
		htmlThemeColor: '@bg',
 | 
			
		||||
		panelHighlight: ':lighten<3<@panel',
 | 
			
		||||
		listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
 | 
			
		||||
		scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
 | 
			
		||||
		inputBorderHover: 'rgba(255, 255, 255, 0.2)',
 | 
			
		||||
		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
 | 
			
		||||
		fgTransparentWeak: ':alpha<0.75<@fg',
 | 
			
		||||
		panelHeaderDivider: 'rgba(0, 0, 0, 0)',
 | 
			
		||||
		scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
	id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b',
 | 
			
		||||
 | 
			
		||||
	name: 'Mi Apricot',
 | 
			
		||||
	name: 'Mi Apricot Light',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
 | 
			
		||||
	base: 'light',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
	id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96',
 | 
			
		||||
 | 
			
		||||
	name: 'Mi Rainy',
 | 
			
		||||
	name: 'Mi Rainy Light',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
 | 
			
		||||
	base: 'light',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
	id: '6128c2a9-5c54-43fe-a47d-17942356470b',
 | 
			
		||||
 | 
			
		||||
	name: 'Mi Vivid',
 | 
			
		||||
	name: 'Mi Vivid Light',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
 | 
			
		||||
	base: 'light',
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ import procesObjectStorage from './processors/object-storage/index';
 | 
			
		||||
import { queueLogger } from './logger';
 | 
			
		||||
import { DriveFile } from '@/models/entities/drive-file';
 | 
			
		||||
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 { 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 inboxLogger = queueLogger.createSubLogger('inbox');
 | 
			
		||||
const dbLogger = queueLogger.createSubLogger('db');
 | 
			
		||||
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
 | 
			
		||||
	.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
 | 
			
		||||
	.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
 | 
			
		||||
@@ -220,12 +229,17 @@ export function createCleanRemoteFilesJob() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function() {
 | 
			
		||||
	if (!envOption.onlyServer) {
 | 
			
		||||
		deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver);
 | 
			
		||||
		inboxQueue.process(config.inboxJobConcurrency || 16, processInbox);
 | 
			
		||||
		processDb(dbQueue);
 | 
			
		||||
		procesObjectStorage(objectStorageQueue);
 | 
			
		||||
	}
 | 
			
		||||
	if (envOption.onlyServer) return;
 | 
			
		||||
 | 
			
		||||
	deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver);
 | 
			
		||||
	inboxQueue.process(config.inboxJobConcurrency || 16, processInbox);
 | 
			
		||||
	processDb(dbQueue);
 | 
			
		||||
	procesObjectStorage(objectStorageQueue);
 | 
			
		||||
 | 
			
		||||
	systemQueue.add('resyncCharts', {
 | 
			
		||||
	}, {
 | 
			
		||||
		repeat: { cron: '0 0 * * *' }
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData } from './types';
 | 
			
		||||
 | 
			
		||||
export const systemQueue = initializeQueue<{}>('system');
 | 
			
		||||
export const deliverQueue = initializeQueue<DeliverJobData>('deliver', config.deliverJobPerSec || 128);
 | 
			
		||||
export const inboxQueue = initializeQueue<InboxJobData>('inbox', config.inboxJobPerSec || 16);
 | 
			
		||||
export const dbQueue = initializeQueue<DbJobData>('db');
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
@@ -15,7 +15,7 @@ export default define(meta, async (ps, me) => {
 | 
			
		||||
	driveChart.resync();
 | 
			
		||||
	notesChart.resync();
 | 
			
		||||
	usersChart.resync();
 | 
			
		||||
	instanceChart.resync();
 | 
			
		||||
 | 
			
		||||
	// TODO: ユーザーごとのチャートもキューに入れて更新する
 | 
			
		||||
	// TODO: インスタンスごとのチャートもキューに入れて更新する
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
	"short_name": "Misskey",
 | 
			
		||||
	"name": "Misskey",
 | 
			
		||||
	"start_url": "/",
 | 
			
		||||
	"display": "minimal-ui",
 | 
			
		||||
	"display": "standalone",
 | 
			
		||||
	"background_color": "#313a42",
 | 
			
		||||
	"theme_color": "#86b300",
 | 
			
		||||
	"icons": [
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user