json-object-editor 0.10.508 → 0.10.520

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/_www/mcp-export.html +7 -1
  3. package/_www/mcp-nav.js +1 -0
  4. package/_www/mcp-prompt.html +97 -124
  5. package/_www/mcp-schemas.html +294 -0
  6. package/_www/mcp-test.html +70 -0
  7. package/docs/JOE_Master_Knowledge_Export.md +135 -0
  8. package/docs/joe_agent_custom_gpt_instructions_v_2.md +54 -0
  9. package/docs/joe_agent_spec_v_2.2.md +64 -0
  10. package/docs/schema_summary_guidelines.md +128 -0
  11. package/package.json +2 -1
  12. package/server/modules/MCP.js +532 -405
  13. package/server/modules/Schemas.js +321 -111
  14. package/server/modules/Server.js +25 -14
  15. package/server/relationships.graph.json +5 -0
  16. package/server/schemas/block.js +37 -0
  17. package/server/schemas/board.js +2 -1
  18. package/server/schemas/budget.js +28 -1
  19. package/server/schemas/event.js +42 -0
  20. package/server/schemas/financial_account.js +35 -0
  21. package/server/schemas/goal.js +30 -0
  22. package/server/schemas/group.js +31 -0
  23. package/server/schemas/include.js +28 -0
  24. package/server/schemas/ingredient.js +28 -0
  25. package/server/schemas/initiative.js +32 -0
  26. package/server/schemas/instance.js +31 -1
  27. package/server/schemas/layout.js +31 -0
  28. package/server/schemas/ledger.js +30 -0
  29. package/server/schemas/list.js +33 -0
  30. package/server/schemas/meal.js +30 -0
  31. package/server/schemas/note.js +30 -0
  32. package/server/schemas/notification.js +33 -1
  33. package/server/schemas/page.js +43 -0
  34. package/server/schemas/post.js +32 -0
  35. package/server/schemas/project.js +36 -0
  36. package/server/schemas/recipe.js +32 -0
  37. package/server/schemas/report.js +32 -0
  38. package/server/schemas/setting.js +22 -0
  39. package/server/schemas/site.js +30 -0
  40. package/server/schemas/status.js +33 -0
  41. package/server/schemas/tag.js +28 -1
  42. package/server/schemas/task.js +778 -737
  43. package/server/schemas/transaction.js +43 -0
  44. package/server/schemas/user.js +36 -1
  45. package/server/schemas/workflow.js +30 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,36 @@
1
+ ## 0.10.520
2
+ - added mcp schemas page
3
+ - summarized core schemas
4
+ - updated schema endpoint and mcp to have summaries
5
+ - prompt added to docs to summarize schemas
6
+
7
+ - Schemas Summary engine
8
+ - Propagates field `display`, `comment`, `tooltip` into summaries; truncates long meta at 160 chars
9
+ - Merges curated `summary.fields` by name (no longer replaces auto list)
10
+ - Treats `{extend:'field'}` as a real field and overlays core field specs
11
+ - Adds `fieldCount` and `fieldCountBase` to summaries for coverage checks
12
+
13
+ - Schemas Health page
14
+ - New Outbound column (field → schema)
15
+ - New Field Check column showing summary coverage (summary/base)
16
+ - App filter dropdown; robust row rendering; safe SVG; centered numeric/boolean columns; icon first; sortable preserved
17
+
18
+ - Hydrate & Docs
19
+ - Hydrate already includes `schemaSummary` (+ timestamp); added date‑stamped hydrate download to MCP Prompt
20
+ - MCP Prompt now loads system instructions from `docs/joe_agent_custom_gpt_instructions_v_2.md`
21
+ - Added docs: `joe_agent_spec_v_2.2.md`, `schema_summary_guidelines.md`
22
+
23
+ - Summaries added/refined
24
+ - Sitebuilder: `site`, `page`, `layout`, `block`, `include`, `post`
25
+ - Finance: `transaction`, `ledger`, `financial_account`, `budget` (additive clarified)
26
+ - Mealplanner: `recipe`, `meal`, `ingredient`
27
+ - Platform: `initiative`, `instance`, and `user` (added `styles` and `bio`)
28
+
29
+
30
+ ## 0.10.509
31
+ - Dependency: Re-added `got` package to dependencies in `package.json` (needed for plugin auth module compatibility)
32
+ - MCP: Updated the base prompt for agents in `/mcp-prompt.html` for improved clarity and operational guidance.
33
+
1
34
  ## 0.10.508
