iobroker.eos-admin 7.9.99 → 7.9.100

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 (39) hide show
  1. package/{INSTALL_TEST_V7.9.99.md → INSTALL_TEST_V7.9.100.md} +2 -2
  2. package/{MERGE_UPDATE_README_V7.9.99.md → MERGE_UPDATE_README_V7.9.100.md} +1 -1
  3. package/NEXOWATT_EOS_BUILD_INFO.json +19 -20
  4. package/{PUBLISH_STABLE_V7.9.99.md → PUBLISH_STABLE_V7.9.100.md} +2 -2
  5. package/README_STABILITY_V7.9.100.md +3 -0
  6. package/{RELEASE_ACCEPTANCE_V7.9.99.md → RELEASE_ACCEPTANCE_V7.9.100.md} +1 -1
  7. package/RELEASE_NOTES_V7.9.100.md +11 -0
  8. package/adminWww/css/eos-auto-update.css +4 -0
  9. package/adminWww/css/eos-ems-overview.css +1 -1
  10. package/adminWww/css/nexowatt-native-shell.css +2 -2
  11. package/adminWww/index.html +21 -19
  12. package/adminWww/js/eos-account-management.js +1 -1
  13. package/adminWww/js/eos-assistant.js +1 -1
  14. package/adminWww/js/eos-auto-update.js +78 -0
  15. package/adminWww/js/eos-ems-overview.js +1 -1
  16. package/adminWww/js/eos-release-watch.js +1 -1
  17. package/adminWww/js/eos-role-bootstrap.js +2 -2
  18. package/adminWww/js/eos-role-ui.js +2 -2
  19. package/adminWww/js/nexowatt-native-shell.js +1 -1
  20. package/adminWww/static/js/nexowatt-stable-login.js +3 -3
  21. package/adminWww/static/js/{nexowatt-stable-v99.js → nexowatt-stable-v100.js} +2 -2
  22. package/build/lib/eosAutoUpdate.js +43 -0
  23. package/build/lib/web.js +48 -0
  24. package/io-package.json +7 -6
  25. package/package.json +20 -14
  26. package/src/lib/eosAutoUpdate.ts +320 -0
  27. package/tools/nexowatt-account-management-selftest.cjs +1 -1
  28. package/tools/nexowatt-auto-update-selftest.cjs +34 -0
  29. package/tools/nexowatt-cache-update-selftest.cjs +6 -5
  30. package/tools/nexowatt-login-layout-selftest.cjs +3 -3
  31. package/tools/nexowatt-merge-update-selftest.cjs +1 -1
  32. package/tools/nexowatt-modern-ui-selftest.cjs +1 -1
  33. package/tools/nexowatt-runtime-cleanup-selftest.cjs +4 -4
  34. package/tools/nexowatt-stable-v100-selftest.cjs +27 -0
  35. package/tools/nexowatt-ui-overview-runtime-selftest.cjs +1 -1
  36. package/tools/nexowatt-validate-package.cjs +11 -11
  37. package/README_STABILITY_V7.9.99.md +0 -18
  38. package/RELEASE_NOTES_V7.9.99.md +0 -9
  39. package/tools/nexowatt-stable-v99-selftest.cjs +0 -25
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "iobroker.eos-admin",
3
- "description": "NexoWatt EOS Admin – stable candidate with truthful EMS overview, verified user passwords and deterministic browser updates.",
4
- "version": "7.9.99",
3
+ "description": "NexoWatt EOS Admin – stable sales release with native automatic NexoWatt Stable adapter updates.",
4
+ "version": "7.9.100",
5
5
  "contributors": [
6
6
  "bluefox <dogafox@gmail.com>",
7
7
  "apollon77",
@@ -118,22 +118,27 @@
118
118
  "tools/nexowatt-version-sync-selftest.cjs",
119
119
  "tools/nexowatt-merge-update-selftest.cjs",
120
120
  "tools/nexowatt-ems-overview-selftest.cjs",
121
- "README_STABILITY_V7.9.99.md",
122
- "RELEASE_NOTES_V7.9.99.md",
123
- "PUBLISH_STABLE_V7.9.99.md",
124
- "INSTALL_TEST_V7.9.99.md",
125
- "RELEASE_ACCEPTANCE_V7.9.99.md",
126
- "MERGE_UPDATE_README_V7.9.99.md",
121
+ "README_STABILITY_V7.9.100.md",
122
+ "RELEASE_NOTES_V7.9.100.md",
123
+ "PUBLISH_STABLE_V7.9.100.md",
124
+ "INSTALL_TEST_V7.9.100.md",
125
+ "RELEASE_ACCEPTANCE_V7.9.100.md",
126
+ "MERGE_UPDATE_README_V7.9.100.md",
127
127
  "README_EMS_OVERVIEW_MERGE.md",
128
128
  "RC73_EOS_ADMIN_MERGE_VALIDATION.md",
129
129
  "VALIDATION_EMS_OVERVIEW_MERGE.md",
130
130
  "NexoWatt_EOS_Admin_EMS_OVERVIEW_MERGE_VALIDATION.md",
131
131
  "tools/nexowatt-apply-ems-overview.cjs",
132
132
  "tools/nexowatt-ui-overview-runtime-selftest.cjs",
133
- "tools/nexowatt-stable-v99-selftest.cjs",
133
+ "tools/nexowatt-stable-v100-selftest.cjs",
134
134
  "tools/nexowatt-password-write-selftest.cjs",
135
135
  "tools/nexowatt-request-security-selftest.cjs",
136
- "tools/nexowatt-cache-update-selftest.cjs"
136
+ "tools/nexowatt-cache-update-selftest.cjs",
137
+ "src/lib/eosAutoUpdate.ts",
138
+ "build/lib/eosAutoUpdate.js",
139
+ "adminWww/js/eos-auto-update.js",
140
+ "adminWww/css/eos-auto-update.css",
141
+ "tools/nexowatt-auto-update-selftest.cjs"
137
142
  ],
