@takagaki/cortex-decisions-viewer 0.4.24 → 0.4.25

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 +180 -17
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -17,6 +17,8 @@ function renderSite(site) {
17
17
  // ※ 投入フォーム設定 site.intake は build.ts が --intake-url/--intake-token/--project-key から付与する。
18
18
  const dataJson = JSON.stringify(site).replace(/</g, "\\u003c");
19
19
  const safeTitle = escapeHtml(site.title);
20
+ // 接続マップ(操作タブ最上部のSVG図)。ビルド時に生成し<template>で埋め込む。fleet情報が無ければ出さない
21
+ const connMap = renderConnectionMap(site);
20
22
  const githubBtn = site.repoBaseUrl
21
23
  ? `<a class="gh-btn" href="${escapeHtml(site.repoBaseUrl)}" target="_blank" rel="noopener" title="コンテキストリポジトリをGitHubで開く">${GITHUB_ICON}</a>`
22
24
  : "";
@@ -45,7 +47,7 @@ function renderSite(site) {
45
47
  <footer class="site-footer">
46
48
  <span id="footer-meta"></span>
47
49
  </footer>
48
- <script type="application/json" id="site-data">${dataJson}</script>
50
+ ${connMap ? `<template id="conn-map">${connMap}</template>\n` : ""}<script type="application/json" id="site-data">${dataJson}</script>
49
51
  <script>${CLIENT_JS}</script>
50
52
  </body>
51
53
  </html>`;
@@ -58,6 +60,106 @@ function escapeHtml(s) {
58
60
  .replace(/>/g, "&gt;")
59
61
  .replace(/"/g, "&quot;");
60
62
  }
63
+ /** 接続マップの能力定義(表示順・絵文字は詳細カードの流儀を踏襲) */
64
+ const CM_CAPS = [
65
+ { cap: "課題管理", icon: "📋", pipe: /sync-backlog/ },
66
+ { cap: "会議", icon: "🎙", pipe: /ingest-minutes/ },
67
+ { cap: "共有資料", icon: "📁", pipe: /sync-materials/ },
68
+ { cap: "デザイン", icon: "🎨", pipe: /sync-designs|update-design-notes/ },
69
+ { cap: "チャット", icon: "💬", pipe: null },
70
+ { cap: "開発", icon: "🐙", pipe: null },
71
+ ];
72
+ /** ツールid→表示名(接続マップのノードラベル用) */
73
+ const CM_TOOL_LABEL = {
74
+ backlog: "Backlog",
75
+ jira: "Jira",
76
+ "google-meet": "Google Meet",
77
+ teams: "Teams",
78
+ "google-drive": "Google Drive",
79
+ box: "Box",
80
+ local: "ローカル",
81
+ figma: "Figma",
82
+ slack: "Slack",
83
+ github: "GitHub",
84
+ };
85
+ /**
86
+ * 接続マップ(データソースとの接続を視覚化したSVG)をビルド時に生成する。
87
+ * 上段に6アダプター(能力)ノード、下中央にリポジトリノード、間をベジェの破線で結ぶ。
88
+ * 状態: 健全=色付き+流れるアニメーション / 問題あり=黄+⚠️ / 未使用=グレー。
89
+ * fleet-status由来の情報が何も無ければ null(マップを出さない・後方互換)。
90
+ */
91
+ function renderConnectionMap(site) {
92
+ const internals = site.internalSources ?? [];
93
+ const externals = site.externalSources ?? [];
94
+ const pipes = site.pipelines ?? [];
95
+ const tools = site.tools ?? {};
96
+ if (!internals.length && !externals.length && !pipes.length && !Object.keys(tools).length) {
97
+ return null;
98
+ }
99
+ const slackExts = externals.filter((s) => s.type === "slack");
100
+ const ghExts = externals.filter((s) => s.type.startsWith("github"));
101
+ const nodes = [];
102
+ const links = [];
103
+ const W = 960;
104
+ const REPO_X = W / 2;
105
+ CM_CAPS.forEach((c, i) => {
106
+ const cx = 80 + i * 160;
107
+ const toolVal = tools[c.cap];
108
+ const internal = internals.find((s) => s.kind === c.cap);
109
+ const isLive = c.cap === "チャット" || c.cap === "開発";
110
+ const liveExts = c.cap === "チャット" ? slackExts : c.cap === "開発" ? ghExts : [];
111
+ // 使用中か: tools宣言がnone以外 / internalSourceが存在(enabled:false除く) / ライブ参照の実体がある
112
+ const used = (toolVal != null && toolVal !== "none") ||
113
+ (internal != null && internal.enabled !== false) ||
114
+ (isLive && liveExts.length > 0);
115
+ const off = toolVal === "none" || internal?.enabled === false || !used;
116
+ // 問題あり: ライブ参照はgate≠okが1件以上、同期ソースは対応パイプラインの直近runがfailure
117
+ const warn = !off &&
118
+ (isLive
119
+ ? liveExts.some((s) => s.gate !== "ok")
120
+ : pipes.some((p) => c.pipe != null && c.pipe.test(p.id) && p.applicable !== false && p.lastConclusion === "failure"));
121
+ const state = off ? "cm-off" : warn ? "cm-warn" : "cm-ok";
122
+ const toolName = toolVal && toolVal !== "none"
123
+ ? CM_TOOL_LABEL[toolVal] ?? toolVal
124
+ : internal
125
+ ? CM_TOOL_LABEL[internal.tool] ?? internal.tool
126
+ : isLive && liveExts.length
127
+ ? CM_TOOL_LABEL[c.cap === "チャット" ? "slack" : "github"]
128
+ : c.cap;
129
+ const label = `${c.icon} ${off && (toolVal === "none" || !toolName) ? c.cap : toolName}`;
130
+ // ノード下の補足: 未使用 / ライブ参照 / 最終同期(時刻はクライアントでローカルタイム整形するため data-ts で渡す)
131
+ let sub = "";
132
+ if (off) {
133
+ sub = `<text class="cm-sync" x="${cx}" y="60" text-anchor="middle">未使用</text>`;
134
+ }
135
+ else if (isLive) {
136
+ sub = `<text class="cm-sync" x="${cx}" y="60" text-anchor="middle">ライブ参照</text>`;
137
+ }
138
+ else if (internal?.lastSync) {
139
+ sub = `<text class="cm-sync" x="${cx}" y="60" text-anchor="middle" data-ts="${escapeHtml(internal.lastSync)}"></text>`;
140
+ }
141
+ const badge = warn ? `<text class="cm-badge" x="${cx + 62}" y="26" text-anchor="middle">⚠️</text>` : "";
142
+ // クリックでページ内の該当詳細カード(アンカー)へスクロールする
143
+ const anchor = `ds-${c.cap}`;
144
+ nodes.push(`<g class="cm-node ${state}" data-target="${anchor}" tabindex="0" role="link" aria-label="${escapeHtml(c.cap)}の詳細へ">` +
145
+ `<rect x="${cx - 74}" y="16" width="148" height="54" rx="10"></rect>` +
146
+ `<text class="cm-label" x="${cx}" y="40" text-anchor="middle">${escapeHtml(label)}</text>` +
147
+ sub +
148
+ badge +
149
+ `</g>`);
150
+ links.push(`<path class="cm-link ${state}" d="M ${cx} 72 C ${cx} 150, ${REPO_X} 160, ${REPO_X} 238"></path>`);
151
+ });
152
+ const repoTitle = site.title.length > 24 ? site.title.slice(0, 23) + "…" : site.title;
153
+ const repo = `<g class="cm-repo">` +
154
+ `<rect x="${REPO_X - 170}" y="240" width="340" height="52" rx="12"></rect>` +
155
+ `<text class="cm-repo-label" x="${REPO_X}" y="272" text-anchor="middle">📦 コンテキストリポジトリ(${escapeHtml(repoTitle)})</text>` +
156
+ `</g>`;
157
+ return (`<svg class="cm-svg" viewBox="0 0 ${W} 312" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="データソースとの接続マップ">` +
158
+ links.join("") +
159
+ nodes.join("") +
160
+ repo +
161
+ `</svg>`);
162
+ }
61
163
  const CSS = `
62
164
  :root {
63
165
  --bg: #f6f7f9;
@@ -360,6 +462,30 @@ a.ds-name:hover { text-decoration: underline; }
360
462
  .ds-group > .ds-list { margin-top: 8px; padding-left: 12px; border-left: 2px solid var(--border); }
361
463
  .badge.ds-sum-ok { background: #e9f7ef; color: #1f7a45; }
362
464
  .badge.ds-sum-warn { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
465
+ /* ---- 接続マップ(操作タブ最上部のSVG図) ---- */
466
+ .conn-map { overflow-x: auto; margin: 4px 0 20px; }
467
+ .conn-map .cm-svg { display: block; min-width: 760px; width: 100%; height: auto; font-family: inherit; }
468
+ .cm-node { cursor: pointer; outline: none; }
469
+ .cm-node rect { fill: var(--surface); stroke: var(--border); stroke-width: 1.5; }
470
+ .cm-node:hover rect, .cm-node:focus rect { stroke: var(--accent); }
471
+ .cm-node.cm-ok rect { stroke: var(--accent); }
472
+ .cm-node.cm-warn rect { fill: #fffbeb; stroke: #fcd34d; }
473
+ .cm-node.cm-off rect { fill: #f8f9fa; stroke: #d1d5db; stroke-dasharray: 4 3; }
474
+ .cm-label { font-size: 13.5px; font-weight: 600; fill: var(--text); }
475
+ .cm-node.cm-warn .cm-label { fill: #92400e; }
476
+ .cm-node.cm-off .cm-label { fill: #9ca3af; }
477
+ .cm-node.cm-off text { filter: grayscale(1); opacity: .8; }
478
+ .cm-sync { font-size: 10.5px; fill: var(--muted); font-variant-numeric: tabular-nums; }
479
+ .cm-node.cm-off .cm-sync { fill: #9ca3af; }
480
+ .cm-badge { font-size: 13px; }
481
+ .cm-link { fill: none; stroke-width: 1.6; stroke-dasharray: 6 6; stroke: #94a3b8; }
482
+ .cm-link.cm-ok { stroke: var(--accent); animation: cm-flow 1.1s linear infinite; }
483
+ .cm-link.cm-warn { stroke: #d97706; }
484
+ .cm-link.cm-off { stroke: #d1d5db; opacity: .6; }
485
+ @keyframes cm-flow { to { stroke-dashoffset: -24; } }
486
+ @media (prefers-reduced-motion: reduce) { .cm-link.cm-ok { animation: none; } }
487
+ .cm-repo rect { fill: var(--accent-weak); stroke: var(--accent); stroke-width: 1.5; }
488
+ .cm-repo-label { font-size: 14px; font-weight: 700; fill: #1e3a8a; }
363
489
  /* 未使用(非活性)行: アダプターとして存在するが、この案件では設定されていないもの */
364
490
  .ds-card.ds-inactive, .ds-pipe.ds-inactive { background: #f8f9fa; border-style: dashed; box-shadow: none; }
365
491
  .ds-inactive .ds-icon { filter: grayscale(1); opacity: .55; }
@@ -516,8 +642,11 @@ const CLIENT_JS = `
516
642
  if (SITE.graph && SITE.graph.nodes.length > 0) {
517
643
  TABS.push({ key: "graph", label: "ナレッジグラフ" });
518
644
  }
519
- // Cortex自体を操作するアクションの受け皿(取り込み等)。今後この種の機能が増えたらここに集約する。
520
- if (SITE.intake) {
645
+ // Cortex自体を操作するアクションと、接続・自動化の情報の受け皿(取り込み・接続マップ・データソース)。
646
+ // 投入フォーム設定 or fleet-status由来の接続情報のどちらかがあれば出す。
647
+ var HAS_FLEET = !!((SITE.internalSources || []).length || (SITE.externalSources || []).length ||
648
+ (SITE.pipelines || []).length || SITE.tools);
649
+ if (SITE.intake || HAS_FLEET) {
521
650
  TABS.push({ key: "ops", label: "操作" });
522
651
  }
523
652
 
@@ -700,7 +829,6 @@ const CLIENT_JS = `
700
829
  var homeBody = el("div", { class: "body" });
701
830
  app.appendChild(homeBody);
702
831
  loadBody(h, homeBody);
703
- renderDataSources();
704
832
  }
705
833
 
706
834
  // ---------- データソースと自動化(fleet-status.json 由来。無ければ非表示) ----------
@@ -775,7 +903,8 @@ const CLIENT_JS = `
775
903
  if (inactive) {
776
904
  col.appendChild(el("span", { class: "ds-inactive-note", text: "未使用(この案件では設定されていません)" }));
777
905
  }
778
- var card = el("div", { class: "ds-card" + (inactive ? " ds-inactive" : "") }, [
906
+ // id は接続マップのノードclickの飛び先(アンカー)
907
+ var card = el("div", { class: "ds-card" + (inactive ? " ds-inactive" : ""), id: "ds-" + (s.kind || "") }, [
779
908
  el("div", { class: "ds-card-main" }, [
780
909
  el("span", { class: "ds-icon", text: DS_KIND_ICON[s.kind] || "📁" }),
781
910
  el("span", { class: "ds-type", text: s.kind || "" }),
@@ -804,9 +933,15 @@ const CLIENT_JS = `
804
933
  var UNIT = { "slack": "チャンネル", "github-issues": "リポジトリ", "github-discussions": "リポジトリ" };
805
934
  var wrap = el("div", { class: "ds-list" });
806
935
  // チャット未使用: Slackグループの位置(先頭)に「存在するが未設定」のグレー行を出す
807
- if (chatOff) wrap.appendChild(inactiveSourceRow("💬", "チャット"));
936
+ if (chatOff) wrap.appendChild(inactiveSourceRow("💬", "チャット", "ds-チャット"));
937
+ var chatAnchored = chatOff;
938
+ var devAnchored = devOff;
808
939
  groups.forEach(function (g) {
809
940
  var meta = DS_TYPE[g.type] || { icon: "🔗", label: g.type || "その他" };
941
+ // 接続マップのノードclickの飛び先(アンカー)。チャット=Slack群の先頭・開発=GitHub群の先頭
942
+ var anchorId = null;
943
+ if (g.type === "slack" && !chatAnchored) { anchorId = "ds-チャット"; chatAnchored = true; }
944
+ if (g.type.indexOf("github") === 0 && !devAnchored) { anchorId = "ds-開発"; devAnchored = true; }
810
945
  var cards = el("div", { class: "ds-list" });
811
946
  g.items.forEach(function (s) { cards.appendChild(extSourceCard(s, meta)); });
812
947
  if (g.items.length >= 4) {
@@ -819,13 +954,16 @@ const CLIENT_JS = `
819
954
  ? el("span", { class: "badge ds-sum-ok", text: "接続OK" })
820
955
  : el("span", { class: "badge ds-sum-warn", text: "⚠️ " + bad + "件に問題" }),
821
956
  ]);
822
- wrap.appendChild(el("details", { class: "ds-group" }, [sum, cards]));
957
+ var det = el("details", { class: "ds-group" }, [sum, cards]);
958
+ if (anchorId) det.id = anchorId;
959
+ wrap.appendChild(det);
823
960
  } else {
961
+ if (anchorId && cards.firstChild) cards.firstChild.id = anchorId;
824
962
  while (cards.firstChild) wrap.appendChild(cards.firstChild);
825
963
  }
826
964
  });
827
965
  // 開発未使用: GitHubグループの位置(末尾)にグレー行を出す
828
- if (devOff) wrap.appendChild(inactiveSourceRow("🐙", "GitHub"));
966
+ if (devOff) wrap.appendChild(inactiveSourceRow("🐙", "GitHub", "ds-開発"));
829
967
  sec.appendChild(wrap);
830
968
  }
831
969
 
@@ -859,8 +997,8 @@ const CLIENT_JS = `
859
997
  app.appendChild(sec);
860
998
  }
861
999
  // 未使用能力のプレースホルダ行(アダプターとして存在するが、この案件では設定されていないもの)
862
- function inactiveSourceRow(icon, typeLabel) {
863
- return el("div", { class: "ds-card ds-inactive" }, [
1000
+ function inactiveSourceRow(icon, typeLabel, anchorId) {
1001
+ return el("div", { class: "ds-card ds-inactive", id: anchorId }, [
864
1002
  el("div", { class: "ds-card-main" }, [
865
1003
  el("span", { class: "ds-icon", text: icon }),
866
1004
  el("span", { class: "ds-type", text: typeLabel }),
@@ -887,14 +1025,39 @@ const CLIENT_JS = `
887
1025
  ]);
888
1026
  }
889
1027
 
890
- // ---------- 操作タブ(Cortex自体への操作。まずは取り込み。今後この受け皿に機能を足す) ----------
1028
+ // ---------- 操作タブ(接続マップ・データソースと自動化・取り込み。今後この受け皿に機能を足す) ----------
891
1029
  function renderOps() {
892
- app.appendChild(el("p", { class: "ops-intro", text: "Cortex にコンテキストを取り込む操作です。ファイルを選ぶかテキストを貼って送信すると、自動で取り込まれます(数分で反映)。" }));
893
- app.appendChild(el("div", { class: "section-label", text: "取り込み" }));
894
- var grid = el("div", { class: "ops-grid" });
895
- grid.appendChild(opsCard("🗒️", "会議の文字起こしを追加", "顧客主催の会議など、自動取り込みできない会議の文字起こしファイル(.txt / .docx 等)から議事録を生成します。", "transcript", "文字起こしを追加"));
896
- grid.appendChild(opsCard("📎", "共有資料を追加", "提案書・仕様書・議事メモなどの資料を Markdown 化して 共有資料 に取り込みます。", "material", "資料を追加"));
897
- app.appendChild(grid);
1030
+ renderConnMap();
1031
+ renderDataSources();
1032
+ if (SITE.intake) {
1033
+ app.appendChild(el("p", { class: "ops-intro", text: "Cortex にコンテキストを取り込む操作です。ファイルを選ぶかテキストを貼って送信すると、自動で取り込まれます(数分で反映)。" }));
1034
+ app.appendChild(el("div", { class: "section-label", text: "取り込み" }));
1035
+ var grid = el("div", { class: "ops-grid" });
1036
+ grid.appendChild(opsCard("🗒️", "会議の文字起こしを追加", "顧客主催の会議など、自動取り込みできない会議の文字起こしファイル(.txt / .docx 等)から議事録を生成します。", "transcript", "文字起こしを追加"));
1037
+ grid.appendChild(opsCard("📎", "共有資料を追加", "提案書・仕様書・議事メモなどの資料を Markdown 化して 共有資料 に取り込みます。", "material", "資料を追加"));
1038
+ app.appendChild(grid);
1039
+ }
1040
+ }
1041
+
1042
+ // ---------- 接続マップ(ビルド時生成のSVGを<template>から取り出して表示) ----------
1043
+ function renderConnMap() {
1044
+ var tpl = document.getElementById("conn-map");
1045
+ if (!tpl) return; // fleet情報が無い(旧fleet-status等)→ マップなし
1046
+ var wrap = el("div", { class: "conn-map" });
1047
+ wrap.appendChild(tpl.content.cloneNode(true));
1048
+ // 最終同期の時刻はブラウザのローカルタイムで整形(詳細カードの表示と揃える)
1049
+ wrap.querySelectorAll("[data-ts]").forEach(function (t) {
1050
+ var s = dsFmtTime(t.getAttribute("data-ts"));
1051
+ if (s) t.textContent = "最終同期 " + s;
1052
+ });
1053
+ // ノードclickで、下の該当詳細カード(アンカー)へスクロール
1054
+ wrap.addEventListener("click", function (ev) {
1055
+ var g = ev.target && ev.target.closest ? ev.target.closest("[data-target]") : null;
1056
+ if (!g) return;
1057
+ var target = document.getElementById(g.getAttribute("data-target"));
1058
+ if (target && target.scrollIntoView) target.scrollIntoView({ behavior: "smooth", block: "center" });
1059
+ });
1060
+ app.appendChild(wrap);
898
1061
  }
899
1062
  function opsCard(icon, title, desc, kind, btnLabel) {
900
1063
  var btn = el("button", { class: "ops-card-btn", text: "+ " + btnLabel });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.4.24",
3
+ "version": "0.4.25",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,