@takagaki/cortex-decisions-viewer 0.4.67 → 0.4.68

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
@@ -1130,6 +1130,11 @@ async function build(opts) {
1130
1130
  projectKey: opts.projectKey,
1131
1131
  }
1132
1132
  : undefined,
1133
+ // **URLとトークンが両方そろったときだけ出す。** 片方だけでは貼れるURLを作れないので、
1134
+ // 手順だけが出て「専用URLが空」という一番混乱する形になる(intake と同じ扱い)
1135
+ backlogWebhook: opts.webhookUrl && opts.webhookToken
1136
+ ? { url: opts.webhookUrl, token: opts.webhookToken }
1137
+ : undefined,
1133
1138
  secretKinds: (opts.secretKinds ?? "")
1134
1139
  .split(",")
1135
1140
  .map((s) => s.trim())
package/dist/cli.js CHANGED
@@ -20,6 +20,8 @@ program
20
20
  .option("--intake-token <token>", "投入フォームの共有秘密トークン(環境変数 INTAKE_TOKEN でも可)")
21
21
  .option("--project-key <key>", "案件キー(投入フォームのルーティングキー)")
22
22
  .option("--secret-kinds <kinds>", "鍵の入力欄を出す連携(カンマ区切り。例: figma,external-sources,backlog)")
