-
-
Notifications
You must be signed in to change notification settings - Fork 5.2k
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
Changes from 14 commits
97e3a51
d084136
77cef1b
7ca0ab2
f4dcbbf
f864283
21ea1cb
e73d8c1
d127495
fdbe833
7cc313b
bff383b
0cf24fe
1c26a35
a0ab036
b10f79f
48c5e02
aa2dd7d
117db82
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
} |
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); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"extends": "../../../../tsconfig.json", | ||
"compilerOptions": { | ||
// ES6 (when there are no imports/exports) generates code that can be run in a web browser. | ||
"module": "ES6", | ||
"declaration": false | ||
}, | ||
"rootDir": ".", | ||
"include": [ | ||
"**/*.ts" | ||
], | ||
"exclude": [ | ||
"*.test.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; |
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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,5 +10,6 @@ | |
], | ||
"exclude": [ | ||
"**/node_modules", | ||
], | ||
"public/js/" | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,7 @@ | |
"scripts": { | ||
"tsc": "tsc --project tsconfig.json", | ||
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json", | ||
"build": "yarn run tsc", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is currently needed because A similar line is added in #9360. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer we keep the existing order - install, build, tsc to keeps things more predictable. So far we managed to do this, so we can probably find a solution here too? In some cases I think we use ts-node to run TypeScript files before they've been built, which would be fine. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is currently required to use There does seem to be a way to run It also might not be sufficient — There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like we're also running I'm not sure using |
||
"test": "jest --verbose=false", | ||
"test-ci": "yarn test" | ||
}, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a fan of moving this to gulp because that's yet another level of indirection. And we used to use gulp for watching and it caused various issues so I'd prefer if we stay as close as possible to the tsc binary for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here are some options:
tsc
for watching/building only requires one call (e.g.tsc --build --watch --preserveWatchOutput
ortsc --build
)..d.ts
files in the referenced project.tsconfig.json
file (other than by changing the current working directory). As theserver
package currently specifies atsconfig.json
forbuild
/watch
commands, I suspect that specifying the directory with the roottsconfig.json
is (or was) important.tsc
withexecCommand
fromgulp
(or similar method: Calltsc
from acompile-typescript.ts
file run byts-node
).tsc
for both projects during thetsc
step (rather than duringbuild
)tsc --watch --project project1 & ; tsc --watch --project project2
)@joplin/utils
to makeexecCommand
available during thebuild
step or modifying@joplin/utils
to also export TypeScript files and moving to a TypeScript gulp configuration file. (Alternatively, re-implementingexecCommand
.)watch-public
command for thepublic/
directory and compile both withtsc --project ./tsconfig.jsoon && tsc --project ./public/tsconfig.json
.tsc
from JavaScript or significantly changing build flags.yarn run watch
won't watch the public directory.webpack
orrollup
and abuild-bundled-js
command that runs duringyarn build
.import
statements in TypeScript that will be built.app-mobile/
)