json-object-editor 0.10.636 → 0.10.639
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 +6 -0
- package/_www/matrix.html +126 -0
- package/_www/mcp-nav.js +6 -0
- package/_www/mcp-prompt.html +116 -116
- package/_www/mcp-schemas.html +32 -2
- package/favicon-dev.png +0 -0
- package/js/favicon-env.js +50 -0
- package/package.json +1 -1
- package/pages/joe.html +6 -4
- package/pages/template.html +5 -4
- package/pages/template_ie.html +5 -4
- package/readme.md +8 -4
- package/server/app-config.js +2 -0
- package/server/modules/Apps.js +3 -0
- package/server/modules/MCP.js +4 -0
- package/server/schemas/ai_widget_conversation.js +229 -229
- package/server/schemas/form.js +41 -0
- package/server/schemas/location.js +24 -0
- package/server/schemas/member.js +35 -0
- package/server/schemas/question.js +36 -0
- package/server/schemas/submission.js +30 -0
- package/web-components/joe-matrix.css +480 -0
- package/web-components/joe-matrix.js +1065 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 0.10.638
|
|
2
|
+
- Matrix: Added schema relationship visualization page at `/_www/matrix.html` with interactive D3.js force-directed graph showing schema-to-schema relationships. Features include app-based filtering, clickable nodes with schema properties panel, SVG icons, and link labels that appear on node selection.
|
|
3
|
+
|
|
4
|
+
## 0.10.637
|
|
5
|
+
- Favicon: Added environment-aware favicon switching - development (localhost) uses `favicon-dev.png` with red "D" indicator, production uses default `favicon.ico`. Implemented via `js/favicon-env.js` script included in all templates and MCP debug pages.
|
|
6
|
+
|
|
1
7
|
## 0.10.635
|
|
2
8
|
- AI Autofill: "Allow web search" is now supported in autofill—if a field's `ai.allowWeb` is set (schema, UI, or API), the `/API/plugin/chatgpt/autofill` endpoint will enable OpenAI's built-in `web_search` for that call. This lets the assistant fetch up-to-date info or run online research when generating field values.
|
|
3
9
|
|
package/_www/matrix.html
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>JOE — Schema Matrix</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<link rel="stylesheet" href="/JsonObjectEditor/web-components/joe-matrix.css"/>
|
|
8
|
+
<style>
|
|
9
|
+
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:0;padding:0;overflow:hidden}
|
|
10
|
+
#matrix-container{width:100vw;height:100vh;position:relative}
|
|
11
|
+
|
|
12
|
+
</style>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<div id="mcp-nav"></div>
|
|
16
|
+
<script src="/JsonObjectEditor/_www/mcp-nav.js"></script>
|
|
17
|
+
|
|
18
|
+
<div id="matrix-container">
|
|
19
|
+
<div class="matrix-controls">
|
|
20
|
+
<label>View Mode:</label>
|
|
21
|
+
<select id="view-mode">
|
|
22
|
+
<option value="schema-to-schema">Schema to Schema (3a)</option>
|
|
23
|
+
<option value="object-relationships" disabled>Object Relationships (3b) - Coming Soon</option>
|
|
24
|
+
<option value="aggregate-relationships" disabled>Aggregate Relationships (3c) - Coming Soon</option>
|
|
25
|
+
</select>
|
|
26
|
+
<label>Filter by App:</label>
|
|
27
|
+
<select id="app-filter">
|
|
28
|
+
<option value="">All</option>
|
|
29
|
+
</select>
|
|
30
|
+
<button id="reset-view">Reset View</button>
|
|
31
|
+
<button id="fit-to-screen">Fit to Screen</button>
|
|
32
|
+
</div>
|
|
33
|
+
<joe-matrix id="joe-matrix-main"></joe-matrix>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<script src="/JsonObjectEditor/js/plugins/c3/d3.v3.min.js"></script>
|
|
37
|
+
<script src="/JsonObjectEditor/web-components/joe-matrix.js"></script>
|
|
38
|
+
<script>
|
|
39
|
+
(function(){
|
|
40
|
+
// URL parameter helpers
|
|
41
|
+
function getUrlParam(name) {
|
|
42
|
+
var params = new URLSearchParams(window.location.search);
|
|
43
|
+
return params.get(name) || '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function setUrlParam(name, value) {
|
|
47
|
+
var url = new URL(window.location);
|
|
48
|
+
if (value) {
|
|
49
|
+
url.searchParams.set(name, value);
|
|
50
|
+
} else {
|
|
51
|
+
url.searchParams.delete(name);
|
|
52
|
+
}
|
|
53
|
+
window.history.replaceState({}, '', url);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// View mode change handler
|
|
57
|
+
var viewMode = document.getElementById('view-mode');
|
|
58
|
+
var appFilter = document.getElementById('app-filter');
|
|
59
|
+
var resetBtn = document.getElementById('reset-view');
|
|
60
|
+
var fitBtn = document.getElementById('fit-to-screen');
|
|
61
|
+
var matrixEl = document.getElementById('joe-matrix-main');
|
|
62
|
+
|
|
63
|
+
// Read app parameter from URL on load
|
|
64
|
+
var initialApp = getUrlParam('app');
|
|
65
|
+
if (initialApp && appFilter) {
|
|
66
|
+
// Wait for options to be populated, then set the value
|
|
67
|
+
var checkInterval = setInterval(function() {
|
|
68
|
+
if (appFilter.options.length > 1) {
|
|
69
|
+
clearInterval(checkInterval);
|
|
70
|
+
// Check if the app exists in the options
|
|
71
|
+
var found = false;
|
|
72
|
+
for (var i = 0; i < appFilter.options.length; i++) {
|
|
73
|
+
if (appFilter.options[i].value === initialApp) {
|
|
74
|
+
appFilter.value = initialApp;
|
|
75
|
+
found = true;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (found && matrixEl && matrixEl.setAppFilter) {
|
|
80
|
+
matrixEl.setAppFilter(initialApp);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}, 100);
|
|
84
|
+
// Clear interval after 5 seconds to avoid infinite checking
|
|
85
|
+
setTimeout(function() { clearInterval(checkInterval); }, 5000);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if(viewMode){
|
|
89
|
+
viewMode.addEventListener('change', function(e){
|
|
90
|
+
if(matrixEl){
|
|
91
|
+
matrixEl.setAttribute('mode', e.target.value);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if(appFilter){
|
|
97
|
+
appFilter.addEventListener('change', function(e){
|
|
98
|
+
var selectedApp = e.target.value || '';
|
|
99
|
+
// Update URL parameter
|
|
100
|
+
setUrlParam('app', selectedApp);
|
|
101
|
+
// Update matrix filter
|
|
102
|
+
if(matrixEl && matrixEl.setAppFilter){
|
|
103
|
+
matrixEl.setAppFilter(selectedApp);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if(resetBtn){
|
|
109
|
+
resetBtn.addEventListener('click', function(){
|
|
110
|
+
if(matrixEl && matrixEl.resetView){
|
|
111
|
+
matrixEl.resetView();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if(fitBtn){
|
|
117
|
+
fitBtn.addEventListener('click', function(){
|
|
118
|
+
if(matrixEl && matrixEl.fitToScreen){
|
|
119
|
+
matrixEl.fitToScreen();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
})();
|
|
124
|
+
</script>
|
|
125
|
+
</body>
|
|
126
|
+
</html>
|
package/_www/mcp-nav.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
'<a href="/mcp-test.html" target="mcp_test_win" rel="noopener">MCP Test</a>',
|
|
9
9
|
'<a href="/mcp-export.html" target="mcp_export_win" rel="noopener">MCP Export</a>',
|
|
10
10
|
'<a href="/mcp-schemas.html" target="mcp_schemas_win" rel="noopener">Schemas</a>',
|
|
11
|
+
'<a href="/matrix.html" target="matrix_win" rel="noopener">Matrix</a>',
|
|
11
12
|
'<a href="/mcp-prompt.html" target="mcp_prompt_win" rel="noopener">MCP Prompt</a>',
|
|
12
13
|
'<a href="/ai-widget-test.html" target="ai_widget_test_win" rel="noopener">AI Widget</a>',
|
|
13
14
|
'<span style="margin-left:auto"></span>',
|
|
@@ -42,6 +43,11 @@
|
|
|
42
43
|
document.addEventListener('DOMContentLoaded', insert);
|
|
43
44
|
document.addEventListener('DOMContentLoaded', updateInstance);
|
|
44
45
|
}else{ insert(); updateInstance(); }
|
|
46
|
+
|
|
47
|
+
// Load favicon environment script for production detection
|
|
48
|
+
var faviconScript = document.createElement('script');
|
|
49
|
+
faviconScript.src = '/JsonObjectEditor/js/favicon-env.js';
|
|
50
|
+
document.head.appendChild(faviconScript);
|
|
45
51
|
})();
|
|
46
52
|
|
|
47
53
|
|
package/_www/mcp-prompt.html
CHANGED
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<title>JOE MCP Prompt</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
|
-
textarea{width:100%;height:360px;font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
10
|
-
.small{font-size:12px;color:#666}
|
|
11
|
-
</style>
|
|
12
|
-
</head>
|
|
13
|
-
<body>
|
|
14
|
-
<div id="mcp-nav"></div>
|
|
15
|
-
<script src="/JsonObjectEditor/_www/mcp-nav.js"></script>
|
|
16
|
-
<h1>Starter Agent Instructions</h1>
|
|
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>
|
|
18
|
-
|
|
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_3.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>
|
|
114
|
-
</body>
|
|
115
|
-
</html>
|
|
116
|
-
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>JOE MCP Prompt</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
|
+
textarea{width:100%;height:360px;font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
10
|
+
.small{font-size:12px;color:#666}
|
|
11
|
+
</style>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<div id="mcp-nav"></div>
|
|
15
|
+
<script src="/JsonObjectEditor/_www/mcp-nav.js"></script>
|
|
16
|
+
<h1>Starter Agent Instructions</h1>
|
|
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>
|
|
18
|
+
|
|
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_3.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>
|
|
114
|
+
</body>
|
|
115
|
+
</html>
|
|
116
|
+
|
package/_www/mcp-schemas.html
CHANGED
|
@@ -180,6 +180,10 @@
|
|
|
180
180
|
return Math.round(score);
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// Read app parameter from URL
|
|
184
|
+
var urlParams = new URLSearchParams(window.location.search);
|
|
185
|
+
var urlApp = urlParams.get('app');
|
|
186
|
+
|
|
183
187
|
// Populate app filter options
|
|
184
188
|
appFilterSel = document.getElementById('appFilter');
|
|
185
189
|
if (appFilterSel){
|
|
@@ -187,6 +191,17 @@
|
|
|
187
191
|
appNames.forEach(function(a){
|
|
188
192
|
var opt=document.createElement('option'); opt.value=a; opt.textContent=a; appFilterSel.appendChild(opt);
|
|
189
193
|
});
|
|
194
|
+
|
|
195
|
+
// Set filter from URL parameter if present (before initial render)
|
|
196
|
+
if (urlApp) {
|
|
197
|
+
// Check if the app exists in the options
|
|
198
|
+
for (var i = 0; i < appFilterSel.options.length; i++) {
|
|
199
|
+
if (appFilterSel.options[i].value === urlApp) {
|
|
200
|
+
appFilterSel.value = urlApp;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
190
205
|
}
|
|
191
206
|
|
|
192
207
|
function getFilteredData(){
|
|
@@ -267,7 +282,7 @@
|
|
|
267
282
|
});
|
|
268
283
|
}
|
|
269
284
|
|
|
270
|
-
// Default sort by name asc
|
|
285
|
+
// Default sort by name asc (filter will be applied automatically if appFilterSel.value is set)
|
|
271
286
|
sortData('name');
|
|
272
287
|
updateHeaderIndicators();
|
|
273
288
|
|
|
@@ -277,7 +292,22 @@
|
|
|
277
292
|
});
|
|
278
293
|
|
|
279
294
|
// Filter handler
|
|
280
|
-
if (appFilterSel){
|
|
295
|
+
if (appFilterSel){
|
|
296
|
+
appFilterSel.onchange = function(){
|
|
297
|
+
// Update URL parameter
|
|
298
|
+
var selectedApp = appFilterSel.value || '';
|
|
299
|
+
var url = new URL(window.location);
|
|
300
|
+
if (selectedApp) {
|
|
301
|
+
url.searchParams.set('app', selectedApp);
|
|
302
|
+
} else {
|
|
303
|
+
url.searchParams.delete('app');
|
|
304
|
+
}
|
|
305
|
+
window.history.replaceState({}, '', url);
|
|
306
|
+
|
|
307
|
+
// Apply filter
|
|
308
|
+
sortData(sortState.key || 'name');
|
|
309
|
+
};
|
|
310
|
+
}
|
|
281
311
|
setStatus('Loaded '+list.length+' schemas', true);
|
|
282
312
|
}catch(e){
|
|
283
313
|
setStatus(e.message||String(e), false);
|
package/favicon-dev.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment-aware favicon changer
|
|
3
|
+
* Changes favicon based on localhost vs production environment
|
|
4
|
+
*/
|
|
5
|
+
(function() {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
// Detect if running on localhost (development)
|
|
9
|
+
var isLocalhost = location.hostname === 'localhost' ||
|
|
10
|
+
location.hostname === '127.0.0.1' ||
|
|
11
|
+
location.hostname === '';
|
|
12
|
+
|
|
13
|
+
if (isLocalhost) {
|
|
14
|
+
// For development: use dev favicon file
|
|
15
|
+
var devFaviconPath = '/JsonObjectEditor/favicon-dev.png';
|
|
16
|
+
|
|
17
|
+
var updateFavicon = function(id) {
|
|
18
|
+
var link = document.getElementById(id);
|
|
19
|
+
if (link) {
|
|
20
|
+
link.href = devFaviconPath;
|
|
21
|
+
link.type = 'image/png';
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Update existing favicon links
|
|
26
|
+
updateFavicon('favicon-shortcut');
|
|
27
|
+
updateFavicon('favicon-icon');
|
|
28
|
+
|
|
29
|
+
// Also update any favicon links without IDs (fallback)
|
|
30
|
+
var allFavicons = document.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"]');
|
|
31
|
+
allFavicons.forEach(function(link) {
|
|
32
|
+
if (!link.id || link.id.indexOf('favicon') === -1) {
|
|
33
|
+
link.href = devFaviconPath;
|
|
34
|
+
link.type = 'image/png';
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// If no favicon links exist, create one
|
|
39
|
+
if (allFavicons.length === 0) {
|
|
40
|
+
var head = document.head || document.getElementsByTagName('head')[0];
|
|
41
|
+
var link = document.createElement('link');
|
|
42
|
+
link.rel = 'icon';
|
|
43
|
+
link.type = 'image/png';
|
|
44
|
+
link.href = devFaviconPath;
|
|
45
|
+
head.appendChild(link);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// For production, keep default favicon (no changes needed)
|
|
49
|
+
})();
|
|
50
|
+
|
package/package.json
CHANGED
package/pages/joe.html
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name='viewport' content='initial-scale=1.0, user-scalable=no' />
|
|
6
6
|
<title>${this.webconfig.name} JOE</title>
|
|
7
|
-
<link rel="shortcut icon" href="/JsonObjectEditor/favicon.ico" type="image/x-icon">
|
|
8
|
-
<link rel="icon" href="/JsonObjectEditor/favicon.ico" type="image/x-icon">
|
|
9
|
-
<link rel="icon" sizes="192x192" href="/JsonObjectEditor/img/ico/android-icon-192x192.png">
|
|
10
|
-
<link rel="apple-touch-icon" sizes="144x144" href="/JsonObjectEditor/img/ico/android-icon-144x144.png" />
|
|
7
|
+
<link rel="shortcut icon" href="/JsonObjectEditor/favicon.ico" type="image/x-icon" id="favicon-shortcut">
|
|
8
|
+
<link rel="icon" href="/JsonObjectEditor/favicon.ico" type="image/x-icon" id="favicon-icon">
|
|
9
|
+
<link rel="icon" sizes="192x192" href="/JsonObjectEditor/img/ico/android-icon-192x192.png" id="favicon-192">
|
|
10
|
+
<link rel="apple-touch-icon" sizes="144x144" href="/JsonObjectEditor/img/ico/android-icon-144x144.png" id="favicon-apple" />
|
|
11
|
+
<script src="/JsonObjectEditor/js/favicon-env.js"></script>
|
|
12
|
+
|
|
11
13
|
<meta name="joe-notes" content="joe.html template" >
|
|
12
14
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
13
15
|
<meta name="mobile-web-app-capable" content="yes">
|
package/pages/template.html
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
<meta name='viewport' content='initial-scale=1.0, user-scalable=no' >
|
|
7
7
|
<title> ${this.webconfig.name} > ${APPNAME} </title>
|
|
8
8
|
<script src="/JsonObjectEditor/js/libs/adapter-latest.js"></script>
|
|
9
|
-
<link rel="shortcut icon" href="/JsonObjectEditor/favicon.ico" type="image/x-icon">
|
|
10
|
-
<link rel="icon" href="/JsonObjectEditor/favicon.ico" type="image/x-icon">
|
|
11
|
-
<link rel="icon" sizes="192x192" href="/JsonObjectEditor/img/ico/android-icon-192x192.png">
|
|
12
|
-
<link rel="apple-touch-icon" sizes="144x144" href="/JsonObjectEditor/img/ico/android-icon-144x144.png" />
|
|
9
|
+
<link rel="shortcut icon" href="/JsonObjectEditor/favicon.ico" type="image/x-icon" id="favicon-shortcut">
|
|
10
|
+
<link rel="icon" href="/JsonObjectEditor/favicon.ico" type="image/x-icon" id="favicon-icon">
|
|
11
|
+
<link rel="icon" sizes="192x192" href="/JsonObjectEditor/img/ico/android-icon-192x192.png" id="favicon-192">
|
|
12
|
+
<link rel="apple-touch-icon" sizes="144x144" href="/JsonObjectEditor/img/ico/android-icon-144x144.png" id="favicon-apple" />
|
|
13
|
+
<script src="/JsonObjectEditor/js/favicon-env.js"></script>
|
|
13
14
|
|
|
14
15
|
<meta name="joe-notes" content="template.html template" >
|
|
15
16
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
package/pages/template_ie.html
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
<meta name='viewport' content='initial-scale=1.0, user-scalable=no' >
|
|
9
9
|
<title> ${this.webconfig.name} > ${APPNAME} </title>
|
|
10
10
|
<script src="/JsonObjectEditor/js/libs/adapter-latest.js"></script>
|
|
11
|
-
<link rel="shortcut icon" href="/JsonObjectEditor/favicon.ico" type="image/x-icon">
|
|
12
|
-
<link rel="icon" href="/JsonObjectEditor/favicon.ico" type="image/x-icon">
|
|
13
|
-
<link rel="icon" sizes="192x192" href="/JsonObjectEditor/img/ico/android-icon-192x192.png">
|
|
14
|
-
<link rel="apple-touch-icon" sizes="144x144" href="/JsonObjectEditor/img/ico/android-icon-144x144.png" />
|
|
11
|
+
<link rel="shortcut icon" href="/JsonObjectEditor/favicon.ico" type="image/x-icon" id="favicon-shortcut">
|
|
12
|
+
<link rel="icon" href="/JsonObjectEditor/favicon.ico" type="image/x-icon" id="favicon-icon">
|
|
13
|
+
<link rel="icon" sizes="192x192" href="/JsonObjectEditor/img/ico/android-icon-192x192.png" id="favicon-192">
|
|
14
|
+
<link rel="apple-touch-icon" sizes="144x144" href="/JsonObjectEditor/img/ico/android-icon-144x144.png" id="favicon-apple" />
|
|
15
|
+
<script src="/JsonObjectEditor/js/favicon-env.js"></script>
|
|
15
16
|
|
|
16
17
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
17
18
|
<meta name="mobile-web-app-capable" content="yes">
|
package/readme.md
CHANGED
|
@@ -37,10 +37,14 @@ JOE is software that allows you to manage data models via JSON objects. There ar
|
|
|
37
37
|
- Auth
|
|
38
38
|
- If users exist, `POST /mcp` requires cookie or Basic Auth (same as other APIs). If no users configured, it is effectively open.
|
|
39
39
|
|
|
40
|
-
- Test
|
|
41
|
-
- JOE ships
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
- Test pages
|
|
41
|
+
- JOE ships several debug/test pages in `/_www/`:
|
|
42
|
+
- `mcp-test.html` - Simple MCP tool tester
|
|
43
|
+
- `mcp-schemas.html` - Schema health and summary viewer
|
|
44
|
+
- `mcp-prompt.html` - MCP prompt/instructions viewer
|
|
45
|
+
- `matrix.html` - Interactive schema relationship visualization with D3.js force-directed graph
|
|
46
|
+
- Access via JOE path: `http://localhost:<PORT>/JsonObjectEditor/_www/<page>.html`
|
|
47
|
+
- If your host app serves its own `_www`, pages can also be available at the root (fallback) if running with the updated server that mounts JOE’s `_www` as a secondary static directory. Then: `http://localhost:<PORT>/<page>.html`
|
|
44
48
|
|
|
45
49
|
- Tools
|
|
46
50
|
- `listSchemas(name?)`, `getSchema(name)`
|
package/server/app-config.js
CHANGED
|
@@ -17,6 +17,8 @@ var apps = function(){
|
|
|
17
17
|
(_joe.search({}).length)+ ' objects';
|
|
18
18
|
if(_joe.User && _joe.User.role == 'super'){
|
|
19
19
|
message+='<br/><br/><joe-button onclick="_joe.SERVER.Cache.reload()" class="joe-button joe-iconed-button joe-grey-button joe-red-button joe-reload-button"> reload cache </joe-button>';
|
|
20
|
+
//add horizontal rule then text links to schems page and matrix page
|
|
21
|
+
message+=`<hr/> <a href="/schemas.html">schemas</a> <a href="/matrix.html">matrix</a>`;
|
|
20
22
|
}
|
|
21
23
|
return message;
|
|
22
24
|
},
|
package/server/modules/Apps.js
CHANGED
|
@@ -39,6 +39,7 @@ function Apps(){
|
|
|
39
39
|
title:specs.title||'<small>JOE Platform v'+JOE.VERSION+'</small>',
|
|
40
40
|
cssclass:'w2 h1',
|
|
41
41
|
content:function(){
|
|
42
|
+
var app = APPINFO || {};
|
|
42
43
|
if(!_joe.Data || !_joe.Data.user){return 'loading data';}
|
|
43
44
|
//console.log(_joe.Data.user);
|
|
44
45
|
|
|
@@ -74,6 +75,8 @@ function Apps(){
|
|
|
74
75
|
message+='<joe-button onclick="_joe.SERVER.Cache.reload()" class="joe-button joe-iconed-button joe-grey-button joe-red-button joe-reload-button"> re-cache </joe-button>';
|
|
75
76
|
}
|
|
76
77
|
+'</div><joe-clear></joe-clear>';
|
|
78
|
+
//add horizontal rule then text links to schems page and matrix page
|
|
79
|
+
message+='<a href="/mcp-schemas.html?app='+app.title.toLowerCase()+'" target="mcp_schemas_win">schemas</a> <a href="/matrix.html?app='+app.title.toLowerCase()+'" target="matrix_win">matrix</a>';
|
|
77
80
|
return message;
|
|
78
81
|
}
|
|
79
82
|
|
package/server/modules/MCP.js
CHANGED
|
@@ -158,6 +158,8 @@ MCP.tools = {
|
|
|
158
158
|
// Get schema definition; summaryOnly to return normalized summary instead of full schema
|
|
159
159
|
getSchema: async ({ name, summaryOnly }, _ctx) => {
|
|
160
160
|
if (!name) throw new Error("Missing required param 'name'");
|
|
161
|
+
const Schemas = getSchemas();
|
|
162
|
+
if (!Schemas) throw new Error('Schemas module not initialized');
|
|
161
163
|
if (summaryOnly) {
|
|
162
164
|
const sum = Schemas && Schemas.summary && Schemas.summary[name];
|
|
163
165
|
if (!sum) throw new Error(`Schema summary for "${name}" not found`);
|
|
@@ -170,6 +172,8 @@ MCP.tools = {
|
|
|
170
172
|
|
|
171
173
|
// Get multiple schemas. With summaryOnly, return summaries; without names, returns all.
|
|
172
174
|
getSchemas: async ({ names, summaryOnly } = {}, _ctx) => {
|
|
175
|
+
const Schemas = getSchemas();
|
|
176
|
+
if (!Schemas) throw new Error('Schemas module not initialized');
|
|
173
177
|
const all = (Schemas && Schemas.schema) || {};
|
|
174
178
|
if (summaryOnly) {
|
|
175
179
|
const allS = (Schemas && Schemas.summary) || {};
|