@unson/brainbase-mcp 0.21.1 → 0.22.0
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/local-web-fetch-bridge.js +1 -1
- package/dist/local-web-host.d.ts +59 -0
- package/dist/local-web-host.js +171 -4
- package/dist/meeting-minutes-http.d.ts +22 -0
- package/dist/meeting-minutes-http.js +252 -0
- package/dist/meeting-minutes-lineage-adapters.d.ts +157 -0
- package/dist/meeting-minutes-lineage-adapters.js +410 -0
- package/dist/meeting-minutes-lineage-http.d.ts +20 -0
- package/dist/meeting-minutes-lineage-http.js +221 -0
- package/dist/meeting-minutes-lineage-lifecycle.d.ts +58 -0
- package/dist/meeting-minutes-lineage-lifecycle.js +146 -0
- package/dist/meeting-minutes-lineage.d.ts +307 -0
- package/dist/meeting-minutes-lineage.js +1030 -0
- package/dist/meeting-minutes-storage-controller.d.ts +182 -0
- package/dist/meeting-minutes-storage-controller.js +366 -0
- package/dist/meeting-minutes-storage-http.d.ts +25 -0
- package/dist/meeting-minutes-storage-http.js +312 -0
- package/dist/meeting-minutes-storage.d.ts +169 -0
- package/dist/meeting-minutes-storage.js +402 -0
- package/dist/meeting-minutes.d.ts +223 -0
- package/dist/meeting-minutes.js +1172 -0
- package/dist/world-extension.js +1 -1
- package/package.json +46 -3
- package/ui/brainbase-tokens.css +35 -35
- package/ui/local-web-shell.css +20 -17
- package/ui/local-web-shell.js +34 -1
- package/ui/meeting-minutes-lineage-http.js +247 -0
- package/ui/meeting-minutes-lineage.css +177 -0
- package/ui/meeting-minutes-lineage.js +485 -0
- package/ui/meeting-minutes-storage.css +170 -0
- package/ui/meeting-minutes-storage.js +388 -0
- package/ui/meeting-minutes.css +215 -0
- package/ui/meeting-minutes.js +622 -0
- package/ui/world/world-district.js +306 -87
- package/ui/world/world-scenery.js +208 -0
- package/ui/world/world-view.css +54 -0
- package/ui/world/world-view.js +65 -205
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/* Storage placement and source status panel. Load after brainbase-tokens.css. */
|
|
2
|
+
|
|
3
|
+
.bb-mms {
|
|
4
|
+
display: grid;
|
|
5
|
+
gap: 18px;
|
|
6
|
+
min-width: 0;
|
|
7
|
+
color: var(--bb-color-ink);
|
|
8
|
+
font-family: var(--bb-font-sans, ui-sans-serif, system-ui, sans-serif);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.bb-mms-header {
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: start;
|
|
14
|
+
justify-content: space-between;
|
|
15
|
+
gap: 16px;
|
|
16
|
+
border-bottom: 1px solid var(--bb-color-border);
|
|
17
|
+
padding-bottom: 14px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.bb-mms-header-copy {
|
|
21
|
+
min-width: 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.bb-mms-title {
|
|
25
|
+
margin: 0;
|
|
26
|
+
font-size: 1.2rem;
|
|
27
|
+
line-height: 1.35;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.bb-mms-status {
|
|
31
|
+
margin: 5px 0 0;
|
|
32
|
+
color: var(--bb-color-muted);
|
|
33
|
+
font-size: .82rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.bb-mms-body {
|
|
37
|
+
display: grid;
|
|
38
|
+
gap: 18px;
|
|
39
|
+
min-width: 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.bb-mms-facts {
|
|
43
|
+
display: grid;
|
|
44
|
+
grid-template-columns: minmax(110px, .4fr) minmax(0, 1fr);
|
|
45
|
+
gap: 9px 16px;
|
|
46
|
+
margin: 0;
|
|
47
|
+
min-width: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.bb-mms-facts dt {
|
|
51
|
+
color: var(--bb-color-muted);
|
|
52
|
+
font-size: .78rem;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.bb-mms-facts dd {
|
|
56
|
+
min-width: 0;
|
|
57
|
+
margin: 0;
|
|
58
|
+
overflow-wrap: anywhere;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.bb-mms-source {
|
|
62
|
+
display: grid;
|
|
63
|
+
gap: 3px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.bb-mms-source-provider {
|
|
67
|
+
font-weight: 680;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.bb-mms-source-locator,
|
|
71
|
+
.bb-mms-source-revision,
|
|
72
|
+
.bb-mms-source-digest {
|
|
73
|
+
overflow-wrap: anywhere;
|
|
74
|
+
color: var(--bb-color-muted);
|
|
75
|
+
font-size: .75rem;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.bb-mms-source-locator {
|
|
79
|
+
color: var(--bb-color-ink);
|
|
80
|
+
font-family: var(--bb-font-mono, ui-monospace, monospace);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.bb-mms-capabilities {
|
|
84
|
+
display: grid;
|
|
85
|
+
gap: 8px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.bb-mms-capabilities h3 {
|
|
89
|
+
margin: 0;
|
|
90
|
+
font-size: .88rem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.bb-mms-capability-list {
|
|
94
|
+
display: grid;
|
|
95
|
+
gap: 5px;
|
|
96
|
+
margin: 0;
|
|
97
|
+
padding-left: 18px;
|
|
98
|
+
color: var(--bb-color-muted);
|
|
99
|
+
font-size: .8rem;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.bb-mms-notice {
|
|
103
|
+
border-left: 3px solid var(--bb-color-warning);
|
|
104
|
+
margin: 0;
|
|
105
|
+
padding: 10px 13px;
|
|
106
|
+
background: var(--bb-color-warning-soft);
|
|
107
|
+
color: var(--bb-color-muted);
|
|
108
|
+
font-size: .8rem;
|
|
109
|
+
line-height: 1.55;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.bb-mms-status-denied .bb-mms-notice,
|
|
113
|
+
.bb-mms-status-unavailable .bb-mms-notice,
|
|
114
|
+
.bb-mms-status-historical_unavailable .bb-mms-notice,
|
|
115
|
+
.bb-mms-status-unconfirmed .bb-mms-notice {
|
|
116
|
+
border-left-color: var(--bb-color-danger);
|
|
117
|
+
background: var(--bb-color-danger-soft);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.bb-mms-placement-settings {
|
|
121
|
+
display: grid;
|
|
122
|
+
gap: 6px;
|
|
123
|
+
max-width: 420px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.bb-mms-placement-settings label {
|
|
127
|
+
display: grid;
|
|
128
|
+
gap: 6px;
|
|
129
|
+
color: var(--bb-color-muted);
|
|
130
|
+
font-size: .78rem;
|
|
131
|
+
font-weight: 680;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.bb-mms-placement-settings select {
|
|
135
|
+
min-height: 38px;
|
|
136
|
+
border: 1px solid var(--bb-color-control-border);
|
|
137
|
+
border-radius: var(--bb-radius-sm, 7px);
|
|
138
|
+
padding: 7px 10px;
|
|
139
|
+
background: var(--bb-color-panel);
|
|
140
|
+
color: var(--bb-color-ink);
|
|
141
|
+
font: inherit;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.bb-mms-placement-settings select:focus-visible,
|
|
145
|
+
.bb-mms-retry:focus-visible {
|
|
146
|
+
outline: 3px solid var(--bb-color-focus);
|
|
147
|
+
outline-offset: 2px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.bb-mms-retry {
|
|
151
|
+
justify-self: start;
|
|
152
|
+
min-height: 38px;
|
|
153
|
+
border: 1px solid var(--bb-color-control-border);
|
|
154
|
+
border-radius: var(--bb-radius-sm, 7px);
|
|
155
|
+
padding: 7px 12px;
|
|
156
|
+
background: var(--bb-color-panel);
|
|
157
|
+
color: var(--bb-color-ink);
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
font: inherit;
|
|
160
|
+
font-weight: 680;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.bb-mms-retry:hover {
|
|
164
|
+
border-color: var(--bb-color-border-strong);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.bb-mms [hidden] {
|
|
168
|
+
display: none !important;
|
|
169
|
+
}
|
|
170
|
+
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Read-only projection of the meeting-minutes storage policy.
|
|
3
|
+
*
|
|
4
|
+
* The host supplies the already-authorized controller result. This module
|
|
5
|
+
* never chooses a provider, fetches a source, or turns an unavailable source
|
|
6
|
+
* into an empty/successful body.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const MEETING_MINUTES_STORAGE_UI_CONTRACT_VERSION = 'brainbase.meeting-minutes-storage-ui.v1';
|
|
10
|
+
|
|
11
|
+
const PLACEMENT_LABELS = Object.freeze({
|
|
12
|
+
native: 'Brainbase内蔵',
|
|
13
|
+
external: '会社指定の外部保存先',
|
|
14
|
+
unknown: '未確認',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const STATUS_LABELS = Object.freeze({
|
|
18
|
+
native: 'Brainbase内蔵の本文',
|
|
19
|
+
available: '外部本文を取得済み',
|
|
20
|
+
denied: '外部本文の権限を確認できません',
|
|
21
|
+
unavailable: '外部保存先を取得できません',
|
|
22
|
+
historical_unavailable: '保存時点の外部版を取得できません',
|
|
23
|
+
unconfirmed: '保存先の状態は未確認です',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const STATUS_NOTICE = Object.freeze({
|
|
27
|
+
denied: '現在の組織権限では本文を読めません。権限があるように表示せず、管理者の設定を確認してください。',
|
|
28
|
+
unavailable: '外部ファイルが見つからないか、接続を確認できません。本文を代用して表示していません。',
|
|
29
|
+
historical_unavailable: '外部保存先に履歴機能がないため、保存時点の版を現在版で代用していません。',
|
|
30
|
+
unconfirmed: '保存先と権限の状態を確認できる応答がありません。',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const CAPABILITY_LABELS = Object.freeze({
|
|
34
|
+
read: '読み取り',
|
|
35
|
+
read_only: '読み取り専用',
|
|
36
|
+
save: '外部保存先への保存',
|
|
37
|
+
history: '履歴取得',
|
|
38
|
+
retention_delete: '保持・削除',
|
|
39
|
+
current_acl: '現在の権限',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function isObject(value) {
|
|
43
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function text(value) {
|
|
47
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function placement(value) {
|
|
51
|
+
return value === 'native' || value === 'external' ? value : 'unknown';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function status(value) {
|
|
55
|
+
return Object.hasOwn(STATUS_LABELS, value) ? value : 'unconfirmed';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sourceRef(value) {
|
|
59
|
+
if (!isObject(value)) return null;
|
|
60
|
+
const provider = text(value.provider);
|
|
61
|
+
const locator = text(value.locator);
|
|
62
|
+
const revision = text(value.revision);
|
|
63
|
+
const digest = text(value.digest);
|
|
64
|
+
if (!provider || !locator || !revision || !digest) return null;
|
|
65
|
+
return { provider, locator, revision, digest };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function capabilities(value) {
|
|
69
|
+
if (!isObject(value)) return null;
|
|
70
|
+
return {
|
|
71
|
+
read: value.read === true,
|
|
72
|
+
read_only: value.read_only === true,
|
|
73
|
+
save: value.save === true,
|
|
74
|
+
history: value.history === true,
|
|
75
|
+
retention_delete: value.retention_delete === true,
|
|
76
|
+
current_acl: text(value.current_acl) ?? '未確認',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Normalize the controller result without inventing a source or body. */
|
|
81
|
+
export function normalizeMeetingMinutesStoragePanel(value) {
|
|
82
|
+
const source = sourceRef(value?.source_ref ?? value?.sourceRef) ?? sourceRef(value?.version?.source_ref ?? value?.version?.sourceRef);
|
|
83
|
+
const normalizedPlacement = placement(value?.placement ?? (source ? 'external' : undefined));
|
|
84
|
+
const normalizedStatus = status(value?.source_status ?? value?.sourceStatus ?? (normalizedPlacement === 'native' ? 'native' : undefined));
|
|
85
|
+
const sourceResult = isObject(value?.source_result) ? value.source_result : null;
|
|
86
|
+
return Object.freeze({
|
|
87
|
+
contractVersion: MEETING_MINUTES_STORAGE_UI_CONTRACT_VERSION,
|
|
88
|
+
placement: normalizedPlacement,
|
|
89
|
+
placementLabel: PLACEMENT_LABELS[normalizedPlacement],
|
|
90
|
+
sourceStatus: normalizedStatus,
|
|
91
|
+
sourceStatusLabel: STATUS_LABELS[normalizedStatus],
|
|
92
|
+
sourceNotice: text(value?.reason ?? value?.sourceNotice) ?? STATUS_NOTICE[normalizedStatus] ?? null,
|
|
93
|
+
source,
|
|
94
|
+
capabilities: capabilities(value?.capabilities ?? sourceResult?.capabilities),
|
|
95
|
+
placementOptions: Array.isArray(value?.placement_options ?? value?.placementOptions)
|
|
96
|
+
? (value.placement_options ?? value.placementOptions).filter((item) => item === 'native' || item === 'external')
|
|
97
|
+
: ['native', 'external'],
|
|
98
|
+
versionId: text(value?.version?.version_id),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getDocument(explicit) {
|
|
103
|
+
const candidate = explicit ?? (typeof document === 'undefined' ? undefined : document);
|
|
104
|
+
if (!candidate || typeof candidate.createElement !== 'function') throw new Error('document_unavailable');
|
|
105
|
+
return candidate;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function makeElement(doc, tag, { className, text: value, attrs = {}, hidden, disabled } = {}) {
|
|
109
|
+
const element = doc.createElement(tag);
|
|
110
|
+
if (className) element.className = className;
|
|
111
|
+
if (value !== undefined && value !== null) element.textContent = String(value);
|
|
112
|
+
for (const [name, attrValue] of Object.entries(attrs)) {
|
|
113
|
+
if (attrValue === undefined || attrValue === null || attrValue === false) continue;
|
|
114
|
+
element.setAttribute(name, attrValue === true ? '' : String(attrValue));
|
|
115
|
+
}
|
|
116
|
+
if (hidden !== undefined) element.hidden = Boolean(hidden);
|
|
117
|
+
if (disabled !== undefined) element.disabled = Boolean(disabled);
|
|
118
|
+
return element;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function append(parent, ...children) {
|
|
122
|
+
parent.append(...children.filter((child) => child !== null && child !== undefined));
|
|
123
|
+
return parent;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function capabilityRows(doc, model) {
|
|
127
|
+
const values = model.capabilities;
|
|
128
|
+
if (!values) return [makeElement(doc, 'li', { className: 'bb-mms-capability is-unconfirmed', text: '機能: 未確認' })];
|
|
129
|
+
const rows = [
|
|
130
|
+
['read', values.read ? '利用可能' : '利用不可'],
|
|
131
|
+
['read_only', values.read_only ? '有効' : '無効'],
|
|
132
|
+
['save', values.save ? '利用可能' : '外部管理'],
|
|
133
|
+
['history', values.history ? '利用可能' : '未対応'],
|
|
134
|
+
['retention_delete', values.retention_delete ? '利用可能' : '外部管理'],
|
|
135
|
+
['current_acl', values.current_acl],
|
|
136
|
+
];
|
|
137
|
+
return rows.map(([key, value]) => makeElement(doc, 'li', {
|
|
138
|
+
className: `bb-mms-capability bb-mms-capability-${key}`,
|
|
139
|
+
text: `${CAPABILITY_LABELS[key]}: ${value}`,
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function render(model, { document: explicitDocument, onRetry, onPlacementChange } = {}) {
|
|
144
|
+
const doc = getDocument(explicitDocument);
|
|
145
|
+
const section = makeElement(doc, 'section', {
|
|
146
|
+
className: `bb-mms bb-mms-status-${model.sourceStatus}`,
|
|
147
|
+
attrs: {
|
|
148
|
+
'data-contract-version': MEETING_MINUTES_STORAGE_UI_CONTRACT_VERSION,
|
|
149
|
+
'aria-label': '議事録の保存先',
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
const heading = makeElement(doc, 'h2', { className: 'bb-mms-title', text: '議事録の保存先' });
|
|
153
|
+
const status = makeElement(doc, 'p', {
|
|
154
|
+
className: 'bb-mms-status',
|
|
155
|
+
attrs: { 'data-source-status': model.sourceStatus },
|
|
156
|
+
text: model.sourceStatusLabel,
|
|
157
|
+
});
|
|
158
|
+
const header = makeElement(doc, 'header', { className: 'bb-mms-header' });
|
|
159
|
+
append(header, makeElement(doc, 'div', { className: 'bb-mms-header-copy' }), status);
|
|
160
|
+
header.children[0].append(heading);
|
|
161
|
+
|
|
162
|
+
const placementLabel = makeElement(doc, 'dt', { text: '本文の正本' });
|
|
163
|
+
const placementValue = makeElement(doc, 'dd', { text: model.placementLabel });
|
|
164
|
+
const placementList = makeElement(doc, 'dl', { className: 'bb-mms-facts' });
|
|
165
|
+
append(placementList, placementLabel, placementValue);
|
|
166
|
+
|
|
167
|
+
if (model.source) {
|
|
168
|
+
const sourceLabel = makeElement(doc, 'dt', { text: '入力元' });
|
|
169
|
+
const sourceValue = makeElement(doc, 'dd', { className: 'bb-mms-source' });
|
|
170
|
+
append(
|
|
171
|
+
sourceValue,
|
|
172
|
+
makeElement(doc, 'span', { className: 'bb-mms-source-provider', text: model.source.provider }),
|
|
173
|
+
makeElement(doc, 'code', { className: 'bb-mms-source-locator', text: model.source.locator }),
|
|
174
|
+
makeElement(doc, 'small', { className: 'bb-mms-source-revision', text: `revision: ${model.source.revision}` }),
|
|
175
|
+
makeElement(doc, 'small', { className: 'bb-mms-source-digest', text: `digest: ${model.source.digest}` }),
|
|
176
|
+
);
|
|
177
|
+
append(placementList, sourceLabel, sourceValue);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const capabilitySection = makeElement(doc, 'section', {
|
|
181
|
+
className: 'bb-mms-capabilities',
|
|
182
|
+
attrs: { 'aria-label': '保存先の機能' },
|
|
183
|
+
});
|
|
184
|
+
append(capabilitySection,
|
|
185
|
+
makeElement(doc, 'h3', { text: '保存先の機能' }),
|
|
186
|
+
makeElement(doc, 'ul', { className: 'bb-mms-capability-list' }),
|
|
187
|
+
);
|
|
188
|
+
capabilitySection.children[1].append(...capabilityRows(doc, model));
|
|
189
|
+
|
|
190
|
+
const body = makeElement(doc, 'div', { className: 'bb-mms-body' });
|
|
191
|
+
append(body, placementList, capabilitySection);
|
|
192
|
+
|
|
193
|
+
if (model.sourceNotice && model.sourceStatus !== 'native' && model.sourceStatus !== 'available') {
|
|
194
|
+
append(body, makeElement(doc, 'p', {
|
|
195
|
+
className: 'bb-mms-notice',
|
|
196
|
+
attrs: { role: 'status', 'data-source-notice': model.sourceStatus },
|
|
197
|
+
text: model.sourceNotice,
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (model.placementOptions.length > 1) {
|
|
202
|
+
const settings = makeElement(doc, 'div', { className: 'bb-mms-placement-settings' });
|
|
203
|
+
const label = makeElement(doc, 'label', { text: '正本の保存形態' });
|
|
204
|
+
const select = makeElement(doc, 'select', { attrs: { name: 'canonical-placement' }, disabled: typeof onPlacementChange !== 'function' });
|
|
205
|
+
for (const option of model.placementOptions) {
|
|
206
|
+
const element = makeElement(doc, 'option', {
|
|
207
|
+
text: PLACEMENT_LABELS[option],
|
|
208
|
+
attrs: { value: option },
|
|
209
|
+
});
|
|
210
|
+
if (option === model.placement) element.selected = true;
|
|
211
|
+
select.append(element);
|
|
212
|
+
}
|
|
213
|
+
if (typeof onPlacementChange === 'function') {
|
|
214
|
+
select.addEventListener('change', () => onPlacementChange(select.value));
|
|
215
|
+
}
|
|
216
|
+
label.append(select);
|
|
217
|
+
settings.append(label);
|
|
218
|
+
body.append(settings);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (typeof onRetry === 'function' && model.sourceStatus !== 'native' && model.sourceStatus !== 'available') {
|
|
222
|
+
const retry = makeElement(doc, 'button', {
|
|
223
|
+
className: 'bb-mms-retry',
|
|
224
|
+
attrs: { type: 'button' },
|
|
225
|
+
text: 'もう一度確認',
|
|
226
|
+
});
|
|
227
|
+
retry.addEventListener('click', () => onRetry());
|
|
228
|
+
body.append(retry);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
append(section, header, body);
|
|
232
|
+
return section;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Create a browser view backed by a host-owned controller result. */
|
|
236
|
+
export function createMeetingMinutesStorageUI({
|
|
237
|
+
root,
|
|
238
|
+
view,
|
|
239
|
+
document: explicitDocument,
|
|
240
|
+
onRetry,
|
|
241
|
+
onPlacementChange,
|
|
242
|
+
autoRender = true,
|
|
243
|
+
} = {}) {
|
|
244
|
+
if (!root || typeof root.replaceChildren !== 'function') throw new TypeError('root is required');
|
|
245
|
+
let current = normalizeMeetingMinutesStoragePanel(view ?? {});
|
|
246
|
+
const update = (next) => {
|
|
247
|
+
current = normalizeMeetingMinutesStoragePanel(next ?? {});
|
|
248
|
+
root.replaceChildren(render(current, { document: explicitDocument, onRetry, onPlacementChange }));
|
|
249
|
+
return current;
|
|
250
|
+
};
|
|
251
|
+
if (autoRender) update(current);
|
|
252
|
+
return Object.freeze({
|
|
253
|
+
update,
|
|
254
|
+
get state() { return current; },
|
|
255
|
+
destroy() { root.replaceChildren(); },
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function sourceFromVersion(version) {
|
|
260
|
+
const source = version?.source_ref;
|
|
261
|
+
if (!isObject(source)) return null;
|
|
262
|
+
const provider = text(source.provider);
|
|
263
|
+
const locator = text(source.locator);
|
|
264
|
+
return provider && locator ? { provider, locator } : null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function storagePath(basePath, detail, documentRecord, version) {
|
|
268
|
+
return `/${encodeURIComponent(detail.meeting.meeting_id)}/minutes/${encodeURIComponent(documentRecord.minutes_id)}/versions/${encodeURIComponent(version.version_id)}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function rebindPath(detail, documentRecord) {
|
|
272
|
+
return `/${encodeURIComponent(detail.meeting.meeting_id)}/minutes/${encodeURIComponent(documentRecord.minutes_id)}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function failureStatus(error) {
|
|
276
|
+
const code = text(error?.code);
|
|
277
|
+
if (code === 'source_denied' || error?.status === 403) return 'denied';
|
|
278
|
+
if (code === 'source_changed' || code === 'historical_unavailable') return 'historical_unavailable';
|
|
279
|
+
return 'unavailable';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function clearExternalBody(bodyRoot) {
|
|
283
|
+
bodyRoot?.replaceChildren?.();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function renderExternalBody(bodyRoot, doc, value) {
|
|
287
|
+
clearExternalBody(bodyRoot);
|
|
288
|
+
if (!bodyRoot || !doc || value?.source_status !== 'available' || typeof value?.body !== 'string') return;
|
|
289
|
+
bodyRoot.append?.(makeElement(doc, 'pre', {
|
|
290
|
+
className: 'bb-minutes-body bb-minutes-external-body',
|
|
291
|
+
text: value.body,
|
|
292
|
+
attrs: { 'data-source-status': 'available' },
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Adds the host-owned storage projection to a selected minutes version.
|
|
298
|
+
* The browser supplies only a placement choice; root and authorization stay
|
|
299
|
+
* in the LocalWebHost configuration.
|
|
300
|
+
*/
|
|
301
|
+
export function createMeetingMinutesStorageExtension({
|
|
302
|
+
basePath = '/api/meeting-minutes',
|
|
303
|
+
defaultExternal = null,
|
|
304
|
+
} = {}) {
|
|
305
|
+
const normalizedBase = String(basePath).replace(/\/+$/u, '');
|
|
306
|
+
const configuredExternal = isObject(defaultExternal)
|
|
307
|
+
&& text(defaultExternal.provider) && text(defaultExternal.locator)
|
|
308
|
+
? { provider: text(defaultExternal.provider), locator: text(defaultExternal.locator) }
|
|
309
|
+
: null;
|
|
310
|
+
return Object.freeze({
|
|
311
|
+
id: 'meeting-minutes-storage',
|
|
312
|
+
mount({ root, bodyRoot, document: explicitDocument, detail, documentRecord, version, request, refresh, setStatus }) {
|
|
313
|
+
const source = sourceFromVersion(version);
|
|
314
|
+
const external = source ?? configuredExternal;
|
|
315
|
+
const initial = {
|
|
316
|
+
placement: source ? 'external' : 'native',
|
|
317
|
+
source_status: source ? 'unconfirmed' : 'native',
|
|
318
|
+
source_ref: version?.source_ref,
|
|
319
|
+
placement_options: external ? ['native', 'external'] : ['native'],
|
|
320
|
+
version: { version_id: version?.version_id, source_ref: version?.source_ref },
|
|
321
|
+
};
|
|
322
|
+
let panel;
|
|
323
|
+
const panelUpdate = (value) => panel?.update(value);
|
|
324
|
+
const call = typeof request === 'function' ? request : null;
|
|
325
|
+
const path = storagePath(normalizedBase, detail, documentRecord, version);
|
|
326
|
+
let requestSerial = 0;
|
|
327
|
+
const reload = async () => {
|
|
328
|
+
if (!call) return;
|
|
329
|
+
const serial = ++requestSerial;
|
|
330
|
+
clearExternalBody(bodyRoot);
|
|
331
|
+
setStatus?.('保存先を確認しています。');
|
|
332
|
+
try {
|
|
333
|
+
const value = await call(path, { headers: { Accept: 'application/json' } });
|
|
334
|
+
if (serial !== requestSerial) return;
|
|
335
|
+
panelUpdate(value);
|
|
336
|
+
renderExternalBody(bodyRoot, explicitDocument, value);
|
|
337
|
+
setStatus?.('保存先を確認しました。', 'success');
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (serial !== requestSerial) return;
|
|
340
|
+
clearExternalBody(bodyRoot);
|
|
341
|
+
panelUpdate({ ...initial, source_status: failureStatus(error), reason: error instanceof Error ? error.message : '保存先を確認できません。' });
|
|
342
|
+
setStatus?.(error instanceof Error ? error.message : '保存先を確認できません。', 'error');
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
const changePlacement = async (kind) => {
|
|
346
|
+
if (!call || (kind !== 'native' && kind !== 'external')) return;
|
|
347
|
+
++requestSerial;
|
|
348
|
+
clearExternalBody(bodyRoot);
|
|
349
|
+
const selectedExternal = source ?? configuredExternal;
|
|
350
|
+
if (kind === 'external' && !selectedExternal) {
|
|
351
|
+
const message = '会社指定の外部保存先がホストに設定されていません。';
|
|
352
|
+
panelUpdate({ ...initial, source_status: 'unavailable', reason: message });
|
|
353
|
+
setStatus?.(message, 'error');
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const placement = kind === 'native'
|
|
357
|
+
? { kind: 'native' }
|
|
358
|
+
: { kind: 'external', ...selectedExternal };
|
|
359
|
+
const body = { expected_revision: documentRecord.revision, placement };
|
|
360
|
+
const idempotency = `meeting-minutes-storage-${version.version_id}-${kind}`;
|
|
361
|
+
setStatus?.(kind === 'native' ? 'Brainbase内蔵へ切り替えています。' : '外部保存先へ切り替えています。');
|
|
362
|
+
try {
|
|
363
|
+
await call(rebindPath(detail, documentRecord), {
|
|
364
|
+
method: 'POST',
|
|
365
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'Idempotency-Key': idempotency },
|
|
366
|
+
body: JSON.stringify(body),
|
|
367
|
+
});
|
|
368
|
+
setStatus?.('保存先を変更しました。', 'success');
|
|
369
|
+
await refresh?.();
|
|
370
|
+
} catch (error) {
|
|
371
|
+
clearExternalBody(bodyRoot);
|
|
372
|
+
const message = error instanceof Error ? error.message : '保存先を変更できません。';
|
|
373
|
+
panelUpdate({ ...initial, source_status: failureStatus(error), reason: message });
|
|
374
|
+
setStatus?.(message, 'error');
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
panel = createMeetingMinutesStorageUI({
|
|
378
|
+
root,
|
|
379
|
+
document: explicitDocument,
|
|
380
|
+
view: initial,
|
|
381
|
+
onRetry: () => void reload(),
|
|
382
|
+
onPlacementChange: (kind) => void changePlacement(kind),
|
|
383
|
+
});
|
|
384
|
+
void reload();
|
|
385
|
+
return panel;
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
}
|