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,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;
|
package/lib/forumCard.js
ADDED
|
@@ -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
|
+
};
|