@takagaki/cortex-decisions-viewer 0.7.1 → 0.9.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/dist/render.js +1292 -37
- package/package.json +1 -1
package/dist/render.js
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UNSETTLED_LIMITS = exports.REFLECT_OPS = exports.REFLECTING_LIMITS = void 0;
|
|
3
4
|
exports.renderSite = renderSite;
|
|
5
|
+
exports.livePending = livePending;
|
|
6
|
+
exports.nextCheckDelay = nextCheckDelay;
|
|
7
|
+
exports.settledPending = settledPending;
|
|
8
|
+
exports.sanitizeRegisteredNow = sanitizeRegisteredNow;
|
|
9
|
+
exports.chatRowsToAdd = chatRowsToAdd;
|
|
10
|
+
exports.devRowsToAdd = devRowsToAdd;
|
|
11
|
+
exports.designRowsToAdd = designRowsToAdd;
|
|
12
|
+
exports.materialRowsToAdd = materialRowsToAdd;
|
|
13
|
+
exports.issueRowsToAdd = issueRowsToAdd;
|
|
14
|
+
exports.unsettledRegisteredCount = unsettledRegisteredCount;
|
|
15
|
+
exports.registeredRowsToAdd = registeredRowsToAdd;
|
|
16
|
+
exports.mergeRegisteredFleet = mergeRegisteredFleet;
|
|
17
|
+
exports.nextStateDelay = nextStateDelay;
|
|
4
18
|
exports.renderConnectionMap = renderConnectionMap;
|
|
5
19
|
/** SiteDataを単一の自己完結HTML(埋め込みデータ+CSS+JS)にレンダリングする */
|
|
6
20
|
function renderSite(site) {
|
|
@@ -179,6 +193,12 @@ const CM_TOOL_LABEL = {
|
|
|
179
193
|
function sourceNeedsAttention(s) {
|
|
180
194
|
if (s.gate === "ok")
|
|
181
195
|
return false;
|
|
196
|
+
// 🔴 **測っていない行を警告に数えない(段階1)。** 状態が無いのは「実測して分からなかった」
|
|
197
|
+
// ではなく「まだ誰も測っていない」——いま登録したばかりの行がこれになる。数えると
|
|
198
|
+
// **登録した直後に「⚠️ 1件に問題」と出て、タイルが警告色になる**(登録は成功している)。
|
|
199
|
+
// `unknown` は残す——あちらは測ったうえで分からなかった、という別の事実。
|
|
200
|
+
if (s.gate == null || s.gate === "")
|
|
201
|
+
return false;
|
|
182
202
|
if (s.goldState === "excluded")
|
|
183
203
|
return false;
|
|
184
204
|
return true;
|
|
@@ -310,6 +330,494 @@ function backlogTargetOf(input) {
|
|
|
310
330
|
return num.test(view[1]) ? null : { domain: domain, projectKey: view[1].toUpperCase() };
|
|
311
331
|
return null;
|
|
312
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* 反映待ちの期限。**15分。**
|
|
335
|
+
*
|
|
336
|
+
* 実測(cortex-context「セットアップ状況の自己チェック」の完了 run 112件、
|
|
337
|
+
* うち workflow_dispatch 51件):
|
|
338
|
+
*
|
|
339
|
+
* | | 全体(112) | dispatch のみ(51) |
|
|
340
|
+
* | min | 4秒 | 6秒 |
|
|
341
|
+
* | p50 | 41秒 | 47秒 |
|
|
342
|
+
* | p90 | 57秒 | 62秒 |
|
|
343
|
+
* | p95 | 65秒 | ― |
|
|
344
|
+
* | max | 400秒 | 400秒 |
|
|
345
|
+
*
|
|
346
|
+
* 必要な余裕は 最悪400秒 + op=state のキャッシュ60秒 + 取り直し間隔とのズレ60秒 = 520秒。
|
|
347
|
+
* 10分では余裕が1分20秒しかなく、自己チェックが待ち行列(concurrency)に入れば超える。
|
|
348
|
+
*
|
|
349
|
+
* 🔴 **期限が無いと、反映が来ない案件で「反映中」が永久に出て、本当の「未設定」を隠す。**
|
|
350
|
+
* 完了 run の 8%(9/112)は failure で、失敗した run はコミットしない。つまり
|
|
351
|
+
* 8%程度の確率で「15分間 反映中 → 未設定に戻る」が起きる(設計どおりの挙動)。
|
|
352
|
+
*/
|
|
353
|
+
const REFLECT_TTL_MS = 15 * 60 * 1000;
|
|
354
|
+
/** 自動で取り直す間隔。60秒(op=state のキャッシュと同じ粒度) */
|
|
355
|
+
const REFLECT_POLL_MS = 60 * 1000;
|
|
356
|
+
/**
|
|
357
|
+
* 期限と間隔を**テストから固定するための入れ物**(infra の STATE_LIMITS と同じ形)。
|
|
358
|
+
*
|
|
359
|
+
* ⚠ **定数そのものを export してはいけない。** `export const` は CommonJS へ落ちるときに
|
|
360
|
+
* 参照が `exports.X` へ書き換えられ、**toString() で流し込んだ先には exports が無いので
|
|
361
|
+
* クライアントJSが丸ごと落ちる**(実際に踏んだ)。流し込む関数から参照する値は、
|
|
362
|
+
* export しないローカルの const に置くこと。
|
|
363
|
+
*/
|
|
364
|
+
exports.REFLECTING_LIMITS = { ttlMs: REFLECT_TTL_MS, pollMs: REFLECT_POLL_MS };
|
|
365
|
+
/**
|
|
366
|
+
* 記録してよい操作の対応表。**既定は「記録しない」(null)。**
|
|
367
|
+
*
|
|
368
|
+
* 🔴 **settle できない操作を記録すると、期限まで「反映中」が出っぱなしになり、
|
|
369
|
+
* 本当の状態を隠す。** 「現れた/消えた」で判定できるものだけを載せる。
|
|
370
|
+
*
|
|
371
|
+
* @see CONFIG_OPS(infra/lambda/intake/logic.mjs)— **突き合わせるテストがある**。
|
|
372
|
+
* op を足してここに書き忘れると、その操作だけ黙って反映中にならない/
|
|
373
|
+
* settle できないものを記録して出っぱなしになる、という気づきにくい壊れ方をする
|
|
374
|
+
*/
|
|
375
|
+
exports.REFLECT_OPS = {
|
|
376
|
+
// ---- 記録する ----
|
|
377
|
+
// 接続先の追加・削除。一覧に「現れた/消えた」で反映を判定できる
|
|
378
|
+
"backlog-target-add": { cap: "課題管理", dir: "add", from: "url" },
|
|
379
|
+
// **削除はキーだけを送る**(サーバもキーだけで消す)ので、スペースは問わない
|
|
380
|
+
//
|
|
381
|
+
// ⚠ **最後の1件を外したときは、反映が届いても settle しない**(engine を読んで確認・2026-08-21)。
|
|
382
|
+
// engine の fleet-status は、正本(issues-config.json)が空になると
|
|
383
|
+
// 同期ミラー(issues/backlog-settings.json)の旧値へフォールバックし、
|
|
384
|
+
// backlogTarget に旧 domain / projectKey を載せ続ける(capabilities も on のまま)。
|
|
385
|
+
// ミラーは履歴保全のため**画面から外しても残す設計**なので、この値は消えない。
|
|
386
|
+
// つまり「消えた」を観測できるのは、ミラーが1つも無い案件(一度も同期していない)だけ。
|
|
387
|
+
// それ以外では**期限(15分)で落ちる**——嘘は残らないが、その間「反映中」と出る。
|
|
388
|
+
// 直すなら engine 側(backlogTarget.source === "secrets" かつ configPath === null を
|
|
389
|
+
// 「正本に接続先が無い」として画面に伝える)で、これは「未設定」の意味を変える
|
|
390
|
+
// 製品判断なので別issueにする。
|
|
391
|
+
"backlog-target-remove": { cap: "課題管理", dir: "remove", from: "backlogProjectKey" },
|
|
392
|
+
// ---- 記録しない ----
|
|
393
|
+
// **値の変更**。読み取りの ON/OFF は「現れた/消えた」では判定できない
|
|
394
|
+
"set-github-source-enabled": null,
|
|
395
|
+
// **表示名の取り直し**。sendConfig を通らない2本目の書き込み口でもある
|
|
396
|
+
"refresh-channel-name": null,
|
|
397
|
+
// 以下は add / remove の形だが、**その能力の「反映中」はまだ作っていない**。
|
|
398
|
+
// 追加フォームには既存の控え(.ds-pending-item「追加済み・反映中」)が出ており、
|
|
399
|
+
// 二重に出さないため、統合は別issueにする(設計 追補 中2)
|
|
400
|
+
"add-channel": null,
|
|
401
|
+
"remove-channel": null,
|
|
402
|
+
"add-github-source": null,
|
|
403
|
+
"remove-github-source": null,
|
|
404
|
+
"add-figma-file": null,
|
|
405
|
+
"remove-figma-file": null,
|
|
406
|
+
"add-drive-folder": null,
|
|
407
|
+
"remove-drive-folder": null,
|
|
408
|
+
// 1件だけ持てた頃の置き換え。いまの画面は出さない(backlog-target-add に統合済み)
|
|
409
|
+
"set-backlog-target": null,
|
|
410
|
+
};
|
|
411
|
+
/**
|
|
412
|
+
* 期限内のものだけ返す。
|
|
413
|
+
*
|
|
414
|
+
* 🔴 **経過時間(now - at)で判定すること。** 取り直した回数で数えると、
|
|
415
|
+
* スリープ復帰やバックグラウンドタブのスロットリングで期限が伸びる。
|
|
416
|
+
*
|
|
417
|
+
* 🔴 **経過時間は「上」だけでなく「下」も見る。** 上限(age < 期限)しか見ないと、
|
|
418
|
+
* **at が未来の記録は永久に期限が切れない**——
|
|
419
|
+
* - at = now + 1時間 … 1時間15分ずっと「反映中」
|
|
420
|
+
* - at = Infinity … 永久に「反映中」のまま60秒ごとに取り直し続ける
|
|
421
|
+
* 実在する経路: **端末の時計が進んだ状態で記録し、NTP で戻る**/別バージョンが書いた値/
|
|
422
|
+
* localStorage の手編集。「期限が必ず切れるから嘘は残らない」というこの設計の
|
|
423
|
+
* 唯一の安全弁が、入口で成立しなくなる。
|
|
424
|
+
*/
|
|
425
|
+
function livePending(list, now) {
|
|
426
|
+
const out = [];
|
|
427
|
+
for (const e of list || []) {
|
|
428
|
+
// NaN・欠けた at・未来の at・Infinity はここで落ちる
|
|
429
|
+
const age = e ? now - e.at : NaN;
|
|
430
|
+
if (!e || !Number.isFinite(age) || !(age >= 0 && age < REFLECT_TTL_MS))
|
|
431
|
+
continue;
|
|
432
|
+
out.push(e);
|
|
433
|
+
}
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* 次に取り直すまでの ms。もう取り直す必要が無ければ null。
|
|
438
|
+
*
|
|
439
|
+
* 🔴 **最後の1回は期限ちょうどに来る**(min(60秒, 期限 − 経過))。
|
|
440
|
+
* 固定60秒で張ると、最後の確認が期限の60秒前になり、そこから期限までに反映が来ても
|
|
441
|
+
* 誰も見ない。さらに**期限そのものに誰も描き直さない**ので、「取り直しは止まったのに
|
|
442
|
+
* 反映中の文字が残る」時間ができる。
|
|
443
|
+
*/
|
|
444
|
+
function nextCheckDelay(list, now) {
|
|
445
|
+
const live = livePending(list, now);
|
|
446
|
+
if (!live.length)
|
|
447
|
+
return null;
|
|
448
|
+
let deadline = Infinity;
|
|
449
|
+
for (const e of live)
|
|
450
|
+
deadline = Math.min(deadline, e.at + REFLECT_TTL_MS);
|
|
451
|
+
const left = deadline - now;
|
|
452
|
+
return left < REFLECT_POLL_MS ? left : REFLECT_POLL_MS;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* 反映が来たものを落とす。
|
|
456
|
+
*
|
|
457
|
+
* @param presentByCap 能力ごとの「**スナップショットが答えている集合**」。
|
|
458
|
+
* 🔴 **「画面に出ているか」を渡してはいけない。** 追加・削除の応答には一覧が添えられ、
|
|
459
|
+
* その場で画面が差し替わるので、記録した瞬間に反映済みと判定されて消える
|
|
460
|
+
* (=機能が丸ごと no-op になる)。渡すのは「未設定」を出すか決めているのと
|
|
461
|
+
* 同じ関数の戻り値(画面側は backlogTargetsNow)。
|
|
462
|
+
* その能力を判定できないときは null を渡す——落とさず、期限に任せる。
|
|
463
|
+
*/
|
|
464
|
+
function settledPending(list, presentByCap) {
|
|
465
|
+
const out = [];
|
|
466
|
+
for (const e of list || []) {
|
|
467
|
+
const present = presentByCap ? presentByCap[e.cap] : null;
|
|
468
|
+
if (!present) {
|
|
469
|
+
out.push(e);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
let hit = false;
|
|
473
|
+
for (const p of present) {
|
|
474
|
+
if (!p || !p.projectKey)
|
|
475
|
+
continue;
|
|
476
|
+
if (String(p.projectKey).toUpperCase() !== String(e.projectKey).toUpperCase())
|
|
477
|
+
continue;
|
|
478
|
+
// 空の domain は「どのスペースでもよい」(削除はキーだけを送るため)
|
|
479
|
+
if (e.domain && String(p.domain || "").toLowerCase() !== String(e.domain).toLowerCase())
|
|
480
|
+
continue;
|
|
481
|
+
hit = true;
|
|
482
|
+
}
|
|
483
|
+
// 消す操作は「消えたら反映済み」。ここを add と同じにすると、削除した接続先が
|
|
484
|
+
// 消えたその瞬間から期限まで「反映中」と言い続ける
|
|
485
|
+
const reflected = e.dir === "remove" ? !hit : hit;
|
|
486
|
+
if (!reflected)
|
|
487
|
+
out.push(e);
|
|
488
|
+
}
|
|
489
|
+
return out;
|
|
490
|
+
}
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
// 登録の即時値(registeredNow)— **まとめ表がまだ知らない行を足す**(段階1)
|
|
493
|
+
//
|
|
494
|
+
// 守るべき不変条件は3つ:
|
|
495
|
+
// 1. **足すだけ・置き換えない**(origin / goldState / notify / gate はまとめ表にしか無い)
|
|
496
|
+
// 2. **既知の欄だけ・葉は文字列だけ**(ライブ専用のキーなので build.ts の網にかからない)
|
|
497
|
+
// 3. **正規化しない**(engine が読むのは生値。揃えにいくと「画面には出るのに読まれない」行ができる)
|
|
498
|
+
// ---------------------------------------------------------------------------
|
|
499
|
+
/** 応答のキー(ASCII)→ 画面の能力名。**対応表はここ1つだけ** */
|
|
500
|
+
const REGISTERED_CAP_NAMES = {
|
|
501
|
+
chat: "チャット",
|
|
502
|
+
dev: "開発",
|
|
503
|
+
design: "デザイン",
|
|
504
|
+
materials: "共有資料",
|
|
505
|
+
issues: "課題管理",
|
|
506
|
+
};
|
|
507
|
+
/** 通してよい欄。**ここに無い欄は1つも通さない**(Lambda 側の列挙と同じ形) */
|
|
508
|
+
const REGISTERED_FIELDS = {
|
|
509
|
+
chat: ["ref", "name", "workspace"],
|
|
510
|
+
dev: ["ref", "type"],
|
|
511
|
+
design: ["key", "name"],
|
|
512
|
+
materials: ["id", "name"],
|
|
513
|
+
issues: ["domain", "projectKey"],
|
|
514
|
+
};
|
|
515
|
+
/** 欠けていたら行として読めない欄(「cm1.backlog.jp / 」のような半端な行を出さない) */
|
|
516
|
+
const REGISTERED_REQUIRED = {
|
|
517
|
+
chat: ["ref"],
|
|
518
|
+
dev: ["ref"],
|
|
519
|
+
design: ["key"],
|
|
520
|
+
materials: ["id"],
|
|
521
|
+
issues: ["domain", "projectKey"],
|
|
522
|
+
};
|
|
523
|
+
/** 開発の種別に許す値。**知らない値は落とす**(画面のラベル・振り分けが決まらない) */
|
|
524
|
+
const REGISTERED_DEV_TYPES = ["github-issues", "github-discussions"];
|
|
525
|
+
/**
|
|
526
|
+
* 応答の registeredNow を、画面が扱ってよい形へ畳む。**通らなかったものは黙って落とす。**
|
|
527
|
+
*
|
|
528
|
+
* 返すのは能力ごとの配列だけ(`asOf` は落とす——「◯◯時点」の出し分けは段階3で1箇所にする)。
|
|
529
|
+
* 1能力も読めなければ null。
|
|
530
|
+
*/
|
|
531
|
+
function sanitizeRegisteredNow(v) {
|
|
532
|
+
if (!v || typeof v !== "object" || Array.isArray(v))
|
|
533
|
+
return null;
|
|
534
|
+
const out = {};
|
|
535
|
+
let any = false;
|
|
536
|
+
for (const cap in REGISTERED_FIELDS) {
|
|
537
|
+
if (!Object.prototype.hasOwnProperty.call(REGISTERED_FIELDS, cap))
|
|
538
|
+
continue;
|
|
539
|
+
const rows = v[cap];
|
|
540
|
+
if (!Array.isArray(rows))
|
|
541
|
+
continue; // 配列でなければキーごと落とす(0件と混ぜない)
|
|
542
|
+
const keep = [];
|
|
543
|
+
for (const r of rows) {
|
|
544
|
+
if (r == null || typeof r !== "object" || Array.isArray(r))
|
|
545
|
+
continue;
|
|
546
|
+
const item = {};
|
|
547
|
+
for (const f of REGISTERED_FIELDS[cap]) {
|
|
548
|
+
const fv = r[f];
|
|
549
|
+
if (typeof fv !== "string" || fv === "")
|
|
550
|
+
continue; // 葉は文字列だけ
|
|
551
|
+
item[f] = fv;
|
|
552
|
+
}
|
|
553
|
+
if (cap === "dev" && item.type && REGISTERED_DEV_TYPES.indexOf(item.type) < 0)
|
|
554
|
+
delete item.type;
|
|
555
|
+
let full = true;
|
|
556
|
+
for (const f of REGISTERED_REQUIRED[cap])
|
|
557
|
+
if (!item[f])
|
|
558
|
+
full = false;
|
|
559
|
+
if (full)
|
|
560
|
+
keep.push(item);
|
|
561
|
+
}
|
|
562
|
+
out[cap] = keep;
|
|
563
|
+
any = true;
|
|
564
|
+
}
|
|
565
|
+
return any ? out : null;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* チャットの行で、まとめ表がまだ知らないもの。
|
|
569
|
+
*
|
|
570
|
+
* 🔴 **鍵はチャンネルID+ワークスペース。** チャンネルIDはワークスペースの中でしか
|
|
571
|
+
* 一意でないので、IDだけで突き合わせると別ワークスペースの同IDを同じものと見て
|
|
572
|
+
* 行が出ない(#104 で同じ理由の事故がある)。
|
|
573
|
+
*/
|
|
574
|
+
function chatRowsToAdd(externals, rows) {
|
|
575
|
+
const have = {};
|
|
576
|
+
for (const s of externals || []) {
|
|
577
|
+
if (!s || s.type !== "slack" || !s.ref)
|
|
578
|
+
continue;
|
|
579
|
+
have[String(s.workspace || "") + "\t" + String(s.ref)] = true;
|
|
580
|
+
}
|
|
581
|
+
const out = [];
|
|
582
|
+
for (const r of rows || []) {
|
|
583
|
+
if (!r || !r.ref)
|
|
584
|
+
continue;
|
|
585
|
+
if (have[String(r.workspace || "") + "\t" + String(r.ref)])
|
|
586
|
+
continue;
|
|
587
|
+
const row = { type: "slack", name: r.name || r.ref, ref: r.ref };
|
|
588
|
+
if (r.workspace)
|
|
589
|
+
row.workspace = r.workspace;
|
|
590
|
+
out.push(row);
|
|
591
|
+
}
|
|
592
|
+
return out;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* 開発の行で、まとめ表がまだ知らないもの。**鍵は `owner/repo` の生文字列。**
|
|
596
|
+
*
|
|
597
|
+
* **種別(issues / discussions)は登録ファイルの宣言をそのまま使う。** 無いときだけ
|
|
598
|
+
* Issues に倒す——ここで決め打ちすると、Discussions だけを登録した案件のラベルが嘘になる。
|
|
599
|
+
*/
|
|
600
|
+
function devRowsToAdd(externals, rows) {
|
|
601
|
+
const have = {};
|
|
602
|
+
for (const s of externals || []) {
|
|
603
|
+
if (!s || String(s.type || "").indexOf("github") !== 0 || !s.ref)
|
|
604
|
+
continue;
|
|
605
|
+
have[String(s.ref)] = true;
|
|
606
|
+
}
|
|
607
|
+
const out = [];
|
|
608
|
+
for (const r of rows || []) {
|
|
609
|
+
if (!r || !r.ref)
|
|
610
|
+
continue;
|
|
611
|
+
if (have[String(r.ref)])
|
|
612
|
+
continue;
|
|
613
|
+
out.push({ type: r.type || "github-issues", name: r.ref, ref: r.ref });
|
|
614
|
+
}
|
|
615
|
+
return out;
|
|
616
|
+
}
|
|
617
|
+
/** デザインの行で、まとめ表がまだ知らないもの(鍵はファイルキー) */
|
|
618
|
+
function designRowsToAdd(files, rows) {
|
|
619
|
+
const have = {};
|
|
620
|
+
for (const f of files || [])
|
|
621
|
+
if (f && f.key)
|
|
622
|
+
have[String(f.key)] = true;
|
|
623
|
+
const out = [];
|
|
624
|
+
for (const r of rows || []) {
|
|
625
|
+
if (!r || !r.key || have[String(r.key)])
|
|
626
|
+
continue;
|
|
627
|
+
// 名前は人が入力した別名。無ければキーを出す(既存の行と同じ扱い)
|
|
628
|
+
out.push({ key: r.key, name: r.name || r.key });
|
|
629
|
+
}
|
|
630
|
+
return out;
|
|
631
|
+
}
|
|
632
|
+
/** 共有資料の行で、まとめ表がまだ知らないもの(鍵はフォルダID) */
|
|
633
|
+
function materialRowsToAdd(ids, rows) {
|
|
634
|
+
const have = {};
|
|
635
|
+
for (const id of ids || [])
|
|
636
|
+
if (typeof id === "string" && id)
|
|
637
|
+
have[id] = true;
|
|
638
|
+
const out = [];
|
|
639
|
+
for (const r of rows || []) {
|
|
640
|
+
if (!r || !r.id || have[String(r.id)])
|
|
641
|
+
continue;
|
|
642
|
+
const row = { id: r.id };
|
|
643
|
+
if (r.name)
|
|
644
|
+
row.name = r.name;
|
|
645
|
+
out.push(row);
|
|
646
|
+
}
|
|
647
|
+
return out;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* 課題管理の行で、まとめ表がまだ知らないもの。
|
|
651
|
+
*
|
|
652
|
+
* **畳み方は `targetsGenerationOf`(サーバの `targetsGeneration`)と同じ**
|
|
653
|
+
* ——ドメインは小文字・キーは大文字。ここだけ別の畳み方にすると、同じ接続先が
|
|
654
|
+
* 二重に並ぶ(片方は削除ボタンを持たない)。
|
|
655
|
+
*/
|
|
656
|
+
function issueRowsToAdd(targets, rows) {
|
|
657
|
+
const key = (d, k) => String(d).toLowerCase() + "/" + String(k).toUpperCase();
|
|
658
|
+
const have = {};
|
|
659
|
+
for (const t of targets || [])
|
|
660
|
+
if (t && t.domain && t.projectKey)
|
|
661
|
+
have[key(t.domain, t.projectKey)] = true;
|
|
662
|
+
const out = [];
|
|
663
|
+
for (const r of rows || []) {
|
|
664
|
+
if (!r || !r.domain || !r.projectKey)
|
|
665
|
+
continue;
|
|
666
|
+
if (have[key(r.domain, r.projectKey)])
|
|
667
|
+
continue;
|
|
668
|
+
out.push({ domain: r.domain, projectKey: r.projectKey });
|
|
669
|
+
}
|
|
670
|
+
return out;
|
|
671
|
+
}
|
|
672
|
+
/** 同期ソース(internalSources)から、その能力の1件を取る */
|
|
673
|
+
function internalOf(list, kind) {
|
|
674
|
+
for (const s of list || [])
|
|
675
|
+
if (s && s.kind === kind)
|
|
676
|
+
return s;
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* まとめ表に足りない行を数える。**「未確定の列がある間は取り直す」の判断材料。**
|
|
681
|
+
*
|
|
682
|
+
* 数えるのは「登録ファイルには有るのに、まとめ表がまだ答えていない行」だけ。
|
|
683
|
+
* まとめ表が既に持っている行は、状態(gate)が無くても数えない——**取り直しても
|
|
684
|
+
* 変わらない**(夜間の実測が入るまで誰も測らない)ので、数えると15分間ずっと
|
|
685
|
+
* GitHub を叩き続けることになる。
|
|
686
|
+
*/
|
|
687
|
+
function unsettledRegisteredCount(view, reg) {
|
|
688
|
+
if (!reg)
|
|
689
|
+
return 0;
|
|
690
|
+
const rows = registeredRowsToAdd(view, reg);
|
|
691
|
+
return rows.externalSources.length + rows.design.length + rows.materials.length + rows.issues.length;
|
|
692
|
+
}
|
|
693
|
+
/** 足すべき行を能力ごとに求める(マージと件数の唯一の出どころ) */
|
|
694
|
+
function registeredRowsToAdd(view, reg) {
|
|
695
|
+
const v = view || {};
|
|
696
|
+
const r = reg || {};
|
|
697
|
+
const internals = v.internalSources || [];
|
|
698
|
+
const design = internalOf(internals, REGISTERED_CAP_NAMES.design);
|
|
699
|
+
const materials = internalOf(internals, REGISTERED_CAP_NAMES.materials);
|
|
700
|
+
const bt = v.backlogTarget;
|
|
701
|
+
const targets = bt && Array.isArray(bt.targets) ? bt.targets : bt ? [bt] : [];
|
|
702
|
+
return {
|
|
703
|
+
externalSources: chatRowsToAdd(v.externalSources, r.chat).concat(devRowsToAdd(v.externalSources, r.dev)),
|
|
704
|
+
design: designRowsToAdd(design && design.figmaFiles, r.design),
|
|
705
|
+
materials: materialRowsToAdd(materials && materials.driveFolderIds, r.materials),
|
|
706
|
+
issues: issueRowsToAdd(targets, r.issues),
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* まとめ表の3キー(externalSources / internalSources / backlogTarget)に、足りない行を足す。
|
|
711
|
+
*
|
|
712
|
+
* **足すだけ。** 既存の行・欄は1つも書き換えない。
|
|
713
|
+
*
|
|
714
|
+
* ⚠ 同期ソースの行がまだ無い能力(=夜間バッチが一度も走っていない案件)には、
|
|
715
|
+
* **その能力の行を作る**——作らないと、登録した Figma ファイル・Drive フォルダを
|
|
716
|
+
* 置く場所が画面に無い。作る行は `kind` と一覧だけで、最終同期・件数・ラベルは装わない。
|
|
717
|
+
* 共有資料だけは `driveState` が無いと一覧そのものが描かれないので、
|
|
718
|
+
* **登録ファイルを実際に読めたという根拠がある場合に限り** `on` を置く
|
|
719
|
+
* (まとめ表が持っている値は上書きしない)。
|
|
720
|
+
*/
|
|
721
|
+
function mergeRegisteredFleet(view, reg) {
|
|
722
|
+
const v = view || {};
|
|
723
|
+
const out = {
|
|
724
|
+
externalSources: v.externalSources,
|
|
725
|
+
internalSources: v.internalSources,
|
|
726
|
+
backlogTarget: v.backlogTarget,
|
|
727
|
+
};
|
|
728
|
+
if (!reg)
|
|
729
|
+
return out;
|
|
730
|
+
const add = registeredRowsToAdd(v, reg);
|
|
731
|
+
if (add.externalSources.length)
|
|
732
|
+
out.externalSources = (v.externalSources || []).concat(add.externalSources);
|
|
733
|
+
if (add.design.length || add.materials.length) {
|
|
734
|
+
const internals = (v.internalSources || []).slice();
|
|
735
|
+
const put = (kind, patch) => {
|
|
736
|
+
for (let i = 0; i < internals.length; i++) {
|
|
737
|
+
if (internals[i] && internals[i].kind === kind) {
|
|
738
|
+
internals[i] = Object.assign({}, internals[i], patch);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
internals.push(Object.assign({ kind: kind }, patch));
|
|
743
|
+
};
|
|
744
|
+
if (add.design.length) {
|
|
745
|
+
const cur = internalOf(internals, REGISTERED_CAP_NAMES.design);
|
|
746
|
+
put(REGISTERED_CAP_NAMES.design, { figmaFiles: ((cur && cur.figmaFiles) || []).concat(add.design) });
|
|
747
|
+
}
|
|
748
|
+
if (add.materials.length) {
|
|
749
|
+
const cur = internalOf(internals, REGISTERED_CAP_NAMES.materials);
|
|
750
|
+
const ids = ((cur && cur.driveFolderIds) || []).concat(add.materials.map((x) => x.id));
|
|
751
|
+
const names = Object.assign({}, (cur && cur.driveFolderNames) || {});
|
|
752
|
+
for (const x of add.materials)
|
|
753
|
+
if (x.name)
|
|
754
|
+
names[x.id] = x.name;
|
|
755
|
+
const patch = { driveFolderIds: ids, driveFolderNames: names };
|
|
756
|
+
// まとめ表が状態を持っていないときだけ置く(持っている値には触らない)
|
|
757
|
+
if (!cur || !cur.driveState)
|
|
758
|
+
patch.driveState = "on";
|
|
759
|
+
put(REGISTERED_CAP_NAMES.materials, patch);
|
|
760
|
+
}
|
|
761
|
+
out.internalSources = internals;
|
|
762
|
+
}
|
|
763
|
+
if (add.issues.length) {
|
|
764
|
+
const bt = v.backlogTarget;
|
|
765
|
+
const cur = bt && Array.isArray(bt.targets) ? bt.targets : bt && bt.domain && bt.projectKey ? [bt] : [];
|
|
766
|
+
const targets = cur.concat(add.issues);
|
|
767
|
+
// 単数の2つは**先頭の接続先と同じ値**(この配列を持たない読み手のために残っている欄)
|
|
768
|
+
out.backlogTarget = Object.assign({}, bt || {}, {
|
|
769
|
+
domain: targets[0].domain,
|
|
770
|
+
projectKey: targets[0].projectKey,
|
|
771
|
+
targets: targets,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
return out;
|
|
775
|
+
}
|
|
776
|
+
// ---------------------------------------------------------------------------
|
|
777
|
+
// 未確定の列がある間の取り直し(段階1の前提)
|
|
778
|
+
//
|
|
779
|
+
// 🔴 **これが無いと、登録したあと画面が二度と更新されない。** 60秒ごとの取り直しは
|
|
780
|
+
// これまで「反映中」の記録がある間だけ張られていたが、registeredNow が入ると
|
|
781
|
+
// 追加した瞬間に「もう出ている」と判定されて記録が消える。
|
|
782
|
+
//
|
|
783
|
+
// **上限とタブ非表示の扱いは、反映待ち(livePending / nextCheckDelay)と同じ考え方にする。**
|
|
784
|
+
// - 上限は**経過時間**で見る(回数カウンタで数えない)。バックグラウンドタブの
|
|
785
|
+
// スロットリングやスリープ復帰は、経過時間で見る限り自己修復する
|
|
786
|
+
// - **可視性のイベントは足さない。** 遅れて起きたタイマがその回で期限切れを落とすので、
|
|
787
|
+
// 非表示のあいだは自然に間隔が伸びるだけ。イベントを足すと、復帰の瞬間に
|
|
788
|
+
// 全タブが同時に叩く形を新しく作ることになる
|
|
789
|
+
// ---------------------------------------------------------------------------
|
|
790
|
+
/** 未確定の行があっても、取り直しを続ける上限(開始からの経過)。反映待ちと同じ15分 */
|
|
791
|
+
const UNSETTLED_TTL_MS = 15 * 60 * 1000;
|
|
792
|
+
/** 取り直しの間隔。60秒(op=state のキャッシュと同じ粒度) */
|
|
793
|
+
const UNSETTLED_POLL_MS = 60 * 1000;
|
|
794
|
+
/**
|
|
795
|
+
* 期限と間隔を**テストから固定するための入れ物**(REFLECTING_LIMITS と同じ作法)。
|
|
796
|
+
*
|
|
797
|
+
* ⚠ **定数そのものを export してはいけない**(CommonJS へ落ちるときに `exports.X` へ
|
|
798
|
+
* 書き換えられ、toString() で流し込んだ先には exports が無い)。
|
|
799
|
+
*/
|
|
800
|
+
exports.UNSETTLED_LIMITS = { ttlMs: UNSETTLED_TTL_MS, pollMs: UNSETTLED_POLL_MS };
|
|
801
|
+
/**
|
|
802
|
+
* 次に取り直すまでの ms。もう取り直す必要が無ければ null。
|
|
803
|
+
*
|
|
804
|
+
* @param count 未確定の行数(0 なら張らない)
|
|
805
|
+
* @param since 取り直しを始めた時刻(Date.now())
|
|
806
|
+
*
|
|
807
|
+
* 🔴 **経過は「上」だけでなく「下」も見る**(livePending と同じ理由)。端末の時計が
|
|
808
|
+
* 進んだ状態で始めて NTP で戻ると、`since` が未来になり**永久に期限が切れない**。
|
|
809
|
+
*/
|
|
810
|
+
function nextStateDelay(count, since, now) {
|
|
811
|
+
if (!(count > 0))
|
|
812
|
+
return null;
|
|
813
|
+
const age = now - since;
|
|
814
|
+
if (!Number.isFinite(age) || age < 0)
|
|
815
|
+
return null;
|
|
816
|
+
const left = UNSETTLED_TTL_MS - age;
|
|
817
|
+
if (left <= 0)
|
|
818
|
+
return null;
|
|
819
|
+
return left < UNSETTLED_POLL_MS ? left : UNSETTLED_POLL_MS;
|
|
820
|
+
}
|
|
313
821
|
function capabilityState(cap, ctx) {
|
|
314
822
|
// **能力→パイプラインの対応はここに持つ。** 呼び出し側に渡させると、
|
|
315
823
|
// クライアント側(この関数を .toString() で受け取る側)が同じ表を書き写すことになり、
|
|
@@ -1589,6 +2097,34 @@ ${capabilityDisplayState.toString()}
|
|
|
1589
2097
|
${backlogTargetOf.toString()}
|
|
1590
2098
|
${sourceNeedsAttention.toString()}
|
|
1591
2099
|
${capabilityState.toString()}
|
|
2100
|
+
// 反映待ち(⏳ 反映中)の判定。**期限・間隔・対応表もビルド時の定義から流し込む**
|
|
2101
|
+
// ——画面側に書き写すと、片方だけ動かしたときに死角(取り直しは止まったのに
|
|
2102
|
+
// 反映中が残る時間)ができ、しかも画面は正常に見えるので気づけない
|
|
2103
|
+
var REFLECT_TTL_MS = ${JSON.stringify(REFLECT_TTL_MS)};
|
|
2104
|
+
var REFLECT_POLL_MS = ${JSON.stringify(REFLECT_POLL_MS)};
|
|
2105
|
+
var REFLECT_OPS = ${JSON.stringify(exports.REFLECT_OPS)};
|
|
2106
|
+
${livePending.toString()}
|
|
2107
|
+
${nextCheckDelay.toString()}
|
|
2108
|
+
${settledPending.toString()}
|
|
2109
|
+
// 登録の即時値(registeredNow)の受け入れとマージ。**判定の実体はビルド時の定義**
|
|
2110
|
+
// ——画面側に書き写すと、許可リストが2枚になって列挙し忘れが静かに消える
|
|
2111
|
+
var REGISTERED_CAP_NAMES = ${JSON.stringify(REGISTERED_CAP_NAMES)};
|
|
2112
|
+
var REGISTERED_FIELDS = ${JSON.stringify(REGISTERED_FIELDS)};
|
|
2113
|
+
var REGISTERED_REQUIRED = ${JSON.stringify(REGISTERED_REQUIRED)};
|
|
2114
|
+
var REGISTERED_DEV_TYPES = ${JSON.stringify(REGISTERED_DEV_TYPES)};
|
|
2115
|
+
var UNSETTLED_TTL_MS = ${JSON.stringify(UNSETTLED_TTL_MS)};
|
|
2116
|
+
var UNSETTLED_POLL_MS = ${JSON.stringify(UNSETTLED_POLL_MS)};
|
|
2117
|
+
${sanitizeRegisteredNow.toString()}
|
|
2118
|
+
${chatRowsToAdd.toString()}
|
|
2119
|
+
${devRowsToAdd.toString()}
|
|
2120
|
+
${designRowsToAdd.toString()}
|
|
2121
|
+
${materialRowsToAdd.toString()}
|
|
2122
|
+
${issueRowsToAdd.toString()}
|
|
2123
|
+
${internalOf.toString()}
|
|
2124
|
+
${registeredRowsToAdd.toString()}
|
|
2125
|
+
${unsettledRegisteredCount.toString()}
|
|
2126
|
+
${mergeRegisteredFleet.toString()}
|
|
2127
|
+
${nextStateDelay.toString()}
|
|
1592
2128
|
// 「はじめに」セクションのチュートリアルデータ(ビルド時に埋め込み。ボタンURL・チャレンジ4の決定は解決済み)
|
|
1593
2129
|
var TUTORIAL = (function () {
|
|
1594
2130
|
var elx = document.getElementById("tutorial-data");
|
|
@@ -2145,6 +2681,19 @@ ${capabilityState.toString()}
|
|
|
2145
2681
|
"no_token": { text: "トークン未設定", cls: "ds-gate-muted" },
|
|
2146
2682
|
"unknown": { text: "不明", cls: "ds-gate-muted" },
|
|
2147
2683
|
};
|
|
2684
|
+
/* ---------------------------------------------------------------------------
|
|
2685
|
+
* まだ誰も測っていない行(段階1)
|
|
2686
|
+
*
|
|
2687
|
+
* いま登録したばかりで、夜間の実測にも「接続を確認」にも掛かっていない状態。
|
|
2688
|
+
* **「未接続」とも「不明」とも書かない**——前者は測って繋がっていなかった、
|
|
2689
|
+
* 後者は測って分からなかった、という別の事実で、どちらも読み手に
|
|
2690
|
+
* 「直す必要がある」と読ませる(登録そのものは成功している)。
|
|
2691
|
+
*
|
|
2692
|
+
* 🔴 **DS_GATE には入れない。** あちらは**実測が返す語彙**で、判定側(slack-gate.mjs)と
|
|
2693
|
+
* 1対1で一致していることをテストが見張っている(足し忘れた判定が「不明」に落ちる事故の番人)。
|
|
2694
|
+
* 未測定は実測の結果ではないので、同じ表に混ぜるとその番人が意味を失う。
|
|
2695
|
+
* ------------------------------------------------------------------------- */
|
|
2696
|
+
var DS_GATE_PENDING = { text: "登録状況を確認中です", cls: "ds-gate-muted" };
|
|
2148
2697
|
/* ---------------------------------------------------------------------------
|
|
2149
2698
|
* no_token には**経路が2つある**(2026-08-18 の不具合報告)
|
|
2150
2699
|
*
|
|
@@ -2249,6 +2798,10 @@ ${capabilityState.toString()}
|
|
|
2249
2798
|
// github-issues / github-discussions のどちらも同じ原因(組織単位の App)
|
|
2250
2799
|
if (String(s.type || "").indexOf("github") === 0) return DS_GATE_NO_TOKEN.github;
|
|
2251
2800
|
}
|
|
2801
|
+
// 🔴 **状態が無い行を「不明」に落とさない(段階1)。** 「不明」は実測して分からなかった
|
|
2802
|
+
// という意味で、まだ測っていない行に出すと嘘になる。**知らない値は従来どおり「不明」**
|
|
2803
|
+
// ——そちらは engine が新しい状態を足したのに画面が知らない、という別の事実
|
|
2804
|
+
if (s.gate == null || s.gate === "") return DS_GATE_PENDING;
|
|
2252
2805
|
return DS_GATE[s.gate] || DS_GATE["unknown"];
|
|
2253
2806
|
}
|
|
2254
2807
|
/**
|
|
@@ -2960,10 +3513,23 @@ ${capabilityState.toString()}
|
|
|
2960
3513
|
*/
|
|
2961
3514
|
var LIVE_REALTIME = null;
|
|
2962
3515
|
/**
|
|
2963
|
-
*
|
|
3516
|
+
* いま登録ファイルに何が書いてあるか(op=state の registeredNow・段階1)。取得前は null。
|
|
3517
|
+
*
|
|
3518
|
+
* **まとめ表(fleet)とは別に持つ。** fleet は夜間バッチの成果物の射影で、こちらは
|
|
3519
|
+
* いまのリポジトリを読んだ結果。同じ入れ物に混ぜると、上に出す「◯◯時点」がどちらを
|
|
3520
|
+
* 指すのか説明できなくなる(realtime を別に持っているのと同じ理由)。
|
|
3521
|
+
*/
|
|
3522
|
+
var LIVE_REGISTERED = null;
|
|
3523
|
+
/** マージ済みの3キー(作り直しは setLive* だけが行う) */
|
|
3524
|
+
var MERGED = null;
|
|
3525
|
+
/** マージの対象になるキー。これ以外は素の値をそのまま返す */
|
|
3526
|
+
var MERGE_KEYS = { externalSources: 1, internalSources: 1, backlogTarget: 1 };
|
|
3527
|
+
|
|
3528
|
+
/**
|
|
3529
|
+
* 接続状況の素の値(まとめ表だけ)。ライブがあればライブ、無ければ静的ビルドの値。
|
|
2964
3530
|
* SITE 側はキー名が fleetGeneratedAt だけ異なる(他は fleet-status.json と同名)。
|
|
2965
3531
|
*/
|
|
2966
|
-
function
|
|
3532
|
+
function rawFleetVal(key) {
|
|
2967
3533
|
// **ライブが取れたら、そちらだけを見る(キー単位で静的へ落ちない)。**
|
|
2968
3534
|
// キーごとにフォールバックすると、たとえば externalSources はライブ・secretSources は
|
|
2969
3535
|
// 前日のビルド、という**2つの時点が混ざった画面**になる。上に出す「◯◯時点」は1つしか
|
|
@@ -2974,6 +3540,378 @@ ${capabilityState.toString()}
|
|
|
2974
3540
|
return SITE[key === "generatedAt" ? "fleetGeneratedAt" : key];
|
|
2975
3541
|
}
|
|
2976
3542
|
|
|
3543
|
+
/**
|
|
3544
|
+
* 画面が読む値。**まとめ表がまだ知らない行を足したもの**を返す(段階1)。
|
|
3545
|
+
*
|
|
3546
|
+
* 🔴 **ここ1箇所で足す。** 一覧・能力タイル・接続マップの注記は全部この関数を通るので、
|
|
3547
|
+
* 足す場所を描画側に散らすと「タイルは1件・一覧は2件」のような食い違いが必ず出る。
|
|
3548
|
+
*/
|
|
3549
|
+
function fleetVal(key) {
|
|
3550
|
+
if (LIVE_REGISTERED && MERGE_KEYS[key]) {
|
|
3551
|
+
if (!MERGED) MERGED = mergeRegisteredFleet(rawView(), LIVE_REGISTERED);
|
|
3552
|
+
return MERGED[key];
|
|
3553
|
+
}
|
|
3554
|
+
return rawFleetVal(key);
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
/** まとめ表がまだ答えていない行の数(取り直しを続けるかの判断) */
|
|
3558
|
+
function unsettledNow() {
|
|
3559
|
+
return unsettledRegisteredCount(rawView(), LIVE_REGISTERED);
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
/** まとめ表(夜間の成果物)だけの3キー。**状態の判定には必ずこちらを渡す** */
|
|
3563
|
+
function rawView() {
|
|
3564
|
+
return {
|
|
3565
|
+
externalSources: rawFleetVal("externalSources"),
|
|
3566
|
+
internalSources: rawFleetVal("internalSources"),
|
|
3567
|
+
backlogTarget: rawFleetVal("backlogTarget"),
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3571
|
+
/**
|
|
3572
|
+
* まとめ表がまだ知らない行を持っている能力(段階1)。
|
|
3573
|
+
*
|
|
3574
|
+
* 🔴 **その能力の状態を「接続OK」「未接続」「未使用」のどれとも言わないため。**
|
|
3575
|
+
* 足した行を状態の判定に混ぜると「1行あるのだから繋がっている」と読んで**接続OK**に落ち、
|
|
3576
|
+
* 同じ画面で**タイルは接続OK・行は「登録状況を確認中です」**が並ぶ。かといって夜間の値を
|
|
3577
|
+
* そのまま出すと、**行はもう出ているのに「接続先が設定されていません」**と言う。
|
|
3578
|
+
* どちらも嘘なので、**行と同じ語**(登録状況を確認中です)に倒す。
|
|
3579
|
+
*/
|
|
3580
|
+
function pendingCaps() {
|
|
3581
|
+
if (!LIVE_REGISTERED) return {};
|
|
3582
|
+
var add = registeredRowsToAdd(rawView(), LIVE_REGISTERED);
|
|
3583
|
+
var out = {};
|
|
3584
|
+
for (var i = 0; i < add.externalSources.length; i++) out[externalCapability(add.externalSources[i].type)] = 1;
|
|
3585
|
+
if (add.design.length) out[REGISTERED_CAP_NAMES.design] = 1;
|
|
3586
|
+
if (add.materials.length) out[REGISTERED_CAP_NAMES.materials] = 1;
|
|
3587
|
+
if (add.issues.length) out[REGISTERED_CAP_NAMES.issues] = 1;
|
|
3588
|
+
return out;
|
|
3589
|
+
}
|
|
3590
|
+
/** その能力に、まとめ表がまだ知らない行があるか */
|
|
3591
|
+
function capPending(cap) { return !!pendingCaps()[cap]; }
|
|
3592
|
+
|
|
3593
|
+
/**
|
|
3594
|
+
* 能力の状態を決めるときに渡す値。**足した行は混ぜない**(誰も測っていないので状態を持たない)。
|
|
3595
|
+
*
|
|
3596
|
+
* ⚠ **これ単体を壊してもテストは赤くならない。** 画面に出る語は capPending が先に
|
|
3597
|
+
* 「登録状況を確認中です」へ倒すので、条件(足した行がある)が同じこの2つは常に一緒に効く。
|
|
3598
|
+
* それでも素の値を渡すのは、**判定関数に嘘の材料を渡さない**ためで、表示側の都合が
|
|
3599
|
+
* 変わったとき(例えば pending の語をやめたとき)に「接続OK」が復活しないようにする。
|
|
3600
|
+
*/
|
|
3601
|
+
function stateCtx() {
|
|
3602
|
+
return {
|
|
3603
|
+
capabilities: rawFleetVal("capabilities") || {},
|
|
3604
|
+
internals: rawFleetVal("internalSources") || [],
|
|
3605
|
+
externals: rawFleetVal("externalSources") || [],
|
|
3606
|
+
pipes: rawFleetVal("pipelines") || [],
|
|
3607
|
+
};
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
// ---------- 反映待ち(「⏳ 反映中」)----------
|
|
3611
|
+
//
|
|
3612
|
+
// ## 何のためか
|
|
3613
|
+
//
|
|
3614
|
+
// 設定を保存してから画面に出るまで、自己チェック(fleet-status.json の作り直し)に
|
|
3615
|
+
// 数分かかる。その間、画面は「未設定」「未接続」「未使用」と出す
|
|
3616
|
+
// ——**保存はできているのに、事実に反する。** 利用者は「保存できていない」と読んで
|
|
3617
|
+
// 同じ操作を繰り返し、「既に登録されています」と言われる(この repo が何度も踏んだ形)。
|
|
3618
|
+
//
|
|
3619
|
+
// **嘘をやめる。** 反映待ちのあいだは「未設定」と言わず「反映中」と言う。
|
|
3620
|
+
//
|
|
3621
|
+
// ## 出すのは状態だけ。行は先出ししない
|
|
3622
|
+
//
|
|
3623
|
+
// 行そのものを先に出すと、保存に失敗していても入っているように見えて気づけないし、
|
|
3624
|
+
// 反映が来ないまま消えない行は削除もできない幽霊になる(撤去した pendingGold の
|
|
3625
|
+
// 「(画面反映待ち)」バッジと同じ罠)。**必ず期限切れする**ので、反映が来なければ
|
|
3626
|
+
// 元の「未設定」に戻る=嘘が残らない。
|
|
3627
|
+
//
|
|
3628
|
+
// ## 期限は Date.now() - at で見る
|
|
3629
|
+
//
|
|
3630
|
+
// 🔴 **回数カウンタで数えない。** バックグラウンドタブのスロットリングやスリープ復帰は、
|
|
3631
|
+
// 経過時間で見る限り自己修復する(遅れて起きたタイマがその回で期限切れを落とす)。
|
|
3632
|
+
|
|
3633
|
+
/** 取り直しのタイマ(setTimeout の連鎖。setInterval は使わない) */
|
|
3634
|
+
var reflectTimer = null;
|
|
3635
|
+
|
|
3636
|
+
/**
|
|
3637
|
+
* 保存先のキー。**案件ごとに分ける**(tutStoreKey と同じ作法)。
|
|
3638
|
+
*
|
|
3639
|
+
* intake が無い案件では案件キーが空文字になるが、それでも他案件のキーとは別物なので、
|
|
3640
|
+
* 別の案件の反映待ちを読むことはない。
|
|
3641
|
+
*
|
|
3642
|
+
* **cortex.pendingGold: と紛らわしい名前にしない。** 前方一致で掃除するコードが
|
|
3643
|
+
* 後から書かれると事故る(撤去済みの pendingGold と別語にしてある)。
|
|
3644
|
+
*/
|
|
3645
|
+
function reflectStoreKey() {
|
|
3646
|
+
return "cortex.reflecting:" + ((SITE.intake && SITE.intake.projectKey) || "");
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
/**
|
|
3650
|
+
* localStorage は使えない環境がある(プライベートモード等)。**必ず try/catch で囲む**
|
|
3651
|
+
*
|
|
3652
|
+
* 🔴 **読み込み側でも「既定は記録しない」を効かせる。** 書き込み側(reflectEntryOf)が
|
|
3653
|
+
* REFLECT_OPS に無い op を弾いても、localStorage の中身は**別バージョンの画面・
|
|
3654
|
+
* 手編集・壊れた値**でいくらでも汚れる。settledPending は「判定できない能力は
|
|
3655
|
+
* 落とさない(期限に任せる)」設計なので、**表に無い cap の記録は期限(15分)まで
|
|
3656
|
+
* そのまま「反映中」として画面に出る**——本当の状態を隠す。
|
|
3657
|
+
* op / cap / dir を対応表と突き合わせ、1つでも食い違えば落とす。
|
|
3658
|
+
*/
|
|
3659
|
+
function readReflecting() {
|
|
3660
|
+
try {
|
|
3661
|
+
var raw = localStorage.getItem(reflectStoreKey());
|
|
3662
|
+
var v = raw ? JSON.parse(raw) : null;
|
|
3663
|
+
if (!Array.isArray(v)) return [];
|
|
3664
|
+
return v.filter(function (e) {
|
|
3665
|
+
if (!e || typeof e.at !== "number" || !Number.isFinite(e.at)) return false;
|
|
3666
|
+
if (!e.cap || !e.projectKey) return false;
|
|
3667
|
+
var d = REFLECT_OPS[String(e.op)];
|
|
3668
|
+
// 対応表に無い op(記録してはいけないもの・未知のもの)は落とす
|
|
3669
|
+
if (!d) return false;
|
|
3670
|
+
// 表と食い違う cap / dir も落とす(表の cap 以外は settle できない)
|
|
3671
|
+
return d.cap === e.cap && d.dir === e.dir;
|
|
3672
|
+
});
|
|
3673
|
+
} catch (e) { return []; }
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3676
|
+
function writeReflecting(list) {
|
|
3677
|
+
try {
|
|
3678
|
+
if (!list || !list.length) localStorage.removeItem(reflectStoreKey());
|
|
3679
|
+
else localStorage.setItem(reflectStoreKey(), JSON.stringify(list));
|
|
3680
|
+
} catch (e) {}
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
/** その能力の情報源(fleet のスナップショット由来) */
|
|
3684
|
+
function internalSourceOf(cap) {
|
|
3685
|
+
var list = fleetVal("internalSources") || [];
|
|
3686
|
+
for (var i = 0; i < list.length; i++) if (list[i] && list[i].kind === cap) return list[i];
|
|
3687
|
+
return null;
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
/**
|
|
3691
|
+
* 能力ごとの「**スナップショットが答えている集合**」。
|
|
3692
|
+
*
|
|
3693
|
+
* 🔴 **「画面に出ているか」で判定してはいけない。** 追加・削除の応答には一覧が
|
|
3694
|
+
* 添えられていて adoptTargets がその場で差し替えるので、**記録した瞬間に
|
|
3695
|
+
* 「もう出ている」と判定されて消え、機能が丸ごと無意味になる**(査読者が実測)。
|
|
3696
|
+
*
|
|
3697
|
+
* 引くのは backlogTargetsNow ——「未設定」を出すか決めているのと**同一の関数**。
|
|
3698
|
+
* 別々に書くと、片方が「未設定」と言い、もう片方が「反映済み」と言う状態を作れてしまう。
|
|
3699
|
+
*
|
|
3700
|
+
* 載っていない能力は判定できない(null)=落とさず期限に任せる。
|
|
3701
|
+
* ただし REFLECT_OPS がその能力の op を記録しないので、実際には溜まらない。
|
|
3702
|
+
*/
|
|
3703
|
+
function reflectPresent() {
|
|
3704
|
+
var out = {};
|
|
3705
|
+
out["課題管理"] = backlogTargetsNow(internalSourceOf("課題管理"));
|
|
3706
|
+
return out;
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
/** いま「反映中」と言うべきもの。**読むたびに期限切れ・反映済みを落として保存し直す** */
|
|
3710
|
+
function reflectingNow() {
|
|
3711
|
+
var before = readReflecting();
|
|
3712
|
+
if (!before.length) return before;
|
|
3713
|
+
var after = settledPending(livePending(before, Date.now()), reflectPresent());
|
|
3714
|
+
if (after.length !== before.length) writeReflecting(after);
|
|
3715
|
+
return after;
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
/** 送った操作から反映待ちの1件を組み立てる。**対応表に無い op は必ず null** */
|
|
3719
|
+
function reflectEntryOf(op, payload, refreshUrl) {
|
|
3720
|
+
var d = REFLECT_OPS[String(op)];
|
|
3721
|
+
if (!d) return null;
|
|
3722
|
+
var t = null;
|
|
3723
|
+
if (d.from === "url") t = backlogTargetOf(payload.url);
|
|
3724
|
+
else if (d.from === "backlogProjectKey") t = { domain: "", projectKey: String(payload.backlogProjectKey || "").toUpperCase() };
|
|
3725
|
+
// 対象を特定できないものは記録しない(settle できず、期限まで出っぱなしになる)
|
|
3726
|
+
if (!t || !t.projectKey) return null;
|
|
3727
|
+
return {
|
|
3728
|
+
cap: d.cap, op: String(op), dir: d.dir,
|
|
3729
|
+
domain: t.domain || "", projectKey: t.projectKey,
|
|
3730
|
+
at: Date.now(), url: refreshUrl || "",
|
|
3731
|
+
};
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
/**
|
|
3735
|
+
* 反映待ちとして記録し、その場で画面を「反映中」に差し替える。
|
|
3736
|
+
*
|
|
3737
|
+
* **同じ接続先の古い記録は捨てる**(追加のあと削除、のような入れ違いを残さない)。
|
|
3738
|
+
*/
|
|
3739
|
+
function rememberReflecting(payload, refreshUrl) {
|
|
3740
|
+
var e = reflectEntryOf(payload && payload.op, payload || {}, refreshUrl);
|
|
3741
|
+
if (!e) return;
|
|
3742
|
+
var list = readReflecting().filter(function (x) {
|
|
3743
|
+
return !(x.cap === e.cap && x.projectKey === e.projectKey && x.domain === e.domain);
|
|
3744
|
+
});
|
|
3745
|
+
list.push(e);
|
|
3746
|
+
writeReflecting(list);
|
|
3747
|
+
// **開いているモーダルの中も含めてその場で差し替える。** 削除で最後の1件を外すと
|
|
3748
|
+
// 応答で一覧が空になり「未設定」の1行が出る——記録しただけでは、その嘘が残る
|
|
3749
|
+
repaintReflect(document);
|
|
3750
|
+
scheduleReflectRefresh();
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
// 差し替える文言。**engine 側の文言は変えない**——engine の detail は
|
|
3754
|
+
// 「そのスナップショット時点では未設定だった」という事実で、嘘ではない。
|
|
3755
|
+
// 変えるのは画面の出し分けだけ。
|
|
3756
|
+
var REFLECT_TEXT = {
|
|
3757
|
+
empty: "⏳ 反映中 — いま変更した接続先を画面に反映しています(数分かかります)。",
|
|
3758
|
+
note: "⏳ 反映中(いま変更した内容を画面に反映しています)",
|
|
3759
|
+
sub: "⏳ 反映中",
|
|
3760
|
+
// 見出し下の説明は出さない(下の「反映中」が説明している)
|
|
3761
|
+
desc: "",
|
|
3762
|
+
};
|
|
3763
|
+
|
|
3764
|
+
/**
|
|
3765
|
+
* 反映が終わったときに、その箇所を**いまのスナップショットから引き直す**関数を
|
|
3766
|
+
* 走らせている最中か。
|
|
3767
|
+
*
|
|
3768
|
+
* 引き直しは一覧の描き直し(paint)まで含み、その中でまた applyReflect が呼ばれる。
|
|
3769
|
+
* 目印が無いと無限に呼び合うので、内側は下地(data-reflect-base)に落とす。
|
|
3770
|
+
*/
|
|
3771
|
+
var reflectRestoring = false;
|
|
3772
|
+
|
|
3773
|
+
/**
|
|
3774
|
+
* 「未設定」と言っている箇所に印を付ける。反映待ちのあいだだけ文言を差し替える。
|
|
3775
|
+
*
|
|
3776
|
+
* @param role empty=一覧の0件行 / note=能力カードの「未使用」 /
|
|
3777
|
+
* sub=タイルの2行目 / desc=モーダルの見出し下
|
|
3778
|
+
* @param fresh 🔴 **反映が届いたときに、届いたデータからこの箇所を引き直す関数。**
|
|
3779
|
+
* 引数はその節点。{ text, hidden } の形で返す(返さなければ下地へ落とす)。
|
|
3780
|
+
* **省くと「反映中 → 未設定」に戻る**——applyReflect のコメントを読むこと。
|
|
3781
|
+
*/
|
|
3782
|
+
function reflectMark(node, cap, role, fresh) {
|
|
3783
|
+
node.setAttribute("data-reflect-cap", cap);
|
|
3784
|
+
node.setAttribute("data-reflect-role", role);
|
|
3785
|
+
node.setAttribute("data-reflect-base", node.textContent || "");
|
|
3786
|
+
if (fresh) node.__reflectFresh = fresh;
|
|
3787
|
+
applyReflect(node, reflectingNow());
|
|
3788
|
+
return node;
|
|
3789
|
+
}
|
|
3790
|
+
|
|
3791
|
+
function applyReflect(node, live) {
|
|
3792
|
+
var cap = node.getAttribute("data-reflect-cap");
|
|
3793
|
+
var role = node.getAttribute("data-reflect-role");
|
|
3794
|
+
var hit = null;
|
|
3795
|
+
for (var i = 0; i < live.length; i++) if (live[i].cap === cap) { hit = live[i]; break; }
|
|
3796
|
+
if (!hit) {
|
|
3797
|
+
// ------------------------------------------------------------------
|
|
3798
|
+
// 反映が来た(あるいは期限が切れた)。
|
|
3799
|
+
//
|
|
3800
|
+
// 🔴 **下地(data-reflect-base)へ戻してはいけない。** 下地は
|
|
3801
|
+
// **モーダルを開いた時点=古いスナップショットで作った文字列**で、その中身は
|
|
3802
|
+
// 「未接続」「未設定」「未使用」——**この機能が直そうとしている誤読そのもの**。
|
|
3803
|
+
// しかも settle が起きるのは「**新しいスナップショットが届いた**」瞬間なので、
|
|
3804
|
+
// 戻した先は既に事実ではない。
|
|
3805
|
+
//
|
|
3806
|
+
// モーダルを開いたまま待つと refreshDataSources が下地を作り直さない
|
|
3807
|
+
// (開いているモーダルの中身が切り離されるため、あのガードは外せない)ので、
|
|
3808
|
+
// 「⏳ 反映中」→(数分)→「未設定」が**閉じるかリロードするまで残った**。
|
|
3809
|
+
//
|
|
3810
|
+
// **届いたデータで引き直す。** 引き直せない箇所だけ下地に落とす。
|
|
3811
|
+
// ------------------------------------------------------------------
|
|
3812
|
+
var f = null;
|
|
3813
|
+
if (node.__reflectFresh && !reflectRestoring) {
|
|
3814
|
+
reflectRestoring = true;
|
|
3815
|
+
try { f = node.__reflectFresh(node); } catch (e) { f = null; }
|
|
3816
|
+
reflectRestoring = false;
|
|
3817
|
+
}
|
|
3818
|
+
node.textContent = f && typeof f.text === "string" ? f.text : (node.getAttribute("data-reflect-base") || "");
|
|
3819
|
+
node.hidden = !!(f && f.hidden);
|
|
3820
|
+
return;
|
|
3821
|
+
}
|
|
3822
|
+
var t = REFLECT_TEXT[role] || "";
|
|
3823
|
+
node.textContent = t;
|
|
3824
|
+
// 「出さない」箇所は空にして hidden にする。**消さずに残す**のは、
|
|
3825
|
+
// 反映が終わったときに元の文へ戻す先が要るため
|
|
3826
|
+
node.hidden = t === "";
|
|
3827
|
+
if (role === "empty" && hit.url) {
|
|
3828
|
+
node.appendChild(document.createTextNode(" "));
|
|
3829
|
+
node.appendChild(el("a", { href: hit.url, target: "_blank", rel: "noopener", text: "反映の状況 →" }));
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
|
|
3833
|
+
/**
|
|
3834
|
+
* 反映中の箇所だけを描き直す。
|
|
3835
|
+
*
|
|
3836
|
+
* 🔴 **これが「モーダルを開いたまま待つ」への答え。** 画面へ反映する唯一の手段だった
|
|
3837
|
+
* refreshDataSources は、モーダルが開いている間は何もしない(下地を入れ替えると
|
|
3838
|
+
* 開いているモーダルの中身が切り離されるので、**そのガードは外せない**)。
|
|
3839
|
+
* ここは該当箇所だけを局所的に差し替えるので、開いたままでも追いつく。
|
|
3840
|
+
*/
|
|
3841
|
+
function repaintReflect(root) {
|
|
3842
|
+
if (!root || !root.querySelectorAll) return;
|
|
3843
|
+
var live = reflectingNow();
|
|
3844
|
+
var nodes = root.querySelectorAll("[data-reflect-role]");
|
|
3845
|
+
for (var i = 0; i < nodes.length; i++) applyReflect(nodes[i], live);
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3848
|
+
/**
|
|
3849
|
+
* 次の取り直しを予約する。
|
|
3850
|
+
*
|
|
3851
|
+
* 🔴 **反映待ちが1件も無ければタイマを張らない。** typeof fetch のガードでは足りない
|
|
3852
|
+
* ——テストは load の**後**に fetch を注入するので、そこで張ってしまうと
|
|
3853
|
+
* jsdom が閉じられず node --test が返らなくなる(この repo で実際に踏んだ形)。
|
|
3854
|
+
*/
|
|
3855
|
+
function scheduleReflectRefresh() {
|
|
3856
|
+
if (reflectTimer) return;
|
|
3857
|
+
var delay = nextCheckDelay(reflectingNow(), Date.now());
|
|
3858
|
+
if (delay == null) return;
|
|
3859
|
+
reflectTimer = setTimeout(reflectTick, delay);
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
// ---------- 未確定の行がある間の取り直し(段階1の前提)----------
|
|
3863
|
+
//
|
|
3864
|
+
// 🔴 **「反映中」の記録がある間だけ取り直す**のをやめ、**まとめ表がまだ答えていない
|
|
3865
|
+
// 行がある間**取り直す。registeredNow が入ると「反映中」は追加した瞬間に settle して
|
|
3866
|
+
// 消えるので、これが無いと**登録した直後に画面が二度と更新されなくなる。**
|
|
3867
|
+
//
|
|
3868
|
+
// 経路は増やさない(loadLiveFleet 1本)。取得の口が2つになると、上に出す
|
|
3869
|
+
// 「◯◯時点」がどちらの値を指すのか説明できなくなる。
|
|
3870
|
+
|
|
3871
|
+
/** 取り直しのタイマ(setTimeout の連鎖。setInterval は使わない) */
|
|
3872
|
+
var stateTimer = null;
|
|
3873
|
+
/** 取り直しを始めた時刻。**上限は経過時間で見る**(回数で数えない) */
|
|
3874
|
+
var stateSince = null;
|
|
3875
|
+
|
|
3876
|
+
/**
|
|
3877
|
+
* 取り直しの予約を、いまの状況に合わせ直す。
|
|
3878
|
+
*
|
|
3879
|
+
* 🔴 **未確定の行が無ければタイマを張らない。** テストは load の後に fetch を注入するので、
|
|
3880
|
+
* 無条件に張ると jsdom が閉じられず node --test が返らなくなる(反映待ちで実際に踏んだ形)。
|
|
3881
|
+
*/
|
|
3882
|
+
function refreshStateTimer() {
|
|
3883
|
+
var n = unsettledNow();
|
|
3884
|
+
if (!n) {
|
|
3885
|
+
if (stateTimer) { clearTimeout(stateTimer); stateTimer = null; }
|
|
3886
|
+
stateSince = null;
|
|
3887
|
+
return;
|
|
3888
|
+
}
|
|
3889
|
+
if (stateTimer) return;
|
|
3890
|
+
if (!SITE.intake || typeof fetch !== "function") return;
|
|
3891
|
+
if (stateSince == null) stateSince = Date.now();
|
|
3892
|
+
var delay = nextStateDelay(n, stateSince, Date.now());
|
|
3893
|
+
if (delay == null) return;
|
|
3894
|
+
stateTimer = setTimeout(stateTick, delay);
|
|
3895
|
+
}
|
|
3896
|
+
|
|
3897
|
+
function stateTick() {
|
|
3898
|
+
stateTimer = null;
|
|
3899
|
+
// 応答が届いた時点で refreshStateTimer が呼ばれ、まだ足りなければ次を張る。
|
|
3900
|
+
// **期限に達していれば張られない**(無限に叩き続けない)
|
|
3901
|
+
loadLiveFleet();
|
|
3902
|
+
}
|
|
3903
|
+
|
|
3904
|
+
function reflectTick() {
|
|
3905
|
+
reflectTimer = null;
|
|
3906
|
+
var live = reflectingNow();
|
|
3907
|
+
// **期限に達した回でも、一度だけ描き直してから止める。** これが無いと
|
|
3908
|
+
// 「取り直しは止まったのに反映中の文字が残る」時間ができる
|
|
3909
|
+
repaintReflect(document);
|
|
3910
|
+
// **新しい取得経路を作らない。** 2箇所になると「いつ時点の値か」が説明できなくなる
|
|
3911
|
+
if (live.length) loadLiveFleet();
|
|
3912
|
+
scheduleReflectRefresh();
|
|
3913
|
+
}
|
|
3914
|
+
|
|
2977
3915
|
/**
|
|
2978
3916
|
* その能力にそのアダプタの操作を出してよいか(#125②)。
|
|
2979
3917
|
*
|
|
@@ -3019,7 +3957,17 @@ ${capabilityState.toString()}
|
|
|
3019
3957
|
return (tool && CM_TOOL_LABEL_C[tool]) || cap;
|
|
3020
3958
|
}
|
|
3021
3959
|
|
|
3022
|
-
|
|
3960
|
+
/**
|
|
3961
|
+
* @param noteIsUnset その説明が「未接続(unset)」由来か。
|
|
3962
|
+
* 🔴 **反映待ちで抑制してよいのはこれだけ。** この経路は「未接続」「OFFに設定されて
|
|
3963
|
+
* います」「**設定に異常があります**」を同じ枠で運んでいる。無条件に抑制すると、
|
|
3964
|
+
* 保存直後の15分間、異常が画面から消える——直したい誤読より悪い
|
|
3965
|
+
*/
|
|
3966
|
+
function openCapabilityModal(cap, items, note, noteIsUnset) {
|
|
3967
|
+
// **開いた能力を覚えて、その登録ファイルを読みに行く(段階1)。** 全能力を毎回
|
|
3968
|
+
// 読ませない代わりに、見ている能力だけを取りに行く
|
|
3969
|
+
noteOpenCap(cap);
|
|
3970
|
+
// 見出しは押したタイルのツール名(Backlog・Google ドライブ…)。capToolLabel の説明を参照
|
|
3023
3971
|
var m = makeModal(capToolLabel(cap), true);
|
|
3024
3972
|
// **何が読み取られるのかを必ず書く。** 一覧に並ぶものと読み取られるものの関係は、
|
|
3025
3973
|
// 書いていないと分からない(チャットは「載っていれば全部」、開発はリポジトリ単位でOFFできる)
|
|
@@ -3031,7 +3979,25 @@ ${capabilityState.toString()}
|
|
|
3031
3979
|
// **なぜこの状態なのかを、押した先でも読めるようにする(#125②)。** タイルは常時出す
|
|
3032
3980
|
// ようになったので、未接続の能力を押したときに中身が空になる場面が増えた。
|
|
3033
3981
|
// 「設定できるものがありません」だけだと、繋げないのか繋ぐ必要が無いのかが分からない
|
|
3034
|
-
if (note)
|
|
3982
|
+
if (note) {
|
|
3983
|
+
var noteEl = el("p", { class: "modal-desc", text: note });
|
|
3984
|
+
// 反映が届いたら**届いたスナップショットから引き直す**(開いた時点の
|
|
3985
|
+
// 「未接続(…)」へ戻さない)。作り方はタイル側の modalNote と同じ式。
|
|
3986
|
+
// ⚠ 引き直しでは能力の代表アダプタの状態を使う(openCapabilityModal は
|
|
3987
|
+
// どのアダプタのタイルから開かれたかを知らない。カタログは今日どの能力も1個)
|
|
3988
|
+
if (noteIsUnset) reflectMark(noteEl, cap, "desc", function () {
|
|
3989
|
+
// 足した行があるなら、この一文(接続先が設定されていません)はもう事実ではない
|
|
3990
|
+
if (capPending(cap)) return { text: "", hidden: true };
|
|
3991
|
+
var capsNow = fleetVal("capabilities") || {};
|
|
3992
|
+
var dNow = capabilityDisplayState(cap, capsNow);
|
|
3993
|
+
var infoNow = capsNow[cap];
|
|
3994
|
+
var t = dNow && dNow !== "on" && infoNow && infoNow.detail
|
|
3995
|
+
? (CAP_STATE_TEXT[dNow] || "") + "(" + infoNow.detail + ")"
|
|
3996
|
+
: "";
|
|
3997
|
+
return { text: t, hidden: t === "" };
|
|
3998
|
+
});
|
|
3999
|
+
m.modal.appendChild(noteEl);
|
|
4000
|
+
}
|
|
3035
4001
|
if (!items.length) {
|
|
3036
4002
|
m.modal.appendChild(el("p", { class: "ds-empty", text: "この能力にはまだ設定できるものがありません。" }));
|
|
3037
4003
|
}
|
|
@@ -3110,11 +4076,18 @@ ${capabilityState.toString()}
|
|
|
3110
4076
|
//
|
|
3111
4077
|
// **取得できたかで文言を変える。** 静的ビルドの値をそのまま「現在値」として見せると、
|
|
3112
4078
|
// まさにここが警告している誤読を自分で起こす。取りに行って失敗したことは隠さない。
|
|
4079
|
+
// 🔴 **足した行があるときは「次回の実行後」と言い切らない(段階1)。** 一覧には
|
|
4080
|
+
// 夜間の値といま読み取った登録が並んでいるので、「反映されるのは次回の実行後」は
|
|
4081
|
+
// **もう出ている行について嘘**になる。時刻を2つ出す形(どの行がいつの値か)は
|
|
4082
|
+
// 段階3で1箇所にまとめるので、ここでは**但し書きを1行足すだけ**にとどめる。
|
|
3113
4083
|
var asOf = fleetVal("generatedAt") ? dsFmtTime(fleetVal("generatedAt")) : "";
|
|
4084
|
+
var hasPending = Object.keys(pendingCaps()).length > 0;
|
|
3114
4085
|
if (asOf) {
|
|
3115
4086
|
var asOfText = LIVE_FLEET_FAILED
|
|
3116
4087
|
? "最新の接続状況を取得できませんでした。表示しているのは " + asOf + " 時点の情報です。"
|
|
3117
|
-
:
|
|
4088
|
+
: hasPending
|
|
4089
|
+
? "接続状況は " + asOf + " 時点の自動チェック結果です。いま登録した分も一覧に出していますが、読み取れているかどうかが分かるのは次回の自動チェックの後になります。"
|
|
4090
|
+
: "接続状況は " + asOf + " 時点の自動チェック結果です。設定を変えた場合、ここに反映されるのは次回の実行後になります。";
|
|
3118
4091
|
sec.appendChild(el("p", {
|
|
3119
4092
|
class: "datasources-asof" + (LIVE_FLEET_FAILED ? " is-stale" : ""),
|
|
3120
4093
|
text: asOfText,
|
|
@@ -3232,9 +4205,29 @@ ${capabilityState.toString()}
|
|
|
3232
4205
|
col.appendChild(el("span", { class: "ds-drive-note", text: "Drive自動同期は未設定(手動で置いたファイルの変換のみ)" }));
|
|
3233
4206
|
}
|
|
3234
4207
|
// 未使用(enabled: false)はグレー行。アダプターの存在は見せつつ、この案件では設定されていないことを示す
|
|
3235
|
-
|
|
4208
|
+
// 🔴 **足した行がある能力では「未使用」と言わない(段階1)。** enabled は夜間の値で、
|
|
4209
|
+
// いま登録した行が一覧に出ているのに「この案件では設定されていません」と並ぶ
|
|
4210
|
+
var inactive = s.enabled === false && !capPending(s.kind);
|
|
3236
4211
|
if (inactive) {
|
|
3237
|
-
|
|
4212
|
+
// **反映待ちのあいだは「⏳ 反映中」に差し替える(消さない)。** 消すと
|
|
4213
|
+
// .ds-inactive の灰色だけが残り、なぜ灰色なのかが読めなくなる
|
|
4214
|
+
col.appendChild(reflectMark(
|
|
4215
|
+
el("span", { class: "ds-inactive-note", text: "未使用(この案件では設定されていません)" }),
|
|
4216
|
+
s.kind, "note",
|
|
4217
|
+
// **反映が届いたら、届いたスナップショットで判定し直す。** 下地へ戻すと
|
|
4218
|
+
// 「⏳ 反映中」→「未使用(この案件では設定されていません)」になり、
|
|
4219
|
+
// データが正しくなった**あと**に嘘をつく。灰色の見た目もここで外す
|
|
4220
|
+
//(節ごとの作り直しはモーダルを閉じるまで走らない)
|
|
4221
|
+
function (n) {
|
|
4222
|
+
var srcNow = internalSourceOf(s.kind);
|
|
4223
|
+
var stillInactive = !!(srcNow && srcNow.enabled === false) && !capPending(s.kind);
|
|
4224
|
+
if (!stillInactive) {
|
|
4225
|
+
var card = n && n.closest ? n.closest(".ds-card") : null;
|
|
4226
|
+
if (card) card.classList.remove("ds-inactive");
|
|
4227
|
+
}
|
|
4228
|
+
return { text: "未使用(この案件では設定されていません)", hidden: !stillInactive };
|
|
4229
|
+
},
|
|
4230
|
+
));
|
|
3238
4231
|
}
|
|
3239
4232
|
// id は接続マップのノードclickの飛び先(アンカー)
|
|
3240
4233
|
// **id は付けない。** #101 以前はこのカードが接続マップの飛び先だったが、
|
|
@@ -3313,19 +4306,8 @@ ${capabilityState.toString()}
|
|
|
3313
4306
|
// 追加のときにワークスペースを選ばせるのをやめたので、
|
|
3314
4307
|
// 「どの組織のものか」「その組織とつながっているか」を画面が言う。
|
|
3315
4308
|
// 分け方は groupSlackByOrg(純関数)が全部持っている
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
return;
|
|
3319
|
-
}
|
|
3320
|
-
// **GitHub も組織ごとに分ける(2026-08-20・Slack からの横展開)。**
|
|
3321
|
-
// それまでは「リポジトリの一覧」と「組織ごとの接続状況」が**別々に並んで**おり、
|
|
3322
|
-
// どの行がどの組織のものか、行を見ても分からなかった。
|
|
3323
|
-
if (g.type === "__github__") {
|
|
3324
|
-
// **meta は行ごとに引く。** issues と discussions が混ざるので、
|
|
3325
|
-
// グループの type から1つに決められない
|
|
3326
|
-
githubRows(g.items).forEach(function (n) { pushCap("開発", n); });
|
|
3327
|
-
return;
|
|
3328
|
-
}
|
|
4309
|
+
// **一覧は下で1つの箱にまとめて出す**(開いたままでも差し替えられるように)
|
|
4310
|
+
if (g.type === "slack" || g.type === "__github__") return;
|
|
3329
4311
|
var cards = el("div", { class: "ds-list" });
|
|
3330
4312
|
g.items.forEach(function (s) { cards.appendChild(extSourceCard(s, meta)); });
|
|
3331
4313
|
if (g.items.length >= 4) {
|
|
@@ -3353,6 +4335,50 @@ ${capabilityState.toString()}
|
|
|
3353
4335
|
});
|
|
3354
4336
|
}
|
|
3355
4337
|
|
|
4338
|
+
// ---- チャットと開発の一覧(開いたままでも追いつく形)----
|
|
4339
|
+
//
|
|
4340
|
+
// 🔴 **行が1件も無くても箱を出す。** 立ち上げ直後の案件=**この仕組みが一番効く案件**は
|
|
4341
|
+
// ここが空だが、モーダルを開いたままチャンネルを足すのはまさにその案件。
|
|
4342
|
+
// 箱が無いと、足した行を入れる場所が画面に存在しない。
|
|
4343
|
+
//
|
|
4344
|
+
// **Slack は組織ごと(slackRows)・GitHub も組織ごと(githubRows)。**
|
|
4345
|
+
// それまでは「行の一覧」と「組織ごとの接続状況」が別々に並んでいて、
|
|
4346
|
+
// どの行がどの組織の話なのか読めなかった(2026-08-20 に揃えた)。
|
|
4347
|
+
function liveExternals(kind) {
|
|
4348
|
+
return (fleetVal("externalSources") || []).filter(function (s) {
|
|
4349
|
+
return kind === "slack" ? s.type === "slack" : String(s.type || "").indexOf("github") === 0;
|
|
4350
|
+
});
|
|
4351
|
+
}
|
|
4352
|
+
/** 行の見た目を決める値だけを並べる(時刻など毎回変わる値は入れない) */
|
|
4353
|
+
function externalsSig(kind, extra) {
|
|
4354
|
+
return JSON.stringify([liveExternals(kind), extra]);
|
|
4355
|
+
}
|
|
4356
|
+
/**
|
|
4357
|
+
* **空の箱を置いてよいのは、そこへ足せる案件だけ。** 投入の口を持たない構成
|
|
4358
|
+
* (素の Decisions ビュー)で行も無い能力に空の箱を積むと、押しても何も無いモーダルになる。
|
|
4359
|
+
*/
|
|
4360
|
+
function liveListWanted(kind) { return !!SITE.intake || liveExternals(kind).length > 0; }
|
|
4361
|
+
if (liveListWanted("slack")) pushCap("チャット", liveList(
|
|
4362
|
+
"チャット",
|
|
4363
|
+
function () { return externalsSig("slack", slackWorkspaces()); },
|
|
4364
|
+
function (box) {
|
|
4365
|
+
var items = liveExternals("slack");
|
|
4366
|
+
if (!items.length) return;
|
|
4367
|
+
var meta = DS_TYPE["slack"] || { icon: "🔗", label: "Slack" };
|
|
4368
|
+
slackRows(items, meta).forEach(function (n) { box.appendChild(n); });
|
|
4369
|
+
},
|
|
4370
|
+
));
|
|
4371
|
+
if (liveListWanted("github")) pushCap("開発", liveList(
|
|
4372
|
+
"開発",
|
|
4373
|
+
function () { return externalsSig("github", fleetVal("githubOwners") || null); },
|
|
4374
|
+
function (box) {
|
|
4375
|
+
var items = liveExternals("github");
|
|
4376
|
+
if (!items.length) return;
|
|
4377
|
+
// **meta は行ごとに引く**(issues と discussions でアイコンとラベルが違う)
|
|
4378
|
+
githubRows(items).forEach(function (n) { box.appendChild(n); });
|
|
4379
|
+
},
|
|
4380
|
+
));
|
|
4381
|
+
|
|
3356
4382
|
// **追加フォームはグループの外に置く。** グループは0件だと描かれず、4件以上だと
|
|
3357
4383
|
// 畳まれる(既定は閉)。中に入れると**新規案件(0件)=一番追加したい場面**で消える。
|
|
3358
4384
|
var adders = renderExternalAdders();
|
|
@@ -3539,9 +4565,17 @@ ${capabilityState.toString()}
|
|
|
3539
4565
|
* @param derived engine が導出した5値(capabilities 由来)。古いデータでは null
|
|
3540
4566
|
* @param adapter このタイルのアダプタ(件数の出し分けに使う)
|
|
3541
4567
|
*/
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
4568
|
+
/**
|
|
4569
|
+
* タイルの2行目。
|
|
4570
|
+
*
|
|
4571
|
+
* @param ins 内部ソースの一覧 / @param srcs 外部ソースの一覧。
|
|
4572
|
+
* **引数で受ける**(節を作ったときの値を掴まない)。反映が届いたあと、
|
|
4573
|
+
* 節を作り直せない場面(モーダルを開いたまま)でも、**届いた一覧で
|
|
4574
|
+
* 引き直す**ために同じ関数を使う——写経すると片方だけ古くなる
|
|
4575
|
+
*/
|
|
4576
|
+
function tileSub(cap, st, derived, adapter, ins, srcs) {
|
|
4577
|
+
var internal = ins.filter(function (s) { return s.kind === cap; })[0];
|
|
4578
|
+
var live = srcs.filter(function (s) { return externalCapability(s.type) === cap; });
|
|
3545
4579
|
var parts = [];
|
|
3546
4580
|
// **導出が on 以外なら、導出の語を優先する(#125②)。** broken は状態としては
|
|
3547
4581
|
// 警告(⚠️)だが、原因は「設定そのものが壊れている」であって同期の失敗ではない。
|
|
@@ -3650,10 +4684,13 @@ ${capabilityState.toString()}
|
|
|
3650
4684
|
*/
|
|
3651
4685
|
function renderAdapterTile(cap, items, adapter, derived, ai) {
|
|
3652
4686
|
var meta = CAP_META[cap] || { icon: "🔗", desc: "" };
|
|
4687
|
+
// 🔴 **状態はまとめ表の素の値で決める(段階1)。** ここに足した行を混ぜると、
|
|
4688
|
+
// 登録直後の1行で「使っている」と判定され、誰も測っていないのに**接続OK**に落ちる
|
|
4689
|
+
// (同じ画面の行は「登録状況を確認中です」)。足した行の扱いは pendingSub が持つ
|
|
3653
4690
|
var st = derived === "unset" && capabilityDisplayState(cap, caps) !== "unset"
|
|
3654
4691
|
// カタログにあるが導出が答えていないアダプタ(今日は発生しない。#107 の前方互換)
|
|
3655
4692
|
? "idle"
|
|
3656
|
-
: capabilityState(cap,
|
|
4693
|
+
: capabilityState(cap, stateCtx());
|
|
3657
4694
|
// **ロゴはタイルの左に独立して置く。** 名前の行に入れると名前の高さに
|
|
3658
4695
|
// 縛られて小さくなる。ロゴで能力を見分けられるのが一覧の主な用途
|
|
3659
4696
|
var logo = ADAPTER_ICONS[adapter];
|
|
@@ -3663,10 +4700,38 @@ ${capabilityState.toString()}
|
|
|
3663
4700
|
// カタログ外の値はここへ来ない(capabilityAdapter が弾く)。旧データの "none" だけは
|
|
3664
4701
|
// 素通しすると顧客の画面に「none」というタイル名が出るので、上で空にしてある
|
|
3665
4702
|
var rawTool = adapter || "";
|
|
4703
|
+
// **2行目は反映待ちのあいだ「⏳ 反映中」にする。** モーダルを開かなくても分かる
|
|
4704
|
+
// ようにするのが目的だが、別のバッジを足すのではなく**この行を差し替える**
|
|
4705
|
+
// ——抑制しないと、同じタイルの中で「⏳ 反映中」と「⦿ 未接続」が並んで矛盾する。
|
|
4706
|
+
// **抑制するのは unset のときだけ**(off / broken は残す。R4 と同じ理由)
|
|
4707
|
+
// まとめ表がまだ知らない行がある能力は、**行と同じ語**に倒す(未接続とも接続OKとも言わない)
|
|
4708
|
+
var pend = capPending(cap);
|
|
4709
|
+
var subEl = el("div", {
|
|
4710
|
+
class: "ds-tile-sub " + (pend ? DS_GATE_PENDING.cls : "ds-gate-" + st),
|
|
4711
|
+
text: pend ? DS_GATE_PENDING.text : tileSub(cap, st, derived, adapter, internals, sources),
|
|
4712
|
+
});
|
|
4713
|
+
// **反映が届いたら、届いたスナップショットから引き直す。** 下地へ戻すと
|
|
4714
|
+
// 「⏳ 反映中」→「⦿ 未接続」になる——データが正しくなったあとに嘘をつく。
|
|
4715
|
+
// 状態の語も色(ds-gate-*)も、判定はタイルを作るときと同じ式で引く
|
|
4716
|
+
if (derived === "unset") reflectMark(subEl, cap, "sub", function (n) {
|
|
4717
|
+
var capsNow = fleetVal("capabilities") || {};
|
|
4718
|
+
var insNow = fleetVal("internalSources") || [];
|
|
4719
|
+
var srcsNow = fleetVal("externalSources") || [];
|
|
4720
|
+
var dNow = adapter === capabilityAdapter(cap, capsNow) ? capabilityDisplayState(cap, capsNow) : "unset";
|
|
4721
|
+
var stNow = dNow === "unset" && capabilityDisplayState(cap, capsNow) !== "unset"
|
|
4722
|
+
? "idle"
|
|
4723
|
+
: capabilityState(cap, stateCtx());
|
|
4724
|
+
var pendNow = capPending(cap);
|
|
4725
|
+
n.className = "ds-tile-sub " + (pendNow ? DS_GATE_PENDING.cls : "ds-gate-" + stNow);
|
|
4726
|
+
return {
|
|
4727
|
+
text: pendNow ? DS_GATE_PENDING.text : tileSub(cap, stNow, dNow, adapter, insNow, srcsNow),
|
|
4728
|
+
hidden: false,
|
|
4729
|
+
};
|
|
4730
|
+
});
|
|
3666
4731
|
var body = el("div", { class: "ds-tile-body" }, [
|
|
3667
4732
|
el("div", { class: "ds-tile-name", text: (rawTool && (CM_TOOL_LABEL_C[rawTool] || rawTool)) || cap }),
|
|
3668
4733
|
el("div", { class: "ds-tile-desc", text: meta.desc }),
|
|
3669
|
-
|
|
4734
|
+
subEl,
|
|
3670
4735
|
]);
|
|
3671
4736
|
// **なぜその状態なのかを添える(#125②)。** engine が書いた一文(detail)で、
|
|
3672
4737
|
// 「何が無いのか」を出す。on のときは読み取り範囲(件数)が既に同じ行にある。
|
|
@@ -3720,10 +4785,13 @@ ${capabilityState.toString()}
|
|
|
3720
4785
|
body,
|
|
3721
4786
|
el("span", { class: "ds-tile-chev", text: "›" }),
|
|
3722
4787
|
]);
|
|
4788
|
+
// ⚠ **ここで足した行(段階1)を見て抑制しない。** unset の一文は開くときに引き直される
|
|
4789
|
+
// (openCapabilityModal の reflectMark の fresh 側が capPending を見る)ので二重になるうえ、
|
|
4790
|
+
// ここで抑えると off / broken まで消える——R4 で「消さない」と決めたもの
|
|
3723
4791
|
var modalNote = derived && derived !== "on" && capInfo && capInfo.detail
|
|
3724
4792
|
? CAP_STATE_TEXT[derived] + "(" + capInfo.detail + ")"
|
|
3725
4793
|
: "";
|
|
3726
|
-
tile.addEventListener("click", function () { openCapabilityModal(cap, items, modalNote); });
|
|
4794
|
+
tile.addEventListener("click", function () { openCapabilityModal(cap, items, modalNote, derived === "unset"); });
|
|
3727
4795
|
sec.appendChild(tile);
|
|
3728
4796
|
}
|
|
3729
4797
|
|
|
@@ -4240,6 +5308,8 @@ ${capabilityState.toString()}
|
|
|
4240
5308
|
function backlogTargetsSection(s) {
|
|
4241
5309
|
var targets = backlogTargetsNow(s);
|
|
4242
5310
|
var editable = backlogTargetsEditable();
|
|
5311
|
+
/** 追加・削除の応答で一覧を受け取ったか(受け取っていたら、そちらが正本) */
|
|
5312
|
+
var adopted = false;
|
|
4243
5313
|
|
|
4244
5314
|
// **Slack / GitHub / Figma / Drive と同じレコードの行にする(2026-08-21・オーナー指摘)。**
|
|
4245
5315
|
// それまでは「現在の接続先」というラベル付きの独自レイアウト(.ds-current /
|
|
@@ -4247,7 +5317,29 @@ ${capabilityState.toString()}
|
|
|
4247
5317
|
var listEl = el("div", { class: "ds-list" });
|
|
4248
5318
|
// **0件の1行は一覧の外に置く**(Figma / Drive と同じ形)。中に .ds-card として
|
|
4249
5319
|
// 置くと「行が1件ある」と読まれ、一覧を数えるテストにも行として数えられる
|
|
4250
|
-
|
|
5320
|
+
// **反映待ちのあいだは「未設定」と言わない。** 保存はできているので事実に反する
|
|
5321
|
+
var EMPTY_TEXT = "同期する接続先はまだ登録されていません(未設定)。";
|
|
5322
|
+
var emptyEl = reflectMark(
|
|
5323
|
+
el("p", { class: "ds-empty", text: EMPTY_TEXT }),
|
|
5324
|
+
"課題管理", "empty",
|
|
5325
|
+
// 🔴 **反映が届いたら、届いた一覧で描き直す。** 下地(=この0件の文言)へ戻すと、
|
|
5326
|
+
// 反映が届いた瞬間に「⏳ 反映中」→「同期する接続先はまだ登録されていません(未設定)。」
|
|
5327
|
+
// になり、**行があるのに無いと言う**。settle は「新しいスナップショットが届いた」
|
|
5328
|
+
// ことなので、そのデータで一覧を作り直す(adoptTargets と同じ形)。
|
|
5329
|
+
// モーダルを開いたまま待っているときは、これが唯一の描き直しの機会
|
|
5330
|
+
function () {
|
|
5331
|
+
// まだ箱を作っていない(=最初の描画の途中)。paint() は箱へ出し入れするので、
|
|
5332
|
+
// ここで走らせない。最初の描画はこの直後の paint() が受け持つ
|
|
5333
|
+
if (!box) return null;
|
|
5334
|
+
// 🔴 **応答の一覧を上書きしない。** 追加・削除の応答に添えられた一覧
|
|
5335
|
+
//(adoptTargets)は、この画面が持っている中でいちばん新しい正本。
|
|
5336
|
+
// スナップショットから引き直すと、外したばかりの接続先が戻って見える
|
|
5337
|
+
if (adopted) return null;
|
|
5338
|
+
targets = backlogTargetsNow(internalSourceOf("課題管理"));
|
|
5339
|
+
paint();
|
|
5340
|
+
return { text: EMPTY_TEXT, hidden: false };
|
|
5341
|
+
},
|
|
5342
|
+
);
|
|
4251
5343
|
// 追加・削除の結果はここに1つだけ出す(行を描き直しても消えない位置)
|
|
4252
5344
|
var msg = el("div", { class: "ds-toggle-msg" });
|
|
4253
5345
|
|
|
@@ -4332,6 +5424,9 @@ ${capabilityState.toString()}
|
|
|
4332
5424
|
var here = emptyEl.parentNode === box;
|
|
4333
5425
|
if (!targets.length && !here) box.insertBefore(emptyEl, listEl.nextSibling);
|
|
4334
5426
|
else if (targets.length && here) box.removeChild(emptyEl);
|
|
5427
|
+
// **出し入れのたびに反映中を当て直す。** 外している間の描き直しはこの行に届かない
|
|
5428
|
+
// ので、戻すときに当てないと古い文言のまま出る
|
|
5429
|
+
applyReflect(emptyEl, reflectingNow());
|
|
4335
5430
|
}
|
|
4336
5431
|
|
|
4337
5432
|
/**
|
|
@@ -4339,6 +5434,9 @@ ${capabilityState.toString()}
|
|
|
4339
5434
|
* (=他の人が先に変えていた。409 の文面をこちらの言葉に差し替える判断に使う)。
|
|
4340
5435
|
*/
|
|
4341
5436
|
function adoptTargets(list) {
|
|
5437
|
+
// **応答の一覧を受け取ったら、以後はスナップショットから引き直さない**
|
|
5438
|
+
//(応答のほうが新しい。反映待ちが終わっても、この一覧を上書きしない)
|
|
5439
|
+
adopted = true;
|
|
4342
5440
|
var before = targetsGenerationOf(targets);
|
|
4343
5441
|
var next = [];
|
|
4344
5442
|
for (var i = 0; i < list.length; i++) {
|
|
@@ -4384,8 +5482,23 @@ ${capabilityState.toString()}
|
|
|
4384
5482
|
|
|
4385
5483
|
// **box を先に作る。** paint() は 0件の1行を box へ出し入れするので、
|
|
4386
5484
|
// 箱が無いと最初の描画で「未設定」の行が出ない
|
|
4387
|
-
var box = el("div", {}, [listEl]);
|
|
5485
|
+
var box = el("div", { "data-live-cap": REGISTERED_CAP_NAMES.issues }, [listEl]);
|
|
4388
5486
|
paint();
|
|
5487
|
+
/**
|
|
5488
|
+
* 取り直しで新しい値が届いたときの差し替え(段階1)。**一覧だけを描き直す。**
|
|
5489
|
+
*
|
|
5490
|
+
* モーダルを開いたまま接続先を足すのが普通の使い方なので、ここが無いと
|
|
5491
|
+
* 足した接続先は閉じるまで画面に出ない。paint() は鍵の欄を作り直さず入れ直すだけなので、
|
|
5492
|
+
* 入力中の鍵と「保存中…」は保たれる。
|
|
5493
|
+
*/
|
|
5494
|
+
box.__rebuild = function () {
|
|
5495
|
+
// 🔴 応答に添えられた一覧(adoptTargets)のほうが新しい。上書きしない
|
|
5496
|
+
if (adopted) return;
|
|
5497
|
+
var next = backlogTargetsNow(internalSourceOf(REGISTERED_CAP_NAMES.issues));
|
|
5498
|
+
if (targetsGenerationOf(next) === targetsGenerationOf(targets)) return;
|
|
5499
|
+
targets = next;
|
|
5500
|
+
paint();
|
|
5501
|
+
};
|
|
4389
5502
|
// 接続先がまだ無い案件の受け皿は、0件の1行の直後に置く(行が無いので中へ入れられない)
|
|
4390
5503
|
if (keySlots[""]) box.appendChild(keySlots[""]);
|
|
4391
5504
|
if (editable) {
|
|
@@ -4557,9 +5670,17 @@ ${capabilityState.toString()}
|
|
|
4557
5670
|
// **ON/OFFのトグルは出さない。** フォルダを追加した時点で取り込みは始まる
|
|
4558
5671
|
// (applyAddDriveFolder が enabled を立てる)ので「始める」を押す機会が無い。
|
|
4559
5672
|
// 止めたいならフォルダを外す——登録そのものが意思表示、という会議と同じ考え方。
|
|
4560
|
-
|
|
5673
|
+
// **一覧は開いたままでも差し替わる箱に入れる(段階1)。** 追加は「モーダルを開いたまま」
|
|
5674
|
+
// 行われるので、ここが固定だと足したフォルダは閉じるまで画面に出ない
|
|
5675
|
+
var driveNow = function () { return internalSourceOf("共有資料") || s; };
|
|
5676
|
+
box.appendChild(liveList("共有資料", function () {
|
|
5677
|
+
var d = driveNow();
|
|
5678
|
+
return JSON.stringify([d.driveFolderIds || [], d.driveFolderNames || {}, d.driveState || ""]);
|
|
5679
|
+
}, function (listBox) {
|
|
5680
|
+
var src = driveNow();
|
|
5681
|
+
var folders = src.driveFolderIds || [];
|
|
4561
5682
|
// 名前は任意。**無ければIDを出す**(既存の案件は名前を持っていない)
|
|
4562
|
-
var fnames =
|
|
5683
|
+
var fnames = src.driveFolderNames || {};
|
|
4563
5684
|
if (folders.length) {
|
|
4564
5685
|
var dlist = el("div", { class: "ds-list" });
|
|
4565
5686
|
folders.forEach(function (id) {
|
|
@@ -4599,8 +5720,8 @@ ${capabilityState.toString()}
|
|
|
4599
5720
|
}));
|
|
4600
5721
|
dlist.appendChild(row);
|
|
4601
5722
|
});
|
|
4602
|
-
|
|
4603
|
-
} else if (
|
|
5723
|
+
listBox.appendChild(dlist);
|
|
5724
|
+
} else if (src.driveState === "empty") {
|
|
4604
5725
|
// **0件を黙って見せない。** 追加フォームだけだと「まだ登録していない」のか
|
|
4605
5726
|
// 「読めていない」のか区別できない。
|
|
4606
5727
|
//
|
|
@@ -4608,8 +5729,9 @@ ${capabilityState.toString()}
|
|
|
4608
5729
|
// driveFolderIds を「1件以上あるときだけ」出すので、キーの有無では
|
|
4609
5730
|
// 未取得(unset / broken)と0件を見分けられない。unset / broken は
|
|
4610
5731
|
// 上の分岐で先に返しているので、ここへ来る empty は「読めていて0件」だけ。
|
|
4611
|
-
|
|
5732
|
+
listBox.appendChild(el("p", { class: "ds-empty", text: "取り込み対象のフォルダはまだ登録されていません。" }));
|
|
4612
5733
|
}
|
|
5734
|
+
}));
|
|
4613
5735
|
box.appendChild(addForm({
|
|
4614
5736
|
title: "+ Google Drive のフォルダを追加",
|
|
4615
5737
|
placeholder: "https://drive.google.com/drive/folders/…",
|
|
@@ -4636,7 +5758,11 @@ ${capabilityState.toString()}
|
|
|
4636
5758
|
if (s.kind === "デザイン" && usable("デザイン", "figma")) {
|
|
4637
5759
|
// 登録済みファイルを1件ずつ外せるようにする(6ファイル抱えている案件があり、
|
|
4638
5760
|
// 古いスプリントのファイルを外したいのが実際に起きる操作)。
|
|
4639
|
-
|
|
5761
|
+
// **一覧は開いたままでも差し替わる箱に入れる(段階1)。** 追加は「モーダルを開いたまま」
|
|
5762
|
+
// 行われるので、ここが固定だと足したファイルは閉じるまで画面に出ない
|
|
5763
|
+
var figsOf = function () { return (internalSourceOf("デザイン") || {}).figmaFiles || []; };
|
|
5764
|
+
box.appendChild(liveList("デザイン", function () { return JSON.stringify(figsOf()); }, function (listBox) {
|
|
5765
|
+
var figs = figsOf();
|
|
4640
5766
|
// **残り件数は「押した時点」で数える。** 一覧(figs)は夜間バッチのスナップショットで、
|
|
4641
5767
|
// このページを開いている間はもちろん、その日のうちに再読み込みしても更新されない。
|
|
4642
5768
|
// 描画時点の件数で判定すると、3件を1件ずつ外していったとき最後の1件でも警告が出ない。
|
|
@@ -4684,12 +5810,13 @@ ${capabilityState.toString()}
|
|
|
4684
5810
|
}));
|
|
4685
5811
|
flist.appendChild(row);
|
|
4686
5812
|
});
|
|
4687
|
-
|
|
5813
|
+
listBox.appendChild(flist);
|
|
4688
5814
|
} else {
|
|
4689
5815
|
// **0件を黙って見せない。** 追加フォームだけだと「まだ登録していない」のか
|
|
4690
5816
|
// 「読めていない」のか区別できない(Driveは driveState で理由まで出している)
|
|
4691
|
-
|
|
5817
|
+
listBox.appendChild(el("p", { class: "ds-empty", text: "同期対象のFigmaファイルはまだ登録されていません。" }));
|
|
4692
5818
|
}
|
|
5819
|
+
}));
|
|
4693
5820
|
// **鍵の欄は一覧の直後に1つ(2026-08-21・オーナー指摘)。**
|
|
4694
5821
|
// Backlog と違い、Figma の鍵は**案件に1本**(ファイルごとではない)。
|
|
4695
5822
|
// 行ごとに出すと同じ欄が並ぶだけなので、一覧をひとまとまりと見て、その下に置く。
|
|
@@ -4939,6 +6066,22 @@ ${capabilityState.toString()}
|
|
|
4939
6066
|
msg.appendChild(el("a", { href: j.refreshUrl, target: "_blank", rel: "noopener", text: "反映の状況 →" }));
|
|
4940
6067
|
}
|
|
4941
6068
|
btn.disabled = false;
|
|
6069
|
+
// **反映待ちとして記録する。** 一覧はスナップショットから描かれるので、
|
|
6070
|
+
// 保存できていても数分は「未設定」と出る。その間だけ「⏳ 反映中」に差し替える。
|
|
6071
|
+
//
|
|
6072
|
+
// ⚠ **onDone の中に置かない。** removeRow は onDone に null を渡しているので、
|
|
6073
|
+
// 中に置くと**接続先の「削除」だけ黙って記録されない**(remove の判定が守りたい当のケース)。
|
|
6074
|
+
//
|
|
6075
|
+
// - changed === false(既に同じものが入っていた)は記録しない——待つものが無い
|
|
6076
|
+
// - refreshUrl が無い(自己チェックを起こせなかった)ときも記録しない
|
|
6077
|
+
// ——待っても来ないので「反映中」が嘘になる
|
|
6078
|
+
if (j.changed !== false && j.refreshUrl) rememberReflecting(payload, j.refreshUrl);
|
|
6079
|
+
// **保存が効いたら、いまの登録を読み直す(段階1)。** 一覧はまとめ表(夜間バッチ)から
|
|
6080
|
+
// 描かれるので、保存しただけでは行が出ない。ここで読み直すと、登録ファイルの中身が
|
|
6081
|
+
// 返ってきて**足りない行として一覧に出る**——さらに「まとめ表がまだ答えていない行」が
|
|
6082
|
+
// できるので、まとめ表が追いつくまで取り直しが続く。
|
|
6083
|
+
// 取得中なら何もしない(応答後に catchUpOpenCaps が拾う)
|
|
6084
|
+
if (j.changed !== false) loadLiveFleet();
|
|
4942
6085
|
// **何も変わっていないなら「追加した」と見せない。** 既に同じものが入っていた場合
|
|
4943
6086
|
// サーバは 200 + changed:false を返す。ここを res.ok だけで判定すると
|
|
4944
6087
|
// 「変更はありません」と言いながら追加した行が出る
|
|
@@ -5573,6 +6716,51 @@ ${capabilityState.toString()}
|
|
|
5573
6716
|
}
|
|
5574
6717
|
|
|
5575
6718
|
|
|
6719
|
+
/* ---------------------------------------------------------------------------
|
|
6720
|
+
* 開いたままのモーダルへ届ける(段階1)
|
|
6721
|
+
*
|
|
6722
|
+
* 🔴 **チャンネルやフォルダを足すのは「モーダルを開いたまま」。** 節ごとの差し替え
|
|
6723
|
+
* (refreshDataSources)はモーダルが開いている間は何もしない——下地を入れ替えると
|
|
6724
|
+
* 開いているモーダルの中身が切り離され、入力中の値も「保存中…」も消えるので、
|
|
6725
|
+
* **あのガードは外せない**。だから「反映中」と同じく、**その箇所だけを差し替える口**を置く。
|
|
6726
|
+
*
|
|
6727
|
+
* 差し替えるのは**レコードの一覧だけ**(追加フォーム・鍵の欄・結果メッセージは触らない)。
|
|
6728
|
+
* 課題管理の adoptTargets が先にこの形を採っている。
|
|
6729
|
+
*
|
|
6730
|
+
* 🔴 **中身が変わっていなければ何もしない。** 60秒ごとの取り直しのたびに作り直すと、
|
|
6731
|
+
* 「削除しました」の表示や取り消し線(.ds-removed)が黙って消える。
|
|
6732
|
+
* ------------------------------------------------------------------------- */
|
|
6733
|
+
|
|
6734
|
+
/**
|
|
6735
|
+
* 開いたままでも追いつく一覧。
|
|
6736
|
+
*
|
|
6737
|
+
* @param cap どの能力の一覧か(差し替えの対象を絞るためだけに使う)
|
|
6738
|
+
* @param sig いまのデータを表す文字列。**変わったときだけ**作り直す
|
|
6739
|
+
* @param build 中身を作る(box を渡すので、その中へ appendChild する)
|
|
6740
|
+
*/
|
|
6741
|
+
function liveList(cap, sig, build) {
|
|
6742
|
+
var box = el("div", { class: "ds-livelist", "data-live-cap": cap });
|
|
6743
|
+
box.__rebuild = function () {
|
|
6744
|
+
var next = sig();
|
|
6745
|
+
if (next === box.__sig) return;
|
|
6746
|
+
box.__sig = next;
|
|
6747
|
+
while (box.firstChild) box.removeChild(box.firstChild);
|
|
6748
|
+
build(box);
|
|
6749
|
+
};
|
|
6750
|
+
box.__rebuild();
|
|
6751
|
+
return box;
|
|
6752
|
+
}
|
|
6753
|
+
|
|
6754
|
+
/** 開いているモーダルの中の一覧だけを差し替える(下地は触らない) */
|
|
6755
|
+
function repaintOpenLists() {
|
|
6756
|
+
var overlay = app.querySelector(".modal-overlay");
|
|
6757
|
+
if (!overlay) return;
|
|
6758
|
+
var nodes = overlay.querySelectorAll("[data-live-cap]");
|
|
6759
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
6760
|
+
if (typeof nodes[i].__rebuild === "function") nodes[i].__rebuild();
|
|
6761
|
+
}
|
|
6762
|
+
}
|
|
6763
|
+
|
|
5576
6764
|
/**
|
|
5577
6765
|
* 取得中フラグ。**完了しても寝かせたままにしない。**
|
|
5578
6766
|
* 一度 true のままだと、タブを移動して戻ってきても再取得せず、設定を変えた人が
|
|
@@ -5580,6 +6768,43 @@ ${capabilityState.toString()}
|
|
|
5580
6768
|
* **操作タブに入り直したときだけ**もう一度取りに行く(ポーリングはしない)。
|
|
5581
6769
|
*/
|
|
5582
6770
|
var liveFleetRequested = false;
|
|
6771
|
+
/**
|
|
6772
|
+
* この画面で開いた能力(op=state に送る識別子)。
|
|
6773
|
+
*
|
|
6774
|
+
* 🔴 **開いている能力の分だけ読む。** Lambda は指定された能力の登録ファイルだけを
|
|
6775
|
+
* 読む(ツリー1本+ファイルの本数)。全能力を毎回読ませると GitHub の枠を削る。
|
|
6776
|
+
*
|
|
6777
|
+
* **一度開いたら覚えておく(閉じても外さない)。** 外すと、次の取り直しで
|
|
6778
|
+
* その能力の行が応答から消え、**タイルの件数だけが行ったり来たりする**。
|
|
6779
|
+
*/
|
|
6780
|
+
var OPEN_CAPS = [];
|
|
6781
|
+
/** 画面の能力名 → op=state の識別子(対応表の実体は REGISTERED_CAP_NAMES) */
|
|
6782
|
+
function stateCapIdOf(cap) {
|
|
6783
|
+
for (var id in REGISTERED_CAP_NAMES) {
|
|
6784
|
+
if (Object.prototype.hasOwnProperty.call(REGISTERED_CAP_NAMES, id) && REGISTERED_CAP_NAMES[id] === cap) return id;
|
|
6785
|
+
}
|
|
6786
|
+
return null;
|
|
6787
|
+
}
|
|
6788
|
+
/** 取得中に開かれた能力があれば、もう一度だけ取りに行く */
|
|
6789
|
+
function catchUpOpenCaps() {
|
|
6790
|
+
if (OPEN_CAPS.length === liveFleetSentCaps.length) return;
|
|
6791
|
+
loadLiveFleet();
|
|
6792
|
+
}
|
|
6793
|
+
/** 能力を開いた(=その登録ファイルを読んでよい)。増えたときだけ取り直す */
|
|
6794
|
+
function noteOpenCap(cap) {
|
|
6795
|
+
var id = stateCapIdOf(cap);
|
|
6796
|
+
if (!id || OPEN_CAPS.indexOf(id) >= 0) return;
|
|
6797
|
+
OPEN_CAPS.push(id);
|
|
6798
|
+
loadLiveFleet();
|
|
6799
|
+
}
|
|
6800
|
+
/**
|
|
6801
|
+
* 直近の取得に載せた能力の一覧。
|
|
6802
|
+
*
|
|
6803
|
+
* **取得中に能力を開いた分を取りこぼさないため。** 取得中の loadLiveFleet は
|
|
6804
|
+
* 何もせずに戻るので、これが無いと「開いたのに、その能力だけ永久に読まれない」
|
|
6805
|
+
* (しかも次の取り直しは未確定の行が無いので張られない=二度と追いつかない)。
|
|
6806
|
+
*/
|
|
6807
|
+
var liveFleetSentCaps = [];
|
|
5583
6808
|
function loadLiveFleet() {
|
|
5584
6809
|
if (liveFleetRequested) return;
|
|
5585
6810
|
var intake = SITE.intake;
|
|
@@ -5588,11 +6813,14 @@ ${capabilityState.toString()}
|
|
|
5588
6813
|
// 素で呼ぶと描画中の ReferenceError になり、「画面が例外を出さずに描き切る」検査を割る。
|
|
5589
6814
|
if (typeof fetch !== "function") return;
|
|
5590
6815
|
liveFleetRequested = true;
|
|
6816
|
+
liveFleetSentCaps = OPEN_CAPS.slice();
|
|
5591
6817
|
var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
|
|
5592
6818
|
fetch(pfx + "op=state&t=" + encodeURIComponent(intake.token), {
|
|
5593
6819
|
method: "POST",
|
|
5594
6820
|
headers: { "content-type": "application/json" },
|
|
5595
|
-
|
|
6821
|
+
// **開いている能力を伝える。** 知らない値は Lambda 側が黙って捨てる(断らない)ので、
|
|
6822
|
+
// 画面と Lambda の版が食い違っても接続状況の表示は消えない
|
|
6823
|
+
body: JSON.stringify({ caps: OPEN_CAPS }),
|
|
5596
6824
|
})
|
|
5597
6825
|
// **非2xxも失敗として扱う。** ビューアとLambdaの版は独立に進むので、
|
|
5598
6826
|
// op=state 未対応のLambdaは 400 を返す。通信エラーだけを見ていると空表示になる。
|
|
@@ -5601,18 +6829,39 @@ ${capabilityState.toString()}
|
|
|
5601
6829
|
// **fleet より先に読む。** fleet-status.json をまだ持たない案件(立ち上げ直後)は
|
|
5602
6830
|
// 下で throw するが、リアルタイム同期の可否は索引由来なので独立に決まる
|
|
5603
6831
|
LIVE_REALTIME = (j && j.realtime) || null;
|
|
6832
|
+
// 🔴 **registeredNow も fleet より先に読む。** まとめ表をまだ持たない案件
|
|
6833
|
+
// (=立ち上げ直後=この仕組みが一番効く案件)は下で throw するので、
|
|
6834
|
+
// ここより後に置くと**その案件だけ素通り**する
|
|
6835
|
+
LIVE_REGISTERED = sanitizeRegisteredNow(j && j.registeredNow);
|
|
6836
|
+
MERGED = null;
|
|
5604
6837
|
if (!j || !j.fleet) throw new Error("no fleet");
|
|
5605
6838
|
LIVE_FLEET = normalizeFleet(j.fleet);
|
|
5606
6839
|
LIVE_FLEET_FAILED = false;
|
|
5607
6840
|
liveFleetRequested = false;
|
|
6841
|
+
MERGED = null;
|
|
5608
6842
|
refreshDataSources();
|
|
6843
|
+
// **モーダルを開いたままでも「反映中」だけは追いつかせる。** refreshDataSources は
|
|
6844
|
+
// モーダルが開いている間・入力中は何もしない(下地を入れ替えると開いているモーダルの
|
|
6845
|
+
// 中身が切り離される)ので、局所的に差し替える口をここに置く
|
|
6846
|
+
repaintReflect(document);
|
|
6847
|
+
repaintOpenLists();
|
|
6848
|
+
// まだ足りない行があるなら取り直しを続ける(無くなったら止める)
|
|
6849
|
+
refreshStateTimer();
|
|
6850
|
+
catchUpOpenCaps();
|
|
5609
6851
|
})
|
|
5610
6852
|
.catch(function () {
|
|
5611
6853
|
// 静的ビルドの値で描き続ける。ただし「古いかもしれない」ことは画面に出す
|
|
5612
6854
|
LIVE_FLEET = null;
|
|
5613
6855
|
LIVE_FLEET_FAILED = true;
|
|
5614
6856
|
liveFleetRequested = false;
|
|
6857
|
+
MERGED = null;
|
|
5615
6858
|
refreshDataSources();
|
|
6859
|
+
repaintReflect(document);
|
|
6860
|
+
repaintOpenLists();
|
|
6861
|
+
// **失敗しても取り直しは続ける**(期限が来れば止まる)。ここで止めると、
|
|
6862
|
+
// 一時的な失敗のあと画面が二度と追いつかない
|
|
6863
|
+
refreshStateTimer();
|
|
6864
|
+
catchUpOpenCaps();
|
|
5616
6865
|
});
|
|
5617
6866
|
}
|
|
5618
6867
|
|
|
@@ -5620,6 +6869,12 @@ ${capabilityState.toString()}
|
|
|
5620
6869
|
renderConnMap();
|
|
5621
6870
|
renderDataSources();
|
|
5622
6871
|
loadLiveFleet();
|
|
6872
|
+
// **まとめ表がまだ答えていない行があれば取り直す。** 取得前は0件なので、
|
|
6873
|
+
// 実際に張られるのは応答が届いてから(loadLiveFleet が呼び直す)
|
|
6874
|
+
refreshStateTimer();
|
|
6875
|
+
// **反映待ちがあれば自動で取り直す(再読み込みしても続く)。** 反映待ちが
|
|
6876
|
+
// 1件も無ければタイマは張らない
|
|
6877
|
+
scheduleReflectRefresh();
|
|
5623
6878
|
// **取り込みカードと鍵カードは能力セクションへ移した**(#101)。
|
|
5624
6879
|
// ここで再度出すと二重になる。手動投入は 会議 / 共有資料 の中にある。
|
|
5625
6880
|
}
|