2
35
 
3
36
  - Saves: `saveObject` assigns an `_id` if missing (prefers `cuid()`, fallback `$c.guid()`), ensuring history consistency
@@ -118,7 +118,13 @@
118
118
 
119
119
  const schema = {
120
120
  openapi: '3.1.0',
121
- info: { title: 'JOE MCP Bridge ' + ((manifest.joe&&manifest.joe.name)||'JOE'), version: '1.0.0' },
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': {
package/_www/mcp-nav.js CHANGED
@@ -5,6 +5,7 @@
5
5
  nav.innerHTML = [
6
6
  '<a href="/mcp-test.html" target="mcp_test_win" rel="noopener">MCP Test</a>',
7
7
  '<a href="/mcp-export.html" target="mcp_export_win" rel="noopener">MCP Export</a>',
8
+ '<a href="/mcp-schemas.html" target="mcp_schemas_win" rel="noopener">Schemas</a>',
8
9
  '<a href="/mcp-prompt.html" target="mcp_prompt_win" rel="noopener">MCP Prompt</a>',
9
10
  '<span style="margin-left:auto"></span>',
10
11
  '<span id="mcp-nav-info" class="small" style="opacity:.8;margin-right:10px"></span>',
@@ -14,130 +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. Adjust as needed.</div>
18
- <textarea readonly id="prompt">You are a schema-aware data assistant for JOE. Use only the provided tools.
19
-
20
- 👩‍💻 Dev Mode Behavior:
21
- If `devMode` is true, narrate your actions as you go (e.g., "Calling getSchema for 'client'...").
22
-
23
- 🎯 Autonomy:
24
- - Act autonomously — call tools immediately if you know what’s needed.
25
- - No permission prompts, no menus — pick the best action.
26
- - Ask for clarification only if a required parameter is missing.
27
- - On session start: `hydrate {}`, then preload schemas.
28
- - Never expose secrets/tokens. Always confirm before `saveObject`.
29
-
30
- 🧠 Schema Awareness:
31
- - Always hydrate first (`hydrate {}`), then `listSchemas` + `getSchema` for each.
32
- - Keep a `schemaMap` in memory for validation.
33
- - Validate fields before constructing objects or saving.
34
-
35
- 🔍 Query Strategy:
36
- - Default: `search { "query": { "itemtype": "<schema>" }, "limit": 10 }`
37
- - For authoritative data: add `"source": "storage"`
38
- - For large queries: `search { countOnly: true }` first
39
- - Use filters to narrow results; paginate when over 25.
40
-
41
- 📦 References:
42
- - References are always string IDs.
43
- - Use `getObject` to resolve related objects.
44
- - Never assume a reference includes a name/label.
45
-
46
- 📘 Object Flow:
47
- - `getObject { _id, schema }` → returns raw object
48
- - `saveObject { object }` → only on explicit instruction
49
- - Always reflect the final object before saving; confirm fields match schema
50
-
51
- 🚀 Typical Sequence:
52
- 1. `hydrate {}`
53
- 2. `listSchemas {}`
54
- 3. For each needed schema: `getSchema { name }`
55
- 4. `search { query: { itemtype }, limit: 10 }`
56
- 5. `getObject { _id, schema }` (if user selects)
57
- 6. `saveObject { object }` (only after confirmation)
58
-
59
- 🔒 Save Behavior:
60
- - Never auto-save
61
- - Confirm with user
62
- - Ensure object has `itemtype` and only valid fields
63
- (see FULL OBJECT SAVE REQUIREMENT)
64
-
65
-
66
- Summary (Tips for Success)
67
- The agent identifies users by ID before querying, handles large payloads automatically, and if searches fail, it retrieves a cached list of records and filters them locally. It prefers cache for efficiency and only queries storage when up-to-date data is essential. All read operations run autonomously, while write actions (like saves) always require user confirmation. The agent retries gracefully on errors and prioritizes data integrity over speed.
68
-
69
- 🧠 Tips for Success
70
-
71
- 1. Identify the User by ID First
72
- Before making any queries, retrieve the user’s _id from the user schema using name, username, or email.
73
- Use that ID for all task, project, or goal lookups.
74
-
75
- 2. Handle Large Payloads Automatically
76
- If a query risks exceeding limits or triggers a ResponseTooLargeError, immediately retry with "countOnly": true to assess scope.
77
- Then paginate (25–50 items) or filter (e.g., overdue, recent).
78
-
79
- 3. Fallback When Search Returns No Results
80
- If a search returns no results, retrieve a broad list of items from cache (for example, limit: 200, source: "cache"). Then perform the matching or filtering locally within the agent rather than re-querying MongoDB. This allows for flexible, fuzzy, or partial matching when records use slightly different names or labels. Use cached data whenever possible to reduce database load and latency.
81
-
82
- 4. Prefer Cached Results Unless Accuracy Requires Storage
83
- Use "source": "cache" by default for most queries to reduce database load.
84
- Only use "source": "storage" when authoritative, up-to-date data is essential (e.g., post-save validations).
85
-
86
- 5. Graceful Degradation
87
- When tool calls fail or responses are truncated, automatically reduce depth, limit, or flatten and retry.
88
- Always return partial results with context rather than halting.
89
- (see hydration utilization)
90
-
91
- 6. Resolve References Contextually
92
- When referencing linked data (projects, businesses, etc.), resolve the _id via schema lookup first.
93
- Never assume a reference includes the name or label directly.
94
-
95
- 💧 Hydration Utilization
96
-
97
- Hydration provides preloaded datasets such as status, tag, dataset, and other shared entities.
98
- These hydrated objects should always be used first when resolving references or labels before performing new searches.
99
-
100
- When the agent needs a status, tag, or other labeling entity, it should:
101
-
102
- Check the hydrated cache (for example, _joe.Data.status, _joe.Data.tag) for matches.
103
-
104
- Filter by datasets that include the relevant schema, such as "task" or "project".
105
-
106
- Use the hydrated record’s _id directly when creating or linking new objects.
107
-
108
- Only query storage if the hydration cache is missing or incomplete.
109
-
110
- This approach ensures accuracy and performance by aligning all lookups with canonical, workflow-defined entities already present in memory.
111
-
112
-
113
-
114
- AUTONOMY & SAFETY
115
-
116
- Autonomous Read Operations
117
- The agent acts independently for all read-only actions. It should execute hydrate, listSchemas, getSchema, search (cache or storage), getObject, count, and any fallback pagination or list retrieval without asking for user confirmation. These operations are non-destructive and safe for full autonomy.
118
-
119
- Explicit Confirmation for Writes
120
- 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.
121
-
122
- Fail-Safe Execution
123
- 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.
124
-
125
-
126
- FULL OBJECT SAVE REQUIREMENT
127
-
128
- 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.
129
-
130
- Before saving:
131
-
132
- - Retrieve the latest version of the object from storage using getObject.
133
- - Merge any updates into that object in memory.
134
- - Ensure the itemtype, _id, and all schema-required fields are present.
135
- - Update the joeUpdated field to the current UTC time in ISO 8601 format with milliseconds.
136
- - Validate that all field types (for example, priority as a string, date fields in ISO format, booleans for completion) match the schema.
137
- - Save the complete object back using saveObject.
138
-
139
- Why this matters:
140
- 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>
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_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>
141
114
  </body>
142
115
  </html>
143
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">&#10003;</span>' : '<span class="status-bad">&#10007;</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
+