From aadd5b95b811dbd9d0fce9e622613a148e4ad7da Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 3 Nov 2018 11:38:00 +0900
Subject: [PATCH] Improve admin dashboard

---
 locales/ja-JP.yml                             |   2 +
 src/client/app/admin/views/ap-log.vue         | 113 ++++++++++++++++++
 src/client/app/admin/views/dashboard.vue      |  23 ++--
 src/client/app/common/views/filters/bytes.ts  |   1 +
 src/client/app/common/views/filters/number.ts |   1 +
 src/queue/processors/http/process-inbox.ts    |  10 ++
 src/remote/activitypub/request.ts             |  10 ++
 src/server/api/stream/channels/ap-log.ts      |  24 ++++
 src/server/api/stream/channels/index.ts       |   2 +
 src/stream.ts                                 |   5 +
 10 files changed, 183 insertions(+), 8 deletions(-)
 create mode 100644 src/client/app/admin/views/ap-log.vue
 create mode 100644 src/server/api/stream/channels/ap-log.ts

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index c1555132d..11866c0fa 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1082,6 +1082,8 @@ admin/views/dashboard.vue:
   dashboard: "ダッシュボード"
   accounts: "アカウント"
   notes: "投稿"
+  drive: "ドライブ"
+  instances: "インスタンス"
   this-instance: "このインスタンス"
   federated: "連合"
   invite: "招待"
