@unson/brainbase-mcp 0.41.0 → 0.41.2

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.
@@ -134,6 +134,11 @@ function normalizeLimit(value, max = MAX_LIMIT) {
134
134
  }
135
135
  return limit;
136
136
  }
137
+ // Optional list filters are only sent to the repository when they carry a value.
138
+ // An empty value must mean "no filter", never "match IS NULL".
139
+ function optionalFilter(key, value) {
140
+ return value == null ? {} : { [key]: value };
141
+ }
137
142
  function normalizeCursor(value) {
138
143
  return normalizeString(value, 'cursor', { max: 500 });
139
144
  }
@@ -885,7 +890,7 @@ export class CanonicalTaskService {
885
890
  return patch;
886
891
  }
887
892
  normalizeListFilters(filters) {
888
- const { status: _status, priority: _priority, projectCode: _projectCode, project_code: _project_code, ...canonicalFilters } = filters;
893
+ const { status: _status, priority: _priority, projectCode: _projectCode, project_code: _project_code, assigneePersonId: _assigneePersonId, dueAfter: _dueAfter, dueBefore: _dueBefore, cursor: _cursor, ...canonicalFilters } = filters;
889
894
  const statuses = normalizeCollection(filters.statuses ?? filters.status, 'statuses');
890
895
  const priorities = normalizeCollection(filters.priorities ?? filters.priority, 'priorities');
891
896
  const projectCodes = normalizeProjectCodes(filters.projectCodes ?? filters.projectCode ?? filters.project_code);
@@ -894,11 +899,11 @@ export class CanonicalTaskService {
894
899
  statuses,
895
900
  priorities,
896
901
  projectCodes,
897
- assigneePersonId: normalizeString(filters.assigneePersonId, 'assigneePersonId', { max: 200 }),
898
- dueAfter: normalizeIsoDate(filters.dueAfter, 'dueAfter'),
899
- dueBefore: normalizeIsoDate(filters.dueBefore, 'dueBefore'),
902
+ ...optionalFilter('assigneePersonId', normalizeString(filters.assigneePersonId, 'assigneePersonId', { max: 200 })),
903
+ ...optionalFilter('dueAfter', normalizeIsoDate(filters.dueAfter, 'dueAfter')),
904
+ ...optionalFilter('dueBefore', normalizeIsoDate(filters.dueBefore, 'dueBefore')),
900
905
  limit: normalizeLimit(filters.limit),
901
- cursor: normalizeCursor(filters.cursor),
906
+ ...optionalFilter('cursor', normalizeCursor(filters.cursor)),
902
907
  };
903
908
  }
904
909
  normalizeSearchFilters(filters) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.41.0",
3
+ "version": "0.41.2",
4
4
  "description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -481,5 +481,5 @@
481
481
  "engines": {
482
482
  "node": ">=20"
483
483
  },
484
- "gitHead": "c460d39f08bd68e8630fcdd000a934017f41203c"
484
+ "gitHead": "193d1d1fea9d177b04a3c6b01069aef202979526"
485
485
  }
