iobroker.autodoc 0.9.35

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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -0
  3. package/admin/autodoc.png +0 -0
  4. package/admin/i18n/de.json +244 -0
  5. package/admin/i18n/en.json +241 -0
  6. package/admin/i18n/es.json +229 -0
  7. package/admin/i18n/fr.json +235 -0
  8. package/admin/i18n/it.json +229 -0
  9. package/admin/i18n/nl.json +229 -0
  10. package/admin/i18n/pl.json +229 -0
  11. package/admin/i18n/pt.json +229 -0
  12. package/admin/i18n/ru.json +229 -0
  13. package/admin/i18n/uk.json +229 -0
  14. package/admin/i18n/zh-cn.json +229 -0
  15. package/admin/jsonConfig.json +1490 -0
  16. package/io-package.json +253 -0
  17. package/lib/adapter-config.d.ts +19 -0
  18. package/lib/aiEnhancer.js +2114 -0
  19. package/lib/autoHostTopologyMermaid.js +195 -0
  20. package/lib/dependencyAnalyzer.js +83 -0
  21. package/lib/diagnosisSnapshot.js +32 -0
  22. package/lib/discovery.js +953 -0
  23. package/lib/docTemplateConfig.js +422 -0
  24. package/lib/documentModel.js +640 -0
  25. package/lib/forumCard.js +70 -0
  26. package/lib/guestHelpContent.js +93 -0
  27. package/lib/guestScriptPrivacy.js +14 -0
  28. package/lib/hostDisplay.js +19 -0
  29. package/lib/htmlRenderer.js +4108 -0
  30. package/lib/htmlThemePresets.js +79 -0
  31. package/lib/htmlToPdf.js +99 -0
  32. package/lib/i18n.js +1309 -0
  33. package/lib/markdownRenderer.js +2025 -0
  34. package/lib/mermaidAutodocPalette.js +165 -0
  35. package/lib/mermaidServerSvg.js +252 -0
  36. package/lib/notifier.js +124 -0
  37. package/lib/quickStartGuide.js +180 -0
  38. package/lib/roleMapper.js +90 -0
  39. package/lib/scriptGroups.js +78 -0
  40. package/lib/versionTracker.js +312 -0
  41. package/main.js +1368 -0
  42. package/package.json +88 -0
