feat: Add rudimentary debugging utilities

/debug: Added a /debug page that's only accessible if a fatal error in configuration has occurred.
settings: The settings page now includes a debugging section with similar content to the debug page. This still needs styling but is otherwise OK.
This commit is contained in:
tecc 2022-11-07 03:02:00 +01:00
parent 2431c96409
commit 615041a927
No known key found for this signature in database
GPG Key ID: 400AAD881FCC028B
22 changed files with 947 additions and 33 deletions

1
.gitignore vendored
View File

@ -23,6 +23,7 @@ node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/logs
# local env files
.env

View File

@ -26,7 +26,7 @@ COPY --from=builder /app/node_modules ./node_modules
VOLUME /app/config
# VOLUME /app/data
ENV CONFIG_FILE=/app/config/notea.yml
ENV CONFIG_FILE=/app/config/notea.yml LOG_DIRECTORY=/app/logs
EXPOSE 3000

View File

@ -0,0 +1,74 @@
import { Button } from '@material-ui/core';
import useI18n from 'libs/web/hooks/use-i18n';
import { FC } from 'react';
import { logLevelToString, DebugInformation } from 'libs/shared/debugging';
export const DebugInfoCopyButton: FC<{
debugInfo: DebugInformation;
}> = ({ debugInfo }) => {
const { t } = useI18n();
function generateDebugInfo(): string {
let data =
'Notea debug information' +
'\nTime ' +
new Date(Date.now()).toISOString() +
'\n\n';
function ensureNewline() {
if (!data.endsWith('\n') && data.length >= 1) {
data += '\n';
}
}
if (debugInfo.issues.length > 0) {
ensureNewline();
data += 'Configuration errors: ';
let i = 1;
const prefixLength = debugInfo.issues.length.toString().length;
for (const issue of debugInfo.issues) {
const prefix = i.toString().padStart(prefixLength, ' ') + ': ';
const empty = ' '.repeat(prefixLength + 2);
data += prefix + issue.name;
data += empty + '// ' + issue.cause;
data += empty + issue.description;
i++;
}
} else {
data += 'No detected configuration errors.';
}
if (debugInfo.logs.length > 0) {
ensureNewline();
for (const log of debugInfo.logs) {
data += `[${new Date(log.time).toISOString()} ${log.name}] ${logLevelToString(log.level)}: ${log.msg}`;
}
}
return data;
}
function copyDebugInfo() {
const text = generateDebugInfo();
navigator.clipboard
.writeText(text)
.then(() => {
// nothing
})
.catch((e) => {
console.error(
'Error when trying to copy debugging information to clipboard: %O',
e
);
});
}
return (
<Button variant="contained" onClick={copyDebugInfo}>
{t('Copy debugging information')}
</Button>
);
};

View File

@ -0,0 +1,17 @@
import { FC } from 'react';
import { Issue } from 'libs/shared/debugging';
import { Issue as IssueDisplay } from './issue';
export const IssueList: FC<{
issues: Array<Issue>;
}> = ({ issues }) => {
return (
<div className="flex flex-col">
{issues.map((v, i) => {
return (
<IssueDisplay key={i} issue={v} id={`issue-${i}`}/>
);
})}
</div>
);
};

165
components/debug/issue.tsx Normal file
View File

@ -0,0 +1,165 @@
import { FC } from 'react';
import {
getNameFromRecommendation,
getNameFromSeverity,
Issue as IssueInfo,
IssueFix,
IssueSeverity
} from 'libs/shared/debugging';
import {
Accordion as MuiAccordion,
AccordionDetails as MuiAccordionDetails,
AccordionSummary as MuiAccordionSummary,
withStyles
} from '@material-ui/core';
import { ChevronDownIcon } from '@heroicons/react/outline';
import useI18n from 'libs/web/hooks/use-i18n';
const Accordion = withStyles({
root: {
boxShadow: 'none',
'&:not(:last-child)': {
borderBottom: 0,
},
'&:before': {
display: 'none',
},
'&$expanded': {
margin: 'auto 0',
},
},
expanded: {
},
})(MuiAccordion);
const AccordionSummary = withStyles({
root: {
backgroundColor: 'rgba(0, 0, 0, .03)',
borderBottom: '1px solid rgba(0, 0, 0, .125)',
borderTopRightRadius: 'inherit',
borderBottomRightRadius: 'inherit',
marginBottom: -1,
minHeight: 56,
'&$expanded': {
minHeight: 56,
borderBottomRightRadius: '0'
},
},
content: {
'&$expanded': {
margin: '12px 0',
}
},
expanded: {},
})(MuiAccordionSummary);
const AccordionDetails = withStyles((theme) => ({
root: {
padding: theme.spacing(2),
},
}))(MuiAccordionDetails);
interface FixProps {
id: string;
fix: IssueFix;
}
const Fix: FC<FixProps> = ({ id, fix }) => {
const i18n = useI18n();
const { t } = i18n;
const steps = fix.steps ?? [];
return (
<Accordion key={id} className={"bg-gray-300"}>
<AccordionSummary
expandIcon={<ChevronDownIcon width=".8em"/>}
aria-controls={`${id}-details`}
id={`${id}-summary`}
>
<div className={"flex flex-col"}>
{fix.recommendation === 0 && (
<span className={"text-xs uppercase"}>{getNameFromRecommendation(fix.recommendation, i18n)}</span>
)}
<span className={"font-bold"}>{fix.description}</span>
</div>
</AccordionSummary>
<AccordionDetails className={"rounded-[inherit]"}>
{steps.length > 0 ? (
<ol className="list-decimal list-inside">
{steps.map((step, i) => {
const stepId = `${id}-step-${i}`;
return (
<li key={stepId}>{step}</li>
);
})}
</ol>
) : (
<span>{t('No steps were provided by Notea to perform this fix.')}</span>
)}
</AccordionDetails>
</Accordion>
);
};
interface IssueProps {
issue: IssueInfo;
id: string;
}
export const Issue: FC<IssueProps> = function (props) {
const { issue, id } = props;
const i18n = useI18n();
const { t } = i18n;
let borderColour: string;
switch (issue.severity) {
case IssueSeverity.SUGGESTION:
borderColour = "border-gray-500";
break;
case IssueSeverity.WARNING:
borderColour = "border-yellow-100";
break;
case IssueSeverity.ERROR:
borderColour = "border-red-500";
break;
case IssueSeverity.FATAL_ERROR:
borderColour = "border-red-900";
break;
}
return (
<Accordion className={`border-l-4 ${borderColour} bg-gray-200`}>
<AccordionSummary
className={"bg-gray-100"}
expandIcon={<ChevronDownIcon width=".8em"/>}
aria-controls={`${id}-details`}
id={`${id}-summary`}
>
<div className={"flex flex-col bg-transparent"}>
<span className={"text-xs uppercase"}>{getNameFromSeverity(issue.severity, i18n)}</span>
<span className={"font-bold"}>{issue.name}</span>
</div>
</AccordionSummary>
<AccordionDetails className={"flex flex-col"}>
<span>{issue.description ?? t('No description was provided for this issue.')}</span>
{issue.fixes.length > 0 ? (
<div className={"mt-1 flex flex-col"}>
<span className={"font-bold"}>{t('Potential fixes')}</span>
<div>
{issue.fixes.map((fix, i) => {
const fixId = `${id}-fix-${i}`;
return (
<Fix
key={fixId}
id={fixId}
fix={fix}
/>
);
})}
</div>
</div>
) : (
<span>{t('No fixes are known by Notea for this issue.')}</span>
)}
</AccordionDetails>
</Accordion>
);
};

27
components/debug/logs.tsx Normal file
View File

@ -0,0 +1,27 @@
import { FC } from 'react';
import { logLevelToString, LogLike } from 'libs/shared/debugging';
interface LogsProps {
logs: Array<LogLike>
}
export const Logs: FC<LogsProps> = (props) => {
return (
<div className={"flex flex-col space-y-1"}>
{props.logs.length > 0 ? props.logs.map((log, i) => {
return (
<div className={"flex flex-col border-l pl-2"} key={i}>
<span className={"text-sm uppercase"}>
{logLevelToString(log.level)} at {new Date(log.time ?? 0).toLocaleString()} from <b>{log.name}</b>
</span>
<span className={"font-mono"}>
{log.msg}
</span>
</div>
);
}) : (
<span>No logs.</span>
)}
</div>
);
};

View File

@ -0,0 +1,20 @@
import { IssueList } from 'components/debug/issue-list';
import type { FC } from 'react';
import { DebugInfoCopyButton } from 'components/debug/debug-info-copy-button';
import { DebugInformation, Issue } from 'libs/shared/debugging';
export const Debugging: FC<{
debugInfo: DebugInformation;
}> = (props) => {
const issues: Array<Issue> = [...props.debugInfo.issues].sort((a, b) => b.severity - a.severity);
return (
<div className="my-2">
<IssueList
issues={issues}
/>
<div className={'flex flex-row my-2'}>
<DebugInfoCopyButton debugInfo={props.debugInfo} />
</div>
</div>
);
};

View File

@ -8,6 +8,8 @@ import { ImportOrExport } from './import-or-export';
import { SnippetInjection } from './snippet-injection';
import useI18n from 'libs/web/hooks/use-i18n';
import { SettingsHeader } from './settings-header';
import { Debugging } from 'components/settings/debugging';
import { DebugInformation } from 'libs/shared/debugging';
export const defaultFieldConfig: TextFieldProps = {
fullWidth: true,
@ -26,7 +28,9 @@ const HR = () => {
return <hr className="my-10 border-gray-200" />;
};
export const SettingsContainer: FC = () => {
export const SettingsContainer: FC<{
debugInfo: DebugInformation
}> = (props) => {
const { t } = useI18n();
return (
@ -36,6 +40,7 @@ export const SettingsContainer: FC = () => {
<Language></Language>
<Theme></Theme>
<EditorWidth></EditorWidth>
<HR />
<SettingsHeader
id="import-and-export"
@ -44,11 +49,21 @@ export const SettingsContainer: FC = () => {
'Import a zip file containing markdown files to this location, or export all pages from this location.'
)}
></SettingsHeader>
<ImportOrExport></ImportOrExport>
<HR />
<SettingsHeader id="sharing" title={t('Sharing')}></SettingsHeader>
<SnippetInjection></SnippetInjection>
<HR />
<SettingsHeader
id="debug"
title={t('Debugging')}
description={t(
'Provides information about your Notea instance that can be helpful when trying to fix problems.'
)}
/>
<Debugging debugInfo={props.debugInfo} />
</section>
);
};

View File

@ -1,6 +1,10 @@
import yaml from 'js-yaml';
import * as env from 'libs/shared/env';
import { existsSync, readFileSync } from 'fs';
import { isProbablyError } from 'libs/shared/util';
import { createLogger, Issue, IssueCategory, IssueFixRecommendation, IssueSeverity } from 'libs/server/debugging';
const logger = createLogger("config");
export type BasicUser = { username: string; password: string };
type BasicMultiUserConfiguration = {
@ -45,23 +49,106 @@ export interface Configuration {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let loaded: Configuration | undefined = undefined;
export function loadConfig() {
enum ErrTitle {
CONFIG_FILE_READ_FAIL = 'Failed to load configuration file',
INVALID_AUTH_CONFIG = 'Invalid authorisation configuration',
CONFIG_FILE_PARSE_FAIL = 'Could not parse configuration file',
}
export function loadConfigAndListErrors(): {
config?: Configuration;
errors: Array<Issue>;
} {
logger.debug("Loading configuration from scratch (loadConfigAndListErrors)");
// TODO: More config errors
const configFile = env.getEnvRaw('CONFIG_FILE', false) ?? './notea.yml';
const errors: Array<Issue> = [];
let baseConfig: Configuration = {} as Configuration;
if (existsSync(configFile)) {
const data = readFileSync(configFile, 'utf-8');
let data;
try {
data = readFileSync(configFile, 'utf-8');
} catch (e) {
let cause;
if (isProbablyError(e)) {
cause = e.message;
}
errors.push({
name: ErrTitle.CONFIG_FILE_READ_FAIL,
description: "The configuration file couldn't be read.",
cause,
severity: IssueSeverity.WARNING,
category: IssueCategory.CONFIG,
fixes: [
{
description: "Make sure Notea has read access to the configuration file",
recommendation: IssueFixRecommendation.NEUTRAL,
},
{
description: 'Make sure no other programme is using the configuration file',
recommendation: IssueFixRecommendation.NEUTRAL
}
],
});
}
if (data) {
try {
baseConfig = yaml.load(data) as Configuration;
} catch (e) {
let cause;
if (isProbablyError(e)) {
cause = e.message;
}
errors.push({
name: ErrTitle.CONFIG_FILE_PARSE_FAIL,
description:
'The configuration file could not be parsed, probably due to a syntax error.',
severity: IssueSeverity.WARNING,
category: IssueCategory.CONFIG,
cause,
fixes: [
{
description: 'Check your configuration file for syntax errors.',
recommendation: IssueFixRecommendation.RECOMMENDED
}
],
});
}
}
}
const disablePassword = env.parseBool(env.getEnvRaw('DISABLE_PASSWORD', false), false);
const disablePassword = env.parseBool(
env.getEnvRaw('DISABLE_PASSWORD', false),
false
);
let auth: AuthConfiguration;
let auth: AuthConfiguration = { type: 'none' };
if (!disablePassword) {
const envPassword = env.getEnvRaw('PASSWORD', false);
if (baseConfig.auth === undefined) {
if (envPassword === undefined) {
throw new Error('Authentication undefined');
errors.push({
name: ErrTitle.INVALID_AUTH_CONFIG,
description:
'Neither the configuration file, the PASSWORD environment variable, nor the DISABLE_PASSWORD environment variable was set.',
severity: IssueSeverity.FATAL_ERROR,
category: IssueCategory.CONFIG,
fixes: [
{
description: 'Include an auth section the configuration file',
recommendation: IssueFixRecommendation.RECOMMENDED,
},
{
description: 'Set the password using the environment variable',
recommendation: IssueFixRecommendation.NEUTRAL,
},
{
description: 'Disable authentication',
recommendation: IssueFixRecommendation.NOT_ADVISED
}
],
});
} else {
auth = {
type: 'basic',
@ -71,14 +158,39 @@ export function loadConfig() {
} else {
auth = baseConfig.auth;
if (envPassword !== undefined) {
throw new Error(
'Cannot specify PASSWORD when auth config section is present'
);
errors.push({
name: ErrTitle.INVALID_AUTH_CONFIG,
description:
'The PASSWORD environment variable cannot be set when the file configuration contains an auth section.',
category: IssueCategory.CONFIG,
severity: IssueSeverity.FATAL_ERROR,
fixes: [
{
description: "Don't set the PASSWORD environment variable prior to running Notea.",
recommendation: IssueFixRecommendation.RECOMMENDED
},
{
description: 'Remove the auth section from your file configuration.',
recommendation: IssueFixRecommendation.NEUTRAL
}
],
});
}
if (auth.type === 'basic') {
if (auth.users) {
// TEMPORARILY;
throw new Error('Multiple users are not yet supported');
errors.push({
name: ErrTitle.INVALID_AUTH_CONFIG,
description: 'Multiple users are not yet supported',
severity: IssueSeverity.FATAL_ERROR,
category: IssueCategory.CONFIG,
fixes: [
{
description: "Change to a single-user configuration.",
recommendation: IssueFixRecommendation.RECOMMENDED
}
]
});
/*for (const user of auth.users) {
user.username = user.username.toString();
@ -132,7 +244,10 @@ export function loadConfig() {
'STORE_PREFIX',
false,
) ?? store.prefix ?? '';
store.proxyAttachments = env.parseBool(env.getEnvRaw('DIRECT_RESPONSE_ATTACHMENT', false), store.proxyAttachments ?? false);
store.proxyAttachments = env.parseBool(
env.getEnvRaw('DIRECT_RESPONSE_ATTACHMENT', false),
store.proxyAttachments ?? false
);
}
let server: ServerConfiguration;
@ -142,20 +257,50 @@ export function loadConfig() {
server = baseConfig.server;
}
{
server.useSecureCookies = env.parseBool(env.getEnvRaw('COOKIE_SECURE', false), server.useSecureCookies ?? process.env.NODE_ENV === 'production');
server.useSecureCookies = env.parseBool(
env.getEnvRaw('COOKIE_SECURE', false),
server.useSecureCookies ?? process.env.NODE_ENV === 'production'
);
server.baseUrl = env.getEnvRaw('BASE_URL', false) ?? server.baseUrl;
}
loaded = {
return {
config: {
auth,
store,
server
server,
},
errors,
};
}
const MAX_ERRORS = 2;
export function loadConfig() {
const result = loadConfigAndListErrors();
if (!result.config) {
const { errors } = result;
let name = errors
.slice(0, MAX_ERRORS)
.map((v) => v.name)
.join(', ');
if (errors.length > MAX_ERRORS) {
const rest = errors.length - MAX_ERRORS;
name += ' and ' + rest + ' other error' + (rest > 1 ? 's' : '');
}
throw new Error(name);
}
loaded = result.config;
return loaded;
}
export function config(): Configuration {
if (!loaded) {
logger.debug("Loading configuration");
loadConfig();
logger.debug("Successfully loaded configuration");
}
return loaded as Configuration;

View File

@ -18,6 +18,7 @@ import { Settings } from 'libs/shared/settings';
import { TreeModel } from 'libs/shared/tree';
import { UserAgentType } from 'libs/shared/ua';
import { useStore } from './middlewares/store';
import { DebugInformation } from 'libs/server/debugging';
export interface ServerState {
store: StoreProvider;
@ -36,6 +37,7 @@ export interface ServerProps {
tree?: TreeModel;
ua?: UserAgentType;
disablePassword: boolean;
debugInformation?: DebugInformation;
}
export type ApiRequest = NextApiRequest & {

68
libs/server/debugging.ts Normal file
View File

@ -0,0 +1,68 @@
import { loadConfigAndListErrors } from 'libs/server/config';
import pino from 'pino';
import pinoPretty from 'pino-pretty';
import Logger = pino.Logger;
import * as path from 'path';
import * as fs from 'fs';
import { DebugInformation, Issue } from 'libs/shared/debugging';
export * from 'libs/shared/debugging'; // here's a lil' lesson in trickery
const runtimeIssues: Array<Issue> = [];
export function reportRuntimeIssue(issue: Issue) {
runtimeIssues.push({
...issue,
isRuntime: true
});
}
export function findIssues(): Array<Issue> {
const issues: Array<Issue> = [];
const cfg = loadConfigAndListErrors();
issues.push(...cfg.errors);
issues.push(...runtimeIssues);
return issues;
}
export function collectDebugInformation(): DebugInformation {
const issues = findIssues();
return {
issues,
logs: []
};
}
function getLogFile(name: string) {
const dir = path.resolve(process.cwd(), process.env.LOG_DIRECTORY ?? 'logs');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, {
recursive: true
});
}
return path.resolve(dir, `${name}.log`);
}
const loggerTransport: Parameters<typeof pino.multistream>[0] = [
{
stream: fs.createWriteStream(getLogFile('debug'), { flags: 'a' }),
level: "debug"
},
{
stream: pinoPretty(),
level: "info"
}
];
const multistream = pino.multistream(loggerTransport);
export function createLogger(name: string): Logger {
return pino({
name,
level: "trace",
}, multistream);
}

View File

@ -0,0 +1,14 @@
import { SSRMiddleware } from '../connect';
import { collectDebugInformation } from 'libs/server/debugging';
export const applyMisconfiguration: SSRMiddleware = async (req, _res, next) => {
// const IS_DEMO = getEnv<boolean>('IS_DEMO', false);
const collected = collectDebugInformation();
req.props = {
...req.props,
debugInformation: collected
};
next();
};

View File

@ -1,19 +1,31 @@
import { DEFAULT_SETTINGS } from 'libs/shared/settings';
import { getSettings } from 'pages/api/settings';
import { SSRMiddleware } from '../connect';
import { Redirect } from 'next';
export const applySettings: SSRMiddleware = async (req, _res, next) => {
const settings = await getSettings(req.state.store);
let settings, redirect: Redirect = req.redirect;
try {
settings = await getSettings(req.state.store);
} catch (e) {
redirect = {
permanent: false,
destination: '/debug'
};
}
let lngDict = {};
if (settings) {
// import language dict
if (settings.locale && settings.locale !== DEFAULT_SETTINGS.locale) {
lngDict = (await import(`locales/${settings.locale}.json`)).default;
}
}
req.props = {
...req.props,
...{ settings, lngDict },
...{ settings, lngDict }
};
req.redirect = redirect;
next();
};

89
libs/shared/debugging.ts Normal file
View File

@ -0,0 +1,89 @@
import { ContextProps as I18nContextProps } from 'libs/web/utils/i18n-provider';
import pino from 'pino';
export enum IssueCategory {
CONFIG = "config",
MISC = "misc",
STORE = "store",
}
export enum IssueSeverity {
/**
* Suggestions are issues that suggest things the user can do to improve
* their experience with Notea.
*/
SUGGESTION = 0,
/**
* Warnings are issues that aren't severe enough to cause major issues by
* themselves, but that *could* cause worse issues.
*/
WARNING = 1,
/**
* Errors are issues that are severe enough to cause major issues by themselves.
* They don't necessarily cause the entire instance to stop working though.
*/
ERROR = 2,
/**
* Fatal errors are issues that must be resolved before Notea starts working.
*/
FATAL_ERROR = 3
}
export function getNameFromSeverity(severity: IssueSeverity, { t }: I18nContextProps) {
switch (severity) {
case IssueSeverity.SUGGESTION:
return t('Suggestion');
case IssueSeverity.WARNING:
return t('Warning');
case IssueSeverity.ERROR:
return t('Error');
case IssueSeverity.FATAL_ERROR:
return t('Fatal error');
}
}
export enum IssueFixRecommendation {
NEUTRAL,
RECOMMENDED,
NOT_ADVISED
}
export function getNameFromRecommendation(recommendation: IssueFixRecommendation, { t }: I18nContextProps) {
switch (recommendation) {
case IssueFixRecommendation.NEUTRAL:
return t('Neutral');
case IssueFixRecommendation.RECOMMENDED:
return t('Recommended');
case IssueFixRecommendation.NOT_ADVISED:
return t('Not advised');
}
}
export interface IssueFix {
recommendation: IssueFixRecommendation;
description: string;
steps?: Array<string>;
}
export interface Issue {
name: string;
description?: string;
category: IssueCategory;
severity: IssueSeverity;
fixes: Array<IssueFix>;
cause?: string;
isRuntime?: boolean;
}
export interface LogLike {
name: string;
msg: string;
pid: number;
time: number;
level: number;
}
export interface DebugInformation {
issues: Array<Issue>;
logs: Array<LogLike>; // TODO: Logging
}
export function logLevelToString(level: number) {
return pino.levels.labels[level];
}

6
libs/shared/util.ts Normal file
View File

@ -0,0 +1,6 @@
export function isProbablyError(e: any): e is Error {
if (!e) return false;
if (e instanceof Error) return true;
return e && e.stack && e.message;
}

View File

@ -10,7 +10,7 @@ const i18n = rosetta<Record<string, string>>();
export const defaultLanguage = DEFAULT_SETTINGS.locale;
export const languages = values(Locale);
interface ContextProps {
export interface ContextProps {
activeLocale: Locale;
t: Rosetta<Record<string, string>>['t'];
locale: (l: Locale, dict: Record<string, string>) => void;

View File

@ -63,6 +63,8 @@
"next-themes": "^0.0.14",
"notistack": "^1.0.7",
"outline-icons": "^1.27.0",
"pino": "^8.7.0",
"pino-pretty": "^9.1.1",
"prosemirror-inputrules": "^1.1.3",
"pupa": "^2.1.1",
"qss": "^2.0.3",

View File

@ -6,15 +6,32 @@ import { formatSettings, Settings } from 'libs/shared/settings';
import { isEqual } from 'lodash';
import { tryJSON } from 'libs/shared/str';
import { StoreProvider } from 'libs/server/store';
import { IssueCategory, IssueFixRecommendation, IssueSeverity, reportRuntimeIssue } from 'libs/server/debugging';
export async function getSettings(store: StoreProvider): Promise<Settings> {
const settingsPath = getPathSettings();
let settings;
try {
if (await store.hasObject(settingsPath)) {
settings = tryJSON<Settings>(
await store.getObject(settingsPath)
);
}
} catch (e) {
reportRuntimeIssue({
category: IssueCategory.STORE,
severity: IssueSeverity.FATAL_ERROR,
name: "Could not get settings",
cause: String(e),
fixes: [
{
description: "Make sure Notea can connect to the store.",
recommendation: IssueFixRecommendation.RECOMMENDED
}
]
});
throw e;
}
const formatted = formatSettings(settings || {});
if (!settings || !isEqual(settings, formatted)) {

91
pages/debug.tsx Normal file
View File

@ -0,0 +1,91 @@
import { ServerProps, ssr, SSRContext } from 'libs/server/connect';
import { applyMisconfiguration } from 'libs/server/middlewares/misconfiguration';
import pkg from 'package.json';
import { IssueList } from 'components/debug/issue-list';
import { DebugInfoCopyButton } from 'components/debug/debug-info-copy-button';
import { DebugInformation, IssueSeverity } from 'libs/shared/debugging';
import { Logs } from 'components/debug/logs';
export function DebugPage({ debugInformation }: ServerProps) {
if (!debugInformation) throw new Error('Missing debug information');
const issues = debugInformation.issues;
const logs = debugInformation.logs ?? [];
return (
<div className="h-screen flex flex-col">
<main className="flex flex-col space-y-4 mx-auto my-auto">
<h1 className="text-4xl font-bold my-auto mx-auto">Backup debugging page</h1>
<div className="flex flex-row my-auto mx-auto">
<DebugInfoCopyButton debugInfo={debugInformation} />
</div>
<div className="flex flex-row space-x-5 my-auto">
{issues.length > 0 && (
<div className={"flex flex-col"}>
<h2 className={"text-2xl"}>Issues</h2>
<IssueList
issues={debugInformation.issues}
/>
</div>
)}
{logs.length > 0 && (
<div>
<h2 className={"text-2xl"}>Logs</h2>
<Logs logs={logs}/>
</div>
)}
{issues.length < 1 && logs.length < 1 && (
<div className={"mx-auto text-lg"}>
No debug information available.
</div>
)}
</div>
</main>
<footer className="flex flex-col my-auto mx-auto">
<div className="mx-auto">
<a
href="https://github.com/notea-org/notea"
target="_blank"
rel="noreferrer"
>
Notea v{pkg.version}
</a>
</div>
<div className="space-x-1">
<span>MIT &copy;</span>
<a
href="https://github.com/notea-org/notea"
target="_blank"
rel="noreferrer"
>
Notea Contributors
</a>
<span>2022</span>
</div>
</footer>
</div>
);
}
export default DebugPage;
export const getServerSideProps = async (ctx: SSRContext) => {
await ssr().use(applyMisconfiguration).run(ctx.req, ctx.res);
// has to be cast to non-null
const debugInformation = ctx.req.props.debugInformation as DebugInformation;
let redirect;
if (!debugInformation.issues.some((v) => v.severity === IssueSeverity.FATAL_ERROR)) {
redirect = {
destination: '/',
permanent: false,
};
}
return {
redirect,
props: ctx.req.props,
};
};

View File

@ -11,8 +11,10 @@ import { applyCsrf } from 'libs/server/middlewares/csrf';
import { SettingFooter } from 'components/settings/setting-footer';
import { SSRContext, ssr } from 'libs/server/connect';
import { applyReset } from 'libs/server/middlewares/reset';
import { applyMisconfiguration } from 'libs/server/middlewares/misconfiguration';
import { DebugInformation } from 'libs/shared/debugging';
const SettingsPage: NextPage<{ tree: TreeModel }> = ({ tree }) => {
const SettingsPage: NextPage<{ debugInformation: DebugInformation, tree: TreeModel }> = ({ tree, debugInformation }) => {
const { t } = useI18n();
return (
@ -23,7 +25,7 @@ const SettingsPage: NextPage<{ tree: TreeModel }> = ({ tree }) => {
<span>{t('Settings')}</span>
</h1>
<SettingsContainer />
<SettingsContainer debugInfo={debugInformation} />
<SettingFooter />
</div>
</section>
@ -41,6 +43,7 @@ export const getServerSideProps = async (ctx: SSRContext) => {
.use(applySettings)
.use(applyCsrf)
.use(applyUA)
.use(applyMisconfiguration)
.run(ctx.req, ctx.res);
return {

View File

@ -9,6 +9,8 @@ module.exports = {
colors: {
gray: colors.gray,
blue: colors.blue,
red: colors.red,
yellow: colors.yellow,
transparent: 'transparent',
current: 'currentColor',
},
@ -26,6 +28,7 @@ module.exports = {
},
fontFamily: {
sans: ['Noto Sans'].concat(defaultConfig.theme.fontFamily['sans']),
mono: defaultConfig.theme.fontFamily['mono']
},
},
variants: {

145
yarn.lock
View File

@ -3225,6 +3225,11 @@ at-least-node@^1.0.0:
resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz"
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
atomic-sleep@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
autoprefixer@^10.2.4:
version "10.4.12"
resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.12.tgz"
@ -3817,7 +3822,7 @@ color@^4.0.1:
color-convert "^2.0.1"
color-string "^1.9.0"
colorette@^2.0.16:
colorette@^2.0.16, colorette@^2.0.7:
version "2.0.19"
resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz"
integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
@ -4117,6 +4122,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
dateformat@^4.6.3:
version "4.6.3"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5"
integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
dayjs@^1.10.4:
version "1.11.5"
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.5.tgz"
@ -4846,6 +4856,11 @@ expect@^27.5.1:
jest-matcher-utils "^27.5.1"
jest-message-util "^27.5.1"
fast-copy@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.0.tgz#875ebf33b13948ae012b6e51d33da5e6e7571ab8"
integrity sha512-4HzS+9pQ5Yxtv13Lhs1Z1unMXamBdn5nA4bEi1abYpDNSpSp7ODYQ1KPMF6nTatfEzgH6/zPvXKU1zvHiUjWlA==
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
@ -4872,6 +4887,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
fast-redact@^3.1.1:
version "3.1.2"
resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa"
integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==
fast-safe-stringify@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz"
@ -5182,6 +5202,17 @@ glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, gl
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^8.0.0:
version "8.0.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e"
integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^5.0.1"
once "^1.3.0"
globals@^11.1.0:
version "11.12.0"
resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz"
@ -5301,6 +5332,14 @@ he@^1.2.0:
resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
help-me@^4.0.1:
version "4.1.0"
resolved "https://registry.yarnpkg.com/help-me/-/help-me-4.1.0.tgz#c105e78ba490d6fcaa61a3d0cd06e0054554efab"
integrity sha512-5HMrkOks2j8Fpu2j5nTLhrBhT7VwHwELpqnSnx802ckofys5MO2SkLpgSz3dgNFHV7IYFX2igm5CM75SmuYidw==
dependencies:
glob "^8.0.0"
readable-stream "^3.6.0"
hex-color-regex@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz"
@ -6283,6 +6322,11 @@ jest@^27.1.0:
import-local "^3.0.2"
jest-cli "^27.5.1"
joycon@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
@ -7191,6 +7235,11 @@ object.values@^1.1.5:
define-properties "^1.1.3"
es-abstract "^1.19.1"
on-exit-leak-free@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4"
integrity sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==
once@1.4.0, once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
@ -7445,6 +7494,56 @@ pinkie@^2.0.0:
resolved "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz"
integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==
pino-abstract-transport@^1.0.0, pino-abstract-transport@v1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3"
integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==
dependencies:
readable-stream "^4.0.0"
split2 "^4.0.0"
pino-pretty@^9.1.1:
version "9.1.1"
resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-9.1.1.tgz#e7d64c1db98266ca428ab56567b844ba780cd0e1"
integrity sha512-iJrnjgR4FWQIXZkUF48oNgoRI9BpyMhaEmihonHeCnZ6F50ZHAS4YGfGBT/ZVNsPmd+hzkIPGzjKdY08+/yAXw==
dependencies:
colorette "^2.0.7"
dateformat "^4.6.3"
fast-copy "^3.0.0"
fast-safe-stringify "^2.1.1"
help-me "^4.0.1"
joycon "^3.1.1"
minimist "^1.2.6"
on-exit-leak-free "^2.1.0"
pino-abstract-transport "^1.0.0"
pump "^3.0.0"
readable-stream "^4.0.0"
secure-json-parse "^2.4.0"
sonic-boom "^3.0.0"
strip-json-comments "^3.1.1"
pino-std-serializers@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.0.0.tgz#4c20928a1bafca122fdc2a7a4a171ca1c5f9c526"
integrity sha512-mMMOwSKrmyl+Y12Ri2xhH1lbzQxwwpuru9VjyJpgFIH4asSj88F2csdMwN6+M5g1Ll4rmsYghHLQJw81tgZ7LQ==
pino@^8.7.0:
version "8.7.0"
resolved "https://registry.yarnpkg.com/pino/-/pino-8.7.0.tgz#58621608a3d8540ae643cdd9194cdd94130c78d9"
integrity sha512-l9sA5uPxmZzwydhMWUcm1gI0YxNnYl8MfSr2h8cwLvOAzQLBLewzF247h/vqHe3/tt6fgtXeG9wdjjoetdI/vA==
dependencies:
atomic-sleep "^1.0.0"
fast-redact "^3.1.1"
on-exit-leak-free "^2.1.0"
pino-abstract-transport v1.0.0
pino-std-serializers "^6.0.0"
process-warning "^2.0.0"
quick-format-unescaped "^4.0.3"
real-require "^0.2.0"
safe-stable-stringify "^2.3.1"
sonic-boom "^3.1.0"
thread-stream "^2.0.0"
pirates@^4.0.4, pirates@^4.0.5:
version "4.0.5"
resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz"
@ -7574,6 +7673,11 @@ prismjs@~1.27.0:
resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz"
integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==
process-warning@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.0.0.tgz#341dbeaac985b90a04ebcd844d50097c7737b2ee"
integrity sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww==
process@^0.11.10:
version "0.11.10"
resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz"
@ -7809,6 +7913,11 @@ queue-microtask@^1.2.2:
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
quick-format-unescaped@^4.0.3:
version "4.0.4"
resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7"
integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
quick-lru@^5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz"
@ -7978,6 +8087,11 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
real-require@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78"
integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==
reduce-css-calc@^2.1.8:
version "2.1.8"
resolved "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz"
@ -8252,6 +8366,11 @@ safe-regex-test@^1.0.0:
get-intrinsic "^1.1.3"
is-regex "^1.1.4"
safe-stable-stringify@^2.3.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.1.tgz#34694bd8a30575b7f94792aa51527551bd733d61"
integrity sha512-dVHE6bMtS/bnL2mwualjc6IxEv1F+OCUpA46pKUj6F8uDbUM0jCCulPqRNPSnWwGNKx5etqMjZYdXtrm5KJZGA==
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.0:
version "2.1.2"
resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz"
@ -8302,6 +8421,11 @@ scroll-into-view-if-needed@^2.2.28:
dependencies:
compute-scroll-into-view "^1.0.17"
secure-json-parse@^2.4.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.5.0.tgz#f929829df2adc7ccfb53703569894d051493a6ac"
integrity sha512-ZQruFgZnIWH+WyO9t5rWt4ZEGqCKPwhiw+YbzTwpmT9elgLrLcfuyUiSnwwjUiVy9r4VM3urtbNF1xmEh9IL2w==
semver-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz"
@ -8448,6 +8572,13 @@ smooth-scroll-into-view-if-needed@^1.1.29:
dependencies:
scroll-into-view-if-needed "^2.2.28"
sonic-boom@^3.0.0, sonic-boom@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.2.0.tgz#ce9f2de7557e68be2e52c8df6d9b052e7d348143"
integrity sha512-SbbZ+Kqj/XIunvIAgUZRlqd6CGQYq71tRRbXR92Za8J/R3Yh4Av+TWENiSiEgnlwckYLyP0YZQWVfyNC0dzLaA==
dependencies:
atomic-sleep "^1.0.0"
source-list-map@^2.0.0:
version "2.0.1"
resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz"
@ -8529,6 +8660,11 @@ split.js@^1.6.0:
resolved "https://registry.npmjs.org/split.js/-/split.js-1.6.5.tgz"
integrity sha512-mPTnGCiS/RiuTNsVhCm9De9cCAUsrNFFviRbADdKiiV+Kk8HKp/0fWu7Kr8pi3/yBmsqLFHuXGT9UUZ+CNLwFw==
split2@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809"
integrity sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
@ -8858,6 +8994,13 @@ text-table@^0.2.0:
resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
thread-stream@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.2.0.tgz#310c03a253f729094ce5d4638ef5186dfa80a9e8"
integrity sha512-rUkv4/fnb4rqy/gGy7VuqK6wE1+1DOCOWy4RMeaV69ZHMP11tQKZvZSip1yTgrKCMZzEMcCL/bKfHvSfDHx+iQ==
dependencies:
real-require "^0.2.0"
throat@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz"