@takagaki/cortex-decisions-viewer 0.12.13 → 0.12.14
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/render.js +260 -52
- package/package.json +1 -1
package/dist/render.js
CHANGED
|
@@ -2080,9 +2080,15 @@ a:hover { text-decoration: underline; }
|
|
|
2080
2080
|
background: #fffbeb; border: 1px solid #fcd34d; border-radius: 8px;
|
|
2081
2081
|
padding: 10px 14px; margin: 12px 0 4px; font-size: 13.5px; color: #92400e; line-height: 1.7;
|
|
2082
2082
|
}
|
|
2083
|
+
/* 警告帯に並ぶ二択(確認済みにする/削除)。AIが起票した記録に人がやることは
|
|
2084
|
+
「引き取る」か「要らないと言う」のどちらかなので、同じ場所に並べる */
|
|
2085
|
+
.draft-note-actions {
|
|
2086
|
+
flex: 0 0 auto; align-self: center; margin-left: auto;
|
|
2087
|
+
display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
|
|
2088
|
+
}
|
|
2083
2089
|
/* 警告帯に並べる「確認済みにする」。帯と同じ系統の色で、押すものだと分かる濃さにする */
|
|
2084
2090
|
.promote-btn {
|
|
2085
|
-
flex: 0 0 auto;
|
|
2091
|
+
flex: 0 0 auto; cursor: pointer; white-space: nowrap;
|
|
2086
2092
|
padding: 6px 14px; border-radius: 999px; border: 1px solid #b45309;
|
|
2087
2093
|
background: #b45309; color: #fff; font-size: 13px; font-weight: 600;
|
|
2088
2094
|
}
|
|
@@ -2133,23 +2139,30 @@ a:hover { text-decoration: underline; }
|
|
|
2133
2139
|
font-size: 12.5px; line-height: 1.7; white-space: pre-wrap; word-break: break-word;
|
|
2134
2140
|
}
|
|
2135
2141
|
/* ---- レコードの削除(?op=delete-record)----
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2142
|
+
置き場は2つ(2026-08-28 のオーナー裁定)。**未確認の記録は警告帯の中**で
|
|
2143
|
+
「確認済みにする」と二択に、**警告帯が出ない記録は上の操作列の末尾**に置く。
|
|
2144
|
+
取り消せない操作なので、どちらでも枠を持たない控えめな文字のボタンにして、
|
|
2145
|
+
隣の実体のあるボタンと重さを分ける。押したあとの見た目は「取り消し線+無効化」 */
|
|
2140
2146
|
.record-delete-btn {
|
|
2141
|
-
background: none; border: 0; padding: 0; cursor: pointer;
|
|
2147
|
+
background: none; border: 0; padding: 0; cursor: pointer; white-space: nowrap;
|
|
2142
2148
|
font-size: 12.5px; color: var(--muted); text-decoration: underline;
|
|
2143
2149
|
}
|
|
2150
|
+
/* 警告帯の中では帯と同じ系統の色に寄せる(帯の外では既定の控えめな灰) */
|
|
2151
|
+
.draft-note .record-delete-btn { color: #92400e; }
|
|
2144
2152
|
.record-delete-btn:hover { color: #b91c1c; }
|
|
2145
2153
|
.record-delete-btn:disabled { cursor: default; text-decoration: none; opacity: 0.7; }
|
|
2146
|
-
.record-delete-msg { font-size: 13px;
|
|
2154
|
+
.record-delete-msg { font-size: 13px; }
|
|
2155
|
+
.record-delete-msg:empty { display: none; }
|
|
2147
2156
|
.record-delete-msg.is-error { color: #b91c1c; }
|
|
2157
|
+
/* 帯の中では1行を占める(ボタンの並びの下へ回り込ませる)。操作列では右寄せのまま畳む */
|
|
2158
|
+
.draft-note > .record-delete-msg { flex: 1 0 100%; }
|
|
2159
|
+
.detail-actions .record-delete-msg { max-width: 320px; text-align: right; }
|
|
2148
2160
|
.deleted-note {
|
|
2149
|
-
display: flex; gap: 8px; align-items: flex-start; margin
|
|
2150
|
-
background: #
|
|
2161
|
+
display: flex; gap: 8px; align-items: flex-start; margin: 4px 0 16px;
|
|
2162
|
+
background: #fff; border: 1px solid var(--border); border-radius: 8px;
|
|
2151
2163
|
padding: 10px 12px; font-size: 13px; line-height: 1.8; color: var(--muted);
|
|
2152
2164
|
}
|
|
2165
|
+
.draft-note > .deleted-note { flex: 1 0 100%; margin-bottom: 0; }
|
|
2153
2166
|
/* 消したものは、消えたと分かる形で残す(一覧・詳細の両方) */
|
|
2154
2167
|
.is-deleted { text-decoration: line-through; color: var(--muted); }
|
|
2155
2168
|
/* 消したレコードでは、直す口(AIで編集・GitHubで編集)を押させない */
|
|
@@ -3078,6 +3091,11 @@ ${deriveCapTile.toString()}
|
|
|
3078
3091
|
// **確認済みにできる記録には、その場で上げるボタンを並べる**(下の promotableRecordPath)。
|
|
3079
3092
|
// ボタンが出せないとき(受け口が未設定・パスが解決できない)は、これまでどおり
|
|
3080
3093
|
// 「frontmatter を直してください」の案内に倒す——押せない画面で押す話をしない
|
|
3094
|
+
//
|
|
3095
|
+
// **削除もここに並べる(2026-08-28 のオーナー裁定)。** AIが起票した記録に対して
|
|
3096
|
+
// 人がやることは「引き取る(確認済みにする)」か「要らないと言う(削除)」の二択で、
|
|
3097
|
+
// 同じ場面の同じ判断なので、口を離さない。**この帯は draft にしか出ない**ので、
|
|
3098
|
+
// 人が確認済みにした記録には削除の口ごと現れない(守りはサーバ側にもある)。
|
|
3081
3099
|
function draftNote(rec) {
|
|
3082
3100
|
if (!rec || rec.rstatus !== "draft") return null;
|
|
3083
3101
|
var path = promotableRecordPath(rec);
|
|
@@ -3087,13 +3105,19 @@ ${deriveCapTile.toString()}
|
|
|
3087
3105
|
? "この内容はAIが自動生成したものです(人間未確認)。内容を確認し、直すところが無ければ「確認済みにする」を押してください。直したいところがあれば「AIで編集」から修正してください。"
|
|
3088
3106
|
: "この内容はAIが自動生成したものです(人間未確認)。必ず内容を確認し、適切な内容に直したうえで frontmatter の status を active に変更してください。" }),
|
|
3089
3107
|
]);
|
|
3108
|
+
var actions = el("div", { class: "draft-note-actions" });
|
|
3090
3109
|
if (path) {
|
|
3091
3110
|
var btn = el("button", { class: "promote-btn", type: "button", text: "確認済みにする" });
|
|
3092
3111
|
btn.addEventListener("click", function () {
|
|
3093
3112
|
openPromoteModal(path, function () { markPromoted(rec, note); });
|
|
3094
3113
|
});
|
|
3095
|
-
|
|
3114
|
+
actions.appendChild(btn);
|
|
3096
3115
|
}
|
|
3116
|
+
var del = buildDeleteControls(rec);
|
|
3117
|
+
if (del) actions.appendChild(del.btn);
|
|
3118
|
+
if (actions.children.length) note.appendChild(actions);
|
|
3119
|
+
// 断られた理由・削除した旨は帯の中に出す(口と結果を離さない)
|
|
3120
|
+
if (del) note.appendChild(del.msg);
|
|
3097
3121
|
return note;
|
|
3098
3122
|
}
|
|
3099
3123
|
|
|
@@ -10161,7 +10185,7 @@ ${deriveCapTile.toString()}
|
|
|
10161
10185
|
filters.appendChild(buildSelect("すべての期間", SITE.months, listState.month, function (v) { listState.month = v; listState.page = 1; drawCards(); }, monthLabel));
|
|
10162
10186
|
}
|
|
10163
10187
|
filters.appendChild(buildSelect("すべてのカテゴリー", SITE.categories, listState.category, function (v) { listState.category = v; listState.page = 1; drawCards(); }));
|
|
10164
|
-
var statusOptsD = statusFilterOptions(SITE.decisions);
|
|
10188
|
+
var statusOptsD = statusFilterOptions(visibleRecords(SITE.decisions));
|
|
10165
10189
|
if (statusOptsD.length) {
|
|
10166
10190
|
filters.appendChild(buildSelect("すべての状態", statusOptsD, listState.status, function (v) { listState.status = v; listState.page = 1; drawCards(); }, statusLabel));
|
|
10167
10191
|
}
|
|
@@ -10189,7 +10213,9 @@ ${deriveCapTile.toString()}
|
|
|
10189
10213
|
|
|
10190
10214
|
function drawCards() {
|
|
10191
10215
|
var q = listState.q.trim().toLowerCase();
|
|
10192
|
-
|
|
10216
|
+
// 削除済みは絞り込みの手前で落とす(分母も一緒に減るので「12/13件」が残らない)
|
|
10217
|
+
var all = visibleRecords(SITE.decisions);
|
|
10218
|
+
var items = all.filter(function (d) {
|
|
10193
10219
|
if (listState.month && d.date.slice(0, 7) !== listState.month) return false;
|
|
10194
10220
|
if (listState.category && d.category !== listState.category) return false;
|
|
10195
10221
|
if (!matchesStatusFilter(d)) return false;
|
|
@@ -10204,14 +10230,14 @@ ${deriveCapTile.toString()}
|
|
|
10204
10230
|
wrap.innerHTML = "";
|
|
10205
10231
|
|
|
10206
10232
|
if (!items.length) {
|
|
10207
|
-
countEl.textContent = "0 / " +
|
|
10233
|
+
countEl.textContent = "0 / " + all.length + " 件";
|
|
10208
10234
|
wrap.appendChild(el("div", { class: "empty", text: "該当する決定はありません" }));
|
|
10209
10235
|
return;
|
|
10210
10236
|
}
|
|
10211
10237
|
|
|
10212
10238
|
var pg = paginate(items);
|
|
10213
10239
|
countEl.textContent =
|
|
10214
|
-
items.length + " / " +
|
|
10240
|
+
items.length + " / " + all.length + " 件" +
|
|
10215
10241
|
(pg.pages > 1 ? "(" + pg.page + "/" + pg.pages + "ページ)" : "");
|
|
10216
10242
|
|
|
10217
10243
|
pg.slice.forEach(function (d) {
|
|
@@ -10246,7 +10272,8 @@ ${deriveCapTile.toString()}
|
|
|
10246
10272
|
function renderGenericList() {
|
|
10247
10273
|
var tab = null;
|
|
10248
10274
|
TABS.forEach(function (t) { if (t.key === listState.tab) tab = t; });
|
|
10249
|
-
|
|
10275
|
+
// 削除済みは描画の手前で落とす(件数の分母・絞り込みの選択肢もこれ1本で揃う)
|
|
10276
|
+
var items = visibleRecords(tab && tab.items ? tab.items : []);
|
|
10250
10277
|
|
|
10251
10278
|
var filters = el("div", { class: "filters" });
|
|
10252
10279
|
var search = el("input", { type: "search", placeholder: "キーワード検索" });
|
|
@@ -10490,9 +10517,10 @@ ${deriveCapTile.toString()}
|
|
|
10490
10517
|
/**
|
|
10491
10518
|
* 確認の文面。**何が起きるかを具体に書く**(消えるだけなのか、除外リストにも載るのか)。
|
|
10492
10519
|
*
|
|
10493
|
-
*
|
|
10494
|
-
*
|
|
10495
|
-
*
|
|
10520
|
+
* **status による出し分けはもう無い。** 2026-08-28 のオーナー裁定は最終的に
|
|
10521
|
+
* 「消せるのは draft だけ」へ落ち着いた(確認済みの記録に重い警告を出して消させる、
|
|
10522
|
+
* ではなく、口ごと出さない)。判定は draftNote の出し分けとサーバの門にあるので、
|
|
10523
|
+
* ここは「何が起きるか」だけを書く。
|
|
10496
10524
|
*/
|
|
10497
10525
|
function deleteConfirmLines(rec, path) {
|
|
10498
10526
|
var lines = ["このレコードを削除しますか?", rec.title];
|
|
@@ -10500,10 +10528,9 @@ ${deriveCapTile.toString()}
|
|
|
10500
10528
|
if (label) {
|
|
10501
10529
|
lines.push("削除すると、夜間のAIが同じ内容を再登録しないよう、" + label + "の除外リストにも追記されます。");
|
|
10502
10530
|
}
|
|
10503
|
-
if (rec.rstatus === "active") lines.push("これは人が確認済みにした記録です。");
|
|
10504
10531
|
return lines;
|
|
10505
10532
|
}
|
|
10506
|
-
async function sendDeleteRecord(path, msg, btn, onOk) {
|
|
10533
|
+
async function sendDeleteRecord(rec, path, msg, btn, onOk) {
|
|
10507
10534
|
var intake = SITE.intake;
|
|
10508
10535
|
if (!intake) return;
|
|
10509
10536
|
function setError(text) {
|
|
@@ -10534,6 +10561,8 @@ ${deriveCapTile.toString()}
|
|
|
10534
10561
|
// **なっている**ので、失敗として見せると「押しても消えない」堂々巡りになる。
|
|
10535
10562
|
// **符号で見る**(promote の ALREADY_ACTIVE と同じ理由)
|
|
10536
10563
|
if (res.ok || (res.status === 409 && j.code === "ALREADY_DELETED")) {
|
|
10564
|
+
// このブラウザでは即座に消えたことにする(次のビルドまで SiteData は古い)
|
|
10565
|
+
rememberDeleted(rec, path);
|
|
10537
10566
|
endBusy();
|
|
10538
10567
|
onOk();
|
|
10539
10568
|
return;
|
|
@@ -10559,52 +10588,219 @@ ${deriveCapTile.toString()}
|
|
|
10559
10588
|
rec.deleted = true;
|
|
10560
10589
|
var title = app.querySelector(".detail-title");
|
|
10561
10590
|
if (title) title.classList.add("is-deleted");
|
|
10562
|
-
|
|
10563
|
-
|
|
10591
|
+
// **置き場は2つある**(draft の警告帯の中/上の操作列)。どちらに出ていても止める
|
|
10592
|
+
var btns = app.querySelectorAll(".record-delete-btn");
|
|
10593
|
+
for (var b = 0; b < btns.length; b++) { btns[b].disabled = true; btns[b].textContent = "削除しました"; }
|
|
10564
10594
|
// もう無い記録を直す口は押させない(AIで編集・GitHubで編集・確認済みにする)
|
|
10565
10595
|
var links = app.querySelectorAll(".detail-actions a.ai-edit-btn, .detail-actions a.edit-btn");
|
|
10566
10596
|
for (var i = 0; i < links.length; i++) disableDetailLink(links[i]);
|
|
10567
10597
|
var promote = app.querySelector(".draft-note .promote-btn");
|
|
10568
10598
|
if (promote) promote.disabled = true;
|
|
10569
|
-
|
|
10570
|
-
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
|
|
10574
|
-
|
|
10575
|
-
|
|
10599
|
+
if (app.querySelector(".deleted-note")) return;
|
|
10600
|
+
var note = el("div", { class: "deleted-note" }, [
|
|
10601
|
+
el("span", { text: "🗑" }),
|
|
10602
|
+
el("span", { text: "削除しました。次のビルド(数分後)でサイトから消えます。" }),
|
|
10603
|
+
]);
|
|
10604
|
+
// 押した口の近くに出す。警告帯があるならその中、無ければ見出しの直下
|
|
10605
|
+
var banner = app.querySelector(".draft-note");
|
|
10606
|
+
if (banner) { banner.appendChild(note); return; }
|
|
10607
|
+
var meta = app.querySelector(".detail-meta");
|
|
10608
|
+
if (meta && meta.parentNode) meta.parentNode.insertBefore(note, meta.nextSibling);
|
|
10576
10609
|
}
|
|
10577
10610
|
/**
|
|
10578
|
-
*
|
|
10579
|
-
*
|
|
10611
|
+
* 人の確認印が付いた記録か。**3値の意味は infra の HUMAN_CONFIRMED_STATUSES と同じ**
|
|
10612
|
+
* (active = 人が確認済みにした印/superseded = active を経た記録の差し替え後/
|
|
10613
|
+
* inactive = 人が退場を記した印)。ここは「押しても断られるボタンを出さない」ための
|
|
10614
|
+
* 出し分けで、**守りはサーバ側**(promote の PROMOTABLE_PATH と同じ関係)。
|
|
10580
10615
|
*/
|
|
10581
|
-
function
|
|
10616
|
+
function humanConfirmedStatus(rec) {
|
|
10617
|
+
var s = rec && rec.rstatus;
|
|
10618
|
+
return s === "active" || s === "superseded" || s === "inactive";
|
|
10619
|
+
}
|
|
10620
|
+
/**
|
|
10621
|
+
* 削除の口(ボタンと、その結果を出す行)。**置き場は呼び出し側が決める。**
|
|
10622
|
+
*
|
|
10623
|
+
* 出さないのは**人の確認印が付いた記録だけ**(2026-08-28 の裁定変更)。原則は
|
|
10624
|
+
* 「人間の承認が挟まったものは削除の選択肢を出さない」で、逆に status が無い記録・
|
|
10625
|
+
* frontmatter が壊れた記録は**画面から片付けたい掃除の主対象**なので消せる。
|
|
10626
|
+
*
|
|
10627
|
+
* 呼び出し側は2つ:
|
|
10628
|
+
* - draftNote …… 未確認の記録。「確認済みにする」と並べて警告帯の中に置く
|
|
10629
|
+
* - 詳細ページの操作列 …… 警告帯が出ない記録。置き場が他に無いのでここに寄せる
|
|
10630
|
+
*/
|
|
10631
|
+
function buildDeleteControls(rec) {
|
|
10632
|
+
if (humanConfirmedStatus(rec)) return null;
|
|
10582
10633
|
var path = deletableRecordPath(rec);
|
|
10583
10634
|
if (!path) return null;
|
|
10584
|
-
|
|
10585
|
-
|
|
10635
|
+
// 「…」は**押すと確認が出る**合図(「このレコードを削除…」から引き継いだ慣習)。
|
|
10636
|
+
// 隣の「確認済みにする」は押した先で名前入力があるので、二択の重さの違いを語尾で示す
|
|
10637
|
+
var btn = el("button", { class: "record-delete-btn", type: "button", text: "削除…" });
|
|
10586
10638
|
var msg = el("div", { class: "record-delete-msg" });
|
|
10587
10639
|
btn.addEventListener("click", async function () {
|
|
10588
10640
|
// **確認を await する。** 落とすと Promise が truthy で確認なしに消える
|
|
10589
10641
|
if (!(await confirmDanger(deleteConfirmLines(rec, path), { okLabel: "削除する", returnTo: btn }))) return;
|
|
10590
|
-
await sendDeleteRecord(path, msg, btn, function () { markRecordDeleted(rec); });
|
|
10642
|
+
await sendDeleteRecord(rec, path, msg, btn, function () { markRecordDeleted(rec); });
|
|
10591
10643
|
});
|
|
10592
|
-
|
|
10593
|
-
wrap.appendChild(msg);
|
|
10594
|
-
return wrap;
|
|
10644
|
+
return { btn: btn, msg: msg };
|
|
10595
10645
|
}
|
|
10596
|
-
/**
|
|
10646
|
+
/**
|
|
10647
|
+
* 警告帯が出ない記録の削除の口を、上の操作列(AIで編集・GitHubで編集の行)の末尾へ置く。
|
|
10648
|
+
*
|
|
10649
|
+
* **draft には出さない**——そちらは警告帯の中で「確認済みにする」と対になっている
|
|
10650
|
+
* (2つ出すと、同じ操作の口がページに2か所できる)。取り消せない操作を高頻度の
|
|
10651
|
+
* 操作の隣に置くことにはなるが、**警告帯の無い記録には他に置き場が無い**。
|
|
10652
|
+
* 見た目は控えめな文字のボタンのままにして、隣の実体のあるボタンと重さを分ける。
|
|
10653
|
+
*/
|
|
10654
|
+
function attachDeleteToEditRow(rec, editRow) {
|
|
10655
|
+
if (!rec || rec.rstatus === "draft") return null;
|
|
10656
|
+
var del = buildDeleteControls(rec);
|
|
10657
|
+
if (!del) return null;
|
|
10658
|
+
editRow.appendChild(del.btn);
|
|
10659
|
+
return del.msg; // 結果を出す行は操作列の下(呼び出し側が最後に並べる)
|
|
10660
|
+
}
|
|
10661
|
+
/** 詳細ページの末尾(本文・提案・frontmatter の後)に、AIの変更履歴と frontmatter を並べる */
|
|
10597
10662
|
function appendRecordFooter(rec) {
|
|
10598
10663
|
var history = buildAiHistory(rec);
|
|
10599
10664
|
if (history) app.appendChild(history);
|
|
10600
10665
|
var fmp = renderFmPanel(rec.fm);
|
|
10601
10666
|
if (fmp) app.appendChild(fmp);
|
|
10602
|
-
var del = buildDeleteSection(rec);
|
|
10603
|
-
if (del) app.appendChild(del);
|
|
10604
10667
|
// 開き直しても削除済みのまま見せる(印は rec が持っている)
|
|
10605
10668
|
if (rec.deleted) markRecordDeleted(rec);
|
|
10606
10669
|
}
|
|
10607
10670
|
|
|
10671
|
+
/* ---------------------------------------------------------------------------
|
|
10672
|
+
* 削除の即時反映(トゥームストーン)
|
|
10673
|
+
* ---------------------------------------------------------------------------
|
|
10674
|
+
* 静的サイトなので、削除の commit が入っても**次のビルドまで SiteData は古いまま**。
|
|
10675
|
+
* 何もしないと「消したのに、一覧にもつながりにもまだ居る」ように見える。
|
|
10676
|
+
* そこで削除した記録をこのブラウザに覚えておき、描画の手前で間引く。
|
|
10677
|
+
*
|
|
10678
|
+
* **覚えるのは押した本人のブラウザだけ。** 他の閲覧者には次のビルドまで見えたまま
|
|
10679
|
+
* (2026-08-28 の裁定で了解済みの範囲)。サーバに人ごとの表示状態を持たせない。
|
|
10680
|
+
*
|
|
10681
|
+
* **いつまでも隠し続けない。** 隠しっぱなしにすると、同じ記録を作り直したのに
|
|
10682
|
+
* 「作ったはずのものが出てこない」状態が永久に続く(破棄の条件は applyDeleteTombstones)。
|
|
10683
|
+
*/
|
|
10684
|
+
function deletedStoreKey() { return "cortex-deleted:" + REPO_SLUG; }
|
|
10685
|
+
/** 「削除より後に始まったビルドか」を判断するときの余裕(下の tombstoneStale) */
|
|
10686
|
+
var TOMBSTONE_MARGIN_MS = 30 * 60 * 1000;
|
|
10687
|
+
|
|
10688
|
+
function readTombstones() {
|
|
10689
|
+
try {
|
|
10690
|
+
var raw = localStorage.getItem(deletedStoreKey());
|
|
10691
|
+
var list = raw ? JSON.parse(raw) : [];
|
|
10692
|
+
return Object.prototype.toString.call(list) === "[object Array]" ? list : [];
|
|
10693
|
+
} catch (e) { return []; }
|
|
10694
|
+
}
|
|
10695
|
+
function writeTombstones(list) {
|
|
10696
|
+
try { localStorage.setItem(deletedStoreKey(), JSON.stringify(list)); } catch (e) {}
|
|
10697
|
+
}
|
|
10698
|
+
/**
|
|
10699
|
+
* 削除できたことを覚える。**同じパスは足さない**(押し直しで配列が伸びない)。
|
|
10700
|
+
*
|
|
10701
|
+
* gen0(削除操作をした画面のビルド時刻)も残す。破棄の判定をできるだけ
|
|
10702
|
+
* **サーバ時刻同士の比較**にするため(下の tombstoneStale)。
|
|
10703
|
+
*/
|
|
10704
|
+
function rememberDeleted(rec, path) {
|
|
10705
|
+
var list = readTombstones();
|
|
10706
|
+
for (var i = 0; i < list.length; i++) {
|
|
10707
|
+
if (list[i] && list[i].path === path) return;
|
|
10708
|
+
}
|
|
10709
|
+
list.push({ path: path, id: (rec && rec.id) || "", at: Date.now(), gen0: SITE.generatedAt || "" });
|
|
10710
|
+
writeTombstones(list);
|
|
10711
|
+
}
|
|
10712
|
+
/**
|
|
10713
|
+
* ビルド時刻の文字列(cli.ts が入れる "YYYY-MM-DD HH:mm UTC")をミリ秒に直す。
|
|
10714
|
+
* 読めなければ NaN——呼び出し側は**隠し続ける側に倒す**(嘘の再表示より安全)。
|
|
10715
|
+
*/
|
|
10716
|
+
function siteTimeMs(s) {
|
|
10717
|
+
if (typeof s !== "string" || !s) return NaN;
|
|
10718
|
+
return Date.parse(s.replace(" UTC", "Z").replace(" ", "T"));
|
|
10719
|
+
}
|
|
10720
|
+
/**
|
|
10721
|
+
* このトゥームストーンはもう用済みか(=削除より後のビルドにも記録が残っている)。
|
|
10722
|
+
*
|
|
10723
|
+
* **generatedAt はコミット時点ではない。** 削除コミットより前の状態から始まった
|
|
10724
|
+
* ビルドが削除の後に完成すると、generatedAt は削除時刻より新しいのに削除は
|
|
10725
|
+
* 入っていない。ビルドの所要時間とクライアント時計のずれを見て 30 分の余裕を取る。
|
|
10726
|
+
* 副作用は「同じ記録を作り直した直後だけ、本人のブラウザで最大30分見えない」。
|
|
10727
|
+
*
|
|
10728
|
+
* **主軸はサーバ時刻同士の比較**(現行の generatedAt と、削除した画面の gen0)。
|
|
10729
|
+
* クライアントの時計が大きく狂っていても機能が丸ごとは死なない(進んだ時計なら
|
|
10730
|
+
* 再表示が遅れるだけ)。
|
|
10731
|
+
*/
|
|
10732
|
+
function tombstoneStale(t, nowMs) {
|
|
10733
|
+
if (isNaN(nowMs)) return false;
|
|
10734
|
+
var byAt = typeof t.at === "number" && nowMs > t.at + TOMBSTONE_MARGIN_MS;
|
|
10735
|
+
var gen0 = siteTimeMs(t.gen0);
|
|
10736
|
+
// gen0 を持たない古い形式のエントリは at だけで見る(後方互換・安全側)
|
|
10737
|
+
if (isNaN(gen0)) return byAt;
|
|
10738
|
+
return byAt && nowMs > gen0 + TOMBSTONE_MARGIN_MS;
|
|
10739
|
+
}
|
|
10740
|
+
/** トゥームストーンの相手を SITE から探す(id で引き、無ければパスで照合) */
|
|
10741
|
+
function findTombstoned(t) {
|
|
10742
|
+
if (t.id && byId[t.id]) return byId[t.id];
|
|
10743
|
+
if (!t.path) return null;
|
|
10744
|
+
var pool = (SITE.decisions || []).slice();
|
|
10745
|
+
(SITE.sections || []).forEach(function (sec) { pool = pool.concat(sec.records || []); });
|
|
10746
|
+
for (var i = 0; i < pool.length; i++) {
|
|
10747
|
+
if (repoPathOf(pool[i]) === t.path) return pool[i];
|
|
10748
|
+
}
|
|
10749
|
+
return null;
|
|
10750
|
+
}
|
|
10751
|
+
/**
|
|
10752
|
+
* ページを開いたときに1回だけ走らせる。**残すのは「まだ SiteData に居て、かつ
|
|
10753
|
+
* 削除より後のビルドでは消えているはず」のものだけ**:
|
|
10754
|
+
*
|
|
10755
|
+
* 1. 相手が SiteData に居ない → ビルドに反映済み。エントリを捨てる
|
|
10756
|
+
* 2. 居るが、削除より後のビルドにも居る → 作り直された(または revert)。
|
|
10757
|
+
* 捨てて**表示に戻す**(隠し続けると「作り直したのに出ない」になる)
|
|
10758
|
+
* 3. それ以外 → 削除済みの印を立てる
|
|
10759
|
+
*/
|
|
10760
|
+
function applyDeleteTombstones() {
|
|
10761
|
+
var list = readTombstones();
|
|
10762
|
+
if (!list.length) return;
|
|
10763
|
+
var nowMs = siteTimeMs(SITE.generatedAt);
|
|
10764
|
+
var kept = [];
|
|
10765
|
+
for (var i = 0; i < list.length; i++) {
|
|
10766
|
+
var t = list[i];
|
|
10767
|
+
if (!t || typeof t.path !== "string" || !t.path) continue;
|
|
10768
|
+
var rec = findTombstoned(t);
|
|
10769
|
+
if (!rec) continue;
|
|
10770
|
+
if (tombstoneStale(t, nowMs)) continue;
|
|
10771
|
+
rec.deleted = true;
|
|
10772
|
+
kept.push(t);
|
|
10773
|
+
}
|
|
10774
|
+
if (kept.length !== list.length) writeTombstones(kept);
|
|
10775
|
+
}
|
|
10776
|
+
applyDeleteTombstones();
|
|
10777
|
+
|
|
10778
|
+
/** 一覧に出すレコード。**描画の手前で間引く**——件数表示も絞り込みの選択肢も自然に揃う */
|
|
10779
|
+
function visibleRecords(items) {
|
|
10780
|
+
return (items || []).filter(function (r) { return !r.deleted; });
|
|
10781
|
+
}
|
|
10782
|
+
/**
|
|
10783
|
+
* 削除済みを除いたグラフ。**消費側で1行ずつ摘まむのではなく、ここ1箇所で作る**
|
|
10784
|
+
* ——グラフ・エクスプローラー・件数表示が別々に間引くと、片方だけ消え残る。
|
|
10785
|
+
*/
|
|
10786
|
+
function graphWithoutDeleted() {
|
|
10787
|
+
var g = SITE.graph || {};
|
|
10788
|
+
var nodes = g.nodes || [];
|
|
10789
|
+
var edges = g.edges || [];
|
|
10790
|
+
var gone = {};
|
|
10791
|
+
var any = false;
|
|
10792
|
+
nodes.forEach(function (n) {
|
|
10793
|
+
var rec = n.recordId ? byId[n.recordId] : null;
|
|
10794
|
+
if (rec && rec.deleted) { gone[n.id] = true; any = true; }
|
|
10795
|
+
});
|
|
10796
|
+
if (!any) return { nodes: nodes, edges: edges };
|
|
10797
|
+
return {
|
|
10798
|
+
nodes: nodes.filter(function (n) { return !gone[n.id]; }),
|
|
10799
|
+
// 削除した点に触れる辺も落とす(残すと相手のいない線が浮く)
|
|
10800
|
+
edges: edges.filter(function (e) { return !gone[e.from] && !gone[e.to]; }),
|
|
10801
|
+
};
|
|
10802
|
+
}
|
|
10803
|
+
|
|
10608
10804
|
// ---------- 汎用詳細 ----------
|
|
10609
10805
|
function renderGenericDetail(r) {
|
|
10610
10806
|
app.innerHTML = "";
|
|
@@ -10624,9 +10820,12 @@ ${deriveCapTile.toString()}
|
|
|
10624
10820
|
if (r.editUrl) {
|
|
10625
10821
|
editRowR.appendChild(el("a", { class: "edit-btn", href: r.editUrl, target: "_blank", rel: "noopener", text: "GitHubで編集" }));
|
|
10626
10822
|
}
|
|
10823
|
+
// 警告帯が出ない記録の削除の口はここ(編集の行の末尾)。draft には出さない
|
|
10824
|
+
var delMsgR = attachDeleteToEditRow(r, editRowR);
|
|
10627
10825
|
head.appendChild(el("div", { class: "detail-actions" }, [
|
|
10628
10826
|
el("div", { class: "detail-actions-row" }, [ traceBtn2 ]),
|
|
10629
10827
|
editRowR.children.length ? editRowR : null,
|
|
10828
|
+
delMsgR,
|
|
10630
10829
|
]));
|
|
10631
10830
|
app.appendChild(head);
|
|
10632
10831
|
|
|
@@ -10682,7 +10881,9 @@ ${deriveCapTile.toString()}
|
|
|
10682
10881
|
var graphState = { showWeak: true, q: "" };
|
|
10683
10882
|
|
|
10684
10883
|
function renderGraph() {
|
|
10685
|
-
|
|
10884
|
+
// 削除済みを除いたグラフを1本作り、件数・凡例・描画の全部をこれで賄う
|
|
10885
|
+
var graph = graphWithoutDeleted();
|
|
10886
|
+
countEl.textContent = "点 " + graph.nodes.length + " / つながり " + graph.edges.length;
|
|
10686
10887
|
|
|
10687
10888
|
var filters = el("div", { class: "filters" });
|
|
10688
10889
|
var search = el("input", { type: "search", placeholder: "キーワードで探す(決定・用語・レポートは本文の中まで探します。見つかった点とそのつながりが光ります)" });
|
|
@@ -10699,7 +10900,7 @@ ${deriveCapTile.toString()}
|
|
|
10699
10900
|
var legend = el("div", { class: "graph-legend" });
|
|
10700
10901
|
var legendShown = {};
|
|
10701
10902
|
Object.keys(GROUP_STYLE).forEach(function (g) {
|
|
10702
|
-
var anyNode =
|
|
10903
|
+
var anyNode = graph.nodes.some(function (n) { return n.group === g; });
|
|
10703
10904
|
if (!anyNode || legendShown[GROUP_STYLE[g].label]) return;
|
|
10704
10905
|
legendShown[GROUP_STYLE[g].label] = true;
|
|
10705
10906
|
var item = el("span", {}, [el("span", { class: "dot" }), document.createTextNode(GROUP_STYLE[g].label)]);
|
|
@@ -10728,19 +10929,19 @@ ${deriveCapTile.toString()}
|
|
|
10728
10929
|
legend.appendChild(toggle);
|
|
10729
10930
|
app.appendChild(legend);
|
|
10730
10931
|
|
|
10731
|
-
startGraph(canvas, search);
|
|
10932
|
+
startGraph(canvas, search, graph);
|
|
10732
10933
|
}
|
|
10733
10934
|
|
|
10734
|
-
function startGraph(canvas, searchInput) {
|
|
10735
|
-
var allEdges =
|
|
10935
|
+
function startGraph(canvas, searchInput, graph) {
|
|
10936
|
+
var allEdges = graph.edges.filter(function (e) {
|
|
10736
10937
|
return graphState.showWeak || !WEAK_RELS[e.rel];
|
|
10737
10938
|
});
|
|
10738
10939
|
// 弱い辺を消したとき、レコード以外の孤立ノードは隠す
|
|
10739
10940
|
var used = {};
|
|
10740
10941
|
allEdges.forEach(function (e) { used[e.from] = true; used[e.to] = true; });
|
|
10741
10942
|
// ビルド時に算出済みの座標(n.x/n.y)があればそれを使う=ウォームアップ不要で開いた瞬間に整列済み。
|
|
10742
|
-
var preLaidOut =
|
|
10743
|
-
var nodes =
|
|
10943
|
+
var preLaidOut = graph.nodes.some(function (n) { return typeof n.x === "number"; });
|
|
10944
|
+
var nodes = graph.nodes
|
|
10744
10945
|
.filter(function (n) { return n.recordId || used[n.id]; })
|
|
10745
10946
|
.map(function (n, i) {
|
|
10746
10947
|
return { id: n.id, label: n.label, group: n.group, recordId: n.recordId, url: n.url,
|
|
@@ -11291,9 +11492,12 @@ ${deriveCapTile.toString()}
|
|
|
11291
11492
|
if (d.editUrl) {
|
|
11292
11493
|
editRowD.appendChild(el("a", { class: "edit-btn", href: d.editUrl, target: "_blank", rel: "noopener", text: "GitHubで編集" }));
|
|
11293
11494
|
}
|
|
11495
|
+
// 警告帯が出ない記録の削除の口はここ(編集の行の末尾)。draft には出さない
|
|
11496
|
+
var delMsgD = attachDeleteToEditRow(d, editRowD);
|
|
11294
11497
|
head.appendChild(el("div", { class: "detail-actions" }, [
|
|
11295
11498
|
el("div", { class: "detail-actions-row" }, [ traceBtn ]),
|
|
11296
11499
|
editRowD.children.length ? editRowD : null,
|
|
11500
|
+
delMsgD,
|
|
11297
11501
|
]));
|
|
11298
11502
|
app.appendChild(head);
|
|
11299
11503
|
|
|
@@ -11362,10 +11566,13 @@ ${deriveCapTile.toString()}
|
|
|
11362
11566
|
supersedes: "置換", reference: "参照", source: "出典", link: "リンク",
|
|
11363
11567
|
};
|
|
11364
11568
|
var explorerState = { folder: null, path: [], q: "", sx: 0, autoRight: false };
|
|
11569
|
+
// 索引もグラフ本体と同じ材料(削除済みを除いたもの)から作る。別々に間引くと、
|
|
11570
|
+
// つながり画面から消した点がエクスプローラーの列にだけ残る
|
|
11571
|
+
var explorerGraph = graphWithoutDeleted();
|
|
11365
11572
|
var nodeById = {};
|
|
11366
|
-
|
|
11573
|
+
explorerGraph.nodes.forEach(function (n) { nodeById[n.id] = n; });
|
|
11367
11574
|
var adjDir = {};
|
|
11368
|
-
|
|
11575
|
+
explorerGraph.edges.forEach(function (e) {
|
|
11369
11576
|
(adjDir[e.from] = adjDir[e.from] || []).push({ id: e.to, rel: e.rel, dir: "out" });
|
|
11370
11577
|
(adjDir[e.to] = adjDir[e.to] || []).push({ id: e.from, rel: e.rel, dir: "in" });
|
|
11371
11578
|
});
|
|
@@ -11382,9 +11589,10 @@ ${deriveCapTile.toString()}
|
|
|
11382
11589
|
// Gold層のフォルダ(1列目)。検索ヒット表示と使い分ける
|
|
11383
11590
|
var goldGroups = [];
|
|
11384
11591
|
if (SITE.home) goldGroups.push({ name: "Home", ids: [SITE.home.id] });
|
|
11385
|
-
|
|
11592
|
+
// 削除済みは列にも件数にも出さない(一覧・グラフと同じ扱いに揃える)
|
|
11593
|
+
goldGroups.push({ name: "Decisions", ids: visibleRecords(SITE.decisions).map(function (d) { return d.id; }) });
|
|
11386
11594
|
(SITE.sections || []).forEach(function (sec) {
|
|
11387
|
-
goldGroups.push({ name: sec.name, ids: sec.records.map(function (r) { return r.id; }) });
|
|
11595
|
+
goldGroups.push({ name: sec.name, ids: visibleRecords(sec.records).map(function (r) { return r.id; }) });
|
|
11388
11596
|
});
|
|
11389
11597
|
|
|
11390
11598
|
function renderExplorer() {
|
package/package.json
CHANGED