@@ -11,7 +11,6 @@ import {
11
11
  workspaceButton,
12
12
  workspaceDefinition,
13
13
  workspaceLedger,
14
- workspaceMetrics,
15
14
  workspaceNotice,
16
15
  workspacePageHeader,
17
16
  workspaceSectionTitle,
@@ -718,11 +717,6 @@ export function renderFoundationOverview(root, state, {
718
717
  surface.append(recordContexts
719
718
  ? makeElement(doc, 'p', { className: 'bb-fov-recorded-notice', text: '記録上の現状は保存時点の記述です。登録状態と検証状態を分けて読みます。' })
720
719
  : workspaceNotice(doc, { label: '読み方', text: '登録は達成・採用・検証済みを意味しません。記録上の現状は保存時点の記述です。' }));
721
- surface.append(workspaceMetrics(doc, [
722
- { label: '目的', value: catalog.objectives.length, note: partial ? '取得済みの定義(全体は未確認)' : '登録された定義' },
723
- { label: '哲学', value: catalog.philosophies.length, note: partial ? '取得済みの版(全体は未確認)' : '登録された版' },
724
- { label: '世界モデル', value: catalog.models.length, note: partial ? '取得済みの検索対象(全体は未確認)' : '検索対象' },
725
- ], { ariaLabel: 'Foundationの登録数' }));
726
720
  surface.append(renderObjectives(doc, catalog, viewState, callbacks, recordContexts));
727
721
  surface.append(renderPhilosophies(doc, catalog, recordContexts));
728
722
  surface.append(renderModels(doc, catalog, viewState, callbacks, recordContexts));
@@ -14,7 +14,6 @@ import {
14
14
  workspaceDefinition,
15
15
  workspaceDetailEmpty,
16
16
  workspaceLedger,
17
- workspaceMetrics,
18
17
  workspaceNotice,
19
18
  workspacePageHeader,
20
19
  workspaceRailBlock,
@@ -1721,22 +1720,6 @@ export function createJudgmentHistoryUI({
1721
1720
  ? `保存された判断の理由、当時の参照、案、実行と結果を表示しています。参照の本文は現在の権限で確認できる場合だけ開きます。${normalSourceSummary ? ` 取得元: ${normalSourceSummary}` : ''}`
1722
1721
  : '判断に残った参照だけを表示しています。実行結果の証拠は参照件数に含めません。',
1723
1722
  }));
1724
- const stats = aggregate.stats ?? {};
1725
- const metricCount = (value, suffix) => value === null || value === undefined ? '未確認' : `${value}${suffix}`;
1726
- wrapper.append(workspaceMetrics(doc, normalMode() ? [
1727
- {
1728
- label: aggregate.query ? '表示対象' : aggregate.countComplete ? '判断件数' : '取得済み',
1729
- value: metricCount(aggregate.countComplete ? stats.judgments : aggregate.retrievedCount, '件'),
1730
- note: aggregate.countComplete ? 'この期間・絞り込みの全件数'
1731
- : aggregate.query ? '取得済み履歴の検索結果・全件数未確認' : '全件数未確認',
1732
- },
1733
- { label: '使った参照', value: metricCount(stats.references, '件'), note: '当時の記録にある一意の参照' },
1734
- { label: '確認済みの結果', value: metricCount(stats.confirmedOutcomes, '件'), note: '成果が確認済みと記録された判断' },
1735
- ] : [
1736
- { label: 'Brainbaseが代わりに判断した件数', value: metricCount(stats.judgments, '件'), note: 'この期間の履歴' },
1737
- { label: 'そこで使った参照', value: metricCount(stats.references, '件'), note: '代理判断に含まれる一意の参照' },
1738
- { label: '判断回数にすると', value: metricCount(stats.equivalent, '回分相当'), note: '代理判断1件を1回分として表示' },
1739
- ], { ariaLabel: '判断履歴の集計' }));
1740
1723
  const emptyText = state.home?.status === 'partial' || (normalMode() && !aggregate.countComplete)
1741
1724
  ? 'この期間に取得できた判断履歴はありません。全件数が未確認のため0件とは確認できません。'
1742
1725
  : 'この期間の判断履歴はありません。';
@@ -16,12 +16,31 @@
16
16
  .bb-minutes-eyebrow,
17
17
  .bb-minutes-label,
18
18
  .bb-minutes-item-meta,
19
+ .bb-minutes-meeting-label,
19
20
  .bb-minutes-rail-title,
20
21
  .bb-minutes-revision {
21
22
  color: var(--bb-color-muted);
22
23
  font-size: 0.82rem;
23
24
  }
24
25
 
26
+ .bb-minutes-meeting-label {
27
+ margin: 0;
28
+ overflow-wrap: anywhere;
29
+ }
30
+
31
+ .bb-minutes-create-guard {
32
+ flex-basis: 100%;
33
+ margin: 0;
34
+ color: var(--bb-color-danger, var(--bb-color-ink));
35
+ }
36
+
37
+ .bb-minutes-edit-guard {
38
+ display: grid;
39
+ gap: var(--bb-space-2);
40
+ margin: 0;
41
+ color: var(--bb-color-danger, var(--bb-color-ink));
42
+ }
43
+
25
44
  .bb-minutes-heading h1,
26
45
  .bb-minutes-heading p,
27
46
  .bb-minutes-pane-head h2,
@@ -16,6 +16,15 @@ function text(value, fallback = '') {
16
16
  return typeof value === 'string' && value.trim() ? value.trim() : fallback;
17
17
  }
18
18
 
19
+ function meetingLabelText(meetingLabel, meeting) {
20
+ if (typeof meetingLabel !== 'function') return '';
21
+ try {
22
+ return text(meetingLabel(meeting));
23
+ } catch {
24
+ return '';
25
+ }
26
+ }
27
+
19
28
  function makeElement(doc, tag, { className, textContent, type, value, disabled, attrs = {} } = {}) {
20
29
  const element = doc.createElement(tag);
21
30
  if (className) element.className = className;
@@ -205,6 +214,9 @@ export function createMeetingMinutesUI({
205
214
  token,
206
215
  basePath = '/api/meeting-minutes',
207
216
  versionActionExtensions = [],
217
+ meetingLabel,
218
+ canCreateMeeting,
219
+ canEditMinutes,
208
220
  } = {}) {
209
221
  if (!root) throw new TypeError('root is required');
210
222
  const doc = explicitDocument ?? (typeof document === 'undefined' ? null : document);
@@ -213,6 +225,8 @@ export function createMeetingMinutesUI({
213
225
  ? fetcher
214
226
  : typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
215
227
  const base = String(basePath).replace(/\/+$/u, '');
228
+ const createBlockedMessage = 'この画面では会議を作成できません。';
229
+ const editBlockedMessage = 'この画面では議事録を編集できません。';
216
230
  const state = {
217
231
  meetings: [],
218
232
  detail: null,
@@ -223,6 +237,8 @@ export function createMeetingMinutesUI({
223
237
  listError: null,
224
238
  detailError: null,
225
239
  };
240
+ let listRequestGeneration = 0;
241
+ let detailRequestGeneration = 0;
226
242
 
227
243
  const screen = makeElement(doc, 'div', { className: 'bb-minutes-screen', attrs: { 'data-contract-version': MEETING_MINUTES_UI_CONTRACT_VERSION } });
228
244
  const heading = makeElement(doc, 'header', { className: 'bb-minutes-heading' });
@@ -274,6 +290,49 @@ export function createMeetingMinutesUI({
274
290
 
275
291
  function clear(node) { node.replaceChildren(); return node; }
276
292
 
293
+ function canCreate() {
294
+ if (typeof canCreateMeeting === 'function') {
295
+ try {
296
+ return canCreateMeeting() === true;
297
+ } catch {
298
+ return false;
299
+ }
300
+ }
301
+ return canCreateMeeting !== false;
302
+ }
303
+
304
+ function canEdit() {
305
+ if (typeof canEditMinutes === 'function') {
306
+ try {
307
+ return canEditMinutes() === true;
308
+ } catch {
309
+ return false;
310
+ }
311
+ }
312
+ return canEditMinutes !== false;
313
+ }
314
+
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
+ }));
321
+ }
322
+
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' } : {},
333
+ }));
334
+ }
335
+
277
336
  function appendLoadError(parent, message, retry) {
278
337
  const notice = makeElement(doc, 'div', {
279
338
  className: 'bb-minutes-load-error',
@@ -288,7 +347,12 @@ export function createMeetingMinutesUI({
288
347
  clear(listPane);
289
348
  const head = makeElement(doc, 'div', { className: 'bb-minutes-pane-head' });
290
349
  head.append(makeElement(doc, 'h2', { textContent: '会議一覧' }));
291
- head.append(button(doc, '会議を作成', () => renderCreateForm(), { className: 'bb-minutes-button is-primary' }));
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);
292
356
  listPane.append(head);
293
357
  if (state.listError) {
294
358
  appendLoadError(listPane, `会議一覧を読み込めませんでした。理由: ${state.listError}`, () => loadMeetings({ restoreFocus: true }));
@@ -307,8 +371,10 @@ export function createMeetingMinutesUI({
307
371
  type: 'button',
308
372
  attrs: { type: 'button' },
309
373
  });
374
+ const label = meetingLabelText(meetingLabel, meeting);
310
375
  item.append(
311
376
  makeElement(doc, 'strong', { textContent: text(meeting.title, '無題の会議') }),
377
+ ...(label ? [makeElement(doc, 'span', { className: 'bb-minutes-meeting-label', textContent: label })] : []),
312
378
  makeElement(doc, 'span', { className: 'bb-minutes-item-meta', textContent: displayDate(meeting.scheduled_at) }),
313
379
  makeElement(doc, 'span', { className: 'bb-minutes-item-meta', textContent: `${Array.isArray(entry.minutes) ? entry.minutes.length : 0}件の議事録` }),
314
380
  );
@@ -382,8 +448,14 @@ export function createMeetingMinutesUI({
382
448
  section.append(content);
383
449
  if (selected) {
384
450
  const actions = makeElement(doc, 'div', { className: 'bb-minutes-actions' });
451
+ const editAllowed = canEdit();
385
452
  const confirm = button(doc, selected.confirmation ? '確認済み' : 'この版を確認', async () => {
386
453
  if (selected.confirmation) return;
454
+ if (!canEdit()) {
455
+ confirm.disabled = true;
456
+ appendEditBlockedNotice(section, { alert: true });
457
+ return;
458
+ }
387
459
  confirm.disabled = true;
388
460
  try {
389
461
  await mutate(`/${encodeURIComponent(detail.meeting.meeting_id)}/minutes/${encodeURIComponent(documentRecord.minutes_id)}/confirm`, {
@@ -396,9 +468,10 @@ export function createMeetingMinutesUI({
396
468
  confirm.disabled = false;
397
469
  setStatus(error instanceof Error ? error.message : '確認を保存できませんでした。', 'error');
398
470
  }
399
- }, { className: 'bb-minutes-button is-primary', disabled: Boolean(selected.confirmation) });
471
+ }, { className: 'bb-minutes-button is-primary', disabled: Boolean(selected.confirmation) || !editAllowed });
400
472
  actions.append(confirm);
401
473
  section.append(actions);
474
+ if (!editAllowed) appendEditBlockedNotice(section);
402
475
  }
403
476
  appendVersionActionExtensions(doc, section, {
404
477
  extensions: versionActionExtensions,
@@ -422,6 +495,10 @@ export function createMeetingMinutesUI({
422
495
  }
423
496
 
424
497
  function appendSaveForm(section, detail, documentRecord) {
498
+ if (!canEdit()) {
499
+ appendEditBlockedNotice(section);
500
+ return;
501
+ }
425
502
  const form = makeElement(doc, 'form', { className: 'bb-minutes-save-form' });
426
503
  form.append(makeElement(doc, 'h4', { textContent: '訂正版を保存' }));
427
504
  const field = labelInput(doc, '本文(保存すると新しい版になります)', { type: 'textarea', name: 'body', placeholder: '議事録の本文を入力' });
@@ -430,6 +507,11 @@ export function createMeetingMinutesUI({
430
507
  form.append(field.wrapper, save);
431
508
  form.addEventListener('submit', async (event) => {
432
509
  event.preventDefault();
510
+ if (!canEdit()) {
511
+ save.disabled = false;
512
+ appendEditBlockedNotice(section, { alert: true });
513
+ return;
514
+ }
433
515
  save.disabled = true;
434
516
  try {
435
517
  await mutate(`/${encodeURIComponent(detail.meeting.meeting_id)}/minutes/${encodeURIComponent(documentRecord.minutes_id)}`, {
@@ -466,6 +548,8 @@ export function createMeetingMinutesUI({
466
548
  const meeting = detail.meeting;
467
549
  const header = makeElement(doc, 'header', { className: 'bb-minutes-detail-head' });
468
550
  header.append(makeElement(doc, 'h2', { textContent: text(meeting.title, '無題の会議') }));
551
+ const label = meetingLabelText(meetingLabel, meeting);
552
+ if (label) header.append(makeElement(doc, 'p', { className: 'bb-minutes-meeting-label', textContent: label }));
469
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('、') : '参加者未登録'}` }));
470
554
  detailPane.append(header);
471
555
  const documents = Array.isArray(detail.minutes) ? detail.minutes : [];
@@ -477,6 +561,11 @@ export function createMeetingMinutesUI({
477
561
  }
478
562
 
479
563
  function renderCreateMinutesForm(detail) {
564
+ if (!canEdit()) {
565
+ const guard = makeElement(doc, 'section', { className: 'bb-minutes-edit-guard' });
566
+ appendEditBlockedNotice(guard);
567
+ return guard;
568
+ }
480
569
  const form = makeElement(doc, 'form', { className: 'bb-minutes-create-form' });
481
570
  form.append(makeElement(doc, 'h3', { textContent: '議事録文書を追加' }));
482
571
  const title = labelInput(doc, '議事録の名前', { name: 'title', value: '議事録' });
@@ -486,6 +575,11 @@ export function createMeetingMinutesUI({
486
575
  form.append(title.wrapper, body.wrapper, save);
487
576
  form.addEventListener('submit', async (event) => {
488
577
  event.preventDefault();
578
+ if (!canEdit()) {
579
+ save.disabled = false;
580
+ appendEditBlockedNotice(form, { alert: true });
581
+ return;
582
+ }
489
583
  save.disabled = true;
490
584
  try {
491
585
  await mutate(`/${encodeURIComponent(detail.meeting.meeting_id)}/minutes`, {
@@ -506,6 +600,10 @@ export function createMeetingMinutesUI({
506
600
 
507
601
  function renderCreateForm() {
508
602
  clear(detailPane);
603
+ if (!canCreate()) {
604
+ appendCreateBlockedNotice(detailPane, { alert: true });
605
+ return;
606
+ }
509
607
  const form = makeElement(doc, 'form', { className: 'bb-minutes-create-form' });
510
608
  form.append(makeElement(doc, 'h2', { textContent: '会議を作成' }), makeElement(doc, 'p', { className: 'bb-minutes-item-meta', textContent: '外部サービスを接続しなくても、会議と議事録を保存できます。' }));
511
609
  const title = labelInput(doc, '会議名', { name: 'title', placeholder: '例: 週次定例' });
@@ -518,6 +616,11 @@ export function createMeetingMinutesUI({
518
616
  form.append(title.wrapper, date.wrapper, participants.wrapper, minutesTitle.wrapper, body.wrapper, save);
519
617
  form.addEventListener('submit', async (event) => {
520
618
  event.preventDefault();
619
+ if (!canCreate()) {
620
+ save.disabled = false;
621
+ appendCreateBlockedNotice(detailPane, { alert: true });
622
+ return;
623
+ }
521
624
  save.disabled = true;
522
625
  try {
523
626
  const scheduledAt = date.input.value ? new Date(date.input.value).toISOString() : undefined;
@@ -542,16 +645,20 @@ export function createMeetingMinutesUI({
542
645
  }
543
646
 
544
647
  async function loadMeetings({ restoreFocus = false } = {}) {
648
+ const requestGeneration = ++listRequestGeneration;
649
+ const isCurrentRequest = () => requestGeneration === listRequestGeneration;
545
650
  state.loading = true;
546
651
  setStatus('会議一覧を読み込んでいます。');
547
652
  try {
548
653
  const payload = await call('');
654
+ if (!isCurrentRequest()) return state.meetings;
549
655
  if (!isRecord(payload) || !Array.isArray(payload.meetings) || payload.absence_confirmed !== true) throw new Error('会議一覧の形式を確認できません。');
550
656
  state.meetings = payload.meetings;
551
657
  state.listError = null;
552
658
  const selectedMeetingStillListed = state.meetingId !== null
553
659
  && state.meetings.some((entry) => entry?.meeting?.meeting_id === state.meetingId);
554
660
  if (state.meetingId !== null && !selectedMeetingStillListed) {
661
+ detailRequestGeneration += 1;
555
662
  state.detail = null;
556
663
  state.meetingId = null;
557
664
  state.minutesId = null;
@@ -567,12 +674,14 @@ export function createMeetingMinutesUI({
567
674
  setStatus(state.meetings.length ? `${state.meetings.length}件の会議を読み込みました。` : '保存された会議はありません。');
568
675
  if (restoreFocus && typeof listPane.focus === 'function') listPane.focus({ preventScroll: true });
569
676
  } catch (error) {
677
+ if (!isCurrentRequest()) return state.meetings;
570
678
  state.listError = loadErrorMessage(error, '会議一覧を読み込めませんでした。');
571
679
  const preserve = preserveLoadedStateOnFailure(error);
572
680
  const hadDetail = Boolean(state.detail) || state.meetingId !== null;
573
681
  if (!preserve) {
574
682
  state.meetings = [];
575
683
  if (hadDetail) {
684
+ detailRequestGeneration += 1;
576
685
  state.detail = null;
577
686
  state.meetingId = null;
578
687
  state.minutesId = null;
@@ -584,12 +693,15 @@ export function createMeetingMinutesUI({
584
693
  if (!preserve && hadDetail) renderDetail(null);
585
694
  setStatus(state.listError, 'error');
586
695
  } finally {
587
- state.loading = false;
696
+ if (isCurrentRequest()) state.loading = false;
588
697
  }
589
698
  return state.meetings;
590
699
  }
591
700
 
592
701
  async function loadDetail(meetingId) {
702
+ const requestGeneration = ++detailRequestGeneration;
703
+ const isCurrentRequest = () => requestGeneration === detailRequestGeneration
704
+ && state.meetingId === meetingId;
593
705
  state.meetingId = meetingId;
594
706
  state.minutesId = null;
595
707
  state.detailError = null;
@@ -597,12 +709,14 @@ export function createMeetingMinutesUI({
597
709
  setStatus('会議詳細を読み込んでいます。');
598
710
  try {
599
711
  const detail = await call(`/${encodeURIComponent(meetingId)}`);
712
+ if (!isCurrentRequest()) return state.detail;
600
713
  if (!isRecord(detail) || !isRecord(detail.meeting) || !Array.isArray(detail.minutes) || !Array.isArray(detail.versions)) throw new Error('会議詳細の形式を確認できません。');
601
714
  state.detail = detail;
602
715
  state.detailError = null;
603
716
  renderDetail(detail);
604
717
  setStatus('会議詳細を読み込みました。');
605
718
  } catch (error) {
719
+ if (!isCurrentRequest()) return state.detail;
606
720
  const preserved = preserveLoadedStateOnFailure(error) && state.detail?.meeting?.meeting_id === meetingId
607
721
  ? state.detail
608
722
  : null;
@@ -216,6 +216,11 @@
216
216
  .knowledge-outcome .knowledge-form-actions,
217
217
  .knowledge-outcome .knowledge-detail-actions,
218
218
  .knowledge-outcome .knowledge-lifecycle-actions { display: flex; flex-wrap: wrap; gap: .45rem; align-items: center; }
219
+ .knowledge-outcome .knowledge-lifecycle-actions > .knowledge-revision-details {
220
+ flex: 1 1 100%;
221
+ min-width: 0;
222
+ }
223
+ .knowledge-outcome .knowledge-revision-details .knowledge-lifecycle-form { width: 100%; }
219
224
  .knowledge-outcome .knowledge-filter-actions {
220
225
  grid-column: 1 / -1;
221
226
  align-self: end;
@@ -1168,7 +1168,7 @@ export function renderLifecycle(root, item, callbacks = {}) {
1168
1168
  if (canEdit && callbacks.onRevision) {
1169
1169
  const revisionDraft = callbacks.revisionDraft && typeof callbacks.revisionDraft === 'object' ? callbacks.revisionDraft : null;
1170
1170
  const draftValue = (key, fallback) => revisionDraft && Object.prototype.hasOwnProperty.call(revisionDraft, key) ? revisionDraft[key] : fallback;
1171
- const revisionDetails = makeElement('details', { className: 'knowledge-action-details' });
1171
+ const revisionDetails = makeElement('details', { className: 'knowledge-action-details knowledge-revision-details' });
1172
1172
  const summary = makeElement('summary', { text: 'この判断を改訂' });
1173
1173
  const form = makeElement('form', { className: 'knowledge-lifecycle-form' });
1174
1174
  const title = makeElement('input', { className: 'knowledge-control', value: draftValue('title', item.title ?? ''), attrs: { name: 'title', maxlength: '240', required: 'required' } });
@@ -1017,10 +1017,18 @@ export function createDistrictView({ doc, stage, reducedMotion = false, focusSel
1017
1017
  }
1018
1018
 
1019
1019
  let running = true;
1020
+ let lastSkyMinute = -1;
1020
1021
  function frame(time) {
1021
1022
  if (!running) return;
1022
1023
  requestAnimationFrame(frame);
1023
1024
  if (!open) return;
1025
+ // Use wall time so an open district also catches up after a suspended tab.
1026
+ const now = new Date();
1027
+ const minute = Math.floor(now.getTime() / 60_000);
1028
+ if (skyMode === 'auto' && minute !== lastSkyMinute) {
1029
+ if (skyAt(now.getHours(), 'auto').phase !== sky.phase) applySky();
1030
+ lastSkyMinute = minute;
1031
+ }
1024
1032
  if (flight) {
1025
1033
  flight.started ??= time;
1026
1034
  const t = Math.min((time - flight.started) / flight.duration, 1);