@@ -0,0 +1,2025 @@
1
+ /**
2
+ * AutoDoc Markdown Renderer Module
3
+ * Renders document models to Markdown format with profile-based content
4
+ */
5
+ const PROFILE_ADMIN = 'admin';
6
+ const PROFILE_USER = 'user';
7
+ const PROFILE_ONBOARDING = 'onboarding';
8
+
9
+ const { groupScriptsByFolder, isGlobalFolderKey } = require('./scriptGroups');
10
+ const { formatOperatingSystemLine } = require('./hostDisplay');
11
+ const {
12
+ DEFAULT_ADMIN_CHAPTER_ORDER,
13
+ USER_HTML_CHAPTER_KEYS,
14
+ ONBOARDING_HTML_CHAPTER_KEYS,
15
+ } = require('./docTemplateConfig');
16
+ const { guestHelpChapterHasContent } = require('./guestHelpContent');
17
+ const { hasFamilyDiagnosisSnapshot, isNodeVersionFlaggedForDiagnosis } = require('./diagnosisSnapshot');
18
+ const { onboardingGuestShowsScriptNames } = require('./guestScriptPrivacy');
19
+ const { sliceQuickStartForOnboarding } = require('./quickStartGuide');
20
+
21
+ /**
22
+ * Escape pipe characters for Markdown pipe tables (single-line cells).
23
+ *
24
+ * @param {*} v
25
+ * @returns {string}
26
+ */
27
+ function mdTableCell(v) {
28
+ return String(v == null ? '' : v)
29
+ .replace(/\|/g, '\\|')
30
+ .replace(/\r?\n/g, ' ')
31
+ .trim();
32
+ }
33
+
34
+ /**
35
+ * Escapes user-controlled text for safe use inside Markdown **bold** (inner segment only).
36
+ *
37
+ * @param {*} v
38
+ * @returns {string}
39
+ */
40
+ function mdEscapeBoldInner(v) {
41
+ return String(v == null ? '' : v)
42
+ .replace(/\r?\n/g, ' ')
43
+ .replace(/\\/g, '\\\\')
44
+ .replace(/([`*_])/g, '\\$1');
45
+ }
46
+
47
+ /**
48
+ * Escapes a single-line fragment used in Markdown outside code (list lines, heading text) so `*`, `_`, etc. do not break emphasis.
49
+ *
50
+ * @param {*} v
51
+ * @returns {string}
52
+ */
53
+ function mdEscapePlainLine(v) {
54
+ return String(v == null ? '' : v)
55
+ .replace(/\r?\n/g, ' ')
56
+ .replace(/\\/g, '\\\\')
57
+ .replace(/([`*_[\]])/g, '\\$1');
58
+ }
59
+
60
+ /**
61
+ * MarkdownRenderer renders the document model to Markdown text.
62
+ *
63
+ * @param {object} adapter ioBroker adapter instance
64
+ * @param {object} i18n i18n instance for translations
65
+ */
66
+ class MarkdownRenderer {
67
+ /**
68
+ * @param {object} adapter ioBroker adapter instance
69
+ * @param {object} i18n i18n instance for translations
70
+ */
71
+ constructor(adapter, i18n) {
72
+ this.adapter = adapter;
73
+ this.i18n = i18n;
74
+ }
75
+
76
+ /**
77
+ * Check if profile includes detail level
78
+ *
79
+ * @param {string} profile Current profile
80
+ * @param {string} detailLevel Detail level (admin, user, basic)
81
+ * @returns {boolean} True if detail should be shown
82
+ */
83
+ shouldShowDetail(profile, detailLevel) {
84
+ const levels = {
85
+ [PROFILE_ADMIN]: ['admin', 'user', 'basic'],
86
+ [PROFILE_USER]: ['user', 'basic'],
87
+ [PROFILE_ONBOARDING]: ['basic'],
88
+ };
89
+ return (levels[profile] || levels[PROFILE_ADMIN]).includes(detailLevel);
90
+ }
91
+
92
+ /**
93
+ * @param {object} docModel
94
+ * @param {string} key
95
+ * @returns {boolean}
96
+ */
97
+ adminChapterHidden(docModel, key) {
98
+ const h = docModel && docModel.adminHiddenChapters;
99
+ return Array.isArray(h) && h.includes(key);
100
+ }
101
+
102
+ /**
103
+ * @param {object} docModel
104
+ * @param {string} key
105
+ * @returns {boolean}
106
+ */
107
+ userChapterHidden(docModel, key) {
108
+ const h = docModel && docModel.userHiddenChapters;
109
+ return Array.isArray(h) && h.includes(key);
110
+ }
111
+
112
+ /**
113
+ * @param {object} docModel
114
+ * @param {string} key
115
+ * @returns {boolean}
116
+ */
117
+ onboardingChapterHidden(docModel, key) {
118
+ const h = docModel && docModel.onboardingHiddenChapters;
119
+ return Array.isArray(h) && h.includes(key);
120
+ }
121
+
122
+ /**
123
+ * @param {object} docModel
124
+ * @param {string} profile
125
+ * @returns {boolean}
126
+ */
127
+ manualContextVisibleForMarkdown(docModel, profile) {
128
+ const mc = docModel.manualContext;
129
+ if (!mc) {
130
+ return false;
131
+ }
132
+ const t = v => v && String(v).trim();
133
+ if (profile === PROFILE_USER) {
134
+ const core =
135
+ !this.userChapterHidden(docModel, 'manual') && (t(mc.description) || t(mc.contact) || t(mc.notes));
136
+ const mer = !this.userChapterHidden(docModel, 'mermaid') && t(mc.mermaidDiagram);
137
+ const merAuto = !this.userChapterHidden(docModel, 'mermaidAuto') && t(mc.autoHostTopologyMermaid);
138
+ const g =
139
+ !this.userChapterHidden(docModel, 'guestHelp') &&
140
+ guestHelpChapterHasContent(mc, docModel, PROFILE_USER);
141
+ const r = !this.userChapterHidden(docModel, 'routines') && t(mc.homeRoutinesNote);
142
+ const pb = !this.userChapterHidden(docModel, 'ownerPlaybook') && t(mc.ownerPlaybookNote);
143
+ return !!(core || mer || merAuto || g || r || pb);
144
+ }
145
+ if (profile === PROFILE_ONBOARDING) {
146
+ const core =
147
+ !this.onboardingChapterHidden(docModel, 'manual') &&
148
+ (t(mc.description) || t(mc.contact) || t(mc.notes));
149
+ // auto-topology never shown in onboarding — only manual mermaid counts here
150
+ const mer = !this.onboardingChapterHidden(docModel, 'mermaid') && t(mc.mermaidDiagram);
151
+ const g =
152
+ !this.onboardingChapterHidden(docModel, 'guestHelp') &&
153
+ guestHelpChapterHasContent(mc, docModel, PROFILE_ONBOARDING);
154
+ const r = !this.onboardingChapterHidden(docModel, 'routines') && t(mc.homeRoutinesNote);
155
+ const pb = !this.onboardingChapterHidden(docModel, 'ownerPlaybook') && t(mc.ownerPlaybookNote);
156
+ return !!(core || mer || g || r || pb);
157
+ }
158
+ return false;
159
+ }
160
+
161
+ /**
162
+ * @param {object} docModel
163
+ * @param {string} profile
164
+ * @returns {string}
165
+ */
166
+ renderCustomSectionsMarkdown(docModel, profile) {
167
+ if (profile === PROFILE_ADMIN && this.adminChapterHidden(docModel, 'custom')) {
168
+ return '';
169
+ }
170
+ if (profile === PROFILE_USER && this.userChapterHidden(docModel, 'custom')) {
171
+ return '';
172
+ }
173
+ if (profile === PROFILE_ONBOARDING && this.onboardingChapterHidden(docModel, 'custom')) {
174
+ return '';
175
+ }
176
+ const list = (docModel && docModel.customDocSections) || [];
177
+ const rows = list.filter(s => !s.profiles || !s.profiles.length || s.profiles.includes(profile));
178
+ if (rows.length === 0) {
179
+ return '';
180
+ }
181
+ const i18n = this.i18n;
182
+ let md = `## ${i18n.t('customDocSectionsTitle') || 'Custom sections'}\n\n<a id="custom-doc-sections"></a>\n\n`;
183
+ for (const s of rows) {
184
+ const body = String(s.bodyMarkdown || '')
185
+ .replace(/^\uFEFF/, '')
186
+ .replace(/^[\r\n]+/, '');
187
+ md += `<a id="${s.anchorId}"></a>\n\n### ${s.title}\n\n${body}\n\n---\n\n`;
188
+ }
189
+ return md;
190
+ }
191
+
192
+ /**
193
+ * Render complete document model to Markdown
194
+ *
195
+ * @param {object} docModel Document model
196
+ * @returns {string} Markdown content
197
+ */
198
+ renderMarkdown(docModel) {
199
+ const config = this.adapter.config;
200
+ const profile = config.profile || PROFILE_ADMIN;
201
+
202
+ let markdown = '';
203
+
204
+ // Title and metadata
205
+ markdown += this.renderHeader(docModel, profile);
206
+
207
+ // Table of contents
208
+ markdown += this.renderTableOfContents(profile, docModel);
209
+
210
+ if (profile === PROFILE_ADMIN) {
211
+ const order = (docModel && docModel.adminChapterOrder) || DEFAULT_ADMIN_CHAPTER_ORDER;
212
+ for (const key of order) {
213
+ markdown += this.renderAdminMarkdownKey(docModel, key);
214
+ }
215
+ return markdown;
216
+ }
217
+
218
+ if (profile === PROFILE_USER) {
219
+ const order = (docModel && docModel.userChapterOrder) || USER_HTML_CHAPTER_KEYS;
220
+ for (const key of order) {
221
+ markdown += this.renderUserMarkdownKey(docModel, key);
222
+ }
223
+ return markdown;
224
+ }
225
+
226
+ // PROFILE_ONBOARDING
227
+ const order = (docModel && docModel.onboardingChapterOrder) || ONBOARDING_HTML_CHAPTER_KEYS;
228
+ for (const key of order) {
229
+ markdown += this.renderOnboardingMarkdownKey(docModel, key);
230
+ }
231
+ return markdown;
232
+ }
233
+
234
+ /**
235
+ * Dispatch one User-profile chapter for Markdown.
236
+ * 'manual' renders the full manualContext block (incl. mermaid, guestHelp, routines, ownerPlaybook);
237
+ * individual sub-keys return '' to avoid double rendering.
238
+ *
239
+ * @param {object} docModel
240
+ * @param {string} key
241
+ * @returns {string} Markdown fragment
242
+ */
243
+ renderUserMarkdownKey(docModel, key) {
244
+ const h = k => this.userChapterHidden(docModel, k);
245
+ switch (key) {
246
+ case 'manual': {
247
+ if (!this.manualContextVisibleForMarkdown(docModel, PROFILE_USER)) {
248
+ return '';
249
+ }
250
+ return this.renderManualContext(
251
+ docModel.manualContext,
252
+ {
253
+ skipManualCore: h('manual'),
254
+ skipMermaid: h('mermaid'),
255
+ skipMermaidAuto: h('mermaidAuto'),
256
+ skipGuestHelp: h('guestHelp'),
257
+ skipRoutines: h('routines'),
258
+ skipOwnerPlaybook: h('ownerPlaybook'),
259
+ },
260
+ {
261
+ adminTroubleshootLinks: false,
262
+ docModelForSnapshot: docModel,
263
+ troubleshootOmitProfile: 'user',
264
+ },
265
+ );
266
+ }
267
+ case 'mermaid':
268
+ case 'guestHelp':
269
+ case 'routines':
270
+ case 'ownerPlaybook':
271
+ return ''; // rendered as part of 'manual' above
272
+ case 'ai': {
273
+ const aiBlock = docModel.ai?.user;
274
+ if (!aiBlock || h('ai')) {
275
+ return '';
276
+ }
277
+ return this.renderAiSection(aiBlock);
278
+ }
279
+ case 'atAGlance':
280
+ if (h('atAGlance') || !(docModel.quickStart && docModel.quickStart.hasContent)) {
281
+ return '';
282
+ }
283
+ return this.renderUserAtAGlanceMarkdown(docModel);
284
+ case 'system':
285
+ if (h('system')) {
286
+ return '';
287
+ }
288
+ return this.renderSystemChapter(docModel, PROFILE_USER);
289
+ case 'adapters':
290
+ if (h('adapters')) {
291
+ return '';
292
+ }
293
+ return this.renderAdaptersChapter(docModel, PROFILE_USER);
294
+ case 'rooms':
295
+ if (h('rooms')) {
296
+ return '';
297
+ }
298
+ return this.renderRoomsChapter(docModel, PROFILE_USER);
299
+ case 'scripts':
300
+ if (h('scripts')) {
301
+ return '';
302
+ }
303
+ return this.renderScriptsChapter(docModel, PROFILE_USER);
304
+ case 'custom':
305
+ return this.renderCustomSectionsMarkdown(docModel, PROFILE_USER);
306
+ case 'troubleshooting':
307
+ if (h('troubleshooting')) {
308
+ return '';
309
+ }
310
+ return this.renderTroubleshooting(docModel, PROFILE_USER);
311
+ default:
312
+ return '';
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Dispatch one Onboarding-profile chapter for Markdown.
318
+ *
319
+ * @param {object} docModel
320
+ * @param {string} key
321
+ * @returns {string} Markdown fragment
322
+ */
323
+ renderOnboardingMarkdownKey(docModel, key) {
324
+ const h = k => this.onboardingChapterHidden(docModel, k);
325
+ switch (key) {
326
+ case 'welcome':
327
+ return ''; // header rendered before the loop
328
+ case 'quickstart':
329
+ if (h('quickstart')) {
330
+ return '';
331
+ }
332
+ return this.renderQuickStart(docModel);
333
+ case 'manual': {
334
+ if (!this.manualContextVisibleForMarkdown(docModel, PROFILE_ONBOARDING)) {
335
+ return '';
336
+ }
337
+ return this.renderManualContext(
338
+ docModel.manualContext,
339
+ {
340
+ skipManualCore: h('manual'),
341
+ skipMermaid: h('mermaid'),
342
+ skipMermaidAuto: true, // auto-topology never shown in onboarding
343
+ skipGuestHelp: h('guestHelp'),
344
+ skipRoutines: h('routines'),
345
+ skipOwnerPlaybook: h('ownerPlaybook'),
346
+ },
347
+ {
348
+ adminTroubleshootLinks: false,
349
+ docModelForSnapshot: docModel,
350
+ troubleshootOmitProfile: 'onboarding',
351
+ },
352
+ );
353
+ }
354
+ case 'mermaid':
355
+ case 'guestHelp':
356
+ case 'routines':
357
+ case 'ownerPlaybook':
358
+ return ''; // rendered as part of 'manual' above
359
+ case 'ai': {
360
+ const aiBlock = docModel.ai?.onboarding;
361
+ if (!aiBlock || h('ai')) {
362
+ return '';
363
+ }
364
+ return this.renderAiSection(aiBlock);
365
+ }
366
+ case 'system':
367
+ if (h('system')) {
368
+ return '';
369
+ }
370
+ return this.renderSystemChapter(docModel, PROFILE_ONBOARDING);
371
+ case 'adapters':
372
+ if (h('adapters')) {
373
+ return '';
374
+ }
375
+ return this.renderAdaptersChapter(docModel, PROFILE_ONBOARDING);
376
+ case 'custom':
377
+ return this.renderCustomSectionsMarkdown(docModel, PROFILE_ONBOARDING);
378
+ default:
379
+ return '';
380
+ }
381
+ }
382
+
383
+ /**
384
+ * @param {object} docModel
385
+ * @param {string} key
386
+ * @returns {string} Markdown fragment (admin profile)
387
+ */
388
+ renderAdminMarkdownKey(docModel, key) {
389
+ switch (key) {
390
+ case 'manual': {
391
+ if (this.adminChapterHidden(docModel, 'manual') || !this.manualContextHasPublicFields(docModel)) {
392
+ return '';
393
+ }
394
+ return this.renderManualContext(
395
+ docModel.manualContext,
396
+ {
397
+ skipMermaid: this.adminChapterHidden(docModel, 'mermaid'),
398
+ skipMermaidAuto: this.adminChapterHidden(docModel, 'mermaidAuto'),
399
+ },
400
+ {
401
+ adminTroubleshootLinks: true,
402
+ troubleshootOmitProfile: 'admin',
403
+ },
404
+ );
405
+ }
406
+ case 'system': {
407
+ if (this.adminChapterHidden(docModel, 'system')) {
408
+ return '';
409
+ }
410
+ return this.renderSystemChapter(docModel, PROFILE_ADMIN);
411
+ }
412
+ case 'adapters': {
413
+ if (this.adminChapterHidden(docModel, 'adapters')) {
414
+ return '';
415
+ }
416
+ return this.renderAdaptersChapter(docModel, PROFILE_ADMIN);
417
+ }
418
+ case 'rooms': {
419
+ if (this.adminChapterHidden(docModel, 'rooms')) {
420
+ return '';
421
+ }
422
+ return this.renderRoomsChapter(docModel, PROFILE_ADMIN);
423
+ }
424
+ case 'scripts': {
425
+ if (this.adminChapterHidden(docModel, 'scripts')) {
426
+ return '';
427
+ }
428
+ return this.renderScriptsChapter(docModel, PROFILE_ADMIN);
429
+ }
430
+ case 'schedule': {
431
+ if (
432
+ this.adminChapterHidden(docModel, 'schedule') ||
433
+ !docModel.scheduleObjects ||
434
+ docModel.scheduleObjects.length === 0
435
+ ) {
436
+ return '';
437
+ }
438
+ return this.renderScheduleObjectsChapter(docModel);
439
+ }
440
+ case 'userdata': {
441
+ if (
442
+ this.adminChapterHidden(docModel, 'userdata') ||
443
+ !docModel.userData ||
444
+ docModel.userData.length === 0
445
+ ) {
446
+ return '';
447
+ }
448
+ return this.renderUserDataMarkdown(docModel.userData);
449
+ }
450
+ case 'aliases': {
451
+ if (
452
+ this.adminChapterHidden(docModel, 'aliases') ||
453
+ !docModel.aliases ||
454
+ docModel.aliases.length === 0
455
+ ) {
456
+ return '';
457
+ }
458
+ return this.renderAliasMarkdown(docModel.aliases);
459
+ }
460
+ case 'maintenance': {
461
+ if (this.adminChapterHidden(docModel, 'maintenance')) {
462
+ return '';
463
+ }
464
+ return this.renderMaintenanceChapter(docModel);
465
+ }
466
+ case 'troubleshooting': {
467
+ if (this.adminChapterHidden(docModel, 'troubleshooting')) {
468
+ return '';
469
+ }
470
+ return this.renderTroubleshooting(docModel, PROFILE_ADMIN);
471
+ }
472
+ case 'appendices': {
473
+ if (this.adminChapterHidden(docModel, 'appendices')) {
474
+ return '';
475
+ }
476
+ return this.renderAppendices(docModel);
477
+ }
478
+ case 'custom': {
479
+ return this.renderCustomSectionsMarkdown(docModel, PROFILE_ADMIN);
480
+ }
481
+ case 'diagnosis': {
482
+ if (this.adminChapterHidden(docModel, 'diagnosis')) {
483
+ return '';
484
+ }
485
+ return this.renderDiagnosis(docModel);
486
+ }
487
+ case 'changelog':
488
+ return '';
489
+ default:
490
+ return '';
491
+ }
492
+ }
493
+
494
+ /**
495
+ * One numbered TOC line for admin profile, or null if the chapter is omitted (hidden or no MD for key).
496
+ *
497
+ * @param {object} docModel
498
+ * @param {(k: string) => boolean} h
499
+ * @param {string} key
500
+ * @param {number} n
501
+ * @returns {string | null}
502
+ */
503
+ buildAdminTocLineForKey(docModel, h, key, n) {
504
+ const i18n = this.i18n;
505
+ switch (key) {
506
+ case 'manual': {
507
+ if (h('manual') || !this.manualContextHasPublicFields(docModel)) {
508
+ return null;
509
+ }
510
+ return `${n}. [${i18n.t('manualInformation')}](#manual-information)`;
511
+ }
512
+ case 'system': {
513
+ if (h('system')) {
514
+ return null;
515
+ }
516
+ return `${n}. [${i18n.t('systemOverview')}](#system-overview)`;
517
+ }
518
+ case 'adapters': {
519
+ if (h('adapters')) {
520
+ return null;
521
+ }
522
+ return `${n}. [${i18n.t('adapterInstances')}](#adapter-instances)`;
523
+ }
524
+ case 'rooms': {
525
+ if (h('rooms')) {
526
+ return null;
527
+ }
528
+ return `${n}. [${i18n.t('roomsAndFunctions')}](#rooms-and-functions)`;
529
+ }
530
+ case 'scripts': {
531
+ if (h('scripts')) {
532
+ return null;
533
+ }
534
+ const scriptsData = docModel.scripts || {};
535
+ const scriptList = scriptsData.scripts || [];
536
+ const scriptsWithRefs = scriptList.filter(s => s.stateRefs && s.stateRefs.length > 0);
537
+ const sharedStates = (scriptsData.stateCrossRef || []).filter(e => e.scripts && e.scripts.length > 1);
538
+ const sub = [];
539
+ if (scriptsWithRefs.length > 0) {
540
+ sub.push(` - [${i18n.t('stateReferences')}](#state-references)`);
541
+ }
542
+ if (sharedStates.length > 0) {
543
+ sub.push(` - [${i18n.t('sharedStates')}](#shared-states)`);
544
+ }
545
+ return [`${n}. [${i18n.t('scripts')}](#scripts)`, ...sub].filter(Boolean).join('\n');
546
+ }
547
+ case 'schedule': {
548
+ if (h('schedule') || !docModel.scheduleObjects || docModel.scheduleObjects.length === 0) {
549
+ return null;
550
+ }
551
+ return `${n}. [${i18n.t('scheduleTypeObjects')}](#schedule-type-objects)`;
552
+ }
553
+ case 'userdata': {
554
+ if (h('userdata') || !docModel.userData || docModel.userData.length === 0) {
555
+ return null;
556
+ }
557
+ return `${n}. [${i18n.t('userDefinedVariables')}](#userdata)`;
558
+ }
559
+ case 'aliases': {
560
+ if (h('aliases') || !docModel.aliases || docModel.aliases.length === 0) {
561
+ return null;
562
+ }
563
+ return `${n}. [${i18n.t('aliases')}](#aliases)`;
564
+ }
565
+ case 'maintenance': {
566
+ if (h('maintenance')) {
567
+ return null;
568
+ }
569
+ return `${n}. [${i18n.t('maintenance')}](#maintenance)`;
570
+ }
571
+ case 'troubleshooting': {
572
+ if (h('troubleshooting')) {
573
+ return null;
574
+ }
575
+ return `${n}. [Troubleshooting](#troubleshooting)`;
576
+ }
577
+ case 'appendices': {
578
+ if (h('appendices')) {
579
+ return null;
580
+ }
581
+ return `${n}. [${i18n.t('appendices')}](#appendices)`;
582
+ }
583
+ case 'custom': {
584
+ const customRows = (docModel && docModel.customDocSections) || [];
585
+ const customForProfile = customRows.filter(
586
+ s => !s.profiles || !s.profiles.length || s.profiles.includes(PROFILE_ADMIN),
587
+ );
588
+ if (!customForProfile.length || h('custom')) {
589
+ return null;
590
+ }
591
+ return `${n}. [${i18n.t('customDocSectionsTitle') || 'Custom sections'}](#custom-doc-sections)`;
592
+ }
593
+ case 'diagnosis': {
594
+ if (h('diagnosis')) {
595
+ return null;
596
+ }
597
+ return `${n}. [${i18n.t('diagnosis')}](#diagnosis)`;
598
+ }
599
+ case 'changelog':
600
+ return null;
601
+ default:
602
+ return null;
603
+ }
604
+ }
605
+
606
+ /**
607
+ * One numbered TOC line for User profile, or null if the chapter is omitted.
608
+ *
609
+ * @param {object} docModel
610
+ * @param {string} key
611
+ * @param {number} n Current number
612
+ * @param {object[]} customForProfile Pre-filtered custom sections for this profile
613
+ * @returns {string | null}
614
+ */
615
+ buildUserTocLineForKey(docModel, key, n, customForProfile) {
616
+ const i18n = this.i18n;
617
+ const h = k => this.userChapterHidden(docModel, k);
618
+ switch (key) {
619
+ case 'atAGlance':
620
+ if (h('atAGlance') || !(docModel.quickStart && docModel.quickStart.hasContent)) {
621
+ return null;
622
+ }
623
+ return `${n}. [${i18n.t('atAGlanceTitle')}](#at-a-glance)`;
624
+ case 'manual':
625
+ if (!this.manualContextVisibleForMarkdown(docModel, PROFILE_USER)) {
626
+ return null;
627
+ }
628
+ return `${n}. [${i18n.t('manualInformation')}](#manual-information)`;
629
+ case 'mermaid':
630
+ case 'guestHelp':
631
+ case 'routines':
632
+ case 'ownerPlaybook':
633
+ return null; // part of 'manual'
634
+ case 'ai':
635
+ return null; // rendered in header area, not in numbered TOC
636
+ case 'system':
637
+ if (h('system')) {
638
+ return null;
639
+ }
640
+ return `${n}. [${i18n.t('systemOverview')}](#system-overview)`;
641
+ case 'adapters':
642
+ if (h('adapters')) {
643
+ return null;
644
+ }
645
+ return `${n}. [${i18n.t('adapterInstances')}](#adapter-instances)`;
646
+ case 'rooms':
647
+ if (h('rooms')) {
648
+ return null;
649
+ }
650
+ return `${n}. [${i18n.t('roomsAndFunctions')}](#rooms-and-functions)`;
651
+ case 'scripts': {
652
+ if (h('scripts')) {
653
+ return null;
654
+ }
655
+ const scriptsData = docModel.scripts || {};
656
+ const scriptList = scriptsData.scripts || [];
657
+ const scriptsWithRefs = scriptList.filter(s => s.stateRefs && s.stateRefs.length > 0);
658
+ const sharedStates = (scriptsData.stateCrossRef || []).filter(e => e.scripts && e.scripts.length > 1);
659
+ const sub = [];
660
+ if (scriptsWithRefs.length > 0) {
661
+ sub.push(` - [${i18n.t('stateReferences')}](#state-references)`);
662
+ }
663
+ if (sharedStates.length > 0) {
664
+ sub.push(` - [${i18n.t('sharedStates')}](#shared-states)`);
665
+ }
666
+ return [`${n}. [${i18n.t('scripts')}](#scripts)`, ...sub].filter(Boolean).join('\n');
667
+ }
668
+ case 'troubleshooting':
669
+ if (h('troubleshooting')) {
670
+ return null;
671
+ }
672
+ return `${n}. [Troubleshooting](#troubleshooting)`;
673
+ case 'custom':
674
+ if (!customForProfile.length || h('custom')) {
675
+ return null;
676
+ }
677
+ return `${n}. [${i18n.t('customDocSectionsTitle') || 'Custom sections'}](#custom-doc-sections)`;
678
+ default:
679
+ return null;
680
+ }
681
+ }
682
+
683
+ /**
684
+ * One numbered TOC line for Onboarding profile, or null if the chapter is omitted.
685
+ *
686
+ * @param {object} docModel
687
+ * @param {string} key
688
+ * @param {number} n Current number
689
+ * @param {object[]} customForProfile Pre-filtered custom sections for this profile
690
+ * @returns {string | null}
691
+ */
692
+ buildOnboardingTocLineForKey(docModel, key, n, customForProfile) {
693
+ const i18n = this.i18n;
694
+ const h = k => this.onboardingChapterHidden(docModel, k);
695
+ switch (key) {
696
+ case 'welcome':
697
+ return null; // part of header
698
+ case 'ai':
699
+ return null; // rendered in header area, not in numbered TOC
700
+ case 'quickstart':
701
+ if (h('quickstart')) {
702
+ return null;
703
+ }
704
+ return `${n}. [${i18n.t('quickStart')}](#quick-start)`;
705
+ case 'system':
706
+ if (h('system')) {
707
+ return null;
708
+ }
709
+ return `${n}. [${i18n.t('systemOverview')}](#system-overview)`;
710
+ case 'adapters':
711
+ if (h('adapters')) {
712
+ return null;
713
+ }
714
+ return `${n}. [${i18n.t('adapterInstances')}](#adapter-instances)`;
715
+ case 'manual':
716
+ if (!this.manualContextVisibleForMarkdown(docModel, PROFILE_ONBOARDING)) {
717
+ return null;
718
+ }
719
+ return `${n}. [${i18n.t('manualInformation')}](#manual-information)`;
720
+ case 'mermaid':
721
+ case 'guestHelp':
722
+ case 'routines':
723
+ case 'ownerPlaybook':
724
+ return null; // part of 'manual'
725
+ case 'custom':
726
+ if (!customForProfile.length || h('custom')) {
727
+ return null;
728
+ }
729
+ return `${n}. [${i18n.t('customDocSectionsTitle') || 'Custom sections'}](#custom-doc-sections)`;
730
+ default:
731
+ return null;
732
+ }
733
+ }
734
+
735
+ /**
736
+ * Render document header
737
+ *
738
+ * @param {object} docModel Document model
739
+ * @param {string} profile Target profile
740
+ * @returns {string} Header markdown
741
+ */
742
+ renderHeader(docModel, profile) {
743
+ const config = this.adapter.config;
744
+ const i18n = this.i18n;
745
+
746
+ return `# ${i18n.t('projectDocumentation', config.projectName || 'ioBroker System')}
747
+
748
+ **${i18n.t('generated')}:** ${new Date(docModel.meta.generatedAt).toLocaleString()}
749
+ **${i18n.t('profile')}:** ${profile}
750
+ **${i18n.t('system')}:** ${config.targetSystem || 'Production'}
751
+ **${i18n.t('trigger')}:** ${docModel.meta.trigger}
752
+
753
+ ---
754
+ `;
755
+ }
756
+
757
+ /**
758
+ * Render AI-generated summary section.
759
+ *
760
+ * @param {{narrative: string, recommendations: string}} ai AI content
761
+ * @returns {string} AI section markdown
762
+ */
763
+ renderAiSection(ai) {
764
+ let md = '> **AI Summary**\n';
765
+ if (ai.narrative) {
766
+ md += `>\n> ${ai.narrative.replace(/\n/g, '\n> ')}\n`;
767
+ }
768
+ if (ai.recommendations) {
769
+ md += `>\n> **Recommendations:**\n> ${ai.recommendations.replace(/\n/g, '\n> ')}\n`;
770
+ }
771
+ md += '\n---\n';
772
+ return md;
773
+ }
774
+
775
+ /**
776
+ * Render table of contents
777
+ *
778
+ * @param {string} profile Documentation profile
779
+ * @param {object} docModel Document model
780
+ * @returns {string} Table of contents markdown
781
+ */
782
+ renderTableOfContents(profile, docModel) {
783
+ const i18n = this.i18n;
784
+ let toc = `## ${i18n.t('tableOfContents')}
785
+
786
+ `;
787
+ const h = profile === PROFILE_ADMIN ? k => this.adminChapterHidden(docModel, k) : () => false;
788
+ const customRows = (docModel && docModel.customDocSections) || [];
789
+ const customForProfile = customRows.filter(
790
+ s => !s.profiles || !s.profiles.length || s.profiles.includes(profile),
791
+ );
792
+ const customToc =
793
+ customForProfile.length > 0 ? customForProfile.map(s => `- [${s.title}](#${s.anchorId})`).join('\n') : '';
794
+
795
+ if (profile === PROFILE_ONBOARDING) {
796
+ const order = (docModel && docModel.onboardingChapterOrder) || ONBOARDING_HTML_CHAPTER_KEYS;
797
+ const lines = [];
798
+ let n = 1;
799
+ for (const k of order) {
800
+ const line = this.buildOnboardingTocLineForKey(docModel, k, n, customForProfile);
801
+ if (line) {
802
+ lines.push(line);
803
+ n += 1;
804
+ }
805
+ }
806
+ toc += `${lines.join('\n')}\n`;
807
+ const oh = k => this.onboardingChapterHidden(docModel, k);
808
+ if (customToc && !oh('custom')) {
809
+ toc += `\n${i18n.t('customDocSectionsTitle') || 'Custom sections'}:\n${customToc}\n`;
810
+ }
811
+ } else if (profile === PROFILE_USER) {
812
+ const order = (docModel && docModel.userChapterOrder) || USER_HTML_CHAPTER_KEYS;
813
+ const lines = [];
814
+ let n = 1;
815
+ for (const k of order) {
816
+ const line = this.buildUserTocLineForKey(docModel, k, n, customForProfile);
817
+ if (line) {
818
+ lines.push(line);
819
+ n += 1;
820
+ }
821
+ }
822
+ toc += `${lines.join('\n')}\n`;
823
+ const uh = k => this.userChapterHidden(docModel, k);
824
+ if (customToc && !uh('custom')) {
825
+ toc += `\n${i18n.t('customDocSectionsTitle') || 'Custom sections'}:\n${customToc}\n`;
826
+ }
827
+ } else {
828
+ // PROFILE_ADMIN — mirror {@link #renderAdminMarkdownKey} / HTML chapter order
829
+ const order = (docModel && docModel.adminChapterOrder) || DEFAULT_ADMIN_CHAPTER_ORDER;
830
+ const lines = [];
831
+ let n = 1;
832
+ for (const k of order) {
833
+ const line = this.buildAdminTocLineForKey(docModel, h, k, n);
834
+ if (line) {
835
+ lines.push(line);
836
+ n += 1;
837
+ }
838
+ }
839
+ toc += `${lines.join('\n')}\n`;
840
+ if (customToc && !h('custom')) {
841
+ toc += `\n${i18n.t('customDocSectionsTitle') || 'Custom sections'}:\n${customToc}\n`;
842
+ }
843
+ }
844
+
845
+ toc += '\n---\n';
846
+ return toc;
847
+ }
848
+
849
+ /**
850
+ * @param {object} item — quick start system line descriptor
851
+ * @param {boolean} [forOnboardingGuest] When true, apply guest privacy for script lines.
852
+ * @returns {string}
853
+ */
854
+ _formatQuickStartSystemLine(item, forOnboardingGuest) {
855
+ const i18n = this.i18n;
856
+ if (!item || !item.kind) {
857
+ return '';
858
+ }
859
+ if (forOnboardingGuest && item.kind === 'script' && !onboardingGuestShowsScriptNames(this.adapter.config)) {
860
+ return '';
861
+ }
862
+ switch (item.kind) {
863
+ case 'roomCount':
864
+ return i18n.t('qsRoomCount', item.n);
865
+ case 'function':
866
+ return i18n.t('qsFunctionRow', item.name, item.memberCount);
867
+ case 'script':
868
+ return i18n.t('qsScriptRow', item.name, item.desc);
869
+ default:
870
+ return '';
871
+ }
872
+ }
873
+
874
+ /**
875
+ * User profile — same `quickStart` model as Onboarding (Markdown anchor matches HTML id).
876
+ *
877
+ * @param {object} docModel Document model
878
+ * @returns {string} Markdown
879
+ */
880
+ renderUserAtAGlanceMarkdown(docModel) {
881
+ const i18n = this.i18n;
882
+ const qs = docModel.quickStart || { hasContent: false, systemItems: [], roomGuides: [] };
883
+ if (!qs.hasContent) {
884
+ return '';
885
+ }
886
+ const showRoomsLink = (qs.roomGuides || []).length > 0 && !this.userChapterHidden(docModel, 'rooms');
887
+ const roomsCrossMd = showRoomsLink
888
+ ? `${i18n.t('qsSeeFullRoomsBefore')}[${i18n.t('roomsAndFunctions')}](#rooms-and-functions)${i18n.t('qsSeeFullRoomsAfter')}\n\n`
889
+ : '';
890
+ const sysBullets = (qs.systemItems || [])
891
+ .map(it => {
892
+ const line = this._formatQuickStartSystemLine(it, false);
893
+ return line ? `- ${mdEscapePlainLine(line)}` : '';
894
+ })
895
+ .filter(Boolean)
896
+ .join('\n');
897
+ const roomBits = (qs.roomGuides || [])
898
+ .map(rg => {
899
+ const head = `#### ${mdEscapePlainLine(rg.name)} (${i18n.t('qsRoomCardDevices', rg.deviceCount)})`;
900
+ const hi = (rg.highlights || [])
901
+ .map(h => {
902
+ const val = h.valueText ? ` — ${mdEscapePlainLine(h.valueText)}` : '';
903
+ return `- ${h.icon || '📦'} **${mdEscapeBoldInner(h.deviceName)}**${val}`;
904
+ })
905
+ .join('\n');
906
+ return `${head}\n\n${hi}`;
907
+ })
908
+ .join('\n\n');
909
+ let md = `## ${i18n.t('atAGlanceTitle')}
910
+
911
+ <a id="at-a-glance"></a>
912
+
913
+ ${i18n.t('atAGlanceIntro')}
914
+
915
+ ${roomsCrossMd}`;
916
+ if (sysBullets) {
917
+ md += `### ${i18n.t('qsSystemTitle')}
918
+
919
+ ${sysBullets}
920
+
921
+ `;
922
+ }
923
+ if (roomBits) {
924
+ md += `### ${i18n.t('qsRoomGuidesTitle')}
925
+
926
+ ${roomBits}
927
+
928
+ `;
929
+ }
930
+ md += '---\n\n';
931
+ return md;
932
+ }
933
+
934
+ /**
935
+ * Render quick start section for Onboarding profile
936
+ *
937
+ * @param {object} docModel Document model
938
+ * @returns {string} Quick start markdown
939
+ */
940
+ renderQuickStart(docModel) {
941
+ const i18n = this.i18n;
942
+ const system = docModel.system;
943
+ const stats = system.statistics;
944
+ const fullQs = docModel.quickStart || { hasContent: false, systemItems: [], roomGuides: [] };
945
+ const qs = sliceQuickStartForOnboarding(fullQs);
946
+
947
+ let structured = '';
948
+ if (qs.hasContent) {
949
+ const sysBullets = (qs.systemItems || [])
950
+ .map(it => {
951
+ const line = this._formatQuickStartSystemLine(it, true);
952
+ return line ? `- ${mdEscapePlainLine(line)}` : '';
953
+ })
954
+ .filter(Boolean)
955
+ .join('\n');
956
+ const roomBits = (qs.roomGuides || [])
957
+ .map(rg => {
958
+ const head = `#### ${mdEscapePlainLine(rg.name)} (${i18n.t('qsRoomCardDevices', rg.deviceCount)})`;
959
+ const hi = (rg.highlights || [])
960
+ .map(h => {
961
+ const val = h.valueText ? ` — ${mdEscapePlainLine(h.valueText)}` : '';
962
+ return `- ${h.icon || '📦'} **${mdEscapeBoldInner(h.deviceName)}**${val}`;
963
+ })
964
+ .join('\n');
965
+ return `${head}\n\n${hi}`;
966
+ })
967
+ .join('\n\n');
968
+ const parts = [i18n.t('quickStartStructuredIntro')];
969
+ if (sysBullets) {
970
+ parts.push(`### ${i18n.t('qsSystemTitle')}\n\n${sysBullets}`);
971
+ }
972
+ if (roomBits) {
973
+ parts.push(`### ${i18n.t('qsRoomGuidesTitle')}\n\n${roomBits}`);
974
+ }
975
+ structured = `${parts.join('\n\n')}\n\n`;
976
+ }
977
+
978
+ return `## ${i18n.t('quickStart')}
979
+
980
+ <a id="quick-start"></a>
981
+
982
+ ${i18n.t('quickStartWelcome')}
983
+
984
+ ${structured}
985
+ ### ${i18n.t('systemStatistics')}
986
+ - **${i18n.t('activeAdapters')}:** ${stats.enabledInstanceCount}
987
+ - **${i18n.t('totalInstances')}:** ${stats.instanceCount}
988
+
989
+ ### ${i18n.t('nextSteps')}
990
+ 1. ${i18n.t('nextStepsOnboarding1')}
991
+ 2. ${i18n.t('nextStepsOnboarding2')}
992
+ 3. ${i18n.t('nextStepsOnboarding3')}
993
+
994
+ ---
995
+ `;
996
+ }
997
+
998
+ /**
999
+ * Render system overview chapter with profile-aware detail level
1000
+ *
1001
+ * @param {object} docModel Document model
1002
+ * @param {string} profile Documentation profile
1003
+ * @returns {string} System chapter markdown
1004
+ */
1005
+ renderSystemChapter(docModel, profile) {
1006
+ const system = docModel.system;
1007
+ const stats = system.statistics;
1008
+ const i18n = this.i18n;
1009
+
1010
+ let markdown = `## ${i18n.t('systemOverview')}
1011
+
1012
+ ### ${i18n.t('projectInformation')}
1013
+ - **${i18n.t('projectName')}:** ${system.projectName}
1014
+ - **${i18n.t('targetSystem')}:** ${system.targetSystem}
1015
+
1016
+ ### ${i18n.t('primaryHost')}
1017
+ - **${i18n.t('name')}:** ${system.primaryHost.name}
1018
+ - **${i18n.t('hostRuntimePlatform')}:** ${system.primaryHost.platform}
1019
+ - **${i18n.t('version')}:** ${system.primaryHost.version}
1020
+ ${system.primaryHost.nodeVersion ? `- **${i18n.t('nodeVersion')}:** ${system.primaryHost.nodeVersion}` : ''}
1021
+ ${system.primaryHost.npmVersion ? `- **${i18n.t('npmVersion')}:** ${system.primaryHost.npmVersion}` : ''}
1022
+ ${system.primaryHost.operatingSystem ? `- **${i18n.t('operatingSystem')}:** ${system.primaryHost.operatingSystem}` : ''}
1023
+
1024
+ ### ${i18n.t('systemStatistics')}
1025
+ - **${i18n.t('totalAdapterInstances')}:** ${stats.instanceCount}
1026
+ - **${i18n.t('enabledInstances')}:** ${stats.enabledInstanceCount}
1027
+ - **${i18n.t('disabledInstances')}:** ${stats.disabledInstanceCount}
1028
+ `;
1029
+
1030
+ // Admin profile: Show all details
1031
+ if (this.shouldShowDetail(profile, 'admin')) {
1032
+ markdown += `- **${i18n.t('totalStateObjects')}:** ${stats.totalStateObjects}
1033
+ - **${i18n.t('writableStates')}:** ${stats.writableStateObjects}
1034
+ - **${i18n.t('readOnlyStates')}:** ${stats.readonlyStateObjects}
1035
+
1036
+ ### ${i18n.t('hosts')}
1037
+ ${system.hosts
1038
+ .map(host => {
1039
+ const osLine = formatOperatingSystemLine(host);
1040
+ let line = `- **${host.name}** — ${i18n.t('hostRuntimePlatform')}: ${host.platform}, ${i18n.t('operatingSystem')}: ${osLine || '—'} — js-controller ${host.version}`;
1041
+ if (host.nodeVersion) {
1042
+ line += `, Node ${host.nodeVersion}`;
1043
+ }
1044
+ if (host.npmVersion) {
1045
+ line += `, npm ${host.npmVersion}`;
1046
+ }
1047
+ return line;
1048
+ })
1049
+ .join('\n')}
1050
+ `;
1051
+ }
1052
+
1053
+ markdown += '\n---\n';
1054
+ return markdown;
1055
+ }
1056
+
1057
+ /**
1058
+ * Render adapters chapter with profile-aware details
1059
+ *
1060
+ * @param {object} docModel Document model
1061
+ * @param {string} profile Documentation profile
1062
+ * @returns {string} Adapters chapter markdown
1063
+ */
1064
+ renderAdaptersChapter(docModel, profile) {
1065
+ const adapters = docModel.adapters;
1066
+ const config = this.adapter.config;
1067
+ const i18n = this.i18n;
1068
+ const totalInstances = adapters.adapters.reduce((sum, a) => sum + a.totalInstances, 0);
1069
+
1070
+ let markdown = `## ${i18n.t('adapterInstances')}
1071
+
1072
+ - **${i18n.t('totalAdapters')}:** ${adapters.totalAdapters}
1073
+ - **${i18n.t('totalInstances')}:** ${totalInstances}
1074
+
1075
+ `;
1076
+
1077
+ if (profile === PROFILE_ADMIN) {
1078
+ const enabledAdapters = adapters.adapters.filter(a => a.enabledInstances > 0);
1079
+ const disabledAdapters = adapters.adapters.filter(a => a.enabledInstances === 0);
1080
+
1081
+ const sectionLabel = `${i18n.t('adapterDetails')} (${enabledAdapters.length}\u00a0${i18n.t('enabledShort')}${disabledAdapters.length > 0 ? `, ${disabledAdapters.length}\u00a0${i18n.t('disabled')}` : ''})`;
1082
+ markdown += `<details>\n<summary><strong>${sectionLabel}</strong></summary>\n\n`;
1083
+ // Compact overview table
1084
+ markdown += `| ${i18n.t('name')} | ${i18n.t('description')} | | ${i18n.t('totalInstances')} / ${i18n.t('enabledShort')} |\n`;
1085
+ markdown += `|---|---|---|---|\n`;
1086
+ for (const adapter of enabledAdapters) {
1087
+ const displayName =
1088
+ adapter.title && adapter.title !== adapter.name
1089
+ ? `**${adapter.title}** \`${adapter.name}\``
1090
+ : `**${adapter.name}**`;
1091
+ const badges = [];
1092
+ if (adapter.connectionType && adapter.connectionType !== 'none' && adapter.connectionType !== '') {
1093
+ badges.push(
1094
+ adapter.connectionType === 'local'
1095
+ ? i18n.t('connTypeLocal')
1096
+ : adapter.connectionType === 'cloud'
1097
+ ? i18n.t('connTypeCloud')
1098
+ : adapter.connectionType,
1099
+ );
1100
+ }
1101
+ if (
1102
+ adapter.dataSource &&
1103
+ adapter.dataSource !== 'none' &&
1104
+ adapter.dataSource !== '' &&
1105
+ adapter.dataSource !== 'assumption'
1106
+ ) {
1107
+ badges.push(
1108
+ adapter.dataSource === 'push'
1109
+ ? i18n.t('dataPush')
1110
+ : adapter.dataSource === 'poll'
1111
+ ? i18n.t('dataPoll')
1112
+ : adapter.dataSource,
1113
+ );
1114
+ }
1115
+ if (adapter.tier) {
1116
+ badges.push(
1117
+ adapter.tier === 1
1118
+ ? i18n.t('tierStable')
1119
+ : adapter.tier === 2
1120
+ ? i18n.t('tierTested')
1121
+ : i18n.t('tierExperimental'),
1122
+ );
1123
+ }
1124
+ const badgeStr = badges.length > 0 ? badges.join(' · ') : '';
1125
+ const desc = adapter.desc || '—';
1126
+ markdown += `| ${displayName} | ${desc} | ${badgeStr} | ${adapter.totalInstances} / ${adapter.enabledInstances} |\n`;
1127
+ }
1128
+ markdown += '\n';
1129
+
1130
+ // Instance details for enabled adapters
1131
+ if (!config.hideInstanceDetailsInMarkdown) {
1132
+ for (const adapter of enabledAdapters) {
1133
+ const displayName =
1134
+ adapter.title && adapter.title !== adapter.name
1135
+ ? `${adapter.title} (\`${adapter.name}\`)`
1136
+ : `\`${adapter.name}\``;
1137
+ markdown += `#### ${displayName}\n`;
1138
+ for (const instance of adapter.instances) {
1139
+ const bits = [
1140
+ `\`${instance.id}\` (${instance.enabled ? i18n.t('enabled') : i18n.t('disabled')}) v${instance.version || '?'}`,
1141
+ ];
1142
+ if (instance.mode && instance.mode !== 'daemon') {
1143
+ bits.push(`${i18n.t('instanceRunMode')}: ${instance.mode}`);
1144
+ }
1145
+ if (instance.scheduleCron && String(instance.scheduleCron).trim()) {
1146
+ bits.push(`${i18n.t('instanceScheduleCron')}: \`${instance.scheduleCron}\``);
1147
+ }
1148
+ if (instance.restartSchedule && String(instance.restartSchedule).trim()) {
1149
+ bits.push(`${i18n.t('instanceRestartCron')}: \`${instance.restartSchedule}\``);
1150
+ }
1151
+ markdown += ` - ${bits.join(' — ')}\n`;
1152
+ }
1153
+ const manualNote =
1154
+ docModel.manualContext &&
1155
+ docModel.manualContext.adapters &&
1156
+ docModel.manualContext.adapters[adapter.name];
1157
+ if (manualNote) {
1158
+ markdown += `\n > ${manualNote}\n`;
1159
+ }
1160
+ markdown += '\n';
1161
+ }
1162
+ }
1163
+
1164
+ // Disabled adapters in collapsible block
1165
+ if (disabledAdapters.length > 0) {
1166
+ const disabledLabel = i18n.t('disabledAdaptersGroup').replace('{0}', disabledAdapters.length);
1167
+ markdown += `<details>\n<summary>${disabledLabel}</summary>\n\n`;
1168
+ markdown += `| ${i18n.t('name')} | ${i18n.t('description')} | | ${i18n.t('totalInstances')} |\n`;
1169
+ markdown += `|---|---|---|---|\n`;
1170
+ for (const adapter of disabledAdapters) {
1171
+ const displayName =
1172
+ adapter.title && adapter.title !== adapter.name
1173
+ ? `**${adapter.title}** \`${adapter.name}\``
1174
+ : `**${adapter.name}**`;
1175
+ const badges = [];
1176
+ if (adapter.connectionType && adapter.connectionType !== 'none' && adapter.connectionType !== '') {
1177
+ badges.push(
1178
+ adapter.connectionType === 'local'
1179
+ ? i18n.t('connTypeLocal')
1180
+ : adapter.connectionType === 'cloud'
1181
+ ? i18n.t('connTypeCloud')
1182
+ : adapter.connectionType,
1183
+ );
1184
+ }
1185
+ if (
1186
+ adapter.dataSource &&
1187
+ adapter.dataSource !== 'none' &&
1188
+ adapter.dataSource !== '' &&
1189
+ adapter.dataSource !== 'assumption'
1190
+ ) {
1191
+ badges.push(
1192
+ adapter.dataSource === 'push'
1193
+ ? i18n.t('dataPush')
1194
+ : adapter.dataSource === 'poll'
1195
+ ? i18n.t('dataPoll')
1196
+ : adapter.dataSource,
1197
+ );
1198
+ }
1199
+ if (adapter.tier) {
1200
+ badges.push(
1201
+ adapter.tier === 1
1202
+ ? i18n.t('tierStable')
1203
+ : adapter.tier === 2
1204
+ ? i18n.t('tierTested')
1205
+ : i18n.t('tierExperimental'),
1206
+ );
1207
+ }
1208
+ const badgeStr = badges.length > 0 ? badges.join(' · ') : '';
1209
+ const desc = adapter.desc || '—';
1210
+ markdown += `| ${displayName} | ${desc} | ${badgeStr} | ${adapter.totalInstances} |\n`;
1211
+ }
1212
+ markdown += `\n</details>\n\n`;
1213
+ }
1214
+ markdown += `\n</details>\n\n`;
1215
+ } else if (profile === PROFILE_USER) {
1216
+ // User: compact table — active adapters only, description prominent
1217
+ const activeCount = adapters.adapters.filter(a => a.enabledInstances > 0).length;
1218
+ const userLabel = `${i18n.t('adapterDetails')} (${activeCount}\u00a0${i18n.t('enabledShort')})`;
1219
+ markdown += `<details>\n<summary><strong>${userLabel}</strong></summary>\n\n`;
1220
+ markdown += `| ${i18n.t('name')} | ${i18n.t('description')} |\n`;
1221
+ markdown += `|---|---|\n`;
1222
+ for (const adapter of adapters.adapters) {
1223
+ if (adapter.enabledInstances === 0) {
1224
+ continue;
1225
+ }
1226
+ const displayName = adapter.title && adapter.title !== adapter.name ? adapter.title : adapter.name;
1227
+ const desc = adapter.desc || '—';
1228
+ const manualNote =
1229
+ docModel.manualContext &&
1230
+ docModel.manualContext.adapters &&
1231
+ docModel.manualContext.adapters[adapter.name]
1232
+ ? ` — _${docModel.manualContext.adapters[adapter.name]}_`
1233
+ : '';
1234
+ markdown += `| **${displayName}** | ${desc}${manualNote} |\n`;
1235
+ }
1236
+ markdown += `\n</details>\n\n`;
1237
+ } else if (profile === PROFILE_ONBOARDING) {
1238
+ // Onboarding: simple bullet list, welcoming tone
1239
+ const obCount = adapters.adapters.filter(a => a.enabledInstances > 0).length;
1240
+ const obLabel = `${i18n.t('adapterDetails')} (${obCount}\u00a0${i18n.t('enabledShort')})`;
1241
+ markdown += `<details>\n<summary><strong>${obLabel}</strong></summary>\n\n`;
1242
+ for (const adapter of adapters.adapters) {
1243
+ if (adapter.enabledInstances === 0) {
1244
+ continue;
1245
+ }
1246
+ const displayName = adapter.title && adapter.title !== adapter.name ? adapter.title : adapter.name;
1247
+ const desc = adapter.desc ? ` — ${adapter.desc}` : '';
1248
+ markdown += `- **${displayName}**${desc}\n`;
1249
+ }
1250
+ markdown += `\n</details>\n\n`;
1251
+ }
1252
+
1253
+ markdown += '---\n';
1254
+ return markdown;
1255
+ }
1256
+
1257
+ /**
1258
+ * Render rooms and functions chapter
1259
+ *
1260
+ * @param {object} docModel Document model
1261
+ * @param {string} profile Documentation profile
1262
+ * @returns {string} Rooms chapter markdown
1263
+ */
1264
+ renderRoomsChapter(docModel, profile) {
1265
+ const roomsData = docModel.rooms;
1266
+ const i18n = this.i18n;
1267
+
1268
+ let markdown = `## ${i18n.t('roomsAndFunctions')}
1269
+
1270
+ ### ${i18n.t('overview')}
1271
+ - **${i18n.t('totalRooms')}:** ${roomsData.totalRooms}
1272
+ - **${i18n.t('totalFunctions')}:** ${roomsData.totalFunctions}
1273
+
1274
+ `;
1275
+
1276
+ if (roomsData.totalRooms === 0) {
1277
+ markdown += `_${i18n.t('noRoomsDefined')}_\n\n`;
1278
+ } else {
1279
+ markdown += `### ${i18n.t('rooms')}\n\n`;
1280
+ for (const room of roomsData.rooms) {
1281
+ markdown += `#### ${room.name}\n`;
1282
+ markdown += `- **${i18n.t('memberCount')}:** ${room.memberCount}\n`;
1283
+
1284
+ // Admin: list individual members with their functions
1285
+ if (profile === PROFILE_ADMIN && room.devices.length > 0) {
1286
+ for (const member of room.devices) {
1287
+ const fnText = member.functions.length > 0 ? ` _(${member.functions.join(', ')})_` : '';
1288
+ markdown += ` - \`${member.id}\`${fnText}\n`;
1289
+ }
1290
+ }
1291
+ markdown += '\n';
1292
+ }
1293
+
1294
+ // Admin: also list functions
1295
+ if (profile === PROFILE_ADMIN && roomsData.functions.length > 0) {
1296
+ markdown += `### ${i18n.t('functions')}\n\n`;
1297
+ for (const fn of roomsData.functions) {
1298
+ markdown += `- **${fn.name}** — ${fn.memberCount} ${i18n.t('memberCount')}\n`;
1299
+ }
1300
+ markdown += '\n';
1301
+ }
1302
+ }
1303
+
1304
+ markdown += '---\n';
1305
+ return markdown;
1306
+ }
1307
+
1308
+ /**
1309
+ * Render scripts chapter
1310
+ *
1311
+ * @param {object} docModel Document model
1312
+ * @param {string} profile Documentation profile
1313
+ * @returns {string} Scripts chapter markdown
1314
+ */
1315
+ renderScriptsChapter(docModel, profile) {
1316
+ const scriptsData = docModel.scripts;
1317
+ const i18n = this.i18n;
1318
+
1319
+ let markdown = `## ${i18n.t('scripts')}
1320
+
1321
+ <a id="scripts"></a>
1322
+
1323
+ ### ${i18n.t('overview')}
1324
+ - **${i18n.t('totalScripts')}:** ${scriptsData.totalScripts}
1325
+ - **${i18n.t('enabledScripts')}:** ${scriptsData.enabledScripts}
1326
+ - **${i18n.t('disabledScripts')}:** ${scriptsData.disabledScripts}
1327
+
1328
+ `;
1329
+
1330
+ if (
1331
+ (profile === PROFILE_USER || profile === PROFILE_ONBOARDING) &&
1332
+ scriptsData.aiAutomationOverview &&
1333
+ String(scriptsData.aiAutomationOverview).trim()
1334
+ ) {
1335
+ markdown += `### ${i18n.t('automationOverviewAi')}\n\n${scriptsData.aiAutomationOverview}\n\n`;
1336
+ }
1337
+
1338
+ if (scriptsData.totalScripts === 0) {
1339
+ markdown += `_${i18n.t('noScriptsDefined')}_\n\n`;
1340
+ } else {
1341
+ const list = profile === PROFILE_USER ? scriptsData.scripts.filter(s => s.enabled) : scriptsData.scripts;
1342
+
1343
+ const emitScript = script => {
1344
+ const statusMark = script.enabled ? '✅' : '⏸';
1345
+ markdown += `#### ${statusMark} ${script.name}`;
1346
+ if (profile !== PROFILE_ADMIN) {
1347
+ const folderLabel = this.scriptFolderLabel(script.folder);
1348
+ markdown += ` _(${folderLabel})_`;
1349
+ }
1350
+ markdown += '\n';
1351
+
1352
+ if (script.desc) {
1353
+ markdown += `${script.desc}\n`;
1354
+ }
1355
+ if (script.aiSummary && profile !== PROFILE_ADMIN) {
1356
+ markdown += `> **${i18n.t('scriptAiSummary')}:** ${script.aiSummary}\n\n`;
1357
+ }
1358
+
1359
+ if (profile === PROFILE_ADMIN) {
1360
+ markdown += `- **${i18n.t('scriptTrigger')}:** ${script.triggerType}
1361
+ - **${i18n.t('scriptStatus')}:** ${script.enabled ? i18n.t('active') : i18n.t('inactive')}
1362
+ `;
1363
+ if (script.engine && String(script.engine).trim()) {
1364
+ markdown += `- **${i18n.t('scriptEngineInstance')}:** ${script.engine}\n`;
1365
+ }
1366
+ }
1367
+ markdown += '\n';
1368
+ };
1369
+
1370
+ if (profile === PROFILE_ADMIN) {
1371
+ markdown += `${i18n.t('scriptsByFolderIntro')}\n\n`;
1372
+ for (const g of groupScriptsByFolder(list)) {
1373
+ const folderTitle = g.folder == null ? i18n.t('scriptFolderRoot') : g.folder;
1374
+ const count = g.scripts.length;
1375
+ markdown += '<details>\n<summary>';
1376
+ markdown += `**${folderTitle}** (${count})`;
1377
+ markdown += '</summary>\n\n';
1378
+ if (isGlobalFolderKey(g.folderKey)) {
1379
+ markdown += `*${i18n.t('scriptsGlobalFolderHint')}*\n\n`;
1380
+ }
1381
+ for (const script of g.scripts) {
1382
+ emitScript(script);
1383
+ }
1384
+ markdown += '\n</details>\n\n';
1385
+ }
1386
+
1387
+ const scriptsWithRefs = list.filter(s => s.stateRefs && s.stateRefs.length > 0);
1388
+ if (scriptsWithRefs.length > 0) {
1389
+ const refTotal = scriptsWithRefs.reduce(
1390
+ (n, s) => n + (s.stateRefs && s.stateRefs.length ? s.stateRefs.length : 0),
1391
+ 0,
1392
+ );
1393
+ markdown += `<a id="state-references"></a>\n\n### ${i18n.t('stateReferences')}\n\n`;
1394
+ markdown += `${i18n.t('stateReferencesDesc')}\n\n`;
1395
+ markdown += '<details>\n<summary>';
1396
+ markdown += i18n.t('stateReferencesExpandSummary', scriptsWithRefs.length, refTotal);
1397
+ markdown += '</summary>\n\n';
1398
+ markdown += `| ${i18n.t('script')} | ${i18n.t('scriptDescription')} | ${i18n.t('referencedStates')} |\n|---|---|---|\n`;
1399
+ for (const script of scriptsWithRefs) {
1400
+ const folderLbl = this.scriptFolderLabel(script.folder);
1401
+ const nameCell = mdTableCell(`${script.name} (${folderLbl})`);
1402
+ const descRaw = script.desc && String(script.desc).trim();
1403
+ const descCell = mdTableCell(descRaw || '—');
1404
+ const refs = (script.stateRefs || []).map(r => `\`${mdTableCell(r)}\``).join(', ');
1405
+ markdown += `| ${nameCell} | ${descCell} | ${refs} |\n`;
1406
+ }
1407
+ markdown += '\n</details>\n\n';
1408
+ }
1409
+
1410
+ const sharedStates = (scriptsData.stateCrossRef || []).filter(e => e.scripts && e.scripts.length > 1);
1411
+ if (sharedStates.length > 0) {
1412
+ markdown += `<a id="shared-states"></a>\n\n### ${i18n.t('sharedStates')}\n\n`;
1413
+ markdown += `${i18n.t('sharedStatesDesc')}\n\n`;
1414
+ markdown += '<details>\n<summary>';
1415
+ markdown += i18n.t('sharedStatesExpandSummary', sharedStates.length);
1416
+ markdown += '</summary>\n\n';
1417
+ markdown += `| ${i18n.t('stateId')} | ${i18n.t('usedByScripts')} |\n|---|---|\n`;
1418
+ for (const entry of sharedStates) {
1419
+ const scriptsCol = mdTableCell(entry.scripts.join(', '));
1420
+ markdown += `| \`${mdTableCell(entry.stateId)}\` | ${scriptsCol} |\n`;
1421
+ }
1422
+ markdown += '\n</details>\n\n';
1423
+ }
1424
+ } else {
1425
+ for (const script of list) {
1426
+ emitScript(script);
1427
+ }
1428
+ }
1429
+ }
1430
+
1431
+ markdown += '---\n';
1432
+ return markdown;
1433
+ }
1434
+
1435
+ /**
1436
+ * Admin: ioBroker objects from getObjectView(system, schedule).
1437
+ *
1438
+ * @param {object} docModel
1439
+ * @returns {string}
1440
+ */
1441
+ renderScheduleObjectsChapter(docModel) {
1442
+ const list = docModel.scheduleObjects || [];
1443
+ if (list.length === 0) {
1444
+ return '';
1445
+ }
1446
+ const i18n = this.i18n;
1447
+ let md = `<a id="schedule-type-objects"></a>\n\n## ${i18n.t('scheduleTypeObjects')}\n\n${i18n.t('scheduleTypeObjectsIntro')}\n\n`;
1448
+ md += `| ${i18n.t('name')} | ${i18n.t('description')} | ${i18n.t('scriptStatus')} |\n|---|---|---|\n`;
1449
+ for (const s of list) {
1450
+ const st = s.enabled ? i18n.t('active') : i18n.t('inactive');
1451
+ md += `| \`${s.id}\` **${s.name}** | ${(s.desc || '—').replace(/\|/g, '\\|')} | ${st} |\n`;
1452
+ }
1453
+ md += '\n---\n';
1454
+ return md;
1455
+ }
1456
+
1457
+ /**
1458
+ * Markdown block: auto documentation links + optional quick-fact lines (Phase 5.x.1).
1459
+ *
1460
+ * @param {object} manualContext
1461
+ * @param {boolean} includeAdmin Include link to admin HTML (Markdown export for Admin profile)
1462
+ * @param {'admin'|'user'|'onboarding'|null} [omitLinkForProfile] Same as HTML: no self-link to the current export profile
1463
+ * @returns {string}
1464
+ */
1465
+ renderTroubleshootGuestMarkdown(manualContext, includeAdmin, omitLinkForProfile = null) {
1466
+ const i18n = this.i18n;
1467
+ const t = v => v && String(v).trim();
1468
+ let out = '';
1469
+ const pl = manualContext && manualContext.troubleshootPublicLinks;
1470
+ if (pl) {
1471
+ const items = [];
1472
+ if (includeAdmin && t(pl.admin) && omitLinkForProfile !== 'admin') {
1473
+ items.push(`- [${i18n.t('troubleshootLinkAdmin')}](${pl.admin})`);
1474
+ }
1475
+ if (t(pl.user) && omitLinkForProfile !== 'user' && omitLinkForProfile !== 'onboarding') {
1476
+ items.push(`- [${i18n.t('troubleshootLinkUser')}](${pl.user})`);
1477
+ }
1478
+ if (t(pl.onboarding) && omitLinkForProfile !== 'onboarding') {
1479
+ items.push(`- [${i18n.t('troubleshootLinkOnboarding')}](${pl.onboarding})`);
1480
+ }
1481
+ if (items.length) {
1482
+ out += `#### ${i18n.t('troubleshootPublicLinksHeading')}
1483
+
1484
+ _${i18n.t('troubleshootPublicLinksIntro')}_
1485
+
1486
+ ${items.join('\n')}
1487
+
1488
+ `;
1489
+ }
1490
+ }
1491
+ const rows = [
1492
+ [i18n.t('troubleshootWifiLabel'), t(manualContext && manualContext.troubleshootWifiHint)],
1493
+ [i18n.t('troubleshootPowerLabel'), t(manualContext && manualContext.troubleshootPowerHint)],
1494
+ [i18n.t('troubleshootWaterLabel'), t(manualContext && manualContext.troubleshootWaterHint)],
1495
+ [i18n.t('troubleshootExtraLabel'), t(manualContext && manualContext.troubleshootExtraHint)],
1496
+ ].filter(([, v]) => v);
1497
+ if (rows.length) {
1498
+ out += `#### ${i18n.t('troubleshootQuickFactsTitle')}
1499
+
1500
+ `;
1501
+ for (const [label, value] of rows) {
1502
+ out += `- **${label}** ${value}\n`;
1503
+ }
1504
+ out += '\n';
1505
+ }
1506
+ return out;
1507
+ }
1508
+
1509
+ /**
1510
+ * Family-facing checklist when Admin diagnosis would flag the Node.js runtime.
1511
+ *
1512
+ * @param {object} docModel Document model
1513
+ * @returns {string} Markdown fragment or empty
1514
+ */
1515
+ renderDiagnosisSnapshotMarkdown(docModel) {
1516
+ if (!hasFamilyDiagnosisSnapshot(docModel)) {
1517
+ return '';
1518
+ }
1519
+ const i18n = this.i18n;
1520
+ const nv = (docModel.system && docModel.system.primaryHost && docModel.system.primaryHost.nodeVersion) || '—';
1521
+ return `#### ${i18n.t('troubleshootSnapshotNodeTitle')}
1522
+
1523
+ _${i18n.t('troubleshootSnapshotDisclaimer')}_
1524
+
1525
+ \`${String(nv)}\`
1526
+
1527
+ 1. ${i18n.t('troubleshootSnapshotNodeStep1')}
1528
+ 2. ${i18n.t('troubleshootSnapshotNodeStep2')}
1529
+ 3. ${i18n.t('troubleshootSnapshotNodeStep3')}
1530
+
1531
+ `;
1532
+ }
1533
+
1534
+ /**
1535
+ * Render manual context chapter
1536
+ *
1537
+ * @param {object} manualContext Manual context from config
1538
+ * @param {{ skipManualCore?: boolean, skipMermaid?: boolean, skipMermaidAuto?: boolean, skipGuestHelp?: boolean, skipRoutines?: boolean, skipOwnerPlaybook?: boolean }} [skip]
1539
+ * @param {{ adminTroubleshootLinks?: boolean, docModelForSnapshot?: object, troubleshootOmitProfile?: 'admin'|'user'|'onboarding'|null }} [options] Admin Markdown: bookmark list; optional `docModelForSnapshot` for family diagnosis checklist; `troubleshootOmitProfile` avoids self-link for that export profile
1540
+ * @returns {string} Manual context markdown
1541
+ */
1542
+ renderManualContext(manualContext, skip = {}, options = {}) {
1543
+ const sk = {
1544
+ skipManualCore: false,
1545
+ skipMermaid: false,
1546
+ skipMermaidAuto: false,
1547
+ skipGuestHelp: false,
1548
+ skipRoutines: false,
1549
+ skipOwnerPlaybook: false,
1550
+ ...skip,
1551
+ };
1552
+ const i18n = this.i18n;
1553
+ const includeAdminLinks = options.adminTroubleshootLinks === true;
1554
+ const omitProfile = options.troubleshootOmitProfile == null ? null : options.troubleshootOmitProfile;
1555
+ const snapModel = options.docModelForSnapshot;
1556
+ const parts = [];
1557
+
1558
+ if (!sk.skipManualCore && manualContext.description) {
1559
+ parts.push(`### ${i18n.t('description')}
1560
+ ${manualContext.description}
1561
+
1562
+ `);
1563
+ }
1564
+
1565
+ if (!sk.skipManualCore && manualContext.contact) {
1566
+ parts.push(`### ${i18n.t('contact')}
1567
+ ${manualContext.contact}
1568
+
1569
+ `);
1570
+ }
1571
+
1572
+ if (!sk.skipManualCore && manualContext.notes) {
1573
+ parts.push(`### ${i18n.t('additionalNotes')}
1574
+ ${manualContext.notes}
1575
+
1576
+ `);
1577
+ }
1578
+
1579
+ if (!sk.skipGuestHelp) {
1580
+ const ts = this.renderTroubleshootGuestMarkdown(manualContext, includeAdminLinks, omitProfile);
1581
+ if (ts) {
1582
+ parts.push(ts);
1583
+ }
1584
+ if (snapModel) {
1585
+ const snap = this.renderDiagnosisSnapshotMarkdown(snapModel);
1586
+ if (snap) {
1587
+ parts.push(snap);
1588
+ }
1589
+ }
1590
+ if (manualContext.guestHelpNote && String(manualContext.guestHelpNote).trim()) {
1591
+ parts.push(`### ${i18n.t('guestHelpTitle')}
1592
+ ${manualContext.guestHelpNote}
1593
+
1594
+ `);
1595
+ }
1596
+ }
1597
+
1598
+ if (!sk.skipRoutines && manualContext.homeRoutinesNote && String(manualContext.homeRoutinesNote).trim()) {
1599
+ parts.push(`### ${i18n.t('homeRoutinesTitle')}
1600
+ _${i18n.t('homeRoutinesIntro')}_
1601
+
1602
+ ${manualContext.homeRoutinesNote}
1603
+
1604
+ `);
1605
+ }
1606
+
1607
+ if (
1608
+ !sk.skipOwnerPlaybook &&
1609
+ manualContext.ownerPlaybookNote &&
1610
+ String(manualContext.ownerPlaybookNote).trim()
1611
+ ) {
1612
+ parts.push(`### ${i18n.t('ownerPlaybookTitle')}
1613
+ _${i18n.t('ownerPlaybookIntro')}_
1614
+
1615
+ ${manualContext.ownerPlaybookNote}
1616
+
1617
+ `);
1618
+ }
1619
+
1620
+ if (!sk.skipMermaid && manualContext.mermaidDiagram && String(manualContext.mermaidDiagram).trim()) {
1621
+ const src = String(manualContext.mermaidDiagram).trim();
1622
+ parts.push(`### ${i18n.t('mermaidDiagramTitle')}
1623
+
1624
+ \`\`\`mermaid
1625
+ ${src}
1626
+ \`\`\`
1627
+
1628
+ `);
1629
+ }
1630
+
1631
+ if (
1632
+ !sk.skipMermaidAuto &&
1633
+ manualContext.autoHostTopologyMermaid &&
1634
+ String(manualContext.autoHostTopologyMermaid).trim()
1635
+ ) {
1636
+ parts.push(`### ${i18n.t('mermaidAutoTopologyTitle')}
1637
+
1638
+ > *${i18n.t('mermaidAutoTopologyMdHint') || 'Auto-Topologie nur im HTML-Export verfügbar (Admin-Profil).'}*
1639
+
1640
+ `);
1641
+ }
1642
+
1643
+ if (parts.length === 0) {
1644
+ return '';
1645
+ }
1646
+
1647
+ return `## ${i18n.t('manualInformation')}
1648
+
1649
+ ${parts.join('')}---
1650
+ `;
1651
+ }
1652
+
1653
+ /**
1654
+ * @param {object} docModel Document model (Admin manual / Markdown)
1655
+ * @returns {boolean}
1656
+ */
1657
+ manualContextHasPublicFields(docModel) {
1658
+ const mc = docModel && docModel.manualContext;
1659
+ if (!mc) {
1660
+ return false;
1661
+ }
1662
+ const t = v => v && String(v).trim();
1663
+ return !!(
1664
+ t(mc.description) ||
1665
+ t(mc.contact) ||
1666
+ t(mc.notes) ||
1667
+ t(mc.mermaidDiagram) ||
1668
+ t(mc.autoHostTopologyMermaid) ||
1669
+ t(mc.homeRoutinesNote) ||
1670
+ t(mc.ownerPlaybookNote) ||
1671
+ guestHelpChapterHasContent(mc, docModel, null)
1672
+ );
1673
+ }
1674
+
1675
+ /**
1676
+ * Render maintenance and diagnostics chapter (Admin only)
1677
+ *
1678
+ * @param {object} docModel Document model
1679
+ * @returns {string} Maintenance chapter markdown
1680
+ */
1681
+ renderMaintenanceChapter(docModel) {
1682
+ const m = docModel.maintenance;
1683
+ const i18n = this.i18n;
1684
+
1685
+ const checkLabels = {
1686
+ projectNarrativeThin: i18n.t('checklistProjectNarrative'),
1687
+ baseUrlUnset: i18n.t('checklistBaseUrlUnset'),
1688
+ checkHostsFound: i18n.t('checkHostsFound'),
1689
+ checkInstancesFound: i18n.t('checkInstancesFound'),
1690
+ checkRoomsDefined: i18n.t('checkRoomsDefined'),
1691
+ checkContactSet: i18n.t('checkContactSet'),
1692
+ checkCustomContent: i18n.t('checkCustomContent'),
1693
+ checkHasDiagram: i18n.t('checkHasDiagram'),
1694
+ checkRoomsHaveDevices: i18n.t('checkRoomsHaveDevices'),
1695
+ checkHasCustomSections: i18n.t('checkHasCustomSections'),
1696
+ checkAiConfigured: i18n.t('checkAiConfigured'),
1697
+ };
1698
+
1699
+ /**
1700
+ * @param {Array<{key:string,ok:boolean,count?:number}>} checks
1701
+ * @param {number} score
1702
+ * @param {string} title
1703
+ * @param {string} desc
1704
+ * @returns {string}
1705
+ */
1706
+ const renderDimension = (checks, score, title, desc) => {
1707
+ let s = `#### ${title} — ${score}%\n`;
1708
+ if (desc) {
1709
+ s += `_${desc}_\n\n`;
1710
+ }
1711
+ for (const item of checks) {
1712
+ const icon = item.ok ? '✅' : '⚠️';
1713
+ const label = checkLabels[item.key] || item.key;
1714
+ const countText =
1715
+ !item.ok && typeof item.count === 'number' && item.count > 0 ? ` (${item.count})` : '';
1716
+ s += `- ${icon} ${label}${countText}\n`;
1717
+ }
1718
+ return `${s}\n`;
1719
+ };
1720
+
1721
+ let markdown = `## ${i18n.t('maintenance')}
1722
+
1723
+ ### ${i18n.t('maintenanceChecklist')}
1724
+
1725
+ _${i18n.t('scoreDesc')}_
1726
+
1727
+ `;
1728
+
1729
+ if (m.scores) {
1730
+ markdown += renderDimension(
1731
+ m.scores.data.checks,
1732
+ m.scores.data.score,
1733
+ i18n.t('scoreDimData'),
1734
+ i18n.t('scoreDimDataDesc'),
1735
+ );
1736
+ markdown += renderDimension(
1737
+ m.scores.manual.checks,
1738
+ m.scores.manual.score,
1739
+ i18n.t('scoreDimManual'),
1740
+ i18n.t('scoreDimManualDesc'),
1741
+ );
1742
+ markdown += renderDimension(
1743
+ m.scores.depth.checks,
1744
+ m.scores.depth.score,
1745
+ i18n.t('scoreDimDepth'),
1746
+ i18n.t('scoreDimDepthDesc'),
1747
+ );
1748
+ }
1749
+
1750
+ markdown += `**${i18n.t('documentationScore')}: ${m.score}%**\n\n`;
1751
+
1752
+ if (m.disabledInstances.length > 0) {
1753
+ markdown += `${i18n.t('disabledInstancesInventoryNote', m.disabledInstances.length)}\n\n`;
1754
+ }
1755
+
1756
+ if (m.checklist.every(item => item.ok)) {
1757
+ markdown += `_${i18n.t('allGood')}_\n\n`;
1758
+ }
1759
+
1760
+ if (m.disabledInstances.length > 0) {
1761
+ markdown += `### ${i18n.t('disabledInstancesHint')}\n`;
1762
+ for (const inst of m.disabledInstances) {
1763
+ markdown += `- \`${inst.id}\`${inst.title && inst.title !== inst.name ? ` — ${inst.title}` : ''}\n`;
1764
+ }
1765
+ markdown += '\n';
1766
+ }
1767
+
1768
+ markdown += '---\n';
1769
+ return markdown;
1770
+ }
1771
+
1772
+ /**
1773
+ * Render diagnosis section for Admin profile (Markdown parity with htmlRenderer.renderDiagnosis).
1774
+ * Omits interactive elements (forum copy button, JS); otherwise mirrors scan status + findings.
1775
+ *
1776
+ * @param {object} docModel Document model
1777
+ * @returns {string} Diagnosis markdown
1778
+ */
1779
+ renderDiagnosis(docModel) {
1780
+ const i18n = this.i18n;
1781
+ const system = docModel.system;
1782
+ const stats = system.statistics;
1783
+ const appendices = docModel.appendices;
1784
+ const primaryHostName = system.primaryHost.name;
1785
+ const hostRes = (system.hostResources || {})[primaryHostName] || {};
1786
+
1787
+ /* RAM */
1788
+ let ramText = '—';
1789
+ if (hostRes.sysTotalMb && hostRes.sysFreeMb != null) {
1790
+ ramText = `${hostRes.sysTotalMb - hostRes.sysFreeMb} / ${hostRes.sysTotalMb} MB`;
1791
+ } else if (hostRes.adapterTotalMb) {
1792
+ ramText = `~${hostRes.adapterTotalMb} MB (${i18n.t('allAdapters') || 'alle Adapter'})`;
1793
+ } else if (hostRes.procMb) {
1794
+ ramText = `~${hostRes.procMb} MB (js-controller)`;
1795
+ }
1796
+ const cpuText = hostRes.cpu != null ? `${hostRes.cpu} %` : null;
1797
+ const activeRepo = (system.location && system.location.activeRepo) || '';
1798
+
1799
+ /* Scan status rows */
1800
+ const rows = [
1801
+ [i18n.t('collectedAt'), new Date(appendices.collectionTimestamp).toLocaleString()],
1802
+ [
1803
+ i18n.t('instancesDetected'),
1804
+ `${stats.instanceCount} (${stats.enabledInstanceCount} ${i18n.t('diagActive')}, ${stats.disabledInstanceCount} ${i18n.t('diagInactive')})`,
1805
+ ],
1806
+ [
1807
+ i18n.t('stateObjectsScanned'),
1808
+ `${appendices.stateSummary.total} (${appendices.stateSummary.writable} ${i18n.t('writable')}, ${appendices.stateSummary.readonly} ${i18n.t('readOnlyStates')})`,
1809
+ ],
1810
+ [i18n.t('hostRuntimePlatform'), system.primaryHost.platform || '—'],
1811
+ [i18n.t('operatingSystem'), system.primaryHost.operatingSystem || '—'],
1812
+ [i18n.t('jsControllerVersion'), system.primaryHost.version || '—'],
1813
+ [i18n.t('nodeVersion'), system.primaryHost.nodeVersion || '—'],
1814
+ [i18n.t('npmVersion'), system.primaryHost.npmVersion || '—'],
1815
+ ['RAM', ramText],
1816
+ ...(cpuText ? [['CPU', cpuText]] : []),
1817
+ [i18n.t('hosts'), primaryHostName],
1818
+ ...(activeRepo ? [[i18n.t('activeRepo') || 'Repository', activeRepo]] : []),
1819
+ ];
1820
+
1821
+ const tableRows = rows.map(([k, v]) => `| ${k} | ${v} |`).join('\n');
1822
+
1823
+ /* Findings */
1824
+ const findings = [];
1825
+ if (isNodeVersionFlaggedForDiagnosis(system.primaryHost.nodeVersion)) {
1826
+ findings.push(i18n.t('nodeVersionOutdated').replace('{0}', system.primaryHost.nodeVersion));
1827
+ }
1828
+ findings.push(i18n.t('osUpdateHint'));
1829
+ const findingsMd = findings.map(f => `- ${f}`).join('\n');
1830
+
1831
+ return `## ${i18n.t('diagnosis')} {#diagnosis}
1832
+
1833
+ ### ${i18n.t('diagScanStatus')}
1834
+
1835
+ | ${i18n.t('diagWhatLabel')} | ${i18n.t('diagWhereLabel')} |
1836
+ |---|---|
1837
+ ${tableRows}
1838
+
1839
+ ### ${i18n.t('diagWhereToLook')}
1840
+
1841
+ | ${i18n.t('diagWhatLabel')} | ${i18n.t('diagWhereLabel')} |
1842
+ |---|---|
1843
+ | ${i18n.t('diagLogsLabel')} | ${i18n.t('diagLogsValue')} |
1844
+ | ${i18n.t('diagAliveLabel')} | \`system.adapter.{name}.0.alive\` ${i18n.t('diagAliveHint')} |
1845
+ | ${i18n.t('diagConnectedLabel')} | \`system.adapter.{name}.0.connected\` ${i18n.t('diagConnectedHint')} |
1846
+
1847
+ ### ${i18n.t('diagFindings')}
1848
+
1849
+ ${findingsMd}
1850
+
1851
+ ---
1852
+ `;
1853
+ }
1854
+
1855
+ /**
1856
+ * Render troubleshooting section for User and Admin profiles
1857
+ *
1858
+ * @param {object} docModel Document model
1859
+ * @param {string} profile Documentation profile
1860
+ * @returns {string} Troubleshooting markdown
1861
+ */
1862
+ renderTroubleshooting(docModel, profile) {
1863
+ const i18n = this.i18n;
1864
+ const system = docModel.system;
1865
+
1866
+ if (profile === PROFILE_ONBOARDING) {
1867
+ return '';
1868
+ }
1869
+
1870
+ let markdown = `## ${i18n.t('troubleshooting')}
1871
+
1872
+ | ${i18n.t('troubleshootLogsLabel')} | ${i18n.t('troubleshootLogsValue')} |
1873
+ | ${i18n.t('troubleshootObjectsLabel')} | ${i18n.t('troubleshootObjectsValue', system.primaryHost.name)} |
1874
+
1875
+ `;
1876
+
1877
+ if (profile === PROFILE_ADMIN) {
1878
+ markdown += `### ${i18n.t('collectorStatus')}
1879
+ - ${i18n.t('instancesDetected')}: ${docModel.system.statistics.instanceCount}
1880
+ - ${i18n.t('stateObjectsScanned')}: ${docModel.appendices.stateSummary.total}
1881
+ - ${i18n.t('hostRuntimePlatform')}: ${system.primaryHost.platform}
1882
+ - ${i18n.t('operatingSystem')}: ${system.primaryHost.operatingSystem || '—'}
1883
+ - ${i18n.t('jsControllerVersion')}: ${system.primaryHost.version}
1884
+ - ${i18n.t('nodeVersion')}: ${system.primaryHost.nodeVersion || '—'}
1885
+ `;
1886
+ }
1887
+
1888
+ markdown += '\n---\n';
1889
+ return markdown;
1890
+ }
1891
+
1892
+ /**
1893
+ * Return a human-readable folder label for a script.
1894
+ *
1895
+ * @param {string|null} folder Raw folder string from discovery (null = root)
1896
+ * @returns {string} Translated folder label
1897
+ */
1898
+ scriptFolderLabel(folder) {
1899
+ const i18n = this.i18n;
1900
+ if (!folder) {
1901
+ return i18n.t('scriptFolderRoot');
1902
+ }
1903
+ if (folder === 'common') {
1904
+ return i18n.t('scriptFolderCommon');
1905
+ }
1906
+ if (folder === 'global') {
1907
+ return i18n.t('scriptFolderGlobal');
1908
+ }
1909
+ return folder;
1910
+ }
1911
+
1912
+ /**
1913
+ * Admin: userdata chapter — collapsed details (parity with HTML export).
1914
+ *
1915
+ * @param {Array} userData
1916
+ * @returns {string}
1917
+ */
1918
+ renderUserDataMarkdown(userData) {
1919
+ const i18n = this.i18n;
1920
+ const groups = {};
1921
+ for (const item of userData) {
1922
+ const key = item.folder || '';
1923
+ if (!groups[key]) {
1924
+ groups[key] = [];
1925
+ }
1926
+ groups[key].push(item);
1927
+ }
1928
+ const totalItems = userData.length;
1929
+ const groupCount = Object.keys(groups).length;
1930
+ let md = `<a id="userdata"></a>\n\n## ${i18n.t('userDefinedVariables')}\n\n`;
1931
+ md += `${i18n.t('userDataDesc')}\n\n`;
1932
+ md += '<details>\n<summary>';
1933
+ md += i18n.t('userdataExpandSummary', totalItems, groupCount);
1934
+ md += '</summary>\n\n';
1935
+ for (const folder of Object.keys(groups).sort()) {
1936
+ const items = groups[folder];
1937
+ const label = folder || i18n.t('scriptFolderRoot');
1938
+ md += `### ${label} (${items.length})\n\n`;
1939
+ md += `| ${i18n.t('name')} | ${i18n.t('type')} | ${i18n.t('value')} | ${i18n.t('description')} |\n|---|---|---|---|\n`;
1940
+ for (const item of items) {
1941
+ const valStr = item.value !== null && item.value !== undefined ? String(item.value) : '—';
1942
+ const unit = item.unit ? ` ${item.unit}` : '';
1943
+ const typeLabel = item.type || '—';
1944
+ const roleLine = item.role ? ` (${item.role})` : '';
1945
+ const nameCol = mdTableCell(`${item.name}${roleLine}`);
1946
+ md += `| ${nameCol} | ${mdTableCell(typeLabel)} | ${mdTableCell(valStr + unit)} | ${mdTableCell(item.desc || '—')} |\n`;
1947
+ }
1948
+ md += '\n';
1949
+ }
1950
+ md += '</details>\n\n---\n\n';
1951
+ return md;
1952
+ }
1953
+
1954
+ /**
1955
+ * Admin: aliases chapter — collapsed details (parity with HTML export).
1956
+ *
1957
+ * @param {Array} aliases
1958
+ * @returns {string}
1959
+ */
1960
+ renderAliasMarkdown(aliases) {
1961
+ const i18n = this.i18n;
1962
+ const groups = {};
1963
+ for (const item of aliases) {
1964
+ const key = item.folder || '';
1965
+ if (!groups[key]) {
1966
+ groups[key] = [];
1967
+ }
1968
+ groups[key].push(item);
1969
+ }
1970
+ const totalItems = aliases.length;
1971
+ const groupCount = Object.keys(groups).length;
1972
+ let md = `<a id="aliases"></a>\n\n## ${i18n.t('aliases')}\n\n`;
1973
+ md += `${i18n.t('aliasesDesc')}\n\n`;
1974
+ md += '<details>\n<summary>';
1975
+ md += i18n.t('aliasesExpandSummary', totalItems, groupCount);
1976
+ md += '</summary>\n\n';
1977
+ for (const folder of Object.keys(groups).sort()) {
1978
+ const items = groups[folder];
1979
+ const label = folder || i18n.t('scriptFolderRoot');
1980
+ md += `### ${label} (${items.length})\n\n`;
1981
+ md += `| ${i18n.t('name')} | ${i18n.t('type')} | ${i18n.t('aliasTarget')} | ${i18n.t('description')} |\n|---|---|---|---|\n`;
1982
+ for (const item of items) {
1983
+ const target =
1984
+ item.readTarget === item.writeTarget || !item.writeTarget
1985
+ ? item.readTarget || '—'
1986
+ : `${item.readTarget || '—'} / ✍ ${item.writeTarget}`;
1987
+ const typeStr = item.unit ? `${item.type} (${item.unit})` : item.type || '—';
1988
+ const roleLine = item.role ? ` (${item.role})` : '';
1989
+ const nameCol = mdTableCell(`${item.name}${roleLine}`);
1990
+ md += `| ${nameCol} | ${mdTableCell(typeStr)} | ${mdTableCell(target)} | ${mdTableCell(item.desc || '—')} |\n`;
1991
+ }
1992
+ md += '\n';
1993
+ }
1994
+ md += '</details>\n\n---\n\n';
1995
+ return md;
1996
+ }
1997
+
1998
+ /**
1999
+ * Render appendices
2000
+ *
2001
+ * @param {object} docModel Document model
2002
+ * @returns {string} Appendices markdown
2003
+ */
2004
+ renderAppendices(docModel) {
2005
+ const appendices = docModel.appendices;
2006
+ const i18n = this.i18n;
2007
+
2008
+ return `## ${i18n.t('appendices')}
2009
+
2010
+ ### ${i18n.t('stateObjectsSummary')}
2011
+ - **${i18n.t('total')}:** ${appendices.stateSummary.total}
2012
+ - **${i18n.t('writable')}:** ${appendices.stateSummary.writable}
2013
+ - **${i18n.t('readOnly')}:** ${appendices.stateSummary.readonly}
2014
+
2015
+ ### ${i18n.t('collectionInformation')}
2016
+ - **${i18n.t('collectedAt')}:** ${new Date(appendices.collectionTimestamp).toLocaleString()}
2017
+ - **${i18n.t('schemaVersion')}:** ${docModel.meta.schemaVersion}
2018
+
2019
+ ---
2020
+ *${i18n.t('generatedBy')}${docModel.meta.version}*
2021
+ `;
2022
+ }
2023
+ }
2024
+
2025
+ module.exports = MarkdownRenderer;