@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.
- package/dist/canonical-task-service.js +10 -5
- package/package.json +2 -2
- package/ui/foundation-overview.js +0 -6
- package/ui/judgment-history.js +0 -17
- package/ui/meeting-minutes.css +19 -0
- package/ui/meeting-minutes.js +117 -3
- package/ui/outcome-knowledge.css +5 -0
- package/ui/outcome-knowledge.js +1 -1
- package/ui/world/world-district.js +8 -0
|
@@ -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
|
|
898
|
-
dueAfter
|
|
899
|
-
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
|
|
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.
|
|
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": "
|
|
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));
|
package/ui/judgment-history.js
CHANGED
|
@@ -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
|
: 'この期間の判断履歴はありません。';
|
package/ui/meeting-minutes.css
CHANGED
|
@@ -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,
|
package/ui/meeting-minutes.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/ui/outcome-knowledge.css
CHANGED
|
@@ -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;
|
package/ui/outcome-knowledge.js
CHANGED
|
@@ -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);
|