wip: ログ
This commit is contained in:
parent
d4c3673315
commit
0c3df4245d
9 changed files with 133 additions and 19 deletions
|
@ -1,6 +1,26 @@
|
|||
import { User } from '../models/entities/user';
|
||||
import { toSignedString } from '../../common/functions/to-signed-string';
|
||||
import {Count} from '../models/count';
|
||||
import {api} from '../services/misskey';
|
||||
import {Score} from '../../common/types/score';
|
||||
import {MiUser} from './update-score';
|
||||
|
||||
/**
|
||||
* ユーザーのスコアを取得します。
|
||||
* @param user ユーザー
|
||||
* @returns ユーザーのスコア
|
||||
*/
|
||||
export const getScores = async (user: User): Promise<Score> => {
|
||||
// TODO 毎回取ってくるのも微妙なので、ある程度キャッシュしたいかも
|
||||
const miUser = await api<MiUser>(user.host, 'users/show', { username: user.username }, user.token);
|
||||
|
||||
return {
|
||||
notesCount: miUser.notesCount,
|
||||
followingCount: miUser.followingCount,
|
||||
followersCount: miUser.followersCount,
|
||||
...getDelta(user, miUser),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ユーザーのスコア差分を取得します。
|
||||
|
|
|
@ -9,7 +9,7 @@ const RETRY_COUNT = 5;
|
|||
/**
|
||||
* Misskey APIを呼び出す
|
||||
*/
|
||||
export const api = async <T = Record<string, unknown>>(host: string, endpoint: string, arg: Record<string, unknown>, token?: string): Promise<T> => {
|
||||
export const api = async <T extends Record<string, unknown> = Record<string, unknown>>(host: string, endpoint: string, arg: Record<string, unknown>, token?: string): Promise<T> => {
|
||||
const a = { ...arg };
|
||||
if (token) {
|
||||
a.i = token;
|
||||
|
|
|
@ -1,5 +1,30 @@
|
|||
import { User } from '../models/entities/user';
|
||||
import { api } from './misskey';
|
||||
import {format} from '../../common/functions/format';
|
||||
import {getScores} from '../functions/get-scores';
|
||||
|
||||
|
||||
/**
|
||||
* アラートを送信する
|
||||
* @param user ユーザー
|
||||
*/
|
||||
export const sendAlert = async (user: User) => {
|
||||
const text = format(user, await getScores(user));
|
||||
switch (user.alertMode) {
|
||||
case 'note':
|
||||
await sendNoteAlert(text, user);
|
||||
break;
|
||||
case 'notification':
|
||||
await sendNotificationAlert(text, user);
|
||||
break;
|
||||
case 'both':
|
||||
await Promise.all([
|
||||
sendNotificationAlert(text, user),
|
||||
sendNoteAlert(text, user),
|
||||
]);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ノートアラートを送信する
|
||||
|
|
5
src/common/types/log.ts
Normal file
5
src/common/types/log.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export type Log = {
|
||||
text: string;
|
||||
level: 'error' | 'warn' | 'info';
|
||||
timestamp: Date;
|
||||
}
|
|
@ -6,11 +6,13 @@ import { useTranslation } from 'react-i18next';
|
|||
import { store } from './store';
|
||||
import { ModalComponent } from './Modal';
|
||||
import { useTheme } from './misc/theme';
|
||||
import { BREAKPOINT_SM } from './const';
|
||||
import {BREAKPOINT_SM, LOCALSTORAGE_KEY_ACCOUNTS} from './const';
|
||||
import { useGetSessionQuery } from './services/session';
|
||||
import { Router } from './Router';
|
||||
import { setMobile } from './store/slices/screen';
|
||||
import {setAccounts, setMobile} from './store/slices/screen';
|
||||
import { GeneralLayout } from './GeneralLayout';
|
||||
import {$get} from './misc/api';
|
||||
import {IUser} from '../common/types/user';
|
||||
|
||||
const AppInner : React.VFC = () => {
|
||||
const { data: session } = useGetSessionQuery(undefined);
|
||||
|
@ -35,6 +37,11 @@ const AppInner : React.VFC = () => {
|
|||
}
|
||||
}, [$location]);
|
||||
|
||||
useEffect(() => {
|
||||
const accounts = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[];
|
||||
Promise.all(accounts.map(token => $get<IUser>('session', token))).then(a => dispatch(setAccounts(a as IUser[])));
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const qMobile = window.matchMedia(`(max-width: ${BREAKPOINT_SM})`);
|
||||
const syncMobile = (ev: MediaQueryListEvent) => dispatch(setMobile(ev.matches));
|
||||
|
|
50
src/frontend/components/LogView.tsx
Normal file
50
src/frontend/components/LogView.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
import React, {useMemo, useState} from 'react';
|
||||
import {Log} from '../../common/types/log';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const LogItem: React.FC<{log: Log}> = ({log}) => {
|
||||
const time = dayjs(log.timestamp).format('hh:mm:ss');
|
||||
|
||||
return (
|
||||
<div className={`log ${log.level}`}>
|
||||
[{time}] {log.text}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LogView: React.FC<{log: Log[]}> = ({log}) => {
|
||||
const [isVisibleInfo, setVisibleInfo] = useState(true);
|
||||
const [isVisibleWarn, setVisibleWarn] = useState(true);
|
||||
const [isVisibleError, setVisibleError] = useState(true);
|
||||
|
||||
const filter = useMemo(() => {
|
||||
const levels: Log['level'][] = [];
|
||||
if (isVisibleError) levels.push('error');
|
||||
if (isVisibleWarn) levels.push('warn');
|
||||
if (isVisibleInfo) levels.push('info');
|
||||
|
||||
return levels;
|
||||
}, [isVisibleError, isVisibleWarn, isVisibleInfo]);
|
||||
|
||||
const filteredLog = useMemo(() => log.filter(l => filter.includes(l.level)), [log, filter]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<label className="input-check">
|
||||
<input type="checkbox" checked={isVisibleInfo} onChange={e => setVisibleInfo(e.target.checked)} />
|
||||
<span><i className="fas fa-circle-info fa-fw" /> INFO</span>
|
||||
</label>
|
||||
<label className="input-check">
|
||||
<input type="checkbox" checked={isVisibleWarn} onChange={e => setVisibleWarn(e.target.checked)} />
|
||||
<span><i className="fas fa-circle-exclamation fa-fw" /> WARN</span>
|
||||
</label>
|
||||
<label className="input-check">
|
||||
<input type="checkbox" checked={isVisibleError} onChange={e => setVisibleError(e.target.checked)} />
|
||||
<span><i className="fas fa-circle-xmark fa-fw" /> ERROR</span>
|
||||
</label>
|
||||
<div className="log-view vstack slim">
|
||||
{filteredLog.map(l => <LogItem log={l} key={l.text} />)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -8,6 +8,8 @@ import { $delete, $get, $post, $put } from '../misc/api';
|
|||
import { showModal } from '../store/slices/screen';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTitle } from '../hooks/useTitle';
|
||||
import {Log} from '../../common/types/log';
|
||||
import {LogView} from '../components/LogView';
|
||||
|
||||
|
||||
export const AdminPage: React.VFC = () => {
|
||||
|
@ -25,7 +27,7 @@ export const AdminPage: React.VFC = () => {
|
|||
const [draftTitle, setDraftTitle] = useState('');
|
||||
const [draftBody, setDraftBody] = useState('');
|
||||
|
||||
const [misshaiLog, setMisshaiLog] = useState<string[] | null>(null);
|
||||
const [misshaiLog, setMisshaiLog] = useState<Log[] | null>(null);
|
||||
|
||||
const submitAnnouncement = async () => {
|
||||
if (selectedAnnouncement) {
|
||||
|
@ -64,7 +66,7 @@ export const AdminPage: React.VFC = () => {
|
|||
};
|
||||
|
||||
const fetchLog = () => {
|
||||
$get<string[]>('admin/misshai/log').then(setMisshaiLog);
|
||||
$get<Log[]>('admin/misshai/log').then(setMisshaiLog);
|
||||
};
|
||||
|
||||
const onClickStartMisshaiAlertWorkerButton = () => {
|
||||
|
@ -163,7 +165,7 @@ export const AdminPage: React.VFC = () => {
|
|||
))}
|
||||
{!isDeleteMode && (
|
||||
<button className="item fluid" onClick={() => setEditMode(true)}>
|
||||
<i className="icon fas fa-plus"/ >
|
||||
<i className="icon fas fa-plus" />
|
||||
Create New
|
||||
</button>
|
||||
)}
|
||||
|
@ -200,7 +202,7 @@ export const AdminPage: React.VFC = () => {
|
|||
ミス廃アラートワーカーを強制起動する
|
||||
</button>
|
||||
<h3>直近のワーカーエラー</h3>
|
||||
<pre><code>{misshaiLog?.join('\n') ?? 'なし'}</code></pre>
|
||||
{misshaiLog && <LogView log={misshaiLog} />}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -1,28 +1,17 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { LOCALSTORAGE_KEY_ACCOUNTS } from '../const';
|
||||
import { IUser } from '../../common/types/user';
|
||||
import { setAccounts } from '../store/slices/screen';
|
||||
import { useGetScoreQuery, useGetSessionQuery } from '../services/session';
|
||||
import { $get } from '../misc/api';
|
||||
import { useAnnouncements } from '../hooks/useAnnouncements';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export const IndexSessionPage: React.VFC = () => {
|
||||
const {t} = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const { data: session } = useGetSessionQuery(undefined);
|
||||
const score = useGetScoreQuery(undefined);
|
||||
|
||||
const announcements = useAnnouncements();
|
||||
|
||||
useEffect(() => {
|
||||
const accounts = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[];
|
||||
Promise.all(accounts.map(token => $get<IUser>('session', token))).then(a => dispatch(setAccounts(a as IUser[])));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<article className="fade">
|
||||
<section>
|
||||
|
|
|
@ -167,3 +167,19 @@ small {
|
|||
width: 1em;
|
||||
vertical-align: -0.1em;
|
||||
}
|
||||
|
||||
.log-view {
|
||||
background-color: var(--black);
|
||||
}
|
||||
|
||||
.log {
|
||||
&.info {
|
||||
color: var(--skyblue);
|
||||
}
|
||||
&.error {
|
||||
color: var(--red);
|
||||
}
|
||||
&.warn {
|
||||
color: var(--yellow);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue