@takagaki/cortex-decisions-viewer 0.4.59 → 0.4.60
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/dist/render.js +580 -117
- package/package.json +1 -1
package/dist/render.js
CHANGED
|
@@ -65,6 +65,26 @@ function escapeHtml(s) {
|
|
|
65
65
|
.replace(/"/g, """);
|
|
66
66
|
}
|
|
67
67
|
/** 接続マップの能力定義(表示順・絵文字は詳細カードの流儀を踏襲) */
|
|
68
|
+
/**
|
|
69
|
+
* アダプタのロゴ。**公式配布のSVGをそのまま埋め込む。**
|
|
70
|
+
*
|
|
71
|
+
* 外部URLを参照しない——この画面は顧客が見るので、参照すると
|
|
72
|
+
* 顧客のブラウザが Slack・Google・Figma のサーバへ接続しに行く(誰がいつ見たかが
|
|
73
|
+
* 各社のログに残る)。参照先が変わればアイコンだけ黙って消える。
|
|
74
|
+
* 既存の GITHUB_ICON と同じ流儀。
|
|
75
|
+
*
|
|
76
|
+
* 出典: Slack/Figma/Google Meet/Google Drive は simple-icons(CC0)、
|
|
77
|
+
* Backlog は ヌーラボ公式のロゴ配布(nulab.com/logo-guidelines/)。
|
|
78
|
+
* 改変は「サイズ指定の付与」と「余分な属性の除去」のみ。配色と形は変えていない。
|
|
79
|
+
*/
|
|
80
|
+
const ADAPTER_ICONS = {
|
|
81
|
+
slack: `<svg width="36" height="36" aria-hidden="true" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><g> <path d="M53.8412698,161.320635 C53.8412698,176.152381 41.8539683,188.139683 27.0222222,188.139683 C12.1904762,188.139683 0.203174603,176.152381 0.203174603,161.320635 C0.203174603,146.488889 12.1904762,134.501587 27.0222222,134.501587 L53.8412698,134.501587 L53.8412698,161.320635 Z M67.2507937,161.320635 C67.2507937,146.488889 79.2380952,134.501587 94.0698413,134.501587 C108.901587,134.501587 120.888889,146.488889 120.888889,161.320635 L120.888889,228.368254 C120.888889,243.2 108.901587,255.187302 94.0698413,255.187302 C79.2380952,255.187302 67.2507937,243.2 67.2507937,228.368254 L67.2507937,161.320635 Z" fill="#E01E5A"></path> <path d="M94.0698413,53.6380952 C79.2380952,53.6380952 67.2507937,41.6507937 67.2507937,26.8190476 C67.2507937,11.9873016 79.2380952,-7.10542736e-15 94.0698413,-7.10542736e-15 C108.901587,-7.10542736e-15 120.888889,11.9873016 120.888889,26.8190476 L120.888889,53.6380952 L94.0698413,53.6380952 Z M94.0698413,67.2507937 C108.901587,67.2507937 120.888889,79.2380952 120.888889,94.0698413 C120.888889,108.901587 108.901587,120.888889 94.0698413,120.888889 L26.8190476,120.888889 C11.9873016,120.888889 0,108.901587 0,94.0698413 C0,79.2380952 11.9873016,67.2507937 26.8190476,67.2507937 L94.0698413,67.2507937 Z" fill="#36C5F0"></path> <path d="M201.549206,94.0698413 C201.549206,79.2380952 213.536508,67.2507937 228.368254,67.2507937 C243.2,67.2507937 255.187302,79.2380952 255.187302,94.0698413 C255.187302,108.901587 243.2,120.888889 228.368254,120.888889 L201.549206,120.888889 L201.549206,94.0698413 Z M188.139683,94.0698413 C188.139683,108.901587 176.152381,120.888889 161.320635,120.888889 C146.488889,120.888889 134.501587,108.901587 134.501587,94.0698413 L134.501587,26.8190476 C134.501587,11.9873016 146.488889,-1.42108547e-14 161.320635,-1.42108547e-14 C176.152381,-1.42108547e-14 188.139683,11.9873016 188.139683,26.8190476 L188.139683,94.0698413 Z" fill="#2EB67D"></path> <path d="M161.320635,201.549206 C176.152381,201.549206 188.139683,213.536508 188.139683,228.368254 C188.139683,243.2 176.152381,255.187302 161.320635,255.187302 C146.488889,255.187302 134.501587,243.2 134.501587,228.368254 L134.501587,201.549206 L161.320635,201.549206 Z M161.320635,188.139683 C146.488889,188.139683 134.501587,176.152381 134.501587,161.320635 C134.501587,146.488889 146.488889,134.501587 161.320635,134.501587 L228.571429,134.501587 C243.403175,134.501587 255.390476,146.488889 255.390476,161.320635 C255.390476,176.152381 243.403175,188.139683 228.571429,188.139683 L161.320635,188.139683 Z" fill="#ECB22E"></path> </g></svg>`,
|
|
82
|
+
figma: `<svg width="36" height="36" aria-hidden="true" viewBox="0 0 256 384" xmlns="http://www.w3.org/2000/svg"><g> <path d="M64,384 C99.328,384 128,355.328 128,320 L128,256 L64,256 C28.672,256 0,284.672 0,320 C0,355.328 28.672,384 64,384 Z" fill="#0ACF83"></path> <path d="M0,192 C0,156.672 28.672,128 64,128 L128,128 L128,256 L64,256 C28.672,256 0,227.328 0,192 Z" fill="#A259FF"></path> <path d="M0,64 C0,28.672 28.672,0 64,0 L128,0 L128,128 L64,128 C28.672,128 0,99.328 0,64 Z" fill="#F24E1E"></path> <path d="M128,0 L192,0 C227.328,0 256,28.672 256,64 C256,99.328 227.328,128 192,128 L128,128 L128,0 Z" fill="#FF7262"></path> <path d="M256,192 C256,227.328 227.328,256 192,256 C156.672,256 128,227.328 128,192 C128,156.672 156.672,128 192,128 C227.328,128 256,156.672 256,192 Z" fill="#1ABCFE"></path> </g></svg>`,
|
|
83
|
+
"google-meet": `<svg width="36" height="36" aria-hidden="true" viewBox="0 0 256 211" xmlns="http://www.w3.org/2000/svg"><g> <polygon fill="#00832D" points="144.822496 105.321856 169.778926 133.848796 203.341343 155.294133 209.178931 105.50196 203.341343 56.8331137 169.136495 75.6715889"></polygon> <path d="M0.000557021739,150.659712 L0.000557021739,193.089915 C0.000557021739,202.77838 7.86384724,210.643527 17.5541688,210.643527 L59.9843714,210.643527 L68.7704609,178.585069 L59.9843714,150.659712 L30.8744153,141.873623 L0.000557021739,150.659712 Z" fill="#0066DA"></path> <polygon fill="#E94235" points="59.9838143 9.9475983e-14 0 59.9838143 30.875715 68.7494798 59.9838143 59.9838143 68.6102243 32.4390893"></polygon> <polygon fill="#2684FC" points="0.000557021739 150.679394 59.9843714 150.679394 59.9843714 59.9832573 0.000557021739 59.9832573"></polygon> <path d="M241.658683,25.3977775 L203.341157,56.8342278 L203.341157,155.29339 L241.818362,186.852385 C247.577967,191.364261 256.003849,187.251584 256.003849,179.930462 L256.003849,32.1785888 C256.003849,24.7757699 247.377439,20.6835169 241.658683,25.3977775" fill="#00AC47"></path> <path d="M144.822496,105.321856 L144.822496,150.659712 L59.9843714,150.659712 L59.9843714,210.643527 L185.787731,210.643527 C195.478053,210.643527 203.341343,202.77838 203.341343,193.089915 L203.341343,155.294133 L144.822496,105.321856 Z" fill="#00AC47"></path> <path d="M185.787731,0 L59.9843714,0 L59.9843714,59.9838143 L144.822496,59.9838143 L144.822496,105.32167 L203.341343,56.832928 L203.341343,17.5536117 C203.341343,7.86329022 195.478053,0 185.787731,0" fill="#FFBA00"></path> </g></svg>`,
|
|
84
|
+
"google-drive": `<svg width="36" height="36" aria-hidden="true" viewBox="0 0 256 229" xmlns="http://www.w3.org/2000/svg"><g> <path d="M19.3542312,196.033928 L30.644172,215.534816 C32.9900287,219.64014 36.3622164,222.86588 40.3210929,225.211737 C51.6602421,210.818376 59.5534225,199.772864 64.000634,192.075201 C68.5137119,184.263529 74.0609657,172.045039 80.6423954,155.41973 C62.9064315,153.085282 49.4659974,151.918058 40.3210929,151.918058 C31.545465,151.918058 18.1051007,153.085282 0,155.41973 C0,159.964996 1.17298825,164.510261 3.51893479,168.615586 L19.3542312,196.033928 Z" fill="#0066DA"></path> <path d="M215.681443,225.211737 C219.64032,222.86588 223.012507,219.64014 225.358364,215.534816 L230.050377,207.470615 L252.483511,168.615586 C254.829368,164.510261 256.002446,159.964996 256.002446,155.41973 C237.79254,153.085282 224.376613,151.918058 215.754667,151.918058 C206.488712,151.918058 193.072785,153.085282 175.506888,155.41973 C182.010479,172.136093 187.484394,184.354584 191.928633,192.075201 C196.412073,199.863919 204.329677,210.909431 215.681443,225.211737 Z" fill="#EA4335"></path> <path d="M128.001268,73.3111515 C141.121182,57.4655263 150.162898,45.2470011 155.126415,36.6555757 C159.123121,29.7376196 163.521739,18.6920726 168.322271,3.51893479 C164.363395,1.1729583 159.818129,0 155.126415,0 L100.876121,0 C96.1841079,0 91.638842,1.31958557 87.6799655,3.51893479 C93.7861943,20.9210065 98.9675428,33.3058067 103.224011,40.6733354 C107.927832,48.8151881 116.186918,59.6944602 128.001268,73.3111515 Z" fill="#00832D"></path> <path d="M175.360141,155.41973 L80.6420959,155.41973 L40.3210929,225.211737 C44.2799694,227.557893 48.8252352,228.730672 53.5172481,228.730672 L202.485288,228.730672 C207.177301,228.730672 211.722567,227.411146 215.681443,225.211737 L175.360141,155.41973 Z" fill="#2684FC"></path> <path d="M128.001268,73.3111515 L87.680265,3.51893479 C83.7213885,5.86488134 80.3489013,9.09044179 78.0030446,13.1960654 L3.51893479,142.223575 C1.17298825,146.329198 0,150.874464 0,155.41973 L80.6423954,155.41973 L128.001268,73.3111515 Z" fill="#00AC47"></path> <path d="M215.241501,77.7099697 L177.999492,13.1960654 C175.653635,9.09044179 172.281148,5.86488134 168.322271,3.51893479 L128.001268,73.3111515 L175.360141,155.41973 L255.855999,155.41973 C255.855999,150.874464 254.682921,146.329198 252.337064,142.223575 L215.241501,77.7099697 Z" fill="#FFBA00"></path> </g></svg>`,
|
|
85
|
+
backlog: `<svg width="20" height="20" aria-hidden="true" viewBox="0 0 142 142" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Nulab-logos-" transform="translate(-1348.000000, -2619.000000)"> <g id="Group-7" transform="translate(1348.000000, 2619.000000)"> <path d="M112.518975,141.27641 L29.5477542,141.27641 C13.8226507,141.27641 0.955380933,128.389559 0.955380933,112.638806 L0.955380933,29.5308457 C0.955380933,13.7800933 13.8226507,0.893241768 29.5477542,0.893241768 L112.518975,0.893241768 C128.244079,0.893241768 141.109945,13.7800933 141.109945,29.5308457 L141.109945,112.638806 C141.109945,128.389559 128.244079,141.27641 112.518975,141.27641" id="Fill-16" fill="#42CE9F"></path> <path d="M60.1988625,61.3236616 C60.1441201,72.9746913 60.1146434,84.3121962 60.1441201,91.0958596 C70.2756738,90.5461368 75.2937266,88.482216 79.2632519,83.7132655 C82.0775724,80.3319784 82.686757,76.4586121 80.9799172,72.8073843 C78.7467083,68.0328101 72.1804284,62.9897012 60.1988625,61.3236616 M53.5357306,104.795345 L53.5357306,104.795345 C51.7348462,104.795345 50.0069517,104.075503 48.7380511,102.796097 L48.7352438,102.793285 C46.7434629,100.784196 46.7153899,98.8622754 46.6648584,95.3741368 C46.6395927,93.6504536 46.6241525,91.2083348 46.6171343,88.2896022 C46.6059051,83.1719784 46.6185379,76.0705725 46.6536292,67.1808101 C46.7153899,51.8855824 46.8262783,36.6381566 46.8262783,36.6381566 L60.3448422,36.7365725 C60.3195765,40.1150477 60.2957144,43.8295428 60.2718523,47.705721 C76.2678604,49.5531269 88.283114,56.4998794 93.2225623,67.065523 C97.1640146,75.4941368 95.8263351,84.9589289 89.6474629,92.3837012 C80.1166718,103.836493 66.5812641,104.795345 53.5357306,104.795345" id="Fill-18" fill="#FFFFFF"></path> </g> </g> </g> </svg>`,
|
|
86
|
+
github: GITHUB_ICON,
|
|
87
|
+
};
|
|
68
88
|
const CM_CAPS = [
|
|
69
89
|
{ cap: "課題管理", icon: "📋", pipe: /sync-backlog/ },
|
|
70
90
|
{ cap: "会議", icon: "🎙", pipe: /ingest-minutes/ },
|
|
@@ -109,6 +129,110 @@ function sourceNeedsAttention(s) {
|
|
|
109
129
|
return false;
|
|
110
130
|
return true;
|
|
111
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* 能力(課題管理・会議・…)が**いまどういう状態か**を1つに決める。
|
|
134
|
+
*
|
|
135
|
+
* **接続マップとクライアント側の両方がこれを呼ぶ。** マップはビルド時にSVGを描き、
|
|
136
|
+
* 画面はブラウザで一覧を描くが、判定が2箇所にあると「同じ画面でマップが⚠️・直下が
|
|
137
|
+
* 『接続OK』」になる(`sourceNeedsAttention` の項に、実際にそうなった経緯がある)。
|
|
138
|
+
* クライアント側へは `.toString()` で埋め込んで共有する(`CLIENT_JS`)ので、
|
|
139
|
+
* **クロージャを持たない純関数に保つこと**——外の変数を掴むと直列化した先で壊れる。
|
|
140
|
+
*
|
|
141
|
+
* 返す値:
|
|
142
|
+
* "off" … 未使用(tools が none / enabled:false / 使っている証拠が無い)
|
|
143
|
+
* "idle" … 宣言はあるが一度も繋がっていない(画面表記は「未接続」)
|
|
144
|
+
* "warn" … 問題あり
|
|
145
|
+
* "ok" … 健全
|
|
146
|
+
*
|
|
147
|
+
* **順序が意味を持つ。** 未接続は警告より優先する——繋がっていない能力のパイプラインが
|
|
148
|
+
* 失敗するのは「壊れた」のではなく「まだ設定が済んでいない」結果で、逆にすると
|
|
149
|
+
* ⚠️の箱に「未接続」と書かれ、繋がっていないのに線が引かれる(実際にそうなった:
|
|
150
|
+
* APIキー未投入で sync-backlog が失敗し、課題管理が警告色で線付きになった)。
|
|
151
|
+
*
|
|
152
|
+
* @param cap 能力名
|
|
153
|
+
* @param ctx 生のフリートデータ { tools, internals, externals, pipes }。
|
|
154
|
+
* **解決済みの値を渡させない。** 振り分けや regex を呼び出し側に持たせると、
|
|
155
|
+
* 呼び出し側ごとに写経が生まれ、片方が書き忘れると判定が静かに変わる。
|
|
156
|
+
*/
|
|
157
|
+
/**
|
|
158
|
+
* 外部ソースの種別(externalSources[].type)→ どの能力に属するか。
|
|
159
|
+
*
|
|
160
|
+
* **対応表はここ1箇所に持つ。** 画面の受け皿(capOf)と状態判定(capabilityState)と
|
|
161
|
+
* 件数の集計(tileSub)が各自で書き写していたため、未知の種別が
|
|
162
|
+
* 「受け皿には入るが、状態は未使用」という食い違いを起こした。
|
|
163
|
+
*
|
|
164
|
+
* **知らない種別は捨てず「その他」へ。** この画面の主眼は「何を Gold に
|
|
165
|
+
* 読んでいるか」の開示なので、読んでいるものが無言で消えるのが一番まずい。
|
|
166
|
+
*/
|
|
167
|
+
const CAP_OTHER = "その他";
|
|
168
|
+
function externalCapability(type) {
|
|
169
|
+
if (type === "slack")
|
|
170
|
+
return "チャット";
|
|
171
|
+
if (String(type).indexOf("github") === 0)
|
|
172
|
+
return "開発";
|
|
173
|
+
return CAP_OTHER;
|
|
174
|
+
}
|
|
175
|
+
function capabilityState(cap, ctx) {
|
|
176
|
+
// **能力→パイプラインの対応はここに持つ。** 呼び出し側に渡させると、
|
|
177
|
+
// クライアント側(この関数を .toString() で受け取る側)が同じ表を書き写すことになり、
|
|
178
|
+
// 書き忘れると warn 判定が丸ごと死ぬ——実データで、東京ヴェルディの課題管理が
|
|
179
|
+
// マップは⚠️・タイルは「接続OK」になる。**この関数が読むものは、この関数が持つ。**
|
|
180
|
+
var PIPE = {
|
|
181
|
+
"課題管理": /sync-backlog/,
|
|
182
|
+
"会議": /ingest-minutes/,
|
|
183
|
+
"共有資料": /sync-materials/,
|
|
184
|
+
"デザイン": /sync-designs|update-design-notes/,
|
|
185
|
+
};
|
|
186
|
+
// **ライブ参照かどうかは、能力名ではなく渡された行から決める。** 名前で決めていたとき、
|
|
187
|
+
// 未知の種別を寄せた「その他」は行を2件持っているのに「未使用」と言い切っていた
|
|
188
|
+
// (読み手は未使用を信じて押さないので、行が消えるより悪い)
|
|
189
|
+
const liveExts = ctx.externals.filter(function (s) { return externalCapability(s.type) === cap; });
|
|
190
|
+
const isLive = cap === "チャット" || cap === "開発" || cap === CAP_OTHER;
|
|
191
|
+
const toolVal = ctx.tools[cap];
|
|
192
|
+
const internal = ctx.internals.filter(function (s) { return s.kind === cap; })[0];
|
|
193
|
+
const pipeRe = PIPE[cap];
|
|
194
|
+
// 使用中か: tools宣言がnone以外 / internalSourceが存在(enabled:false除く) / ライブ参照の実体がある
|
|
195
|
+
const used = (toolVal != null && toolVal !== "none") ||
|
|
196
|
+
(internal != null && internal.enabled !== false) ||
|
|
197
|
+
(isLive && liveExts.length > 0);
|
|
198
|
+
const off = toolVal === "none" || (internal != null && internal.enabled === false) || !used;
|
|
199
|
+
// 問題あり: ライブ参照はgate≠okが1件以上、同期ソースは対応パイプラインの直近runがfailure
|
|
200
|
+
const warn = !off &&
|
|
201
|
+
(isLive
|
|
202
|
+
? liveExts.some(function (s) {
|
|
203
|
+
return sourceNeedsAttention(s);
|
|
204
|
+
})
|
|
205
|
+
: ctx.pipes.some(function (p) {
|
|
206
|
+
return (pipeRe != null && pipeRe.test(p.id) && p.applicable !== false && p.lastConclusion === "failure");
|
|
207
|
+
}));
|
|
208
|
+
// 「実際に繋がっている」の証拠は共通で次のどれか:
|
|
209
|
+
// - 一度でも同期が走った(lastSync)
|
|
210
|
+
// - 正本のURLが導出できる(url。設定ファイルが実在して初めて出る)
|
|
211
|
+
// - 会議は取り込みが有効(ingestState === "on")
|
|
212
|
+
// - ライブ参照は読む対象が1件以上ある
|
|
213
|
+
//
|
|
214
|
+
// **能力ごとに場当たりで判定しない。** 最初は会議とライブ参照だけを見ていたが、
|
|
215
|
+
// 課題管理が「宣言があるだけ」で接続済みに見えていた——同期が一度も走っておらず、
|
|
216
|
+
// APIキーすら入っていない案件で。指摘されるまで気づけなかった。
|
|
217
|
+
//
|
|
218
|
+
// **空振りした同期の時刻を「繋がっている証拠」にしない。**
|
|
219
|
+
// 対象外のパイプライン(applicable: false)も実行はされ、何もせず成功で終わり、
|
|
220
|
+
// その時刻が lastSync に入る。それを根拠にすると、Figmaのトークンも無く
|
|
221
|
+
// ファイルも0件の案件が「接続済み」として青い線で繋がる(実際にそうなっていた)。
|
|
222
|
+
// パイプライン一覧では「applicable:false は最終成功を出さない(スキップ正常終了のrunが
|
|
223
|
+
// 成功表示に見えるため)」と決めているのに、接続判定だけがその値を信じていた。
|
|
224
|
+
const pipeSkipped = ctx.pipes.some(function (p) {
|
|
225
|
+
return pipeRe != null && pipeRe.test(p.id) && p.applicable === false;
|
|
226
|
+
});
|
|
227
|
+
const syncedForReal = internal != null && ((internal.lastSync && !pipeSkipped) || internal.url);
|
|
228
|
+
const idle = !off &&
|
|
229
|
+
(isLive
|
|
230
|
+
? liveExts.length === 0
|
|
231
|
+
: cap === "会議"
|
|
232
|
+
? (internal == null ? undefined : internal.ingestState) !== "on"
|
|
233
|
+
: !syncedForReal);
|
|
234
|
+
return off ? "off" : idle ? "idle" : warn ? "warn" : "ok";
|
|
235
|
+
}
|
|
112
236
|
function renderConnectionMap(site) {
|
|
113
237
|
const internals = site.internalSources ?? [];
|
|
114
238
|
const externals = site.externalSources ?? [];
|
|
@@ -129,48 +253,13 @@ function renderConnectionMap(site) {
|
|
|
129
253
|
const internal = internals.find((s) => s.kind === c.cap);
|
|
130
254
|
const isLive = c.cap === "チャット" || c.cap === "開発";
|
|
131
255
|
const liveExts = c.cap === "チャット" ? slackExts : c.cap === "開発" ? ghExts : [];
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
(isLive
|
|
140
|
-
// カード側の集計(sourceNeedsAttention)と同じ判定にする。ここだけ接続状態だけを見ていると、
|
|
141
|
-
// マップが⚠️なのに直下のグループが「接続OK」という矛盾が同じ画面に出る。
|
|
142
|
-
? liveExts.some((s) => sourceNeedsAttention(s))
|
|
143
|
-
: pipes.some((p) => c.pipe != null && c.pipe.test(p.id) && p.applicable !== false && p.lastConclusion === "failure"));
|
|
144
|
-
// **宣言されているだけで、実際には何も繋がっていない**状態を分けて表す。
|
|
145
|
-
//
|
|
146
|
-
// 会議に Teams、チャットに Teams を宣言した案件で、どちらも「接続済み」の見た目になっていた。
|
|
147
|
-
// notetaker は Teams の文字起こしを取り込めず(Google Meet 前提)、Teams の MCP も配っていないので、
|
|
148
|
-
// **実体は何も繋がっていない**。それを繋がって見せると、設定が足りないことに誰も気づけない。
|
|
149
|
-
//
|
|
150
|
-
// - 会議: 取り込みが有効(ingestState === "on")でなければ「自動連携なし」
|
|
151
|
-
// - チャット/開発: ライブ参照の実体(登録されたチャンネル・リポジトリ)が無ければ同じ
|
|
152
|
-
//
|
|
153
|
-
// **能力ごとに場当たりで判定しない。** 最初は会議とライブ参照だけを見ていたが、
|
|
154
|
-
// 課題管理が「宣言があるだけ」で接続済み(青)に見えていた——同期が一度も走っておらず、
|
|
155
|
-
// APIキーすら入っていない案件で。指摘されるまで気づけなかった。
|
|
156
|
-
//
|
|
157
|
-
// 「実際に繋がっている」の証拠は共通で次のどれか:
|
|
158
|
-
// - 一度でも同期が走った(lastSync)
|
|
159
|
-
// - 正本のURLが導出できる(url。設定ファイルが実在して初めて出る)
|
|
160
|
-
// - 会議は取り込みが有効(ingestState === "on")
|
|
161
|
-
// - ライブ参照は読む対象が1件以上ある
|
|
162
|
-
const idle = !off &&
|
|
163
|
-
(isLive
|
|
164
|
-
? liveExts.length === 0
|
|
165
|
-
: c.cap === "会議"
|
|
166
|
-
? internal?.ingestState !== "on"
|
|
167
|
-
: !(internal?.lastSync || internal?.url));
|
|
168
|
-
// **未接続は警告より優先する。** 繋がっていない能力のパイプラインが失敗するのは
|
|
169
|
-
// 「壊れた」のではなく「まだ設定が済んでいない」結果で、順番を逆にすると
|
|
170
|
-
// **⚠️の箱に「未接続」と書かれ、しかも繋がっていないのに線が引かれる**(実際にそうなった:
|
|
171
|
-
// APIキー未投入で sync-backlog が失敗し、課題管理が警告色で線付きになった)。
|
|
172
|
-
// 失敗そのものは下の「自動化パイプライン」の一覧に出るので、情報は失われない。
|
|
173
|
-
const state = off ? "cm-off" : idle ? "cm-idle" : warn ? "cm-warn" : "cm-ok";
|
|
256
|
+
// **判定は capabilityState に一本化している。** 画面側(クライアントJS)も同じ関数を
|
|
257
|
+
// 呼ぶので、ここで写経すると「同じ画面でマップが⚠️・直下が『接続OK』」が復活する。
|
|
258
|
+
const st = capabilityState(c.cap, { tools, internals, externals, pipes });
|
|
259
|
+
const off = st === "off";
|
|
260
|
+
const idle = st === "idle";
|
|
261
|
+
const warn = st === "warn";
|
|
262
|
+
const state = "cm-" + st;
|
|
174
263
|
// **ラベルは能力名(会議・課題管理…)。ツール名は下に小さく置く。**
|
|
175
264
|
// ツールは「その能力をいま何で実現しているか」という差し替え可能なアダプタで、
|
|
176
265
|
// 主語ではない。ツール名を主にすると、Teams のように複数の能力に使われるものが
|
|
@@ -746,6 +835,8 @@ a:hover { text-decoration: underline; }
|
|
|
746
835
|
.datasources-asof.is-stale { color: var(--warn, #a15c00); }
|
|
747
836
|
.conn-map-asof { color: var(--muted); font-size: 12px; margin: 6px 0 0; text-align: right; }
|
|
748
837
|
.ds-sub { font-size: 13px; font-weight: 700; color: var(--muted); margin: 20px 0 8px; }
|
|
838
|
+
/* 能力の小見出し。同じ能力の設定(状態・追加・鍵)を1か所に集めるための区切り */
|
|
839
|
+
.ds-cap { font-size: 15px; font-weight: 700; margin: 26px 0 10px; padding-bottom: 6px; border-bottom: 1px solid var(--line, #e5e7eb); }
|
|
749
840
|
.ds-list { display: flex; flex-direction: column; gap: 10px; }
|
|
750
841
|
.ds-card {
|
|
751
842
|
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
|
|
@@ -833,6 +924,30 @@ a.ds-name:hover { text-decoration: underline; }
|
|
|
833
924
|
**幅と色まで指定する。** border-style だけだと、border を持たない .ds-excluded 側は
|
|
834
925
|
初期値の medium(≈3px)/currentColor に解決され、押した向きによって太さ3倍・色が別物になる */
|
|
835
926
|
.badge.is-pending { border: 1px dashed currentColor; opacity: 0.85; }
|
|
927
|
+
/* 能力モーダルは一覧(17チャンネル・19リポジトリ)を入れるので広く取る。
|
|
928
|
+
既定の480pxでは、チャンネル名+バッジ2つ+トグルが1行に収まらず折り返す。
|
|
929
|
+
画面の広さいっぱいまで使う——この画面は一覧を読むためのもので、
|
|
930
|
+
余白より行の読みやすさを優先する */
|
|
931
|
+
/* **セレクタを .modal.modal-wide にする(詳細度を上げる)。** 素の .modal-wide だと
|
|
932
|
+
後ろにある .modal の max-width:480px と詳細度が並び、**後勝ちで負ける**。
|
|
933
|
+
クラスは付いているのに効かず、見た目は「少し窮屈」なだけなので気づきにくい */
|
|
934
|
+
.modal.modal-wide { max-width: min(1040px, 92vw); }
|
|
935
|
+
.modal-overlay:has(.modal-wide) { padding: 16px; }
|
|
936
|
+
.ds-tile { display: flex; align-items: center; gap: 12px; width: 100%; text-align: left; cursor: pointer; background: var(--card, #fff); border: 1px solid var(--line, #e5e7eb); border-radius: 12px; padding: 14px 16px; }
|
|
937
|
+
.ds-tile:hover { border-color: #c7cdd6; }
|
|
938
|
+
.ds-tile-body { flex: 1; min-width: 0; }
|
|
939
|
+
.ds-tile-name { font-weight: 700; }
|
|
940
|
+
/* ロゴはタイルの左に独立して置き、名前の高さに縛られないようにする。
|
|
941
|
+
一覧を眺めるとき、先に目に入るのは文字よりロゴなので大きく取る */
|
|
942
|
+
.ds-tile-logo {
|
|
943
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
944
|
+
width: 40px; height: 40px; flex: 0 0 40px; font-size: 26px; line-height: 1;
|
|
945
|
+
}
|
|
946
|
+
.ds-tile-logo svg { width: 100%; height: 100%; }
|
|
947
|
+
.ds-tile-desc { font-size: 12.5px; color: var(--muted); }
|
|
948
|
+
.ds-tile-sub { font-size: 12.5px; margin-top: 2px; }
|
|
949
|
+
.ds-tile-chev { color: var(--muted); }
|
|
950
|
+
.ds-tile-note { font-size: 12.5px; color: #a15c00; margin-top: 2px; }
|
|
836
951
|
.badge.ds-undeclared { background: #fff4e5; color: #9a6700; }
|
|
837
952
|
.ds-toggle { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 6px; }
|
|
838
953
|
.ds-toggle-btn { font-size: 12px; padding: 3px 10px; border-radius: 999px; border: 1px solid var(--border); background: #fff; color: var(--text); cursor: pointer; }
|
|
@@ -872,6 +987,8 @@ a.ds-name:hover { text-decoration: underline; }
|
|
|
872
987
|
.ds-addform .sec-input { margin-bottom: 6px; }
|
|
873
988
|
.sec-input-inline { width: auto; max-width: 220px; margin-bottom: 0; }
|
|
874
989
|
.sec-how { color: var(--muted); font-size: 12px; line-height: 1.7; margin: 0 0 10px; }
|
|
990
|
+
/* 入力欄のすぐ下。貼る直前に読める位置に置く */
|
|
991
|
+
.sec-note { color: var(--muted); font-size: 12px; line-height: 1.7; margin: 6px 0 0; }
|
|
875
992
|
.sec-how p { margin: 0; }
|
|
876
993
|
/* 取り込みの条件は箇条書きで出す(ANDであることを読み飛ばせないように) */
|
|
877
994
|
.sec-how-list { margin: 4px 0; padding-left: 1.3em; }
|
|
@@ -907,7 +1024,13 @@ button.ops-card-btn:disabled { opacity: .6; cursor: default; }
|
|
|
907
1024
|
box-shadow: 0 12px 40px rgba(16,24,40,0.25); width: 100%; max-width: 480px;
|
|
908
1025
|
max-height: 90vh; overflow-y: auto; padding: 22px; display: flex; flex-direction: column; gap: 12px;
|
|
909
1026
|
}
|
|
1027
|
+
.modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
|
|
910
1028
|
.modal-title { font-size: 17px; font-weight: 700; margin: 0; color: var(--text); }
|
|
1029
|
+
.modal-x {
|
|
1030
|
+
flex: 0 0 auto; border: none; background: none; cursor: pointer; font-size: 18px; line-height: 1;
|
|
1031
|
+
color: var(--muted); padding: 2px 6px; border-radius: 6px;
|
|
1032
|
+
}
|
|
1033
|
+
.modal-x:hover { background: var(--bg); color: var(--text); }
|
|
911
1034
|
.modal-desc { font-size: 13px; color: var(--muted); line-height: 1.7; margin: 0; }
|
|
912
1035
|
.modal-field { display: flex; flex-direction: column; gap: 4px; }
|
|
913
1036
|
.modal-field-label { font-size: 12px; font-weight: 600; color: var(--muted); }
|
|
@@ -976,6 +1099,21 @@ button.ops-card-btn:disabled { opacity: .6; cursor: default; }
|
|
|
976
1099
|
const CLIENT_JS = `
|
|
977
1100
|
(function () {
|
|
978
1101
|
var SITE = JSON.parse(document.getElementById("site-data").textContent);
|
|
1102
|
+
var ADAPTER_ICONS = ${JSON.stringify(ADAPTER_ICONS)};
|
|
1103
|
+
|
|
1104
|
+
// ---- ビルド時TSと共有する判定(唯一の正本は上の関数定義) ----
|
|
1105
|
+
//
|
|
1106
|
+
// **写経しない。** 接続マップ(ビルド時にSVGを描く)と画面(ブラウザで一覧を描く)が
|
|
1107
|
+
// 別々に同じ判定を持つと、同じ画面でマップが⚠️・直下が「接続OK」になる。
|
|
1108
|
+
// 実際に起きたので sourceNeedsAttention のコメントに経緯が残っている。
|
|
1109
|
+
// ここは関数の実体をそのまま流し込んでいるので、直すのは上の1箇所だけでよい。
|
|
1110
|
+
//
|
|
1111
|
+
// **この2つはクロージャを持たない純関数に保つこと。** 外の変数を掴むと、
|
|
1112
|
+
// 直列化した先(ここ)で undefined を参照して壊れる。
|
|
1113
|
+
var CAP_OTHER = ${JSON.stringify(CAP_OTHER)};
|
|
1114
|
+
${externalCapability.toString()}
|
|
1115
|
+
${sourceNeedsAttention.toString()}
|
|
1116
|
+
${capabilityState.toString()}
|
|
979
1117
|
// 「はじめに」セクションのチュートリアルデータ(ビルド時に埋め込み。ボタンURL・チャレンジ4の決定は解決済み)
|
|
980
1118
|
var TUTORIAL = (function () {
|
|
981
1119
|
var elx = document.getElementById("tutorial-data");
|
|
@@ -1546,6 +1684,35 @@ const CLIENT_JS = `
|
|
|
1546
1684
|
}
|
|
1547
1685
|
|
|
1548
1686
|
/** 組み立てだけを行う(設置は呼び出し側)。ライブ差し替えで同じ節を作り直すため分けている */
|
|
1687
|
+
/** ツールid→表示名(クライアント側。接続マップの CM_TOOL_LABEL と同じ内容) */
|
|
1688
|
+
var CM_TOOL_LABEL_C = {
|
|
1689
|
+
backlog: "Backlog", jira: "Jira", "google-meet": "Google Meet", teams: "Teams",
|
|
1690
|
+
"google-drive": "Google Drive", box: "Box", local: "ローカル", figma: "Figma",
|
|
1691
|
+
slack: "Slack", github: "GitHub", none: "",
|
|
1692
|
+
};
|
|
1693
|
+
|
|
1694
|
+
/**
|
|
1695
|
+
* 能力モーダル。**中身は既存の部品をそのまま入れる。**
|
|
1696
|
+
* 一覧・追加フォーム・鍵・手動投入は作り直さない(先出しバッジやライブ差し替えを壊さないため)。
|
|
1697
|
+
*/
|
|
1698
|
+
function openCapabilityModal(cap, items) {
|
|
1699
|
+
var m = makeModal(cap, true);
|
|
1700
|
+
// **凡例を必ず入れる。** goldState の4状態は凡例があって初めて読める
|
|
1701
|
+
if (cap === "チャット" || cap === "開発") {
|
|
1702
|
+
m.modal.appendChild(el("p", { class: "modal-desc", text: "「Gold昇格の読み取り対象」のものだけを毎晩の Gold 昇格が読み取ります。「除外」は意図的に外したもの、「未宣言(対象外)」はまだ判断されていないもので、どちらも読み取りません。" }));
|
|
1703
|
+
}
|
|
1704
|
+
if (!items.length) {
|
|
1705
|
+
m.modal.appendChild(el("p", { class: "ds-empty", text: "この能力にはまだ設定できるものがありません。" }));
|
|
1706
|
+
}
|
|
1707
|
+
items.forEach(function (n) { m.modal.appendChild(n); });
|
|
1708
|
+
m.modal.appendChild(el("div", { class: "modal-actions" }, [
|
|
1709
|
+
el("button", { class: "ds-toggle-btn", type: "button", text: "閉じる" }),
|
|
1710
|
+
]));
|
|
1711
|
+
m.modal.querySelector(".modal-actions button").addEventListener("click", m.close);
|
|
1712
|
+
m.overlay.appendChild(m.modal);
|
|
1713
|
+
showModal(m.overlay, m.modal);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1549
1716
|
function buildDataSources() {
|
|
1550
1717
|
var internals = fleetVal("internalSources") || [];
|
|
1551
1718
|
var sources = fleetVal("externalSources") || [];
|
|
@@ -1554,11 +1721,30 @@ const CLIENT_JS = `
|
|
|
1554
1721
|
var tools = fleetVal("tools") || {};
|
|
1555
1722
|
var chatOff = tools["チャット"] === "none";
|
|
1556
1723
|
var devOff = tools["開発"] === "none";
|
|
1557
|
-
|
|
1724
|
+
// **この節に入るものが1つでもあるなら出す。**
|
|
1725
|
+
//
|
|
1726
|
+
// 条件を「データ行があるか」で書いていて、2回続けて漏らした——
|
|
1727
|
+
// 1回目: 鍵カードを移したのに条件へ足さず、新規案件で鍵が丸ごと消えた
|
|
1728
|
+
// 2回目: tools の宣言だけの案件(Teams等)でタイルが出ず、
|
|
1729
|
+
// 「未対応です」と伝える場所も接続マップのアンカーの受け皿も無くなった
|
|
1730
|
+
//
|
|
1731
|
+
// 個別に足し続けると次も漏らすので、**この節に出す候補を列挙して判定する**形にする。
|
|
1732
|
+
var hasSecrets = ((SITE.secretKinds || []).filter(function (k) { return SECRET_FORMS[k]; })).length > 0;
|
|
1733
|
+
var hasTools = Object.keys(tools).length > 0;
|
|
1734
|
+
// **投入の口も候補。** fleet-status.json はGitHub Actionsが夜間に生成するので、
|
|
1735
|
+
// 立ち上げ直後の案件には存在しない。それを理由に節ごと消すと、
|
|
1736
|
+
// 文字起こし・資料を入れる口が**一番必要な瞬間に**画面から無くなる
|
|
1737
|
+
var hasIntake = !!SITE.intake;
|
|
1738
|
+
if (!internals.length && !sources.length && !pipes.length && !hasTools && !hasSecrets && !hasIntake) return null;
|
|
1558
1739
|
|
|
1559
1740
|
var sec = el("section", { class: "datasources" });
|
|
1560
1741
|
sec.appendChild(el("h2", { class: "datasources-head", text: "データソースと自動化" }));
|
|
1561
|
-
|
|
1742
|
+
// **まだ何も繋がっていない案件では、文言を変える。** 接続がゼロの状態で
|
|
1743
|
+
// 「接続している情報源の一覧です」と書くと、中身(手動投入だけ)と噛み合わない
|
|
1744
|
+
var connected = internals.length > 0 || sources.length > 0 || hasTools;
|
|
1745
|
+
sec.appendChild(el("p", { class: "datasources-intro", text: connected
|
|
1746
|
+
? "このCortexが接続している情報源と、毎晩自動でGoldに昇格する処理の一覧です。"
|
|
1747
|
+
: "情報源はまだ接続されていません。下から手元のファイルを取り込めます(自動連携の設定はこれから行います)。" }));
|
|
1562
1748
|
// **いつ時点の情報かを必ず出す。** この節は夜間バッチが作るスナップショットを読んでいるので、
|
|
1563
1749
|
// 設定を変えても次の生成までは古い値のまま。時刻が無いと「変えたのに反映されない」ではなく
|
|
1564
1750
|
// 「変えたはずなのに違う値が出ている」と読まれてしまう(実際に設定変更直後にそうなった)。
|
|
@@ -1576,8 +1762,26 @@ const CLIENT_JS = `
|
|
|
1576
1762
|
}));
|
|
1577
1763
|
}
|
|
1578
1764
|
|
|
1765
|
+
// ---- 能力ごとの受け皿 ----
|
|
1766
|
+
//
|
|
1767
|
+
// **能力の一覧は tools のキーで決める。データ行の有無で決めない。**
|
|
1768
|
+
// internalSources に現れるのは 課題管理/会議/共有資料/デザイン の4つだけで、
|
|
1769
|
+
// チャットと開発は externalSources 側にしかない。さらに tools.チャット = "teams" の
|
|
1770
|
+
// 案件(三菱)は**どちらにも行が無い**——Cortex が Teams に対応していないため。
|
|
1771
|
+
// 行の有無でセクションを作ると、その案件でチャットが丸ごと消える。
|
|
1772
|
+
// 「未対応です」と伝えるべき場面で何も出ないのが一番悪い。
|
|
1773
|
+
var CAP_ORDER = ["課題管理", "会議", "共有資料", "チャット", "デザイン", "開発"];
|
|
1774
|
+
var capBox = {};
|
|
1775
|
+
var manualBox = {};
|
|
1776
|
+
// 対応表は externalCapability が持つ(状態判定・件数の集計と同じものを使う)
|
|
1777
|
+
var capOf = externalCapability;
|
|
1778
|
+
function pushCap(cap, node) {
|
|
1779
|
+
if (!cap || !node) return;
|
|
1780
|
+
if (!capBox[cap]) capBox[cap] = [];
|
|
1781
|
+
capBox[cap].push(node);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1579
1784
|
if (internals.length) {
|
|
1580
|
-
sec.appendChild(el("div", { class: "ds-sub", text: "リポジトリ内の情報源(自動同期)" }));
|
|
1581
1785
|
// **「いま同期する」ボタンは出さない。** 受け口(Apps Script の Webアプリ)を
|
|
1582
1786
|
// 公開できないため、押しても届かない。
|
|
1583
1787
|
//
|
|
@@ -1589,7 +1793,6 @@ const CLIENT_JS = `
|
|
|
1589
1793
|
// 最悪60分→20分)。押す口が要るなら、bot の資格情報で Lambda が認証する経路を
|
|
1590
1794
|
// 用意してからここを戻す(syncNowRow と Lambda の op=sync は残してある)。
|
|
1591
1795
|
// sec.appendChild(syncNowRow());
|
|
1592
|
-
var ilist = el("div", { class: "ds-list" });
|
|
1593
1796
|
internals.forEach(function (s) {
|
|
1594
1797
|
// ラベルの下に補足行(会議の照合キー・Drive未設定の注記)を重ねられるよう縦組みにする
|
|
1595
1798
|
var col = el("div", { class: "ds-col" }, [ dsNameEl(s.label || "", s.url) ]);
|
|
@@ -1652,13 +1855,14 @@ const CLIENT_JS = `
|
|
|
1652
1855
|
// 会議・共有資料・デザインの操作をカード内に置く(その情報源の文脈で押せるように)
|
|
1653
1856
|
var ctl = internalControls(s, inactive);
|
|
1654
1857
|
if (ctl) card.appendChild(ctl);
|
|
1655
|
-
|
|
1858
|
+
// 能力ごとの受け皿へ入れる(並べるのは下の CAP_ORDER のループ)
|
|
1859
|
+
pushCap(s.kind, card);
|
|
1656
1860
|
});
|
|
1657
|
-
sec.appendChild(ilist);
|
|
1658
1861
|
}
|
|
1659
1862
|
|
|
1660
1863
|
if (sources.length || chatOff || devOff) {
|
|
1661
|
-
|
|
1864
|
+
// **見出しは付けない。** #101 で行は能力ごとのモーダルへ移ったので、
|
|
1865
|
+
// ここに見出しを置くと中身ゼロの見出しが節の先頭に立つ(実際に8案件すべてで出ていた)
|
|
1662
1866
|
// ツール(type)別にグループ化し、多いグループ(4件以上)は折りたたむ
|
|
1663
1867
|
var groups = [];
|
|
1664
1868
|
var byType = {};
|
|
@@ -1668,9 +1872,8 @@ const CLIENT_JS = `
|
|
|
1668
1872
|
g.items.push(s);
|
|
1669
1873
|
});
|
|
1670
1874
|
var UNIT = { "slack": "チャンネル", "github-issues": "リポジトリ", "github-discussions": "リポジトリ" };
|
|
1671
|
-
var wrap = el("div", { class: "ds-list" });
|
|
1672
1875
|
// チャット未使用: Slackグループの位置(先頭)に「存在するが未設定」のグレー行を出す
|
|
1673
|
-
if (chatOff)
|
|
1876
|
+
if (chatOff) pushCap("チャット", inactiveSourceRow("💬", "チャット", "ds-チャット"));
|
|
1674
1877
|
var chatAnchored = chatOff;
|
|
1675
1878
|
var devAnchored = devOff;
|
|
1676
1879
|
groups.forEach(function (g) {
|
|
@@ -1683,14 +1886,12 @@ const CLIENT_JS = `
|
|
|
1683
1886
|
g.items.forEach(function (s) { cards.appendChild(extSourceCard(s, meta)); });
|
|
1684
1887
|
if (g.items.length >= 4) {
|
|
1685
1888
|
// 折りたたみ(既定は閉)。summaryに件数と状態の要約を出す。
|
|
1686
|
-
//
|
|
1687
|
-
//
|
|
1688
|
-
// 使っているなら届いていないこと自体が問題なので拾う。gold 不明は警告する側に倒す。
|
|
1889
|
+
// **判定は共有の sourceNeedsAttention を呼ぶ。** 以前はここに中身を写経していたが、
|
|
1890
|
+
// 片方だけ直すと同じ画面でマップが⚠️・直下が「接続OK」になる(実際に起きた)。
|
|
1689
1891
|
var bad = 0;
|
|
1690
1892
|
var noToken = 0;
|
|
1691
1893
|
g.items.forEach(function (s) {
|
|
1692
|
-
if (s
|
|
1693
|
-
if (s.gold === false && s.notify !== true) return;
|
|
1894
|
+
if (!sourceNeedsAttention(s)) return;
|
|
1694
1895
|
bad++;
|
|
1695
1896
|
if (s.gate === "no_token") noToken++;
|
|
1696
1897
|
});
|
|
@@ -1706,30 +1907,198 @@ const CLIENT_JS = `
|
|
|
1706
1907
|
]);
|
|
1707
1908
|
var det = el("details", { class: "ds-group" }, [sum, cards]);
|
|
1708
1909
|
if (anchorId) det.id = anchorId;
|
|
1709
|
-
|
|
1910
|
+
pushCap(capOf(g.type), det);
|
|
1710
1911
|
} else {
|
|
1711
1912
|
if (anchorId && cards.firstChild) cards.firstChild.id = anchorId;
|
|
1712
|
-
|
|
1913
|
+
// **appendChild と違い pushCap はノードを動かさない。** firstChild を条件にすると
|
|
1914
|
+
// 永久に真のままになる(実際に無限ループを作った)。先に配列へ取り出す。
|
|
1915
|
+
var kids = [];
|
|
1916
|
+
for (var ci = 0; ci < cards.childNodes.length; ci++) kids.push(cards.childNodes[ci]);
|
|
1917
|
+
kids.forEach(function (n) { pushCap(capOf(g.type), n); });
|
|
1713
1918
|
}
|
|
1714
1919
|
});
|
|
1715
|
-
// 開発未使用: GitHubグループの位置(末尾)にグレー行を出す
|
|
1716
|
-
if (devOff) wrap.appendChild(inactiveSourceRow("🐙", "GitHub", "ds-開発"));
|
|
1717
|
-
sec.appendChild(wrap);
|
|
1718
1920
|
}
|
|
1719
|
-
//
|
|
1720
|
-
|
|
1721
|
-
|
|
1921
|
+
// 開発未使用: グレー行も能力の受け皿へ
|
|
1922
|
+
if (devOff) pushCap("開発", inactiveSourceRow("🐙", "GitHub", "ds-開発"));
|
|
1923
|
+
|
|
1924
|
+
// **追加フォームはグループの外に置く。** グループは0件だと描かれず、4件以上だと
|
|
1925
|
+
// 畳まれる(既定は閉)。中に入れると**新規案件(0件)=一番追加したい場面**で消える。
|
|
1722
1926
|
var adders = renderExternalAdders();
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1927
|
+
pushCap("チャット", adders.querySelector('[data-adder="slack"]'));
|
|
1928
|
+
pushCap("開発", adders.querySelector('[data-adder="github"]'));
|
|
1929
|
+
|
|
1930
|
+
// 連携の鍵は、その能力のところへ置く(画面下まで往復させない)
|
|
1931
|
+
var SECRET_CAP = { figma: "デザイン", backlog: "課題管理", "external-sources": "開発" };
|
|
1932
|
+
(SITE.secretKinds || []).forEach(function (k) {
|
|
1933
|
+
if (!SECRET_FORMS[k]) return;
|
|
1934
|
+
pushCap(SECRET_CAP[k], secretCard(k, SECRET_FORMS[k]));
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
// **手動投入はアダプタに依存しないので、別タイルにする。**
|
|
1938
|
+
// Google Meet でも Teams でも、文字起こしを手で入れる操作は同じ。
|
|
1939
|
+
// アダプタのタイルに混ぜると、Teams案件(自動取り込みが使えない=手動投入が
|
|
1940
|
+
// 唯一の口)で「未対応」のタイルの裏に隠れてしまう。
|
|
1941
|
+
manualBox["会議"] = intakeCardFor("会議");
|
|
1942
|
+
manualBox["共有資料"] = intakeCardFor("共有資料");
|
|
1943
|
+
|
|
1944
|
+
// ---- 能力ごとにタイルを並べる ----
|
|
1945
|
+
//
|
|
1946
|
+
// **タイルは「押すかどうか」を判断できるだけの情報を持つ。**
|
|
1947
|
+
// 押さないと分からない設計にすると、問題があること自体に気づけない。
|
|
1948
|
+
// いまの一覧が一番価値を出しているのはそこなので、畳んで失わないようにする。
|
|
1949
|
+
var CAP_META = {
|
|
1950
|
+
"課題管理": { icon: "📋", desc: "課題・ドキュメントの同期" },
|
|
1951
|
+
"会議": { icon: "🎙", desc: "文字起こしから議事録を生成" },
|
|
1952
|
+
"共有資料": { icon: "📁", desc: "資料をMarkdown化して取り込み" },
|
|
1953
|
+
"チャット": { icon: "💬", desc: "チャンネルの会話をGoldの材料に" },
|
|
1954
|
+
"デザイン": { icon: "🎨", desc: "画面インベントリの同期" },
|
|
1955
|
+
"開発": { icon: "🐙", desc: "IssuesをGoldの材料に" },
|
|
1956
|
+
};
|
|
1957
|
+
var STATE_LABEL = { ok: "● 接続OK", idle: "⦿ 未接続", off: "未使用", warn: "⚠️" };
|
|
1958
|
+
var SECRET_OF_CAP = { "デザイン": "figma", "課題管理": "backlog", "開発": "external-sources" };
|
|
1959
|
+
|
|
1960
|
+
/** タイルの2行目(状態と読み取り範囲)。**件数は出せる能力だけ出す** */
|
|
1961
|
+
function tileSub(cap, st) {
|
|
1962
|
+
var internal = internals.filter(function (s) { return s.kind === cap; })[0];
|
|
1963
|
+
var live = sources.filter(function (s) { return externalCapability(s.type) === cap; });
|
|
1964
|
+
var parts = [];
|
|
1965
|
+
if (st === "warn") {
|
|
1966
|
+
if (live.length) {
|
|
1967
|
+
var bad = 0, noToken = 0;
|
|
1968
|
+
live.forEach(function (s) {
|
|
1969
|
+
if (!sourceNeedsAttention(s)) return;
|
|
1970
|
+
bad++;
|
|
1971
|
+
if (s.gate === "no_token") noToken++;
|
|
1972
|
+
});
|
|
1973
|
+
parts.push(noToken === bad && bad > 0
|
|
1974
|
+
? "⚠️ " + bad + "件が読めていません(トークン未設定)"
|
|
1975
|
+
: "⚠️ " + bad + "件に問題");
|
|
1976
|
+
} else {
|
|
1977
|
+
// 内部能力の warn はパイプラインの失敗由来。数えるものが無いので件数を出さない
|
|
1978
|
+
parts.push("⚠️ 同期が失敗しています");
|
|
1979
|
+
}
|
|
1980
|
+
} else {
|
|
1981
|
+
// **off には2つの意味が同居している。** 「使わないと宣言した」(tools が none)と
|
|
1982
|
+
// 「まだ何も分かっていない」(fleet-status.json を夜間バッチがまだ作っていない)。
|
|
1983
|
+
// 後者を「未使用」と書くと、立ち上げ直後の案件で顧客が
|
|
1984
|
+
// 「Backlog も Slack も使わない案件だ」と読む——繋ぐ前提で契約しているのに
|
|
1985
|
+
var declaredOff = tools[cap] === "none" || (internal != null && internal.enabled === false);
|
|
1986
|
+
parts.push(st === "off" && !declaredOff ? "未設定" : STATE_LABEL[st] || "");
|
|
1987
|
+
}
|
|
1988
|
+
// 読み取り範囲。**この画面は顧客も見る。何をGoldに読んでいるかの開示が主眼**
|
|
1989
|
+
//
|
|
1990
|
+
// **繋がっているときだけ出す。** 「未使用」と「2リポジトリ/2件をGoldに使用」が
|
|
1991
|
+
// 同じ行に並ぶと読めない。tools を none に倒しても externalSources は翌朝の
|
|
1992
|
+
// 再生成まで残るので、使うのをやめた宣言の直後に必ず通る。
|
|
1993
|
+
// 「⦿ 未接続 Driveフォルダ 0件」も同様(最終同期を出さないのと同じ理由)
|
|
1994
|
+
var connected = st === "ok" || st === "warn";
|
|
1995
|
+
if (!connected) {
|
|
1996
|
+
// 状態だけ出す
|
|
1997
|
+
} else if (live.length) {
|
|
1998
|
+
// 単位は分かるものだけ。知らない種別に勝手な単位を付けない
|
|
1999
|
+
var unit = cap === "チャット" ? "チャンネル" : cap === "開発" ? "リポジトリ" : "件";
|
|
2000
|
+
var gold = live.filter(function (s) { return s.gold === true; }).length;
|
|
2001
|
+
parts.push(live.length + unit + "/" + gold + "件をGoldに使用");
|
|
2002
|
+
} else if (cap === "共有資料" && tools[cap] === "google-drive" && internal && internal.driveFolderIds) {
|
|
2003
|
+
// **ツールを見る。** local の案件(gdo・三菱)は Drive を使っていないのに
|
|
2004
|
+
// 「ローカル」と名乗ったタイルが「Driveフォルダ 0件」と語っていた
|
|
2005
|
+
parts.push("Driveフォルダ " + internal.driveFolderIds.length + "件");
|
|
2006
|
+
} else if (cap === "デザイン" && internal && internal.figmaFiles) {
|
|
2007
|
+
// **キーが無ければ何も出さない。** 未取得を「登録ゼロ」と断定して顧客に見せない
|
|
2008
|
+
parts.push("Figmaファイル " + internal.figmaFiles.length + "件");
|
|
2009
|
+
}
|
|
2010
|
+
// **未接続・未使用のときは最終同期を出さない。** 空振りしたrunの時刻なので、
|
|
2011
|
+
// 「未接続」と並べると「一度は繋がった」と読めてしまう
|
|
2012
|
+
if (internal && internal.lastSync && st !== "off" && st !== "idle") {
|
|
2013
|
+
var t = dsFmtTime(internal.lastSync);
|
|
2014
|
+
if (t) parts.push("最終同期 " + t);
|
|
2015
|
+
}
|
|
2016
|
+
return parts.join(" ");
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// **CAP_ORDER に無い能力も並べる。** 既知の順に並べたあと、受け皿に入った
|
|
2020
|
+
// 未知のもの(未対応のツール・新しい種別)を後ろに足す
|
|
2021
|
+
var extraCaps = Object.keys(capBox).filter(function (c) { return CAP_ORDER.indexOf(c) === -1; });
|
|
2022
|
+
var anyCap = false;
|
|
2023
|
+
CAP_ORDER.concat(extraCaps).forEach(function (cap) {
|
|
2024
|
+
var items = capBox[cap] || [];
|
|
2025
|
+
var declared = tools[cap] != null;
|
|
2026
|
+
// **tools に宣言があれば、行が無くてもタイルを出す。** 出さないと
|
|
2027
|
+
// Teams案件でチャットが丸ごと消え、「未対応です」と伝える場所も無くなる。
|
|
2028
|
+
// 接続マップのアンカー(ds-<能力>)の受け皿にもなる(いま2案件で解決できていない)
|
|
2029
|
+
// **手動投入はアダプタに依存しない。** 宣言や行の有無で弾くと、
|
|
2030
|
+
// fleet-status.json がまだ無い新規案件で投入の口ごと消える
|
|
2031
|
+
if (!items.length && !declared && !manualBox[cap]) return;
|
|
2032
|
+
anyCap = true;
|
|
2033
|
+
sec.appendChild(el("div", { class: "ds-cap", text: cap }));
|
|
2034
|
+
// アダプタのタイルは、宣言か行があるときだけ。手動投入だけが理由でここへ来た
|
|
2035
|
+
// 新規案件に、中身の無い「会議」タイルを立てても押す先が無い
|
|
2036
|
+
if (items.length || declared) renderAdapterTile(cap, items);
|
|
2037
|
+
renderManualTile(cap);
|
|
2038
|
+
});
|
|
2039
|
+
|
|
2040
|
+
function renderAdapterTile(cap, items) {
|
|
2041
|
+
var meta = CAP_META[cap] || { icon: "🔗", desc: "" };
|
|
2042
|
+
var st = capabilityState(cap, { tools: tools, internals: internals, externals: sources, pipes: pipes });
|
|
2043
|
+
// **ロゴはタイルの左に独立して置く。** 名前の行に入れると名前の高さに
|
|
2044
|
+
// 縛られて小さくなる。ロゴで能力を見分けられるのが一覧の主な用途
|
|
2045
|
+
var logo = ADAPTER_ICONS[tools[cap]];
|
|
2046
|
+
var ic = el("span", { class: "ds-tile-logo" });
|
|
2047
|
+
if (logo) ic.innerHTML = logo; // 公式ロゴ。ビルド時に埋め込み済みで外部通信は起きない
|
|
2048
|
+
else ic.textContent = meta.icon;
|
|
2049
|
+
// "none"(未使用の宣言)はツール名ではない。素通しすると顧客の画面に
|
|
2050
|
+
// 「none」というタイル名が出る(接続マップ側で一度直したのと同じ穴)
|
|
2051
|
+
var rawTool = tools[cap] && tools[cap] !== "none" ? tools[cap] : "";
|
|
2052
|
+
var body = el("div", { class: "ds-tile-body" }, [
|
|
2053
|
+
el("div", { class: "ds-tile-name", text: (rawTool && (CM_TOOL_LABEL_C[rawTool] || rawTool)) || cap }),
|
|
2054
|
+
el("div", { class: "ds-tile-desc", text: meta.desc }),
|
|
2055
|
+
el("div", { class: "ds-tile-sub ds-gate-" + st, text: tileSub(cap, st) }),
|
|
2056
|
+
]);
|
|
2057
|
+
// トークン未設定は**状態とは別軸**。状態機械に混ぜると判定が1箇所でなくなる。
|
|
2058
|
+
// ただし「使わない」と**宣言した**能力では出さない——設定する必要が無いものに
|
|
2059
|
+
// ついて設定を促す偽の警告になる(sushiro のデザインで出ていた)。
|
|
2060
|
+
//
|
|
2061
|
+
// **状態(off)ではなく宣言(none)で判断する。** off には「none と書いた」と
|
|
2062
|
+
// 「まだ何も書いていない」の両方が落ちてくる。状態で切ると、tools が空の
|
|
2063
|
+
// 新規案件——**まさに何を入れれば繋がるのかを知りたい場面**——から、
|
|
2064
|
+
// 鍵が未設定であることの手がかりまで消える
|
|
2065
|
+
var sk = SECRET_OF_CAP[cap];
|
|
2066
|
+
if (tools[cap] !== "none" && sk && (SITE.secretKinds || []).indexOf(sk) >= 0 && !((fleetVal("secretSources") || {})[sk])) {
|
|
2067
|
+
body.appendChild(el("div", { class: "ds-tile-note", text: "⚠ トークンが未設定です" }));
|
|
2068
|
+
}
|
|
2069
|
+
var tile = el("button", { class: "ds-tile", type: "button", id: "ds-" + cap }, [
|
|
2070
|
+
ic,
|
|
2071
|
+
body,
|
|
2072
|
+
el("span", { class: "ds-tile-chev", text: "›" }),
|
|
2073
|
+
]);
|
|
2074
|
+
tile.addEventListener("click", function () { openCapabilityModal(cap, items); });
|
|
2075
|
+
sec.appendChild(tile);
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// アダプタ非依存の操作(手動投入)は同じ能力の中で別タイルにする
|
|
2079
|
+
function renderManualTile(cap) {
|
|
2080
|
+
var manual = manualBox[cap];
|
|
2081
|
+
if (manual) {
|
|
2082
|
+
var mBody = el("div", { class: "ds-tile-body" }, [
|
|
2083
|
+
el("div", { class: "ds-tile-name", text: cap === "会議" ? "文字起こしを手動で追加" : "資料を手動で追加" }),
|
|
2084
|
+
el("div", { class: "ds-tile-desc", text: cap === "会議"
|
|
2085
|
+
? "自動取り込みできない会議(顧客主催・別ツール)の文字起こしから議事録を作ります"
|
|
2086
|
+
: "手元のファイルやテキストを Markdown 化して取り込みます" }),
|
|
2087
|
+
]);
|
|
2088
|
+
var mTile = el("button", { class: "ds-tile", type: "button", id: "ds-manual-" + cap }, [
|
|
2089
|
+
el("span", { class: "ds-tile-logo", text: "📥" }),
|
|
2090
|
+
mBody,
|
|
2091
|
+
el("span", { class: "ds-tile-chev", text: "›" }),
|
|
2092
|
+
]);
|
|
2093
|
+
mTile.addEventListener("click", function () {
|
|
2094
|
+
openCapabilityModal(cap === "会議" ? "文字起こしを手動で追加" : "資料を手動で追加", [manual]);
|
|
2095
|
+
});
|
|
2096
|
+
sec.appendChild(mTile);
|
|
1726
2097
|
}
|
|
1727
|
-
sec.appendChild(adders);
|
|
1728
2098
|
}
|
|
1729
2099
|
|
|
1730
|
-
//
|
|
1731
|
-
|
|
1732
|
-
if (internals.length || sources.length || chatOff || devOff) {
|
|
2100
|
+
// 注記は全能力の後ろに1つ。**Gold の4状態は凡例があって初めて読める。**
|
|
2101
|
+
if (anyCap) {
|
|
1733
2102
|
sec.appendChild(el("p", { class: "ds-note", text: "「Gold昇格の読み取り対象」のものだけを毎晩の Gold 昇格が読み取ります。「除外」は意図的に外したもの、「未宣言(対象外)」はまだ判断されていないもので、どちらも読み取りません(チャンネル・リポジトリ単位で切り替えられます)。" }));
|
|
1734
2103
|
}
|
|
1735
2104
|
|
|
@@ -2093,6 +2462,7 @@ const CLIENT_JS = `
|
|
|
2093
2462
|
];
|
|
2094
2463
|
},
|
|
2095
2464
|
build: function (url, name) { return { op: "add-channel", url: url, name: name }; },
|
|
2465
|
+
adder: "slack",
|
|
2096
2466
|
}));
|
|
2097
2467
|
|
|
2098
2468
|
if (usable("開発", "github")) {
|
|
@@ -2115,6 +2485,7 @@ const CLIENT_JS = `
|
|
|
2115
2485
|
];
|
|
2116
2486
|
},
|
|
2117
2487
|
build: function (repo, name) { return { op: "add-github-source", repo: repo, name: name }; },
|
|
2488
|
+
adder: "github",
|
|
2118
2489
|
}));
|
|
2119
2490
|
}
|
|
2120
2491
|
return box;
|
|
@@ -2308,7 +2679,7 @@ const CLIENT_JS = `
|
|
|
2308
2679
|
// このセッションで追加したものを並べる場所(リロードすると消える=正式な一覧は上に出る)
|
|
2309
2680
|
var pending = el("div", { class: "ds-pending" });
|
|
2310
2681
|
|
|
2311
|
-
var box = el("div", { class: "ds-addform" }, [
|
|
2682
|
+
var box = el("div", opts.adder ? { class: "ds-addform", "data-adder": opts.adder } : { class: "ds-addform" }, [
|
|
2312
2683
|
el("div", { class: "ds-addform-title", text: opts.title }),
|
|
2313
2684
|
opts.hint ? el("p", { class: "sec-how", text: opts.hint }) : null,
|
|
2314
2685
|
main,
|
|
@@ -2578,6 +2949,9 @@ const CLIENT_JS = `
|
|
|
2578
2949
|
*/
|
|
2579
2950
|
function refreshDataSources() {
|
|
2580
2951
|
if (listState.tab !== "ops") return;
|
|
2952
|
+
// **モーダルが開いている間は差し替えない。** 下地を入れ替えると、開いているモーダルの
|
|
2953
|
+
// 中身が切り離されたノードになる(閉じるときに refreshDataSources を呼び直す)
|
|
2954
|
+
if (isModalOpen()) return;
|
|
2581
2955
|
var old = app.querySelector(".datasources");
|
|
2582
2956
|
// **触っている最中は差し替えない。** この節の中には追加フォームの入力欄があり、
|
|
2583
2957
|
// 貼りかけのURLや「保存中…」の表示ごと作り直してしまう。保存が成功していても
|
|
@@ -2595,27 +2969,8 @@ const CLIENT_JS = `
|
|
|
2595
2969
|
else app.insertBefore(next, app.firstChild);
|
|
2596
2970
|
}
|
|
2597
2971
|
updateConnMapAsOf();
|
|
2598
|
-
updateSecretStates();
|
|
2599
2972
|
}
|
|
2600
2973
|
|
|
2601
|
-
/**
|
|
2602
|
-
* 鍵カードの「現在: ◯◯ から取得中」だけを更新する。
|
|
2603
|
-
* **カードごと作り直さない**(貼りかけのトークンが消える)。表示は1行なので差し替えで足りる。
|
|
2604
|
-
*/
|
|
2605
|
-
function updateSecretStates() {
|
|
2606
|
-
var sources = fleetVal("secretSources") || {};
|
|
2607
|
-
var cards = app.querySelectorAll("[data-secret-kind]");
|
|
2608
|
-
for (var i = 0; i < cards.length; i++) {
|
|
2609
|
-
var kind = cards[i].getAttribute("data-secret-kind");
|
|
2610
|
-
var line = cards[i].querySelector(".sec-state");
|
|
2611
|
-
if (!line) continue;
|
|
2612
|
-
var src = sources[kind] || "";
|
|
2613
|
-
line.className = src
|
|
2614
|
-
? "sec-state" + (src === "Secrets Manager" ? " is-managed" : "")
|
|
2615
|
-
: "sec-state is-none";
|
|
2616
|
-
line.textContent = src ? "現在: " + src + " から取得中" : "現在: 未設定";
|
|
2617
|
-
}
|
|
2618
|
-
}
|
|
2619
2974
|
|
|
2620
2975
|
/**
|
|
2621
2976
|
* 取得中フラグ。**完了しても寝かせたままにしない。**
|
|
@@ -2661,15 +3016,25 @@ const CLIENT_JS = `
|
|
|
2661
3016
|
renderConnMap();
|
|
2662
3017
|
renderDataSources();
|
|
2663
3018
|
loadLiveFleet();
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
3019
|
+
// **取り込みカードと鍵カードは能力セクションへ移した**(#101)。
|
|
3020
|
+
// ここで再度出すと二重になる。手動投入は 会議 / 共有資料 の中にある。
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
/**
|
|
3024
|
+
* 手動投入のカード(文字起こし・資料)。**その能力の中に置く。**
|
|
3025
|
+
* 「顧客主催の会議など、自動取り込みできない会議」に使うものなので、
|
|
3026
|
+
* 会議の設定を見ている文脈で押せるのが自然。以前は画面下部の別セクションにあり、
|
|
3027
|
+
* 会議の設定と往復する必要があった。
|
|
3028
|
+
*/
|
|
3029
|
+
function intakeCardFor(cap) {
|
|
3030
|
+
if (!SITE.intake) return null;
|
|
3031
|
+
if (cap === "会議") {
|
|
3032
|
+
return opsCard("🗒️", "会議の文字起こしを追加", "顧客主催の会議など、自動取り込みできない会議の文字起こしファイル(.txt / .docx 等)から議事録を生成します。ファイルを選ぶかテキストを貼って送信すると、数分で反映されます。", "transcript", "文字起こしを追加");
|
|
2672
3033
|
}
|
|
3034
|
+
if (cap === "共有資料") {
|
|
3035
|
+
return opsCard("📎", "共有資料を追加", "提案書・仕様書・議事メモなどの資料を Markdown 化して 共有資料 に取り込みます。ファイルを選ぶかテキストを貼って送信すると、数分で反映されます。", "material", "資料を追加");
|
|
3036
|
+
}
|
|
3037
|
+
return null;
|
|
2673
3038
|
}
|
|
2674
3039
|
|
|
2675
3040
|
/** 鍵の入力欄に出す説明。kind は Lambda 側(resolveSecretTarget)と綴りを合わせること */
|
|
@@ -2714,18 +3079,10 @@ const CLIENT_JS = `
|
|
|
2714
3079
|
* **入れた値は読み出せない。** 表示・確認の経路は作っていないので、間違えたら入れ直す。
|
|
2715
3080
|
* 保存先は案件ごとに分かれており、他の案件には影響しない。
|
|
2716
3081
|
* 反映は次回のワークフロー実行から(接続状況の表示が変わるのは翌朝の自己チェック後)。
|
|
3082
|
+
*
|
|
3083
|
+
* カードは #101 で能力モーダルの中へ移した(buildDataSources の pushCap 経由)。
|
|
3084
|
+
* まとめて並べる renderSecrets() は呼び出し元が無くなったので消してある。
|
|
2717
3085
|
*/
|
|
2718
|
-
function renderSecrets() {
|
|
2719
|
-
var kinds = (SITE.secretKinds || []).filter(function (k) { return SECRET_FORMS[k]; });
|
|
2720
|
-
if (!kinds.length) return;
|
|
2721
|
-
|
|
2722
|
-
app.appendChild(el("div", { class: "section-label", text: "連携の鍵" }));
|
|
2723
|
-
app.appendChild(el("p", { class: "ops-intro", text: "各ツールを読むためのトークンです。入れた値はここには表示されません(保存だけできます)。保存先は案件ごとに分かれているので、他の案件には影響しません。" }));
|
|
2724
|
-
var grid = el("div", { class: "ops-grid" });
|
|
2725
|
-
kinds.forEach(function (k) { grid.appendChild(secretCard(k, SECRET_FORMS[k])); });
|
|
2726
|
-
app.appendChild(grid);
|
|
2727
|
-
}
|
|
2728
|
-
|
|
2729
3086
|
function secretCard(kind, meta) {
|
|
2730
3087
|
// **いま何で動いているか**を先に出す。入れ直したのに反映されていない状態が
|
|
2731
3088
|
// 画面で分かるようにする(接続状況は夜間バッチのスナップショットなので、
|
|
@@ -2794,6 +3151,11 @@ const CLIENT_JS = `
|
|
|
2794
3151
|
el("p", { class: "sec-how", text: meta.how }),
|
|
2795
3152
|
meta.selfServe ? el("p", { class: "sec-how", text: meta.selfServe.note }) : null,
|
|
2796
3153
|
input,
|
|
3154
|
+
// **扱いの説明はカードに持たせる。** 節の見出しの下に置いていたときは、
|
|
3155
|
+
// #101 でカードだけがモーダルへ移り、説明だけが画面から消えた。
|
|
3156
|
+
// 顧客にトークンの貼り付けを求める画面で、貼った値がどう扱われるかの
|
|
3157
|
+
// 説明が落ちるのは、いちばん落としてはいけない種類の欠落
|
|
3158
|
+
el("p", { class: "sec-note", text: "入れた値はここには表示されません(保存だけできます)。保存先は案件ごとに分かれているので、他の案件には影響しません。" }),
|
|
2797
3159
|
el("div", { class: "ds-toggle" }, [btn, msg]),
|
|
2798
3160
|
]);
|
|
2799
3161
|
}
|
|
@@ -2858,13 +3220,115 @@ const CLIENT_JS = `
|
|
|
2858
3220
|
control,
|
|
2859
3221
|
]);
|
|
2860
3222
|
}
|
|
3223
|
+
/**
|
|
3224
|
+
* モーダルの土台。**能力モーダルと投入モーダルで共有する。**
|
|
3225
|
+
*
|
|
3226
|
+
* **app の中に置く(body 直下にしない)。** isModalOpen() が
|
|
3227
|
+
* app.querySelector(".modal-overlay") で見ているので、外に出すと
|
|
3228
|
+
* 「開いている」と判定されず、ライブ差し替えが .datasources を作り直す。
|
|
3229
|
+
* モーダルの中身は差し替え前の古いノードのまま取り残され、トグルを押しても
|
|
3230
|
+
* 結果は切り離されたノードに書かれる——「押したのに何も起きない」に見える。
|
|
3231
|
+
* position:fixed なので app 内でも前面に出る(#app に transform/filter/contain は無い)。
|
|
3232
|
+
*
|
|
3233
|
+
* @returns { overlay, modal, close }
|
|
3234
|
+
*/
|
|
3235
|
+
var modalSeq = 0;
|
|
3236
|
+
function makeModal(titleText, wide) {
|
|
3237
|
+
// **二重オープンを防ぐ。** タイルを続けて押すとオーバーレイが重なり、
|
|
3238
|
+
// 1枚閉じても下の1枚が残って操作を受け付けない状態になる。
|
|
3239
|
+
// **前のものは close() で畳む。** DOM から外すだけだと keydown が
|
|
3240
|
+
// document に残り続け、押すたびに1本ずつ増える
|
|
3241
|
+
// **押し替えのときは差し替えを走らせない。** close() は「見送っていたライブ値を
|
|
3242
|
+
// 取り込む」ために .datasources を作り直すが、いま押されたタイルは**古い節の
|
|
3243
|
+
// ノード**で、掴んでいる一覧も古い。作り直すと、下のタイルは「2リポジトリ」・
|
|
3244
|
+
// 上のモーダルは「1リポジトリ」という食い違いになる。
|
|
3245
|
+
// 取り込みは**最後の1枚を閉じたとき**に行えば足りる
|
|
3246
|
+
var existing = app.querySelector(".modal-overlay");
|
|
3247
|
+
if (existing) {
|
|
3248
|
+
if (existing.__close) existing.__close(true);
|
|
3249
|
+
else if (existing.parentNode) existing.parentNode.removeChild(existing);
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
// 開く前にフォーカスがどこにあったかを覚え、閉じたら戻す。
|
|
3253
|
+
// 戻さないと、キーボードだけで操作している人は一覧の先頭へ飛ばされる
|
|
3254
|
+
var returnTo = document.activeElement;
|
|
3255
|
+
var overlay = el("div", { class: "modal-overlay" });
|
|
3256
|
+
var modal = el("div", { class: wide ? "modal modal-wide" : "modal", role: "dialog", "aria-modal": "true" });
|
|
3257
|
+
// **dialog に名前を付ける。** 無いとスクリーンリーダは「ダイアログ」としか
|
|
3258
|
+
// 読まず、どの能力を開いたのかが分からない。直下に見出しがあるのに
|
|
3259
|
+
// 参照していなかった
|
|
3260
|
+
if (titleText) {
|
|
3261
|
+
modalSeq += 1;
|
|
3262
|
+
var titleId = "modal-title-" + modalSeq;
|
|
3263
|
+
// **見出し行に✕を置く。** 「閉じる」は末尾にしかなく、19リポジトリのモーダルでは
|
|
3264
|
+
// マウスだけの人が全部スクロールしないと届かない(キーボードはフォーカスが
|
|
3265
|
+
// 閉じるに載るので届く)
|
|
3266
|
+
var x = el("button", { class: "modal-x", type: "button", "aria-label": "閉じる", text: "✕" });
|
|
3267
|
+
x.addEventListener("click", function () { close(); });
|
|
3268
|
+
modal.appendChild(el("div", { class: "modal-head" }, [
|
|
3269
|
+
el("h2", { class: "modal-title", id: titleId, text: titleText }),
|
|
3270
|
+
x,
|
|
3271
|
+
]));
|
|
3272
|
+
modal.setAttribute("aria-labelledby", titleId);
|
|
3273
|
+
} else {
|
|
3274
|
+
modal.setAttribute("aria-label", "設定");
|
|
3275
|
+
}
|
|
3276
|
+
overlay.__close = close;
|
|
3277
|
+
|
|
3278
|
+
function close(skipRefresh) {
|
|
3279
|
+
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
|
3280
|
+
var behind = app.querySelector(".datasources");
|
|
3281
|
+
if (behind) { behind.removeAttribute("inert"); behind.removeAttribute("aria-hidden"); }
|
|
3282
|
+
document.removeEventListener("keydown", onKey);
|
|
3283
|
+
if (returnTo && typeof returnTo.focus === "function" && returnTo.isConnected !== false) returnTo.focus();
|
|
3284
|
+
// **閉じたら差し替えを試し直す。** 開いている間はライブ値の反映を見送るが、
|
|
3285
|
+
// 見送ったままだと再取得の口が無く、タブを離れて戻るまで古い値のままになる。
|
|
3286
|
+
if (skipRefresh !== true && typeof refreshDataSources === "function") refreshDataSources();
|
|
3287
|
+
}
|
|
3288
|
+
function onKey(e) { if (e.key === "Escape") close(); }
|
|
3289
|
+
document.addEventListener("keydown", onKey);
|
|
3290
|
+
overlay.addEventListener("click", function (e) { if (e.target === overlay) close(); });
|
|
3291
|
+
return { overlay: overlay, modal: modal, close: close };
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
/**
|
|
3295
|
+
* モーダルを app に載せて、中へフォーカスを移す。
|
|
3296
|
+
*
|
|
3297
|
+
* **「最初の focusable」に載せてはいけない。** 一覧の先頭に来るのは
|
|
3298
|
+
* 「Gold昇格を止める」「読み取りを止める」「このファイルを外す」といった
|
|
3299
|
+
* 設定を書き換えるボタンで、しかも**狭める向きは意図的に確認を出さない**
|
|
3300
|
+
* (stateToggle の confirmOn は広げる向きだけ)。タイルを Enter で開いて
|
|
3301
|
+
* もう一度 Enter を押しただけで、確認なしに設定ファイルが書き換わる。
|
|
3302
|
+
* 移す先は閉じるボタン——押しても何も壊れないものにする。
|
|
3303
|
+
*/
|
|
3304
|
+
function showModal(overlay, modal) {
|
|
3305
|
+
// **下地を触れなくする。** 無いと Tab がモーダルの外へ抜け、読み上げも
|
|
3306
|
+
// 背後の一覧を読み続ける(モーダルを開いた意味が無くなる)
|
|
3307
|
+
var behind = app.querySelector(".datasources");
|
|
3308
|
+
if (behind) { behind.setAttribute("inert", ""); behind.setAttribute("aria-hidden", "true"); }
|
|
3309
|
+
app.appendChild(overlay);
|
|
3310
|
+
var safe = modal.querySelector(".modal-actions button, .modal-cancel");
|
|
3311
|
+
if (!safe) {
|
|
3312
|
+
// 閉じるボタンが無い形のモーダル用。見出しに載せて読み上げの起点にする
|
|
3313
|
+
modal.setAttribute("tabindex", "-1");
|
|
3314
|
+
safe = modal;
|
|
3315
|
+
}
|
|
3316
|
+
if (typeof safe.focus === "function") safe.focus();
|
|
3317
|
+
}
|
|
3318
|
+
/** モーダルが開いているか(ライブ差し替えの見送り判定に使う) */
|
|
3319
|
+
function isModalOpen() { return !!app.querySelector(".modal-overlay"); }
|
|
3320
|
+
|
|
2861
3321
|
function openIntakeModal(kind) {
|
|
2862
3322
|
var intake = SITE.intake;
|
|
2863
3323
|
if (!intake) return;
|
|
2864
3324
|
var isT = kind === "transcript";
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
3325
|
+
// **土台は makeModal で作る。** 以前は独自に overlay を組んで body 直下へ
|
|
3326
|
+
// 置いていたため、能力モーダルの上に2枚目が重なり、Escape が下の1枚だけを
|
|
3327
|
+
// 閉じて上が残った。isModalOpen も app しか見ないので、開いている間の
|
|
3328
|
+
// ライブ差し替えの見送りも効いていなかった
|
|
3329
|
+
var mk = makeModal(isT ? "会議の文字起こしを追加" : "共有資料を追加", false);
|
|
3330
|
+
var overlay = mk.overlay;
|
|
3331
|
+
var modal = mk.modal;
|
|
2868
3332
|
modal.appendChild(el("p", { class: "modal-desc", text: isT
|
|
2869
3333
|
? "文字起こしファイル(テキスト・Word・PDF・字幕 等)を選んで送信してください。(送信後、数分で議事録が自動生成されます)"
|
|
2870
3334
|
: "資料ファイル(PDF・Word・PowerPoint・Excel・テキスト・画像 等)を選んで送信してください。(送信後、数分で内容が Markdown に変換されて取り込まれます)" }));
|
|
@@ -2892,7 +3356,7 @@ const CLIENT_JS = `
|
|
|
2892
3356
|
modal.appendChild(status);
|
|
2893
3357
|
modal.appendChild(el("div", { class: "modal-actions" }, [cancelBtn, submitBtn]));
|
|
2894
3358
|
overlay.appendChild(modal);
|
|
2895
|
-
|
|
3359
|
+
showModal(overlay, modal);
|
|
2896
3360
|
|
|
2897
3361
|
function showStatus(msg, isError) { status.className = "modal-status" + (isError ? " modal-status-error" : ""); status.textContent = msg; }
|
|
2898
3362
|
function update() { submitBtn.disabled = !(nameInput.value.trim() && selectedFiles.length); }
|
|
@@ -2928,9 +3392,8 @@ const CLIENT_JS = `
|
|
|
2928
3392
|
dropzone.addEventListener("drop", function (e) { e.preventDefault(); dropzone.classList.remove("dragover"); addFiles(e.dataTransfer.files); });
|
|
2929
3393
|
nameInput.addEventListener("input", update);
|
|
2930
3394
|
|
|
2931
|
-
|
|
2932
|
-
cancelBtn.addEventListener("click", close);
|
|
2933
|
-
overlay.addEventListener("click", function (e) { if (e.target === overlay) close(); });
|
|
3395
|
+
// 閉じる経路(Escape・背景クリック)は makeModal 側に用意されている
|
|
3396
|
+
cancelBtn.addEventListener("click", mk.close);
|
|
2934
3397
|
submitBtn.addEventListener("click", function () { submitIntake(kind, selectedFiles, nameInput.value, status, submitBtn); });
|
|
2935
3398
|
}
|
|
2936
3399
|
async function submitIntake(kind, files, submitter, status, submitBtn) {
|
package/package.json
CHANGED