From 18469b12e2f21c7396db3ee864edbf7ca867f052 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 24 Nov 2024 13:51:29 -0600 Subject: [PATCH] test a new initialization Signed-off-by: eternal-flame-AD --- packages/backend/src/server/web/boot.js | 184 +++++++++++++++--- packages/backend/src/server/web/style.css | 22 +++ packages/backend/src/server/web/systemd.ts | 161 +++++++++++++++ .../src/server/web/views/base-embed.pug | 1 + .../backend/src/server/web/views/base.pug | 1 + 5 files changed, 347 insertions(+), 22 deletions(-) create mode 100644 packages/backend/src/server/web/systemd.ts diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index a04640d993..987eb32cdb 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -5,6 +5,136 @@ 'use strict'; +class TaskDom { + constructor(tty_dom, id, promise) { + this.tty_dom = tty_dom; + this.id = id; + this.promise = promise; + this.started = Date.now(); + this.state = { state: 'running' }; + this.interval = setInterval(() => { + this.render(); + }, 500); + this.persistentDom = null; + promise.then(() => { + this.state = { state: 'done' }; + clearInterval(this.interval); + this.render(); + }).catch((e) => { + this.state = { state: 'failed', message: e.message.toString() }; + clearInterval(this.interval); + this.render(); + }); + } + render() { + switch (this.state.state) { + case 'running': + if (this.persistentDom === null) { + this.persistentDom = this.formatRunning(); + this.tty_dom.appendChild(this.persistentDom); + } + else { + this.persistentDom.innerHTML = this.formatRunning().innerHTML; + } + break; + case 'done': + if (this.persistentDom === null) { + this.persistentDom = this.formatDone(); + this.tty_dom.appendChild(this.persistentDom); + } + else { + this.persistentDom.innerHTML = this.formatDone().innerHTML; + } + break; + case 'failed': + if (this.persistentDom === null) { + this.persistentDom = this.formatFailed(this.state.message); + this.tty_dom.appendChild(this.persistentDom); + } + else { + this.persistentDom.innerHTML = this.formatFailed(this.state.message).innerHTML; + } + break; + } + } + formatRunning() { + const shiftArray = (arr, n) => { + return arr.slice(n).concat(arr.slice(0, n)); + }; + const elapsed_secs = Math.floor((Date.now() - this.started) / 1000); + const stars = shiftArray(['*', '*', '*', ' ', ' ', ' '], elapsed_secs % 6); + const spanStatus = document.createElement('span'); + spanStatus.innerText = stars.join(''); + spanStatus.className = 'tty-status-running'; + const spanMessage = document.createElement('span'); + spanMessage.innerText = `A start job is running for ${this.id} (${elapsed_secs}s / no limit)`; + const div = document.createElement('div'); + div.className = 'tty-line'; + div.innerHTML = '['; + div.appendChild(spanStatus); + div.innerHTML += '] '; + div.appendChild(spanMessage); + return div; + } + formatDone() { + const elapsed_secs = Math.floor((Date.now() - this.started) / 1000); + const spanStatus = document.createElement('span'); + spanStatus.innerText = ' OK '; + spanStatus.className = 'tty-status-ok'; + const spanMessage = document.createElement('span'); + spanMessage.innerText = `Finished ${this.id} in ${elapsed_secs}s`; + const div = document.createElement('div'); + div.className = 'tty-line'; + div.innerHTML = '['; + div.appendChild(spanStatus); + div.innerHTML += '] '; + div.appendChild(spanMessage); + return div; + } + formatFailed(message) { + const elapsed_secs = Math.floor((Date.now() - this.started) / 1000); + const spanStatus = document.createElement('span'); + spanStatus.innerText = 'FAILED'; + spanStatus.className = 'tty-status-failed'; + const spanMessage = document.createElement('span'); + spanMessage.innerText = `Failed ${this.id} in ${elapsed_secs}s: ${message}`; + const div = document.createElement('div'); + div.className = 'tty-line'; + div.innerHTML = '['; + div.appendChild(spanStatus); + div.innerHTML += '] '; + div.appendChild(spanMessage); + return div; + } +} +class Systemd { + constructor() { + this.tty_dom = document.querySelector('#tty'); + console.log('Systemd started'); + } + async start(id, promise) { + const task = new TaskDom(this.tty_dom, id, promise); + await task.promise; + } + async startSync(id, func) { + const task = new TaskDom(this.tty_dom, id, new Promise((resolve, reject) => { + try { + resolve(func()); + } + catch (e) { + reject(e); + } + })); + return await task.promise; + } + emergency_mode() { + const div = document.createElement('div'); + div.className = 'tty-line'; + div.innerText = 'You are in emergency mode. Type Ctrl-Shift-I to view logs.'; + this.tty_dom.appendChild(div); + } +} + // ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので (async () => { window.onerror = (e) => { @@ -16,6 +146,8 @@ renderError('SOMETHING_HAPPENED_IN_PROMISE', e); }; + const systemd = new Systemd(); + let forceError = localStorage.getItem('forceError'); if (forceError != null) { renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.'); @@ -37,7 +169,7 @@ } } - const metaRes = await window.fetch('/api/meta', { + const metaRes = await systemd.start('Fetch /api/meta',window.fetch('/api/meta', { method: 'POST', body: JSON.stringify({}), credentials: 'omit', @@ -45,12 +177,12 @@ headers: { 'Content-Type': 'application/json', }, - }); + })); if (metaRes.status !== 200) { renderError('META_FETCH'); return; } - const meta = await metaRes.json(); + const meta = await systemd.start('Parse /api/meta', metaRes.json()); const v = meta.version; if (v == null) { renderError('META_FETCH_V'); @@ -63,7 +195,7 @@ lang = 'en-US'; } - const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`); + const localRes = await systemd.start(window.fetch(`/assets/locales/${lang}.${v}.json`)); if (localRes.status === 200) { localStorage.setItem('lang', lang); localStorage.setItem('locale', await localRes.text()); @@ -86,10 +218,10 @@ // タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある if (document.readyState !== 'loading') { - importAppScript(); + systemd.start('import App Script', importAppScript()); } else { window.addEventListener('DOMContentLoaded', () => { - importAppScript(); + systemd.start('import App Script', importAppScript()); }); } //#endregion @@ -97,19 +229,21 @@ //#region Theme const theme = localStorage.getItem('theme'); if (theme) { - for (const [k, v] of Object.entries(JSON.parse(theme))) { - document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); + await systemd.startSync('Apply theme', () => { + for (const [k, v] of Object.entries(JSON.parse(theme))) { + document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); - // HTMLの theme-color 適用 - if (k === 'htmlThemeColor') { - for (const tag of document.head.children) { - if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { - tag.setAttribute('content', v); - break; + // HTMLの theme-color 適用 + if (k === 'htmlThemeColor') { + for (const tag of document.head.children) { + if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { + tag.setAttribute('content', v); + break; + } } } } - } + }); } const colorScheme = localStorage.getItem('colorScheme'); if (colorScheme) { @@ -134,15 +268,19 @@ const customCss = localStorage.getItem('customCss'); if (customCss && customCss.length > 0) { - const style = document.createElement('style'); - style.innerHTML = customCss; - document.head.appendChild(style); + await systemd.startSync('Apply custom CSS', () => { + const style = document.createElement('style'); + style.innerHTML = customCss; + document.head.appendChild(style); + }); } async function addStyle(styleText) { - let css = document.createElement('style'); - css.appendChild(document.createTextNode(styleText)); - document.head.appendChild(css); + await systemd.startSync('Apply custom Style', () => { + let css = document.createElement('style'); + css.appendChild(document.createTextNode(styleText)); + document.head.appendChild(css); + }); } async function renderError(code, details) { @@ -151,6 +289,8 @@ await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); } + systemd.emergency_mode(); + let errorsElement = document.getElementById('errors'); if (!errorsElement) { @@ -160,7 +300,7 @@ -

Failed to load
読み込みに失敗しました

+

You are in emergency mode! Failed to load
読み込みに失敗しました

diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css index 81f1838a4a..e40912f5ee 100644 --- a/packages/backend/src/server/web/style.css +++ b/packages/backend/src/server/web/style.css @@ -9,6 +9,28 @@ html { color: var(--MI_THEME-fg); } +#tty { + font-family: monospace; + z-index: 9999; + opacity: 1; +} + +#tty > tty-line { + display: block; +} + +#tty > tty-line .tty-status-ok { + color: green; +} + +#tty > tty-line .tty-status-error { + color: darkred; +} + +#tty > tty-line .tty-status-running { + color: red; +} + #splash { position: fixed; z-index: 10000; diff --git a/packages/backend/src/server/web/systemd.ts b/packages/backend/src/server/web/systemd.ts new file mode 100644 index 0000000000..80f5950273 --- /dev/null +++ b/packages/backend/src/server/web/systemd.ts @@ -0,0 +1,161 @@ +class TaskDom{ + private started = Date.now(); + private state: { + state: 'running' + } | { + state: 'done' + } | { + state: 'failed' + message: string + } = { state: 'running' }; + + private interval = setInterval(() => { + this.render(); + }, 500); + + private persistentDom : HTMLDivElement | null = null; + + constructor( + private tty_dom: HTMLDivElement, + private id: string, + public promise: Promise) { + + promise.then(() => { + this.state = { state: 'done' }; + clearInterval(this.interval); + this.render(); + }).catch((e) => { + this.state = { state: 'failed', message: e.message.toString() }; + clearInterval(this.interval); + this.render(); + }); + } + + private render() { + switch (this.state.state) { + case 'running': + if (this.persistentDom === null) { + this.persistentDom = this.formatRunning(); + this.tty_dom.appendChild(this.persistentDom); + } else { + this.persistentDom.innerHTML = this.formatRunning().innerHTML; + } + break; + case 'done': + if (this.persistentDom === null) { + this.persistentDom = this.formatDone(); + this.tty_dom.appendChild(this.persistentDom); + } else { + this.persistentDom.innerHTML = this.formatDone().innerHTML; + } + break; + case 'failed': + if (this.persistentDom === null) { + this.persistentDom = this.formatFailed(this.state.message); + this.tty_dom.appendChild(this.persistentDom); + } else { + this.persistentDom.innerHTML = this.formatFailed(this.state.message).innerHTML; + } + break; + } + } + + private formatRunning(): HTMLDivElement { + const shiftArray = (arr: T[], n: number): T[] => { + return arr.slice(n).concat(arr.slice(0, n)); + }; + + const elapsed_secs = Math.floor((Date.now() - this.started) / 1000); + const stars = shiftArray(['*', '*', '*', ' ', ' ', ' '], elapsed_secs % 6); + + const spanStatus = document.createElement('span'); + + spanStatus.innerText = stars.join(''); + spanStatus.className = 'tty-status-running'; + + const spanMessage = document.createElement('span'); + spanMessage.innerText = `A start job is running for ${this.id} (${elapsed_secs}s / no limit)`; + + const div = document.createElement('div'); + div.className = 'tty-line'; + div.innerHTML = '['; + div.appendChild(spanStatus); + div.innerHTML += '] '; + div.appendChild(spanMessage); + + return div; + } + + private formatDone(): HTMLDivElement { + const elapsed_secs = Math.floor((Date.now() - this.started) / 1000); + + const spanStatus = document.createElement('span'); + spanStatus.innerText = ' OK '; + spanStatus.className = 'tty-status-ok'; + + const spanMessage = document.createElement('span'); + spanMessage.innerText = `Finished ${this.id} in ${elapsed_secs}s`; + + const div = document.createElement('div'); + div.className = 'tty-line'; + div.innerHTML = '['; + div.appendChild(spanStatus); + div.innerHTML += '] '; + div.appendChild(spanMessage); + + return div; + } + + private formatFailed(message: string): HTMLDivElement { + const elapsed_secs = Math.floor((Date.now() - this.started) / 1000); + + const spanStatus = document.createElement('span'); + spanStatus.innerText = 'FAILED'; + spanStatus.className = 'tty-status-failed'; + + const spanMessage = document.createElement('span'); + spanMessage.innerText = `Failed ${this.id} in ${elapsed_secs}s: ${message}`; + + const div = document.createElement('div'); + div.className = 'tty-line'; + div.innerHTML = '['; + div.appendChild(spanStatus); + div.innerHTML += '] '; + div.appendChild(spanMessage); + + return div; + } +} + +export class Systemd { + private tty_dom: HTMLDivElement; + constructor() { + this.tty_dom = document.querySelector('#tty') as HTMLDivElement; + + console.log('Systemd started'); + } + + async start(id: string, promise: Promise): Promise { + const task = new TaskDom(this.tty_dom, id, promise); + await task.promise; + } + + async startSync(id: string, func: () => T): Promise { + const task = new TaskDom(this.tty_dom, id, new Promise((resolve, reject) => { + try { + resolve(func()); + } catch (e) { + reject(e); + } + })); + + return await task.promise; + } + + public emergency_mode() { + const div = document.createElement('div'); + div.className = 'tty-line'; + div.innerText = 'You are in emergency mode. Type Ctrl-Shift-I to view logs.'; + this.tty_dom.appendChild(div); + } +} \ No newline at end of file diff --git a/packages/backend/src/server/web/views/base-embed.pug b/packages/backend/src/server/web/views/base-embed.pug index bc1352ace4..c434f33747 100644 --- a/packages/backend/src/server/web/views/base-embed.pug +++ b/packages/backend/src/server/web/views/base-embed.pug @@ -56,6 +56,7 @@ html(class='embed') br | Please turn on your JavaScript div#splash + div#tty img#splashIcon(src= icon || '/static-assets/splash.png') div#splashSpinner Loading... diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 8fbfdacabd..f8e43368cd 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -73,6 +73,7 @@ html br | Please turn on your JavaScript div#splash + div#tty img#splashIcon(src= icon || '/static-assets/splash.png') div#splashSpinner Loading...