@takagaki/cortex-decisions-viewer 0.12.214 → 0.12.217

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.
Files changed (2) hide show
  1. package/dist/render.js +221 -265
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -2336,8 +2336,8 @@ const SECRET_NOW_STATES = ["registered", "missing", "unknown"];
2336
2336
  /**
2337
2337
  * `secretsNow`(鍵が登録されているか・列2)の許可リスト。
2338
2338
  *
2339
- * 応答の1行は `{ adapter, kind, state, source, space? }`。画面が使うのは kind / state /
2340
- * space だけで、**adapter は使わない**(鍵カードは種別で引く)。source は「どの棚が
2339
+ * 応答の1行は `{ adapter, kind, state, source, space?, savedAt? }`。画面が使うのは kind / state /
2340
+ * space / savedAt だけで、**adapter は使わない**(鍵カードは種別で引く)。source は「どの棚が
2341
2341
  * 答えたか」の内部語なので、画面に出す語へ畳むのは読み出し側(`secretStateFromNow`)。
2342
2342
  */
2343
2343
  function sanitizeSecretsNow(v) {
@@ -2359,6 +2359,9 @@ function sanitizeSecretsNow(v) {
2359
2359
  // 揃えないと**入っている鍵を「未登録」と言う**(`issuesRefOf` と同じ理由)
2360
2360
  if (typeof r.space === "string" && r.space)
2361
2361
  row.space = r.space.toLowerCase();
2362
+ // 保存日時(受付が Secrets Manager の LastChangedDate から載せる ISO)。日付として読める形だけ通す(札の「M/D に保存」の材料・便 KU)
2363
+ if (typeof r.savedAt === "string" && /^\d{4}-\d{2}-\d{2}T[\d:.]+(Z|[+-]\d{2}:?\d{2})$/.test(r.savedAt) && !isNaN(new Date(r.savedAt).getTime()))
2364
+ row.savedAt = r.savedAt;
2362
2365
  out.push(row);
2363
2366
  }
2364
2367
  return out.length ? out : null;
@@ -2393,7 +2396,7 @@ function secretStateFromNow(rows, kind, space) {
2393
2396
  hit = mine[0];
2394
2397
  }
2395
2398
  if (hit.state === "registered")
2396
- return { known: "yes", src: "Secrets Manager" };
2399
+ return hit.savedAt ? { known: "yes", src: "Secrets Manager", savedAt: hit.savedAt } : { known: "yes", src: "Secrets Manager" };
2397
2400
  if (hit.state === "missing")
2398
2401
  return { known: "no", src: "" };
2399
2402
  return null;
@@ -5734,7 +5737,6 @@ a.ds-tile:hover { text-decoration: none; }
5734
5737
  .ds-more { margin-top: 14px; }
5735
5738
  .ds-more > summary { cursor: pointer; color: var(--muted); font-size: 12px; }
5736
5739
  .ds-more[open] > summary { margin-bottom: 8px; }
5737
- .ds-more-key { margin: 6px 0 0; font-size: 12.5px; color: var(--text); display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
5738
5740
  /* 行の状態に添える「次に誰が何をするか」(2026-08-18)。**必ず自分の行を占める**
5739
5741
  (flex: 1 1 100%)——状態バッジの横に押し込むと、いちばん読ませたい一文が
5740
5742
  カード幅に負けて省略される。状態の語より弱く、しかし読める大きさで出す */
@@ -5794,7 +5796,10 @@ a.ds-tile:hover { text-decoration: none; }
5794
5796
  .ops-intro { color: var(--muted); font-size: 14px; line-height: 1.8; margin: 4px 0 8px; }
5795
5797
  .ops-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }
5796
5798
  .sec-state { font-size: 12.5px; font-weight: 600; margin: 0 0 8px; color: var(--muted); }
5797
- .sec-state.is-managed { color: #1a7f37; }
5799
+ .sec-state.is-managed, .sec-state.is-ok { color: #1a7f37; }
5800
+ .sec-state.is-failed { color: #b42318; }
5801
+ .sec-state.is-checking { color: var(--muted); font-weight: 400; font-style: italic; }
5802
+ .sec-saved { margin: 6px 0 0; }
5798
5803
  .sec-state.is-none { color: var(--muted); font-weight: 400; }
5799
5804
  /* 登録されているか分からない(engine がスペース別の状況をまだ出していない)。
5800
5805
  **未登録と同じ見た目にしない**——断定していないことが色でも分かるようにする */
@@ -5830,16 +5835,6 @@ a.ds-tile:hover { text-decoration: none; }
5830
5835
  .ops-card.ds-keycard > .ops-card-head { gap: 8px; }
5831
5836
  .ops-card.ds-keycard .ops-card-icon { font-size: 16px; }
5832
5837
  .ops-card.ds-keycard .ops-card-title { font-size: 13px; }
5833
- /* 押すまで入力欄を出さない(2026-08-21・オーナー指摘)。summary はボタンに見せる */
5834
- .sec-input-fold { border: none; margin: 0; }
5835
- .sec-input-fold > summary {
5836
- cursor: pointer; display: inline-block; list-style: none;
5837
- padding: 5px 12px; border-radius: 999px; border: 1px solid var(--border);
5838
- background: #fff; color: var(--text); font-size: 12.5px; font-weight: 600;
5839
- }
5840
- .sec-input-fold > summary::-webkit-details-marker { display: none; }
5841
- .sec-input-fold > summary:hover { border-color: var(--accent); color: var(--accent); }
5842
- .sec-input-fold[open] > summary { margin-bottom: 8px; }
5843
5838
  .ds-addform { border: 1px dashed var(--border); border-radius: 8px; padding: 12px 14px; margin-top: 10px; background: #fff; }
5844
5839
  .ds-addform-title { font-size: 13px; font-weight: 700; color: var(--text); margin-bottom: 6px; }
5845
5840
  .ds-addform .sec-input { margin-bottom: 6px; }
@@ -10156,7 +10151,7 @@ ${story_1.splitByNameLinks.toString()}
10156
10151
  // 🔴 **Backlog の行の一手(c-07 の 2 本目)。** 行の中身(そのスペースのキーの有無・同期経路・
10157
10152
  // スペースの数)で決まるので関数で持つ。row は { domain, projectKey, gate }。
10158
10153
  // DS_GATE_CAP.issues は札(APIキーが未登録です)だけを持ち、help を持たせない(一手の置き場は 1 か所)。
10159
- // 部品の型は 文字列 / { copy, bare } / { act: "backlog-key" | "backlog-route-on", label, space }
10154
+ // 部品の型は 文字列 / { copy, bare } / { act: "backlog-route-on", label }
10160
10155
  backlog: {
10161
10156
  no_token: {
10162
10157
  resting: function (row) { return backlogRowHelpParts(row); },
@@ -10298,21 +10293,16 @@ ${story_1.splitByNameLinks.toString()}
10298
10293
  /**
10299
10294
  * 行の一手に置くボタン(c-07)。**押せないボタンは作らない**(null)。
10300
10295
  * - "connect" … [接続する]。Slack の承認画面へ送る(slackConnectInline)。投入の口が無ければ null
10301
- * - "figma-key" … [トークンを入れる]/[トークンを入れ直す]。下の「Figma のトークン」の欄の入力へ移る
10302
- * - "backlog-key" … [入れる]/[入れ直す]。**そのスペースの**鍵カードを開いて入力へ移る(arg=スペース)。
10303
- * 受け皿が無いスペースには作らない(別のスペースの欄へ落とさない)
10296
+ * 🔴 **鍵を入れるボタンは作らない(便 KU)。** 鍵の欄は接続先の行の中に最初から見えている(secretCard)ので、
10297
+ * 「入れる」の入口を行の一手に重ねない(同じ鍵に[入れる]が 3 か所あった)
10304
10298
  * - "backlog-route-on" … [切り替える]。同期経路を固定 IP 経由にする(setBacklogRoute)
10305
10299
  * @param label 省略時は既定の語
10306
- * @param arg 部品が要る材料(backlog-key のスペース)
10300
+ * @param arg 部品が要る材料(今は使う一手が無い)
10307
10301
  */
10308
10302
  function actNode(act, label, arg) {
10309
10303
  if (!SITE.intake) return null;
10310
10304
  if (act === "connect") return slackConnectInline(label || "接続する");
10311
- // Backlog の行の一手(c-07 の 2 本目)。arg はスペース(ドメイン)。**受け皿が無ければ作らない**
10312
- if (act === "backlog-key") return backlogKeyJump(arg, label || "入れる");
10313
10305
  if (act === "backlog-route-on") return backlogRouteOnButton(label || "切り替える");
10314
- // 鍵の欄(KEY_SLOTS.figma)が無い構成では、移る先が無いので出さない
10315
- if (act === "figma-key") return secretKindsShown().indexOf("figma") >= 0 ? figmaKeyJump(label || "トークンを入れる") : null;
10316
10306
  return null;
10317
10307
  }
10318
10308
  /**
@@ -10488,13 +10478,12 @@ ${story_1.splitByNameLinks.toString()}
10488
10478
  },
10489
10479
  issues: { no_token: { text: "APIキーが未登録です", cls: "ds-gate-muted" } },
10490
10480
  design: {
10491
- // 🔴 **鍵が無い行の一手は、行の中の 1 行(c-07)。** [トークンを入れる]が下の「Figma のトークン」の欄の入力へ移る
10481
+ // 🔴 **鍵が無い行の一手は、行の中の 1 行(c-07)。** 一手の文が下の「Figma のトークン」の欄を指す(ボタンは無い・便 KU)
10492
10482
  no_token: {
10493
10483
  text: "トークンが未登録です",
10494
10484
  cls: "ds-gate-muted",
10495
- help: "Figma のトークンを入れてください。",
10485
+ help: "Figma のトークンを、下の欄に入れてください。",
10496
10486
  helpLink: { href: FIGMA_SETTINGS_URL, label: "Figma の設定でトークンを発行する →" },
10497
- act: "figma-key",
10498
10487
  },
10499
10488
  // 「アクセス不可」では、トークンの失効なのか(403)ファイルを共有していないのか(404)が
10500
10489
  // 画面から見分けられない(理由が文字の中だけ=機械の語に依存した分岐は書かない・c-07)。
@@ -10505,9 +10494,7 @@ ${story_1.splitByNameLinks.toString()}
10505
10494
  unreachable: {
10506
10495
  text: "ファイルを読めません",
10507
10496
  cls: "ds-gate-warn",
10508
- help: "このファイルに、トークンを発行したアカウントを閲覧者として招待してください(トークンが切れているときは入れ直してください)。",
10509
- act: "figma-key",
10510
- actLabel: "トークンを入れ直す",
10497
+ help: "このファイルに、トークンを発行したアカウントを閲覧者として招待してください(トークンが切れているときは、下の欄に入れ直してください)。",
10511
10498
  },
10512
10499
  },
10513
10500
  };
@@ -10516,6 +10503,9 @@ ${story_1.splitByNameLinks.toString()}
10516
10503
  * 知らない値は 「DS_GATE」 と同じく「不明」(engine が新しい語を足したのに画面が知らない、
10517
10504
  * という事実を黙って消さない)。
10518
10505
  */
10506
+ /** 接続の状態と[接続を確認]を鍵の部品が持つ能力か(Backlog=issues・Figma=design)。行はこの 2 つを持たない(便 KU) */
10507
+ function keyCardOwnsStatus(capId) { return capId === "issues" || capId === "design"; }
10508
+
10519
10509
  function dsGateForCap(capId, status) {
10520
10510
  var byCap = DS_GATE_CAP[capId];
10521
10511
  if (byCap && byCap[status]) return byCap[status];
@@ -11109,24 +11099,6 @@ ${story_1.splitByNameLinks.toString()}
11109
11099
  return wrap;
11110
11100
  }
11111
11101
 
11112
- /**
11113
- * [トークンを入れる]/[トークンを入れ直す](Figma の行の一手・c-07)。
11114
- * 一覧の直後にある「Figma のトークン」の欄(KEY_SLOTS.figma)の入力へ移って focus する。
11115
- * **欄は 0 件のあいだ畳まれている**が、行がある=ファイルが 1 件以上あるので畳まれていない。
11116
- * 入力が見つからなければ何もしない(鍵の欄を作る口が無い構成)。
11117
- */
11118
- function figmaKeyJump(label) {
11119
- var btn = el("button", { class: "ds-toggle-btn ds-act-inline", type: "button", text: label || "トークンを入れる" });
11120
- btn.addEventListener("click", function () {
11121
- var slot = KEY_SLOTS.figma;
11122
- var input = slot && slot.querySelector("input, textarea");
11123
- if (!input) return;
11124
- try { if (input.scrollIntoView) input.scrollIntoView({ block: "center" }); } catch (e) { /* 動かせなくても focus は試す */ }
11125
- input.focus();
11126
- });
11127
- return btn;
11128
- }
11129
-
11130
11102
  /** 台帳から、そのワークスペースの接続状態を引く(無ければ null=断定しない) */
11131
11103
  function tokenStateOf(workspaces, teamId) {
11132
11104
  var list = workspaces || [];
@@ -12425,32 +12397,15 @@ ${story_1.splitByNameLinks.toString()}
12425
12397
  // **対応表に無い種別は隠さない**(keyTargetCount の既定と同じく出す側に倒す)。
12426
12398
  // 3つ目の鍵が足されて分岐を書き忘れた場合は key-card-visibility の [全種0件] が落ちる
12427
12399
  var hide = shown[KEY_CARDS[i].kind] === false;
12428
- // 🔴 **Backlog の行の中のカードは、0 件の判定(上)に加えて「見せる理由があるとき」だけ見せる**
12429
- //(c-07 の 2 本目)。0 件が勝つ——0 件のあいだは理由があっても出さない(順序の逆な案内になる)
12430
- if (!hide && KEY_CARDS[i].compact) hide = !backlogKeyCardWanted(KEY_CARDS[i]);
12431
12400
  KEY_CARDS[i].el.hidden = hide;
12432
- if (!hide && KEY_CARDS[i].compact && KEY_CARDS[i].refresh) KEY_CARDS[i].refresh();
12401
+ // 🔴 札は見せる瞬間に当て直す(作ったときの答えは古いことがある。取り直しが後から届く・#227)
12402
+ if (!hide && KEY_CARDS[i].refresh) KEY_CARDS[i].refresh();
12433
12403
  }
12434
12404
  eachKeySlot(function (slot) {
12435
12405
  slot.hidden = !slot.querySelector("[data-secret-kind]:not([hidden])");
12436
12406
  });
12437
12407
  }
12438
12408
 
12439
- /**
12440
- * Backlog の行の中の鍵カードを、いま見せるか(c-07 の 2 本目・スペースごとの判定)。次の 4 つのどれか:
12441
- * ① キーが要る(backlogKeyNeededForSpace=登録状況が「未登録」・足した直後で答えがまだ無い等)
12442
- * ② 一手の[入れる]/[入れ直す]・「その他」の[入れ直す]が押された(forced)
12443
- * ③ 触り始めた(開いた・値がある・結果の文がある)。🔴 **保存直後の「✓ 保存しました」と入力中の値を畳まない**(#223)
12444
- * 使えているスペースのカードは、ふだん隠れている(DOM には常に在る=入力中の値・結果は消えない)。
12445
- */
12446
- function backlogKeyCardWanted(ent) {
12447
- if (ent.forced) return true;
12448
- if (ent.fold && ent.fold.open) return true;
12449
- if (ent.input && ent.input.value !== "") return true;
12450
- if (ent.msg && ent.msg.textContent !== "") return true;
12451
- return backlogKeyNeededForSpace(ent.space);
12452
- }
12453
-
12454
12409
  /** いま在る鍵の受け皿(.ds-keyrow)を順に渡す。置き場の正本は KEY_SLOTS */
12455
12410
  function eachKeySlot(fn) {
12456
12411
  var slots = KEY_SLOTS.backlog || {};
@@ -13046,6 +13001,7 @@ ${story_1.splitByNameLinks.toString()}
13046
13001
  KEY_CARDS = [];
13047
13002
  // Backlog の節の行の控え・結果の帯・描き直しの口も作り直す(切り離された古い節を掴んだままにしない)
13048
13003
  BACKLOG_UI = { rows: [], msg: null, repaintRows: null };
13004
+ FIGMA_UI = { keys: [] };
13049
13005
  // 作り直しはスナップショットから描き直すこと。応答で採った件数はここで捨てる
13050
13006
  ADOPTED_TARGET_COUNT = null;
13051
13007
  var internals = fleetVal("internalSources") || [];
@@ -14501,6 +14457,7 @@ ${story_1.splitByNameLinks.toString()}
14501
14457
  MERGED = null;
14502
14458
  repaintOpenLists();
14503
14459
  refreshDataSources();
14460
+ syncKeyCardVisibility(); // 鍵の札(接続OK/失敗)も同じ結果で当て直す
14504
14461
  }
14505
14462
  // 本物の行を 1 回だけ取りに行く(据え置きのまま・「reloadLiveState」の項)
14506
14463
  reloadLiveState();
@@ -15452,7 +15409,7 @@ ${story_1.splitByNameLinks.toString()}
15452
15409
  }
15453
15410
  var ipLabel = ip ? "固定の送信元 IP(" + ip + ")" : "固定の送信元 IP";
15454
15411
  if (keyState === "no") {
15455
- return "同期は" + ipLabel + "から行います。お客様にこの IP の許可を依頼し、接続先の行の「API キーを入れてください」から登録すると同期が始まります。";
15412
+ return "同期は" + ipLabel + "から行います。お客様にこの IP の許可を依頼し、接続先の行の下の欄に API キーを貼って[保存して接続を確認]を押すと同期が始まります。";
15456
15413
  }
15457
15414
  // 2 文に収める(実装レビューの指摘)。押した直後の帯(サーバの文面)も「許可を依頼」を言うが、
15458
15415
  // 帯は数秒で読み終える結果で、この行は次に開いたときも残る状態——役目が違うので両方に置く
@@ -15551,7 +15508,7 @@ ${story_1.splitByNameLinks.toString()}
15551
15508
  * 部品の置き場:
15552
15509
  * - 一手 … DS_GATE_RESTING_HELP.backlog → backlogRowHelpParts(行の中身で決まる)
15553
15510
  * - キーの欄 … これまでと同じスペースごとの受け皿(KEY_SLOTS.backlog・#223)に同じカード。
15554
- * 変えるのは見え方だけ(syncKeyCardVisibility の backlogKeyCardWanted)
15511
+ * 変えるのは見え方だけ(syncKeyCardVisibility)
15555
15512
  * - 同期経路の切り替え … setBacklogRoute に 1 本化。入口はタイルの一手・[接続を確認]の結果・「その他」のスイッチ
15556
15513
  * - 札(事実)… 経路札「固定 IP 経由」・リアルタイム札(appendBacklogChips)
15557
15514
  * ------------------------------------------------------------------------- */
@@ -15562,6 +15519,8 @@ ${story_1.splitByNameLinks.toString()}
15562
15519
  * 🔴 **鍵の置き場の正本は KEY_SLOTS。ここは行の控えだけ**(#223 の「節の中に受け皿の控えを持たない」は守る)。
15563
15520
  */
15564
15521
  var BACKLOG_UI = { rows: [], msg: null, repaintRows: null };
15522
+ /** Figma の鍵が効くファイル(鍵カードの札が引く。一覧を描くときに入れ替える) */
15523
+ var FIGMA_UI = { keys: [] };
15565
15524
 
15566
15525
  /** いま見えている行のスペース(小文字・重複なし) */
15567
15526
  function backlogDomainsNow() {
@@ -15614,27 +15573,13 @@ ${story_1.splitByNameLinks.toString()}
15614
15573
  return false;
15615
15574
  }
15616
15575
 
15617
- /** そのスペースの鍵カードを見せるか(行のどれかが要るか。行が無ければ要らない) */
15618
- function backlogKeyNeededForSpace(space) {
15619
- var domains = backlogDomainsNow();
15620
- var dom = space || (domains.length === 1 ? domains[0] : "");
15621
- if (!dom) return false;
15622
- var rows = BACKLOG_UI.rows || [];
15623
- for (var i = 0; i < rows.length; i++) {
15624
- if (String(rows[i].domain || "").toLowerCase() !== dom) continue;
15625
- var r = reachableStateOf("issues", issuesRefOf(rows[i].domain, rows[i].projectKey));
15626
- if (backlogKeyNeeded(dom, r ? r.status : null)) return true;
15627
- }
15628
- return false;
15629
- }
15630
-
15631
15576
  /**
15632
15577
  * Backlog の行に出す一手のパーツ(無ければ null)。**スペースをまたがない**(ボタンはその行のスペースの受け皿だけを引く)。
15633
15578
  *
15634
- * キーが要る → 「API キーを入れてください。[入れる]」(複数スペースならスペース名つき)
15635
- * 読めない・通常経路 → 「読めません。API キーを入れ直してください。[入れ直す] Backlog に IP 制限があるなら、固定 IP 経由で読んでください。[切り替える]」
15636
- * 読めない・固定 IP → 「お客様に固定の送信元 IP(…)の許可を依頼してください。[コピー] 許可済みなら、API キーを入れ直してください。[入れ直す]」
15637
- * 読めない・経路不明 → 「読めません。API キーを入れ直してください。[入れ直す]」(切り替えが押せるか分からないので[切り替える]は出さない)
15579
+ * キーが要る → 「API キーを、下の欄に入れてください。」(複数スペースならスペース名つき)
15580
+ * 読めない・通常経路 → 「読めません。下の欄に API キーを入れ直してください。 Backlog に IP 制限があるなら、固定 IP 経由で読んでください。[切り替える]」
15581
+ * 読めない・固定 IP → 「お客様に固定の送信元 IP(…)の許可を依頼してください。[コピー] 許可済みなら、下の欄に API キーを入れ直してください。」
15582
+ * 読めない・経路不明 → 「読めません。下の欄に API キーを入れ直してください。」(切り替えが押せるか分からないので[切り替える]は出さない)
15638
15583
  *
15639
15584
  * 🔴 **理由の語(HTTP の番号等)には依存しない。** 401・403・404 は画面から見分けられないので、読めない行は広い方の 1 行。
15640
15585
  * IP 制限と確定できるのは[接続を確認]の応答の reason だけ(appendBacklogProbeSwitch)。
@@ -15646,10 +15591,10 @@ ${story_1.splitByNameLinks.toString()}
15646
15591
  var dom = String(row.domain || "").toLowerCase();
15647
15592
  var multi = backlogDomainsNow().length > 1;
15648
15593
  if (backlogKeyNeeded(dom, row.gate)) {
15649
- return [(multi ? dom + " の " : "") + "API キーを入れてください。", { act: "backlog-key", space: dom, label: "入れる" }];
15594
+ // 🔴 鍵の欄は接続先の行の中に最初から見えている(便 KU)。ここは指す文だけでボタンは置かない
15595
+ return [(multi ? dom + " の " : "") + "API キーを、下の欄に入れてください。"];
15650
15596
  }
15651
15597
  if (row.gate !== "unreachable") return null;
15652
- var reenter = { act: "backlog-key", space: dom, label: "入れ直す" };
15653
15598
  var route = LIVE_BACKLOG_ROUTE;
15654
15599
  if (route === "fixed-ip") {
15655
15600
  // IP が取れていなければ IP も[コピー]も出さない(嘘の値を書かない・#516)
@@ -15657,9 +15602,9 @@ ${story_1.splitByNameLinks.toString()}
15657
15602
  var head = ip
15658
15603
  ? ["お客様に固定の送信元 IP(" + ip + ")の許可を依頼してください。", { copy: ip, bare: true }]
15659
15604
  : ["お客様に固定の送信元 IP の許可を依頼してください。"];
15660
- return head.concat([" 許可済みなら、API キーを入れ直してください。", reenter]);
15605
+ return head.concat([" 許可済みなら、下の欄に API キーを入れ直してください。"]);
15661
15606
  }
15662
- var base = ["読めません。API キーを入れ直してください。", reenter];
15607
+ var base = ["読めません。下の欄に API キーを入れ直してください。"];
15663
15608
  if (route === "runner") {
15664
15609
  // 切り替えは案件全体の経路(スペースごとではない)。複数スペースでは強めに言う
15665
15610
  var scope = multi
@@ -15690,54 +15635,6 @@ ${story_1.splitByNameLinks.toString()}
15690
15635
  return appendReachableReason(row, "issues", ref);
15691
15636
  }
15692
15637
 
15693
- /** そのスペースの鍵の受け皿(無ければ null)。名無しの受け皿は、スペースが 1 つ以下のあいだだけ代わりを務める(ensureKeySlots の項) */
15694
- function backlogKeySlotFor(space) {
15695
- var slots = KEY_SLOTS.backlog || {};
15696
- var d = String(space || "").toLowerCase();
15697
- if (d && slots[d]) return slots[d];
15698
- if (slots[""] && backlogDomainsNow().length <= 1) return slots[""];
15699
- return null;
15700
- }
15701
-
15702
- /** そのスペースの鍵カードの控え(無ければ null)。**ほかのスペースのカードへ落とさない** */
15703
- function backlogKeyEntryFor(space) {
15704
- var d = String(space || "").toLowerCase();
15705
- var list = [];
15706
- for (var i = 0; i < KEY_CARDS.length; i++) {
15707
- if (KEY_CARDS[i].kind === "backlog" && KEY_CARDS[i].compact) list.push(KEY_CARDS[i]);
15708
- }
15709
- for (var j = 0; j < list.length; j++) if (d && list[j].space === d) return list[j];
15710
- if (backlogDomainsNow().length <= 1) {
15711
- for (var k = 0; k < list.length; k++) if (list[k].space === "") return list[k];
15712
- }
15713
- return null;
15714
- }
15715
-
15716
- /** そのスペースの鍵カードを見せて、入力へ移る([入れる]・[入れ直す])。見つからなければ false */
15717
- function openBacklogKeyCard(space) {
15718
- var ent = backlogKeyEntryFor(space);
15719
- if (!ent) return false;
15720
- ent.forced = true;
15721
- ent.fold.open = true;
15722
- syncKeyCardVisibility();
15723
- try { if (ent.input.scrollIntoView) ent.input.scrollIntoView({ block: "center" }); } catch (e) { /* 動かせなくても focus は試す */ }
15724
- ent.input.focus();
15725
- return true;
15726
- }
15727
-
15728
- /**
15729
- * [入れる]/[入れ直す]。**そのスペースの受け皿が無ければ作らない**(別のスペースの欄へ移さない)。
15730
- * 🔴 **作る条件は受け皿(KEY_SLOTS)だけ。カードの控えは押した時点で引く**——節は鍵カードより先に組み立てられる
15731
- *(buildDataSources の順序)ので、行を描く時点ではカードがまだ無い。受け皿は節が先に作っている
15732
- */
15733
- function backlogKeyJump(space, label) {
15734
- if (secretKindsShown().indexOf("backlog") < 0) return null;
15735
- if (!backlogKeySlotFor(space)) return null;
15736
- var btn = el("button", { class: "ds-toggle-btn ds-act-inline", type: "button", text: label || "入れる" });
15737
- btn.addEventListener("click", function () { openBacklogKeyCard(space); });
15738
- return btn;
15739
- }
15740
-
15741
15638
  /**
15742
15639
  * 同期経路の切り替え(固定 IP 経由 ⇄ 通常)。**スイッチ(「その他」)とタイルの[切り替える]が同じこの関数を呼ぶ**
15743
15640
  * ——操作を 2 本書かない(op・値・確認なし・応答の route を正とする・changed:false の取り直しは 1 つだけ持つ)。
@@ -16079,6 +15976,8 @@ ${story_1.splitByNameLinks.toString()}
16079
15976
  * 行の組み立ての最後に 「appendReachableReason」 を呼ぶ。
16080
15977
  */
16081
15978
  function appendReachableChip(row, capId, ref, workspace) {
15979
+ // 🔴 鍵で読む能力(Backlog・Figma)の接続の状態は、鍵の部品(secretCard)の札 1 か所だけ(便 KU)。行にも出すと同じ材料の札が窓の中で 2 か所に見える
15980
+ if (keyCardOwnsStatus(capId)) return row;
16082
15981
  var r = reachableStateOf(capId, ref, workspace);
16083
15982
  if (!r) return row;
16084
15983
  var gate = dsGateForCap(capId, r.status);
@@ -16273,7 +16172,8 @@ ${story_1.splitByNameLinks.toString()}
16273
16172
  // 🔴 表の案件(組み直した案件)には起こす同期が無い(押すと「確認を開始できませんでした」になる)。2 つとも出さない。
16274
16173
  // 見分けは設定の画面と同じ 1 か所(viewMode)。今までの案件(file)はここを素通り=画面は 1 バイトも変わらない
16275
16174
  if (viewMode() === "table") return row;
16276
- var checkBtn = el("button", { class: "ds-toggle-btn", type: "button", text: "接続を確認" });
16175
+ // 鍵で読む能力の[接続を確認]は鍵の部品の[保存して接続を確認]だけ(便 KU)。行には[今すぐ同期]だけ残す
16176
+ var checkBtn = keyCardOwnsStatus(capId) ? null : el("button", { class: "ds-toggle-btn", type: "button", text: "接続を確認" });
16277
16177
  var syncBtn = el("button", { class: "ds-toggle-btn", type: "button", text: "今すぐ同期" });
16278
16178
  var note = el("p", { class: "ds-probe-wait" });
16279
16179
  var phase = probeWaitPhaseOf(capId, ref);
@@ -16282,7 +16182,7 @@ ${story_1.splitByNameLinks.toString()}
16282
16182
  else if (phase === "timeout") note.textContent = PROBE_WAIT_TIMEOUT_TEXT;
16283
16183
  else note.hidden = true;
16284
16184
 
16285
- checkBtn.addEventListener("click", async function () {
16185
+ if (checkBtn) checkBtn.addEventListener("click", async function () {
16286
16186
  // 押している間は**ボタンだけ**が「確認中…」になる。行の中身は変えない(#1328)。
16287
16187
  // 前回の返事(「同期を開始しました」等)は押した時点で消す
16288
16188
  var endBusy = beginBusy(checkBtn, "確認中…");
@@ -16349,7 +16249,7 @@ ${story_1.splitByNameLinks.toString()}
16349
16249
  endBusy();
16350
16250
  });
16351
16251
 
16352
- row.appendChild(checkBtn);
16252
+ if (checkBtn) row.appendChild(checkBtn);
16353
16253
  row.appendChild(syncBtn);
16354
16254
  row.appendChild(note);
16355
16255
  return row;
@@ -16881,7 +16781,7 @@ ${story_1.splitByNameLinks.toString()}
16881
16781
  // 🔴 **追加の欄は URL の入力と[追加]だけ(c-07 の 2 本目)。** 手順の文は置かない(hint を渡さない)。
16882
16782
  // 外した情報の行き先:
16883
16783
  // - 「起票先は変わりません」「毎晩同期します」「外すには」… 上部の説明文(openCapabilityModal)
16884
- // - 「スペースごとに別のAPIキー」… 足した行の一手「◯◯ の API キーを入れてください」と、「その他」のスペースごとの行
16784
+ // - 「スペースごとに別のAPIキー」… 足した行の中のスペースごとの鍵の欄(札つき)
16885
16785
  // - 「専用 URL の登録が要ります」… 追加した行のリアルタイム札(未受信)と、一覧の下の一手[設定する]
16886
16786
  build: function (url) {
16887
16787
  return { op: "backlog-target-add", url: url, baseGeneration: targetsGenerationOf(targets) };
@@ -16940,21 +16840,6 @@ ${story_1.splitByNameLinks.toString()}
16940
16840
  box.appendChild(rtBox);
16941
16841
 
16942
16842
  var routeWrap = backlogRouteSwitch();
16943
- var keyRows = liveList("backlog-key-rows", function () {
16944
- var ds = backlogDomainsNow();
16945
- return JSON.stringify(ds.map(function (d) { return [d, secretStateOf("backlog", d, ds.length).known]; }));
16946
- }, function (b) {
16947
- var ds = backlogDomainsNow();
16948
- ds.forEach(function (d) {
16949
- var st = secretStateOf("backlog", d, ds.length);
16950
- var word = st.known === "yes" ? "登録済みです" : st.known === "no" ? "未登録です" : "確認中です";
16951
- // ボタンはそのスペースの鍵カードを開く(受け皿が無ければ作らない)
16952
- b.appendChild(el("p", { class: "ds-more-key" }, [
16953
- el("span", { text: d + ":API キーは" + word }),
16954
- backlogKeyJump(d, st.known === "no" ? "入れる" : "入れ直す"),
16955
- ]));
16956
- });
16957
- });
16958
16843
  var rtMore = liveList("backlog-realtime-more", function () {
16959
16844
  var st = backlogRealtimeState();
16960
16845
  return JSON.stringify([st ? [st.total, st.live] : null, backlogFactsSigOf()]);
@@ -16970,7 +16855,6 @@ ${story_1.splitByNameLinks.toString()}
16970
16855
  moreEl = el("details", { class: "ds-more" }, [
16971
16856
  el("summary", { text: "その他" }),
16972
16857
  routeWrap,
16973
- keyRows,
16974
16858
  rtMore,
16975
16859
  ]);
16976
16860
  box.appendChild(moreEl);
@@ -16984,7 +16868,6 @@ ${story_1.splitByNameLinks.toString()}
16984
16868
  moreEl.hidden = !(keyTargetCount("課題管理") > 0 || LIVE_BACKLOG_ROUTE === "fixed-ip");
16985
16869
  rtBox.__rebuild();
16986
16870
  if (routeWrap.__live) routeWrap.__live.__rebuild();
16987
- keyRows.__rebuild();
16988
16871
  rtMore.__rebuild();
16989
16872
  }
16990
16873
  }
@@ -17328,6 +17211,7 @@ ${story_1.splitByNameLinks.toString()}
17328
17211
  // サーバが削除を認めた印なので、ここから先に同じ鍵が現れたら**貼り直された別の登録**
17329
17212
  var figKeys = figs.map(function (f) { return f.key; });
17330
17213
  figsRemoved = figsRemoved.filter(function (k) { return figKeys.indexOf(k) >= 0; });
17214
+ FIGMA_UI.keys = figKeys.filter(function (k) { return figsRemoved.indexOf(k) < 0; });
17331
17215
  // **残り件数は「押した時点」で数える。** 一覧(figs)は夜間バッチのスナップショットで、
17332
17216
  // このページを開いている間はもちろん、その日のうちに再読み込みしても更新されない。
17333
17217
  // 描画時点の件数で判定すると、3件を1件ずつ外していったとき最後の1件でも警告が出ない。
@@ -19585,6 +19469,8 @@ ${story_1.splitByNameLinks.toString()}
19585
19469
  "figma": {
19586
19470
  icon: "🎨",
19587
19471
  title: "Figma のトークン",
19472
+ // 欄の案内(placeholder)。窓の他の文言・発行のしかたと同じ語(便 KU)
19473
+ paste: "アクセストークンを貼り付け",
19588
19474
  // **必要なスコープは実装が叩くAPIから決まる。** sync-designs は
19589
19475
  // GET /v1/files/{key} → file_content:read(画面インベントリ)
19590
19476
  // GET /v1/files/{key}/comments → file_comments:read(指摘の同期)
@@ -19674,6 +19560,8 @@ ${story_1.splitByNameLinks.toString()}
19674
19560
  "backlog": {
19675
19561
  icon: "📋",
19676
19562
  title: "Backlog のAPIキー",
19563
+ // 欄の案内(placeholder)。窓の他の文言・発行のしかたと同じ語(便 KU)
19564
+ paste: "API キーを貼り付け",
19677
19565
  how: "Backlog → 個人設定 → API で発行(同期先の Backlog プロジェクトが見えるアカウントで)",
19678
19566
  // ⚠ **desc は足さない。** 「毎晩同期するために使います」の 1 行は「カードが長すぎる」で
19679
19567
  // 外した経緯があり、secret-card-guidance.test.mjs が空の段落ごと見張っている。
@@ -19875,9 +19763,8 @@ ${story_1.splitByNameLinks.toString()}
19875
19763
  var stateSpace = (opts && opts.stateSpace) || "";
19876
19764
  var spaceCount = (opts && opts.spaceCount) || 0;
19877
19765
  var inRow = !!(opts && opts.inRow);
19878
- // 🔴 **Backlog の行の中のカードは新しい見え方(c-07 の 2 本目)。** 見出しを持たず(行の中なので「何の鍵か」は
19879
- // 一手の文が言う)、ふだんは隠れていて、キーが無いと分かったとき・[入れる]/[入れ直す]を押したとき・
19880
- // 触り始めた後だけ見える(syncKeyCardVisibility)。旧カード(行の外・intake 無しの案件)と Figma は従来どおり。
19766
+ // 🔴 **Backlog の行の中のカードは見出しを持たない**(行の中なので「何の鍵か」は札と欄が言う)。
19767
+ // 接続先が 1 件以上あるあいだは常に見える(0 件のときだけ syncKeyCardVisibility が畳む)。
19881
19768
  var compact = kind === "backlog" && inRow;
19882
19769
  /**
19883
19770
  * 鍵がどこまで効くのか(#229)。**置き場所だけでは伝わらないので、言葉にする。**
@@ -19892,11 +19779,9 @@ ${story_1.splitByNameLinks.toString()}
19892
19779
  // **いま登録されているか**を先に出す。入れ直したのに反映されていない状態が
19893
19780
  // 画面で分かるようにする(接続状況は夜間バッチのスナップショットなので、
19894
19781
  // 保存の直後は前日の値のまま。時刻は上の節に出している)。
19895
- var st = secretStateOf(kind, stateSpace, spaceCount);
19896
19782
  var cardTitle = meta.title;
19897
- // 🔴 **「登録済み」は「読めている」を意味しない。** 鍵が入っていても、そのアカウントから
19898
- // プロジェクトが見えなければ同期は失敗する。**「接続OK」「同期OK」とは書かないこと**
19899
- //(同期できているかのバッジは、夜間処理が接続先ごとの成否を保存してからの別作業)。
19783
+ // 🔴 **「登録済み」は「読めている」を意味しない。** 「接続OK」を札に出してよいのは、そのスペース(Figma は全ファイル)の
19784
+ // 行の「届くか」がすべて ok のときだけ(statusOf)。測っていなければ「登録済み」止まり。
19900
19785
  //
19901
19786
  // **前方一致で見る。** #105 の移行期は「Secrets Manager(旧名)」も来る。
19902
19787
  // 完全一致にすると、旧名で動いている案件だけが repo secret と同じ見た目になり、
@@ -19907,27 +19792,146 @@ ${story_1.splitByNameLinks.toString()}
19907
19792
  // 同じ 1 語にし、**生の値は data-src に置く**(判定の is-managed は生の値のまま)。
19908
19793
  // 知らない値が来ても生で出さない——project-wording.test.mjs が画面の語を見張る
19909
19794
  var state = el("p", { class: "sec-state" });
19910
- applySecretState(state, st);
19911
-
19912
- var input = el("input", { class: "sec-input", type: "password", placeholder: "トークンを貼り付け", autocomplete: "off" });
19795
+ // 🔴 **鍵の窓は 1 か所・欄は最初から見えている・ボタンは 1 つ(便 KU)。** 以前は[入れる]で開く畳み+[保存]+別の[接続を確認]の 3 段で、
19796
+ // 同じ鍵の[入れる]が行の一手・「その他」・畳みの 3 か所にあった。いまは 状態の札 → 伏せ字の欄(空のまま)→ [保存して接続を確認]。
19797
+ // 保存した値は読み戻さない(書き込み専用)。登録済みでも欄は空で、入れ直すときだけ貼る(伏せ字を欄に入れて伏せ字ごと保存する事故を避ける)
19798
+ var cap = kind === "backlog" ? "issues" : "design";
19799
+ var input = el("input", { class: "sec-input", type: "password", placeholder: meta.paste || "トークンを貼り付け", autocomplete: "off", spellcheck: "false" });
19913
19800
  var btn = el("button", { class: "ds-toggle-btn", type: "button", text: "保存" });
19914
19801
  var msg = el("div", { class: "ds-toggle-msg" });
19802
+ var savedNote = el("p", { class: "sec-note sec-saved" });
19803
+ // 札が読む材料の控え。checking=保存〜確認の最中、savedHere=この画面で保存した(正本 secretsNow は最大 10 秒古い答えを返すので、registered を返すまで「登録済み」以上に固定する)
19804
+ var ent = { kind: kind, el: null, compact: compact, space: stateSpace, input: input, msg: msg, checking: false, savedHere: false };
19805
+
19806
+ function spaceCountNow() { return kind === "backlog" ? backlogDomainsNow().length : spaceCount; }
19807
+ /** この鍵が効く接続先の ref(Backlog=そのスペースの行・Figma=一覧のファイル)。登録済みの一覧だけを引く=孤児の行を混ぜない */
19808
+ function refsNow() {
19809
+ var out = [];
19810
+ if (kind === "backlog") {
19811
+ var ds = backlogDomainsNow();
19812
+ var dom = stateSpace || (ds.length === 1 ? ds[0] : "");
19813
+ var rows = BACKLOG_UI.rows || [];
19814
+ for (var i = 0; i < rows.length; i++) {
19815
+ if (dom && String(rows[i].domain || "").toLowerCase() === dom) out.push(issuesRefOf(rows[i].domain, rows[i].projectKey));
19816
+ }
19817
+ } else {
19818
+ out = (FIGMA_UI.keys || []).slice();
19819
+ }
19820
+ return out;
19821
+ }
19822
+ /** 接続の確認の口があるか。組み直した案件(表)も同じ口(受付 op=probe の direct・同期を起こさずその場で 1 本叩く)で確かめる */
19823
+ function probeOk() { return !!SITE.intake && !!dispatchProbeTypeOf(cap); }
19824
+ /** 「M/D に保存」。受付の savedAt(Secrets Manager の LastChangedDate)から出す。ブラウザには覚えない(端末が変わると消えるため) */
19825
+ function savedDate(st) {
19826
+ var d = st && st.savedAt ? new Date(st.savedAt) : null;
19827
+ return d && !isNaN(d.getTime()) ? (d.getMonth() + 1) + "/" + d.getDate() : "";
19828
+ }
19829
+ function secretNow() {
19830
+ var s = secretStateOf(kind, stateSpace, spaceCountNow());
19831
+ if (ent.savedHere) {
19832
+ if (s.known === "yes") ent.savedHere = false; // 正本が追いついた
19833
+ else return { known: "yes", src: "" };
19834
+ }
19835
+ return s;
19836
+ }
19837
+ /**
19838
+ * 札 1 つの決め方。**未登録/確認中/接続OK〈時刻〉/失敗〈理由の一言〉/登録済み**。
19839
+ * 「失敗」の一言は行の札の語(dsGateForCap)をそのまま使う。測っていない行・鍵が無かった頃の古い行(no_token)は数えず、
19840
+ * 一部だけ測れているときも「接続OK」と言い切らない(登録済みに倒す)。
19841
+ */
19842
+ function statusOf(st) {
19843
+ if (ent.checking) return ["is-checking", "確認中"];
19844
+ if (st.known === "no") return ["is-none", "未登録"];
19845
+ if (st.known !== "yes") return ["is-unknown", "確認中"];
19846
+ var refs = refsNow(), bad = null, ok = 0, latest = "";
19847
+ for (var i = 0; i < refs.length; i++) {
19848
+ var r = reachableStateOf(cap, refs[i]);
19849
+ if (!r || r.status === "no_token") continue;
19850
+ if (r.status === "ok") { ok++; if (String(r.at || "") > latest) latest = String(r.at || ""); }
19851
+ else if (!bad) bad = r;
19852
+ }
19853
+ if (bad) return ["is-failed", "失敗(" + dsGateForCap(cap, bad.status).text + ")"];
19854
+ if (ok && ok === refs.length) {
19855
+ var t = dsFmtTime(latest);
19856
+ return ["is-ok", "接続OK" + (t ? "(" + t + ")" : "")];
19857
+ }
19858
+ return null; // 登録済み
19859
+ }
19860
+ function paint() {
19861
+ var st = secretNow();
19862
+ var s = statusOf(st);
19863
+ if (s) {
19864
+ // 取得元は札の語に関わらず data-src に置く(判定の is-managed は生の値の前方一致・#841)
19865
+ var srcRaw = st.known === "yes" ? (st.src || "") : "";
19866
+ state.className = "sec-state " + s[0] + (srcRaw.indexOf("Secrets Manager") === 0 ? " is-managed" : "");
19867
+ if (srcRaw) state.setAttribute("data-src", srcRaw); else state.removeAttribute("data-src");
19868
+ state.textContent = s[1];
19869
+ } else {
19870
+ applySecretState(state, st);
19871
+ }
19872
+ var reg = st.known === "yes";
19873
+ var d = savedDate(st);
19874
+ savedNote.hidden = !reg;
19875
+ savedNote.textContent = reg ? "登録済み" + (d ? "(" + d + " に保存)" : "") + "。入れ直すときだけ貼ってください" : "";
19876
+ if (!btn.disabled) btn.textContent = probeOk() ? "保存して接続を確認" : "保存";
19877
+ if (preferred) {
19878
+ var want = st.known === "no";
19879
+ if (want && !preferred.parentNode && card) card.insertBefore(preferred, input);
19880
+ else if (!want && preferred.parentNode) preferred.parentNode.removeChild(preferred);
19881
+ }
19882
+ }
19883
+ /** 保存のあとの接続の確認。結果は行と同じ形で置く(applyProbeResult)。返すのは添える 1 文(無ければ空) */
19884
+ var ipReason = false;
19885
+ async function checkAfterSave() {
19886
+ ipReason = false;
19887
+ var type = dispatchProbeTypeOf(cap);
19888
+ // Figma は回数制限(1 分あたりの枠)に収まるよう先頭の 2 ファイルだけ。残りは各行の[接続を確認]
19889
+ var refs = refsNow().slice(0, kind === "figma" ? 2 : 20);
19890
+ for (var i = 0; i < refs.length; i++) {
19891
+ // 無応答で札が「確認中」のまま固まらないよう、1 本ごとに 15 秒で見切る
19892
+ var ctrl = typeof AbortController === "function" ? new AbortController() : null;
19893
+ var giveUp = ctrl ? setTimeout(function () { ctrl.abort(); }, 15000) : null;
19894
+ try {
19895
+ var res = await callIntake("probe", { type: type, ref: refs[i] }, ctrl ? { signal: ctrl.signal } : undefined);
19896
+ var j = await res.json().catch(function () { return {}; });
19897
+ if (giveUp != null) clearTimeout(giveUp);
19898
+ if (res.ok && j.gate) {
19899
+ applyProbeResult(cap, refs[i], "", { gate: j.gate, checkedAt: j.checkedAt, needed: "", detail: j.detail || "" });
19900
+ } else if (!res.ok) {
19901
+ return "接続の確認はできませんでした:" + ((j && j.message) || "少し待ってからもう一度お試しください。");
19902
+ } else if (j.reason === "ip-restricted") {
19903
+ // 原因が分かる応答は、固定文ではなく原因の文を出す。[切り替える]は呼び出し側が添える
19904
+ ipReason = true;
19905
+ return j.message || "この場では確認できませんでした(IP 制限の可能性)。";
19906
+ } else {
19907
+ return "接続の確認は少し後に札へ出ます。";
19908
+ }
19909
+ } catch (e) {
19910
+ if (giveUp != null) clearTimeout(giveUp);
19911
+ return "接続の確認は通信に失敗しました。";
19912
+ }
19913
+ }
19914
+ return "";
19915
+ }
19915
19916
 
19916
19917
  btn.addEventListener("click", async function () {
19917
19918
  var value = input.value;
19918
- if (!value.trim()) { msg.className = "ds-toggle-msg is-error"; msg.textContent = "⚠ トークンを入力してください。"; return; }
19919
+ if (!value.trim()) {
19920
+ msg.className = "ds-toggle-msg";
19921
+ // 登録済みで空のまま押しても何も変えない(置き換えるのは、貼ったときだけ)
19922
+ msg.textContent = secretNow().known === "yes" ? "何も変えていません。入れ直すときだけ値を貼ってください。" : "";
19923
+ if (secretNow().known !== "yes") { msg.className = "ds-toggle-msg is-error"; msg.textContent = "⚠ トークンを入力してください。"; }
19924
+ return;
19925
+ }
19919
19926
 
19920
- // 押している間はボタンごと「処理中…」にする(#222・beginBusy の項)
19927
+ // 保存〜確認の全体を 1 回の busy で囲む(#222・beginBusy の項)
19921
19928
  var endBusy = beginBusy(btn);
19922
19929
  msg.className = "ds-toggle-msg";
19923
19930
  msg.textContent = "保存中…";
19924
19931
  var intake = SITE.intake;
19925
19932
  try {
19926
19933
  // **シークレット名は送らない。** どの連携かだけを送り、書き込み先はサーバが案件から決める。
19927
- // **Backlog はスペースだけ名乗る(#138)。** 名前を組み立てるのはサーバで、
19928
- // こちらが渡すのは「どのスペースの鍵か」という材料だけ。
19929
- // **押した時点で読む。** 組み立て時に固めると、接続先を登録した直後に
19930
- // 貼った鍵が「スペースを特定できません」で断られる
19934
+ // **押した時点で読む。** 組み立て時に固めると、接続先を登録した直後に貼った鍵が「スペースを特定できません」で断られる
19931
19935
  var res = await callIntake("secret", secretPayload(intake, kind, value, cardSpace));
19932
19936
  var j = await res.json().catch(function () { return {}; });
19933
19937
  if (!res.ok) {
@@ -19937,30 +19941,39 @@ ${story_1.splitByNameLinks.toString()}
19937
19941
  return;
19938
19942
  }
19939
19943
  msg.className = "ds-toggle-msg is-ok";
19940
- // 🔴 **「保存中…」を消してから成功を書く(#220)。** ここが抜けていて、実機の
19941
- // 画面に「保存中…✓ 保存しました。同期を開始しました。」と**2つの状態が同居**して
19942
- // いた。appendChild は足すだけなので、消す一文が要る(sendConfig 側には在った)。
19943
- // **語を書く前に必ずここを通すこと**——次に文面を足す人が同じ形で踏む
19944
+ // 🔴 **「保存中…」を消してから成功を書く(#220)。** appendChild は足すだけなので、消す一文が要る
19944
19945
  msg.textContent = "";
19945
19946
  msg.appendChild(document.createTextNode("✓ " + (j.message || "保存しました。")));
19946
- // 起動できたときだけ、実行状況への導線を出す(鍵を入れた直後に効いたか確かめられる)
19947
19947
  if (j.runsUrl) {
19948
19948
  msg.appendChild(document.createTextNode(" "));
19949
19949
  msg.appendChild(el("a", { href: j.runUrl || j.runsUrl, target: "_blank", rel: "noopener", title: "GitHub Actions の実行ログを開きます(プロジェクトリポジトリへのアクセスが要ります)", text: "実行状況を見る →" })); // その run を特定できていればそれ、無ければ一覧
19950
19950
  }
19951
19951
  // 入れた値は画面に残さない(肩越しに見えるのを防ぐ)
19952
19952
  input.value = "";
19953
- endBusy();
19954
- // 🔴 **Backlog の行の中のカードは、保存したら行の一手を当て直す**(「API キーを入れてください」を
19955
- // 登録状況の正本に合わせて消す・c-07 の 2 本目)。正本(secretsNow)は取り直しで届く
19956
- if (compact) {
19957
- if (typeof BACKLOG_UI.repaintRows === "function") BACKLOG_UI.repaintRows();
19958
- loadLiveFleet(true);
19953
+ ent.savedHere = true;
19954
+ var willCheck = probeOk() && refsNow().length > 0;
19955
+ ent.checking = willCheck;
19956
+ paint();
19957
+ // 🔴 Backlog の行の一手は登録状況の正本に合わせて当て直す。正本(secretsNow)は取り直しで届く
19958
+ if (compact && typeof BACKLOG_UI.repaintRows === "function") BACKLOG_UI.repaintRows();
19959
+ if (willCheck) {
19960
+ var note = await checkAfterSave();
19961
+ if (note) msg.appendChild(document.createTextNode(" " + note));
19962
+ if (ipReason) appendBacklogProbeSwitch(msg, cap, { reason: "ip-restricted" });
19959
19963
  }
19964
+ ent.checking = false;
19965
+ endBusy();
19966
+ paint();
19967
+ loadLiveFleet(true);
19960
19968
  } catch (e) {
19961
- msg.className = "ds-toggle-msg is-error";
19962
- msg.textContent = "✗ 通信に失敗しました。";
19969
+ ent.checking = false;
19970
+ // 保存が済んだあとの不調を「通信に失敗」と言わない(貼り直しに誘導しない)
19971
+ if (!ent.savedHere) {
19972
+ msg.className = "ds-toggle-msg is-error";
19973
+ msg.textContent = "✗ 通信に失敗しました。";
19974
+ }
19963
19975
  endBusy();
19976
+ paint();
19964
19977
  }
19965
19978
  });
19966
19979
 
@@ -19972,21 +19985,17 @@ ${story_1.splitByNameLinks.toString()}
19972
19985
  meta.note ? el("p", { class: "sec-how", text: meta.note }) : null,
19973
19986
  meta.selfServe ? el("p", { class: "sec-how", text: meta.selfServe.note }) : null,
19974
19987
  ].filter(Boolean);
19975
- // **「畳んである中に何があるか」を summary に書く。** ただの「詳細」だと、
19976
- // 発行のしかたを探している人がここを開く手掛かりを持てない(rt カードと同じ流儀)
19988
+ // **「畳んである中に何があるか」を summary に書く。**
19977
19989
  var guidanceBox = el("details", { class: "sec-fold" }, [el("summary", { text: "発行のしかた" })]);
19978
19990
  for (var gi = 0; gi < guidance.length; gi++) guidanceBox.appendChild(guidance[gi]);
19979
- // **手順の動画+画像(#547・#1353)。** 文章(guidance)の下に足す。
19980
- // meta.guide を持つ種別(Figma)はカルーセル1つに乗せる。meta.guide も meta.shots も
19981
- // 持たない種別(Backlog)ではこの節は素通りする
19991
+ // **手順の動画+画像(#547・#1353)。** meta.guide を持つ種別(Figma)はカルーセル1つに乗せる
19982
19992
  if (meta.guide && meta.guideInModal) {
19983
19993
  // モーダルの上に出している種別(Backlog・Figma)は、ここには重ねない
19984
19994
  } else if (meta.guide) {
19985
19995
  var carousel = buildGuideCarousel(kind, meta.guide);
19986
19996
  if (carousel) guidanceBox.appendChild(carousel);
19987
19997
  } else if (meta.shots) {
19988
- // 後方互換(旧形)。今のところ参照するエントリは無い——将来 Google Chat/Meet を
19989
- // この口に載せ替えるまでの踏み板として残す
19998
+ // 後方互換(旧形)
19990
19999
  for (var si = 0; si < meta.shots.length; si++) {
19991
20000
  var shot = meta.shots[si];
19992
20001
  guidanceBox.appendChild(el("p", { class: "sec-how", text: shot.heading }));
@@ -19994,86 +20003,33 @@ ${story_1.splitByNameLinks.toString()}
19994
20003
  }
19995
20004
  }
19996
20005
 
19997
- // **押すまで入力欄を出さない(2026-08-21・オーナー指摘)。**
19998
- // 「今のAPIキーのセクション、欄があると思うんだけど…押すとフォームだけ出てくる」。
19999
- //
20000
- // 🔴 **前回とは向きが逆。** 前回は「.sec-input は畳まない」と決めていたが、
20001
- // 鍵の欄がレコードの行の下へ移り、**行に常時ぶら下がる入力欄**になったため、
20002
- // 一覧の見通しを取り戻す側へ倒した。**状態(.sec-state)は畳まない**ので、
20003
- // 「保存したのに反映されていない」は開かなくても読める。
20004
- //
20005
- // ⚠ **DOM には常に置く**(押すまで生成しない作りにしない)。ライブ差し替えは
20006
- // input のノードを直接触るし、テストも card.querySelector("input.sec-input") で掴む。
20007
- // ⚠ **<summary> は button ではない**ので、「保存」を掴む既存の取り方は変わらない。
20008
- var inputFold = el("details", { class: "sec-input-fold" }, [
20009
- // Backlog の行の中のカードは、一手の語([入れる]・[入れ直す])と揃える。Figma は「登録する」のまま
20010
- el("summary", { class: "sec-input-summary", text: st.known === "yes" ? "入れ直す" : (compact ? "入れる" : "登録する") }),
20011
- ]);
20012
- inputFold.appendChild(input);
20013
- inputFold.appendChild(el("div", { class: "ds-toggle" }, [btn, msg]));
20014
- // 🔴 **値の扱いの 1 文は、開いた中に残す**(顧客に鍵の貼り付けを求める画面でいちばん落としてはいけない説明・#101)。
20015
- // 短くした版(行の中のカード)。「他のプロジェクトには影響しません」は仕組みの事実で、利用者が選ぶものではないので外す
20016
- if (compact) inputFold.appendChild(el("p", { class: "sec-note", text: "入れた値は保存だけでき、あとから表示されません。" }));
20017
-
20018
- // ライブ取得後に状態の行だけを差し替えるための目印(カードごと作り直すと入力が消える)
20019
- //
20020
- // **並びは「状態 → 貼る(畳む) → 畳んだ手順」。**
20021
- //
20022
- // 🔴 **.sec-state / .sec-preferred は <details> の外に置くこと。**
20023
- // - .sec-state … 保存したのに反映されていない状態が分かる唯一の手段。
20024
- // 押すまで見えないと成立しない
20025
- // - .sec-preferred … 「誰のアカウントで発行するのか」を**手順より先に**伝える行。
20026
- // 未登録のときだけ出る=これから貼る人にだけ出るので、畳むと存在理由が消える
20006
+ // 🔴 **.sec-state / .sec-preferred は常に見える場所に置く。** 札は「保存したのに反映されていない」が分かる唯一の手段。
20007
+ // .sec-preferred は「誰のアカウントで発行するのか」を手順より先に伝える行(未登録のときだけ出る)
20027
20008
  var preferred = meta.selfServe ? el("p", { class: "sec-preferred", text: meta.selfServe.text }) : null;
20028
20009
  var card = el("div", { class: "ops-card" + (inRow ? " ds-keycard" : ""), "data-secret-kind": kind }, [
20029
- // 行の中の Backlog カードは見出しを持たない(何の鍵かは一手の文が言う)
20010
+ // 行の中の Backlog カードは見出しを持たない(何の鍵かは札と欄が言う)
20030
20011
  compact ? null : el("div", { class: "ops-card-head" }, [
20031
20012
  el("span", { class: "ops-card-icon", text: meta.icon }),
20032
20013
  el("h3", { class: "ops-card-title", text: cardTitle }),
20033
20014
  ]),
20034
- // **desc は無い種別がある(2026-08-21)。** el() は text をそのまま textContent へ
20035
- // 代入するので、無条件に出すと画面に文字列 undefined が出る
20036
20015
  meta.desc ? el("p", { class: "ops-card-desc", text: meta.desc }) : null,
20037
- // **鍵の効く範囲は、登録状況より先に言う。** 「登録済み」と読んだ後では
20038
- // 「どのファイルの話か」を確かめ直すことになる(#229)
20016
+ // **鍵の効く範囲は、登録状況より先に言う。** (#229)
20039
20017
  scopeNote ? el("p", { class: "sec-scope", text: scopeNote }) : null,
20040
20018
  state,
20041
- // **未登録のときだけ、先に試すべき手を出す。** 既に動いているカードに
20042
- // 「まずこれをやれ」と出すと、直す必要が無いものを触らせることになる。
20043
- // **「確認中」にも出さない**——入っているかもしれないものを触らせない
20044
- st.known === "no" ? preferred : null,
20045
- inputFold,
20019
+ input,
20020
+ el("div", { class: "ds-toggle" }, [btn, msg]),
20021
+ savedNote,
20022
+ // 🔴 **値の扱いの 1 文は常に出す**(顧客に鍵の貼り付けを求める画面でいちばん落としてはいけない説明・#101)
20023
+ el("p", { class: "sec-note", text: compact
20024
+ ? "入れた値は保存だけでき、あとから表示されません。"
20025
+ : "入れた値はここには表示されません(保存だけできます)。保存先はプロジェクトごとに分かれているので、他のプロジェクトには影響しません。" }),
20046
20026
  guidanceBox,
20047
- // **扱いの説明はカードに持たせる。** 節の見出しの下に置いていたときは、
20048
- // #101 でカードだけがモーダルへ移り、説明だけが画面から消えた。
20049
- // 顧客にトークンの貼り付けを求める画面で、貼った値がどう扱われるかの
20050
- // 説明が落ちるのは、いちばん落としてはいけない種類の欠落
20051
- // 行の中の Backlog カードは、同じ 1 文を開いた中(上の inputFold)に短くして持つ
20052
- compact ? null : el("p", { class: "sec-note", text: "入れた値はここには表示されません(保存だけできます)。保存先はプロジェクトごとに分かれているので、他のプロジェクトには影響しません。" }),
20053
20027
  ]);
20054
- // **作った鍵カードは1枚残らずここに載せる。** 出し入れ(接続先が0件のときに畳む)は
20055
- // DOM 検索ではなくこの控えで回す——カードは能力モーダルの中身で、タイルを押すまで
20056
- // 文書に入っていないため(syncKeyCardVisibility の項)。
20057
- // compact(Backlog の行の中)のカードは、見え方の規則(backlogKeyCardWanted)が要る材料を一緒に持つ:
20058
- // space=状態を引くスペース(行のドメイン。名無しの受け皿は空文字)・forced=一手・「その他」から開かれた・
20059
- // fold/input/msg=触り始めたかの判定と、入力への移動・refresh=見せる瞬間の状態の当て直し
20060
- var entry = { kind: kind, el: card, compact: compact, space: stateSpace, forced: false, fold: inputFold, input: input, msg: msg };
20061
- if (compact) {
20062
- entry.refresh = function () {
20063
- // 作ったときの答えは古いことがある(取り直しが後から届く)。見せる瞬間に状態の行と summary と
20064
- // 「自分で繋げばよい」の 1 行を当て直す(入れた直後に前の答えのまま出さない・#227)
20065
- var now = secretStateOf(kind, stateSpace, backlogDomainsNow().length);
20066
- applySecretState(state, now);
20067
- var sum = inputFold.querySelector(".sec-input-summary");
20068
- if (sum) sum.textContent = now.known === "yes" ? "入れ直す" : "入れる";
20069
- if (preferred) {
20070
- var want = now.known === "no";
20071
- if (want && !preferred.parentNode) card.insertBefore(preferred, inputFold);
20072
- else if (!want && preferred.parentNode) preferred.parentNode.removeChild(preferred);
20073
- }
20074
- };
20075
- }
20076
- KEY_CARDS.push(entry);
20028
+ ent.el = card;
20029
+ ent.refresh = paint;
20030
+ paint();
20031
+ // **作った鍵カードは1枚残らずここに載せる。** 出し入れ(接続先が0件のときに畳む)は DOM 検索ではなくこの控えで回す
20032
+ KEY_CARDS.push(ent);
20077
20033
  return card;
20078
20034
  }
20079
20035
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.214",
3
+ "version": "0.12.217",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,