@takagaki/cortex-decisions-viewer 0.12.9 → 0.12.11
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 +78 -0
- package/dist/render.js +270 -21
- package/package.json +1 -1
package/dist/build.js
CHANGED
|
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.readFleetSources = readFleetSources;
|
|
40
40
|
exports.readGoldProposals = readGoldProposals;
|
|
41
41
|
exports.buildHomeGrounding = buildHomeGrounding;
|
|
42
|
+
exports.collectMeetings = collectMeetings;
|
|
42
43
|
exports.findBacklogDir = findBacklogDir;
|
|
43
44
|
exports.build = build;
|
|
44
45
|
exports.createRefResolver = createRefResolver;
|
|
@@ -797,6 +798,79 @@ async function findDirByMarker(repoRoot, marker, fallback) {
|
|
|
797
798
|
}
|
|
798
799
|
return fallback;
|
|
799
800
|
}
|
|
801
|
+
/** 会議の回を入れるディレクトリの名前(`会議/<会議名>/20260120/`) */
|
|
802
|
+
const MEETING_DATE_DIR = /^\d{8}$/;
|
|
803
|
+
/**
|
|
804
|
+
* すでにある会議を列挙する(#277。投入モーダルの「会議名」の選択肢になる)。
|
|
805
|
+
*
|
|
806
|
+
* **「会議名」は画面から自由に打てるが、打たせるのは最後の手段にする。** 手で打つと
|
|
807
|
+
* 表記ゆれ(`定例` と `定例MTG`)で同じ会議が2つのディレクトリに割れ、議事録の連番も
|
|
808
|
+
* 履歴も分かれる。すでにある名前を選べれば、その事故はほとんど起きない。
|
|
809
|
+
*
|
|
810
|
+
* 会議とみなすのは「**日付ディレクトリ(`YYYYMMDD`)を直接ぶら下げているディレクトリ**」。
|
|
811
|
+
* 名前の規則ではなく実際の中身で決めるので、`会議/定例/デイリースクラム/20260120/` のような
|
|
812
|
+
* 入れ子でも当たる(相対パスは `定例/デイリースクラム`)。
|
|
813
|
+
*
|
|
814
|
+
* **会議ディレクトリ直下に日付ディレクトリがある形(相対パスが空)は除外する。**
|
|
815
|
+
* その形は「会議名なしで投入された回」で、選択肢に出しても送り先にできない
|
|
816
|
+
* (`meeting` が空文字になり Lambda が 400 を返す)。
|
|
817
|
+
*
|
|
818
|
+
* `minuteDates` は、その日付ディレクトリに議事録(`*_minutes.md`)が**すでにある**日付。
|
|
819
|
+
* 同じ日に文字起こしを足しても議事録は作り直されないので、送る前に画面で伝えるために使う。
|
|
820
|
+
*/
|
|
821
|
+
async function collectMeetings(repoRoot) {
|
|
822
|
+
const meetingDir = await findDirByMarker(repoRoot, "ingest-config.json", "会議");
|
|
823
|
+
const rootAbs = path.join(repoRoot, meetingDir);
|
|
824
|
+
const out = [];
|
|
825
|
+
const hasMinute = async (dirAbs) => {
|
|
826
|
+
try {
|
|
827
|
+
for (const e of await node_fs_1.promises.readdir(dirAbs, { withFileTypes: true })) {
|
|
828
|
+
if (e.isFile() && e.name.endsWith("_minutes.md"))
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
// 読めない回は「議事録なし」扱い(注意を出さないだけで、投入は妨げない)
|
|
834
|
+
}
|
|
835
|
+
return false;
|
|
836
|
+
};
|
|
837
|
+
const walk = async (relParts) => {
|
|
838
|
+
let entries;
|
|
839
|
+
try {
|
|
840
|
+
entries = await node_fs_1.promises.readdir(path.join(rootAbs, ...relParts), { withFileTypes: true });
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
return; // 会議ディレクトリが無い案件(未導入)はここで終わる
|
|
844
|
+
}
|
|
845
|
+
const dirs = entries.filter((e) => e.isDirectory() &&
|
|
846
|
+
e.name !== "node_modules" &&
|
|
847
|
+
e.name !== "templates" &&
|
|
848
|
+
!e.name.startsWith("."));
|
|
849
|
+
const rel = relParts.join("/");
|
|
850
|
+
const dates = dirs.filter((d) => MEETING_DATE_DIR.test(d.name)).map((d) => d.name);
|
|
851
|
+
if (dates.length && rel) {
|
|
852
|
+
const minuteDates = [];
|
|
853
|
+
for (const d of dates.sort()) {
|
|
854
|
+
if (await hasMinute(path.join(rootAbs, ...relParts, d)))
|
|
855
|
+
minuteDates.push(d);
|
|
856
|
+
}
|
|
857
|
+
out.push({ path: rel, minuteDates });
|
|
858
|
+
}
|
|
859
|
+
// 日付ディレクトリの中は回の中身なので降りない(会議名にはならない)。
|
|
860
|
+
// 深さは Lambda 側の上限(isValidMeetingDir の 5 セグメント)と揃える——
|
|
861
|
+
// それより深い会議を選択肢に出しても、選んだ瞬間に 400 で断られるだけ
|
|
862
|
+
if (relParts.length >= 5)
|
|
863
|
+
return;
|
|
864
|
+
for (const d of dirs) {
|
|
865
|
+
if (MEETING_DATE_DIR.test(d.name))
|
|
866
|
+
continue;
|
|
867
|
+
await walk([...relParts, d.name]);
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
await walk([]);
|
|
871
|
+
out.sort((a, b) => a.path.localeCompare(b.path, "ja"));
|
|
872
|
+
return out;
|
|
873
|
+
}
|
|
800
874
|
/** exporter が作る区画名(engine の `BACKLOG_SECTIONS` と同じ固定の名前) */
|
|
801
875
|
const BACKLOG_SECTIONS = ["issues", "documents", "wiki"];
|
|
802
876
|
/**
|
|
@@ -1543,6 +1617,9 @@ async function build(opts) {
|
|
|
1543
1617
|
computeLayout(graph); // 各ノードに x/y を書き込む(クライアントはこれを初期位置に使いウォームアップ省略)
|
|
1544
1618
|
// データソースと自動化(fleet-status.json 由来)。無ければ undefined でセクション非表示。
|
|
1545
1619
|
const fleet = await readFleetSources(repoRoot);
|
|
1620
|
+
// すでにある会議(投入モーダルの「会議名」の選択肢)。intake の有無に関わらず載せる——
|
|
1621
|
+
// 出し分けは表示側の仕事で、ここで絞ると「投入の口を後から付けたら選択肢が空」になる
|
|
1622
|
+
const meetings = await collectMeetings(repoRoot);
|
|
1546
1623
|
const site = {
|
|
1547
1624
|
title: opts.title,
|
|
1548
1625
|
generatedAt: opts.generatedAt,
|
|
@@ -1564,6 +1641,7 @@ async function build(opts) {
|
|
|
1564
1641
|
projectKey: opts.projectKey,
|
|
1565
1642
|
}
|
|
1566
1643
|
: undefined,
|
|
1644
|
+
meetings,
|
|
1567
1645
|
// **URLとトークンが両方そろったときだけ出す。** 片方だけでは貼れるURLを作れないので、
|
|
1568
1646
|
// 手順だけが出て「専用URLが空」という一番混乱する形になる(intake と同じ扱い)
|
|
1569
1647
|
backlogWebhook: opts.webhookUrl && opts.webhookToken
|
package/dist/render.js
CHANGED
|
@@ -512,8 +512,22 @@ function sanitizeStateNow(v) {
|
|
|
512
512
|
return out.length ? out : null;
|
|
513
513
|
}
|
|
514
514
|
/**
|
|
515
|
-
*
|
|
516
|
-
*
|
|
515
|
+
* ストアの「届くか」を行に重ねる。**まとめ表の時刻とは比べない。**
|
|
516
|
+
* fleet-status.json は chat / dev の gate・needed を焼き込まない(観測の一本化・工程1。
|
|
517
|
+
* 生成側の明示許可リストに無い)ので、この関数が触る行ではストアの行が
|
|
518
|
+
* **常に唯一かつ最良の答え**である。比べる相手がそもそも居ない。
|
|
519
|
+
*
|
|
520
|
+
* 🔴 **かつて在った鮮度ガード(`hit.at <= generatedAt` なら捨てる)は外した(#280)。**
|
|
521
|
+
* fleet-status.json は push のたびに再生成されるので、夜間の観測はほぼ常に「古い」側へ
|
|
522
|
+
* 回り、恒久的に捨てられていた——画面が「登録状況を確認中です」のまま戻らなくなる。
|
|
523
|
+
* 新旧の勝ち負けはサーバ側(DynamoDB の SK と、書き込み時の条件式)が解決済みで、
|
|
524
|
+
* 画面で二重に判定しない。ISO の秒精度とミリ秒精度を辞書順で比べると新旧が逆転する
|
|
525
|
+
* ハザード(`infra/lambda/shared/connection-state-keys.mjs` に記述あり)も、
|
|
526
|
+
* 比較をやめたことで消えている。
|
|
527
|
+
*
|
|
528
|
+
* ⚠ 工程1より前の cortex-engine が作った fleet-status.json には gate が焼き込まれて
|
|
529
|
+
* いることがあるが、push のたびに再生成される設計上それが残るのは休眠案件だけで、
|
|
530
|
+
* 休眠案件ではストアの行が唯一かつ最新の観測なので実害は無い。
|
|
517
531
|
*
|
|
518
532
|
* 対象はチャットと開発の行(段B。夜間チェックがこの2つを書く)。
|
|
519
533
|
*
|
|
@@ -529,7 +543,7 @@ function sanitizeStateNow(v) {
|
|
|
529
543
|
* 分からない状態に戻る。上書きするときは空でも必ず載せる——夜間の古い needed が
|
|
530
544
|
* 新しい判定の横に残ると、直したはずのスコープ名がいつまでも出る
|
|
531
545
|
*/
|
|
532
|
-
function overlayStateOnFleet(view, rows
|
|
546
|
+
function overlayStateOnFleet(view, rows) {
|
|
533
547
|
const v = view || {};
|
|
534
548
|
if (!rows || !rows.length || !Array.isArray(v.externalSources))
|
|
535
549
|
return v;
|
|
@@ -553,8 +567,6 @@ function overlayStateOnFleet(view, rows, generatedAt) {
|
|
|
553
567
|
hit = byKey["dev\t" + String(s.ref)];
|
|
554
568
|
if (!hit)
|
|
555
569
|
return s;
|
|
556
|
-
if (generatedAt && hit.at <= generatedAt)
|
|
557
|
-
return s;
|
|
558
570
|
changed = true;
|
|
559
571
|
return Object.assign({}, s, {
|
|
560
572
|
gate: hit.status,
|
|
@@ -2740,6 +2752,21 @@ button.ops-card-btn:disabled { opacity: .6; cursor: default; }
|
|
|
2740
2752
|
.file-chip > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 260px; }
|
|
2741
2753
|
.file-chip-x { border: none; background: none; cursor: pointer; color: var(--muted); font-size: 16px; line-height: 1; padding: 0 4px; }
|
|
2742
2754
|
.file-chip-x:hover { color: #dc2626; }
|
|
2755
|
+
/* 会議日(#277)。ファイル名から推せなかったチップは黄色にして「当日を仮置きした」ことを見せる */
|
|
2756
|
+
.file-chip-date {
|
|
2757
|
+
border: 1px solid var(--border); border-radius: 6px; padding: 1px 5px; font-size: 11.5px;
|
|
2758
|
+
color: var(--text); background: var(--surface); font-family: inherit;
|
|
2759
|
+
}
|
|
2760
|
+
.file-chip.is-unguessed { background: #fffbeb; border-color: #fcd34d; color: #92400e; }
|
|
2761
|
+
.file-chip-guess { font-size: 11px; font-weight: 700; color: #92400e; white-space: nowrap; }
|
|
2762
|
+
/* 会議名の欄(選択肢+新規入力+入力エラー) */
|
|
2763
|
+
.meeting-field { display: flex; flex-direction: column; gap: 6px; }
|
|
2764
|
+
.meeting-error { min-height: 0; }
|
|
2765
|
+
/* 「その日はもう議事録がある」の注意。送信は妨げないので、エラー(赤)とは色を変える */
|
|
2766
|
+
.modal-warn {
|
|
2767
|
+
background: #fffbeb; border: 1px solid #fcd34d; border-radius: 8px;
|
|
2768
|
+
padding: 8px 12px; font-size: 12.5px; color: #92400e; line-height: 1.7;
|
|
2769
|
+
}
|
|
2743
2770
|
.summary-box { background: var(--accent-weak); border: 1px solid #dce6fb; border-radius: var(--radius); padding: 12px 16px; color: #1e3a8a; }
|
|
2744
2771
|
.deciders { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
2745
2772
|
.refs { margin: 0; padding-left: 18px; }
|
|
@@ -4723,12 +4750,11 @@ ${deriveCapTile.toString()}
|
|
|
4723
4750
|
function fleetVal(key) {
|
|
4724
4751
|
if ((LIVE_REGISTERED || LIVE_STATE) && MERGE_KEYS[key]) {
|
|
4725
4752
|
// 登録の追加 → 状態の上書き、の順で1回だけ合成する。
|
|
4726
|
-
//
|
|
4753
|
+
// 状態ストアの答えは常に勝つ(まとめ表は gate を焼き込まない。overlayStateOnFleet の項)
|
|
4727
4754
|
if (!MERGED) {
|
|
4728
4755
|
MERGED = overlayStateOnFleet(
|
|
4729
4756
|
mergeRegisteredFleet(rawView(), LIVE_REGISTERED),
|
|
4730
4757
|
LIVE_STATE,
|
|
4731
|
-
String(rawFleetVal("generatedAt") || ""),
|
|
4732
4758
|
);
|
|
4733
4759
|
}
|
|
4734
4760
|
return MERGED[key];
|
|
@@ -4862,7 +4888,6 @@ ${deriveCapTile.toString()}
|
|
|
4862
4888
|
var ov = overlayStateOnFleet(
|
|
4863
4889
|
{ externalSources: rawFleetVal("externalSources") || [] },
|
|
4864
4890
|
LIVE_STATE,
|
|
4865
|
-
String(rawFleetVal("generatedAt") || ""),
|
|
4866
4891
|
);
|
|
4867
4892
|
return {
|
|
4868
4893
|
// 🔴 **能力の状態は正本(capabilitiesNow)を通す(工程1.5)。** タイルの語は
|
|
@@ -9673,6 +9698,46 @@ ${deriveCapTile.toString()}
|
|
|
9673
9698
|
]);
|
|
9674
9699
|
var nameInput = el("input", { type: "text", class: "modal-name", placeholder: "投入者として記録されます" });
|
|
9675
9700
|
modal.appendChild(fieldRow("ファイル(必須)", dropzone));
|
|
9701
|
+
|
|
9702
|
+
// ---- 会議名と会議日(文字起こしのみ・#277)----
|
|
9703
|
+
//
|
|
9704
|
+
// **どの会議の・いつの回か**が決まらないと、送ったファイルは置き場所を持てない。
|
|
9705
|
+
// 以前は名前も日付もサーバ側の推測に任せていたので、同じ会議が表記ゆれで2つの
|
|
9706
|
+
// ディレクトリに割れたり、別の日の回として入ったりした。ここで人に確定させる。
|
|
9707
|
+
var meetingList = isT && SITE.meetings ? SITE.meetings : [];
|
|
9708
|
+
var MEETING_NEW = "__new__";
|
|
9709
|
+
var BACKSLASH = String.fromCharCode(92);
|
|
9710
|
+
var meetingSelect = null;
|
|
9711
|
+
var meetingNewInput = null;
|
|
9712
|
+
var meetingError = null;
|
|
9713
|
+
var dupWarn = null;
|
|
9714
|
+
// ユーザーが会議を自分で選んだか。**選んだ後はファイル名からの推測で上書きしない**
|
|
9715
|
+
var meetingTouched = false;
|
|
9716
|
+
if (isT) {
|
|
9717
|
+
var meetingBox = el("div", { class: "meeting-field" });
|
|
9718
|
+
if (meetingList.length) {
|
|
9719
|
+
meetingSelect = el("select", { class: "modal-name meeting-select" });
|
|
9720
|
+
meetingSelect.appendChild(el("option", { value: "", text: "会議を選択してください" }));
|
|
9721
|
+
meetingList.forEach(function (m) {
|
|
9722
|
+
meetingSelect.appendChild(el("option", { value: m.path, text: m.path }));
|
|
9723
|
+
});
|
|
9724
|
+
meetingSelect.appendChild(el("option", { value: MEETING_NEW, text: "+ 新しい会議名を入力" }));
|
|
9725
|
+
meetingBox.appendChild(meetingSelect);
|
|
9726
|
+
}
|
|
9727
|
+
meetingNewInput = el("input", { type: "text", class: "modal-name meeting-new", placeholder: "新しい会議名(例: デイリースクラム)" });
|
|
9728
|
+
// 選択肢がある案件では、明示的に「新しい会議名」を選ぶまで隠す。
|
|
9729
|
+
// 選択肢が1件も無い案件(初回投入)は入力欄だけを出す
|
|
9730
|
+
if (meetingSelect) meetingNewInput.hidden = true;
|
|
9731
|
+
meetingBox.appendChild(meetingNewInput);
|
|
9732
|
+
meetingError = el("div", { class: "modal-status modal-status-error meeting-error" });
|
|
9733
|
+
meetingError.hidden = true;
|
|
9734
|
+
meetingBox.appendChild(meetingError);
|
|
9735
|
+
modal.appendChild(fieldRow("会議名(必須)", meetingBox));
|
|
9736
|
+
dupWarn = el("div", { class: "modal-warn meeting-dup-warn" });
|
|
9737
|
+
dupWarn.hidden = true;
|
|
9738
|
+
modal.appendChild(dupWarn);
|
|
9739
|
+
}
|
|
9740
|
+
|
|
9676
9741
|
modal.appendChild(fieldRow("お名前(必須)", nameInput));
|
|
9677
9742
|
var status = el("div", { class: "modal-status" });
|
|
9678
9743
|
var cancelBtn = el("button", { class: "modal-cancel", text: "閉じる" });
|
|
@@ -9684,13 +9749,166 @@ ${deriveCapTile.toString()}
|
|
|
9684
9749
|
showModal(overlay, modal);
|
|
9685
9750
|
|
|
9686
9751
|
function showStatus(msg, isError) { status.className = "modal-status" + (isError ? " modal-status-error" : ""); status.textContent = msg; }
|
|
9687
|
-
|
|
9752
|
+
|
|
9753
|
+
// ---- 会議名の確定 ----
|
|
9754
|
+
function isFreeMeeting() { return !meetingSelect || meetingSelect.value === MEETING_NEW; }
|
|
9755
|
+
/**
|
|
9756
|
+
* 自由入力の会議名を整える。
|
|
9757
|
+
*
|
|
9758
|
+
* **NFC に寄せる。** macOS の Finder から拾った名前は濁点が分解された形(NFD)で
|
|
9759
|
+
* 来ることがあり、見た目が同じなのに別ディレクトリになる。
|
|
9760
|
+
* **既存の選択肢には触らない**——あちらはビルド時にリポジトリから読んだ実物なので、
|
|
9761
|
+
* 画面で書き換えると、その会議に入らなくなる。
|
|
9762
|
+
*/
|
|
9763
|
+
function normalizeMeeting(s) {
|
|
9764
|
+
var v = String(s == null ? "" : s);
|
|
9765
|
+
if (v.normalize) v = v.normalize("NFC");
|
|
9766
|
+
return v.trim();
|
|
9767
|
+
}
|
|
9768
|
+
function meetingValue() {
|
|
9769
|
+
if (!isFreeMeeting()) return meetingSelect.value;
|
|
9770
|
+
return meetingNewInput ? normalizeMeeting(meetingNewInput.value) : "";
|
|
9771
|
+
}
|
|
9772
|
+
/** 会議名が送れない理由(送れるなら空文字) */
|
|
9773
|
+
function meetingProblem() {
|
|
9774
|
+
var v = meetingValue();
|
|
9775
|
+
if (!v) return "会議名を選んでください。";
|
|
9776
|
+
if (!isFreeMeeting()) return "";
|
|
9777
|
+
if (v === "." || v === "..") return "会議名に . や .. は使えません。";
|
|
9778
|
+
// 先頭ドットは、作れても以後の選択肢に出ない(既存会議の列挙が隠しディレクトリを
|
|
9779
|
+
// 見ないため)。次の投入者が同じ会議を選べず表記ゆれの二重化になるので、最初から断る
|
|
9780
|
+
if (v.charAt(0) === ".") return "会議名を . から始めることはできません。";
|
|
9781
|
+
if (v.indexOf("/") !== -1 || v.indexOf(BACKSLASH) !== -1) {
|
|
9782
|
+
return "会議名にスラッシュや円記号(パスの区切り)は使えません。1つの階層の名前にしてください。";
|
|
9783
|
+
}
|
|
9784
|
+
return "";
|
|
9785
|
+
}
|
|
9786
|
+
function refreshMeetingError() {
|
|
9787
|
+
if (!meetingError) return;
|
|
9788
|
+
// **開いた直後に赤くしない。** 未入力は「まだ書いていない」だけで、間違いではない。
|
|
9789
|
+
// 打った内容が使えないときだけ理由を出す(未入力は送信ボタンが無効なことで伝わる)
|
|
9790
|
+
var typed = isFreeMeeting() && meetingNewInput && normalizeMeeting(meetingNewInput.value);
|
|
9791
|
+
var msg = typed ? meetingProblem() : "";
|
|
9792
|
+
meetingError.textContent = msg;
|
|
9793
|
+
meetingError.hidden = !msg;
|
|
9794
|
+
}
|
|
9795
|
+
/**
|
|
9796
|
+
* ファイル名に既存の会議名が入っていたら、その会議を選ぶ。
|
|
9797
|
+
* 一致が複数あるときは長いほうを採る(「定例」と「開発定例」なら後者が意図に近い)。
|
|
9798
|
+
*/
|
|
9799
|
+
function autoPickMeeting(names) {
|
|
9800
|
+
if (!meetingSelect || meetingTouched || meetingSelect.value) return;
|
|
9801
|
+
var best = null;
|
|
9802
|
+
meetingList.forEach(function (m) {
|
|
9803
|
+
var seg = String(m.path).split("/").pop();
|
|
9804
|
+
if (!seg) return;
|
|
9805
|
+
for (var i = 0; i < names.length; i++) {
|
|
9806
|
+
if (String(names[i]).indexOf(seg) !== -1) {
|
|
9807
|
+
if (!best || seg.length > best.seg.length) best = { seg: seg, path: m.path };
|
|
9808
|
+
return;
|
|
9809
|
+
}
|
|
9810
|
+
}
|
|
9811
|
+
});
|
|
9812
|
+
if (best) meetingSelect.value = best.path;
|
|
9813
|
+
}
|
|
9814
|
+
|
|
9815
|
+
// ---- 会議日(ファイルごと)----
|
|
9816
|
+
function pad2(n) { return ("0" + n).slice(-2); }
|
|
9817
|
+
function todayIso() {
|
|
9818
|
+
var d = new Date();
|
|
9819
|
+
return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate());
|
|
9820
|
+
}
|
|
9821
|
+
function validYmd(y, m, d) {
|
|
9822
|
+
var mm = Number(m), dd = Number(d);
|
|
9823
|
+
return Number(y) >= 2000 && mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31;
|
|
9824
|
+
}
|
|
9825
|
+
/**
|
|
9826
|
+
* ファイル名から会議日を推す(推せなければ null)。
|
|
9827
|
+
* 区切りつき(2026-01-20 / 2026年1月20日)→ 8桁の連続(20260120)の順に試す。
|
|
9828
|
+
*
|
|
9829
|
+
* **区切りつきを先に見る。** フラット置き時代の投入分は「投入日8桁-元名」の形で、
|
|
9830
|
+
* 元名の側に本当の会議日が区切りつきで入っていることがある
|
|
9831
|
+
* (実例: 20260819-2026-01-20__デイリースクラム….txt)。8桁を先に採ると
|
|
9832
|
+
* 投入日のほうを会議日として推してしまい、しかも「推定成功」の見た目になる。
|
|
9833
|
+
*/
|
|
9834
|
+
function guessMeetingDate(name) {
|
|
9835
|
+
// **変数を s・p・ext・src・internal と名付けない。** fleet-passthrough の走査は
|
|
9836
|
+
// その名前に続くプロパティを「画面が読む fleet のフィールド」とみなすので、
|
|
9837
|
+
// 文字列メソッドを呼んだだけで誤検知になる
|
|
9838
|
+
var nm = String(name || "");
|
|
9839
|
+
var sp = nm.match(/(20\\d\\d)[-_./年]\\s?(\\d{1,2})[-_./月]\\s?(\\d{1,2})[日]?/);
|
|
9840
|
+
if (sp && validYmd(sp[1], sp[2], sp[3])) return sp[1] + "-" + pad2(Number(sp[2])) + "-" + pad2(Number(sp[3]));
|
|
9841
|
+
var m8 = nm.match(/20\\d{6}/);
|
|
9842
|
+
if (m8 && validYmd(m8[0].slice(0, 4), m8[0].slice(4, 6), m8[0].slice(6, 8))) {
|
|
9843
|
+
return m8[0].slice(0, 4) + "-" + m8[0].slice(4, 6) + "-" + m8[0].slice(6, 8);
|
|
9844
|
+
}
|
|
9845
|
+
return null;
|
|
9846
|
+
}
|
|
9847
|
+
function compactDate(iso) { return String(iso || "").split("-").join(""); }
|
|
9848
|
+
|
|
9849
|
+
// ---- すでに議事録がある日 ----
|
|
9850
|
+
function selectedMeeting() {
|
|
9851
|
+
var v = meetingValue();
|
|
9852
|
+
if (!v) return null;
|
|
9853
|
+
for (var i = 0; i < meetingList.length; i++) if (meetingList[i].path === v) return meetingList[i];
|
|
9854
|
+
return null;
|
|
9855
|
+
}
|
|
9856
|
+
function dupEntries() {
|
|
9857
|
+
var m = selectedMeeting();
|
|
9858
|
+
if (!m || !m.minuteDates || !m.minuteDates.length) return [];
|
|
9859
|
+
return selectedFiles.filter(function (e) {
|
|
9860
|
+
return e.date && m.minuteDates.indexOf(compactDate(e.date)) !== -1;
|
|
9861
|
+
});
|
|
9862
|
+
}
|
|
9863
|
+
function refreshDupWarn() {
|
|
9864
|
+
if (!dupWarn) return;
|
|
9865
|
+
var dups = dupEntries();
|
|
9866
|
+
dupWarn.hidden = !dups.length;
|
|
9867
|
+
dupWarn.textContent = dups.length
|
|
9868
|
+
? "⚠ この日付には既に議事録があります。ファイルは保存されますが、議事録の自動生成は行われません: "
|
|
9869
|
+
+ dups.map(function (e) { return e.file.name; }).join(", ")
|
|
9870
|
+
: "";
|
|
9871
|
+
}
|
|
9872
|
+
|
|
9873
|
+
function update() {
|
|
9874
|
+
refreshMeetingError();
|
|
9875
|
+
refreshDupWarn();
|
|
9876
|
+
var ok = !!(nameInput.value.trim() && selectedFiles.length);
|
|
9877
|
+
// 文字起こしは会議名が確定し、各ファイルの日付が入っているときだけ送れる
|
|
9878
|
+
if (isT) {
|
|
9879
|
+
ok = ok && !meetingProblem() && selectedFiles.every(function (e) { return !!e.date; });
|
|
9880
|
+
}
|
|
9881
|
+
submitBtn.disabled = !ok;
|
|
9882
|
+
}
|
|
9688
9883
|
function renderFiles() {
|
|
9689
9884
|
fileListEl.textContent = "";
|
|
9690
|
-
selectedFiles.forEach(function (
|
|
9885
|
+
selectedFiles.forEach(function (entry, idx) {
|
|
9886
|
+
var chip = el("span", { class: "file-chip" + (isT && !entry.guessed ? " is-unguessed" : "") });
|
|
9887
|
+
chip.appendChild(el("span", { text: entry.file.name }));
|
|
9888
|
+
if (isT) {
|
|
9889
|
+
// 推せなかったファイルは当日を仮置きしてある。**そのまま送られないよう印を出す**
|
|
9890
|
+
var note = entry.guessed ? null : el("span", { class: "file-chip-guess", text: "日付を確認" });
|
|
9891
|
+
var dateInput = el("input", { type: "date", class: "file-chip-date", "aria-label": entry.file.name + " の会議日" });
|
|
9892
|
+
dateInput.value = entry.date;
|
|
9893
|
+
// **クリックがドロップゾーンへ抜けないようにする。** 抜けるとファイル選択が
|
|
9894
|
+
// 開き、日付を直そうとしただけでダイアログが出る
|
|
9895
|
+
dateInput.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
9896
|
+
dateInput.addEventListener("change", function () {
|
|
9897
|
+
entry.date = dateInput.value;
|
|
9898
|
+
entry.guessed = true; // 人が触った日付は「確認済み」——注意を出し続けない
|
|
9899
|
+
// **ここで renderFiles() を呼ばない。** チップごと作り直すと入力欄が
|
|
9900
|
+
// 差し替わり、日付を打っている途中でフォーカスが飛ぶ
|
|
9901
|
+
chip.className = "file-chip";
|
|
9902
|
+
if (note && note.parentNode) note.parentNode.removeChild(note);
|
|
9903
|
+
update();
|
|
9904
|
+
});
|
|
9905
|
+
chip.appendChild(dateInput);
|
|
9906
|
+
if (note) chip.appendChild(note);
|
|
9907
|
+
}
|
|
9691
9908
|
var x = el("button", { class: "file-chip-x", type: "button", text: "×" });
|
|
9692
9909
|
x.addEventListener("click", function (e) { e.stopPropagation(); selectedFiles.splice(idx, 1); renderFiles(); update(); });
|
|
9693
|
-
|
|
9910
|
+
chip.appendChild(x);
|
|
9911
|
+
fileListEl.appendChild(chip);
|
|
9694
9912
|
});
|
|
9695
9913
|
}
|
|
9696
9914
|
function isAllowed(f) {
|
|
@@ -9702,9 +9920,16 @@ ${deriveCapTile.toString()}
|
|
|
9702
9920
|
}
|
|
9703
9921
|
function addFiles(list) {
|
|
9704
9922
|
var rejected = [];
|
|
9923
|
+
var addedNames = [];
|
|
9705
9924
|
Array.prototype.slice.call(list || []).forEach(function (f) {
|
|
9706
|
-
if (isAllowed(f))
|
|
9925
|
+
if (!isAllowed(f)) { rejected.push(f.name || "(名前なし)"); return; }
|
|
9926
|
+
var guess = isT ? guessMeetingDate(f.name) : null;
|
|
9927
|
+
// 推せなければ当日を仮置きする。**空のままにしない**——日付が空の入力欄は
|
|
9928
|
+
// 「入れなくてよい」に見えるが、実際は送れない(送信ボタンが無効なだけ)
|
|
9929
|
+
selectedFiles.push({ file: f, date: isT ? (guess || todayIso()) : "", guessed: !!guess });
|
|
9930
|
+
addedNames.push(f.name || "");
|
|
9707
9931
|
});
|
|
9932
|
+
if (isT && addedNames.length) autoPickMeeting(addedNames);
|
|
9708
9933
|
renderFiles();
|
|
9709
9934
|
update();
|
|
9710
9935
|
if (rejected.length) showStatus("⚠ 対応していない形式のため追加できませんでした: " + rejected.join(", ") + "(対応形式: " + allowedLabel + ")", true);
|
|
@@ -9716,24 +9941,41 @@ ${deriveCapTile.toString()}
|
|
|
9716
9941
|
dropzone.addEventListener("dragleave", function () { dropzone.classList.remove("dragover"); });
|
|
9717
9942
|
dropzone.addEventListener("drop", function (e) { e.preventDefault(); dropzone.classList.remove("dragover"); addFiles(e.dataTransfer.files); });
|
|
9718
9943
|
nameInput.addEventListener("input", update);
|
|
9944
|
+
if (meetingSelect) {
|
|
9945
|
+
meetingSelect.addEventListener("change", function () {
|
|
9946
|
+
meetingTouched = true;
|
|
9947
|
+
meetingNewInput.hidden = meetingSelect.value !== MEETING_NEW;
|
|
9948
|
+
// 選択肢に戻したら打ちかけの名前は捨てる(隠れた入力が送られるのを防ぐ)
|
|
9949
|
+
if (meetingSelect.value !== MEETING_NEW) meetingNewInput.value = "";
|
|
9950
|
+
update();
|
|
9951
|
+
});
|
|
9952
|
+
}
|
|
9953
|
+
if (meetingNewInput) meetingNewInput.addEventListener("input", update);
|
|
9719
9954
|
|
|
9720
9955
|
// 閉じる経路(Escape・背景クリック)は makeModal 側に用意されている
|
|
9721
9956
|
cancelBtn.addEventListener("click", function () { mk.close(); });
|
|
9722
|
-
submitBtn.addEventListener("click", function () {
|
|
9957
|
+
submitBtn.addEventListener("click", function () {
|
|
9958
|
+
// 全ファイルが「議事録のある日」なら、成功メッセージも「生成はされない」側に寄せる。
|
|
9959
|
+
// ✓ の直後に「数分後に議事録が自動生成され…」と出すと、待っても出ない
|
|
9960
|
+
var allDup = isT && selectedFiles.length > 0 && dupEntries().length === selectedFiles.length;
|
|
9961
|
+
submitIntake(kind, selectedFiles, nameInput.value, status, submitBtn, isT ? meetingValue() : null, allDup);
|
|
9962
|
+
});
|
|
9723
9963
|
}
|
|
9724
|
-
async function submitIntake(kind,
|
|
9964
|
+
async function submitIntake(kind, entries, submitter, status, submitBtn, meeting, allDup) {
|
|
9725
9965
|
var intake = SITE.intake;
|
|
9966
|
+
var isT = kind === "transcript";
|
|
9726
9967
|
function setStatus(msg, isError) { status.className = "modal-status" + (isError ? " modal-status-error" : ""); status.textContent = msg; }
|
|
9727
9968
|
if (!submitter || !submitter.trim()) { setStatus("⚠ お名前を入力してください。", true); return; }
|
|
9728
|
-
if (!
|
|
9969
|
+
if (!entries || !entries.length) { setStatus("⚠ ファイルを選択してください。", true); return; }
|
|
9970
|
+
if (isT && !meeting) { setStatus("⚠ 会議名を選んでください。", true); return; }
|
|
9729
9971
|
submitBtn.disabled = true;
|
|
9730
9972
|
var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
|
|
9731
9973
|
function fail(msg) { setStatus("✗ " + msg, true); submitBtn.disabled = false; }
|
|
9732
9974
|
try {
|
|
9733
9975
|
var uploaded = [];
|
|
9734
|
-
for (var i = 0; i <
|
|
9735
|
-
var f =
|
|
9736
|
-
setStatus("アップロード中… (" + (i + 1) + "/" +
|
|
9976
|
+
for (var i = 0; i < entries.length; i++) {
|
|
9977
|
+
var f = entries[i].file;
|
|
9978
|
+
setStatus("アップロード中… (" + (i + 1) + "/" + entries.length + ") " + f.name, false);
|
|
9737
9979
|
var pres;
|
|
9738
9980
|
try {
|
|
9739
9981
|
pres = await fetch(pfx + "op=presign&t=" + encodeURIComponent(intake.token), {
|
|
@@ -9747,20 +9989,27 @@ ${deriveCapTile.toString()}
|
|
|
9747
9989
|
put = await fetch(pres.j.url, { method: "PUT", headers: { "content-type": f.type || "application/octet-stream" }, body: f });
|
|
9748
9990
|
} catch (net2) { return fail("ファイルのアップロードに失敗しました(通信エラー)。もう一度お試しください。"); }
|
|
9749
9991
|
if (!put.ok) return fail("ファイルのアップロードに失敗しました(" + put.status + ")。ファイルのサイズ・形式をご確認ください。");
|
|
9750
|
-
|
|
9992
|
+
var up = { key: pres.j.key, filename: f.name };
|
|
9993
|
+
// 会議日は YYYYMMDD で送る(そのまま日付ディレクトリの名前になる)
|
|
9994
|
+
if (isT) up.date = String(entries[i].date || "").split("-").join("");
|
|
9995
|
+
uploaded.push(up);
|
|
9751
9996
|
}
|
|
9752
9997
|
setStatus("取り込み中…", false);
|
|
9753
9998
|
var sub;
|
|
9754
9999
|
try {
|
|
10000
|
+
var payload = { projectKey: intake.projectKey, kind: kind, files: uploaded, submitter: submitter.trim() };
|
|
10001
|
+
if (isT) payload.meeting = meeting;
|
|
9755
10002
|
sub = await fetch(pfx + "op=submit&t=" + encodeURIComponent(intake.token), {
|
|
9756
10003
|
method: "POST", headers: { "content-type": "application/json" },
|
|
9757
|
-
body: JSON.stringify(
|
|
10004
|
+
body: JSON.stringify(payload),
|
|
9758
10005
|
}).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); });
|
|
9759
10006
|
} catch (net3) { return fail("サーバーに接続できませんでした。もう一度お試しください。"); }
|
|
9760
10007
|
if (!sub.ok) return fail((sub.j && sub.j.message) || "取り込みに失敗しました。時間をおいて再度お試しください。");
|
|
9761
10008
|
status.className = "modal-status";
|
|
9762
10009
|
status.textContent = "";
|
|
9763
|
-
status.appendChild(el("div", { class: "modal-ok", text:
|
|
10010
|
+
status.appendChild(el("div", { class: "modal-ok", text: allDup
|
|
10011
|
+
? "✓ 送信しました。保存されました(この日付には既に議事録があるため、議事録の自動生成は行われません)"
|
|
10012
|
+
: "✓ 送信しました。数分後に" + (isT ? "議事録が自動生成され" : "内容がMarkdownに変換されて") + "反映されます。" }));
|
|
9764
10013
|
var paths = (sub.j && sub.j.committed) || [];
|
|
9765
10014
|
if (paths.length && sub.j.repo) {
|
|
9766
10015
|
var br = SITE.branch || "main";
|
package/package.json
CHANGED