@takagaki/cortex-decisions-viewer 0.12.45 → 0.12.47

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/build.js CHANGED
@@ -538,7 +538,11 @@ async function readFleetSources(repoRoot) {
538
538
  .filter((x) => TOOL_KINDS.indexOf(x.kind) >= 0 && TOOL_STATES.indexOf(x.state) >= 0);
539
539
  // **items が 0 件でも器は通す。** 「まだ 1 つも接続が無い」(接続の導線を出す)と
540
540
  // 「この項目を知らない古い生成物」(何も出さない)は別の状態
541
- tools = { version: 1, items: items, paragraph: String(t.paragraph ?? "") };
541
+ //
542
+ // 🔴 **`paragraph` は受け渡さない(#501 で #275 の一部を取り消した)。** engine は
543
+ // 「この案件が使っているツール: …」の段落を載せ続けているが、AI の起動文には
544
+ // 付けないと決めたので、画面データにも運ばない(知らないキーとして落ちる)
545
+ tools = { version: 1, items: items };
542
546
  }
543
547
  }
544
548
  // 能力ごとの導出済みの状態(#125①が載せ、#125②で画面の正本になった)。
package/dist/render.js CHANGED
@@ -409,15 +409,14 @@ function projectRepoSlug(repoBaseUrl) {
409
409
  * 「この案件を Claude Code Web で開く」URL。
410
410
  *
411
411
  * 🔴 **AI 導線はこの1本を呼ぶ(`claudeCodeUrl` の呼び出しを写さない)。** 呼び出し側で
412
- * 組み直すと、引数を1つ落とした側だけが**開発リポジトリを載せない/ツールの段落を
413
- * 付けない**——「同じ先」が静かに割れる。指示文・slug の導出・URL の組み立ての3つを、
412
+ * 組み直すと、引数を1つ落とした側だけが**開発リポジトリを載せない**——「同じ先」が
413
+ * 静かに割れる。指示文・slug の導出・URL の組み立ての3つを、
414
414
  * ここと ASK_PROMPT / projectRepoSlug に閉じておく。
415
415
  *
416
416
  * `?? []` は必須——build.ts は fleet-status.json が無い案件で externalSources を持たない。
417
- * 第4引数は使っているツールの段落(#275・全導線に同じ段落を付ける)。
418
417
  */
419
418
  function askAiUrl(site) {
420
- return claudeCodeUrl(ASK_PROMPT, projectRepoSlug(site.repoBaseUrl), site.externalSources ?? [], site.tools && site.tools.paragraph);
419
+ return claudeCodeUrl(ASK_PROMPT, projectRepoSlug(site.repoBaseUrl), site.externalSources ?? []);
421
420
  }
422
421
  /**
423
422
  * owner/repo の形か(ポータル web/src/features/harness/harness-link.ts の REPOSITORY_SLUG と同じ形)。
@@ -496,18 +495,16 @@ function claudeCodeRepositories(projectRepo, devRefs) {
496
495
  * repositories はカンマ区切りで**素のまま**載せる(公式の例がその形で、スラッシュも
497
496
  * エンコードしない)。指示文だけ encodeURIComponent する。
498
497
  *
499
- * `toolsParagraph`(#275)は「この案件が使っているツール: …」の 1 段落。非空なら
500
- * **指示文の先頭に付ける**。7 導線すべてがこの 1 関数を通るので、ここに置くだけで
501
- * どのボタンから開いても AI が最初に案件の道具立てを知る——**呼び出し側で足さないこと**
502
- * (足すと、足し忘れたボタンだけ文脈の無い起動になる)。
503
- * `data-ai-prompt` には元の指示文を残す(貼り替えもこの関数を通るので段落は保たれる)。
498
+ * 🔴 **指示文には前置きを足さない(#501・#275 の取り消し)。** かつては
499
+ * 「この案件が使っているツール: …」の 1 段落をここで先頭に付けていたが、オーナー判断で
500
+ * やめた。`href` の指示文と貼り替えの目印(`data-ai-prompt`)は**同じ文字列**に保つ
501
+ * ——ここで足すと、貼り替えのたびに前置きが増えるかどうかを両側で気にすることになる。
504
502
  */
505
- function claudeCodeUrl(prompt, projectRepo, externals, toolsParagraph) {
503
+ function claudeCodeUrl(prompt, projectRepo, externals) {
506
504
  var repos = claudeCodeRepositories(projectRepo, devRepositoryRefs(externals));
507
505
  if (!repos.length)
508
506
  return CLAUDE_CODE_HOME;
509
- var text = toolsParagraph ? String(toolsParagraph) + "\n\n" + prompt : prompt;
510
- return "https://claude.ai/code?repositories=" + repos.join(",") + "&prompt=" + encodeURIComponent(text);
507
+ return "https://claude.ai/code?repositories=" + repos.join(",") + "&prompt=" + encodeURIComponent(prompt);
511
508
  }
512
509
  // ---------------------------------------------------------------------------
513
510
  // Home の「使用ツール」(engine #275)— 一覧は engine が登録簿から算出した `SITE.tools`
@@ -3692,6 +3689,19 @@ a.ds-tile:hover { text-decoration: none; }
3692
3689
  .ds-toggle-msg { font-size: 12px; color: var(--muted); }
3693
3690
  .ds-toggle-msg.is-ok { color: #1a7f37; }
3694
3691
  .ds-toggle-msg.is-error { color: #b42318; }
3692
+ /* Backlog の同期経路のスイッチ(#516)。role=switch の button に、トラックとつまみを描く。
3693
+ 語(.ds-switch-label)は押している間も差し替えない(状態は下の 1 行が言う) */
3694
+ .ds-route { margin-top: 10px; }
3695
+ .ds-switch { display: inline-flex; align-items: center; gap: 8px; padding: 0; border: 0; background: none; font: inherit; font-size: 13px; color: var(--text); cursor: pointer; text-align: left; }
3696
+ .ds-switch:disabled { opacity: .55; cursor: default; }
3697
+ .ds-switch.is-busy { cursor: wait; opacity: .75; }
3698
+ .ds-switch-track { position: relative; flex: 0 0 auto; width: 34px; height: 20px; border-radius: 999px; background: #c9ced6; transition: background .15s; }
3699
+ .ds-switch[aria-checked="true"] .ds-switch-track { background: var(--accent); }
3700
+ .ds-switch-knob { position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.25); transition: left .15s; }
3701
+ .ds-switch[aria-checked="true"] .ds-switch-knob { left: 16px; }
3702
+ .ds-switch:focus-visible .ds-switch-track { outline: 2px solid var(--accent); outline-offset: 2px; }
3703
+ .ds-switch-label { min-width: 0; white-space: normal; overflow-wrap: anywhere; }
3704
+ .ds-route-line { margin: 4px 0 0; }
3695
3705
  .badge.ds-notify { background: #eff6ff; color: #1e40af; border: 1px solid #bfdbfe; }
3696
3706
  /* 読み取りを止めてあるリポジトリ(2026-08-21)。**それまではトグルの向きが示していた**ので、
3697
3707
  トグルを撤去したいま、読み取られていないことを示すのはこのバッジだけ。
@@ -4706,10 +4716,8 @@ ${calendarAddUrl.toString()}
4706
4716
  // 🔴 **var MERGE_KEYS の宣言より上に codeUrl を呼ぶトップレベル文を足さないこと。**
4707
4717
  // fleetVal は MERGE_KEYS を読むが、var の初期化は宣言行に達するまで走らない
4708
4718
  // (関数宣言の巻き上げでは undefined を読む)。呼び出しは全て描画(route)以降に置く
4709
- // 第4引数は使っているツールの段落(#275)。**焼き込んだ SITE から取る**——この段落は
4710
- // engine が毎朝 fleet-status.json に載せるもので、ライブ(op=state)は運んでこない
4711
4719
  function codeUrl(prompt) {
4712
- return claudeCodeUrl(prompt, REPO_SLUG, fleetVal("externalSources") || [], SITE.tools && SITE.tools.paragraph);
4720
+ return claudeCodeUrl(prompt, REPO_SLUG, fleetVal("externalSources") || []);
4713
4721
  }
4714
4722
  /**
4715
4723
  * 🔴 **「1件も登録されていない」と「確かめられなかった」を分ける(#409)。**
@@ -7593,6 +7601,21 @@ ${calendarAddUrl.toString()}
7593
7601
  * 「いつ時点の情報か」の注記がどちらを指すのか説明できなくなる。
7594
7602
  */
7595
7603
  var LIVE_REALTIME = null;
7604
+ /**
7605
+ * Backlog の同期経路(op=state のトップレベル backlogRoute・#516)。
7606
+ *
7607
+ * 値は 4 つ: undefined=まだ答えが無い/"fixed-ip"=固定 IP 経由(正本に印がある)/
7608
+ * "runner"=通常の経路/null=正本を読めなかった(サーバが null を返した・取得に失敗した・
7609
+ * この値を知らない古い Lambda)。**null では切り替えさせない**(分からないのに書かせない)。
7610
+ * 押した直後は backlog-route-set の応答で先に更新し、次の op=state が追いつく。
7611
+ */
7612
+ var LIVE_BACKLOG_ROUTE = undefined;
7613
+ /**
7614
+ * 固定の送信元 IP(op=state の realtime.fixedEgressIp・backlog-route-set の応答)。
7615
+ * **画面に定数を持たない**——出どころは EIP の資源 → Intake の env の 1 本道で、
7616
+ * 値が変わったときに画面だけ古いままにならない。取れていなければ空文字(IP を書かない)。
7617
+ */
7618
+ var LIVE_FIXED_IP = "";
7596
7619
  /**
7597
7620
  * いま登録ファイルに何が書いてあるか(op=state の registeredNow・段階1)。取得前は null。
7598
7621
  *
@@ -8060,6 +8083,21 @@ ${calendarAddUrl.toString()}
8060
8083
  */
8061
8084
  var KEY_POINTING_CHECKS = { backlog_secrets: "課題管理", figma_token: "デザイン" };
8062
8085
 
8086
+ /**
8087
+ * 鍵の入力欄を出す種別(#516)。
8088
+ *
8089
+ * **投入の口(intake)があるなら Backlog の鍵は必ず出す。** 固定 IP 経由で同期する案件は
8090
+ * かつて CDK が secretKinds から backlog を落としていた(画面から入れても効かなかった頃の
8091
+ * 名残)。いまは固定 IP 案件も同じ棚の鍵を画面から入れられる(#503 設計 v2 §5)ので、
8092
+ * 写し(viewer-config.json)がまだ古い案件でも欄を出す。判定は接続先フォームと同じ線
8093
+ *(backlogTargetsEditable)——2 か所で違う線を引くと、フォームは出るのに鍵だけ無い案件ができる。
8094
+ */
8095
+ function secretKindsShown() {
8096
+ var kinds = (SITE.secretKinds || []).slice();
8097
+ if (SITE.intake && kinds.indexOf("backlog") < 0) kinds.push("backlog");
8098
+ return kinds;
8099
+ }
8100
+
8063
8101
  /**
8064
8102
  * 応答に添えられた接続先の一覧を採ったときの件数(Backlog の adoptTargets)。
8065
8103
  *
@@ -8416,7 +8454,7 @@ ${calendarAddUrl.toString()}
8416
8454
  // 「未対応です」と伝える場所も接続マップのアンカーの受け皿も無くなった
8417
8455
  //
8418
8456
  // 個別に足し続けると次も漏らすので、**この節に出す候補を列挙して判定する**形にする。
8419
- var hasSecrets = ((SITE.secretKinds || []).filter(function (k) { return SECRET_FORMS[k]; })).length > 0;
8457
+ var hasSecrets = (secretKindsShown().filter(function (k) { return SECRET_FORMS[k]; })).length > 0;
8420
8458
  // **fleet のデータが取れているか、だけを見る(#125②)。** 宣言(tools)は撤去した(⑤)
8421
8459
  var hasCapModel = hasCaps;
8422
8460
  // **投入の口も候補。** fleet-status.json はGitHub Actionsが夜間に生成するので、
@@ -8832,7 +8870,9 @@ ${calendarAddUrl.toString()}
8832
8870
  // **GitHub(「external-sources」)はここに無い。** 読み取りトークンは廃止し、
8833
8871
  // 読み取りは GitHub App に一本化した。**この表に戻すと入力欄が復活する**
8834
8872
  // (対応表そのものは SECRET_CAP。3つの口で共有するので上に置いてある)
8835
- (SITE.secretKinds || []).forEach(function (k) {
8873
+ // **種別の一覧は secretKindsShown()(#516)。** intake がある案件は写しの secretKinds に
8874
+ // backlog が無くても Backlog の鍵を出す(固定 IP 案件も画面から鍵を入れる)
8875
+ secretKindsShown().forEach(function (k) {
8836
8876
  if (!SECRET_FORMS[k]) return;
8837
8877
  // **意図的にOFFにした能力にだけ、入力欄を出さない(0.4.71)。** 使わないと決めたものに
8838
8878
  // 鍵を求めるのは、設定する必要が無いものに設定を促す偽の警告と同じ。
@@ -9282,7 +9322,7 @@ ${calendarAddUrl.toString()}
9282
9322
  var tokenRelevant = (derived != null ? derived !== "off" : true)
9283
9323
  && keyTargetCount(cap) > 0
9284
9324
  && !(derive && derive.keyMissing);
9285
- if (tokenRelevant && sk && (SITE.secretKinds || []).indexOf(sk) >= 0 && !secretPresentForTile(sk)) {
9325
+ if (tokenRelevant && sk && secretKindsShown().indexOf(sk) >= 0 && !secretPresentForTile(sk)) {
9286
9326
  body.appendChild(el("div", { class: "ds-tile-note", text: "⚠ トークンが未設定です" }));
9287
9327
  }
9288
9328
  // 🔴 **セットアップの残り(checks)の長文はタイルに出さない(#230・オーナー指摘)。**
@@ -10053,14 +10093,140 @@ ${calendarAddUrl.toString()}
10053
10093
  * 立ち上げ直後の案件にも実在する。ミラーで判定すると、**まだ一度も繋いでいない案件=
10054
10094
  * これから繋ぎたい案件**だけがフォームを出せない(実機で踏んだ)。
10055
10095
  *
10056
- * 出さないのは、この画面からの投入に対応していない固定IP同期の案件だけ。
10057
- * **その見分けは SITE.secretKinds に backlog が入っているかで行う。**
10058
- * 固定IPかどうかを直接示す値は画面に来ておらず(インフラ側の設定にしか無い)、
10059
- * 鍵の欄を出すかどうかの判定が同じ線で引かれているので、代理として正確に使える。
10060
- * インフラ側で secretKinds の条件を変えるときは、**ここの意味も一緒に変わる**。
10096
+ * **判定は「投入の口(intake)があるか」(#516・#503 設計 v2 §4)。** それまでは
10097
+ * SITE.secretKinds に backlog があるかで見ていた——固定 IP 経由で同期する案件は
10098
+ * 画面からの投入に対応しておらず、CDK が secretKinds から backlog を落としていたので、
10099
+ * その代理として使えた。いまは固定 IP 案件も同じ正本・同じ棚に画面から書けるので、
10100
+ * 出さない理由は「書き込む口が無い」だけになった。**secretKinds に戻さないこと**——
10101
+ * 写し(viewer-config.json)が古い案件で、接続先も鍵も画面から入れられなくなる。
10102
+ * 鍵の欄の判定(secretKindsShown)も同じ線。
10061
10103
  */
10062
10104
  function backlogTargetsEditable() {
10063
- return (SITE.secretKinds || []).indexOf("backlog") >= 0;
10105
+ return !!SITE.intake;
10106
+ }
10107
+
10108
+ /**
10109
+ * サーバが返した同期経路を 3 値に畳む(#516)。**知らない語は null(読めない)に倒す**
10110
+ * ——新しい語を「通常」と読むと、印が付いている案件でスイッチが OFF に見える。
10111
+ */
10112
+ function backlogRouteOf(v) {
10113
+ return v === "fixed-ip" || v === "runner" ? v : null;
10114
+ }
10115
+
10116
+ /** スイッチの語(4 状態で変えない。状態は下の 1 行が言う) */
10117
+ var BACKLOG_ROUTE_SWITCH_LABEL = "Backlog に IP 制限がある(固定 IP 経由で同期する)";
10118
+
10119
+ /**
10120
+ * スイッチの下の 1 行(#516・#503 設計 v2 §6 の表と v3 §6 の ON 行)。
10121
+ *
10122
+ * 症状 → 一手の 1〜2 文。**技術語(CodeBuild・Lambda・runner)を出さない。**
10123
+ * @param route LIVE_BACKLOG_ROUTE(undefined / "fixed-ip" / "runner" / null)
10124
+ * @param ip 固定の送信元 IP(無ければ空文字。**無いときは IP を書かない**——書けば嘘)
10125
+ * @param count いま画面に出ている接続先の件数
10126
+ * @param keyState Backlog の鍵の登録状況("yes" / "no" / "unknown")。**"no" のときだけ**
10127
+ * 鍵の登録を一手に出す。分からないときに「登録すると始まる」と言うと、
10128
+ * 入れてある人に入れ直させる
10129
+ */
10130
+ function backlogRouteLine(route, ip, count, keyState) {
10131
+ if (route === undefined) return "設定を確認しています…";
10132
+ if (route === null) return "設定を読み取れないため、いまは切り替えられません。";
10133
+ if (route !== "fixed-ip") {
10134
+ return count > 0
10135
+ ? "Backlog の IP 制限で同期が失敗する案件は、これを入れてください。"
10136
+ : "先に接続先を登録してください。";
10137
+ }
10138
+ var ipLabel = ip ? "固定の送信元 IP(" + ip + ")" : "固定の送信元 IP";
10139
+ if (keyState === "no") {
10140
+ return "同期は" + ipLabel + "から行います。お客様にこの IP の許可を依頼し、上の「Backlog のAPIキー」を登録すると同期が始まります。";
10141
+ }
10142
+ // 2 文に収める(実装レビューの指摘)。押した直後の帯(サーバの文面)も「許可を依頼」を言うが、
10143
+ // 帯は数秒で読み終える結果で、この行は次に開いたときも残る状態——役目が違うので両方に置く
10144
+ return "同期は" + ipLabel + "から行います。お客様にこの IP の許可を依頼してください(反映は毎晩 21:00 ごろの定期同期と、Webhook の受信から数分後です)。";
10145
+ }
10146
+
10147
+ /**
10148
+ * Backlog の同期経路の切り替えスイッチ(#516・#503 設計 v2 §6 / v3 §6・分類 A4)。
10149
+ *
10150
+ * 置き場は接続先の一覧の下・リアルタイム同期の上。部品はスイッチ 1 つ+下の 1 行+結果の帯。
10151
+ *
10152
+ * **確認ダイアログは出さない(A4)。** ON も OFF も同じスイッチで即時に戻せる(可逆)。
10153
+ * 押した記録はサーバの監査に残る。
10154
+ *
10155
+ * **押せないのは 3 つ。** 印を読めない(null)・まだ答えが無い(undefined)・OFF で接続先 0 件
10156
+ *(サーバも 400 で断る)。ON で接続先 0 件は押せる——止める方向を塞がない(T4 の契約)。
10157
+ *
10158
+ * **状態の正本は LIVE_BACKLOG_ROUTE。** 押して成功したら応答の route で先に更新し、
10159
+ * その後の op=state(sendConfig が起こす)が追いつく。**画面の側で「押したから ON」と
10160
+ * 決めない**——断られたときに向きだけ変わる形にしない。
10161
+ *
10162
+ * **結果の帯は差し替えの箱の外に置く。** 応答の文面(「お客様に IP の許可を依頼してください」)は
10163
+ * 次の一手そのもので、数秒後に op=state が届いて箱を描き直した瞬間に消えると、
10164
+ * 一番読ませたい文が読み終える前に消える(接続先の一覧の「結果は一覧の外」と同じ罠)。
10165
+ *
10166
+ * **IP の値は画面に持たない。** op=state(realtime.fixedEgressIp)と切り替えの応答
10167
+ *(fixedEgressIp)から取る。取れていなければ IP を書かない。
10168
+ */
10169
+ function backlogRouteSwitch() {
10170
+ var wrap = el("div", { class: "ds-route" });
10171
+ var msg = el("div", { class: "ds-toggle-msg" });
10172
+ var live = liveList("backlog-route", function () {
10173
+ return JSON.stringify([
10174
+ LIVE_BACKLOG_ROUTE === undefined ? "?" : LIVE_BACKLOG_ROUTE,
10175
+ LIVE_FIXED_IP,
10176
+ keyTargetCount("課題管理"),
10177
+ secretRegisteredAnywhere(LIVE_SECRETS, "backlog"),
10178
+ ]);
10179
+ }, function (box) {
10180
+ var route = LIVE_BACKLOG_ROUTE;
10181
+ var on = route === "fixed-ip";
10182
+ var count = keyTargetCount("課題管理");
10183
+ var keyState = secretRegisteredAnywhere(LIVE_SECRETS, "backlog");
10184
+ var disabled = route === undefined || route === null || (!on && count === 0);
10185
+ var btn = el("button", {
10186
+ class: "ds-switch",
10187
+ type: "button",
10188
+ role: "switch",
10189
+ "aria-checked": on ? "true" : "false",
10190
+ }, [
10191
+ el("span", { class: "ds-switch-track", "aria-hidden": "true" }, [el("span", { class: "ds-switch-knob" })]),
10192
+ el("span", { class: "ds-switch-label", text: BACKLOG_ROUTE_SWITCH_LABEL }),
10193
+ ]);
10194
+ if (disabled) btn.disabled = true;
10195
+ btn.addEventListener("click", async function () {
10196
+ if (btn.disabled) return;
10197
+ var next = !on;
10198
+ // **語は差し替えない**(beginBusy はボタンの語を「処理中…」に変える)。スイッチの語は
10199
+ // 状態を言う 1 行と対なので、押している間だけ消えると何を押したのか読めなくなる。
10200
+ // 押せない見た目にして、帯(msg)の「保存中…」で進んでいることを示す
10201
+ btn.disabled = true;
10202
+ btn.classList.add("is-busy");
10203
+ msg.className = "ds-toggle-msg";
10204
+ msg.textContent = "";
10205
+ // **onDone ではなく hooks.onSaved で受ける(実装レビューの指摘)。** onDone は
10206
+ // changed:false では流れない。正本が既に送った向きだった回(別の人・別のタブが先に
10207
+ // 切り替えていた)はサーバが changed:false・route 無しで返すので、onDone だと
10208
+ // スイッチが古い向きのまま「✓ 変更はありません」が出て、次の取り直しまで直らない。
10209
+ // onSaved は res.ok なら changed を問わず流れる(冪等な op の受け口)
10210
+ await sendConfig({ op: "backlog-route-set", route: next ? "fixed-ip" : "runner" }, msg, null, null, {
10211
+ onSaved: function (j) {
10212
+ // **応答の値を正とする。** 無ければ送った向き(サーバは断らなかった=正本はその向き)
10213
+ LIVE_BACKLOG_ROUTE = backlogRouteOf(j && j.route) || (next ? "fixed-ip" : "runner");
10214
+ if (j && j.fixedEgressIp) LIVE_FIXED_IP = String(j.fixedEgressIp);
10215
+ live.__rebuild();
10216
+ // changed:false では sendConfig が読み直しを起こさない。正本の値で画面を揃えるために自分で起こす
10217
+ if (j && j.changed === false) loadLiveFleet(true);
10218
+ },
10219
+ });
10220
+ // 断られた・通信に失敗したときはこのボタンが残っている(成功時は描き直されて外れている)
10221
+ btn.disabled = disabled;
10222
+ btn.classList.remove("is-busy");
10223
+ });
10224
+ box.appendChild(el("div", { class: "ds-toggle ds-route-row" }, [btn]));
10225
+ box.appendChild(el("p", { class: "sec-how ds-route-line", text: backlogRouteLine(route, LIVE_FIXED_IP, count, keyState) }));
10226
+ });
10227
+ wrap.appendChild(live);
10228
+ wrap.appendChild(msg);
10229
+ return wrap;
10064
10230
  }
10065
10231
 
10066
10232
  /**
@@ -10424,9 +10590,10 @@ ${calendarAddUrl.toString()}
10424
10590
  */
10425
10591
  function ensureKeySlots() {
10426
10592
  if (!editable) return;
10427
- // 受け皿を作る経路を通っていない案件(intake が無い・固定IP同期)はここも通さない
10593
+ // 受け皿を作る経路を通っていない案件(intake が無い)はここも通さない
10428
10594
  if (!KEY_SLOTS.backlog || !Object.keys(KEY_SLOTS.backlog).length) return;
10429
- if ((SITE.secretKinds || []).indexOf("backlog") < 0 || !SECRET_FORMS["backlog"]) return;
10595
+ // 種別の判定は鍵カードを作るループと同じ線(secretKindsShown・#516)
10596
+ if (secretKindsShown().indexOf("backlog") < 0 || !SECRET_FORMS["backlog"]) return;
10430
10597
  var domains = [];
10431
10598
  for (var i = 0; i < targets.length; i++) {
10432
10599
  var dm = String(targets[i].domain || "").toLowerCase();
@@ -10477,9 +10644,9 @@ ${calendarAddUrl.toString()}
10477
10644
  // 呼ぶので、fleet が接続先を持たない案件(同期ミラーのURLから1行だけ作っている案件)では
10478
10645
  // **空を返す**——行は出るのに鍵の欄が1つも付かない、という空振りになる。
10479
10646
  //
10480
- // **この画面から投入できない案件(固定IP同期)には出さない。** 押しても Lambda
10481
- // 「固定IP経由のため対応していません」で断るので、導線そのものを作らない。
10482
- // 判定は鍵カードを出すループの条件と同じ線(SITE.secretKinds に backlog があるか)。
10647
+ // **固定 IP 経由の案件にも出す(#516)。** かつては Lambda が「固定IP経由のため
10648
+ // 対応していません」で断るので導線を作らなかったが、いまは同じ棚に入る(#503 T4)。
10649
+ // 判定は鍵カードを出すループの条件と同じ線(secretKindsShown=intake があるか)。
10483
10650
  if (editable && !Object.keys(KEY_SLOTS.backlog).length) {
10484
10651
  // 保存時にスペースを名乗ってよいのは、名簿(fleet の backlogTarget)由来の行だけ。
10485
10652
  // 同期ミラーのURLから作った行で名乗ると、名簿とミラーがずれている案件で
@@ -10685,10 +10852,14 @@ ${calendarAddUrl.toString()}
10685
10852
  // 実在する。同期ミラーの有無で門前払いすると、**新規案件がこの画面から
10686
10853
  // Backlog に繋げない**——設定UIを置いている意味が無くなる
10687
10854
  box.appendChild(backlogTargetsSection(s));
10688
- if (!backlogTargetsEditable()) {
10689
- box.appendChild(el("p", { class: "sec-how", text: "この案件の Backlog は別の経路で同期しているため、この画面からは接続先を変更できません。変更が必要なときは cortex の担当者にご連絡ください。" }));
10690
- used = true;
10691
- }
10855
+ // **「この画面からは接続先を変更できません」の枝は撤去した(#516)。** ゲートが
10856
+ // 「intake があるか」になり、この関数は intake の無い案件では上で return しているので、
10857
+ // 到達しない枝だった(到達不能な枝は変異検証の網から漏れる)。
10858
+ //
10859
+ // **接続先の下・リアルタイム同期の上に、同期経路のスイッチを置く(#516・設計 v2 §6)。**
10860
+ // #497 の「固定 IP 経由で同期できます(切り替えは運用側)」の案内と画面の IP 定数は
10861
+ // これに置き換わった——切り替えは担当者が自分で押す。
10862
+ box.appendChild(backlogRouteSwitch());
10692
10863
  // **接続先の下に置く。** リアルタイム同期は「どのプロジェクトを見るか」が
10693
10864
  // 決まった後の話で、順序が逆だと、接続先を設定していない案件が先に
10694
10865
  // Webhook を貼りに行く(貼っても届かない)
@@ -10971,7 +11142,7 @@ ${calendarAddUrl.toString()}
10971
11142
  // how / note)。全文をここに移すと二重になり、片方だけ直る形になる
10972
11143
  // ——カードは接続マップとタイルの注記と条件を揃えているので、動かすと壊れやすい。
10973
11144
  hint: [
10974
- "Figma でトークンを発行します(読み取りだけ。スコープ2つ)。手順は「Figma のトークン」の欄にあります(ファイルを追加すると上に出ます)。",
11145
+ "Figma でトークンを発行します(読み取りだけ。スコープ2つ。Dev または Full 席の方が発行)。手順は「Figma のトークン」の欄にあります(ファイルを追加すると上に出ます)。",
10975
11146
  "発行したアカウントを、読み取りたい Figma ファイルに閲覧者として招待します(招待が無いと、トークンが正しくても読めません)。",
10976
11147
  "そのファイルのURLを下の欄に貼って「追加」を押します。",
10977
11148
  "追加したファイルは、画面名と Figma へのリンクが毎晩同期されます(画像は取り込まれません)。",
@@ -12386,6 +12557,10 @@ ${calendarAddUrl.toString()}
12386
12557
  // **fleet より先に読む。** fleet-status.json をまだ持たない案件(立ち上げ直後)は
12387
12558
  // 下で throw するが、リアルタイム同期の可否は索引由来なので独立に決まる
12388
12559
  LIVE_REALTIME = (j && j.realtime) || null;
12560
+ // 同期経路と固定 IP(#516)。**サーバが知らない値・返さない回は null(読めない)**
12561
+ // ——undefined のままにすると「確認しています」が永久に残る
12562
+ LIVE_BACKLOG_ROUTE = backlogRouteOf(j && j.backlogRoute);
12563
+ if (LIVE_REALTIME && LIVE_REALTIME.fixedEgressIp) LIVE_FIXED_IP = String(LIVE_REALTIME.fixedEgressIp);
12389
12564
  // 🔴 **registeredNow も fleet より先に読む。** まとめ表をまだ持たない案件
12390
12565
  // (=立ち上げ直後=この仕組みが一番効く案件)は下で throw するので、
12391
12566
  // ここより後に置くと**その案件だけ素通り**する
@@ -12436,6 +12611,9 @@ ${calendarAddUrl.toString()}
12436
12611
  liveFleetRequested = false;
12437
12612
  liveAnswered = true;
12438
12613
  MERGED = null;
12614
+ // 同期経路(#516)。**一度も答えが無いまま失敗したら「読めない」に倒す**(切り替えさせない)。
12615
+ // 前の答え(または切り替え直後の応答の値)があるなら、それを捨てない
12616
+ if (LIVE_BACKLOG_ROUTE === undefined) LIVE_BACKLOG_ROUTE = null;
12439
12617
  // 🔴 **確認中に倒した図を焼いた表示へ戻す(#256・設計v2 の3)。** ライブが
12440
12618
  // 取れなかったときのフォールバックはここに残る——戻さないと「確認中…」の
12441
12619
  // ままの図が残り、オフラインでは**何も分からない画面**になる。
@@ -12563,8 +12741,11 @@ ${calendarAddUrl.toString()}
12563
12741
  * **出すのは「手順どおり設定すれば実際に効く案件」だけ。** 判断は Lambda(op=state)が返す
12564
12742
  * realtime に委ねる——画面が持っている静的データからは「ルーティング索引に自分の行が
12565
12743
  * あるか」が分からないし、2箇所で判定すると片方だけがずれる。
12566
- * - supported: false … 固定IP経由で同期している案件。**黙って欠けさせず**理由を1行出す
12744
+ * - via: "fixed-ip" … 固定 IP 経由で同期する案件(#516・#503 設計 v3 §6)。手順は同じ。
12745
+ * 反映が「約1分」ではなく「数分」になることだけを言う
12567
12746
  * - ready: false … 索引にまだ行が無い。設定しても届かないので手順を出さない
12747
+ * 「supported: false(固定 IP は未対応)」の枝は撤去した——固定 IP 案件でも Webhook を
12748
+ * 受ける(分類 A2)。その値を返す古い Lambda に当たっても、手順を出す側に倒す
12568
12749
  *
12569
12750
  * **URLに載せるトークンは案件別**(SITE.backlogWebhook)。艦隊共通トークンを載せると、
12570
12751
  * この画面を見られる人が全案件ぶんの偽イベントを投げられる(types.ts の backlogWebhook 参照)。
@@ -12575,14 +12756,10 @@ ${calendarAddUrl.toString()}
12575
12756
  */
12576
12757
  function realtimeSyncCard() {
12577
12758
  var rt = LIVE_REALTIME;
12578
- // **未対応(固定IP経由の同期)だけは、手順ではなく理由を出す。** ここは索引の状態ではなく
12579
- // 経路の話で、設定しても効かないことが確定している唯一の形。黙って欠けさせない
12580
- if (rt && rt.supported === false) {
12581
- return el("p", {
12582
- class: "sec-how",
12583
- text: "この案件は固定IP経由でBacklogを同期しているため、リアルタイム同期(Webhook)には未対応です。反映は毎晩の同期のタイミングになります。",
12584
- });
12585
- }
12759
+ // 固定 IP 経由か(#516)。**経路の正本は op=state の realtime.via**——スイッチの状態
12760
+ //(LIVE_BACKLOG_ROUTE)から引かない。あちらは押した直後に先に変わるが、受信したイベントを
12761
+ // どちらが同期するかはサーバ側の索引が決めるので、同じ応答の値で言う
12762
+ var viaFixedIp = !!(rt && rt.via === "fixed-ip");
12586
12763
  var wh = SITE.backlogWebhook;
12587
12764
  // **URLが無い案件には出さない。** 貼るものが無い手順は、一番読めない形になる
12588
12765
  if (!wh || !wh.url || !wh.token) return null;
@@ -12654,7 +12831,11 @@ ${calendarAddUrl.toString()}
12654
12831
  msg,
12655
12832
  // **この画面は顧客も見る。** URLに秘密が入っていることと、その扱いを書いておく
12656
12833
  el("p", { class: "sec-note", text: "このURLはこの案件専用です(他の案件には届きません)。秘密のトークンを含むので、貼り付け先は Backlog のこのプロジェクトの設定欄だけにしてください。" }),
12657
- el("p", { class: "sec-how", text: "反映は受信から約1分後です(1分ごとのミニバッチでまとめて取り込みます)。初回は上の接続先の設定と、夜間同期が一度済んでいることが前提になります。" }),
12834
+ // 固定 IP 経由の案件は反映が「約1分」にならない(1分のまとめ+固定 IP の同期の起動で数分)。
12835
+ // **同じ行を差し替える**——2 行に分けると「約1分」と「数分」が並んで矛盾する
12836
+ el("p", { class: "sec-how", text: viaFixedIp
12837
+ ? "固定 IP 経由で同期します(反映は受信から数分後です)。初回は上の接続先の設定と、夜間同期が一度済んでいることが前提になります。"
12838
+ : "反映は受信から約1分後です(1分ごとのミニバッチでまとめて取り込みます)。初回は上の接続先の設定と、夜間同期が一度済んでいることが前提になります。" }),
12658
12839
  ];
12659
12840
 
12660
12841
  if (live) {
@@ -12688,7 +12869,10 @@ ${calendarAddUrl.toString()}
12688
12869
  el("span", { class: "rt-live-badge", text: "✓ 稼働中" }),
12689
12870
  ]),
12690
12871
  lines,
12691
- el("p", { class: "ops-card-desc", text: "Backlog の変更をすぐ取り込んでいます(毎晩の同期を待ちません)。設定は済んでいるので、このままお使いください。" }),
12872
+ // 固定 IP 経由の案件は「すぐ」ではなく「数分」(手順は畳んであるので、ここでも言う)
12873
+ el("p", { class: "ops-card-desc", text: viaFixedIp
12874
+ ? "Backlog の変更を固定 IP 経由で取り込んでいます(反映まで数分・毎晩の同期を待ちません)。設定は済んでいるので、このままお使いください。"
12875
+ : "Backlog の変更をすぐ取り込んでいます(毎晩の同期を待ちません)。設定は済んでいるので、このままお使いください。" }),
12692
12876
  det,
12693
12877
  ]);
12694
12878
  }
@@ -12743,7 +12927,8 @@ ${calendarAddUrl.toString()}
12743
12927
  // **スコープが正しくても、招待が無いと読めない。** トークンは「そのアカウントが
12744
12928
  // 見られるファイル」しか読めないので、招待漏れは 404 になる。権限の問題に見えないため
12745
12929
  // 現場で切り分けに時間がかかる(実際に踏んだ)。入力欄の隣に書いておく。
12746
- note: "発行したアカウントを、対象の Figma ファイルに閲覧者として招待しておいてください(招待が無いとトークンが正しくても読めません)。",
12930
+ note: "発行したアカウントを、対象の Figma ファイルに閲覧者として招待しておいてください(招待が無いとトークンが正しくても読めません)。"
12931
+ + "発行するのは、対象ファイルのあるチームで Dev または Full の席を持つ方にしてください。閲覧席(View/Collab)のトークンは取得回数が月 20 回までで、夜間同期が数日で止まります。",
12747
12932
  },
12748
12933
  "backlog": {
12749
12934
  icon: "📋",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.45",
3
+ "version": "0.12.47",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,