iobroker.eos-admin 7.9.99 → 7.10.0

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 (45) hide show
  1. package/{INSTALL_TEST_V7.9.99.md → INSTALL_TEST_V7.10.0.md} +2 -2
  2. package/{MERGE_UPDATE_README_V7.9.99.md → MERGE_UPDATE_README_V7.10.0.md} +1 -1
  3. package/NEXOWATT_EOS_BUILD_INFO.json +21 -21
  4. package/{PUBLISH_STABLE_V7.9.99.md → PUBLISH_STABLE_V7.10.0.md} +2 -2
  5. package/README_STABILITY_V7.10.0.md +3 -0
  6. package/{RELEASE_ACCEPTANCE_V7.9.99.md → RELEASE_ACCEPTANCE_V7.10.0.md} +1 -1
  7. package/RELEASE_NOTES_V7.10.0.md +17 -0
  8. package/adminWww/assets/bootstrap-COulQZax-v84.js +1 -1
  9. package/adminWww/css/eos-auto-update.css +4 -0
  10. package/adminWww/css/eos-ems-overview.css +1 -1
  11. package/adminWww/css/nexowatt-native-shell.css +432 -2
  12. package/adminWww/index.html +21 -19
  13. package/adminWww/js/eos-account-management.js +1 -1
  14. package/adminWww/js/eos-assistant.js +1 -1
  15. package/adminWww/js/eos-auto-update.js +78 -0
  16. package/adminWww/js/eos-ems-overview.js +1 -1
  17. package/adminWww/js/eos-release-watch.js +1 -1
  18. package/adminWww/js/eos-role-bootstrap.js +69 -12
  19. package/adminWww/js/eos-role-ui.js +2 -2
  20. package/adminWww/js/nexowatt-native-shell.js +1 -1
  21. package/adminWww/static/js/nexowatt-stable-login.js +70 -43
  22. package/adminWww/static/js/nexowatt-stable-v7100.js +111 -0
  23. package/build/lib/eosAutoUpdate.js +43 -0
  24. package/build/lib/eosPassword.js +49 -39
  25. package/build/lib/web.js +142 -19
  26. package/build/main.js +1 -1
  27. package/io-package.json +7 -6
  28. package/package.json +20 -14
  29. package/src/lib/eosAutoUpdate.ts +320 -0
  30. package/tools/nexowatt-account-management-selftest.cjs +2 -2
  31. package/tools/nexowatt-auto-update-selftest.cjs +34 -0
  32. package/tools/nexowatt-cache-update-selftest.cjs +6 -5
  33. package/tools/nexowatt-login-layout-selftest.cjs +11 -10
  34. package/tools/nexowatt-merge-update-selftest.cjs +1 -1
  35. package/tools/nexowatt-modern-ui-selftest.cjs +1 -1
  36. package/tools/nexowatt-password-write-selftest.cjs +13 -41
  37. package/tools/nexowatt-runtime-cleanup-selftest.cjs +6 -4
  38. package/tools/nexowatt-stable-v7100-selftest.cjs +17 -0
  39. package/tools/nexowatt-sync-release-version.cjs +4 -1
  40. package/tools/nexowatt-ui-overview-runtime-selftest.cjs +1 -1
  41. package/tools/nexowatt-validate-package.cjs +11 -11
  42. package/README_STABILITY_V7.9.99.md +0 -18
  43. package/RELEASE_NOTES_V7.9.99.md +0 -9
  44. package/adminWww/static/js/nexowatt-stable-v99.js +0 -33
  45. package/tools/nexowatt-stable-v99-selftest.cjs +0 -25