138
143
  "scripts": {
139
144
  "npm": "npm i -f && cd src-admin && npm i -f",
@@ -168,7 +173,7 @@
168
173
  "nexowatt:repo-entry": "node tools/nexowatt-generate-repo-entry.cjs",
169
174
  "nexowatt:patch-repo": "node tools/nexowatt-patch-repo.cjs",
170
175
  "nexowatt:repair-update": "node tools/nexowatt-repair-eos-admin-update.cjs",
171
- "check:eos-stability": "node tools/nexowatt-stability-check.cjs && node tools/nexowatt-esm-syntax-selftest.cjs && node tools/nexowatt-import-integrity-selftest.cjs && node tools/nexowatt-entrypoint-smoke-selftest.cjs && node tools/nexowatt-frontend-graph-check.cjs && node tools/nexowatt-policy-selftest.cjs && node tools/nexowatt-publish-channel-selftest.cjs && node tools/nexowatt-release-defaults-selftest.cjs && node tools/nexowatt-role-access-selftest.cjs && node tools/nexowatt-internal-reserve-selftest.cjs && node tools/nexowatt-first-login-selftest.cjs && node tools/nexowatt-account-management-selftest.cjs && node tools/nexowatt-cache-update-selftest.cjs && node tools/nexowatt-request-security-selftest.cjs && node tools/nexowatt-password-write-selftest.cjs && node tools/nexowatt-modern-ui-selftest.cjs && node tools/nexowatt-login-layout-selftest.cjs && node tools/nexowatt-branding-selftest.cjs && node tools/nexowatt-default-port-selftest.cjs && node tools/nexowatt-module-popup-selftest.cjs && node tools/nexowatt-datapoint-selftest.cjs && node tools/nexowatt-update-selftest.cjs && node tools/nexowatt-datapoint-interaction-selftest.cjs && node tools/nexowatt-manual-write-policy-selftest.cjs && node tools/nexowatt-value-column-selftest.cjs && node tools/nexowatt-native-shell-selftest.cjs && node tools/nexowatt-assistant-separation-selftest.cjs && node tools/nexowatt-runtime-cleanup-selftest.cjs && node tools/nexowatt-version-sync-selftest.cjs && node tools/nexowatt-merge-update-selftest.cjs && npm run check:eos-stable-v99 && node tools/nexowatt-ems-overview-selftest.cjs && node tools/nexowatt-ui-overview-runtime-selftest.cjs",
176
+ "check:eos-stability": "node tools/nexowatt-stability-check.cjs && node tools/nexowatt-esm-syntax-selftest.cjs && node tools/nexowatt-import-integrity-selftest.cjs && node tools/nexowatt-entrypoint-smoke-selftest.cjs && node tools/nexowatt-frontend-graph-check.cjs && node tools/nexowatt-policy-selftest.cjs && node tools/nexowatt-publish-channel-selftest.cjs && node tools/nexowatt-release-defaults-selftest.cjs && node tools/nexowatt-role-access-selftest.cjs && node tools/nexowatt-internal-reserve-selftest.cjs && node tools/nexowatt-first-login-selftest.cjs && node tools/nexowatt-account-management-selftest.cjs && node tools/nexowatt-cache-update-selftest.cjs && node tools/nexowatt-request-security-selftest.cjs && node tools/nexowatt-password-write-selftest.cjs && node tools/nexowatt-modern-ui-selftest.cjs && node tools/nexowatt-login-layout-selftest.cjs && node tools/nexowatt-branding-selftest.cjs && node tools/nexowatt-default-port-selftest.cjs && node tools/nexowatt-module-popup-selftest.cjs && node tools/nexowatt-datapoint-selftest.cjs && node tools/nexowatt-update-selftest.cjs && node tools/nexowatt-datapoint-interaction-selftest.cjs && node tools/nexowatt-manual-write-policy-selftest.cjs && node tools/nexowatt-value-column-selftest.cjs && node tools/nexowatt-native-shell-selftest.cjs && node tools/nexowatt-assistant-separation-selftest.cjs && node tools/nexowatt-runtime-cleanup-selftest.cjs && node tools/nexowatt-version-sync-selftest.cjs && node tools/nexowatt-merge-update-selftest.cjs && npm run check:eos-stable-v100 && node tools/nexowatt-ems-overview-selftest.cjs && node tools/nexowatt-ui-overview-runtime-selftest.cjs",
172
177
  "nexowatt:patch-built-frontend": "node tools/nexowatt-patch-built-frontend.cjs",
173
178
  "clean:eos-runtime": "node tools/nexowatt-clean-legacy-runtime.cjs",
174
179
  "precheck:eos-package": "npm run sync:eos-version && npm run prepare:eos-release-defaults && npm run clean:eos-runtime",
@@ -192,10 +197,11 @@
192
197
  "sync:eos-version": "node tools/nexowatt-sync-release-version.cjs",
193
198
  "test:eos-version-sync": "node tools/nexowatt-version-sync-selftest.cjs",
194
199
  "verify:eos-merge": "node tools/nexowatt-merge-update-selftest.cjs",
195
- "check:eos-stable-v99": "node tools/nexowatt-stable-v99-selftest.cjs",
196
200
  "test:eos-password-write": "node tools/nexowatt-password-write-selftest.cjs",
197
201
  "test:eos-request-security": "node tools/nexowatt-request-security-selftest.cjs",
198
- "test:eos-cache-update": "node tools/nexowatt-cache-update-selftest.cjs"
202
+ "test:eos-cache-update": "node tools/nexowatt-cache-update-selftest.cjs",
203
+ "check:eos-stable-v100": "node tools/nexowatt-stable-v100-selftest.cjs",
204
+ "check:eos-auto-update": "node tools/nexowatt-auto-update-selftest.cjs"
199
205
  },
200
206
  "author": "NexoWatt",
201
207
  "private": false,
@@ -206,7 +212,7 @@
206
212
  },
207
213
  "nexowattReleasePolicy": {
208
214
  "distTag": "latest",
209
- "purpose": "accepted stable NexoWatt EOS sales-product release 7.9.99"
215
+ "purpose": "accepted stable NexoWatt EOS sales-product release 7.9.100"
210
216
  },
211
217
  "publisher": "NexoWatt"
212
218
  }
