@takagaki/cortex-decisions-viewer 0.4.74 → 0.4.76
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 +85 -14
- package/dist/render.js +333 -122
- package/package.json +1 -1
package/dist/build.js
CHANGED
|
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.readFleetSources = readFleetSources;
|
|
40
|
+
exports.findBacklogDir = findBacklogDir;
|
|
40
41
|
exports.build = build;
|
|
41
42
|
exports.createRefResolver = createRefResolver;
|
|
42
43
|
const node_fs_1 = require("node:fs");
|
|
@@ -146,8 +147,31 @@ async function readFleetSources(repoRoot) {
|
|
|
146
147
|
// 自動導出されたものは登録ファイルから消しても翌日また現れるので、
|
|
147
148
|
// explicit のときだけ削除ボタンを出す。
|
|
148
149
|
origin: s.origin != null ? String(s.origin) : undefined,
|
|
150
|
+
// どの Slack ワークスペースのチャンネルか(#104)。**列挙し忘れると静かに消える**
|
|
151
|
+
// (このマッピングは素通しではなく明示の許可リスト)。落とすと、同じIDの
|
|
152
|
+
// チャンネルを2つのワークスペースに持つ案件で Gold トグルが別の行を撃つ
|
|
153
|
+
workspace: s.workspace != null ? String(s.workspace) : undefined,
|
|
149
154
|
}))
|
|
150
155
|
: undefined;
|
|
156
|
+
// 接続済み Slack ワークスペース(#104)。**空配列と undefined を区別して通す**——
|
|
157
|
+
// 空配列は「まだ1つも繋いでいない」(接続の導線を出してよい)、undefined は
|
|
158
|
+
// 「この項目を知らない古い生成物」(何も言えないので出さない)。
|
|
159
|
+
// ここで `|| undefined` に潰すと、その2つが同じ表示になる
|
|
160
|
+
const slackWorkspaces = Array.isArray(obj.slackWorkspaces)
|
|
161
|
+
? obj.slackWorkspaces
|
|
162
|
+
.filter((w) => w != null && typeof w === "object")
|
|
163
|
+
.map((w) => ({
|
|
164
|
+
teamId: String(w.teamId ?? ""),
|
|
165
|
+
// 表示名が無ければ team_id。**空欄にしない**(どれのことか分からない行になる)
|
|
166
|
+
name: String(w.name ?? "") || String(w.teamId ?? ""),
|
|
167
|
+
domain: w.domain != null ? String(w.domain) : undefined,
|
|
168
|
+
connectedAt: w.connectedAt != null ? String(w.connectedAt) : undefined,
|
|
169
|
+
channels: typeof w.channels === "number" ? w.channels : undefined,
|
|
170
|
+
// ok / missing / revoked / unknown。**潰さない**(次の一手が違う)
|
|
171
|
+
tokenState: w.tokenState != null ? String(w.tokenState) : undefined,
|
|
172
|
+
}))
|
|
173
|
+
.filter((w) => w.teamId !== "")
|
|
174
|
+
: undefined;
|
|
151
175
|
// セットアップの充足チェック。**missing だけを通す。**
|
|
152
176
|
// na(使わないと宣言した能力)を通すと「設定する必要が無いものに設定を促す」偽の警告になり、
|
|
153
177
|
// #101 で潰した穴が復活する。unknown は AWS/gh へ到達できなかった回に出るので、
|
|
@@ -249,17 +273,8 @@ async function readFleetSources(repoRoot) {
|
|
|
249
273
|
applicable: typeof p.applicable === "boolean" ? p.applicable : undefined,
|
|
250
274
|
}))
|
|
251
275
|
: undefined;
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
if (obj.tools != null && typeof obj.tools === "object" && !Array.isArray(obj.tools)) {
|
|
255
|
-
tools = {};
|
|
256
|
-
for (const [k, v] of Object.entries(obj.tools)) {
|
|
257
|
-
if (typeof v === "string")
|
|
258
|
-
tools[k] = v;
|
|
259
|
-
}
|
|
260
|
-
if (!Object.keys(tools).length)
|
|
261
|
-
tools = undefined;
|
|
262
|
-
}
|
|
276
|
+
// **宣言(tools)は読まない(#125 ⑤)。** engine が出力から落としたので、
|
|
277
|
+
// 残っているのは再生成前の古いスナップショットだけ。読まずに捨てる。
|
|
263
278
|
// 能力ごとの導出済みの状態(#125①が載せ、#125②で画面の正本になった)。
|
|
264
279
|
//
|
|
265
280
|
// **列挙し忘れは静かに消える。** このマッピングは素通しではなく明示の許可リストで、
|
|
@@ -313,7 +328,7 @@ async function readFleetSources(repoRoot) {
|
|
|
313
328
|
const secretSources = obj.secretSources && typeof obj.secretSources === "object"
|
|
314
329
|
? obj.secretSources
|
|
315
330
|
: undefined;
|
|
316
|
-
return { externalSources, internalSources, pipelines,
|
|
331
|
+
return { externalSources, slackWorkspaces, internalSources, pipelines, capabilities, fleetGeneratedAt, secretSources, checks, githubOwners, backlogTarget };
|
|
317
332
|
}
|
|
318
333
|
/** パスがディレクトリとして存在するか */
|
|
319
334
|
async function isDirectory(dirAbs) {
|
|
@@ -491,9 +506,65 @@ async function findDirByMarker(repoRoot, marker, fallback) {
|
|
|
491
506
|
}
|
|
492
507
|
return fallback;
|
|
493
508
|
}
|
|
509
|
+
/** exporter が作る区画名(engine の `BACKLOG_SECTIONS` と同じ固定の名前) */
|
|
510
|
+
const BACKLOG_SECTIONS = ["issues", "documents", "wiki"];
|
|
511
|
+
/**
|
|
512
|
+
* 課題管理の能力ディレクトリ(`<ここ>/issues` に課題ミラーが入る)を探す。
|
|
513
|
+
*
|
|
514
|
+
* **汎用の `findDirByMarker` では足りない。** `backlog-settings.json` は能力ディレクトリの
|
|
515
|
+
* 直下ではなく**区画の中**(`<課題管理>/issues/` 等)にあり、しかも接続先が複数になると
|
|
516
|
+
* さらに1階層落ちる(`<課題管理>/<キー>/{issues,…}/`・#127)。汎用版は最上位の
|
|
517
|
+
* ディレクトリ名しか返さないので、入れ子形の案件では `<課題管理>/issues` という
|
|
518
|
+
* 実在しないパスを見に行き、**課題キー→Backlog URL の表が空になる**
|
|
519
|
+
* (グラフの課題ノードが全部リポ内パス止まりになり、Backlog へ飛べない)。
|
|
520
|
+
*
|
|
521
|
+
* 規則の出どころは cortex-engine の `plugin/scripts/issues-config.mjs`(`findBacklogDir`)。
|
|
522
|
+
* **あちらを変えたらここも変えること**(`viewer/test/backlog-nested-layout.test.mjs` が見張る)。
|
|
523
|
+
*
|
|
524
|
+
* 浅い順(ルート → 深さ1 → 深さ2)に見て最初に当たったものを採る。深い側は後ろなので、
|
|
525
|
+
* いまフラット形で見つかっている案件の答えは1つも動かない。
|
|
526
|
+
*/
|
|
527
|
+
async function findBacklogDir(repoRoot, fallback) {
|
|
528
|
+
const dirsIn = async (rel) => {
|
|
529
|
+
try {
|
|
530
|
+
return (await node_fs_1.promises.readdir(path.join(repoRoot, rel), { withFileTypes: true }))
|
|
531
|
+
.filter((e) => e.isDirectory() && e.name !== "node_modules" && !e.name.startsWith("."))
|
|
532
|
+
.map((e) => e.name)
|
|
533
|
+
.sort();
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
return [];
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
const hasMarker = async (rel) => {
|
|
540
|
+
for (const s of BACKLOG_SECTIONS) {
|
|
541
|
+
try {
|
|
542
|
+
await node_fs_1.promises.stat(path.join(repoRoot, rel, s, "backlog-settings.json"));
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
// 次の区画へ
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return false;
|
|
550
|
+
};
|
|
551
|
+
if (await hasMarker(""))
|
|
552
|
+
return fallback; // ルート直下に区画がある形(`""` を返すと絶対パスになる)
|
|
553
|
+
const dirs = await dirsIn("");
|
|
554
|
+
for (const d of dirs)
|
|
555
|
+
if (await hasMarker(d))
|
|
556
|
+
return d;
|
|
557
|
+
for (const d of dirs) {
|
|
558
|
+
for (const sub of await dirsIn(d)) {
|
|
559
|
+
if (await hasMarker(`${d}/${sub}`))
|
|
560
|
+
return `${d}/${sub}`;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return fallback;
|
|
564
|
+
}
|
|
494
565
|
async function buildTargetUrlMap(repoRoot, repoBaseUrl, branch) {
|
|
495
566
|
const map = new Map();
|
|
496
|
-
const issuesDir = await
|
|
567
|
+
const issuesDir = await findBacklogDir(repoRoot, "課題管理");
|
|
497
568
|
const designDir = await findDirByMarker(repoRoot, "figma.json", "デザイン");
|
|
498
569
|
const meetingDir = await findDirByMarker(repoRoot, "ingest-config.json", "会議");
|
|
499
570
|
const materialsDir = await findDirByMarker(repoRoot, "materials-config.json", "共有資料");
|
|
@@ -1189,11 +1260,11 @@ async function build(opts) {
|
|
|
1189
1260
|
.map((s) => s.trim())
|
|
1190
1261
|
.filter(Boolean),
|
|
1191
1262
|
externalSources: fleet.externalSources,
|
|
1263
|
+
slackWorkspaces: fleet.slackWorkspaces,
|
|
1192
1264
|
fleetGeneratedAt: fleet.fleetGeneratedAt,
|
|
1193
1265
|
secretSources: fleet.secretSources,
|
|
1194
1266
|
internalSources: fleet.internalSources,
|
|
1195
1267
|
pipelines: fleet.pipelines,
|
|
1196
|
-
tools: fleet.tools,
|
|
1197
1268
|
capabilities: fleet.capabilities,
|
|
1198
1269
|
checks: fleet.checks,
|
|
1199
1270
|
githubOwners: fleet.githubOwners,
|
package/dist/render.js
CHANGED
|
@@ -132,15 +132,19 @@ const CM_CAPS = [
|
|
|
132
132
|
{ cap: "チャット", icon: "💬", pipe: null },
|
|
133
133
|
{ cap: "開発", icon: "🐙", pipe: null },
|
|
134
134
|
];
|
|
135
|
-
/**
|
|
135
|
+
/**
|
|
136
|
+
* ツールid→表示名(接続マップのノードラベル用)。
|
|
137
|
+
*
|
|
138
|
+
* **キーは ADAPTER_CATALOG の値だけ。** かつては jira / teams / box / local / none も
|
|
139
|
+
* 持っていたが、これらは宣言(Home.md の `tools`)にしか現れない値だった。宣言を撤去した
|
|
140
|
+
* いま、ここに来るのはカタログのアダプタだけ(`capabilityAdapter` がカタログ外を弾く)。
|
|
141
|
+
* **未対応ツールの表示名を復活させないこと**——名前を出した瞬間「対応している」と
|
|
142
|
+
* 読まれるのが #124 の症状そのもので、状態を出す場所すら無いまま名前だけが並ぶ。
|
|
143
|
+
*/
|
|
136
144
|
const CM_TOOL_LABEL = {
|
|
137
145
|
backlog: "Backlog",
|
|
138
|
-
jira: "Jira",
|
|
139
146
|
"google-meet": "Google Meet",
|
|
140
|
-
teams: "Teams",
|
|
141
147
|
"google-drive": "Google Drive",
|
|
142
|
-
box: "Box",
|
|
143
|
-
local: "ローカル",
|
|
144
148
|
figma: "Figma",
|
|
145
149
|
slack: "Slack",
|
|
146
150
|
github: "GitHub",
|
|
@@ -178,8 +182,8 @@ function sourceNeedsAttention(s) {
|
|
|
178
182
|
* **クロージャを持たない純関数に保つこと**——外の変数を掴むと直列化した先で壊れる。
|
|
179
183
|
*
|
|
180
184
|
* 返す値:
|
|
181
|
-
* "off" … 未使用(
|
|
182
|
-
* "idle" …
|
|
185
|
+
* "off" … 未使用(enabled:false / 使っている証拠が無い)
|
|
186
|
+
* "idle" … 設定はあるが一度も繋がっていない(画面表記は「未接続」)
|
|
183
187
|
* "warn" … 問題あり
|
|
184
188
|
* "ok" … 健全
|
|
185
189
|
*
|
|
@@ -189,7 +193,7 @@ function sourceNeedsAttention(s) {
|
|
|
189
193
|
* APIキー未投入で sync-backlog が失敗し、課題管理が警告色で線付きになった)。
|
|
190
194
|
*
|
|
191
195
|
* @param cap 能力名
|
|
192
|
-
* @param ctx 生のフリートデータ {
|
|
196
|
+
* @param ctx 生のフリートデータ { capabilities, internals, externals, pipes }。
|
|
193
197
|
* **解決済みの値を渡させない。** 振り分けや regex を呼び出し側に持たせると、
|
|
194
198
|
* 呼び出し側ごとに写経が生まれ、片方が書き忘れると判定が静かに変わる。
|
|
195
199
|
*/
|
|
@@ -311,7 +315,6 @@ function capabilityState(cap, ctx) {
|
|
|
311
315
|
// (読み手は未使用を信じて押さないので、行が消えるより悪い)
|
|
312
316
|
const liveExts = ctx.externals.filter(function (s) { return externalCapability(s.type) === cap; });
|
|
313
317
|
const isLive = cap === "チャット" || cap === "開発" || cap === CAP_OTHER;
|
|
314
|
-
const toolVal = ctx.tools[cap];
|
|
315
318
|
const internal = ctx.internals.filter(function (s) { return s.kind === cap; })[0];
|
|
316
319
|
const pipeRe = PIPE[cap];
|
|
317
320
|
// ---- #125②: 導出(capabilities)があるならそれが状態の正本 ----
|
|
@@ -346,12 +349,16 @@ function capabilityState(cap, ctx) {
|
|
|
346
349
|
// 答えが割れることはない。**この2つを分けるなら、写像とここの両方を直すこと。**
|
|
347
350
|
return "idle";
|
|
348
351
|
}
|
|
349
|
-
// ---- ここから下は capabilities
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
352
|
+
// ---- ここから下は capabilities を持たない fleet-status.json のための防御 ----
|
|
353
|
+
//
|
|
354
|
+
// **宣言(tools)はもう見ない(#125 ⑤)。** 艦隊の fleet-status.json は全案件が
|
|
355
|
+
// capabilities を持つので、通常ここへは来ない。残してあるのは、生成前・取得失敗・
|
|
356
|
+
// 途中で壊れたデータで**画面が落ちない**ようにするためだけ——実体(同期ソースの行・
|
|
357
|
+
// ライブ参照)だけで判定し、無ければ「まだ繋がっていない」側へ倒す。
|
|
358
|
+
// **ここに宣言を読む道を足さないこと。** 足した瞬間、実態と食い違える経路が復活する。
|
|
359
|
+
const used = (internal != null && internal.enabled !== false) ||
|
|
353
360
|
(isLive && liveExts.length > 0);
|
|
354
|
-
const off =
|
|
361
|
+
const off = (internal != null && internal.enabled === false) || !used;
|
|
355
362
|
// 問題あり: ライブ参照はgate≠okが1件以上、同期ソースは対応パイプラインの直近runがfailure
|
|
356
363
|
const warn = !off &&
|
|
357
364
|
(isLive
|
|
@@ -395,21 +402,38 @@ function capabilityState(cap, ctx) {
|
|
|
395
402
|
: !syncedForReal);
|
|
396
403
|
return off ? "off" : idle ? "idle" : warn ? "warn" : "ok";
|
|
397
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* `repoBaseUrl`(https://github.com/owner/repo)からリポジトリ名だけを取り出す。
|
|
407
|
+
*
|
|
408
|
+
* **owner は出さない。** 画面は1案件ぶんしか映さないので、どの組織かは常に同じ——
|
|
409
|
+
* 出しても名前が長くなるだけで、読み手の判断は変わらない。
|
|
410
|
+
* 取れない形(URLが無い・GitHub 以外・末尾が空)では空文字を返し、呼び出し側が落とす。
|
|
411
|
+
*
|
|
412
|
+
* **github.com のURLだけを受ける。** 隣に GitHub のロゴを置くので、別ホストの名前を
|
|
413
|
+
* 通すとロゴが嘘になる。
|
|
414
|
+
*/
|
|
415
|
+
function repoNameOf(repoBaseUrl) {
|
|
416
|
+
if (!repoBaseUrl)
|
|
417
|
+
return "";
|
|
418
|
+
const m = /^https?:\/\/github\.com\/(.+)$/.exec(String(repoBaseUrl));
|
|
419
|
+
if (!m)
|
|
420
|
+
return "";
|
|
421
|
+
// **末尾スラッシュを先に落とす。** 後にすると `repo.git/` の `.git` が残る
|
|
422
|
+
const slug = m[1].replace(/\/+$/, "").replace(/\.git$/, "");
|
|
423
|
+
const name = slug.split("/").pop() || "";
|
|
424
|
+
// owner だけの URL(github.com/owner)を名前と取り違えない
|
|
425
|
+
return slug.includes("/") ? name : "";
|
|
426
|
+
}
|
|
398
427
|
function renderConnectionMap(site) {
|
|
399
428
|
const internals = site.internalSources ?? [];
|
|
400
429
|
const externals = site.externalSources ?? [];
|
|
401
430
|
const pipes = site.pipelines ?? [];
|
|
402
|
-
const tools = site.tools ?? {};
|
|
403
431
|
const caps = site.capabilities ?? {};
|
|
404
432
|
// **fleet のデータが1つでも取れているか、だけで判定する(#125②)。**
|
|
405
433
|
// 素の Decisions ビュー(fleet-status.json が無い構成)ではマップを出さない現行の意図は
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
if (!internals.length &&
|
|
409
|
-
!externals.length &&
|
|
410
|
-
!pipes.length &&
|
|
411
|
-
!Object.keys(tools).length &&
|
|
412
|
-
!Object.keys(caps).length) {
|
|
434
|
+
// そのまま。宣言(`tools`)は条件から外した(⑤)——艦隊の fleet-status.json は
|
|
435
|
+
// すべて capabilities を持つので、宣言はもうマップの有無を左右しない。
|
|
436
|
+
if (!internals.length && !externals.length && !pipes.length && !Object.keys(caps).length) {
|
|
413
437
|
return null;
|
|
414
438
|
}
|
|
415
439
|
const hasCaps = Object.keys(caps).length > 0;
|
|
@@ -421,13 +445,12 @@ function renderConnectionMap(site) {
|
|
|
421
445
|
const REPO_X = W / 2;
|
|
422
446
|
CM_CAPS.forEach((c, i) => {
|
|
423
447
|
const cx = 80 + i * 160;
|
|
424
|
-
const toolVal = tools[c.cap];
|
|
425
448
|
const internal = internals.find((s) => s.kind === c.cap);
|
|
426
449
|
const isLive = c.cap === "チャット" || c.cap === "開発";
|
|
427
450
|
const liveExts = c.cap === "チャット" ? slackExts : c.cap === "開発" ? ghExts : [];
|
|
428
451
|
// **判定は capabilityState に一本化している。** 画面側(クライアントJS)も同じ関数を
|
|
429
452
|
// 呼ぶので、ここで写経すると「同じ画面でマップが⚠️・直下が『接続OK』」が復活する。
|
|
430
|
-
const st = capabilityState(c.cap, {
|
|
453
|
+
const st = capabilityState(c.cap, { capabilities: caps, internals, externals, pipes });
|
|
431
454
|
const off = st === "off";
|
|
432
455
|
const idle = st === "idle";
|
|
433
456
|
const warn = st === "warn";
|
|
@@ -437,22 +460,19 @@ function renderConnectionMap(site) {
|
|
|
437
460
|
// ツールは「その能力をいま何で実現しているか」という差し替え可能なアダプタで、
|
|
438
461
|
// 主語ではない。ツール名を主にすると、Teams のように複数の能力に使われるものが
|
|
439
462
|
// 同じ名前で並び(会議とチャットに Teams が2つ)、何が繋がっているのか読めなくなる。
|
|
440
|
-
// "none"(未使用の宣言)はツール名ではないので出さない。そのまま表示すると
|
|
441
|
-
// 「ツール: none」という読めない行になる(実際にそう出ていた)
|
|
442
463
|
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
464
|
+
// **カタログのアダプタを常に出す(#125②)。** 未接続でも「何で繋ぐのか」が
|
|
465
|
+
// 分からないと入口にならない。カタログ外の値は capabilityAdapter が弾く。
|
|
466
|
+
// capabilities を持たないデータでは、実体(同期ソースの行・ライブ参照)から拾う(⑤)。
|
|
445
467
|
const rawTool = hasCaps
|
|
446
468
|
? capabilityAdapter(c.cap, caps)
|
|
447
|
-
:
|
|
448
|
-
?
|
|
449
|
-
:
|
|
450
|
-
?
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
: "github"
|
|
455
|
-
: "";
|
|
469
|
+
: internal && internal.tool && internal.tool !== "none"
|
|
470
|
+
? internal.tool
|
|
471
|
+
: isLive && liveExts.length
|
|
472
|
+
? c.cap === "チャット"
|
|
473
|
+
? "slack"
|
|
474
|
+
: "github"
|
|
475
|
+
: "";
|
|
456
476
|
const toolName = rawTool ? CM_TOOL_LABEL[rawTool] ?? rawTool : "";
|
|
457
477
|
const label = `${c.icon} ${c.cap}`;
|
|
458
478
|
// 2行目=ツール名(小さく)/3行目=状態(未使用・自動連携なし・ライブ参照・最終同期)
|
|
@@ -508,10 +528,26 @@ function renderConnectionMap(site) {
|
|
|
508
528
|
links.push(`<path class="cm-link ${state}" d="M ${cx} 78 C ${cx} 152, ${REPO_X} 162, ${REPO_X} 238"></path>`);
|
|
509
529
|
}
|
|
510
530
|
});
|
|
511
|
-
|
|
531
|
+
// **中央ノードは GitHub のリポジトリ名そのもの(オーナー指示・2026-08-12)。**
|
|
532
|
+
// 以前は「📦 コンテキストリポジトリ(案件の表示名)」だったが、役割名は図の文脈から自明で、
|
|
533
|
+
// 表示名は上下のノードとも重複していた。実際に開く先はリポジトリなので、その名前で呼ぶ。
|
|
534
|
+
// 名前は repoBaseUrl から導出する(新しいデータは足さない)。取れない構成では
|
|
535
|
+
// 案件の表示名へ落とす——名前の無い箱にしない。
|
|
536
|
+
const repoName = repoNameOf(site.repoBaseUrl) || site.title;
|
|
537
|
+
const repoLabel = repoName.length > 24 ? repoName.slice(0, 23) + "…" : repoName;
|
|
538
|
+
// アイコンと名前を**まとめて中央へ置く**。text-anchor=middle だけだとアイコンの分だけ
|
|
539
|
+
// 全体が右に寄るので、合計幅を見積もってから左端を決める。日本語が混じる案件名へ
|
|
540
|
+
// フォールバックした場合に備え、全角は倍幅で数える(14px/700 の実測に合わせた概算)
|
|
541
|
+
const ICON = 18;
|
|
542
|
+
const GAP = 8;
|
|
543
|
+
const textW = [...repoLabel].reduce((w, ch) => w + (ch.charCodeAt(0) < 128 ? 8.2 : 14), 0);
|
|
544
|
+
const startX = REPO_X - (ICON + GAP + textW) / 2;
|
|
512
545
|
const repo = `<g class="cm-repo">` +
|
|
513
546
|
`<rect x="${REPO_X - 170}" y="240" width="340" height="52" rx="12"></rect>` +
|
|
514
|
-
|
|
547
|
+
// ロゴはビルド時に埋め込む(ヘッダ右上・開発ソースのカードと同じ GITHUB_ICON)。外部通信は起きない。
|
|
548
|
+
// 入れ子の svg を transform で置き、色は cm-repo-logo の color を継ぐ(fill="currentColor")
|
|
549
|
+
`<g class="cm-repo-logo" transform="translate(${startX.toFixed(1)}, ${266 - ICON / 2 - 1})">${GITHUB_ICON}</g>` +
|
|
550
|
+
`<text class="cm-repo-label" x="${(startX + ICON + GAP).toFixed(1)}" y="272">${escapeHtml(repoLabel)}</text>` +
|
|
515
551
|
`</g>`;
|
|
516
552
|
return (`<svg class="cm-svg" viewBox="0 0 ${W} 312" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="データソースとの接続マップ">` +
|
|
517
553
|
links.join("") +
|
|
@@ -554,15 +590,14 @@ function buildTutorialData(site, codeUrl, repoSlug) {
|
|
|
554
590
|
},
|
|
555
591
|
];
|
|
556
592
|
// ここから先は「書き込み・外部連携」の体験。案件の構成に無い能力のチャレンジは出さない(表示条件つき)
|
|
557
|
-
const tools = site.tools ?? {};
|
|
558
593
|
const caps = site.capabilities ?? {};
|
|
559
594
|
const internals = site.internalSources ?? [];
|
|
560
595
|
const externals = site.externalSources ?? [];
|
|
561
596
|
/**
|
|
562
|
-
* その能力が配線済みか。**導出(capabilities
|
|
597
|
+
* その能力が配線済みか。**導出(capabilities)だけで決める(#125②)。**
|
|
563
598
|
* 宣言でチャレンジを出していた頃は、Backlog を宣言しているだけで鍵も入っていない案件に
|
|
564
599
|
* 「記票してみよう」が出ていた——押すと必ず失敗する体験を最初の5分に置くことになる。
|
|
565
|
-
*
|
|
600
|
+
* 導出が無いデータでは null を返し、呼び出し側が実体(行の有無)だけで判断する(⑤)。
|
|
566
601
|
*/
|
|
567
602
|
const capOn = (cap) => {
|
|
568
603
|
const s = capabilityRawState(cap, caps);
|
|
@@ -578,8 +613,7 @@ function buildTutorialData(site, codeUrl, repoSlug) {
|
|
|
578
613
|
}
|
|
579
614
|
// Backlogに記票する体験(課題管理にBacklogを使う案件のみ出す)
|
|
580
615
|
const usesBacklog = capOn("課題管理") ??
|
|
581
|
-
(
|
|
582
|
-
internals.some((s) => s.kind === "課題管理" && s.tool === "backlog" && s.enabled !== false));
|
|
616
|
+
internals.some((s) => s.kind === "課題管理" && s.tool === "backlog" && s.enabled !== false);
|
|
583
617
|
if (usesBacklog) {
|
|
584
618
|
specs.push({
|
|
585
619
|
title: "Backlogに記票してみよう",
|
|
@@ -588,9 +622,7 @@ function buildTutorialData(site, codeUrl, repoSlug) {
|
|
|
588
622
|
});
|
|
589
623
|
}
|
|
590
624
|
// 画面インベントリを読む体験(デザインを同期している案件のみ出す)
|
|
591
|
-
const usesDesign = capOn("デザイン") ??
|
|
592
|
-
((tools["デザイン"] != null && tools["デザイン"] !== "none") ||
|
|
593
|
-
internals.some((s) => s.kind === "デザイン" && s.enabled !== false));
|
|
625
|
+
const usesDesign = capOn("デザイン") ?? internals.some((s) => s.kind === "デザイン" && s.enabled !== false);
|
|
594
626
|
if (usesDesign) {
|
|
595
627
|
specs.push({
|
|
596
628
|
title: "デザインを読んでみよう",
|
|
@@ -599,7 +631,7 @@ function buildTutorialData(site, codeUrl, repoSlug) {
|
|
|
599
631
|
});
|
|
600
632
|
}
|
|
601
633
|
// 完了をSlackに知らせる体験(チャットにSlackを宣言している案件のみ出す。最終チャレンジ)
|
|
602
|
-
const usesSlack = capOn("チャット") ??
|
|
634
|
+
const usesSlack = capOn("チャット") ?? externals.some((s) => s.type === "slack");
|
|
603
635
|
if (usesSlack) {
|
|
604
636
|
specs.push({
|
|
605
637
|
title: "Slackに完了を知らせよう",
|
|
@@ -1132,6 +1164,8 @@ a.ds-name:hover { text-decoration: underline; }
|
|
|
1132
1164
|
@media (prefers-reduced-motion: reduce) { .cm-link.cm-ok { animation: none; } }
|
|
1133
1165
|
.cm-repo rect { fill: var(--accent-weak); stroke: var(--accent); stroke-width: 1.5; }
|
|
1134
1166
|
.cm-repo-label { font-size: 14px; font-weight: 700; fill: #1e3a8a; }
|
|
1167
|
+
/* GitHubロゴは fill="currentColor" なので、色は color で渡す(ラベルと同色に揃える) */
|
|
1168
|
+
.cm-repo-logo { color: #1e3a8a; }
|
|
1135
1169
|
/* 未使用(非活性)行: アダプターとして存在するが、この案件では設定されていないもの */
|
|
1136
1170
|
.ds-card.ds-inactive, .ds-pipe.ds-inactive { background: #f8f9fa; border-style: dashed; box-shadow: none; }
|
|
1137
1171
|
.ds-inactive .ds-icon { filter: grayscale(1); opacity: .55; }
|
|
@@ -1216,6 +1250,16 @@ a.ds-name:hover { text-decoration: underline; }
|
|
|
1216
1250
|
/* 登録の由来(#118)。**薄く出す**——読み取りの可否や Gold 対象かより弱い情報で、
|
|
1217
1251
|
ここが目立つと「除外」「未宣言」の判断のバッジと同じ強さに見えてしまう */
|
|
1218
1252
|
.badge.ds-origin { background: transparent; color: var(--muted); border: 1px dashed var(--border); }
|
|
1253
|
+
/* どの Slack ワークスペースのチャンネルか(#104)。**由来のバッジと同じ強さにする**——
|
|
1254
|
+
「Gold 対象か」より弱い、どれのことかを見分けるための情報。強くすると判断のバッジと競る */
|
|
1255
|
+
.badge.ds-workspace { background: transparent; color: var(--muted); border: 1px solid var(--border); }
|
|
1256
|
+
/* 接続済み Slack ワークスペースの一覧(#104)。追加フォームの中に置く */
|
|
1257
|
+
.ds-ws-list { display: flex; flex-direction: column; gap: 6px; margin: 8px 0 10px; }
|
|
1258
|
+
.ds-ws { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; font-size: 12.5px; }
|
|
1259
|
+
.ds-ws-name { color: var(--text); font-weight: 600; word-break: break-all; }
|
|
1260
|
+
.ds-ws-count { color: var(--muted); flex: 0 0 auto; }
|
|
1261
|
+
/* 1つも繋いでいない状態。**空白にしない**——一覧が出ていないのか繋がっていないのかが読めない */
|
|
1262
|
+
.ds-ws-empty { color: var(--muted); font-size: 12.5px; }
|
|
1219
1263
|
/* 全組織が接続済みのときに、トークン入力欄の代わりに出す1行(#118) */
|
|
1220
1264
|
.ds-all-connected { color: var(--muted); }
|
|
1221
1265
|
.ds-gate { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; font-weight: 600; flex: 0 0 auto; }
|
|
@@ -1463,11 +1507,16 @@ ${capabilityState.toString()}
|
|
|
1463
1507
|
}
|
|
1464
1508
|
// Cortex自体を操作するアクションと、接続・自動化の情報の受け皿(取り込み・接続マップ・データソース)。
|
|
1465
1509
|
// 投入フォーム設定 or fleet-status由来の接続情報のどちらかがあれば出す。
|
|
1466
|
-
// **fleet のデータが取れているか、だけで決める(#125②)。** 能力の宣言(tools
|
|
1467
|
-
//
|
|
1468
|
-
// Decisions
|
|
1510
|
+
// **fleet のデータが取れているか、だけで決める(#125②)。** 能力の宣言(tools)は撤去した(⑤)。
|
|
1511
|
+
// **常に真にはしない**——fleet-status.json を持たない素の
|
|
1512
|
+
// Decisions ビューでは操作タブそのものを出さない現行の意図を保つ。
|
|
1513
|
+
//
|
|
1514
|
+
// **鍵(secretKinds)も候補に数える。** 下の節(buildDataSources)は鍵だけの案件でも
|
|
1515
|
+
// 出す作りだが、タブが立たなければその判定に辿り着けない。宣言を撤去するまでは
|
|
1516
|
+
// 空の宣言オブジェクトが truthy でここを通していたため、鍵だけの案件が通っていたのは
|
|
1517
|
+
// **偶然**だった(テストもその偶然に乗っていた)。候補として明示する
|
|
1469
1518
|
var HAS_FLEET = !!((SITE.internalSources || []).length || (SITE.externalSources || []).length ||
|
|
1470
|
-
(SITE.pipelines || []).length || SITE.capabilities || SITE.
|
|
1519
|
+
(SITE.pipelines || []).length || SITE.capabilities || (SITE.secretKinds || []).length);
|
|
1471
1520
|
if (SITE.intake || HAS_FLEET) {
|
|
1472
1521
|
TABS.push({ key: "ops", label: "操作" });
|
|
1473
1522
|
}
|
|
@@ -2018,23 +2067,18 @@ ${capabilityState.toString()}
|
|
|
2018
2067
|
* 誤った封鎖を生んでいた(実際に Slack を渡されているのに追加できない案件があった)。
|
|
2019
2068
|
* カタログはこの製品が実装しているアダプタの定義なので、押して必ず失敗することはない。
|
|
2020
2069
|
*
|
|
2021
|
-
*
|
|
2022
|
-
*
|
|
2023
|
-
* 必ず400になるボタンが復活する。
|
|
2070
|
+
* **宣言によるゲートは撤去した(⑤)。** capabilities を持たないデータでも
|
|
2071
|
+
* カタログだけで判断する——判定の道は1本にする。
|
|
2024
2072
|
*/
|
|
2025
2073
|
function capabilityUsable(cap, tool) {
|
|
2026
|
-
|
|
2027
|
-
if (Object.keys(caps).length) return (ADAPTER_CATALOG[cap] || []).indexOf(tool) >= 0;
|
|
2028
|
-
var v = (fleetVal("tools") || {})[cap];
|
|
2029
|
-
return !v || v === "none" || v === tool;
|
|
2074
|
+
return (ADAPTER_CATALOG[cap] || []).indexOf(tool) >= 0;
|
|
2030
2075
|
}
|
|
2031
2076
|
|
|
2032
2077
|
/** 組み立てだけを行う(設置は呼び出し側)。ライブ差し替えで同じ節を作り直すため分けている */
|
|
2033
2078
|
/** ツールid→表示名(クライアント側。接続マップの CM_TOOL_LABEL と同じ内容) */
|
|
2034
2079
|
var CM_TOOL_LABEL_C = {
|
|
2035
|
-
backlog: "Backlog",
|
|
2036
|
-
|
|
2037
|
-
slack: "Slack", github: "GitHub", none: "",
|
|
2080
|
+
backlog: "Backlog", "google-meet": "Google Meet", "google-drive": "Google Drive",
|
|
2081
|
+
figma: "Figma", slack: "Slack", github: "GitHub",
|
|
2038
2082
|
};
|
|
2039
2083
|
|
|
2040
2084
|
/**
|
|
@@ -2073,11 +2117,6 @@ ${capabilityState.toString()}
|
|
|
2073
2117
|
// 能力ごとの導出済みの状態(#125② の正本)。持たない古いデータでは {}
|
|
2074
2118
|
var caps = fleetVal("capabilities") || {};
|
|
2075
2119
|
var hasCaps = Object.keys(caps).length > 0;
|
|
2076
|
-
// 能力→ツールの宣言(例: チャット: "none")。**capabilities が無いときだけ効く劣化表示。**
|
|
2077
|
-
// "none" の能力は「未使用」のグレー行で存在だけ見せる
|
|
2078
|
-
var tools = (!hasCaps && fleetVal("tools")) || {};
|
|
2079
|
-
var chatOff = tools["チャット"] === "none";
|
|
2080
|
-
var devOff = tools["開発"] === "none";
|
|
2081
2120
|
// **この節に入るものが1つでもあるなら出す。**
|
|
2082
2121
|
//
|
|
2083
2122
|
// 条件を「データ行があるか」で書いていて、2回続けて漏らした——
|
|
@@ -2087,9 +2126,8 @@ ${capabilityState.toString()}
|
|
|
2087
2126
|
//
|
|
2088
2127
|
// 個別に足し続けると次も漏らすので、**この節に出す候補を列挙して判定する**形にする。
|
|
2089
2128
|
var hasSecrets = ((SITE.secretKinds || []).filter(function (k) { return SECRET_FORMS[k]; })).length > 0;
|
|
2090
|
-
// **fleet のデータが取れているか、だけを見る(#125②)。** 宣言(tools
|
|
2091
|
-
|
|
2092
|
-
var hasCapModel = hasCaps || Object.keys(tools).length > 0;
|
|
2129
|
+
// **fleet のデータが取れているか、だけを見る(#125②)。** 宣言(tools)は撤去した(⑤)
|
|
2130
|
+
var hasCapModel = hasCaps;
|
|
2093
2131
|
// **投入の口も候補。** fleet-status.json はGitHub Actionsが夜間に生成するので、
|
|
2094
2132
|
// 立ち上げ直後の案件には存在しない。それを理由に節ごと消すと、
|
|
2095
2133
|
// 文字起こし・資料を入れる口が**一番必要な瞬間に**画面から無くなる
|
|
@@ -2246,7 +2284,7 @@ ${capabilityState.toString()}
|
|
|
2246
2284
|
});
|
|
2247
2285
|
}
|
|
2248
2286
|
|
|
2249
|
-
if (sources.length
|
|
2287
|
+
if (sources.length) {
|
|
2250
2288
|
// **見出しは付けない。** #101 で行は能力ごとのモーダルへ移ったので、
|
|
2251
2289
|
// ここに見出しを置くと中身ゼロの見出しが節の先頭に立つ(実際に8案件すべてで出ていた)
|
|
2252
2290
|
// ツール(type)別にグループ化し、多いグループ(4件以上)は折りたたむ
|
|
@@ -2258,9 +2296,6 @@ ${capabilityState.toString()}
|
|
|
2258
2296
|
g.items.push(s);
|
|
2259
2297
|
});
|
|
2260
2298
|
var UNIT = { "slack": "チャンネル", "github-issues": "リポジトリ", "github-discussions": "リポジトリ" };
|
|
2261
|
-
// チャット未使用: Slackグループの位置(先頭)に「存在するが未設定」のグレー行を出す
|
|
2262
|
-
// **アンカーは渡さない**(タイルが飛び先。同 §id の重複を作らない)
|
|
2263
|
-
if (chatOff) pushCap("チャット", inactiveSourceRow("💬", "チャット", null));
|
|
2264
2299
|
groups.forEach(function (g) {
|
|
2265
2300
|
var meta = DS_TYPE[g.type] || { icon: "🔗", label: g.type || "その他" };
|
|
2266
2301
|
var cards = el("div", { class: "ds-list" });
|
|
@@ -2297,12 +2332,13 @@ ${capabilityState.toString()}
|
|
|
2297
2332
|
}
|
|
2298
2333
|
});
|
|
2299
2334
|
}
|
|
2300
|
-
// 開発未使用: グレー行も能力の受け皿へ
|
|
2301
|
-
if (devOff) pushCap("開発", inactiveSourceRow("🐙", "GitHub", null));
|
|
2302
2335
|
|
|
2303
2336
|
// **追加フォームはグループの外に置く。** グループは0件だと描かれず、4件以上だと
|
|
2304
2337
|
// 畳まれる(既定は閉)。中に入れると**新規案件(0件)=一番追加したい場面**で消える。
|
|
2305
2338
|
var adders = renderExternalAdders();
|
|
2339
|
+
// **接続の入口はチャンネル追加より前に置く。** 顧客ワークスペースを繋いでいない状態で
|
|
2340
|
+
// 先に追加フォームを見せると、貼れるのに読まれないチャンネルを登録することになる
|
|
2341
|
+
pushCap("チャット", slackConnectBox());
|
|
2306
2342
|
pushCap("チャット", adders.querySelector('[data-adder="slack"]'));
|
|
2307
2343
|
// **組織ごとの接続状況を、開発の一番上に置く。** トークンは組織ごとにしか
|
|
2308
2344
|
// 効かない(Fine-grained PAT も GitHub App のインストールトークンも1組織分)ので、
|
|
@@ -2450,10 +2486,11 @@ ${capabilityState.toString()}
|
|
|
2450
2486
|
// 両方に出すと、同じ失敗が1画面に2回並ぶ。実行状況はパイプライン節に任せ、
|
|
2451
2487
|
// ここでは直し方だけをその行に添える(下の renderPipelines で使う)
|
|
2452
2488
|
var PIPE_CHECK = { "update-gold": "nightly_decisionlog", "sync-backlog": "nightly_backlog", "sync-designs": "figma_sync" };
|
|
2489
|
+
// **engine が出しているチェックだけを並べる。** meeting_minutes / channels_json は
|
|
2490
|
+
// 同語反復(適用されるときは必ず ok)だったので engine が撤去した(#125 ⑤)。
|
|
2491
|
+
// 割り当ての無いチェックは消えず、下の「セットアップの残り」へ落ちる
|
|
2453
2492
|
var CHECK_CAP = {
|
|
2454
2493
|
backlog_secrets: "課題管理", backlog_synced: "課題管理",
|
|
2455
|
-
meeting_minutes: "会議",
|
|
2456
|
-
channels_json: "チャット",
|
|
2457
2494
|
figma_token: "デザイン", figma_inventory: "デザイン",
|
|
2458
2495
|
dev_sources_registered: "開発", dev_issues_derived: "開発",
|
|
2459
2496
|
};
|
|
@@ -2515,12 +2552,12 @@ ${capabilityState.toString()}
|
|
|
2515
2552
|
// 上のマップと直下の一覧で語が割れない
|
|
2516
2553
|
parts.push((CAP_STATE_MARK[st] || "") + (CAP_STATE_TEXT[derived] || "状態が判定できません"));
|
|
2517
2554
|
} else {
|
|
2518
|
-
// **off には2つの意味が同居している。**
|
|
2555
|
+
// **off には2つの意味が同居している。** 「意図的に止めている」(enabled:false)と
|
|
2519
2556
|
// 「まだ何も分かっていない」(fleet-status.json を夜間バッチがまだ作っていない)。
|
|
2520
2557
|
// 後者を「未使用」と書くと、立ち上げ直後の案件で顧客が
|
|
2521
2558
|
// 「Backlog も Slack も使わない案件だ」と読む——繋ぐ前提で契約しているのに
|
|
2522
|
-
var
|
|
2523
|
-
parts.push(st === "off" && !
|
|
2559
|
+
var intentionalOff = internal != null && internal.enabled === false;
|
|
2560
|
+
parts.push(st === "off" && !intentionalOff ? "未設定" : STATE_LABEL[st] || "");
|
|
2524
2561
|
}
|
|
2525
2562
|
// 読み取り範囲。**この画面は顧客も見る。何をGoldに読んでいるかの開示が主眼**
|
|
2526
2563
|
//
|
|
@@ -2564,11 +2601,9 @@ ${capabilityState.toString()}
|
|
|
2564
2601
|
// ——未接続のタイルは「これから繋げる入口」で、消すと繋ぎ方を知る場所が無くなる。
|
|
2565
2602
|
// カタログは製品の定義なので、案件データが何であってもここは変わらない
|
|
2566
2603
|
var adapters = (hasCaps && ADAPTER_CATALOG[cap]) || [];
|
|
2567
|
-
//
|
|
2568
|
-
var declared = tools[cap] != null;
|
|
2569
|
-
// **手動投入はアダプタに依存しない。** 宣言や行の有無で弾くと、
|
|
2604
|
+
// **手動投入はアダプタに依存しない。** 行の有無で弾くと、
|
|
2570
2605
|
// fleet-status.json がまだ無い新規案件で投入の口ごと消える
|
|
2571
|
-
if (!adapters.length && !items.length && !
|
|
2606
|
+
if (!adapters.length && !items.length && !manualBox[cap]) return;
|
|
2572
2607
|
anyCap = true;
|
|
2573
2608
|
sec.appendChild(el("div", { class: "ds-cap", text: cap }));
|
|
2574
2609
|
if (adapters.length) {
|
|
@@ -2579,11 +2614,11 @@ ${capabilityState.toString()}
|
|
|
2579
2614
|
adapters.forEach(function (a, ai) {
|
|
2580
2615
|
renderAdapterTile(cap, items, a, a === active ? capabilityDisplayState(cap, caps) : "unset", ai);
|
|
2581
2616
|
});
|
|
2582
|
-
} else if (items.length
|
|
2583
|
-
//
|
|
2584
|
-
//
|
|
2585
|
-
|
|
2586
|
-
renderAdapterTile(cap, items,
|
|
2617
|
+
} else if (items.length) {
|
|
2618
|
+
// capabilities を持たないデータ: アダプタのタイルは行があるときだけ。手動投入だけが
|
|
2619
|
+
// 理由でここへ来た新規案件に、中身の無い「会議」タイルを立てても押す先が無い。
|
|
2620
|
+
// アダプタ名は名乗らない(宣言を撤去したので、名乗れる根拠がここには無い・⑤)
|
|
2621
|
+
renderAdapterTile(cap, items, "", null, 0);
|
|
2587
2622
|
}
|
|
2588
2623
|
renderManualTile(cap);
|
|
2589
2624
|
});
|
|
@@ -2591,7 +2626,7 @@ ${capabilityState.toString()}
|
|
|
2591
2626
|
/**
|
|
2592
2627
|
* 能力×アダプタ1件のタイル。
|
|
2593
2628
|
*
|
|
2594
|
-
* @param adapter
|
|
2629
|
+
* @param adapter アダプタ識別子(カタログ由来。capabilities が無いデータでは空)
|
|
2595
2630
|
* @param derived engine が導出した5値。旧データ(capabilities 無し)では null
|
|
2596
2631
|
* @param ai その能力の中での並び順。先頭だけが接続マップの飛び先(ds-<能力>)になる
|
|
2597
2632
|
*/
|
|
@@ -2600,7 +2635,7 @@ ${capabilityState.toString()}
|
|
|
2600
2635
|
var st = derived === "unset" && capabilityDisplayState(cap, caps) !== "unset"
|
|
2601
2636
|
// カタログにあるが導出が答えていないアダプタ(今日は発生しない。#107 の前方互換)
|
|
2602
2637
|
? "idle"
|
|
2603
|
-
: capabilityState(cap, {
|
|
2638
|
+
: capabilityState(cap, { capabilities: caps, internals: internals, externals: sources, pipes: pipes });
|
|
2604
2639
|
// **ロゴはタイルの左に独立して置く。** 名前の行に入れると名前の高さに
|
|
2605
2640
|
// 縛られて小さくなる。ロゴで能力を見分けられるのが一覧の主な用途
|
|
2606
2641
|
var logo = ADAPTER_ICONS[adapter];
|
|
@@ -2647,7 +2682,8 @@ ${capabilityState.toString()}
|
|
|
2647
2682
|
// **注記の条件は、入力欄を出す条件と同じにする(0.4.71)。** 別々にすると
|
|
2648
2683
|
// 「入力欄はあるのに未設定だと言われない」「言われるのに入力欄が無い」のどちらかが起きる。
|
|
2649
2684
|
// 0.4.70 は unset で入力欄を畳んでいたので注記も抑えていたが、案A で欄を出すようにしたため揃えた
|
|
2650
|
-
|
|
2685
|
+
// 導出が無いデータでは判断材料が無いので、注記を出す側(=入力欄と揃う側)に倒す
|
|
2686
|
+
var tokenRelevant = derived != null ? derived !== "off" : true;
|
|
2651
2687
|
if (!tokenIsFallback && tokenRelevant && sk && (SITE.secretKinds || []).indexOf(sk) >= 0 && !((fleetVal("secretSources") || {})[sk])) {
|
|
2652
2688
|
body.appendChild(el("div", { class: "ds-tile-note", text: "⚠ トークンが未設定です" }));
|
|
2653
2689
|
}
|
|
@@ -2772,17 +2808,8 @@ ${capabilityState.toString()}
|
|
|
2772
2808
|
var sec = buildDataSources();
|
|
2773
2809
|
if (sec) app.appendChild(sec);
|
|
2774
2810
|
}
|
|
2775
|
-
//
|
|
2776
|
-
|
|
2777
|
-
return el("div", { class: "ds-card ds-inactive", id: anchorId }, [
|
|
2778
|
-
el("div", { class: "ds-card-main" }, [
|
|
2779
|
-
el("span", { class: "ds-icon", text: icon }),
|
|
2780
|
-
el("span", { class: "ds-type", text: typeLabel }),
|
|
2781
|
-
el("span", { class: "ds-name", text: "未使用" }),
|
|
2782
|
-
]),
|
|
2783
|
-
el("span", { class: "ds-inactive-note", text: "この案件では設定されていません" }),
|
|
2784
|
-
]);
|
|
2785
|
-
}
|
|
2811
|
+
// **「未使用」のグレー行は持たない(#125 ⑤)。** 宣言が none の能力にだけ出していた行で、
|
|
2812
|
+
// 宣言を撤去したいま出す条件が無い。未接続の能力は能力タイル(常時表示)が状態と入口を持つ。
|
|
2786
2813
|
// 外部ソース1件のカード(グループ化表示・展開表示の両方から使う)
|
|
2787
2814
|
/**
|
|
2788
2815
|
* Gold昇格の切り替えボタン。押すと設定ファイル(channels.json)が書き換わる。
|
|
@@ -2819,7 +2846,12 @@ ${capabilityState.toString()}
|
|
|
2819
2846
|
"有効にしますか?",
|
|
2820
2847
|
],
|
|
2821
2848
|
build: function (next) {
|
|
2822
|
-
|
|
2849
|
+
// **どのワークスペースのチャンネルかを必ず送る(#104)。** 落とすと、サーバ側は
|
|
2850
|
+
// 既定ワークスペースの同じIDのエントリを探しに行き、「対象チャンネルが
|
|
2851
|
+
// 見つかりません」か、最悪**別のチャンネルを Gold に上げる**
|
|
2852
|
+
var payload = { op: "set-channel-gold", ref: s.ref, value: next };
|
|
2853
|
+
if (s.workspace) payload.workspace = s.workspace;
|
|
2854
|
+
return payload;
|
|
2823
2855
|
},
|
|
2824
2856
|
// 保存できた直後に、この行のバッジだけを「反映中」に差し替える。
|
|
2825
2857
|
// 一覧そのものは触らない(結果メッセージや他行の状態を巻き込まないため)。
|
|
@@ -2828,8 +2860,9 @@ ${capabilityState.toString()}
|
|
|
2828
2860
|
// 画面が出している値と一致するので待つものが無い。ここで消さないと、
|
|
2829
2861
|
// 何も保留していないのに (反映中) が残り、リロードで消えて不可解に見える。
|
|
2830
2862
|
var back = next === (s.gold === true);
|
|
2831
|
-
|
|
2832
|
-
|
|
2863
|
+
// 控えの鍵もワークスペース込み(#104。pendingKeyOf の doc)
|
|
2864
|
+
if (back) clearPendingGold(pendingKeyOf(s));
|
|
2865
|
+
else markPendingGold(pendingKeyOf(s), next);
|
|
2833
2866
|
var card = toggleEl && toggleEl.parentNode;
|
|
2834
2867
|
var badges = card && card.querySelector(".ds-badges");
|
|
2835
2868
|
if (!badges) return;
|
|
@@ -3153,12 +3186,121 @@ ${capabilityState.toString()}
|
|
|
3153
3186
|
return wrap;
|
|
3154
3187
|
}
|
|
3155
3188
|
|
|
3189
|
+
/* ---------------------------------------------------------------------------
|
|
3190
|
+
* Slack ワークスペースの接続(#104)
|
|
3191
|
+
*
|
|
3192
|
+
* 顧客のワークスペースを読み始める日の入口。押す → サーバが署名つきの承認リンクを
|
|
3193
|
+
* 作って返す → そこへ遷移 → 顧客が Slack で承認 → 別の Lambda が受け取って棚に鍵を入れる。
|
|
3194
|
+
*
|
|
3195
|
+
* **リンクを静的に焼き込まない。** 承認リンクには「この案件へ書き込んでよい」という
|
|
3196
|
+
* 10分間有効な署名(state)が載っている。HTML に埋めると、配信物を読める人(=その案件の
|
|
3197
|
+
* Basic 認証を通れる人)が期限のないリンクを手に入れることになるし、
|
|
3198
|
+
* ビルド時点の state はどのみち期限切れで使えない。押した瞬間にサーバから貰う。
|
|
3199
|
+
*
|
|
3200
|
+
* **「トークン」という語を出さない。** 顧客に見せる画面なので、内部語ではなく
|
|
3201
|
+
* 「接続する/読み取りたいチャンネルに招待する」で通す(内部語排除の原則の資格情報版)。
|
|
3202
|
+
* ------------------------------------------------------------------------- */
|
|
3203
|
+
function slackConnectBox() {
|
|
3204
|
+
if (!SITE.intake) return null;
|
|
3205
|
+
// **この項目を知らない古い fleet-status では何も出さない。** 「0件」と「知らない」を
|
|
3206
|
+
// 同じ扱いにすると、engine が @v1 で止まっている案件に「まだ接続されていません」と
|
|
3207
|
+
// 嘘を出すことになる(実際には社内ワークスペースで動いている)
|
|
3208
|
+
var list = slackWorkspaces();
|
|
3209
|
+
if (!list) return null;
|
|
3210
|
+
|
|
3211
|
+
var box = el("div", { class: "ds-addform", "data-adder": "slack-workspace" }, [
|
|
3212
|
+
el("div", { class: "ds-addform-title", text: "Slack ワークスペースを接続" }),
|
|
3213
|
+
el("p", {
|
|
3214
|
+
class: "sec-how",
|
|
3215
|
+
text:
|
|
3216
|
+
"お客様の Slack ワークスペースのチャンネルを読み取り対象にするための接続です。" +
|
|
3217
|
+
"ボタンを押すと Slack の承認画面が開きます(承認するのはワークスペースの管理者です)。" +
|
|
3218
|
+
"お願いする権限は読み取りだけで、Cortex から投稿することはありません。" +
|
|
3219
|
+
"接続したあと、読み取りたいチャンネルにボットを招待し、この画面から追加してください。",
|
|
3220
|
+
}),
|
|
3221
|
+
]);
|
|
3222
|
+
|
|
3223
|
+
// 接続済みの一覧。**0件でも「まだありません」と書く**——空白だと、
|
|
3224
|
+
// 一覧が出ていないのか繋がっていないのかが読めない
|
|
3225
|
+
var listBox = el("div", { class: "ds-ws-list" });
|
|
3226
|
+
if (!list.length) {
|
|
3227
|
+
listBox.appendChild(el("div", { class: "ds-ws-empty", text: "接続済みのワークスペースはまだありません。" }));
|
|
3228
|
+
} else {
|
|
3229
|
+
list.forEach(function (w) {
|
|
3230
|
+
var row = el("div", { class: "ds-ws" }, [
|
|
3231
|
+
el("span", { class: "ds-ws-name", text: w.name || w.teamId }),
|
|
3232
|
+
el("span", {
|
|
3233
|
+
class: "ds-ws-count",
|
|
3234
|
+
text: (w.channels || 0) + "チャンネル",
|
|
3235
|
+
}),
|
|
3236
|
+
]);
|
|
3237
|
+
// **revoked と missing を分けて出す。** 前者は「接続し直してください」、
|
|
3238
|
+
// 後者は「まだ接続が完了していない」で、次の一手が違う。
|
|
3239
|
+
// unknown(確かめられなかった回)には何も出さない——繋がっているのに
|
|
3240
|
+
// 「接続し直して」と言う方が、黙っているより悪い。
|
|
3241
|
+
// 注: ここは埋め込みJSのテンプレートリテラル内。**コメントでもバッククォートを使わない**
|
|
3242
|
+
if (w.tokenState === "revoked") {
|
|
3243
|
+
row.appendChild(el("span", {
|
|
3244
|
+
class: "ds-gate ds-gate-warn",
|
|
3245
|
+
text: "⚠️ 接続が切れています",
|
|
3246
|
+
title: "Slack 側で Cortex のアプリが削除されたか、接続が無効になっています。もう一度接続してください。",
|
|
3247
|
+
}));
|
|
3248
|
+
} else if (w.tokenState === "missing") {
|
|
3249
|
+
row.appendChild(el("span", {
|
|
3250
|
+
class: "ds-gate ds-gate-warn",
|
|
3251
|
+
text: "⚠️ 未接続",
|
|
3252
|
+
title: "このワークスペースのチャンネルが登録されていますが、接続が見つかりません。もう一度接続してください。",
|
|
3253
|
+
}));
|
|
3254
|
+
} else if (w.tokenState === "ok") {
|
|
3255
|
+
row.appendChild(el("span", { class: "ds-gate ds-gate-ok", text: "● 接続OK" }));
|
|
3256
|
+
}
|
|
3257
|
+
listBox.appendChild(row);
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
3260
|
+
box.appendChild(listBox);
|
|
3261
|
+
|
|
3262
|
+
var btn = el("button", { class: "ds-toggle-btn", type: "button", text: "Slack ワークスペースを接続" });
|
|
3263
|
+
var msg = el("div", { class: "ds-toggle-msg" });
|
|
3264
|
+
btn.addEventListener("click", async function () {
|
|
3265
|
+
var intake = SITE.intake;
|
|
3266
|
+
if (!intake) return;
|
|
3267
|
+
btn.disabled = true;
|
|
3268
|
+
msg.className = "ds-toggle-msg";
|
|
3269
|
+
msg.textContent = "承認画面を準備しています…";
|
|
3270
|
+
try {
|
|
3271
|
+
var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
|
|
3272
|
+
var res = await fetch(pfx + "op=slack-oauth-start&t=" + encodeURIComponent(intake.token), {
|
|
3273
|
+
method: "POST",
|
|
3274
|
+
headers: { "content-type": "application/json" },
|
|
3275
|
+
body: "{}",
|
|
3276
|
+
});
|
|
3277
|
+
var j = await res.json().catch(function () { return {}; });
|
|
3278
|
+
if (!res.ok || !j.url) {
|
|
3279
|
+
// サーバの文言をそのまま出す(未設定のときは「準備中です」が返る)
|
|
3280
|
+
msg.className = "ds-toggle-msg is-error";
|
|
3281
|
+
msg.textContent = "✗ " + (j.message || "接続を開始できませんでした。");
|
|
3282
|
+
btn.disabled = false;
|
|
3283
|
+
return;
|
|
3284
|
+
}
|
|
3285
|
+
// **同じタブで遷移する。** 別タブだと、承認後の「接続しました」を見た人が
|
|
3286
|
+
// 元のタブに戻る動線を自分で見つけることになる
|
|
3287
|
+
location.href = j.url;
|
|
3288
|
+
} catch (e) {
|
|
3289
|
+
msg.className = "ds-toggle-msg is-error";
|
|
3290
|
+
msg.textContent = "✗ 接続を開始できませんでした。時間をおいて試してください。";
|
|
3291
|
+
btn.disabled = false;
|
|
3292
|
+
}
|
|
3293
|
+
});
|
|
3294
|
+
box.appendChild(el("div", { class: "ds-toggle" }, [btn, msg]));
|
|
3295
|
+
return box;
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3156
3298
|
/**
|
|
3157
3299
|
* 外部ソースの追加フォーム(Slack・GitHub)。
|
|
3158
3300
|
*
|
|
3159
|
-
* **カタログでゲートする(#125②)。** 宣言(tools
|
|
3160
|
-
*
|
|
3161
|
-
*
|
|
3301
|
+
* **カタログでゲートする(#125②)。** 宣言(tools)によるゲートは撤去した(⑤)。
|
|
3302
|
+
* カタログはこの製品が実装しているアダプタの定義なので、出したボタンが
|
|
3303
|
+
* 「押せるのに必ず失敗する」ことはない。
|
|
3162
3304
|
*/
|
|
3163
3305
|
function renderExternalAdders() {
|
|
3164
3306
|
var box = el("div", {});
|
|
@@ -3198,7 +3340,14 @@ ${capabilityState.toString()}
|
|
|
3198
3340
|
"追加しますか?",
|
|
3199
3341
|
];
|
|
3200
3342
|
},
|
|
3201
|
-
|
|
3343
|
+
// **選んだワークスペースを送る(#104)。** 空文字=社内ワークスペースなのでキーごと落とす
|
|
3344
|
+
// (サーバ側は「未宣言・null・空」を区別しない約束で、無いことで既定を表す)
|
|
3345
|
+
build: function (url, name, workspace) {
|
|
3346
|
+
var payload = { op: "add-channel", url: url, name: name };
|
|
3347
|
+
if (workspace) payload.workspace = workspace;
|
|
3348
|
+
return payload;
|
|
3349
|
+
},
|
|
3350
|
+
workspacePicker: true,
|
|
3202
3351
|
adder: "slack",
|
|
3203
3352
|
}));
|
|
3204
3353
|
|
|
@@ -3383,6 +3532,24 @@ ${capabilityState.toString()}
|
|
|
3383
3532
|
var name = opts.needsName
|
|
3384
3533
|
? el("input", { class: "sec-input", type: "text", placeholder: opts.namePlaceholder || "表示名", autocomplete: "off" })
|
|
3385
3534
|
: null;
|
|
3535
|
+
// 接続先ワークスペースの選択(#104・Slackの追加だけ)。
|
|
3536
|
+
//
|
|
3537
|
+
// **接続済みのワークスペースが1つも無ければ出さない。** 選択肢が「社内」1つだけの
|
|
3538
|
+
// プルダウンは、選ぶものが無いのに1手増やすだけ。既存の全案件がこの状態から始まる。
|
|
3539
|
+
//
|
|
3540
|
+
// **チャンネルのURLから推測しない。** URL のホスト名(acme.slack.com)は
|
|
3541
|
+
// ワークスペースの改名で変わるし、共有チャンネルでは貼った人のホストになる。
|
|
3542
|
+
// 推測して間違えると「追加できたのに読まれない」チャンネルができ、
|
|
3543
|
+
// 翌朝の接続状況で初めて分かる。人に選ばせる。
|
|
3544
|
+
var wsList = opts.workspacePicker ? slackWorkspaces() || [] : [];
|
|
3545
|
+
var wsSelect = null;
|
|
3546
|
+
if (wsList.length) {
|
|
3547
|
+
wsSelect = el("select", { class: "sec-input" });
|
|
3548
|
+
wsSelect.appendChild(el("option", { value: "", text: "社内ワークスペース(クラスメソッド)" }));
|
|
3549
|
+
wsList.forEach(function (w) {
|
|
3550
|
+
wsSelect.appendChild(el("option", { value: w.teamId, text: w.name || w.teamId }));
|
|
3551
|
+
});
|
|
3552
|
+
}
|
|
3386
3553
|
var submitLabel = opts.submitLabel || "追加";
|
|
3387
3554
|
var btn = el("button", { class: "ds-toggle-btn", type: "button", text: submitLabel });
|
|
3388
3555
|
var msg = el("div", { class: "ds-toggle-msg" });
|
|
@@ -3401,7 +3568,7 @@ ${capabilityState.toString()}
|
|
|
3401
3568
|
if (opts.confirmLines && !confirmDanger(opts.confirmLines(main.value.trim()))) { msg.textContent = ""; return; }
|
|
3402
3569
|
var added = main.value.trim();
|
|
3403
3570
|
var addedName = name ? name.value.trim() : "";
|
|
3404
|
-
var payload = opts.build(added, addedName);
|
|
3571
|
+
var payload = opts.build(added, addedName, wsSelect ? wsSelect.value : "");
|
|
3405
3572
|
sendConfig(payload, msg, btn, function () {
|
|
3406
3573
|
// 追加できたら入力欄を空にする(同じものを二度押ししないように)
|
|
3407
3574
|
main.value = "";
|
|
@@ -3428,6 +3595,7 @@ ${capabilityState.toString()}
|
|
|
3428
3595
|
opts.hint ? el("p", { class: "sec-how", text: opts.hint }) : null,
|
|
3429
3596
|
main,
|
|
3430
3597
|
name,
|
|
3598
|
+
wsSelect,
|
|
3431
3599
|
el("div", { class: "ds-toggle" }, [btn, msg]),
|
|
3432
3600
|
pending,
|
|
3433
3601
|
]);
|
|
@@ -3505,18 +3673,27 @@ ${capabilityState.toString()}
|
|
|
3505
3673
|
function writePending(map) {
|
|
3506
3674
|
try { sessionStorage.setItem(PENDING_KEY, JSON.stringify(map)); } catch (_) {}
|
|
3507
3675
|
}
|
|
3508
|
-
function clearPendingGold(
|
|
3509
|
-
if (!
|
|
3676
|
+
function clearPendingGold(key) {
|
|
3677
|
+
if (!key) return;
|
|
3510
3678
|
var map = readPending();
|
|
3511
|
-
if (!(
|
|
3512
|
-
delete map[
|
|
3679
|
+
if (!(key in map)) return;
|
|
3680
|
+
delete map[key];
|
|
3513
3681
|
writePending(map);
|
|
3514
3682
|
}
|
|
3683
|
+
/**
|
|
3684
|
+
* 控えの鍵。**チャンネルIDだけでは足りない(#104)。**
|
|
3685
|
+
* IDはワークスペースの中でしか一意でないので、同じIDのチャンネルを2つのワークスペースに
|
|
3686
|
+
* 持つ案件では、片方で押した「反映中」が**もう片方の行にも出る**。
|
|
3687
|
+
*/
|
|
3688
|
+
function pendingKeyOf(s) {
|
|
3689
|
+
if (!s || !s.ref) return "";
|
|
3690
|
+
return s.workspace ? s.workspace + "\t" + s.ref : s.ref;
|
|
3691
|
+
}
|
|
3515
3692
|
/** 押した内容を控える。fleet-status が追いつくまでの間だけ効く */
|
|
3516
|
-
function markPendingGold(
|
|
3517
|
-
if (!
|
|
3693
|
+
function markPendingGold(key, value) {
|
|
3694
|
+
if (!key) return;
|
|
3518
3695
|
var map = readPending();
|
|
3519
|
-
map[
|
|
3696
|
+
map[key] = { value: value === true, at: Date.now() };
|
|
3520
3697
|
writePending(map);
|
|
3521
3698
|
}
|
|
3522
3699
|
/**
|
|
@@ -3524,9 +3701,10 @@ ${capabilityState.toString()}
|
|
|
3524
3701
|
* 捨てないと、正しくなった後も (反映中) が残り続ける。
|
|
3525
3702
|
*/
|
|
3526
3703
|
function pendingGoldFor(s) {
|
|
3527
|
-
|
|
3704
|
+
var key = pendingKeyOf(s);
|
|
3705
|
+
if (!key) return null;
|
|
3528
3706
|
var map = readPending();
|
|
3529
|
-
var e = map[
|
|
3707
|
+
var e = map[key];
|
|
3530
3708
|
if (!e) return null;
|
|
3531
3709
|
// **「値が一致したら捨てる」だけでは足りない。** 別の人が直後に逆を押して、
|
|
3532
3710
|
// その結果で追いついた場合、値が一致しないので控えが残り、TTL切れまで
|
|
@@ -3559,6 +3737,26 @@ ${capabilityState.toString()}
|
|
|
3559
3737
|
* **「登録から削除」が出てはいけない行に出て**、押しても翌日また現れる。
|
|
3560
3738
|
* 全案件の再生成が回りきったら、github 側のこの分岐を落とす。
|
|
3561
3739
|
*/
|
|
3740
|
+
/**
|
|
3741
|
+
* 接続済み Slack ワークスペース(#104)。**空配列と undefined を区別する**——
|
|
3742
|
+
* 空配列は「まだ1つも繋いでいない」(接続の導線を出してよい)、undefined は
|
|
3743
|
+
* 「この項目を知らない古い fleet-status」(何も言えないので導線も一覧も出さない)。
|
|
3744
|
+
* ライブ(op=state)があればそちら、無ければ静的ビルドの値。
|
|
3745
|
+
*/
|
|
3746
|
+
function slackWorkspaces() {
|
|
3747
|
+
var v = fleetVal("slackWorkspaces");
|
|
3748
|
+
return Array.isArray(v) ? v : null;
|
|
3749
|
+
}
|
|
3750
|
+
/** team_id → 表示名。宣言はあるが一覧に無いワークスペースは team_id のまま出す */
|
|
3751
|
+
function workspaceLabel(teamId) {
|
|
3752
|
+
if (!teamId) return "";
|
|
3753
|
+
var list = slackWorkspaces() || [];
|
|
3754
|
+
for (var i = 0; i < list.length; i++) {
|
|
3755
|
+
if (list[i].teamId === teamId) return list[i].name || teamId;
|
|
3756
|
+
}
|
|
3757
|
+
return teamId;
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3562
3760
|
var ORIGIN_LABEL = { explicit: "明示登録", derived: "自動検出", both: "明示登録+自動検出" };
|
|
3563
3761
|
var ORIGIN_HELP = {
|
|
3564
3762
|
explicit: "開発の登録ファイル(dev-sources.json)に登録されているリポジトリです。ここから登録を削除できます。",
|
|
@@ -3579,6 +3777,16 @@ ${capabilityState.toString()}
|
|
|
3579
3777
|
else if (s.goldState === "undeclared") badges.appendChild(el("span", { class: "badge ds-undeclared", text: "未宣言(対象外)" }));
|
|
3580
3778
|
else if (s.gold === false) badges.appendChild(el("span", { class: "badge ds-excluded", text: "除外" }));
|
|
3581
3779
|
if (s.notify === true) badges.appendChild(el("span", { class: "badge ds-notify", text: "通知先" }));
|
|
3780
|
+
// **どのワークスペースのチャンネルかを出す(#104)。** 同じチャンネル名が
|
|
3781
|
+
// 2つのワークスペースに並びうるので、これが無いと「どっちの #general か」が読めない。
|
|
3782
|
+
// 宣言なし(社内ワークスペース)には何も出さない——これまでどおりの見た目を保つ
|
|
3783
|
+
if (s.type === "slack" && s.workspace) {
|
|
3784
|
+
badges.appendChild(el("span", {
|
|
3785
|
+
class: "badge ds-workspace",
|
|
3786
|
+
text: workspaceLabel(s.workspace),
|
|
3787
|
+
title: "接続した Slack ワークスペースのチャンネルです(読み取り専用の接続なので、通知は届きません)。",
|
|
3788
|
+
}));
|
|
3789
|
+
}
|
|
3582
3790
|
// 登録の由来(#118)。**「登録から削除」が出る/出ないの理由がこれ。** 導出されたリポは
|
|
3583
3791
|
// 登録ファイルから消しても翌日また現れるので削除を出していないが、画面には
|
|
3584
3792
|
// 「同じ見た目の行なのに、片方にだけ削除がある」としか映らず、毎回考え直すことになる。
|
|
@@ -3990,8 +4198,11 @@ ${capabilityState.toString()}
|
|
|
3990
4198
|
// 画面で分かるようにする(接続状況は夜間バッチのスナップショットなので、
|
|
3991
4199
|
// 保存の直後は前日の値のまま。時刻は上の節に出している)。
|
|
3992
4200
|
var src = (fleetVal("secretSources") || {})[kind] || "";
|
|
4201
|
+
// **前方一致で見る。** #105 の移行期は「Secrets Manager(旧名)」も来る。
|
|
4202
|
+
// 完全一致にすると、旧名で動いている案件だけが repo secret と同じ見た目になり、
|
|
4203
|
+
// 「管理下にあるのに未移行」という一番知りたい状態が画面から消える
|
|
3993
4204
|
var state = src
|
|
3994
|
-
? el("p", { class: "sec-state" + (src
|
|
4205
|
+
? el("p", { class: "sec-state" + (src.indexOf("Secrets Manager") === 0 ? " is-managed" : ""),
|
|
3995
4206
|
text: "現在: " + src + " から取得中" })
|
|
3996
4207
|
: el("p", { class: "sec-state is-none", text: "現在: 未設定" });
|
|
3997
4208
|
|
package/package.json
CHANGED