@@ -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
+ }
@@ -11,8 +11,8 @@ for(const code of [webSource,webBuilt]){
11
11
  must(!code.includes('const explicitEndUser = targetAccess.groups.some'),'obsolete explicit end-user re-classifier remains');
12
12
  must(!code.includes('setForeignObjectAsync(targetUserId, user)'),'reset still writes stale full user object');
13
13
  }
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}`);
14
+ for(const marker of ['apiTarget: normalized.userId','apiTarget: normalized.userName','verifyAfterWrite','passwordNotPersisted','passwordVerificationFailed'])must(passwordSource.includes(marker),`password helper marker missing: ${marker}`);
15
+ for(const marker of ["VERSION = 'v7100-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.10.0'},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.10.0',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.10.0',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)');
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
- const fs=require('node:fs');const path=require('node:path');const root=path.resolve(__dirname,'..');
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
- 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'));
3
+ const fs=require('node:fs'),path=require('node:path');const root=path.resolve(__dirname,'..');const read=r=>fs.readFileSync(path.join(root,r),'utf8');const fail=m=>{console.error(`[NexoWatt EOS login layout] ${m}`);process.exit(1)};const must=(c,m)=>{if(!c)fail(m)};
4
+ const css=read('adminWww/css/nexowatt-native-shell.css'),sourceCss=read('src-admin/public/css/nexowatt-native-shell.css'),login=read('src-admin/src/login/Login.tsx'),bundle=read('adminWww/assets/bootstrap-COulQZax-v84.js'),boot=read('adminWww/js/eos-role-bootstrap.js'),stableLogin=read('adminWww/static/js/nexowatt-stable-login.js'),stable=read('adminWww/static/js/nexowatt-stable-v7100.js'),index=read('adminWww/index.html'),info=JSON.parse(read('NEXOWATT_EOS_BUILD_INFO.json'));
7
5
  must(css===sourceCss,'login CSS source/build drift');
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}`);
6
+ for(const marker of ['NexoWatt EOS 7.10.0 – restored clean login geometry','NexoWatt EOS 7.10.0 final – static labels above login fields','width: min(424px, calc(100vw - 20px)) !important','width: min(372px, calc(100vw - 48px)) !important','position: static !important','transform: none !important','display: none !important','overflow: visible !important'])must(css.includes(marker),`login marker missing: ${marker}`);
9
7
  must(!css.includes('width: min(520px, calc(100vw - 24px))')&&!css.includes('width: min(460px, calc(100vw - 56px))'),'oversized v96 geometry remains');
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');
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');
14
- console.log('[NexoWatt EOS login layout] OK');
8
+ must(stableLogin.includes('normalizeGeometry')&&stableLogin.includes('MuiInputLabel-shrink')&&stableLogin.includes("removeAttribute('data-nw-login-card')")&&stableLogin.includes("removeAttribute('aria-hidden')"),'login cleanup/static-label runtime missing');
9
+ must(!/setAttribute\(['"]data-nw-login-card|createElement\(['"]style/.test(stableLogin),'stable-login runtime creates geometry attributes/styles');
10
+ must(!/width:min\(460px|min-height:540px|createElement\(['"]style/.test(stable),'stable runtime injects login geometry');
11
+ must(boot.includes("const card = form?.closest?.('.MuiPaper-root')")&&!boot.includes("closest?.('.MuiPaper-root,form,main,section')"),'login Paper ownership incorrect');
12
+ must(login.includes('minHeight: 0')&&login.includes('maxWidth: 372')&&login.includes("marginTop: 18")&&login.includes('width: 90')&&(login.match(/inputLabel: \{ shrink: true \}/g)||[]).length>=2,'native Login source does not match product contract');
13
+ for(const marker of ['p:"26px 28px 24px"','width:"calc(100% - 48px)"','maxWidth:372','width:90,height:90','marginTop:18','inputLabel:{shrink:!0}'])must(bundle.includes(marker),`productive login bundle marker missing: ${marker}`);
14
+ must(index.includes(`nexowatt-native-shell.css?v=${info.shellCacheTag}`)&&index.includes('nexowatt-stable-v7100.js?v=7100'),'v7100 login assets not active');
15
+ console.log('[NexoWatt EOS login layout] OK (single Paper, static labels, compact responsive geometry)');
@@ -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.10.0.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','v7100 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');
@@ -1,46 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
  const assert=require('node:assert/strict');
4
- const fs=require('node:fs');
5
- const path=require('node:path');
6
- const {normalizeEosPasswordTarget,setEosUserPasswordWithVerification}=require('../build/lib/eosPassword.js');
7
- const root=path.resolve(__dirname,'..');
8
- const source=fs.readFileSync(path.join(root,'src/lib/eosPassword.ts'),'utf8');
9
- const web=fs.readFileSync(path.join(root,'src/lib/web.ts'),'utf8');
10
-
11
- function adapterFixture({verify=true,persist=true,callback=false}={}){
12
- let password='old';
13
- let hash=persist?'$old':'';
14
- const calls=[];
15
- const adapter={
16
- calls,
17
- getForeignObjectAsync:async id=>id==='system.user.user'?{_id:id,common:{password:hash},native:{}}:null,
18
- checkPasswordAsync:async (user,value,options)=>{calls.push({kind:'check',user,value,options});return [verify&&user==='user'&&value===password,'system.user.user'];},
19
- };
20
- const set=async(user,value,options)=>{calls.push({kind:'set',user,value,options});if(user!=='user')throw new Error('shortUserNameRequired');if(persist){password=value;hash=`$hash:${value}`;}};
21
- if(callback) adapter.setPassword=(user,value,options,cb)=>set(user,value,options).then(()=>cb(),cb);
22
- else adapter.setPasswordAsync=set;
23
- return adapter;
4
+ const {normalizeEosPasswordTarget,setEosUserPasswordWithVerification,verifyEosUserPasswordCredential}=require('../build/lib/eosPassword.js');
5
+ function adapter({canonicalFails=false, optionsFail=false, verify=true}={}){
6
+ const user={_id:'system.user.user',common:{password:'$old'},native:{}};let accepted='old';const calls=[];
7
+ const set=async(target,password,options)=>{calls.push({target,password,options});if(canonicalFails&&target==='system.user.user')throw new Error('canonicalRejected');if(optionsFail&&options)throw new Error('optionsRejected');user.common.password=`$${Date.now()}:${password}`;accepted=password};
8
+ return {calls,getForeignObjectAsync:async id=>id==='system.user.user'?user:null,setPasswordAsync:set,checkPasswordAsync:async(target,password)=>[verify&&['system.user.user','user'].includes(target)&&password===accepted,'system.user.user']};
24
9
  }
25
-
26
10
  (async()=>{
27
- assert.deepEqual(normalizeEosPasswordTarget('user'),{userId:'system.user.user',userName:'user'});
28
- assert.deepEqual(normalizeEosPasswordTarget('system.user.user'),{userId:'system.user.user',userName:'user'});
29
- assert.throws(()=>normalizeEosPasswordTarget('system.user.bad.name'),/invalidPasswordTarget/);
30
- for(const fixture of [adapterFixture(),adapterFixture({callback:true})]){
31
- const result=await setEosUserPasswordWithVerification(fixture,'system.user.user','Strong!2026');
32
- assert.equal(result.userName,'user');
33
- assert.equal(fixture.calls.find(c=>c.kind==='set').user,'user','password API must receive short user name');
34
- assert.deepEqual(fixture.calls.find(c=>c.kind==='set').options,{user:'system.user.admin'});
35
- assert.equal(fixture.calls.find(c=>c.kind==='check').user,'user','password check must receive short user name');
36
- }
37
- const noCheck=adapterFixture();delete noCheck.checkPasswordAsync;
38
- await setEosUserPasswordWithVerification(noCheck,'user','NoCheck!2026');
39
- await assert.rejects(()=>setEosUserPasswordWithVerification(adapterFixture({persist:false}),'user','Missing!2026'),/passwordNotPersisted/);
40
- await assert.rejects(()=>setEosUserPasswordWithVerification(adapterFixture({verify:false}),'user','Verify!2026'),/passwordVerificationFailed/);
41
- for(const marker of ['setPasswordAsync(userName','checkPasswordAsync(userName','common?.password','userId.replace(/^system\\.user\\./']) assert(source.includes(marker),`source marker missing: ${marker}`);
42
- assert(!/setForeignObject|common\.password\s*=/.test(source),'helper must never hash/write common.password itself');
43
- assert(web.includes('extendForeignObjectAsync')&&web.includes('passwordMetadataNotPersisted'),'first-login metadata persistence guard missing');
44
- assert(!web.includes('setForeignObjectAsync(access.userId, userObject)'),'stale full-user first-login write remains');
45
- console.log('[NexoWatt EOS password write] OK (short name, persistence, credential and native-only metadata)');
46
- })().catch(error=>{console.error(error?.stack||error);process.exit(1);});
11
+ assert.deepEqual(normalizeEosPasswordTarget('user'),{userId:'system.user.user',userName:'user'});
12
+ const normal=adapter();const a=await setEosUserPasswordWithVerification(normal,'system.user.user','Strong!2026');assert.equal(a.apiTarget,'system.user.user');
13
+ await verifyEosUserPasswordCredential(normal,'system.user.user','Strong!2026');
14
+ const legacy=adapter({canonicalFails:true});const b=await setEosUserPasswordWithVerification(legacy,'system.user.user','Legacy!2026');assert.equal(b.apiTarget,'user');
15
+ const noOptions=adapter({optionsFail:true});const c=await setEosUserPasswordWithVerification(noOptions,'system.user.user','NoOptions!2026');assert.ok(['system.user.user','user'].includes(c.apiTarget));assert.ok(noOptions.calls.some(call=>call.options===null));
16
+ await assert.rejects(()=>setEosUserPasswordWithVerification(adapter({verify:false}),'user','Fail!2026'),/passwordVerificationFailed/);
17
+ console.log('[NexoWatt EOS password write v7100] OK');
18
+ })().catch(error=>{console.error(error.stack||error);process.exit(1)});
@@ -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: 7100 }));
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-v7100.js',
26
+ 'src-admin/public/static/js/nexowatt-stable-v7100.js',
27
27
  'adminWww/static/js/nexowatt-stable-login.js',
28
28
  ];
29
29
  const remove = [
@@ -34,6 +34,8 @@ try {
34
34
  'adminWww/js/eos-branding.js',
35
35
  'adminWww/css/eos-branding.css',
36
36
  'src-admin/public/js/eos-security-ui.js',
37
+ 'adminWww/static/js/nexowatt-stable-v100.js',
38
+ 'src-admin/public/static/js/nexowatt-stable-v99.js',
37
39
  'adminWww/static/js/nexowatt-stable-v98.js',
38
40
  'src-admin/public/static/js/nexowatt-stable-v97.js',
39
41
  ];
@@ -45,7 +47,7 @@ try {
45
47
 
46
48
  const result = cleanLegacyRuntime({ root: temp, quiet: true, silent: true });
47
49
  assert.strictEqual(result.activeRuntime, 82);
48
- assert.strictEqual(result.activeShell, 99);
50
+ assert.strictEqual(result.activeShell, 7100);
49
51
  assert.strictEqual(result.removed.length, remove.length);
50
52
  for (const rel of keep) assert.ok(fs.existsSync(path.join(temp, rel)), `current runtime removed: ${rel}`);
51
53
  for (const rel of remove) assert.ok(!fs.existsSync(path.join(temp, rel)), `legacy file remained: ${rel}`);
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const fs=require('node:fs'),path=require('node:path');const root=path.resolve(__dirname,'..');const read=r=>fs.readFileSync(path.join(root,r),'utf8');const must=(v,m)=>{if(!v){console.error(`[NexoWatt EOS stable v7100] ${m}`);process.exit(1)}};
4
+ const p=JSON.parse(read('package.json')),io=JSON.parse(read('io-package.json')),info=JSON.parse(read('NEXOWATT_EOS_BUILD_INFO.json')),css=read('adminWww/css/nexowatt-native-shell.css'),stable=read('adminWww/static/js/nexowatt-stable-v7100.js'),login=read('adminWww/static/js/nexowatt-stable-login.js'),boot=read('adminWww/js/eos-role-bootstrap.js'),password=read('src/lib/eosPassword.ts'),web=read('src/lib/web.ts'),index=read('adminWww/index.html'),auto=read('src/lib/eosAutoUpdate.ts');
5
+ must(p.version==='7.10.0'&&io.version==='7.10.0'&&io.common.version==='7.10.0'&&info.version==='7.10.0','version drift');
6
+ must(info.shellCacheVersion===7100&&info.shellCacheTag==='7100','cache tag drift');
7
+ must(index.includes('nexowatt-stable-v7100.js?v=7100')&&index.includes('eos-auto-update.js?v=7100'),'active assets missing');
8
+ must(stable.includes('ensureCockpitScroll')&&stable.includes('disableCockpitScroll')&&stable.includes("overflowY: 'scroll'")&&stable.includes("paper.addEventListener('wheel'")&&stable.includes('var(--nx-content-top, 154px)'),'scroll runtime missing or route cleanup incomplete');
9
+ must(css.includes('restored clean login geometry')&&css.includes('static labels above login fields')&&css.includes('position: static !important')&&css.includes('overflow-y: scroll !important'),'login/scroll CSS missing');
10
+ must(login.includes('MuiInputLabel-shrink')&&login.includes('normalizeGeometry')&&login.includes("removeAttribute('aria-hidden')"),'login static-label runtime missing');
11
+ must(boot.includes("const card = form?.closest?.('.MuiPaper-root')")&&!boot.includes("closest?.('.MuiPaper-root,form,main,section')"),'login Paper ownership incorrect');
12
+ must(boot.includes('waitForPasswordSocket')&&boot.includes('socket.changePassword')&&boot.includes('passwordAlreadyWritten')&&boot.includes('postWithTimeout'),'native user-editor password path missing');
13
+ must(password.includes('apiTarget: normalized.userId')&&password.includes('apiTarget: normalized.userName')&&password.includes('verifyAfterWrite')&&password.includes('verifyEosUserPasswordCredential'),'password compatibility/verification missing');
14
+ must(web.includes('EosPasswordSetupTicket')&&web.includes('passwordAlreadyWritten')&&web.includes('verifyEosUserPasswordCredential'),'setup ticket/finalization path missing');
15
+ must(!/issueEosPasswordSetupTicket[\s\S]{0,600}invalidateEosPasswordSetupTicketsForUser\(userId\)/.test(web),'policy refresh invalidates first-login ticket');
16
+ must(auto.includes('eosNexoWattAutoUpdate !== false')&&auto.includes("const POLICY: NexoWattAutoUpdatePolicy = 'major'"),'NexoWatt stable auto-update default missing');
17
+ console.log('[NexoWatt EOS stable v7100] OK');
@@ -32,7 +32,10 @@ function syncReleaseVersion(rootDir) {
32
32
  throw new Error(`Invalid package.json version: ${version || '<missing>'}`);
33
33
  }
34
34
 
35
- const releaseNo = version.match(/^7\.9\.(\d+)/)?.[1] || version.replace(/[^0-9A-Za-z]+/g, '-');
35
+ const parsed = version.match(/^(\d+)\.(\d+)\.(\d+)/);
36
+ const releaseNo = parsed
37
+ ? (parsed[1] === '7' && parsed[2] === '9' ? parsed[3] : `${parsed[1]}${parsed[2]}${parsed[3]}`)
38
+ : version.replace(/[^0-9A-Za-z]+/g, '-');
36
39
  const expectedBase = `https://unpkg.com/iobroker.eos-admin@${version}`;
37
40
  const changes = [];
38
41
 
@@ -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=7100','eos-ems-overview.js?v=7100','nexowatt-stable-v7100.js?v=7100','eos-release-watch.js?v=7100'])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');