@@ -0,0 +1,320 @@
1
+ /**
2
+ * NexoWatt stable adapter auto-update policy manager.
3
+ *
4
+ * This class does not install packages itself. It configures ioBroker's native
5
+ * adapterAutoUpgrade manager so only installed NexoWatt adapters present in the
6
+ * detected Stable repository receive the `major` policy. All other adapters and
7
+ * existing user policies are preserved.
8
+ */
9
+
10
+ export type NexoWattAutoUpdatePolicy = 'none' | 'patch' | 'minor' | 'major';
11
+
12
+ interface AdapterObjectLike {
13
+ _id?: string;
14
+ common?: Record<string, any>;
15
+ native?: Record<string, any>;
16
+ }
17
+
18
+ interface NexoWattAutoUpdateAdapter {
19
+ namespace: string;
20
+ host?: string;
21
+ log: { debug: (message: string) => void; info: (message: string) => void; warn: (message: string) => void; error: (message: string) => void };
22
+ getForeignObjectAsync: (id: string) => Promise<any>;
23
+ setForeignObjectAsync: (id: string, object: any, options?: any) => Promise<any>;
24
+ extendForeignObjectAsync: (id: string, object: any, options?: any) => Promise<any>;
25
+ getObjectViewAsync: (design: string, search: string, params: any, options?: any) => Promise<any>;
26
+ sendToHostAsync?: (host: string, command: string, message: any) => Promise<any>;
27
+ }
28
+
29
+ export interface NexoWattStableUpdateStatus {
30
+ enabled: boolean;
31
+ repository: string;
32
+ managedAdapters: string[];
33
+ availableStableAdapters: string[];
34
+ lastSync: number;
35
+ lastRepositoryRefresh: number;
36
+ error: string;
37
+ policy: 'major';
38
+ source: 'ioBroker-native-auto-upgrade';
39
+ }
40
+
41
+ interface PersistedState {
42
+ previousPolicies?: Record<string, NexoWattAutoUpdatePolicy | null>;
43
+ previousRepositoryEnabled?: boolean | null;
44
+ previousActiveRepo?: string[];
45
+ selectedRepository?: string;
46
+ ownsActiveRepoOrder?: boolean;
47
+ managedAdapters?: string[];
48
+ availableStableAdapters?: string[];
49
+ lastSync?: number;
50
+ lastRepositoryRefresh?: number;
51
+ error?: string;
52
+ }
53
+
54
+ const ADMIN_USER = 'system.user.admin';
55
+ const POLICY: NexoWattAutoUpdatePolicy = 'major';
56
+ const RECONCILE_INTERVAL_MS = 6 * 60 * 60 * 1000;
57
+ const REPOSITORY_REFRESH_DELAY_MS = 2500;
58
+
59
+ const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value));
60
+ const normalizedName = (value: unknown): string => String(value || '').trim().replace(/^system\.adapter\./, '').replace(/\.\d+$/, '');
61
+ const stableVersion = (value: unknown): boolean => /^\d+\.\d+\.\d+$/.test(String(value || '').trim());
62
+
63
+ export function isNexoWattRepositoryEntry(name: string, entry: any): boolean {
64
+ const adapterName = normalizedName(entry?.name || name).toLowerCase();
65
+ if (adapterName === 'eos-admin' || adapterName.startsWith('nexowatt-') || adapterName.startsWith('eos-')) return true;
66
+ const evidence = JSON.stringify({
67
+ name,
68
+ adapterName,
69
+ title: entry?.title,
70
+ desc: entry?.desc,
71
+ author: entry?.author,
72
+ authors: entry?.authors,
73
+ publisher: entry?.publisher,
74
+ url: entry?.url,
75
+ meta: entry?.meta,
76
+ readme: entry?.readme,
77
+ extIcon: entry?.extIcon,
78
+ licenseInformation: entry?.licenseInformation,
79
+ }).toLowerCase();
80
+ return evidence.includes('nexowatt') || evidence.includes('github.com/nexowatt/');
81
+ }
82
+
83
+ export class NexoWattStableUpdateManager {
84
+ private timer?: NodeJS.Timeout;
85
+ private reconcilePromise?: Promise<NexoWattStableUpdateStatus>;
86
+ private refreshTimer?: NodeJS.Timeout;
87
+ private retryTimer?: NodeJS.Timeout;
88
+
89
+ constructor(private readonly adapter: NexoWattAutoUpdateAdapter) {}
90
+
91
+ private get instanceId(): string { return `system.adapter.${this.adapter.namespace}`; }
92
+
93
+ async start(): Promise<NexoWattStableUpdateStatus> {
94
+ const status = await this.reconcile('startup');
95
+ if (!this.timer) {
96
+ this.timer = setInterval(() => void this.reconcile('interval'), RECONCILE_INTERVAL_MS);
97
+ this.timer.unref?.();
98
+ }
99
+ return status;
100
+ }
101
+
102
+ stop(): void {
103
+ if (this.timer) clearInterval(this.timer);
104
+ if (this.refreshTimer) clearTimeout(this.refreshTimer);
105
+ if (this.retryTimer) clearTimeout(this.retryTimer);
106
+ this.timer = undefined;
107
+ this.refreshTimer = undefined;
108
+ this.retryTimer = undefined;
109
+ }
110
+
111
+ async getStatus(refresh = false): Promise<NexoWattStableUpdateStatus> {
112
+ if (refresh) return this.reconcile('status');
113
+ const instance = await this.adapter.getForeignObjectAsync(this.instanceId);
114
+ return this.statusFromNative(instance?.native || {});
115
+ }
116
+
117
+ async setEnabled(enabled: boolean): Promise<NexoWattStableUpdateStatus> {
118
+ await this.adapter.extendForeignObjectAsync(this.instanceId, { native: { eosNexoWattAutoUpdate: enabled } }, { user: ADMIN_USER });
119
+ return this.reconcile(enabled ? 'enabled' : 'disabled');
120
+ }
121
+
122
+ async reconcile(reason = 'manual'): Promise<NexoWattStableUpdateStatus> {
123
+ if (this.reconcilePromise) return this.reconcilePromise;
124
+ this.reconcilePromise = this.reconcileInternal(reason).finally(() => { this.reconcilePromise = undefined; });
125
+ return this.reconcilePromise;
126
+ }
127
+
128
+ private statusFromNative(native: Record<string, any>): NexoWattStableUpdateStatus {
129
+ const state = (native.eosNexoWattAutoUpdateState || {}) as PersistedState;
130
+ return {
131
+ enabled: native.eosNexoWattAutoUpdate !== false,
132
+ repository: String(state.selectedRepository || ''),
133
+ managedAdapters: Array.isArray(state.managedAdapters) ? state.managedAdapters.map(String).sort() : [],
134
+ availableStableAdapters: Array.isArray(state.availableStableAdapters) ? state.availableStableAdapters.map(String).sort() : [],
135
+ lastSync: Number(state.lastSync || 0),
136
+ lastRepositoryRefresh: Number(state.lastRepositoryRefresh || 0),
137
+ error: String(state.error || ''),
138
+ policy: POLICY,
139
+ source: 'ioBroker-native-auto-upgrade',
140
+ };
141
+ }
142
+
143
+ private async reconcileInternal(reason: string): Promise<NexoWattStableUpdateStatus> {
144
+ const instance = await this.adapter.getForeignObjectAsync(this.instanceId);
145
+ const native = instance?.native || {};
146
+ const enabled = native.eosNexoWattAutoUpdate !== false;
147
+ const state = clone((native.eosNexoWattAutoUpdateState || {}) as PersistedState);
148
+ try {
149
+ if (!enabled) return await this.disable(state, reason);
150
+ return await this.enable(state, reason);
151
+ } catch (error) {
152
+ const message = error instanceof Error ? error.message : String(error || 'unknownError');
153
+ state.error = message;
154
+ state.lastSync = Date.now();
155
+ await this.persist(enabled, state);
156
+ if (enabled) this.scheduleRetry();
157
+ this.adapter.log.warn(`[NexoWatt stable updates] ${message}`);
158
+ return this.statusFromNative({ eosNexoWattAutoUpdate: enabled, eosNexoWattAutoUpdateState: state });
159
+ }
160
+ }
161
+
162
+ private async readInstalledAdapters(): Promise<Map<string, AdapterObjectLike>> {
163
+ const view = await this.adapter.getObjectViewAsync('system', 'adapter', {
164
+ startkey: 'system.adapter.',
165
+ endkey: 'system.adapter.\u9999',
166
+ }, { user: ADMIN_USER });
167
+ const result = new Map<string, AdapterObjectLike>();
168
+ for (const row of view?.rows || []) {
169
+ const object = row?.value || row?.doc;
170
+ const name = normalizedName(object?.common?.name || row?.id || object?._id);
171
+ if (name && object?.common?.version) result.set(name, object);
172
+ }
173
+ return result;
174
+ }
175
+
176
+ private async readRepositorySelection(systemConfig: any): Promise<{ name: string; entries: Record<string, any>; stableNames: string[] }> {
177
+ const repositories = await this.adapter.getForeignObjectAsync('system.repositories');
178
+ const rows = repositories?.native?.repositories || {};
179
+ const active = Array.isArray(systemConfig?.common?.activeRepo) ? systemConfig.common.activeRepo.map(String) : [];
180
+ const candidates = Object.entries(rows).map(([name, repo]: [string, any]) => {
181
+ const entries = repo?.json && typeof repo.json === 'object' ? repo.json : {};
182
+ const stableNames = Object.entries(entries)
183
+ .filter(([entryName, entry]) => stableVersion((entry as any)?.version) && isNexoWattRepositoryEntry(entryName, entry))
184
+ .map(([entryName, entry]) => normalizedName((entry as any)?.name || entryName))
185
+ .filter(Boolean);
186
+ return { name, entries, stableNames: [...new Set(stableNames)].sort(), activeIndex: active.indexOf(name) };
187
+ }).filter(row => row.stableNames.length > 0);
188
+ if (!candidates.length) throw new Error('nexowattStableRepositoryNotFound');
189
+ candidates.sort((a, b) => {
190
+ const aa = a.activeIndex < 0 ? Number.MAX_SAFE_INTEGER : a.activeIndex;
191
+ const bb = b.activeIndex < 0 ? Number.MAX_SAFE_INTEGER : b.activeIndex;
192
+ return aa - bb || b.stableNames.length - a.stableNames.length || a.name.localeCompare(b.name);
193
+ });
194
+ return candidates[0];
195
+ }
196
+
197
+ private async enable(state: PersistedState, reason: string): Promise<NexoWattStableUpdateStatus> {
198
+ const systemConfig = await this.adapter.getForeignObjectAsync('system.config');
199
+ if (!systemConfig?.common) throw new Error('systemConfigUnavailable');
200
+ const selected = await this.readRepositorySelection(systemConfig);
201
+ const installed = await this.readInstalledAdapters();
202
+ const managedNames = selected.stableNames.filter(name => installed.has(name)).sort();
203
+ if (!managedNames.length) throw new Error('noInstalledNexoWattStableAdapters');
204
+
205
+ state.previousPolicies ||= {};
206
+ for (const name of managedNames) {
207
+ if (!(name in state.previousPolicies)) {
208
+ const current = installed.get(name)?.common?.automaticUpgrade;
209
+ state.previousPolicies[name] = current == null ? null : current as NexoWattAutoUpdatePolicy;
210
+ }
211
+ await this.writeAdapterPolicy(name, POLICY);
212
+ }
213
+
214
+ const common = systemConfig.common;
215
+ const activeRepo = Array.isArray(common.activeRepo) ? [...common.activeRepo] : [];
216
+ const auto = common.adapterAutoUpgrade && typeof common.adapterAutoUpgrade === 'object'
217
+ ? clone(common.adapterAutoUpgrade)
218
+ : { repositories: {}, defaultPolicy: 'none' };
219
+ auto.repositories = auto.repositories && typeof auto.repositories === 'object' ? { ...auto.repositories } : {};
220
+ if (state.previousRepositoryEnabled === undefined) {
221
+ state.previousRepositoryEnabled = Object.prototype.hasOwnProperty.call(auto.repositories, selected.name)
222
+ ? Boolean(auto.repositories[selected.name])
223
+ : null;
224
+ }
225
+ auto.repositories[selected.name] = true;
226
+ if (!auto.defaultPolicy) auto.defaultPolicy = 'none';
227
+ common.adapterAutoUpgrade = auto;
228
+
229
+ if (activeRepo[0] !== selected.name) {
230
+ if (!state.previousActiveRepo) state.previousActiveRepo = [...activeRepo];
231
+ state.ownsActiveRepoOrder = true;
232
+ common.activeRepo = [selected.name, ...activeRepo.filter((name: string) => name !== selected.name)];
233
+ }
234
+ await this.adapter.setForeignObjectAsync('system.config', systemConfig, { user: ADMIN_USER });
235
+
236
+ state.selectedRepository = selected.name;
237
+ state.availableStableAdapters = selected.stableNames;
238
+ state.managedAdapters = managedNames;
239
+ state.lastSync = Date.now();
240
+ state.error = '';
241
+ await this.persist(true, state);
242
+ this.scheduleRepositoryRefresh(selected.name, state);
243
+ this.adapter.log.info(`[NexoWatt stable updates] Enabled for ${managedNames.join(', ')} from repository "${selected.name}" (${reason})`);
244
+ return this.statusFromNative({ eosNexoWattAutoUpdate: true, eosNexoWattAutoUpdateState: state });
245
+ }
246
+
247
+ private async disable(state: PersistedState, reason: string): Promise<NexoWattStableUpdateStatus> {
248
+ for (const [name, previous] of Object.entries(state.previousPolicies || {})) {
249
+ const current = await this.adapter.getForeignObjectAsync(`system.adapter.${normalizedName(name)}`);
250
+ if (current?.common?.automaticUpgrade === POLICY) await this.writeAdapterPolicy(name, previous);
251
+ }
252
+ const systemConfig = await this.adapter.getForeignObjectAsync('system.config');
253
+ if (systemConfig?.common) {
254
+ const auto = systemConfig.common.adapterAutoUpgrade && typeof systemConfig.common.adapterAutoUpgrade === 'object'
255
+ ? clone(systemConfig.common.adapterAutoUpgrade)
256
+ : null;
257
+ if (auto && state.selectedRepository) {
258
+ auto.repositories = auto.repositories && typeof auto.repositories === 'object' ? { ...auto.repositories } : {};
259
+ if (auto.repositories[state.selectedRepository] === true) {
260
+ if (state.previousRepositoryEnabled === null || state.previousRepositoryEnabled === undefined) delete auto.repositories[state.selectedRepository];
261
+ else auto.repositories[state.selectedRepository] = state.previousRepositoryEnabled;
262
+ }
263
+ systemConfig.common.adapterAutoUpgrade = auto;
264
+ }
265
+ if (state.ownsActiveRepoOrder && Array.isArray(state.previousActiveRepo)
266
+ && Array.isArray(systemConfig.common.activeRepo)
267
+ && systemConfig.common.activeRepo[0] === state.selectedRepository) {
268
+ systemConfig.common.activeRepo = [...state.previousActiveRepo];
269
+ }
270
+ await this.adapter.setForeignObjectAsync('system.config', systemConfig, { user: ADMIN_USER });
271
+ }
272
+ state.previousPolicies = {};
273
+ state.previousRepositoryEnabled = undefined;
274
+ state.previousActiveRepo = undefined;
275
+ state.ownsActiveRepoOrder = false;
276
+ state.managedAdapters = [];
277
+ state.lastSync = Date.now();
278
+ state.error = '';
279
+ await this.persist(false, state);
280
+ this.adapter.log.info(`[NexoWatt stable updates] Disabled and previous policies restored (${reason})`);
281
+ return this.statusFromNative({ eosNexoWattAutoUpdate: false, eosNexoWattAutoUpdateState: state });
282
+ }
283
+
284
+ private async writeAdapterPolicy(name: string, policy: NexoWattAutoUpdatePolicy | null): Promise<void> {
285
+ const id = `system.adapter.${normalizedName(name)}`;
286
+ const object = await this.adapter.getForeignObjectAsync(id);
287
+ if (!object?.common) return;
288
+ if (policy === null) delete object.common.automaticUpgrade;
289
+ else object.common.automaticUpgrade = policy;
290
+ await this.adapter.setForeignObjectAsync(id, object, { user: ADMIN_USER });
291
+ }
292
+
293
+ private async persist(enabled: boolean, state: PersistedState): Promise<void> {
294
+ await this.adapter.extendForeignObjectAsync(this.instanceId, {
295
+ native: { eosNexoWattAutoUpdate: enabled, eosNexoWattAutoUpdateState: state },
296
+ }, { user: ADMIN_USER });
297
+ }
298
+
299
+ private scheduleRetry(): void {
300
+ if (this.retryTimer) clearTimeout(this.retryTimer);
301
+ this.retryTimer = setTimeout(() => void this.reconcile('retry'), 60_000);
302
+ this.retryTimer.unref?.();
303
+ }
304
+
305
+ private scheduleRepositoryRefresh(repository: string, state: PersistedState): void {
306
+ if (!this.adapter.sendToHostAsync || !this.adapter.host) return;
307
+ if (this.refreshTimer) clearTimeout(this.refreshTimer);
308
+ this.refreshTimer = setTimeout(() => {
309
+ void this.adapter.sendToHostAsync?.(this.adapter.host!, 'getRepository', { repo: repository, update: true })
310
+ .then(async () => {
311
+ const current = await this.adapter.getForeignObjectAsync(this.instanceId);
312
+ if (current?.native?.eosNexoWattAutoUpdate === false) return;
313
+ state.lastRepositoryRefresh = Date.now();
314
+ await this.persist(true, state);
315
+ })
316
+ .catch(error => this.adapter.log.warn(`[NexoWatt stable updates] Repository refresh failed: ${error instanceof Error ? error.message : error}`));
317
+ }, REPOSITORY_REFRESH_DELAY_MS);
318
+ this.refreshTimer.unref?.();
319
+ }
320
+ }
@@ -12,7 +12,7 @@ for(const code of [webSource,webBuilt]){
12
12
  must(!code.includes('setForeignObjectAsync(targetUserId, user)'),'reset still writes stale full user object');
13
13
  }
