Merge branch 'develop' into pizzax-indexeddb
This commit is contained in:
		
						commit
						ac93a1808b
					
				
					 11 changed files with 192 additions and 168 deletions
				
			
		| 
						 | 
					@ -29,6 +29,7 @@ You should also include the user name that made the change.
 | 
				
			||||||
=======
 | 
					=======
 | 
				
			||||||
- Server: Add rate limit to i/notifications @tamaina
 | 
					- Server: Add rate limit to i/notifications @tamaina
 | 
				
			||||||
- Client: Improve files page of control panel @syuilo
 | 
					- Client: Improve files page of control panel @syuilo
 | 
				
			||||||
 | 
					- Client: Show warning in control panel when there is an unresolved abuse report @syuilo
 | 
				
			||||||
- Improve player detection in URL preview @mei23
 | 
					- Improve player detection in URL preview @mei23
 | 
				
			||||||
- Add Badge Image to Push Notification #8012 @tamaina
 | 
					- Add Badge Image to Push Notification #8012 @tamaina
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -850,6 +850,12 @@ rateLimitExceeded: "レート制限を超えました"
 | 
				
			||||||
cropImage: "画像のクロップ"
 | 
					cropImage: "画像のクロップ"
 | 
				
			||||||
cropImageAsk: "画像をクロップしますか?"
 | 
					cropImageAsk: "画像をクロップしますか?"
 | 
				
			||||||
file: "ファイル"
 | 
					file: "ファイル"
 | 
				
			||||||
 | 
					recentNHours: "直近{n}時間"
 | 
				
			||||||
 | 
					recentNDays: "直近{n}日"
 | 
				
			||||||
 | 
					noEmailServerWarning: "メールサーバーの設定がされていません。"
 | 
				
			||||||
 | 
					thereIsUnresolvedAbuseReportWarning: "未対応の通報があります。"
 | 
				
			||||||
 | 
					recommended: "推奨"
 | 
				
			||||||
 | 
					check: "チェック"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
_emailUnavailable:
 | 
					_emailUnavailable:
 | 
				
			||||||
  used: "既に使用されています"
 | 
					  used: "既に使用されています"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,13 @@
 | 
				
			||||||
import { db } from '@/db/postgre.js';
 | 
					import { db } from '@/db/postgre.js';
 | 
				
			||||||
import { Instance } from '@/models/entities/instance.js';
 | 
					import { Instance } from '@/models/entities/instance.js';
 | 
				
			||||||
