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.
- package/LICENSE +21 -0
- package/README.md +126 -0
- package/admin/autodoc.png +0 -0
- package/admin/i18n/de.json +244 -0
- package/admin/i18n/en.json +241 -0
- package/admin/i18n/es.json +229 -0
- package/admin/i18n/fr.json +235 -0
- package/admin/i18n/it.json +229 -0
- package/admin/i18n/nl.json +229 -0
- package/admin/i18n/pl.json +229 -0
- package/admin/i18n/pt.json +229 -0
- package/admin/i18n/ru.json +229 -0
- package/admin/i18n/uk.json +229 -0
- package/admin/i18n/zh-cn.json +229 -0
- package/admin/jsonConfig.json +1490 -0
- package/io-package.json +253 -0
- package/lib/adapter-config.d.ts +19 -0
- package/lib/aiEnhancer.js +2114 -0
- package/lib/autoHostTopologyMermaid.js +195 -0
- package/lib/dependencyAnalyzer.js +83 -0
- package/lib/diagnosisSnapshot.js +32 -0
- package/lib/discovery.js +953 -0
- package/lib/docTemplateConfig.js +422 -0
- package/lib/documentModel.js +640 -0
- package/lib/forumCard.js +70 -0
- package/lib/guestHelpContent.js +93 -0
- package/lib/guestScriptPrivacy.js +14 -0
- package/lib/hostDisplay.js +19 -0
- package/lib/htmlRenderer.js +4108 -0
- package/lib/htmlThemePresets.js +79 -0
- package/lib/htmlToPdf.js +99 -0
- package/lib/i18n.js +1309 -0
- package/lib/markdownRenderer.js +2025 -0
- package/lib/mermaidAutodocPalette.js +165 -0
- package/lib/mermaidServerSvg.js +252 -0
- package/lib/notifier.js +124 -0
- package/lib/quickStartGuide.js +180 -0
- package/lib/roleMapper.js +90 -0
- package/lib/scriptGroups.js +78 -0
- package/lib/versionTracker.js +312 -0
- package/main.js +1368 -0
- 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;
|