14
14
  for(const marker of ['setPasswordAsync(userName','checkPasswordAsync(userName','passwordNotPersisted','passwordVerificationFailed'])must(passwordSource.includes(marker),`password helper marker missing: ${marker}`);
15
- for(const marker of ["VERSION = 'v99-account-management-password-write'","['admin', 'installer']",'nexowatt/account/manage','nexowatt/account/reset','X-NexoWatt-EOS-Account-Reset','data.canResetInstaller === true',"account.role === 'enduser'",'Admin/Service','Installateure','NEXOWATT_EOS_ACCOUNT_MANAGEMENT','ensureEntrySurface',"currentRoute() !== 'tab-users'",'eos-account-management-entry','Startpasswort „nexowatt“','passwordVerificationFailed'])must(sourceUi.includes(marker),`UI marker missing: ${marker}`);
15
+ for(const marker of ["VERSION = 'v100-account-management-password-write'","['admin', 'installer']",'nexowatt/account/manage','nexowatt/account/reset','X-NexoWatt-EOS-Account-Reset','data.canResetInstaller === true',"account.role === 'enduser'",'Admin/Service','Installateure','NEXOWATT_EOS_ACCOUNT_MANAGEMENT','ensureEntrySurface',"currentRoute() !== 'tab-users'",'eos-account-management-entry','Startpasswort „nexowatt“','passwordVerificationFailed'])must(sourceUi.includes(marker),`UI marker missing: ${marker}`);
16
16
  must(sourceUi===builtUi,'account management source/build drift');