23
+ .option("--webhook-url <url>", "Backlog Webhook 受信のLambda URL(環境変数 WEBHOOK_URL でも可)")
24
+ .option("--webhook-token <token>", "リアルタイム同期の**案件別**トークン(環境変数 WEBHOOK_TOKEN でも可。艦隊共通トークンを渡さないこと)")
23
25
  .action(async (dir, opts) => {
24
26
  try {
25
27
  const result = await (0, build_1.build)({
@@ -34,6 +36,8 @@ program
34
36
  intakeToken: opts.intakeToken ?? process.env.INTAKE_TOKEN,
35
37
  projectKey: opts.projectKey,
36
38
  secretKinds: opts.secretKinds,
39
+ webhookUrl: opts.webhookUrl ?? process.env.WEBHOOK_URL,
40
+ webhookToken: opts.webhookToken ?? process.env.WEBHOOK_TOKEN,
37
41
  });
38
42
  console.log(`✓ ${result.count} 件のレコードを ${result.outDir} にビルドしました`);
39
43
  }
package/dist/render.js CHANGED
@@ -891,7 +891,28 @@ a.ds-name:hover { text-decoration: underline; }
891
891
  .ds-note { color: var(--muted); font-size: 12px; line-height: 1.7; margin: 6px 0 0; }
892
892
  .ds-sync { font-size: 12.5px; color: var(--muted); font-variant-numeric: tabular-nums; flex: 0 0 auto; }
893
893
  .ds-col { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1 1 auto; }
894
- .ds-keys { font-size: 12px; color: var(--muted); line-height: 1.6; }
894
+ /* 会議名に入れる目印。**この行が会議カードで一番強い。** 取り込まれるかどうかは
895
+ 会議名にこれが入っているかで決まるので、補足文の一つに埋もれさせない(#117) */
896
+ .ds-keys {
897
+ display: flex; align-items: center; flex-wrap: wrap; gap: 4px 8px;
898
+ margin: 4px 0 2px; padding: 8px 10px;
899
+ background: var(--accent-weak); border: 1px solid #c7d7fe; border-radius: 8px;
900
+ font-size: 13px; color: var(--text); line-height: 1.5;
901
+ }
902
+ .ds-keys-label { font-weight: 700; }
903
+ .ds-keys-code {
904
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
905
+ font-size: 14px; font-weight: 700; color: var(--accent);
906
+ background: #fff; border: 1px solid #c7d7fe; border-radius: 6px; padding: 1px 8px;
907
+ }
908
+ .ds-keys-or { font-size: 12px; color: var(--muted); }
909
+ /* 経過措置で登録された会議名。読み取り専用の記録なので、目印より弱く出す(#117) */
910
+ .ds-legacy { margin-top: 4px; font-size: 12px; color: var(--muted); line-height: 1.6; }
911
+ .ds-legacy-chips { display: flex; flex-wrap: wrap; gap: 4px 6px; margin-top: 3px; }
912
+ .ds-legacy-chip {
913
+ font-size: 11.5px; color: var(--muted);
914
+ background: #f4f5f7; border: 1px solid var(--border); border-radius: 999px; padding: 1px 8px;
915
+ }
895
916
  .ds-drive-note { font-size: 12px; color: #6b7280; line-height: 1.6; }
896
917
  .ds-group { border: none; margin: 0; }
897
918
  .ds-group > summary {
@@ -1015,6 +1036,11 @@ a.ds-name:hover { text-decoration: underline; }
1015
1036
  .ds-toggle-msg.is-ok { color: #1a7f37; }
1016
1037
  .ds-toggle-msg.is-error { color: #b42318; }
1017
1038
  .badge.ds-notify { background: #eff6ff; color: #1e40af; border: 1px solid #bfdbfe; }
1039
+ /* 登録の由来(#118)。**薄く出す**——読み取りの可否や Gold 対象かより弱い情報で、
1040
+ ここが目立つと「除外」「未宣言」の判断のバッジと同じ強さに見えてしまう */
1041
+ .badge.ds-origin { background: transparent; color: var(--muted); border: 1px dashed var(--border); }
1042
+ /* 全組織が接続済みのときに、トークン入力欄の代わりに出す1行(#118) */
1043
+ .ds-all-connected { color: var(--muted); }
1018
1044
  .ds-gate { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; font-weight: 600; flex: 0 0 auto; }
1019
1045
  .ds-gate::before { content: ""; width: 8px; height: 8px; border-radius: 999px; background: currentColor; }
1020
1046
  .ds-gate-ok { color: #1f7a45; }
@@ -1063,6 +1089,12 @@ a.ds-name:hover { text-decoration: underline; }
1063
1089
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
1064
1090
  padding: 18px; box-shadow: var(--shadow); display: flex; flex-direction: column; gap: 10px;
1065
1091
  }
1092
+ /* リアルタイム同期の設定手順(#113)。番号付きなのは「上から順に1回やる」作業だから */
1093
+ .rt-steps { margin: 0; padding-left: 1.4em; color: var(--text); font-size: 13px; line-height: 1.9; }
1094
+ .rt-steps li { margin: 2px 0; }
1095
+ /* URLは長いので、入力欄を伸ばしてボタンを右に置く(折り返しても崩れないように wrap) */
1096
+ .rt-url-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
1097
+ .rt-url { flex: 1 1 260px; margin-bottom: 0; font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; }
1066
1098
  .ops-card-head { display: flex; align-items: center; gap: 10px; }
1067
1099
  .ops-card-icon { font-size: 22px; line-height: 1; }
1068
1100
  .ops-card-title { font-size: 15px; font-weight: 700; color: var(--text); margin: 0; }
@@ -1731,6 +1763,14 @@ ${capabilityState.toString()}
1731
1763
  */
1732
1764
  var LIVE_FLEET = null;
1733
1765
  var LIVE_FLEET_FAILED = false;
1766
+ /**
1767
+ * リアルタイム同期の可否(op=state の realtime。#113)。取得前・取得失敗時は null。
1768
+ *
1769
+ * **fleet とは別に持つ。** fleet は fleet-status.json(夜間バッチの成果物)の射影で、
1770
+ * こちらはルーティング索引(DynamoDB)を引いた結果。同じ入れ物に混ぜると、
1771
+ * 「いつ時点の情報か」の注記がどちらを指すのか説明できなくなる。
1772
+ */
1773
+ var LIVE_REALTIME = null;
1734
1774
  /**
1735
1775
  * 接続状況の値を引く。ライブがあればライブ、無ければ静的ビルドの値。
1736
1776
  * SITE 側はキー名が fleetGeneratedAt だけ異なる(他は fleet-status.json と同名)。
@@ -1881,43 +1921,46 @@ ${capabilityState.toString()}
1881
1921
  }
1882
1922
  var aliases = (s.kind === "会議" && s.ingestState === "on" && s.meetingAliases) || [];
1883
1923
  if (signal) {
1884
- col.appendChild(el("span", {
1924
+ // **カードで一番強い要素にする(#117)。** 実機で見たとき、目印の案内が
1925
+ // 別名の一覧や注記と同じ細字で並んでいて、「まず会議名にこれを入れる」が
1926
+ // 伝わらなかった。最初に決めた合図が唯一の真実なので、そこだけを太く出す。
1927
+ //
1928
+ // **括弧を付けた形でそのまま出す。** 語だけ見せると 【KC定例】 のように
1929
+ // 混ぜて書かれ、どこも一致せず未仕分けへ落ちる(実際に起きた)。
1930
+ col.appendChild(el("div", {
1885
1931
  class: "ds-keys",
1886
- text: "会議名に入れる目印: 【" + signal + "】 または [" + signal + "]",
1887
1932
  title: "会議名のどこかにこの括弧付きの目印が入っていると、この案件に振り分けられます(大小文字は問いません)。括弧の中に他の文字を混ぜると一致しません。",
1888
- }));
1933
+ }, [
1934
+ el("span", { class: "ds-keys-label", text: "会議名にこれを入れてください" }),
1935
+ el("code", { class: "ds-keys-code", text: "【" + signal + "】" }),
1936
+ el("span", { class: "ds-keys-or", text: "または" }),
1937
+ el("code", { class: "ds-keys-code", text: "[" + signal + "]" }),
1938
+ ]));
1889
1939
  }
1890
1940
  if (aliases.length) {
1891
- // **目印が無くても入る会議。** 既に走っていて改名を頼みにくい定例を、
1892
- // いまの名前のまま登録したもの。出さないと「なぜこれだけ入るのか」
1893
- // 「新しい定例をどこに足すのか」が画面から分からない。
1941
+ // **読み取り専用の記録として出す(#117)。** 目印が無くても入る会議=括弧運用へ
1942
+ // 移る前から走っていて、改名を頼めなかった定例。出さないと「なぜこれだけ入るのか」が
1943
+ // 画面から分からないので残すが、**操作は一切出さない**。
1894
1944
  //
1895
- // **1件ずつ外せるようにする。** 追加はできて削除はできない、だと
1896
- // 間違えて登録した名前(日付つきのファイル名など)を消す手段が無い
1897
- col.appendChild(el("span", {
1898
- class: "ds-keys",
1899
- text: "目印なしで取り込む会議:",
1900
- title: "この名前の会議は、目印が無くてもこの案件に振り分けられます(大小文字・前後の空白は問いません。ファイル名に使えない記号(コロン・スラッシュ等)は同じものとして扱います)。既に走っている定例を改名せずに取り込むための登録です。",
1901
- }));
1902
- var alist = el("div", { class: "ds-subitems" });
1945
+ // 以前は1件ずつ外せるようにしていた。実機確認でユーザーが「最初に決めたキー(合図)が
1946
+ // 唯一真実」と確定させたので、画面から触れる余地を無くす——別名を足したり外したり
1947
+ // できる限り、合図と別名のどちらが正なのかが人によって揺れ続ける。
1948
+ // 変更が要るときはリポジトリの ingest-config.json を直接編集する(Lambda も断る)。
1949
+ var legacy = el("div", { class: "ds-legacy" }, [
1950
+ el("span", {
1951
+ text: "経過措置で登録された会議名(目印が無くても取り込まれます。変更はリポジトリの ingest-config.json で行います)",
1952
+ }),
1953
+ ]);
1954
+ var chips = el("div", { class: "ds-legacy-chips" });
1903
1955
  aliases.forEach(function (a) {
1904
- var row = el("div", { class: "ds-subitem" }, [el("span", { class: "ds-name", text: a })]);
1905
- row.appendChild(removeButton({
1906
- label: "この会議を外す",
1907
- confirmLines: function () {
1908
- return [
1909
- "「" + a + "」を取り込み対象から外します。",
1910
- "",
1911
- "以後この名前の会議は、会議名に目印を入れない限り取り込まれません。",
1912
- "",
1913
- "外しますか?",
1914
- ];
1915
- },
1916
- build: function () { return { op: "remove-meeting-alias", alias: a }; },
1956
+ chips.appendChild(el("span", {
1957
+ class: "ds-legacy-chip",
1958
+ text: a,
1959
+ title: "この名前の会議は、目印が無くてもこの案件に振り分けられます(大小文字・前後の空白は問いません。ファイル名に使えない記号(コロン・スラッシュ等)は同じものとして扱います)。",
1917
1960
  }));
1918
- alist.appendChild(row);
1919
1961
  });
1920
- col.appendChild(alist);
1962
+ legacy.appendChild(chips);
1963
+ col.appendChild(legacy);
1921
1964
  }
1922
1965
  if (s.driveSync === false) {
1923
1966
  col.appendChild(el("span", { class: "ds-drive-note", text: "Drive自動同期は未設定(手動で置いたファイルの変換のみ)" }));
@@ -2062,10 +2105,40 @@ ${capabilityState.toString()}
2062
2105
 
2063
2106
  pushCap("開発", adders.querySelector('[data-adder="github"]'));
2064
2107
 
2108
+ /**
2109
+ * GitHub の読み取りトークンを入れる欄が要るか(#118)。
2110
+ *
2111
+ * トークンは **GitHub App が入っていない組織のフォールバック**でしかない。全組織が
2112
+ * App(か登録済みトークン)で読めている案件に入力欄を出すと、実機で言われたとおり
2113
+ * 「App に移行したのに、なぜトークンを求められるのか」になる。押す必要が無いものを
2114
+ * 押させるのは、未設定の警告を出し続けるのと同じ種類の誤りなので畳む。
2115
+ *
2116
+ * **判断材料が無いときは出す。** githubOwners は fleet-status.json 由来で、
2117
+ * 夜間バッチがまだ走っていない案件には無い。「情報が無い」を「接続済み」に寄せると、
2118
+ * **一番繋ぎたい立ち上げ直後の案件**から唯一の入口が消える。
2119
+ *
2120
+ * **unknown(確かめられなかった組織)も「要る」側に倒す。** 実態は none かもしれず、
2121
+ * 隠す方に倒すと直せなくなる(画面には組織ごとに「確認できませんでした」と出ている)。
2122
+ */
2123
+ function githubTokenNeeded() {
2124
+ var list = fleetVal("githubOwners") || [];
2125
+ if (!list.length) return true;
2126
+ return list.some(function (o) { return !o || (o.source !== "app" && o.source !== "token"); });
2127
+ }
2128
+
2065
2129
  // 連携の鍵は、その能力のところへ置く(画面下まで往復させない)
2066
2130
  var SECRET_CAP = { figma: "デザイン", backlog: "課題管理", "external-sources": "開発" };
2067
2131
  (SITE.secretKinds || []).forEach(function (k) {
2068
2132
  if (!SECRET_FORMS[k]) return;
2133
+ // 全組織が繋がっているなら、入力欄ではなく**そう言う1行**に畳む(#118)。
2134
+ // 何も出さないと「トークンはどこで入れるのか」が分からなくなる
2135
+ if (k === "external-sources" && !githubTokenNeeded()) {
2136
+ pushCap(SECRET_CAP[k], el("p", {
2137
+ class: "sec-how ds-all-connected",
2138
+ text: "すべての組織に接続済みです(GitHub App / 登録済みトークン)。読み取りトークンの登録は要りません。",
2139
+ }));
2140
+ return;
2141
+ }
2069
2142
  pushCap(SECRET_CAP[k], secretCard(k, SECRET_FORMS[k]));
2070
2143
  });
2071
2144
 
@@ -2242,7 +2315,11 @@ ${capabilityState.toString()}
2242
2315
  // 新規案件——**まさに何を入れれば繋がるのかを知りたい場面**——から、
2243
2316
  // 鍵が未設定であることの手がかりまで消える
2244
2317
  var sk = SECRET_OF_CAP[cap];
2245
- if (tools[cap] !== "none" && sk && (SITE.secretKinds || []).indexOf(sk) >= 0 && !((fleetVal("secretSources") || {})[sk])) {
2318
+ // **GitHub は「トークンが無い」=「読めていない」ではない(#118)。** App で読めている
2319
+ // 案件に未設定の警告を出すと、直す必要が無いものを直させることになる。
2320
+ // 読めていない組織があるときは、この下の checks とタイルの状態(warn)が拾う
2321
+ var tokenIsFallback = sk === "external-sources" && !githubTokenNeeded();
2322
+ if (!tokenIsFallback && tools[cap] !== "none" && sk && (SITE.secretKinds || []).indexOf(sk) >= 0 && !((fleetVal("secretSources") || {})[sk])) {
2246
2323
  body.appendChild(el("div", { class: "ds-tile-note", text: "⚠ トークンが未設定です" }));
2247
2324
  }
2248
2325
  // セットアップの残りのうち、この能力のもの。**鍵の注記と同じ位置**に出して
@@ -2458,24 +2535,13 @@ ${capabilityState.toString()}
2458
2535
  // 宣言で隠すと、動いているのに説明が出ない状態になる。
2459
2536
  if (s.kind === "会議" && s.ingestState === "on") {
2460
2537
  box.appendChild(meetingHowTo());
2461
- // **別名の追加は、既に別名がある案件にだけ出す(#117)。**
2538
+ // **会議名の登録を画面から変える口は置かない(#117 第2弾)。**
2462
2539
  // 別名は括弧付きの合図運用へ移った際の経過措置で、既存案件のメンバーに定例の
2463
- // 改名を強いないために置いたもの。新規案件は合図1つだけで運用する。
2464
- // ここでフォームを出し続けると「新しい定例は名前を登録して足す」と読まれ、
2465
- // 畳むはずの仕組みが案件をまたいで増えていく(Lambda 側でも同じ条件で断る)。
2466
- if ((s.meetingAliases || []).length) {
2467
- box.appendChild(addForm({
2468
- title: "+ 会議名をそのまま取り込み対象にする",
2469
- placeholder: "例: GDO様 定例",
2470
- requiredNote: "会議名を入力してください。",
2471
- // 先頭の1文が「これは増やすものではない」を伝える。使い方の説明はその後
2472
- hint: "既存の定例を改名せずに拾うための経過措置の設定です。新しい会議は会議名に合図(【…】)を入れてください。ここに入れた名前と**完全に一致する**会議だけが取り込まれます(部分一致はしません)。カレンダーに出ている会議名だけを入れてください——日付や「- Gemini によるメモ」のような後ろに付く文字列を含めると、どの会議とも一致しません。Bot の招待は別途必要です。",
2473
- confirmLines: function (v) {
2474
- return ["この名前の会議を取り込み対象にします。", "", v, "", "追加しますか?"];
2475
- },
2476
- build: function (alias) { return { op: "add-meeting-alias", alias: alias }; },
2477
- }));
2478
- }
2540
+ // 改名を強いないために置いたもの。第1弾では「既に別名がある案件にだけ追加口を出す」
2541
+ // としたが、実機確認でユーザーが**「最初に決めたキー(合図)が唯一真実」**と確定させた。
2542
+ // 経過措置の案件にフォームを残すと、その案件の中では別名が現役の登録手段に見え続け、
2543
+ // 合図と別名のどちらが正なのかが人によって揺れる。画面は合図1つだけを案内し、
2544
+ // 別名は上のチップ(読み取り専用の記録)で見せるに留める。Lambda 側も add/remove とも断る。
2479
2545
  used = true;
2480
2546
  } else if (s.kind === "会議" && usable("会議", "google-meet")) {
2481
2547
  if (s.ingestState === "unset") {
@@ -2534,6 +2600,11 @@ ${capabilityState.toString()}
2534
2600
  },
2535
2601
  build: function (url) { return { op: "set-backlog-target", url: url }; },
2536
2602
  }));
2603
+ // **接続先の下に置く。** リアルタイム同期は「どのプロジェクトを見るか」が
2604
+ // 決まった後の話で、順序が逆だと、接続先を設定していない案件が先に
2605
+ // Webhook を貼りに行く(貼っても届かない)
2606
+ var rt = realtimeSyncCard();
2607
+ if (rt) box.appendChild(rt);
2537
2608
  used = true;
2538
2609
  }
2539
2610
 
@@ -2698,7 +2769,7 @@ ${capabilityState.toString()}
2698
2769
  wrap.appendChild(el("p", { text: "次の2つが揃った会議だけが取り込まれます。" }));
2699
2770
  var ul = el("ul", { class: "sec-how-list" });
2700
2771
  ul.appendChild(el("li", { text: "cortex-notetaker Bot が招待されている(定例はシリーズに1回でOK)" }));
2701
- ul.appendChild(el("li", { text: "会議名に上の語のどれかが含まれている" }));
2772
+ ul.appendChild(el("li", { text: "会議名に上の目印が入っている" }));
2702
2773
  wrap.appendChild(ul);
2703
2774
  wrap.appendChild(el("p", { text: "どちらか一方だけでは取り込まれません。" }));
2704
2775
  return wrap;
@@ -3087,6 +3158,18 @@ ${capabilityState.toString()}
3087
3158
  return e;
3088
3159
  }
3089
3160
 
3161
+ /**
3162
+ * 登録の由来の表示(#118)。値は fleet-status.json の origin
3163
+ * (explicit … 登録ファイルに手で書いた / derived … .gitmodules 等からの自動導出 / both)。
3164
+ * **知らない値には何も出さない**(生の内部値を顧客の画面に出さないため)。
3165
+ */
3166
+ var ORIGIN_LABEL = { explicit: "明示登録", derived: "自動検出", both: "明示登録+自動検出" };
3167
+ var ORIGIN_HELP = {
3168
+ explicit: "開発の登録ファイル(dev-sources.json)に登録されているリポジトリです。ここから登録を削除できます。",
3169
+ derived: ".gitmodules から自動で見つかったリポジトリです。登録ファイルに無いので削除はできません(読み取りを止めることはできます)。",
3170
+ both: "登録ファイルにも書かれていて、.gitmodules からも見つかっているリポジトリです。",
3171
+ };
3172
+
3090
3173
  function extSourceCard(s, meta) {
3091
3174
  var badges = el("div", { class: "ds-badges" });
3092
3175
  var pending = pendingGoldFor(s);
@@ -3100,6 +3183,17 @@ ${capabilityState.toString()}
3100
3183
  else if (s.goldState === "undeclared") badges.appendChild(el("span", { class: "badge ds-undeclared", text: "未宣言(対象外)" }));
3101
3184
  else if (s.gold === false) badges.appendChild(el("span", { class: "badge ds-excluded", text: "除外" }));
3102
3185
  if (s.notify === true) badges.appendChild(el("span", { class: "badge ds-notify", text: "通知先" }));
3186
+ // 登録の由来(#118)。**「登録から削除」が出る/出ないの理由がこれ。** 導出されたリポは
3187
+ // 登録ファイルから消しても翌日また現れるので削除を出していないが、画面には
3188
+ // 「同じ見た目の行なのに、片方にだけ削除がある」としか映らず、毎回考え直すことになる。
3189
+ // GitHub の行だけに出す(削除の出し分けがあるのはここだけで、他は全部 explicit)
3190
+ if (String(s.type).indexOf("github") === 0 && ORIGIN_LABEL[s.origin]) {
3191
+ badges.appendChild(el("span", {
3192
+ class: "badge ds-origin",
3193
+ text: ORIGIN_LABEL[s.origin],
3194
+ title: ORIGIN_HELP[s.origin],
3195
+ }));
3196
+ }
3103
3197
  var gate = DS_GATE[s.gate] || DS_GATE["unknown"];
3104
3198
  var card = el("div", { class: "ds-card" }, [
3105
3199
  el("div", { class: "ds-card-main" }, [
@@ -3285,6 +3379,9 @@ ${capabilityState.toString()}
3285
3379
  // op=state 未対応のLambdaは 400 を返す。通信エラーだけを見ていると空表示になる。
3286
3380
  .then(function (res) { if (!res.ok) throw new Error("http " + res.status); return res.json(); })
3287
3381
  .then(function (j) {
3382
+ // **fleet より先に読む。** fleet-status.json をまだ持たない案件(立ち上げ直後)は
3383
+ // 下で throw するが、リアルタイム同期の可否は索引由来なので独立に決まる
3384
+ LIVE_REALTIME = (j && j.realtime) || null;
3288
3385
  if (!j || !j.fleet) throw new Error("no fleet");
3289
3386
  LIVE_FLEET = normalizeFleet(j.fleet);
3290
3387
  LIVE_FLEET_FAILED = false;
@@ -3325,6 +3422,75 @@ ${capabilityState.toString()}
3325
3422
  return null;
3326
3423
  }
3327
3424
 
3425
+ /**
3426
+ * リアルタイム同期(Backlog Webhook)の設定手順(#113)。
3427
+ *
3428
+ * **出すのは「手順どおり設定すれば実際に効く案件」だけ。** 判断は Lambda(op=state)が返す
3429
+ * realtime に委ねる——画面が持っている静的データからは「ルーティング索引に自分の行が
3430
+ * あるか」が分からないし、2箇所で判定すると片方だけがずれる。
3431
+ * - supported: false … 固定IP経由で同期している案件。**黙って欠けさせず**理由を1行出す
3432
+ * - ready: false … 索引にまだ行が無い。設定しても届かないので手順を出さない
3433
+ *
3434
+ * **URLに載せるトークンは案件別**(SITE.backlogWebhook)。艦隊共通トークンを載せると、
3435
+ * この画面を見られる人が全案件ぶんの偽イベントを投げられる(types.ts の backlogWebhook 参照)。
3436
+ */
3437
+ function realtimeSyncCard() {
3438
+ var rt = LIVE_REALTIME;
3439
+ if (!rt) return null; // 取得前・取得失敗・Backlogを使わない案件
3440
+ if (rt.supported === false) {
3441
+ return el("p", {
3442
+ class: "sec-how",
3443
+ text: "この案件は固定IP経由でBacklogを同期しているため、リアルタイム同期(Webhook)には未対応です。反映は毎晩の同期のタイミングになります。",
3444
+ });
3445
+ }
3446
+ var wh = SITE.backlogWebhook;
3447
+ if (!rt.ready || !wh || !wh.url || !wh.token) return null;
3448
+ var url = wh.url + (wh.url.indexOf("?") === -1 ? "?" : "&") + "t=" + encodeURIComponent(wh.token);
3449
+
3450
+ // URLは**選択できる形**で出す(コピーボタンが使えない環境でも手で取れるように)
3451
+ var field = el("input", { class: "sec-input rt-url", type: "text", readonly: "readonly", spellcheck: "false" });
3452
+ field.value = url;
3453
+ var copyBtn = el("button", { class: "ds-toggle-btn", type: "button", text: "コピー" });
3454
+ var msg = el("div", { class: "ds-toggle-msg" });
3455
+ copyBtn.addEventListener("click", function () {
3456
+ var done = function () { msg.className = "ds-toggle-msg is-ok"; msg.textContent = "✓ コピーしました。"; };
3457
+ var fallback = function () {
3458
+ // clipboard API が無い/拒否された環境(http・古いブラウザ・権限拒否)。
3459
+ // **黙って何も起きないにしない**——選択まではしてやって、次の一手を伝える
3460
+ try {
3461
+ field.focus();
3462
+ field.select();
3463
+ if (document.execCommand && document.execCommand("copy")) return done();
3464
+ } catch (e) { /* 下の案内へ */ }
3465
+ msg.className = "ds-toggle-msg is-error";
3466
+ msg.textContent = "⚠ 自動コピーできませんでした。選択された文字列を手でコピーしてください。";
3467
+ };
3468
+ try {
3469
+ if (navigator.clipboard && navigator.clipboard.writeText) {
3470
+ navigator.clipboard.writeText(url).then(done, fallback);
3471
+ } else fallback();
3472
+ } catch (e) { fallback(); }
3473
+ });
3474
+
3475
+ return el("div", { class: "ops-card rt-card" }, [
3476
+ el("div", { class: "ops-card-head" }, [
3477
+ el("span", { class: "ops-card-icon", text: "⚡" }),
3478
+ el("h3", { class: "ops-card-title", text: "リアルタイム同期" }),
3479
+ ]),
3480
+ el("p", { class: "ops-card-desc", text: "Backlog で課題やコメントが変わったときに、その内容をすぐ取り込みます(毎晩の同期を待たずに反映されます)。Backlog 側で一度だけ設定してください。" }),
3481
+ el("ol", { class: "rt-steps" }, [
3482
+ el("li", { text: "Backlog のプロジェクト設定 → インテグレーション → Webhook を開く" }),
3483
+ el("li", { text: "「WebHook URL」に下の案件専用URLを貼り付ける" }),
3484
+ el("li", { text: "通知イベントで「課題の追加」「課題の更新」「課題にコメント」等にチェックを入れて保存する" }),
3485
+ ]),
3486
+ el("div", { class: "rt-url-row" }, [field, copyBtn]),
3487
+ msg,
3488
+ // **この画面は顧客も見る。** URLに秘密が入っていることと、その扱いを書いておく
3489
+ el("p", { class: "sec-note", text: "このURLはこの案件専用です(他の案件には届きません)。秘密のトークンを含むので、貼り付け先は Backlog のこのプロジェクトの設定欄だけにしてください。" }),
3490
+ el("p", { class: "sec-how", text: "反映は受信から約1分後です(1分ごとのミニバッチでまとめて取り込みます)。初回は上の接続先の設定と、夜間同期が一度済んでいることが前提になります。" }),
3491
+ ]);
3492
+ }
3493
+
3328
3494
  /** 鍵の入力欄に出す説明。kind は Lambda 側(resolveSecretTarget)と綴りを合わせること */
3329
3495
  var SECRET_FORMS = {
3330
3496
  "figma": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.4.67",
3
+ "version": "0.4.68",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,