@takagaki/cortex-decisions-viewer 0.12.46 → 0.12.48

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 +287 -53
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -966,7 +966,7 @@ function sanitizeRegisteredNow(v) {
966
966
  * 写し忘れても赤くならなかったので、突き合わせは viewer/test/state-vocab-parity.test.mjs。
967
967
  */
968
968
  const STATE_STATUSES = {
969
- reachable: ["ok", "not_in_channel", "missing_scope", "no_token", "unreachable", "unknown", "not_in_space", "no_credentials", "no_consent", "transcripts_api_disabled", "no_access_policy", "speaker_attribution_off", "folder_not_granted"],
969
+ reachable: ["ok", "not_in_channel", "missing_scope", "no_token", "unreachable", "unknown", "not_in_space", "no_credentials", "admin_approval_pending", "no_consent", "transcripts_api_disabled", "no_access_policy", "speaker_attribution_off", "folder_not_granted"],
970
970
  ingested: ["ok", "failed", "skipped"],
971
971
  // 案件カレンダーは作れたか(tools #427)。行の描画は #428。ここに無いと、中央が報告した
972
972
  // 「作成済み」の行が overlay に届く前に落ちて、画面は永久に「作成中」のまま
@@ -1931,7 +1931,7 @@ const BAD_STATE_STATUSES = {
1931
1931
  // 案件カレンダー(tools #427)。**`requested` は中立**(依頼を受けた・作成中)で悪くない。
1932
1932
  // 入れると、押した直後の 15 分が全部⚠️になる
1933
1933
  calendar: ["failed"],
1934
- reachable: ["not_in_channel", "missing_scope", "no_token", "unreachable", "not_in_space", "no_credentials", "no_consent", "transcripts_api_disabled", "no_access_policy", "speaker_attribution_off", "folder_not_granted"],
1934
+ reachable: ["not_in_channel", "missing_scope", "no_token", "unreachable", "not_in_space", "no_credentials", "admin_approval_pending", "no_consent", "transcripts_api_disabled", "no_access_policy", "speaker_attribution_off", "folder_not_granted"],
1935
1935
  };
1936
1936
  /**
1937
1937
  * 案件カレンダーの ID の形か(#428)。
@@ -3689,6 +3689,19 @@ a.ds-tile:hover { text-decoration: none; }
3689
3689
  .ds-toggle-msg { font-size: 12px; color: var(--muted); }
3690
3690
  .ds-toggle-msg.is-ok { color: #1a7f37; }
3691
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; }
3692
3705
  .badge.ds-notify { background: #eff6ff; color: #1e40af; border: 1px solid #bfdbfe; }
3693
3706
  /* 読み取りを止めてあるリポジトリ(2026-08-21)。**それまではトグルの向きが示していた**ので、
3694
3707
  トグルを撤去したいま、読み取られていないことを示すのはこのバッジだけ。
@@ -3726,6 +3739,12 @@ a.ds-tile:hover { text-decoration: none; }
3726
3739
  (flex: 1 1 100%)——状態バッジの横に押し込むと、いちばん読ませたい一文が
3727
3740
  カード幅に負けて省略される。状態の語より弱く、しかし読める大きさで出す */
3728
3741
  .ds-gate-help { flex: 1 1 100%; margin: 4px 0 0; font-size: 12px; line-height: 1.7; color: #6b7280; }
3742
+ /* 管理者への依頼文(#521)。手順が複数行あるので details に畳む——開いたまま出すと、
3743
+ 1行のはずの案内(.ds-gate-help / .ds-probe-help)が一覧を占領する */
3744
+ .ds-gate-request { margin-top: 4px; }
3745
+ .ds-gate-request > summary { cursor: pointer; color: var(--muted); font-size: 12px; }
3746
+ .ds-gate-request[open] > summary { margin-bottom: 6px; }
3747
+ .ds-gate-request-line { margin: 2px 0; font-size: 12px; line-height: 1.7; color: #6b7280; }
3729
3748
  /* 「接続を確認」を押した結果(2026-08-19)。**必ず自分の行を占める**(flex: 1 1 100%)。
3730
3749
  夜間のスナップショットの状態バッジと**同時に2つの判定を並べない**ため、押したあとは
3731
3750
  状態バッジをこの中の判定へ置き換える(同じ画面で「⚠️」と「接続OK」が並ぶのが、
@@ -6163,6 +6182,30 @@ ${calendarAddUrl.toString()}
6163
6182
  cls: "ds-gate-muted",
6164
6183
  help: "管理者の設定が済むと読み取りが始まります。",
6165
6184
  },
6185
+ // HTTP 403(#521)。アプリはスペースに追加済みだが、**顧客の Google Workspace 管理者**が
6186
+ // Marketplace からの管理者インストールをまだ済ませていない。not_in_space(スペースの
6187
+ // メンバーが直せる)にも no_credentials(艦隊側の鍵が無い=顧客に打つ手が無い)にも
6188
+ // 寄せない——直せるのが「顧客の管理者」という、どちらとも別の人・別の操作だから。
6189
+ // **機械語(permission_denied)は出さない**(#521 で分類できたので、もう「不明」ではない)。
6190
+ //
6191
+ // 依頼文(手順つき)は長いので details に畳む。**help がそのまま summary の文になる**
6192
+ // (gateHelpNodes・#521)——「帯は症状・行は一手」の1文は details の外に出さなくても
6193
+ // summary は常に見えるので、開かなくても次の一手は読める。行の補足は最大1段の約束
6194
+ // (github-state-reason.test.mjs)を守るため、help と request を別々の要素にしない
6195
+ "admin_approval_pending": {
6196
+ text: "管理者の承認待ち",
6197
+ cls: "ds-gate-warn",
6198
+ help: "貴社の Google Workspace 管理者がこのアプリを承認すると、読み取りが始まります。",
6199
+ request: {
6200
+ lines: [
6201
+ "貴社の Google Workspace 管理者に、次の手順で「Cortex by Classmethod」の利用承認をご依頼ください。",
6202
+ "1. Google 管理コンソール(admin.google.com)を開く",
6203
+ "2. 「アプリ」→「Google Workspace Marketplace アプリ」を開く",
6204
+ "3. 「アプリを追加」から「Cortex by Classmethod」を検索する",
6205
+ "4. 組織全体(または該当の組織部門)へのインストールを選び、データへのアクセスを許可する",
6206
+ ],
6207
+ },
6208
+ },
6166
6209
  // --- ここから Microsoft 365(#335)。**値の正本は infra 側の語彙表**
6167
6210
  // (slack-gate.mjs の SLACK_GATE_VALUES と connection-state-keys.mjs の
6168
6211
  // REACHABLE_STATUSES)で、1対1の一致を infra のテストと
@@ -6446,6 +6489,35 @@ ${calendarAddUrl.toString()}
6446
6489
  if (s.gate == null || s.gate === "") return DS_GATE_PENDING;
6447
6490
  return DS_GATE[s.gate] || DS_GATE["unknown"];
6448
6491
  }
6492
+ /**
6493
+ * dsGateOf が持つ「次の一手」を、**必ず1個の DOM ノード**にする。呼び出しは2箇所
6494
+ * (行の常時表示・「接続を確認」を押した結果)で、gate.help / gate.request が
6495
+ * 正本——ここを通さずに文を書き写すと、片方だけ古くなる(DS_GATE の項と同じ理由)。
6496
+ *
6497
+ * 🔴 **要素を2つに割らない(行の補足は最大1段・github-state-reason.test.mjs
6498
+ * 「行の補足は最大1段」)。** gate.request(依頼文。手順が複数行あるもの)があるときは、
6499
+ * **help の1文を details の summary にする**——summary は開閉と無関係に常に見えるので、
6500
+ * 「帯は症状・行は一手」は畳まなくても読め、依頼文の手順だけが details の中に畳まれる
6501
+ * (#521・オーナー指摘「繰り返さない」)。help だけの p と request だけの details を
6502
+ * 別々に足すと、同じ ds-gate-help が2つ並んで上の番人に落ちる。
6503
+ *
6504
+ * @param cls 呼び出し側の文脈を表す class(行は「ds-gate-help」、押した結果は
6505
+ * 「ds-probe-help」)。**「ds-gate-help」を付けた要素だけが、押した瞬間に隠される**
6506
+ * (下の probeSection の stale 処理)ので、混同すると二重表示か消え忘れが起きる
6507
+ */
6508
+ function gateHelpNodes(gate, cls) {
6509
+ if (gate.request) {
6510
+ var det = el("details", { class: cls + " ds-gate-request" }, [
6511
+ el("summary", { text: gate.help || "詳しい手順" }),
6512
+ ]);
6513
+ gate.request.lines.forEach(function (line) {
6514
+ det.appendChild(el("p", { class: "ds-gate-request-line", text: line }));
6515
+ });
6516
+ return [det];
6517
+ }
6518
+ if (gate.help) return [el("p", { class: cls, text: gate.help })];
6519
+ return [];
6520
+ }
6449
6521
  /**
6450
6522
  * 一覧・タイルに出す「⚠️ …」の要約。**呼び出しは2箇所(グループの summary と能力タイル)。**
6451
6523
  * 写経すると片方だけ古くなり、同じ画面で語が割れる(この節では実際に起きた)。
@@ -7588,6 +7660,21 @@ ${calendarAddUrl.toString()}
7588
7660
  * 「いつ時点の情報か」の注記がどちらを指すのか説明できなくなる。
7589
7661
  */
7590
7662
  var LIVE_REALTIME = null;
7663
+ /**
7664
+ * Backlog の同期経路(op=state のトップレベル backlogRoute・#516)。
7665
+ *
7666
+ * 値は 4 つ: undefined=まだ答えが無い/"fixed-ip"=固定 IP 経由(正本に印がある)/
7667
+ * "runner"=通常の経路/null=正本を読めなかった(サーバが null を返した・取得に失敗した・
7668
+ * この値を知らない古い Lambda)。**null では切り替えさせない**(分からないのに書かせない)。
7669
+ * 押した直後は backlog-route-set の応答で先に更新し、次の op=state が追いつく。
7670
+ */
7671
+ var LIVE_BACKLOG_ROUTE = undefined;
7672
+ /**
7673
+ * 固定の送信元 IP(op=state の realtime.fixedEgressIp・backlog-route-set の応答)。
7674
+ * **画面に定数を持たない**——出どころは EIP の資源 → Intake の env の 1 本道で、
7675
+ * 値が変わったときに画面だけ古いままにならない。取れていなければ空文字(IP を書かない)。
7676
+ */
7677
+ var LIVE_FIXED_IP = "";
7591
7678
  /**
7592
7679
  * いま登録ファイルに何が書いてあるか(op=state の registeredNow・段階1)。取得前は null。
7593
7680
  *
@@ -8056,11 +8143,19 @@ ${calendarAddUrl.toString()}
8056
8143
  var KEY_POINTING_CHECKS = { backlog_secrets: "課題管理", figma_token: "デザイン" };
8057
8144
 
8058
8145
  /**
8059
- * Backlog の固定送信元IP(NAT GatewayのEIP)。IP制限のある顧客スペースは、
8060
- * この1つの値を許可リストに入れれば固定IP経由で同期できる(艦隊共通1本)。
8061
- * 出どころ: BacklogSyncFixedIpスタックの出力 FixedEgressIp。値が変わったらそちらを直す。
8146
+ * 鍵の入力欄を出す種別(#516)。
8147
+ *
8148
+ * **投入の口(intake)があるなら Backlog の鍵は必ず出す。** 固定 IP 経由で同期する案件は
8149
+ * かつて CDK が secretKinds から backlog を落としていた(画面から入れても効かなかった頃の
8150
+ * 名残)。いまは固定 IP 案件も同じ棚の鍵を画面から入れられる(#503 設計 v2 §5)ので、
8151
+ * 写し(viewer-config.json)がまだ古い案件でも欄を出す。判定は接続先フォームと同じ線
8152
+ *(backlogTargetsEditable)——2 か所で違う線を引くと、フォームは出るのに鍵だけ無い案件ができる。
8062
8153
  */
8063
- var BACKLOG_FIXED_EGRESS_IP = "54.238.206.32";
8154
+ function secretKindsShown() {
8155
+ var kinds = (SITE.secretKinds || []).slice();
8156
+ if (SITE.intake && kinds.indexOf("backlog") < 0) kinds.push("backlog");
8157
+ return kinds;
8158
+ }
8064
8159
 
8065
8160
  /**
8066
8161
  * 応答に添えられた接続先の一覧を採ったときの件数(Backlog の adoptTargets)。
@@ -8418,7 +8513,7 @@ ${calendarAddUrl.toString()}
8418
8513
  // 「未対応です」と伝える場所も接続マップのアンカーの受け皿も無くなった
8419
8514
  //
8420
8515
  // 個別に足し続けると次も漏らすので、**この節に出す候補を列挙して判定する**形にする。
8421
- var hasSecrets = ((SITE.secretKinds || []).filter(function (k) { return SECRET_FORMS[k]; })).length > 0;
8516
+ var hasSecrets = (secretKindsShown().filter(function (k) { return SECRET_FORMS[k]; })).length > 0;
8422
8517
  // **fleet のデータが取れているか、だけを見る(#125②)。** 宣言(tools)は撤去した(⑤)
8423
8518
  var hasCapModel = hasCaps;
8424
8519
  // **投入の口も候補。** fleet-status.json はGitHub Actionsが夜間に生成するので、
@@ -8834,7 +8929,9 @@ ${calendarAddUrl.toString()}
8834
8929
  // **GitHub(「external-sources」)はここに無い。** 読み取りトークンは廃止し、
8835
8930
  // 読み取りは GitHub App に一本化した。**この表に戻すと入力欄が復活する**
8836
8931
  // (対応表そのものは SECRET_CAP。3つの口で共有するので上に置いてある)
8837
- (SITE.secretKinds || []).forEach(function (k) {
8932
+ // **種別の一覧は secretKindsShown()(#516)。** intake がある案件は写しの secretKinds に
8933
+ // backlog が無くても Backlog の鍵を出す(固定 IP 案件も画面から鍵を入れる)
8934
+ secretKindsShown().forEach(function (k) {
8838
8935
  if (!SECRET_FORMS[k]) return;
8839
8936
  // **意図的にOFFにした能力にだけ、入力欄を出さない(0.4.71)。** 使わないと決めたものに
8840
8937
  // 鍵を求めるのは、設定する必要が無いものに設定を促す偽の警告と同じ。
@@ -9284,7 +9381,7 @@ ${calendarAddUrl.toString()}
9284
9381
  var tokenRelevant = (derived != null ? derived !== "off" : true)
9285
9382
  && keyTargetCount(cap) > 0
9286
9383
  && !(derive && derive.keyMissing);
9287
- if (tokenRelevant && sk && (SITE.secretKinds || []).indexOf(sk) >= 0 && !secretPresentForTile(sk)) {
9384
+ if (tokenRelevant && sk && secretKindsShown().indexOf(sk) >= 0 && !secretPresentForTile(sk)) {
9288
9385
  body.appendChild(el("div", { class: "ds-tile-note", text: "⚠ トークンが未設定です" }));
9289
9386
  }
9290
9387
  // 🔴 **セットアップの残り(checks)の長文はタイルに出さない(#230・オーナー指摘)。**
@@ -9591,18 +9688,21 @@ ${calendarAddUrl.toString()}
9591
9688
  * 行の常時表示(DS_GATE の 「help」)が既に持っている——写経すると、同じ行の常時表示と
9592
9689
  * 押した結果で語が割れる(この節が何度も直してきた形)。DS_GATE から引いて1文だけ出す。
9593
9690
  *
9594
- * ⚠ **403(Workspace 管理者の承認待ち)は Phase 0 の採取まで 「unknown」。** その回は
9595
- * サーバの機械語(「permission_denied」)をそのまま出す。**「時間をおいて」と言わない**
9596
- * ——待っても直らない(管理者が承認するまで同じ応答が返る)。
9691
+ * ⚠ **403(Workspace 管理者の承認待ち)は #521 で「admin_approval_pending」に分類済み。**
9692
+ * 機械語(「permission_denied」)はもう出さない。**「時間をおいて」とも言わない**
9693
+ * ——待っても直らない(管理者が承認するまで同じ応答が返る)。401 / 429 は依然
9694
+ * Phase 0 未採取で「unknown」のまま、サーバの機械語をそのまま出す。
9597
9695
  */
9598
9696
  function googleChatProbeHelpNodes(s, r) {
9599
- var p = function (t) { return el("p", { class: "ds-probe-help", text: t }); };
9600
9697
  // 成功。**1行で終わり**(状態の語が既に「接続OK」と出ている)
9601
9698
  if (r.gate === "ok") return [];
9602
- // not_in_space / no_credentials は行の常時表示と同じ1文(DS_GATE から引く)。
9699
+ // not_in_space / no_credentials / admin_approval_pending は行の常時表示と同じ文面
9700
+ // (DS_GATE から引く。依頼文の details も gateHelpNodes 経由で同じものが出る)。
9603
9701
  // 押した瞬間に行側の 「.ds-gate-help」 は隠れるので、二重には出ない
9604
- var help = dsGateOf({ type: s.type, gate: r.gate }).help;
9605
- if (help) return [p(help)];
9702
+ var gate = dsGateOf({ type: s.type, gate: r.gate });
9703
+ var nodes = gateHelpNodes(gate, "ds-probe-help");
9704
+ if (nodes.length) return nodes;
9705
+ var p = function (t) { return el("p", { class: "ds-probe-help", text: t }); };
9606
9706
  if (r.gate === "unreachable") {
9607
9707
  // 帯(「アクセス不可」)が症状を言っている。**ここは一手だけ**
9608
9708
  return [p("時間をおいて、もう一度「接続を確認」を押してください。")];
@@ -10055,14 +10155,140 @@ ${calendarAddUrl.toString()}
10055
10155
  * 立ち上げ直後の案件にも実在する。ミラーで判定すると、**まだ一度も繋いでいない案件=
10056
10156
  * これから繋ぎたい案件**だけがフォームを出せない(実機で踏んだ)。
10057
10157
  *
10058
- * 出さないのは、この画面からの投入に対応していない固定IP同期の案件だけ。
10059
- * **その見分けは SITE.secretKinds に backlog が入っているかで行う。**
10060
- * 固定IPかどうかを直接示す値は画面に来ておらず(インフラ側の設定にしか無い)、
10061
- * 鍵の欄を出すかどうかの判定が同じ線で引かれているので、代理として正確に使える。
10062
- * インフラ側で secretKinds の条件を変えるときは、**ここの意味も一緒に変わる**。
10158
+ * **判定は「投入の口(intake)があるか」(#516・#503 設計 v2 §4)。** それまでは
10159
+ * SITE.secretKinds に backlog があるかで見ていた——固定 IP 経由で同期する案件は
10160
+ * 画面からの投入に対応しておらず、CDK が secretKinds から backlog を落としていたので、
10161
+ * その代理として使えた。いまは固定 IP 案件も同じ正本・同じ棚に画面から書けるので、
10162
+ * 出さない理由は「書き込む口が無い」だけになった。**secretKinds に戻さないこと**——
10163
+ * 写し(viewer-config.json)が古い案件で、接続先も鍵も画面から入れられなくなる。
10164
+ * 鍵の欄の判定(secretKindsShown)も同じ線。
10063
10165
  */
10064
10166
  function backlogTargetsEditable() {
10065
- return (SITE.secretKinds || []).indexOf("backlog") >= 0;
10167
+ return !!SITE.intake;
10168
+ }
10169
+
10170
+ /**
10171
+ * サーバが返した同期経路を 3 値に畳む(#516)。**知らない語は null(読めない)に倒す**
10172
+ * ——新しい語を「通常」と読むと、印が付いている案件でスイッチが OFF に見える。
10173
+ */
10174
+ function backlogRouteOf(v) {
10175
+ return v === "fixed-ip" || v === "runner" ? v : null;
10176
+ }
10177
+
10178
+ /** スイッチの語(4 状態で変えない。状態は下の 1 行が言う) */
10179
+ var BACKLOG_ROUTE_SWITCH_LABEL = "Backlog に IP 制限がある(固定 IP 経由で同期する)";
10180
+
10181
+ /**
10182
+ * スイッチの下の 1 行(#516・#503 設計 v2 §6 の表と v3 §6 の ON 行)。
10183
+ *
10184
+ * 症状 → 一手の 1〜2 文。**技術語(CodeBuild・Lambda・runner)を出さない。**
10185
+ * @param route LIVE_BACKLOG_ROUTE(undefined / "fixed-ip" / "runner" / null)
10186
+ * @param ip 固定の送信元 IP(無ければ空文字。**無いときは IP を書かない**——書けば嘘)
10187
+ * @param count いま画面に出ている接続先の件数
10188
+ * @param keyState Backlog の鍵の登録状況("yes" / "no" / "unknown")。**"no" のときだけ**
10189
+ * 鍵の登録を一手に出す。分からないときに「登録すると始まる」と言うと、
10190
+ * 入れてある人に入れ直させる
10191
+ */
10192
+ function backlogRouteLine(route, ip, count, keyState) {
10193
+ if (route === undefined) return "設定を確認しています…";
10194
+ if (route === null) return "設定を読み取れないため、いまは切り替えられません。";
10195
+ if (route !== "fixed-ip") {
10196
+ return count > 0
10197
+ ? "Backlog の IP 制限で同期が失敗する案件は、これを入れてください。"
10198
+ : "先に接続先を登録してください。";
10199
+ }
10200
+ var ipLabel = ip ? "固定の送信元 IP(" + ip + ")" : "固定の送信元 IP";
10201
+ if (keyState === "no") {
10202
+ return "同期は" + ipLabel + "から行います。お客様にこの IP の許可を依頼し、上の「Backlog のAPIキー」を登録すると同期が始まります。";
10203
+ }
10204
+ // 2 文に収める(実装レビューの指摘)。押した直後の帯(サーバの文面)も「許可を依頼」を言うが、
10205
+ // 帯は数秒で読み終える結果で、この行は次に開いたときも残る状態——役目が違うので両方に置く
10206
+ return "同期は" + ipLabel + "から行います。お客様にこの IP の許可を依頼してください(反映は毎晩 21:00 ごろの定期同期と、Webhook の受信から数分後です)。";
10207
+ }
10208
+
10209
+ /**
10210
+ * Backlog の同期経路の切り替えスイッチ(#516・#503 設計 v2 §6 / v3 §6・分類 A4)。
10211
+ *
10212
+ * 置き場は接続先の一覧の下・リアルタイム同期の上。部品はスイッチ 1 つ+下の 1 行+結果の帯。
10213
+ *
10214
+ * **確認ダイアログは出さない(A4)。** ON も OFF も同じスイッチで即時に戻せる(可逆)。
10215
+ * 押した記録はサーバの監査に残る。
10216
+ *
10217
+ * **押せないのは 3 つ。** 印を読めない(null)・まだ答えが無い(undefined)・OFF で接続先 0 件
10218
+ *(サーバも 400 で断る)。ON で接続先 0 件は押せる——止める方向を塞がない(T4 の契約)。
10219
+ *
10220
+ * **状態の正本は LIVE_BACKLOG_ROUTE。** 押して成功したら応答の route で先に更新し、
10221
+ * その後の op=state(sendConfig が起こす)が追いつく。**画面の側で「押したから ON」と
10222
+ * 決めない**——断られたときに向きだけ変わる形にしない。
10223
+ *
10224
+ * **結果の帯は差し替えの箱の外に置く。** 応答の文面(「お客様に IP の許可を依頼してください」)は
10225
+ * 次の一手そのもので、数秒後に op=state が届いて箱を描き直した瞬間に消えると、
10226
+ * 一番読ませたい文が読み終える前に消える(接続先の一覧の「結果は一覧の外」と同じ罠)。
10227
+ *
10228
+ * **IP の値は画面に持たない。** op=state(realtime.fixedEgressIp)と切り替えの応答
10229
+ *(fixedEgressIp)から取る。取れていなければ IP を書かない。
10230
+ */
10231
+ function backlogRouteSwitch() {
10232
+ var wrap = el("div", { class: "ds-route" });
10233
+ var msg = el("div", { class: "ds-toggle-msg" });
10234
+ var live = liveList("backlog-route", function () {
10235
+ return JSON.stringify([
10236
+ LIVE_BACKLOG_ROUTE === undefined ? "?" : LIVE_BACKLOG_ROUTE,
10237
+ LIVE_FIXED_IP,
10238
+ keyTargetCount("課題管理"),
10239
+ secretRegisteredAnywhere(LIVE_SECRETS, "backlog"),
10240
+ ]);
10241
+ }, function (box) {
10242
+ var route = LIVE_BACKLOG_ROUTE;
10243
+ var on = route === "fixed-ip";
10244
+ var count = keyTargetCount("課題管理");
10245
+ var keyState = secretRegisteredAnywhere(LIVE_SECRETS, "backlog");
10246
+ var disabled = route === undefined || route === null || (!on && count === 0);
10247
+ var btn = el("button", {
10248
+ class: "ds-switch",
10249
+ type: "button",
10250
+ role: "switch",
10251
+ "aria-checked": on ? "true" : "false",
10252
+ }, [
10253
+ el("span", { class: "ds-switch-track", "aria-hidden": "true" }, [el("span", { class: "ds-switch-knob" })]),
10254
+ el("span", { class: "ds-switch-label", text: BACKLOG_ROUTE_SWITCH_LABEL }),
10255
+ ]);
10256
+ if (disabled) btn.disabled = true;
10257
+ btn.addEventListener("click", async function () {
10258
+ if (btn.disabled) return;
10259
+ var next = !on;
10260
+ // **語は差し替えない**(beginBusy はボタンの語を「処理中…」に変える)。スイッチの語は
10261
+ // 状態を言う 1 行と対なので、押している間だけ消えると何を押したのか読めなくなる。
10262
+ // 押せない見た目にして、帯(msg)の「保存中…」で進んでいることを示す
10263
+ btn.disabled = true;
10264
+ btn.classList.add("is-busy");
10265
+ msg.className = "ds-toggle-msg";
10266
+ msg.textContent = "";
10267
+ // **onDone ではなく hooks.onSaved で受ける(実装レビューの指摘)。** onDone は
10268
+ // changed:false では流れない。正本が既に送った向きだった回(別の人・別のタブが先に
10269
+ // 切り替えていた)はサーバが changed:false・route 無しで返すので、onDone だと
10270
+ // スイッチが古い向きのまま「✓ 変更はありません」が出て、次の取り直しまで直らない。
10271
+ // onSaved は res.ok なら changed を問わず流れる(冪等な op の受け口)
10272
+ await sendConfig({ op: "backlog-route-set", route: next ? "fixed-ip" : "runner" }, msg, null, null, {
10273
+ onSaved: function (j) {
10274
+ // **応答の値を正とする。** 無ければ送った向き(サーバは断らなかった=正本はその向き)
10275
+ LIVE_BACKLOG_ROUTE = backlogRouteOf(j && j.route) || (next ? "fixed-ip" : "runner");
10276
+ if (j && j.fixedEgressIp) LIVE_FIXED_IP = String(j.fixedEgressIp);
10277
+ live.__rebuild();
10278
+ // changed:false では sendConfig が読み直しを起こさない。正本の値で画面を揃えるために自分で起こす
10279
+ if (j && j.changed === false) loadLiveFleet(true);
10280
+ },
10281
+ });
10282
+ // 断られた・通信に失敗したときはこのボタンが残っている(成功時は描き直されて外れている)
10283
+ btn.disabled = disabled;
10284
+ btn.classList.remove("is-busy");
10285
+ });
10286
+ box.appendChild(el("div", { class: "ds-toggle ds-route-row" }, [btn]));
10287
+ box.appendChild(el("p", { class: "sec-how ds-route-line", text: backlogRouteLine(route, LIVE_FIXED_IP, count, keyState) }));
10288
+ });
10289
+ wrap.appendChild(live);
10290
+ wrap.appendChild(msg);
10291
+ return wrap;
10066
10292
  }
10067
10293
 
10068
10294
  /**
@@ -10426,9 +10652,10 @@ ${calendarAddUrl.toString()}
10426
10652
  */
10427
10653
  function ensureKeySlots() {
10428
10654
  if (!editable) return;
10429
- // 受け皿を作る経路を通っていない案件(intake が無い・固定IP同期)はここも通さない
10655
+ // 受け皿を作る経路を通っていない案件(intake が無い)はここも通さない
10430
10656
  if (!KEY_SLOTS.backlog || !Object.keys(KEY_SLOTS.backlog).length) return;
10431
- if ((SITE.secretKinds || []).indexOf("backlog") < 0 || !SECRET_FORMS["backlog"]) return;
10657
+ // 種別の判定は鍵カードを作るループと同じ線(secretKindsShown・#516)
10658
+ if (secretKindsShown().indexOf("backlog") < 0 || !SECRET_FORMS["backlog"]) return;
10432
10659
  var domains = [];
10433
10660
  for (var i = 0; i < targets.length; i++) {
10434
10661
  var dm = String(targets[i].domain || "").toLowerCase();
@@ -10479,9 +10706,9 @@ ${calendarAddUrl.toString()}
10479
10706
  // 呼ぶので、fleet が接続先を持たない案件(同期ミラーのURLから1行だけ作っている案件)では
10480
10707
  // **空を返す**——行は出るのに鍵の欄が1つも付かない、という空振りになる。
10481
10708
  //
10482
- // **この画面から投入できない案件(固定IP同期)には出さない。** 押しても Lambda
10483
- // 「固定IP経由のため対応していません」で断るので、導線そのものを作らない。
10484
- // 判定は鍵カードを出すループの条件と同じ線(SITE.secretKinds に backlog があるか)。
10709
+ // **固定 IP 経由の案件にも出す(#516)。** かつては Lambda が「固定IP経由のため
10710
+ // 対応していません」で断るので導線を作らなかったが、いまは同じ棚に入る(#503 T4)。
10711
+ // 判定は鍵カードを出すループの条件と同じ線(secretKindsShown=intake があるか)。
10485
10712
  if (editable && !Object.keys(KEY_SLOTS.backlog).length) {
10486
10713
  // 保存時にスペースを名乗ってよいのは、名簿(fleet の backlogTarget)由来の行だけ。
10487
10714
  // 同期ミラーのURLから作った行で名乗ると、名簿とミラーがずれている案件で
@@ -10687,25 +10914,19 @@ ${calendarAddUrl.toString()}
10687
10914
  // 実在する。同期ミラーの有無で門前払いすると、**新規案件がこの画面から
10688
10915
  // Backlog に繋げない**——設定UIを置いている意味が無くなる
10689
10916
  box.appendChild(backlogTargetsSection(s));
10690
- if (!backlogTargetsEditable()) {
10691
- box.appendChild(el("p", { class: "sec-how", text: "この案件の Backlog は別の経路で同期しているため、この画面からは接続先を変更できません。変更が必要なときは cortex の担当者にご連絡ください。" }));
10692
- used = true;
10693
- }
10917
+ // **「この画面からは接続先を変更できません」の枝は撤去した(#516)。** ゲートが
10918
+ // 「intake があるか」になり、この関数は intake の無い案件では上で return しているので、
10919
+ // 到達しない枝だった(到達不能な枝は変異検証の網から漏れる)。
10920
+ //
10921
+ // **接続先の下・リアルタイム同期の上に、同期経路のスイッチを置く(#516・設計 v2 §6)。**
10922
+ // #497 の「固定 IP 経由で同期できます(切り替えは運用側)」の案内と画面の IP 定数は
10923
+ // これに置き換わった——切り替えは担当者が自分で押す。
10924
+ box.appendChild(backlogRouteSwitch());
10694
10925
  // **接続先の下に置く。** リアルタイム同期は「どのプロジェクトを見るか」が
10695
10926
  // 決まった後の話で、順序が逆だと、接続先を設定していない案件が先に
10696
10927
  // Webhook を貼りに行く(貼っても届かない)
10697
10928
  var rt = realtimeSyncCard();
10698
10929
  if (rt) box.appendChild(rt);
10699
- // **モーダルの末尾に固定IP案内を1つだけ足す(#497)。** 固定IP経由の案件
10700
- // (backlogTargetsEditable()===false)は既に3行上で「この画面からは接続先を
10701
- // 変更できません」を出して担当者連絡に倒しているので、二重に言わない。
10702
- if (backlogTargetsEditable()) {
10703
- box.appendChild(el("p", {
10704
- class: "sec-how",
10705
- text: "Backlog に IP 制限がかかっている場合は、固定の送信元 IP(" + BACKLOG_FIXED_EGRESS_IP
10706
- + ")経由で同期できます。お客様側でこの IP の許可を依頼したうえで、運用担当に連絡してください(切り替えは運用側で行います)。",
10707
- }));
10708
- }
10709
10930
  used = true;
10710
10931
  }
10711
10932
 
@@ -12015,7 +12236,7 @@ ${calendarAddUrl.toString()}
12015
12236
  //
12016
12237
  // **ボタンより後ろに置く。** カードは横並びの flex なので、間に挟むと
12017
12238
  // 「登録を解除」が案内文の下の行へ落ちて、既存案件の行の形が変わる
12018
- if (gate.help) card.appendChild(el("p", { class: "ds-gate-help", text: gate.help }));
12239
+ gateHelpNodes(gate, "ds-gate-help").forEach(function (n) { card.appendChild(n); });
12019
12240
  // **最後に測った理由と直し方を、押す前から出す(#346)。** 「アクセス不可 8時間前に確認」
12020
12241
  // だけでは、押さない人(初見の PM 等)が止まる。表にエントリがある状態は定型の補足
12021
12242
  // (リンク込み)だけ、無い状態は needed → detail のフォールバック。どちらも ds-gate-help
@@ -12398,6 +12619,10 @@ ${calendarAddUrl.toString()}
12398
12619
  // **fleet より先に読む。** fleet-status.json をまだ持たない案件(立ち上げ直後)は
12399
12620
  // 下で throw するが、リアルタイム同期の可否は索引由来なので独立に決まる
12400
12621
  LIVE_REALTIME = (j && j.realtime) || null;
12622
+ // 同期経路と固定 IP(#516)。**サーバが知らない値・返さない回は null(読めない)**
12623
+ // ——undefined のままにすると「確認しています」が永久に残る
12624
+ LIVE_BACKLOG_ROUTE = backlogRouteOf(j && j.backlogRoute);
12625
+ if (LIVE_REALTIME && LIVE_REALTIME.fixedEgressIp) LIVE_FIXED_IP = String(LIVE_REALTIME.fixedEgressIp);
12401
12626
  // 🔴 **registeredNow も fleet より先に読む。** まとめ表をまだ持たない案件
12402
12627
  // (=立ち上げ直後=この仕組みが一番効く案件)は下で throw するので、
12403
12628
  // ここより後に置くと**その案件だけ素通り**する
@@ -12448,6 +12673,9 @@ ${calendarAddUrl.toString()}
12448
12673
  liveFleetRequested = false;
12449
12674
  liveAnswered = true;
12450
12675
  MERGED = null;
12676
+ // 同期経路(#516)。**一度も答えが無いまま失敗したら「読めない」に倒す**(切り替えさせない)。
12677
+ // 前の答え(または切り替え直後の応答の値)があるなら、それを捨てない
12678
+ if (LIVE_BACKLOG_ROUTE === undefined) LIVE_BACKLOG_ROUTE = null;
12451
12679
  // 🔴 **確認中に倒した図を焼いた表示へ戻す(#256・設計v2 の3)。** ライブが
12452
12680
  // 取れなかったときのフォールバックはここに残る——戻さないと「確認中…」の
12453
12681
  // ままの図が残り、オフラインでは**何も分からない画面**になる。
@@ -12575,8 +12803,11 @@ ${calendarAddUrl.toString()}
12575
12803
  * **出すのは「手順どおり設定すれば実際に効く案件」だけ。** 判断は Lambda(op=state)が返す
12576
12804
  * realtime に委ねる——画面が持っている静的データからは「ルーティング索引に自分の行が
12577
12805
  * あるか」が分からないし、2箇所で判定すると片方だけがずれる。
12578
- * - supported: false … 固定IP経由で同期している案件。**黙って欠けさせず**理由を1行出す
12806
+ * - via: "fixed-ip" … 固定 IP 経由で同期する案件(#516・#503 設計 v3 §6)。手順は同じ。
12807
+ * 反映が「約1分」ではなく「数分」になることだけを言う
12579
12808
  * - ready: false … 索引にまだ行が無い。設定しても届かないので手順を出さない
12809
+ * 「supported: false(固定 IP は未対応)」の枝は撤去した——固定 IP 案件でも Webhook を
12810
+ * 受ける(分類 A2)。その値を返す古い Lambda に当たっても、手順を出す側に倒す
12580
12811
  *
12581
12812
  * **URLに載せるトークンは案件別**(SITE.backlogWebhook)。艦隊共通トークンを載せると、
12582
12813
  * この画面を見られる人が全案件ぶんの偽イベントを投げられる(types.ts の backlogWebhook 参照)。
@@ -12587,14 +12818,10 @@ ${calendarAddUrl.toString()}
12587
12818
  */
12588
12819
  function realtimeSyncCard() {
12589
12820
  var rt = LIVE_REALTIME;
12590
- // **未対応(固定IP経由の同期)だけは、手順ではなく理由を出す。** ここは索引の状態ではなく
12591
- // 経路の話で、設定しても効かないことが確定している唯一の形。黙って欠けさせない
12592
- if (rt && rt.supported === false) {
12593
- return el("p", {
12594
- class: "sec-how",
12595
- text: "この案件は固定IP経由でBacklogを同期しているため、リアルタイム同期(Webhook)には未対応です。反映は毎晩の同期のタイミングになります。",
12596
- });
12597
- }
12821
+ // 固定 IP 経由か(#516)。**経路の正本は op=state の realtime.via**——スイッチの状態
12822
+ //(LIVE_BACKLOG_ROUTE)から引かない。あちらは押した直後に先に変わるが、受信したイベントを
12823
+ // どちらが同期するかはサーバ側の索引が決めるので、同じ応答の値で言う
12824
+ var viaFixedIp = !!(rt && rt.via === "fixed-ip");
12598
12825
  var wh = SITE.backlogWebhook;
12599
12826
  // **URLが無い案件には出さない。** 貼るものが無い手順は、一番読めない形になる
12600
12827
  if (!wh || !wh.url || !wh.token) return null;
@@ -12666,7 +12893,11 @@ ${calendarAddUrl.toString()}
12666
12893
  msg,
12667
12894
  // **この画面は顧客も見る。** URLに秘密が入っていることと、その扱いを書いておく
12668
12895
  el("p", { class: "sec-note", text: "このURLはこの案件専用です(他の案件には届きません)。秘密のトークンを含むので、貼り付け先は Backlog のこのプロジェクトの設定欄だけにしてください。" }),
12669
- el("p", { class: "sec-how", text: "反映は受信から約1分後です(1分ごとのミニバッチでまとめて取り込みます)。初回は上の接続先の設定と、夜間同期が一度済んでいることが前提になります。" }),
12896
+ // 固定 IP 経由の案件は反映が「約1分」にならない(1分のまとめ+固定 IP の同期の起動で数分)。
12897
+ // **同じ行を差し替える**——2 行に分けると「約1分」と「数分」が並んで矛盾する
12898
+ el("p", { class: "sec-how", text: viaFixedIp
12899
+ ? "固定 IP 経由で同期します(反映は受信から数分後です)。初回は上の接続先の設定と、夜間同期が一度済んでいることが前提になります。"
12900
+ : "反映は受信から約1分後です(1分ごとのミニバッチでまとめて取り込みます)。初回は上の接続先の設定と、夜間同期が一度済んでいることが前提になります。" }),
12670
12901
  ];
12671
12902
 
12672
12903
  if (live) {
@@ -12700,7 +12931,10 @@ ${calendarAddUrl.toString()}
12700
12931
  el("span", { class: "rt-live-badge", text: "✓ 稼働中" }),
12701
12932
  ]),
12702
12933
  lines,
12703
- el("p", { class: "ops-card-desc", text: "Backlog の変更をすぐ取り込んでいます(毎晩の同期を待ちません)。設定は済んでいるので、このままお使いください。" }),
12934
+ // 固定 IP 経由の案件は「すぐ」ではなく「数分」(手順は畳んであるので、ここでも言う)
12935
+ el("p", { class: "ops-card-desc", text: viaFixedIp
12936
+ ? "Backlog の変更を固定 IP 経由で取り込んでいます(反映まで数分・毎晩の同期を待ちません)。設定は済んでいるので、このままお使いください。"
12937
+ : "Backlog の変更をすぐ取り込んでいます(毎晩の同期を待ちません)。設定は済んでいるので、このままお使いください。" }),
12704
12938
  det,
12705
12939
  ]);
12706
12940
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.46",
3
+ "version": "0.12.48",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,