17
17
  must(!sourceUi.includes("launcher.className = 'eos-account-management-launcher'"),'standalone account launcher remains');
18
18
  must(!sourceUi.includes('new MutationObserver'),'account management adds a broad observer');
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const assert=require('node:assert/strict');
4
+ const {NexoWattStableUpdateManager,isNexoWattRepositoryEntry}=require('../build/lib/eosAutoUpdate.js');
5
+ const objects=new Map();
6
+ objects.set('system.adapter.eos-admin.0',{_id:'system.adapter.eos-admin.0',common:{name:'eos-admin',version:'7.9.100'},native:{eosNexoWattAutoUpdate:true}});
7
+ objects.set('system.config',{_id:'system.config',common:{activeRepo:['stable'],adapterAutoUpgrade:{repositories:{stable:false},defaultPolicy:'none'}},native:{}});
8
+ objects.set('system.repositories',{_id:'system.repositories',native:{repositories:{stable:{json:{'eos-admin':{name:'eos-admin',version:'7.9.100',meta:'https://github.com/NexoWatt/ioBroker.eos-admin'},'nexowatt-ui':{name:'nexowatt-ui',version:'0.8.198',publisher:'NexoWatt'},'nexowatt-beta':{name:'nexowatt-beta',version:'1.0.0-beta.1',publisher:'NexoWatt'},admin:{name:'admin',version:'7.7.0',publisher:'ioBroker'}}}}}});
9
+ objects.set('system.adapter.eos-admin',{_id:'system.adapter.eos-admin',common:{name:'eos-admin',version:'7.9.100',automaticUpgrade:'patch'},native:{}});
10
+ objects.set('system.adapter.nexowatt-ui',{_id:'system.adapter.nexowatt-ui',common:{name:'nexowatt-ui',version:'0.8.197'},native:{}});
11
+ objects.set('system.adapter.admin',{_id:'system.adapter.admin',common:{name:'admin',version:'7.7.0',automaticUpgrade:'minor'},native:{}});
12
+ const copy=v=>JSON.parse(JSON.stringify(v));
13
+ const refresh=[];
14
+ const adapter={namespace:'eos-admin.0',host:'eos-host',log:{debug(){},info(){},warn(){},error(){}},getForeignObjectAsync:async id=>copy(objects.get(id)||null),setForeignObjectAsync:async(id,obj)=>{objects.set(id,copy(obj));},extendForeignObjectAsync:async(id,part)=>{const base=copy(objects.get(id)||{_id:id,common:{},native:{}});base.native={...(base.native||{}),...(part.native||{})};base.common={...(base.common||{}),...(part.common||{})};objects.set(id,base);},getObjectViewAsync:async()=>({rows:[...objects.entries()].filter(([id])=>/^system\.adapter\.[^.]+$/.test(id)).map(([id,value])=>({id,value:copy(value)}))}),sendToHostAsync:async(...args)=>{refresh.push(args);}};
15
+ (async()=>{
16
+ assert.equal(isNexoWattRepositoryEntry('nexowatt-ui',{publisher:'NexoWatt'}),true);
17
+ assert.equal(isNexoWattRepositoryEntry('admin',{publisher:'ioBroker'}),false);
18
+ const manager=new NexoWattStableUpdateManager(adapter);
19
+ const enabled=await manager.reconcile('test');
20
+ assert.equal(enabled.enabled,true);assert.equal(enabled.repository,'stable');assert.deepEqual(enabled.managedAdapters,['eos-admin','nexowatt-ui']);
21
+ assert.equal(objects.get('system.adapter.eos-admin').common.automaticUpgrade,'major');
22
+ assert.equal(objects.get('system.adapter.nexowatt-ui').common.automaticUpgrade,'major');
23
+ assert.equal(objects.get('system.adapter.admin').common.automaticUpgrade,'minor');
24
+ assert.equal(objects.get('system.config').common.adapterAutoUpgrade.repositories.stable,true);
25
+ // A manual user change while EOS management is active must not be overwritten on disable.
26
+ objects.get('system.adapter.nexowatt-ui').common.automaticUpgrade='minor';
27
+ const disabled=await manager.setEnabled(false);assert.equal(disabled.enabled,false);
28
+ assert.equal(objects.get('system.adapter.eos-admin').common.automaticUpgrade,'patch');
29
+ assert.equal(objects.get('system.adapter.nexowatt-ui').common.automaticUpgrade,'minor');
30
+ assert.equal(objects.get('system.adapter.admin').common.automaticUpgrade,'minor');
31
+ assert.equal(objects.get('system.config').common.adapterAutoUpgrade.repositories.stable,false);
32
+ manager.stop();
33
+ console.log('[NexoWatt EOS auto update] OK (native policy, stable-only scope, third-party isolation and restore)');
34
+ })().catch(e=>{console.error(e.stack||e);process.exit(1)});
@@ -2,10 +2,11 @@
2
2
  'use strict';
