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,640 @@
1
+ /**
2
+ * AutoDoc Document Model Module
3
+ * Builds structured document models from raw system data
4
+ */
5
+ const { extractStateRefs, buildCrossRef } = require('./dependencyAnalyzer');
6
+ const { formatOperatingSystemLine } = require('./hostDisplay');
7
+ const { mapRole } = require('./roleMapper');
8
+ const {
9
+ parseAdminHiddenChapters,
10
+ parseAdminChapterOrder,
11
+ parseUserChapterOrder,
12
+ parseOnboardingChapterOrder,
13
+ parseCustomDocSections,
14
+ parseUserHiddenChapters,
15
+ parseOnboardingHiddenChapters,
16
+ } = require('./docTemplateConfig');
17
+ const { buildQuickStartGuide } = require('./quickStartGuide');
18
+ const { buildAutoHostTopologyMermaid } = require('./autoHostTopologyMermaid');
19
+
20
+ // Default minimum trimmed length for project description (overridable via config).
21
+ const DEFAULT_MIN_PROJECT_DESCRIPTION_CHARS = 40;
22
+ /** Cap for owner-supplied Mermaid source (`manualMermaidDiagram`) — keeps exports bounded. */
23
+ const MAX_MANUAL_MERMAID_CHARS = 12000;
24
+
25
+ /**
26
+ * @param {Array<{ok: boolean}>} checks Array of check results
27
+ * @returns {number} 0–100 percentage of passing checks (100 when list is empty)
28
+ */
29
+ function scorePercent(checks) {
30
+ return checks.length > 0 ? Math.round((checks.filter(c => c.ok).length / checks.length) * 100) : 100;
31
+ }
32
+
33
+ /**
34
+ * @param {unknown} value Raw native config value
35
+ * @param {number} fallback Used when `value` is not a finite integer
36
+ * @param {number} min Inclusive lower bound
37
+ * @param {number} max Inclusive upper bound
38
+ * @returns {number} Integer clamped to [min, max] or `fallback`
39
+ */
40
+ function clampConfigInt(value, fallback, min, max) {
41
+ const n = parseInt(String(value), 10);
42
+ if (!Number.isFinite(n)) {
43
+ return fallback;
44
+ }
45
+ return Math.min(max, Math.max(min, n));
46
+ }
47
+
48
+ /**
49
+ * Document model builder class.
50
+ */
51
+ class DocumentModel {
52
+ /**
53
+ * @param {object} adapter ioBroker adapter instance
54
+ */
55
+ constructor(adapter) {
56
+ this.adapter = adapter;
57
+ }
58
+
59
+ /**
60
+ * Build complete document model from raw data
61
+ *
62
+ * @param {object} rawData Raw system data
63
+ * @param {string} trigger Generation trigger
64
+ * @param {{ publicDocUrls?: { admin?: string, user?: string, onboarding?: string } }} [options] Pre-built public URLs (same as HTML/QR) for hybrid troubleshooting links.
65
+ * @returns {Promise<object>} Document model
66
+ */
67
+ async buildDocumentModel(rawData, trigger, options = {}) {
68
+ const config = this.adapter.config;
69
+
70
+ // Parse manual context first — maintenance checklist uses it (public links merged below).
71
+ const manualContext = this.parseManualContext(config);
72
+ const pub = options && options.publicDocUrls;
73
+ if (pub && (pub.user || pub.onboarding || pub.admin)) {
74
+ manualContext.troubleshootPublicLinks = {
75
+ admin: (pub.admin && String(pub.admin).trim()) || '',
76
+ user: (pub.user && String(pub.user).trim()) || '',
77
+ onboarding: (pub.onboarding && String(pub.onboarding).trim()) || '',
78
+ };
79
+ } else {
80
+ manualContext.troubleshootPublicLinks = { admin: '', user: '', onboarding: '' };
81
+ }
82
+
83
+ // Filter instances based on configuration
84
+ const filteredInstances = this.filterInstances(rawData.instances, config);
85
+
86
+ // Build system information
87
+ const systemInfo = this.buildSystemInfo(rawData, filteredInstances);
88
+
89
+ // Build adapter information
90
+ const adapterInfo = this.buildAdapterInfo(filteredInstances);
91
+
92
+ manualContext.autoHostTopologyMermaid = buildAutoHostTopologyMermaid(adapterInfo.hosts, {
93
+ enabled: config.autoMermaidHostGraph === true,
94
+ maxNodes: clampConfigInt(config.autoMermaidHostGraphMaxNodes, 40, 8, 80),
95
+ });
96
+
97
+ // Build rooms and functions
98
+ const rooms = this.buildRooms(rawData, rawData.deviceMap || {}, rawData.liveStates || {});
99
+
100
+ // Build scripts section
101
+ const scripts = this.buildScripts(rawData);
102
+
103
+ // Build maintenance (checklist uses rooms + manualContext + config + rawData for host check)
104
+ const maintenance = this.buildMaintenance(filteredInstances, rooms, scripts, manualContext, config, rawData);
105
+
106
+ // Build appendices
107
+ const appendices = this.buildAppendices(rawData, filteredInstances);
108
+
109
+ // Build metadata
110
+ const meta = this.buildMetadata(trigger);
111
+
112
+ const quickStart = buildQuickStartGuide(rooms, scripts);
113
+
114
+ return {
115
+ meta,
116
+ system: systemInfo,
117
+ adapters: adapterInfo,
118
+ rooms,
119
+ scripts,
120
+ quickStart,
121
+ maintenance,
122
+ appendices,
123
+ systemConfig: this.buildSystemConfig(rawData),
124
+ manualContext,
125
+ userData: rawData.userData || [],
126
+ aliases: rawData.aliases || [],
127
+ scheduleObjects: rawData.scheduleObjects || [],
128
+ adminHiddenChapters: parseAdminHiddenChapters(config),
129
+ adminChapterOrder: parseAdminChapterOrder(config),
130
+ userHiddenChapters: parseUserHiddenChapters(config),
131
+ userChapterOrder: parseUserChapterOrder(config),
132
+ onboardingHiddenChapters: parseOnboardingHiddenChapters(config),
133
+ onboardingChapterOrder: parseOnboardingChapterOrder(config),
134
+ customDocSections: parseCustomDocSections(config),
135
+ // Non-rendered config reference used by AI enhancer for owner hints prompt injection
136
+ _adapterConfig: { aiOwnerHints: config.aiOwnerHints || '' },
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Filter adapter instances based on configuration
142
+ *
143
+ * @param {Array} instances Raw instances
144
+ * @param {object} config Adapter configuration
145
+ * @returns {Array} Filtered instances
146
+ */
147
+ filterInstances(instances, config) {
148
+ let filtered = instances;
149
+
150
+ // Filter by enabled status
151
+ if (config.onlyEnabledInstances) {
152
+ filtered = filtered.filter(instance => instance.enabled);
153
+ }
154
+
155
+ // Limit number of instances
156
+ if (config.maxDocumentedInstances && config.maxDocumentedInstances > 0) {
157
+ filtered = filtered.slice(0, config.maxDocumentedInstances);
158
+ }
159
+
160
+ return filtered;
161
+ }
162
+
163
+ /**
164
+ * Build system information section
165
+ *
166
+ * @param {object} rawData Raw system data
167
+ * @param {Array} instances Filtered instances
168
+ * @returns {object} System information
169
+ */
170
+ buildSystemInfo(rawData, instances) {
171
+ const primaryHost = rawData.hosts[0] || {};
172
+
173
+ return {
174
+ projectName: this.adapter.config.projectName || 'ioBroker System',
175
+ targetSystem: this.adapter.config.targetSystem || 'Production',
176
+ primaryHost: {
177
+ name: primaryHost.name || 'Unknown',
178
+ platform: primaryHost.platform || 'Unknown',
179
+ version: primaryHost.version || 'Unknown',
180
+ nodeVersion: primaryHost.nodeVersion || '',
181
+ npmVersion: primaryHost.npmVersion || '',
182
+ osRelease: primaryHost.osRelease || '',
183
+ osArch: primaryHost.osArch || '',
184
+ osType: primaryHost.osType || '',
185
+ operatingSystem: formatOperatingSystemLine(primaryHost),
186
+ },
187
+ hosts: rawData.hosts,
188
+ hostResources: rawData.hostResources || {},
189
+ location: {
190
+ city: (rawData.systemConfig && rawData.systemConfig.city) || '',
191
+ country: (rawData.systemConfig && rawData.systemConfig.country) || '',
192
+ timezone: (rawData.systemConfig && rawData.systemConfig.timezone) || '',
193
+ latitude: (rawData.systemConfig && rawData.systemConfig.latitude) || null,
194
+ longitude: (rawData.systemConfig && rawData.systemConfig.longitude) || null,
195
+ activeRepo: (rawData.systemConfig && rawData.systemConfig.activeRepo) || '',
196
+ },
197
+ statistics: {
198
+ instanceCount: instances.length,
199
+ enabledInstanceCount: instances.filter(i => i.enabled).length,
200
+ disabledInstanceCount: instances.filter(i => !i.enabled).length,
201
+ totalStateObjects: rawData.stateSummary.total,
202
+ writableStateObjects: rawData.stateSummary.writable,
203
+ readonlyStateObjects: rawData.stateSummary.readonly,
204
+ pendingUpdates: rawData.pendingUpdates || 0,
205
+ lastBackup: rawData.lastBackup || null,
206
+ },
207
+ };
208
+ }
209
+
210
+ /**
211
+ * Build adapter information section
212
+ *
213
+ * @param {Array} instances Filtered instances
214
+ * @returns {object} Adapter information
215
+ */
216
+ buildAdapterInfo(instances) {
217
+ // Group instances by adapter
218
+ const adapters = {};
219
+ const hosts = {};
220
+
221
+ for (const instance of instances) {
222
+ // Group by adapter
223
+ if (!adapters[instance.adapter]) {
224
+ adapters[instance.adapter] = {
225
+ name: instance.adapter,
226
+ title: instance.title || instance.adapter,
227
+ desc: instance.desc || '',
228
+ instances: [],
229
+ totalInstances: 0,
230
+ enabledInstances: 0,
231
+ connectionType: instance.connectionType || '',
232
+ dataSource: instance.dataSource || '',
233
+ tier: instance.tier || 0,
234
+ };
235
+ }
236
+
237
+ adapters[instance.adapter].instances.push(instance);
238
+ adapters[instance.adapter].totalInstances++;
239
+
240
+ if (instance.enabled) {
241
+ adapters[instance.adapter].enabledInstances++;
242
+ }
243
+
244
+ // Group by host
245
+ if (!hosts[instance.host]) {
246
+ hosts[instance.host] = [];
247
+ }
248
+ hosts[instance.host].push(instance);
249
+ }
250
+
251
+ return {
252
+ adapters: Object.values(adapters),
253
+ hosts,
254
+ totalAdapters: Object.keys(adapters).length,
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Build rooms section with member counts and function assignments
260
+ *
261
+ * @param {object} rawData Raw system data
262
+ * @param {object} deviceMap Map of device data keyed by state ID
263
+ * @param {object} liveStates Map of live state values keyed by state ID
264
+ * @returns {object} Rooms section
265
+ */
266
+ buildRooms(rawData, deviceMap, liveStates) {
267
+ const rooms = rawData.rooms || [];
268
+ const functions = rawData.functions || [];
269
+
270
+ // Build a map: memberId → [functionName, ...]
271
+ const memberFunctions = {};
272
+ for (const fn of functions) {
273
+ for (const memberId of fn.members) {
274
+ if (!memberFunctions[memberId]) {
275
+ memberFunctions[memberId] = [];
276
+ }
277
+ memberFunctions[memberId].push(fn.name);
278
+ }
279
+ }
280
+
281
+ const roomList = rooms.map(room => ({
282
+ id: room.id,
283
+ name: room.name,
284
+ memberCount: room.members.length,
285
+ devices: room.members.map(memberId => {
286
+ const device = deviceMap[memberId];
287
+ const live = liveStates[memberId];
288
+ const roleInfo = mapRole(device ? device.role : '');
289
+ return {
290
+ id: memberId,
291
+ deviceName: device ? device.deviceName : memberId.split('.').pop(),
292
+ role: device ? device.role : '',
293
+ category: roleInfo.category,
294
+ icon: roleInfo.icon,
295
+ labelKey: roleInfo.labelKey,
296
+ unit: device ? device.unit : '',
297
+ currentValue: live !== undefined ? live.val : null,
298
+ functions: memberFunctions[memberId] || [],
299
+ };
300
+ }),
301
+ }));
302
+
303
+ return {
304
+ rooms: roomList,
305
+ functions: functions.map(fn => ({ id: fn.id, name: fn.name, memberCount: fn.members.length })),
306
+ totalRooms: roomList.length,
307
+ totalFunctions: functions.length,
308
+ unassignedCount: rawData.instances.filter(
309
+ inst =>
310
+ inst.enabled &&
311
+ !rooms.some(r => r.members.some(m => m.startsWith(inst.id.replace('system.adapter.', '')))),
312
+ ).length,
313
+ };
314
+ }
315
+
316
+ /**
317
+ * Build system configuration section (city, country, language, geo-coordinates)
318
+ *
319
+ * @param {object} rawData Raw system data
320
+ * @returns {object} System configuration
321
+ */
322
+ buildSystemConfig(rawData) {
323
+ const sc = rawData.systemConfig || {};
324
+ return {
325
+ city: sc.city || '',
326
+ country: sc.country || '',
327
+ language: sc.language || 'en',
328
+ latitude: sc.latitude || null,
329
+ longitude: sc.longitude || null,
330
+ };
331
+ }
332
+
333
+ /**
334
+ * Detect trigger type from script source code (regex-based, best-effort)
335
+ *
336
+ * @param {string} source Script source code
337
+ * @param {string} engineType Engine type
338
+ * @returns {string} Detected trigger type
339
+ */
340
+ detectTriggerType(source, engineType) {
341
+ if (engineType && engineType.toLowerCase().includes('blockly')) {
342
+ return 'blockly';
343
+ }
344
+ if (!source) {
345
+ return 'unknown';
346
+ }
347
+ if (/schedule\s*\(/.test(source)) {
348
+ return 'schedule';
349
+ }
350
+ if (/on\s*\(/.test(source) || /subscribe\s*\(/.test(source)) {
351
+ return 'subscribe';
352
+ }
353
+ if (/onStart|on\s*\(\s*['"]start['"]/.test(source)) {
354
+ return 'on-start';
355
+ }
356
+ return 'unknown';
357
+ }
358
+
359
+ /**
360
+ * Build scripts section
361
+ *
362
+ * @param {object} rawData Raw system data
363
+ * @returns {object} Scripts section
364
+ */
365
+ buildScripts(rawData) {
366
+ const rawScripts = rawData.scripts || [];
367
+
368
+ const scriptList = rawScripts.map(s => ({
369
+ id: s.id,
370
+ name: s.name,
371
+ folder: s.folder,
372
+ enabled: s.enabled,
373
+ engineType: s.engineType,
374
+ engine: s.engine || '',
375
+ desc: s.desc,
376
+ schedule: s.schedule || '',
377
+ triggerType: this.detectTriggerType(s.source, s.engineType),
378
+ stateRefs: extractStateRefs(s.source),
379
+ }));
380
+
381
+ const crossRef = buildCrossRef(scriptList);
382
+
383
+ return {
384
+ scripts: scriptList,
385
+ totalScripts: scriptList.length,
386
+ enabledScripts: scriptList.filter(s => s.enabled).length,
387
+ disabledScripts: scriptList.filter(s => !s.enabled).length,
388
+ stateCrossRef: crossRef,
389
+ };
390
+ }
391
+
392
+ /**
393
+ * Build maintenance and diagnostics section.
394
+ * Returns three independent sub-scores:
395
+ * data – did autodoc successfully read all system data?
396
+ * manual – has the user filled in their own content?
397
+ * depth – does the documentation go beyond a raw data dump?
398
+ *
399
+ * @param {Array} instances Filtered adapter instances
400
+ * @param {object} rooms Return value of {@link DocumentModel#buildRooms}
401
+ * @param {object} scripts Return value of {@link DocumentModel#buildScripts}
402
+ * @param {object} manualContext Normalised manual context from config
403
+ * @param {object} config Adapter native config
404
+ * @param {object} rawData Original raw data (for host availability check)
405
+ * @returns {object} Maintenance section
406
+ */
407
+ buildMaintenance(instances, rooms, scripts, manualContext, config, rawData) {
408
+ const disabledInstances = instances.filter(inst => !inst.enabled);
409
+ const c = config || {};
410
+ const rd = rawData || {};
411
+
412
+ const minDescLen = clampConfigInt(
413
+ c.maintenanceScoreMinDescriptionChars,
414
+ DEFAULT_MIN_PROJECT_DESCRIPTION_CHARS,
415
+ 5,
416
+ 2000,
417
+ );
418
+
419
+ // ── Score 1: Datenerfassung — did autodoc get the data? ─────────────
420
+ const hostsFound = Array.isArray(rd.hosts) && rd.hosts.length > 0;
421
+ const instancesFound = instances.some(i => i.enabled);
422
+ const roomsDefined = !!(rooms && rooms.totalRooms > 0);
423
+
424
+ const dataChecks = [
425
+ { key: 'checkHostsFound', ok: hostsFound },
426
+ { key: 'checkInstancesFound', ok: instancesFound },
427
+ { key: 'checkRoomsDefined', ok: roomsDefined },
428
+ ];
429
+
430
+ // ── Score 2: Manuelle Inhalte — has the user provided their content? ─
431
+ const descLen =
432
+ manualContext && manualContext.description ? String(manualContext.description).trim().length : 0;
433
+ const descriptionOk = descLen >= minDescLen;
434
+
435
+ const baseUrlRaw = c.baseUrl != null ? String(c.baseUrl).trim() : '';
436
+ const baseUrlOk = baseUrlRaw.length > 0;
437
+
438
+ const contactSet = !!(
439
+ manualContext &&
440
+ manualContext.contact &&
441
+ String(manualContext.contact).trim().length > 0
442
+ );
443
+ const hasCustomContent = !!(
444
+ manualContext &&
445
+ (String(manualContext.notes || '').trim().length > 0 ||
446
+ String(manualContext.guestHelpNote || '').trim().length > 0 ||
447
+ String(manualContext.homeRoutinesNote || '').trim().length > 0 ||
448
+ String(manualContext.ownerPlaybookNote || '').trim().length > 0 ||
449
+ Object.keys(manualContext.adapters || {}).length > 0 ||
450
+ Object.keys(manualContext.rooms || {}).length > 0)
451
+ );
452
+
453
+ const manualChecks = [];
454
+ if (c.maintenanceScoreCheckDescription !== false) {
455
+ manualChecks.push({ key: 'projectNarrativeThin', ok: descriptionOk });
456
+ }
457
+ if (c.maintenanceScoreCheckBaseUrl !== false) {
458
+ manualChecks.push({ key: 'baseUrlUnset', ok: baseUrlOk });
459
+ }
460
+ manualChecks.push({ key: 'checkContactSet', ok: contactSet });
461
+ manualChecks.push({ key: 'checkCustomContent', ok: hasCustomContent });
462
+
463
+ // ── Score 3: Dokumentationstiefe — does it go beyond a raw data dump? ─
464
+ const hasDiagram = !!(
465
+ (manualContext && manualContext.mermaidDiagram && String(manualContext.mermaidDiagram).trim().length > 0) ||
466
+ c.autoMermaidHostGraph === true
467
+ );
468
+
469
+ const roomsHaveDevices = !!(rooms && Array.isArray(rooms.rooms) && rooms.rooms.some(r => r.memberCount > 0));
470
+
471
+ // parseCustomDocSections already strips entries without title+body, so a
472
+ // non-empty result guarantees at least one chapter with real content.
473
+ const hasCustomSections = parseCustomDocSections(c).length > 0;
474
+
475
+ // AI enrichment: check only added when scripts exist.
476
+ // Passes when AI provider is configured — "have you enabled AI enrichment?" is itself
477
+ // a depth signal. Auto-passes (check omitted) when no scripts are present.
478
+ const aiProvider = String(c.aiProvider || '')
479
+ .trim()
480
+ .toLowerCase();
481
+ const aiConfigured = aiProvider !== '' && aiProvider !== 'none';
482
+ const scriptsExist = scripts && scripts.totalScripts > 0;
483
+ const aiDepthOk = aiConfigured;
484
+
485
+ const depthChecks = [];
486
+ depthChecks.push({ key: 'checkHasDiagram', ok: hasDiagram });
487
+ depthChecks.push({ key: 'checkRoomsHaveDevices', ok: roomsHaveDevices });
488
+ depthChecks.push({ key: 'checkHasCustomSections', ok: hasCustomSections });
489
+ if (scriptsExist) {
490
+ depthChecks.push({ key: 'checkAiConfigured', ok: aiDepthOk });
491
+ }
492
+
493
+ // ── Overall: average of the three dimension scores ───────────────────
494
+ const dataScore = scorePercent(dataChecks);
495
+ const manualScore = scorePercent(manualChecks);
496
+ const depthScore = scorePercent(depthChecks);
497
+ const overall = Math.round((dataScore + manualScore + depthScore) / 3);
498
+
499
+ return {
500
+ disabledInstances: disabledInstances.map(i => ({ id: i.id, name: i.name, title: i.title })),
501
+ scores: {
502
+ data: { checks: dataChecks, score: dataScore },
503
+ manual: { checks: manualChecks, score: manualScore },
504
+ depth: { checks: depthChecks, score: depthScore },
505
+ },
506
+ score: overall,
507
+ // Flat checklist kept for any external JSON consumers (deprecated in favour of scores)
508
+ checklist: [...dataChecks, ...manualChecks, ...depthChecks],
509
+ };
510
+ }
511
+
512
+ /**
513
+ * Build appendices section
514
+ *
515
+ * @param {object} rawData Raw system data
516
+ * @param {Array} instances Filtered instances
517
+ * @returns {object} Appendices
518
+ */
519
+ buildAppendices(rawData, instances) {
520
+ return {
521
+ stateSummary: rawData.stateSummary,
522
+ rawInstances: instances,
523
+ collectionTimestamp: rawData.collectedAt,
524
+ };
525
+ }
526
+
527
+ /**
528
+ * Parse and normalise manualContext from adapter config.
529
+ * Reads dedicated UI fields (projectDescription, manualContact, additionalNotes,
530
+ * guestHelpNote, homeRoutinesNote, ownerPlaybookNote, manualMermaidDiagram, troubleshoot* hints, adapterNotes table, roomNotes table) with fallback to legacy manualContext JSON.
531
+ *
532
+ * @param {object} config Full adapter config object
533
+ * @returns {object} Normalised manualContext
534
+ */
535
+ parseManualContext(config) {
536
+ // Parse legacy JSON field as fallback base
537
+ let legacy = {};
538
+ const raw = config && config.manualContext;
539
+ if (raw) {
540
+ if (typeof raw === 'string') {
541
+ try {
542
+ legacy = JSON.parse(raw);
543
+ } catch {
544
+ legacy = {};
545
+ }
546
+ } else if (typeof raw === 'object') {
547
+ legacy = raw;
548
+ }
549
+ }
550
+
551
+ // New dedicated fields take precedence over legacy JSON
552
+ const description = (config && config.projectDescription) || legacy.description || '';
553
+ const contact = (config && config.manualContact) || legacy.contact || '';
554
+ const notes = (config && config.additionalNotes) || legacy.notes || '';
555
+ const guestHelpNote = (config && config.guestHelpNote) || legacy.guestHelpNote || '';
556
+ const homeRoutinesNote = (config && config.homeRoutinesNote) || legacy.homeRoutinesNote || '';
557
+ const ownerPlaybookNote = (config && config.ownerPlaybookNote) || legacy.ownerPlaybookNote || '';
558
+ let mermaidDiagram = (config && config.manualMermaidDiagram) || legacy.mermaidDiagram || '';
559
+ mermaidDiagram = String(mermaidDiagram).replace(/\r\n/g, '\n').trim();
560
+ if (mermaidDiagram.length > MAX_MANUAL_MERMAID_CHARS) {
561
+ mermaidDiagram = mermaidDiagram.slice(0, MAX_MANUAL_MERMAID_CHARS);
562
+ }
563
+ const troubleshootWifiHint = (config && config.troubleshootWifiHint) || legacy.troubleshootWifiHint || '';
564
+ const troubleshootPowerHint = (config && config.troubleshootPowerHint) || legacy.troubleshootPowerHint || '';
565
+ const troubleshootWaterHint = (config && config.troubleshootWaterHint) || legacy.troubleshootWaterHint || '';
566
+ const troubleshootExtraHint = (config && config.troubleshootExtraHint) || legacy.troubleshootExtraHint || '';
567
+
568
+ // adapterNotes table: [{adapter: "telegram", note: "..."}] → {telegram: "..."}
569
+ const adapters = {};
570
+ if (config && Array.isArray(config.adapterNotes)) {
571
+ for (const row of config.adapterNotes) {
572
+ if (row.adapter && row.note) {
573
+ adapters[row.adapter.trim()] = row.note.trim();
574
+ }
575
+ }
576
+ }
577
+ // Merge legacy adapter notes (new table entries win on conflict)
578
+ if (legacy.adapters && typeof legacy.adapters === 'object') {
579
+ for (const [k, v] of Object.entries(legacy.adapters)) {
580
+ if (!adapters[k]) {
581
+ adapters[k] = v;
582
+ }
583
+ }
584
+ }
585
+
586
+ // roomNotes table: [{room: "Wohnzimmer", note: "..."}] → {Wohnzimmer: "..."}
587
+ const rooms = {};
588
+ if (config && Array.isArray(config.roomNotes)) {
589
+ for (const row of config.roomNotes) {
590
+ if (row.room && row.note) {
591
+ rooms[row.room.trim()] = row.note.trim();
592
+ }
593
+ }
594
+ }
595
+ // Merge legacy room notes (new table entries win on conflict)
596
+ if (legacy.rooms && typeof legacy.rooms === 'object') {
597
+ for (const [k, v] of Object.entries(legacy.rooms)) {
598
+ if (!rooms[k]) {
599
+ rooms[k] = v;
600
+ }
601
+ }
602
+ }
603
+
604
+ return {
605
+ description,
606
+ contact,
607
+ notes,
608
+ guestHelpNote,
609
+ homeRoutinesNote,
610
+ ownerPlaybookNote,
611
+ mermaidDiagram,
612
+ troubleshootWifiHint,
613
+ troubleshootPowerHint,
614
+ troubleshootWaterHint,
615
+ troubleshootExtraHint,
616
+ adapters,
617
+ rooms,
618
+ };
619
+ }
620
+
621
+ /**
622
+ * Build document metadata
623
+ *
624
+ * @param {string} trigger Generation trigger
625
+ * @returns {object} Metadata
626
+ */
627
+ buildMetadata(trigger) {
628
+ // schemaVersion = export JSON shape revision (not the adapter package semver).
629
+ return {
630
+ schemaVersion: 'autodoc-json-1',
631
+ generatedAt: new Date().toISOString(),
632
+ trigger: trigger,
633
+ generator: 'ioBroker.autodoc',
634
+ version: this.adapter.version || '0.0.0',
635
+ language: this.adapter.config.language || 'en',
636
+ };
637
+ }
638
+ }
639
+
640
+ module.exports = DocumentModel;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Plaintext "system card" for forum posts — shared by Admin HTML copy button and jsonConfig sendTo.
3
+ *
4
+ * @param {object} docModel Document model (same shape as htmlRenderer diagnosis)
5
+ * @param {import('./i18n')} i18n - i18n instance for user-visible labels
6
+ * @returns {{ forumData: object, plaintext: string }} - structured fields plus copy-ready plain text
7
+ */
8
+ function buildForumCard(docModel, i18n) {
9
+ const system = docModel.system;
10
+ const stats = system.statistics;
11
+ const appendices = docModel.appendices;
12
+ const primaryHostName = system.primaryHost.name;
13
+ const hostRes = (system.hostResources || {})[primaryHostName] || {};
14
+
15
+ let ramForumText = '—';
16
+ if (hostRes.sysTotalMb && hostRes.sysFreeMb !== null) {
17
+ const usedMb = hostRes.sysTotalMb - hostRes.sysFreeMb;
18
+ ramForumText = `${usedMb} / ${hostRes.sysTotalMb} MB`;
19
+ } else if (hostRes.adapterTotalMb) {
20
+ ramForumText = `~${hostRes.adapterTotalMb} MB (${i18n.t('allAdapters') || 'all adapters'})`;
21
+ } else if (hostRes.procMb) {
22
+ ramForumText = `~${hostRes.procMb} MB (js-controller)`;
23
+ }
24
+ const cpuVal = hostRes.cpu !== null && hostRes.cpu !== undefined ? `${hostRes.cpu} %` : null;
25
+
26
+ const activeRepo = (system.location && system.location.activeRepo) || '';
27
+
28
+ const instancesStr = `${stats.instanceCount} (${stats.enabledInstanceCount} ${i18n.t('diagActive')}, ${stats.disabledInstanceCount} ${i18n.t('diagInactive')})`;
29
+ const stateObjectsStr = `${appendices.stateSummary.total} (${appendices.stateSummary.writable} ${i18n.t('writable')}, ${appendices.stateSummary.readonly} ${i18n.t('readOnlyStates')})`;
30
+
31
+ const forumData = {
32
+ instances: instancesStr,
33
+ stateObjects: stateObjectsStr,
34
+ runtime: system.primaryHost.platform,
35
+ operatingSystem: system.primaryHost.operatingSystem || '—',
36
+ jsController: system.primaryHost.version,
37
+ nodejs: system.primaryHost.nodeVersion || '—',
38
+ npm: system.primaryHost.npmVersion || '—',
39
+ ram: ramForumText,
40
+ cpu: cpuVal || '—',
41
+ host: primaryHostName,
42
+ repo: activeRepo || '—',
43
+ };
44
+
45
+ const fence = '```';
46
+ const lines = [
47
+ fence,
48
+ `${i18n.t('instancesDetected')}: ${forumData.instances}`,
49
+ `${i18n.t('stateObjectsScanned')}: ${forumData.stateObjects}`,
50
+ `${i18n.t('hostRuntimePlatform')}: ${forumData.runtime}`,
51
+ `${i18n.t('operatingSystem')}: ${forumData.operatingSystem}`,
52
+ `${i18n.t('jsControllerVersion')}: ${forumData.jsController}`,
53
+ `${i18n.t('nodeVersion')}: ${forumData.nodejs}`,
54
+ `${i18n.t('npmVersion')}: ${forumData.npm}`,
55
+ `RAM: ${forumData.ram}`,
56
+ `CPU: ${forumData.cpu}`,
57
+ `${i18n.t('hosts')}: ${forumData.host}`,
58
+ `${i18n.t('activeRepo') || 'Repository'}: ${forumData.repo}`,
59
+ fence,
60
+ ];
61
+
62
+ return {
63
+ forumData,
64
+ plaintext: lines.join('\n'),
65
+ };
66
+ }
67
+
68
+ module.exports = {
69
+ buildForumCard,
70
+ };