@takagaki/cortex-decisions-viewer 0.4.48 → 0.4.50

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
@@ -36,6 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.readFleetSources = readFleetSources;
39
40
  exports.build = build;
40
41
  const node_fs_1 = require("node:fs");
41
42
  const path = __importStar(require("node:path"));
@@ -140,6 +141,10 @@ async function readFleetSources(repoRoot) {
140
141
  goldState: s.goldState != null ? String(s.goldState) : undefined,
141
142
  notify: typeof s.notify === "boolean" ? s.notify : undefined,
142
143
  gate: s.gate != null ? String(s.gate) : undefined,
144
+ // どこから来たか(derived / explicit / both)。**削除を出してよいかの判断に要る。**
145
+ // 自動導出されたものは登録ファイルから消しても翌日また現れるので、
146
+ // explicit のときだけ削除ボタンを出す。
147
+ origin: s.origin != null ? String(s.origin) : undefined,
143
148
  }))
144
149
  : undefined;
145
150
  const internalSources = Array.isArray(obj.internalSources)
@@ -156,6 +161,20 @@ async function readFleetSources(repoRoot) {
156
161
  : undefined,
157
162
  driveSync: typeof s.driveSync === "boolean" ? s.driveSync : undefined,
158
163
  enabled: typeof s.enabled === "boolean" ? s.enabled : undefined,
164
+ // 会議・資料の ON/OFF の現在値。**ここに列挙しないと画面へ届かない**
165
+ // (このマッピングは素通しではなく明示の許可リスト)。
166
+ // 生成が古い fleet-status.json には無いので undefined になりうる=「不明」として扱う。
167
+ ingestState: s.ingestState != null ? String(s.ingestState) : undefined,
168
+ driveState: s.driveState != null ? String(s.driveState) : undefined,
169
+ driveFolderCount: typeof s.driveFolderCount === "number" ? s.driveFolderCount : undefined,
170
+ // 登録済みFigmaファイルの一覧。**1件ずつ外せるようにするために要る**
171
+ // (先頭1件のURLしか無かった頃は、6ファイル持つ案件でどれも選べなかった)。
172
+ figmaFiles: Array.isArray(s.figmaFiles)
173
+ ? s.figmaFiles
174
+ .filter((f) => f != null && typeof f === "object")
175
+ .map((f) => ({ key: String(f.key ?? ""), name: String(f.name ?? "") }))
176
+ .filter((f) => f.key)
177
+ : undefined,
159
178
  }))
160
179
  : undefined;
161
180
  const pipelines = Array.isArray(obj.pipelines)