3
3
  const fs=require('node:fs');const path=require('node:path');const root=path.resolve(__dirname,'..');
4
4
  const read=rel=>fs.readFileSync(path.join(root,rel),'utf8');const fail=m=>{console.error(`[NexoWatt EOS cache update] ${m}`);process.exit(1)};const must=(c,m)=>{if(!c)fail(m)};
5
- const source=read('src/lib/web.ts'),built=read('build/lib/web.js'),index=read('adminWww/index.html'),dev=read('src-admin/index.html'),watch=read('adminWww/js/eos-release-watch.js');
5
+ const source=read('src/lib/web.ts'),built=read('build/lib/web.js'),index=read('adminWww/index.html'),dev=read('src-admin/index.html'),watch=read('adminWww/js/eos-release-watch.js'),pkg=JSON.parse(read('package.json')),info=JSON.parse(read('NEXOWATT_EOS_BUILD_INFO.json'));
6
+ const release=pkg.version,tag=String(info.shellCacheTag||info.shellCacheVersion),shell=Number(info.shellCacheVersion);
6
7
  for(const code of [source,built])for(const marker of ['isEosCurrentAsset','applyNoStoreHeaders','Surrogate-Control','this.indexHTML = await this.prepareIndex'])must(code.includes(marker),`server marker missing: ${marker}`);
