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 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
 
@@ -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
 
@@ -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
+
@@ -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){ appFilterSel.onchange = function(){ sortData(sortState.key || 'name'); }; }
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);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-object-editor",
3
- "version": "0.10.636",
3
+ "version": "0.10.639",
4
4
  "description": "JOE the Json Object Editor | Platform Edition",
5
5
  "main": "app.js",
6
6
  "scripts": {
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">
@@ -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">
@@ -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 page
41
- - JOE ships a simple tester at `/_www/mcp-test.html` inside the package.
42
- - Access via JOE path: `http://localhost:<PORT>/JsonObjectEditor/_www/mcp-test.html`
43
- - If your host app serves its own `_www`, the tester 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>/mcp-test.html`
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)`
@@ -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
  },
@@ -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
 
@@ -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) || {};