package/dist/render.js CHANGED
@@ -747,6 +747,12 @@ a.ds-name:hover { text-decoration: underline; }
747
747
  .ds-toggle-btn { font-size: 12px; padding: 3px 10px; border-radius: 999px; border: 1px solid var(--border); background: #fff; color: var(--text); cursor: pointer; }
748
748
  .ds-toggle-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
749
749
  .ds-toggle-btn:disabled { opacity: .6; cursor: default; }
750
+ .ds-toggle-btn.ds-danger { color: #b42318; border-color: #f3c6c1; }
751
+ .ds-toggle-btn.ds-danger:hover:not(:disabled) { border-color: #b42318; color: #b42318; background: #fff5f4; }
752
+ .ds-subitems { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; }
753
+ .ds-subitem { border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; background: #fff; }
754
+ /* 削除できたことを行に残す。一覧そのものは翌朝まで更新されないので、消えたように見せない */
755
+ .ds-removed .ds-name { text-decoration: line-through; color: var(--muted); }
750
756
  .ds-toggle-msg { font-size: 12px; color: var(--muted); }
751
757
  .ds-toggle-msg.is-ok { color: #1a7f37; }
752
758
  .ds-toggle-msg.is-error { color: #b42318; }
@@ -770,6 +776,10 @@ a.ds-name:hover { text-decoration: underline; }
770
776
  .sec-state { font-size: 12.5px; font-weight: 600; margin: 0 0 8px; color: var(--muted); }
771
777
  .sec-state.is-managed { color: #1a7f37; }
772
778
  .sec-state.is-none { color: var(--muted); font-weight: 400; }
779
+ .ds-addform { border: 1px dashed var(--border); border-radius: 8px; padding: 12px 14px; margin-top: 10px; background: #fff; }
780
+ .ds-addform-title { font-size: 13px; font-weight: 700; color: var(--text); margin-bottom: 6px; }
781
+ .ds-addform .sec-input { margin-bottom: 6px; }
782
+ .sec-input-inline { width: auto; max-width: 220px; margin-bottom: 0; }
773
783
  .sec-how { color: var(--muted); font-size: 12px; line-height: 1.7; margin: 0 0 10px; }
774
784
  .sec-input { width: 100%; box-sizing: border-box; font-size: 13px; padding: 7px 10px; margin-bottom: 8px; border: 1px solid var(--border); border-radius: 6px; background: #fff; color: var(--text); }
775
785
  .sec-input:focus { outline: none; border-color: var(--accent); }
@@ -1397,14 +1407,35 @@ const CLIENT_JS = `
1397
1407
  internals.forEach(function (s) {
1398
1408
  // ラベルの下に補足行(会議の照合キー・Drive未設定の注記)を重ねられるよう縦組みにする
1399
1409
  var col = el("div", { class: "ds-col" }, [ dsNameEl(s.label || "", s.url) ]);
1400
- var keys = s.matchKeys || [];
1410
+ // 照合キーの表示はここ1箇所に集約する(下の案内と二重に出さない)。
1411
+ // **ON/OFFのトグルは出さない。** 取り込むかどうかは「Bot を会議に招待したか」で決まる
1412
+ // (Router.gs:「フィルタは持たない…招待が同意のゲート」)。enabled はルーティングの
1413
+ // 可否であって取り込みの可否ではないので、OFFにすると**Botは録り続けるのに
1414
+ // 文字起こしがINBOXに溜まる**という、招待しないより悪い状態を作れてしまう。
1415
+ // **案件キーを先頭に足す。** 照合は「案件キー → 顧客名 → meetingNamePatterns」の
1416
+ // 3段で行われるが、fleet-status が持つのは後ろ2つだけ。第1段は艦隊レジストリの
1417
+ // キーで、投入設定から取れる。運用規約「会議名に案件キーを含める」の本体なので、
1418
+ // これが抜けていると一番使われている目印が画面に出ない。
1419
+ var keys = [];
1420
+ // **自動取り込みが動いているときだけ出す。** Teams等で手動運用している案件に
1421
+ // 会議名の条件を出すと、自動で拾われると誤解させる(実際には拾われない)。
1422
+ if (s.kind === "会議" && s.ingestState === "on") {
1423
+ // 案件キーは艦隊レジストリのもので、案件リポ自身は知らない(fleet-status にも無い)。
1424
+ // ビルド時に注入される投入設定からしか取れないため、設定UIが未配線の案件では出ない。
1425
+ if (SITE.intake && SITE.intake.projectKey) keys.push(SITE.intake.projectKey);
1426
+ (s.matchKeys || []).forEach(function (k) {
1427
+ // 照合は大小文字を区別しないので、違いだけの重複は出さない(「cortex / Cortex」は雑音)
1428
+ var dup = keys.some(function (x) { return x.toLowerCase() === String(k).toLowerCase(); });
1429
+ if (!dup) keys.push(k);
1430
+ });
1431
+ }
1401
1432
  if (keys.length) {
1402
- var shown = keys.slice(0, 5).join(" / ");
1403
- if (keys.length > 5) shown += " 他" + (keys.length - 5) + "件";
1433
+ var shown = keys.slice(0, 6).join(" / ");
1434
+ if (keys.length > 6) shown += " 他" + (keys.length - 6) + "件";
1404
1435
  col.appendChild(el("span", {
1405
1436
  class: "ds-keys",
1406
1437
  text: "取り込み対象の会議名: " + shown,
1407
- title: "会議名にこれらの語が含まれていると、自動でこのCortexに取り込まれます",
1438
+ title: "会議名にこれらの語が含まれていると、この案件に振り分けられます",
1408
1439
  }));
1409
1440
  }
1410
1441
  if (s.driveSync === false) {
@@ -1426,6 +1457,9 @@ const CLIENT_JS = `
1426
1457
  ? el("span", { class: "ds-sync", text: "最終同期: " + dsFmtTime(s.lastSync) })
1427
1458
  : null,
1428
1459
  ]);
1460
+ // 会議・共有資料・デザインの操作をカード内に置く(その情報源の文脈で押せるように)
1461
+ var ctl = internalControls(s, inactive);
1462
+ if (ctl) card.appendChild(ctl);
1429
1463
  ilist.appendChild(card);
1430
1464
  });
1431
1465
  sec.appendChild(ilist);
@@ -1490,6 +1524,16 @@ const CLIENT_JS = `
1490
1524
  if (devOff) wrap.appendChild(inactiveSourceRow("🐙", "GitHub", "ds-開発"));
1491
1525
  sec.appendChild(wrap);
1492
1526
  }
1527
+ // **追加フォームは外部ソースの節の外に置く。** グループは0件だと描かれず、4件以上だと
1528
+ // 畳まれる(既定は閉)。さらに節そのものが「1件以上あるとき」しか描かれないので、
1529
+ // 中に入れると**新規案件(0件)=一番追加したい場面**で消える。
1530
+ var adders = renderExternalAdders();
1531
+ if (adders.childNodes.length) {
1532
+ if (!sources.length && !chatOff && !devOff) {
1533
+ sec.appendChild(el("div", { class: "ds-sub", text: "外部ソース(ライブ参照)" }));
1534
+ }
1535
+ sec.appendChild(adders);
1536
+ }
1493
1537
 
1494
1538
  // 注記は内部・外部の両グループの後ろに1つ(内部の直下だけに置くと外部ソースが
1495
1539
  // Gold昇格の対象外に見えるため)。除外の方法もここで案内する。
@@ -1549,82 +1593,449 @@ const CLIENT_JS = `
1549
1593
  * それを伝えないと「保存できていない」と受け取られるので、応答のメッセージをそのまま出す。
1550
1594
  */
1551
1595
  function goldToggle(s) {
1552
- var wrap = el("div", { class: "ds-toggle" });
1553
- var on = s.gold === true;
1554
- var btn = el("button", {
1555
- class: "ds-toggle-btn" + (on ? " is-on" : ""),
1556
- type: "button",
1557
- text: on ? "Gold昇格を止める" : "Gold昇格を始める",
1596
+ // **共通部品(stateToggle)に載せる。** 以前はここだけ変更者名をダイアログで聞いており、
1597
+ // 同じカードの中で2つの流儀が並んでいた。送信・エラー表示・向きの反転も個別実装で、
1598
+ // 片方だけ直したときにずれる状態だった。
1599
+ return stateToggle({
1600
+ on: s.gold === true,
1601
+ labelOn: "Gold昇格を止める",
1602
+ labelOff: "Gold昇格を始める",
1603
+ confirmOn: [
1604
+ "「" + (s.name || s.ref) + "」の内容を、顧客も見る Decision・用語集に蒸留するようになります。",
1605
+ "",
1606
+ "社内限定の会話が含まれるチャンネルでは、顧客に見せられない内容が出てしまう可能性があります。",
1607
+ "顧客がこのチャンネルにいる(=すでに会話を見ている)場合にだけ有効にしてください。",
1608
+ "",
1609
+ "※ 今夜の蒸留(22時ごろ)までに止めれば取り込まれません。一度生成された記録は自動では消えません。",
1610
+ "",
1611
+ "有効にしますか?",
1612
+ ],
1613
+ build: function (next) {
1614
+ return { op: "set-channel-gold", ref: s.ref, value: next };
1615
+ },
1558
1616
  });
1559
- var msg = el("div", { class: "ds-toggle-msg" });
1560
- wrap.appendChild(btn);
1561
- wrap.appendChild(msg);
1617
+ }
1562
1618
 
1563
- btn.addEventListener("click", async function () {
1564
- var next = !on;
1565
- if (next) {
1566
- var ok = window.confirm(
1567
- "「" + (s.name || s.ref) + "」の内容を、顧客も見る Decision・用語集に蒸留するようになります。" +
1568
- String.fromCharCode(10, 10) +
1569
- "社内限定の会話が含まれるチャンネルでは、顧客に見せられない内容が出てしまう可能性があります。" +
1570
- String.fromCharCode(10) +
1571
- "顧客がこのチャンネルにいる(=すでに会話を見ている)場合にだけ有効にしてください。" +
1572
- String.fromCharCode(10, 10) +
1573
- "※ 今夜の蒸留(22時ごろ)までに止めれば取り込まれません。一度生成された記録は自動では消えません。" +
1574
- String.fromCharCode(10, 10) +
1575
- "有効にしますか?",
1576
- );
1577
- if (!ok) return;
1619
+ /**
1620
+ * リポジトリ内の情報源に足す操作(会議の取り込みON/OFF・Driveフォルダ追加とON/OFF・Figmaファイル追加)。
1621
+ *
1622
+ * **状態が「不明」のときは操作を出さない。** fleet-status.json の生成が古い案件では
1623
+ * ingestState / driveState が入っていないので、ONともOFFとも言えない。
1624
+ * そこにトグルを出すと、押した結果が現在値と食い違って見える。
1625
+ *
1626
+ * **設定ファイルが無い(unset)ときも出さない。** 雛形から作れるのは外部ソースの登録だけで、
1627
+ * 会議・資料の設定は押しても404になる。リポジトリ側の整備が要ることを文字で伝える。
1628
+ */
1629
+ function internalControls(s, inactive) {
1630
+ // **inactive(未使用)でも出す。** 未使用=tools が none/未宣言 という状態で、
1631
+ // まさにここから設定を始めたい。以前は inactive で早期returnしており、
1632
+ // 「足したい案件でだけ足せない」という逆転になっていた。
1633
+ // 押しても必ず失敗するもの(別ツールが宣言済み)は usable() で弾く。
1634
+ if (!SITE.intake) return null;
1635
+ var tools = SITE.tools || {};
1636
+ var usable = function (cap, tool) { var v = tools[cap]; return !v || v === "none" || v === tool; };
1637
+ var box = el("div", {});
1638
+ var used = false;
1639
+
1640
+ // **ON になっている案件は、宣言(Home.md の tools)に関わらず案内を出す。**
1641
+ // 取り込みの実際のゲートは ingest-config.json の enabled で、中央の Apps Script
1642
+ // (Projects.gs)は Home.md から client しか読まない——tools は見ていない。
1643
+ // つまり「tools: teams なのに enabled: true」なら**実際に取り込まれる**ので、
1644
+ // 宣言で隠すと、動いているのに説明が出ない状態になる。
1645
+ if (s.kind === "会議" && s.ingestState === "on") {
1646
+ box.appendChild(meetingHowTo());
1647
+ used = true;
1648
+ } else if (s.kind === "会議" && usable("会議", "google-meet")) {
1649
+ if (s.ingestState === "unset") {
1650
+ box.appendChild(el("p", { class: "sec-how", text: "会議の取り込み設定がこのリポジトリにありません(ingest-config.json)。先に設置が必要です。" }));
1651
+ used = true;
1652
+ } else if (s.ingestState === "broken") {
1653
+ box.appendChild(el("p", { class: "sec-how", text: "会議の取り込み設定(ingest-config.json)が壊れているため、ここからは変更できません。手で直してください。" }));
1654
+ used = true;
1655
+ } else if (s.ingestState === "off") {
1656
+ // 自動取り込みを止めている案件(例: Teams を使っていて手動で取り込む運用)。
1657
+ // ここでトグルを出すと「押せば始まる」と読めるが、実際は設定ファイルの意思表示なので
1658
+ // 画面から気軽に反転させるものではない。状態だけを伝える。
1659
+ box.appendChild(el("p", { class: "sec-how", text: "この案件は会議の自動取り込みを行わない設定です(ingest-config.json の enabled が false)。" }));
1660
+ used = true;
1578
1661
  }
1579
- var editor = window.prompt("変更者のお名前を入力してください(履歴に残ります)", "");
1580
- if (editor === null) return;
1581
- if (!editor.trim()) { msg.className = "ds-toggle-msg is-error"; msg.textContent = "⚠ お名前を入力してください。"; return; }
1662
+ }
1582
1663
 
1583
- btn.disabled = true;
1584
- msg.className = "ds-toggle-msg";
1585
- msg.textContent = "保存中…";
1586
- var intake = SITE.intake;
1587
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
1588
- try {
1589
- var res = await fetch(pfx + "op=config&t=" + encodeURIComponent(intake.token), {
1590
- method: "POST",
1591
- headers: { "content-type": "application/json" },
1592
- body: JSON.stringify({
1593
- file: "チャット/channels.json",
1594
- op: "set-channel-gold",
1595
- ref: s.ref,
1596
- value: next,
1597
- editor: editor,
1598
- }),
1599
- });
1600
- var j = await res.json().catch(function () { return {}; });
1601
- if (!res.ok) {
1602
- msg.className = "ds-toggle-msg is-error";
1603
- msg.textContent = "" + (j.message || "保存できませんでした。");
1604
- btn.disabled = false;
1605
- return;
1664
+ if (s.kind === "共有資料" && usable("共有資料", "google-drive")) {
1665
+ if (s.driveState === "unset") {
1666
+ box.appendChild(el("p", { class: "sec-how", text: "資料の同期設定がこのリポジトリにありません(materials-config.json)。先に設置が必要です。" }));
1667
+ used = true;
1668
+ } else if (s.driveState === "broken") {
1669
+ // 壊れていると書き込みも失敗する。フォームを出して押させない
1670
+ box.appendChild(el("p", { class: "sec-how", text: "資料の同期設定(materials-config.json)が壊れているため、ここからは変更できません。手で直してください。" }));
1671
+ used = true;
1672
+ } else if (s.driveState) {
1673
+ // フォルダが1件も無いのに ON/OFF だけ出しても意味が無いので、追加フォームを先に出す
1674
+ box.appendChild(addForm({
1675
+ title: "+ Google Drive のフォルダを追加",
1676
+ placeholder: "https://drive.google.com/drive/folders/…",
1677
+ namePlaceholder: "メモ(任意)",
1678
+ requiredNote: "フォルダのURLを入力してください。",
1679
+ hint: "このフォルダの中身が 共有資料 にコピーされ、Markdown 化されます。取り込み用アカウントをそのフォルダの閲覧者に招待しておいてください(招待が無いと読めません)。",
1680
+ confirmLines: function (v) {
1681
+ return [
1682
+ "このフォルダの中身を、顧客も見るリポジトリにコピーします。",
1683
+ "",
1684
+ "貼ったURLがこの案件のものか、もう一度確認してください。",
1685
+ "",
1686
+ v,
1687
+ "",
1688
+ "追加しますか?",
1689
+ ];
1690
+ },
1691
+ build: function (url, name) { return { op: "add-drive-folder", url: url, name: name }; },
1692
+ }));
1693
+ // **ON/OFFのトグルは出さない。** フォルダを追加した時点で取り込みは始まる
1694
+ // (applyAddDriveFolder が enabled を立てる)ので「始める」を押す機会が無い。
1695
+ // 止めたいならフォルダを外す——登録そのものが意思表示、という会議と同じ考え方。
1696
+ var folders = s.driveFolderIds || [];
1697
+ if (folders.length) {
1698
+ var dlist = el("div", { class: "ds-subitems" });
1699
+ folders.forEach(function (id) {
1700
+ var row = el("div", { class: "ds-subitem" }, [
1701
+ dsNameEl(id, "https://drive.google.com/drive/folders/" + id),
1702
+ ]);
1703
+ row.appendChild(removeButton({
1704
+ label: "このフォルダを外す",
1705
+ confirmLines: function () {
1706
+ var lines = [
1707
+ "このフォルダを資料の取り込み対象から外します。",
1708
+ "",
1709
+ "次回の同期から、このフォルダの中身は取り込まれなくなります。",
1710
+ "すでに取り込まれた資料はリポジトリに残ります(不要なら手で削除してください)。",
1711
+ ];
1712
+ if (folders.length === 1) {
1713
+ lines.push("", "これが最後の1件です。外すと資料の自動取り込みは止まります。");
1714
+ }
1715
+ lines.push("", id, "", "外しますか?");
1716
+ return lines;
1717
+ },
1718
+ build: function () { return { op: "remove-drive-folder", id: id }; },
1719
+ onDone: function () { row.className = "ds-subitem ds-removed"; },
1720
+ }));
1721
+ dlist.appendChild(row);
1722
+ });
1723
+ box.appendChild(dlist);
1606
1724
  }
1607
- msg.className = "ds-toggle-msg is-ok";
1608
- msg.textContent = "✓ " + (j.message || "保存しました。");
1609
- // **押した内容をボタンに反映し、逆向きの操作を必ず残す。**
1610
- // バッジ(Gold昇格の読み取り対象/除外)は夜間バッチのスナップショット由来なので変えない。
1611
- // 変えると「もう効いている」と誤解させるため。変えるのはボタンだけ。
1612
- // ここを disabled のままにすると、誤って有効にした直後に**同じ画面から止められなくなる**。
1613
- // 表示が直るのは翌朝で、その間に22時のGold昇格が走り、生成されたレコードは自動削除されない
1614
- // (Gold層は追記専用)。危険な方向は押せるのに戻す方向が塞がる、という状態は許容できない。
1615
- on = next;
1616
- btn.textContent = on ? "Gold昇格を止める" : "Gold昇格を始める";
1617
- btn.className = "ds-toggle-btn" + (on ? " is-on" : "");
1618
- btn.disabled = false;
1619
- } catch (e) {
1725
+ used = true;
1726
+ }
1727
+ }
1728
+
1729
+ if (s.kind === "デザイン" && usable("デザイン", "figma")) {
1730
+ // 登録済みファイルを1件ずつ外せるようにする(6ファイル抱えている案件があり、
1731
+ // 古いスプリントのファイルを外したいのが実際に起きる操作)。
1732
+ var figs = s.figmaFiles || [];
1733
+ // **残り件数は「押した時点」で数える。** 一覧(figs)は夜間バッチのスナップショットで、
1734
+ // このページを開いている間はもちろん、その日のうちに再読み込みしても更新されない。
1735
+ // 描画時点の件数で判定すると、3件を1件ずつ外していったとき最後の1件でも警告が出ない。
1736
+ var figsLeft = figs.length;
1737
+ if (figs.length) {
1738
+ var flist = el("div", { class: "ds-subitems" });
1739
+ figs.forEach(function (f) {
1740
+ var row = el("div", { class: "ds-subitem" }, [
1741
+ dsNameEl(f.name || f.key, "https://www.figma.com/design/" + f.key),
1742
+ ]);
1743
+ row.appendChild(removeButton({
1744
+ label: "このファイルを外す",
1745
+ confirmLines: function () {
1746
+ var lines = [
1747
+ "「" + (f.name || f.key) + "」をデザイン同期の対象から外します。",
1748
+ "",
1749
+ "次回の同期から、このファイルの画面は画面インベントリに現れなくなります。",
1750
+ ];
1751
+ // **最後の1件は結果が違う。** 同期は空の設定だと何もせずに終わるので、
1752
+ // 取り込み済みの画面インベントリが消えずに残る(sync_designs.py は
1753
+ // 全再生成の手前で早期returnする)。ここで伝えないと放置される。
1754
+ if (figsLeft === 1) {
1755
+ lines.push(
1756
+ "",
1757
+ "これが最後の1件です。外すとデザイン同期は何もしなくなりますが、すでに取り込んだ画面インベントリはリポジトリに残ります(不要なら手で削除してください)。",
1758
+ );
1759
+ }
1760
+ lines.push("", "外しますか?");
1761
+ return lines;
1762
+ },
1763
+ build: function () { return { op: "remove-figma-file", key: f.key }; },
1764
+ onDone: function () {
1765
+ row.className = "ds-subitem ds-removed";
1766
+ figsLeft -= 1;
1767
+ },
1768
+ }));
1769
+ flist.appendChild(row);
1770
+ });
1771
+ box.appendChild(flist);
1772
+ }
1773
+ box.appendChild(addForm({
1774
+ title: "+ Figmaファイルを追加",
1775
+ placeholder: "https://www.figma.com/design/…",
1776
+ namePlaceholder: "メモ(任意)",
1777
+ requiredNote: "FigmaのURLを入力してください。",
1778
+ hint: "このファイルの画面インベントリ(画面名・サムネイル)が毎晩同期され、顧客も見るリポジトリに入ります。",
1779
+ confirmLines: function (v) {
1780
+ return [
1781
+ "このFigmaファイルの画面名とサムネイルを、顧客も見るリポジトリに取り込みます。",
1782
+ "",
1783
+ "貼ったURLがこの案件のものか、もう一度確認してください。",
1784
+ "",
1785
+ v,
1786
+ "",
1787
+ "追加しますか?",
1788
+ ];
1789
+ },
1790
+ build: function (url, name) { return { op: "add-figma-file", url: url, name: name }; },
1791
+ }));
1792
+ used = true;
1793
+ }
1794
+
1795
+ return used ? box : null;
1796
+ }
1797
+
1798
+ /**
1799
+ * 会議の取り込みの案内。**操作ではなく条件を示す。**
1800
+ *
1801
+ * 取り込まれるには2つとも要る:
1802
+ * ① cortex-notetaker Bot が会議に招待されている(=取り込む意思。これが同意のゲート)
1803
+ * ② 会議名が照合キーのどれかを含む(含まなければ、録れても案件に振り分けられない)
1804
+ *
1805
+ * ②の材料を画面が出さないと、招待したのに届かない理由が分からない。
1806
+ */
1807
+ function meetingHowTo() {
1808
+ // 会議名の条件は上の行(ds-keys)が出す。ここは「誰を招待するか」だけを言う
1809
+ return el("p", {
1810
+ class: "sec-how",
1811
+ text:
1812
+ "会議に cortex-notetaker Bot を招待すると、その会議の文字起こしが取り込まれ、議事録が作られます。" +
1813
+ "招待しなければ取り込まれません(招待が取り込みの合図です)。上の会議名を含む会議が対象です。",
1814
+ });
1815
+ }
1816
+
1817
+ /**
1818
+ * 外部ソースの追加フォーム(Slack・GitHub)。
1819
+ *
1820
+ * **tools の宣言でゲートする。** 別のツールが宣言されている案件(チャット: teams 等)では
1821
+ * サーバが必ず400で断るので、押せるのに必ず失敗するボタンを出さない。
1822
+ */
1823
+ function renderExternalAdders() {
1824
+ var box = el("div", {});
1825
+ if (!SITE.intake) return box;
1826
+ var tools = SITE.tools || {};
1827
+ var usable = function (cap, tool) { var v = tools[cap]; return !v || v === "none" || v === tool; };
1828
+
1829
+ if (usable("チャット", "slack")) {
1830
+ box.appendChild(addForm({
1831
+ title: "+ Slackチャンネルを追加",
1832
+ placeholder: "https://…slack.com/archives/C0XXXXXXX",
1833
+ namePlaceholder: "表示名(例: #案件-共有)",
1834
+ needsName: true,
1835
+ requiredNote: "チャンネルのURLを入力してください。",
1836
+ hint: "チャンネル名を右クリック →「リンクをコピー」で出るURLを貼ってください。追加しただけでは Gold 昇格には載りません(載せるには各チャンネルの「Gold昇格を始める」を押します)。",
1837
+ // **「Goldに載る」と書かない。** 追加だけでは載らないので嘘になる。
1838
+ // 確認する理由は「中央Botで読みに行く」「他案件の取り違え」。
1839
+ confirmLines: function (v) {
1840
+ return [
1841
+ "このチャンネルを、Cortex が読みに行く対象に追加します。",
1842
+ "",
1843
+ "貼ったURLがこの案件のものか、もう一度確認してください。",
1844
+ "",
1845
+ v,
1846
+ "",
1847
+ "追加しますか?",
1848
+ ];
1849
+ },
1850
+ build: function (url, name) { return { op: "add-channel", url: url, name: name }; },
1851
+ }));
1852
+ }
1853
+
1854
+ if (usable("開発", "github")) {
1855
+ box.appendChild(addForm({
1856
+ title: "+ GitHubリポジトリを追加",
1857
+ placeholder: "owner/repo または https://github.com/owner/repo",
1858
+ namePlaceholder: "メモ(任意)",
1859
+ requiredNote: "リポジトリを入力してください。",
1860
+ hint: "このリポジトリの Issues が、毎晩の Gold 昇格の材料になります。",
1861
+ confirmLines: function (v) {
1862
+ return [
1863
+ "このリポジトリの Issues を、Gold 昇格の材料に追加します。",
1864
+ "",
1865
+ "追加した時点で読み取りの対象になります。",
1866
+ "Issues の内容から、顧客も見る Decision・用語集が作られます。",
1867
+ "",
1868
+ v,
1869
+ "",
1870
+ "追加しますか?",
1871
+ ];
1872
+ },
1873
+ build: function (repo, name) { return { op: "add-github-source", repo: repo, name: name }; },
1874
+ }));
1875
+ }
1876
+ return box;
1877
+ }
1878
+
1879
+ // ---------- 設定の変更(?op=config)の共通部品 ----------
1880
+ //
1881
+ // 確認 → 送信 → 結果表示、までを1箇所に置く。3つの操作で書き写すと、
1882
+ // 片方だけ直したときにずれる(既存の goldToggle で使っている作法をそのまま一般化したもの)。
1883
+
1884
+ /**
1885
+ * サーバへ設定変更を送る。**成功しても一覧やバッジは書き換えない**
1886
+ * (接続状況は夜間バッチのスナップショットなので、「もう効いている」と誤解させないため)。
1887
+ * 呼び出し側は onDone で自分のボタンの向きだけを反転させる。
1888
+ */
1889
+ async function sendConfig(payload, msg, btn, onDone) {
1890
+ var intake = SITE.intake;
1891
+ if (!intake) return;
1892
+ var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
1893
+ btn.disabled = true;
1894
+ msg.className = "ds-toggle-msg";
1895
+ msg.textContent = "保存中…";
1896
+ try {
1897
+ var res = await fetch(pfx + "op=config&t=" + encodeURIComponent(intake.token), {
1898
+ method: "POST",
1899
+ headers: { "content-type": "application/json" },
1900
+ body: JSON.stringify(payload),
1901
+ });
1902
+ var j = await res.json().catch(function () { return {}; });
1903
+ if (!res.ok) {
1904
+ // サーバの文言をそのまま出す(貼り方の案内・取り違えの警告を含んでいる)
1620
1905
  msg.className = "ds-toggle-msg is-error";
1621
- msg.textContent = "✗ 通信に失敗しました。";
1906
+ msg.textContent = "✗ " + (j.message || "保存できませんでした。");
1622
1907
  btn.disabled = false;
1908
+ return;
1909
+ }
1910
+ msg.className = "ds-toggle-msg is-ok";
1911
+ msg.textContent = "";
1912
+ msg.appendChild(document.createTextNode("✓ " + (j.message || "保存しました。")));
1913
+ // **起動できたときだけ導線を出す。** 押した直後に「効いたか」を確かめられるのが要点。
1914
+ // 起動できなかった案件(権限・スタブ未配布)ではリンクを出さない——押しても何も無い
1915
+ if (j.runsUrl) {
1916
+ msg.appendChild(document.createTextNode(" "));
1917
+ msg.appendChild(el("a", { href: j.runsUrl, target: "_blank", rel: "noopener", text: "実行状況を見る →" }));
1623
1918
  }
1919
+ btn.disabled = false;
1920
+ if (onDone) onDone();
1921
+ } catch (e) {
1922
+ msg.className = "ds-toggle-msg is-error";
1923
+ msg.textContent = "✗ 通信に失敗しました。";
1924
+ btn.disabled = false;
1925
+ }
1926
+ }
1927
+
1928
+ /**
1929
+ * 危険な方向にだけ出す確認。**何が起きるかを具体に書く**(「よろしいですか」だけにしない)。
1930
+ *
1931
+ * **不安を煽らない。** 「全案件共通のアカウントで読むので他案件のものも指定できてしまいます」と
1932
+ * 書いていたが、取り違えはサーバ側が409で実際に弾く(findRefInOtherProjects)。
1933
+ * 実在する守りがあるのに脅し文を重ねると、操作者が萎縮するだけで安全は増えない。
1934
+ * 「貼ったURLがこの案件のものか確認してください」で足りる。
1935
+ */
1936
+ function confirmDanger(lines) {
1937
+ return window.confirm(lines.join(String.fromCharCode(10)));
1938
+ }
1939
+
1940
+ /**
1941
+ * ON/OFF のトグル。**戻す操作を必ず残す**(誤って有効にしたとき翌朝まで止められないのは許容できない)。
1942
+ * @param opts { on, labelOn, labelOff, confirmOn, build(next) }
1943
+ */
1944
+ function stateToggle(opts) {
1945
+ var wrap = el("div", { class: "ds-toggle" });
1946
+ var on = opts.on === true;
1947
+ var btn = el("button", { class: "ds-toggle-btn" + (on ? " is-on" : ""), type: "button" });
1948
+ var msg = el("div", { class: "ds-toggle-msg" });
1949
+ var paint = function () {
1950
+ btn.textContent = on ? opts.labelOn : opts.labelOff;
1951
+ btn.className = "ds-toggle-btn" + (on ? " is-on" : "");
1952
+ };
1953
+ paint();
1954
+
1955
+ btn.addEventListener("click", function () {
1956
+ var next = !on;
1957
+ // 読み始める方向(危険)だけ確認する。止める方向は素通し
1958
+ if (next && opts.confirmOn && !confirmDanger(opts.confirmOn)) { msg.textContent = ""; return; }
1959
+ sendConfig(opts.build(next), msg, btn, function () {
1960
+ // **押した内容をボタンに反映し、逆向きの操作を残す。** バッジや一覧は触らない
1961
+ on = next;
1962
+ paint();
1963
+ });
1624
1964
  });
1965
+ wrap.appendChild(btn);
1966
+ wrap.appendChild(msg);
1625
1967
  return wrap;
1626
1968
  }
1627
1969
 
1970
+ /**
1971
+ * 削除ボタン。**必ず確認を出す**(stateToggle は「広げる方向」だけ確認するが、削除は
1972
+ * 向きに関わらず取り消せないので常に確認する)。
1973
+ *
1974
+ * 成功しても一覧からは消さない。接続状況は夜間バッチのスナップショットで、消したつもりの行が
1975
+ * 翌朝まで残るのが正しい状態のため——行に取り消し線を引き、ボタンだけを無効にする
1976
+ * (二度押しすると「登録されていません」で失敗するので、押せなくする)。
1977
+ * @param opts { label, confirmLines(), build(), onDone() }
1978
+ */
1979
+ function removeButton(opts) {
1980
+ var wrap = el("div", { class: "ds-toggle" });
1981
+ var btn = el("button", { class: "ds-toggle-btn ds-danger", type: "button", text: opts.label || "削除" });
1982
+ var msg = el("div", { class: "ds-toggle-msg" });
1983
+
1984
+ btn.addEventListener("click", function () {
1985
+ if (!confirmDanger(opts.confirmLines())) { msg.textContent = ""; return; }
1986
+ sendConfig(opts.build(), msg, btn, function () {
1987
+ btn.disabled = true;
1988
+ if (opts.onDone) opts.onDone();
1989
+ });
1990
+ });
1991
+
1992
+ wrap.appendChild(btn);
1993
+ wrap.appendChild(msg);
1994
+ return wrap;
1995
+ }
1996
+
1997
+ /**
1998
+ * 「+…を追加」のフォーム。URLをそのまま貼れるようにする(IDだけを要求すると転記ミスが増える)。
1999
+ * @param opts { title, placeholder, hint, needsName, confirmLines, build(values) }
2000
+ */
2001
+ function addForm(opts) {
2002
+ var main = el("input", { class: "sec-input", type: "text", placeholder: opts.placeholder, autocomplete: "off" });
2003
+ var name = opts.needsName
2004
+ ? el("input", { class: "sec-input", type: "text", placeholder: opts.namePlaceholder || "表示名", autocomplete: "off" })
2005
+ : null;
2006
+ var btn = el("button", { class: "ds-toggle-btn", type: "button", text: "追加" });
2007
+ var msg = el("div", { class: "ds-toggle-msg" });
2008
+
2009
+ btn.addEventListener("click", function () {
2010
+ if (!main.value.trim()) {
2011
+ msg.className = "ds-toggle-msg is-error";
2012
+ msg.textContent = "⚠ " + (opts.requiredNote || "入力してください。");
2013
+ return;
2014
+ }
2015
+ if (name && !name.value.trim()) {
2016
+ msg.className = "ds-toggle-msg is-error";
2017
+ msg.textContent = "⚠ 表示名を入力してください。";
2018
+ return;
2019
+ }
2020
+ if (opts.confirmLines && !confirmDanger(opts.confirmLines(main.value.trim()))) { msg.textContent = ""; return; }
2021
+ var payload = opts.build(main.value.trim(), name ? name.value.trim() : "");
2022
+ sendConfig(payload, msg, btn, function () {
2023
+ // 追加できたら入力欄を空にする(同じものを二度押ししないように)
2024
+ main.value = "";
2025
+ if (name) name.value = "";
2026
+ });
2027
+ });
2028
+
2029
+ var box = el("div", { class: "ds-addform" }, [
2030
+ el("div", { class: "ds-addform-title", text: opts.title }),
2031
+ opts.hint ? el("p", { class: "sec-how", text: opts.hint }) : null,
2032
+ main,
2033
+ name,
2034
+ el("div", { class: "ds-toggle" }, [btn, msg]),
2035
+ ]);
2036
+ return box;
2037
+ }
2038
+
1628
2039
  function extSourceCard(s, meta) {
1629
2040
  var badges = el("div", { class: "ds-badges" });
1630
2041
  // 「意図的に外した」と「宣言し忘れ」を分けて出す。同じ「除外」と書くと、
@@ -1648,6 +2059,47 @@ const CLIENT_JS = `
1648
2059
  if (SITE.intake && s.type === "slack" && s.ref) {
1649
2060
  card.appendChild(goldToggle(s));
1650
2061
  }
2062
+ // GitHubリポの読み取り ON/OFF。OFF は exclude で表現される(導出されたリポも確実に落ちる)。
2063
+ if (SITE.intake && s.type.indexOf("github") === 0 && s.ref) {
2064
+ card.appendChild(stateToggle({
2065
+ on: s.goldState !== "excluded",
2066
+ labelOn: "読み取りを止める",
2067
+ labelOff: "読み取りを始める",
2068
+ confirmOn: [
2069
+ "このリポジトリの Issues を、Gold 昇格の材料に戻します。",
2070
+ "",
2071
+ "Issues の内容から、顧客も見る Decision・用語集が作られます。",
2072
+ "",
2073
+ s.ref,
2074
+ "",
2075
+ "有効にしますか?",
2076
+ ],
2077
+ build: function (next) {
2078
+ return { op: "set-github-source-enabled", repo: s.ref, value: next };
2079
+ },
2080
+ }));
2081
+ // **削除は「手で登録したもの」にだけ出す。** .gitmodules 等から自動で見つかったリポは
2082
+ // 登録ファイルから消しても翌日また現れる(=消えないのに消せたように見える)。
2083
+ // 止めたいだけなら上の「読み取りを止める」で足りる。
2084
+ if (s.origin === "explicit") {
2085
+ card.appendChild(removeButton({
2086
+ label: "登録から削除",
2087
+ confirmLines: function () {
2088
+ return [
2089
+ "「" + s.ref + "」を、読み取り対象の登録から削除します。",
2090
+ "",
2091
+ "読み取りを一時的に止めたいだけなら「読み取りを止める」で足ります(あとから戻せます)。",
2092
+ "",
2093
+ "すでに作られた Decision・用語集は消えません。",
2094
+ "",
2095
+ "削除しますか?",
2096
+ ];
2097
+ },
2098
+ build: function () { return { op: "remove-github-source", repo: s.ref }; },
2099
+ onDone: function () { card.classList.add("ds-removed"); },
2100
+ }));
2101
+ }
2102
+ }
1651
2103
  return card;
1652
2104
  }
1653
2105
 
@@ -1717,15 +2169,12 @@ const CLIENT_JS = `
1717
2169
  : el("p", { class: "sec-state is-none", text: "現在: 未設定" });
1718
2170
 
1719
2171
  var input = el("input", { class: "sec-input", type: "password", placeholder: "トークンを貼り付け", autocomplete: "off" });
1720
- var who = el("input", { class: "sec-input", type: "text", placeholder: "変更者のお名前(履歴に残ります)", autocomplete: "off" });
1721
2172
  var btn = el("button", { class: "ds-toggle-btn", type: "button", text: "保存" });
1722
2173
  var msg = el("div", { class: "ds-toggle-msg" });
1723
2174
 
1724
2175
  btn.addEventListener("click", async function () {
1725
2176
  var value = input.value;
1726
- var editor = who.value;
1727
2177
  if (!value.trim()) { msg.className = "ds-toggle-msg is-error"; msg.textContent = "⚠ トークンを入力してください。"; return; }
1728
- if (!editor.trim()) { msg.className = "ds-toggle-msg is-error"; msg.textContent = "⚠ お名前を入力してください。"; return; }
1729
2178
 
1730
2179
  btn.disabled = true;
1731
2180
  msg.className = "ds-toggle-msg";
@@ -1737,7 +2186,7 @@ const CLIENT_JS = `
1737
2186
  method: "POST",
1738
2187
  headers: { "content-type": "application/json" },
1739
2188
  // **シークレット名は送らない。** どの連携かだけを送り、書き込み先はサーバが案件から決める。
1740
- body: JSON.stringify({ projectKey: intake.projectKey, kind: kind, value: value, editor: editor }),
2189
+ body: JSON.stringify({ projectKey: intake.projectKey, kind: kind, value: value }),
1741
2190
  });
1742
2191
  var j = await res.json().catch(function () { return {}; });
1743
2192
  if (!res.ok) {
@@ -1747,8 +2196,13 @@ const CLIENT_JS = `
1747
2196
  return;
1748
2197
  }
1749
2198
  msg.className = "ds-toggle-msg is-ok";
1750
- msg.textContent = "✓ " + (j.message || "保存しました。");
1751
- // 入れた値は画面に残さない(肩越しに見えるのを防ぐ)。お名前は続けて直せるよう残す。
2199
+ msg.appendChild(document.createTextNode("✓ " + (j.message || "保存しました。")));
2200
+ // 起動できたときだけ、実行状況への導線を出す(鍵を入れた直後に効いたか確かめられる)
2201
+ if (j.runsUrl) {
2202
+ msg.appendChild(document.createTextNode(" "));
2203
+ msg.appendChild(el("a", { href: j.runsUrl, target: "_blank", rel: "noopener", text: "実行状況を見る →" }));
2204
+ }
2205
+ // 入れた値は画面に残さない(肩越しに見えるのを防ぐ)
1752
2206
  input.value = "";
1753
2207
  btn.disabled = false;
1754
2208
  } catch (e) {
@@ -1767,7 +2221,6 @@ const CLIENT_JS = `
1767
2221
  state,
1768
2222
  el("p", { class: "sec-how", text: meta.how }),
1769
2223
  input,
1770
- who,
1771
2224
  el("div", { class: "ds-toggle" }, [btn, msg]),
1772
2225
  ]);
1773
2226
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.4.48",
3
+ "version": "0.4.50",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -20,7 +20,8 @@
20
20
  "scripts": {
21
21
  "build": "tsc -p tsconfig.json",
22
22
  "prepublishOnly": "npm run build",
23
- "demo": "npm run build && node dist/cli.js build ../../aidd-project-cortex/Cortex/Decisions/records --out .demo-site"
23
+ "demo": "npm run build && node dist/cli.js build ../../aidd-project-cortex/Cortex/Decisions/records --out .demo-site",
24
+ "test": "npm run build && node --test test/*.test.mjs"
24
25
  },
25
26
  "dependencies": {
26
27
  "commander": "^12.1.0",
@@ -29,6 +30,7 @@
29
30
  },
30
31
  "devDependencies": {
31
32
  "@types/node": "^22.10.0",
33
+ "jsdom": "^29.1.1",
32
34
  "linkedom": "^0.18.13",
33
35
  "typescript": "^5.7.0"
34
36
  }