summaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2020-02-17 02:21:27 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2020-02-17 02:21:27 +0900
commitf45fb56e15e2925aca192867db0ef4ebb15d1f02 (patch)
tree364e1f7dfceb3c37372f943219ab17dc8c6a63a4 /src/client
parent12.11.0 (diff)
downloadsharkey-f45fb56e15e2925aca192867db0ef4ebb15d1f02.tar.gz
sharkey-f45fb56e15e2925aca192867db0ef4ebb15d1f02.tar.bz2
sharkey-f45fb56e15e2925aca192867db0ef4ebb15d1f02.zip
Improve instance info page
Diffstat (limited to 'src/client')
-rw-r--r--src/client/app.vue21
-rw-r--r--src/client/components/instance-stats.vue (renamed from src/client/pages/instance/stats.vue)198
-rw-r--r--src/client/pages/about.vue18
-rw-r--r--src/client/pages/instance/index.vue674
-rw-r--r--src/client/pages/instance/monitor.vue381
-rw-r--r--src/client/pages/instance/settings.vue413
-rw-r--r--src/client/router.ts3
7 files changed, 952 insertions, 756 deletions
diff --git a/src/client/app.vue b/src/client/app.vue
index 1bfcd9e158..9a984a27ff 100644
--- a/src/client/app.vue
+++ b/src/client/app.vue
@@ -413,9 +413,14 @@ export default Vue.extend({
this.$root.menu({
items: [{
type: 'link',
- text: this.$t('statistics'),
- to: '/instance/stats',
- icon: faChartBar,
+ text: this.$t('dashboard'),
+ to: '/instance',
+ icon: faTachometerAlt,
+ }, null, {
+ type: 'link',
+ text: this.$t('settings'),
+ to: '/instance/settings',
+ icon: faCog,
}, {
type: 'link',
text: this.$t('customEmojis'),
@@ -433,11 +438,6 @@ export default Vue.extend({
icon: faCloud,
}, {
type: 'link',
- text: this.$t('monitor'),
- to: '/instance/monitor',
- icon: faTachometerAlt,
- }, {
- type: 'link',
text: this.$t('jobQueue'),
to: '/instance/queue',
icon: faExchangeAlt,
@@ -451,11 +451,6 @@ export default Vue.extend({
text: this.$t('announcements'),
to: '/instance/announcements',
icon: faBroadcastTower,
- }, null, {
- type: 'link',
- text: this.$t('general'),
- to: '/instance',
- icon: faCog,
}],
align: 'left',
fixed: true,
diff --git a/src/client/pages/instance/stats.vue b/src/client/components/instance-stats.vue
index 4883d8c873..a2625f4ab6 100644
--- a/src/client/pages/instance/stats.vue
+++ b/src/client/components/instance-stats.vue
@@ -1,8 +1,91 @@
<template>
-<div class="mk-instance-stats">
+<div class="zbcjwnqg">
+ <div class="stats" v-if="info">
+ <div class="_panel">
+ <div>
+ <b><fa :icon="faUser"/>{{ $t('users') }}</b>
+ <small>{{ $t('local') }}</small>
+ </div>
+ <div>
+ <dl class="total">
+ <dt>{{ $t('total') }}</dt>
+ <dd>{{ info.originalUsersCount | number }}</dd>
+ </dl>
+ <dl class="diff" :class="{ inc: usersLocalDoD > 0 }">
+ <dt>{{ $t('dayOverDayChanges') }}</dt>
+ <dd>{{ usersLocalDoD | number }}</dd>
+ </dl>
+ <dl class="diff" :class="{ inc: usersLocalWoW > 0 }">
+ <dt>{{ $t('weekOverWeekChanges') }}</dt>
+ <dd>{{ usersLocalWoW | number }}</dd>
+ </dl>
+ </div>
+ </div>
+ <div class="_panel">
+ <div>
+ <b><fa :icon="faUser"/>{{ $t('users') }}</b>
+ <small>{{ $t('remote') }}</small>
+ </div>
+ <div>
+ <dl class="total">
+ <dt>{{ $t('total') }}</dt>
+ <dd>{{ (info.usersCount - info.originalUsersCount) | number }}</dd>
+ </dl>
+ <dl class="diff" :class="{ inc: usersRemoteDoD > 0 }">
+ <dt>{{ $t('dayOverDayChanges') }}</dt>
+ <dd>{{ usersRemoteDoD | number }}</dd>
+ </dl>
+ <dl class="diff" :class="{ inc: usersRemoteWoW > 0 }">
+ <dt>{{ $t('weekOverWeekChanges') }}</dt>
+ <dd>{{ usersRemoteWoW | number }}</dd>
+ </dl>
+ </div>
+ </div>
+ <div class="_panel">
+ <div>
+ <b><fa :icon="faPencilAlt"/>{{ $t('notes') }}</b>
+ <small>{{ $t('local') }}</small>
+ </div>
+ <div>
+ <dl class="total">
+ <dt>{{ $t('total') }}</dt>
+ <dd>{{ info.originalNotesCount | number }}</dd>
+ </dl>
+ <dl class="diff" :class="{ inc: notesLocalDoD > 0 }">
+ <dt>{{ $t('dayOverDayChanges') }}</dt>
+ <dd>{{ notesLocalDoD | number }}</dd>
+ </dl>
+ <dl class="diff" :class="{ inc: notesLocalWoW > 0 }">
+ <dt>{{ $t('weekOverWeekChanges') }}</dt>
+ <dd>{{ notesLocalWoW | number }}</dd>
+ </dl>
+ </div>
+ </div>
+ <div class="_panel">
+ <div>
+ <b><fa :icon="faPencilAlt"/>{{ $t('notes') }}</b>
+ <small>{{ $t('remote') }}</small>
+ </div>
+ <div>
+ <dl class="total">
+ <dt>{{ $t('total') }}</dt>
+ <dd>{{ (info.notesCount - info.originalNotesCount) | number }}</dd>
+ </dl>
+ <dl class="diff" :class="{ inc: notesRemoteDoD > 0 }">
+ <dt>{{ $t('dayOverDayChanges') }}</dt>
+ <dd>{{ notesRemoteDoD | number }}</dd>
+ </dl>
+ <dl class="diff" :class="{ inc: notesRemoteWoW > 0 }">
+ <dt>{{ $t('weekOverWeekChanges') }}</dt>
+ <dd>{{ notesRemoteWoW | number }}</dd>
+ </dl>
+ </div>
+ </div>
+ </div>
+
<section class="_card">
<div class="_title"><fa :icon="faChartBar"/> {{ $t('statistics') }}</div>
- <div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
+ <div class="_content" style="margin-top: -8px;">
<div class="selects" style="display: flex;">
<mk-select v-model="chartSrc" style="margin: 0; flex: 1;">
<optgroup :label="$t('federation')">
@@ -40,10 +123,10 @@
<script lang="ts">
import Vue from 'vue';
-import { faChartBar } from '@fortawesome/free-solid-svg-icons';
+import { faChartBar, faUser, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import Chart from 'chart.js';
-import i18n from '../../i18n';
-import MkSelect from '../../components/ui/select.vue';
+import i18n from '../i18n';
+import MkSelect from './ui/select.vue';
const chartLimit = 90;
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
@@ -59,24 +142,27 @@ const alpha = (hex, a) => {
export default Vue.extend({
i18n,
- metaInfo() {
- return {
- title: `${this.$t('statistics')} | ${this.$t('instance')}`
- };
- },
-
components: {
MkSelect
},
data() {
return {
+ info: null,
+ notesLocalWoW: 0,
+ notesLocalDoD: 0,
+ notesRemoteWoW: 0,
+ notesRemoteDoD: 0,
+ usersLocalWoW: 0,
+ usersLocalDoD: 0,
+ usersRemoteWoW: 0,
+ usersRemoteDoD: 0,
now: null,
chart: null,
chartInstance: null,
chartSrc: 'notes',
chartSpan: 'hour',
- faChartBar
+ faChartBar, faUser, faPencilAlt
}
},
@@ -121,6 +207,8 @@ export default Vue.extend({
},
async created() {
+ this.info = await this.$root.api('stats');
+
this.now = new Date();
const [perHour, perDay] = await Promise.all([Promise.all([
@@ -154,6 +242,15 @@ export default Vue.extend({
}
};
+ this.notesLocalWoW = this.info.originalNotesCount - chart.perDay.notes.local.total[7];
+ this.notesLocalDoD = this.info.originalNotesCount - chart.perDay.notes.local.total[1];
+ this.notesRemoteWoW = (this.info.notesCount - this.info.originalNotesCount) - chart.perDay.notes.remote.total[7];
+ this.notesRemoteDoD = (this.info.notesCount - this.info.originalNotesCount) - chart.perDay.notes.remote.total[1];
+ this.usersLocalWoW = this.info.usersCount - chart.perDay.users.local.total[7];
+ this.usersLocalDoD = this.info.usersCount - chart.perDay.users.local.total[1];
+ this.usersRemoteWoW = (this.info.usersCount - this.info.originalUsersCount) - chart.perDay.users.remote.total[7];
+ this.usersRemoteDoD = (this.info.usersCount - this.info.originalUsersCount) - chart.perDay.users.remote.total[1];
+
this.chart = chart;
this.renderChart();
@@ -489,3 +586,80 @@ export default Vue.extend({
}
});
</script>
+
+<style lang="scss" scoped>
+.zbcjwnqg {
+ > .stats {
+ display: flex;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ margin: calc(0px - var(--margin) / 2);
+ margin-bottom: calc(var(--margin) / 2);
+
+ > div {
+ display: flex;
+ flex: 1 0 213px;
+ margin: calc(var(--margin) / 2);
+ box-sizing: border-box;
+ padding: 16px 20px;
+
+ > div {
+ width: 50%;
+
+ &:first-child {
+ > b {
+ display: block;
+
+ > [data-icon] {
+ width: 16px;
+ margin-right: 8px;
+ }
+ }
+
+ > small {
+ margin-left: 16px + 8px;
+ opacity: 0.7;
+ }
+ }
+
+ &:last-child {
+ > dl {
+ display: flex;
+ margin: 0;
+ line-height: 1.5em;
+
+ > dt,
+ > dd {
+ width: 50%;
+ margin: 0;
+ }
+
+ > dt {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ }
+
+ &.total {
+ > dt,
+ > dd {
+ font-weight: bold;
+ }
+ }
+
+ &.diff.inc {
+ > dd {
+ color: #82c11c;
+
+ &:before {
+ content: "+";
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/pages/about.vue b/src/client/pages/about.vue
index e39600b487..a3a4b6ac73 100644
--- a/src/client/pages/about.vue
+++ b/src/client/pages/about.vue
@@ -12,14 +12,12 @@
<div><b>{{ $t('administrator') }}</b><span>{{ meta.maintainerName }}</span></div>
<div><b></b><span>{{ meta.maintainerEmail }}</span></div>
</div>
- <div class="_content table" v-if="stats">
- <div><b>{{ $t('users') }}</b><span>{{ stats.originalUsersCount | number }}</span></div>
- <div><b>{{ $t('notes') }}</b><span>{{ stats.originalNotesCount | number }}</span></div>
- </div>
<div class="_content table">
<div><b>Misskey</b><span>v{{ version }}</span></div>
</div>
</section>
+
+ <mk-instance-stats style="margin-top: var(--margin);"/>
</div>
</template>
@@ -28,6 +26,7 @@ import Vue from 'vue';
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { version } from '../config';
import i18n from '../i18n';
+import MkInstanceStats from '../components/instance-stats.vue';
export default Vue.extend({
i18n,
@@ -38,10 +37,13 @@ export default Vue.extend({
};
},
+ components: {
+ MkInstanceStats
+ },
+
data() {
return {
version,
- stats: null,
serverInfo: null,
faInfoCircle
}
@@ -52,12 +54,6 @@ export default Vue.extend({
return this.$store.state.instance.meta;
},
},
-
- created() {
- this.$root.api('stats').then(res => {
- this.stats = res;
- });
- },
});
</script>
diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue
index 5a48232417..db88982330 100644
--- a/src/client/pages/instance/index.vue
+++ b/src/client/pages/instance/index.vue
@@ -1,169 +1,54 @@
<template>
-<div v-if="meta" class="mk-instance-page">
+<div v-if="meta" class="xhexznfu">
<portal to="icon"><fa :icon="faServer"/></portal>
<portal to="title">{{ $t('instance') }}</portal>
- <section class="_card info">
- <div class="_title"><fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div>
- <div class="_content">
- <mk-input v-model="name">{{ $t('instanceName') }}</mk-input>
- <mk-textarea v-model="description">{{ $t('instanceDescription') }}</mk-textarea>
- <mk-input v-model="iconUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('iconUrl') }}</mk-input>
- <mk-input v-model="bannerUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('bannerUrl') }}</mk-input>
- <mk-input v-model="tosUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('tosUrl') }}</mk-input>
- <mk-input v-model="maintainerName">{{ $t('maintainerName') }}</mk-input>
- <mk-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="faEnvelope"/></template>{{ $t('maintainerEmail') }}</mk-input>
- </div>
- <div class="_footer">
- <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
- </div>
- </section>
-
- <section class="_card info">
- <div class="_content">
- <mk-input v-model="maxNoteTextLength" type="number" :save="() => save()" style="margin:0;"><template #icon><fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</mk-input>
- </div>
- <div class="_content">
- <mk-switch v-model="enableLocalTimeline" @change="save()">{{ $t('enableLocalTimeline') }}</mk-switch>
- <mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch>
- <mk-info>{{ $t('disablingTimelinesInfo') }}</mk-info>
- </div>
- </section>
-
- <section class="_card info">
- <div class="_title"><fa :icon="faUser"/> {{ $t('registration') }}</div>
- <div class="_content">
- <mk-switch v-model="enableRegistration" @change="save()">{{ $t('enableRegistration') }}</mk-switch>
- <mk-button v-if="!enableRegistration" @click="invite">{{ $t('invite') }}</mk-button>
- </div>
- </section>
-
- <section class="_card">
- <div class="_title"><fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div>
- <div class="_content">
- <mk-switch v-model="enableRecaptcha">{{ $t('enableRecaptcha') }}</mk-switch>
- <template v-if="enableRecaptcha">
- <mk-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSiteKey') }}</mk-input>
- <mk-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSecretKey') }}</mk-input>
- </template>
- </div>
- <div class="_content" v-if="enableRecaptcha && recaptchaSiteKey">
- <header>{{ $t('preview') }}</header>
- <div ref="recaptcha" style="margin: 16px 0 0 0;" :key="recaptchaSiteKey"></div>
- </div>
- <div class="_footer">
- <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
- </div>
- </section>
-
- <section class="_card">
- <div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
- <div class="_content">
- <mk-switch v-model="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></mk-switch>
- <template v-if="enableServiceWorker">
- <mk-horizon-group inputs class="fit-bottom">
- <mk-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Public key</mk-input>
- <mk-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Private key</mk-input>
- </mk-horizon-group>
- </template>
- </div>
- <div class="_footer">
- <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
- </div>
- </section>
-
- <section class="_card">
- <div class="_title"><fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div>
- <div class="_content">
- <mk-textarea v-model="pinnedUsers">
- <template #desc>{{ $t('pinnedUsersDescription') }} <button class="_textButton" @click="addPinUser">{{ $t('addUser') }}</button></template>
- </mk-textarea>
- </div>
- <div class="_footer">
- <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
- </div>
- </section>
-
- <section class="_card">
- <div class="_title"><fa :icon="faCloud"/> {{ $t('files') }}</div>
- <div class="_content">
- <mk-switch v-model="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></mk-switch>
- <mk-switch v-model="proxyRemoteFiles">{{ $t('proxyRemoteFiles') }}<template #desc>{{ $t('proxyRemoteFilesDescription') }}</template></mk-switch>
- <mk-input v-model="localDriveCapacityMb" type="number">{{ $t('driveCapacityPerLocalAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
- <mk-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" style="margin-bottom: 0;">{{ $t('driveCapacityPerRemoteAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
- </div>
- <div class="_footer">
- <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
- </div>
- </section>
+ <mk-instance-stats style="margin-bottom: var(--margin);"/>
- <section class="_card">
- <div class="_title"><fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
- <div class="_content">
- <mk-input :value="proxyAccount ? proxyAccount.username : null" style="margin: 0;" disabled><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></mk-input>
- <mk-button primary @click="chooseProxyAccount">{{ $t('chooseProxyAccount') }}</mk-button>
+ <section class="_card chart">
+ <div class="_title"><fa :icon="faMicrochip"/> {{ $t('cpuAndMemory') }}</div>
+ <div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
+ <canvas ref="cpumem"></canvas>
</div>
- </section>
-
- <section class="_card">
- <div class="_title"><fa :icon="faBan"/> {{ $t('blockedInstances') }}</div>
- <div class="_content">
- <mk-textarea v-model="blockedHosts">
- <template #desc>{{ $t('blockedInstancesDescription') }}</template>
- </mk-textarea>
- </div>
- <div class="_footer">
- <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+ <div class="_content" v-if="serverInfo">
+ <div class="table">
+ <div class="row">
+ <div class="cell"><div class="label">CPU</div>{{ serverInfo.cpu.model }}</div>
+ </div>
+ <div class="row">
+ <div class="cell"><div class="label">MEM total</div>{{ serverInfo.mem.total | bytes }}</div>
+ <div class="cell"><div class="label">MEM used</div>{{ memUsage | bytes }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
+ <div class="cell"><div class="label">MEM free</div>{{ serverInfo.mem.total - memUsage | bytes }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
+ </div>
+ </div>
</div>
</section>
-
- <section class="_card">
- <div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
- <div class="_content">
- <header><fa :icon="faTwitter"/> Twitter</header>
- <mk-switch v-model="enableTwitterIntegration">{{ $t('enable') }}</mk-switch>
- <template v-if="enableTwitterIntegration">
- <mk-info>Callback URL: {{ `${url}/api/tw/cb` }}</mk-info>
- <mk-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Key</mk-input>
- <mk-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Secret</mk-input>
- </template>
- </div>
- <div class="_content">
- <header><fa :icon="faGithub"/> GitHub</header>
- <mk-switch v-model="enableGithubIntegration">{{ $t('enable') }}</mk-switch>
- <template v-if="enableGithubIntegration">
- <mk-info>Callback URL: {{ `${url}/api/gh/cb` }}</mk-info>
- <mk-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
- <mk-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
- </template>
+ <section class="_card chart">
+ <div class="_title"><fa :icon="faHdd"/> {{ $t('disk') }}</div>
+ <div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
+ <canvas ref="disk"></canvas>
</div>
- <div class="_content">
- <header><fa :icon="faDiscord"/> Discord</header>
- <mk-switch v-model="enableDiscordIntegration">{{ $t('enable') }}</mk-switch>
- <template v-if="enableDiscordIntegration">
- <mk-info>Callback URL: {{ `${url}/api/dc/cb` }}</mk-info>
- <mk-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
- <mk-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
- </template>
- </div>
- <div class="_footer">
- <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+ <div class="_content" v-if="serverInfo">
+ <div class="table">
+ <div class="row">
+ <div class="cell"><div class="label">Disk total</div>{{ serverInfo.fs.total | bytes }}</div>
+ <div class="cell"><div class="label">Disk used</div>{{ serverInfo.fs.used | bytes }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
+ <div class="cell"><div class="label">Disk free</div>{{ serverInfo.fs.total - serverInfo.fs.used | bytes }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
+ </div>
+ </div>
</div>
</section>
-
- <section class="_card info">
- <div class="_title"><fa :icon="faInfoCircle"/> {{ $t('instanceInfo') }}</div>
- <div class="_content table" v-if="stats">
- <div><b>{{ $t('users') }}</b><span>{{ stats.originalUsersCount | number }}</span></div>
- <div><b>{{ $t('notes') }}</b><span>{{ stats.originalNotesCount | number }}</span></div>
+ <section class="_card chart">
+ <div class="_title"><fa :icon="faExchangeAlt"/> {{ $t('network') }}</div>
+ <div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
+ <canvas ref="net"></canvas>
</div>
- <div class="_content table">
- <div><b>Misskey</b><span>v{{ version }}</span></div>
- </div>
- <div class="_content table" v-if="serverInfo">
- <div><b>Node.js</b><span>{{ serverInfo.node }}</span></div>
- <div><b>PostgreSQL</b><span>v{{ serverInfo.psql }}</span></div>
- <div><b>Redis</b><span>v{{ serverInfo.redis }}</span></div>
+ <div class="_content" v-if="serverInfo">
+ <div class="table">
+ <div class="row">
+ <div class="cell"><div class="label">Interface</div>{{ serverInfo.net.interface }}</div>
+ </div>
+ </div>
</div>
</section>
</div>
@@ -171,18 +56,19 @@
<script lang="ts">
import Vue from 'vue';
-import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt } from '@fortawesome/free-solid-svg-icons';
-import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons';
-import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
-import MkButton from '../../components/ui/button.vue';
-import MkInput from '../../components/ui/input.vue';
-import MkTextarea from '../../components/ui/textarea.vue';
-import MkSwitch from '../../components/ui/switch.vue';
-import MkInfo from '../../components/ui/info.vue';
-import MkUserSelect from '../../components/user-select.vue';
+import { faServer, faExchangeAlt, faMicrochip, faHdd } from '@fortawesome/free-solid-svg-icons';
+import Chart from 'chart.js';
+import MkInstanceStats from '../../components/instance-stats.vue';
import { version, url } from '../../config';
import i18n from '../../i18n';
-import getAcct from '../../../misc/acct/render';
+
+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})`;
+};
export default Vue.extend({
i18n,
@@ -194,11 +80,7 @@ export default Vue.extend({
},
components: {
- MkButton,
- MkInput,
- MkTextarea,
- MkSwitch,
- MkInfo,
+ MkInstanceStats,
},
data() {
@@ -207,41 +89,11 @@ export default Vue.extend({
url,
stats: null,
serverInfo: null,
- proxyAccount: null,
- proxyAccountId: null,
- cacheRemoteFiles: false,
- proxyRemoteFiles: false,
- localDriveCapacityMb: 0,
- remoteDriveCapacityMb: 0,
- blockedHosts: '',
- pinnedUsers: '',
- maintainerName: null,
- maintainerEmail: null,
- name: null,
- description: null,
- tosUrl: null,
- bannerUrl: null,
- iconUrl: null,
- maxNoteTextLength: 0,
- enableRegistration: false,
- enableLocalTimeline: false,
- enableGlobalTimeline: false,
- enableRecaptcha: false,
- recaptchaSiteKey: null,
- recaptchaSecretKey: null,
- enableServiceWorker: false,
- swPublicKey: null,
- swPrivateKey: null,
- enableTwitterIntegration: false,
- twitterConsumerKey: null,
- twitterConsumerSecret: null,
- enableGithubIntegration: false,
- githubClientId: null,
- githubClientSecret: null,
- enableDiscordIntegration: false,
- discordClientId: null,
- discordClientSecret: null,
- faPencilAlt, faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt
+ connection: null,
+ memUsage: 0,
+ chartCpuMem: null,
+ chartNet: null,
+ faServer, faExchangeAlt, faMicrochip, faHdd
}
},
@@ -251,160 +103,308 @@ export default Vue.extend({
},
},
- created() {
- this.name = this.meta.name;
- this.description = this.meta.description;
- this.tosUrl = this.meta.tosUrl;
- this.bannerUrl = this.meta.bannerUrl;
- this.iconUrl = this.meta.iconUrl;
- this.maintainerName = this.meta.maintainerName;
- this.maintainerEmail = this.meta.maintainerEmail;
- this.maxNoteTextLength = this.meta.maxNoteTextLength;
- this.enableRegistration = !this.meta.disableRegistration;
- this.enableLocalTimeline = !this.meta.disableLocalTimeline;
- this.enableGlobalTimeline = !this.meta.disableGlobalTimeline;
- this.enableRecaptcha = this.meta.enableRecaptcha;
- this.recaptchaSiteKey = this.meta.recaptchaSiteKey;
- this.recaptchaSecretKey = this.meta.recaptchaSecretKey;
- this.proxyAccountId = this.meta.proxyAccountId;
- this.cacheRemoteFiles = this.meta.cacheRemoteFiles;
- this.proxyRemoteFiles = this.meta.proxyRemoteFiles;
- this.localDriveCapacityMb = this.meta.driveCapacityPerLocalUserMb;
- this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb;
- this.blockedHosts = this.meta.blockedHosts.join('\n');
- this.pinnedUsers = this.meta.pinnedUsers.join('\n');
- this.enableServiceWorker = this.meta.enableServiceWorker;
- this.swPublicKey = this.meta.swPublickey;
- this.swPrivateKey = this.meta.swPrivateKey;
- this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
- this.twitterConsumerKey = this.meta.twitterConsumerKey;
- this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
- this.enableGithubIntegration = this.meta.enableGithubIntegration;
- this.githubClientId = this.meta.githubClientId;
- this.githubClientSecret = this.meta.githubClientSecret;
- this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
- this.discordClientId = this.meta.discordClientId;
- this.discordClientSecret = this.meta.discordClientSecret;
+ mounted() {
+ Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
- if (this.proxyAccountId) {
- this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
- this.proxyAccount = proxyAccount;
- });
- }
+ this.chartCpuMem = new Chart(this.$refs.cpumem, {
+ type: 'line',
+ data: {
+ labels: [],
+ datasets: [{
+ label: 'CPU',
+ pointRadius: 0,
+ lineTension: 0,
+ borderWidth: 2,
+ borderColor: '#86b300',
+ backgroundColor: alpha('#86b300', 0.1),
+ data: []
+ }, {
+ label: 'MEM (active)',
+ pointRadius: 0,
+ lineTension: 0,
+ borderWidth: 2,
+ borderColor: '#935dbf',
+ backgroundColor: alpha('#935dbf', 0.02),
+ data: []
+ }, {
+ label: 'MEM (used)',
+ pointRadius: 0,
+ lineTension: 0,
+ borderWidth: 2,
+ borderColor: '#935dbf',
+ borderDash: [5, 5],
+ fill: false,
+ data: []
+ }]
+ },
+ options: {
+ aspectRatio: 3,
+ layout: {
+ padding: {
+ left: 0,
+ right: 0,
+ top: 8,
+ bottom: 0
+ }
+ },
+ legend: {
+ position: 'bottom',
+ labels: {
+ boxWidth: 16,
+ }
+ },
+ scales: {
+ xAxes: [{
+ gridLines: {
+ display: false
+ },
+ ticks: {
+ display: false
+ }
+ }],
+ yAxes: [{
+ position: 'right',
+ ticks: {
+ display: false,
+ max: 100
+ }
+ }]
+ },
+ tooltips: {
+ intersect: false,
+ mode: 'index',
+ }
+ }
+ });
- this.$root.api('admin/server-info').then(res => {
- this.serverInfo = res;
+ this.chartNet = new Chart(this.$refs.net, {
+ type: 'line',
+ data: {
+ labels: [],
+ datasets: [{
+ label: 'In',
+ pointRadius: 0,
+ lineTension: 0,
+ borderWidth: 2,
+ borderColor: '#94a029',
+ backgroundColor: alpha('#94a029', 0.1),
+ data: []
+ }, {
+ label: 'Out',
+ pointRadius: 0,
+ lineTension: 0,
+ borderWidth: 2,
+ borderColor: '#ff9156',
+ backgroundColor: alpha('#ff9156', 0.1),
+ data: []
+ }]
+ },
+ options: {
+ aspectRatio: 3,
+ layout: {
+ padding: {
+ left: 0,
+ right: 0,
+ top: 8,
+ bottom: 0
+ }
+ },
+ legend: {
+ position: 'bottom',
+ labels: {
+ boxWidth: 16,
+ }
+ },
+ scales: {
+ xAxes: [{
+ gridLines: {
+ display: false
+ },
+ ticks: {
+ display: false
+ }
+ }],
+ yAxes: [{
+ position: 'right',
+ ticks: {
+ display: false,
+ }
+ }]
+ },
+ tooltips: {
+ intersect: false,
+ mode: 'index',
+ }
+ }
});
- this.$root.api('stats').then(res => {
- this.stats = res;
+ this.chartDisk = new Chart(this.$refs.disk, {
+ type: 'line',
+ data: {
+ labels: [],
+ datasets: [{
+ label: 'Read',
+ pointRadius: 0,
+ lineTension: 0,
+ borderWidth: 2,
+ borderColor: '#94a029',
+ backgroundColor: alpha('#94a029', 0.1),
+ data: []
+ }, {
+ label: 'Write',
+ pointRadius: 0,
+ lineTension: 0,
+ borderWidth: 2,
+ borderColor: '#ff9156',
+ backgroundColor: alpha('#ff9156', 0.1),
+ data: []
+ }]
+ },
+ options: {
+ aspectRatio: 3,
+ layout: {
+ padding: {
+ left: 0,
+ right: 0,
+ top: 8,
+ bottom: 0
+ }
+ },
+ legend: {
+ position: 'bottom',
+ labels: {
+ boxWidth: 16,
+ }
+ },
+ scales: {
+ xAxes: [{
+ gridLines: {
+ display: false
+ },
+ ticks: {
+ display: false
+ }
+ }],
+ yAxes: [{
+ position: 'right',
+ ticks: {
+ display: false,
+ }
+ }]
+ },
+ tooltips: {
+ intersect: false,
+ mode: 'index',
+ }
+ }
});
- },
- mounted() {
- const renderRecaptchaPreview = () => {
- if (!(window as any).grecaptcha) return;
- if (!this.$refs.recaptcha) return;
- if (!this.recaptchaSiteKey) return;
- (window as any).grecaptcha.render(this.$refs.recaptcha, {
- sitekey: this.recaptchaSiteKey
+ this.$root.api('admin/server-info', {}).then(res => {
+ this.serverInfo = res;
+
+ this.connection = this.$root.stream.useSharedConnection('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
});
- };
- window.onRecaotchaLoad = () => {
- renderRecaptchaPreview();
- };
- const head = document.getElementsByTagName('head')[0];
- const script = document.createElement('script');
- script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaotchaLoad');
- head.appendChild(script);
- this.$watch('enableRecaptcha', () => {
- renderRecaptchaPreview();
- });
- this.$watch('recaptchaSiteKey', () => {
- renderRecaptchaPreview();
});
},
+ beforeDestroy() {
+ this.connection.off('stats', this.onStats);
+ this.connection.off('statsLog', this.onStatsLog);
+ this.connection.dispose();
+ },
+
methods: {
- addPinUser() {
- this.$root.new(MkUserSelect, {}).$once('selected', user => {
- this.pinnedUsers = this.pinnedUsers.trim();
- this.pinnedUsers += '\n@' + getAcct(user);
- this.pinnedUsers = this.pinnedUsers.trim();
- });
- },
+ onStats(stats) {
+ 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;
- chooseProxyAccount() {
- this.$root.new(MkUserSelect, {}).$once('selected', user => {
- this.proxyAccount = user;
- this.proxyAccountId = user.id;
- this.save(true);
- });
+ 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();
},
- save(withDialog = false) {
- this.$root.api('admin/update-meta', {
- name: this.name,
- description: this.description,
- tosUrl: this.tosUrl,
- bannerUrl: this.bannerUrl,
- iconUrl: this.iconUrl,
- maintainerName: this.maintainerName,
- maintainerEmail: this.maintainerEmail,
- maxNoteTextLength: this.maxNoteTextLength,
- disableRegistration: !this.enableRegistration,
- disableLocalTimeline: !this.enableLocalTimeline,
- disableGlobalTimeline: !this.enableGlobalTimeline,
- enableRecaptcha: this.enableRecaptcha,
- recaptchaSiteKey: this.recaptchaSiteKey,
- recaptchaSecretKey: this.recaptchaSecretKey,
- proxyAccountId: this.proxyAccountId,
- cacheRemoteFiles: this.cacheRemoteFiles,
- proxyRemoteFiles: this.proxyRemoteFiles,
- localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
- remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
- blockedHosts: this.blockedHosts.split('\n') || [],
- pinnedUsers: this.pinnedUsers ? this.pinnedUsers.split('\n') : [],
- enableServiceWorker: this.enableServiceWorker,
- swPublicKey: this.swPublicKey,
- swPrivateKey: this.swPrivateKey,
- enableTwitterIntegration: this.enableTwitterIntegration,
- twitterConsumerKey: this.twitterConsumerKey,
- twitterConsumerSecret: this.twitterConsumerSecret,
- enableGithubIntegration: this.enableGithubIntegration,
- githubClientId: this.githubClientId,
- githubClientSecret: this.githubClientSecret,
- enableDiscordIntegration: this.enableDiscordIntegration,
- discordClientId: this.discordClientId,
- discordClientSecret: this.discordClientSecret,
- }).then(() => {
- this.$store.dispatch('instance/fetch');
- if (withDialog) {
- this.$root.dialog({
- type: 'success',
- iconOnly: true, autoClose: true
- });
- }
- }).catch(e => {
- this.$root.dialog({
- type: 'error',
- text: e
- });
- });
+ onStatsLog(statsLog) {
+ for (const stats of statsLog.reverse()) {
+ this.onStats(stats);
+ }
}
}
});
</script>
<style lang="scss" scoped>
-.mk-instance-page {
- > .info {
- > .table {
- > div {
- display: flex;
+.xhexznfu {
+ > .stats {
+ display: flex;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ margin: calc(0px - var(--margin) / 2);
+ margin-bottom: calc(var(--margin) / 2);
+
+ > div {
+ flex: 1 0 213px;
+ margin: calc(var(--margin) / 2);
+ box-sizing: border-box;
+ padding: 16px;
+ }
+ }
+
+ > .chart {
+ > ._content {
+ > .table {
+ > .row {
+ display: flex;
+
+ &:not(:last-child) {
+ margin-bottom: 16px;
+
+ @media (max-width: 500px) {
+ margin-bottom: 8px;
+ }
+ }
+
+ > .cell {
+ flex: 1;
+
+ > .label {
+ font-size: 80%;
+ opacity: 0.7;
- > * {
- flex: 1;
+ > .icon {
+ margin-right: 4px;
+ display: none;
+ }
+ }
+ }
}
}
}
diff --git a/src/client/pages/instance/monitor.vue b/src/client/pages/instance/monitor.vue
deleted file mode 100644
index b75755126b..0000000000
--- a/src/client/pages/instance/monitor.vue
+++ /dev/null
@@ -1,381 +0,0 @@
-<template>
-<div class="mk-instance-monitor">
- <section class="_card">
- <div class="_title"><fa :icon="faMicrochip"/> {{ $t('cpuAndMemory') }}</div>
- <div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
- <canvas ref="cpumem"></canvas>
- </div>
- <div class="_content" v-if="serverInfo">
- <div class="table">
- <div class="row">
- <div class="cell"><div class="label">CPU</div>{{ serverInfo.cpu.model }}</div>
- </div>
- <div class="row">
- <div class="cell"><div class="label">MEM total</div>{{ serverInfo.mem.total | bytes }}</div>
- <div class="cell"><div class="label">MEM used</div>{{ memUsage | bytes }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
- <div class="cell"><div class="label">MEM free</div>{{ serverInfo.mem.total - memUsage | bytes }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
- </div>
- </div>
- </div>
- </section>
- <section class="_card">
- <div class="_title"><fa :icon="faHdd"/> {{ $t('disk') }}</div>
- <div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
- <canvas ref="disk"></canvas>
- </div>
- <div class="_content" v-if="serverInfo">
- <div class="table">
- <div class="row">
- <div class="cell"><div class="label">Disk total</div>{{ serverInfo.fs.total | bytes }}</div>
- <div class="cell"><div class="label">Disk used</div>{{ serverInfo.fs.used | bytes }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
- <div class="cell"><div class="label">Disk free</div>{{ serverInfo.fs.total - serverInfo.fs.used | bytes }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
- </div>
- </div>
- </div>
- </section>
- <section class="_card">
- <div class="_title"><fa :icon="faExchangeAlt"/> {{ $t('network') }}</div>
- <div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
- <canvas ref="net"></canvas>
- </div>
- <div class="_content" v-if="serverInfo">
- <div class="table">
- <div class="row">
- <div class="cell"><div class="label">Interface</div>{{ serverInfo.net.interface }}</div>
- </div>
- </div>
- </div>
- </section>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import { faTachometerAlt, faExchangeAlt, faMicrochip, faHdd } from '@fortawesome/free-solid-svg-icons';
-import Chart from 'chart.js';
-import i18n from '../../i18n';
-
-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})`;
-};
-
-export default Vue.extend({
- i18n,
-
- metaInfo() {
- return {
- title: `${this.$t('monitor')} | ${this.$t('instance')}`
- };
- },
-
- components: {
- },
-
- data() {
- return {
- connection: null,
- serverInfo: null,
- memUsage: 0,
- chartCpuMem: null,
- chartNet: null,
- faTachometerAlt, faExchangeAlt, faMicrochip, faHdd
- }
- },
-
- mounted() {
- Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
-
- this.chartCpuMem = new Chart(this.$refs.cpumem, {
- type: 'line',
- data: {
- labels: [],
- datasets: [{
- label: 'CPU',
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: '#86b300',
- backgroundColor: alpha('#86b300', 0.1),
- data: []
- }, {
- label: 'MEM (active)',
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: '#935dbf',
- backgroundColor: alpha('#935dbf', 0.02),
- data: []
- }, {
- label: 'MEM (used)',
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: '#935dbf',
- borderDash: [5, 5],
- fill: false,
- data: []
- }]
- },
- options: {
- aspectRatio: 3,
- layout: {
- padding: {
- left: 0,
- right: 0,
- top: 8,
- bottom: 0
- }
- },
- legend: {
- position: 'bottom',
- labels: {
- boxWidth: 16,
- }
- },
- scales: {
- xAxes: [{
- gridLines: {
- display: false
- },
- ticks: {
- display: false
- }
- }],
- yAxes: [{
- position: 'right',
- ticks: {
- display: false,
- max: 100
- }
- }]
- },
- tooltips: {
- intersect: false,
- mode: 'index',
- }
- }
- });
-
- this.chartNet = new Chart(this.$refs.net, {
- type: 'line',
- data: {
- labels: [],
- datasets: [{
- label: 'In',
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: '#94a029',
- backgroundColor: alpha('#94a029', 0.1),
- data: []
- }, {
- label: 'Out',
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: '#ff9156',
- backgroundColor: alpha('#ff9156', 0.1),
- data: []
- }]
- },
- options: {
- aspectRatio: 3,
- layout: {
- padding: {
- left: 0,
- right: 0,
- top: 8,
- bottom: 0
- }
- },
- legend: {
- position: 'bottom',
- labels: {
- boxWidth: 16,
- }
- },
- scales: {
- xAxes: [{
- gridLines: {
- display: false
- },
- ticks: {
- display: false
- }
- }],
- yAxes: [{
- position: 'right',
- ticks: {
- display: false,
- }
- }]
- },
- tooltips: {
- intersect: false,
- mode: 'index',
- }
- }
- });
-
- this.chartDisk = new Chart(this.$refs.disk, {
- type: 'line',
- data: {
- labels: [],
- datasets: [{
- label: 'Read',
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: '#94a029',
- backgroundColor: alpha('#94a029', 0.1),
- data: []
- }, {
- label: 'Write',
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: '#ff9156',
- backgroundColor: alpha('#ff9156', 0.1),
- data: []
- }]
- },
- options: {
- aspectRatio: 3,
- layout: {
- padding: {
- left: 0,
- right: 0,
- top: 8,
- bottom: 0
- }
- },
- legend: {
- position: 'bottom',
- labels: {
- boxWidth: 16,
- }
- },
- scales: {
- xAxes: [{
- gridLines: {
- display: false
- },
- ticks: {
- display: false
- }
- }],
- yAxes: [{
- position: 'right',
- ticks: {
- display: false,
- }
- }]
- },
- tooltips: {
- intersect: false,
- mode: 'index',
- }
- }
- });
-
- this.$root.api('admin/server-info', {}).then(res => {
- this.serverInfo = res;
-
- this.connection = this.$root.stream.useSharedConnection('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
- });
- });
- },
-
- beforeDestroy() {
- this.connection.off('stats', this.onStats);
- this.connection.off('statsLog', this.onStatsLog);
- this.connection.dispose();
- },
-
- methods: {
- onStats(stats) {
- 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);
- }
- }
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-instance-monitor {
- > section {
- > ._content {
- > .table {
- > .row {
- display: flex;
-
- &:not(:last-child) {
- margin-bottom: 16px;
-
- @media (max-width: 500px) {
- margin-bottom: 8px;
- }
- }
-
- > .cell {
- flex: 1;
-
- > .label {
- font-size: 80%;
- opacity: 0.7;
-
- > .icon {
- margin-right: 4px;
- display: none;
- }
- }
- }
- }
- }
- }
- }
-}
-</style>
diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue
new file mode 100644
index 0000000000..914565298a
--- /dev/null
+++ b/src/client/pages/instance/settings.vue
@@ -0,0 +1,413 @@
+<template>
+<div v-if="meta" class="yihovjtf">
+ <portal to="icon"><fa :icon="faCog"/></portal>
+ <portal to="title">{{ $t('settings') }}</portal>
+
+ <section class="_card info">
+ <div class="_title"><fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div>
+ <div class="_content">
+ <mk-input v-model="name">{{ $t('instanceName') }}</mk-input>
+ <mk-textarea v-model="description">{{ $t('instanceDescription') }}</mk-textarea>
+ <mk-input v-model="iconUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('iconUrl') }}</mk-input>
+ <mk-input v-model="bannerUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('bannerUrl') }}</mk-input>
+ <mk-input v-model="tosUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('tosUrl') }}</mk-input>
+ <mk-input v-model="maintainerName">{{ $t('maintainerName') }}</mk-input>
+ <mk-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="faEnvelope"/></template>{{ $t('maintainerEmail') }}</mk-input>
+ </div>
+ <div class="_footer">
+ <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+ </div>
+ </section>
+
+ <section class="_card info">
+ <div class="_content">
+ <mk-input v-model="maxNoteTextLength" type="number" :save="() => save()" style="margin:0;"><template #icon><fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</mk-input>
+ </div>
+ <div class="_content">
+ <mk-switch v-model="enableLocalTimeline" @change="save()">{{ $t('enableLocalTimeline') }}</mk-switch>
+ <mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch>
+ <mk-info>{{ $t('disablingTimelinesInfo') }}</mk-info>
+ </div>
+ </section>
+
+ <section class="_card info">
+ <div class="_title"><fa :icon="faUser"/> {{ $t('registration') }}</div>
+ <div class="_content">
+ <mk-switch v-model="enableRegistration" @change="save()">{{ $t('enableRegistration') }}</mk-switch>
+ <mk-button v-if="!enableRegistration" @click="invite">{{ $t('invite') }}</mk-button>
+ </div>
+ </section>
+
+ <section class="_card">
+ <div class="_title"><fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div>
+ <div class="_content">
+ <mk-switch v-model="enableRecaptcha">{{ $t('enableRecaptcha') }}</mk-switch>
+ <template v-if="enableRecaptcha">
+ <mk-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSiteKey') }}</mk-input>
+ <mk-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSecretKey') }}</mk-input>
+ </template>
+ </div>
+ <div class="_content" v-if="enableRecaptcha && recaptchaSiteKey">
+ <header>{{ $t('preview') }}</header>
+ <div ref="recaptcha" style="margin: 16px 0 0 0;" :key="recaptchaSiteKey"></div>
+ </div>
+ <div class="_footer">
+ <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+ </div>
+ </section>
+
+ <section class="_card">
+ <div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
+ <div class="_content">
+ <mk-switch v-model="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></mk-switch>
+ <template v-if="enableServiceWorker">
+ <mk-horizon-group inputs class="fit-bottom">
+ <mk-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Public key</mk-input>
+ <mk-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Private key</mk-input>
+ </mk-horizon-group>
+ </template>
+ </div>
+ <div class="_footer">
+ <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+ </div>
+ </section>
+
+ <section class="_card">
+ <div class="_title"><fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div>
+ <div class="_content">
+ <mk-textarea v-model="pinnedUsers">
+ <template #desc>{{ $t('pinnedUsersDescription') }} <button class="_textButton" @click="addPinUser">{{ $t('addUser') }}</button></template>
+ </mk-textarea>
+ </div>
+ <div class="_footer">
+ <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+ </div>
+ </section>
+
+ <section class="_card">
+ <div class="_title"><fa :icon="faCloud"/> {{ $t('files') }}</div>
+ <div class="_content">
+ <mk-switch v-model="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></mk-switch>
+ <mk-switch v-model="proxyRemoteFiles">{{ $t('proxyRemoteFiles') }}<template #desc>{{ $t('proxyRemoteFilesDescription') }}</template></mk-switch>
+ <mk-input v-model="localDriveCapacityMb" type="number">{{ $t('driveCapacityPerLocalAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
+ <mk-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" style="margin-bottom: 0;">{{ $t('driveCapacityPerRemoteAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
+ </div>
+ <div class="_footer">
+ <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+ </div>
+ </section>
+
+ <section class="_card">
+ <div class="_title"><fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
+ <div class="_content">
+ <mk-input :value="proxyAccount ? proxyAccount.username : null" style="margin: 0;" disabled><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></mk-input>
+ <mk-button primary @click="chooseProxyAccount">{{ $t('chooseProxyAccount') }}</mk-button>
+ </div>
+ </section>
+
+ <section class="_card">
+ <div class="_title"><fa :icon="faBan"/> {{ $t('blockedInstances') }}</div>
+ <div class="_content">
+ <mk-textarea v-model="blockedHosts">
+ <template #desc>{{ $t('blockedInstancesDescription') }}</template>
+ </mk-textarea>
+ </div>
+ <div class="_footer">
+ <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+ </div>
+ </section>
+
+ <section class="_card">
+ <div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
+ <div class="_content">
+ <header><fa :icon="faTwitter"/> Twitter</header>
+ <mk-switch v-model="enableTwitterIntegration">{{ $t('enable') }}</mk-switch>
+ <template v-if="enableTwitterIntegration">
+ <mk-info>Callback URL: {{ `${url}/api/tw/cb` }}</mk-info>
+ <mk-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Key</mk-input>
+ <mk-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Secret</mk-input>
+ </template>
+ </div>
+ <div class="_content">
+ <header><fa :icon="faGithub"/> GitHub</header>
+ <mk-switch v-model="enableGithubIntegration">{{ $t('enable') }}</mk-switch>
+ <template v-if="enableGithubIntegration">
+ <mk-info>Callback URL: {{ `${url}/api/gh/cb` }}</mk-info>
+ <mk-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
+ <mk-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
+ </template>
+ </div>
+ <div class="_content">
+ <header><fa :icon="faDiscord"/> Discord</header>
+ <mk-switch v-model="enableDiscordIntegration">{{ $t('enable') }}</mk-switch>
+ <template v-if="enableDiscordIntegration">
+ <mk-info>Callback URL: {{ `${url}/api/dc/cb` }}</mk-info>
+ <mk-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
+ <mk-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
+ </template>
+ </div>
+ <div class="_footer">
+ <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+ </div>
+ </section>
+
+ <section class="_card info">
+ <div class="_title"><fa :icon="faInfoCircle"/> {{ $t('instanceInfo') }}</div>
+ <div class="_content table" v-if="stats">
+ <div><b>{{ $t('users') }}</b><span>{{ stats.originalUsersCount | number }}</span></div>
+ <div><b>{{ $t('notes') }}</b><span>{{ stats.originalNotesCount | number }}</span></div>
+ </div>
+ <div class="_content table">
+ <div><b>Misskey</b><span>v{{ version }}</span></div>
+ </div>
+ <div class="_content table" v-if="serverInfo">
+ <div><b>Node.js</b><span>{{ serverInfo.node }}</span></div>
+ <div><b>PostgreSQL</b><span>v{{ serverInfo.psql }}</span></div>
+ <div><b>Redis</b><span>v{{ serverInfo.redis }}</span></div>
+ </div>
+ </section>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt } from '@fortawesome/free-solid-svg-icons';
+import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons';
+import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
+import MkButton from '../../components/ui/button.vue';
+import MkInput from '../../components/ui/input.vue';
+import MkTextarea from '../../components/ui/textarea.vue';
+import MkSwitch from '../../components/ui/switch.vue';
+import MkInfo from '../../components/ui/info.vue';
+import MkUserSelect from '../../components/user-select.vue';
+import { version, url } from '../../config';
+import i18n from '../../i18n';
+import getAcct from '../../../misc/acct/render';
+
+export default Vue.extend({
+ i18n,
+
+ metaInfo() {
+ return {
+ title: this.$t('instance') as string
+ };
+ },
+
+ components: {
+ MkButton,
+ MkInput,
+ MkTextarea,
+ MkSwitch,
+ MkInfo,
+ },
+
+ data() {
+ return {
+ version,
+ url,
+ stats: null,
+ serverInfo: null,
+ proxyAccount: null,
+ proxyAccountId: null,
+ cacheRemoteFiles: false,
+ proxyRemoteFiles: false,
+ localDriveCapacityMb: 0,
+ remoteDriveCapacityMb: 0,
+ blockedHosts: '',
+ pinnedUsers: '',
+ maintainerName: null,
+ maintainerEmail: null,
+ name: null,
+ description: null,
+ tosUrl: null,
+ bannerUrl: null,
+ iconUrl: null,
+ maxNoteTextLength: 0,
+ enableRegistration: false,
+ enableLocalTimeline: false,
+ enableGlobalTimeline: false,
+ enableRecaptcha: false,
+ recaptchaSiteKey: null,
+ recaptchaSecretKey: null,
+ enableServiceWorker: false,
+ swPublicKey: null,
+ swPrivateKey: null,
+ enableTwitterIntegration: false,
+ twitterConsumerKey: null,
+ twitterConsumerSecret: null,
+ enableGithubIntegration: false,
+ githubClientId: null,
+ githubClientSecret: null,
+ enableDiscordIntegration: false,
+ discordClientId: null,
+ discordClientSecret: null,
+ faPencilAlt, faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt
+ }
+ },
+
+ computed: {
+ meta() {
+ return this.$store.state.instance.meta;
+ },
+ },
+
+ created() {
+ this.name = this.meta.name;
+ this.description = this.meta.description;
+ this.tosUrl = this.meta.tosUrl;
+ this.bannerUrl = this.meta.bannerUrl;
+ this.iconUrl = this.meta.iconUrl;
+ this.maintainerName = this.meta.maintainerName;
+ this.maintainerEmail = this.meta.maintainerEmail;
+ this.maxNoteTextLength = this.meta.maxNoteTextLength;
+ this.enableRegistration = !this.meta.disableRegistration;
+ this.enableLocalTimeline = !this.meta.disableLocalTimeline;
+ this.enableGlobalTimeline = !this.meta.disableGlobalTimeline;
+ this.enableRecaptcha = this.meta.enableRecaptcha;
+ this.recaptchaSiteKey = this.meta.recaptchaSiteKey;
+ this.recaptchaSecretKey = this.meta.recaptchaSecretKey;
+ this.proxyAccountId = this.meta.proxyAccountId;
+ this.cacheRemoteFiles = this.meta.cacheRemoteFiles;
+ this.proxyRemoteFiles = this.meta.proxyRemoteFiles;
+ this.localDriveCapacityMb = this.meta.driveCapacityPerLocalUserMb;
+ this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb;
+ this.blockedHosts = this.meta.blockedHosts.join('\n');
+ this.pinnedUsers = this.meta.pinnedUsers.join('\n');
+ this.enableServiceWorker = this.meta.enableServiceWorker;
+ this.swPublicKey = this.meta.swPublickey;
+ this.swPrivateKey = this.meta.swPrivateKey;
+ this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
+ this.twitterConsumerKey = this.meta.twitterConsumerKey;
+ this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
+ this.enableGithubIntegration = this.meta.enableGithubIntegration;
+ this.githubClientId = this.meta.githubClientId;
+ this.githubClientSecret = this.meta.githubClientSecret;
+ this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
+ this.discordClientId = this.meta.discordClientId;
+ this.discordClientSecret = this.meta.discordClientSecret;
+
+ if (this.proxyAccountId) {
+ this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
+ this.proxyAccount = proxyAccount;
+ });
+ }
+
+ this.$root.api('admin/server-info').then(res => {
+ this.serverInfo = res;
+ });
+
+ this.$root.api('stats').then(res => {
+ this.stats = res;
+ });
+ },
+
+ mounted() {
+ const renderRecaptchaPreview = () => {
+ if (!(window as any).grecaptcha) return;
+ if (!this.$refs.recaptcha) return;
+ if (!this.recaptchaSiteKey) return;
+ (window as any).grecaptcha.render(this.$refs.recaptcha, {
+ sitekey: this.recaptchaSiteKey
+ });
+ };
+ window.onRecaotchaLoad = () => {
+ renderRecaptchaPreview();
+ };
+ const head = document.getElementsByTagName('head')[0];
+ const script = document.createElement('script');
+ script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaotchaLoad');
+ head.appendChild(script);
+ this.$watch('enableRecaptcha', () => {
+ renderRecaptchaPreview();
+ });
+ this.$watch('recaptchaSiteKey', () => {
+ renderRecaptchaPreview();
+ });
+ },
+
+ methods: {
+ addPinUser() {
+ this.$root.new(MkUserSelect, {}).$once('selected', user => {
+ this.pinnedUsers = this.pinnedUsers.trim();
+ this.pinnedUsers += '\n@' + getAcct(user);
+ this.pinnedUsers = this.pinnedUsers.trim();
+ });
+ },
+
+ chooseProxyAccount() {
+ this.$root.new(MkUserSelect, {}).$once('selected', user => {
+ this.proxyAccount = user;
+ this.proxyAccountId = user.id;
+ this.save(true);
+ });
+ },
+
+ save(withDialog = false) {
+ this.$root.api('admin/update-meta', {
+ name: this.name,
+ description: this.description,
+ tosUrl: this.tosUrl,
+ bannerUrl: this.bannerUrl,
+ iconUrl: this.iconUrl,
+ maintainerName: this.maintainerName,
+ maintainerEmail: this.maintainerEmail,
+ maxNoteTextLength: this.maxNoteTextLength,
+ disableRegistration: !this.enableRegistration,
+ disableLocalTimeline: !this.enableLocalTimeline,
+ disableGlobalTimeline: !this.enableGlobalTimeline,
+ enableRecaptcha: this.enableRecaptcha,
+ recaptchaSiteKey: this.recaptchaSiteKey,
+ recaptchaSecretKey: this.recaptchaSecretKey,
+ proxyAccountId: this.proxyAccountId,
+ cacheRemoteFiles: this.cacheRemoteFiles,
+ proxyRemoteFiles: this.proxyRemoteFiles,
+ localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
+ remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
+ blockedHosts: this.blockedHosts.split('\n') || [],
+ pinnedUsers: this.pinnedUsers ? this.pinnedUsers.split('\n') : [],
+ enableServiceWorker: this.enableServiceWorker,
+ swPublicKey: this.swPublicKey,
+ swPrivateKey: this.swPrivateKey,
+ enableTwitterIntegration: this.enableTwitterIntegration,
+ twitterConsumerKey: this.twitterConsumerKey,
+ twitterConsumerSecret: this.twitterConsumerSecret,
+ enableGithubIntegration: this.enableGithubIntegration,
+ githubClientId: this.githubClientId,
+ githubClientSecret: this.githubClientSecret,
+ enableDiscordIntegration: this.enableDiscordIntegration,
+ discordClientId: this.discordClientId,
+ discordClientSecret: this.discordClientSecret,
+ }).then(() => {
+ this.$store.dispatch('instance/fetch');
+ if (withDialog) {
+ this.$root.dialog({
+ type: 'success',
+ iconOnly: true, autoClose: true
+ });
+ }
+ }).catch(e => {
+ this.$root.dialog({
+ type: 'error',
+ text: e
+ });
+ });
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.yihovjtf {
+ > .info {
+ > .table {
+ > div {
+ display: flex;
+
+ > * {
+ flex: 1;
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/router.ts b/src/client/router.ts
index a226cc9734..fe3de70a05 100644
--- a/src/client/router.ts
+++ b/src/client/router.ts
@@ -49,9 +49,8 @@ export const router = new VueRouter({
{ path: '/instance/emojis', component: page('instance/emojis') },
{ path: '/instance/users', component: page('instance/users') },
{ path: '/instance/files', component: page('instance/files') },
- { path: '/instance/monitor', component: page('instance/monitor') },
{ path: '/instance/queue', component: page('instance/queue') },
- { path: '/instance/stats', component: page('instance/stats') },
+ { path: '/instance/settings', component: page('instance/settings') },
{ path: '/instance/federation', component: page('instance/federation') },
{ path: '/instance/announcements', component: page('instance/announcements') },
{ path: '/notes/:note', name: 'note', component: page('note') },