From 4e4c559db6964cbf17fcadf38d55fc79c995ca42 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 22 Oct 2021 05:36:48 +0900
Subject: Migrate to Chart.js v3 (#7896)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* 定期的にresync

* Update overview.vue

* wip

* wip
---
 src/client/pages/instance/queue.chart.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/client/pages/instance/queue.chart.vue')

diff --git a/src/client/pages/instance/queue.chart.vue b/src/client/pages/instance/queue.chart.vue
index 887fe9a574..4f8fd762bb 100644
--- a/src/client/pages/instance/queue.chart.vue
+++ b/src/client/pages/instance/queue.chart.vue
@@ -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',
-- 
cgit v1.2.3-freya


From 81a0ee4b2d089b9266d79595707a28f655f5cb27 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 22 Oct 2021 17:37:51 +0900
Subject: client: change url /instance -> /admin

---
 src/client/pages/admin/abuses.vue                  | 170 ++++++++
 src/client/pages/admin/ads.vue                     | 146 +++++++
 src/client/pages/admin/announcements.vue           | 134 ++++++
 src/client/pages/admin/bot-protection.vue          | 138 ++++++
 src/client/pages/admin/database.vue                |  61 +++
 src/client/pages/admin/email-settings.vue          | 128 ++++++
 src/client/pages/admin/emoji-edit-dialog.vue       | 120 ++++++
 src/client/pages/admin/emojis.vue                  | 262 ++++++++++++
 src/client/pages/admin/file-dialog.vue             | 129 ++++++
 src/client/pages/admin/files-settings.vue          |  93 ++++
 src/client/pages/admin/files.vue                   | 209 +++++++++
 src/client/pages/admin/index.vue                   | 373 ++++++++++++++++
 src/client/pages/admin/instance-block.vue          |  72 ++++
 src/client/pages/admin/instance.vue                | 321 ++++++++++++++
 src/client/pages/admin/integrations-discord.vue    |  85 ++++
 src/client/pages/admin/integrations-github.vue     |  85 ++++
 src/client/pages/admin/integrations-twitter.vue    |  85 ++++
 src/client/pages/admin/integrations.vue            |  74 ++++
 src/client/pages/admin/logs.vue                    |  97 +++++
 src/client/pages/admin/metrics.vue                 | 472 +++++++++++++++++++++
 src/client/pages/admin/object-storage.vue          | 155 +++++++
 src/client/pages/admin/other-settings.vue          |  83 ++++
 src/client/pages/admin/overview.vue                | 188 ++++++++
 src/client/pages/admin/proxy-account.vue           |  87 ++++
 src/client/pages/admin/queue.chart.vue             | 218 ++++++++++
 src/client/pages/admin/queue.vue                   |  73 ++++
 src/client/pages/admin/relays.vue                  |  99 +++++
 src/client/pages/admin/security.vue                |  83 ++++
 src/client/pages/admin/service-worker.vue          |  85 ++++
 src/client/pages/admin/settings.vue                | 151 +++++++
 src/client/pages/admin/users.vue                   | 261 ++++++++++++
 src/client/pages/instance-info.vue                 |   2 +-
 src/client/pages/instance/abuses.vue               | 170 --------
 src/client/pages/instance/ads.vue                  | 146 -------
 src/client/pages/instance/announcements.vue        | 134 ------
 src/client/pages/instance/bot-protection.vue       | 138 ------
 src/client/pages/instance/database.vue             |  61 ---
 src/client/pages/instance/email-settings.vue       | 128 ------
 src/client/pages/instance/emoji-edit-dialog.vue    | 120 ------
 src/client/pages/instance/emojis.vue               | 262 ------------
 src/client/pages/instance/file-dialog.vue          | 129 ------
 src/client/pages/instance/files-settings.vue       |  93 ----
 src/client/pages/instance/files.vue                | 209 ---------
 src/client/pages/instance/index.vue                | 373 ----------------
 src/client/pages/instance/instance-block.vue       |  72 ----
 src/client/pages/instance/instance.vue             | 321 --------------
 src/client/pages/instance/integrations-discord.vue |  85 ----
 src/client/pages/instance/integrations-github.vue  |  85 ----
 src/client/pages/instance/integrations-twitter.vue |  85 ----
 src/client/pages/instance/integrations.vue         |  74 ----
 src/client/pages/instance/logs.vue                 |  97 -----
 src/client/pages/instance/metrics.vue              | 472 ---------------------
 src/client/pages/instance/object-storage.vue       | 155 -------
 src/client/pages/instance/other-settings.vue       |  83 ----
 src/client/pages/instance/overview.vue             | 188 --------
 src/client/pages/instance/proxy-account.vue        |  87 ----
 src/client/pages/instance/queue.chart.vue          | 218 ----------
 src/client/pages/instance/queue.vue                |  73 ----
 src/client/pages/instance/relays.vue               |  99 -----
 src/client/pages/instance/security.vue             |  83 ----
 src/client/pages/instance/service-worker.vue       |  85 ----
 src/client/pages/instance/settings.vue             | 151 -------
 src/client/pages/instance/users.vue                | 261 ------------
 src/client/router.ts                               |   4 +-
 src/client/ui/_common_/sidebar.vue                 |   2 +-
 src/client/ui/default.header.vue                   |   2 +-
 src/client/ui/default.sidebar.vue                  |   2 +-
 67 files changed, 4743 insertions(+), 4743 deletions(-)
 create mode 100644 src/client/pages/admin/abuses.vue
 create mode 100644 src/client/pages/admin/ads.vue
 create mode 100644 src/client/pages/admin/announcements.vue
 create mode 100644 src/client/pages/admin/bot-protection.vue
 create mode 100644 src/client/pages/admin/database.vue
 create mode 100644 src/client/pages/admin/email-settings.vue
 create mode 100644 src/client/pages/admin/emoji-edit-dialog.vue
 create mode 100644 src/client/pages/admin/emojis.vue
 create mode 100644 src/client/pages/admin/file-dialog.vue
 create mode 100644 src/client/pages/admin/files-settings.vue
 create mode 100644 src/client/pages/admin/files.vue
 create mode 100644 src/client/pages/admin/index.vue
 create mode 100644 src/client/pages/admin/instance-block.vue
 create mode 100644 src/client/pages/admin/instance.vue
 create mode 100644 src/client/pages/admin/integrations-discord.vue
 create mode 100644 src/client/pages/admin/integrations-github.vue
 create mode 100644 src/client/pages/admin/integrations-twitter.vue
 create mode 100644 src/client/pages/admin/integrations.vue
 create mode 100644 src/client/pages/admin/logs.vue
 create mode 100644 src/client/pages/admin/metrics.vue
 create mode 100644 src/client/pages/admin/object-storage.vue
 create mode 100644 src/client/pages/admin/other-settings.vue
 create mode 100644 src/client/pages/admin/overview.vue
 create mode 100644 src/client/pages/admin/proxy-account.vue
 create mode 100644 src/client/pages/admin/queue.chart.vue
 create mode 100644 src/client/pages/admin/queue.vue
 create mode 100644 src/client/pages/admin/relays.vue
 create mode 100644 src/client/pages/admin/security.vue
 create mode 100644 src/client/pages/admin/service-worker.vue
 create mode 100644 src/client/pages/admin/settings.vue
 create mode 100644 src/client/pages/admin/users.vue
 delete mode 100644 src/client/pages/instance/abuses.vue
 delete mode 100644 src/client/pages/instance/ads.vue
 delete mode 100644 src/client/pages/instance/announcements.vue
 delete mode 100644 src/client/pages/instance/bot-protection.vue
 delete mode 100644 src/client/pages/instance/database.vue
 delete mode 100644 src/client/pages/instance/email-settings.vue
 delete mode 100644 src/client/pages/instance/emoji-edit-dialog.vue
 delete mode 100644 src/client/pages/instance/emojis.vue
 delete mode 100644 src/client/pages/instance/file-dialog.vue
 delete mode 100644 src/client/pages/instance/files-settings.vue
 delete mode 100644 src/client/pages/instance/files.vue
 delete mode 100644 src/client/pages/instance/index.vue
 delete mode 100644 src/client/pages/instance/instance-block.vue
 delete mode 100644 src/client/pages/instance/instance.vue
 delete mode 100644 src/client/pages/instance/integrations-discord.vue
 delete mode 100644 src/client/pages/instance/integrations-github.vue
 delete mode 100644 src/client/pages/instance/integrations-twitter.vue
 delete mode 100644 src/client/pages/instance/integrations.vue
 delete mode 100644 src/client/pages/instance/logs.vue
 delete mode 100644 src/client/pages/instance/metrics.vue
 delete mode 100644 src/client/pages/instance/object-storage.vue
 delete mode 100644 src/client/pages/instance/other-settings.vue
 delete mode 100644 src/client/pages/instance/overview.vue
 delete mode 100644 src/client/pages/instance/proxy-account.vue
 delete mode 100644 src/client/pages/instance/queue.chart.vue
 delete mode 100644 src/client/pages/instance/queue.vue
 delete mode 100644 src/client/pages/instance/relays.vue
 delete mode 100644 src/client/pages/instance/security.vue
 delete mode 100644 src/client/pages/instance/service-worker.vue
 delete mode 100644 src/client/pages/instance/settings.vue
 delete mode 100644 src/client/pages/instance/users.vue

(limited to 'src/client/pages/instance/queue.chart.vue')

diff --git a/src/client/pages/admin/abuses.vue b/src/client/pages/admin/abuses.vue
new file mode 100644
index 0000000000..29da8cc2c5
--- /dev/null
+++ b/src/client/pages/admin/abuses.vue
@@ -0,0 +1,170 @@
+<template>
+<div class="lcixvhis">
+	<div class="_section reports">
+		<div class="_content">
+			<div class="inputs" style="display: flex;">
+				<MkSelect v-model="state" style="margin: 0; flex: 1;">
+					<template #label>{{ $ts.state }}</template>
+					<option value="all">{{ $ts.all }}</option>
+					<option value="unresolved">{{ $ts.unresolved }}</option>
+					<option value="resolved">{{ $ts.resolved }}</option>
+				</MkSelect>
+				<MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
+					<template #label>{{ $ts.targetUserOrigin }}</template>
+					<option value="combined">{{ $ts.all }}</option>
+					<option value="local">{{ $ts.local }}</option>
+					<option value="remote">{{ $ts.remote }}</option>
+				</MkSelect>
+				<MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
+					<template #label>{{ $ts.reporterOrigin }}</template>
+					<option value="combined">{{ $ts.all }}</option>
+					<option value="local">{{ $ts.local }}</option>
+					<option value="remote">{{ $ts.remote }}</option>
+				</MkSelect>
+			</div>
+			<!-- TODO
+			<div class="inputs" style="display: flex; padding-top: 1.2em;">
+				<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()">
+					<span>{{ $ts.username }}</span>
+				</MkInput>
+				<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()" :disabled="pagination.params().origin === 'local'">
+					<span>{{ $ts.host }}</span>
+				</MkInput>
+			</div>
+			-->
+
+			<MkPagination :pagination="pagination" #default="{items}" ref="reports" style="margin-top: var(--margin);">
+				<div class="bcekxzvu _card _gap" v-for="report in items" :key="report.id">
+					<div class="_content target">
+						<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/>
+						<div class="info">
+							<MkUserName class="name" :user="report.targetUser"/>
+							<div class="acct">@{{ acct(report.targetUser) }}</div>
+						</div>
+					</div>
+					<div class="_content">
+						<div>
+							<Mfm :text="report.comment"/>
+						</div>
+						<hr>
+						<div>Reporter: <MkAcct :user="report.reporter"/></div>
+						<div><MkTime :time="report.createdAt"/></div>
+					</div>
+					<div class="_footer">
+						<div v-if="report.assignee">Assignee: <MkAcct :user="report.assignee"/></div>
+						<MkButton @click="resolve(report)" primary v-if="!report.resolved">{{ $ts.abuseMarkAsResolved }}</MkButton>
+					</div>
+				</div>
+			</MkPagination>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { parseAcct } from '@/misc/acct';
+import MkButton from '@client/components/ui/button.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkSelect from '@client/components/form/select.vue';
+import MkPagination from '@client/components/ui/pagination.vue';
+import { acct } from '@client/filters/user';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkInput,
+		MkSelect,
+		MkPagination,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.abuseReports,
+				icon: 'fas fa-exclamation-circle',
+				bg: 'var(--bg)',
+			},
+			searchUsername: '',
+			searchHost: '',
+			state: 'unresolved',
+			reporterOrigin: 'combined',
+			targetUserOrigin: 'combined',
+			pagination: {
+				endpoint: 'admin/abuse-user-reports',
+				limit: 10,
+				params: () => ({
+					state: this.state,
+					reporterOrigin: this.reporterOrigin,
+					targetUserOrigin: this.targetUserOrigin,
+				}),
+			},
+		}
+	},
+
+	watch: {
+		state() {
+			this.$refs.reports.reload();
+		},
+
+		reporterOrigin() {
+			this.$refs.reports.reload();
+		},
+
+		targetUserOrigin() {
+			this.$refs.reports.reload();
+		},
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		acct,
+
+		resolve(report) {
+			os.apiWithDialog('admin/resolve-abuse-user-report', {
+				reportId: report.id,
+			}).then(() => {
+				this.$refs.reports.removeItem(item => item.id === report.id);
+			});
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.lcixvhis {
+	margin: var(--margin);
+}
+
+.bcekxzvu {
+	> .target {
+		display: flex;
+		width: 100%;
+		box-sizing: border-box;
+		text-align: left;
+		align-items: center;
+					
+		> .avatar {
+			width: 42px;
+			height: 42px;
+		}
+
+		> .info {
+			margin-left: 0.3em;
+			padding: 0 8px;
+			flex: 1;
+
+			> .name {
+				font-weight: bold;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/admin/ads.vue b/src/client/pages/admin/ads.vue
new file mode 100644
index 0000000000..e776f99a4c
--- /dev/null
+++ b/src/client/pages/admin/ads.vue
@@ -0,0 +1,146 @@
+<template>
+<div>
+	<MkHeader :info="header"/>
+	<div class="uqshojas">
+		<section class="_card _gap ads" v-for="ad in ads">
+			<div class="_content ad">
+				<MkAd v-if="ad.url" :specify="ad"/>
+				<MkInput v-model="ad.url" type="url">
+					<template #label>URL</template>
+				</MkInput>
+				<MkInput v-model="ad.imageUrl">
+					<template #label>{{ $ts.imageUrl }}</template>
+				</MkInput>
+				<div style="margin: 32px 0;">
+					<MkRadio v-model="ad.place" value="square">square</MkRadio>
+					<MkRadio v-model="ad.place" value="horizontal">horizontal</MkRadio>
+					<MkRadio v-model="ad.place" value="horizontal-big">horizontal-big</MkRadio>
+				</div>
+				<!--
+				<div style="margin: 32px 0;">
+					{{ $ts.priority }}
+					<MkRadio v-model="ad.priority" value="high">{{ $ts.high }}</MkRadio>
+					<MkRadio v-model="ad.priority" value="middle">{{ $ts.middle }}</MkRadio>
+					<MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio>
+				</div>
+				-->
+				<MkInput v-model="ad.ratio" type="number">
+					<template #label>{{ $ts.ratio }}</template>
+				</MkInput>
+				<MkInput v-model="ad.expiresAt" type="date">
+					<template #label>{{ $ts.expiration }}</template>
+				</MkInput>
+				<MkTextarea v-model="ad.memo">
+					<template #label>{{ $ts.memo }}</template>
+				</MkTextarea>
+				<div class="buttons">
+					<MkButton class="button" inline @click="save(ad)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
+					<MkButton class="button" inline @click="remove(ad)" danger><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
+				</div>
+			</div>
+		</section>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@client/components/ui/button.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
+import MkRadio from '@client/components/form/radio.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkInput,
+		MkTextarea,
+		MkRadio,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.ads,
+				icon: 'fas fa-audio-description',
+				bg: 'var(--bg)',
+			},
+			header: {
+				title: this.$ts.ads,
+				icon: 'fas fa-audio-description',
+				bg: 'var(--bg)',
+				actions: [{
+					asFullButton: true,
+					icon: 'fas fa-plus',
+					text: this.$ts.add,
+					handler: this.add,
+				}],
+			},
+			ads: [],
+		}
+	},
+
+	created() {
+		os.api('admin/ad/list').then(ads => {
+			this.ads = ads;
+		});
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		add() {
+			this.ads.unshift({
+				id: null,
+				memo: '',
+				place: 'square',
+				priority: 'middle',
+				ratio: 1,
+				url: '',
+				imageUrl: null,
+				expiresAt: null,
+			});
+		},
+
+		remove(ad) {
+			os.dialog({
+				type: 'warning',
+				text: this.$t('removeAreYouSure', { x: ad.url }),
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+				this.ads = this.ads.filter(x => x != ad);
+				os.apiWithDialog('admin/ad/delete', {
+					id: ad.id
+				});
+			});
+		},
+
+		save(ad) {
+			if (ad.id == null) {
+				os.apiWithDialog('admin/ad/create', {
+					...ad,
+					expiresAt: new Date(ad.expiresAt).getTime()
+				});
+			} else {
+				os.apiWithDialog('admin/ad/update', {
+					...ad,
+					expiresAt: new Date(ad.expiresAt).getTime()
+				});
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.uqshojas {
+	margin: var(--margin);
+}
+</style>
diff --git a/src/client/pages/admin/announcements.vue b/src/client/pages/admin/announcements.vue
new file mode 100644
index 0000000000..78637c095a
--- /dev/null
+++ b/src/client/pages/admin/announcements.vue
@@ -0,0 +1,134 @@
+<template>
+<div>
+	<MkHeader :info="header"/>
+
+	<div class="ztgjmzrw">
+		<section class="_card _gap announcements" v-for="announcement in announcements">
+			<div class="_content announcement">
+				<MkInput v-model="announcement.title">
+					<template #label>{{ $ts.title }}</template>
+				</MkInput>
+				<MkTextarea v-model="announcement.text">
+					<template #label>{{ $ts.text }}</template>
+				</MkTextarea>
+				<MkInput v-model="announcement.imageUrl">
+					<template #label>{{ $ts.imageUrl }}</template>
+				</MkInput>
+				<p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p>
+				<div class="buttons">
+					<MkButton class="button" inline @click="save(announcement)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
+					<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
+				</div>
+			</div>
+		</section>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@client/components/ui/button.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkInput,
+		MkTextarea,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.announcements,
+				icon: 'fas fa-broadcast-tower',
+				bg: 'var(--bg)',
+			},
+			header: {
+				title: this.$ts.announcements,
+				icon: 'fas fa-broadcast-tower',
+				bg: 'var(--bg)',
+				actions: [{
+					asFullButton: true,
+					icon: 'fas fa-plus',
+					text: this.$ts.add,
+					handler: this.add,
+				}],
+			},
+			announcements: [],
+		}
+	},
+
+	created() {
+		os.api('admin/announcements/list').then(announcements => {
+			this.announcements = announcements;
+		});
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		add() {
+			this.announcements.unshift({
+				id: null,
+				title: '',
+				text: '',
+				imageUrl: null
+			});
+		},
+
+		remove(announcement) {
+			os.dialog({
+				type: 'warning',
+				text: this.$t('removeAreYouSure', { x: announcement.title }),
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+				this.announcements = this.announcements.filter(x => x != announcement);
+				os.api('admin/announcements/delete', announcement);
+			});
+		},
+
+		save(announcement) {
+			if (announcement.id == null) {
+				os.api('admin/announcements/create', announcement).then(() => {
+					os.dialog({
+						type: 'success',
+						text: this.$ts.saved
+					});
+				}).catch(e => {
+					os.dialog({
+						type: 'error',
+						text: e
+					});
+				});
+			} else {
+				os.api('admin/announcements/update', announcement).then(() => {
+					os.dialog({
+						type: 'success',
+						text: this.$ts.saved
+					});
+				}).catch(e => {
+					os.dialog({
+						type: 'error',
+						text: e
+					});
+				});
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ztgjmzrw {
+	margin: var(--margin);
+}
+</style>
diff --git a/src/client/pages/admin/bot-protection.vue b/src/client/pages/admin/bot-protection.vue
new file mode 100644
index 0000000000..731f114cc2
--- /dev/null
+++ b/src/client/pages/admin/bot-protection.vue
@@ -0,0 +1,138 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormRadios v-model="provider">
+			<template #desc><i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}</template>
+			<option :value="null">{{ $ts.none }} ({{ $ts.notRecommended }})</option>
+			<option value="hcaptcha">hCaptcha</option>
+			<option value="recaptcha">reCAPTCHA</option>
+		</FormRadios>
+
+		<template v-if="provider === 'hcaptcha'">
+			<div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
+				<div class="_debobigegoLabel">hCaptcha</div>
+				<div class="main">
+					<FormInput v-model="hcaptchaSiteKey">
+						<template #prefix><i class="fas fa-key"></i></template>
+						<span>{{ $ts.hcaptchaSiteKey }}</span>
+					</FormInput>
+					<FormInput v-model="hcaptchaSecretKey">
+						<template #prefix><i class="fas fa-key"></i></template>
+						<span>{{ $ts.hcaptchaSecretKey }}</span>
+					</FormInput>
+				</div>
+			</div>
+			<div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
+				<div class="_debobigegoLabel">{{ $ts.preview }}</div>
+				<div class="_debobigegoPanel" style="padding: var(--debobigegoContentHMargin);">
+					<MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
+				</div>
+			</div>
+		</template>
+		<template v-else-if="provider === 'recaptcha'">
+			<div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
+				<div class="_debobigegoLabel">reCAPTCHA</div>
+				<div class="main">
+					<FormInput v-model="recaptchaSiteKey">
+						<template #prefix><i class="fas fa-key"></i></template>
+						<span>{{ $ts.recaptchaSiteKey }}</span>
+					</FormInput>
+					<FormInput v-model="recaptchaSecretKey">
+						<template #prefix><i class="fas fa-key"></i></template>
+						<span>{{ $ts.recaptchaSecretKey }}</span>
+					</FormInput>
+				</div>
+			</div>
+			<div v-if="recaptchaSiteKey" class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
+				<div class="_debobigegoLabel">{{ $ts.preview }}</div>
+				<div class="_debobigegoPanel" style="padding: var(--debobigegoContentHMargin);">
+					<MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/>
+				</div>
+			</div>
+		</template>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import FormRadios from '@client/components/debobigego/radios.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 FormInfo from '@client/components/debobigego/info.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+	components: {
+		FormRadios,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormInfo,
+		FormSuspense,
+		MkCaptcha: defineAsyncComponent(() => import('@client/components/captcha.vue')),
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.botProtection,
+				icon: 'fas fa-shield-alt'
+			},
+			provider: null,
+			enableHcaptcha: false,
+			hcaptchaSiteKey: null,
+			hcaptchaSecretKey: null,
+			enableRecaptcha: false,
+			recaptchaSiteKey: null,
+			recaptchaSecretKey: null,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.enableHcaptcha = meta.enableHcaptcha;
+			this.hcaptchaSiteKey = meta.hcaptchaSiteKey;
+			this.hcaptchaSecretKey = meta.hcaptchaSecretKey;
+			this.enableRecaptcha = meta.enableRecaptcha;
+			this.recaptchaSiteKey = meta.recaptchaSiteKey;
+			this.recaptchaSecretKey = meta.recaptchaSecretKey;
+
+			this.provider = this.enableHcaptcha ? 'hcaptcha' : this.enableRecaptcha ? 'recaptcha' : null;
+
+			this.$watch(() => this.provider, () => {
+				this.enableHcaptcha = this.provider === 'hcaptcha';
+				this.enableRecaptcha = this.provider === 'recaptcha';
+			});
+		},
+	
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				enableHcaptcha: this.enableHcaptcha,
+				hcaptchaSiteKey: this.hcaptchaSiteKey,
+				hcaptchaSecretKey: this.hcaptchaSecretKey,
+				enableRecaptcha: this.enableRecaptcha,
+				recaptchaSiteKey: this.recaptchaSiteKey,
+				recaptchaSecretKey: this.recaptchaSecretKey,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/admin/database.vue b/src/client/pages/admin/database.vue
new file mode 100644
index 0000000000..ffbeed8b30
--- /dev/null
+++ b/src/client/pages/admin/database.vue
@@ -0,0 +1,61 @@
+<template>
+<FormBase>
+	<FormSuspense :p="databasePromiseFactory" v-slot="{ result: database }">
+		<FormGroup v-for="table in database" :key="table[0]">
+			<template #label>{{ table[0] }}</template>
+			<FormKeyValueView>
+				<template #key>Size</template>
+				<template #value>{{ bytes(table[1].size) }}</template>
+			</FormKeyValueView>
+			<FormKeyValueView>
+				<template #key>Records</template>
+				<template #value>{{ number(table[1].count) }}</template>
+			</FormKeyValueView>
+		</FormGroup>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import bytes from '@client/filters/bytes';
+import number from '@client/filters/number';
+
+export default defineComponent({
+	components: {
+		FormSuspense,
+		FormKeyValueView,
+		FormBase,
+		FormGroup,
+		FormLink,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.database,
+				icon: 'fas fa-database',
+				bg: 'var(--bg)',
+			},
+			databasePromiseFactory: () => os.api('admin/get-table-stats', {}).then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size)),
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		bytes, number,
+	}
+});
+</script>
diff --git a/src/client/pages/admin/email-settings.vue b/src/client/pages/admin/email-settings.vue
new file mode 100644
index 0000000000..ebf724fcdd
--- /dev/null
+++ b/src/client/pages/admin/email-settings.vue
@@ -0,0 +1,128 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormSwitch v-model="enableEmail">{{ $ts.enableEmail }}<template #desc>{{ $ts.emailConfigInfo }}</template></FormSwitch>
+
+		<template v-if="enableEmail">
+			<FormInput v-model="email" type="email">
+				<span>{{ $ts.emailAddress }}</span>
+			</FormInput>
+
+			<div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
+				<div class="_debobigegoLabel">{{ $ts.smtpConfig }}</div>
+				<div class="main">
+					<FormInput v-model="smtpHost">
+						<span>{{ $ts.smtpHost }}</span>
+					</FormInput>
+					<FormInput v-model="smtpPort" type="number">
+						<span>{{ $ts.smtpPort }}</span>
+					</FormInput>
+					<FormInput v-model="smtpUser">
+						<span>{{ $ts.smtpUser }}</span>
+					</FormInput>
+					<FormInput v-model="smtpPass" type="password">
+						<span>{{ $ts.smtpPass }}</span>
+					</FormInput>
+					<FormInfo>{{ $ts.emptyToDisableSmtpAuth }}</FormInfo>
+					<FormSwitch v-model="smtpSecure">{{ $ts.smtpSecure }}<template #desc>{{ $ts.smtpSecureInfo }}</template></FormSwitch>
+				</div>
+			</div>
+
+			<FormButton @click="testEmail">{{ $ts.testEmail }}</FormButton>
+		</template>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/debobigego/switch.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 FormInfo from '@client/components/debobigego/info.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormInfo,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.emailServer,
+				icon: 'fas fa-envelope',
+				bg: 'var(--bg)',
+			},
+			enableEmail: false,
+			email: null,
+			smtpSecure: false,
+			smtpHost: '',
+			smtpPort: 0,
+			smtpUser: '',
+			smtpPass: '',
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.enableEmail = meta.enableEmail;
+			this.email = meta.email;
+			this.smtpSecure = meta.smtpSecure;
+			this.smtpHost = meta.smtpHost;
+			this.smtpPort = meta.smtpPort;
+			this.smtpUser = meta.smtpUser;
+			this.smtpPass = meta.smtpPass;
+		},
+
+		async testEmail() {
+			const { canceled, result: destination } = await os.dialog({
+				title: this.$ts.destination,
+				input: {
+					placeholder: this.$instance.maintainerEmail
+				}
+			});
+			if (canceled) return;
+			os.apiWithDialog('admin/send-email', {
+				to: destination,
+				subject: 'Test email',
+				text: 'Yo'
+			});
+		},
+
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				enableEmail: this.enableEmail,
+				email: this.email,
+				smtpSecure: this.smtpSecure,
+				smtpHost: this.smtpHost,
+				smtpPort: this.smtpPort,
+				smtpUser: this.smtpUser,
+				smtpPass: this.smtpPass,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/admin/emoji-edit-dialog.vue b/src/client/pages/admin/emoji-edit-dialog.vue
new file mode 100644
index 0000000000..4854c69884
--- /dev/null
+++ b/src/client/pages/admin/emoji-edit-dialog.vue
@@ -0,0 +1,120 @@
+<template>
+<XModalWindow ref="dialog"
+	:width="370"
+	:with-ok-button="true"
+	@close="$refs.dialog.close()"
+	@closed="$emit('closed')"
+	@ok="ok()"
+>
+	<template #header>:{{ emoji.name }}:</template>
+
+	<div class="_monolithic_">
+		<div class="yigymqpb _section">
+			<img :src="emoji.url" class="img"/>
+			<MkInput class="_formBlock" v-model="name">
+				<template #label>{{ $ts.name }}</template>
+			</MkInput>
+			<MkInput class="_formBlock" v-model="category" :datalist="categories">
+				<template #label>{{ $ts.category }}</template>
+			</MkInput>
+			<MkInput class="_formBlock" v-model="aliases">
+				<template #label>{{ $ts.tags }}</template>
+				<template #caption>{{ $ts.setMultipleBySeparatingWithSpace }}</template>
+			</MkInput>
+			<MkButton danger @click="del()"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton>
+		</div>
+	</div>
+</XModalWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XModalWindow from '@client/components/ui/modal-window.vue';
+import MkButton from '@client/components/ui/button.vue';
+import MkInput from '@client/components/form/input.vue';
+import * as os from '@client/os';
+import { unique } from '../../../prelude/array';
+
+export default defineComponent({
+	components: {
+		XModalWindow,
+		MkButton,
+		MkInput,
+	},
+
+	props: {
+		emoji: {
+			required: true,
+		}
+	},
+
+	emits: ['done', 'closed'],
+
+	data() {
+		return {
+			name: this.emoji.name,
+			category: this.emoji.category,
+			aliases: this.emoji.aliases?.join(' '),
+			categories: [],
+		}
+	},
+
+	created() {
+		os.api('meta', { detail: false }).then(({ emojis }) => {
+			this.categories = unique(emojis.map((x: any) => x.category || '').filter((x: string) => x !== ''));
+		});
+	},
+
+	methods: {
+		ok() {
+			this.update();
+		},
+
+		async update() {
+			await os.apiWithDialog('admin/emoji/update', {
+				id: this.emoji.id,
+				name: this.name,
+				category: this.category,
+				aliases: this.aliases.split(' '),
+			});
+
+			this.$emit('done', {
+				updated: {
+					name: this.name,
+					category: this.category,
+					aliases: this.aliases.split(' '),
+				}
+			});
+			this.$refs.dialog.close();
+		},
+
+		async del() {
+			const { canceled } = await os.dialog({
+				type: 'warning',
+				text: this.$t('removeAreYouSure', { x: this.emoji.name }),
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			os.api('admin/emoji/remove', {
+				id: this.emoji.id
+			}).then(() => {
+				this.$emit('done', {
+					deleted: true
+				});
+				this.$refs.dialog.close();
+			});
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.yigymqpb {
+	> .img {
+		display: block;
+		height: 64px;
+		margin: 0 auto;
+	}
+}
+</style>
diff --git a/src/client/pages/admin/emojis.vue b/src/client/pages/admin/emojis.vue
new file mode 100644
index 0000000000..4cd34b046d
--- /dev/null
+++ b/src/client/pages/admin/emojis.vue
@@ -0,0 +1,262 @@
+<template>
+<div class="ogwlenmc">
+	<MkHeader :info="header"/>
+
+	<div class="local" v-if="tab === 'local'">
+		<MkInput v-model="query" :debounce="true" type="search" style="margin: var(--margin);">
+			<template #prefix><i class="fas fa-search"></i></template>
+			<template #label>{{ $ts.search }}</template>
+		</MkInput>
+		<MkPagination :pagination="pagination" ref="emojis">
+			<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
+			<template #default="{items}">
+				<div class="ldhfsamy">
+					<button class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="edit(emoji)">
+						<img :src="emoji.url" class="img" :alt="emoji.name"/>
+						<div class="body">
+							<div class="name _monospace">{{ emoji.name }}</div>
+							<div class="info">{{ emoji.category }}</div>
+						</div>
+					</button>
+				</div>
+			</template>
+		</MkPagination>
+	</div>
+
+	<div class="remote" v-else-if="tab === 'remote'">
+		<MkInput v-model="queryRemote" :debounce="true" type="search" style="margin: var(--margin);">
+			<template #prefix><i class="fas fa-search"></i></template>
+			<template #label>{{ $ts.search }}</template>
+		</MkInput>
+		<MkInput v-model="host" :debounce="true" style="margin: var(--margin);">
+			<template #label>{{ $ts.host }}</template>
+		</MkInput>
+		<MkPagination :pagination="remotePagination" ref="remoteEmojis">
+			<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
+			<template #default="{items}">
+				<div class="ldhfsamy">
+					<div class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="remoteMenu(emoji, $event)">
+						<img :src="emoji.url" class="img" :alt="emoji.name"/>
+						<div class="body">
+							<div class="name _monospace">{{ emoji.name }}</div>
+							<div class="info">{{ emoji.host }}</div>
+						</div>
+					</div>
+				</div>
+			</template>
+		</MkPagination>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import MkButton from '@client/components/ui/button.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkPagination from '@client/components/ui/pagination.vue';
+import MkTab from '@client/components/tab.vue';
+import { selectFile } from '@client/scripts/select-file';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+
+export default defineComponent({
+	components: {
+		MkTab,
+		MkButton,
+		MkInput,
+		MkPagination,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.customEmojis,
+				icon: 'fas fa-laugh',
+				bg: 'var(--bg)',
+			},
+			header: computed(() => ({
+				title: this.$ts.customEmojis,
+				icon: 'fas fa-laugh',
+				bg: 'var(--bg)',
+				actions: [{
+					asFullButton: true,
+					icon: 'fas fa-plus',
+					text: this.$ts.addEmoji,
+					handler: this.add,
+				}],
+				tabs: [{
+					active: this.tab === 'local',
+					title: this.$ts.local,
+					onClick: () => { this.tab = 'local'; },
+				}, {
+					active: this.tab === 'remote',
+					title: this.$ts.remote,
+					onClick: () => { this.tab = 'remote'; },
+				},]
+			})),
+			tab: 'local',
+			query: null,
+			queryRemote: null,
+			host: '',
+			pagination: {
+				endpoint: 'admin/emoji/list',
+				limit: 30,
+				params: computed(() => ({
+					query: (this.query && this.query !== '') ? this.query : null
+				}))
+			},
+			remotePagination: {
+				endpoint: 'admin/emoji/list-remote',
+				limit: 30,
+				params: computed(() => ({
+					query: (this.queryRemote && this.queryRemote !== '') ? this.queryRemote : null,
+					host: (this.host && this.host !== '') ? this.host : null
+				}))
+			},
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async add(e) {
+			const files = await selectFile(e.currentTarget || e.target, null, true);
+
+			const promise = Promise.all(files.map(file => os.api('admin/emoji/add', {
+				fileId: file.id,
+			})));
+			promise.then(() => {
+				this.$refs.emojis.reload();
+			});
+			os.promiseDialog(promise);
+		},
+
+		edit(emoji) {
+			os.popup(import('./emoji-edit-dialog.vue'), {
+				emoji: emoji
+			}, {
+				done: result => {
+					if (result.updated) {
+						this.$refs.emojis.replaceItem(item => item.id === emoji.id, {
+							...emoji,
+							...result.updated
+						});
+					} else if (result.deleted) {
+						this.$refs.emojis.removeItem(item => item.id === emoji.id);
+					}
+				},
+			}, 'closed');
+		},
+
+		im(emoji) {
+			os.apiWithDialog('admin/emoji/copy', {
+				emojiId: emoji.id,
+			});
+		},
+
+		remoteMenu(emoji, ev) {
+			os.popupMenu([{
+				type: 'label',
+				text: ':' + emoji.name + ':',
+			}, {
+				text: this.$ts.import,
+				icon: 'fas fa-plus',
+				action: () => { this.im(emoji) }
+			}], ev.currentTarget || ev.target);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ogwlenmc {
+	> .local {
+		.ldhfsamy {
+			display: grid;
+			grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
+			grid-gap: 12px;
+			margin: var(--margin);
+	
+			> .emoji {
+				display: flex;
+				align-items: center;
+				padding: 12px;
+				text-align: left;
+
+				&:hover {
+					color: var(--accent);
+				}
+
+				> .img {
+					width: 42px;
+					height: 42px;
+				}
+
+				> .body {
+					padding: 0 0 0 8px;
+					white-space: nowrap;
+					overflow: hidden;
+
+					> .name {
+						text-overflow: ellipsis;
+						overflow: hidden;
+					}
+
+					> .info {
+						opacity: 0.5;
+						text-overflow: ellipsis;
+						overflow: hidden;
+					}
+				}
+			}
+		}
+	}
+
+	> .remote {
+		.ldhfsamy {
+			display: grid;
+			grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
+			grid-gap: 12px;
+			margin: var(--margin);
+
+			> .emoji {
+				display: flex;
+				align-items: center;
+				padding: 12px;
+				text-align: left;
+
+				&:hover {
+					color: var(--accent);
+				}
+
+				> .img {
+					width: 32px;
+					height: 32px;
+				}
+
+				> .body {
+					padding: 0 0 0 8px;
+					white-space: nowrap;
+					overflow: hidden;
+
+					> .name {
+						text-overflow: ellipsis;
+						overflow: hidden;
+					}
+
+					> .info {
+						opacity: 0.5;
+						font-size: 90%;
+						text-overflow: ellipsis;
+						overflow: hidden;
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/admin/file-dialog.vue b/src/client/pages/admin/file-dialog.vue
new file mode 100644
index 0000000000..02d83e5022
--- /dev/null
+++ b/src/client/pages/admin/file-dialog.vue
@@ -0,0 +1,129 @@
+<template>
+<XModalWindow ref="dialog"
+	:width="370"
+	@close="$refs.dialog.close()"
+	@closed="$emit('closed')"
+>
+	<template #header v-if="file">{{ file.name }}</template>
+	<div class="cxqhhsmd" v-if="file">
+		<div class="_section">
+			<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
+			<div class="info">
+				<span style="margin-right: 1em;">{{ file.type }}</span>
+				<span>{{ bytes(file.size) }}</span>
+				<MkTime :time="file.createdAt" mode="detail" style="display: block;"/>
+			</div>
+		</div>
+		<div class="_section">
+			<div class="_content">
+				<MkSwitch @update:modelValue="toggleIsSensitive" v-model="isSensitive">NSFW</MkSwitch>
+			</div>
+		</div>
+		<div class="_section">
+			<div class="_content">
+				<MkButton full @click="showUser"><i class="fas fa-external-link-square-alt"></i> {{ $ts.user }}</MkButton>
+				<MkButton full danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton>
+			</div>
+		</div>
+		<div class="_section" v-if="info">
+			<details class="_content rawdata">
+				<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre>
+			</details>
+		</div>
+	</div>
+</XModalWindow>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import MkButton from '@client/components/ui/button.vue';
+import MkSwitch from '@client/components/form/switch.vue';
+import XModalWindow from '@client/components/ui/modal-window.vue';
+import MkDriveFileThumbnail from '@client/components/drive-file-thumbnail.vue';
+import Progress from '@client/scripts/loading';
+import bytes from '@client/filters/bytes';
+import * as os from '@client/os';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkSwitch,
+		XModalWindow,
+		MkDriveFileThumbnail,
+	},
+
+	props: {
+		fileId: {
+			required: true,
+		}
+	},
+
+	emits: ['closed'],
+
+	data() {
+		return {
+			file: null,
+			info: null,
+			isSensitive: false,
+		};
+	},
+
+	created() {
+		this.fetch();
+	},
+
+	methods: {
+		async fetch() {
+			Progress.start();
+			this.file = await os.api('drive/files/show', { fileId: this.fileId });
+			this.info = await os.api('admin/drive/show-file', { fileId: this.fileId });
+			this.isSensitive = this.file.isSensitive;
+			Progress.done();
+		},
+
+		showUser() {
+			os.pageWindow(`/user-info/${this.file.userId}`);
+		},
+
+		async del() {
+			const { canceled } = await os.dialog({
+				type: 'warning',
+				text: this.$t('removeAreYouSure', { x: this.file.name }),
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			os.apiWithDialog('drive/files/delete', {
+				fileId: this.file.id
+			});
+		},
+
+		async toggleIsSensitive(v) {
+			await os.api('drive/files/update', { fileId: this.fileId, isSensitive: v });
+			this.isSensitive = v;
+		},
+
+		bytes
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.cxqhhsmd {
+	> ._section {
+		> .thumbnail {
+			height: 150px;
+			max-width: 100%;
+		}
+
+		> .info {
+			text-align: center;
+			margin-top: 8px;
+		}
+		
+		> .rawdata {
+			overflow: auto;
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/admin/files-settings.vue b/src/client/pages/admin/files-settings.vue
new file mode 100644
index 0000000000..8aefa9e90d
--- /dev/null
+++ b/src/client/pages/admin/files-settings.vue
@@ -0,0 +1,93 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormSwitch v-model="cacheRemoteFiles">
+			{{ $ts.cacheRemoteFiles }}
+			<template #desc>{{ $ts.cacheRemoteFilesDescription }}</template>
+		</FormSwitch>
+
+		<FormSwitch v-model="proxyRemoteFiles">
+			{{ $ts.proxyRemoteFiles }}
+			<template #desc>{{ $ts.proxyRemoteFilesDescription }}</template>
+		</FormSwitch>
+
+		<FormInput v-model="localDriveCapacityMb" type="number">
+			<span>{{ $ts.driveCapacityPerLocalAccount }}</span>
+			<template #suffix>MB</template>
+			<template #desc>{{ $ts.inMb }}</template>
+		</FormInput>
+
+		<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">
+			<span>{{ $ts.driveCapacityPerRemoteAccount }}</span>
+			<template #suffix>MB</template>
+			<template #desc>{{ $ts.inMb }}</template>
+		</FormInput>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/debobigego/switch.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 FormSuspense from '@client/components/debobigego/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.files,
+				icon: 'fas fa-cloud',
+				bg: 'var(--bg)',
+			},
+			cacheRemoteFiles: false,
+			proxyRemoteFiles: false,
+			localDriveCapacityMb: 0,
+			remoteDriveCapacityMb: 0,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.cacheRemoteFiles = meta.cacheRemoteFiles;
+			this.proxyRemoteFiles = meta.proxyRemoteFiles;
+			this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
+			this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
+		},
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				cacheRemoteFiles: this.cacheRemoteFiles,
+				proxyRemoteFiles: this.proxyRemoteFiles,
+				localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
+				remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/admin/files.vue b/src/client/pages/admin/files.vue
new file mode 100644
index 0000000000..55189cfd84
--- /dev/null
+++ b/src/client/pages/admin/files.vue
@@ -0,0 +1,209 @@
+<template>
+<div class="xrmjdkdw">
+	<MkContainer :foldable="true" class="lookup">
+		<template #header><i class="fas fa-search"></i> {{ $ts.lookup }}</template>
+		<div class="xrmjdkdw-lookup">
+			<MkInput class="item" v-model="q" type="text" @enter="find()">
+				<template #label>{{ $ts.fileIdOrUrl }}</template>
+			</MkInput>
+			<MkButton @click="find()" primary><i class="fas fa-search"></i> {{ $ts.lookup }}</MkButton>
+		</div>
+	</MkContainer>
+
+	<div class="_section">
+		<div class="_content">
+			<div class="inputs" style="display: flex;">
+				<MkSelect v-model="origin" style="margin: 0; flex: 1;">
+					<template #label>{{ $ts.instance }}</template>
+					<option value="combined">{{ $ts.all }}</option>
+					<option value="local">{{ $ts.local }}</option>
+					<option value="remote">{{ $ts.remote }}</option>
+				</MkSelect>
+				<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params().origin === 'local'">
+					<template #label>{{ $ts.host }}</template>
+				</MkInput>
+			</div>
+			<div class="inputs" style="display: flex; padding-top: 1.2em;">
+				<MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;">
+					<template #label>MIME type</template>
+				</MkInput>
+			</div>
+			<MkPagination :pagination="pagination" #default="{items}" class="urempief" ref="files">
+				<button class="file _panel _button _gap" v-for="file in items" :key="file.id" @click="show(file, $event)">
+					<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
+					<div class="body">
+						<div>
+							<small style="opacity: 0.7;">{{ file.name }}</small>
+						</div>
+						<div>
+							<MkAcct v-if="file.user" :user="file.user"/>
+							<div v-else>{{ $ts.system }}</div>
+						</div>
+						<div>
+							<span style="margin-right: 1em;">{{ file.type }}</span>
+							<span>{{ bytes(file.size) }}</span>
+						</div>
+						<div>
+							<span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span>
+						</div>
+					</div>
+				</button>
+			</MkPagination>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@client/components/ui/button.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkSelect from '@client/components/form/select.vue';
+import MkPagination from '@client/components/ui/pagination.vue';
+import MkContainer from '@client/components/ui/container.vue';
+import MkDriveFileThumbnail from '@client/components/drive-file-thumbnail.vue';
+import bytes from '@client/filters/bytes';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkInput,
+		MkSelect,
+		MkPagination,
+		MkContainer,
+		MkDriveFileThumbnail,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.files,
+				icon: 'fas fa-cloud',
+				bg: 'var(--bg)',
+				actions: [{
+					text: this.$ts.clearCachedFiles,
+					icon: 'fas fa-trash-alt',
+					handler: this.clear
+				}]
+			},
+			q: null,
+			origin: 'local',
+			type: null,
+			searchHost: '',
+			pagination: {
+				endpoint: 'admin/drive/files',
+				limit: 10,
+				params: () => ({
+					type: (this.type && this.type !== '') ? this.type : null,
+					origin: this.origin,
+					hostname: (this.hostname && this.hostname !== '') ? this.hostname : null,
+				}),
+			},
+		}
+	},
+
+	watch: {
+		type() {
+			this.$refs.files.reload();
+		},
+		origin() {
+			this.$refs.files.reload();
+		},
+		searchHost() {
+			this.$refs.files.reload();
+		},
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		clear() {
+			os.dialog({
+				type: 'warning',
+				text: this.$ts.clearCachedFilesConfirm,
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+
+				os.apiWithDialog('admin/drive/clean-remote-files', {});
+			});
+		},
+
+		show(file, ev) {
+			os.popup(import('./file-dialog.vue'), {
+				fileId: file.id
+			}, {}, 'closed');
+		},
+
+		find() {
+			os.api('admin/drive/show-file', this.q.startsWith('http://') || this.q.startsWith('https://') ? { url: this.q.trim() } : { fileId: this.q.trim() }).then(file => {
+				this.show(file);
+			}).catch(e => {
+				if (e.code === 'NO_SUCH_FILE') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts.notFound
+					});
+				}
+			});
+		},
+
+		bytes
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.xrmjdkdw {
+	margin: var(--margin);
+
+	> .lookup {
+		margin-bottom: 16px;
+	}
+
+	.urempief {
+		margin-top: var(--margin);
+
+		> .file {
+			display: flex;
+			width: 100%;
+			box-sizing: border-box;
+			text-align: left;
+			align-items: center;
+
+			&:hover {
+				color: var(--accent);
+			}
+
+			> .thumbnail {
+				width: 128px;
+				height: 128px;
+			}
+
+			> .body {
+				margin-left: 0.3em;
+				padding: 8px;
+				flex: 1;
+
+				@media (max-width: 500px) {
+					font-size: 14px;
+				}
+			}
+		}
+	}
+}
+
+.xrmjdkdw-lookup {
+	padding: 16px;
+
+	> .item {
+		margin-bottom: 16px;
+	}
+}
+</style>
diff --git a/src/client/pages/admin/index.vue b/src/client/pages/admin/index.vue
new file mode 100644
index 0000000000..7094f2d4bf
--- /dev/null
+++ b/src/client/pages/admin/index.vue
@@ -0,0 +1,373 @@
+<template>
+<div class="hiyeyicy" :class="{ wide: !narrow }" ref="el">
+	<div class="nav" v-if="!narrow || page == null">
+		<MkHeader :info="header"></MkHeader>
+	
+		<div class="lxpfedzu">
+			<img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
+		</div>
+
+		<MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
+		<MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/admin/bot-protection" class="_link">{{ $ts.configure }}</MkA></MkInfo>
+
+		<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
+	</div>
+	<div class="main">
+		<component :is="component" :key="page" @info="onInfo" v-bind="pageProps"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, reactive, ref, watch } from 'vue';
+import { i18n } from '@client/i18n';
+import MkSuperMenu from '@client/components/ui/super-menu.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import MkInfo from '@client/components/ui/info.vue';
+import { scroll } from '@client/scripts/scroll';
+import { instance } from '@client/instance';
+import * as symbols from '@client/symbols';
+import * as os from '@client/os';
+import { lookupUser } from '@client/scripts/lookup-user';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		MkSuperMenu,
+		FormGroup,
+		FormButton,
+		MkInfo,
+	},
+
+	props: {
+		initialPage: {
+			type: String,
+			required: false
+		}
+	},
+
+	setup(props, context) {
+		const indexInfo = {
+			title: i18n.locale.instance,
+			icon: 'fas fa-cog',
+			bg: 'var(--bg)',
+		};
+		const INFO = ref(indexInfo);
+		const page = ref(props.initialPage);
+		const narrow = ref(false);
+		const view = ref(null);
+		const el = ref(null);
+		const onInfo = (viewInfo) => {
+			INFO.value = viewInfo;
+		};
+		const pageProps = ref({});
+
+		const isEmpty = (x: any) => x == null || x == '';
+
+		const noMaintainerInformation = ref(false);
+		const noBotProtection = ref(false);
+
+		os.api('meta', { detail: true }).then(meta => {
+			// TODO: 設定が完了しても残ったままになるので、ストリーミングでmeta更新イベントを受け取ってよしなに更新する
+			noMaintainerInformation.value = isEmpty(meta.maintainerName) || isEmpty(meta.maintainerEmail);
+			noBotProtection.value = !meta.enableHcaptcha && !meta.enableRecaptcha;
+		});
+
+		const menuDef = computed(() => [{
+			title: i18n.locale.quickAction,
+			items: [{
+				type: 'button',
+				icon: 'fas fa-search',
+				text: i18n.locale.lookup,
+				action: lookup,
+			}, ...(instance.disableRegistration ? [{
+				type: 'button',
+				icon: 'fas fa-user',
+				text: i18n.locale.invite,
+				action: invite,
+			}] : [])],
+		}, {
+			title: i18n.locale.administration,
+			items: [{
+				icon: 'fas fa-tachometer-alt',
+				text: i18n.locale.dashboard,
+				to: '/admin/overview',
+				active: page.value === 'overview',
+			}, {
+				icon: 'fas fa-users',
+				text: i18n.locale.users,
+				to: '/admin/users',
+				active: page.value === 'users',
+			}, {
+				icon: 'fas fa-laugh',
+				text: i18n.locale.customEmojis,
+				to: '/admin/emojis',
+				active: page.value === 'emojis',
+			}, {
+				icon: 'fas fa-globe',
+				text: i18n.locale.federation,
+				to: '/admin/federation',
+				active: page.value === 'federation',
+			}, {
+				icon: 'fas fa-clipboard-list',
+				text: i18n.locale.jobQueue,
+				to: '/admin/queue',
+				active: page.value === 'queue',
+			}, {
+				icon: 'fas fa-cloud',
+				text: i18n.locale.files,
+				to: '/admin/files',
+				active: page.value === 'files',
+			}, {
+				icon: 'fas fa-broadcast-tower',
+				text: i18n.locale.announcements,
+				to: '/admin/announcements',
+				active: page.value === 'announcements',
+			}, {
+				icon: 'fas fa-audio-description',
+				text: i18n.locale.ads,
+				to: '/admin/ads',
+				active: page.value === 'ads',
+			}, {
+				icon: 'fas fa-exclamation-circle',
+				text: i18n.locale.abuseReports,
+				to: '/admin/abuses',
+				active: page.value === 'abuses',
+			}],
+		}, {
+			title: i18n.locale.settings,
+			items: [{
+				icon: 'fas fa-cog',
+				text: i18n.locale.general,
+				to: '/admin/settings',
+				active: page.value === 'settings',
+			}, {
+				icon: 'fas fa-cloud',
+				text: i18n.locale.files,
+				to: '/admin/files-settings',
+				active: page.value === 'files-settings',
+			}, {
+				icon: 'fas fa-envelope',
+				text: i18n.locale.emailServer,
+				to: '/admin/email-settings',
+				active: page.value === 'email-settings',
+			}, {
+				icon: 'fas fa-cloud',
+				text: i18n.locale.objectStorage,
+				to: '/admin/object-storage',
+				active: page.value === 'object-storage',
+			}, {
+				icon: 'fas fa-lock',
+				text: i18n.locale.security,
+				to: '/admin/security',
+				active: page.value === 'security',
+			}, {
+				icon: 'fas fa-bolt',
+				text: 'ServiceWorker',
+				to: '/admin/service-worker',
+				active: page.value === 'service-worker',
+			}, {
+				icon: 'fas fa-globe',
+				text: i18n.locale.relays,
+				to: '/admin/relays',
+				active: page.value === 'relays',
+			}, {
+				icon: 'fas fa-share-alt',
+				text: i18n.locale.integration,
+				to: '/admin/integrations',
+				active: page.value === 'integrations',
+			}, {
+				icon: 'fas fa-ban',
+				text: i18n.locale.instanceBlocking,
+				to: '/admin/instance-block',
+				active: page.value === 'instance-block',
+			}, {
+				icon: 'fas fa-ghost',
+				text: i18n.locale.proxyAccount,
+				to: '/admin/proxy-account',
+				active: page.value === 'proxy-account',
+			}, {
+				icon: 'fas fa-cogs',
+				text: i18n.locale.other,
+				to: '/admin/other-settings',
+				active: page.value === 'other-settings',
+			}],
+		}, {
+			title: i18n.locale.info,
+			items: [{
+				icon: 'fas fa-database',
+				text: i18n.locale.database,
+				to: '/admin/database',
+				active: page.value === 'database',
+			}, {
+				icon: 'fas fa-stream',
+				text: i18n.locale.logs,
+				to: '/admin/logs',
+				active: page.value === 'logs',
+			}],
+		}]);
+		const component = computed(() => {
+			if (page.value == null) return null;
+			switch (page.value) {
+				case 'overview': return defineAsyncComponent(() => import('./overview.vue'));
+				case 'users': return defineAsyncComponent(() => import('./users.vue'));
+				case 'emojis': return defineAsyncComponent(() => import('./emojis.vue'));
+				case 'federation': return defineAsyncComponent(() => import('../federation.vue'));
+				case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
+				case 'files': return defineAsyncComponent(() => import('./files.vue'));
+				case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
+				case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
+				case 'database': return defineAsyncComponent(() => import('./database.vue'));
+				case 'logs': return defineAsyncComponent(() => import('./logs.vue'));
+				case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
+				case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
+				case 'files-settings': return defineAsyncComponent(() => import('./files-settings.vue'));
+				case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue'));
+				case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
+				case 'security': return defineAsyncComponent(() => import('./security.vue'));
+				case 'bot-protection': return defineAsyncComponent(() => import('./bot-protection.vue'));
+				case 'service-worker': return defineAsyncComponent(() => import('./service-worker.vue'));
+				case 'relays': return defineAsyncComponent(() => import('./relays.vue'));
+				case 'integrations': return defineAsyncComponent(() => import('./integrations.vue'));
+				case 'integrations/twitter': return defineAsyncComponent(() => import('./integrations-twitter.vue'));
+				case 'integrations/github': return defineAsyncComponent(() => import('./integrations-github.vue'));
+				case 'integrations/discord': return defineAsyncComponent(() => import('./integrations-discord.vue'));
+				case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue'));
+				case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue'));
+				case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue'));
+			}
+		});
+
+		watch(component, () => {
+			pageProps.value = {};
+
+			nextTick(() => {
+				scroll(el.value, { top: 0 });
+			});
+		}, { immediate: true });
+
+		watch(() => props.initialPage, () => {
+			if (props.initialPage == null && !narrow.value) {
+				page.value = 'overview';
+			} else {
+				page.value = props.initialPage;
+				if (props.initialPage == null) {
+					INFO.value = indexInfo;
+				}
+			}
+		});
+
+		onMounted(() => {
+			narrow.value = el.value.offsetWidth < 800;
+			if (!narrow.value) {
+				page.value = 'overview';
+			}
+		});
+
+		const invite = () => {
+			os.api('admin/invite').then(x => {
+				os.dialog({
+					type: 'info',
+					text: x.code
+				});
+			}).catch(e => {
+				os.dialog({
+					type: 'error',
+					text: e
+				});
+			});
+		};
+
+		const lookup = (ev) => {
+			os.popupMenu([{
+				text: i18n.locale.user,
+				icon: 'fas fa-user',
+				action: () => {
+					lookupUser();
+				}
+			}, {
+				text: i18n.locale.note,
+				icon: 'fas fa-pencil-alt',
+				action: () => {
+					alert('TODO');
+				}
+			}, {
+				text: i18n.locale.file,
+				icon: 'fas fa-cloud',
+				action: () => {
+					alert('TODO');
+				}
+			}, {
+				text: i18n.locale.instance,
+				icon: 'fas fa-globe',
+				action: () => {
+					alert('TODO');
+				}
+			}], ev.currentTarget || ev.target);
+		};
+
+		return {
+			[symbols.PAGE_INFO]: INFO,
+			menuDef,
+			header: {
+				title: i18n.locale.controllPanel,
+			},
+			noMaintainerInformation,
+			noBotProtection,
+			page,
+			narrow,
+			view,
+			el,
+			onInfo,
+			pageProps,
+			component,
+			invite,
+			lookup,
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.hiyeyicy {
+	&.wide {
+		display: flex;
+		margin: 0 auto;
+		height: 100%;
+
+		> .nav {
+			width: 32%;
+			max-width: 280px;
+			box-sizing: border-box;
+			border-right: solid 0.5px var(--divider);
+			overflow: auto;
+			height: 100%;
+		}
+
+		> .main {
+			flex: 1;
+			min-width: 0;
+			--baseContentWidth: 100%;
+		}
+	}
+
+	> .nav {
+		> .info {
+			margin: 16px;
+		}
+	}
+}
+
+.lxpfedzu {
+	margin: 16px;
+
+	> .icon {
+		display: block;
+		margin: auto;
+		height: 42px;
+		border-radius: 8px;
+	}
+}
+</style>
diff --git a/src/client/pages/admin/instance-block.vue b/src/client/pages/admin/instance-block.vue
new file mode 100644
index 0000000000..105cdb4941
--- /dev/null
+++ b/src/client/pages/admin/instance-block.vue
@@ -0,0 +1,72 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormTextarea v-model="blockedHosts">
+			<span>{{ $ts.blockedInstances }}</span>
+			<template #desc>{{ $ts.blockedInstancesDescription }}</template>
+		</FormTextarea>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/debobigego/switch.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 * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormTextarea,
+		FormInfo,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.instanceBlocking,
+				icon: 'fas fa-ban',
+				bg: 'var(--bg)',
+			},
+			blockedHosts: '',
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.blockedHosts = meta.blockedHosts.join('\n');
+		},
+
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				blockedHosts: this.blockedHosts.split('\n') || [],
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/admin/instance.vue b/src/client/pages/admin/instance.vue
new file mode 100644
index 0000000000..5572fbbf75
--- /dev/null
+++ b/src/client/pages/admin/instance.vue
@@ -0,0 +1,321 @@
+<template>
+<XModalWindow ref="dialog"
+	:width="520"
+	:height="500"
+	@close="$refs.dialog.close()"
+	@closed="$emit('closed')"
+>
+	<template #header>{{ instance.host }}</template>
+	<div class="mk-instance-info">
+		<div class="_table section">
+			<div class="_row">
+				<div class="_cell">
+					<div class="_label">{{ $ts.software }}</div>
+					<div class="_data">{{ instance.softwareName || '?' }}</div>
+				</div>
+				<div class="_cell">
+					<div class="_label">{{ $ts.version }}</div>
+					<div class="_data">{{ instance.softwareVersion || '?' }}</div>
+				</div>
+			</div>
+		</div>
+		<div class="_table data section">
+			<div class="_row">
+				<div class="_cell">
+					<div class="_label">{{ $ts.registeredAt }}</div>
+					<div class="_data">{{ new Date(instance.caughtAt).toLocaleString() }} (<MkTime :time="instance.caughtAt"/>)</div>
+				</div>
+			</div>
+			<div class="_row">
+				<div class="_cell">
+					<div class="_label">{{ $ts.following }}</div>
+					<button class="_data _textButton" @click="showFollowing()">{{ number(instance.followingCount) }}</button>
+				</div>
+				<div class="_cell">
+					<div class="_label">{{ $ts.followers }}</div>
+					<button class="_data _textButton" @click="showFollowers()">{{ number(instance.followersCount) }}</button>
+				</div>
+			</div>
+			<div class="_row">
+				<div class="_cell">
+					<div class="_label">{{ $ts.users }}</div>
+					<button class="_data _textButton" @click="showUsers()">{{ number(instance.usersCount) }}</button>
+				</div>
+				<div class="_cell">
+					<div class="_label">{{ $ts.notes }}</div>
+					<div class="_data">{{ number(instance.notesCount) }}</div>
+				</div>
+			</div>
+			<div class="_row">
+				<div class="_cell">
+					<div class="_label">{{ $ts.files }}</div>
+					<div class="_data">{{ number(instance.driveFiles) }}</div>
+				</div>
+				<div class="_cell">
+					<div class="_label">{{ $ts.storageUsage }}</div>
+					<div class="_data">{{ bytes(instance.driveUsage) }}</div>
+				</div>
+			</div>
+			<div class="_row">
+				<div class="_cell">
+					<div class="_label">{{ $ts.latestRequestSentAt }}</div>
+					<div class="_data"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
+				</div>
+				<div class="_cell">
+					<div class="_label">{{ $ts.latestStatus }}</div>
+					<div class="_data">{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</div>
+				</div>
+			</div>
+			<div class="_row">
+				<div class="_cell">
+					<div class="_label">{{ $ts.latestRequestReceivedAt }}</div>
+					<div class="_data"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
+				</div>
+			</div>
+		</div>
+		<div class="chart">
+			<div class="header">
+				<span class="label">{{ $ts.charts }}</span>
+				<div class="selects">
+					<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
+						<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>
+						<option value="day">{{ $ts.perDay }}</option>
+					</MkSelect>
+				</div>
+			</div>
+			<div class="chart">
+				<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
+			</div>
+		</div>
+		<div class="operations section">
+			<span class="label">{{ $ts.operations }}</span>
+			<MkSwitch v-model="isSuspended" class="switch">{{ $ts.stopActivityDelivery }}</MkSwitch>
+			<MkSwitch :model-value="isBlocked" class="switch" @update:modelValue="changeBlock">{{ $ts.blockThisInstance }}</MkSwitch>
+			<details>
+				<summary>{{ $ts.deleteAllFiles }}</summary>
+				<MkButton @click="deleteAllFiles()" style="margin: 0.5em 0 0.5em 0;"><i class="fas fa-trash-alt"></i> {{ $ts.deleteAllFiles }}</MkButton>
+			</details>
+			<details>
+				<summary>{{ $ts.removeAllFollowing }}</summary>
+				<MkButton @click="removeAllFollowing()" style="margin: 0.5em 0 0.5em 0;"><i class="fas fa-minus-circle"></i> {{ $ts.removeAllFollowing }}</MkButton>
+				<MkInfo warn>{{ $t('removeAllFollowingDescription', { host: instance.host }) }}</MkInfo>
+			</details>
+		</div>
+		<details class="metadata section">
+			<summary class="label">{{ $ts.metadata }}</summary>
+			<pre><code>{{ JSON.stringify(instance, null, 2) }}</code></pre>
+		</details>
+	</div>
+</XModalWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+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';
+
+export default defineComponent({
+	components: {
+		XModalWindow,
+		MkSelect,
+		MkButton,
+		MkSwitch,
+		MkInfo,
+		MkChart,
+	},
+
+	props: {
+		instance: {
+			type: Object,
+			required: true
+		}
+	},
+
+	emits: ['closed'],
+
+	data() {
+		return {
+			isSuspended: this.instance.isSuspended,
+			chartSrc: 'requests',
+			chartSpan: 'hour',
+		};
+	},
+
+	computed: {
+		meta() {
+			return this.$instance;
+		},
+
+		isBlocked() {
+			return this.meta && this.meta.blockedHosts && this.meta.blockedHosts.includes(this.instance.host);
+		}
+	},
+
+	watch: {
+		isSuspended() {
+			os.api('admin/federation/update-instance', {
+				host: this.instance.host,
+				isSuspended: this.isSuspended
+			});
+		},
+	},
+
+	methods: {
+		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)
+			});
+		},
+
+		removeAllFollowing() {
+			os.apiWithDialog('admin/federation/remove-all-following', {
+				host: this.instance.host
+			});
+		},
+
+		deleteAllFiles() {
+			os.apiWithDialog('admin/federation/delete-all-files', {
+				host: this.instance.host
+			});
+		},
+
+		showFollowing() {
+			os.modal(MkUsersDialog, {
+				title: this.$ts.instanceFollowing,
+				pagination: {
+					endpoint: 'federation/following',
+					limit: 10,
+					params: {
+						host: this.instance.host
+					}
+				},
+				extract: item => item.follower
+			});
+		},
+
+		showFollowers() {
+			os.modal(MkUsersDialog, {
+				title: this.$ts.instanceFollowers,
+				pagination: {
+					endpoint: 'federation/followers',
+					limit: 10,
+					params: {
+						host: this.instance.host
+					}
+				},
+				extract: item => item.followee
+			});
+		},
+
+		showUsers() {
+			os.modal(MkUsersDialog, {
+				title: this.$ts.instanceUsers,
+				pagination: {
+					endpoint: 'federation/users',
+					limit: 10,
+					params: {
+						host: this.instance.host
+					}
+				}
+			});
+		},
+
+		bytes,
+
+		number
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-instance-info {
+	overflow: auto;
+
+	> .section {
+		padding: 16px 32px;
+
+		@media (max-width: 500px) {
+			padding: 8px 16px;
+		}
+
+		&:not(:first-child) {
+			border-top: solid 0.5px var(--divider);
+		}
+	}
+
+	> .chart {
+		border-top: solid 0.5px var(--divider);
+		padding: 16px 0 12px 0;
+
+		> .header {
+			padding: 0 32px;
+
+			@media (max-width: 500px) {
+				padding: 0 16px;
+			}
+
+			> .label {
+				font-size: 80%;
+				opacity: 0.7;
+			}
+
+			> .selects {
+				display: flex;
+			}
+		}
+
+		> .chart {
+			padding: 0 16px;
+
+			@media (max-width: 500px) {
+				padding: 0;
+			}
+		}
+	}
+
+	> .operations {
+		> .label {
+			font-size: 80%;
+			opacity: 0.7;
+		}
+
+		> .switch {
+			margin: 16px 0;
+		}
+	}
+
+	> .metadata {
+		> .label {
+			font-size: 80%;
+			opacity: 0.7;
+		}
+
+		> pre > code {
+			display: block;
+			max-height: 200px;
+			overflow: auto;
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/admin/integrations-discord.vue b/src/client/pages/admin/integrations-discord.vue
new file mode 100644
index 0000000000..c33b24f17f
--- /dev/null
+++ b/src/client/pages/admin/integrations-discord.vue
@@ -0,0 +1,85 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormSwitch v-model="enableDiscordIntegration">
+			{{ $ts.enable }}
+		</FormSwitch>
+
+		<template v-if="enableDiscordIntegration">
+			<FormInfo>Callback URL: {{ `${url}/api/dc/cb` }}</FormInfo>
+		
+			<FormInput v-model="discordClientId">
+				<template #prefix><i class="fas fa-key"></i></template>
+				Client ID
+			</FormInput>
+
+			<FormInput v-model="discordClientSecret">
+				<template #prefix><i class="fas fa-key"></i></template>
+				Client Secret
+			</FormInput>
+		</template>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/debobigego/switch.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 FormInfo from '@client/components/debobigego/info.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormInfo,
+		FormButton,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: 'Discord',
+				icon: 'fab fa-discord'
+			},
+			enableDiscordIntegration: false,
+			discordClientId: null,
+			discordClientSecret: null,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.enableDiscordIntegration = meta.enableDiscordIntegration;
+			this.discordClientId = meta.discordClientId;
+			this.discordClientSecret = meta.discordClientSecret;
+		},
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				enableDiscordIntegration: this.enableDiscordIntegration,
+				discordClientId: this.discordClientId,
+				discordClientSecret: this.discordClientSecret,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/admin/integrations-github.vue b/src/client/pages/admin/integrations-github.vue
new file mode 100644
index 0000000000..cdf85868ff
--- /dev/null
+++ b/src/client/pages/admin/integrations-github.vue
@@ -0,0 +1,85 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormSwitch v-model="enableGithubIntegration">
+			{{ $ts.enable }}
+		</FormSwitch>
+
+		<template v-if="enableGithubIntegration">
+			<FormInfo>Callback URL: {{ `${url}/api/gh/cb` }}</FormInfo>
+		
+			<FormInput v-model="githubClientId">
+				<template #prefix><i class="fas fa-key"></i></template>
+				Client ID
+			</FormInput>
+
+			<FormInput v-model="githubClientSecret">
+				<template #prefix><i class="fas fa-key"></i></template>
+				Client Secret
+			</FormInput>
+		</template>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/debobigego/switch.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 FormInfo from '@client/components/debobigego/info.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormInfo,
+		FormButton,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: 'GitHub',
+				icon: 'fab fa-github'
+			},
+			enableGithubIntegration: false,
+			githubClientId: null,
+			githubClientSecret: null,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.enableGithubIntegration = meta.enableGithubIntegration;
+			this.githubClientId = meta.githubClientId;
+			this.githubClientSecret = meta.githubClientSecret;
+		},
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				enableGithubIntegration: this.enableGithubIntegration,
+				githubClientId: this.githubClientId,
+				githubClientSecret: this.githubClientSecret,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/admin/integrations-twitter.vue b/src/client/pages/admin/integrations-twitter.vue
new file mode 100644
index 0000000000..ed7d097d0a
--- /dev/null
+++ b/src/client/pages/admin/integrations-twitter.vue
@@ -0,0 +1,85 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormSwitch v-model="enableTwitterIntegration">
+			{{ $ts.enable }}
+		</FormSwitch>
+
+		<template v-if="enableTwitterIntegration">
+			<FormInfo>Callback URL: {{ `${url}/api/tw/cb` }}</FormInfo>
+		
+			<FormInput v-model="twitterConsumerKey">
+				<template #prefix><i class="fas fa-key"></i></template>
+				Consumer Key
+			</FormInput>
+
+			<FormInput v-model="twitterConsumerSecret">
+				<template #prefix><i class="fas fa-key"></i></template>
+				Consumer Secret
+			</FormInput>
+		</template>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/debobigego/switch.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 FormInfo from '@client/components/debobigego/info.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormInfo,
+		FormButton,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: 'Twitter',
+				icon: 'fab fa-twitter'
+			},
+			enableTwitterIntegration: false,
+			twitterConsumerKey: null,
+			twitterConsumerSecret: null,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.enableTwitterIntegration = meta.enableTwitterIntegration;
+			this.twitterConsumerKey = meta.twitterConsumerKey;
+			this.twitterConsumerSecret = meta.twitterConsumerSecret;
+		},
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				enableTwitterIntegration: this.enableTwitterIntegration,
+				twitterConsumerKey: this.twitterConsumerKey,
+				twitterConsumerSecret: this.twitterConsumerSecret,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/admin/integrations.vue b/src/client/pages/admin/integrations.vue
new file mode 100644
index 0000000000..bdc2cec4d0
--- /dev/null
+++ b/src/client/pages/admin/integrations.vue
@@ -0,0 +1,74 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormLink to="/admin/integrations/twitter">
+			<i class="fab fa-twitter"></i> Twitter
+			<template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template>
+		</FormLink>
+		<FormLink to="/admin/integrations/github">
+			<i class="fab fa-github"></i> GitHub
+			<template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template>
+		</FormLink>
+		<FormLink to="/admin/integrations/discord">
+			<i class="fab fa-discord"></i> Discord
+			<template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template>
+		</FormLink>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormLink from '@client/components/debobigego/link.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 * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+	components: {
+		FormLink,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormTextarea,
+		FormInfo,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.integration,
+				icon: 'fas fa-share-alt',
+				bg: 'var(--bg)',
+			},
+			enableTwitterIntegration: false,
+			enableGithubIntegration: false,
+			enableDiscordIntegration: false,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.enableTwitterIntegration = meta.enableTwitterIntegration;
+			this.enableGithubIntegration = meta.enableGithubIntegration;
+			this.enableDiscordIntegration = meta.enableDiscordIntegration;
+		},
+	}
+});
+</script>
diff --git a/src/client/pages/admin/logs.vue b/src/client/pages/admin/logs.vue
new file mode 100644
index 0000000000..74aea0fc45
--- /dev/null
+++ b/src/client/pages/admin/logs.vue
@@ -0,0 +1,97 @@
+<template>
+<div class="_section">
+	<div class="_inputs">
+		<MkInput v-model="logDomain" :debounce="true">
+			<template #label>{{ $ts.domain }}</template>
+		</MkInput>
+		<MkSelect v-model="logLevel">
+			<template #label>Level</template>
+			<option value="all">All</option>
+			<option value="info">Info</option>
+			<option value="success">Success</option>
+			<option value="warning">Warning</option>
+			<option value="error">Error</option>
+			<option value="debug">Debug</option>
+		</MkSelect>
+	</div>
+
+	<div class="logs">
+		<code v-for="log in logs" :key="log.id" :class="log.level">
+			<details>
+				<summary><MkTime :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary>
+				<!--<vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty>-->
+			</details>
+		</code>
+	</div>
+
+	<MkButton @click="deleteAllLogs()" primary><i class="fas fa-trash-alt"></i> {{ $ts.deleteAll }}</MkButton>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@client/components/ui/button.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkSelect from '@client/components/form/select.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkInput,
+		MkSelect,
+		MkTextarea,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.serverLogs,
+				icon: 'fas fa-stream'
+			},
+			logs: [],
+			logLevel: 'all',
+			logDomain: '',
+		}
+	},
+
+	watch: {
+		logLevel() {
+			this.logs = [];
+			this.fetchLogs();
+		},
+		logDomain() {
+			this.logs = [];
+			this.fetchLogs();
+		}
+	},
+
+	created() {
+		this.fetchLogs();
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		fetchLogs() {
+			os.api('admin/logs', {
+				level: this.logLevel === 'all' ? null : this.logLevel,
+				domain: this.logDomain === '' ? null : this.logDomain,
+				limit: 30
+			}).then(logs => {
+				this.logs = logs.reverse();
+			});
+		},
+
+		deleteAllLogs() {
+			os.apiWithDialog('admin/delete-logs');
+		},
+	}
+});
+</script>
diff --git a/src/client/pages/admin/metrics.vue b/src/client/pages/admin/metrics.vue
new file mode 100644
index 0000000000..da36f6c688
--- /dev/null
+++ b/src/client/pages/admin/metrics.vue
@@ -0,0 +1,472 @@
+<template>
+<div class="_debobigegoItem">
+	<div class="_debobigegoLabel"><i class="fas fa-microchip"></i> {{ $ts.cpuAndMemory }}</div>
+	<div class="_debobigegoPanel xhexznfu">
+		<div>
+			<canvas :ref="cpumem"></canvas>
+		</div>
+		<div v-if="serverInfo">
+			<div class="_table">
+				<div class="_row">
+					<div class="_cell"><div class="_label">MEM total</div>{{ bytes(serverInfo.mem.total) }}</div>
+					<div class="_cell"><div class="_label">MEM used</div>{{ bytes(memUsage) }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
+					<div class="_cell"><div class="_label">MEM free</div>{{ bytes(serverInfo.mem.total - memUsage) }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+<div class="_debobigegoItem">
+	<div class="_debobigegoLabel"><i class="fas fa-hdd"></i> {{ $ts.disk }}</div>
+	<div class="_debobigegoPanel xhexznfu">
+		<div>
+			<canvas :ref="disk"></canvas>
+		</div>
+		<div v-if="serverInfo">
+			<div class="_table">
+				<div class="_row">
+					<div class="_cell"><div class="_label">Disk total</div>{{ bytes(serverInfo.fs.total) }}</div>
+					<div class="_cell"><div class="_label">Disk used</div>{{ bytes(serverInfo.fs.used) }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
+					<div class="_cell"><div class="_label">Disk free</div>{{ bytes(serverInfo.fs.total - serverInfo.fs.used) }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+<div class="_debobigegoItem">
+	<div class="_debobigegoLabel"><i class="fas fa-exchange-alt"></i> {{ $ts.network }}</div>
+	<div class="_debobigegoPanel xhexznfu">
+		<div>
+			<canvas :ref="net"></canvas>
+		</div>
+		<div v-if="serverInfo">
+			<div class="_table">
+				<div class="_row">
+					<div class="_cell"><div class="_label">Interface</div>{{ serverInfo.net.interface }}</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+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';
+import MkContainer from '@client/components/ui/container.vue';
+import MkFolder from '@client/components/ui/folder.vue';
+import MkwFederation from '../../widgets/federation.vue';
+import { version, url } from '@client/config';
+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);
+	const g = parseInt(result[2], 16);
+	const b = parseInt(result[3], 16);
+	return `rgba(${r}, ${g}, ${b}, ${a})`;
+};
+import * as os from '@client/os';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkSelect,
+		MkInput,
+		MkContainer,
+		MkFolder,
+		MkwFederation,
+	},
+
+	data() {
+		return {
+			version,
+			url,
+			stats: null,
+			serverInfo: null,
+			connection: null,
+			queueConnection: markRaw(os.stream.useChannel('queueStats')),
+			memUsage: 0,
+			chartCpuMem: null,
+			chartNet: null,
+			jobs: [],
+			logs: [],
+			logLevel: 'all',
+			logDomain: '',
+			modLogs: [],
+			dbInfo: null,
+			overviewHeight: '1fr',
+			queueHeight: '1fr',
+			paused: false,
+		}
+	},
+
+	computed: {
+		gridColor() {
+			// TODO: var(--panel)の色が暗いか明るいかで判定する
+			return this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+		},
+	},
+
+	mounted() {
+		this.fetchJobs();
+
+		Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+
+		os.api('admin/server-info', {}).then(res => {
+			this.serverInfo = res;
+
+			this.connection = markRaw(os.stream.useChannel('serverStats'));
+			this.connection.on('stats', this.onStats);
+			this.connection.on('statsLog', this.onStatsLog);
+			this.connection.send('requestLog', {
+				id: Math.random().toString().substr(2, 8),
+				length: 150
+			});
+
+			this.$nextTick(() => {
+				this.queueConnection.send('requestLog', {
+					id: Math.random().toString().substr(2, 8),
+					length: 200
+				});
+			});
+		});
+	},
+
+	beforeUnmount() {
+		if (this.connection) {
+			this.connection.off('stats', this.onStats);
+			this.connection.off('statsLog', this.onStatsLog);
+			this.connection.dispose();
+		}
+		this.queueConnection.dispose();
+	},
+
+	methods: {
+		cpumem(el) {
+			if (this.chartCpuMem != null) return;
+			this.chartCpuMem = markRaw(new Chart(el, {
+				type: 'line',
+				data: {
+					labels: [],
+					datasets: [{
+						label: 'CPU',
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderColor: '#86b300',
+						backgroundColor: alpha('#86b300', 0.1),
+						data: []
+					}, {
+						label: 'MEM (active)',
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderColor: '#935dbf',
+						backgroundColor: alpha('#935dbf', 0.02),
+						data: []
+					}, {
+						label: 'MEM (used)',
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderColor: '#935dbf',
+						borderDash: [5, 5],
+						fill: false,
+						data: []
+					}]
+				},
+				options: {
+					aspectRatio: 3,
+					layout: {
+						padding: {
+							left: 16,
+							right: 16,
+							top: 16,
+							bottom: 0
+						}
+					},
+					legend: {
+						position: 'bottom',
+						labels: {
+							boxWidth: 16,
+						}
+					},
+					scales: {
+						x: {
+							gridLines: {
+								display: false,
+								color: this.gridColor,
+								zeroLineColor: this.gridColor,
+							},
+							ticks: {
+								display: false,
+							}
+						},
+						y: {
+							position: 'right',
+							gridLines: {
+								display: true,
+								color: this.gridColor,
+								zeroLineColor: this.gridColor,
+							},
+							ticks: {
+								display: false,
+								max: 100
+							}
+						}
+					},
+					tooltips: {
+						intersect: false,
+						mode: 'index',
+					}
+				}
+			}));
+		},
+
+		net(el) {
+			if (this.chartNet != null) return;
+			this.chartNet = markRaw(new Chart(el, {
+				type: 'line',
+				data: {
+					labels: [],
+					datasets: [{
+						label: 'In',
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderColor: '#94a029',
+						backgroundColor: alpha('#94a029', 0.1),
+						data: []
+					}, {
+						label: 'Out',
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderColor: '#ff9156',
+						backgroundColor: alpha('#ff9156', 0.1),
+						data: []
+					}]
+				},
+				options: {
+					aspectRatio: 3,
+					layout: {
+						padding: {
+							left: 16,
+							right: 16,
+							top: 16,
+							bottom: 0
+						}
+					},
+					legend: {
+						position: 'bottom',
+						labels: {
+							boxWidth: 16,
+						}
+					},
+					scales: {
+						x: {
+							gridLines: {
+								display: false,
+								color: this.gridColor,
+								zeroLineColor: this.gridColor,
+							},
+							ticks: {
+								display: false
+							}
+						},
+						y: {
+							position: 'right',
+							gridLines: {
+								display: true,
+								color: this.gridColor,
+								zeroLineColor: this.gridColor,
+							},
+							ticks: {
+								display: false,
+							}
+						}
+					},
+					tooltips: {
+						intersect: false,
+						mode: 'index',
+					}
+				}
+			}));
+		},
+
+		disk(el) {
+			if (this.chartDisk != null) return;
+			this.chartDisk = markRaw(new Chart(el, {
+				type: 'line',
+				data: {
+					labels: [],
+					datasets: [{
+						label: 'Read',
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderColor: '#94a029',
+						backgroundColor: alpha('#94a029', 0.1),
+						data: []
+					}, {
+						label: 'Write',
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderColor: '#ff9156',
+						backgroundColor: alpha('#ff9156', 0.1),
+						data: []
+					}]
+				},
+				options: {
+					aspectRatio: 3,
+					layout: {
+						padding: {
+							left: 16,
+							right: 16,
+							top: 16,
+							bottom: 0
+						}
+					},
+					legend: {
+						position: 'bottom',
+						labels: {
+							boxWidth: 16,
+						}
+					},
+					scales: {
+						x: {
+							gridLines: {
+								display: false,
+								color: this.gridColor,
+								zeroLineColor: this.gridColor,
+							},
+							ticks: {
+								display: false
+							}
+						},
+						y: {
+							position: 'right',
+							gridLines: {
+								display: true,
+								color: this.gridColor,
+								zeroLineColor: this.gridColor,
+							},
+							ticks: {
+								display: false,
+							}
+						}
+					},
+					tooltips: {
+						intersect: false,
+						mode: 'index',
+					}
+				}
+			}));
+		},
+
+		fetchJobs() {
+			os.api('admin/queue/deliver-delayed', {}).then(jobs => {
+				this.jobs = jobs;
+			});
+		},
+
+		onStats(stats) {
+			if (this.paused) return;
+
+			const cpu = (stats.cpu * 100).toFixed(0);
+			const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
+			const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0);
+			this.memUsage = stats.mem.active;
+
+			this.chartCpuMem.data.labels.push('');
+			this.chartCpuMem.data.datasets[0].data.push(cpu);
+			this.chartCpuMem.data.datasets[1].data.push(memActive);
+			this.chartCpuMem.data.datasets[2].data.push(memUsed);
+			this.chartNet.data.labels.push('');
+			this.chartNet.data.datasets[0].data.push(stats.net.rx);
+			this.chartNet.data.datasets[1].data.push(stats.net.tx);
+			this.chartDisk.data.labels.push('');
+			this.chartDisk.data.datasets[0].data.push(stats.fs.r);
+			this.chartDisk.data.datasets[1].data.push(stats.fs.w);
+			if (this.chartCpuMem.data.datasets[0].data.length > 150) {
+				this.chartCpuMem.data.labels.shift();
+				this.chartCpuMem.data.datasets[0].data.shift();
+				this.chartCpuMem.data.datasets[1].data.shift();
+				this.chartCpuMem.data.datasets[2].data.shift();
+				this.chartNet.data.labels.shift();
+				this.chartNet.data.datasets[0].data.shift();
+				this.chartNet.data.datasets[1].data.shift();
+				this.chartDisk.data.labels.shift();
+				this.chartDisk.data.datasets[0].data.shift();
+				this.chartDisk.data.datasets[1].data.shift();
+			}
+			this.chartCpuMem.update();
+			this.chartNet.update();
+			this.chartDisk.update();
+		},
+
+		onStatsLog(statsLog) {
+			for (const stats of [...statsLog].reverse()) {
+				this.onStats(stats);
+			}
+		},
+
+		bytes,
+
+		number,
+
+		pause() {
+			this.paused = true;
+		},
+
+		resume() {
+			this.paused = false;
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.xhexznfu {
+	> div:nth-child(2) {
+		padding: 16px;
+		border-top: solid 0.5px var(--divider);
+	}
+}
+</style>
diff --git a/src/client/pages/admin/object-storage.vue b/src/client/pages/admin/object-storage.vue
new file mode 100644
index 0000000000..2d765270e6
--- /dev/null
+++ b/src/client/pages/admin/object-storage.vue
@@ -0,0 +1,155 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormSwitch v-model="useObjectStorage">{{ $ts.useObjectStorage }}</FormSwitch>
+
+		<template v-if="useObjectStorage">
+			<FormInput v-model="objectStorageBaseUrl">
+				<span>{{ $ts.objectStorageBaseUrl }}</span>
+				<template #desc>{{ $ts.objectStorageBaseUrlDesc }}</template>
+			</FormInput>
+
+			<FormInput v-model="objectStorageBucket">
+				<span>{{ $ts.objectStorageBucket }}</span>
+				<template #desc>{{ $ts.objectStorageBucketDesc }}</template>
+			</FormInput>
+
+			<FormInput v-model="objectStoragePrefix">
+				<span>{{ $ts.objectStoragePrefix }}</span>
+				<template #desc>{{ $ts.objectStoragePrefixDesc }}</template>
+			</FormInput>
+
+			<FormInput v-model="objectStorageEndpoint">
+				<span>{{ $ts.objectStorageEndpoint }}</span>
+				<template #desc>{{ $ts.objectStorageEndpointDesc }}</template>
+			</FormInput>
+
+			<FormInput v-model="objectStorageRegion">
+				<span>{{ $ts.objectStorageRegion }}</span>
+				<template #desc>{{ $ts.objectStorageRegionDesc }}</template>
+			</FormInput>
+
+			<FormInput v-model="objectStorageAccessKey">
+				<template #prefix><i class="fas fa-key"></i></template>
+				<span>Access key</span>
+			</FormInput>
+
+			<FormInput v-model="objectStorageSecretKey">
+				<template #prefix><i class="fas fa-key"></i></template>
+				<span>Secret key</span>
+			</FormInput>
+
+			<FormSwitch v-model="objectStorageUseSSL">
+				{{ $ts.objectStorageUseSSL }}
+				<template #desc>{{ $ts.objectStorageUseSSLDesc }}</template>
+			</FormSwitch>
+
+			<FormSwitch v-model="objectStorageUseProxy">
+				{{ $ts.objectStorageUseProxy }}
+				<template #desc>{{ $ts.objectStorageUseProxyDesc }}</template>
+			</FormSwitch>
+
+			<FormSwitch v-model="objectStorageSetPublicRead">
+				{{ $ts.objectStorageSetPublicRead }}
+			</FormSwitch>
+
+			<FormSwitch v-model="objectStorageS3ForcePathStyle">
+				s3ForcePathStyle
+			</FormSwitch>
+		</template>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/debobigego/switch.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 FormSuspense from '@client/components/debobigego/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.objectStorage,
+				icon: 'fas fa-cloud',
+				bg: 'var(--bg)',
+			},
+			useObjectStorage: false,
+			objectStorageBaseUrl: null,
+			objectStorageBucket: null,
+			objectStoragePrefix: null,
+			objectStorageEndpoint: null,
+			objectStorageRegion: null,
+			objectStoragePort: null,
+			objectStorageAccessKey: null,
+			objectStorageSecretKey: null,
+			objectStorageUseSSL: false,
+			objectStorageUseProxy: false,
+			objectStorageSetPublicRead: false,
+			objectStorageS3ForcePathStyle: true,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.useObjectStorage = meta.useObjectStorage;
+			this.objectStorageBaseUrl = meta.objectStorageBaseUrl;
+			this.objectStorageBucket = meta.objectStorageBucket;
+			this.objectStoragePrefix = meta.objectStoragePrefix;
+			this.objectStorageEndpoint = meta.objectStorageEndpoint;
+			this.objectStorageRegion = meta.objectStorageRegion;
+			this.objectStoragePort = meta.objectStoragePort;
+			this.objectStorageAccessKey = meta.objectStorageAccessKey;
+			this.objectStorageSecretKey = meta.objectStorageSecretKey;
+			this.objectStorageUseSSL = meta.objectStorageUseSSL;
+			this.objectStorageUseProxy = meta.objectStorageUseProxy;
+			this.objectStorageSetPublicRead = meta.objectStorageSetPublicRead;
+			this.objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle;
+		},
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				useObjectStorage: this.useObjectStorage,
+				objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null,
+				objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null,
+				objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null,
+				objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null,
+				objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null,
+				objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null,
+				objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null,
+				objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null,
+				objectStorageUseSSL: this.objectStorageUseSSL,
+				objectStorageUseProxy: this.objectStorageUseProxy,
+				objectStorageSetPublicRead: this.objectStorageSetPublicRead,
+				objectStorageS3ForcePathStyle: this.objectStorageS3ForcePathStyle,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/admin/other-settings.vue b/src/client/pages/admin/other-settings.vue
new file mode 100644
index 0000000000..4e55df41fb
--- /dev/null
+++ b/src/client/pages/admin/other-settings.vue
@@ -0,0 +1,83 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormGroup>
+			<FormInput v-model="summalyProxy">
+				<template #prefix><i class="fas fa-link"></i></template>
+				Summaly Proxy URL
+			</FormInput>
+		</FormGroup>
+		<FormGroup>
+			<FormInput v-model="deeplAuthKey">
+				<template #prefix><i class="fas fa-key"></i></template>
+				DeepL Auth Key
+			</FormInput>
+			<FormSwitch v-model="deeplIsPro">
+				Pro account
+			</FormSwitch>
+		</FormGroup>
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/debobigego/switch.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 FormSuspense from '@client/components/debobigego/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.other,
+				icon: 'fas fa-cogs',
+				bg: 'var(--bg)',
+			},
+			summalyProxy: '',
+			deeplAuthKey: '',
+			deeplIsPro: false,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.summalyProxy = meta.summalyProxy;
+			this.deeplAuthKey = meta.deeplAuthKey;
+			this.deeplIsPro = meta.deeplIsPro;
+		},
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				summalyProxy: this.summalyProxy,
+				deeplAuthKey: this.deeplAuthKey,
+				deeplIsPro: this.deeplIsPro,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/admin/overview.vue b/src/client/pages/admin/overview.vue
new file mode 100644
index 0000000000..327a6492e6
--- /dev/null
+++ b/src/client/pages/admin/overview.vue
@@ -0,0 +1,188 @@
+<template>
+<div>
+	<MkHeader :info="header"/>
+
+	<div class="edbbcaef">
+		<div v-if="stats" class="cfcdecdf" style="margin: var(--margin)">
+			<div class="number _panel">
+				<div class="label">Users</div>
+				<div class="value _monospace">
+					{{ number(stats.originalUsersCount) }}
+					<MkNumberDiff v-if="usersComparedToThePrevDay != null" 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 != null" class="diff" :value="notesComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><template #before>(</template><template #after>)</template></MkNumberDiff>
+				</div>
+			</div>
+		</div>
+
+		<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/>-->
+
+		<MkFolder style="margin: var(--margin)">
+			<template #header><i class="fas fa-info-circle"></i> {{ $ts.info }}</template>
+			<div class="cfcdecdf">
+				<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>
+		</MkFolder>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, version as vueVersion } from 'vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.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 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';
+import bytes from '@client/filters/bytes';
+import number from '@client/filters/number';
+import MkInstanceInfo from './instance.vue';
+import XMetrics from './metrics.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+
+export default defineComponent({
+	components: {
+		MkNumberDiff,
+		FormKeyValueView,
+		MkInstanceStats,
+		MkContainer,
+		MkFolder,
+		XMetrics,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.dashboard,
+				icon: 'fas fa-tachometer-alt',
+				bg: 'var(--bg)',
+			},
+			header: {
+				title: this.$ts.dashboard,
+				icon: 'fas fa-tachometer-alt',
+			},
+			version,
+			vueVersion,
+			url,
+			stats: null,
+			meta: null,
+			serverInfo: null,
+			usersComparedToThePrevDay: null,
+			notesComparedToThePrevDay: null,
+			fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
+			fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
+		}
+	},
+
+	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 showInstanceInfo(q) {
+			let instance = q;
+			if (typeof q === 'string') {
+				instance = await os.api('federation/show-instance', {
+					host: q
+				});
+			}
+			os.popup(MkInstanceInfo, {
+				instance: instance
+			}, {}, 'closed');
+		},
+
+		bytes,
+
+		number,
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.edbbcaef {
+	.cfcdecdf {
+		display: grid;
+		grid-gap: 8px;
+		grid-template-columns: repeat(auto-fill,minmax(150px,1fr));
+
+		> .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>
diff --git a/src/client/pages/admin/proxy-account.vue b/src/client/pages/admin/proxy-account.vue
new file mode 100644
index 0000000000..b1ece19710
--- /dev/null
+++ b/src/client/pages/admin/proxy-account.vue
@@ -0,0 +1,87 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormGroup>
+			<FormKeyValueView>
+				<template #key>{{ $ts.proxyAccount }}</template>
+				<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : $ts.none }}</template>
+			</FormKeyValueView>
+			<template #caption>{{ $ts.proxyAccountDescription }}</template>
+		</FormGroup>
+
+		<FormButton @click="chooseProxyAccount" primary>{{ $ts.selectAccount }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } 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 * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+	components: {
+		FormKeyValueView,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormTextarea,
+		FormInfo,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.proxyAccount,
+				icon: 'fas fa-ghost',
+				bg: 'var(--bg)',
+			},
+			proxyAccount: null,
+			proxyAccountId: null,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.proxyAccountId = meta.proxyAccountId;
+			if (this.proxyAccountId) {
+				this.proxyAccount = await os.api('users/show', { userId: this.proxyAccountId });
+			}
+		},
+
+		chooseProxyAccount() {
+			os.selectUser().then(user => {
+				this.proxyAccount = user;
+				this.proxyAccountId = user.id;
+				this.save();
+			});
+		},
+
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				proxyAccountId: this.proxyAccountId,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/admin/queue.chart.vue b/src/client/pages/admin/queue.chart.vue
new file mode 100644
index 0000000000..4f8fd762bb
--- /dev/null
+++ b/src/client/pages/admin/queue.chart.vue
@@ -0,0 +1,218 @@
+<template>
+<div class="_debobigegoItem">
+	<div class="_debobigegoLabel"><slot name="title"></slot></div>
+	<div class="_debobigegoPanel pumxzjhg">
+		<div class="_table status">
+			<div class="_row">
+				<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
+				<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
+				<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
+				<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
+			</div>
+		</div>
+		<div class="">
+			<canvas ref="chart"></canvas>
+		</div>
+		<div class="jobs">
+			<div v-if="jobs.length > 0">
+				<div v-for="job in jobs" :key="job[0]">
+					<span>{{ job[0] }}</span>
+					<span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
+				</div>
+			</div>
+			<span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import Chart from 'chart.js';
+import number from '@client/filters/number';
+
+const alpha = (hex, a) => {
+	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
+	const r = parseInt(result[1], 16);
+	const g = parseInt(result[2], 16);
+	const b = parseInt(result[3], 16);
+	return `rgba(${r}, ${g}, ${b}, ${a})`;
+};
+import * as os from '@client/os';
+
+export default defineComponent({
+	props: {
+		domain: {
+			required: true
+		},
+		connection: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			chart: null,
+			jobs: [],
+			activeSincePrevTick: 0,
+			active: 0,
+			waiting: 0,
+			delayed: 0,
+		}
+	},
+
+	mounted() {
+		this.fetchJobs();
+
+		// TODO: var(--panel)の色が暗いか明るいかで判定する
+		const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+
+		Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+
+		this.chart = markRaw(new Chart(this.$refs.chart, {
+			type: 'line',
+			data: {
+				labels: [],
+				datasets: [{
+					label: 'Process',
+					pointRadius: 0,
+					lineTension: 0,
+					borderWidth: 2,
+					borderColor: '#00E396',
+					backgroundColor: alpha('#00E396', 0.1),
+					data: []
+				}, {
+					label: 'Active',
+					pointRadius: 0,
+					lineTension: 0,
+					borderWidth: 2,
+					borderColor: '#00BCD4',
+					backgroundColor: alpha('#00BCD4', 0.1),
+					data: []
+				}, {
+					label: 'Waiting',
+					pointRadius: 0,
+					lineTension: 0,
+					borderWidth: 2,
+					borderColor: '#FFB300',
+					backgroundColor: alpha('#FFB300', 0.1),
+					data: []
+				}, {
+					label: 'Delayed',
+					pointRadius: 0,
+					lineTension: 0,
+					borderWidth: 2,
+					borderColor: '#E53935',
+					borderDash: [5, 5],
+					fill: false,
+					data: []
+				}]
+			},
+			options: {
+				aspectRatio: 3,
+				layout: {
+					padding: {
+						left: 16,
+						right: 16,
+						top: 16,
+						bottom: 12
+					}
+				},
+				legend: {
+					position: 'bottom',
+					labels: {
+						boxWidth: 16,
+					}
+				},
+				scales: {
+					xAxes: [{
+						gridLines: {
+							display: false,
+							color: gridColor,
+							zeroLineColor: gridColor,
+						},
+						ticks: {
+							display: false
+						}
+					}],
+					yAxes: [{
+						position: 'right',
+						gridLines: {
+							display: true,
+							color: gridColor,
+							zeroLineColor: gridColor,
+						},
+						ticks: {
+							display: false,
+						}
+					}]
+				},
+				tooltips: {
+					intersect: false,
+					mode: 'index',
+				}
+			}
+		}));
+
+		this.connection.on('stats', this.onStats);
+		this.connection.on('statsLog', this.onStatsLog);
+	},
+
+	beforeUnmount() {
+		this.connection.off('stats', this.onStats);
+		this.connection.off('statsLog', this.onStatsLog);
+	},
+
+	methods: {
+		onStats(stats) {
+			this.activeSincePrevTick = stats[this.domain].activeSincePrevTick;
+			this.active = stats[this.domain].active;
+			this.waiting = stats[this.domain].waiting;
+			this.delayed = stats[this.domain].delayed;
+			this.chart.data.labels.push('');
+			this.chart.data.datasets[0].data.push(stats[this.domain].activeSincePrevTick);
+			this.chart.data.datasets[1].data.push(stats[this.domain].active);
+			this.chart.data.datasets[2].data.push(stats[this.domain].waiting);
+			this.chart.data.datasets[3].data.push(stats[this.domain].delayed);
+			if (this.chart.data.datasets[0].data.length > 200) {
+				this.chart.data.labels.shift();
+				this.chart.data.datasets[0].data.shift();
+				this.chart.data.datasets[1].data.shift();
+				this.chart.data.datasets[2].data.shift();
+				this.chart.data.datasets[3].data.shift();
+			}
+			this.chart.update();
+		},
+
+		onStatsLog(statsLog) {
+			for (const stats of [...statsLog].reverse()) {
+				this.onStats(stats);
+			}
+		},
+
+		fetchJobs() {
+			os.api(this.domain === 'inbox' ? 'admin/queue/inbox-delayed' : this.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(jobs => {
+				this.jobs = jobs;
+			});
+		},
+
+		number
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.pumxzjhg {
+	> .status {
+		padding: 16px;
+		border-bottom: solid 0.5px var(--divider);
+	}
+
+	> .jobs {
+		padding: 16px;
+		border-top: solid 0.5px var(--divider);
+		max-height: 180px;
+		overflow: auto;
+	}
+}
+</style>
diff --git a/src/client/pages/admin/queue.vue b/src/client/pages/admin/queue.vue
new file mode 100644
index 0000000000..f88825eb19
--- /dev/null
+++ b/src/client/pages/admin/queue.vue
@@ -0,0 +1,73 @@
+<template>
+<FormBase>
+	<XQueue :connection="connection" domain="inbox">
+		<template #title>In</template>
+	</XQueue>
+	<XQueue :connection="connection" domain="deliver">
+		<template #title>Out</template>
+	</XQueue>
+	<FormButton @click="clear()" danger><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</FormButton>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import MkButton from '@client/components/ui/button.vue';
+import XQueue from './queue.chart.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormButton,
+		MkButton,
+		XQueue,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.jobQueue,
+				icon: 'fas fa-clipboard-list',
+				bg: 'var(--bg)',
+			},
+			connection: markRaw(os.stream.useChannel('queueStats')),
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+
+		this.$nextTick(() => {
+			this.connection.send('requestLog', {
+				id: Math.random().toString().substr(2, 8),
+				length: 200
+			});
+		});
+	},
+
+	beforeUnmount() {
+		this.connection.dispose();
+	},
+
+	methods: {
+		clear() {
+			os.dialog({
+				type: 'warning',
+				title: this.$ts.clearQueueConfirmTitle,
+				text: this.$ts.clearQueueConfirmText,
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+
+				os.apiWithDialog('admin/queue/clear', {});
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/admin/relays.vue b/src/client/pages/admin/relays.vue
new file mode 100644
index 0000000000..7d7888eaa8
--- /dev/null
+++ b/src/client/pages/admin/relays.vue
@@ -0,0 +1,99 @@
+<template>
+<FormBase class="relaycxt">
+	<FormButton @click="addRelay" primary><i class="fas fa-plus"></i> {{ $ts.addRelay }}</FormButton>
+
+	<div class="_debobigegoItem" v-for="relay in relays" :key="relay.inbox">
+		<div class="_debobigegoPanel" style="padding: 16px;">
+			<div>{{ relay.inbox }}</div>
+			<div>{{ $t(`_relayStatus.${relay.status}`) }}</div>
+			<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
+		</div>
+	</div>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@client/components/ui/button.vue';
+import MkInput from '@client/components/form/input.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormButton,
+		MkButton,
+		MkInput,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.relays,
+				icon: 'fas fa-globe',
+				bg: 'var(--bg)',
+			},
+			relays: [],
+			inbox: '',
+		}
+	},
+
+	created() {
+		this.refresh();
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async addRelay() {
+			const { canceled, result: inbox } = await os.dialog({
+				title: this.$ts.addRelay,
+				input: {
+					placeholder: this.$ts.inboxUrl
+				}
+			});
+			if (canceled) return;
+			os.api('admin/relays/add', {
+				inbox
+			}).then((relay: any) => {
+				this.refresh();
+			}).catch((e: any) => {
+				os.dialog({
+					type: 'error',
+					text: e.message || e
+				});
+			});
+		},
+
+		remove(inbox: string) {
+			os.api('admin/relays/remove', {
+				inbox
+			}).then(() => {
+				this.refresh();
+			}).catch((e: any) => {
+				os.dialog({
+					type: 'error',
+					text: e.message || e
+				});
+			});
+		},
+
+		refresh() {
+			os.api('admin/relays/list').then((relays: any) => {
+				this.relays = relays;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/src/client/pages/admin/security.vue b/src/client/pages/admin/security.vue
new file mode 100644
index 0000000000..4365b6800c
--- /dev/null
+++ b/src/client/pages/admin/security.vue
@@ -0,0 +1,83 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormLink to="/admin/bot-protection">
+			<i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}
+			<template #suffix v-if="enableHcaptcha">hCaptcha</template>
+			<template #suffix v-else-if="enableRecaptcha">reCAPTCHA</template>
+			<template #suffix v-else>{{ $ts.none }} ({{ $ts.notRecommended }})</template>
+		</FormLink>
+
+		<FormSwitch v-model="enableRegistration">{{ $ts.enableRegistration }}</FormSwitch>
+
+		<FormSwitch v-model="emailRequiredForSignup">{{ $ts.emailRequiredForSignup }}</FormSwitch>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormSwitch from '@client/components/debobigego/switch.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 FormInfo from '@client/components/debobigego/info.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+	components: {
+		FormLink,
+		FormSwitch,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormInfo,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.security,
+				icon: 'fas fa-lock',
+				bg: 'var(--bg)',
+			},
+			enableHcaptcha: false,
+			enableRecaptcha: false,
+			enableRegistration: false,
+			emailRequiredForSignup: false,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.enableHcaptcha = meta.enableHcaptcha;
+			this.enableRecaptcha = meta.enableRecaptcha;
+			this.enableRegistration = !meta.disableRegistration;
+			this.emailRequiredForSignup = meta.emailRequiredForSignup;
+		},
+	
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				disableRegistration: !this.enableRegistration,
+				emailRequiredForSignup: this.emailRequiredForSignup,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/admin/service-worker.vue b/src/client/pages/admin/service-worker.vue
new file mode 100644
index 0000000000..430e02ad2e
--- /dev/null
+++ b/src/client/pages/admin/service-worker.vue
@@ -0,0 +1,85 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormSwitch v-model="enableServiceWorker">
+			{{ $ts.enableServiceworker }}
+			<template #desc>{{ $ts.serviceworkerInfo }}</template>
+		</FormSwitch>
+
+		<template v-if="enableServiceWorker">
+			<FormInput v-model="swPublicKey">
+				<template #prefix><i class="fas fa-key"></i></template>
+				Public key
+			</FormInput>
+
+			<FormInput v-model="swPrivateKey">
+				<template #prefix><i class="fas fa-key"></i></template>
+				Private key
+			</FormInput>
+		</template>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/debobigego/switch.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 FormSuspense from '@client/components/debobigego/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: 'ServiceWorker',
+				icon: 'fas fa-bolt',
+				bg: 'var(--bg)',
+			},
+			enableServiceWorker: false,
+			swPublicKey: null,
+			swPrivateKey: null,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.enableServiceWorker = meta.enableServiceWorker;
+			this.swPublicKey = meta.swPublickey;
+			this.swPrivateKey = meta.swPrivateKey;
+		},
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				enableServiceWorker: this.enableServiceWorker,
+				swPublicKey: this.swPublicKey,
+				swPrivateKey: this.swPrivateKey,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/admin/settings.vue b/src/client/pages/admin/settings.vue
new file mode 100644
index 0000000000..7bd363e5f3
--- /dev/null
+++ b/src/client/pages/admin/settings.vue
@@ -0,0 +1,151 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormInput v-model="name">
+			<span>{{ $ts.instanceName }}</span>
+		</FormInput>
+
+		<FormTextarea v-model="description">
+			<span>{{ $ts.instanceDescription }}</span>
+		</FormTextarea>
+
+		<FormInput v-model="iconUrl">
+			<template #prefix><i class="fas fa-link"></i></template>
+			<span>{{ $ts.iconUrl }}</span>
+		</FormInput>
+
+		<FormInput v-model="bannerUrl">
+			<template #prefix><i class="fas fa-link"></i></template>
+			<span>{{ $ts.bannerUrl }}</span>
+		</FormInput>
+
+		<FormInput v-model="backgroundImageUrl">
+			<template #prefix><i class="fas fa-link"></i></template>
+			<span>{{ $ts.backgroundImageUrl }}</span>
+		</FormInput>
+
+		<FormInput v-model="tosUrl">
+			<template #prefix><i class="fas fa-link"></i></template>
+			<span>{{ $ts.tosUrl }}</span>
+		</FormInput>
+
+		<FormInput v-model="maintainerName">
+			<span>{{ $ts.maintainerName }}</span>
+		</FormInput>
+
+		<FormInput v-model="maintainerEmail" type="email">
+			<template #prefix><i class="fas fa-envelope"></i></template>
+			<span>{{ $ts.maintainerEmail }}</span>
+		</FormInput>
+
+		<FormTextarea v-model="pinnedUsers">
+			<span>{{ $ts.pinnedUsers }}</span>
+			<template #desc>{{ $ts.pinnedUsersDescription }}</template>
+		</FormTextarea>
+
+		<FormInput v-model="maxNoteTextLength" type="number">
+			<template #prefix><i class="fas fa-pencil-alt"></i></template>
+			<span>{{ $ts.maxNoteTextLength }}</span>
+		</FormInput>
+
+		<FormSwitch v-model="enableLocalTimeline">{{ $ts.enableLocalTimeline }}</FormSwitch>
+		<FormSwitch v-model="enableGlobalTimeline">{{ $ts.enableGlobalTimeline }}</FormSwitch>
+		<FormInfo>{{ $ts.disablingTimelinesInfo }}</FormInfo>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/debobigego/switch.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 * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormTextarea,
+		FormInfo,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.general,
+				icon: 'fas fa-cog',
+				bg: 'var(--bg)',
+			},
+			name: null,
+			description: null,
+			tosUrl: null as string | null,
+			maintainerName: null,
+			maintainerEmail: null,
+			iconUrl: null,
+			bannerUrl: null,
+			backgroundImageUrl: null,
+			maxNoteTextLength: 0,
+			enableLocalTimeline: false,
+			enableGlobalTimeline: false,
+			pinnedUsers: '',
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.name = meta.name;
+			this.description = meta.description;
+			this.tosUrl = meta.tosUrl;
+			this.iconUrl = meta.iconUrl;
+			this.bannerUrl = meta.bannerUrl;
+			this.backgroundImageUrl = meta.backgroundImageUrl;
+			this.maintainerName = meta.maintainerName;
+			this.maintainerEmail = meta.maintainerEmail;
+			this.maxNoteTextLength = meta.maxNoteTextLength;
+			this.enableLocalTimeline = !meta.disableLocalTimeline;
+			this.enableGlobalTimeline = !meta.disableGlobalTimeline;
+			this.pinnedUsers = meta.pinnedUsers.join('\n');
+		},
+
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				name: this.name,
+				description: this.description,
+				tosUrl: this.tosUrl,
+				iconUrl: this.iconUrl,
+				bannerUrl: this.bannerUrl,
+				backgroundImageUrl: this.backgroundImageUrl,
+				maintainerName: this.maintainerName,
+				maintainerEmail: this.maintainerEmail,
+				maxNoteTextLength: this.maxNoteTextLength,
+				disableLocalTimeline: !this.enableLocalTimeline,
+				disableGlobalTimeline: !this.enableGlobalTimeline,
+				pinnedUsers: this.pinnedUsers.split('\n'),
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/pages/admin/users.vue b/src/client/pages/admin/users.vue
new file mode 100644
index 0000000000..f7f9306b70
--- /dev/null
+++ b/src/client/pages/admin/users.vue
@@ -0,0 +1,261 @@
+<template>
+<div class="lknzcolw">
+	<MkHeader :info="header"/>
+
+	<div class="users">
+		<div class="inputs">
+			<MkSelect v-model="sort" style="flex: 1;">
+				<template #label>{{ $ts.sort }}</template>
+				<option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option>
+				<option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option>
+				<option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option>
+				<option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option>
+			</MkSelect>
+			<MkSelect v-model="state" style="flex: 1;">
+				<template #label>{{ $ts.state }}</template>
+				<option value="all">{{ $ts.all }}</option>
+				<option value="available">{{ $ts.normal }}</option>
+				<option value="admin">{{ $ts.administrator }}</option>
+				<option value="moderator">{{ $ts.moderator }}</option>
+				<option value="silenced">{{ $ts.silence }}</option>
+				<option value="suspended">{{ $ts.suspend }}</option>
+			</MkSelect>
+			<MkSelect v-model="origin" style="flex: 1;">
+				<template #label>{{ $ts.instance }}</template>
+				<option value="combined">{{ $ts.all }}</option>
+				<option value="local">{{ $ts.local }}</option>
+				<option value="remote">{{ $ts.remote }}</option>
+			</MkSelect>
+		</div>
+		<div class="inputs">
+			<MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()">
+				<template #prefix>@</template>
+				<template #label>{{ $ts.username }}</template>
+			</MkInput>
+			<MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()" :disabled="pagination.params().origin === 'local'">
+				<template #prefix>@</template>
+				<template #label>{{ $ts.host }}</template>
+			</MkInput>
+		</div>
+
+		<MkPagination :pagination="pagination" #default="{items}" class="users" ref="users">
+			<button class="user _panel _button _gap" v-for="user in items" :key="user.id" @click="show(user)">
+				<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
+				<div class="body">
+					<header>
+						<MkUserName class="name" :user="user"/>
+						<span class="acct">@{{ acct(user) }}</span>
+						<span class="staff" v-if="user.isAdmin"><i class="fas fa-bookmark"></i></span>
+						<span class="staff" v-if="user.isModerator"><i class="far fa-bookmark"></i></span>
+						<span class="punished" v-if="user.isSilenced"><i class="fas fa-microphone-slash"></i></span>
+						<span class="punished" v-if="user.isSuspended"><i class="fas fa-snowflake"></i></span>
+					</header>
+					<div>
+						<span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span>
+					</div>
+					<div>
+						<span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span>
+					</div>
+				</div>
+			</button>
+		</MkPagination>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@client/components/ui/button.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkSelect from '@client/components/form/select.vue';
+import MkPagination from '@client/components/ui/pagination.vue';
+import { acct } from '@client/filters/user';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { lookupUser } from '@client/scripts/lookup-user';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkInput,
+		MkSelect,
+		MkPagination,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.users,
+				icon: 'fas fa-users',
+				bg: 'var(--bg)',
+			},
+			header: {
+				title: this.$ts.users,
+				icon: 'fas fa-users',
+				bg: 'var(--bg)',
+				actions: [{
+					icon: 'fas fa-search',
+					text: this.$ts.search,
+					handler: this.searchUser
+				}, {
+					asFullButton: true,
+					icon: 'fas fa-plus',
+					text: this.$ts.addUser,
+					handler: this.addUser
+				}, {
+					asFullButton: true,
+					icon: 'fas fa-search',
+					text: this.$ts.lookup,
+					handler: this.lookupUser
+				}]
+			},
+			sort: '+createdAt',
+			state: 'all',
+			origin: 'local',
+			searchUsername: '',
+			searchHost: '',
+			pagination: {
+				endpoint: 'admin/show-users',
+				limit: 10,
+				params: () => ({
+					sort: this.sort,
+					state: this.state,
+					origin: this.origin,
+					username: this.searchUsername,
+					hostname: this.searchHost,
+				}),
+				offsetMode: true
+			},
+		}
+	},
+
+	watch: {
+		sort() {
+			this.$refs.users.reload();
+		},
+		state() {
+			this.$refs.users.reload();
+		},
+		origin() {
+			this.$refs.users.reload();
+		},
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		lookupUser,
+
+		searchUser() {
+			os.selectUser().then(user => {
+				this.show(user);
+			});
+		},
+
+		async addUser() {
+			const { canceled: canceled1, result: username } = await os.dialog({
+				title: this.$ts.username,
+				input: true
+			});
+			if (canceled1) return;
+
+			const { canceled: canceled2, result: password } = await os.dialog({
+				title: this.$ts.password,
+				input: { type: 'password' }
+			});
+			if (canceled2) return;
+
+			os.apiWithDialog('admin/accounts/create', {
+				username: username,
+				password: password,
+			}).then(res => {
+				this.$refs.users.reload();
+			});
+		},
+
+		show(user) {
+			os.pageWindow(`/user-info/${user.id}`);
+		},
+
+		acct
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.lknzcolw {
+	> .users {
+		margin: var(--margin);
+
+		> .inputs {
+			display: flex;
+			margin-bottom: 16px;
+
+			> * {
+				margin-right: 16px;
+
+				&:last-child {
+					margin-right: 0;
+				}
+			}
+		}
+	
+		> .users {
+			margin-top: var(--margin);
+
+			> .user {
+				display: flex;
+				width: 100%;
+				box-sizing: border-box;
+				text-align: left;
+				align-items: center;
+				padding: 16px;
+
+				&:hover {
+					color: var(--accent);
+				}
+
+				> .avatar {
+					width: 60px;
+					height: 60px;
+				}
+
+				> .body {
+					margin-left: 0.3em;
+					padding: 0 8px;
+					flex: 1;
+
+					@media (max-width: 500px) {
+						font-size: 14px;
+					}
+
+					> header {
+						> .name {
+							font-weight: bold;
+						}
+
+						> .acct {
+							margin-left: 8px;
+							opacity: 0.7;
+						}
+
+						> .staff {
+							margin-left: 0.5em;
+							color: var(--badge);
+						}
+
+						> .punished {
+							margin-left: 0.5em;
+							color: #4dabf7;
+						}
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/instance-info.vue b/src/client/pages/instance-info.vue
index 7a4cd5f016..291ceb5dfd 100644
--- a/src/client/pages/instance-info.vue
+++ b/src/client/pages/instance-info.vue
@@ -149,7 +149,7 @@ import * as os from '@client/os';
 import number from '@client/filters/number';
 import bytes from '@client/filters/bytes';
 import * as symbols from '@client/symbols';
-import MkInstanceInfo from '@client/pages/instance/instance.vue';
+import MkInstanceInfo from '@client/pages/admin/instance.vue';
 
 export default defineComponent({
 	components: {
diff --git a/src/client/pages/instance/abuses.vue b/src/client/pages/instance/abuses.vue
deleted file mode 100644
index 29da8cc2c5..0000000000
--- a/src/client/pages/instance/abuses.vue
+++ /dev/null
@@ -1,170 +0,0 @@
-<template>
-<div class="lcixvhis">
-	<div class="_section reports">
-		<div class="_content">
-			<div class="inputs" style="display: flex;">
-				<MkSelect v-model="state" style="margin: 0; flex: 1;">
-					<template #label>{{ $ts.state }}</template>
-					<option value="all">{{ $ts.all }}</option>
-					<option value="unresolved">{{ $ts.unresolved }}</option>
-					<option value="resolved">{{ $ts.resolved }}</option>
-				</MkSelect>
-				<MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
-					<template #label>{{ $ts.targetUserOrigin }}</template>
-					<option value="combined">{{ $ts.all }}</option>
-					<option value="local">{{ $ts.local }}</option>
-					<option value="remote">{{ $ts.remote }}</option>
-				</MkSelect>
-				<MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
-					<template #label>{{ $ts.reporterOrigin }}</template>
-					<option value="combined">{{ $ts.all }}</option>
-					<option value="local">{{ $ts.local }}</option>
-					<option value="remote">{{ $ts.remote }}</option>
-				</MkSelect>
-			</div>
-			<!-- TODO
-			<div class="inputs" style="display: flex; padding-top: 1.2em;">
-				<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()">
-					<span>{{ $ts.username }}</span>
-				</MkInput>
-				<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()" :disabled="pagination.params().origin === 'local'">
-					<span>{{ $ts.host }}</span>
-				</MkInput>
-			</div>
-			-->
-
-			<MkPagination :pagination="pagination" #default="{items}" ref="reports" style="margin-top: var(--margin);">
-				<div class="bcekxzvu _card _gap" v-for="report in items" :key="report.id">
-					<div class="_content target">
-						<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/>
-						<div class="info">
-							<MkUserName class="name" :user="report.targetUser"/>
-							<div class="acct">@{{ acct(report.targetUser) }}</div>
-						</div>
-					</div>
-					<div class="_content">
-						<div>
-							<Mfm :text="report.comment"/>
-						</div>
-						<hr>
-						<div>Reporter: <MkAcct :user="report.reporter"/></div>
-						<div><MkTime :time="report.createdAt"/></div>
-					</div>
-					<div class="_footer">
-						<div v-if="report.assignee">Assignee: <MkAcct :user="report.assignee"/></div>
-						<MkButton @click="resolve(report)" primary v-if="!report.resolved">{{ $ts.abuseMarkAsResolved }}</MkButton>
-					</div>
-				</div>
-			</MkPagination>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { parseAcct } from '@/misc/acct';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import { acct } from '@client/filters/user';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkSelect,
-		MkPagination,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.abuseReports,
-				icon: 'fas fa-exclamation-circle',
-				bg: 'var(--bg)',
-			},
-			searchUsername: '',
-			searchHost: '',
-			state: 'unresolved',
-			reporterOrigin: 'combined',
-			targetUserOrigin: 'combined',
-			pagination: {
-				endpoint: 'admin/abuse-user-reports',
-				limit: 10,
-				params: () => ({
-					state: this.state,
-					reporterOrigin: this.reporterOrigin,
-					targetUserOrigin: this.targetUserOrigin,
-				}),
-			},
-		}
-	},
-
-	watch: {
-		state() {
-			this.$refs.reports.reload();
-		},
-
-		reporterOrigin() {
-			this.$refs.reports.reload();
-		},
-
-		targetUserOrigin() {
-			this.$refs.reports.reload();
-		},
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		acct,
-
-		resolve(report) {
-			os.apiWithDialog('admin/resolve-abuse-user-report', {
-				reportId: report.id,
-			}).then(() => {
-				this.$refs.reports.removeItem(item => item.id === report.id);
-			});
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.lcixvhis {
-	margin: var(--margin);
-}
-
-.bcekxzvu {
-	> .target {
-		display: flex;
-		width: 100%;
-		box-sizing: border-box;
-		text-align: left;
-		align-items: center;
-					
-		> .avatar {
-			width: 42px;
-			height: 42px;
-		}
-
-		> .info {
-			margin-left: 0.3em;
-			padding: 0 8px;
-			flex: 1;
-
-			> .name {
-				font-weight: bold;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/instance/ads.vue b/src/client/pages/instance/ads.vue
deleted file mode 100644
index e776f99a4c..0000000000
--- a/src/client/pages/instance/ads.vue
+++ /dev/null
@@ -1,146 +0,0 @@
-<template>
-<div>
-	<MkHeader :info="header"/>
-	<div class="uqshojas">
-		<section class="_card _gap ads" v-for="ad in ads">
-			<div class="_content ad">
-				<MkAd v-if="ad.url" :specify="ad"/>
-				<MkInput v-model="ad.url" type="url">
-					<template #label>URL</template>
-				</MkInput>
-				<MkInput v-model="ad.imageUrl">
-					<template #label>{{ $ts.imageUrl }}</template>
-				</MkInput>
-				<div style="margin: 32px 0;">
-					<MkRadio v-model="ad.place" value="square">square</MkRadio>
-					<MkRadio v-model="ad.place" value="horizontal">horizontal</MkRadio>
-					<MkRadio v-model="ad.place" value="horizontal-big">horizontal-big</MkRadio>
-				</div>
-				<!--
-				<div style="margin: 32px 0;">
-					{{ $ts.priority }}
-					<MkRadio v-model="ad.priority" value="high">{{ $ts.high }}</MkRadio>
-					<MkRadio v-model="ad.priority" value="middle">{{ $ts.middle }}</MkRadio>
-					<MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio>
-				</div>
-				-->
-				<MkInput v-model="ad.ratio" type="number">
-					<template #label>{{ $ts.ratio }}</template>
-				</MkInput>
-				<MkInput v-model="ad.expiresAt" type="date">
-					<template #label>{{ $ts.expiration }}</template>
-				</MkInput>
-				<MkTextarea v-model="ad.memo">
-					<template #label>{{ $ts.memo }}</template>
-				</MkTextarea>
-				<div class="buttons">
-					<MkButton class="button" inline @click="save(ad)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
-					<MkButton class="button" inline @click="remove(ad)" danger><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
-				</div>
-			</div>
-		</section>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import MkRadio from '@client/components/form/radio.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkTextarea,
-		MkRadio,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.ads,
-				icon: 'fas fa-audio-description',
-				bg: 'var(--bg)',
-			},
-			header: {
-				title: this.$ts.ads,
-				icon: 'fas fa-audio-description',
-				bg: 'var(--bg)',
-				actions: [{
-					asFullButton: true,
-					icon: 'fas fa-plus',
-					text: this.$ts.add,
-					handler: this.add,
-				}],
-			},
-			ads: [],
-		}
-	},
-
-	created() {
-		os.api('admin/ad/list').then(ads => {
-			this.ads = ads;
-		});
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		add() {
-			this.ads.unshift({
-				id: null,
-				memo: '',
-				place: 'square',
-				priority: 'middle',
-				ratio: 1,
-				url: '',
-				imageUrl: null,
-				expiresAt: null,
-			});
-		},
-
-		remove(ad) {
-			os.dialog({
-				type: 'warning',
-				text: this.$t('removeAreYouSure', { x: ad.url }),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-				this.ads = this.ads.filter(x => x != ad);
-				os.apiWithDialog('admin/ad/delete', {
-					id: ad.id
-				});
-			});
-		},
-
-		save(ad) {
-			if (ad.id == null) {
-				os.apiWithDialog('admin/ad/create', {
-					...ad,
-					expiresAt: new Date(ad.expiresAt).getTime()
-				});
-			} else {
-				os.apiWithDialog('admin/ad/update', {
-					...ad,
-					expiresAt: new Date(ad.expiresAt).getTime()
-				});
-			}
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.uqshojas {
-	margin: var(--margin);
-}
-</style>
diff --git a/src/client/pages/instance/announcements.vue b/src/client/pages/instance/announcements.vue
deleted file mode 100644
index 78637c095a..0000000000
--- a/src/client/pages/instance/announcements.vue
+++ /dev/null
@@ -1,134 +0,0 @@
-<template>
-<div>
-	<MkHeader :info="header"/>
-
-	<div class="ztgjmzrw">
-		<section class="_card _gap announcements" v-for="announcement in announcements">
-			<div class="_content announcement">
-				<MkInput v-model="announcement.title">
-					<template #label>{{ $ts.title }}</template>
-				</MkInput>
-				<MkTextarea v-model="announcement.text">
-					<template #label>{{ $ts.text }}</template>
-				</MkTextarea>
-				<MkInput v-model="announcement.imageUrl">
-					<template #label>{{ $ts.imageUrl }}</template>
-				</MkInput>
-				<p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p>
-				<div class="buttons">
-					<MkButton class="button" inline @click="save(announcement)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
-					<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
-				</div>
-			</div>
-		</section>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkTextarea,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.announcements,
-				icon: 'fas fa-broadcast-tower',
-				bg: 'var(--bg)',
-			},
-			header: {
-				title: this.$ts.announcements,
-				icon: 'fas fa-broadcast-tower',
-				bg: 'var(--bg)',
-				actions: [{
-					asFullButton: true,
-					icon: 'fas fa-plus',
-					text: this.$ts.add,
-					handler: this.add,
-				}],
-			},
-			announcements: [],
-		}
-	},
-
-	created() {
-		os.api('admin/announcements/list').then(announcements => {
-			this.announcements = announcements;
-		});
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		add() {
-			this.announcements.unshift({
-				id: null,
-				title: '',
-				text: '',
-				imageUrl: null
-			});
-		},
-
-		remove(announcement) {
-			os.dialog({
-				type: 'warning',
-				text: this.$t('removeAreYouSure', { x: announcement.title }),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-				this.announcements = this.announcements.filter(x => x != announcement);
-				os.api('admin/announcements/delete', announcement);
-			});
-		},
-
-		save(announcement) {
-			if (announcement.id == null) {
-				os.api('admin/announcements/create', announcement).then(() => {
-					os.dialog({
-						type: 'success',
-						text: this.$ts.saved
-					});
-				}).catch(e => {
-					os.dialog({
-						type: 'error',
-						text: e
-					});
-				});
-			} else {
-				os.api('admin/announcements/update', announcement).then(() => {
-					os.dialog({
-						type: 'success',
-						text: this.$ts.saved
-					});
-				}).catch(e => {
-					os.dialog({
-						type: 'error',
-						text: e
-					});
-				});
-			}
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ztgjmzrw {
-	margin: var(--margin);
-}
-</style>
diff --git a/src/client/pages/instance/bot-protection.vue b/src/client/pages/instance/bot-protection.vue
deleted file mode 100644
index 731f114cc2..0000000000
--- a/src/client/pages/instance/bot-protection.vue
+++ /dev/null
@@ -1,138 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormRadios v-model="provider">
-			<template #desc><i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}</template>
-			<option :value="null">{{ $ts.none }} ({{ $ts.notRecommended }})</option>
-			<option value="hcaptcha">hCaptcha</option>
-			<option value="recaptcha">reCAPTCHA</option>
-		</FormRadios>
-
-		<template v-if="provider === 'hcaptcha'">
-			<div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
-				<div class="_debobigegoLabel">hCaptcha</div>
-				<div class="main">
-					<FormInput v-model="hcaptchaSiteKey">
-						<template #prefix><i class="fas fa-key"></i></template>
-						<span>{{ $ts.hcaptchaSiteKey }}</span>
-					</FormInput>
-					<FormInput v-model="hcaptchaSecretKey">
-						<template #prefix><i class="fas fa-key"></i></template>
-						<span>{{ $ts.hcaptchaSecretKey }}</span>
-					</FormInput>
-				</div>
-			</div>
-			<div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
-				<div class="_debobigegoLabel">{{ $ts.preview }}</div>
-				<div class="_debobigegoPanel" style="padding: var(--debobigegoContentHMargin);">
-					<MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
-				</div>
-			</div>
-		</template>
-		<template v-else-if="provider === 'recaptcha'">
-			<div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
-				<div class="_debobigegoLabel">reCAPTCHA</div>
-				<div class="main">
-					<FormInput v-model="recaptchaSiteKey">
-						<template #prefix><i class="fas fa-key"></i></template>
-						<span>{{ $ts.recaptchaSiteKey }}</span>
-					</FormInput>
-					<FormInput v-model="recaptchaSecretKey">
-						<template #prefix><i class="fas fa-key"></i></template>
-						<span>{{ $ts.recaptchaSecretKey }}</span>
-					</FormInput>
-				</div>
-			</div>
-			<div v-if="recaptchaSiteKey" class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
-				<div class="_debobigegoLabel">{{ $ts.preview }}</div>
-				<div class="_debobigegoPanel" style="padding: var(--debobigegoContentHMargin);">
-					<MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/>
-				</div>
-			</div>
-		</template>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import FormRadios from '@client/components/debobigego/radios.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 FormInfo from '@client/components/debobigego/info.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormRadios,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormInfo,
-		FormSuspense,
-		MkCaptcha: defineAsyncComponent(() => import('@client/components/captcha.vue')),
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.botProtection,
-				icon: 'fas fa-shield-alt'
-			},
-			provider: null,
-			enableHcaptcha: false,
-			hcaptchaSiteKey: null,
-			hcaptchaSecretKey: null,
-			enableRecaptcha: false,
-			recaptchaSiteKey: null,
-			recaptchaSecretKey: null,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.enableHcaptcha = meta.enableHcaptcha;
-			this.hcaptchaSiteKey = meta.hcaptchaSiteKey;
-			this.hcaptchaSecretKey = meta.hcaptchaSecretKey;
-			this.enableRecaptcha = meta.enableRecaptcha;
-			this.recaptchaSiteKey = meta.recaptchaSiteKey;
-			this.recaptchaSecretKey = meta.recaptchaSecretKey;
-
-			this.provider = this.enableHcaptcha ? 'hcaptcha' : this.enableRecaptcha ? 'recaptcha' : null;
-
-			this.$watch(() => this.provider, () => {
-				this.enableHcaptcha = this.provider === 'hcaptcha';
-				this.enableRecaptcha = this.provider === 'recaptcha';
-			});
-		},
-	
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				enableHcaptcha: this.enableHcaptcha,
-				hcaptchaSiteKey: this.hcaptchaSiteKey,
-				hcaptchaSecretKey: this.hcaptchaSecretKey,
-				enableRecaptcha: this.enableRecaptcha,
-				recaptchaSiteKey: this.recaptchaSiteKey,
-				recaptchaSecretKey: this.recaptchaSecretKey,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/instance/database.vue b/src/client/pages/instance/database.vue
deleted file mode 100644
index ffbeed8b30..0000000000
--- a/src/client/pages/instance/database.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="databasePromiseFactory" v-slot="{ result: database }">
-		<FormGroup v-for="table in database" :key="table[0]">
-			<template #label>{{ table[0] }}</template>
-			<FormKeyValueView>
-				<template #key>Size</template>
-				<template #value>{{ bytes(table[1].size) }}</template>
-			</FormKeyValueView>
-			<FormKeyValueView>
-				<template #key>Records</template>
-				<template #value>{{ number(table[1].count) }}</template>
-			</FormKeyValueView>
-		</FormGroup>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import bytes from '@client/filters/bytes';
-import number from '@client/filters/number';
-
-export default defineComponent({
-	components: {
-		FormSuspense,
-		FormKeyValueView,
-		FormBase,
-		FormGroup,
-		FormLink,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.database,
-				icon: 'fas fa-database',
-				bg: 'var(--bg)',
-			},
-			databasePromiseFactory: () => os.api('admin/get-table-stats', {}).then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size)),
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		bytes, number,
-	}
-});
-</script>
diff --git a/src/client/pages/instance/email-settings.vue b/src/client/pages/instance/email-settings.vue
deleted file mode 100644
index ebf724fcdd..0000000000
--- a/src/client/pages/instance/email-settings.vue
+++ /dev/null
@@ -1,128 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormSwitch v-model="enableEmail">{{ $ts.enableEmail }}<template #desc>{{ $ts.emailConfigInfo }}</template></FormSwitch>
-
-		<template v-if="enableEmail">
-			<FormInput v-model="email" type="email">
-				<span>{{ $ts.emailAddress }}</span>
-			</FormInput>
-
-			<div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
-				<div class="_debobigegoLabel">{{ $ts.smtpConfig }}</div>
-				<div class="main">
-					<FormInput v-model="smtpHost">
-						<span>{{ $ts.smtpHost }}</span>
-					</FormInput>
-					<FormInput v-model="smtpPort" type="number">
-						<span>{{ $ts.smtpPort }}</span>
-					</FormInput>
-					<FormInput v-model="smtpUser">
-						<span>{{ $ts.smtpUser }}</span>
-					</FormInput>
-					<FormInput v-model="smtpPass" type="password">
-						<span>{{ $ts.smtpPass }}</span>
-					</FormInput>
-					<FormInfo>{{ $ts.emptyToDisableSmtpAuth }}</FormInfo>
-					<FormSwitch v-model="smtpSecure">{{ $ts.smtpSecure }}<template #desc>{{ $ts.smtpSecureInfo }}</template></FormSwitch>
-				</div>
-			</div>
-
-			<FormButton @click="testEmail">{{ $ts.testEmail }}</FormButton>
-		</template>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.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 FormInfo from '@client/components/debobigego/info.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormInfo,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.emailServer,
-				icon: 'fas fa-envelope',
-				bg: 'var(--bg)',
-			},
-			enableEmail: false,
-			email: null,
-			smtpSecure: false,
-			smtpHost: '',
-			smtpPort: 0,
-			smtpUser: '',
-			smtpPass: '',
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.enableEmail = meta.enableEmail;
-			this.email = meta.email;
-			this.smtpSecure = meta.smtpSecure;
-			this.smtpHost = meta.smtpHost;
-			this.smtpPort = meta.smtpPort;
-			this.smtpUser = meta.smtpUser;
-			this.smtpPass = meta.smtpPass;
-		},
-
-		async testEmail() {
-			const { canceled, result: destination } = await os.dialog({
-				title: this.$ts.destination,
-				input: {
-					placeholder: this.$instance.maintainerEmail
-				}
-			});
-			if (canceled) return;
-			os.apiWithDialog('admin/send-email', {
-				to: destination,
-				subject: 'Test email',
-				text: 'Yo'
-			});
-		},
-
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				enableEmail: this.enableEmail,
-				email: this.email,
-				smtpSecure: this.smtpSecure,
-				smtpHost: this.smtpHost,
-				smtpPort: this.smtpPort,
-				smtpUser: this.smtpUser,
-				smtpPass: this.smtpPass,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/instance/emoji-edit-dialog.vue b/src/client/pages/instance/emoji-edit-dialog.vue
deleted file mode 100644
index 4854c69884..0000000000
--- a/src/client/pages/instance/emoji-edit-dialog.vue
+++ /dev/null
@@ -1,120 +0,0 @@
-<template>
-<XModalWindow ref="dialog"
-	:width="370"
-	:with-ok-button="true"
-	@close="$refs.dialog.close()"
-	@closed="$emit('closed')"
-	@ok="ok()"
->
-	<template #header>:{{ emoji.name }}:</template>
-
-	<div class="_monolithic_">
-		<div class="yigymqpb _section">
-			<img :src="emoji.url" class="img"/>
-			<MkInput class="_formBlock" v-model="name">
-				<template #label>{{ $ts.name }}</template>
-			</MkInput>
-			<MkInput class="_formBlock" v-model="category" :datalist="categories">
-				<template #label>{{ $ts.category }}</template>
-			</MkInput>
-			<MkInput class="_formBlock" v-model="aliases">
-				<template #label>{{ $ts.tags }}</template>
-				<template #caption>{{ $ts.setMultipleBySeparatingWithSpace }}</template>
-			</MkInput>
-			<MkButton danger @click="del()"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton>
-		</div>
-	</div>
-</XModalWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XModalWindow from '@client/components/ui/modal-window.vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import * as os from '@client/os';
-import { unique } from '../../../prelude/array';
-
-export default defineComponent({
-	components: {
-		XModalWindow,
-		MkButton,
-		MkInput,
-	},
-
-	props: {
-		emoji: {
-			required: true,
-		}
-	},
-
-	emits: ['done', 'closed'],
-
-	data() {
-		return {
-			name: this.emoji.name,
-			category: this.emoji.category,
-			aliases: this.emoji.aliases?.join(' '),
-			categories: [],
-		}
-	},
-
-	created() {
-		os.api('meta', { detail: false }).then(({ emojis }) => {
-			this.categories = unique(emojis.map((x: any) => x.category || '').filter((x: string) => x !== ''));
-		});
-	},
-
-	methods: {
-		ok() {
-			this.update();
-		},
-
-		async update() {
-			await os.apiWithDialog('admin/emoji/update', {
-				id: this.emoji.id,
-				name: this.name,
-				category: this.category,
-				aliases: this.aliases.split(' '),
-			});
-
-			this.$emit('done', {
-				updated: {
-					name: this.name,
-					category: this.category,
-					aliases: this.aliases.split(' '),
-				}
-			});
-			this.$refs.dialog.close();
-		},
-
-		async del() {
-			const { canceled } = await os.dialog({
-				type: 'warning',
-				text: this.$t('removeAreYouSure', { x: this.emoji.name }),
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			os.api('admin/emoji/remove', {
-				id: this.emoji.id
-			}).then(() => {
-				this.$emit('done', {
-					deleted: true
-				});
-				this.$refs.dialog.close();
-			});
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.yigymqpb {
-	> .img {
-		display: block;
-		height: 64px;
-		margin: 0 auto;
-	}
-}
-</style>
diff --git a/src/client/pages/instance/emojis.vue b/src/client/pages/instance/emojis.vue
deleted file mode 100644
index 4cd34b046d..0000000000
--- a/src/client/pages/instance/emojis.vue
+++ /dev/null
@@ -1,262 +0,0 @@
-<template>
-<div class="ogwlenmc">
-	<MkHeader :info="header"/>
-
-	<div class="local" v-if="tab === 'local'">
-		<MkInput v-model="query" :debounce="true" type="search" style="margin: var(--margin);">
-			<template #prefix><i class="fas fa-search"></i></template>
-			<template #label>{{ $ts.search }}</template>
-		</MkInput>
-		<MkPagination :pagination="pagination" ref="emojis">
-			<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
-			<template #default="{items}">
-				<div class="ldhfsamy">
-					<button class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="edit(emoji)">
-						<img :src="emoji.url" class="img" :alt="emoji.name"/>
-						<div class="body">
-							<div class="name _monospace">{{ emoji.name }}</div>
-							<div class="info">{{ emoji.category }}</div>
-						</div>
-					</button>
-				</div>
-			</template>
-		</MkPagination>
-	</div>
-
-	<div class="remote" v-else-if="tab === 'remote'">
-		<MkInput v-model="queryRemote" :debounce="true" type="search" style="margin: var(--margin);">
-			<template #prefix><i class="fas fa-search"></i></template>
-			<template #label>{{ $ts.search }}</template>
-		</MkInput>
-		<MkInput v-model="host" :debounce="true" style="margin: var(--margin);">
-			<template #label>{{ $ts.host }}</template>
-		</MkInput>
-		<MkPagination :pagination="remotePagination" ref="remoteEmojis">
-			<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
-			<template #default="{items}">
-				<div class="ldhfsamy">
-					<div class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="remoteMenu(emoji, $event)">
-						<img :src="emoji.url" class="img" :alt="emoji.name"/>
-						<div class="body">
-							<div class="name _monospace">{{ emoji.name }}</div>
-							<div class="info">{{ emoji.host }}</div>
-						</div>
-					</div>
-				</div>
-			</template>
-		</MkPagination>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import MkTab from '@client/components/tab.vue';
-import { selectFile } from '@client/scripts/select-file';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkTab,
-		MkButton,
-		MkInput,
-		MkPagination,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.customEmojis,
-				icon: 'fas fa-laugh',
-				bg: 'var(--bg)',
-			},
-			header: computed(() => ({
-				title: this.$ts.customEmojis,
-				icon: 'fas fa-laugh',
-				bg: 'var(--bg)',
-				actions: [{
-					asFullButton: true,
-					icon: 'fas fa-plus',
-					text: this.$ts.addEmoji,
-					handler: this.add,
-				}],
-				tabs: [{
-					active: this.tab === 'local',
-					title: this.$ts.local,
-					onClick: () => { this.tab = 'local'; },
-				}, {
-					active: this.tab === 'remote',
-					title: this.$ts.remote,
-					onClick: () => { this.tab = 'remote'; },
-				},]
-			})),
-			tab: 'local',
-			query: null,
-			queryRemote: null,
-			host: '',
-			pagination: {
-				endpoint: 'admin/emoji/list',
-				limit: 30,
-				params: computed(() => ({
-					query: (this.query && this.query !== '') ? this.query : null
-				}))
-			},
-			remotePagination: {
-				endpoint: 'admin/emoji/list-remote',
-				limit: 30,
-				params: computed(() => ({
-					query: (this.queryRemote && this.queryRemote !== '') ? this.queryRemote : null,
-					host: (this.host && this.host !== '') ? this.host : null
-				}))
-			},
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async add(e) {
-			const files = await selectFile(e.currentTarget || e.target, null, true);
-
-			const promise = Promise.all(files.map(file => os.api('admin/emoji/add', {
-				fileId: file.id,
-			})));
-			promise.then(() => {
-				this.$refs.emojis.reload();
-			});
-			os.promiseDialog(promise);
-		},
-
-		edit(emoji) {
-			os.popup(import('./emoji-edit-dialog.vue'), {
-				emoji: emoji
-			}, {
-				done: result => {
-					if (result.updated) {
-						this.$refs.emojis.replaceItem(item => item.id === emoji.id, {
-							...emoji,
-							...result.updated
-						});
-					} else if (result.deleted) {
-						this.$refs.emojis.removeItem(item => item.id === emoji.id);
-					}
-				},
-			}, 'closed');
-		},
-
-		im(emoji) {
-			os.apiWithDialog('admin/emoji/copy', {
-				emojiId: emoji.id,
-			});
-		},
-
-		remoteMenu(emoji, ev) {
-			os.popupMenu([{
-				type: 'label',
-				text: ':' + emoji.name + ':',
-			}, {
-				text: this.$ts.import,
-				icon: 'fas fa-plus',
-				action: () => { this.im(emoji) }
-			}], ev.currentTarget || ev.target);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ogwlenmc {
-	> .local {
-		.ldhfsamy {
-			display: grid;
-			grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
-			grid-gap: 12px;
-			margin: var(--margin);
-	
-			> .emoji {
-				display: flex;
-				align-items: center;
-				padding: 12px;
-				text-align: left;
-
-				&:hover {
-					color: var(--accent);
-				}
-
-				> .img {
-					width: 42px;
-					height: 42px;
-				}
-
-				> .body {
-					padding: 0 0 0 8px;
-					white-space: nowrap;
-					overflow: hidden;
-
-					> .name {
-						text-overflow: ellipsis;
-						overflow: hidden;
-					}
-
-					> .info {
-						opacity: 0.5;
-						text-overflow: ellipsis;
-						overflow: hidden;
-					}
-				}
-			}
-		}
-	}
-
-	> .remote {
-		.ldhfsamy {
-			display: grid;
-			grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
-			grid-gap: 12px;
-			margin: var(--margin);
-
-			> .emoji {
-				display: flex;
-				align-items: center;
-				padding: 12px;
-				text-align: left;
-
-				&:hover {
-					color: var(--accent);
-				}
-
-				> .img {
-					width: 32px;
-					height: 32px;
-				}
-
-				> .body {
-					padding: 0 0 0 8px;
-					white-space: nowrap;
-					overflow: hidden;
-
-					> .name {
-						text-overflow: ellipsis;
-						overflow: hidden;
-					}
-
-					> .info {
-						opacity: 0.5;
-						font-size: 90%;
-						text-overflow: ellipsis;
-						overflow: hidden;
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/instance/file-dialog.vue b/src/client/pages/instance/file-dialog.vue
deleted file mode 100644
index 02d83e5022..0000000000
--- a/src/client/pages/instance/file-dialog.vue
+++ /dev/null
@@ -1,129 +0,0 @@
-<template>
-<XModalWindow ref="dialog"
-	:width="370"
-	@close="$refs.dialog.close()"
-	@closed="$emit('closed')"
->
-	<template #header v-if="file">{{ file.name }}</template>
-	<div class="cxqhhsmd" v-if="file">
-		<div class="_section">
-			<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
-			<div class="info">
-				<span style="margin-right: 1em;">{{ file.type }}</span>
-				<span>{{ bytes(file.size) }}</span>
-				<MkTime :time="file.createdAt" mode="detail" style="display: block;"/>
-			</div>
-		</div>
-		<div class="_section">
-			<div class="_content">
-				<MkSwitch @update:modelValue="toggleIsSensitive" v-model="isSensitive">NSFW</MkSwitch>
-			</div>
-		</div>
-		<div class="_section">
-			<div class="_content">
-				<MkButton full @click="showUser"><i class="fas fa-external-link-square-alt"></i> {{ $ts.user }}</MkButton>
-				<MkButton full danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton>
-			</div>
-		</div>
-		<div class="_section" v-if="info">
-			<details class="_content rawdata">
-				<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre>
-			</details>
-		</div>
-	</div>
-</XModalWindow>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkSwitch from '@client/components/form/switch.vue';
-import XModalWindow from '@client/components/ui/modal-window.vue';
-import MkDriveFileThumbnail from '@client/components/drive-file-thumbnail.vue';
-import Progress from '@client/scripts/loading';
-import bytes from '@client/filters/bytes';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkSwitch,
-		XModalWindow,
-		MkDriveFileThumbnail,
-	},
-
-	props: {
-		fileId: {
-			required: true,
-		}
-	},
-
-	emits: ['closed'],
-
-	data() {
-		return {
-			file: null,
-			info: null,
-			isSensitive: false,
-		};
-	},
-
-	created() {
-		this.fetch();
-	},
-
-	methods: {
-		async fetch() {
-			Progress.start();
-			this.file = await os.api('drive/files/show', { fileId: this.fileId });
-			this.info = await os.api('admin/drive/show-file', { fileId: this.fileId });
-			this.isSensitive = this.file.isSensitive;
-			Progress.done();
-		},
-
-		showUser() {
-			os.pageWindow(`/user-info/${this.file.userId}`);
-		},
-
-		async del() {
-			const { canceled } = await os.dialog({
-				type: 'warning',
-				text: this.$t('removeAreYouSure', { x: this.file.name }),
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			os.apiWithDialog('drive/files/delete', {
-				fileId: this.file.id
-			});
-		},
-
-		async toggleIsSensitive(v) {
-			await os.api('drive/files/update', { fileId: this.fileId, isSensitive: v });
-			this.isSensitive = v;
-		},
-
-		bytes
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.cxqhhsmd {
-	> ._section {
-		> .thumbnail {
-			height: 150px;
-			max-width: 100%;
-		}
-
-		> .info {
-			text-align: center;
-			margin-top: 8px;
-		}
-		
-		> .rawdata {
-			overflow: auto;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/instance/files-settings.vue b/src/client/pages/instance/files-settings.vue
deleted file mode 100644
index 8aefa9e90d..0000000000
--- a/src/client/pages/instance/files-settings.vue
+++ /dev/null
@@ -1,93 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormSwitch v-model="cacheRemoteFiles">
-			{{ $ts.cacheRemoteFiles }}
-			<template #desc>{{ $ts.cacheRemoteFilesDescription }}</template>
-		</FormSwitch>
-
-		<FormSwitch v-model="proxyRemoteFiles">
-			{{ $ts.proxyRemoteFiles }}
-			<template #desc>{{ $ts.proxyRemoteFilesDescription }}</template>
-		</FormSwitch>
-
-		<FormInput v-model="localDriveCapacityMb" type="number">
-			<span>{{ $ts.driveCapacityPerLocalAccount }}</span>
-			<template #suffix>MB</template>
-			<template #desc>{{ $ts.inMb }}</template>
-		</FormInput>
-
-		<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">
-			<span>{{ $ts.driveCapacityPerRemoteAccount }}</span>
-			<template #suffix>MB</template>
-			<template #desc>{{ $ts.inMb }}</template>
-		</FormInput>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.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 FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.files,
-				icon: 'fas fa-cloud',
-				bg: 'var(--bg)',
-			},
-			cacheRemoteFiles: false,
-			proxyRemoteFiles: false,
-			localDriveCapacityMb: 0,
-			remoteDriveCapacityMb: 0,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.cacheRemoteFiles = meta.cacheRemoteFiles;
-			this.proxyRemoteFiles = meta.proxyRemoteFiles;
-			this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
-			this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
-		},
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				cacheRemoteFiles: this.cacheRemoteFiles,
-				proxyRemoteFiles: this.proxyRemoteFiles,
-				localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
-				remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/instance/files.vue b/src/client/pages/instance/files.vue
deleted file mode 100644
index 55189cfd84..0000000000
--- a/src/client/pages/instance/files.vue
+++ /dev/null
@@ -1,209 +0,0 @@
-<template>
-<div class="xrmjdkdw">
-	<MkContainer :foldable="true" class="lookup">
-		<template #header><i class="fas fa-search"></i> {{ $ts.lookup }}</template>
-		<div class="xrmjdkdw-lookup">
-			<MkInput class="item" v-model="q" type="text" @enter="find()">
-				<template #label>{{ $ts.fileIdOrUrl }}</template>
-			</MkInput>
-			<MkButton @click="find()" primary><i class="fas fa-search"></i> {{ $ts.lookup }}</MkButton>
-		</div>
-	</MkContainer>
-
-	<div class="_section">
-		<div class="_content">
-			<div class="inputs" style="display: flex;">
-				<MkSelect v-model="origin" style="margin: 0; flex: 1;">
-					<template #label>{{ $ts.instance }}</template>
-					<option value="combined">{{ $ts.all }}</option>
-					<option value="local">{{ $ts.local }}</option>
-					<option value="remote">{{ $ts.remote }}</option>
-				</MkSelect>
-				<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params().origin === 'local'">
-					<template #label>{{ $ts.host }}</template>
-				</MkInput>
-			</div>
-			<div class="inputs" style="display: flex; padding-top: 1.2em;">
-				<MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;">
-					<template #label>MIME type</template>
-				</MkInput>
-			</div>
-			<MkPagination :pagination="pagination" #default="{items}" class="urempief" ref="files">
-				<button class="file _panel _button _gap" v-for="file in items" :key="file.id" @click="show(file, $event)">
-					<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
-					<div class="body">
-						<div>
-							<small style="opacity: 0.7;">{{ file.name }}</small>
-						</div>
-						<div>
-							<MkAcct v-if="file.user" :user="file.user"/>
-							<div v-else>{{ $ts.system }}</div>
-						</div>
-						<div>
-							<span style="margin-right: 1em;">{{ file.type }}</span>
-							<span>{{ bytes(file.size) }}</span>
-						</div>
-						<div>
-							<span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span>
-						</div>
-					</div>
-				</button>
-			</MkPagination>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import MkContainer from '@client/components/ui/container.vue';
-import MkDriveFileThumbnail from '@client/components/drive-file-thumbnail.vue';
-import bytes from '@client/filters/bytes';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkSelect,
-		MkPagination,
-		MkContainer,
-		MkDriveFileThumbnail,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.files,
-				icon: 'fas fa-cloud',
-				bg: 'var(--bg)',
-				actions: [{
-					text: this.$ts.clearCachedFiles,
-					icon: 'fas fa-trash-alt',
-					handler: this.clear
-				}]
-			},
-			q: null,
-			origin: 'local',
-			type: null,
-			searchHost: '',
-			pagination: {
-				endpoint: 'admin/drive/files',
-				limit: 10,
-				params: () => ({
-					type: (this.type && this.type !== '') ? this.type : null,
-					origin: this.origin,
-					hostname: (this.hostname && this.hostname !== '') ? this.hostname : null,
-				}),
-			},
-		}
-	},
-
-	watch: {
-		type() {
-			this.$refs.files.reload();
-		},
-		origin() {
-			this.$refs.files.reload();
-		},
-		searchHost() {
-			this.$refs.files.reload();
-		},
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		clear() {
-			os.dialog({
-				type: 'warning',
-				text: this.$ts.clearCachedFilesConfirm,
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				os.apiWithDialog('admin/drive/clean-remote-files', {});
-			});
-		},
-
-		show(file, ev) {
-			os.popup(import('./file-dialog.vue'), {
-				fileId: file.id
-			}, {}, 'closed');
-		},
-
-		find() {
-			os.api('admin/drive/show-file', this.q.startsWith('http://') || this.q.startsWith('https://') ? { url: this.q.trim() } : { fileId: this.q.trim() }).then(file => {
-				this.show(file);
-			}).catch(e => {
-				if (e.code === 'NO_SUCH_FILE') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts.notFound
-					});
-				}
-			});
-		},
-
-		bytes
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.xrmjdkdw {
-	margin: var(--margin);
-
-	> .lookup {
-		margin-bottom: 16px;
-	}
-
-	.urempief {
-		margin-top: var(--margin);
-
-		> .file {
-			display: flex;
-			width: 100%;
-			box-sizing: border-box;
-			text-align: left;
-			align-items: center;
-
-			&:hover {
-				color: var(--accent);
-			}
-
-			> .thumbnail {
-				width: 128px;
-				height: 128px;
-			}
-
-			> .body {
-				margin-left: 0.3em;
-				padding: 8px;
-				flex: 1;
-
-				@media (max-width: 500px) {
-					font-size: 14px;
-				}
-			}
-		}
-	}
-}
-
-.xrmjdkdw-lookup {
-	padding: 16px;
-
-	> .item {
-		margin-bottom: 16px;
-	}
-}
-</style>
diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue
deleted file mode 100644
index 7b07bf2dde..0000000000
--- a/src/client/pages/instance/index.vue
+++ /dev/null
@@ -1,373 +0,0 @@
-<template>
-<div class="hiyeyicy" :class="{ wide: !narrow }" ref="el">
-	<div class="nav" v-if="!narrow || page == null">
-		<MkHeader :info="header"></MkHeader>
-	
-		<div class="lxpfedzu">
-			<img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
-		</div>
-
-		<MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/instance/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
-		<MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/instance/bot-protection" class="_link">{{ $ts.configure }}</MkA></MkInfo>
-
-		<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
-	</div>
-	<div class="main">
-		<component :is="component" :key="page" @info="onInfo" v-bind="pageProps"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, reactive, ref, watch } from 'vue';
-import { i18n } from '@client/i18n';
-import MkSuperMenu from '@client/components/ui/super-menu.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import MkInfo from '@client/components/ui/info.vue';
-import { scroll } from '@client/scripts/scroll';
-import { instance } from '@client/instance';
-import * as symbols from '@client/symbols';
-import * as os from '@client/os';
-import { lookupUser } from '@client/scripts/lookup-user';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		MkSuperMenu,
-		FormGroup,
-		FormButton,
-		MkInfo,
-	},
-
-	props: {
-		initialPage: {
-			type: String,
-			required: false
-		}
-	},
-
-	setup(props, context) {
-		const indexInfo = {
-			title: i18n.locale.instance,
-			icon: 'fas fa-cog',
-			bg: 'var(--bg)',
-		};
-		const INFO = ref(indexInfo);
-		const page = ref(props.initialPage);
-		const narrow = ref(false);
-		const view = ref(null);
-		const el = ref(null);
-		const onInfo = (viewInfo) => {
-			INFO.value = viewInfo;
-		};
-		const pageProps = ref({});
-
-		const isEmpty = (x: any) => x == null || x == '';
-
-		const noMaintainerInformation = ref(false);
-		const noBotProtection = ref(false);
-
-		os.api('meta', { detail: true }).then(meta => {
-			// TODO: 設定が完了しても残ったままになるので、ストリーミングでmeta更新イベントを受け取ってよしなに更新する
-			noMaintainerInformation.value = isEmpty(meta.maintainerName) || isEmpty(meta.maintainerEmail);
-			noBotProtection.value = !meta.enableHcaptcha && !meta.enableRecaptcha;
-		});
-
-		const menuDef = computed(() => [{
-			title: i18n.locale.quickAction,
-			items: [{
-				type: 'button',
-				icon: 'fas fa-search',
-				text: i18n.locale.lookup,
-				action: lookup,
-			}, ...(instance.disableRegistration ? [{
-				type: 'button',
-				icon: 'fas fa-user',
-				text: i18n.locale.invite,
-				action: invite,
-			}] : [])],
-		}, {
-			title: i18n.locale.administration,
-			items: [{
-				icon: 'fas fa-tachometer-alt',
-				text: i18n.locale.dashboard,
-				to: '/instance/overview',
-				active: page.value === 'overview',
-			}, {
-				icon: 'fas fa-users',
-				text: i18n.locale.users,
-				to: '/instance/users',
-				active: page.value === 'users',
-			}, {
-				icon: 'fas fa-laugh',
-				text: i18n.locale.customEmojis,
-				to: '/instance/emojis',
-				active: page.value === 'emojis',
-			}, {
-				icon: 'fas fa-globe',
-				text: i18n.locale.federation,
-				to: '/instance/federation',
-				active: page.value === 'federation',
-			}, {
-				icon: 'fas fa-clipboard-list',
-				text: i18n.locale.jobQueue,
-				to: '/instance/queue',
-				active: page.value === 'queue',
-			}, {
-				icon: 'fas fa-cloud',
-				text: i18n.locale.files,
-				to: '/instance/files',
-				active: page.value === 'files',
-			}, {
-				icon: 'fas fa-broadcast-tower',
-				text: i18n.locale.announcements,
-				to: '/instance/announcements',
-				active: page.value === 'announcements',
-			}, {
-				icon: 'fas fa-audio-description',
-				text: i18n.locale.ads,
-				to: '/instance/ads',
-				active: page.value === 'ads',
-			}, {
-				icon: 'fas fa-exclamation-circle',
-				text: i18n.locale.abuseReports,
-				to: '/instance/abuses',
-				active: page.value === 'abuses',
-			}],
-		}, {
-			title: i18n.locale.settings,
-			items: [{
-				icon: 'fas fa-cog',
-				text: i18n.locale.general,
-				to: '/instance/settings',
-				active: page.value === 'settings',
-			}, {
-				icon: 'fas fa-cloud',
-				text: i18n.locale.files,
-				to: '/instance/files-settings',
-				active: page.value === 'files-settings',
-			}, {
-				icon: 'fas fa-envelope',
-				text: i18n.locale.emailServer,
-				to: '/instance/email-settings',
-				active: page.value === 'email-settings',
-			}, {
-				icon: 'fas fa-cloud',
-				text: i18n.locale.objectStorage,
-				to: '/instance/object-storage',
-				active: page.value === 'object-storage',
-			}, {
-				icon: 'fas fa-lock',
-				text: i18n.locale.security,
-				to: '/instance/security',
-				active: page.value === 'security',
-			}, {
-				icon: 'fas fa-bolt',
-				text: 'ServiceWorker',
-				to: '/instance/service-worker',
-				active: page.value === 'service-worker',
-			}, {
-				icon: 'fas fa-globe',
-				text: i18n.locale.relays,
-				to: '/instance/relays',
-				active: page.value === 'relays',
-			}, {
-				icon: 'fas fa-share-alt',
-				text: i18n.locale.integration,
-				to: '/instance/integrations',
-				active: page.value === 'integrations',
-			}, {
-				icon: 'fas fa-ban',
-				text: i18n.locale.instanceBlocking,
-				to: '/instance/instance-block',
-				active: page.value === 'instance-block',
-			}, {
-				icon: 'fas fa-ghost',
-				text: i18n.locale.proxyAccount,
-				to: '/instance/proxy-account',
-				active: page.value === 'proxy-account',
-			}, {
-				icon: 'fas fa-cogs',
-				text: i18n.locale.other,
-				to: '/instance/other-settings',
-				active: page.value === 'other-settings',
-			}],
-		}, {
-			title: i18n.locale.info,
-			items: [{
-				icon: 'fas fa-database',
-				text: i18n.locale.database,
-				to: '/instance/database',
-				active: page.value === 'database',
-			}, {
-				icon: 'fas fa-stream',
-				text: i18n.locale.logs,
-				to: '/instance/logs',
-				active: page.value === 'logs',
-			}],
-		}]);
-		const component = computed(() => {
-			if (page.value == null) return null;
-			switch (page.value) {
-				case 'overview': return defineAsyncComponent(() => import('./overview.vue'));
-				case 'users': return defineAsyncComponent(() => import('./users.vue'));
-				case 'emojis': return defineAsyncComponent(() => import('./emojis.vue'));
-				case 'federation': return defineAsyncComponent(() => import('../federation.vue'));
-				case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
-				case 'files': return defineAsyncComponent(() => import('./files.vue'));
-				case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
-				case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
-				case 'database': return defineAsyncComponent(() => import('./database.vue'));
-				case 'logs': return defineAsyncComponent(() => import('./logs.vue'));
-				case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
-				case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
-				case 'files-settings': return defineAsyncComponent(() => import('./files-settings.vue'));
-				case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue'));
-				case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
-				case 'security': return defineAsyncComponent(() => import('./security.vue'));
-				case 'bot-protection': return defineAsyncComponent(() => import('./bot-protection.vue'));
-				case 'service-worker': return defineAsyncComponent(() => import('./service-worker.vue'));
-				case 'relays': return defineAsyncComponent(() => import('./relays.vue'));
-				case 'integrations': return defineAsyncComponent(() => import('./integrations.vue'));
-				case 'integrations/twitter': return defineAsyncComponent(() => import('./integrations-twitter.vue'));
-				case 'integrations/github': return defineAsyncComponent(() => import('./integrations-github.vue'));
-				case 'integrations/discord': return defineAsyncComponent(() => import('./integrations-discord.vue'));
-				case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue'));
-				case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue'));
-				case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue'));
-			}
-		});
-
-		watch(component, () => {
-			pageProps.value = {};
-
-			nextTick(() => {
-				scroll(el.value, { top: 0 });
-			});
-		}, { immediate: true });
-
-		watch(() => props.initialPage, () => {
-			if (props.initialPage == null && !narrow.value) {
-				page.value = 'overview';
-			} else {
-				page.value = props.initialPage;
-				if (props.initialPage == null) {
-					INFO.value = indexInfo;
-				}
-			}
-		});
-
-		onMounted(() => {
-			narrow.value = el.value.offsetWidth < 800;
-			if (!narrow.value) {
-				page.value = 'overview';
-			}
-		});
-
-		const invite = () => {
-			os.api('admin/invite').then(x => {
-				os.dialog({
-					type: 'info',
-					text: x.code
-				});
-			}).catch(e => {
-				os.dialog({
-					type: 'error',
-					text: e
-				});
-			});
-		};
-
-		const lookup = (ev) => {
-			os.popupMenu([{
-				text: i18n.locale.user,
-				icon: 'fas fa-user',
-				action: () => {
-					lookupUser();
-				}
-			}, {
-				text: i18n.locale.note,
-				icon: 'fas fa-pencil-alt',
-				action: () => {
-					alert('TODO');
-				}
-			}, {
-				text: i18n.locale.file,
-				icon: 'fas fa-cloud',
-				action: () => {
-					alert('TODO');
-				}
-			}, {
-				text: i18n.locale.instance,
-				icon: 'fas fa-globe',
-				action: () => {
-					alert('TODO');
-				}
-			}], ev.currentTarget || ev.target);
-		};
-
-		return {
-			[symbols.PAGE_INFO]: INFO,
-			menuDef,
-			header: {
-				title: i18n.locale.controllPanel,
-			},
-			noMaintainerInformation,
-			noBotProtection,
-			page,
-			narrow,
-			view,
-			el,
-			onInfo,
-			pageProps,
-			component,
-			invite,
-			lookup,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.hiyeyicy {
-	&.wide {
-		display: flex;
-		margin: 0 auto;
-		height: 100%;
-
-		> .nav {
-			width: 32%;
-			max-width: 280px;
-			box-sizing: border-box;
-			border-right: solid 0.5px var(--divider);
-			overflow: auto;
-			height: 100%;
-		}
-
-		> .main {
-			flex: 1;
-			min-width: 0;
-			--baseContentWidth: 100%;
-		}
-	}
-
-	> .nav {
-		> .info {
-			margin: 16px;
-		}
-	}
-}
-
-.lxpfedzu {
-	margin: 16px;
-
-	> .icon {
-		display: block;
-		margin: auto;
-		height: 42px;
-		border-radius: 8px;
-	}
-}
-</style>
diff --git a/src/client/pages/instance/instance-block.vue b/src/client/pages/instance/instance-block.vue
deleted file mode 100644
index 105cdb4941..0000000000
--- a/src/client/pages/instance/instance-block.vue
+++ /dev/null
@@ -1,72 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormTextarea v-model="blockedHosts">
-			<span>{{ $ts.blockedInstances }}</span>
-			<template #desc>{{ $ts.blockedInstancesDescription }}</template>
-		</FormTextarea>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.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 * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormTextarea,
-		FormInfo,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.instanceBlocking,
-				icon: 'fas fa-ban',
-				bg: 'var(--bg)',
-			},
-			blockedHosts: '',
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.blockedHosts = meta.blockedHosts.join('\n');
-		},
-
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				blockedHosts: this.blockedHosts.split('\n') || [],
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/instance/instance.vue b/src/client/pages/instance/instance.vue
deleted file mode 100644
index 5572fbbf75..0000000000
--- a/src/client/pages/instance/instance.vue
+++ /dev/null
@@ -1,321 +0,0 @@
-<template>
-<XModalWindow ref="dialog"
-	:width="520"
-	:height="500"
-	@close="$refs.dialog.close()"
-	@closed="$emit('closed')"
->
-	<template #header>{{ instance.host }}</template>
-	<div class="mk-instance-info">
-		<div class="_table section">
-			<div class="_row">
-				<div class="_cell">
-					<div class="_label">{{ $ts.software }}</div>
-					<div class="_data">{{ instance.softwareName || '?' }}</div>
-				</div>
-				<div class="_cell">
-					<div class="_label">{{ $ts.version }}</div>
-					<div class="_data">{{ instance.softwareVersion || '?' }}</div>
-				</div>
-			</div>
-		</div>
-		<div class="_table data section">
-			<div class="_row">
-				<div class="_cell">
-					<div class="_label">{{ $ts.registeredAt }}</div>
-					<div class="_data">{{ new Date(instance.caughtAt).toLocaleString() }} (<MkTime :time="instance.caughtAt"/>)</div>
-				</div>
-			</div>
-			<div class="_row">
-				<div class="_cell">
-					<div class="_label">{{ $ts.following }}</div>
-					<button class="_data _textButton" @click="showFollowing()">{{ number(instance.followingCount) }}</button>
-				</div>
-				<div class="_cell">
-					<div class="_label">{{ $ts.followers }}</div>
-					<button class="_data _textButton" @click="showFollowers()">{{ number(instance.followersCount) }}</button>
-				</div>
-			</div>
-			<div class="_row">
-				<div class="_cell">
-					<div class="_label">{{ $ts.users }}</div>
-					<button class="_data _textButton" @click="showUsers()">{{ number(instance.usersCount) }}</button>
-				</div>
-				<div class="_cell">
-					<div class="_label">{{ $ts.notes }}</div>
-					<div class="_data">{{ number(instance.notesCount) }}</div>
-				</div>
-			</div>
-			<div class="_row">
-				<div class="_cell">
-					<div class="_label">{{ $ts.files }}</div>
-					<div class="_data">{{ number(instance.driveFiles) }}</div>
-				</div>
-				<div class="_cell">
-					<div class="_label">{{ $ts.storageUsage }}</div>
-					<div class="_data">{{ bytes(instance.driveUsage) }}</div>
-				</div>
-			</div>
-			<div class="_row">
-				<div class="_cell">
-					<div class="_label">{{ $ts.latestRequestSentAt }}</div>
-					<div class="_data"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
-				</div>
-				<div class="_cell">
-					<div class="_label">{{ $ts.latestStatus }}</div>
-					<div class="_data">{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</div>
-				</div>
-			</div>
-			<div class="_row">
-				<div class="_cell">
-					<div class="_label">{{ $ts.latestRequestReceivedAt }}</div>
-					<div class="_data"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
-				</div>
-			</div>
-		</div>
-		<div class="chart">
-			<div class="header">
-				<span class="label">{{ $ts.charts }}</span>
-				<div class="selects">
-					<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
-						<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>
-						<option value="day">{{ $ts.perDay }}</option>
-					</MkSelect>
-				</div>
-			</div>
-			<div class="chart">
-				<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
-			</div>
-		</div>
-		<div class="operations section">
-			<span class="label">{{ $ts.operations }}</span>
-			<MkSwitch v-model="isSuspended" class="switch">{{ $ts.stopActivityDelivery }}</MkSwitch>
-			<MkSwitch :model-value="isBlocked" class="switch" @update:modelValue="changeBlock">{{ $ts.blockThisInstance }}</MkSwitch>
-			<details>
-				<summary>{{ $ts.deleteAllFiles }}</summary>
-				<MkButton @click="deleteAllFiles()" style="margin: 0.5em 0 0.5em 0;"><i class="fas fa-trash-alt"></i> {{ $ts.deleteAllFiles }}</MkButton>
-			</details>
-			<details>
-				<summary>{{ $ts.removeAllFollowing }}</summary>
-				<MkButton @click="removeAllFollowing()" style="margin: 0.5em 0 0.5em 0;"><i class="fas fa-minus-circle"></i> {{ $ts.removeAllFollowing }}</MkButton>
-				<MkInfo warn>{{ $t('removeAllFollowingDescription', { host: instance.host }) }}</MkInfo>
-			</details>
-		</div>
-		<details class="metadata section">
-			<summary class="label">{{ $ts.metadata }}</summary>
-			<pre><code>{{ JSON.stringify(instance, null, 2) }}</code></pre>
-		</details>
-	</div>
-</XModalWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-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';
-
-export default defineComponent({
-	components: {
-		XModalWindow,
-		MkSelect,
-		MkButton,
-		MkSwitch,
-		MkInfo,
-		MkChart,
-	},
-
-	props: {
-		instance: {
-			type: Object,
-			required: true
-		}
-	},
-
-	emits: ['closed'],
-
-	data() {
-		return {
-			isSuspended: this.instance.isSuspended,
-			chartSrc: 'requests',
-			chartSpan: 'hour',
-		};
-	},
-
-	computed: {
-		meta() {
-			return this.$instance;
-		},
-
-		isBlocked() {
-			return this.meta && this.meta.blockedHosts && this.meta.blockedHosts.includes(this.instance.host);
-		}
-	},
-
-	watch: {
-		isSuspended() {
-			os.api('admin/federation/update-instance', {
-				host: this.instance.host,
-				isSuspended: this.isSuspended
-			});
-		},
-	},
-
-	methods: {
-		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)
-			});
-		},
-
-		removeAllFollowing() {
-			os.apiWithDialog('admin/federation/remove-all-following', {
-				host: this.instance.host
-			});
-		},
-
-		deleteAllFiles() {
-			os.apiWithDialog('admin/federation/delete-all-files', {
-				host: this.instance.host
-			});
-		},
-
-		showFollowing() {
-			os.modal(MkUsersDialog, {
-				title: this.$ts.instanceFollowing,
-				pagination: {
-					endpoint: 'federation/following',
-					limit: 10,
-					params: {
-						host: this.instance.host
-					}
-				},
-				extract: item => item.follower
-			});
-		},
-
-		showFollowers() {
-			os.modal(MkUsersDialog, {
-				title: this.$ts.instanceFollowers,
-				pagination: {
-					endpoint: 'federation/followers',
-					limit: 10,
-					params: {
-						host: this.instance.host
-					}
-				},
-				extract: item => item.followee
-			});
-		},
-
-		showUsers() {
-			os.modal(MkUsersDialog, {
-				title: this.$ts.instanceUsers,
-				pagination: {
-					endpoint: 'federation/users',
-					limit: 10,
-					params: {
-						host: this.instance.host
-					}
-				}
-			});
-		},
-
-		bytes,
-
-		number
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-instance-info {
-	overflow: auto;
-
-	> .section {
-		padding: 16px 32px;
-
-		@media (max-width: 500px) {
-			padding: 8px 16px;
-		}
-
-		&:not(:first-child) {
-			border-top: solid 0.5px var(--divider);
-		}
-	}
-
-	> .chart {
-		border-top: solid 0.5px var(--divider);
-		padding: 16px 0 12px 0;
-
-		> .header {
-			padding: 0 32px;
-
-			@media (max-width: 500px) {
-				padding: 0 16px;
-			}
-
-			> .label {
-				font-size: 80%;
-				opacity: 0.7;
-			}
-
-			> .selects {
-				display: flex;
-			}
-		}
-
-		> .chart {
-			padding: 0 16px;
-
-			@media (max-width: 500px) {
-				padding: 0;
-			}
-		}
-	}
-
-	> .operations {
-		> .label {
-			font-size: 80%;
-			opacity: 0.7;
-		}
-
-		> .switch {
-			margin: 16px 0;
-		}
-	}
-
-	> .metadata {
-		> .label {
-			font-size: 80%;
-			opacity: 0.7;
-		}
-
-		> pre > code {
-			display: block;
-			max-height: 200px;
-			overflow: auto;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/instance/integrations-discord.vue b/src/client/pages/instance/integrations-discord.vue
deleted file mode 100644
index c33b24f17f..0000000000
--- a/src/client/pages/instance/integrations-discord.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormSwitch v-model="enableDiscordIntegration">
-			{{ $ts.enable }}
-		</FormSwitch>
-
-		<template v-if="enableDiscordIntegration">
-			<FormInfo>Callback URL: {{ `${url}/api/dc/cb` }}</FormInfo>
-		
-			<FormInput v-model="discordClientId">
-				<template #prefix><i class="fas fa-key"></i></template>
-				Client ID
-			</FormInput>
-
-			<FormInput v-model="discordClientSecret">
-				<template #prefix><i class="fas fa-key"></i></template>
-				Client Secret
-			</FormInput>
-		</template>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.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 FormInfo from '@client/components/debobigego/info.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormInfo,
-		FormButton,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: 'Discord',
-				icon: 'fab fa-discord'
-			},
-			enableDiscordIntegration: false,
-			discordClientId: null,
-			discordClientSecret: null,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.enableDiscordIntegration = meta.enableDiscordIntegration;
-			this.discordClientId = meta.discordClientId;
-			this.discordClientSecret = meta.discordClientSecret;
-		},
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				enableDiscordIntegration: this.enableDiscordIntegration,
-				discordClientId: this.discordClientId,
-				discordClientSecret: this.discordClientSecret,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/instance/integrations-github.vue b/src/client/pages/instance/integrations-github.vue
deleted file mode 100644
index cdf85868ff..0000000000
--- a/src/client/pages/instance/integrations-github.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormSwitch v-model="enableGithubIntegration">
-			{{ $ts.enable }}
-		</FormSwitch>
-
-		<template v-if="enableGithubIntegration">
-			<FormInfo>Callback URL: {{ `${url}/api/gh/cb` }}</FormInfo>
-		
-			<FormInput v-model="githubClientId">
-				<template #prefix><i class="fas fa-key"></i></template>
-				Client ID
-			</FormInput>
-
-			<FormInput v-model="githubClientSecret">
-				<template #prefix><i class="fas fa-key"></i></template>
-				Client Secret
-			</FormInput>
-		</template>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.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 FormInfo from '@client/components/debobigego/info.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormInfo,
-		FormButton,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: 'GitHub',
-				icon: 'fab fa-github'
-			},
-			enableGithubIntegration: false,
-			githubClientId: null,
-			githubClientSecret: null,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.enableGithubIntegration = meta.enableGithubIntegration;
-			this.githubClientId = meta.githubClientId;
-			this.githubClientSecret = meta.githubClientSecret;
-		},
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				enableGithubIntegration: this.enableGithubIntegration,
-				githubClientId: this.githubClientId,
-				githubClientSecret: this.githubClientSecret,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/instance/integrations-twitter.vue b/src/client/pages/instance/integrations-twitter.vue
deleted file mode 100644
index ed7d097d0a..0000000000
--- a/src/client/pages/instance/integrations-twitter.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormSwitch v-model="enableTwitterIntegration">
-			{{ $ts.enable }}
-		</FormSwitch>
-
-		<template v-if="enableTwitterIntegration">
-			<FormInfo>Callback URL: {{ `${url}/api/tw/cb` }}</FormInfo>
-		
-			<FormInput v-model="twitterConsumerKey">
-				<template #prefix><i class="fas fa-key"></i></template>
-				Consumer Key
-			</FormInput>
-
-			<FormInput v-model="twitterConsumerSecret">
-				<template #prefix><i class="fas fa-key"></i></template>
-				Consumer Secret
-			</FormInput>
-		</template>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.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 FormInfo from '@client/components/debobigego/info.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormInfo,
-		FormButton,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: 'Twitter',
-				icon: 'fab fa-twitter'
-			},
-			enableTwitterIntegration: false,
-			twitterConsumerKey: null,
-			twitterConsumerSecret: null,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.enableTwitterIntegration = meta.enableTwitterIntegration;
-			this.twitterConsumerKey = meta.twitterConsumerKey;
-			this.twitterConsumerSecret = meta.twitterConsumerSecret;
-		},
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				enableTwitterIntegration: this.enableTwitterIntegration,
-				twitterConsumerKey: this.twitterConsumerKey,
-				twitterConsumerSecret: this.twitterConsumerSecret,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/instance/integrations.vue b/src/client/pages/instance/integrations.vue
deleted file mode 100644
index 6964ae5704..0000000000
--- a/src/client/pages/instance/integrations.vue
+++ /dev/null
@@ -1,74 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormLink to="/instance/integrations/twitter">
-			<i class="fab fa-twitter"></i> Twitter
-			<template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template>
-		</FormLink>
-		<FormLink to="/instance/integrations/github">
-			<i class="fab fa-github"></i> GitHub
-			<template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template>
-		</FormLink>
-		<FormLink to="/instance/integrations/discord">
-			<i class="fab fa-discord"></i> Discord
-			<template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template>
-		</FormLink>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormLink from '@client/components/debobigego/link.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 * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormLink,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormTextarea,
-		FormInfo,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.integration,
-				icon: 'fas fa-share-alt',
-				bg: 'var(--bg)',
-			},
-			enableTwitterIntegration: false,
-			enableGithubIntegration: false,
-			enableDiscordIntegration: false,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.enableTwitterIntegration = meta.enableTwitterIntegration;
-			this.enableGithubIntegration = meta.enableGithubIntegration;
-			this.enableDiscordIntegration = meta.enableDiscordIntegration;
-		},
-	}
-});
-</script>
diff --git a/src/client/pages/instance/logs.vue b/src/client/pages/instance/logs.vue
deleted file mode 100644
index 74aea0fc45..0000000000
--- a/src/client/pages/instance/logs.vue
+++ /dev/null
@@ -1,97 +0,0 @@
-<template>
-<div class="_section">
-	<div class="_inputs">
-		<MkInput v-model="logDomain" :debounce="true">
-			<template #label>{{ $ts.domain }}</template>
-		</MkInput>
-		<MkSelect v-model="logLevel">
-			<template #label>Level</template>
-			<option value="all">All</option>
-			<option value="info">Info</option>
-			<option value="success">Success</option>
-			<option value="warning">Warning</option>
-			<option value="error">Error</option>
-			<option value="debug">Debug</option>
-		</MkSelect>
-	</div>
-
-	<div class="logs">
-		<code v-for="log in logs" :key="log.id" :class="log.level">
-			<details>
-				<summary><MkTime :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary>
-				<!--<vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty>-->
-			</details>
-		</code>
-	</div>
-
-	<MkButton @click="deleteAllLogs()" primary><i class="fas fa-trash-alt"></i> {{ $ts.deleteAll }}</MkButton>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkSelect,
-		MkTextarea,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.serverLogs,
-				icon: 'fas fa-stream'
-			},
-			logs: [],
-			logLevel: 'all',
-			logDomain: '',
-		}
-	},
-
-	watch: {
-		logLevel() {
-			this.logs = [];
-			this.fetchLogs();
-		},
-		logDomain() {
-			this.logs = [];
-			this.fetchLogs();
-		}
-	},
-
-	created() {
-		this.fetchLogs();
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		fetchLogs() {
-			os.api('admin/logs', {
-				level: this.logLevel === 'all' ? null : this.logLevel,
-				domain: this.logDomain === '' ? null : this.logDomain,
-				limit: 30
-			}).then(logs => {
-				this.logs = logs.reverse();
-			});
-		},
-
-		deleteAllLogs() {
-			os.apiWithDialog('admin/delete-logs');
-		},
-	}
-});
-</script>
diff --git a/src/client/pages/instance/metrics.vue b/src/client/pages/instance/metrics.vue
deleted file mode 100644
index da36f6c688..0000000000
--- a/src/client/pages/instance/metrics.vue
+++ /dev/null
@@ -1,472 +0,0 @@
-<template>
-<div class="_debobigegoItem">
-	<div class="_debobigegoLabel"><i class="fas fa-microchip"></i> {{ $ts.cpuAndMemory }}</div>
-	<div class="_debobigegoPanel xhexznfu">
-		<div>
-			<canvas :ref="cpumem"></canvas>
-		</div>
-		<div v-if="serverInfo">
-			<div class="_table">
-				<div class="_row">
-					<div class="_cell"><div class="_label">MEM total</div>{{ bytes(serverInfo.mem.total) }}</div>
-					<div class="_cell"><div class="_label">MEM used</div>{{ bytes(memUsage) }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
-					<div class="_cell"><div class="_label">MEM free</div>{{ bytes(serverInfo.mem.total - memUsage) }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
-				</div>
-			</div>
-		</div>
-	</div>
-</div>
-<div class="_debobigegoItem">
-	<div class="_debobigegoLabel"><i class="fas fa-hdd"></i> {{ $ts.disk }}</div>
-	<div class="_debobigegoPanel xhexznfu">
-		<div>
-			<canvas :ref="disk"></canvas>
-		</div>
-		<div v-if="serverInfo">
-			<div class="_table">
-				<div class="_row">
-					<div class="_cell"><div class="_label">Disk total</div>{{ bytes(serverInfo.fs.total) }}</div>
-					<div class="_cell"><div class="_label">Disk used</div>{{ bytes(serverInfo.fs.used) }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
-					<div class="_cell"><div class="_label">Disk free</div>{{ bytes(serverInfo.fs.total - serverInfo.fs.used) }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
-				</div>
-			</div>
-		</div>
-	</div>
-</div>
-<div class="_debobigegoItem">
-	<div class="_debobigegoLabel"><i class="fas fa-exchange-alt"></i> {{ $ts.network }}</div>
-	<div class="_debobigegoPanel xhexznfu">
-		<div>
-			<canvas :ref="net"></canvas>
-		</div>
-		<div v-if="serverInfo">
-			<div class="_table">
-				<div class="_row">
-					<div class="_cell"><div class="_label">Interface</div>{{ serverInfo.net.interface }}</div>
-				</div>
-			</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-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';
-import MkContainer from '@client/components/ui/container.vue';
-import MkFolder from '@client/components/ui/folder.vue';
-import MkwFederation from '../../widgets/federation.vue';
-import { version, url } from '@client/config';
-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);
-	const g = parseInt(result[2], 16);
-	const b = parseInt(result[3], 16);
-	return `rgba(${r}, ${g}, ${b}, ${a})`;
-};
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkSelect,
-		MkInput,
-		MkContainer,
-		MkFolder,
-		MkwFederation,
-	},
-
-	data() {
-		return {
-			version,
-			url,
-			stats: null,
-			serverInfo: null,
-			connection: null,
-			queueConnection: markRaw(os.stream.useChannel('queueStats')),
-			memUsage: 0,
-			chartCpuMem: null,
-			chartNet: null,
-			jobs: [],
-			logs: [],
-			logLevel: 'all',
-			logDomain: '',
-			modLogs: [],
-			dbInfo: null,
-			overviewHeight: '1fr',
-			queueHeight: '1fr',
-			paused: false,
-		}
-	},
-
-	computed: {
-		gridColor() {
-			// TODO: var(--panel)の色が暗いか明るいかで判定する
-			return this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
-		},
-	},
-
-	mounted() {
-		this.fetchJobs();
-
-		Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
-
-		os.api('admin/server-info', {}).then(res => {
-			this.serverInfo = res;
-
-			this.connection = markRaw(os.stream.useChannel('serverStats'));
-			this.connection.on('stats', this.onStats);
-			this.connection.on('statsLog', this.onStatsLog);
-			this.connection.send('requestLog', {
-				id: Math.random().toString().substr(2, 8),
-				length: 150
-			});
-
-			this.$nextTick(() => {
-				this.queueConnection.send('requestLog', {
-					id: Math.random().toString().substr(2, 8),
-					length: 200
-				});
-			});
-		});
-	},
-
-	beforeUnmount() {
-		if (this.connection) {
-			this.connection.off('stats', this.onStats);
-			this.connection.off('statsLog', this.onStatsLog);
-			this.connection.dispose();
-		}
-		this.queueConnection.dispose();
-	},
-
-	methods: {
-		cpumem(el) {
-			if (this.chartCpuMem != null) return;
-			this.chartCpuMem = markRaw(new Chart(el, {
-				type: 'line',
-				data: {
-					labels: [],
-					datasets: [{
-						label: 'CPU',
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderColor: '#86b300',
-						backgroundColor: alpha('#86b300', 0.1),
-						data: []
-					}, {
-						label: 'MEM (active)',
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderColor: '#935dbf',
-						backgroundColor: alpha('#935dbf', 0.02),
-						data: []
-					}, {
-						label: 'MEM (used)',
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderColor: '#935dbf',
-						borderDash: [5, 5],
-						fill: false,
-						data: []
-					}]
-				},
-				options: {
-					aspectRatio: 3,
-					layout: {
-						padding: {
-							left: 16,
-							right: 16,
-							top: 16,
-							bottom: 0
-						}
-					},
-					legend: {
-						position: 'bottom',
-						labels: {
-							boxWidth: 16,
-						}
-					},
-					scales: {
-						x: {
-							gridLines: {
-								display: false,
-								color: this.gridColor,
-								zeroLineColor: this.gridColor,
-							},
-							ticks: {
-								display: false,
-							}
-						},
-						y: {
-							position: 'right',
-							gridLines: {
-								display: true,
-								color: this.gridColor,
-								zeroLineColor: this.gridColor,
-							},
-							ticks: {
-								display: false,
-								max: 100
-							}
-						}
-					},
-					tooltips: {
-						intersect: false,
-						mode: 'index',
-					}
-				}
-			}));
-		},
-
-		net(el) {
-			if (this.chartNet != null) return;
-			this.chartNet = markRaw(new Chart(el, {
-				type: 'line',
-				data: {
-					labels: [],
-					datasets: [{
-						label: 'In',
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderColor: '#94a029',
-						backgroundColor: alpha('#94a029', 0.1),
-						data: []
-					}, {
-						label: 'Out',
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderColor: '#ff9156',
-						backgroundColor: alpha('#ff9156', 0.1),
-						data: []
-					}]
-				},
-				options: {
-					aspectRatio: 3,
-					layout: {
-						padding: {
-							left: 16,
-							right: 16,
-							top: 16,
-							bottom: 0
-						}
-					},
-					legend: {
-						position: 'bottom',
-						labels: {
-							boxWidth: 16,
-						}
-					},
-					scales: {
-						x: {
-							gridLines: {
-								display: false,
-								color: this.gridColor,
-								zeroLineColor: this.gridColor,
-							},
-							ticks: {
-								display: false
-							}
-						},
-						y: {
-							position: 'right',
-							gridLines: {
-								display: true,
-								color: this.gridColor,
-								zeroLineColor: this.gridColor,
-							},
-							ticks: {
-								display: false,
-							}
-						}
-					},
-					tooltips: {
-						intersect: false,
-						mode: 'index',
-					}
-				}
-			}));
-		},
-
-		disk(el) {
-			if (this.chartDisk != null) return;
-			this.chartDisk = markRaw(new Chart(el, {
-				type: 'line',
-				data: {
-					labels: [],
-					datasets: [{
-						label: 'Read',
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderColor: '#94a029',
-						backgroundColor: alpha('#94a029', 0.1),
-						data: []
-					}, {
-						label: 'Write',
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderColor: '#ff9156',
-						backgroundColor: alpha('#ff9156', 0.1),
-						data: []
-					}]
-				},
-				options: {
-					aspectRatio: 3,
-					layout: {
-						padding: {
-							left: 16,
-							right: 16,
-							top: 16,
-							bottom: 0
-						}
-					},
-					legend: {
-						position: 'bottom',
-						labels: {
-							boxWidth: 16,
-						}
-					},
-					scales: {
-						x: {
-							gridLines: {
-								display: false,
-								color: this.gridColor,
-								zeroLineColor: this.gridColor,
-							},
-							ticks: {
-								display: false
-							}
-						},
-						y: {
-							position: 'right',
-							gridLines: {
-								display: true,
-								color: this.gridColor,
-								zeroLineColor: this.gridColor,
-							},
-							ticks: {
-								display: false,
-							}
-						}
-					},
-					tooltips: {
-						intersect: false,
-						mode: 'index',
-					}
-				}
-			}));
-		},
-
-		fetchJobs() {
-			os.api('admin/queue/deliver-delayed', {}).then(jobs => {
-				this.jobs = jobs;
-			});
-		},
-
-		onStats(stats) {
-			if (this.paused) return;
-
-			const cpu = (stats.cpu * 100).toFixed(0);
-			const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
-			const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0);
-			this.memUsage = stats.mem.active;
-
-			this.chartCpuMem.data.labels.push('');
-			this.chartCpuMem.data.datasets[0].data.push(cpu);
-			this.chartCpuMem.data.datasets[1].data.push(memActive);
-			this.chartCpuMem.data.datasets[2].data.push(memUsed);
-			this.chartNet.data.labels.push('');
-			this.chartNet.data.datasets[0].data.push(stats.net.rx);
-			this.chartNet.data.datasets[1].data.push(stats.net.tx);
-			this.chartDisk.data.labels.push('');
-			this.chartDisk.data.datasets[0].data.push(stats.fs.r);
-			this.chartDisk.data.datasets[1].data.push(stats.fs.w);
-			if (this.chartCpuMem.data.datasets[0].data.length > 150) {
-				this.chartCpuMem.data.labels.shift();
-				this.chartCpuMem.data.datasets[0].data.shift();
-				this.chartCpuMem.data.datasets[1].data.shift();
-				this.chartCpuMem.data.datasets[2].data.shift();
-				this.chartNet.data.labels.shift();
-				this.chartNet.data.datasets[0].data.shift();
-				this.chartNet.data.datasets[1].data.shift();
-				this.chartDisk.data.labels.shift();
-				this.chartDisk.data.datasets[0].data.shift();
-				this.chartDisk.data.datasets[1].data.shift();
-			}
-			this.chartCpuMem.update();
-			this.chartNet.update();
-			this.chartDisk.update();
-		},
-
-		onStatsLog(statsLog) {
-			for (const stats of [...statsLog].reverse()) {
-				this.onStats(stats);
-			}
-		},
-
-		bytes,
-
-		number,
-
-		pause() {
-			this.paused = true;
-		},
-
-		resume() {
-			this.paused = false;
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.xhexznfu {
-	> div:nth-child(2) {
-		padding: 16px;
-		border-top: solid 0.5px var(--divider);
-	}
-}
-</style>
diff --git a/src/client/pages/instance/object-storage.vue b/src/client/pages/instance/object-storage.vue
deleted file mode 100644
index 2d765270e6..0000000000
--- a/src/client/pages/instance/object-storage.vue
+++ /dev/null
@@ -1,155 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormSwitch v-model="useObjectStorage">{{ $ts.useObjectStorage }}</FormSwitch>
-
-		<template v-if="useObjectStorage">
-			<FormInput v-model="objectStorageBaseUrl">
-				<span>{{ $ts.objectStorageBaseUrl }}</span>
-				<template #desc>{{ $ts.objectStorageBaseUrlDesc }}</template>
-			</FormInput>
-
-			<FormInput v-model="objectStorageBucket">
-				<span>{{ $ts.objectStorageBucket }}</span>
-				<template #desc>{{ $ts.objectStorageBucketDesc }}</template>
-			</FormInput>
-
-			<FormInput v-model="objectStoragePrefix">
-				<span>{{ $ts.objectStoragePrefix }}</span>
-				<template #desc>{{ $ts.objectStoragePrefixDesc }}</template>
-			</FormInput>
-
-			<FormInput v-model="objectStorageEndpoint">
-				<span>{{ $ts.objectStorageEndpoint }}</span>
-				<template #desc>{{ $ts.objectStorageEndpointDesc }}</template>
-			</FormInput>
-
-			<FormInput v-model="objectStorageRegion">
-				<span>{{ $ts.objectStorageRegion }}</span>
-				<template #desc>{{ $ts.objectStorageRegionDesc }}</template>
-			</FormInput>
-
-			<FormInput v-model="objectStorageAccessKey">
-				<template #prefix><i class="fas fa-key"></i></template>
-				<span>Access key</span>
-			</FormInput>
-
-			<FormInput v-model="objectStorageSecretKey">
-				<template #prefix><i class="fas fa-key"></i></template>
-				<span>Secret key</span>
-			</FormInput>
-
-			<FormSwitch v-model="objectStorageUseSSL">
-				{{ $ts.objectStorageUseSSL }}
-				<template #desc>{{ $ts.objectStorageUseSSLDesc }}</template>
-			</FormSwitch>
-
-			<FormSwitch v-model="objectStorageUseProxy">
-				{{ $ts.objectStorageUseProxy }}
-				<template #desc>{{ $ts.objectStorageUseProxyDesc }}</template>
-			</FormSwitch>
-
-			<FormSwitch v-model="objectStorageSetPublicRead">
-				{{ $ts.objectStorageSetPublicRead }}
-			</FormSwitch>
-
-			<FormSwitch v-model="objectStorageS3ForcePathStyle">
-				s3ForcePathStyle
-			</FormSwitch>
-		</template>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.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 FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.objectStorage,
-				icon: 'fas fa-cloud',
-				bg: 'var(--bg)',
-			},
-			useObjectStorage: false,
-			objectStorageBaseUrl: null,
-			objectStorageBucket: null,
-			objectStoragePrefix: null,
-			objectStorageEndpoint: null,
-			objectStorageRegion: null,
-			objectStoragePort: null,
-			objectStorageAccessKey: null,
-			objectStorageSecretKey: null,
-			objectStorageUseSSL: false,
-			objectStorageUseProxy: false,
-			objectStorageSetPublicRead: false,
-			objectStorageS3ForcePathStyle: true,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.useObjectStorage = meta.useObjectStorage;
-			this.objectStorageBaseUrl = meta.objectStorageBaseUrl;
-			this.objectStorageBucket = meta.objectStorageBucket;
-			this.objectStoragePrefix = meta.objectStoragePrefix;
-			this.objectStorageEndpoint = meta.objectStorageEndpoint;
-			this.objectStorageRegion = meta.objectStorageRegion;
-			this.objectStoragePort = meta.objectStoragePort;
-			this.objectStorageAccessKey = meta.objectStorageAccessKey;
-			this.objectStorageSecretKey = meta.objectStorageSecretKey;
-			this.objectStorageUseSSL = meta.objectStorageUseSSL;
-			this.objectStorageUseProxy = meta.objectStorageUseProxy;
-			this.objectStorageSetPublicRead = meta.objectStorageSetPublicRead;
-			this.objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle;
-		},
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				useObjectStorage: this.useObjectStorage,
-				objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null,
-				objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null,
-				objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null,
-				objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null,
-				objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null,
-				objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null,
-				objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null,
-				objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null,
-				objectStorageUseSSL: this.objectStorageUseSSL,
-				objectStorageUseProxy: this.objectStorageUseProxy,
-				objectStorageSetPublicRead: this.objectStorageSetPublicRead,
-				objectStorageS3ForcePathStyle: this.objectStorageS3ForcePathStyle,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/instance/other-settings.vue b/src/client/pages/instance/other-settings.vue
deleted file mode 100644
index 4e55df41fb..0000000000
--- a/src/client/pages/instance/other-settings.vue
+++ /dev/null
@@ -1,83 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormGroup>
-			<FormInput v-model="summalyProxy">
-				<template #prefix><i class="fas fa-link"></i></template>
-				Summaly Proxy URL
-			</FormInput>
-		</FormGroup>
-		<FormGroup>
-			<FormInput v-model="deeplAuthKey">
-				<template #prefix><i class="fas fa-key"></i></template>
-				DeepL Auth Key
-			</FormInput>
-			<FormSwitch v-model="deeplIsPro">
-				Pro account
-			</FormSwitch>
-		</FormGroup>
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.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 FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.other,
-				icon: 'fas fa-cogs',
-				bg: 'var(--bg)',
-			},
-			summalyProxy: '',
-			deeplAuthKey: '',
-			deeplIsPro: false,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.summalyProxy = meta.summalyProxy;
-			this.deeplAuthKey = meta.deeplAuthKey;
-			this.deeplIsPro = meta.deeplIsPro;
-		},
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				summalyProxy: this.summalyProxy,
-				deeplAuthKey: this.deeplAuthKey,
-				deeplIsPro: this.deeplIsPro,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/instance/overview.vue b/src/client/pages/instance/overview.vue
deleted file mode 100644
index 327a6492e6..0000000000
--- a/src/client/pages/instance/overview.vue
+++ /dev/null
@@ -1,188 +0,0 @@
-<template>
-<div>
-	<MkHeader :info="header"/>
-
-	<div class="edbbcaef">
-		<div v-if="stats" class="cfcdecdf" style="margin: var(--margin)">
-			<div class="number _panel">
-				<div class="label">Users</div>
-				<div class="value _monospace">
-					{{ number(stats.originalUsersCount) }}
-					<MkNumberDiff v-if="usersComparedToThePrevDay != null" 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 != null" class="diff" :value="notesComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><template #before>(</template><template #after>)</template></MkNumberDiff>
-				</div>
-			</div>
-		</div>
-
-		<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/>-->
-
-		<MkFolder style="margin: var(--margin)">
-			<template #header><i class="fas fa-info-circle"></i> {{ $ts.info }}</template>
-			<div class="cfcdecdf">
-				<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>
-		</MkFolder>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, version as vueVersion } from 'vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.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 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';
-import bytes from '@client/filters/bytes';
-import number from '@client/filters/number';
-import MkInstanceInfo from './instance.vue';
-import XMetrics from './metrics.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkNumberDiff,
-		FormKeyValueView,
-		MkInstanceStats,
-		MkContainer,
-		MkFolder,
-		XMetrics,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.dashboard,
-				icon: 'fas fa-tachometer-alt',
-				bg: 'var(--bg)',
-			},
-			header: {
-				title: this.$ts.dashboard,
-				icon: 'fas fa-tachometer-alt',
-			},
-			version,
-			vueVersion,
-			url,
-			stats: null,
-			meta: null,
-			serverInfo: null,
-			usersComparedToThePrevDay: null,
-			notesComparedToThePrevDay: null,
-			fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
-			fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
-		}
-	},
-
-	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 showInstanceInfo(q) {
-			let instance = q;
-			if (typeof q === 'string') {
-				instance = await os.api('federation/show-instance', {
-					host: q
-				});
-			}
-			os.popup(MkInstanceInfo, {
-				instance: instance
-			}, {}, 'closed');
-		},
-
-		bytes,
-
-		number,
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.edbbcaef {
-	.cfcdecdf {
-		display: grid;
-		grid-gap: 8px;
-		grid-template-columns: repeat(auto-fill,minmax(150px,1fr));
-
-		> .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>
diff --git a/src/client/pages/instance/proxy-account.vue b/src/client/pages/instance/proxy-account.vue
deleted file mode 100644
index b1ece19710..0000000000
--- a/src/client/pages/instance/proxy-account.vue
+++ /dev/null
@@ -1,87 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormGroup>
-			<FormKeyValueView>
-				<template #key>{{ $ts.proxyAccount }}</template>
-				<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : $ts.none }}</template>
-			</FormKeyValueView>
-			<template #caption>{{ $ts.proxyAccountDescription }}</template>
-		</FormGroup>
-
-		<FormButton @click="chooseProxyAccount" primary>{{ $ts.selectAccount }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } 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 * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormKeyValueView,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormTextarea,
-		FormInfo,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.proxyAccount,
-				icon: 'fas fa-ghost',
-				bg: 'var(--bg)',
-			},
-			proxyAccount: null,
-			proxyAccountId: null,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.proxyAccountId = meta.proxyAccountId;
-			if (this.proxyAccountId) {
-				this.proxyAccount = await os.api('users/show', { userId: this.proxyAccountId });
-			}
-		},
-
-		chooseProxyAccount() {
-			os.selectUser().then(user => {
-				this.proxyAccount = user;
-				this.proxyAccountId = user.id;
-				this.save();
-			});
-		},
-
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				proxyAccountId: this.proxyAccountId,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/instance/queue.chart.vue b/src/client/pages/instance/queue.chart.vue
deleted file mode 100644
index 4f8fd762bb..0000000000
--- a/src/client/pages/instance/queue.chart.vue
+++ /dev/null
@@ -1,218 +0,0 @@
-<template>
-<div class="_debobigegoItem">
-	<div class="_debobigegoLabel"><slot name="title"></slot></div>
-	<div class="_debobigegoPanel pumxzjhg">
-		<div class="_table status">
-			<div class="_row">
-				<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
-				<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
-				<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
-				<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
-			</div>
-		</div>
-		<div class="">
-			<canvas ref="chart"></canvas>
-		</div>
-		<div class="jobs">
-			<div v-if="jobs.length > 0">
-				<div v-for="job in jobs" :key="job[0]">
-					<span>{{ job[0] }}</span>
-					<span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
-				</div>
-			</div>
-			<span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import Chart from 'chart.js';
-import number from '@client/filters/number';
-
-const alpha = (hex, a) => {
-	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
-	const r = parseInt(result[1], 16);
-	const g = parseInt(result[2], 16);
-	const b = parseInt(result[3], 16);
-	return `rgba(${r}, ${g}, ${b}, ${a})`;
-};
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		domain: {
-			required: true
-		},
-		connection: {
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			chart: null,
-			jobs: [],
-			activeSincePrevTick: 0,
-			active: 0,
-			waiting: 0,
-			delayed: 0,
-		}
-	},
-
-	mounted() {
-		this.fetchJobs();
-
-		// TODO: var(--panel)の色が暗いか明るいかで判定する
-		const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
-
-		Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
-
-		this.chart = markRaw(new Chart(this.$refs.chart, {
-			type: 'line',
-			data: {
-				labels: [],
-				datasets: [{
-					label: 'Process',
-					pointRadius: 0,
-					lineTension: 0,
-					borderWidth: 2,
-					borderColor: '#00E396',
-					backgroundColor: alpha('#00E396', 0.1),
-					data: []
-				}, {
-					label: 'Active',
-					pointRadius: 0,
-					lineTension: 0,
-					borderWidth: 2,
-					borderColor: '#00BCD4',
-					backgroundColor: alpha('#00BCD4', 0.1),
-					data: []
-				}, {
-					label: 'Waiting',
-					pointRadius: 0,
-					lineTension: 0,
-					borderWidth: 2,
-					borderColor: '#FFB300',
-					backgroundColor: alpha('#FFB300', 0.1),
-					data: []
-				}, {
-					label: 'Delayed',
-					pointRadius: 0,
-					lineTension: 0,
-					borderWidth: 2,
-					borderColor: '#E53935',
-					borderDash: [5, 5],
-					fill: false,
-					data: []
-				}]
-			},
-			options: {
-				aspectRatio: 3,
-				layout: {
-					padding: {
-						left: 16,
-						right: 16,
-						top: 16,
-						bottom: 12
-					}
-				},
-				legend: {
-					position: 'bottom',
-					labels: {
-						boxWidth: 16,
-					}
-				},
-				scales: {
-					xAxes: [{
-						gridLines: {
-							display: false,
-							color: gridColor,
-							zeroLineColor: gridColor,
-						},
-						ticks: {
-							display: false
-						}
-					}],
-					yAxes: [{
-						position: 'right',
-						gridLines: {
-							display: true,
-							color: gridColor,
-							zeroLineColor: gridColor,
-						},
-						ticks: {
-							display: false,
-						}
-					}]
-				},
-				tooltips: {
-					intersect: false,
-					mode: 'index',
-				}
-			}
-		}));
-
-		this.connection.on('stats', this.onStats);
-		this.connection.on('statsLog', this.onStatsLog);
-	},
-
-	beforeUnmount() {
-		this.connection.off('stats', this.onStats);
-		this.connection.off('statsLog', this.onStatsLog);
-	},
-
-	methods: {
-		onStats(stats) {
-			this.activeSincePrevTick = stats[this.domain].activeSincePrevTick;
-			this.active = stats[this.domain].active;
-			this.waiting = stats[this.domain].waiting;
-			this.delayed = stats[this.domain].delayed;
-			this.chart.data.labels.push('');
-			this.chart.data.datasets[0].data.push(stats[this.domain].activeSincePrevTick);
-			this.chart.data.datasets[1].data.push(stats[this.domain].active);
-			this.chart.data.datasets[2].data.push(stats[this.domain].waiting);
-			this.chart.data.datasets[3].data.push(stats[this.domain].delayed);
-			if (this.chart.data.datasets[0].data.length > 200) {
-				this.chart.data.labels.shift();
-				this.chart.data.datasets[0].data.shift();
-				this.chart.data.datasets[1].data.shift();
-				this.chart.data.datasets[2].data.shift();
-				this.chart.data.datasets[3].data.shift();
-			}
-			this.chart.update();
-		},
-
-		onStatsLog(statsLog) {
-			for (const stats of [...statsLog].reverse()) {
-				this.onStats(stats);
-			}
-		},
-
-		fetchJobs() {
-			os.api(this.domain === 'inbox' ? 'admin/queue/inbox-delayed' : this.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(jobs => {
-				this.jobs = jobs;
-			});
-		},
-
-		number
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.pumxzjhg {
-	> .status {
-		padding: 16px;
-		border-bottom: solid 0.5px var(--divider);
-	}
-
-	> .jobs {
-		padding: 16px;
-		border-top: solid 0.5px var(--divider);
-		max-height: 180px;
-		overflow: auto;
-	}
-}
-</style>
diff --git a/src/client/pages/instance/queue.vue b/src/client/pages/instance/queue.vue
deleted file mode 100644
index f88825eb19..0000000000
--- a/src/client/pages/instance/queue.vue
+++ /dev/null
@@ -1,73 +0,0 @@
-<template>
-<FormBase>
-	<XQueue :connection="connection" domain="inbox">
-		<template #title>In</template>
-	</XQueue>
-	<XQueue :connection="connection" domain="deliver">
-		<template #title>Out</template>
-	</XQueue>
-	<FormButton @click="clear()" danger><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</FormButton>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import XQueue from './queue.chart.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormButton,
-		MkButton,
-		XQueue,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.jobQueue,
-				icon: 'fas fa-clipboard-list',
-				bg: 'var(--bg)',
-			},
-			connection: markRaw(os.stream.useChannel('queueStats')),
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-
-		this.$nextTick(() => {
-			this.connection.send('requestLog', {
-				id: Math.random().toString().substr(2, 8),
-				length: 200
-			});
-		});
-	},
-
-	beforeUnmount() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		clear() {
-			os.dialog({
-				type: 'warning',
-				title: this.$ts.clearQueueConfirmTitle,
-				text: this.$ts.clearQueueConfirmText,
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				os.apiWithDialog('admin/queue/clear', {});
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/instance/relays.vue b/src/client/pages/instance/relays.vue
deleted file mode 100644
index 7d7888eaa8..0000000000
--- a/src/client/pages/instance/relays.vue
+++ /dev/null
@@ -1,99 +0,0 @@
-<template>
-<FormBase class="relaycxt">
-	<FormButton @click="addRelay" primary><i class="fas fa-plus"></i> {{ $ts.addRelay }}</FormButton>
-
-	<div class="_debobigegoItem" v-for="relay in relays" :key="relay.inbox">
-		<div class="_debobigegoPanel" style="padding: 16px;">
-			<div>{{ relay.inbox }}</div>
-			<div>{{ $t(`_relayStatus.${relay.status}`) }}</div>
-			<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
-		</div>
-	</div>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormButton,
-		MkButton,
-		MkInput,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.relays,
-				icon: 'fas fa-globe',
-				bg: 'var(--bg)',
-			},
-			relays: [],
-			inbox: '',
-		}
-	},
-
-	created() {
-		this.refresh();
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async addRelay() {
-			const { canceled, result: inbox } = await os.dialog({
-				title: this.$ts.addRelay,
-				input: {
-					placeholder: this.$ts.inboxUrl
-				}
-			});
-			if (canceled) return;
-			os.api('admin/relays/add', {
-				inbox
-			}).then((relay: any) => {
-				this.refresh();
-			}).catch((e: any) => {
-				os.dialog({
-					type: 'error',
-					text: e.message || e
-				});
-			});
-		},
-
-		remove(inbox: string) {
-			os.api('admin/relays/remove', {
-				inbox
-			}).then(() => {
-				this.refresh();
-			}).catch((e: any) => {
-				os.dialog({
-					type: 'error',
-					text: e.message || e
-				});
-			});
-		},
-
-		refresh() {
-			os.api('admin/relays/list').then((relays: any) => {
-				this.relays = relays;
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/src/client/pages/instance/security.vue b/src/client/pages/instance/security.vue
deleted file mode 100644
index a854b6dbd0..0000000000
--- a/src/client/pages/instance/security.vue
+++ /dev/null
@@ -1,83 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormLink to="/instance/bot-protection">
-			<i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}
-			<template #suffix v-if="enableHcaptcha">hCaptcha</template>
-			<template #suffix v-else-if="enableRecaptcha">reCAPTCHA</template>
-			<template #suffix v-else>{{ $ts.none }} ({{ $ts.notRecommended }})</template>
-		</FormLink>
-
-		<FormSwitch v-model="enableRegistration">{{ $ts.enableRegistration }}</FormSwitch>
-
-		<FormSwitch v-model="emailRequiredForSignup">{{ $ts.emailRequiredForSignup }}</FormSwitch>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormSwitch from '@client/components/debobigego/switch.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 FormInfo from '@client/components/debobigego/info.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormLink,
-		FormSwitch,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormInfo,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.security,
-				icon: 'fas fa-lock',
-				bg: 'var(--bg)',
-			},
-			enableHcaptcha: false,
-			enableRecaptcha: false,
-			enableRegistration: false,
-			emailRequiredForSignup: false,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.enableHcaptcha = meta.enableHcaptcha;
-			this.enableRecaptcha = meta.enableRecaptcha;
-			this.enableRegistration = !meta.disableRegistration;
-			this.emailRequiredForSignup = meta.emailRequiredForSignup;
-		},
-	
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				disableRegistration: !this.enableRegistration,
-				emailRequiredForSignup: this.emailRequiredForSignup,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/instance/service-worker.vue b/src/client/pages/instance/service-worker.vue
deleted file mode 100644
index 430e02ad2e..0000000000
--- a/src/client/pages/instance/service-worker.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormSwitch v-model="enableServiceWorker">
-			{{ $ts.enableServiceworker }}
-			<template #desc>{{ $ts.serviceworkerInfo }}</template>
-		</FormSwitch>
-
-		<template v-if="enableServiceWorker">
-			<FormInput v-model="swPublicKey">
-				<template #prefix><i class="fas fa-key"></i></template>
-				Public key
-			</FormInput>
-
-			<FormInput v-model="swPrivateKey">
-				<template #prefix><i class="fas fa-key"></i></template>
-				Private key
-			</FormInput>
-		</template>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.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 FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: 'ServiceWorker',
-				icon: 'fas fa-bolt',
-				bg: 'var(--bg)',
-			},
-			enableServiceWorker: false,
-			swPublicKey: null,
-			swPrivateKey: null,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.enableServiceWorker = meta.enableServiceWorker;
-			this.swPublicKey = meta.swPublickey;
-			this.swPrivateKey = meta.swPrivateKey;
-		},
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				enableServiceWorker: this.enableServiceWorker,
-				swPublicKey: this.swPublicKey,
-				swPrivateKey: this.swPrivateKey,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue
deleted file mode 100644
index 7bd363e5f3..0000000000
--- a/src/client/pages/instance/settings.vue
+++ /dev/null
@@ -1,151 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormInput v-model="name">
-			<span>{{ $ts.instanceName }}</span>
-		</FormInput>
-
-		<FormTextarea v-model="description">
-			<span>{{ $ts.instanceDescription }}</span>
-		</FormTextarea>
-
-		<FormInput v-model="iconUrl">
-			<template #prefix><i class="fas fa-link"></i></template>
-			<span>{{ $ts.iconUrl }}</span>
-		</FormInput>
-
-		<FormInput v-model="bannerUrl">
-			<template #prefix><i class="fas fa-link"></i></template>
-			<span>{{ $ts.bannerUrl }}</span>
-		</FormInput>
-
-		<FormInput v-model="backgroundImageUrl">
-			<template #prefix><i class="fas fa-link"></i></template>
-			<span>{{ $ts.backgroundImageUrl }}</span>
-		</FormInput>
-
-		<FormInput v-model="tosUrl">
-			<template #prefix><i class="fas fa-link"></i></template>
-			<span>{{ $ts.tosUrl }}</span>
-		</FormInput>
-
-		<FormInput v-model="maintainerName">
-			<span>{{ $ts.maintainerName }}</span>
-		</FormInput>
-
-		<FormInput v-model="maintainerEmail" type="email">
-			<template #prefix><i class="fas fa-envelope"></i></template>
-			<span>{{ $ts.maintainerEmail }}</span>
-		</FormInput>
-
-		<FormTextarea v-model="pinnedUsers">
-			<span>{{ $ts.pinnedUsers }}</span>
-			<template #desc>{{ $ts.pinnedUsersDescription }}</template>
-		</FormTextarea>
-
-		<FormInput v-model="maxNoteTextLength" type="number">
-			<template #prefix><i class="fas fa-pencil-alt"></i></template>
-			<span>{{ $ts.maxNoteTextLength }}</span>
-		</FormInput>
-
-		<FormSwitch v-model="enableLocalTimeline">{{ $ts.enableLocalTimeline }}</FormSwitch>
-		<FormSwitch v-model="enableGlobalTimeline">{{ $ts.enableGlobalTimeline }}</FormSwitch>
-		<FormInfo>{{ $ts.disablingTimelinesInfo }}</FormInfo>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.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 * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormTextarea,
-		FormInfo,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.general,
-				icon: 'fas fa-cog',
-				bg: 'var(--bg)',
-			},
-			name: null,
-			description: null,
-			tosUrl: null as string | null,
-			maintainerName: null,
-			maintainerEmail: null,
-			iconUrl: null,
-			bannerUrl: null,
-			backgroundImageUrl: null,
-			maxNoteTextLength: 0,
-			enableLocalTimeline: false,
-			enableGlobalTimeline: false,
-			pinnedUsers: '',
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.name = meta.name;
-			this.description = meta.description;
-			this.tosUrl = meta.tosUrl;
-			this.iconUrl = meta.iconUrl;
-			this.bannerUrl = meta.bannerUrl;
-			this.backgroundImageUrl = meta.backgroundImageUrl;
-			this.maintainerName = meta.maintainerName;
-			this.maintainerEmail = meta.maintainerEmail;
-			this.maxNoteTextLength = meta.maxNoteTextLength;
-			this.enableLocalTimeline = !meta.disableLocalTimeline;
-			this.enableGlobalTimeline = !meta.disableGlobalTimeline;
-			this.pinnedUsers = meta.pinnedUsers.join('\n');
-		},
-
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				name: this.name,
-				description: this.description,
-				tosUrl: this.tosUrl,
-				iconUrl: this.iconUrl,
-				bannerUrl: this.bannerUrl,
-				backgroundImageUrl: this.backgroundImageUrl,
-				maintainerName: this.maintainerName,
-				maintainerEmail: this.maintainerEmail,
-				maxNoteTextLength: this.maxNoteTextLength,
-				disableLocalTimeline: !this.enableLocalTimeline,
-				disableGlobalTimeline: !this.enableGlobalTimeline,
-				pinnedUsers: this.pinnedUsers.split('\n'),
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/instance/users.vue b/src/client/pages/instance/users.vue
deleted file mode 100644
index f7f9306b70..0000000000
--- a/src/client/pages/instance/users.vue
+++ /dev/null
@@ -1,261 +0,0 @@
-<template>
-<div class="lknzcolw">
-	<MkHeader :info="header"/>
-
-	<div class="users">
-		<div class="inputs">
-			<MkSelect v-model="sort" style="flex: 1;">
-				<template #label>{{ $ts.sort }}</template>
-				<option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option>
-				<option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option>
-				<option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option>
-				<option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option>
-			</MkSelect>
-			<MkSelect v-model="state" style="flex: 1;">
-				<template #label>{{ $ts.state }}</template>
-				<option value="all">{{ $ts.all }}</option>
-				<option value="available">{{ $ts.normal }}</option>
-				<option value="admin">{{ $ts.administrator }}</option>
-				<option value="moderator">{{ $ts.moderator }}</option>
-				<option value="silenced">{{ $ts.silence }}</option>
-				<option value="suspended">{{ $ts.suspend }}</option>
-			</MkSelect>
-			<MkSelect v-model="origin" style="flex: 1;">
-				<template #label>{{ $ts.instance }}</template>
-				<option value="combined">{{ $ts.all }}</option>
-				<option value="local">{{ $ts.local }}</option>
-				<option value="remote">{{ $ts.remote }}</option>
-			</MkSelect>
-		</div>
-		<div class="inputs">
-			<MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()">
-				<template #prefix>@</template>
-				<template #label>{{ $ts.username }}</template>
-			</MkInput>
-			<MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()" :disabled="pagination.params().origin === 'local'">
-				<template #prefix>@</template>
-				<template #label>{{ $ts.host }}</template>
-			</MkInput>
-		</div>
-
-		<MkPagination :pagination="pagination" #default="{items}" class="users" ref="users">
-			<button class="user _panel _button _gap" v-for="user in items" :key="user.id" @click="show(user)">
-				<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
-				<div class="body">
-					<header>
-						<MkUserName class="name" :user="user"/>
-						<span class="acct">@{{ acct(user) }}</span>
-						<span class="staff" v-if="user.isAdmin"><i class="fas fa-bookmark"></i></span>
-						<span class="staff" v-if="user.isModerator"><i class="far fa-bookmark"></i></span>
-						<span class="punished" v-if="user.isSilenced"><i class="fas fa-microphone-slash"></i></span>
-						<span class="punished" v-if="user.isSuspended"><i class="fas fa-snowflake"></i></span>
-					</header>
-					<div>
-						<span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span>
-					</div>
-					<div>
-						<span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span>
-					</div>
-				</div>
-			</button>
-		</MkPagination>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import { acct } from '@client/filters/user';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { lookupUser } from '@client/scripts/lookup-user';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkSelect,
-		MkPagination,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.users,
-				icon: 'fas fa-users',
-				bg: 'var(--bg)',
-			},
-			header: {
-				title: this.$ts.users,
-				icon: 'fas fa-users',
-				bg: 'var(--bg)',
-				actions: [{
-					icon: 'fas fa-search',
-					text: this.$ts.search,
-					handler: this.searchUser
-				}, {
-					asFullButton: true,
-					icon: 'fas fa-plus',
-					text: this.$ts.addUser,
-					handler: this.addUser
-				}, {
-					asFullButton: true,
-					icon: 'fas fa-search',
-					text: this.$ts.lookup,
-					handler: this.lookupUser
-				}]
-			},
-			sort: '+createdAt',
-			state: 'all',
-			origin: 'local',
-			searchUsername: '',
-			searchHost: '',
-			pagination: {
-				endpoint: 'admin/show-users',
-				limit: 10,
-				params: () => ({
-					sort: this.sort,
-					state: this.state,
-					origin: this.origin,
-					username: this.searchUsername,
-					hostname: this.searchHost,
-				}),
-				offsetMode: true
-			},
-		}
-	},
-
-	watch: {
-		sort() {
-			this.$refs.users.reload();
-		},
-		state() {
-			this.$refs.users.reload();
-		},
-		origin() {
-			this.$refs.users.reload();
-		},
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		lookupUser,
-
-		searchUser() {
-			os.selectUser().then(user => {
-				this.show(user);
-			});
-		},
-
-		async addUser() {
-			const { canceled: canceled1, result: username } = await os.dialog({
-				title: this.$ts.username,
-				input: true
-			});
-			if (canceled1) return;
-
-			const { canceled: canceled2, result: password } = await os.dialog({
-				title: this.$ts.password,
-				input: { type: 'password' }
-			});
-			if (canceled2) return;
-
-			os.apiWithDialog('admin/accounts/create', {
-				username: username,
-				password: password,
-			}).then(res => {
-				this.$refs.users.reload();
-			});
-		},
-
-		show(user) {
-			os.pageWindow(`/user-info/${user.id}`);
-		},
-
-		acct
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.lknzcolw {
-	> .users {
-		margin: var(--margin);
-
-		> .inputs {
-			display: flex;
-			margin-bottom: 16px;
-
-			> * {
-				margin-right: 16px;
-
-				&:last-child {
-					margin-right: 0;
-				}
-			}
-		}
-	
-		> .users {
-			margin-top: var(--margin);
-
-			> .user {
-				display: flex;
-				width: 100%;
-				box-sizing: border-box;
-				text-align: left;
-				align-items: center;
-				padding: 16px;
-
-				&:hover {
-					color: var(--accent);
-				}
-
-				> .avatar {
-					width: 60px;
-					height: 60px;
-				}
-
-				> .body {
-					margin-left: 0.3em;
-					padding: 0 8px;
-					flex: 1;
-
-					@media (max-width: 500px) {
-						font-size: 14px;
-					}
-
-					> header {
-						> .name {
-							font-weight: bold;
-						}
-
-						> .acct {
-							margin-left: 8px;
-							opacity: 0.7;
-						}
-
-						> .staff {
-							margin-left: 0.5em;
-							color: var(--badge);
-						}
-
-						> .punished {
-							margin-left: 0.5em;
-							color: #4dabf7;
-						}
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/router.ts b/src/client/router.ts
index 56dc948669..f588bb04fc 100644
--- a/src/client/router.ts
+++ b/src/client/router.ts
@@ -70,8 +70,8 @@ const defaultRoutes = [
 	{ path: '/my/antennas/:antennaId', component: page('my-antennas/edit'), props: true },
 	{ path: '/my/clips', component: page('my-clips/index') },
 	{ path: '/scratchpad', component: page('scratchpad') },
-	{ path: '/instance/:page(.*)?', component: page('instance/index'), props: route => ({ initialPage: route.params.page || null }) },
-	{ path: '/instance', component: page('instance/index') },
+	{ path: '/admin/:page(.*)?', component: page('admin/index'), props: route => ({ initialPage: route.params.page || null }) },
+	{ path: '/admin', component: page('admin/index') },
 	{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) },
 	{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) },
 	{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) },
diff --git a/src/client/ui/_common_/sidebar.vue b/src/client/ui/_common_/sidebar.vue
index d00327b096..ec2a17ab86 100644
--- a/src/client/ui/_common_/sidebar.vue
+++ b/src/client/ui/_common_/sidebar.vue
@@ -25,7 +25,7 @@
 					</component>
 				</template>
 				<div class="divider"></div>
-				<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance" v-click-anime>
+				<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" v-click-anime>
 					<i class="fas fa-server fa-fw"></i><span class="text">{{ $ts.instance }}</span>
 				</MkA>
 				<button class="item _button" @click="more" v-click-anime>
diff --git a/src/client/ui/default.header.vue b/src/client/ui/default.header.vue
index 4f6363e82d..908a4719a4 100644
--- a/src/client/ui/default.header.vue
+++ b/src/client/ui/default.header.vue
@@ -13,7 +13,7 @@
 				</component>
 			</template>
 			<div class="divider"></div>
-			<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime v-tooltip="$ts.instance">
+			<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime v-tooltip="$ts.instance">
 				<i class="fas fa-server fa-fw"></i>
 			</MkA>
 			<button class="item _button" @click="more" v-click-anime>
diff --git a/src/client/ui/default.sidebar.vue b/src/client/ui/default.sidebar.vue
index e36febb7fa..cce74a53cd 100644
--- a/src/client/ui/default.sidebar.vue
+++ b/src/client/ui/default.sidebar.vue
@@ -20,7 +20,7 @@
 		</component>
 	</template>
 	<div class="divider"></div>
-	<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime>
+	<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime>
 		<i class="fas fa-server fa-fw"></i><span class="text">{{ $ts.instance }}</span>
 	</MkA>
 	<button class="item _button" @click="more" v-click-anime>
-- 
cgit v1.2.3-freya

