iobroker.autodoc 0.9.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -0
  3. package/admin/autodoc.png +0 -0
  4. package/admin/i18n/de.json +244 -0
  5. package/admin/i18n/en.json +241 -0
  6. package/admin/i18n/es.json +229 -0
  7. package/admin/i18n/fr.json +235 -0
  8. package/admin/i18n/it.json +229 -0
  9. package/admin/i18n/nl.json +229 -0
  10. package/admin/i18n/pl.json +229 -0
  11. package/admin/i18n/pt.json +229 -0
  12. package/admin/i18n/ru.json +229 -0
  13. package/admin/i18n/uk.json +229 -0
  14. package/admin/i18n/zh-cn.json +229 -0
  15. package/admin/jsonConfig.json +1490 -0
  16. package/io-package.json +253 -0
  17. package/lib/adapter-config.d.ts +19 -0
  18. package/lib/aiEnhancer.js +2114 -0
  19. package/lib/autoHostTopologyMermaid.js +195 -0
  20. package/lib/dependencyAnalyzer.js +83 -0
  21. package/lib/diagnosisSnapshot.js +32 -0
  22. package/lib/discovery.js +953 -0
  23. package/lib/docTemplateConfig.js +422 -0
  24. package/lib/documentModel.js +640 -0
  25. package/lib/forumCard.js +70 -0
  26. package/lib/guestHelpContent.js +93 -0
  27. package/lib/guestScriptPrivacy.js +14 -0
  28. package/lib/hostDisplay.js +19 -0
  29. package/lib/htmlRenderer.js +4108 -0
  30. package/lib/htmlThemePresets.js +79 -0
  31. package/lib/htmlToPdf.js +99 -0
  32. package/lib/i18n.js +1309 -0
  33. package/lib/markdownRenderer.js +2025 -0
  34. package/lib/mermaidAutodocPalette.js +165 -0
  35. package/lib/mermaidServerSvg.js +252 -0
  36. package/lib/notifier.js +124 -0
  37. package/lib/quickStartGuide.js +180 -0
  38. package/lib/roleMapper.js +90 -0
  39. package/lib/scriptGroups.js +78 -0
  40. package/lib/versionTracker.js +312 -0
  41. package/main.js +1368 -0
  42. package/package.json +88 -0
@@ -0,0 +1,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;