@oimdb/mcp 1.2.0 → 3.0.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/README.md +25 -129
- package/bin/oimdb-mcp.cjs +13 -14
- package/bin/server.cjs +10 -1
- package/dist/index.cjs +32 -305
- package/dist/index.d.cts +13 -65
- package/dist/index.d.ts +13 -65
- package/dist/index.js +37 -302
- package/package.json +15 -8
- package/bin/cdp.cjs +0 -98
- package/bin/devtools.cjs +0 -17
- package/ui/app.css +0 -323
- package/ui/app.js +0 -409
- package/ui/index.html +0 -63
package/ui/app.js
DELETED
|
@@ -1,409 +0,0 @@
|
|
|
1
|
-
/* OIMDB DevTools standalone UI — polls /api/inspect */
|
|
2
|
-
|
|
3
|
-
// ── State ──────────────────────────────────────────────────────
|
|
4
|
-
|
|
5
|
-
let autoTimer = null;
|
|
6
|
-
let expanded = new Set();
|
|
7
|
-
|
|
8
|
-
// ── Elements ───────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
const screenWaiting = document.getElementById('screen-waiting');
|
|
11
|
-
const screenMain = document.getElementById('screen-main');
|
|
12
|
-
const connDot = document.getElementById('conn-dot');
|
|
13
|
-
const connLabel = document.getElementById('conn-label');
|
|
14
|
-
const lastUpdated = document.getElementById('last-updated');
|
|
15
|
-
const badgeCollections = document.getElementById('badge-collections');
|
|
16
|
-
const badgeComputeds = document.getElementById('badge-computeds');
|
|
17
|
-
const listCollections = document.getElementById('list-collections');
|
|
18
|
-
const listComputeds = document.getElementById('list-computeds');
|
|
19
|
-
const listHistory = document.getElementById('list-history');
|
|
20
|
-
const btnRefresh = document.getElementById('btn-refresh');
|
|
21
|
-
const chkAuto = document.getElementById('chk-auto');
|
|
22
|
-
|
|
23
|
-
// ── Fetch ───────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
async function poll() {
|
|
26
|
-
try {
|
|
27
|
-
const base = location.origin !== 'null' ? location.origin : '';
|
|
28
|
-
const res = await fetch(base + '/api/inspect');
|
|
29
|
-
const json = await res.json();
|
|
30
|
-
|
|
31
|
-
if (!json.connected || !json.data) {
|
|
32
|
-
showWaiting();
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
showMain();
|
|
37
|
-
setConnected(json.tab);
|
|
38
|
-
render(json.data);
|
|
39
|
-
lastUpdated.textContent = new Date().toLocaleTimeString();
|
|
40
|
-
} catch {
|
|
41
|
-
showWaiting();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ── Layout ──────────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
function showWaiting() {
|
|
48
|
-
connDot.className = 'conn-dot disconnected';
|
|
49
|
-
connLabel.textContent = 'Waiting for browser…';
|
|
50
|
-
screenWaiting.classList.remove('hidden');
|
|
51
|
-
screenMain.classList.add('hidden');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function showMain() {
|
|
55
|
-
screenWaiting.classList.add('hidden');
|
|
56
|
-
screenMain.classList.remove('hidden');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function setConnected(tab) {
|
|
60
|
-
connDot.className = 'conn-dot connected';
|
|
61
|
-
connLabel.textContent = tab ? tab.title : 'Connected';
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ── Render ──────────────────────────────────────────────────────
|
|
65
|
-
|
|
66
|
-
function render(state) {
|
|
67
|
-
const collections = state.collections || {};
|
|
68
|
-
const computeds = state.computeds || {};
|
|
69
|
-
const history = state.history || [];
|
|
70
|
-
|
|
71
|
-
badgeCollections.textContent = Object.keys(collections).length;
|
|
72
|
-
badgeComputeds.textContent = Object.keys(computeds).length;
|
|
73
|
-
|
|
74
|
-
renderCollections(collections);
|
|
75
|
-
renderComputeds(computeds);
|
|
76
|
-
renderHistory(history);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ── Collections ─────────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
function renderCollections(cols) {
|
|
82
|
-
const entries = Object.entries(cols);
|
|
83
|
-
if (!entries.length) {
|
|
84
|
-
listCollections.innerHTML = '<div class="empty">No collections registered.</div>';
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
listCollections.innerHTML = '';
|
|
88
|
-
entries.forEach(([name, info]) => listCollections.appendChild(buildCollRow(name, info)));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function buildCollRow(name, info) {
|
|
92
|
-
const row = document.createElement('div');
|
|
93
|
-
row.className = 'c-row';
|
|
94
|
-
|
|
95
|
-
const isOpen = expanded.has(name);
|
|
96
|
-
|
|
97
|
-
const summary = document.createElement('div');
|
|
98
|
-
summary.className = 'c-summary';
|
|
99
|
-
|
|
100
|
-
const arrow = document.createElement('span');
|
|
101
|
-
arrow.className = 'c-arrow' + (isOpen ? ' open' : '');
|
|
102
|
-
arrow.textContent = '▶';
|
|
103
|
-
|
|
104
|
-
const nameEl = document.createElement('span');
|
|
105
|
-
nameEl.className = 'c-name';
|
|
106
|
-
nameEl.textContent = name;
|
|
107
|
-
|
|
108
|
-
const count = document.createElement('span');
|
|
109
|
-
count.className = 'c-count';
|
|
110
|
-
count.textContent = `${info.count} entities`;
|
|
111
|
-
|
|
112
|
-
summary.append(arrow, nameEl, count);
|
|
113
|
-
|
|
114
|
-
const detail = document.createElement('div');
|
|
115
|
-
detail.className = 'c-detail' + (isOpen ? ' open' : '');
|
|
116
|
-
detail.innerHTML = buildCollMeta(info);
|
|
117
|
-
|
|
118
|
-
// Entity table
|
|
119
|
-
const table = buildEntityTable(info);
|
|
120
|
-
if (table) detail.appendChild(table);
|
|
121
|
-
|
|
122
|
-
summary.addEventListener('click', () => {
|
|
123
|
-
const opening = detail.classList.toggle('open');
|
|
124
|
-
arrow.classList.toggle('open', opening);
|
|
125
|
-
if (opening) expanded.add(name);
|
|
126
|
-
else expanded.delete(name);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
row.append(summary, detail);
|
|
130
|
-
return row;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function buildCollMeta(info) {
|
|
134
|
-
const parts = [];
|
|
135
|
-
|
|
136
|
-
if (info.sampleEntity && typeof info.sampleEntity === 'object') {
|
|
137
|
-
const tags = Object.entries(info.sampleEntity)
|
|
138
|
-
.map(([k, v]) => `<span class="tag">${esc(k)}: <span style="opacity:.6">${esc(typeof v)}</span></span>`)
|
|
139
|
-
.join('');
|
|
140
|
-
parts.push(drow('fields', tags));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const idxEntries = Object.entries(info.indexes || {});
|
|
144
|
-
if (idxEntries.length) {
|
|
145
|
-
const tags = idxEntries.map(([n, idx]) =>
|
|
146
|
-
`<span class="tag">${esc(n)} <span style="opacity:.6">(${idx.keyCount})</span></span>`
|
|
147
|
-
).join('');
|
|
148
|
-
parts.push(drow('indexes', tags));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const relEntries = Object.entries(info.relations || {});
|
|
152
|
-
if (relEntries.length) {
|
|
153
|
-
const tags = relEntries.map(([f, t]) =>
|
|
154
|
-
`<span class="tag">${esc(f)}<span class="tag-rel-arrow">→</span>${esc(t)}</span>`
|
|
155
|
-
).join('');
|
|
156
|
-
parts.push(drow('relations', tags));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (info.description) {
|
|
160
|
-
parts.push(drow('desc', `<span style="color:var(--text-dim)">${esc(info.description)}</span>`));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return parts.join('') || '';
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ── Entity table ────────────────────────────────────────────────
|
|
167
|
-
|
|
168
|
-
const MAX_ENTITY_ROWS = 200;
|
|
169
|
-
|
|
170
|
-
function buildEntityTable(info) {
|
|
171
|
-
const entities = info.entities;
|
|
172
|
-
if (!Array.isArray(entities) || entities.length === 0) return null;
|
|
173
|
-
|
|
174
|
-
const first = entities[0];
|
|
175
|
-
if (!first || typeof first !== 'object') return null;
|
|
176
|
-
const cols = Object.keys(first);
|
|
177
|
-
if (cols.length === 0) return null;
|
|
178
|
-
|
|
179
|
-
const shown = entities.slice(0, MAX_ENTITY_ROWS);
|
|
180
|
-
|
|
181
|
-
const wrap = document.createElement('div');
|
|
182
|
-
wrap.className = 'entity-wrap';
|
|
183
|
-
|
|
184
|
-
const tableWrap = document.createElement('div');
|
|
185
|
-
tableWrap.className = 'entity-table-wrap';
|
|
186
|
-
|
|
187
|
-
const table = document.createElement('table');
|
|
188
|
-
table.className = 'entity-table';
|
|
189
|
-
|
|
190
|
-
// Head
|
|
191
|
-
const thead = document.createElement('thead');
|
|
192
|
-
const headRow = document.createElement('tr');
|
|
193
|
-
cols.forEach(col => {
|
|
194
|
-
const th = document.createElement('th');
|
|
195
|
-
th.textContent = col;
|
|
196
|
-
headRow.appendChild(th);
|
|
197
|
-
});
|
|
198
|
-
thead.appendChild(headRow);
|
|
199
|
-
table.appendChild(thead);
|
|
200
|
-
|
|
201
|
-
// Body
|
|
202
|
-
const tbody = document.createElement('tbody');
|
|
203
|
-
shown.forEach(entity => {
|
|
204
|
-
const tr = document.createElement('tr');
|
|
205
|
-
cols.forEach(col => {
|
|
206
|
-
const td = document.createElement('td');
|
|
207
|
-
const val = (entity)[col];
|
|
208
|
-
td.textContent = fmtCell(val);
|
|
209
|
-
td.title = JSON.stringify(val);
|
|
210
|
-
tr.appendChild(td);
|
|
211
|
-
});
|
|
212
|
-
tbody.appendChild(tr);
|
|
213
|
-
});
|
|
214
|
-
table.appendChild(tbody);
|
|
215
|
-
tableWrap.appendChild(table);
|
|
216
|
-
wrap.appendChild(tableWrap);
|
|
217
|
-
|
|
218
|
-
if (entities.length > MAX_ENTITY_ROWS) {
|
|
219
|
-
const note = document.createElement('div');
|
|
220
|
-
note.className = 'entity-note';
|
|
221
|
-
note.textContent = `showing ${MAX_ENTITY_ROWS} of ${entities.length}`;
|
|
222
|
-
wrap.appendChild(note);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return wrap;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function fmtCell(v) {
|
|
229
|
-
if (v === null || v === undefined) return '—';
|
|
230
|
-
if (typeof v === 'object') {
|
|
231
|
-
const s = JSON.stringify(v);
|
|
232
|
-
return s.length > 40 ? s.slice(0, 40) + '…' : s;
|
|
233
|
-
}
|
|
234
|
-
const s = String(v);
|
|
235
|
-
return s.length > 60 ? s.slice(0, 60) + '…' : s;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function drow(label, content) {
|
|
239
|
-
return `<div class="d-row"><span class="d-label">${esc(label)}</span>${content}</div>`;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ── Computeds ───────────────────────────────────────────────────
|
|
243
|
-
|
|
244
|
-
function renderComputeds(comps) {
|
|
245
|
-
const entries = Object.entries(comps);
|
|
246
|
-
if (!entries.length) {
|
|
247
|
-
listComputeds.innerHTML = '<div class="empty">No computeds registered.</div>';
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
listComputeds.innerHTML = '';
|
|
251
|
-
entries.forEach(([name, info]) => listComputeds.appendChild(buildCompRow(name, info)));
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function buildCompRow(name, info) {
|
|
255
|
-
const row = document.createElement('div');
|
|
256
|
-
row.className = 'comp-row';
|
|
257
|
-
|
|
258
|
-
let cls, label;
|
|
259
|
-
if (!info.isReady) {
|
|
260
|
-
cls = 'pending'; label = 'PENDING';
|
|
261
|
-
} else if (info.needsRecompute) {
|
|
262
|
-
cls = 'stale'; label = 'STALE';
|
|
263
|
-
} else {
|
|
264
|
-
cls = 'fresh'; label = 'FRESH';
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const top = document.createElement('div');
|
|
268
|
-
top.className = 'comp-top';
|
|
269
|
-
|
|
270
|
-
const dot = document.createElement('span');
|
|
271
|
-
dot.className = `status-dot ${cls}`;
|
|
272
|
-
|
|
273
|
-
const nameEl = document.createElement('span');
|
|
274
|
-
nameEl.className = 'comp-name';
|
|
275
|
-
nameEl.textContent = name;
|
|
276
|
-
|
|
277
|
-
const pill = document.createElement('span');
|
|
278
|
-
pill.className = `status-pill ${cls}`;
|
|
279
|
-
pill.textContent = label;
|
|
280
|
-
|
|
281
|
-
top.append(dot, nameEl, pill);
|
|
282
|
-
|
|
283
|
-
if (info.isReady && info.currentValue !== undefined) {
|
|
284
|
-
const val = document.createElement('span');
|
|
285
|
-
val.className = 'comp-value';
|
|
286
|
-
const prefix = info.needsRecompute ? 'last: ' : '';
|
|
287
|
-
val.textContent = prefix + fmt(info.currentValue);
|
|
288
|
-
val.title = JSON.stringify(info.currentValue, null, 2);
|
|
289
|
-
top.appendChild(val);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
row.appendChild(top);
|
|
293
|
-
|
|
294
|
-
const deps = info.deps || [];
|
|
295
|
-
if (deps.length) {
|
|
296
|
-
const depsEl = document.createElement('div');
|
|
297
|
-
depsEl.className = 'comp-deps';
|
|
298
|
-
|
|
299
|
-
const lbl = document.createElement('span');
|
|
300
|
-
lbl.className = 'comp-deps-label';
|
|
301
|
-
lbl.textContent = 'deps:';
|
|
302
|
-
depsEl.appendChild(lbl);
|
|
303
|
-
|
|
304
|
-
deps.forEach(dep => {
|
|
305
|
-
const tag = document.createElement('span');
|
|
306
|
-
if (dep.name) {
|
|
307
|
-
tag.className = 'tag';
|
|
308
|
-
tag.textContent = dep.name;
|
|
309
|
-
} else {
|
|
310
|
-
tag.className = 'tag tag-warn';
|
|
311
|
-
tag.textContent = '⚠ unregistered';
|
|
312
|
-
tag.title = 'Register this source in your debug file';
|
|
313
|
-
}
|
|
314
|
-
depsEl.appendChild(tag);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
row.appendChild(depsEl);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
return row;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// ── Flush History ───────────────────────────────────────────────
|
|
324
|
-
|
|
325
|
-
function renderHistory(history) {
|
|
326
|
-
if (!history.length) {
|
|
327
|
-
listHistory.innerHTML = '<div class="empty">No flushes recorded yet.<br>Call <code>registry.trackFlushes(...)</code> in debug.ts.</div>';
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
listHistory.innerHTML = '';
|
|
331
|
-
history.forEach((record, i) => {
|
|
332
|
-
const prev = history[i + 1];
|
|
333
|
-
listHistory.appendChild(buildFlushRow(record, prev));
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function buildFlushRow(record, prev) {
|
|
338
|
-
const row = document.createElement('div');
|
|
339
|
-
row.className = 'flush-row';
|
|
340
|
-
|
|
341
|
-
const time = document.createElement('span');
|
|
342
|
-
time.className = 'flush-time';
|
|
343
|
-
time.textContent = new Date(record.time).toLocaleTimeString('en', {
|
|
344
|
-
hour12: false,
|
|
345
|
-
hour: '2-digit',
|
|
346
|
-
minute: '2-digit',
|
|
347
|
-
second: '2-digit',
|
|
348
|
-
fractionalSecondDigits: 3,
|
|
349
|
-
});
|
|
350
|
-
row.appendChild(time);
|
|
351
|
-
|
|
352
|
-
const tags = document.createElement('span');
|
|
353
|
-
tags.className = 'flush-tags';
|
|
354
|
-
|
|
355
|
-
for (const [name, count] of Object.entries(record.counts)) {
|
|
356
|
-
const prevCount = prev ? (prev.counts[name] ?? 0) : 0;
|
|
357
|
-
const diff = count - prevCount;
|
|
358
|
-
const tag = document.createElement('span');
|
|
359
|
-
tag.className = 'flush-tag';
|
|
360
|
-
|
|
361
|
-
if (diff === 0 && i !== 0) continue; // skip unchanged (unless first flush)
|
|
362
|
-
const sign = diff > 0 ? '+' : '';
|
|
363
|
-
tag.textContent = diff !== 0 ? `${name} ${sign}${diff}` : `${name} ${count}`;
|
|
364
|
-
tag.className = 'flush-tag' + (diff > 0 ? ' flush-add' : diff < 0 ? ' flush-del' : '');
|
|
365
|
-
tags.appendChild(tag);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (!tags.children.length) {
|
|
369
|
-
const tag = document.createElement('span');
|
|
370
|
-
tag.className = 'flush-tag flush-no-change';
|
|
371
|
-
tag.textContent = 'no changes';
|
|
372
|
-
tags.appendChild(tag);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
row.appendChild(tags);
|
|
376
|
-
return row;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// ── Helpers ─────────────────────────────────────────────────────
|
|
380
|
-
|
|
381
|
-
function esc(s) {
|
|
382
|
-
return String(s)
|
|
383
|
-
.replace(/&/g, '&')
|
|
384
|
-
.replace(/</g, '<')
|
|
385
|
-
.replace(/>/g, '>');
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function fmt(v) {
|
|
389
|
-
if (v === null) return 'null';
|
|
390
|
-
if (typeof v === 'string') return `"${v}"`;
|
|
391
|
-
if (typeof v === 'object') {
|
|
392
|
-
const s = JSON.stringify(v);
|
|
393
|
-
return s.length > 48 ? s.slice(0, 48) + '…' : s;
|
|
394
|
-
}
|
|
395
|
-
return String(v);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// ── Auto-refresh ─────────────────────────────────────────────────
|
|
399
|
-
|
|
400
|
-
function startAuto() { stopAuto(); autoTimer = setInterval(poll, 1000); }
|
|
401
|
-
function stopAuto() { if (autoTimer !== null) { clearInterval(autoTimer); autoTimer = null; } }
|
|
402
|
-
|
|
403
|
-
// ── Boot ────────────────────────────────────────────────────────
|
|
404
|
-
|
|
405
|
-
btnRefresh.addEventListener('click', poll);
|
|
406
|
-
chkAuto.addEventListener('change', () => chkAuto.checked ? startAuto() : stopAuto());
|
|
407
|
-
|
|
408
|
-
poll();
|
|
409
|
-
startAuto();
|
package/ui/index.html
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>OIMDB DevTools</title>
|
|
7
|
-
<link rel="stylesheet" href="app.css">
|
|
8
|
-
</head>
|
|
9
|
-
<body>
|
|
10
|
-
<header id="toolbar">
|
|
11
|
-
<div class="toolbar-brand">
|
|
12
|
-
<span class="brand-name">OIMDB</span>
|
|
13
|
-
<span class="brand-sub">DevTools</span>
|
|
14
|
-
</div>
|
|
15
|
-
<div class="toolbar-status">
|
|
16
|
-
<span id="conn-dot" class="conn-dot"></span>
|
|
17
|
-
<span id="conn-label">Waiting for browser…</span>
|
|
18
|
-
</div>
|
|
19
|
-
<div class="toolbar-actions">
|
|
20
|
-
<span id="last-updated"></span>
|
|
21
|
-
<button id="btn-refresh" title="Refresh now">⟳</button>
|
|
22
|
-
<label class="auto-label">
|
|
23
|
-
<input type="checkbox" id="chk-auto" checked>
|
|
24
|
-
Auto
|
|
25
|
-
</label>
|
|
26
|
-
</div>
|
|
27
|
-
</header>
|
|
28
|
-
|
|
29
|
-
<div id="screen-waiting">
|
|
30
|
-
<div class="waiting-card">
|
|
31
|
-
<div class="waiting-title">Waiting for browser</div>
|
|
32
|
-
<p>Open your app and make sure <code>debug.ts</code> calls:</p>
|
|
33
|
-
<pre>registry.connect(); // ws://localhost:7432</pre>
|
|
34
|
-
<p class="waiting-hint">Then reload the page.</p>
|
|
35
|
-
</div>
|
|
36
|
-
</div>
|
|
37
|
-
|
|
38
|
-
<div id="screen-main" class="hidden">
|
|
39
|
-
<div id="col-left" class="col">
|
|
40
|
-
<div class="col-header">
|
|
41
|
-
Collections
|
|
42
|
-
<span class="badge" id="badge-collections">0</span>
|
|
43
|
-
</div>
|
|
44
|
-
<div id="list-collections" class="col-body"></div>
|
|
45
|
-
</div>
|
|
46
|
-
<div id="col-right" class="col">
|
|
47
|
-
<div class="col-section">
|
|
48
|
-
<div class="col-header">
|
|
49
|
-
Computeds
|
|
50
|
-
<span class="badge" id="badge-computeds">0</span>
|
|
51
|
-
</div>
|
|
52
|
-
<div id="list-computeds" class="col-body col-body-flex"></div>
|
|
53
|
-
</div>
|
|
54
|
-
<div class="col-section col-section-history">
|
|
55
|
-
<div class="col-header">Flushes</div>
|
|
56
|
-
<div id="list-history" class="col-body col-body-flex"></div>
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
<script src="app.js"></script>
|
|
62
|
-
</body>
|
|
63
|
-
</html>
|