walover-line-harness-gui 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/server/cli-version.js +52 -17
- package/src/server/guide.js +54 -10
- package/src/server/prompt-contract.js +5 -3
- package/src/server/pty.js +8 -1
- package/src/server/state.js +8 -2
- package/src/web/index.html +128 -20
package/package.json
CHANGED
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
* そのため**動作確認済みの版を固定して呼ぶ**。
|
|
7
7
|
*
|
|
8
8
|
* ただし固定したまま黙っていると、本家の修正に永久に追いつかない。
|
|
9
|
-
* 起動時に npm の `latest`
|
|
10
|
-
*
|
|
9
|
+
* 起動時に npm の `latest` と比べ、差があれば知らせる。自動では追従しない。
|
|
10
|
+
* 追従には契約の再照合(`npm run check:contract`)が要る。
|
|
11
|
+
*
|
|
12
|
+
* **強さを取り違えない。** 新しい版が出ているだけなら参加者はこのまま進めてよく、
|
|
13
|
+
* 警告として出すと進めてよいのに手が止まる。一方、固定版が npm で見つからないのは
|
|
14
|
+
* 人が確かめる必要がある。この区別を {@link VersionNotice} の tone で表す。
|
|
11
15
|
*
|
|
12
16
|
* > npm パッケージ(`create-line-harness`)とアプリ本体(`line-oss-crm`)は
|
|
13
17
|
* > バージョンが別で、プロンプトは npm パッケージ側にある。
|
|
@@ -64,7 +68,23 @@ export async function resolveVersion(spec = "latest", options = {}) {
|
|
|
64
68
|
* @property {string} pinned 固定している版
|
|
65
69
|
* @property {string|null} latest npm の最新版。取れなければ null
|
|
66
70
|
* @property {"pinned-is-latest"|"newer-available"|"pinned-is-newer"|"unknown"} status
|
|
67
|
-
* @property {
|
|
71
|
+
* @property {VersionNotice|null} notice 画面に出すもの
|
|
72
|
+
* @property {string|null} warning ターミナルに出す平文(notice から作る)
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 画面に出すときの中身。
|
|
77
|
+
*
|
|
78
|
+
* **読者は非開発者の勉強会参加者。** 版が古いこと自体は参加者の関心事ではなく、
|
|
79
|
+
* 知りたいのは「自分はこのまま進めてよいのか」だけ。
|
|
80
|
+
* headline はその答えを先に書き、版番号のような事情は detail に送る。
|
|
81
|
+
*
|
|
82
|
+
* tone は強さ。`info` は畳んで出してよいもの、`warn` は人の判断が要るもの。
|
|
83
|
+
*
|
|
84
|
+
* @typedef {object} VersionNotice
|
|
85
|
+
* @property {"info"|"warn"} tone
|
|
86
|
+
* @property {string} headline 参加者が最初に読む一文
|
|
87
|
+
* @property {string} detail 事情の説明。畳んだ中に置く
|
|
68
88
|
*/
|
|
69
89
|
|
|
70
90
|
/**
|
|
@@ -84,6 +104,7 @@ export async function checkCliVersion(options = {}) {
|
|
|
84
104
|
pinned,
|
|
85
105
|
latest: null,
|
|
86
106
|
status: "unknown",
|
|
107
|
+
notice: null,
|
|
87
108
|
warning: null,
|
|
88
109
|
};
|
|
89
110
|
}
|
|
@@ -91,31 +112,45 @@ export async function checkCliVersion(options = {}) {
|
|
|
91
112
|
const order = compareVersions(pinned, latest);
|
|
92
113
|
|
|
93
114
|
if (order === 0) {
|
|
94
|
-
return { pinned, latest, status: "pinned-is-latest", warning: null };
|
|
115
|
+
return { pinned, latest, status: "pinned-is-latest", notice: null, warning: null };
|
|
95
116
|
}
|
|
96
117
|
|
|
97
118
|
if (order < 0) {
|
|
98
|
-
|
|
119
|
+
// 新しい版が出ているだけ。**参加者はこのまま進めてよい。**
|
|
120
|
+
// ここを警告として強く出すと、進めてよいのに手が止まる
|
|
121
|
+
return withWarning({
|
|
99
122
|
pinned,
|
|
100
123
|
latest,
|
|
101
124
|
status: "newer-available",
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
125
|
+
notice: {
|
|
126
|
+
tone: "info",
|
|
127
|
+
headline: `このまま進めて構いません。動作確認済みの v${pinned} でセットアップします。`,
|
|
128
|
+
detail:
|
|
129
|
+
`本家 CLI の新しい版 v${latest} が出ています(このツールが確認済みなのは v${pinned})。` +
|
|
130
|
+
"追従にはプロンプト契約の再照合と検証が要るため、自動では追いかけません。",
|
|
131
|
+
},
|
|
132
|
+
});
|
|
107
133
|
}
|
|
108
134
|
|
|
109
|
-
// 固定版のほうが新しい。npm
|
|
110
|
-
|
|
135
|
+
// 固定版のほうが新しい。npm 側が取り下げられた場合などに起きる。
|
|
136
|
+
// これは**人が確かめる必要がある**ので、弱めない
|
|
137
|
+
return withWarning({
|
|
111
138
|
pinned,
|
|
112
139
|
latest,
|
|
113
140
|
status: "pinned-is-newer",
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"npm
|
|
117
|
-
|
|
118
|
-
|
|
141
|
+
notice: {
|
|
142
|
+
tone: "warn",
|
|
143
|
+
headline: "固定している版が npm で見つかりません。開発者に連絡してください。",
|
|
144
|
+
detail:
|
|
145
|
+
`固定している v${pinned} が npm の最新 v${latest} より新しくなっています。` +
|
|
146
|
+
"npm 側で取り下げられた可能性があります。動作未確認のため、確認が必要です。",
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** ターミナル用の平文。画面と同じ内容を1つの文字列にする */
|
|
152
|
+
function withWarning(status) {
|
|
153
|
+
return { ...status, warning: `${status.notice.headline}\n${status.notice.detail}` };
|
|
119
154
|
}
|
|
120
155
|
|
|
121
156
|
/** @type {Promise<VersionStatus>|null} */
|
package/src/server/guide.js
CHANGED
|
@@ -33,6 +33,27 @@ const CF_R2 = "https://dash.cloudflare.com/?to=/:account/r2/overview";
|
|
|
33
33
|
* @property {{url: string, label: string}} [manual] 手順書の該当箇所
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* R2 の有効化。
|
|
38
|
+
*
|
|
39
|
+
* 実測で**最初のブロッカーであり、最大の脱落ポイント**だった箇所。
|
|
40
|
+
* 始める前の前提条件({@link PREFLIGHT})としても、本家が実行中に訊いてくる
|
|
41
|
+
* `r2Enabled` の場面でも、同じものを見せる。
|
|
42
|
+
*
|
|
43
|
+
* @type {Guide}
|
|
44
|
+
*/
|
|
45
|
+
const R2_GUIDE = {
|
|
46
|
+
steps: [
|
|
47
|
+
"Cloudflare にログインする",
|
|
48
|
+
"支払い情報(クレジットカード)を登録する",
|
|
49
|
+
"R2 の画面で「Purchase R2」などのボタンを押し、利用を開始する",
|
|
50
|
+
"もう一度 R2 の画面を開き、バケットの一覧(空でよい)が出ることを確かめる",
|
|
51
|
+
],
|
|
52
|
+
warn: "「カード登録済み」と「R2 有効化済み」は別です。カードを登録しただけで進むと、あとの手順で必ず失敗します(実測でも、カード登録済みのアカウントが code: 10042 で止まりました)。",
|
|
53
|
+
links: [{ label: "Cloudflare の R2 を開く", url: CF_R2 }],
|
|
54
|
+
manual: { url: `${KIT}/cloudflare.html#step3`, label: "1. Cloudflare の準備 — Step 3:R2 を有効化する" },
|
|
55
|
+
};
|
|
56
|
+
|
|
36
57
|
/**
|
|
37
58
|
* プロンプト id をキーにする。フォームの欄も、実行中に人が答える場面も同じ扱いにできる。
|
|
38
59
|
* @type {Record<string, Guide>}
|
|
@@ -154,16 +175,7 @@ export const GUIDE = {
|
|
|
154
175
|
},
|
|
155
176
|
|
|
156
177
|
// ── 実行中に人が答える場面 ───────────────────────────────────────
|
|
157
|
-
r2Enabled:
|
|
158
|
-
steps: [
|
|
159
|
-
"Cloudflare ダッシュボードの R2 を開く",
|
|
160
|
-
"支払い情報(クレジットカード)を登録する",
|
|
161
|
-
"R2 の利用を開始する",
|
|
162
|
-
],
|
|
163
|
-
warn: "「カード登録済み」と「R2 有効化済み」は別です。カードを登録しただけで進むと、あとの手順で必ず失敗します。",
|
|
164
|
-
links: [{ label: "Cloudflare の R2 を開く", url: CF_R2 }],
|
|
165
|
-
manual: { url: `${KIT}/cloudflare.html`, label: "1. Cloudflare の準備" },
|
|
166
|
-
},
|
|
178
|
+
r2Enabled: R2_GUIDE,
|
|
167
179
|
|
|
168
180
|
subdomainManualCheck: {
|
|
169
181
|
steps: [
|
|
@@ -175,6 +187,38 @@ export const GUIDE = {
|
|
|
175
187
|
},
|
|
176
188
|
};
|
|
177
189
|
|
|
190
|
+
/**
|
|
191
|
+
* 始める前に済ませておくこと。
|
|
192
|
+
*
|
|
193
|
+
* R2 の有効化は、本家が実行の途中で「R2 の有効化は終わりましたか?」と訊く。
|
|
194
|
+
* だが実機では、**そこで初めて気づいて手が止まる参加者が多かった**。
|
|
195
|
+
* 有効化には別タブでの作業とクレジットカードの登録が要るので、
|
|
196
|
+
* 入力を全部済ませたあとに知らされるのでは遅い。
|
|
197
|
+
*
|
|
198
|
+
* フォームより前に出し、**開始の前提条件**として扱う。
|
|
199
|
+
* 実行中の `r2Enabled` の場面は残す(そこで初めて出すのをやめるだけ)。
|
|
200
|
+
*
|
|
201
|
+
* @typedef {object} Prerequisite
|
|
202
|
+
* @property {string} id 対応するプロンプト id
|
|
203
|
+
* @property {string} title 画面の見出し
|
|
204
|
+
* @property {string} body なぜ先にやるのか
|
|
205
|
+
* @property {string} confirmLabel 参加者に確認させる文
|
|
206
|
+
* @property {Guide} guide 作業の手順
|
|
207
|
+
*
|
|
208
|
+
* @type {Prerequisite[]}
|
|
209
|
+
*/
|
|
210
|
+
export const PREFLIGHT = [
|
|
211
|
+
{
|
|
212
|
+
id: "r2Enabled",
|
|
213
|
+
title: "始める前に:Cloudflare の R2 を有効化してください",
|
|
214
|
+
body:
|
|
215
|
+
"R2 は画像などの保管場所です(10GB まで無料)。有効化しないまま始めると、" +
|
|
216
|
+
"LINE の値をすべて入力し終えたあと、終盤の保管場所づくりで必ず失敗します。",
|
|
217
|
+
confirmLabel: "R2 を有効化しました(R2 の画面にバケットの一覧が出ています)",
|
|
218
|
+
guide: R2_GUIDE,
|
|
219
|
+
},
|
|
220
|
+
];
|
|
221
|
+
|
|
178
222
|
/** @param {string} promptId */
|
|
179
223
|
export function guideFor(promptId) {
|
|
180
224
|
return GUIDE[promptId] ?? null;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
/** 契約が対象とする本家 CLI のバージョン。ズレの検知は #5 で行う */
|
|
26
|
-
export const CONTRACT_CLI_VERSION = "0.2.
|
|
26
|
+
export const CONTRACT_CLI_VERSION = "0.2.12";
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* @typedef {object} PromptSpec
|
|
@@ -133,7 +133,9 @@ export const PROMPTS = [
|
|
|
133
133
|
id: "lineChannelSecret",
|
|
134
134
|
message: "チャネルシークレット(英数字)",
|
|
135
135
|
kind: "text",
|
|
136
|
-
|
|
136
|
+
// 0.2.12 で本家が p.text → p.password にした。値行は伏字(▪)になり、
|
|
137
|
+
// placeholder は無くなった。宣言したままだと第二の照合が外れて止まる。
|
|
138
|
+
// message が一意なので、外しても取り違えは起きない
|
|
137
139
|
secret: true,
|
|
138
140
|
answer: "form",
|
|
139
141
|
field: "lineChannelSecret",
|
|
@@ -148,7 +150,7 @@ export const PROMPTS = [
|
|
|
148
150
|
id: "lineChannelAccessToken",
|
|
149
151
|
message: "チャネルアクセストークン(長期)",
|
|
150
152
|
kind: "text",
|
|
151
|
-
|
|
153
|
+
// 同上。0.2.12 で p.password になった
|
|
152
154
|
secret: true,
|
|
153
155
|
answer: "form",
|
|
154
156
|
field: "lineChannelAccessToken",
|
package/src/server/pty.js
CHANGED
|
@@ -201,8 +201,15 @@ function resolveLaunch(command, args) {
|
|
|
201
201
|
if (command !== "npx" && command !== "npm") return { command, args };
|
|
202
202
|
|
|
203
203
|
// /d = AutoRun をスキップ、/s = 引用符の扱いを素直にする
|
|
204
|
+
//
|
|
205
|
+
// chcp 65001 はコンソールのコードページを UTF-8 にするため。
|
|
206
|
+
// 日本語環境の既定は CP932 で、本家が出す日本語がそのままだと文字化けする
|
|
207
|
+
// (勉強会の実機で確認)。
|
|
204
208
|
const shell = process.env.ComSpec || "cmd.exe";
|
|
205
|
-
return {
|
|
209
|
+
return {
|
|
210
|
+
command: shell,
|
|
211
|
+
args: ["/d", "/s", "/c", `chcp 65001 >nul && ${[command, ...args].join(" ")}`],
|
|
212
|
+
};
|
|
206
213
|
}
|
|
207
214
|
|
|
208
215
|
/**
|
package/src/server/state.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
validateAnswer,
|
|
20
20
|
crossCheckAnswers,
|
|
21
21
|
} from "./prompt-contract.js";
|
|
22
|
-
import { guideFor, GUIDE } from "./guide.js";
|
|
22
|
+
import { guideFor, GUIDE, PREFLIGHT } from "./guide.js";
|
|
23
23
|
|
|
24
24
|
/** @type {{ answers: Record<string, string|boolean>, run: SetupRun|null }} */
|
|
25
25
|
const state = {
|
|
@@ -75,10 +75,16 @@ export function filledFields() {
|
|
|
75
75
|
.map(([k]) => k);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* #2 のフォームが参照する、入力欄の定義と**始める前の前提条件**。値は一切含まない。
|
|
80
|
+
*
|
|
81
|
+
* 前提条件をここに載せるのは、画面がフォームを組み立てる前に読む唯一の定義だから。
|
|
82
|
+
* R2 の有効化は、入力を始める前に見えていないと意味がない。
|
|
83
|
+
*/
|
|
79
84
|
export function formSchema() {
|
|
80
85
|
return {
|
|
81
86
|
cliVersion: CONTRACT_CLI_VERSION,
|
|
87
|
+
prerequisites: PREFLIGHT,
|
|
82
88
|
groups: FORM_GROUPS,
|
|
83
89
|
fields: FORM_FIELDS.map((f) => {
|
|
84
90
|
const spec = PROMPTS.find((p) => p.id === f.promptId);
|
package/src/web/index.html
CHANGED
|
@@ -63,12 +63,41 @@
|
|
|
63
63
|
.banner.bad { border-color: var(--err-line); background: var(--err-bg); color: var(--err); }
|
|
64
64
|
.banner strong { display: block; }
|
|
65
65
|
|
|
66
|
+
/*
|
|
67
|
+
* 版のズレのように「知らせるが、進めてよい」もの。
|
|
68
|
+
* 警告(.banner)と同じ強さで出すと、進めてよいのに手が止まる
|
|
69
|
+
*/
|
|
70
|
+
.note { font-size: .85rem; color: var(--muted); margin: 0 0 1rem; }
|
|
71
|
+
.note > summary { cursor: pointer; list-style-position: inside; }
|
|
72
|
+
.note > summary::marker { color: var(--muted); }
|
|
73
|
+
.note .note-detail { margin: .4rem 0 0 1.1rem; padding-left: .7rem;
|
|
74
|
+
border-left: 2px solid var(--line); white-space: pre-line; }
|
|
75
|
+
|
|
66
76
|
.status { display: flex; align-items: center; gap: .5rem; font-size: .85rem; color: var(--muted);
|
|
67
77
|
margin-bottom: 1.5rem; }
|
|
68
78
|
.dot { width: .55rem; height: .55rem; border-radius: 50%; background: var(--muted); flex: none; }
|
|
69
79
|
.dot.ok { background: var(--brand); }
|
|
70
80
|
.dot.bad { background: var(--err); }
|
|
71
81
|
|
|
82
|
+
/*
|
|
83
|
+
* 始める前の前提条件(R2 の有効化)。
|
|
84
|
+
*
|
|
85
|
+
* 実機では、有効化しないまま始めてしまう参加者が多かった。フォームより前に、
|
|
86
|
+
* 見落としようのない形で置く。囲みの上の警告(.banner)と同じ黄色の面だが、
|
|
87
|
+
* こちらは**開始の条件**なので、枠を太くして見出しを立てる。
|
|
88
|
+
*/
|
|
89
|
+
.preflight { border: 2px solid var(--warn-line); background: var(--warn-bg);
|
|
90
|
+
border-radius: .6rem; padding: 1.1rem 1.25rem; margin-bottom: 1.75rem; }
|
|
91
|
+
.preflight h2 { font-size: 1.05rem; margin: 0 0 .35rem; }
|
|
92
|
+
.preflight .why { margin: 0 0 .75rem; font-size: .9rem; }
|
|
93
|
+
.preflight .guide-body { padding: 0; font-size: .88rem; }
|
|
94
|
+
/* 黄色の面の上で、いちばん効かせたい一文が沈まないようにする */
|
|
95
|
+
.preflight .guide-warn { background: var(--bg); }
|
|
96
|
+
.gate { display: flex; align-items: flex-start; gap: .6rem; font-weight: 600; font-size: .92rem;
|
|
97
|
+
margin-top: .9rem; padding-top: .85rem; border-top: 1px solid var(--warn-line); }
|
|
98
|
+
.gate input { margin-top: .4rem; flex: none; }
|
|
99
|
+
.gate-note { font-size: .85rem; color: var(--muted); }
|
|
100
|
+
|
|
72
101
|
fieldset { border: 1px solid var(--line); border-radius: .6rem; padding: 1.25rem;
|
|
73
102
|
margin: 0 0 1.25rem; border-left-width: 4px; }
|
|
74
103
|
fieldset[data-group="project"] { border-left-color: var(--brand); }
|
|
@@ -205,6 +234,8 @@
|
|
|
205
234
|
<span class="dot" id="dot"></span><span id="status">確認中…</span>
|
|
206
235
|
</div>
|
|
207
236
|
|
|
237
|
+
<section id="preflight" hidden></section>
|
|
238
|
+
|
|
208
239
|
<form id="form" autocomplete="off" novalidate hidden></form>
|
|
209
240
|
|
|
210
241
|
<section id="run" hidden>
|
|
@@ -264,10 +295,21 @@
|
|
|
264
295
|
);
|
|
265
296
|
}
|
|
266
297
|
|
|
267
|
-
// 本家 CLI
|
|
298
|
+
// 本家 CLI の版のズレ。
|
|
299
|
+
//
|
|
300
|
+
// 固定版で動かすので、新しい版が出ているだけなら**参加者はこのまま進めてよい**。
|
|
301
|
+
// そこを警告として強く出すと、進めてよいのに手が止まる(実機で起きた)。
|
|
302
|
+
// 強さはサーバーが tone で決める。ここはそれに従って出し分けるだけ
|
|
268
303
|
try {
|
|
269
304
|
const version = await (await fetch("/api/version")).json();
|
|
270
|
-
if (version.
|
|
305
|
+
if (version.notice?.tone === "warn") {
|
|
306
|
+
banner(version.notice.headline, version.notice.detail, true);
|
|
307
|
+
} else if (version.notice) {
|
|
308
|
+
const box = el("details", { className: "note" });
|
|
309
|
+
box.append(el("summary", { textContent: version.notice.headline }));
|
|
310
|
+
box.append(el("p", { className: "note-detail", textContent: version.notice.detail }));
|
|
311
|
+
$("banners").append(box);
|
|
312
|
+
}
|
|
271
313
|
} catch {
|
|
272
314
|
/* 版が確認できなくても入力は進められる */
|
|
273
315
|
}
|
|
@@ -297,6 +339,37 @@
|
|
|
297
339
|
const form = $("form");
|
|
298
340
|
const inputs = new Map();
|
|
299
341
|
|
|
342
|
+
/**
|
|
343
|
+
* 始める前に済ませておくこと(R2 の有効化)。
|
|
344
|
+
*
|
|
345
|
+
* 実機では、有効化しないまま始めてしまう参加者が多かった。本家がそれを訊くのは
|
|
346
|
+
* 実行の途中で、そこまで来て初めて気づくと手が止まる。**フォームより前**に出し、
|
|
347
|
+
* 確認するまで「セットアップを開始する」を押せなくする。
|
|
348
|
+
*/
|
|
349
|
+
const gates = [];
|
|
350
|
+
for (const item of schema.prerequisites ?? []) {
|
|
351
|
+
const box = el("div", { className: "preflight" });
|
|
352
|
+
box.append(el("h2", { textContent: item.title }));
|
|
353
|
+
if (item.body) box.append(el("p", { className: "why", textContent: item.body }));
|
|
354
|
+
// ここは畳まない。開かないと読めない形では、素通りされる
|
|
355
|
+
if (item.guide) box.append(guideBody(item.guide));
|
|
356
|
+
|
|
357
|
+
const check = el("input", { type: "checkbox", id: `gate-${item.id}` });
|
|
358
|
+
check.addEventListener("change", syncGate);
|
|
359
|
+
gates.push(check);
|
|
360
|
+
box.append(
|
|
361
|
+
el("div", { className: "gate" }, [
|
|
362
|
+
check,
|
|
363
|
+
el("label", { htmlFor: `gate-${item.id}`, textContent: item.confirmLabel }),
|
|
364
|
+
]),
|
|
365
|
+
);
|
|
366
|
+
$("preflight").append(box);
|
|
367
|
+
}
|
|
368
|
+
$("preflight").hidden = gates.length === 0;
|
|
369
|
+
|
|
370
|
+
/** 前提がすべて確認済みか */
|
|
371
|
+
const gateReady = () => gates.every((g) => g.checked);
|
|
372
|
+
|
|
300
373
|
for (const group of schema.groups) {
|
|
301
374
|
const fields = schema.fields.filter((f) => f.group === group.id);
|
|
302
375
|
if (fields.length === 0) continue;
|
|
@@ -385,7 +458,17 @@
|
|
|
385
458
|
const box = el("details", { className: "guide" });
|
|
386
459
|
if (open) box.open = true;
|
|
387
460
|
box.append(el("summary", { textContent: "どこで取るか" }));
|
|
461
|
+
box.append(guideBody(guide, box));
|
|
462
|
+
return box;
|
|
463
|
+
}
|
|
388
464
|
|
|
465
|
+
/**
|
|
466
|
+
* 案内の中身。畳んだ中にも、前提条件のように畳まずに出す場所にも使う。
|
|
467
|
+
*
|
|
468
|
+
* @param guide 案内
|
|
469
|
+
* @param details 畳んで出す場合、その details。画像はそのとき初めて読む
|
|
470
|
+
*/
|
|
471
|
+
function guideBody(guide, details = null) {
|
|
389
472
|
const body = el("div", { className: "guide-body" });
|
|
390
473
|
|
|
391
474
|
if (guide.steps?.length) {
|
|
@@ -409,21 +492,21 @@
|
|
|
409
492
|
}
|
|
410
493
|
|
|
411
494
|
if (guide.images?.length) {
|
|
412
|
-
// 開いたときに初めて読みにいく。畳まれたままなら要求も出さない
|
|
413
495
|
const slot = el("div");
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
|
|
496
|
+
const load = () => {
|
|
497
|
+
for (const image of guide.images) {
|
|
498
|
+
const img = el("img", { src: image.url, alt: image.alt });
|
|
499
|
+
// ネットワークが無いと読めない。読めなくても手順は残す
|
|
500
|
+
img.addEventListener("error", () => img.remove());
|
|
501
|
+
slot.append(img);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
// 畳んで出すなら、開いたときに初めて読みにいく。畳まれたままなら要求も出さない
|
|
505
|
+
if (details && !details.open) {
|
|
506
|
+
details.addEventListener("toggle", () => details.open && load(), { once: true });
|
|
507
|
+
} else {
|
|
508
|
+
load();
|
|
509
|
+
}
|
|
427
510
|
body.append(slot);
|
|
428
511
|
}
|
|
429
512
|
|
|
@@ -436,8 +519,7 @@
|
|
|
436
519
|
);
|
|
437
520
|
}
|
|
438
521
|
|
|
439
|
-
|
|
440
|
-
return box;
|
|
522
|
+
return body;
|
|
441
523
|
}
|
|
442
524
|
|
|
443
525
|
/** サーバーから渡された規則で見る。画面側で独自の規則を作らない */
|
|
@@ -477,15 +559,39 @@
|
|
|
477
559
|
const askBox = $("run-ask");
|
|
478
560
|
const screen = $("run-screen");
|
|
479
561
|
let startButton = null;
|
|
562
|
+
let gateNote = null;
|
|
563
|
+
/** 開始を投げている最中。この間にチェックを外されても、押し直せるようにはしない */
|
|
564
|
+
let starting = false;
|
|
480
565
|
|
|
481
566
|
function showStart() {
|
|
482
567
|
if (startButton) return;
|
|
483
568
|
startButton = el("button", { className: "primary", type: "button", textContent: "セットアップを開始する" });
|
|
484
569
|
startButton.addEventListener("click", startRun);
|
|
485
|
-
|
|
570
|
+
gateNote = el("span", { className: "gate-note" });
|
|
571
|
+
form.querySelector(".actions").append(startButton, gateNote);
|
|
572
|
+
syncGate();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** 前提を確認するまで開始させない。押せない理由は、押せないボタンの隣に置く */
|
|
576
|
+
function syncGate() {
|
|
577
|
+
if (!startButton || starting) return;
|
|
578
|
+
const ready = gateReady();
|
|
579
|
+
startButton.disabled = !ready;
|
|
580
|
+
gateNote.replaceChildren();
|
|
581
|
+
if (!ready) {
|
|
582
|
+
gateNote.append(document.createTextNode("開始するには、"));
|
|
583
|
+
gateNote.append(el("a", { href: "#preflight", textContent: "ページ上部の前提条件" }));
|
|
584
|
+
gateNote.append(document.createTextNode("を確認してください。"));
|
|
585
|
+
}
|
|
486
586
|
}
|
|
487
587
|
|
|
488
588
|
async function startRun() {
|
|
589
|
+
// 画面の状態がずれていても、前提が未確認のまま走り出さない
|
|
590
|
+
if (!gateReady()) {
|
|
591
|
+
syncGate();
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
starting = true;
|
|
489
595
|
startButton.disabled = true;
|
|
490
596
|
const res = await fetch("/api/run/start", {
|
|
491
597
|
method: "POST",
|
|
@@ -495,11 +601,13 @@
|
|
|
495
601
|
if (!res.ok) {
|
|
496
602
|
result.textContent = (await res.json()).error ?? "開始できませんでした。";
|
|
497
603
|
result.className = "result bad";
|
|
498
|
-
|
|
604
|
+
starting = false;
|
|
605
|
+
syncGate();
|
|
499
606
|
return;
|
|
500
607
|
}
|
|
501
608
|
// 入力はサーバーに渡した。ここから先は実行の画面
|
|
502
609
|
for (const input of inputs.values()) input.disabled = true;
|
|
610
|
+
for (const gate of gates) gate.disabled = true;
|
|
503
611
|
submit.disabled = true;
|
|
504
612
|
runBox.hidden = false;
|
|
505
613
|
runBox.scrollIntoView({ behavior: "smooth", block: "start" });
|