json-object-editor 0.10.668 → 0.10.671

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.
@@ -0,0 +1,563 @@
1
+ module.exports = function(data, options) {
2
+ const protocol = data.protocol || data.ITEM;
3
+
4
+ if (!protocol) {
5
+ return '<div style="padding: 20px; color: #dc3545;">Error: No protocol data available</div>';
6
+ }
7
+
8
+ // Helper functions
9
+ function getRef(refId) {
10
+ if (!refId) return null;
11
+ try {
12
+ return $J.get(refId);
13
+ } catch(e) {
14
+ console.error('Error getting reference:', refId, e);
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function extractText(rendering) {
20
+ if (!rendering) return '';
21
+ if (typeof rendering === 'string') {
22
+ return rendering.replace(/<[^>]*>/g, '').trim();
23
+ }
24
+ return String(rendering);
25
+ }
26
+
27
+ function preserveHtml(html) {
28
+ if (!html) return '';
29
+ if (typeof html === 'string') {
30
+ return html.trim();
31
+ }
32
+ return String(html);
33
+ }
34
+
35
+ function escapeHtml(text) {
36
+ if (!text) return '';
37
+ return String(text)
38
+ .replace(/&/g, '&' + 'amp;')
39
+ .replace(/</g, '&' + 'lt;')
40
+ .replace(/>/g, '&' + 'gt;')
41
+ .replace(/"/g, '&' + 'quot;')
42
+ .replace(/'/g, '&' + '#039;');
43
+ }
44
+
45
+ function formatDate(dateString) {
46
+ if (!dateString) return '';
47
+ try {
48
+ const date = new Date(dateString);
49
+ return date.toLocaleDateString('en-US', {
50
+ year: 'numeric',
51
+ month: 'long',
52
+ day: 'numeric'
53
+ });
54
+ } catch(e) {
55
+ return '';
56
+ }
57
+ }
58
+
59
+ function splitList(text) {
60
+ if (!text) return [];
61
+ return text.split(/\n|•|·|-/).map(s => s.trim()).filter(Boolean);
62
+ }
63
+
64
+ // Get client (handle both array and string formats)
65
+ let clientId = protocol.client;
66
+ if (Array.isArray(clientId)) {
67
+ clientId = clientId.length > 0 ? clientId[0] : null;
68
+ }
69
+ const client = clientId ? getRef(clientId) : null;
70
+
71
+ // Build report data
72
+ const reportData = {
73
+ protocol_name: protocol.name || '',
74
+ protocol_headline: protocol.info || '',
75
+ name: client ? (client.name || '') : '',
76
+ email: client ? (client.email || '') : '',
77
+ phone_number: client ? (client.phone || '') : '',
78
+ generated_on: formatDate(protocol.joeUpdated || protocol.created),
79
+ protocol_conditions: [],
80
+ main_concerns: [],
81
+ top_outcomes: [],
82
+ client_symptoms: '',
83
+ urine_ph: protocol.urine_analysis_ph ? String(protocol.urine_analysis_ph) : '',
84
+ urine_status: protocol.urine_status || '',
85
+ blood_pressure_analysis_headline: protocol.blood_pressure_analysis_headline || '',
86
+ urine_analysis_headline: protocol.urine_analysis_headline || '',
87
+ condition_summaries: []
88
+ };
89
+
90
+ // Client data
91
+ if (client) {
92
+ if (client.main_concerns) {
93
+ reportData.main_concerns = splitList(extractText(client.main_concerns));
94
+ }
95
+ if (client.top_outcomes) {
96
+ reportData.top_outcomes = splitList(extractText(client.top_outcomes));
97
+ }
98
+ if (client.client_symptoms) {
99
+ reportData.client_symptoms = extractText(client.client_symptoms);
100
+ }
101
+ }
102
+
103
+ // Protocol conditions
104
+ if (protocol.conditions && Array.isArray(protocol.conditions)) {
105
+ reportData.protocol_conditions = protocol.conditions.map(function(condId) {
106
+ const cond = getRef(condId);
107
+ return cond ? cond.name : null;
108
+ }).filter(Boolean);
109
+ }
110
+
111
+ // Build phases
112
+ const phases = [];
113
+ for (let i = 1; i <= 5; i++) {
114
+ const phaseHeadline = protocol[`phase_${i}_headline`] || '';
115
+ const phaseDesc = protocol[`phase_${i}_description`] ? preserveHtml(protocol[`phase_${i}_description`]) : '';
116
+ const phaseRecIds = protocol[`phase_${i}_recommendations`] || [];
117
+ const phaseOverrides = protocol[`phase_${i}_overrides`] || [];
118
+
119
+ // Handle both array and single value formats
120
+ const recIds = Array.isArray(phaseRecIds) ? phaseRecIds : (phaseRecIds ? [phaseRecIds] : []);
121
+
122
+ // Build override lookup map - match rec _id on recToOverride
123
+ const overrideMap = {};
124
+ if (Array.isArray(phaseOverrides)) {
125
+ phaseOverrides.forEach(function(override) {
126
+ const recIdField = `phase_${i}_recToOverride`;
127
+ const dosageField = `phase_${i}_dosage`;
128
+ const instructionsField = `phase_${i}_instructions`;
129
+
130
+ const recId = override[recIdField];
131
+ if (recId) {
132
+ overrideMap[recId] = {
133
+ dosage: override[dosageField] || '',
134
+ instructions: override[instructionsField] || ''
135
+ };
136
+ }
137
+ });
138
+ }
139
+
140
+ const recommendations = recIds.map(function(recId) {
141
+ if (!recId || typeof recId !== 'string') return null;
142
+ try {
143
+ const rec = getRef(recId);
144
+ if (!rec || !rec.name) return null;
145
+
146
+ const override = overrideMap[recId] || {};
147
+
148
+ return {
149
+ _id: recId,
150
+ name: rec.name || '',
151
+ short_description: rec.info || '',
152
+ long_description: extractText(rec.description) || '',
153
+ dosage: override.dosage || rec.standard_dosage || '',
154
+ overrideInstructions: override.instructions || '',
155
+ domain: rec.recommendation_domain || 'product'
156
+ };
157
+ } catch(e) {
158
+ console.error('Error processing recommendation:', recId, e);
159
+ return null;
160
+ }
161
+ }).filter(Boolean);
162
+
163
+ // Always add phase if it has headline or description, even without recommendations
164
+ if (phaseHeadline || phaseDesc) {
165
+ phases.push({
166
+ headline: phaseHeadline,
167
+ description: phaseDesc,
168
+ recommendations: recommendations
169
+ });
170
+ }
171
+ }
172
+
173
+ // Collect all unique recommendations from all phases
174
+ const allRecommendationsMap = new Map();
175
+ phases.forEach(function(phase) {
176
+ phase.recommendations.forEach(function(rec) {
177
+ if (rec._id && !allRecommendationsMap.has(rec._id)) {
178
+ allRecommendationsMap.set(rec._id, rec);
179
+ }
180
+ });
181
+ });
182
+
183
+ // Group recommendations by domain
184
+ const allRecommendations = Array.from(allRecommendationsMap.values());
185
+ const recommendationsByDomain = {
186
+ product: [],
187
+ dietary: [],
188
+ activity: []
189
+ };
190
+
191
+ allRecommendations.forEach(function(rec) {
192
+ const domain = rec.domain || 'product';
193
+ if (recommendationsByDomain[domain]) {
194
+ recommendationsByDomain[domain].push(rec);
195
+ } else {
196
+ recommendationsByDomain.product.push(rec); // Fallback
197
+ }
198
+ });
199
+
200
+ // Condition summaries
201
+ if (protocol.conditions && Array.isArray(protocol.conditions)) {
202
+ reportData.condition_summaries = protocol.conditions.map(function(condId) {
203
+ const cond = getRef(condId);
204
+ if (!cond) return null;
205
+ return {
206
+ name: cond.name || '',
207
+ description: extractText(cond.long_description || cond.description || '')
208
+ };
209
+ }).filter(Boolean);
210
+ }
211
+
212
+ // HTML generation helpers
213
+ function renderPill(label, value) {
214
+ if (!value) return '';
215
+ return `<span class="pill"><small>${escapeHtml(label)}</small> ${escapeHtml(value)}</span>`;
216
+ }
217
+
218
+ function renderPhaseRecommendationLink(rec) {
219
+ const recId = rec._id || rec.name.toLowerCase().replace(/\s+/g, '-');
220
+ const dosageHtml = rec.dosage ? `<div class="dose">${escapeHtml(rec.dosage)}</div>` : '';
221
+ const instructionsHtml = rec.overrideInstructions ? `<div style="font-size: 11px; color: #666; margin-top: 6px; font-style: italic; padding-left: 4px;">${escapeHtml(rec.overrideInstructions)}</div>` : '';
222
+
223
+ return `
224
+ <div style="margin-bottom: 8px;">
225
+ <div style="display:flex; justify-content:space-between; gap:10px; align-items:flex-start;">
226
+ <div style="flex:1;">
227
+ <a href="#rec-${recId}" class="rec-link">${escapeHtml(rec.name)}</a>
228
+ ${instructionsHtml}
229
+ </div>
230
+ ${dosageHtml}
231
+ </div>
232
+ </div>
233
+ `;
234
+ }
235
+
236
+ function renderFullRecommendation(rec) {
237
+ const recId = rec._id || rec.name.toLowerCase().replace(/\s+/g, '-');
238
+ return `
239
+ <div class="rec" id="rec-${recId}">
240
+ <div class="rec-top">
241
+ <div>
242
+ <div class="rec-name">${escapeHtml(rec.name)}</div>
243
+ ${rec.short_description ? `<div class="muted" style="margin-top:6px">
244
+ ${escapeHtml(rec.short_description)}
245
+ <button class="toggle" type="button" data-toggle="more" aria-expanded="false">+</button>
246
+ </div>` : ''}
247
+ </div>
248
+ ${rec.dosage ? `<div class="dose">${escapeHtml(rec.dosage)}</div>` : ''}
249
+ </div>
250
+ <div class="more">
251
+ ${rec.long_description ? `<div class="muted">${escapeHtml(rec.long_description)}</div>` : ''}
252
+ </div>
253
+ </div>
254
+ `;
255
+ }
256
+
257
+ function renderPhase(phase, i) {
258
+ if (!phase.headline && !phase.description && phase.recommendations.length === 0) {
259
+ return '';
260
+ }
261
+ var pmap = ["Phase 1: Stabilization",
262
+ "Phase 2: Foundations",
263
+ "Phase 3: Repair",
264
+ "Phase 4: Optimization",
265
+ "Phase 5: Integration & Maintenance"];
266
+
267
+ const phaseInstructions = protocol[`phase_${i + 1}_instructions`] || '';
268
+ const instructionsHtml = phaseInstructions ? preserveHtml(phaseInstructions) : '';
269
+
270
+ return `
271
+ <div class="phase">
272
+ <div class="head">
273
+ <div class="phase-label">${pmap[i]}
274
+ ${phase.headline ? `<div><small><i>${escapeHtml(phase.headline)}</i></small></div>` : ''}
275
+ </div>
276
+ ${phase.description ? `<div class="muted">${phase.description}</div>` : ''}
277
+ </div>
278
+ ${phase.recommendations.length > 0 || instructionsHtml ? `
279
+ <div class="recs" style="display:grid; grid-template-columns: 1fr 1fr; gap: 16px; padding:12px;">
280
+ <div>
281
+ <h4 style="margin:0 0 8px; font-size:13px; color:var(--plum);">Recommendations</h4>
282
+ <div style="display:flex; flex-direction:column; gap:6px;">
283
+ ${phase.recommendations.map(rec => renderPhaseRecommendationLink(rec)).join('')}
284
+ </div>
285
+ </div>
286
+ ${instructionsHtml ? `
287
+ <div>
288
+ <h4 style="margin:0 0 8px; font-size:13px; color:var(--plum);">Instructions</h4>
289
+ <div class="muted">${instructionsHtml}</div>
290
+ </div>
291
+ ` : '<div></div>'}
292
+ </div>
293
+ ` : ''}
294
+ </div>
295
+ `;
296
+ }
297
+
298
+ // Generate HTML
299
+ const html = `
300
+ <!doctype html>
301
+ <html lang="en">
302
+ <head>
303
+ <meta charset="utf-8" />
304
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
305
+ <title>Harmonious Wellness — Protocol Report</title>
306
+ <style>
307
+ :root{
308
+ --plum:#3a1430;
309
+ --gold:#c6a44d;
310
+ --olive:#6c7b3b;
311
+ --ink:#0f172a;
312
+ --muted:#475569;
313
+ --bg:#0b0810;
314
+ --paper:#ffffff;
315
+ --soft:#f8fafc;
316
+ --line:#e5e7eb;
317
+ --max: 980px;
318
+ --radius: 16px;
319
+ --font: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
320
+ }
321
+
322
+ html,body{height:100%;}
323
+ body{margin:0; font-family:var(--font); background:var(--bg); color:var(--ink);}
324
+
325
+ .topbar{position:sticky; top:0; z-index:10; background:rgba(11,8,16,.92); border-bottom:1px solid rgba(255,255,255,.10);}
326
+ .topbar .inner{max-width:var(--max); margin:0 auto; padding:10px 14px; display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap;}
327
+ .brand{display:flex; align-items:center; gap:10px; color:#f1f5f9;}
328
+ .mark{width:28px; height:28px; border-radius:10px; background:linear-gradient(135deg,var(--olive),var(--gold)); box-shadow:0 0 0 3px rgba(198,164,77,.15);}
329
+ .brand b{letter-spacing:.2px;}
330
+ .actions{display:flex; gap:8px; flex-wrap:wrap;}
331
+ button{border:1px solid rgba(255,255,255,.18); background:rgba(255,255,255,.06); color:#e2e8f0; padding:8px 10px; border-radius:999px; font-weight:700; cursor:pointer; transition:opacity 0.2s;}
332
+ button:hover{opacity:0.8;}
333
+ button.primary{border-color:rgba(198,164,77,.55); background:rgba(198,164,77,.18); color:#fff7e6;}
334
+
335
+ .wrap{max-width:var(--max); margin:18px auto 52px; padding:0 14px;}
336
+ .paper{background:var(--paper); border-radius:22px; overflow:hidden; border:1px solid rgba(255,255,255,.06);}
337
+
338
+ .hero{padding:18px; border-bottom:1px solid var(--line); background:linear-gradient(180deg,#fff,#fbfdff);}
339
+ .hero h1{margin:0; font-size:22px; line-height:1.2;}
340
+ .hero .sub{margin-top:6px; color:var(--muted); font-weight:700;}
341
+ .meta{display:flex; gap:10px; flex-wrap:wrap; margin-top:12px;}
342
+ .pill{display:inline-flex; gap:8px; align-items:center; padding:6px 10px; border:1px solid var(--line); background:var(--soft); border-radius:999px; font-weight:800; font-size:13px;}
343
+ .pill small{color:var(--muted); font-weight:800;}
344
+
345
+ .section{padding:16px 18px; border-bottom:1px solid var(--line);}
346
+ .section:last-child{border-bottom:none;}
347
+ .section h2{margin:0 0 10px; font-size:16px;}
348
+ .grid{display:grid; gap:10px;}
349
+ .two{grid-template-columns: repeat(2, minmax(0,1fr));}
350
+ @media (max-width:780px){.two{grid-template-columns:1fr;}}
351
+
352
+ .card{border:1px solid var(--line); border-radius:var(--radius); padding:12px; background:#fff;}
353
+ .card h3{margin:0 0 8px; font-size:13px; color:var(--plum);}
354
+ .muted{color:var(--muted); line-height:1.55;}
355
+ ul{margin:0; padding-left:18px; color:var(--muted); font-weight:650;}
356
+ li{margin:6px 0;}
357
+ .phase-label{font-weight:bold;}
358
+ .phase{border:1px solid var(--line); border-radius:18px; overflow:hidden; margin-bottom:12px;}
359
+ .phase .head{padding:12px; background:linear-gradient(180deg,#fff,#f9fbff); border-bottom:1px solid var(--line);}
360
+ .phase .head b{color:var(--plum);}
361
+ .phase .head p{margin:6px 0 0;}
362
+ .phase .recs{padding:12px; display:grid; gap:10px;}
363
+
364
+ .rec{border:1px solid var(--line); border-radius:14px; padding:10px;}
365
+ .rec-top{display:flex; justify-content:space-between; gap:10px; align-items:flex-start;}
366
+ .rec-name{font-weight:900;}
367
+ .rec-link{color:var(--plum); text-decoration:none; font-weight:600; padding:4px 0; display:block; transition:opacity 0.2s;}
368
+ .rec-link:hover{text-decoration:underline; opacity:0.8;}
369
+ .dose{font-size:12px; font-weight:900; color:var(--plum); background:rgba(58,20,48,.06); border:1px solid rgba(58,20,48,.12); padding:6px 10px; border-radius:999px; white-space:nowrap;}
370
+ .more{display:none; margin-top:8px; padding-top:8px; border-top:1px dashed rgba(15,23,42,.18);}
371
+ .toggle{margin-left:8px; border:1px solid rgba(15,23,42,.18); background:#fff; color:var(--ink); padding:3px 8px; border-radius:999px; font-weight:900; font-size:12px; cursor:pointer; transition:background 0.2s;}
372
+ .toggle:hover{background:#f0f0f0;}
373
+
374
+ body.pdf-mode .toggle{display:none;}
375
+ body.pdf-mode .more{display:block;}
376
+
377
+ @media print{
378
+ body{background:#fff;}
379
+ .topbar{display:none;}
380
+ .wrap{max-width:100%; margin:0; padding:0;}
381
+ .paper{border-radius:0;}
382
+ .more{display:block !important;}
383
+ .toggle{display:none !important;}
384
+ .phase{break-inside:avoid; page-break-inside:avoid;}
385
+ }
386
+ </style>
387
+ </head>
388
+ <body>
389
+ <div class="topbar">
390
+ <div class="inner">
391
+ <div class="brand">
392
+ <div class="mark" aria-hidden="true"></div>
393
+ <div>
394
+ <b>Harmonious Wellness</b>
395
+ <div style="font-size:12px; color:#cbd5e1; font-weight:700;">Protocol Report</div>
396
+ </div>
397
+ </div>
398
+ <div class="actions">
399
+ <button id="pdfBtn" aria-pressed="false">📄 PDF Mode</button>
400
+ <button class="primary" onclick="window.print()">🖨️ Print / Save</button>
401
+ </div>
402
+ </div>
403
+ </div>
404
+
405
+ <main class="wrap">
406
+ <article class="paper" id="report">
407
+ <section class="hero">
408
+ <h1>${escapeHtml(reportData.protocol_name)}</h1>
409
+ ${reportData.protocol_headline ? `<div class="sub">${escapeHtml(reportData.protocol_headline)}</div>` : ''}
410
+ <div class="meta">
411
+ ${reportData.name ? renderPill('Client', reportData.name) : ''}
412
+ ${reportData.generated_on ? renderPill('Generated', reportData.generated_on) : ''}
413
+ ${reportData.email ? renderPill('Email', reportData.email) : ''}
414
+ ${reportData.phone_number ? renderPill('Phone', reportData.phone_number) : ''}
415
+ </div>
416
+ </section>
417
+
418
+ <section class="section">
419
+ <h2>Intentions and Goals</h2>
420
+ <div class="grid two">
421
+ <div class="card">
422
+ <h3>Conditions of focus</h3>
423
+ <div class="muted">${reportData.protocol_conditions.length > 0 ? escapeHtml(reportData.protocol_conditions.join(', ')) : 'None specified'}</div>
424
+ </div>
425
+ <div class="card">
426
+ <h3>Main Glands, Organs, or Systems Involved</h3>
427
+ <div class="muted">${reportData.client_symptoms || 'Not specified'}</div>
428
+ </div>
429
+ </div>
430
+
431
+ <div class="grid two" style="margin-top:10px">
432
+ <div class="card">
433
+ <h3>Top Client Concerns</h3>
434
+ ${reportData.main_concerns.length > 0 ? `<ul>${reportData.main_concerns.map(c => `<li>${escapeHtml(c)}</li>`).join('')}</ul>` : '<div class="muted">Not specified</div>'}
435
+ </div>
436
+ <div class="card">
437
+ <h3>Other Key Notes from Consult</h3>
438
+ ${reportData.top_outcomes.length > 0 ? `<ul>${reportData.top_outcomes.map(o => `<li>${escapeHtml(o)}</li>`).join('')}</ul>` : '<div class="muted">Not specified</div>'}
439
+ </div>
440
+ </div>
441
+
442
+ <div class="grid two" style="margin-top:10px">
443
+ <div class="card">
444
+ <h3>Kidney Filtration</h3>
445
+ <div class="muted">Urine pH: ${reportData.urine_ph || 'N/A'}</div>
446
+ <div class="muted">Status: ${reportData.urine_status || 'N/A'}</div>
447
+ </div>
448
+ <div class="card">
449
+ <h3>Analysis</h3>
450
+ ${reportData.blood_pressure_analysis_headline ? `<div class="muted">${escapeHtml(reportData.blood_pressure_analysis_headline)}</div>` : ''}
451
+ ${reportData.urine_analysis_headline ? `<div class="muted" style="margin-top:8px">${escapeHtml(reportData.urine_analysis_headline)}</div>` : ''}
452
+ </div>
453
+ </div>
454
+ </section>
455
+ <section class="section">
456
+
457
+ <h2>Protocol Overview</h2>
458
+ <div class="card">
459
+ <h2>${protocol.info}</h2>
460
+ ${protocol.description}
461
+
462
+
463
+ </div>
464
+ </section>
465
+ ${phases.length > 0 ? `
466
+ <section class="section">
467
+ <h2>Protocol Phases & Recommendations</h2>
468
+ ${phases.map((phase, i) => renderPhase(phase, i)).join('')}
469
+ </section>
470
+ ` : ''}
471
+
472
+ ${allRecommendations.length > 0 ? `
473
+ <section class="section" id="recommendations">
474
+ <h2>Recommendations</h2>
475
+ ${Object.keys(recommendationsByDomain).map(function(domain) {
476
+ const recs = recommendationsByDomain[domain];
477
+ if (recs.length === 0) return '';
478
+ const domainLabel = domain.charAt(0).toUpperCase() + domain.slice(1);
479
+ return `
480
+ <div style="margin-bottom:20px">
481
+ <h3 style="margin:0 0 12px; font-size:14px; color:var(--plum); text-transform:capitalize;">${domainLabel}</h3>
482
+ <div style="display:grid; gap:10px;">
483
+ ${recs.map(renderFullRecommendation).join('')}
484
+ </div>
485
+ </div>
486
+ `;
487
+ }).filter(Boolean).join('')}
488
+ </section>
489
+ ` : ''}
490
+
491
+ <section class="section">
492
+ <h2>Education & Resources</h2>
493
+ ${reportData.condition_summaries.length > 0 ? `
494
+ <div class="card">
495
+ <h3>Key Conditions We're Supporting</h3>
496
+ ${reportData.condition_summaries.map(c => `
497
+ <div style="margin-top:10px">
498
+ <div style="font-weight:900; color:var(--plum)">${escapeHtml(c.name)}</div>
499
+ ${c.description ? `<div class="muted">${escapeHtml(c.description)}</div>` : ''}
500
+ </div>
501
+ `).join('')}
502
+ </div>
503
+ ` : ''}
504
+ <div class="card" style="margin-top:10px">
505
+ <h3>Products Referenced in Protocol</h3>
506
+ <div class="muted">List and links for products referenced in this protocol will be emailed separately.</div>
507
+ </div>
508
+ </section>
509
+
510
+ <section class="section">
511
+ <h2>Closing Note</h2>
512
+ <div class="muted">
513
+ ${reportData.name ? escapeHtml(reportData.name.split(' ')[0]) : 'Client'}, This is a ton of information and oftentimes it takes some digestion and processing. If you would like to book a follow up appointment or just hop on the phone to discuss any of this please let me know. I am here to support you! I also make tailor made tinctures and teas so feel free to let me know if that is of interest. Deep blessings on the next step in your health journey! I am available by email or text if you have questions along the way. Please never hesitate to reach out! I would love to at least connect back in 3 months to hear how things are going and recommend adjustments/modifications as needed. You can book your follow-up appointment online here. Select either a Short Follow up or Long Follow up. There is a small fee for these follow ups. However, accessibility is a value of mine and I will never turn anyone away for lack of funds. Please let me know and I will share my sliding scale range. Thank you ${reportData.name ? escapeHtml(reportData.name) : 'you'} for trusting and allowing me to support you. The work works. Keep the faith. Live inspired. Blessings on your wellness journey and enhanced vitality! ~ Kelly Shay
514
+ </div>
515
+ </section>
516
+ </article>
517
+ </main>
518
+
519
+ <script>
520
+ (function() {
521
+ // PDF Mode toggle
522
+ const pdfBtn = document.getElementById('pdfBtn');
523
+ if (pdfBtn) {
524
+ pdfBtn.addEventListener('click', function() {
525
+ const enabled = !document.body.classList.contains('pdf-mode');
526
+ document.body.classList.toggle('pdf-mode', enabled);
527
+ pdfBtn.setAttribute('aria-pressed', String(enabled));
528
+ });
529
+ }
530
+
531
+ // Expand/collapse recommendations (only in recommendations section)
532
+ document.addEventListener('click', function(e) {
533
+ const toggle = e.target.closest('[data-toggle="more"]');
534
+ if (!toggle || document.body.classList.contains('pdf-mode')) return;
535
+
536
+ const rec = toggle.closest('.rec');
537
+ if (!rec) return;
538
+
539
+ // Only expand if in recommendations section
540
+ if (!rec.closest('#recommendations')) return;
541
+
542
+ const more = rec.querySelector('.more');
543
+ if (!more) return;
544
+
545
+ const expanded = toggle.getAttribute('aria-expanded') === 'true';
546
+ toggle.setAttribute('aria-expanded', String(!expanded));
547
+ toggle.textContent = expanded ? '+' : '−';
548
+ more.style.display = expanded ? 'none' : 'block';
549
+ });
550
+ })();
551
+ </script>
552
+ </body>
553
+ </html>
554
+ `;
555
+
556
+ // Use renderHTMLFramework if available, otherwise return raw HTML
557
+ if (options && options.renderHTMLFramework) {
558
+ return options.renderHTMLFramework(data.REPORT, protocol, html);
559
+ }
560
+
561
+ return html;
562
+ };
563
+
@@ -15,8 +15,8 @@ These summaries are served by MCP hydrate and by GET `/API/schema/:name?summaryO
15
15
  - searchableFields: Array of string field names that are good for text queries
16
16
  - allowedSorts: Whitelist of sortable fields agents should use
17
17
  - relationships:
18
- - outbound: Array of { field, targetSchema, cardinality } where cardinality is 'one' | 'many'
19
- - inbound: { graphRef: 'server/relationships.graph.json' } (do not hand‑maintain inbound; its computed from the graph later)
18
+ - outbound: Array of { field, targetSchema, cardinality } where cardinality is 'one' | 'many' (auto-generated from fields; see Relationships section below)
19
+ - inbound: { graphRef: 'server/relationships.graph.json' } (do not hand‑maintain inbound; it's computed from the graph later)
20
20
  - joeManagedFields: ["created","joeUpdated"] (always include)
21
21
  - fields: Array of normalized field descriptors:
22
22
  - Required: name, type
@@ -26,6 +26,36 @@ Notes
26
26
  - display/comment/tooltip are enriched automatically at runtime from the schema’s field definitions; include them in the summary when they clarify usage.
27
27
  - For select fields, add enumValues when known (e.g., role, priority, content_type).
28
28
 
29
+ ## Relationships (outbound)
30
+
31
+ **Auto-generation**: The `relationships.outbound` array is automatically generated from fields in your schema definition that have `isReference: true` and a `targetSchema` value. Cardinality is determined automatically:
32
+ - `isArray: true` or `type: 'objectList'` → `cardinality: 'many'`
33
+ - Single reference field → `cardinality: 'one'`
34
+
35
+ **How it works**: In your schema file, mark reference fields like this:
36
+ ```javascript
37
+ fields: [
38
+ { name: 'project', type: 'string', isReference: true, targetSchema: 'project' },
39
+ { name: 'members', type: 'string', isArray: true, isReference: true, targetSchema: 'user' }
40
+ ]
41
+ ```
42
+ The system will automatically generate:
43
+ ```javascript
44
+ relationships: {
45
+ outbound: [
46
+ { field: 'project', targetSchema: 'project', cardinality: 'one' },
47
+ { field: 'members', targetSchema: 'user', cardinality: 'many' }
48
+ ]
49
+ }
50
+ ```
51
+
52
+ **Manual override**: You can manually specify `relationships.outbound` in the `summary` block if you need to:
53
+ - Add relationships not detected by field analysis (e.g., implicit relationships)
54
+ - Correct incorrectly inferred relationships
55
+ - Provide clearer field names or target schemas
56
+
57
+ **Best practice**: Let automatic generation handle most cases. Ensure your schema fields are properly marked with `isReference: true` and `targetSchema`, and relationships will be generated automatically. Only manually specify if you need to override or add relationships not captured by field definitions.
58
+
29
59
  ## Cardinality guidance
30
60
 
31
61
  - A single reference (select/goto a single item) → 'one'
@@ -79,6 +109,7 @@ Rules:
79
109
  - Prefer concise, high‑signal text for description and purpose.
80
110
  - Do NOT populate inbound manually; leave graphRef as is.
81
111
  - Include enumValues for selects where known; omit when unknown.
112
+ - For relationships.outbound: Only specify manually if you need to override automatic detection. Otherwise, ensure your schema fields have `isReference: true` and `targetSchema` set, and relationships will be auto-generated.
82
113
  ```
83
114
 
84
115
  ## Example (task)
@@ -119,10 +150,18 @@ summary: {
119
150
  ## How to validate quickly
120
151
 
121
152
  - Open: `/API/schema/<name>?summaryOnly=true`
122
- - Check: description/purpose read well; labelField exists in fields; allowedSorts are valid; outbound list references real schemas; no inbound entries besides graphRef; joeManagedFields present.
153
+ - Check:
154
+ - description/purpose read well
155
+ - labelField exists in fields
156
+ - allowedSorts are valid
157
+ - **relationships.outbound entries match fields with `isReference: true`** (or are intentional overrides)
158
+ - outbound list references real schemas
159
+ - no inbound entries besides graphRef
160
+ - joeManagedFields present
123
161
 
124
162
  ## Notes for maintainers
125
163
 
126
164
  - Runtime will enrich fields with display/comment/tooltip from the schema when available.
127
165
  - Inbound relationships are not hand‑maintained; a graph builder will populate the shared graph later.
128
166
  - For core vs instance: `source` is inferred automatically but can be overridden inside `summary` if needed.
167
+ - **Relationships are auto-generated**: Ensure your schema fields are marked with `isReference: true` and `targetSchema` in the schema definition (not just in the summary). The system will automatically build `relationships.outbound` from these fields. Only manually specify `relationships.outbound` in the summary if you need to override or add relationships not captured by field definitions.