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,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 5.x.2 — structured Quick Start + room guide picks from discovery (no invented facts).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const MAX_SYSTEM_ITEMS = 5;
|
|
6
|
+
const MAX_ROOMS = 8;
|
|
7
|
+
const HIGHLIGHTS_PER_ROOM = 3;
|
|
8
|
+
const MAX_SCRIPT_SNIPPET = 120;
|
|
9
|
+
|
|
10
|
+
/** Caps for guest Onboarding quick start (same facts, shorter than User “at a glance”). */
|
|
11
|
+
const ONBOARDING_VIEW_MAX_SYSTEM_ITEMS = 3;
|
|
12
|
+
const ONBOARDING_VIEW_MAX_ROOMS = 4;
|
|
13
|
+
const ONBOARDING_VIEW_HIGHLIGHTS_PER_ROOM = 2;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Narrow the shared `quickStart` model for Onboarding HTML/Markdown (Phase 5.x.2 guest UX).
|
|
17
|
+
*
|
|
18
|
+
* @param {{ hasContent?: boolean, systemItems?: object[], roomGuides?: object[] } | null | undefined} qs Full quick-start payload from `buildQuickStartGuide`
|
|
19
|
+
* @returns {{ hasContent: boolean, systemItems: object[], roomGuides: object[] }} Shorter lists for guest presentation
|
|
20
|
+
*/
|
|
21
|
+
function sliceQuickStartForOnboarding(qs) {
|
|
22
|
+
if (!qs || !qs.hasContent) {
|
|
23
|
+
return { hasContent: false, systemItems: [], roomGuides: [] };
|
|
24
|
+
}
|
|
25
|
+
const systemItems = (qs.systemItems || []).slice(0, ONBOARDING_VIEW_MAX_SYSTEM_ITEMS);
|
|
26
|
+
const roomGuides = (qs.roomGuides || []).slice(0, ONBOARDING_VIEW_MAX_ROOMS).map(rg => ({
|
|
27
|
+
...rg,
|
|
28
|
+
highlights: (rg.highlights || []).slice(0, ONBOARDING_VIEW_HIGHLIGHTS_PER_ROOM),
|
|
29
|
+
}));
|
|
30
|
+
const hasContent = systemItems.length > 0 || roomGuides.some(r => (r.highlights || []).length > 0);
|
|
31
|
+
return { hasContent, systemItems, roomGuides };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {object} roomsBlock — return value of DocumentModel#buildRooms
|
|
36
|
+
* @param {object} scriptsBlock — { scripts: Array }
|
|
37
|
+
* @returns {{ hasContent: boolean, systemItems: object[], roomGuides: object[] }} - structured quick-start payload for the renderer
|
|
38
|
+
*/
|
|
39
|
+
function buildQuickStartGuide(roomsBlock, scriptsBlock) {
|
|
40
|
+
const systemItems = [];
|
|
41
|
+
const roomsB = roomsBlock || {};
|
|
42
|
+
const fnList = Array.isArray(roomsB.functions) ? [...roomsB.functions] : [];
|
|
43
|
+
fnList.sort((a, b) => (b.memberCount || 0) - (a.memberCount || 0));
|
|
44
|
+
const topFn = fnList.slice(0, 3);
|
|
45
|
+
|
|
46
|
+
if (roomsB.totalRooms > 0) {
|
|
47
|
+
systemItems.push({
|
|
48
|
+
kind: 'roomCount',
|
|
49
|
+
n: roomsB.totalRooms,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const f of topFn) {
|
|
54
|
+
if (systemItems.length >= MAX_SYSTEM_ITEMS) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
if (f && f.name) {
|
|
58
|
+
systemItems.push({
|
|
59
|
+
kind: 'function',
|
|
60
|
+
name: f.name,
|
|
61
|
+
memberCount: f.memberCount || 0,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const scriptList = (scriptsBlock && scriptsBlock.scripts) || [];
|
|
67
|
+
const withDesc = scriptList.filter(s => s.enabled && s.desc && String(s.desc).trim());
|
|
68
|
+
withDesc.sort((a, b) => {
|
|
69
|
+
const na = String((a && a.name) || (a && a.id) || '');
|
|
70
|
+
const nb = String((b && b.name) || (b && b.id) || '');
|
|
71
|
+
return na.localeCompare(nb, undefined, { sensitivity: 'base' });
|
|
72
|
+
});
|
|
73
|
+
for (const s of withDesc) {
|
|
74
|
+
if (systemItems.length >= MAX_SYSTEM_ITEMS) {
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
const line = String(s.desc).split('\n')[0].trim().slice(0, MAX_SCRIPT_SNIPPET);
|
|
78
|
+
if (line) {
|
|
79
|
+
systemItems.push({
|
|
80
|
+
kind: 'script',
|
|
81
|
+
name: s.name || s.id,
|
|
82
|
+
desc: line,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
while (systemItems.length > MAX_SYSTEM_ITEMS) {
|
|
88
|
+
systemItems.pop();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const roomList = Array.isArray(roomsB.rooms) ? [...roomsB.rooms] : [];
|
|
92
|
+
roomList.sort((a, b) => {
|
|
93
|
+
const da = a && Array.isArray(a.devices) ? a.devices.length : 0;
|
|
94
|
+
const db = b && Array.isArray(b.devices) ? b.devices.length : 0;
|
|
95
|
+
if (db !== da) {
|
|
96
|
+
return db - da;
|
|
97
|
+
}
|
|
98
|
+
const na = a && a.name ? String(a.name) : '';
|
|
99
|
+
const nb = b && b.name ? String(b.name) : '';
|
|
100
|
+
return na.localeCompare(nb, undefined, { sensitivity: 'base' });
|
|
101
|
+
});
|
|
102
|
+
const roomGuides = [];
|
|
103
|
+
let roomIndex = 0;
|
|
104
|
+
for (const room of roomList) {
|
|
105
|
+
if (roomIndex >= MAX_ROOMS) {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
if (!room || !room.name) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const devs = Array.isArray(room.devices) ? [...room.devices] : [];
|
|
112
|
+
devs.sort((a, b) => {
|
|
113
|
+
const as = a && a.currentValue != null && a.currentValue !== '' ? 1 : 0;
|
|
114
|
+
const bs = b && b.currentValue != null && b.currentValue !== '' ? 1 : 0;
|
|
115
|
+
return bs - as;
|
|
116
|
+
});
|
|
117
|
+
const byCat = new Map();
|
|
118
|
+
const rest = [];
|
|
119
|
+
for (const d of devs) {
|
|
120
|
+
if (!d) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const cat = d.category || 'other';
|
|
124
|
+
if (!byCat.has(cat)) {
|
|
125
|
+
byCat.set(cat, d);
|
|
126
|
+
} else {
|
|
127
|
+
rest.push(d);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const picked = Array.from(byCat.values());
|
|
131
|
+
for (const d of rest) {
|
|
132
|
+
if (picked.length >= HIGHLIGHTS_PER_ROOM) {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
if (!picked.includes(d)) {
|
|
136
|
+
picked.push(d);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const d of devs) {
|
|
140
|
+
if (picked.length >= HIGHLIGHTS_PER_ROOM) {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
if (!picked.includes(d)) {
|
|
144
|
+
picked.push(d);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const highlights = picked.slice(0, HIGHLIGHTS_PER_ROOM).map(d => ({
|
|
148
|
+
deviceName: d.deviceName || '',
|
|
149
|
+
icon: d.icon || '📦',
|
|
150
|
+
category: d.category || '',
|
|
151
|
+
valueText:
|
|
152
|
+
d.currentValue != null && d.currentValue !== ''
|
|
153
|
+
? String(d.currentValue) + (d.unit ? ` ${d.unit}` : '')
|
|
154
|
+
: '',
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
if (highlights.length > 0) {
|
|
158
|
+
roomGuides.push({
|
|
159
|
+
name: room.name,
|
|
160
|
+
deviceCount: room.memberCount != null ? room.memberCount : devs.length,
|
|
161
|
+
highlights,
|
|
162
|
+
});
|
|
163
|
+
roomIndex += 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const hasContent = systemItems.length > 0 || roomGuides.length > 0;
|
|
168
|
+
return { hasContent, systemItems, roomGuides };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
buildQuickStartGuide,
|
|
173
|
+
sliceQuickStartForOnboarding,
|
|
174
|
+
MAX_ROOMS,
|
|
175
|
+
HIGHLIGHTS_PER_ROOM,
|
|
176
|
+
MAX_SYSTEM_ITEMS,
|
|
177
|
+
ONBOARDING_VIEW_MAX_SYSTEM_ITEMS,
|
|
178
|
+
ONBOARDING_VIEW_MAX_ROOMS,
|
|
179
|
+
ONBOARDING_VIEW_HIGHLIGHTS_PER_ROOM,
|
|
180
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Maps ioBroker state roles to normalized device categories with icons.
|
|
5
|
+
* ioBroker roles are inconsistent across adapters — this normalizes them.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const ROLE_MAP = [
|
|
9
|
+
// Licht
|
|
10
|
+
{ pattern: /^switch\.light/, category: 'light', icon: '💡' },
|
|
11
|
+
{ pattern: /^level\.dimmer/, category: 'dimmer', icon: '💡' },
|
|
12
|
+
{ pattern: /^level\.color/, category: 'light', icon: '💡' },
|
|
13
|
+
{ pattern: /^light\./, category: 'light', icon: '💡' },
|
|
14
|
+
// Rolllade / Jalousie
|
|
15
|
+
{ pattern: /^level\.blind/, category: 'blind', icon: '🪟' },
|
|
16
|
+
{ pattern: /^blind\./, category: 'blind', icon: '🪟' },
|
|
17
|
+
{ pattern: /^action\.stop/, category: 'blind', icon: '🪟' },
|
|
18
|
+
// Thermostat / Temperatur
|
|
19
|
+
{ pattern: /^level\.temperature/, category: 'thermostat', icon: '🌡️' },
|
|
20
|
+
{ pattern: /^value\.temperature/, category: 'thermostat', icon: '🌡️' },
|
|
21
|
+
{ pattern: /^thermostat\./, category: 'thermostat', icon: '🌡️' },
|
|
22
|
+
// Feuchtigkeit
|
|
23
|
+
{ pattern: /^value\.humidity/, category: 'humidity', icon: '💧' },
|
|
24
|
+
// Bewegung
|
|
25
|
+
{ pattern: /^sensor\.motion/, category: 'motion', icon: '🚶' },
|
|
26
|
+
// Tür / Fenster
|
|
27
|
+
{ pattern: /^sensor\.door/, category: 'door', icon: '🚪' },
|
|
28
|
+
{ pattern: /^sensor\.window/, category: 'window', icon: '🪟' },
|
|
29
|
+
{ pattern: /^sensor\.contact/, category: 'door', icon: '🚪' },
|
|
30
|
+
// Alarm
|
|
31
|
+
{ pattern: /^alarm/, category: 'alarm', icon: '🚨' },
|
|
32
|
+
{ pattern: /^sensor\.alarm/, category: 'alarm', icon: '🚨' },
|
|
33
|
+
// Schloss
|
|
34
|
+
{ pattern: /^switch\.lock/, category: 'lock', icon: '🔒' },
|
|
35
|
+
{ pattern: /^lock\./, category: 'lock', icon: '🔒' },
|
|
36
|
+
// Steckdose / Schalter
|
|
37
|
+
{ pattern: /^switch$/, category: 'switch', icon: '🔌' },
|
|
38
|
+
{ pattern: /^switch\./, category: 'switch', icon: '🔌' },
|
|
39
|
+
// Medien
|
|
40
|
+
{ pattern: /^media\./, category: 'media', icon: '🎵' },
|
|
41
|
+
{ pattern: /^button\.play/, category: 'media', icon: '🎵' },
|
|
42
|
+
// Kamera
|
|
43
|
+
{ pattern: /^camera\./, category: 'camera', icon: '📷' },
|
|
44
|
+
// Strom / Energie
|
|
45
|
+
{ pattern: /^value\.power/, category: 'power', icon: '⚡' },
|
|
46
|
+
{ pattern: /^value\.current/, category: 'power', icon: '⚡' },
|
|
47
|
+
{ pattern: /^value\.voltage/, category: 'power', icon: '⚡' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const CATEGORY_LABEL_KEYS = {
|
|
51
|
+
light: 'catLight',
|
|
52
|
+
dimmer: 'catDimmer',
|
|
53
|
+
blind: 'catBlind',
|
|
54
|
+
thermostat: 'catThermostat',
|
|
55
|
+
humidity: 'catHumidity',
|
|
56
|
+
motion: 'catMotion',
|
|
57
|
+
door: 'catDoor',
|
|
58
|
+
window: 'catWindow',
|
|
59
|
+
alarm: 'catAlarm',
|
|
60
|
+
lock: 'catLock',
|
|
61
|
+
switch: 'catSwitch',
|
|
62
|
+
media: 'catMedia',
|
|
63
|
+
camera: 'catCamera',
|
|
64
|
+
power: 'catPower',
|
|
65
|
+
other: 'catOther',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Map an ioBroker role string to a normalized category descriptor.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} role ioBroker role string (e.g. "level.temperature")
|
|
72
|
+
* @returns {{ category: string, icon: string, labelKey: string }} - normalized role category for UI
|
|
73
|
+
*/
|
|
74
|
+
function mapRole(role) {
|
|
75
|
+
if (!role) {
|
|
76
|
+
return { category: 'other', icon: '📦', labelKey: CATEGORY_LABEL_KEYS.other };
|
|
77
|
+
}
|
|
78
|
+
for (const entry of ROLE_MAP) {
|
|
79
|
+
if (entry.pattern.test(role)) {
|
|
80
|
+
return {
|
|
81
|
+
category: entry.category,
|
|
82
|
+
icon: entry.icon,
|
|
83
|
+
labelKey: CATEGORY_LABEL_KEYS[entry.category] || CATEGORY_LABEL_KEYS.other,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { category: 'other', icon: '📦', labelKey: CATEGORY_LABEL_KEYS.other };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { mapRole, CATEGORY_LABEL_KEYS };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group JavaScript adapter scripts by folder path (from script.js.* object id).
|
|
3
|
+
* Used for HTML/Markdown export: same ordering as in ioBroker Admin tree.
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string|null|undefined} folder Folder segment from script id (e.g. "global", "Wohnung/Licht")
|
|
9
|
+
* @returns {string} Internal map key (__root__ for scripts directly under script.js.)
|
|
10
|
+
*/
|
|
11
|
+
function folderStorageKey(folder) {
|
|
12
|
+
return folder == null || folder === '' ? '__root__' : folder;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} key folderStorageKey result
|
|
17
|
+
* @returns {boolean} true if the folder is the global script tree
|
|
18
|
+
*/
|
|
19
|
+
function isGlobalFolderKey(key) {
|
|
20
|
+
return key === 'global' || key.startsWith('global/');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} a - first folder key
|
|
25
|
+
* @param {string} b - second folder key
|
|
26
|
+
* @returns {number} localeCompare-style sort delta
|
|
27
|
+
*/
|
|
28
|
+
function compareFolderStorageKeys(a, b) {
|
|
29
|
+
const rank = key => {
|
|
30
|
+
if (key === 'global' || key.startsWith('global/')) {
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
if (key === 'common' || key.startsWith('common/')) {
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
if (key === '__root__') {
|
|
37
|
+
return 9;
|
|
38
|
+
}
|
|
39
|
+
return 5;
|
|
40
|
+
};
|
|
41
|
+
const ra = rank(a);
|
|
42
|
+
const rb = rank(b);
|
|
43
|
+
if (ra !== rb) {
|
|
44
|
+
return ra - rb;
|
|
45
|
+
}
|
|
46
|
+
return String(a).localeCompare(String(b), undefined, { sensitivity: 'base' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {Array<{ folder?: string|null, name?: string }>} scripts - flat script list from discovery
|
|
51
|
+
* @returns {Array<{ folderKey: string, folder: string|null, scripts: object[] }>} groups for export ordering
|
|
52
|
+
*/
|
|
53
|
+
function groupScriptsByFolder(scripts) {
|
|
54
|
+
const map = new Map();
|
|
55
|
+
for (const s of scripts) {
|
|
56
|
+
const key = folderStorageKey(s.folder);
|
|
57
|
+
if (!map.has(key)) {
|
|
58
|
+
map.set(key, []);
|
|
59
|
+
}
|
|
60
|
+
map.get(key).push(s);
|
|
61
|
+
}
|
|
62
|
+
const sortedKeys = [...map.keys()].sort(compareFolderStorageKeys);
|
|
63
|
+
return sortedKeys.map(key => ({
|
|
64
|
+
folderKey: key,
|
|
65
|
+
folder: key === '__root__' ? null : key,
|
|
66
|
+
scripts: map
|
|
67
|
+
.get(key)
|
|
68
|
+
.sort((a, b) =>
|
|
69
|
+
String(a.name || '').localeCompare(String(b.name || ''), undefined, { sensitivity: 'base' }),
|
|
70
|
+
),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
groupScriptsByFolder,
|
|
76
|
+
isGlobalFolderKey,
|
|
77
|
+
folderStorageKey,
|
|
78
|
+
};
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version Tracking Module
|
|
3
|
+
* Manages documentation version history and changelog
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Max entries kept in state `versioning.changelog` (newest first). */
|
|
7
|
+
const CHANGELOG_MAX_ENTRIES = 6;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} docModel Document model
|
|
11
|
+
* @returns {Map<string, { version: string, title: string, adapter: string }>} - instance id to version metadata
|
|
12
|
+
*/
|
|
13
|
+
function buildInstanceVersionMap(docModel) {
|
|
14
|
+
const map = new Map();
|
|
15
|
+
const root = docModel && docModel.adapters;
|
|
16
|
+
if (!root || !Array.isArray(root.adapters)) {
|
|
17
|
+
return map;
|
|
18
|
+
}
|
|
19
|
+
for (const ag of root.adapters) {
|
|
20
|
+
for (const inst of ag.instances || []) {
|
|
21
|
+
if (!inst || !inst.id) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
map.set(inst.id, {
|
|
25
|
+
version: inst.version != null ? String(inst.version) : '',
|
|
26
|
+
title: inst.title || inst.adapter || inst.name || '',
|
|
27
|
+
adapter: inst.adapter || '',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return map;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Detect adapter instance version bumps (same instance id, different common.version).
|
|
36
|
+
*
|
|
37
|
+
* @param {Map} prevMap Previous id → meta
|
|
38
|
+
* @param {Map} currMap Current id → meta
|
|
39
|
+
* @returns {Array<object>} Change entries compatible with compareVersions
|
|
40
|
+
*/
|
|
41
|
+
function diffAdapterInstanceVersions(prevMap, currMap) {
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const [id, curr] of currMap) {
|
|
44
|
+
if (!prevMap.has(id)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const prev = prevMap.get(id);
|
|
48
|
+
if (prev.version === curr.version) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const label = (curr.title || curr.adapter || id).trim() || id;
|
|
52
|
+
out.push({
|
|
53
|
+
type: 'adapter_version',
|
|
54
|
+
instanceId: id,
|
|
55
|
+
adapterTitle: label,
|
|
56
|
+
previous: prev.version,
|
|
57
|
+
current: curr.version,
|
|
58
|
+
/** Stored for Markdown export / JSON; HTML uses i18n `changelogMsgAdapterVersion` when adapterTitle is set */
|
|
59
|
+
message: `Adapter "${label}" (${id}): ${prev.version || '?'} → ${curr.version || '?'}`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
out.sort((a, b) => a.instanceId.localeCompare(b.instanceId));
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Tracks documentation generation versions and changelog entries.
|
|
68
|
+
*/
|
|
69
|
+
class VersionTracker {
|
|
70
|
+
/**
|
|
71
|
+
* @param {object} adapter ioBroker adapter instance
|
|
72
|
+
*/
|
|
73
|
+
constructor(adapter) {
|
|
74
|
+
this.adapter = adapter;
|
|
75
|
+
this.storagePrefix = `${adapter.namespace}.versioning`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Generate semantic version
|
|
80
|
+
* Format: YYYY.MM.DD.HH
|
|
81
|
+
*
|
|
82
|
+
* @returns {string} Version string
|
|
83
|
+
*/
|
|
84
|
+
generateVersion() {
|
|
85
|
+
const now = new Date();
|
|
86
|
+
const year = now.getFullYear();
|
|
87
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
88
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
89
|
+
const hour = String(now.getHours()).padStart(2, '0');
|
|
90
|
+
return `${year}.${month}.${day}.${hour}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get version comparison data
|
|
95
|
+
*
|
|
96
|
+
* @param {object} currentDocModel Current document model
|
|
97
|
+
* @param {object} previousDocModel Previous document model
|
|
98
|
+
* @returns {object} Change summary
|
|
99
|
+
*/
|
|
100
|
+
compareVersions(currentDocModel, previousDocModel) {
|
|
101
|
+
if (!previousDocModel || !previousDocModel.system || !previousDocModel.system.statistics) {
|
|
102
|
+
return {
|
|
103
|
+
isInitial: true,
|
|
104
|
+
changes: [],
|
|
105
|
+
summary: 'Initial documentation generation',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const changes = [];
|
|
110
|
+
const current = (currentDocModel.system && currentDocModel.system.statistics) || {};
|
|
111
|
+
const previous = previousDocModel.system.statistics;
|
|
112
|
+
|
|
113
|
+
// Adapter instance version changes (explains many enabled_instances / state_objects deltas)
|
|
114
|
+
const prevInst = buildInstanceVersionMap(previousDocModel);
|
|
115
|
+
const currInst = buildInstanceVersionMap(currentDocModel);
|
|
116
|
+
changes.push(...diffAdapterInstanceVersions(prevInst, currInst));
|
|
117
|
+
|
|
118
|
+
// Check instance count changes
|
|
119
|
+
if (current.instanceCount !== previous.instanceCount) {
|
|
120
|
+
changes.push({
|
|
121
|
+
type: 'instance_count',
|
|
122
|
+
previous: previous.instanceCount,
|
|
123
|
+
current: current.instanceCount,
|
|
124
|
+
message: `Adapter instances changed from ${previous.instanceCount} to ${current.instanceCount}`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check enabled instances changes
|
|
129
|
+
if (current.enabledInstanceCount !== previous.enabledInstanceCount) {
|
|
130
|
+
changes.push({
|
|
131
|
+
type: 'enabled_instances',
|
|
132
|
+
previous: previous.enabledInstanceCount,
|
|
133
|
+
current: current.enabledInstanceCount,
|
|
134
|
+
message: `Enabled instances changed from ${previous.enabledInstanceCount} to ${current.enabledInstanceCount}`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check state object count changes
|
|
139
|
+
if (current.totalStateObjects !== previous.totalStateObjects) {
|
|
140
|
+
changes.push({
|
|
141
|
+
type: 'state_objects',
|
|
142
|
+
previous: previous.totalStateObjects,
|
|
143
|
+
current: current.totalStateObjects,
|
|
144
|
+
message: `State objects changed from ${previous.totalStateObjects} to ${current.totalStateObjects}`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check project name changes
|
|
149
|
+
if (currentDocModel.system.projectName !== previousDocModel.system.projectName) {
|
|
150
|
+
changes.push({
|
|
151
|
+
type: 'project_name',
|
|
152
|
+
previous: previousDocModel.system.projectName,
|
|
153
|
+
current: currentDocModel.system.projectName,
|
|
154
|
+
message: `Project name changed from "${previousDocModel.system.projectName}" to "${currentDocModel.system.projectName}"`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
isInitial: false,
|
|
160
|
+
changes: changes,
|
|
161
|
+
summary: changes.length === 0 ? 'No significant changes detected' : `${changes.length} change(s) detected`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Build changelog entry
|
|
167
|
+
*
|
|
168
|
+
* @param {string} version Version number
|
|
169
|
+
* @param {object} changeData Changes from compareVersions
|
|
170
|
+
* @returns {object} Changelog entry
|
|
171
|
+
*/
|
|
172
|
+
buildChangelogEntry(version, changeData) {
|
|
173
|
+
return {
|
|
174
|
+
version: version,
|
|
175
|
+
timestamp: new Date().toISOString(),
|
|
176
|
+
summary: changeData.summary,
|
|
177
|
+
isInitial: changeData.isInitial,
|
|
178
|
+
changes: changeData.changes,
|
|
179
|
+
changeCount: changeData.changes.length,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get previous version data from state
|
|
185
|
+
*
|
|
186
|
+
* @returns {Promise<object|null>} Previous document model or null
|
|
187
|
+
*/
|
|
188
|
+
async getPreviousVersion() {
|
|
189
|
+
try {
|
|
190
|
+
const state = await this.adapter.getStateAsync(`${this.storagePrefix}.lastDocumentModel`);
|
|
191
|
+
if (state && state.val) {
|
|
192
|
+
return JSON.parse(String(state.val));
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
this.adapter.log.warn(`Could not retrieve previous version: ${error.message}`);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Store current version for comparison
|
|
203
|
+
*
|
|
204
|
+
* @param {object} docModel Document model to store
|
|
205
|
+
* @returns {Promise<void>}
|
|
206
|
+
*/
|
|
207
|
+
async storeCurrentVersion(docModel) {
|
|
208
|
+
try {
|
|
209
|
+
await this.adapter.setStateAsync(`${this.storagePrefix}.lastDocumentModel`, {
|
|
210
|
+
val: JSON.stringify(docModel),
|
|
211
|
+
ack: true,
|
|
212
|
+
});
|
|
213
|
+
} catch (error) {
|
|
214
|
+
this.adapter.log.warn(`Could not store current version: ${error.message}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Add changelog entry to history
|
|
220
|
+
*
|
|
221
|
+
* @param {object} entry Changelog entry
|
|
222
|
+
* @returns {Promise<void>}
|
|
223
|
+
*/
|
|
224
|
+
async appendChangelog(entry) {
|
|
225
|
+
try {
|
|
226
|
+
const state = await this.adapter.getStateAsync(`${this.storagePrefix}.changelog`);
|
|
227
|
+
let changelog = [];
|
|
228
|
+
|
|
229
|
+
if (state && state.val) {
|
|
230
|
+
try {
|
|
231
|
+
changelog = JSON.parse(String(state.val));
|
|
232
|
+
if (!Array.isArray(changelog)) {
|
|
233
|
+
changelog = [];
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
changelog = [];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
changelog.unshift(entry);
|
|
241
|
+
changelog = changelog.slice(0, CHANGELOG_MAX_ENTRIES);
|
|
242
|
+
|
|
243
|
+
await this.adapter.setStateAsync(`${this.storagePrefix}.changelog`, {
|
|
244
|
+
val: JSON.stringify(changelog, null, 2),
|
|
245
|
+
ack: true,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
await this.adapter.setStateAsync(`${this.storagePrefix}.latestVersion`, {
|
|
249
|
+
val: entry.version,
|
|
250
|
+
ack: true,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await this.adapter.setStateAsync(`${this.storagePrefix}.changeCount`, {
|
|
254
|
+
val: entry.changeCount,
|
|
255
|
+
ack: true,
|
|
256
|
+
});
|
|
257
|
+
} catch (error) {
|
|
258
|
+
this.adapter.log.warn(`Could not append changelog: ${error.message}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Get current changelog
|
|
264
|
+
*
|
|
265
|
+
* @returns {Promise<Array>} Array of changelog entries
|
|
266
|
+
*/
|
|
267
|
+
async getChangelog() {
|
|
268
|
+
try {
|
|
269
|
+
const state = await this.adapter.getStateAsync(`${this.storagePrefix}.changelog`);
|
|
270
|
+
if (state && state.val) {
|
|
271
|
+
return JSON.parse(String(state.val));
|
|
272
|
+
}
|
|
273
|
+
return [];
|
|
274
|
+
} catch (error) {
|
|
275
|
+
this.adapter.log.warn(`Could not retrieve changelog: ${error.message}`);
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Format changelog to markdown
|
|
282
|
+
*
|
|
283
|
+
* @param {Array} changelog Changelog entries
|
|
284
|
+
* @returns {string} Markdown formatted changelog
|
|
285
|
+
*/
|
|
286
|
+
formatChangelogMarkdown(changelog) {
|
|
287
|
+
if (!changelog || changelog.length === 0) {
|
|
288
|
+
return '# Version History\n\nNo versions recorded yet.\n';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let markdown = '# Version History\n\n';
|
|
292
|
+
|
|
293
|
+
for (const entry of changelog) {
|
|
294
|
+
markdown += `## Version ${entry.version}\n`;
|
|
295
|
+
markdown += `**Date:** ${new Date(entry.timestamp).toLocaleString()}\n`;
|
|
296
|
+
markdown += `**Summary:** ${entry.summary}\n`;
|
|
297
|
+
|
|
298
|
+
if (entry.changes.length > 0) {
|
|
299
|
+
markdown += '\n### Changes\n';
|
|
300
|
+
for (const change of entry.changes) {
|
|
301
|
+
markdown += `- **${change.type}**: ${change.message}\n`;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
markdown += '\n';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return markdown;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
module.exports = VersionTracker;
|