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,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small Mermaid flowchart: adapter instances grouped by ioBroker host.
|
|
3
|
+
* Phase 5.x.3 — bounded auto-graph only (hard node limit); not a full dependency graph.
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const MAX_OUTPUT_CHARS = 12000;
|
|
8
|
+
|
|
9
|
+
function shortInstanceId(fullId) {
|
|
10
|
+
const s = String(fullId || '');
|
|
11
|
+
const p = 'system.adapter.';
|
|
12
|
+
if (s.startsWith(p)) {
|
|
13
|
+
return s.slice(p.length);
|
|
14
|
+
}
|
|
15
|
+
return s;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function mermaidEscapeLabel(s, maxLen) {
|
|
19
|
+
let t = String(s || '')
|
|
20
|
+
.replace(/\r\n/g, '\n')
|
|
21
|
+
.replace(/[\r\n]+/g, ' ')
|
|
22
|
+
.replace(/["]/g, "'")
|
|
23
|
+
.replace(/[[\]]/g, ' ')
|
|
24
|
+
.trim();
|
|
25
|
+
if (t.length > maxLen) {
|
|
26
|
+
t = `${t.slice(0, maxLen - 1)}…`;
|
|
27
|
+
}
|
|
28
|
+
return t;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function instanceGraphLabel(inst) {
|
|
32
|
+
let s = shortInstanceId(inst.id);
|
|
33
|
+
if (inst.enabled === false) {
|
|
34
|
+
s += ' (off)';
|
|
35
|
+
}
|
|
36
|
+
return s;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fair cap: round-robin take up to `max` instances across hosts.
|
|
41
|
+
*
|
|
42
|
+
* @param {Array<{host:string,inst:object,label:string}>} rows Per-instance rows with host bucket key
|
|
43
|
+
* @param {number} max Maximum number of instances to keep
|
|
44
|
+
* @returns {Array<{host:string,inst:object,label:string}>} Possibly shortened list
|
|
45
|
+
*/
|
|
46
|
+
function pickInstancesRoundRobin(rows, max) {
|
|
47
|
+
if (rows.length <= max) {
|
|
48
|
+
return rows.slice();
|
|
49
|
+
}
|
|
50
|
+
const byHost = {};
|
|
51
|
+
for (const r of rows) {
|
|
52
|
+
if (!byHost[r.host]) {
|
|
53
|
+
byHost[r.host] = [];
|
|
54
|
+
}
|
|
55
|
+
byHost[r.host].push(r);
|
|
56
|
+
}
|
|
57
|
+
const hosts = Object.keys(byHost).sort((a, b) => String(a).localeCompare(String(b)));
|
|
58
|
+
const picked = [];
|
|
59
|
+
let idx = 0;
|
|
60
|
+
while (picked.length < max) {
|
|
61
|
+
let progressed = false;
|
|
62
|
+
for (const h of hosts) {
|
|
63
|
+
if (picked.length >= max) {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
const bucket = byHost[h];
|
|
67
|
+
if (idx < bucket.length) {
|
|
68
|
+
picked.push(bucket[idx]);
|
|
69
|
+
progressed = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!progressed) {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
idx += 1;
|
|
76
|
+
}
|
|
77
|
+
return picked;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build Mermaid source for host-grouped adapter instances.
|
|
82
|
+
*
|
|
83
|
+
* @param {Record<string, object[]>} hostsMap Host name → instances (`docModel.adapters.hosts`)
|
|
84
|
+
* @param {{ enabled?: boolean, maxNodes?: number }} opts Toggle and instance-node budget (clamped 8–200)
|
|
85
|
+
* @returns {string} Mermaid flowchart source or ''
|
|
86
|
+
*/
|
|
87
|
+
function buildAutoHostTopologyMermaid(hostsMap, opts) {
|
|
88
|
+
if (!opts || opts.enabled !== true || !hostsMap || typeof hostsMap !== 'object') {
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
let maxNodes = parseInt(String(opts.maxNodes != null ? opts.maxNodes : 40), 10);
|
|
92
|
+
if (!Number.isFinite(maxNodes)) {
|
|
93
|
+
maxNodes = 40;
|
|
94
|
+
}
|
|
95
|
+
maxNodes = Math.min(200, Math.max(8, maxNodes));
|
|
96
|
+
|
|
97
|
+
const hostNames = Object.keys(hostsMap).sort((a, b) => String(a).localeCompare(String(b)));
|
|
98
|
+
if (hostNames.length === 0) {
|
|
99
|
+
return '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const rows = [];
|
|
103
|
+
let totalAvailable = 0;
|
|
104
|
+
for (const host of hostNames) {
|
|
105
|
+
const list = hostsMap[host] || [];
|
|
106
|
+
const sorted = [...list].sort((a, b) => shortInstanceId(a.id).localeCompare(shortInstanceId(b.id)));
|
|
107
|
+
for (const inst of sorted) {
|
|
108
|
+
const hKey = host != null && String(host) !== '' ? String(host) : '(host)';
|
|
109
|
+
rows.push({
|
|
110
|
+
host: hKey,
|
|
111
|
+
inst,
|
|
112
|
+
label: instanceGraphLabel(inst),
|
|
113
|
+
});
|
|
114
|
+
totalAvailable += 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (rows.length === 0) {
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const picked = pickInstancesRoundRobin(rows, maxNodes);
|
|
122
|
+
const byHost = {};
|
|
123
|
+
for (const r of picked) {
|
|
124
|
+
if (!byHost[r.host]) {
|
|
125
|
+
byHost[r.host] = [];
|
|
126
|
+
}
|
|
127
|
+
byHost[r.host].push(r);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const hostOrder = hostNames.filter(h => byHost[h] && byHost[h].length > 0);
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* Grid layout via invisible links (~~~, Mermaid ≥10.2):
|
|
134
|
+
* Nodes are split into columns; within each column they are chained with ~~~ so
|
|
135
|
+
* the layout engine places them compactly instead of distributing isolated nodes
|
|
136
|
+
* with large gaps. flowchart LR + direction TB per subgraph → columns appear
|
|
137
|
+
* side-by-side inside each host box; multiple host boxes appear side-by-side.
|
|
138
|
+
* Auto column count: ceil(sqrt(n)) capped at 5 keeps the grid roughly square.
|
|
139
|
+
*
|
|
140
|
+
* Active instances get class `activeNode` (slate-400 fill, dark text) — neutral
|
|
141
|
+
* enough for both light and dark themes. Disabled instances get `offNode`
|
|
142
|
+
* (darker fill, dashed border, muted text) so they are visually distinct.
|
|
143
|
+
*/
|
|
144
|
+
const lines = [
|
|
145
|
+
'flowchart LR',
|
|
146
|
+
'classDef activeNode fill:#94a3b8,stroke:#64748b,color:#1e293b',
|
|
147
|
+
'classDef offNode fill:#475569,stroke:#64748b,color:#94a3b8,stroke-dasharray:5 3',
|
|
148
|
+
];
|
|
149
|
+
if (totalAvailable > picked.length) {
|
|
150
|
+
lines.push(`%% ${picked.length} / ${totalAvailable} instances (limit ${maxNodes})`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let sid = 0;
|
|
154
|
+
let nid = 0;
|
|
155
|
+
for (const host of hostOrder) {
|
|
156
|
+
const sg = `sg${sid}`;
|
|
157
|
+
sid += 1;
|
|
158
|
+
const hostLbl = mermaidEscapeLabel(`Host: ${host}`, 120);
|
|
159
|
+
const hostRows = byHost[host];
|
|
160
|
+
const numCols = Math.min(5, Math.max(1, Math.ceil(Math.sqrt(hostRows.length))));
|
|
161
|
+
|
|
162
|
+
/* Assign stable node ids and split into columns (distribute row-by-row). */
|
|
163
|
+
// eslint-disable-next-line jsdoc/check-tag-names -- @type required: TS infers [] as never[] inside Array.from initializer
|
|
164
|
+
/** @type {string[][]} */
|
|
165
|
+
const colBuckets = Array.from({ length: numCols }, () => []);
|
|
166
|
+
const nodeIds = hostRows.map(r => {
|
|
167
|
+
const id = `n${nid}`;
|
|
168
|
+
nid += 1;
|
|
169
|
+
return { id, lbl: mermaidEscapeLabel(r.label, 100), off: r.inst.enabled === false };
|
|
170
|
+
});
|
|
171
|
+
nodeIds.forEach((n, i) => colBuckets[i % numCols].push(n.id));
|
|
172
|
+
|
|
173
|
+
lines.push(` subgraph ${sg} ["${hostLbl}"]`);
|
|
174
|
+
lines.push(` direction TB`);
|
|
175
|
+
/* Node declarations — disabled instances get the offNode class */
|
|
176
|
+
for (const { id, lbl, off } of nodeIds) {
|
|
177
|
+
lines.push(off ? ` ${id}["${lbl}"]:::offNode` : ` ${id}["${lbl}"]:::activeNode`);
|
|
178
|
+
}
|
|
179
|
+
/* Invisible column chains → compact vertical stacking */
|
|
180
|
+
for (const col of colBuckets) {
|
|
181
|
+
if (col.length > 1) {
|
|
182
|
+
lines.push(` ${col.join(' ~~~ ')}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
lines.push(' end');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let out = lines.join('\n');
|
|
189
|
+
if (out.length > MAX_OUTPUT_CHARS) {
|
|
190
|
+
out = `${out.slice(0, MAX_OUTPUT_CHARS - 24)}\n%% [truncated]`;
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = { buildAutoHostTopologyMermaid };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AutoDoc Dependency Analyzer
|
|
3
|
+
* Extracts ioBroker state references from JavaScript/Blockly script source code.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Functions that receive a state ID as their first argument
|
|
7
|
+
const STATE_FUNCTIONS = [
|
|
8
|
+
'getState',
|
|
9
|
+
'getStateAsync',
|
|
10
|
+
'setState',
|
|
11
|
+
'setStateAsync',
|
|
12
|
+
'createState',
|
|
13
|
+
'createStateAsync',
|
|
14
|
+
'deleteState',
|
|
15
|
+
'deleteStateAsync',
|
|
16
|
+
'existsState',
|
|
17
|
+
'getObject',
|
|
18
|
+
'getObjectAsync',
|
|
19
|
+
'setObject',
|
|
20
|
+
'setObjectAsync',
|
|
21
|
+
'subscribeStates',
|
|
22
|
+
'unsubscribeStates',
|
|
23
|
+
'on',
|
|
24
|
+
'subscribe',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Regex: capture the first string argument of the above functions
|
|
28
|
+
// Handles single quotes, double quotes and backticks (static strings only)
|
|
29
|
+
const FUNC_PATTERN = new RegExp(`(?:${STATE_FUNCTIONS.join('|')})\\s*\\(\\s*(['"\`])([^'"\`\\n]{3,80})\\1`, 'g');
|
|
30
|
+
|
|
31
|
+
// A valid ioBroker state ID contains at least one dot-separated numeric segment
|
|
32
|
+
// e.g. hm-rpc.0.ABC123.STATE or javascript.0.myVar
|
|
33
|
+
const STATE_ID_RE = /^[a-zA-Z0-9_-]+\.\d+\.[a-zA-Z0-9_.%-]+$/;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extract all likely state IDs referenced in a script source string.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} source JavaScript source code
|
|
39
|
+
* @returns {string[]} Deduplicated, sorted list of referenced state IDs
|
|
40
|
+
*/
|
|
41
|
+
function extractStateRefs(source) {
|
|
42
|
+
if (!source) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const found = new Set();
|
|
47
|
+
let match;
|
|
48
|
+
|
|
49
|
+
FUNC_PATTERN.lastIndex = 0;
|
|
50
|
+
while ((match = FUNC_PATTERN.exec(source)) !== null) {
|
|
51
|
+
const candidate = match[2].trim();
|
|
52
|
+
if (STATE_ID_RE.test(candidate)) {
|
|
53
|
+
found.add(candidate);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return [...found].sort();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build a cross-reference map: stateId → list of script names that reference it.
|
|
62
|
+
*
|
|
63
|
+
* @param {Array<{name: string, stateRefs: string[]}>} scripts Scripts with stateRefs
|
|
64
|
+
* @returns {Array<{stateId: string, scripts: string[]}>} Sorted cross-reference entries
|
|
65
|
+
*/
|
|
66
|
+
function buildCrossRef(scripts) {
|
|
67
|
+
const map = new Map();
|
|
68
|
+
|
|
69
|
+
for (const script of scripts) {
|
|
70
|
+
for (const stateId of script.stateRefs) {
|
|
71
|
+
if (!map.has(stateId)) {
|
|
72
|
+
map.set(stateId, []);
|
|
73
|
+
}
|
|
74
|
+
map.get(stateId).push(script.name);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return [...map.entries()]
|
|
79
|
+
.map(([stateId, scriptNames]) => ({ stateId, scripts: scriptNames }))
|
|
80
|
+
.sort((a, b) => a.stateId.localeCompare(b.stateId));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { extractStateRefs, buildCrossRef };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data-driven "snapshot" signals for family-facing troubleshooting (Phase 5.x.1).
|
|
3
|
+
* Mirrors the Admin diagnosis node check — keep logic in one place.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} [nodeVersion] e.g. v22.4.0
|
|
8
|
+
* @returns {boolean} True if Admin diagnosis would flag Node (non-LTS or < 20)
|
|
9
|
+
*/
|
|
10
|
+
function isNodeVersionFlaggedForDiagnosis(nodeVersion) {
|
|
11
|
+
if (!nodeVersion) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const match = String(nodeVersion).match(/v?(\d+)/);
|
|
15
|
+
const major = match ? parseInt(match[1], 10) : 0;
|
|
16
|
+
return major > 0 && (major < 20 || major % 2 !== 0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {object} [docModel] - document model; used to read host Node version for Admin-parity check
|
|
21
|
+
* @returns {boolean} True if User/Onboarding should show a concrete auto-checklist (currently: Node only)
|
|
22
|
+
*/
|
|
23
|
+
function hasFamilyDiagnosisSnapshot(docModel) {
|
|
24
|
+
return isNodeVersionFlaggedForDiagnosis(
|
|
25
|
+
docModel && docModel.system && docModel.system.primaryHost ? docModel.system.primaryHost.nodeVersion : '',
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
isNodeVersionFlaggedForDiagnosis,
|
|
31
|
+
hasFamilyDiagnosisSnapshot,
|
|
32
|
+
};
|