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 @@