@unson/brainbase-mcp 0.41.6 → 0.41.7

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.
@@ -4,10 +4,43 @@
4
4
  * The screen only speaks the provider-independent HTTP contract. A host
5
5
  * supplies the fetcher, token and base path; no calendar, repository, or
6
6
  * organization connector is assumed here.
7
+ *
8
+ * List order and labels are a projection. Canonical records are not rewritten.
9
+ * Calendar links stay out until a meeting-side event id is verified.
7
10
  */
8
11
 
9
12
  export const MEETING_MINUTES_UI_CONTRACT_VERSION = 'brainbase.meeting-minutes-ui.v1';
10
13
 
14
+ const WEEKDAYS = ['日', '月', '火', '水', '木', '金', '土'];
15
+ const TOKYO = 'Asia/Tokyo';
16
+
17
+ function tokyoParts(date) {
18
+ const parts = new Intl.DateTimeFormat('en-US', {
19
+ timeZone: TOKYO,
20
+ year: 'numeric',
21
+ month: '2-digit',
22
+ day: '2-digit',
23
+ hour: '2-digit',
24
+ minute: '2-digit',
25
+ hourCycle: 'h23',
26
+ }).formatToParts(date);
27
+ const value = (type) => parts.find((part) => part.type === type)?.value ?? '';
28
+ let hour = Number(value('hour'));
29
+ if (hour === 24) hour = 0;
30
+ return {
31
+ year: Number(value('year')),
32
+ month: Number(value('month')),
33
+ day: Number(value('day')),
34
+ hour,
35
+ minute: Number(value('minute')),
36
+ };
37
+ }
38
+
39
+ function tokyoDayNumber(date) {
40
+ const { year, month, day } = tokyoParts(date);
41
+ return Math.floor(Date.UTC(year, month - 1, day) / 86400000);
42
+ }
43
+
11
44
  function isRecord(value) {
12
45
  return value !== null && typeof value === 'object' && !Array.isArray(value);
13
46
  }
@@ -62,18 +95,161 @@ function labelInput(doc, label, { type = 'text', value = '', name, placeholder }
62
95
  return { wrapper, input };
63
96
  }
64
97
 