import { Packed } from '@/misc/schema.js';
 | 
					import { Packed } from '@/misc/schema.js';
 | 
				
			||||||
 | 
					import { fetchMeta } from '@/misc/fetch-meta.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const InstanceRepository = db.getRepository(Instance).extend({
 | 
					export const InstanceRepository = db.getRepository(Instance).extend({
 | 
				
			||||||
	async pack(
 | 
						async pack(
 | 
				
			||||||
		instance: Instance,
 | 
							instance: Instance,
 | 
				
			||||||
	): Promise<Packed<'FederationInstance'>> {
 | 
						): Promise<Packed<'FederationInstance'>> {
 | 
				
			||||||
 | 
							const meta = await fetchMeta();
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			id: instance.id,
 | 
								id: instance.id,
 | 
				
			||||||
			caughtAt: instance.caughtAt.toISOString(),
 | 
								caughtAt: instance.caughtAt.toISOString(),
 | 
				
			||||||
| 
						 | 
					@ -18,6 +20,7 @@ export const InstanceRepository = db.getRepository(Instance).extend({
 | 
				
			||||||
			lastCommunicatedAt: instance.lastCommunicatedAt.toISOString(),
 | 
								lastCommunicatedAt: instance.lastCommunicatedAt.toISOString(),
 | 
				
			||||||
			isNotResponding: instance.isNotResponding,
 | 
								isNotResponding: instance.isNotResponding,
 | 
				
			||||||
			isSuspended: instance.isSuspended,
 | 
								isSuspended: instance.isSuspended,
 | 
				
			||||||
 | 
								isBlocked: meta.blockedHosts.includes(instance.host),
 | 
				
			||||||
			softwareName: instance.softwareName,
 | 
								softwareName: instance.softwareName,
 | 
				
			||||||
			softwareVersion: instance.softwareVersion,
 | 
								softwareVersion: instance.softwareVersion,
 | 
				
			||||||
			openRegistrations: instance.openRegistrations,
 | 
								openRegistrations: instance.openRegistrations,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -52,6 +52,10 @@ export const packedFederationInstanceSchema = {
 | 
				
			||||||
			type: 'boolean',
 | 
								type: 'boolean',
 | 
				
			||||||
			optional: false, nullable: false,
 | 
								optional: false, nullable: false,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							isBlocked: {
 | 
				
			||||||
 | 
								type: 'boolean',
 | 
				
			||||||
 | 
								optional: false, nullable: false,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		softwareName: {
 | 
							softwareName: {
 | 
				
			||||||
			type: 'string',
 | 
								type: 'string',
 | 
				
			||||||
			optional: false, nullable: true,
 | 
								optional: false, nullable: true,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -79,7 +79,8 @@ function resolve() {
 | 
				
			||||||
			align-items: center;
 | 
								align-items: center;
 | 
				
			||||||
			padding: 14px;
 | 
								padding: 14px;
 | 
				
			||||||
			border-radius: 8px;
 | 
								border-radius: 8px;
 | 
				
			||||||
			background-image: linear-gradient(45deg, rgb(255 196 0 / 15%) 16.67%, transparent 16.67%, transparent 50%, rgb(255 196 0 / 15%) 50%, rgb(255 196 0 / 15%) 66.67%, transparent 66.67%, transparent 100%);
 | 
								--c: rgb(255 196 0 / 15%);
 | 
				
			||||||
 | 
								background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
 | 
				
			||||||
			background-size: 16px 16px;
 | 
								background-size: 16px 16px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			> .avatar {
 | 
								> .avatar {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										96
									
								
								packages/client/src/components/instance-info.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								packages/client/src/components/instance-info.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,96 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<div :class="[$style.root, { yellow: instance.isNotResponding, red: instance.isBlocked, gray: instance.isSuspended }]">
 | 
				
			||||||
 | 
						<img v-if="instance.iconUrl" class="icon" :src="instance.iconUrl" alt=""/>
 | 
				
			||||||
 | 
						<div class="body">
 | 
				
			||||||
 | 
							<span class="host">{{ instance.host }}</span>
 | 
				
			||||||
 | 
							<span class="sub">{{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</span>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<MkMiniChart v-if="chart" class="chart" :src="chart.requests.received"/>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import * as misskey from 'misskey-js';
 | 
				
			||||||
 | 
					import MkMiniChart from '@/components/mini-chart.vue';
 | 
				
			||||||
 | 
					import * as os from '@/os';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
						instance: misskey.entities.Instance;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const chart = $ref(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					os.api('charts/instance', { host: props.instance.host, limit: 16, span: 'hour' }).then(res => {
 | 
				
			||||||
 | 
						chart = res;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss" module>
 | 
				
			||||||
 | 
					.root {
 | 
				
			||||||
 | 
						$bodyTitleHieght: 18px;
 | 
				
			||||||
 | 
						$bodyInfoHieght: 16px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						display: flex;
 | 
				
			||||||
 | 
						align-items: center;
 | 
				
			||||||
 | 
						padding: 16px;
 | 
				
			||||||
 | 
						background: var(--panel);
 | 
				
			||||||
 | 
						border-radius: 8px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> :global(.icon) {
 | 
				
			||||||
 | 
							display: block;
 | 
				
			||||||
 | 
							width: ($bodyTitleHieght + $bodyInfoHieght);
 | 
				
			||||||
 | 
							height: ($bodyTitleHieght + $bodyInfoHieght);
 | 
				
			||||||
 | 
							object-fit: cover;
 | 
				
			||||||
 | 
							border-radius: 4px;
 | 
				
			||||||
 | 
							margin-right: 8px;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> :global(.body) {
 | 
				
			||||||
 | 
							flex: 1;
 | 
				
			||||||
 | 
							overflow: hidden;
 | 
				
			||||||
 | 
							font-size: 0.9em;
 | 
				
			||||||
 | 
							color: var(--fg);
 | 
				
			||||||
 | 
							padding-right: 8px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> :global(.host) {
 | 
				
			||||||
 | 
								display: block;
 | 
				
			||||||
 | 
								width: 100%;
 | 
				
			||||||
 | 
								white-space: nowrap;
 | 
				
			||||||
 | 
								overflow: hidden;
 | 
				
			||||||
 | 
								text-overflow: ellipsis;
 | 
				
			||||||
 | 
								line-height: $bodyTitleHieght;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> :global(.sub) {
 | 
				
			||||||
 | 
								font-size: 75%;
 | 
				
			||||||
 | 
								opacity: 0.7;
 | 
				
			||||||
 | 
								line-height: $bodyInfoHieght;
 | 
				
			||||||
 | 
								white-space: nowrap;
 | 
				
			||||||
 | 
								overflow: hidden;
 | 
				
			||||||
 | 
								text-overflow: ellipsis;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> :global(.chart) {
 | 
				
			||||||
 | 
							height: 30px;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&:global(.yellow) {
 | 
				
			||||||
 | 
							--c: rgb(255 196 0 / 15%);
 | 
				
			||||||
 | 
							background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
 | 
				
			||||||
 | 
							background-size: 16px 16px;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&:global(.red) {
 | 
				
			||||||
 | 
							--c: rgb(255 0 0 / 15%);
 | 
				
			||||||
 | 
							background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
 | 
				
			||||||
 | 
							background-size: 16px 16px;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&:global(.gray) {
 | 
				
			||||||
 | 
							--c: var(--bg);
 | 
				
			||||||
 | 
							background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
 | 
				
			||||||
 | 
							background-size: 16px 16px;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@
 | 
				
			||||||
		<FormSuspense :p="init">
 | 
							<FormSuspense :p="init">
 | 
				
			||||||
			<div class="_formRoot">
 | 
								<div class="_formRoot">
 | 
				
			||||||
				<FormSwitch v-model="enableEmail" class="_formBlock">
 | 
									<FormSwitch v-model="enableEmail" class="_formBlock">
 | 
				
			||||||
					<template #label>{{ i18n.ts.enableEmail }}</template>
 | 
										<template #label>{{ i18n.ts.enableEmail }} ({{ i18n.ts.recommended }})</template>
 | 
				
			||||||
					<template #caption>{{ i18n.ts.emailConfigInfo }}</template>
 | 
										<template #caption>{{ i18n.ts.emailConfigInfo }}</template>
 | 
				
			||||||
				</FormSwitch>
 | 
									</FormSwitch>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,8 +7,10 @@
 | 
				
			||||||
					<img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
 | 
										<img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<MkInfo v-if="thereIsUnresolvedAbuseReport" warn class="info">{{ $ts.thereIsUnresolvedAbuseReportWarning }} <MkA to="/admin/abuses" class="_link">{{ $ts.check }}</MkA></MkInfo>
 | 
				
			||||||
				<MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
 | 
									<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/security" class="_link">{{ $ts.configure }}</MkA></MkInfo>
 | 
									<MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ $ts.configure }}</MkA></MkInfo>
 | 
				
			||||||
 | 
									<MkInfo v-if="noEmailServer" warn class="info">{{ $ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				<MkSuperMenu :def="menuDef" :grid="initialPage == null"></MkSuperMenu>
 | 
									<MkSuperMenu :def="menuDef" :grid="initialPage == null"></MkSuperMenu>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
| 
						 | 
					@ -58,6 +60,15 @@ let el = $ref(null);
 | 
				
			||||||
let pageProps = $ref({});
 | 
					let pageProps = $ref({});
 | 
				
			||||||
let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
 | 
					let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
 | 
				
			||||||
let noBotProtection = !instance.enableHcaptcha && !instance.enableRecaptcha;
 | 
					let noBotProtection = !instance.enableHcaptcha && !instance.enableRecaptcha;
 | 
				
			||||||
 | 
					let noEmailServer = !instance.enableEmail;
 | 
				
			||||||
 | 
					let thereIsUnresolvedAbuseReport = $ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					os.api('admin/abuse-user-reports', {
 | 
				
			||||||
 | 
						state: 'unresolved',
 | 
				
			||||||
 | 
						limit: 1,
 | 
				
			||||||
 | 
					}).then(reports => {
 | 
				
			||||||
 | 
						if (reports.length > 0) thereIsUnresolvedAbuseReport = true;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const NARROW_THRESHOLD = 600;
 | 
					const NARROW_THRESHOLD = 600;
 | 
				
			||||||
const ro = new ResizeObserver((entries, observer) => {
 | 
					const ro = new ResizeObserver((entries, observer) => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,51 +41,8 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
 | 
								<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
 | 
				
			||||||
				<div class="dqokceoi">
 | 
									<div class="dqokceoi">
 | 
				
			||||||
					<MkA v-for="instance in items" :key="instance.id" class="instance" :to="`/instance-info/${instance.host}`">
 | 
										<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Last communicated: ${new Date(instance.lastCommunicatedAt).toLocaleString()}\nStatus: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`" :behavior="'window'">
 | 
				
			||||||
						<div class="host"><img :src="instance.faviconUrl">{{ instance.host }}</div>
 | 
											<MkInstanceInfo :instance="instance"/>
 | 
				
			||||||
						<div class="table">
 | 
					 | 
				
			||||||
							<div class="cell">
 | 
					 | 
				
			||||||
								<div class="key">{{ $ts.registeredAt }}</div>
 | 
					 | 
				
			||||||
								<div class="value"><MkTime :time="instance.caughtAt"/></div>
 | 
					 | 
				
			||||||
							</div>
 | 
					 | 
				
			||||||
							<div class="cell">
 | 
					 | 
				
			||||||
								<div class="key">{{ $ts.software }}</div>
 | 
					 | 
				
			||||||
								<div class="value">{{ instance.softwareName || `(${$ts.unknown})` }}</div>
 | 
					 | 
				
			||||||
							</div>
 | 
					 | 
				
			||||||
							<div class="cell">
 | 
					 | 
				
			||||||
								<div class="key">{{ $ts.version }}</div>
 | 
					 | 
				
			||||||
								<div class="value">{{ instance.softwareVersion || `(${$ts.unknown})` }}</div>
 | 
					 | 
				
			||||||
							</div>
 | 
					 | 
				
			||||||
							<div class="cell">
 | 
					 | 
				
			||||||
								<div class="key">{{ $ts.users }}</div>
 | 
					 | 
				
			||||||
								<div class="value">{{ instance.usersCount }}</div>
 | 
					 | 
				
			||||||
							</div>
 | 
					 | 
				
			||||||
							<div class="cell">
 | 
					 | 
				
			||||||
								<div class="key">{{ $ts.notes }}</div>
 | 
					 | 
				
			||||||
								<div class="value">{{ instance.notesCount }}</div>
 | 
					 | 
				
			||||||
							</div>
 | 
					 | 
				
			||||||
							<div class="cell">
 | 
					 | 
				
			||||||
								<div class="key">{{ $ts.sent }}</div>
 | 
					 | 
				
			||||||
								<div class="value"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
 | 
					 | 
				
			||||||
							</div>
 | 
					 | 
				
			||||||
							<div class="cell">
 | 
					 | 
				
			||||||
								<div class="key">{{ $ts.received }}</div>
 | 
					 | 
				
			||||||
								<div class="value"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
 | 
					 | 
				
			||||||
							</div>
 | 
					 | 
				
			||||||
						</div>
 | 
					 | 
				
			||||||
						<div class="footer">
 | 
					 | 
				
			||||||
							<span class="status" :class="getStatus(instance)">{{ getStatus(instance) }}</span>
 | 
					 | 
				
			||||||
							<span class="pubSub">
 | 
					 | 
				
			||||||
								<span v-if="instance.followersCount > 0" class="sub"><i class="fas fa-caret-down icon"></i>Sub</span>
 | 
					 | 
				
			||||||
								<span v-else class="sub"><i class="fas fa-caret-down icon"></i>-</span>
 | 
					 | 
				
			||||||
								<span v-if="instance.followingCount > 0" class="pub"><i class="fas fa-caret-up icon"></i>Pub</span>
 | 
					 | 
				
			||||||
								<span v-else class="pub"><i class="fas fa-caret-up icon"></i>-</span>
 | 
					 | 
				
			||||||
							</span>
 | 
					 | 
				
			||||||
							<span class="right">
 | 
					 | 
				
			||||||
								<span class="latestStatus">{{ instance.latestStatus || '-' }}</span>
 | 
					 | 
				
			||||||
								<span class="lastCommunicatedAt"><MkTime :time="instance.lastCommunicatedAt"/></span>
 | 
					 | 
				
			||||||
							</span>
 | 
					 | 
				
			||||||
						</div>
 | 
					 | 
				
			||||||
					</MkA>
 | 
										</MkA>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</MkPagination>
 | 
								</MkPagination>
 | 
				
			||||||
| 
						 | 
					@ -100,6 +57,7 @@ import MkButton from '@/components/ui/button.vue';
 | 
				
			||||||
import MkInput from '@/components/form/input.vue';
 | 
					import MkInput from '@/components/form/input.vue';
 | 
				
			||||||
import MkSelect from '@/components/form/select.vue';
 | 
					import MkSelect from '@/components/form/select.vue';
 | 
				
			||||||
import MkPagination from '@/components/ui/pagination.vue';
 | 
					import MkPagination from '@/components/ui/pagination.vue';
 | 
				
			||||||
 | 
					import MkInstanceInfo from '@/components/instance-info.vue';
 | 
				
			||||||
import FormSplit from '@/components/form/split.vue';
 | 
					import FormSplit from '@/components/form/split.vue';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
import { i18n } from '@/i18n';
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
| 
						 | 
					@ -127,9 +85,10 @@ const pagination = {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getStatus(instance) {
 | 
					function getStatus(instance) {
 | 
				
			||||||
	if (instance.isSuspended) return 'suspended';
 | 
						if (instance.isSuspended) return 'Suspended';
 | 
				
			||||||
	if (instance.isNotResponding) return 'error';
 | 
						if (instance.isBlocked) return 'Blocked';
 | 
				
			||||||
	return 'alive';
 | 
						if (instance.isNotResponding) return 'Error';
 | 
				
			||||||
 | 
						return 'Alive';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const headerActions = $computed(() => []);
 | 
					const headerActions = $computed(() => []);
 | 
				
			||||||
| 
						 | 
					@ -156,86 +115,8 @@ definePageMetadata({
 | 
				
			||||||
	grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
 | 
						grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
 | 
				
			||||||
	grid-gap: 12px;
 | 
						grid-gap: 12px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	> .instance {
 | 
						> .instance:hover {
 | 
				
			||||||
		padding: 16px;
 | 
							text-decoration: none;
 | 
				
			||||||
		background: var(--panel);
 | 
					 | 
				
			||||||
		border-radius: 8px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		&:hover {
 | 
					 | 
				
			||||||
			text-decoration: none;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		> .host {
 | 
					 | 
				
			||||||
			font-weight: bold;
 | 
					 | 
				
			||||||
			white-space: nowrap;
 | 
					 | 
				
			||||||
			overflow: hidden;
 | 
					 | 
				
			||||||
			text-overflow: ellipsis;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			> img {
 | 
					 | 
				
			||||||
				width: 18px;
 | 
					 | 
				
			||||||
				height: 18px;
 | 
					 | 
				
			||||||
				margin-right: 6px;
 | 
					 | 
				
			||||||
				vertical-align: middle;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		> .table {
 | 
					 | 
				
			||||||
			display: grid;
 | 
					 | 
				
			||||||
			grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
 | 
					 | 
				
			||||||
			grid-gap: 6px;
 | 
					 | 
				
			||||||
			margin: 6px 0;
 | 
					 | 
				
			||||||
			font-size: 70%;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			> .cell {
 | 
					 | 
				
			||||||
				> .key, > .value {
 | 
					 | 
				
			||||||
					white-space: nowrap;
 | 
					 | 
				
			||||||
					overflow: hidden;
 | 
					 | 
				
			||||||
					text-overflow: ellipsis;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				> .key {
 | 
					 | 
				
			||||||
					opacity: 0.7;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				> .value {
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		> .footer {
 | 
					 | 
				
			||||||
			display: flex;
 | 
					 | 
				
			||||||
			align-items: center;
 | 
					 | 
				
			||||||
			font-size: 0.9em;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			> .status {
 | 
					 | 
				
			||||||
				&.suspended {
 | 
					 | 
				
			||||||
					opacity: 0.5;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				&.error {
 | 
					 | 
				
			||||||
					color: var(--error);
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				&.alive {
 | 
					 | 
				
			||||||
					color: var(--success);
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			> .pubSub {
 | 
					 | 
				
			||||||
				margin-left: 8px;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			> .right {
 | 
					 | 
				
			||||||
				margin-left: auto;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				> .latestStatus {
 | 
					 | 
				
			||||||
					border: solid 1px var(--divider);
 | 
					 | 
				
			||||||
					border-radius: 4px;
 | 
					 | 
				
			||||||
					margin: 0 8px;
 | 
					 | 
				
			||||||
					padding: 0 4px;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,8 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<MkStickyContainer>
 | 
					<MkStickyContainer>
 | 
				
			||||||
	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 | 
						<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 | 
				
			||||||
	<MkSpacer :content-max="600" :margin-min="16" :margin-max="32">
 | 
						<MkSpacer v-if="instance" :content-max="600" :margin-min="16" :margin-max="32">
 | 
				
			||||||
		<div v-if="instance" class="_formRoot">
 | 
							<div v-if="tab === 'overview'" class="_formRoot">
 | 
				
			||||||
			<div class="fnfelxur">
 | 
								<div class="fnfelxur">
 | 
				
			||||||
				<img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/>
 | 
									<img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
| 
						 | 
					@ -64,37 +64,6 @@
 | 
				
			||||||
				</MkKeyValue>
 | 
									</MkKeyValue>
 | 
				
			||||||
			</FormSection>
 | 
								</FormSection>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			<FormSection>
 | 
					 | 
				
			||||||
				<template #label>{{ $ts.statistics }}</template>
 | 
					 | 
				
			||||||
				<div class="cmhjzshl">
 | 
					 | 
				
			||||||
					<div class="selects">
 | 
					 | 
				
			||||||
						<MkSelect v-model="chartSrc" style="margin: 0 10px 0 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 class="chart">
 | 
					 | 
				
			||||||
						<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
 | 
					 | 
				
			||||||
					</div>
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
			</FormSection>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			<MkObjectView tall :value="instance">
 | 
					 | 
				
			||||||
			</MkObjectView>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			<FormSection>
 | 
								<FormSection>
 | 
				
			||||||
				<template #label>Well-known resources</template>
 | 
									<template #label>Well-known resources</template>
 | 
				
			||||||
				<FormLink :to="`https://${host}/.well-known/host-meta`" external style="margin-bottom: 8px;">host-meta</FormLink>
 | 
									<FormLink :to="`https://${host}/.well-known/host-meta`" external style="margin-bottom: 8px;">host-meta</FormLink>
 | 
				
			||||||
| 
						 | 
					@ -104,6 +73,35 @@
 | 
				
			||||||
				<FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink>
 | 
									<FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink>
 | 
				
			||||||
			</FormSection>
 | 
								</FormSection>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
 | 
							<div v-if="tab === 'chart'" class="_formRoot">
 | 
				
			||||||
 | 
								<div class="cmhjzshl">
 | 
				
			||||||
 | 
									<div class="selects">
 | 
				
			||||||
 | 
										<MkSelect v-model="chartSrc" style="margin: 0 10px 0 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>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
									<div class="charts">
 | 
				
			||||||
 | 
										<div class="label">{{ i18n.t('recentNHours', { n: 90 }) }}</div>
 | 
				
			||||||
 | 
										<MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
 | 
				
			||||||
 | 
										<div class="label">{{ i18n.t('recentNDays', { n: 90 }) }}</div>
 | 
				
			||||||
 | 
										<MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div v-if="tab === 'raw'" class="_formRoot">
 | 
				
			||||||
 | 
								<MkObjectView tall :value="instance">
 | 
				
			||||||
 | 
								</MkObjectView>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
	</MkSpacer>
 | 
						</MkSpacer>
 | 
				
			||||||
</MkStickyContainer>
 | 
					</MkStickyContainer>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					@ -125,17 +123,18 @@ import number from '@/filters/number';
 | 
				
			||||||
import bytes from '@/filters/bytes';
 | 
					import bytes from '@/filters/bytes';
 | 
				
			||||||
import { iAmModerator } from '@/account';
 | 
					import { iAmModerator } from '@/account';
 | 
				
			||||||
import { definePageMetadata } from '@/scripts/page-metadata';
 | 
					import { definePageMetadata } from '@/scripts/page-metadata';
 | 
				
			||||||
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps<{
 | 
					const props = defineProps<{
 | 
				
			||||||
	host: string;
 | 
						host: string;
 | 
				
			||||||
}>();
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let tab = $ref('overview');
 | 
				
			||||||
let meta = $ref<misskey.entities.DetailedInstanceMetadata | null>(null);
 | 
					let meta = $ref<misskey.entities.DetailedInstanceMetadata | null>(null);
 | 
				
			||||||
let instance = $ref<misskey.entities.Instance | null>(null);
 | 
					let instance = $ref<misskey.entities.Instance | null>(null);
 | 
				
			||||||
let suspended = $ref(false);
 | 
					let suspended = $ref(false);
 | 
				
			||||||
let isBlocked = $ref(false);
 | 
					let isBlocked = $ref(false);
 | 
				
			||||||
let chartSrc = $ref('instance-requests');
 | 
					let chartSrc = $ref('instance-requests');
 | 
				
			||||||
let chartSpan = $ref('hour');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function fetch() {
 | 
					async function fetch() {
 | 
				
			||||||
	if (iAmModerator) {
 | 
						if (iAmModerator) {
 | 
				
			||||||
| 
						 | 
					@ -184,11 +183,26 @@ const headerActions = $computed(() => [{
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
}]);
 | 
					}]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const headerTabs = $computed(() => []);
 | 
					const headerTabs = $computed(() => [{
 | 
				
			||||||
 | 
						active: tab === 'overview',
 | 
				
			||||||
 | 
						title: i18n.ts.overview,
 | 
				
			||||||
 | 
						icon: 'fas fa-info-circle',
 | 
				
			||||||
 | 
						onClick: () => { tab = 'overview'; },
 | 
				
			||||||
 | 
					}, {
 | 
				
			||||||
 | 
						active: tab === 'chart',
 | 
				
			||||||
 | 
						title: i18n.ts.charts,
 | 
				
			||||||
 | 
						icon: 'fas fa-chart-simple',
 | 
				
			||||||
 | 
						onClick: () => { tab = 'chart'; },
 | 
				
			||||||
 | 
					}, {
 | 
				
			||||||
 | 
						active: tab === 'raw',
 | 
				
			||||||
 | 
						title: 'Raw data',
 | 
				
			||||||
 | 
						icon: 'fas fa-code',
 | 
				
			||||||
 | 
						onClick: () => { tab = 'raw'; },
 | 
				
			||||||
 | 
					}]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
definePageMetadata({
 | 
					definePageMetadata({
 | 
				
			||||||
	title: props.host,
 | 
						title: props.host,
 | 
				
			||||||
	icon: 'fas fa-info-circle',
 | 
						icon: 'fas fa-server',
 | 
				
			||||||
	bg: 'var(--bg)',
 | 
						bg: 'var(--bg)',
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -208,5 +222,12 @@ definePageMetadata({
 | 
				
			||||||
		display: flex;
 | 
							display: flex;
 | 
				
			||||||
		margin: 0 0 16px 0;
 | 
							margin: 0 0 16px 0;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> .charts {
 | 
				
			||||||
 | 
							> .label {
 | 
				
			||||||
 | 
								margin-bottom: 12px;
 | 
				
			||||||
 | 
								font-weight: bold;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,13 +11,13 @@ export class I18n<T extends Record<string, any>> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// string にしているのは、ドット区切りでのパス指定を許可するため
 | 
						// string にしているのは、ドット区切りでのパス指定を許可するため
 | 
				
			||||||
	// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
 | 
						// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
 | 
				
			||||||
	public t(key: string, args?: Record<string, string>): string {
 | 
						public t(key: string, args?: Record<string, string | number>): string {
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string;
 | 
								let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (args) {
 | 
								if (args) {
 | 
				
			||||||
				for (const [k, v] of Object.entries(args)) {
 | 
									for (const [k, v] of Object.entries(args)) {
 | 
				
			||||||
					str = str.replace(`{${k}}`, v);
 | 
										str = str.replace(`{${k}}`, v.toString());
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return str;
 | 
								return str;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue