Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server: Resolves #9355: Add sync debug page to Joplin Server #9368

Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
97e3a51
Draft: Create debug report route on joplin server
personalizedrefrigerator Nov 21, 2023
d084136
Switch to seperate sync debug page
personalizedrefrigerator Nov 25, 2023
77cef1b
Move new JS to separate .js file
personalizedrefrigerator Nov 25, 2023
7ca0ab2
Update labels
personalizedrefrigerator Nov 25, 2023
f4dcbbf
Disable submit button while processing
personalizedrefrigerator Nov 25, 2023
f864283
Merge branch 'dev' into pr/joplin-server-debug-report
personalizedrefrigerator Nov 25, 2023
21ea1cb
Set up TypeScript building for the public/js directory
personalizedrefrigerator Nov 28, 2023
e73d8c1
Fix .gitignore
personalizedrefrigerator Nov 28, 2023
d127495
Simplify tsconfig.json
personalizedrefrigerator Nov 28, 2023
fdbe833
Remove debug logging
personalizedrefrigerator Nov 28, 2023
7cc313b
Fix indentation
personalizedrefrigerator Nov 28, 2023
bff383b
eslintignore: Ignore generated .d.ts files in server/public/js
personalizedrefrigerator Nov 28, 2023
0cf24fe
Compile TypeScript using Gulp
personalizedrefrigerator Nov 28, 2023
1c26a35
Fix build
personalizedrefrigerator Nov 28, 2023
a0ab036
Adjust CSS naming to be closer to RCSS
personalizedrefrigerator Nov 29, 2023
b10f79f
Merge remote-tracking branch 'upstream/dev' into pr/joplin-server-deb…
personalizedrefrigerator Dec 2, 2023
48c5e02
Move tsc to package.json
personalizedrefrigerator Dec 2, 2023
aa2dd7d
Additional documentation
personalizedrefrigerator Dec 5, 2023
117db82
Simplify additional documentation
personalizedrefrigerator Dec 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,7 @@ packages/renderer/index.js
packages/renderer/noteStyle.js
packages/renderer/pathUtils.js
packages/renderer/utils.js
packages/server/public/js/index/sync_debug.js
packages/tools/build-release-stats.test.js
packages/tools/build-release-stats.js
packages/tools/build-translation.js
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,7 @@ packages/renderer/index.js
packages/renderer/noteStyle.js
packages/renderer/pathUtils.js
packages/renderer/utils.js
packages/server/public/js/index/sync_debug.js
packages/tools/build-release-stats.test.js
packages/tools/build-release-stats.js
packages/tools/build-translation.js
Expand Down
4 changes: 3 additions & 1 deletion packages/server/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ db-*.sqlite
logs/
tests/temp/
temp/
.env
.env
public/**/*.d.ts
public/js/tsconfig.tsbuildinfo
4 changes: 2 additions & 2 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
"devDropDb": "node dist/app.js --env dev --drop-db",
"start": "node dist/app.js",
"generateTypes": "rm -f db-buildTypes.sqlite && yarn run start --env buildTypes migrate latest && node dist/tools/generateTypes.js && mv db-buildTypes.sqlite schema.sqlite",
"tsc": "tsc --project tsconfig.json",
"tsc": "tsc --build",
personalizedrefrigerator marked this conversation as resolved.
Show resolved Hide resolved
"test": "jest --verbose=false",
"test-ci": "yarn test",
"test-debug": "node --inspect node_modules/.bin/jest -- --verbose=false",
"clean": "gulp clean",
"populateDatabase": "JOPLIN_TESTS_SERVER_DB=pg node dist/utils/testing/populateDatabase",
"stripeListen": "stripe listen --forward-to http://joplincloud.local:22300/stripe/webhook",
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json"
"watch": "tsc --build --watch --preserveWatchOutput"
},
"dependencies": {
"@aws-sdk/client-s3": "3.296.0",
Expand Down
37 changes: 37 additions & 0 deletions packages/server/public/css/index/sync_debug.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@


personalizedrefrigerator marked this conversation as resolved.
Show resolved Hide resolved
#debug-tools-container {
--border-color: #444;
}

#debug-tools-container legend {
font-weight: bold;
}

#debug-tools-container fieldset {
border-left: 1px solid var(--border-color);
padding-left: 5px;
margin-top: 14px;
/* Required for proper overflow. See
https://stackoverflow.com/a/29499408 */
min-width: 0;
}

#debug-tools-container .output {
border: 1px solid var(--border-color);
border-radius: 5px;
margin: 5px;
padding: 5px;
}

#debug-tools-container .output pre {
white-space: pre-wrap;
word-wrap: break-word;
max-height: 500px;
overflow-y: auto;
border-radius: 5px;
}

#debug-tools-container .output.empty {
display: none;
}
157 changes: 157 additions & 0 deletions packages/server/public/js/index/sync_debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@

const itemLinkToId = (link: string) => {
// Examples of supported links:
// - joplin://x-callback-url/openFolder?id=6c8caeec01a34c0f95487a04ebb79cb9
// - joplin://x-callback-url/openNote?id=6c8caeec01a34c0f95487a04ebb79cb9
// - :/6c8caeec01a34c0f95487a04ebb79cb9
// - /home/user/.config/joplin-desktop/resources/6c8caeec01a34c0f95487a04ebb79cb9.svg
// - [title](:/6c8caeec01a34c0f95487a04ebb79cb9)
const linkRegexs = [
// External item
/^joplin:\/\/x-callback-url\/(?:openFolder|openNote)\?id=(\w+)$/,

// Internal links
/^\/:(\w+)$/,
/^!?\[.*\]\(:\/(\w+)\)$/,

// Resource file URLs
/^(?:file:\/\/)?.*[/\\]resources[/\\](\w+)\.\w+$/,
];

for (const regex of linkRegexs) {
const match = regex.exec(link);
if (match) {
return match[1];
}
}

return link;
};

type OnItemCheckerSubmitCallback = (
itemId: string, outputHeading: HTMLElement, outputDetails: HTMLElement,
)=> Promise<void>;

const setUpItemChecker = (parent: HTMLElement, onSubmit: OnItemCheckerSubmitCallback) => {
const button = document.createElement('button');
button.innerText = 'Submit';

const input = parent.querySelector('input');
const outputContainer = document.createElement('div');
outputContainer.classList.add('output', 'empty');

const outputHeading = document.createElement('h3');
const outputDetailsContainer = document.createElement('details');
const outputDetailsContent = document.createElement('pre');

outputHeading.setAttribute('aria-live', 'polite');

outputDetailsContainer.appendChild(outputDetailsContent);
outputContainer.replaceChildren(outputHeading, outputDetailsContainer);

button.onclick = async () => {
outputHeading.innerText = '⏳ Loading...';
outputDetailsContent.innerText = '';
outputContainer.classList.remove('error');
outputContainer.classList.remove('empty');
outputContainer.classList.add('loading');
button.disabled = true;

try {
await onSubmit(itemLinkToId(input.value), outputHeading, outputDetailsContent);
outputContainer.classList.remove('loading');
} catch (error) {
outputHeading.innerText = `⚠️ Error: ${error}`;
outputContainer.classList.add('error');
}

button.disabled = false;
};

parent.appendChild(button);
parent.appendChild(outputContainer);
};

const checkForItemOnServer = async (
itemId: string, outputHeadingElement: HTMLElement, outputDetailsElement: HTMLElement,
) => {
const fetchResult = await fetch(`/api/items/root:/${encodeURIComponent(itemId)}.md:/`);

if (fetchResult.ok) {
const result = await fetchResult.text();
outputHeadingElement.innerText = 'Item found!';
outputDetailsElement.innerText = result;
} else {
outputHeadingElement.innerText = `Item ${itemId}: ${fetchResult.statusText}`;
outputDetailsElement.innerText = '';
}
};

