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
package/main.js
ADDED
|
@@ -0,0 +1,1368 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Created with @iobroker/create-adapter v3.1.2
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { createHash } = require('node:crypto');
|
|
9
|
+
const utils = require('@iobroker/adapter-core');
|
|
10
|
+
const QRCode = require('qrcode');
|
|
11
|
+
|
|
12
|
+
// Import modular components
|
|
13
|
+
const Discovery = require('./lib/discovery');
|
|
14
|
+
const DocumentModel = require('./lib/documentModel');
|
|
15
|
+
const MarkdownRenderer = require('./lib/markdownRenderer');
|
|
16
|
+
const HtmlRenderer = require('./lib/htmlRenderer');
|
|
17
|
+
const I18n = require('./lib/i18n');
|
|
18
|
+
const VersionTracker = require('./lib/versionTracker');
|
|
19
|
+
const Notifier = require('./lib/notifier');
|
|
20
|
+
const AiEnhancer = require('./lib/aiEnhancer');
|
|
21
|
+
const { buildForumCard } = require('./lib/forumCard');
|
|
22
|
+
|
|
23
|
+
/** When `documentationStatesMode` is metadata — full exports live only under adapter /files. */
|
|
24
|
+
const DOCS_STATE_METADATA_PLACEHOLDER =
|
|
25
|
+
'[AutoDoc] Full content is stored only in adapter files (autodoc-latest.md, autodoc-latest.json, autodoc-admin.html). Open Files in Admin or use info.htmlUrl*.';
|
|
26
|
+
/** Valid minimal JSON for `documentation.json` state when not duplicating the full export. */
|
|
27
|
+
const DOCS_JSON_METADATA_PLACEHOLDER =
|
|
28
|
+
'{"_autodoc":"Full document model is in autodoc-latest.json under adapter files — not duplicated in states."}';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* SHA-256 hex digest of a UTF-8 string (for export identity without storing large payloads).
|
|
32
|
+
*
|
|
33
|
+
* @param {string} s - UTF-8 text to hash
|
|
34
|
+
* @returns {string} 64 hex chars
|
|
35
|
+
*/
|
|
36
|
+
function sha256HexUtf8(s) {
|
|
37
|
+
return createHash('sha256').update(String(s), 'utf8').digest('hex');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* SHA-256 hex digest of binary data (PDF export fingerprints).
|
|
42
|
+
*
|
|
43
|
+
* @param {Buffer} buf -
|
|
44
|
+
* @returns {string} 64 hex chars, or empty string if not a Buffer
|
|
45
|
+
*/
|
|
46
|
+
function sha256HexBuffer(buf) {
|
|
47
|
+
if (!Buffer.isBuffer(buf)) {
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
class Autodoc extends utils.Adapter {
|
|
54
|
+
/**
|
|
55
|
+
* @param {object} [options] Adapter options.
|
|
56
|
+
*/
|
|
57
|
+
constructor(options) {
|
|
58
|
+
super({
|
|
59
|
+
...options,
|
|
60
|
+
name: 'autodoc',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Initialize modular components
|
|
64
|
+
this.discovery = new Discovery(this);
|
|
65
|
+
this.documentModel = new DocumentModel(this);
|
|
66
|
+
this.i18n = new I18n();
|
|
67
|
+
this.markdownRenderer = new MarkdownRenderer(this, this.i18n);
|
|
68
|
+
this.htmlRenderer = new HtmlRenderer(this, this.i18n);
|
|
69
|
+
this.versionTracker = new VersionTracker(this);
|
|
70
|
+
this.notifier = new Notifier(this);
|
|
71
|
+
this.aiEnhancer = new AiEnhancer(this);
|
|
72
|
+
|
|
73
|
+
// Timer for periodic auto-generation
|
|
74
|
+
this.autoGenerateInterval = null;
|
|
75
|
+
|
|
76
|
+
// Debounce timer for event-based generation
|
|
77
|
+
this.eventGenerateDebounce = null;
|
|
78
|
+
|
|
79
|
+
/** Prevents overlapping runs (startup + manual + sendTo would otherwise fight Ollama and file writes). */
|
|
80
|
+
this._documentationGenerationInProgress = false;
|
|
81
|
+
|
|
82
|
+
/** Set by `action.cancelScriptSourceAi` (button/state/sendTo) — checked between script KI calls. */
|
|
83
|
+
this._cancelScriptSourceAiRequested = false;
|
|
84
|
+
|
|
85
|
+
this.on('ready', this.onReady.bind(this));
|
|
86
|
+
this.on('stateChange', this.onStateChange.bind(this));
|
|
87
|
+
this.on('objectChange', this.onObjectChange.bind(this));
|
|
88
|
+
this.on('message', this.onMessage.bind(this));
|
|
89
|
+
this.on('unload', this.onUnload.bind(this));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Is called when databases are connected and adapter received configuration.
|
|
94
|
+
*/
|
|
95
|
+
async onReady() {
|
|
96
|
+
await this.createStates();
|
|
97
|
+
|
|
98
|
+
await this.setObjectNotExistsAsync('files', {
|
|
99
|
+
type: 'meta',
|
|
100
|
+
common: {
|
|
101
|
+
name: 'Files',
|
|
102
|
+
type: 'meta.user',
|
|
103
|
+
},
|
|
104
|
+
native: {},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await this.setStateAsync('info.connection', { val: false, ack: true });
|
|
108
|
+
|
|
109
|
+
this.log.info('AutoDoc adapter starting');
|
|
110
|
+
await this.checkMultihostPlacement();
|
|
111
|
+
this.log.debug(`config projectName: ${this.config.projectName || ''}`);
|
|
112
|
+
this.log.debug(`config targetSystem: ${this.config.targetSystem || ''}`);
|
|
113
|
+
this.log.debug(`config autoGenerateOnStart: ${this.config.autoGenerateOnStart}`);
|
|
114
|
+
this.log.debug(`config onlyEnabledInstances: ${this.config.onlyEnabledInstances}`);
|
|
115
|
+
this.log.debug(`config hideInstanceDetailsInMarkdown: ${this.config.hideInstanceDetailsInMarkdown}`);
|
|
116
|
+
this.log.debug(`config maxDocumentedInstances: ${this.config.maxDocumentedInstances}`);
|
|
117
|
+
|
|
118
|
+
await this.subscribeStatesAsync('action.generate');
|
|
119
|
+
await this.subscribeStatesAsync('action.cancelScriptSourceAi');
|
|
120
|
+
await this.subscribeStatesAsync('action.download*');
|
|
121
|
+
await this.subscribeStatesAsync('action.exportPdf');
|
|
122
|
+
|
|
123
|
+
// Subscribe to adapter instance object changes for event-based generation
|
|
124
|
+
if (this.config.autoGenerateOnEvents) {
|
|
125
|
+
await this.subscribeForeignObjectsAsync('system.adapter.*');
|
|
126
|
+
this.log.info('Subscribed to adapter instance changes for event-based generation');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Set documentation language
|
|
130
|
+
const language = this.config.language || 'en';
|
|
131
|
+
this.i18n.setLanguage(language);
|
|
132
|
+
this.log.debug(`Using documentation language: ${language}`);
|
|
133
|
+
|
|
134
|
+
// Check if HTML template has changed since last generation — force regenerate if so
|
|
135
|
+
const { RENDERER_VERSION } = require('./lib/htmlRenderer');
|
|
136
|
+
const storedTemplateVersion = await this.getStateAsync('info.templateVersion');
|
|
137
|
+
const templateChanged = !storedTemplateVersion || storedTemplateVersion.val !== RENDERER_VERSION;
|
|
138
|
+
if (templateChanged) {
|
|
139
|
+
this.log.info(
|
|
140
|
+
`HTML template updated (${storedTemplateVersion ? storedTemplateVersion.val : 'none'} → ${RENDERER_VERSION}), forcing regeneration`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Run first generation in the background so slow steps (e.g. two Ollama calls) do not block
|
|
145
|
+
// onReady — otherwise info.connection stays false and the instance stays red for minutes or forever on error.
|
|
146
|
+
if (this.config.autoGenerateOnStart || templateChanged) {
|
|
147
|
+
const reasons = [];
|
|
148
|
+
if (this.config.autoGenerateOnStart) {
|
|
149
|
+
reasons.push('autoGenerateOnStart');
|
|
150
|
+
}
|
|
151
|
+
if (templateChanged) {
|
|
152
|
+
reasons.push('renderer/template version mismatch');
|
|
153
|
+
}
|
|
154
|
+
this.log.info(
|
|
155
|
+
`Queuing documentation generation on startup (${reasons.join(', ')}) — runs in background; watch for "Documentation generated via startup"`,
|
|
156
|
+
);
|
|
157
|
+
this.generateDocumentation('startup').catch(error => {
|
|
158
|
+
this.log.error(`Startup documentation generation failed: ${error.message}`);
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
this.log.info(
|
|
162
|
+
'Skipping startup documentation: autoGenerateOnStart is false and info.templateVersion already matches the current HTML renderer — HTML files are unchanged until you enable startup generation, trigger manual generate, or install an adapter version with a new renderer',
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Setup periodic auto-generation if interval is configured
|
|
167
|
+
if (this.config.autoGenerateInterval && this.config.autoGenerateInterval > 0) {
|
|
168
|
+
const intervalMs = this.config.autoGenerateInterval * 60 * 60 * 1000;
|
|
169
|
+
this.log.info(
|
|
170
|
+
`Setting up automatic documentation generation every ${this.config.autoGenerateInterval} hours`,
|
|
171
|
+
);
|
|
172
|
+
const updateNextGeneration = async () => {
|
|
173
|
+
const next = new Date(Date.now() + intervalMs);
|
|
174
|
+
await this.setStateAsync('info.nextGeneration', { val: next.toISOString(), ack: true });
|
|
175
|
+
};
|
|
176
|
+
await updateNextGeneration();
|
|
177
|
+
this.autoGenerateInterval = setInterval(async () => {
|
|
178
|
+
this.log.debug('Auto-generating documentation on schedule');
|
|
179
|
+
try {
|
|
180
|
+
await this.generateDocumentation('scheduled');
|
|
181
|
+
} catch (error) {
|
|
182
|
+
this.log.error(`Scheduled documentation generation failed: ${error.message}`);
|
|
183
|
+
}
|
|
184
|
+
await updateNextGeneration();
|
|
185
|
+
}, intervalMs);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
await this.setStateAsync('info.connection', { val: true, ack: true });
|
|
189
|
+
this.log.info('AutoDoc adapter started');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Is called when a subscribed foreign object changes.
|
|
194
|
+
* Triggers debounced documentation regeneration on adapter install/enable/disable.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} id Object ID.
|
|
197
|
+
* @param {object | null} obj New object value, or null if deleted.
|
|
198
|
+
*/
|
|
199
|
+
onObjectChange(id, obj) {
|
|
200
|
+
// Only react to adapter instance objects (system.adapter.NAME.NUMBER)
|
|
201
|
+
if (!id.startsWith('system.adapter.') || id.split('.').length !== 4) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const adapterName = id.split('.')[2];
|
|
206
|
+
if (adapterName === 'autodoc') {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const event = obj === null ? 'removed' : 'changed';
|
|
211
|
+
this.log.debug(`Adapter instance ${id} ${event} - scheduling documentation update`);
|
|
212
|
+
|
|
213
|
+
// Debounce: wait 30 seconds after the last change before regenerating
|
|
214
|
+
if (this.eventGenerateDebounce) {
|
|
215
|
+
clearTimeout(this.eventGenerateDebounce);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.eventGenerateDebounce = setTimeout(async () => {
|
|
219
|
+
this.eventGenerateDebounce = null;
|
|
220
|
+
this.log.info('Regenerating documentation after adapter change');
|
|
221
|
+
try {
|
|
222
|
+
await this.generateDocumentation('event');
|
|
223
|
+
} catch (error) {
|
|
224
|
+
this.log.error(`Event-based documentation generation failed: ${error.message}`);
|
|
225
|
+
}
|
|
226
|
+
}, 30000);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Is called when adapter shuts down - callback has to be called under any circumstances.
|
|
231
|
+
*
|
|
232
|
+
* @param {() => void} callback Function that finalizes adapter shutdown.
|
|
233
|
+
*/
|
|
234
|
+
onUnload(callback) {
|
|
235
|
+
// Clear periodic auto-generation timer
|
|
236
|
+
if (this.autoGenerateInterval) {
|
|
237
|
+
clearInterval(this.autoGenerateInterval);
|
|
238
|
+
this.autoGenerateInterval = null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Clear event debounce timer
|
|
242
|
+
if (this.eventGenerateDebounce) {
|
|
243
|
+
clearTimeout(this.eventGenerateDebounce);
|
|
244
|
+
this.eventGenerateDebounce = null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
this.setStateAsync('info.connection', { val: false, ack: true })
|
|
248
|
+
.then(() => callback())
|
|
249
|
+
.catch(() => callback());
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Whether the user requested to skip the rest of the optional script-source AI phase (setState / Admin button / sendTo).
|
|
254
|
+
*
|
|
255
|
+
* @returns {boolean} True if cancel was requested; cleared in `clearScriptSourceAiCancelRequest` after the script phase ends.
|
|
256
|
+
*/
|
|
257
|
+
isScriptSourceAiCancelRequested() {
|
|
258
|
+
return this._cancelScriptSourceAiRequested === true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Clear the script-source cancel flag (called when starting a new script KI run so an old request does not apply).
|
|
263
|
+
*
|
|
264
|
+
* @returns {void}
|
|
265
|
+
*/
|
|
266
|
+
clearScriptSourceAiCancelRequest() {
|
|
267
|
+
this._cancelScriptSourceAiRequested = false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Create custom states for the adapter.
|
|
272
|
+
*/
|
|
273
|
+
async createStates() {
|
|
274
|
+
/* eslint-disable-next-line jsdoc/check-tag-names -- JSDoc Record for checkJs/tsc */
|
|
275
|
+
/** @type {Record<string, ioBroker.StateCommon>} */
|
|
276
|
+
const definitions = {
|
|
277
|
+
'action.generate': {
|
|
278
|
+
name: 'Generate documentation',
|
|
279
|
+
type: 'boolean',
|
|
280
|
+
role: 'button',
|
|
281
|
+
read: false,
|
|
282
|
+
write: true,
|
|
283
|
+
def: false,
|
|
284
|
+
},
|
|
285
|
+
'action.downloadMarkdown': {
|
|
286
|
+
name: 'Download markdown documentation',
|
|
287
|
+
type: 'boolean',
|
|
288
|
+
role: 'button',
|
|
289
|
+
read: false,
|
|
290
|
+
write: true,
|
|
291
|
+
def: false,
|
|
292
|
+
},
|
|
293
|
+
'action.downloadJson': {
|
|
294
|
+
name: 'Download JSON documentation',
|
|
295
|
+
type: 'boolean',
|
|
296
|
+
role: 'button',
|
|
297
|
+
read: false,
|
|
298
|
+
write: true,
|
|
299
|
+
def: false,
|
|
300
|
+
},
|
|
301
|
+
'action.downloadHtml': {
|
|
302
|
+
name: 'Download HTML documentation',
|
|
303
|
+
type: 'boolean',
|
|
304
|
+
role: 'button',
|
|
305
|
+
read: false,
|
|
306
|
+
write: true,
|
|
307
|
+
def: false,
|
|
308
|
+
},
|
|
309
|
+
'action.exportPdf': {
|
|
310
|
+
name: 'Export PDF (three profiles from latest HTML)',
|
|
311
|
+
type: 'boolean',
|
|
312
|
+
role: 'button',
|
|
313
|
+
read: false,
|
|
314
|
+
write: true,
|
|
315
|
+
def: false,
|
|
316
|
+
},
|
|
317
|
+
'action.cancelScriptSourceAi': {
|
|
318
|
+
name: 'Cancel AI script source explanations (running generation)',
|
|
319
|
+
type: 'boolean',
|
|
320
|
+
role: 'button',
|
|
321
|
+
read: false,
|
|
322
|
+
write: true,
|
|
323
|
+
def: false,
|
|
324
|
+
},
|
|
325
|
+
'documentation.lastMarkdownFile': {
|
|
326
|
+
name: 'Last generated markdown filename',
|
|
327
|
+
type: 'string',
|
|
328
|
+
role: 'text',
|
|
329
|
+
read: true,
|
|
330
|
+
write: false,
|
|
331
|
+
def: '',
|
|
332
|
+
},
|
|
333
|
+
'documentation.lastHtmlFile': {
|
|
334
|
+
name: 'Last generated HTML filename',
|
|
335
|
+
type: 'string',
|
|
336
|
+
role: 'text',
|
|
337
|
+
read: true,
|
|
338
|
+
write: false,
|
|
339
|
+
def: '',
|
|
340
|
+
},
|
|
341
|
+
'documentation.lastJsonFile': {
|
|
342
|
+
name: 'Last generated JSON filename',
|
|
343
|
+
type: 'string',
|
|
344
|
+
role: 'text',
|
|
345
|
+
read: true,
|
|
346
|
+
write: false,
|
|
347
|
+
def: '',
|
|
348
|
+
},
|
|
349
|
+
'documentation.markdown': {
|
|
350
|
+
name: 'Last generated markdown content',
|
|
351
|
+
type: 'string',
|
|
352
|
+
role: 'text',
|
|
353
|
+
read: true,
|
|
354
|
+
write: false,
|
|
355
|
+
def: '',
|
|
356
|
+
},
|
|
357
|
+
'documentation.html': {
|
|
358
|
+
name: 'Last generated HTML content',
|
|
359
|
+
type: 'string',
|
|
360
|
+
role: 'text',
|
|
361
|
+
read: true,
|
|
362
|
+
write: false,
|
|
363
|
+
def: '',
|
|
364
|
+
},
|
|
365
|
+
'documentation.json': {
|
|
366
|
+
name: 'Last generated JSON content',
|
|
367
|
+
type: 'string',
|
|
368
|
+
role: 'json',
|
|
369
|
+
read: true,
|
|
370
|
+
write: false,
|
|
371
|
+
def: '{}',
|
|
372
|
+
},
|
|
373
|
+
'documentation.stateSummary': {
|
|
374
|
+
name: 'State objects summary (JSON)',
|
|
375
|
+
type: 'string',
|
|
376
|
+
role: 'json',
|
|
377
|
+
read: true,
|
|
378
|
+
write: false,
|
|
379
|
+
def: '{}',
|
|
380
|
+
},
|
|
381
|
+
'documentation.exportHashes': {
|
|
382
|
+
name: 'SHA-256 of latest MD / JSON / Admin HTML exports (hex)',
|
|
383
|
+
type: 'string',
|
|
384
|
+
role: 'json',
|
|
385
|
+
read: true,
|
|
386
|
+
write: false,
|
|
387
|
+
def: '{}',
|
|
388
|
+
},
|
|
389
|
+
'info.lastGeneration': {
|
|
390
|
+
name: 'Last generation timestamp',
|
|
391
|
+
type: 'string',
|
|
392
|
+
role: 'text',
|
|
393
|
+
read: true,
|
|
394
|
+
write: false,
|
|
395
|
+
def: '',
|
|
396
|
+
},
|
|
397
|
+
'info.nextGeneration': {
|
|
398
|
+
name: 'Next scheduled generation timestamp',
|
|
399
|
+
type: 'string',
|
|
400
|
+
role: 'text',
|
|
401
|
+
read: true,
|
|
402
|
+
write: false,
|
|
403
|
+
def: '',
|
|
404
|
+
},
|
|
405
|
+
'info.lastTrigger': {
|
|
406
|
+
name: 'Last generation trigger',
|
|
407
|
+
type: 'string',
|
|
408
|
+
role: 'text',
|
|
409
|
+
read: true,
|
|
410
|
+
write: false,
|
|
411
|
+
def: '',
|
|
412
|
+
},
|
|
413
|
+
'info.templateVersion': {
|
|
414
|
+
name: 'HTML renderer version used for last generation',
|
|
415
|
+
type: 'string',
|
|
416
|
+
role: 'text',
|
|
417
|
+
read: true,
|
|
418
|
+
write: false,
|
|
419
|
+
def: '',
|
|
420
|
+
},
|
|
421
|
+
'info.summary': {
|
|
422
|
+
name: 'Documentation summary',
|
|
423
|
+
type: 'string',
|
|
424
|
+
role: 'text',
|
|
425
|
+
read: true,
|
|
426
|
+
write: false,
|
|
427
|
+
def: '',
|
|
428
|
+
},
|
|
429
|
+
'info.systemLanguage': {
|
|
430
|
+
name: 'System language',
|
|
431
|
+
type: 'string',
|
|
432
|
+
role: 'text',
|
|
433
|
+
read: true,
|
|
434
|
+
write: false,
|
|
435
|
+
def: '',
|
|
436
|
+
},
|
|
437
|
+
'info.instanceCount': {
|
|
438
|
+
name: 'Number of adapter instances',
|
|
439
|
+
type: 'number',
|
|
440
|
+
role: 'value',
|
|
441
|
+
read: true,
|
|
442
|
+
write: false,
|
|
443
|
+
def: 0,
|
|
444
|
+
},
|
|
445
|
+
'info.enabledInstanceCount': {
|
|
446
|
+
name: 'Number of enabled adapter instances',
|
|
447
|
+
type: 'number',
|
|
448
|
+
role: 'value',
|
|
449
|
+
read: true,
|
|
450
|
+
write: false,
|
|
451
|
+
def: 0,
|
|
452
|
+
},
|
|
453
|
+
'info.disabledInstanceCount': {
|
|
454
|
+
name: 'Number of disabled adapter instances',
|
|
455
|
+
type: 'number',
|
|
456
|
+
role: 'value',
|
|
457
|
+
read: true,
|
|
458
|
+
write: false,
|
|
459
|
+
def: 0,
|
|
460
|
+
},
|
|
461
|
+
'info.instanceHosts': {
|
|
462
|
+
name: 'Instance host summary',
|
|
463
|
+
type: 'string',
|
|
464
|
+
role: 'json',
|
|
465
|
+
read: true,
|
|
466
|
+
write: false,
|
|
467
|
+
def: '{}',
|
|
468
|
+
},
|
|
469
|
+
'info.hostName': {
|
|
470
|
+
name: 'Host name',
|
|
471
|
+
type: 'string',
|
|
472
|
+
role: 'text',
|
|
473
|
+
read: true,
|
|
474
|
+
write: false,
|
|
475
|
+
def: '',
|
|
476
|
+
},
|
|
477
|
+
'info.hostPlatform': {
|
|
478
|
+
name: 'ioBroker host platform (e.g. Javascript/Node.js)',
|
|
479
|
+
type: 'string',
|
|
480
|
+
role: 'text',
|
|
481
|
+
read: true,
|
|
482
|
+
write: false,
|
|
483
|
+
def: '',
|
|
484
|
+
},
|
|
485
|
+
'info.hostOperatingSystem': {
|
|
486
|
+
name: 'Host operating system (from js-controller, native.os)',
|
|
487
|
+
type: 'string',
|
|
488
|
+
role: 'text',
|
|
489
|
+
read: true,
|
|
490
|
+
write: false,
|
|
491
|
+
def: '',
|
|
492
|
+
},
|
|
493
|
+
'info.hostVersion': {
|
|
494
|
+
name: 'Host version',
|
|
495
|
+
type: 'string',
|
|
496
|
+
role: 'text',
|
|
497
|
+
read: true,
|
|
498
|
+
write: false,
|
|
499
|
+
def: '',
|
|
500
|
+
},
|
|
501
|
+
'info.totalStateObjects': {
|
|
502
|
+
name: 'Total number of detected state objects',
|
|
503
|
+
type: 'number',
|
|
504
|
+
role: 'value',
|
|
505
|
+
read: true,
|
|
506
|
+
write: false,
|
|
507
|
+
def: 0,
|
|
508
|
+
},
|
|
509
|
+
'info.writableStateObjects': {
|
|
510
|
+
name: 'Number of writable state objects',
|
|
511
|
+
type: 'number',
|
|
512
|
+
role: 'value',
|
|
513
|
+
read: true,
|
|
514
|
+
write: false,
|
|
515
|
+
def: 0,
|
|
516
|
+
},
|
|
517
|
+
'info.readonlyStateObjects': {
|
|
518
|
+
name: 'Number of read-only state objects',
|
|
519
|
+
type: 'number',
|
|
520
|
+
role: 'value',
|
|
521
|
+
read: true,
|
|
522
|
+
write: false,
|
|
523
|
+
def: 0,
|
|
524
|
+
},
|
|
525
|
+
'info.htmlUrl': {
|
|
526
|
+
name: 'Direct URL to latest HTML documentation (primary profile)',
|
|
527
|
+
type: 'string',
|
|
528
|
+
role: 'url',
|
|
529
|
+
read: true,
|
|
530
|
+
write: false,
|
|
531
|
+
def: '',
|
|
532
|
+
},
|
|
533
|
+
'info.htmlUrlAdmin': {
|
|
534
|
+
name: 'Direct URL to Admin HTML documentation',
|
|
535
|
+
type: 'string',
|
|
536
|
+
role: 'url',
|
|
537
|
+
read: true,
|
|
538
|
+
write: false,
|
|
539
|
+
def: '',
|
|
540
|
+
},
|
|
541
|
+
'info.htmlUrlUser': {
|
|
542
|
+
name: 'Direct URL to User/Family HTML documentation',
|
|
543
|
+
type: 'string',
|
|
544
|
+
role: 'url',
|
|
545
|
+
read: true,
|
|
546
|
+
write: false,
|
|
547
|
+
def: '',
|
|
548
|
+
},
|
|
549
|
+
'info.htmlUrlOnboarding': {
|
|
550
|
+
name: 'Direct URL to Onboarding HTML documentation',
|
|
551
|
+
type: 'string',
|
|
552
|
+
role: 'url',
|
|
553
|
+
read: true,
|
|
554
|
+
write: false,
|
|
555
|
+
def: '',
|
|
556
|
+
},
|
|
557
|
+
'info.forumCardPlain': {
|
|
558
|
+
name: 'Forum system card (plaintext, last generation)',
|
|
559
|
+
type: 'string',
|
|
560
|
+
role: 'text',
|
|
561
|
+
read: true,
|
|
562
|
+
write: false,
|
|
563
|
+
def: '',
|
|
564
|
+
},
|
|
565
|
+
'info.aiScriptSourceProgress': {
|
|
566
|
+
name: 'AI script source progress (current/total, or —)',
|
|
567
|
+
type: 'string',
|
|
568
|
+
role: 'text',
|
|
569
|
+
read: true,
|
|
570
|
+
write: false,
|
|
571
|
+
def: '—',
|
|
572
|
+
},
|
|
573
|
+
'versioning.lastDocumentModel': {
|
|
574
|
+
name: 'Last generated document model (JSON)',
|
|
575
|
+
type: 'string',
|
|
576
|
+
role: 'json',
|
|
577
|
+
read: true,
|
|
578
|
+
write: false,
|
|
579
|
+
def: '{}',
|
|
580
|
+
},
|
|
581
|
+
'versioning.latestVersion': {
|
|
582
|
+
name: 'Latest documentation version',
|
|
583
|
+
type: 'string',
|
|
584
|
+
role: 'text',
|
|
585
|
+
read: true,
|
|
586
|
+
write: false,
|
|
587
|
+
def: '',
|
|
588
|
+
},
|
|
589
|
+
'versioning.changeCount': {
|
|
590
|
+
name: 'Number of changes in latest version',
|
|
591
|
+
type: 'number',
|
|
592
|
+
role: 'value',
|
|
593
|
+
read: true,
|
|
594
|
+
write: false,
|
|
595
|
+
def: 0,
|
|
596
|
+
},
|
|
597
|
+
'versioning.changelog': {
|
|
598
|
+
name: 'Complete changelog history',
|
|
599
|
+
type: 'string',
|
|
600
|
+
role: 'json',
|
|
601
|
+
read: true,
|
|
602
|
+
write: false,
|
|
603
|
+
def: '[]',
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
for (const [id, common] of Object.entries(definitions)) {
|
|
608
|
+
await this.setObjectNotExistsAsync(id, {
|
|
609
|
+
type: 'state',
|
|
610
|
+
common,
|
|
611
|
+
native: {},
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Build human readable summary string.
|
|
618
|
+
*
|
|
619
|
+
* @param {object} docModel Structured documentation model.
|
|
620
|
+
* @returns {string} Summary string.
|
|
621
|
+
*/
|
|
622
|
+
buildSummary(docModel) {
|
|
623
|
+
const stateSummary = docModel.appendices.stateSummary;
|
|
624
|
+
|
|
625
|
+
return `Dokumentation für "${docModel.system.projectName}" erzeugt: ${docModel.system.statistics.instanceCount} Instanzen, ${docModel.system.statistics.enabledInstanceCount} aktiviert, ${docModel.system.statistics.disabledInstanceCount} deaktiviert, ${stateSummary.total} State-Objekte (${stateSummary.writable} schreibbar, ${stateSummary.readonly} nur lesbar).`;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Delete oldest timestamped autodoc files, keeping only the newest `maxFiles` of each type.
|
|
630
|
+
*
|
|
631
|
+
* @param {string} basePath ioBroker file namespace path.
|
|
632
|
+
* @param {number} maxFiles Maximum number of timestamped files to keep per type.
|
|
633
|
+
* @returns {Promise<void>}
|
|
634
|
+
*/
|
|
635
|
+
async rotateFiles(basePath, maxFiles) {
|
|
636
|
+
try {
|
|
637
|
+
const files = await this.readDirAsync(basePath, '');
|
|
638
|
+
const names = files.map(f => f.file);
|
|
639
|
+
|
|
640
|
+
// md and json: autodoc-TIMESTAMP.md / .json
|
|
641
|
+
for (const ext of ['md', 'json']) {
|
|
642
|
+
const pattern = /^autodoc-\d{4}-\d{2}-\d{2}T/;
|
|
643
|
+
const typed = names.filter(n => n.endsWith(`.${ext}`) && pattern.test(n)).sort();
|
|
644
|
+
if (typed.length > maxFiles) {
|
|
645
|
+
const toDelete = typed.slice(0, typed.length - maxFiles);
|
|
646
|
+
for (const name of toDelete) {
|
|
647
|
+
try {
|
|
648
|
+
await this.delFileAsync(basePath, name);
|
|
649
|
+
this.log.debug(`Rotated old file: ${name}`);
|
|
650
|
+
} catch (e) {
|
|
651
|
+
this.log.warn(`Could not delete old file ${name}: ${e.message}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// html: autodoc-{profile}-TIMESTAMP.html (three profiles)
|
|
658
|
+
for (const profile of ['admin', 'user', 'onboarding']) {
|
|
659
|
+
const pattern = new RegExp(`^autodoc-${profile}-\\d{4}-\\d{2}-\\d{2}T`);
|
|
660
|
+
const typed = names.filter(n => n.endsWith('.html') && pattern.test(n)).sort();
|
|
661
|
+
if (typed.length > maxFiles) {
|
|
662
|
+
const toDelete = typed.slice(0, typed.length - maxFiles);
|
|
663
|
+
for (const name of toDelete) {
|
|
664
|
+
try {
|
|
665
|
+
await this.delFileAsync(basePath, name);
|
|
666
|
+
this.log.debug(`Rotated old file: ${name}`);
|
|
667
|
+
} catch (e) {
|
|
668
|
+
this.log.warn(`Could not delete old file ${name}: ${e.message}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
} catch (e) {
|
|
674
|
+
this.log.warn(`File rotation failed: ${e.message}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Warn if AutoDoc is running on a non-primary host in a multihost setup.
|
|
680
|
+
* AutoDoc should run on the master to ensure correct filesystem export and npm access.
|
|
681
|
+
*
|
|
682
|
+
* @returns {Promise<void>}
|
|
683
|
+
*/
|
|
684
|
+
async checkMultihostPlacement() {
|
|
685
|
+
try {
|
|
686
|
+
const hostsView = await this.getObjectViewAsync('system', 'host', {});
|
|
687
|
+
const hostIds = (hostsView && hostsView.rows ? hostsView.rows : [])
|
|
688
|
+
.map(r => r.id)
|
|
689
|
+
.filter(Boolean)
|
|
690
|
+
.sort();
|
|
691
|
+
if (hostIds.length <= 1) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const currentHostId = `system.host.${this.host}`;
|
|
696
|
+
if (hostIds[0] !== currentHostId) {
|
|
697
|
+
this.log.warn(
|
|
698
|
+
`Multihost setup detected (${hostIds.length} hosts). AutoDoc is running on "${this.host}" which may not be the master host. ` +
|
|
699
|
+
`For correct filesystem export and npm access, AutoDoc should run on the master. ` +
|
|
700
|
+
`Detected hosts: ${hostIds.map(h => h.replace('system.host.', '')).join(', ')}`,
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
} catch (e) {
|
|
704
|
+
this.log.debug(`Multihost check skipped: ${e.message}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* @returns {boolean} True when large documentation strings are not stored in object states.
|
|
710
|
+
*/
|
|
711
|
+
isDocumentationStatesMetadataOnly() {
|
|
712
|
+
const m = String(this.config.documentationStatesMode || 'full').toLowerCase();
|
|
713
|
+
return m === 'metadata' || m === 'metadata_only' || m === 'files_only';
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Read a UTF-8 file from this adapter's ioBroker file namespace.
|
|
718
|
+
*
|
|
719
|
+
* @param {string} basePath e.g. `autodoc.0.files`
|
|
720
|
+
* @param {string} filename File name under that namespace
|
|
721
|
+
* @returns {Promise<string>} file body as UTF-8 or empty if missing
|
|
722
|
+
*/
|
|
723
|
+
async readAdapterFileUtf8(basePath, filename) {
|
|
724
|
+
const res = await this.readFileAsync(basePath, filename);
|
|
725
|
+
if (res == null) {
|
|
726
|
+
return '';
|
|
727
|
+
}
|
|
728
|
+
const raw = res.file !== undefined ? res.file : res;
|
|
729
|
+
if (Buffer.isBuffer(raw)) {
|
|
730
|
+
return raw.toString('utf8');
|
|
731
|
+
}
|
|
732
|
+
if (typeof raw === 'string') {
|
|
733
|
+
return raw;
|
|
734
|
+
}
|
|
735
|
+
return String(raw);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Build a direct URL to autodoc-latest.html via the web or admin adapter.
|
|
740
|
+
*
|
|
741
|
+
* @returns {Promise<string>} URL string or empty string if not determinable.
|
|
742
|
+
*/
|
|
743
|
+
async buildBaseUrl() {
|
|
744
|
+
try {
|
|
745
|
+
// User-configured base URL takes priority (solves Docker/hostname issues)
|
|
746
|
+
if (this.config.baseUrl) {
|
|
747
|
+
let base = this.config.baseUrl.trim().replace(/\/$/, '');
|
|
748
|
+
if (!base.startsWith('http://') && !base.startsWith('https://')) {
|
|
749
|
+
base = `http://${base}`;
|
|
750
|
+
}
|
|
751
|
+
return base;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Auto-detect: try web adapter for port, fall back to admin
|
|
755
|
+
const host = this.host || 'localhost';
|
|
756
|
+
const webObj = await this.getForeignObjectAsync('system.adapter.web.0');
|
|
757
|
+
if (webObj && webObj.native) {
|
|
758
|
+
const port = webObj.native.port || 8082;
|
|
759
|
+
const secure = webObj.native.secure ? 'https' : 'http';
|
|
760
|
+
return `${secure}://${host}:${port}`;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return `http://${host}:8081`;
|
|
764
|
+
} catch (e) {
|
|
765
|
+
this.log.warn(`Could not build base URL: ${e.message}`);
|
|
766
|
+
return '';
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Persist generated documentation and info states.
|
|
772
|
+
*
|
|
773
|
+
* @param {object} docModel Structured documentation model.
|
|
774
|
+
* @param {string} markdown Markdown output.
|
|
775
|
+
* @param {{ admin: string, user: string, onboarding: string }} htmlAll HTML per profile.
|
|
776
|
+
* @param {string} json JSON output.
|
|
777
|
+
* @param {string} [prebuiltBaseUrl] Pre-built base URL (avoids a second buildBaseUrl() call).
|
|
778
|
+
* @returns {Promise<void>} Promise that resolves when states are written.
|
|
779
|
+
*/
|
|
780
|
+
async persistDocumentation(docModel, markdown, htmlAll, json, prebuiltBaseUrl) {
|
|
781
|
+
try {
|
|
782
|
+
const now = new Date();
|
|
783
|
+
const pad = n => String(n).padStart(2, '0');
|
|
784
|
+
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
|
|
785
|
+
const basePath = `${this.namespace}.files`;
|
|
786
|
+
|
|
787
|
+
// Save timestamped markdown + json
|
|
788
|
+
const markdownFilename = `autodoc-${timestamp}.md`;
|
|
789
|
+
await this.writeFileAsync(basePath, markdownFilename, markdown);
|
|
790
|
+
this.log.info(`Markdown saved to /files/${this.namespace}/${markdownFilename}`);
|
|
791
|
+
|
|
792
|
+
const jsonFilename = `autodoc-${timestamp}.json`;
|
|
793
|
+
await this.writeFileAsync(basePath, jsonFilename, json);
|
|
794
|
+
|
|
795
|
+
// Save all three HTML profiles (timestamped)
|
|
796
|
+
const profiles = ['admin', 'user', 'onboarding'];
|
|
797
|
+
for (const profile of profiles) {
|
|
798
|
+
const content = htmlAll[profile];
|
|
799
|
+
if (content) {
|
|
800
|
+
const filename = `autodoc-${profile}-${timestamp}.html`;
|
|
801
|
+
await this.writeFileAsync(basePath, filename, content);
|
|
802
|
+
this.log.info(`HTML (${profile}) saved to /files/${this.namespace}/${filename}`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Save fixed "latest" files for direct access
|
|
807
|
+
await this.writeFileAsync(basePath, 'autodoc-latest.md', markdown);
|
|
808
|
+
await this.writeFileAsync(basePath, 'autodoc-latest.json', json);
|
|
809
|
+
await this.writeFileAsync(basePath, 'autodoc-admin.html', htmlAll.admin);
|
|
810
|
+
await this.writeFileAsync(basePath, 'autodoc-user.html', htmlAll.user);
|
|
811
|
+
await this.writeFileAsync(basePath, 'autodoc-onboarding.html', htmlAll.onboarding);
|
|
812
|
+
// Keep autodoc-latest.html pointing to the admin profile for backward compat
|
|
813
|
+
await this.writeFileAsync(basePath, 'autodoc-latest.html', htmlAll.admin);
|
|
814
|
+
|
|
815
|
+
// Rotate old timestamped files
|
|
816
|
+
const maxFiles = this.config.maxStoredFiles > 0 ? this.config.maxStoredFiles : 3;
|
|
817
|
+
await this.rotateFiles(basePath, maxFiles);
|
|
818
|
+
|
|
819
|
+
// Optional filesystem export — write HTML files to a real OS path outside ioBroker's DB
|
|
820
|
+
await this.exportToFilesystem(htmlAll);
|
|
821
|
+
|
|
822
|
+
// Build and store profile URLs (use pre-built URL from generateDocumentation if available)
|
|
823
|
+
const baseUrl = prebuiltBaseUrl !== undefined ? prebuiltBaseUrl : await this.buildBaseUrl();
|
|
824
|
+
const filePath = profile => `/files/${this.namespace}.files/autodoc-${profile}.html`;
|
|
825
|
+
await this.setStateAsync('info.htmlUrlAdmin', { val: `${baseUrl}${filePath('admin')}`, ack: true });
|
|
826
|
+
await this.setStateAsync('info.htmlUrlUser', { val: `${baseUrl}${filePath('user')}`, ack: true });
|
|
827
|
+
await this.setStateAsync('info.htmlUrlOnboarding', {
|
|
828
|
+
val: `${baseUrl}${filePath('onboarding')}`,
|
|
829
|
+
ack: true,
|
|
830
|
+
});
|
|
831
|
+
// Legacy state: keep pointing to admin
|
|
832
|
+
const htmlUrl = `${baseUrl}${filePath('admin')}`;
|
|
833
|
+
await this.setStateAsync('info.htmlUrl', { val: htmlUrl, ack: true });
|
|
834
|
+
|
|
835
|
+
// Update info states (keep metadata as states for quick access)
|
|
836
|
+
const summary = this.buildSummary(docModel);
|
|
837
|
+
const stateSummaryJson = JSON.stringify(docModel.appendices.stateSummary, null, 2);
|
|
838
|
+
const hostSummaryJson = JSON.stringify(docModel.system.hosts, null, 2);
|
|
839
|
+
|
|
840
|
+
await this.setStateAsync('documentation.lastMarkdownFile', { val: markdownFilename, ack: true });
|
|
841
|
+
await this.setStateAsync('documentation.lastHtmlFile', {
|
|
842
|
+
val: `autodoc-admin-${timestamp}.html`,
|
|
843
|
+
ack: true,
|
|
844
|
+
});
|
|
845
|
+
await this.setStateAsync('documentation.lastJsonFile', { val: jsonFilename, ack: true });
|
|
846
|
+
|
|
847
|
+
const metadataOnly = this.isDocumentationStatesMetadataOnly();
|
|
848
|
+
if (metadataOnly) {
|
|
849
|
+
this.log.info(
|
|
850
|
+
'Documentation states: metadata-only mode — markdown/HTML/JSON are not duplicated in object states (see /files autodoc-latest.*).',
|
|
851
|
+
);
|
|
852
|
+
await this.setStateAsync('documentation.markdown', { val: DOCS_STATE_METADATA_PLACEHOLDER, ack: true });
|
|
853
|
+
await this.setStateAsync('documentation.html', { val: DOCS_STATE_METADATA_PLACEHOLDER, ack: true });
|
|
854
|
+
await this.setStateAsync('documentation.json', { val: DOCS_JSON_METADATA_PLACEHOLDER, ack: true });
|
|
855
|
+
} else {
|
|
856
|
+
await this.setStateAsync('documentation.markdown', { val: markdown, ack: true });
|
|
857
|
+
await this.setStateAsync('documentation.html', { val: htmlAll.admin, ack: true });
|
|
858
|
+
await this.setStateAsync('documentation.json', { val: json, ack: true });
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
let exportHashes = {
|
|
862
|
+
'autodoc-latest.md': sha256HexUtf8(markdown),
|
|
863
|
+
'autodoc-latest.json': sha256HexUtf8(json),
|
|
864
|
+
'autodoc-admin.html': sha256HexUtf8(htmlAll.admin),
|
|
865
|
+
};
|
|
866
|
+
await this.setStateAsync('documentation.exportHashes', {
|
|
867
|
+
val: JSON.stringify(exportHashes),
|
|
868
|
+
ack: true,
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
await this.setStateAsync('documentation.stateSummary', { val: stateSummaryJson, ack: true });
|
|
872
|
+
|
|
873
|
+
await this.setStateAsync('info.summary', { val: summary, ack: true });
|
|
874
|
+
await this.setStateAsync('info.lastTrigger', { val: docModel.meta.trigger, ack: true });
|
|
875
|
+
await this.setStateAsync('info.lastGeneration', { val: docModel.meta.generatedAt, ack: true });
|
|
876
|
+
await this.setStateAsync('info.templateVersion', {
|
|
877
|
+
val: require('./lib/htmlRenderer').RENDERER_VERSION,
|
|
878
|
+
ack: true,
|
|
879
|
+
});
|
|
880
|
+
await this.setStateAsync('info.systemLanguage', { val: docModel.meta.language, ack: true });
|
|
881
|
+
await this.setStateAsync('info.instanceCount', {
|
|
882
|
+
val: docModel.system.statistics.instanceCount,
|
|
883
|
+
ack: true,
|
|
884
|
+
});
|
|
885
|
+
await this.setStateAsync('info.enabledInstanceCount', {
|
|
886
|
+
val: docModel.system.statistics.enabledInstanceCount,
|
|
887
|
+
ack: true,
|
|
888
|
+
});
|
|
889
|
+
await this.setStateAsync('info.disabledInstanceCount', {
|
|
890
|
+
val: docModel.system.statistics.disabledInstanceCount,
|
|
891
|
+
ack: true,
|
|
892
|
+
});
|
|
893
|
+
await this.setStateAsync('info.instanceHosts', { val: hostSummaryJson, ack: true });
|
|
894
|
+
await this.setStateAsync('info.hostName', { val: docModel.system.primaryHost.name, ack: true });
|
|
895
|
+
await this.setStateAsync('info.hostPlatform', { val: docModel.system.primaryHost.platform, ack: true });
|
|
896
|
+
await this.setStateAsync('info.hostOperatingSystem', {
|
|
897
|
+
val: docModel.system.primaryHost.operatingSystem || '',
|
|
898
|
+
ack: true,
|
|
899
|
+
});
|
|
900
|
+
await this.setStateAsync('info.hostVersion', { val: docModel.system.primaryHost.version, ack: true });
|
|
901
|
+
await this.setStateAsync('info.totalStateObjects', {
|
|
902
|
+
val: docModel.appendices.stateSummary.total,
|
|
903
|
+
ack: true,
|
|
904
|
+
});
|
|
905
|
+
await this.setStateAsync('info.writableStateObjects', {
|
|
906
|
+
val: docModel.appendices.stateSummary.writable,
|
|
907
|
+
ack: true,
|
|
908
|
+
});
|
|
909
|
+
await this.setStateAsync('info.readonlyStateObjects', {
|
|
910
|
+
val: docModel.appendices.stateSummary.readonly,
|
|
911
|
+
ack: true,
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
const forum = buildForumCard(docModel, this.i18n);
|
|
915
|
+
await this.setStateAsync('info.forumCardPlain', { val: forum.plaintext, ack: true });
|
|
916
|
+
|
|
917
|
+
if (this.config.pdfExportAfterGeneration) {
|
|
918
|
+
const pdfHashes = await this.runPdfExport(htmlAll, `after-generation (${docModel.meta.trigger})`);
|
|
919
|
+
if (pdfHashes && Object.keys(pdfHashes).length > 0) {
|
|
920
|
+
exportHashes = { ...exportHashes, ...pdfHashes };
|
|
921
|
+
await this.setStateAsync('documentation.exportHashes', {
|
|
922
|
+
val: JSON.stringify(exportHashes),
|
|
923
|
+
ack: true,
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
} catch (error) {
|
|
928
|
+
this.log.error(`Error persisting documentation: ${error.message}`);
|
|
929
|
+
throw error;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Export HTML profiles to a real filesystem path (opt-in via config.exportPath).
|
|
935
|
+
* Runs after the ioBroker file write — failures produce a warning but do not abort generation.
|
|
936
|
+
*
|
|
937
|
+
* @param {{ admin: string, user: string, onboarding: string }} htmlAll HTML per profile.
|
|
938
|
+
* @returns {Promise<void>}
|
|
939
|
+
*/
|
|
940
|
+
async exportToFilesystem(htmlAll) {
|
|
941
|
+
const exportPath = (this.config.exportPath || '').trim();
|
|
942
|
+
if (!exportPath) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
try {
|
|
947
|
+
await fs.promises.mkdir(exportPath, { recursive: true });
|
|
948
|
+
const profiles = ['admin', 'user', 'onboarding'];
|
|
949
|
+
for (const profile of profiles) {
|
|
950
|
+
const content = htmlAll[profile];
|
|
951
|
+
if (!content) {
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
const dest = path.join(exportPath, `autodoc-${profile}.html`);
|
|
955
|
+
await fs.promises.writeFile(dest, content, 'utf8');
|
|
956
|
+
}
|
|
957
|
+
this.log.info(`Filesystem export written to: ${exportPath}`);
|
|
958
|
+
} catch (e) {
|
|
959
|
+
this.log.warn(`Filesystem export failed (${exportPath}): ${e.message} — ioBroker output unaffected`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Write `autodoc-*.pdf` to ioBroker /files and optionally to `exportPath` (mirrors HTML export).
|
|
965
|
+
*
|
|
966
|
+
* @param {{ admin?: string, user?: string, onboarding?: string }} htmlAll HTML UTF-8 strings.
|
|
967
|
+
* @param {string} contextLabel Log context (e.g. trigger name).
|
|
968
|
+
* @returns {Promise<Record<string, string>>} Map `autodoc-*.pdf` filename → SHA-256 hex for files written under /files
|
|
969
|
+
*/
|
|
970
|
+
async runPdfExport(htmlAll, contextLabel) {
|
|
971
|
+
const pdfHashes = {};
|
|
972
|
+
try {
|
|
973
|
+
const { renderProfilesToPdfBuffers } = require('./lib/htmlToPdf');
|
|
974
|
+
|
|
975
|
+
const buffers = await renderProfilesToPdfBuffers(htmlAll, line =>
|
|
976
|
+
this.log.info(`PDF (${contextLabel}): ${line}`),
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
const basePath = `${this.namespace}.files`;
|
|
980
|
+
const minBytes = 800;
|
|
981
|
+
|
|
982
|
+
for (const profile of ['admin', 'user', 'onboarding']) {
|
|
983
|
+
const buf = buffers[profile];
|
|
984
|
+
if (buf && buf.length >= minBytes) {
|
|
985
|
+
const fname = `autodoc-${profile}.pdf`;
|
|
986
|
+
await this.writeFileAsync(basePath, fname, buf);
|
|
987
|
+
const hex = sha256HexBuffer(buf);
|
|
988
|
+
if (hex) {
|
|
989
|
+
pdfHashes[fname] = hex;
|
|
990
|
+
}
|
|
991
|
+
this.log.info(
|
|
992
|
+
`PDF (${contextLabel}): saved files/${this.namespace}/${fname} (${buf.length} bytes)`,
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
await this.exportPdfBuffersToFilesystem(buffers, contextLabel);
|
|
998
|
+
} catch (e) {
|
|
999
|
+
this.log.warn(`PDF (${contextLabel}) skipped or failed: ${e.message}`);
|
|
1000
|
+
}
|
|
1001
|
+
return pdfHashes;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Merge PDF SHA-256 entries into `documentation.exportHashes` (manual export or late PDF run).
|
|
1006
|
+
*
|
|
1007
|
+
* @param {Record<string, string>} pdfHashes -
|
|
1008
|
+
* @returns {Promise<void>}
|
|
1009
|
+
*/
|
|
1010
|
+
async mergeExportHashesPdf(pdfHashes) {
|
|
1011
|
+
if (!pdfHashes || Object.keys(pdfHashes).length === 0) {
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
try {
|
|
1015
|
+
const st = await this.getStateAsync('documentation.exportHashes');
|
|
1016
|
+
const raw = st && Object.prototype.hasOwnProperty.call(st, 'val') ? st.val : null;
|
|
1017
|
+
let base = {};
|
|
1018
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
1019
|
+
try {
|
|
1020
|
+
const parsed = JSON.parse(raw);
|
|
1021
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
1022
|
+
base = parsed;
|
|
1023
|
+
}
|
|
1024
|
+
} catch {
|
|
1025
|
+
/* keep base */
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
const merged = { ...base, ...pdfHashes };
|
|
1029
|
+
await this.setStateAsync('documentation.exportHashes', {
|
|
1030
|
+
val: JSON.stringify(merged),
|
|
1031
|
+
ack: true,
|
|
1032
|
+
});
|
|
1033
|
+
} catch (e) {
|
|
1034
|
+
this.log.debug(`mergeExportHashesPdf: ${e.message}`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Copy generated PDF buffers to `config.exportPath` when set (same semantics as HTML export).
|
|
1040
|
+
*
|
|
1041
|
+
* @param {Partial<Record<string, Buffer>>} buffers Named PDF buffers per profile.
|
|
1042
|
+
* @param {string} contextLabel Used in log prefix.
|
|
1043
|
+
* @returns {Promise<void>}
|
|
1044
|
+
*/
|
|
1045
|
+
async exportPdfBuffersToFilesystem(buffers, contextLabel) {
|
|
1046
|
+
const exportPath = (this.config.exportPath || '').trim();
|
|
1047
|
+
if (!exportPath) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
await fs.promises.mkdir(exportPath, { recursive: true });
|
|
1052
|
+
for (const profile of ['admin', 'user', 'onboarding']) {
|
|
1053
|
+
const buf = buffers[profile];
|
|
1054
|
+
if (buf && buf.length > 500) {
|
|
1055
|
+
const dest = path.join(exportPath, `autodoc-${profile}.pdf`);
|
|
1056
|
+
await fs.promises.writeFile(dest, buf);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
this.log.info(`PDF (${contextLabel}): filesystem mirrors written to ${exportPath}`);
|
|
1060
|
+
} catch (e) {
|
|
1061
|
+
this.log.warn(`PDF (${contextLabel}): filesystem export failed (${exportPath}): ${e.message}`);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Build PDFs from the latest stored HTML profiles (button under adapter states).
|
|
1067
|
+
*
|
|
1068
|
+
* @returns {Promise<void>}
|
|
1069
|
+
*/
|
|
1070
|
+
async exportPdfFromLatestHtmlFiles() {
|
|
1071
|
+
const basePath = `${this.namespace}.files`;
|
|
1072
|
+
const htmlAll = {
|
|
1073
|
+
admin: await this.readAdapterFileUtf8(basePath, 'autodoc-admin.html'),
|
|
1074
|
+
user: await this.readAdapterFileUtf8(basePath, 'autodoc-user.html'),
|
|
1075
|
+
onboarding: await this.readAdapterFileUtf8(basePath, 'autodoc-onboarding.html'),
|
|
1076
|
+
};
|
|
1077
|
+
const any = htmlAll.admin || htmlAll.user || htmlAll.onboarding;
|
|
1078
|
+
if (!any) {
|
|
1079
|
+
this.log.warn('PDF export: no autodoc-*.html files found — run documentation generation first.');
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
const pdfHashes = await this.runPdfExport(htmlAll, 'manual-exportPdf-state');
|
|
1083
|
+
await this.mergeExportHashesPdf(pdfHashes);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Generate and store documentation.
|
|
1088
|
+
*
|
|
1089
|
+
* @param {string} trigger Generation trigger source.
|
|
1090
|
+
*/
|
|
1091
|
+
async generateDocumentation(trigger) {
|
|
1092
|
+
if (this._documentationGenerationInProgress) {
|
|
1093
|
+
this.log.warn(
|
|
1094
|
+
`Documentation generation is already running — ignoring duplicate trigger "${trigger}". With Ollama, one run can take many minutes (two model calls + optional German polish). Wait for "Documentation generated via …" before starting another.`,
|
|
1095
|
+
);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
this._documentationGenerationInProgress = true;
|
|
1099
|
+
try {
|
|
1100
|
+
this.log.info(
|
|
1101
|
+
`Documentation generation (${trigger}): 1/5 — discovery (scanning system, may take a bit on large installs)…`,
|
|
1102
|
+
);
|
|
1103
|
+
const rawData = await this.discovery.collectRawData();
|
|
1104
|
+
|
|
1105
|
+
this.log.info(`Documentation generation (${trigger}): 2/5 — building document model…`);
|
|
1106
|
+
const baseUrl = await this.buildBaseUrl();
|
|
1107
|
+
const profileFilePath = profile => `/files/${this.namespace}.files/autodoc-${profile}.html`;
|
|
1108
|
+
const publicDocUrls = {
|
|
1109
|
+
admin: baseUrl ? `${baseUrl}${profileFilePath('admin')}` : '',
|
|
1110
|
+
user: baseUrl ? `${baseUrl}${profileFilePath('user')}` : '',
|
|
1111
|
+
onboarding: baseUrl ? `${baseUrl}${profileFilePath('onboarding')}` : '',
|
|
1112
|
+
};
|
|
1113
|
+
const docModel = await this.documentModel.buildDocumentModel(rawData, trigger, { publicDocUrls });
|
|
1114
|
+
|
|
1115
|
+
const version = this.versionTracker.generateVersion();
|
|
1116
|
+
docModel.meta.version = version;
|
|
1117
|
+
|
|
1118
|
+
const previousDocModel = await this.versionTracker.getPreviousVersion();
|
|
1119
|
+
const changeData = this.versionTracker.compareVersions(docModel, previousDocModel);
|
|
1120
|
+
|
|
1121
|
+
docModel.changelog = await this.versionTracker.getChangelog();
|
|
1122
|
+
|
|
1123
|
+
this.log.info(
|
|
1124
|
+
`Documentation generation (${trigger}): 3/5 — AI enhancement (disabled=instant; else two LLM calls — local Ollama often several minutes each)…`,
|
|
1125
|
+
);
|
|
1126
|
+
docModel.ai = await this.aiEnhancer.enhance(docModel, rawData);
|
|
1127
|
+
|
|
1128
|
+
this.log.info(`Documentation generation (${trigger}): 4/5 — rendering Markdown and HTML…`);
|
|
1129
|
+
const markdown = this.markdownRenderer.renderMarkdown(docModel);
|
|
1130
|
+
|
|
1131
|
+
// Pre-build URLs and QR code SVGs so HTML is fully self-contained (no CDN)
|
|
1132
|
+
const renderUrls = publicDocUrls;
|
|
1133
|
+
const renderQrSvgs = {};
|
|
1134
|
+
for (const [profile, url] of Object.entries(renderUrls)) {
|
|
1135
|
+
if (url) {
|
|
1136
|
+
try {
|
|
1137
|
+
renderQrSvgs[profile] = await QRCode.toString(url, { type: 'svg', margin: 1, width: 120 });
|
|
1138
|
+
} catch (e) {
|
|
1139
|
+
this.log.debug(`QR code generation skipped for ${profile}: ${e.message}`);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
const renderOptions = { urls: renderUrls, qrSvgs: renderQrSvgs };
|
|
1144
|
+
|
|
1145
|
+
const htmlAll = await this.htmlRenderer.renderAllHtml(docModel, renderOptions);
|
|
1146
|
+
const json = JSON.stringify(docModel, null, 2);
|
|
1147
|
+
|
|
1148
|
+
this.log.info(`Documentation generation (${trigger}): 5/5 — writing files and updating states…`);
|
|
1149
|
+
await this.persistDocumentation(docModel, markdown, htmlAll, json, baseUrl);
|
|
1150
|
+
|
|
1151
|
+
await this.versionTracker.storeCurrentVersion(docModel);
|
|
1152
|
+
|
|
1153
|
+
const changelogEntry = this.versionTracker.buildChangelogEntry(version, changeData);
|
|
1154
|
+
await this.versionTracker.appendChangelog(changelogEntry);
|
|
1155
|
+
|
|
1156
|
+
await this.notifier.send(docModel, changeData);
|
|
1157
|
+
|
|
1158
|
+
this.log.info(`Documentation generated via ${trigger} (v${version}) - ${changeData.summary}`);
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
this.log.error(`Error generating documentation: ${error.message}`);
|
|
1161
|
+
throw error;
|
|
1162
|
+
} finally {
|
|
1163
|
+
this._documentationGenerationInProgress = false;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Copy latest documentation from adapter files to a fixed filename (e.g. autodoc.md).
|
|
1169
|
+
* Prefers content from `autodoc-latest.*` files; falls back to legacy full state only if not a metadata placeholder.
|
|
1170
|
+
*
|
|
1171
|
+
* @param {string} stateId State id suffix e.g. `documentation.markdown` (no namespace).
|
|
1172
|
+
* @param {string} filename Target filename under the adapter file namespace.
|
|
1173
|
+
*/
|
|
1174
|
+
async downloadFile(stateId, filename) {
|
|
1175
|
+
const basePath = `${this.namespace}.files`;
|
|
1176
|
+
const sourceMap = {
|
|
1177
|
+
'documentation.markdown': 'autodoc-latest.md',
|
|
1178
|
+
'documentation.json': 'autodoc-latest.json',
|
|
1179
|
+
'documentation.html': 'autodoc-admin.html',
|
|
1180
|
+
};
|
|
1181
|
+
const sourceName = sourceMap[stateId];
|
|
1182
|
+
if (!sourceName) {
|
|
1183
|
+
this.log.warn(`Download: unknown state mapping for ${stateId}`);
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
try {
|
|
1187
|
+
let content = '';
|
|
1188
|
+
try {
|
|
1189
|
+
content = await this.readAdapterFileUtf8(basePath, sourceName);
|
|
1190
|
+
} catch (e) {
|
|
1191
|
+
this.log.debug(`readAdapterFileUtf8(${sourceName}): ${e.message}`);
|
|
1192
|
+
}
|
|
1193
|
+
if (!String(content || '').trim()) {
|
|
1194
|
+
const state = await this.getStateAsync(stateId);
|
|
1195
|
+
const sv = state && state.val != null ? String(state.val) : '';
|
|
1196
|
+
if (sv.startsWith('[AutoDoc]')) {
|
|
1197
|
+
// metadata-only placeholder
|
|
1198
|
+
} else if (stateId === 'documentation.json' && sv === DOCS_JSON_METADATA_PLACEHOLDER) {
|
|
1199
|
+
// metadata-only JSON placeholder
|
|
1200
|
+
} else if (sv) {
|
|
1201
|
+
content = sv;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
if (!String(content || '').trim()) {
|
|
1205
|
+
this.log.warn(`No content for download (${sourceName}). Generate documentation first.`);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
await this.writeFileAsync(basePath, filename, String(content));
|
|
1209
|
+
this.log.info(`File ${filename} written to /files/${this.namespace}/${filename}`);
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
this.log.error(`Download failed for ${filename}: ${error.message}`);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* Is called if a subscribed state changes.
|
|
1217
|
+
*
|
|
1218
|
+
* @param {string} id State ID.
|
|
1219
|
+
* @param {ioBroker.State | null | undefined} state State object.
|
|
1220
|
+
*/
|
|
1221
|
+
async onStateChange(id, state) {
|
|
1222
|
+
if (!state) {
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
if (id === `${this.namespace}.action.generate` && state.ack === false && state.val === true) {
|
|
1227
|
+
this.log.info('Manual generate command received');
|
|
1228
|
+
try {
|
|
1229
|
+
await this.generateDocumentation('manual');
|
|
1230
|
+
} catch (err) {
|
|
1231
|
+
this.log.error(`Manual generation failed: ${err.message}`);
|
|
1232
|
+
}
|
|
1233
|
+
await this.setStateAsync('action.generate', { val: false, ack: true });
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
if (id === `${this.namespace}.action.downloadMarkdown` && state.ack === false && state.val === true) {
|
|
1238
|
+
this.log.info('Manual markdown download command received');
|
|
1239
|
+
try {
|
|
1240
|
+
await this.downloadFile('documentation.markdown', 'autodoc.md');
|
|
1241
|
+
} catch (err) {
|
|
1242
|
+
this.log.error(`Markdown download failed: ${err.message}`);
|
|
1243
|
+
}
|
|
1244
|
+
await this.setStateAsync('action.downloadMarkdown', { val: false, ack: true });
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
if (id === `${this.namespace}.action.downloadJson` && state.ack === false && state.val === true) {
|
|
1249
|
+
this.log.info('Manual JSON download command received');
|
|
1250
|
+
try {
|
|
1251
|
+
await this.downloadFile('documentation.json', 'autodoc.json');
|
|
1252
|
+
} catch (err) {
|
|
1253
|
+
this.log.error(`JSON download failed: ${err.message}`);
|
|
1254
|
+
}
|
|
1255
|
+
await this.setStateAsync('action.downloadJson', { val: false, ack: true });
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (id === `${this.namespace}.action.downloadHtml` && state.ack === false && state.val === true) {
|
|
1260
|
+
this.log.info('Manual HTML download command received');
|
|
1261
|
+
try {
|
|
1262
|
+
await this.downloadFile('documentation.html', 'autodoc.html');
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
this.log.error(`HTML download failed: ${err.message}`);
|
|
1265
|
+
}
|
|
1266
|
+
await this.setStateAsync('action.downloadHtml', { val: false, ack: true });
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (id === `${this.namespace}.action.exportPdf` && state.ack === false && state.val === true) {
|
|
1271
|
+
this.log.info('PDF export command received (latest HTML files)');
|
|
1272
|
+
try {
|
|
1273
|
+
await this.exportPdfFromLatestHtmlFiles();
|
|
1274
|
+
} catch (err) {
|
|
1275
|
+
this.log.error(`PDF export failed: ${err.message}`);
|
|
1276
|
+
}
|
|
1277
|
+
await this.setStateAsync('action.exportPdf', { val: false, ack: true });
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
if (id === `${this.namespace}.action.cancelScriptSourceAi` && state.ack === false && state.val === true) {
|
|
1282
|
+
this._cancelScriptSourceAiRequested = true;
|
|
1283
|
+
this.log.info(
|
|
1284
|
+
'Cancel script-source AI: stop requested — will take effect after the current script request finishes (if any), then the rest of the script phase is skipped.',
|
|
1285
|
+
);
|
|
1286
|
+
await this.setStateAsync('action.cancelScriptSourceAi', { val: false, ack: true });
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Handle sendTo messages — used by the "Generate now" button and external scripts.
|
|
1291
|
+
*
|
|
1292
|
+
* @param {object} obj Message object from ioBroker.
|
|
1293
|
+
*/
|
|
1294
|
+
async onMessage(obj) {
|
|
1295
|
+
if (!obj || typeof obj !== 'object' || !obj.command) {
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
if (obj.command === 'generateNow') {
|
|
1300
|
+
this.log.info('Generate-now requested via sendTo');
|
|
1301
|
+
if (obj.callback) {
|
|
1302
|
+
this.sendTo(obj.from, obj.command, { result: 'ok' }, obj.callback);
|
|
1303
|
+
}
|
|
1304
|
+
this.generateDocumentation('manual').catch(err => {
|
|
1305
|
+
this.log.error(`sendTo generate failed: ${err.message}`);
|
|
1306
|
+
});
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
if (obj.command === 'cancelScriptSourceAi') {
|
|
1311
|
+
this._cancelScriptSourceAiRequested = true;
|
|
1312
|
+
this.log.info(
|
|
1313
|
+
'Cancel script-source AI: stop requested (sendTo) — takes effect after the current script KI request finishes.',
|
|
1314
|
+
);
|
|
1315
|
+
if (obj.callback) {
|
|
1316
|
+
this.sendTo(obj.from, obj.command, { result: 'ok' }, obj.callback);
|
|
1317
|
+
}
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
if (obj.command === 'getStatus') {
|
|
1322
|
+
try {
|
|
1323
|
+
const lastGen = await this.getStateAsync('info.lastGeneration');
|
|
1324
|
+
const nextGen = await this.getStateAsync('info.nextGeneration');
|
|
1325
|
+
const lastTrigger = await this.getStateAsync('info.lastTrigger');
|
|
1326
|
+
const lastVal = lastGen && lastGen.val ? String(lastGen.val) : '';
|
|
1327
|
+
const nextVal = nextGen && nextGen.val ? String(nextGen.val) : '';
|
|
1328
|
+
const triggerVal = lastTrigger && lastTrigger.val ? String(lastTrigger.val) : '';
|
|
1329
|
+
const display = lastVal
|
|
1330
|
+
? `${lastVal}${triggerVal ? ` (${triggerVal})` : ''}${nextVal ? ` · Next: ${nextVal}` : ''}`
|
|
1331
|
+
: 'Not yet generated';
|
|
1332
|
+
if (obj.callback) {
|
|
1333
|
+
this.sendTo(obj.from, obj.command, display, obj.callback);
|
|
1334
|
+
}
|
|
1335
|
+
} catch {
|
|
1336
|
+
if (obj.callback) {
|
|
1337
|
+
this.sendTo(obj.from, obj.command, 'Error reading status', obj.callback);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
if (obj.command === 'getForumCard') {
|
|
1344
|
+
try {
|
|
1345
|
+
const st = await this.getStateAsync('info.forumCardPlain');
|
|
1346
|
+
const text = st && st.val != null ? String(st.val).trim() : '';
|
|
1347
|
+
const display = text || 'Generate documentation first — no forum card yet.';
|
|
1348
|
+
if (obj.callback) {
|
|
1349
|
+
this.sendTo(obj.from, obj.command, display, obj.callback);
|
|
1350
|
+
}
|
|
1351
|
+
} catch {
|
|
1352
|
+
if (obj.callback) {
|
|
1353
|
+
this.sendTo(obj.from, obj.command, 'Error reading forum card', obj.callback);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
if (require.main !== module) {
|
|
1361
|
+
/**
|
|
1362
|
+
* @param {Partial<utils.AdapterOptions>} [options] Adapter options.
|
|
1363
|
+
* @returns {Autodoc} Adapter instance.
|
|
1364
|
+
*/
|
|
1365
|
+
module.exports = options => new Autodoc(options);
|
|
1366
|
+
} else {
|
|
1367
|
+
new Autodoc();
|
|
1368
|
+
}
|