diff --git a/src/client/app/admin/views/ap-log.vue b/src/client/app/admin/views/ap-log.vue
new file mode 100644
index 000000000..a26627a90
--- /dev/null
+++ b/src/client/app/admin/views/ap-log.vue
@@ -0,0 +1,113 @@
+<template>
+<div class="hyhctythnmwihguaaapnbrbszsjqxpio">
+	<table>
+		<thead>
+			<tr>
+				<th>%fa:exchange-alt% In/Out</th>
+				<th>%fa:server% Host</th>
+				<th>%fa:bolt% Activity</th>
+				<th>%fa:user% Actor</th>
+			</tr>
+		</thead>
+		<tbody>
+			<tr v-for="log in logs" :key="log.id">
+				<td :class="log.direction">{{ log.direction == 'in' ? '<' : '>' }} {{ log.direction }}</td>
+				<td>{{ log.host }}</td>
+				<td>{{ log.activity }}</td>
+				<td>@{{ log.actor }}</td>
+			</tr>
+		</tbody>
+	</table>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	data() {
+		return {
+			logs: [],
+			connection: null
+		};
+	},
+
+	mounted() {
+		this.connection = (this as any).os.stream.useSharedConnection('apLog');
+		this.connection.on('stats', this.onLog);
+		this.connection.on('statsLog', this.onLogs);
+		this.connection.send('requestLog', {
+			id: Math.random().toString().substr(2, 8),
+			length: 50
+		});
+
+		setInterval(() => {
+			this.onLog({
+				direction: ['in', 'out'][Math.floor(Math.random() * 2)],
+				activity: 'Create',
+				host: 'misskey.ai',
+				actor: 'foobar'
+			});
+		}, 1000);
+	},
+
+	beforeDestroy() {
+		this.connection.dispose();
+	},
+
+	methods: {
+		onLog(log) {
+			log.id = Math.random();
+			this.logs.unshift(log);
+			if (this.logs.length > 50) this.logs.pop();
+		},
+
+		onLogs(logs) {
+			logs.reverse().forEach(log => this.onLog(log));
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.hyhctythnmwihguaaapnbrbszsjqxpio
+	display block
+	padding 16px
+	height 250px
+	overflow auto
+	box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
+	background var(--face)
+	border-radius 8px
+
+	> table
+		width 100%
+		max-width 100%
+		overflow auto
+		border-spacing 0
+		border-collapse collapse
+		color #555
+
+		thead
+			font-weight bold
+			border-bottom solid 2px #eee
+
+			tr
+				th
+					text-align left
+
+		tbody
+			tr
+				&:nth-child(odd)
+					background #fbfbfb
+
+		th, td
+			padding 8px 16px
+			min-width 128px
+
+		td.in
+			color #d26755
+
+		td.out
+			color #55bb83
+
+</style>
diff --git a/src/client/app/admin/views/dashboard.vue b/src/client/app/admin/views/dashboard.vue
index 95000d4ba..04a4de874 100644
--- a/src/client/app/admin/views/dashboard.vue
+++ b/src/client/app/admin/views/dashboard.vue
@@ -7,6 +7,7 @@
 		<p><b>Node</b><span>{{ meta.node }}</span></p>
 		<p>藍ちゃかわいい</p>
 	</header>
+
 	<div v-if="stats" class="stats">
 		<div>
 			<div>
@@ -34,22 +35,22 @@
 		</div>
 		<div>
 			<div>
-				<div>%fa:user%</div>
+				<div>%fa:database%</div>
 				<div>
-					<span>%i18n:@accounts%</span>
-					<b>{{ stats.usersCount | number }}</b>
+					<span>%i18n:@drive%</span>
+					<b>{{ stats.driveUsageLocal | bytes }}</b>
 				</div>
 			</div>
 			<div>
-				<span>%fa:globe% %i18n:@federated%</span>
+				<span>%fa:home% %i18n:@this-instance%</span>
 			</div>
 		</div>
 		<div>
 			<div>
-				<div>%fa:pencil-alt%</div>
+				<div>%fa:hdd R%</div>
 				<div>
-					<span>%i18n:@notes%</span>
-					<b>{{ stats.notesCount | number }}</b>
+					<span>%i18n:@instances%</span>
+					<b>{{ stats.instances | number }}</b>
 				</div>
 			</div>
 			<div>
@@ -65,6 +66,10 @@
 	<div class="cpu-memory">
 		<x-cpu-memory :connection="connection"/>
 	</div>
+
+	<div class="ap">
+		<x-ap-log/>
+	</div>
 </div>
 </template>
 
@@ -72,11 +77,13 @@
 import Vue from "vue";
 import XCpuMemory from "./cpu-memory.vue";
 import XCharts from "./charts.vue";
+import XApLog from "./ap-log.vue";
 
 export default Vue.extend({
 	components: {
 		XCpuMemory,
-		XCharts
+		XCharts,
+		XApLog
 	},
 	data() {
 		return {
diff --git a/src/client/app/common/views/filters/bytes.ts b/src/client/app/common/views/filters/bytes.ts
index f7a1b2690..5b5d966cf 100644
--- a/src/client/app/common/views/filters/bytes.ts
+++ b/src/client/app/common/views/filters/bytes.ts
@@ -1,6 +1,7 @@
 import Vue from 'vue';
 
 Vue.filter('bytes', (v, digits = 0) => {
+	if (v == null) return '?';
 	const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
 	if (v == 0) return '0';
 	const isMinus = v < 0;
diff --git a/src/client/app/common/views/filters/number.ts b/src/client/app/common/views/filters/number.ts
index d9f48229d..08f9fea80 100644
--- a/src/client/app/common/views/filters/number.ts
+++ b/src/client/app/common/views/filters/number.ts
@@ -1,5 +1,6 @@
 import Vue from 'vue';
 
 Vue.filter('number', (n) => {
+	if (n == null) return 'N/A';
 	return n.toLocaleString();
 });
diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts
index 8e6b3769d..87f0fbfb8 100644
--- a/src/queue/processors/http/process-inbox.ts
+++ b/src/queue/processors/http/process-inbox.ts
@@ -8,6 +8,7 @@ import perform from '../../../remote/activitypub/perform';
 import { resolvePerson, updatePerson } from '../../../remote/activitypub/models/person';
 import { toUnicode } from 'punycode';
 import { URL } from 'url';
+import { publishApLogStream } from '../../../stream';
 
 const log = debug('misskey:queue:inbox');
 
@@ -61,6 +62,15 @@ export default async (job: bq.Job, done: any): Promise<void> => {
 		}) as IRemoteUser;
 	}
 
+	//#region Log
+	publishApLogStream({
+		direction: 'in',
+		activity: activity.type,
+		host: user.host,
+		actor: user.username
+	});
+	//#endregion
+
 	// Update activityの場合は、ここで署名検証/更新処理まで実施して終了
 	if (activity.type === 'Update') {
 		if (activity.object && activity.object.type === 'Person') {
diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts
index 177b6f458..68c53e0c6 100644
--- a/src/remote/activitypub/request.ts
+++ b/src/remote/activitypub/request.ts
@@ -6,6 +6,7 @@ const crypto = require('crypto');
 
 import config from '../../config';
 import { ILocalUser } from '../../models/user';
+import { publishApLogStream } from '../../stream';
 
 const log = debug('misskey:activitypub:deliver');
 
@@ -64,4 +65,13 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso
 	});
 
 	req.end(data);
+
+	//#region Log
+	publishApLogStream({
+		direction: 'out',
+		activity: object.type,
+		host: null,
+		actor: user.username
+	});
+	//#endregion
 });
diff --git a/src/server/api/stream/channels/ap-log.ts b/src/server/api/stream/channels/ap-log.ts
new file mode 100644
index 000000000..dfa1cc702
--- /dev/null
+++ b/src/server/api/stream/channels/ap-log.ts
@@ -0,0 +1,24 @@
+import autobind from 'autobind-decorator';
+import Channel from '../channel';
+
+export default class extends Channel {
+	public readonly chName = 'apLog';
+	public static shouldShare = true;
+
+	@autobind
+	public async init(params: any) {
+		// Subscribe events
+		this.subscriber.on('apLog', this.onLog);
+	}
+
+	@autobind
+	private async onLog(log: any) {
+		this.send('log', log);
+	}
+
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off('apLog', this.onLog);
+	}
+}
diff --git a/src/server/api/stream/channels/index.ts b/src/server/api/stream/channels/index.ts
index 7e71590d0..7248579ab 100644
--- a/src/server/api/stream/channels/index.ts
+++ b/src/server/api/stream/channels/index.ts
@@ -10,6 +10,7 @@ import messaging from './messaging';
 import messagingIndex from './messaging-index';
 import drive from './drive';
 import hashtag from './hashtag';
+import apLog from './ap-log';
 import gamesReversi from './games/reversi';
 import gamesReversiGame from './games/reversi-game';
 
@@ -26,6 +27,7 @@ export default {
 	messagingIndex,
 	drive,
 	hashtag,
+	apLog,
 	gamesReversi,
 	gamesReversiGame
 };
diff --git a/src/stream.ts b/src/stream.ts
index b222a45ca..543421726 100644
--- a/src/stream.ts
+++ b/src/stream.ts
@@ -100,6 +100,10 @@ class Publisher {
 	public publishHashtagStream = (note: any): void => {
 		this.publish('hashtag', null, note);
 	}
+
+	public publishApLogStream = (log: any): void => {
+		this.publish('apLog', null, log);
+	}
 }
 
 const publisher = new Publisher();
@@ -119,3 +123,4 @@ export const publishLocalTimelineStream = publisher.publishLocalTimelineStream;
 export const publishHybridTimelineStream = publisher.publishHybridTimelineStream;
 export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream;
 export const publishHashtagStream = publisher.publishHashtagStream;
+export const publishApLogStream = publisher.publishApLogStream;