7
- for(const marker of ["const RELEASE = '7.9.99'",'nexowatt.eos.loadedRelease','cache: \'no-store\'','window.location.replace','navigator.serviceWorker?.getRegistrations'])must(watch.includes(marker),`watcher marker missing: ${marker}`);
8
- for(const html of [index,dev])for(const marker of ['eos-release-watch.js?v=99','nexowatt-stable-v99.js?v=99','nexowatt-native-shell.css?v=99'])must(html.includes(marker),`index marker missing: ${marker}`);
9
- must(index.includes('hostInit-v84.js?v=99')&&index.includes('index-CQZugZ1z-v84.js?v=99'),'productive module entry cache tags missing');
10
- must(!/\?v=(?:97|98)(?:["'])/.test(index),'stale product cache tag remains');
8
+ for(const marker of [`const RELEASE = '${release}'`,'nexowatt.eos.loadedRelease','cache: \'no-store\'','window.location.replace','navigator.serviceWorker?.getRegistrations'])must(watch.includes(marker),`watcher marker missing: ${marker}`);
9
+ for(const html of [index,dev])for(const marker of [`eos-release-watch.js?v=${tag}`,`nexowatt-stable-v${shell}.js?v=${tag}`,`nexowatt-native-shell.css?v=${tag}`])must(html.includes(marker),`index marker missing: ${marker}`);
10
+ must(index.includes(`hostInit-v84.js?v=${tag}`)&&index.includes(`index-CQZugZ1z-v84.js?v=${tag}`),'productive module entry cache tags missing');
11
+ must(!/\?v=(?:97|98|99)(?:["'])/.test(index),'stale product cache tag remains');
11
12
  console.log('[NexoWatt EOS cache update] OK (no-store entrypoints and release watcher)');
@@ -3,12 +3,12 @@
3
3
  const fs=require('node:fs');const path=require('node:path');const root=path.resolve(__dirname,'..');
4
4
  const read=rel=>fs.readFileSync(path.join(root,rel),'utf8');const fail=m=>{console.error(`[NexoWatt EOS login layout] ${m}`);process.exit(1)};const must=(c,m)=>{if(!c)fail(m)};
5
5
  const css=read('adminWww/css/nexowatt-native-shell.css'),sourceCss=read('src-admin/public/css/nexowatt-native-shell.css');
6
- const login=read('src-admin/src/login/Login.tsx'),stableLogin=read('adminWww/static/js/nexowatt-stable-login.js'),stable=read('adminWww/static/js/nexowatt-stable-v99.js'),index=read('adminWww/index.html'),info=JSON.parse(read('NEXOWATT_EOS_BUILD_INFO.json'));
6
+ const login=read('src-admin/src/login/Login.tsx'),stableLogin=read('adminWww/static/js/nexowatt-stable-login.js'),stable=read('adminWww/static/js/nexowatt-stable-v100.js'),index=read('adminWww/index.html'),info=JSON.parse(read('NEXOWATT_EOS_BUILD_INFO.json'));
7
7
  must(css===sourceCss,'login CSS source/build drift');
8
8
  for(const marker of ['outer frame is decorative only','orderly login, moderately enlarged','width: min(440px, calc(100vw - 20px)) !important','height: min(580px, calc(100dvh - 20px)) !important','width: min(380px, calc(100vw - 52px)) !important','min-height: 510px !important','height: auto !important','overflow: visible !important'])must(css.includes(marker),`login marker missing: ${marker}`);
9
9
  must(!css.includes('width: min(520px, calc(100vw - 24px))')&&!css.includes('width: min(460px, calc(100vw - 56px))'),'oversized v96 geometry remains');
10
10
  must(!/data-nw-login-card|data-nw-login-shell|min-height:590px|width:min\(460px|createElement\(['"]style/.test(stableLogin),'stable-login runtime mutates geometry');
11
- must(!/width:min\(460px|min-height:540px|createElement\(['"]style/.test(stable),'stable v99 runtime injects login geometry');
11
+ must(!/width:min\(460px|min-height:540px|createElement\(['"]style/.test(stable),'stable v100 runtime injects login geometry');
12
12
  must(login.includes('minHeight: 510')&&login.includes("overflowY: 'visible'")&&login.includes("maxHeight: 'none'"),'native Login source does not match product contract');
13
- must(index.includes(`nexowatt-native-shell.css?v=${info.shellCacheTag}`)&&index.includes('nexowatt-stable-v99.js?v=99'),'v99 login assets not active');
13
+ must(index.includes(`nexowatt-native-shell.css?v=${info.shellCacheTag}`)&&index.includes('nexowatt-stable-v100.js?v=100'),'v100 login assets not active');
14
14
  console.log('[NexoWatt EOS login layout] OK');
@@ -16,7 +16,7 @@ for (const file of [
16
16
  'MERGE_UPDATE.ps1',
17
17
  'MERGE_UPDATE.cmd',
18
18
  'MERGE_UPDATE.sh',
19
- 'MERGE_UPDATE_README_V7.9.99.md',
19
+ 'MERGE_UPDATE_README_V7.9.100.md',
20
20
  'tools/nexowatt-apply-ems-overview.cjs',
21
21
  'tools/nexowatt-sync-release-version.cjs',
22
22
  'tools/nexowatt-version-sync-selftest.cjs',
@@ -29,7 +29,7 @@ for (const marker of ['eos-account-management.js','nexowatt-native-shell.css','e
29
29
  for (const marker of ['NexoWatt EOS', 'Zugänge & Rollen', 'Endkunden-Zugänge', 'ensureEntrySurface']) {
30
30
  if (!accountUi.includes(marker)) fail(`modern account surface missing label: ${marker}`);
31
31
  }
32
- for (const marker of ['installIntegratedFirstLogin','v99 normal-login first activation','eos-passwordless-card','Idempotent writes are essential here']) {
32
+ for (const marker of ['installIntegratedFirstLogin','v100 normal-login first activation','eos-passwordless-card','Idempotent writes are essential here']) {
33
33
  if (!bootstrap.includes(marker)) fail(`integrated first-activation surface missing: ${marker}`);
34
34
  }
35
35
  if (/data-eos-account=|selector\.innerHTML/.test(bootstrap)) fail('compact login still injects role buttons or a second panel');
@@ -15,15 +15,15 @@ try {
15
15
  fs.mkdirSync(path.join(temp, 'src-admin', 'public', 'js'), { recursive: true });
16
16
  fs.mkdirSync(path.join(temp, 'src-admin', 'public', 'static', 'js'), { recursive: true });
17
17
  fs.mkdirSync(path.join(temp, 'adminWww', 'static', 'js'), { recursive: true });
18
- fs.writeFileSync(path.join(temp, 'NEXOWATT_EOS_BUILD_INFO.json'), JSON.stringify({ runtimeEntry: 'v82', shellCacheVersion: 99 }));
18
+ fs.writeFileSync(path.join(temp, 'NEXOWATT_EOS_BUILD_INFO.json'), JSON.stringify({ runtimeEntry: 'v82', shellCacheVersion: 100 }));
19
19
 
20
20
  const keep = [
21
21
  'adminWww/assets/bootstrap-current-v82.js',
22
22
  'adminWww/assets/Objects-current-v82.js',
23
23
  'adminWww/remoteEntry-v82.js',
24
24
  'adminWww/remoteEntry.js',
25
- 'adminWww/static/js/nexowatt-stable-v99.js',
26
- 'src-admin/public/static/js/nexowatt-stable-v99.js',
25
+ 'adminWww/static/js/nexowatt-stable-v100.js',
26
+ 'src-admin/public/static/js/nexowatt-stable-v100.js',
27
27
  'adminWww/static/js/nexowatt-stable-login.js',
28
28
  ];
29
29
  const remove = [
@@ -45,7 +45,7 @@ try {
45
45
 
46
46
  const result = cleanLegacyRuntime({ root: temp, quiet: true, silent: true });
47
47
  assert.strictEqual(result.activeRuntime, 82);
48
- assert.strictEqual(result.activeShell, 99);
48
+ assert.strictEqual(result.activeShell, 100);
49
49
  assert.strictEqual(result.removed.length, remove.length);
50
50
  for (const rel of keep) assert.ok(fs.existsSync(path.join(temp, rel)), `current runtime removed: ${rel}`);
51
51
  for (const rel of remove) assert.ok(!fs.existsSync(path.join(temp, rel)), `legacy file remained: ${rel}`);
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const fs=require('node:fs'),path=require('node:path');
4
+ const root=path.resolve(__dirname,'..');
5
+ const read=r=>fs.readFileSync(path.join(root,r),'utf8');
6
+ const must=(c,m)=>{if(!c){console.error(`[NexoWatt EOS stable v100] ${m}`);process.exit(1)}};
7
+ const pkg=JSON.parse(read('package.json'));
8
+ const io=JSON.parse(read('io-package.json'));
9
+ const info=JSON.parse(read('NEXOWATT_EOS_BUILD_INFO.json'));
10
+ const source=read('src/lib/eosAutoUpdate.ts');
11
+ const built=read('build/lib/eosAutoUpdate.js');
12
+ const web=read('src/lib/web.ts');
13
+ const webBuilt=read('build/lib/web.js');
14
+ const ui=read('adminWww/js/eos-auto-update.js');
15
+ const index=read('adminWww/index.html');
16
+ must(pkg.version==='7.9.100'&&io.version==='7.9.100'&&io.common.version==='7.9.100'&&info.version==='7.9.100','version drift');
17
+ must(io.native.eosNexoWattAutoUpdate===true,'auto update is not enabled by default');
18
+ for(const marker of ['adapterAutoUpgrade','automaticUpgrade','isNexoWattRepositoryEntry','previousPolicies','previousRepositoryEnabled','getRepository']){
19
+ must(source.includes(marker)&&built.includes(marker),`manager marker missing: ${marker}`);
20
+ }
21
+ must(source.includes("const POLICY: NexoWattAutoUpdatePolicy = 'major'")&&built.includes("const POLICY = 'major'"),'manager major policy marker missing');
22
+ must(source.includes("auto.defaultPolicy = 'none'")&&source.includes('stableVersion'),'non-NexoWatt/stable isolation missing');
23
+ must(web.includes('/nexowatt/updates/status')&&web.includes('/nexowatt/updates/settings')&&webBuilt.includes('/nexowatt/updates/settings'),'update API missing');
24
+ must(ui.includes('Automatische Adapter-Updates')&&ui.includes('X-NexoWatt-EOS-Auto-Update')&&ui.includes('jederzeit änderbar'),'admin toggle missing');
25
+ must(index.includes('eos-auto-update.css?v=100')&&index.includes('eos-auto-update.js?v=100')&&index.includes('nexowatt-stable-v100.js?v=100'),'v100 assets missing');
26
+ must(!index.includes('?v=99')&&!index.includes('stable-v99'),'stale v99 browser assets active');
27
+ console.log('[NexoWatt EOS stable v100] OK');
@@ -5,7 +5,7 @@ const runtime=read('adminWww/js/eos-ems-overview.js');const source=read('src-adm
5
5
  if(runtime!==source)fail('runtime source/build drift');
6
6
  for(const marker of ['nexowatt-ems-overview-v1','nexowatt-ui.*.info.adminOverview.*','system.adapter.nexowatt-ui.*.alive','POLL_MS = 5000','STALE_MS = 20000',"REQUIRED_UI_VERSION = '0.8.198'",'getForeignStates','Keine Kachel besitzt Schreibhoheit','currentDecisions','§14a-Fallback','nexowatt-eos-admin-socket-ready','readUiAdapterInfo','socketFromReact'])if(!runtime.includes(marker))fail(`runtime marker missing: ${marker}`);
7
7
  for(const forbidden of ['setForeignState','setState(','sendToHost','writeFile','extendObject'])if(runtime.includes(forbidden))fail(`read-only runtime contains ${forbidden}`);
8
- for(const marker of ['eos-ems-overview.css?v=99','eos-ems-overview.js?v=99','nexowatt-stable-v99.js?v=99','eos-release-watch.js?v=99'])if(!index.includes(marker))fail(`index marker missing: ${marker}`);
8
+ for(const marker of ['eos-ems-overview.css?v=100','eos-ems-overview.js?v=100','nexowatt-stable-v100.js?v=100','eos-release-watch.js?v=100'])if(!index.includes(marker))fail(`index marker missing: ${marker}`);
9
9
  if(runtime.includes('Vollständige EMS-Diagnose öffnen'))fail('unsupported EMS destination remains visible');
10
10
  if(!intro.includes('eos-overview-edit-controls')||!introBundle.includes('eos-overview-edit-controls'))fail('edit control is not inside bottom card grid');
11
11
  if(!css.includes('#eos-ems-overview-runtime')||!css.includes('.eos-ems-overview-metrics')||!css.includes('startup and compatibility messages stay compact'))fail('EMS card CSS incomplete');