json-object-editor 0.10.509 → 0.10.521
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/CHANGELOG.md +35 -0
- package/_www/mcp-export.html +11 -4
- package/_www/mcp-nav.js +8 -4
- package/_www/mcp-prompt.html +96 -121
- package/_www/mcp-schemas.html +294 -0
- package/_www/mcp-test.html +86 -0
- package/docs/JOE_Master_Knowledge_Export.md +135 -0
- package/docs/joe_agent_custom_gpt_instructions_v_2.md +54 -0
- package/docs/joe_agent_spec_v_2.2.md +64 -0
- package/docs/schema_summary_guidelines.md +128 -0
- package/package.json +1 -1
- package/readme.md +525 -469
- package/server/modules/MCP.js +606 -405
- package/server/modules/Schemas.js +321 -111
- package/server/modules/Server.js +26 -15
- package/server/modules/Storage.js +9 -0
- package/server/relationships.graph.json +5 -0
- package/server/schemas/block.js +37 -0
- package/server/schemas/board.js +2 -1
- package/server/schemas/budget.js +28 -1
- package/server/schemas/event.js +42 -0
- package/server/schemas/financial_account.js +35 -0
- package/server/schemas/goal.js +30 -0
- package/server/schemas/group.js +31 -0
- package/server/schemas/include.js +28 -0
- package/server/schemas/ingredient.js +28 -0
- package/server/schemas/initiative.js +32 -0
- package/server/schemas/instance.js +31 -1
- package/server/schemas/layout.js +31 -0
- package/server/schemas/ledger.js +30 -0
- package/server/schemas/list.js +33 -0
- package/server/schemas/meal.js +30 -0
- package/server/schemas/note.js +30 -0
- package/server/schemas/notification.js +33 -1
- package/server/schemas/page.js +43 -0
- package/server/schemas/post.js +32 -0
- package/server/schemas/project.js +36 -0
- package/server/schemas/recipe.js +32 -0
- package/server/schemas/report.js +32 -0
- package/server/schemas/setting.js +22 -0
- package/server/schemas/site.js +30 -0
- package/server/schemas/status.js +33 -0
- package/server/schemas/tag.js +28 -1
- package/server/schemas/task.js +778 -737
- package/server/schemas/transaction.js +43 -0
- package/server/schemas/user.js +36 -1
- package/server/schemas/workflow.js +30 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
+
## 0.10.521
|
|
2
|
+
- MCP
|
|
3
|
+
- Added `saveObjects` batch save tool (bounded concurrency, stopOnError). Updated MCP Test preset and Export starter instructions.
|
|
4
|
+
|
|
5
|
+
## 0.10.520
|
|
6
|
+
- added mcp schemas page
|
|
7
|
+
- summarized core schemas
|
|
8
|
+
- updated schema endpoint and mcp to have summaries
|
|
9
|
+
- prompt added to docs to summarize schemas
|
|
10
|
+
|
|
11
|
+
- Schemas Summary engine
|
|
12
|
+
- Propagates field `display`, `comment`, `tooltip` into summaries; truncates long meta at 160 chars
|
|
13
|
+
- Merges curated `summary.fields` by name (no longer replaces auto list)
|
|
14
|
+
- Treats `{extend:'field'}` as a real field and overlays core field specs
|
|
15
|
+
- Adds `fieldCount` and `fieldCountBase` to summaries for coverage checks
|
|
16
|
+
|
|
17
|
+
- Schemas Health page
|
|
18
|
+
- New Outbound column (field → schema)
|
|
19
|
+
- New Field Check column showing summary coverage (summary/base)
|
|
20
|
+
- App filter dropdown; robust row rendering; safe SVG; centered numeric/boolean columns; icon first; sortable preserved
|
|
21
|
+
|
|
22
|
+
- Hydrate & Docs
|
|
23
|
+
- Hydrate already includes `schemaSummary` (+ timestamp); added date‑stamped hydrate download to MCP Prompt
|
|
24
|
+
- MCP Prompt now loads system instructions from `docs/joe_agent_custom_gpt_instructions_v_2.md`
|
|
25
|
+
- Added docs: `joe_agent_spec_v_2.2.md`, `schema_summary_guidelines.md`
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
- Summaries added/refined
|
|
30
|
+
- Sitebuilder: `site`, `page`, `layout`, `block`, `include`, `post`
|
|
31
|
+
- Finance: `transaction`, `ledger`, `financial_account`, `budget` (additive clarified)
|
|
32
|
+
- Mealplanner: `recipe`, `meal`, `ingredient`
|
|
33
|
+
- Platform: `initiative`, `instance`, and `user` (added `styles` and `bio`)
|
|
34
|
+
|
|
35
|
+
|
|
1
36
|
## 0.10.509
|
|
2
37
|
- Dependency: Re-added `got` package to dependencies in `package.json` (needed for plugin auth module compatibility)
|
|
3
38
|
- MCP: Updated the base prompt for agents in `/mcp-prompt.html` for improved clarity and operational guidance.
|
package/_www/mcp-export.html
CHANGED
|
@@ -118,7 +118,13 @@
|
|
|
118
118
|
|
|
119
119
|
const schema = {
|
|
120
120
|
openapi: '3.1.0',
|
|
121
|
-
|
|
121
|
+
// Callout to JOE version for quick reference
|
|
122
|
+
'x-joeVersion': (manifest.joe && manifest.joe.version) || '',
|
|
123
|
+
info: {
|
|
124
|
+
title: 'JOE MCP Bridge — ' + ((manifest.joe&&manifest.joe.name)||'JOE'),
|
|
125
|
+
version: '1.0.0',
|
|
126
|
+
description: 'Generated for JOE ' + (((manifest.joe&&manifest.joe.version)||'').toString() || '')
|
|
127
|
+
},
|
|
122
128
|
servers: [{ url: serverUrl.replace(/\/$/,'') }],
|
|
123
129
|
paths: {
|
|
124
130
|
'/mcp': {
|
|
@@ -191,19 +197,20 @@
|
|
|
191
197
|
'- Ask one brief clarification only if a required parameter is missing.',
|
|
192
198
|
'- On a new session: call hydrate {} first, then proceed.',
|
|
193
199
|
'- Keep results scoped (limit 10–25). Flatten is optional and off by default; enable only when needed.',
|
|
194
|
-
'- Never expose secrets/tokens. Confirm with the user before saveObject.',
|
|
200
|
+
'- Never expose secrets/tokens. Confirm with the user before saveObject/saveObjects.',
|
|
195
201
|
'',
|
|
196
202
|
'Typical flow:',
|
|
197
203
|
'- listSchemas {}, getSchema { "name": "<schema>" }',
|
|
198
204
|
'- search { "query": { "itemtype": "<schema>" }, "limit": 10 } (cache) or { "source": "storage" } when authoritative results are needed',
|
|
199
205
|
'- getObject { "_id": "<id>", "schema": "<schema>" } for a single item',
|
|
200
|
-
'- saveObject { "object": { ... } } only on explicit user request',
|
|
206
|
+
'- saveObject { "object": { ... } } or saveObjects { "objects": [ ... ], "concurrency": 5 } only on explicit user request',
|
|
201
207
|
'',
|
|
202
208
|
'Examples:',
|
|
203
209
|
'- listSchemas {}',
|
|
204
210
|
'- getSchema { "name": "client" }',
|
|
205
211
|
'- search { "schema": "client", "source": "storage", "query": { "status": "active" }, "limit": 10 }',
|
|
206
|
-
'- getObject { "_id": "123", "schema": "client" }'
|
|
212
|
+
'- getObject { "_id": "123", "schema": "client" }',
|
|
213
|
+
'- saveObjects { "objects": [{ "itemtype":"client", "name":"Batch A" }], "stopOnError": false, "concurrency": 5 }'
|
|
207
214
|
].join('\n');
|
|
208
215
|
$('starter').value = starter;
|
|
209
216
|
|
package/_www/mcp-nav.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
;(function(){
|
|
2
2
|
function buildNav(){
|
|
3
3
|
var nav = document.createElement('nav');
|
|
4
|
-
nav.setAttribute('style','display:flex;
|
|
5
|
-
nav
|
|
4
|
+
nav.setAttribute('style','display:flex;flex-direction:column;gap:6px;margin-bottom:8px');
|
|
5
|
+
var infoRow = '<div id="mcp-nav-info" class="small" style="opacity:.8"></div>';
|
|
6
|
+
var linksRow = [
|
|
7
|
+
'<div style="display:flex;gap:10px;align-items:center">',
|
|
6
8
|
'<a href="/mcp-test.html" target="mcp_test_win" rel="noopener">MCP Test</a>',
|
|
7
9
|
'<a href="/mcp-export.html" target="mcp_export_win" rel="noopener">MCP Export</a>',
|
|
10
|
+
'<a href="/mcp-schemas.html" target="mcp_schemas_win" rel="noopener">Schemas</a>',
|
|
8
11
|
'<a href="/mcp-prompt.html" target="mcp_prompt_win" rel="noopener">MCP Prompt</a>',
|
|
9
12
|
'<span style="margin-left:auto"></span>',
|
|
10
|
-
'<span id="mcp-nav-info" class="small" style="opacity:.8;margin-right:10px"></span>',
|
|
11
13
|
'<a href="/privacy" target="privacy_win" rel="noopener">Privacy</a>',
|
|
12
|
-
'<a href="/terms" target="terms_win" rel="noopener">Terms</a>'
|
|
14
|
+
'<a href="/terms" target="terms_win" rel="noopener">Terms</a>',
|
|
15
|
+
'</div>'
|
|
13
16
|
].join('');
|
|
17
|
+
nav.innerHTML = infoRow + linksRow;
|
|
14
18
|
return nav;
|
|
15
19
|
}
|
|
16
20
|
function insert(){
|
package/_www/mcp-prompt.html
CHANGED
|
@@ -14,128 +14,103 @@
|
|
|
14
14
|
<div id="mcp-nav"></div>
|
|
15
15
|
<script src="/JsonObjectEditor/_www/mcp-nav.js"></script>
|
|
16
16
|
<h1>Starter Agent Instructions</h1>
|
|
17
|
-
<div class="small">Copy into your Custom GPT or Assistant system prompt.
|
|
18
|
-
<textarea readonly id="prompt">You are a schema-aware data assistant for JOE. Use only the provided tools.
|
|
17
|
+
<div class="small">Copy into your Custom GPT or Assistant system prompt. This text is loaded from docs/joe_agent_custom_gpt_instructions_v_2.md (source of truth).</div>
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
Explicit Confirmation for Writes
|
|
116
|
-
Any operation that modifies or creates data must be confirmed by the user before execution. This includes saveObject or any update or creation that writes to storage. Before saving, validate the object against its schema and reflect it back to the user for confirmation.
|
|
117
|
-
|
|
118
|
-
Fail-Safe Execution
|
|
119
|
-
If a tool call fails, retry safely by reducing depth or limit, or switching to cache. Never guess or infer field values or relationships. Always prioritize data integrity over speed for write operations.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
FULL OBJECT SAVE REQUIREMENT
|
|
123
|
-
|
|
124
|
-
When performing a saveObject operation, always include the complete object rather than only the updated fields. Partial updates are not supported by the JOE MCP interface and will fail validation.
|
|
125
|
-
|
|
126
|
-
⚙️ DO NOT perform a saveObject operation unless the following have occurred in order:
|
|
127
|
-
1. A getObject call for the item’s _id and schema from storage (not cache).
|
|
128
|
-
2. The retrieved object is used as the base object.
|
|
129
|
-
3. The agent merges changes into that full object.
|
|
130
|
-
4. The joeUpdated field is refreshed to the current UTC timestamp (ISO 8601 with milliseconds).
|
|
131
|
-
5. The agent validates all field types against schemaMap.
|
|
132
|
-
6. The agent reflects the complete merged object to the user for confirmation.
|
|
133
|
-
7. Only then execute saveObject with the complete, validated object.
|
|
134
|
-
|
|
135
|
-
If any of the above steps are skipped, throw an internal error (“JOE Save Policy Violation”) and stop execution.
|
|
136
|
-
|
|
137
|
-
Why this matters:
|
|
138
|
-
The JOE MCP interface replaces the full document on save rather than applying a patch. If required fields or key metadata (such as itemtype or project references) are missing, the record can become invalid or incomplete. Full-object saves guarantee schema integrity, consistent relationships, and a complete audit trail.</textarea>
|
|
19
|
+
<h3>Downloads</h3>
|
|
20
|
+
<div class="small">Quickly export JSON helpful for agent setup and offline review.</div>
|
|
21
|
+
<div class="row" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin:8px 0 16px;">
|
|
22
|
+
<label for="base" style="margin:0">Base URL</label>
|
|
23
|
+
<input id="base" value="" placeholder="http://localhost:2025" style="min-width:280px"/>
|
|
24
|
+
<button id="downloadApps">Download apps.json</button>
|
|
25
|
+
<button id="downloadHydrate">Download hydrate-<date>.json</button>
|
|
26
|
+
<span id="dlStatus" class="small"></span>
|
|
27
|
+
</div>
|
|
28
|
+
<textarea readonly id="prompt" placeholder="Loading instructions from docs…"></textarea>
|
|
29
|
+
|
|
30
|
+
<script>
|
|
31
|
+
(function(){
|
|
32
|
+
var $ = function(id){ return document.getElementById(id); };
|
|
33
|
+
var base = $('base');
|
|
34
|
+
var dlBtn = $('downloadApps');
|
|
35
|
+
var dlHydrateBtn = $('downloadHydrate');
|
|
36
|
+
var dlStatus = $('dlStatus');
|
|
37
|
+
var promptBox = $('prompt');
|
|
38
|
+
base.value = base.value || location.origin;
|
|
39
|
+
|
|
40
|
+
function setStatus(msg, ok){
|
|
41
|
+
dlStatus.textContent = msg||'';
|
|
42
|
+
dlStatus.className = 'small ' + (ok===true?'good': ok===false?'bad':'');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function fetchJSON(url, opts){
|
|
46
|
+
const res = await fetch(url, opts);
|
|
47
|
+
const ct = res.headers.get('content-type')||'';
|
|
48
|
+
const isJSON = ct.indexOf('application/json') >= 0;
|
|
49
|
+
if(!res.ok){
|
|
50
|
+
let detail = isJSON ? await res.json().catch(function(){return {};}) : await res.text();
|
|
51
|
+
throw new Error('HTTP '+res.status+': '+(isJSON?JSON.stringify(detail):detail));
|
|
52
|
+
}
|
|
53
|
+
return isJSON ? res.json() : res.text();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function callMCP(method, params){
|
|
57
|
+
const url = base.value.replace(/\/$/,'') + '/mcp';
|
|
58
|
+
const body = { jsonrpc: '2.0', id: String(Date.now()), method: method, params: params||{} };
|
|
59
|
+
return fetchJSON(url, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
dlBtn.onclick = async function(){
|
|
63
|
+
setStatus('Fetching apps...', null);
|
|
64
|
+
try{
|
|
65
|
+
const resp = await callMCP('listApps', {});
|
|
66
|
+
const data = (resp && (resp.result||resp)) || {};
|
|
67
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
68
|
+
const url = URL.createObjectURL(blob);
|
|
69
|
+
const a = document.createElement('a');
|
|
70
|
+
a.href = url;
|
|
71
|
+
a.download = 'apps.json';
|
|
72
|
+
document.body.appendChild(a);
|
|
73
|
+
a.click();
|
|
74
|
+
setTimeout(function(){ URL.revokeObjectURL(url); a.remove(); }, 0);
|
|
75
|
+
setStatus('Downloaded', true);
|
|
76
|
+
} catch(e){
|
|
77
|
+
setStatus(e.message||String(e), false);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Load system instructions from docs (source of truth)
|
|
82
|
+
(function loadDocs(){
|
|
83
|
+
var path = '/JsonObjectEditor/docs/joe_agent_custom_gpt_instructions_v_2.md';
|
|
84
|
+
fetch(path).then(function(r){ if(!r.ok) throw new Error('HTTP '+r.status); return r.text(); })
|
|
85
|
+
.then(function(text){ promptBox.value = text; })
|
|
86
|
+
.catch(function(e){ promptBox.value = 'Failed to load instructions: '+(e.message||String(e)); });
|
|
87
|
+
})();
|
|
88
|
+
|
|
89
|
+
dlHydrateBtn.onclick = async function(){
|
|
90
|
+
setStatus('Fetching hydrate...', null);
|
|
91
|
+
try{
|
|
92
|
+
const resp = await callMCP('hydrate', {});
|
|
93
|
+
const data = (resp && (resp.result||resp)) || {};
|
|
94
|
+
const now = new Date();
|
|
95
|
+
const yyyy = now.getFullYear();
|
|
96
|
+
const mm = String(now.getMonth()+1).padStart(2,'0');
|
|
97
|
+
const dd = String(now.getDate()).padStart(2,'0');
|
|
98
|
+
const fname = `hydrate_${yyyy}-${mm}-${dd}.json`;
|
|
99
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
100
|
+
const url = URL.createObjectURL(blob);
|
|
101
|
+
const a = document.createElement('a');
|
|
102
|
+
a.href = url;
|
|
103
|
+
a.download = fname;
|
|
104
|
+
document.body.appendChild(a);
|
|
105
|
+
a.click();
|
|
106
|
+
setTimeout(function(){ URL.revokeObjectURL(url); a.remove(); }, 0);
|
|
107
|
+
setStatus('Downloaded', true);
|
|
108
|
+
} catch(e){
|
|
109
|
+
setStatus(e.message||String(e), false);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
})();
|
|
113
|
+
</script>
|
|
139
114
|
</body>
|
|
140
115
|
</html>
|
|
141
116
|
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>JOE MCP — Schemas Health</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<style>
|
|
8
|
+
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:20px}
|
|
9
|
+
h1{margin:8px 0}
|
|
10
|
+
.small{font-size:12px;color:#666}
|
|
11
|
+
table{width:100%;border-collapse:collapse;margin-top:10px}
|
|
12
|
+
th,td{border:1px solid #e1e4e8;padding:8px;text-align:left;font-size:14px}
|
|
13
|
+
th.sortable{cursor:pointer}
|
|
14
|
+
th.sorted-asc, th.sorted-desc{background:#eef}
|
|
15
|
+
th .sort-ind{font-size:12px;opacity:.6;margin-left:6px}
|
|
16
|
+
.chips{display:flex;gap:6px;flex-wrap:wrap}
|
|
17
|
+
.chip{display:inline-block;padding:2px 8px;border-radius:12px;background:#eef;border:1px solid #ccd;font-size:12px}
|
|
18
|
+
.chip.joe{background:#11bcd6;color:#fff;border-color:#0fa5bb}
|
|
19
|
+
.icon-cell{width:72px}
|
|
20
|
+
.icon64{width:64px;height:64px;display:flex;align-items:center;justify-content:center}
|
|
21
|
+
.icon64 svg{width:64px;height:64px;display:block}
|
|
22
|
+
.health-badge{display:inline-block;padding:2px 8px;border-radius:12px;font-size:12px;border:1px solid transparent}
|
|
23
|
+
.health-good{background:#0a7d00;color:#fff;border-color:#0a7d00}
|
|
24
|
+
.health-warn{background:#f0c040;color:#000;border-color:#e5b93b}
|
|
25
|
+
.health-bad{background:#d93025;color:#fff;border-color:#d93025}
|
|
26
|
+
.t-center{text-align:center}
|
|
27
|
+
th{background:#f6f8fa}
|
|
28
|
+
.status-ok{color:#0a7d00;font-size:22px}
|
|
29
|
+
.status-bad{color:#b00020;font-size:22px}
|
|
30
|
+
.badge{display:inline-block;padding:2px 6px;border-radius:10px;font-size:12px;background:#eef;border:1px solid #ccd}
|
|
31
|
+
a[target]{text-decoration:none}
|
|
32
|
+
</style>
|
|
33
|
+
</head>
|
|
34
|
+
<body>
|
|
35
|
+
<div id="mcp-nav"></div>
|
|
36
|
+
<script src="/JsonObjectEditor/_www/mcp-nav.js"></script>
|
|
37
|
+
<h1>Schema Health</h1>
|
|
38
|
+
<div class="small">Summary of all schemas available in this JOE instance.</div>
|
|
39
|
+
|
|
40
|
+
<div class="row" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin:8px 0 16px;">
|
|
41
|
+
<label for="base" style="margin:0">Base URL</label>
|
|
42
|
+
<input id="base" value="" placeholder="http://localhost:2025" style="min-width:280px" />
|
|
43
|
+
<button id="refresh">Refresh</button>
|
|
44
|
+
<span id="status" class="small"></span>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="row" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin:4px 0 10px;">
|
|
48
|
+
<label for="appFilter" style="margin:0">Filter by App</label>
|
|
49
|
+
<select id="appFilter"><option value="">All</option></select>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<table>
|
|
53
|
+
<thead>
|
|
54
|
+
<tr>
|
|
55
|
+
<th>Icon</th>
|
|
56
|
+
<th class="sortable" data-key="name">Schema <span class="sort-ind"></span></th>
|
|
57
|
+
<th class="sortable" data-key="count">Count <span class="sort-ind"></span></th>
|
|
58
|
+
<th class="sortable" data-key="default">Default <span class="sort-ind"></span></th>
|
|
59
|
+
<th class="sortable" data-key="summary">Summary <span class="sort-ind"></span></th>
|
|
60
|
+
<th class="sortable" data-key="source">Source <span class="sort-ind"></span></th>
|
|
61
|
+
<th class="sortable" data-key="fieldcheck">Field Check <span class="sort-ind"></span></th>
|
|
62
|
+
<th>Outbound</th>
|
|
63
|
+
<th>Apps</th>
|
|
64
|
+
<th class="sortable" data-key="health">Synthesis Health <span class="sort-ind"></span></th>
|
|
65
|
+
<th>API (summary)</th>
|
|
66
|
+
</tr>
|
|
67
|
+
</thead>
|
|
68
|
+
<tbody id="rows"></tbody>
|
|
69
|
+
</table>
|
|
70
|
+
|
|
71
|
+
<script>
|
|
72
|
+
(function(){
|
|
73
|
+
var $ = function(id){ return document.getElementById(id); };
|
|
74
|
+
var base = $('base');
|
|
75
|
+
var refresh = $('refresh');
|
|
76
|
+
var status = $('status');
|
|
77
|
+
var rows = $('rows');
|
|
78
|
+
var appFilterSel = null;
|
|
79
|
+
base.value = base.value || location.origin;
|
|
80
|
+
|
|
81
|
+
var defaultSchemasFallback = ['user','group','goal','initiative','event','report','tag','status','workflow','list','notification','note','include','instance','setting'];
|
|
82
|
+
|
|
83
|
+
function setStatus(msg, ok){
|
|
84
|
+
status.textContent = msg||'';
|
|
85
|
+
status.className = 'small ' + (ok===true?'status-ok': ok===false?'status-bad':'');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function fetchJSON(url){
|
|
89
|
+
const res = await fetch(url);
|
|
90
|
+
if(!res.ok){ throw new Error('HTTP '+res.status+' '+(await res.text())); }
|
|
91
|
+
return res.json();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function chunk(arr, size){
|
|
95
|
+
var out=[], i=0; for(i=0;i<arr.length;i+=size){ out.push(arr.slice(i,i+size)); } return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function check(mark){
|
|
99
|
+
return mark ? '<span class="status-ok">✓</span>' : '<span class="status-bad">✗</span>';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function load(){
|
|
103
|
+
try{
|
|
104
|
+
setStatus('Loading...', null);
|
|
105
|
+
rows.innerHTML = '';
|
|
106
|
+
const baseUrl = base.value.replace(/\/$/,'');
|
|
107
|
+
const list = await fetchJSON(baseUrl + '/API/list/schemas');
|
|
108
|
+
// Fetch full schemas in chunks (includes curated summary when present)
|
|
109
|
+
var full = {};
|
|
110
|
+
var counts = {};
|
|
111
|
+
var chunks = chunk(list, 25);
|
|
112
|
+
for (var i=0;i<chunks.length;i++){
|
|
113
|
+
var names = chunks[i].join(',');
|
|
114
|
+
var got = await fetchJSON(baseUrl + '/API/schemas/' + encodeURIComponent(names));
|
|
115
|
+
var ds = await fetchJSON(baseUrl + '/API/datasets/' + encodeURIComponent(names));
|
|
116
|
+
Object.assign(full, got.schemas || {});
|
|
117
|
+
Object.assign(counts, (ds && ds.counts) || {});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Fetch apps via MCP listApps
|
|
121
|
+
async function callMCP(method, params){
|
|
122
|
+
const url = baseUrl + '/mcp';
|
|
123
|
+
const body = { jsonrpc:'2.0', id:String(Date.now()), method:method, params:params||{} };
|
|
124
|
+
const resp = await fetch(url, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)});
|
|
125
|
+
if(!resp.ok){ throw new Error('HTTP '+resp.status+' '+(await resp.text())); }
|
|
126
|
+
const j = await resp.json();
|
|
127
|
+
return j && (j.result||j) || {};
|
|
128
|
+
}
|
|
129
|
+
var appMap = {};
|
|
130
|
+
try{
|
|
131
|
+
var la = await callMCP('listApps', {});
|
|
132
|
+
appMap = (la && la.apps) || {};
|
|
133
|
+
}catch(_e){ appMap = {}; }
|
|
134
|
+
var data = list.map(function(name){
|
|
135
|
+
var fs = full[name] || {};
|
|
136
|
+
var usedBy = [];
|
|
137
|
+
for (var appName in appMap){
|
|
138
|
+
var a = appMap[appName] || {};
|
|
139
|
+
var cols = Array.isArray(a.collections) ? a.collections : [];
|
|
140
|
+
if (cols.indexOf(name) !== -1){ usedBy.push(appName); }
|
|
141
|
+
}
|
|
142
|
+
// If schema is a default core schema, show only the JOE app chip to avoid noise
|
|
143
|
+
if ((!!fs.default_schema)){
|
|
144
|
+
usedBy = ['joe'];
|
|
145
|
+
}
|
|
146
|
+
var outbound = [];
|
|
147
|
+
try{
|
|
148
|
+
var rel = (fs.summary && fs.summary.relationships && fs.summary.relationships.outbound) || [];
|
|
149
|
+
outbound = rel.filter(function(r){ return r && r.field && r.targetSchema; })
|
|
150
|
+
.map(function(r){ return { field: r.field, schema: r.targetSchema }; });
|
|
151
|
+
}catch(_e){ outbound = []; }
|
|
152
|
+
|
|
153
|
+
var fieldCount = (fs.summary && typeof fs.summary.fieldCount === 'number') ? fs.summary.fieldCount : ((fs.summary && fs.summary.fields && fs.summary.fields.length) || 0);
|
|
154
|
+
var fieldCountBase = (fs.summary && typeof fs.summary.fieldCountBase === 'number') ? fs.summary.fieldCountBase : fieldCount;
|
|
155
|
+
var fieldCoverage = fieldCountBase ? Math.round((fieldCount/fieldCountBase)*100) : 0;
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
name: name,
|
|
159
|
+
count: counts[name] || 0,
|
|
160
|
+
default: !!fs.default_schema || false,
|
|
161
|
+
summary: !!fs.summary,
|
|
162
|
+
source: (fs.summary && fs.summary.source) || (fs.__origin === 'core' ? 'core' : 'instance'),
|
|
163
|
+
apiUrl: baseUrl + '/API/schema/' + encodeURIComponent(name) + '?summaryOnly=true',
|
|
164
|
+
apps: usedBy.sort(),
|
|
165
|
+
outbound: outbound,
|
|
166
|
+
fieldcheck: fieldCoverage,
|
|
167
|
+
fieldcheckText: fieldCount + '/' + fieldCountBase + ' ('+fieldCoverage+'%)',
|
|
168
|
+
menuicon: fs.menuicon || ''
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
function computeHealth(item){
|
|
173
|
+
var signals = 4; // Update when adding more health signals
|
|
174
|
+
var step = 100/signals;
|
|
175
|
+
var score = 0;
|
|
176
|
+
if ((item.count||0) > 0) score += step;
|
|
177
|
+
if (item.menuicon) score += step;
|
|
178
|
+
if (!!item.summary) score += step;
|
|
179
|
+
if ((item.apps||[]).length > 0) score += step;
|
|
180
|
+
return Math.round(score);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Populate app filter options
|
|
184
|
+
appFilterSel = document.getElementById('appFilter');
|
|
185
|
+
if (appFilterSel){
|
|
186
|
+
var appNames = Object.keys(appMap||{}).sort();
|
|
187
|
+
appNames.forEach(function(a){
|
|
188
|
+
var opt=document.createElement('option'); opt.value=a; opt.textContent=a; appFilterSel.appendChild(opt);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getFilteredData(){
|
|
193
|
+
var selected = appFilterSel && appFilterSel.value || '';
|
|
194
|
+
if (!selected) return data;
|
|
195
|
+
return data.filter(function(d){ return (d.apps||[]).indexOf(selected) !== -1; });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function renderTable(sorted){
|
|
199
|
+
rows.innerHTML = '';
|
|
200
|
+
(sorted||[]).forEach(function(item){
|
|
201
|
+
var health = computeHealth(item);
|
|
202
|
+
var hClass = health === 100 ? 'health-good' : (health >= 75 ? 'health-warn' : 'health-bad');
|
|
203
|
+
var tr = document.createElement('tr');
|
|
204
|
+
function td(html, cls){ var c=document.createElement('td'); if(cls){c.className=cls;} c.innerHTML=html||''; return c; }
|
|
205
|
+
function safeIcon(svg){
|
|
206
|
+
try{
|
|
207
|
+
if(!svg || svg.indexOf('<svg') === -1) return '';
|
|
208
|
+
var m = svg.match(/<path[^>]*d="([^"]+)"/i);
|
|
209
|
+
if (m && m[1] && !/^[Mm]/.test(m[1])) return '';
|
|
210
|
+
return svg;
|
|
211
|
+
}catch(_e){ return ''; }
|
|
212
|
+
}
|
|
213
|
+
var iconHTML = safeIcon(item.menuicon);
|
|
214
|
+
tr.appendChild(td(iconHTML ? ('<div class="icon64">'+iconHTML+'</div>') : '', 'icon-cell'));
|
|
215
|
+
tr.appendChild(td('<strong>'+item.name+'</strong>'));
|
|
216
|
+
tr.appendChild(td(String(item.count), 't-center'));
|
|
217
|
+
tr.appendChild(td(check(item.default), 't-center'));
|
|
218
|
+
tr.appendChild(td(check(item.summary)));
|
|
219
|
+
tr.appendChild(td(item.source === 'core' ? 'Core' : 'Instance', 't-center'));
|
|
220
|
+
tr.appendChild(td(item.fieldcheckText||'', 't-center'));
|
|
221
|
+
tr.appendChild(td('<div class="chips">'+ (item.outbound||[]).map(function(ln){ return '<span class="chip">'+ln.field+' → '+ln.schema+'</span>'; }).join(' ') +'</div>'));
|
|
222
|
+
tr.appendChild(td('<div class="chips">'+ (item.apps||[]).map(function(app){ var cls = (app==='joe'?'chip joe':'chip'); var link='/JOE/'+app+'#'+item.name; return '<a class="'+cls+'" href="'+link+'" target="_app_'+app+'">'+app+'</a>'; }).join(' ') +'</div>'));
|
|
223
|
+
tr.appendChild(td('<span class="health-badge '+hClass+'">'+health+'%</span>', 't-center'));
|
|
224
|
+
tr.appendChild(td('<a href="'+item.apiUrl+'" target="_schemas_api_'+item.name+'">/API/schema/'+item.name+'?summaryOnly=true</a>'));
|
|
225
|
+
rows.appendChild(tr);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
var sortState = { key:null, dir:'asc' };
|
|
230
|
+
function sortData(key){
|
|
231
|
+
if (sortState.key === key){ sortState.dir = (sortState.dir === 'asc') ? 'desc' : 'asc'; }
|
|
232
|
+
else {
|
|
233
|
+
sortState.key = key;
|
|
234
|
+
// For boolean/numeric columns, default to true-first or highest-first
|
|
235
|
+
if (key === 'default' || key === 'summary' || key === 'count' || key === 'health') { sortState.dir = 'desc'; }
|
|
236
|
+
else { sortState.dir = 'asc'; }
|
|
237
|
+
}
|
|
238
|
+
var dir = sortState.dir === 'asc' ? 1 : -1;
|
|
239
|
+
var baseList = getFilteredData();
|
|
240
|
+
var sorted = baseList.slice().sort(function(a,b){
|
|
241
|
+
var av = a[key], bv = b[key];
|
|
242
|
+
if (key === 'health'){ av = computeHealth(a); bv = computeHealth(b); }
|
|
243
|
+
// Normalize booleans for deterministic sort
|
|
244
|
+
if (typeof av === 'boolean') { av = av ? 1 : 0; }
|
|
245
|
+
if (typeof bv === 'boolean') { bv = bv ? 1 : 0; }
|
|
246
|
+
if (typeof av === 'string' && typeof bv === 'string'){
|
|
247
|
+
av = av.toLowerCase(); bv = bv.toLowerCase();
|
|
248
|
+
}
|
|
249
|
+
if (av > bv) return 1*dir;
|
|
250
|
+
if (av < bv) return -1*dir;
|
|
251
|
+
return 0;
|
|
252
|
+
});
|
|
253
|
+
renderTable(sorted);
|
|
254
|
+
updateHeaderIndicators();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function updateHeaderIndicators(){
|
|
258
|
+
var ths = document.querySelectorAll('th.sortable');
|
|
259
|
+
Array.prototype.forEach.call(ths, function(th){
|
|
260
|
+
th.classList.remove('sorted-asc','sorted-desc');
|
|
261
|
+
var ind = th.querySelector('.sort-ind');
|
|
262
|
+
if (ind) ind.textContent = '';
|
|
263
|
+
if (th.getAttribute('data-key') === sortState.key){
|
|
264
|
+
th.classList.add(sortState.dir === 'asc' ? 'sorted-asc' : 'sorted-desc');
|
|
265
|
+
if (ind) ind.textContent = sortState.dir === 'asc' ? '▲' : '▼';
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Default sort by name asc
|
|
271
|
+
sortData('name');
|
|
272
|
+
updateHeaderIndicators();
|
|
273
|
+
|
|
274
|
+
// Attach header sort handlers
|
|
275
|
+
Array.prototype.forEach.call(document.querySelectorAll('th.sortable'), function(th){
|
|
276
|
+
th.onclick = function(){ sortData(th.getAttribute('data-key')); };
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Filter handler
|
|
280
|
+
if (appFilterSel){ appFilterSel.onchange = function(){ sortData(sortState.key || 'name'); }; }
|
|
281
|
+
setStatus('Loaded '+list.length+' schemas', true);
|
|
282
|
+
}catch(e){
|
|
283
|
+
setStatus(e.message||String(e), false);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
refresh.onclick = load;
|
|
288
|
+
setTimeout(load, 50);
|
|
289
|
+
})();
|
|
290
|
+
</script>
|
|
291
|
+
</body>
|
|
292
|
+
</html>
|
|
293
|
+
|
|
294
|
+
|