65
- function displayDate(value) {
66
- if (!value) return '日時未登録';
98
+ function parseDate(value) {
99
+ if (typeof value !== 'string' || !value.trim()) return null;
67
100
  const date = new Date(value);
68
- return Number.isNaN(date.getTime()) ? value : date.toLocaleString('ja-JP');
101
+ return Number.isNaN(date.getTime()) ? null : date;
69
102
  }
70
103
 
71
- function localDateTime(value) {
72
- if (!value) return '';
73
- const date = new Date(value);
74
- if (Number.isNaN(date.getTime())) return '';
75
- const pad = (part) => String(part).padStart(2, '0');
76
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
104
+ function pad(part) {
105
+ return String(part).padStart(2, '0');
106
+ }
107
+
108
+ function formatClock(date) {
109
+ const { hour, minute } = tokyoParts(date);
110
+ return `${pad(hour)}:${pad(minute)}`;
111
+ }
112
+
113
+ function formatMonthDay(date) {
114
+ const { month, day } = tokyoParts(date);
115
+ return `${month}/${day}`;
116
+ }
117
+
118
+ function formatStamp(value) {
119
+ const date = parseDate(value);
120
+ return date ? `${formatMonthDay(date)} ${formatClock(date)}` : '';
121
+ }
122
+
123
+ function dayHeading(date, today) {
124
+ const { year, month, day } = tokyoParts(date);
125
+ const weekday = WEEKDAYS[new Date(Date.UTC(year, month - 1, day)).getUTCDay()];
126
+ const label = `${month}/${day}(${weekday})`;
127
+ return tokyoDayNumber(date) === tokyoDayNumber(today) ? `今日 ${label}` : label;
128
+ }
129
+
130
+ function dateKey(date) {
131
+ const { year, month, day } = tokyoParts(date);
132
+ return `${year}-${month}-${day}`;
133
+ }
134
+
135
+ function relativeDay(date, today) {
136
+ const days = tokyoDayNumber(today) - tokyoDayNumber(date);
137
+ if (days === 0) return '今日';
138
+ if (days === 1) return '昨日';
139
+ if (days > 1) return `${days}日前`;
140
+ if (days === -1) return '明日';
141
+ return `${Math.abs(days)}日後`;
142
+ }
143
+
144
+ function httpUrl(value) {
145
+ const raw = text(value);
146
+ if (!/^https?:\/\//i.test(raw)) return '';
147
+ try {
148
+ const url = new URL(raw);
149
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return '';
150
+ return url.href;
151
+ } catch {
152
+ return '';
153
+ }
154
+ }
155
+
156
+ function firstDate(values) {
157
+ for (const value of values) {
158
+ const date = parseDate(value);
159
+ if (date) return date;
160
+ }
161
+ return null;
162
+ }
163
+
164
+ function calendarStart(meeting) {
165
+ const meta = isRecord(meeting?.metadata) ? meeting.metadata : {};
166
+ const event = isRecord(meeting?.calendar_event) ? meeting.calendar_event
167
+ : isRecord(meta.calendar_event) ? meta.calendar_event : null;
168
+ return firstDate([
169
+ meeting?.calendar_start,
170
+ event?.start,
171
+ event?.starts_at,
172
+ event?.start_at,
173
+ meta.calendar_start,
174
+ meta.event_start,
175
+ meta.scheduled_start,
176
+ ]);
177
+ }
178
+
179
+ function listedVersions(entry) {
180
+ const versions = [];
181
+ if (Array.isArray(entry?.versions)) versions.push(...entry.versions.filter(isRecord));
182
+ for (const documentRecord of Array.isArray(entry?.minutes) ? entry.minutes : []) {
183
+ if (Array.isArray(documentRecord?.versions)) versions.push(...documentRecord.versions.filter(isRecord));
184
+ }
185
+ return versions;
186
+ }
187
+
188
+ function latestVersionTime(entry) {
189
+ let latest = null;
190
+ for (const version of listedVersions(entry)) {
191
+ const date = parseDate(version.created_at);
192
+ if (date && (!latest || date > latest)) latest = date;
193
+ }
194
+ return latest ?? firstDate([
195
+ entry?.latest_version_created_at,
196
+ entry?.meeting?.latest_version_created_at,
197
+ isRecord(entry?.meeting?.metadata) ? entry.meeting.metadata.latest_version_created_at : null,
198
+ ]);
199
+ }
200
+
201
+ function importTime(entry) {
202
+ let latest = parseDate(entry?.meeting?.created_at);
203
+ for (const documentRecord of Array.isArray(entry?.minutes) ? entry.minutes : []) {
204
+ const date = parseDate(documentRecord?.created_at);
205
+ if (date && (!latest || date > latest)) latest = date;
206
+ }
207
+ return latest;
208
+ }
209
+
210
+ function meetingTiming(entry) {
211
+ const meeting = isRecord(entry?.meeting) ? entry.meeting : {};
212
+ const scheduled = parseDate(meeting.scheduled_at);
213
+ if (scheduled) return { kind: 'scheduled', source: 'scheduled', at: scheduled.getTime(), date: scheduled };
214
+ const calendar = calendarStart(meeting);
215
+ if (calendar) return { kind: 'fallback', source: 'calendar', at: calendar.getTime(), date: calendar };
216
+ const versionAt = latestVersionTime(entry);
217
+ if (versionAt) return { kind: 'fallback', source: 'version', at: versionAt.getTime(), date: versionAt };
218
+ const imported = importTime(entry);
219
+ return { kind: 'unregistered', source: 'import', at: imported ? imported.getTime() : Number.NEGATIVE_INFINITY, date: imported };
220
+ }
221
+
222
+ function unscheduledLabel(timing) {
223
+ if (!timing?.date) return '会議の日時が未登録';
224
+ const day = formatMonthDay(timing.date);
225
+ if (timing.source === 'calendar') return `日時未登録・予定の開始 ${day}`;
226
+ if (timing.source === 'version') return `日時未登録・版の作成 ${day}`;
227
+ return `日時未登録・取り込み ${day}`;
228
+ }
229
+
230
+ function byCreatedAt(versions) {
231
+ return versions
232
+ .map((version, index) => ({ version, index }))
233
+ .sort((left, right) => {
234
+ const leftTime = parseDate(left.version?.created_at)?.getTime() ?? Number.NEGATIVE_INFINITY;
235
+ const rightTime = parseDate(right.version?.created_at)?.getTime() ?? Number.NEGATIVE_INFINITY;
236
+ if (leftTime !== rightTime) return leftTime - rightTime;
237
+ return left.index - right.index;
238
+ })
239
+ .map((item) => item.version);
240
+ }
241
+
242
+ function sortMeetings(meetings) {
243
+ return meetings
244
+ .map((entry, index) => ({ entry, index, timing: meetingTiming(entry) }))
245
+ .sort((left, right) => {
246
+ const leftBottom = left.timing.kind === 'unregistered';
247
+ const rightBottom = right.timing.kind === 'unregistered';
248
+ if (leftBottom !== rightBottom) return leftBottom ? 1 : -1;
249
+ if (left.timing.at !== right.timing.at) return right.timing.at - left.timing.at;
250
+ return left.index - right.index;
251
+ })
252
+ .map((item) => item.entry);
77
253
  }
78
254
 
79
255
  function idempotencyKey() {
@@ -113,25 +289,71 @@ function preserveLoadedStateOnFailure(error) {
113
289
  function sourceLabel(source) {
114
290
  if (!isRecord(source)) return '';
115
291
  const provider = text(source.provider, '外部ソース');
116
- const locator = text(source.locator, '場所未確認');
117
- const revision = text(source.revision, '版未確認');
292
+ const locator = text(source.locator, '場所は記録されていません');
293
+ const revision = text(source.revision, '版は記録されていません');
118
294
  return `${provider} / ${locator} / ${revision}`;
119
295
  }
120
296
 
297
+ function renderMarkdown(doc, body) {
298
+ const root = makeElement(doc, 'div', { className: 'bb-minutes-markdown' });
299
+ const lines = String(body).replace(/\r\n/g, '\n').split('\n');
300
+ let list = null;
301
+ let listKind = null;
302
+ const closeList = () => {
303
+ if (list) root.append(list);
304
+ list = null;
305
+ listKind = null;
306
+ };
307
+ for (const line of lines) {
308
+ if (/^\s*[-*_]{3,}\s*$/u.test(line)) {
309
+ closeList();
310
+ continue;
311
+ }
312
+ const heading = /^(#{1,6})\s+(.+)$/u.exec(line);
313
+ if (heading) {
314
+ closeList();
315
+ const level = Math.min(heading[1].length, 3);
316
+ root.append(makeElement(doc, `h${level + 2}`, {
317
+ className: 'bb-minutes-md-heading',
318
+ textContent: heading[2].trim(),
319
+ }));
320
+ continue;
321
+ }
322
+ const bullet = /^\s*[-*]\s+(.+)$/u.exec(line);
323
+ const numbered = /^\s*\d+[.)]\s+(.+)$/u.exec(line);
324
+ if (bullet || numbered) {
325
+ const kind = bullet ? 'ul' : 'ol';
326
+ if (!list || listKind !== kind) {
327
+ closeList();
328
+ list = makeElement(doc, kind, { className: 'bb-minutes-md-list' });
329
+ listKind = kind;
330
+ }
331
+ list.append(makeElement(doc, 'li', { textContent: (bullet ?? numbered)[1].trim() }));
332
+ continue;
333
+ }
334
+ if (!line.trim()) {
335
+ closeList();
336
+ continue;
337
+ }
338
+ closeList();
339
+ root.append(makeElement(doc, 'p', { className: 'bb-minutes-md-paragraph', textContent: line }));
340
+ }
341
+ closeList();
342
+ return root;
343
+ }
344
+
121
345
  function versionContent(doc, version) {
122
346
  if (!isRecord(version)) return makeElement(doc, 'p', { textContent: '版の形式を確認できません。', className: 'bb-minutes-warning' });
123
347
  if (isRecord(version.source_ref)) {
124
- return makeElement(doc, 'p', { className: 'bb-minutes-source', textContent: `外部参照: ${sourceLabel(version.source_ref)}` });
125
- }
126
- if (typeof version.body === 'string') {
127
- return makeElement(doc, 'pre', { className: 'bb-minutes-body', textContent: version.body });
348
+ return makeElement(doc, 'p', { className: 'bb-minutes-source', textContent: `本文は元の記録にあります。${sourceLabel(version.source_ref)}` });
128
349
  }
350
+ if (typeof version.body === 'string') return renderMarkdown(doc, version.body);
129
351
  return makeElement(doc, 'p', { className: 'bb-minutes-warning', textContent: '本文または外部参照を確認できません。' });
130
352
  }
131
353
 
132
354
  function detailVersions(detail, minutesId) {
133
355
  if (!isRecord(detail) || !Array.isArray(detail.versions)) return [];
134
- return detail.versions.filter((version) => version?.minutes_id === minutesId);
356
+ return byCreatedAt(detail.versions.filter((version) => version?.minutes_id === minutesId));
135
357
  }
136
358
 
137
359
  function versionLabel(versions, version) {
@@ -139,6 +361,82 @@ function versionLabel(versions, version) {
139
361
  return index >= 0 ? `第${index + 1}版` : '版';
140
362
  }
141
363
 
364
+ function confirmationDetail(confirmation) {
365
+ const when = formatStamp(confirmation?.confirmed_at) || text(confirmation?.confirmed_at);
366
+ const person = text(confirmation?.actor_id);
367
+ if (when && person) return `${when} ${person}が確認`;
368
+ if (when) return `${when} 確認した人の記録がありません`;
369
+ if (person) return `${person}が確認`;
370
+ return '確認済み';
371
+ }
372
+
373
+ function versionState(version, versions, latest) {
374
+ if (!isRecord(version)) return { code: 'unknown', label: '確認の状態を確かめられません', tone: 'muted' };
375
+ if (version.creating === true || version.status === 'creating') {
376
+ return { code: 'creating', label: '版の作成中', tone: 'muted' };
377
+ }
378
+ if (version.confirmation != null && !isRecord(version.confirmation)) {
379
+ return { code: 'unknown', label: '確認の状態を確かめられません', tone: 'muted' };
380
+ }
381
+ const isLatest = Boolean(latest) && version.version_id === latest.version_id;
382
+ if (isRecord(version.confirmation)) {
383
+ return { code: 'confirmed', label: '確認済み', tone: 'ok', detail: confirmationDetail(version.confirmation) };
384
+ }
385
+ if (!isLatest && latest) {
386
+ return { code: 'replaced', label: `確認されないまま${versionLabel(versions, latest)}に置き換え`, tone: 'muted' };
387
+ }
388
+ return { code: 'pending', label: '確認待ち', tone: 'wait' };
389
+ }
390
+
391
+ function creatorFact(version) {
392
+ const creator = text(version?.created_actor_id);
393
+ return creator ? `作成 ${creator}` : '作成者は未記録';
394
+ }
395
+
396
+ function historyStamp(version, today) {
397
+ const created = parseDate(version?.created_at);
398
+ return created
399
+ ? `${formatMonthDay(created)} ${formatClock(created)} 作成(${relativeDay(created, today)})`
400
+ : '作成日時は未記録';
401
+ }
402
+
403
+ function historySecondLine(version, versions, latest) {
404
+ const state = versionState(version, versions, latest);
405
+ const confirmed = state.code === 'confirmed'
406
+ ? (state.detail || confirmationDetail(version.confirmation))
407
+ : state.label;
408
+ const parts = [creatorFact(version), confirmed];
409
+ if (version?.predecessor_version_id) {
410
+ const predecessor = versions.find((candidate) => candidate.version_id === version.predecessor_version_id);
411
+ parts.push(`${versionLabel(versions, predecessor)}を訂正`);
412
+ }
413
+ return parts.join('・');
414
+ }
415
+
416
+ function listedVersionNumber(entry) {
417
+ const versions = listedVersions(entry);
418
+ const documents = Array.isArray(entry?.minutes) ? entry.minutes : [];
419
+ if (versions.length >= 2) {
420
+ const currentId = documents.map((documentRecord) => text(documentRecord?.current_version_id)).find(Boolean);
421
+ const index = versions.findIndex((version) => version.version_id === currentId);
422
+ return index >= 0 ? index + 1 : versions.length;
423
+ }
424
+ const count = documents.reduce((max, documentRecord) => Math.max(
425
+ max,
426
+ Array.isArray(documentRecord?.version_ids) ? documentRecord.version_ids.length : 0,
427
+ ), 0);
428
+ return count >= 2 ? count : 0;
429
+ }
430
+
431
+ function listState(entry) {
432
+ const versions = listedVersions(entry);
433
+ if (!versions.length) return null;
434
+ const documents = Array.isArray(entry?.minutes) ? entry.minutes : [];
435
+ const currentId = documents.map((documentRecord) => text(documentRecord?.current_version_id)).find(Boolean);
436
+ const latest = versions.find((version) => version.version_id === currentId) ?? versions[versions.length - 1];
437
+ return versionState(latest, versions, latest);
438
+ }
439
+
142
440
  function appendIdentifiers(doc, parent, { meetingId, minutesId, versionId } = {}) {
143
441
  const details = makeElement(doc, 'details', { className: 'bb-minutes-identifiers' });
144
442
  details.append(makeElement(doc, 'summary', { textContent: '識別情報' }));
@@ -217,6 +515,7 @@ export function createMeetingMinutesUI({
217
515
  meetingLabel,
218
516
  canCreateMeeting,
219
517
  canEditMinutes,
518
+ now = () => new Date(),
220
519
  } = {}) {
221
520
  if (!root) throw new TypeError('root is required');
222
521
  const doc = explicitDocument ?? (typeof document === 'undefined' ? null : document);
@@ -225,8 +524,6 @@ export function createMeetingMinutesUI({
225
524
  ? fetcher
226
525
  : typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
227
526
  const base = String(basePath).replace(/\/+$/u, '');
228
- const createBlockedMessage = 'この画面では会議を作成できません。';
229
- const editBlockedMessage = 'この画面では議事録を編集できません。';
230
527
  const state = {
231
528
  meetings: [],
232
529
  detail: null,
@@ -245,19 +542,29 @@ export function createMeetingMinutesUI({
245
542
  heading.append(
246
543
  makeElement(doc, 'div', { className: 'bb-minutes-eyebrow', textContent: page?.source ?? '正本の議事録' }),
247
544
  makeElement(doc, 'h1', { textContent: '議事録' }),
248
- makeElement(doc, 'p', { className: 'bb-minutes-lead', textContent: '会議と議事録を保存し、版と確認履歴を同じ場所で確かめます。' }),
545
+ makeElement(doc, 'p', { className: 'bb-minutes-lead', textContent: '新しい会議から読み、確認待ちを見つけて確認します。' }),
249
546
  );
250
547
  const status = makeElement(doc, 'p', { className: 'bb-minutes-status', attrs: { role: 'status', 'aria-live': 'polite' } });
251
548
  const columns = makeElement(doc, 'div', { className: 'bb-minutes-columns' });
252
549
  const listPane = makeElement(doc, 'section', {
253
550
  className: 'bb-minutes-list-pane',
254
- attrs: { 'aria-label': '会議一覧', tabindex: '-1' },
551
+ attrs: { 'aria-label': '議事録', tabindex: '-1' },
255
552
  });
256
- const detailPane = makeElement(doc, 'section', { className: 'bb-minutes-detail-pane', attrs: { 'aria-label': '会議詳細' } });
553
+ const detailPane = makeElement(doc, 'section', { className: 'bb-minutes-detail-pane', attrs: { 'aria-label': '議事録詳細' } });
257
554
  columns.append(listPane, detailPane);
258
555
  screen.append(heading, status, columns);
259
556
  root.replaceChildren(screen);
260
557
 
558
+ function clock() {
559
+ try {
560
+ const value = typeof now === 'function' ? now() : new Date();
561
+ if (value instanceof Date && !Number.isNaN(value.getTime())) return value;
562
+ } catch {
563
+ /* use the runtime clock */
564
+ }
565
+ return new Date();
566
+ }
567
+
261
568
  function setStatus(message, kind = '') {
262
569
  status.textContent = message;
263
570
  status.className = `bb-minutes-status${kind ? ` is-${kind}` : ''}`;
@@ -312,25 +619,41 @@ export function createMeetingMinutesUI({
312
619
  return canEditMinutes !== false;
313
620
  }
314
621
 
315
- function appendCreateBlockedNotice(parent, { alert = false } = {}) {
316
- parent.append(makeElement(doc, 'p', {
317
- className: 'bb-minutes-create-guard',
318
- textContent: createBlockedMessage,
319
- attrs: alert ? { role: 'alert' } : {},
320
- }));
622
+ function stateChip(versionStatus, { detailed = false } = {}) {
623
+ const label = detailed && versionStatus.detail ? versionStatus.detail : versionStatus.label;
624
+ return makeElement(doc, 'span', {
625
+ className: `bb-minutes-state is-${versionStatus.tone}`,
626
+ textContent: label,
627
+ });
321
628
  }
322
629
 
323
- function appendEditBlockedNotice(parent, { alert = false } = {}) {
324
- const existing = [...(parent.children ?? [])].find((child) => child?.className === 'bb-minutes-edit-guard');
325
- if (existing) {
326
- if (alert) existing.setAttribute('role', 'alert');
327
- return existing;
328
- }
329
- parent.append(makeElement(doc, 'p', {
330
- className: 'bb-minutes-edit-guard',
331
- textContent: editBlockedMessage,
332
- attrs: alert ? { role: 'alert' } : {},
630
+ function appendLinePart(line, node) {
631
+ if (line.children.length) line.append(makeElement(doc, 'span', { className: 'bb-minutes-line-sep', textContent: ' · ' }));
632
+ line.append(node);
633
+ }
634
+
635
+ function appendReadOnlyNotice(parent, version) {
636
+ const source = isRecord(version?.source_ref) ? version.source_ref : null;
637
+ const url = httpUrl(source?.locator);
638
+ const provider = text(source?.provider, '元の記録');
639
+ const notice = makeElement(doc, 'div', { className: 'bb-minutes-edit-guard is-info' });
640
+ notice.append(makeElement(doc, 'p', {
641
+ textContent: canEdit()
642
+ ? 'この画面では読むことと確認だけができます。'
643
+ : 'この画面では読むことだけができます。確認はプロジェクトの編集権限がある人が行います。',
333
644
  }));
645
+ if (url) {
646
+ const line = makeElement(doc, 'p', { textContent: '本文を直すときは、元の記録で編集してください。' });
647
+ line.append(makeElement(doc, 'a', {
648
+ className: 'bb-minutes-source-link',
649
+ textContent: `元の記録を開く(${provider})`,
650
+ attrs: { href: url, target: '_blank', rel: 'noopener noreferrer' },
651
+ }));
652
+ notice.append(line);
653
+ } else {
654
+ notice.append(makeElement(doc, 'p', { textContent: '本文を直すときは、議事録を取り込んだ元で編集してください。' }));
655
+ }
656
+ parent.append(notice);
334
657
  }
335
658
 
336
659
  function appendLoadError(parent, message, retry) {
@@ -343,92 +666,178 @@ export function createMeetingMinutesUI({
343
666
  parent.append(notice);
344
667
  }
345
668
 
669
+ function renderMeetingRow(entry) {
670
+ const meeting = entry.meeting;
671
+ const timing = meetingTiming(entry);
672
+ const item = makeElement(doc, 'button', {
673
+ className: `bb-minutes-meeting-item${state.meetingId === meeting.meeting_id ? ' is-selected' : ''}`,
674
+ type: 'button',
675
+ attrs: { type: 'button' },
676
+ });
677
+ item.append(makeElement(doc, 'strong', {
678
+ className: 'bb-minutes-item-title',
679
+ textContent: text(meeting.title, '無題の会議'),
680
+ }));
681
+ const line = makeElement(doc, 'span', { className: 'bb-minutes-item-line' });
682
+ if (timing.kind === 'scheduled') {
683
+ appendLinePart(line, makeElement(doc, 'span', { textContent: formatClock(timing.date) }));
684
+ } else if (timing.kind === 'fallback' || (timing.kind === 'unregistered' && timing.date)) {
685
+ appendLinePart(line, makeElement(doc, 'span', { textContent: unscheduledLabel(timing) }));
686
+ }
687
+ const label = meetingLabelText(meetingLabel, meeting);
688
+ if (label) appendLinePart(line, makeElement(doc, 'span', { className: 'bb-minutes-meeting-label', textContent: label }));
689
+ const rowState = listState(entry);
690
+ if (rowState) appendLinePart(line, stateChip(rowState));
691
+ const versionNumber = listedVersionNumber(entry);
692
+ if (versionNumber >= 2) appendLinePart(line, makeElement(doc, 'span', { textContent: `第${versionNumber}版` }));
693
+ const minutesCount = Array.isArray(entry.minutes) ? entry.minutes.length : 0;
694
+ if (minutesCount >= 2) appendLinePart(line, makeElement(doc, 'span', { textContent: `議事録 ${minutesCount}件` }));
695
+ item.append(line);
696
+ item.addEventListener('click', () => void loadDetail(meeting.meeting_id));
697
+ return item;
698
+ }
699
+
346
700
  function renderList() {
347
701
  clear(listPane);
348
702
  const head = makeElement(doc, 'div', { className: 'bb-minutes-pane-head' });
349
- head.append(makeElement(doc, 'h2', { textContent: '会議一覧' }));
350
- const createAllowed = canCreate();
351
- head.append(button(doc, '会議を作成', () => renderCreateForm(), {
352
- className: 'bb-minutes-button is-primary',
353
- disabled: !createAllowed,
354
- }));
355
- if (!createAllowed) appendCreateBlockedNotice(head);
703
+ const meetings = state.meetings.filter((entry) => isRecord(entry?.meeting) && typeof entry.meeting.meeting_id === 'string');
704
+ if (canCreate()) {
705
+ head.append(button(doc, '会議を作成', () => renderCreateForm(), { className: 'bb-minutes-button is-primary' }));
706
+ }
707
+ head.append(makeElement(doc, 'p', { className: 'bb-minutes-sort-note', textContent: `新しい順・${meetings.length}件` }));
356
708
  listPane.append(head);
709
+ if (!canCreate()) {
710
+ listPane.append(makeElement(doc, 'p', {
711
+ className: 'bb-minutes-create-guard',
712
+ textContent: '会議を作成する権限がありません。作成はプロジェクトの編集権限がある人が行います。',
713
+ }));
714
+ }
357
715
  if (state.listError) {
358
716
  appendLoadError(listPane, `会議一覧を読み込めませんでした。理由: ${state.listError}`, () => loadMeetings({ restoreFocus: true }));
359
717
  }
360
- if (state.meetings.length === 0) {
718
+ if (meetings.length === 0) {
361
719
  if (state.listError) return;
362
- listPane.append(makeElement(doc, 'p', { className: 'bb-minutes-empty', textContent: '保存された会議はありません。会議を作成するとここに表示されます。' }));
720
+ listPane.append(makeElement(doc, 'p', {
721
+ className: 'bb-minutes-empty',
722
+ textContent: 'まだ議事録はありません。会議を取り込むと、ここに新しい順で並びます。',
723
+ }));
363
724
  return;
364
725
  }
365
726
  const list = makeElement(doc, 'div', { className: 'bb-minutes-meeting-list' });
366
- for (const entry of state.meetings) {
367
- const meeting = entry?.meeting;
368
- if (!isRecord(meeting) || typeof meeting.meeting_id !== 'string') continue;
369
- const item = makeElement(doc, 'button', {
370
- className: `bb-minutes-meeting-item${state.meetingId === meeting.meeting_id ? ' is-selected' : ''}`,
371
- type: 'button',
372
- attrs: { type: 'button' },
373
- });
374
- const label = meetingLabelText(meetingLabel, meeting);
375
- item.append(
376
- makeElement(doc, 'strong', { textContent: text(meeting.title, '無題の会議') }),
377
- ...(label ? [makeElement(doc, 'span', { className: 'bb-minutes-meeting-label', textContent: label })] : []),
378
- makeElement(doc, 'span', { className: 'bb-minutes-item-meta', textContent: displayDate(meeting.scheduled_at) }),
379
- makeElement(doc, 'span', { className: 'bb-minutes-item-meta', textContent: `${Array.isArray(entry.minutes) ? entry.minutes.length : 0}件の議事録` }),
380
- );
381
- item.addEventListener('click', () => void loadDetail(meeting.meeting_id));
382
- list.append(item);
727
+ const today = clock();
728
+ let currentKey = null;
729
+ for (const entry of meetings) {
730
+ const timing = meetingTiming(entry);
731
+ if (timing.kind === 'unregistered') continue;
732
+ const key = dateKey(timing.date);
733
+ if (key !== currentKey) {
734
+ currentKey = key;
735
+ list.append(makeElement(doc, 'h3', {
736
+ className: 'bb-minutes-date-heading',
737
+ textContent: dayHeading(timing.date, today),
738
+ }));
739
+ }
740
+ list.append(renderMeetingRow(entry));
741
+ }
742
+ const unregistered = meetings.filter((entry) => meetingTiming(entry).kind === 'unregistered');
743
+ if (unregistered.length) {
744
+ list.append(makeElement(doc, 'h3', {
745
+ className: 'bb-minutes-date-heading',
746
+ textContent: '会議の日時が未登録(取り込んだ日の新しい順)',
747
+ }));
748
+ for (const entry of unregistered) list.append(renderMeetingRow(entry));
383
749
  }
384
750
  listPane.append(list);
385
751
  }
386
752
 
387
- function selectedDetail() {
388
- return state.detail && state.detail.meeting?.meeting_id === state.meetingId ? state.detail : null;
389
- }
390
-
391
- function renderRail(version, documentRecord) {
753
+ function renderRail(version, documentRecord, meeting) {
392
754
  if (!rail) return;
393
755
  clear(rail);
394
756
  const block = makeElement(doc, 'section', { className: 'bb-minutes-rail-block' });
395
- block.append(makeElement(doc, 'h2', { textContent: '確認対象' }));
396
- if (!version) {
397
- block.append(makeElement(doc, 'p', { textContent: '版を選ぶと、本文と確認状態をここに表示します。' }));
398
- } else {
399
- const versions = detailVersions(state.detail, documentRecord?.minutes_id);
400
- block.append(
401
- makeElement(doc, 'p', { className: 'bb-minutes-rail-title', textContent: text(documentRecord?.title, '議事録') }),
402
- makeElement(doc, 'p', { textContent: versionLabel(versions, version) }),
403
- makeElement(doc, 'p', { textContent: version.confirmation ? `確認済み(${displayDate(version.confirmation.confirmed_at)})` : '未確認' }),
404
- );
757
+ block.append(makeElement(doc, 'h2', { textContent: '関連' }));
758
+ if (!meeting && !version) {
759
+ block.append(makeElement(doc, 'p', { textContent: '議事録を選ぶと、ここに本文が出ます。' }));
760
+ rail.append(block);
761
+ return;
405
762
  }
763
+ block.append(makeElement(doc, 'h3', { textContent: '予定' }));
764
+ block.append(makeElement(doc, 'p', { className: 'bb-minutes-rail-note', textContent: '予定とのつながりは未対応です。' }));
765
+ block.append(makeElement(doc, 'h3', { textContent: 'プロジェクト' }));
766
+ const label = meetingLabelText(meetingLabel, meeting);
767
+ block.append(makeElement(doc, 'p', { textContent: label || 'プロジェクトは記録されていません' }));
768
+ appendIdentifiers(doc, block, {
769
+ meetingId: meeting?.meeting_id,
770
+ minutesId: documentRecord?.minutes_id,
771
+ versionId: version?.version_id,
772
+ });
406
773
  rail.append(block);
407
774
  }
408
775
 
409
- function renderVersion(detail, documentRecord, selectedVersion) {
776
+ function renderVersion(detail, documentRecord, selectedVersion, noticeState) {
410
777
  const versions = detailVersions(detail, documentRecord.minutes_id);
411
- const selected = selectedVersion ?? versions.find((version) => version.version_id === documentRecord.current_version_id) ?? versions[versions.length - 1];
778
+ const latest = versions.find((version) => version.version_id === documentRecord.current_version_id) ?? versions[versions.length - 1] ?? null;
779
+ const selected = selectedVersion ?? latest;
412
780
  state.versionId = selected?.version_id ?? null;
413
781
  const selectedLabel = versionLabel(versions, selected);
782
+ const selectedState = versionState(selected, versions, latest);
783
+ const latestState = versionState(latest, versions, latest);
784
+ const viewingOld = Boolean(selected && latest && selected.version_id !== latest.version_id);
414
785
  const section = makeElement(doc, 'section', { className: 'bb-minutes-document' });
415
- const header = makeElement(doc, 'div', { className: 'bb-minutes-document-head' });
416
- header.append(
417
- makeElement(doc, 'div', { className: 'bb-minutes-document-title', textContent: text(documentRecord.title, '議事録') }),
418
- makeElement(doc, 'span', { className: 'bb-minutes-revision', textContent: `現在 ${selectedLabel}` }),
419
- );
420
- section.append(header);
421
- const history = makeElement(doc, 'div', { className: 'bb-minutes-version-history', attrs: { 'aria-label': '版履歴' } });
422
- history.append(makeElement(doc, 'h4', { textContent: '版履歴' }));
786
+ if (viewingOld) {
787
+ const banner = makeElement(doc, 'div', { className: 'bb-minutes-old-version' });
788
+ const latestName = latestState.code === 'confirmed' ? '確認済み' : latestState.label;
789
+ banner.append(makeElement(doc, 'p', {
790
+ textContent: `古い版を表示しています。最新は${versionLabel(versions, latest)}(${latestName})です。`,
791
+ }));
792
+ banner.append(button(doc, '最新の版を見る', () => {
793
+ clear(detailPane);
794
+ renderDetail(detail, documentRecord, latest);
795
+ }));
796
+ section.append(banner);
797
+ }
798
+ const band = makeElement(doc, 'div', { className: 'bb-minutes-version-band' });
799
+ const latestMark = selected && latest && selected.version_id === latest.version_id ? '(最新)' : '';
800
+ band.append(makeElement(doc, 'strong', { textContent: `${selectedLabel}${latestMark}` }));
801
+ const created = formatStamp(selected?.created_at);
802
+ if (created) band.append(makeElement(doc, 'span', { className: 'bb-minutes-item-meta', textContent: created }));
803
+ band.append(stateChip(selectedState, { detailed: true }));
804
+ const history = makeElement(doc, 'div', { className: 'bb-minutes-version-history is-collapsed', attrs: { 'aria-label': '版の履歴' } });
805
+ const historyButton = button(doc, `版の履歴(${versions.length}件)`, () => {
806
+ const collapsed = history.className.includes('is-collapsed');
807
+ history.className = collapsed ? 'bb-minutes-version-history' : 'bb-minutes-version-history is-collapsed';
808
+ historyButton.textContent = collapsed ? `版の履歴を閉じる(${versions.length}件)` : `版の履歴(${versions.length}件)`;
809
+ });
810
+ band.append(historyButton);
811
+ section.append(band);
812
+ if (!noticeState.placed) {
813
+ noticeState.placed = true;
814
+ appendReadOnlyNotice(section, selected);
815
+ }
423
816
  for (const version of [...versions].reverse()) {
424
- const selectedClass = version.version_id === selected?.version_id ? ' is-selected' : '';
425
- const versionButton = makeElement(doc, 'button', { className: `bb-minutes-version${selectedClass}`, type: 'button', attrs: { type: 'button' } });
817
+ const versionButton = makeElement(doc, 'button', {
818
+ className: `bb-minutes-version${version.version_id === selected?.version_id ? ' is-selected' : ''}`,
819
+ type: 'button',
820
+ attrs: { type: 'button' },
821
+ });
822
+ const itemState = versionState(version, versions, latest);
823
+ const isLatest = Boolean(latest) && version.version_id === latest.version_id;
824
+ const line1 = makeElement(doc, 'span', { className: 'bb-minutes-version-line' });
825
+ line1.append(
826
+ makeElement(doc, 'strong', { textContent: `${versionLabel(versions, version)}${isLatest ? '(最新)' : ''}` }),
827
+ stateChip(itemState),
828
+ makeElement(doc, 'span', { className: 'bb-minutes-version-stamp', textContent: historyStamp(version, clock()) }),
829
+ );
426
830
  versionButton.append(
427
- makeElement(doc, 'strong', { textContent: versionLabel(versions, version) }),
428
- makeElement(doc, 'span', { textContent: version.confirmation ? '確認済み' : '未確認' }),
429
- makeElement(doc, 'span', { className: 'bb-minutes-item-meta', textContent: displayDate(version.created_at) }),
831
+ line1,
832
+ makeElement(doc, 'span', {
833
+ className: 'bb-minutes-version-line is-secondary',
834
+ textContent: historySecondLine(version, versions, latest),
835
+ }),
430
836
  );
431
- versionButton.addEventListener('click', () => { clear(detailPane); renderDetail(detail, documentRecord, version); });
837
+ versionButton.addEventListener('click', () => {
838
+ clear(detailPane);
839
+ renderDetail(detail, documentRecord, version);
840
+ });
432
841
  history.append(versionButton);
433
842
  }
434
843
  section.append(history);
@@ -437,23 +846,13 @@ export function createMeetingMinutesUI({
437
846
  className: 'bb-minutes-external-body-slot',
438
847
  attrs: { 'data-minutes-external-body': 'true' },
439
848
  });
440
- content.append(makeElement(doc, 'h4', { textContent: `${selectedLabel}の本文` }), versionContent(doc, selected), bodyRoot);
441
- if (selected?.predecessor_version_id) {
442
- const predecessor = versions.find((version) => version.version_id === selected.predecessor_version_id);
443
- content.append(makeElement(doc, 'p', {
444
- className: 'bb-minutes-item-meta',
445
- textContent: `訂正元: ${versionLabel(versions, predecessor)}`,
446
- }));
447
- }
849
+ content.append(versionContent(doc, selected), bodyRoot);
448
850
  section.append(content);
449
- if (selected) {
450
- const actions = makeElement(doc, 'div', { className: 'bb-minutes-actions' });
451
- const editAllowed = canEdit();
452
- const confirm = button(doc, selected.confirmation ? '確認済み' : 'この版を確認', async () => {
453
- if (selected.confirmation) return;
851
+ if (selected && canEdit() && !isRecord(selected.confirmation) && selectedState.code !== 'unknown' && selectedState.code !== 'creating') {
852
+ const confirm = button(doc, 'この版を確認済みにする', async () => {
454
853
  if (!canEdit()) {
455
- confirm.disabled = true;
456
- appendEditBlockedNotice(section, { alert: true });
854
+ clear(detailPane);
855
+ renderDetail(detail, documentRecord, selected);
457
856
  return;
458
857
  }
459
858
  confirm.disabled = true;
@@ -462,16 +861,14 @@ export function createMeetingMinutesUI({
462
861
  version_id: selected.version_id,
463
862
  expected_revision: documentRecord.revision,
464
863
  });
465
- setStatus('確認を保存しました。', 'success');
466
864
  await loadDetail(detail.meeting.meeting_id);
865
+ if (!state.detailError) setStatus(`${selectedLabel}を確認済みにしました。`, 'success');
467
866
  } catch (error) {
468
867
  confirm.disabled = false;
469
868
  setStatus(error instanceof Error ? error.message : '確認を保存できませんでした。', 'error');
470
869
  }
471
- }, { className: 'bb-minutes-button is-primary', disabled: Boolean(selected.confirmation) || !editAllowed });
472
- actions.append(confirm);
473
- section.append(actions);
474
- if (!editAllowed) appendEditBlockedNotice(section);
870
+ }, { className: 'bb-minutes-button is-primary' });
871
+ band.append(confirm);
475
872
  }
476
873
  appendVersionActionExtensions(doc, section, {
477
874
  extensions: versionActionExtensions,
@@ -489,45 +886,10 @@ export function createMeetingMinutesUI({
489
886
  minutesId: documentRecord.minutes_id,
490
887
  versionId: selected?.version_id,
491
888
  });
492
- appendSaveForm(section, detail, documentRecord);
493
- renderRail(selected, documentRecord);
889
+ renderRail(selected, documentRecord, detail.meeting);
494
890
  return section;
495
891
  }
496
892
 
497
- function appendSaveForm(section, detail, documentRecord) {
498
- if (!canEdit()) {
499
- appendEditBlockedNotice(section);
500
- return;
501
- }
502
- const form = makeElement(doc, 'form', { className: 'bb-minutes-save-form' });
503
- form.append(makeElement(doc, 'h4', { textContent: '訂正版を保存' }));
504
- const field = labelInput(doc, '本文(保存すると新しい版になります)', { type: 'textarea', name: 'body', placeholder: '議事録の本文を入力' });
505
- field.input.rows = 8;
506
- const save = makeElement(doc, 'button', { className: 'bb-minutes-button is-primary', textContent: '新しい版を保存', type: 'submit' });
507
- form.append(field.wrapper, save);
508
- form.addEventListener('submit', async (event) => {
509
- event.preventDefault();
510
- if (!canEdit()) {
511
- save.disabled = false;
512
- appendEditBlockedNotice(section, { alert: true });
513
- return;
514
- }
515
- save.disabled = true;
516
- try {
517
- await mutate(`/${encodeURIComponent(detail.meeting.meeting_id)}/minutes/${encodeURIComponent(documentRecord.minutes_id)}`, {
518
- body: field.input.value,
519
- expected_revision: documentRecord.revision,
520
- });
521
- setStatus('新しい版を保存しました。', 'success');
522
- await loadDetail(detail.meeting.meeting_id);
523
- } catch (error) {
524
- save.disabled = false;
525
- setStatus(error instanceof Error ? error.message : '版を保存できませんでした。', 'error');
526
- }
527
- });
528
- section.append(form);
529
- }
530
-
531
893
  function renderDetail(detail, documentRecord = null, selectedVersion = null) {
532
894
  clear(detailPane);
533
895
  if (state.detailError) {
@@ -538,11 +900,11 @@ export function createMeetingMinutesUI({
538
900
  }
539
901
  if (!detail || !isRecord(detail.meeting)) {
540
902
  if (state.detailError) {
541
- renderRail(null, null);
903
+ renderRail(null, null, null);
542
904
  return;
543
905
  }
544
- detailPane.append(makeElement(doc, 'p', { className: 'bb-minutes-empty', textContent: '会議を選ぶと詳細を表示します。' }));
545
- renderRail(null, null);
906
+ detailPane.append(makeElement(doc, 'p', { className: 'bb-minutes-empty', textContent: '議事録を選ぶと、ここに本文が出ます。' }));
907
+ renderRail(null, null, null);
546
908
  return;
547
909
  }
548
910
  const meeting = detail.meeting;
@@ -550,58 +912,43 @@ export function createMeetingMinutesUI({
550
912
  header.append(makeElement(doc, 'h2', { textContent: text(meeting.title, '無題の会議') }));
551
913
  const label = meetingLabelText(meetingLabel, meeting);
552
914
  if (label) header.append(makeElement(doc, 'p', { className: 'bb-minutes-meeting-label', textContent: label }));
553
- header.append(makeElement(doc, 'p', { className: 'bb-minutes-item-meta', textContent: `${displayDate(meeting.scheduled_at)} / ${Array.isArray(meeting.participant_ids) && meeting.participant_ids.length ? meeting.participant_ids.join('、') : '参加者未登録'}` }));
915
+ const timing = meetingTiming({ meeting, minutes: detail.minutes, versions: detail.versions });
916
+ const when = timing.kind === 'scheduled'
917
+ ? `${formatMonthDay(timing.date)} ${formatClock(timing.date)}`
918
+ : unscheduledLabel(timing);
919
+ const participants = Array.isArray(meeting.participant_ids) && meeting.participant_ids.length
920
+ ? meeting.participant_ids.join('、')
921
+ : '';
922
+ header.append(makeElement(doc, 'p', {
923
+ className: 'bb-minutes-item-meta',
924
+ textContent: participants ? `${when} / ${participants}` : when,
925
+ }));
554
926
  detailPane.append(header);
555
927
  const documents = Array.isArray(detail.minutes) ? detail.minutes : [];
556
- if (documents.length === 0) detailPane.append(makeElement(doc, 'p', { className: 'bb-minutes-warning', textContent: '議事録文書を読み取れません。' }));
557
- for (const documentRecordValue of documents) {
558
- detailPane.append(renderVersion(detail, documentRecordValue, documentRecordValue.minutes_id === documentRecord?.minutes_id ? selectedVersion : null));
928
+ const noticeState = { placed: false };
929
+ if (documents.length === 0) {
930
+ detailPane.append(makeElement(doc, 'p', { className: 'bb-minutes-warning', textContent: '議事録文書を読み取れません。' }));
931
+ appendReadOnlyNotice(detailPane, null);
932
+ noticeState.placed = true;
559
933
  }
560
- detailPane.append(renderCreateMinutesForm(detail));
561
- }
562
-
563
- function renderCreateMinutesForm(detail) {
564
- if (!canEdit()) {
565
- const guard = makeElement(doc, 'section', { className: 'bb-minutes-edit-guard' });
566
- appendEditBlockedNotice(guard);
567
- return guard;
934
+ for (const documentRecordValue of documents) {
935
+ detailPane.append(renderVersion(
936
+ detail,
937
+ documentRecordValue,
938
+ documentRecordValue.minutes_id === documentRecord?.minutes_id ? selectedVersion : null,
939
+ noticeState,
940
+ ));
568
941
  }
569
- const form = makeElement(doc, 'form', { className: 'bb-minutes-create-form' });
570
- form.append(makeElement(doc, 'h3', { textContent: '議事録文書を追加' }));
571
- const title = labelInput(doc, '議事録の名前', { name: 'title', value: '議事録' });
572
- const body = labelInput(doc, '本文', { type: 'textarea', name: 'body', placeholder: '本文を入力' });
573
- body.input.rows = 5;
574
- const save = makeElement(doc, 'button', { className: 'bb-minutes-button', textContent: '議事録を追加', type: 'submit' });
575
- form.append(title.wrapper, body.wrapper, save);
576
- form.addEventListener('submit', async (event) => {
577
- event.preventDefault();
578
- if (!canEdit()) {
579
- save.disabled = false;
580
- appendEditBlockedNotice(form, { alert: true });
581
- return;
582
- }
583
- save.disabled = true;
584
- try {
585
- await mutate(`/${encodeURIComponent(detail.meeting.meeting_id)}/minutes`, {
586
- title: title.input.value,
587
- body: body.input.value,
588
- expected_revision: detail.snapshot_revision,
589
- });
590
- setStatus('議事録文書を追加しました。', 'success');
591
- await loadMeetings();
592
- await loadDetail(detail.meeting.meeting_id);
593
- } catch (error) {
594
- save.disabled = false;
595
- setStatus(error instanceof Error ? error.message : '議事録を追加できませんでした。', 'error');
596
- }
597
- });
598
- return form;
599
942
  }
600
943
 
601
944
  function renderCreateForm() {
602
945
  clear(detailPane);
603
946
  if (!canCreate()) {
604
- appendCreateBlockedNotice(detailPane, { alert: true });
947
+ detailPane.append(makeElement(doc, 'p', { className: 'bb-minutes-empty', textContent: '議事録を選ぶと、ここに本文が出ます。' }));
948
+ detailPane.append(makeElement(doc, 'p', {
949
+ className: 'bb-minutes-create-guard',
950
+ textContent: '会議を作成する権限がありません。作成はプロジェクトの編集権限がある人が行います。',
951
+ }));
605
952
  return;
606
953
  }
607
954
  const form = makeElement(doc, 'form', { className: 'bb-minutes-create-form' });
@@ -617,8 +964,14 @@ export function createMeetingMinutesUI({
617
964
  form.addEventListener('submit', async (event) => {
618
965
  event.preventDefault();
619
966
  if (!canCreate()) {
620
- save.disabled = false;
621
- appendCreateBlockedNotice(detailPane, { alert: true });
967
+ save.disabled = true;
968
+ renderList();
969
+ clear(detailPane);
970
+ detailPane.append(makeElement(doc, 'p', { className: 'bb-minutes-empty', textContent: '議事録を選ぶと、ここに本文が出ます。' }));
971
+ detailPane.append(makeElement(doc, 'p', {
972
+ className: 'bb-minutes-create-guard',
973
+ textContent: '会議を作成する権限がありません。作成はプロジェクトの編集権限がある人が行います。',
974
+ }));
622
975
  return;
623
976
  }
624
977
  save.disabled = true;
@@ -653,7 +1006,7 @@ export function createMeetingMinutesUI({
653
1006
  const payload = await call('');
654
1007
  if (!isCurrentRequest()) return state.meetings;
655
1008
  if (!isRecord(payload) || !Array.isArray(payload.meetings) || payload.absence_confirmed !== true) throw new Error('会議一覧の形式を確認できません。');
656
- state.meetings = payload.meetings;
1009
+ state.meetings = sortMeetings(payload.meetings);
657
1010
  state.listError = null;
658
1011
  const selectedMeetingStillListed = state.meetingId !== null
659
1012
  && state.meetings.some((entry) => entry?.meeting?.meeting_id === state.meetingId);
@@ -670,8 +1023,8 @@ export function createMeetingMinutesUI({
670
1023
  if (hadDetailError) renderDetail(null);
671
1024
  }
672
1025
  renderList();
673
- if (state.detailError && state.detail === null) renderDetail(null);
674
- setStatus(state.meetings.length ? `${state.meetings.length}件の会議を読み込みました。` : '保存された会議はありません。');
1026
+ if (!state.detail) renderDetail(null);
1027
+ setStatus(state.meetings.length ? `${state.meetings.length}件の会議を読み込みました。` : 'まだ議事録はありません。');
675
1028
  if (restoreFocus && typeof listPane.focus === 'function') listPane.focus({ preventScroll: true });
676
1029
  } catch (error) {
677
1030
  if (!isCurrentRequest()) return state.meetings;
@@ -728,6 +1081,7 @@ export function createMeetingMinutesUI({
728
1081
  return state.detail;
729
1082
  }
730
1083
 
1084
+ renderDetail(null);
731
1085
  void loadMeetings();
732
1086
  return {
733
1087
  contractVersion: MEETING_MINUTES_UI_CONTRACT_VERSION,