const checkForItemInInitialDiff = async (
itemId: string, outputHeadingElement: HTMLElement, outputDetailsElement: HTMLElement,
) => {
let cursor: string|undefined = undefined;

const waitForTimeout = (timeout: number) => {
return new Promise<void>(resolve => {
setTimeout(() => resolve(), timeout);
});
};

const readDiff = async function*() {
let hasMore = true;
let page = 1;
while (hasMore) {
const fetchResult = await fetch(
`/api/items/root/delta${
cursor ? `?cursor=${encodeURIComponent(cursor)}` : ''
}`,
);
if (!fetchResult.ok) {
throw new Error(`Error fetching items: ${fetchResult.statusText}`);
}

const json = await fetchResult.json();
hasMore = json.has_more;
cursor = json.cursor;

for (const item of json.items) {
yield item;
}

outputHeadingElement.innerText = `Processing page ${page++}...`;

// Avoid sending requests too frequently
await waitForTimeout(200); // ms
}
};

const allItems = [];
const matches = [];
let stoppedEarly = false;
for await (const item of readDiff()) {
// Include console logging to provide more information if readDiff() fails.
// eslint-disable-next-line no-console
console.log('Checking item', item);

if (item.item_name === `${itemId}.md`) {
matches.push(item);
stoppedEarly = true;
}
allItems.push(item);
}

outputHeadingElement.innerText
= matches.length > 0 ? 'Found in initial sync diff' : `Item ${itemId}: Not in initial sync diff`;

const stoppedEarlyDescription = (
stoppedEarly ? '\n Stopped fetching items after finding a match. Item list is incomplete.' : ''
);
outputDetailsElement.innerText
= JSON.stringify(allItems, undefined, ' ') + stoppedEarlyDescription;
};

document.addEventListener('DOMContentLoaded', () => {
setUpItemChecker(document.querySelector('#note-on-server-check'), checkForItemOnServer);
setUpItemChecker(document.querySelector('#note-in-diff-check'), checkForItemInInitialDiff);
});
19 changes: 19 additions & 0 deletions packages/server/public/js/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"module": "ES6",

// "declaration" and "composite" must be `true` to use TypeScript
// projects.
"declaration": true,
"composite": true,
"outDir": "./"
},
"rootDir": ".",
"include": [
"**/*.ts"
],
"exclude": [
"*.test.ts"
]
}
24 changes: 24 additions & 0 deletions packages/server/src/routes/index/sync_debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { SubPath } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
import { ErrorMethodNotAllowed } from '../../utils/errors';
import { contextSessionId } from '../../utils/requestUtils';
import defaultView from '../../utils/defaultView';

const router = new Router(RouteType.Web);

router.get('sync_debug', async (_path: SubPath, ctx: AppContext) => {
contextSessionId(ctx);

if (ctx.method !== 'GET') {
throw new ErrorMethodNotAllowed();
}

const view = defaultView('sync_debug', 'Sync Debug');
view.cssFiles = ['index/sync_debug'];
view.jsFiles = ['index/sync_debug'];
return view;
});

export default router;
2 changes: 2 additions & 0 deletions packages/server/src/routes/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import adminUserDeletions from './admin/user_deletions';
import adminUsers from './admin/users';

import indexChanges from './index/changes';
import indexSyncDebug from './index/sync_debug';
import indexHelp from './index/help';
import indexHome from './index/home';
import indexItems from './index/items';
Expand Down Expand Up @@ -56,6 +57,7 @@ const routes: Routers = {
'admin/users': adminUsers,

'changes': indexChanges,
'sync_debug': indexSyncDebug,
'help': indexHelp,
'home': indexHome,
'items': indexItems,
Expand Down
24 changes: 24 additions & 0 deletions packages/server/src/views/index/sync_debug.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<h1 class="title">Debugging</h1>
<p>
Use these tools to debug sync issues with Joplin Server.
</p>

<div id="debug-tools-container">
<fieldset id="note-on-server-check">
<legend>
Check if an item (note, notebook, resource, tag) is present on the server
</legend>
<label for="note-on-server-input">External link or ID:</label>
<input type="text" id="note-on-server-input"/>
<noscript>JavaScript required</noscript>
</fieldset>

<fieldset id="note-in-diff-check">
<legend>
Check if an item is sent to Joplin to download on an initial sync
</legend>
<label for="note-on-server-input">External link or ID:</label>
<input type="text" id="note-in-diff-input"/>
<noscript>JavaScript required</noscript>
</fieldset>
</div>
4 changes: 4 additions & 0 deletions packages/server/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@
],
"exclude": [
"**/node_modules",
"public/js/"
],
"references": [
{ "path": "./public/js/" }
]
}
3 changes: 2 additions & 1 deletion packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ module.exports = {
'packages/app-mobile/ios/**',
'packages/fork-sax/**',
'packages/lib/plugin_types/**',
'packages/server/**',
'packages/server/src/**',
'packages/server/dist/**',
'packages/utils/**',
],
}).filter(f => !f.endsWith('.d.ts'));
Expand Down
Loading