iobroker.eos-admin 7.9.93 → 7.9.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL_TEST_V7.9.96.md +11 -0
- package/MERGE_UPDATE_README_V7.9.96.md +5 -0
- package/NEXOWATT_EOS_BUILD_INFO.json +19 -19
- package/NexoWatt_EOS_Admin_EMS_OVERVIEW_MERGE_VALIDATION.md +44 -0
- package/PUBLISH_STABLE_V7.9.96.md +10 -0
- package/RC73_EOS_ADMIN_MERGE_VALIDATION.md +78 -0
- package/README_EMS_OVERVIEW_MERGE.md +45 -0
- package/README_STABILITY_V7.9.96.md +11 -0
- package/RELEASE_ACCEPTANCE_V7.9.96.md +10 -0
- package/RELEASE_NOTES_V7.9.96.md +12 -0
- package/VALIDATION_EMS_OVERVIEW_MERGE.md +35 -0
- package/adminWww/assets/Adapters-B5_jQ7DE-v84.js +1 -1
- package/adminWww/assets/Intro-DkwRiz1n-v84.js +2 -2
- package/adminWww/assets/index-D2ymscJA-v84.js +1 -1
- package/adminWww/css/eos-ems-overview.css +62 -0
- package/adminWww/css/nexowatt-native-shell.css +104 -158
- package/adminWww/index.html +15 -11
- package/adminWww/js/eos-account-management.js +1 -1
- package/adminWww/js/eos-assistant.js +5 -556
- package/adminWww/js/eos-basic-settings.js +1 -1
- package/adminWww/js/eos-branding-sanitizer.js +1 -3
- package/adminWww/js/eos-ems-overview.js +140 -0
- package/adminWww/js/eos-native-security.js +1 -1
- package/adminWww/js/eos-policy-client.js +1 -1
- package/adminWww/js/eos-role-bootstrap.js +4 -4
- package/adminWww/js/eos-role-ui.js +34 -38
- package/adminWww/js/nexowatt-native-shell.js +11 -6
- package/adminWww/static/js/nexowatt-stable-login.js +79 -0
- package/adminWww/static/js/nexowatt-stable-v96.js +82 -0
- package/build/lib/chat/assistantKnowledge.js +6 -21
- package/build/lib/chat/chatOrchestrator.js +38 -33
- package/build/lib/chat/mcpClientManager.js +1 -1
- package/build/lib/nexowattStableAuth.js +74 -0
- package/build/lib/web.js +24 -7
- package/build/main.js +123 -308
- package/io-package.json +25 -16
- package/package.json +32 -15
- package/tools/nexowatt-account-management-selftest.cjs +2 -3
- package/tools/nexowatt-apply-ems-overview.cjs +115 -0
- package/tools/nexowatt-clean-legacy-runtime.cjs +4 -0
- package/tools/nexowatt-ems-overview-selftest.cjs +35 -0
- package/tools/nexowatt-internal-reserve-selftest.cjs +1 -3
- package/tools/nexowatt-login-layout-selftest.cjs +43 -22
- package/tools/nexowatt-merge-update-selftest.cjs +69 -0
- package/tools/nexowatt-modern-ui-selftest.cjs +4 -5
- package/tools/nexowatt-stable-v96-selftest.cjs +13 -0
- package/tools/nexowatt-sync-release-version.cjs +181 -0
- package/tools/nexowatt-ui-overview-runtime-selftest.cjs +16 -0
- package/tools/nexowatt-validate-package.cjs +41 -10
- package/tools/nexowatt-version-sync-selftest.cjs +86 -0
- package/INSTALL_TEST_V7.9.93.md +0 -12
- package/PUBLISH_STABLE_V7.9.93.md +0 -19
- package/README_STABILITY_V7.9.93.md +0 -15
- package/RELEASE_ACCEPTANCE_V7.9.93.md +0 -15
- package/RELEASE_NOTES_V7.9.93.md +0 -10
|
@@ -76,48 +76,53 @@ class ChatOrchestrator {
|
|
|
76
76
|
this.mcp = mcp;
|
|
77
77
|
this.adapter = adapter;
|
|
78
78
|
}
|
|
79
|
-
/** System prompt describing
|
|
80
|
-
buildSystemPrompt(language, mode, uiContext
|
|
81
|
-
const roleText = accessRole === 'admin'
|
|
82
|
-
? 'Admin / NexoWatt Service with full service visibility'
|
|
83
|
-
: accessRole === 'installer'
|
|
84
|
-
? 'Installer with commissioning and troubleshooting visibility'
|
|
85
|
-
: 'End user with only explicitly permitted Smart Home and operating visibility';
|
|
79
|
+
/** System prompt describing the assistant's role, tool usage and the current permission mode. */
|
|
80
|
+
buildSystemPrompt(language, mode, uiContext) {
|
|
86
81
|
const lines = [
|
|
87
|
-
'You are
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
'
|
|
91
|
-
'
|
|
82
|
+
'You are the ioBroker Admin Assistant, an in-app helper inside the ioBroker admin web UI.',
|
|
83
|
+
'You help the user operate their ioBroker installation: explain how to do things in the UI,',
|
|
84
|
+
'recommend which adapter to use for a device or service, and answer questions about the',
|
|
85
|
+
"user's actual system (their states, objects, devices grouped by room/function, installed",
|
|
86
|
+
'adapters, running instances, logs and historical values).',
|
|
92
87
|
'',
|
|
93
|
-
|
|
94
|
-
'
|
|
95
|
-
'
|
|
88
|
+
'KEY CAPABILITY: you can navigate the admin UI yourself. When the user asks you to OPEN or GO to',
|
|
89
|
+
'something, call the navigate_admin_ui tool with the hash route — it opens the tab OR the specific',
|
|
90
|
+
'dialog immediately (no confirmation, it only changes the view), e.g.',
|
|
91
|
+
'{"hash":"#tab-hosts/base-settings/system.host.MSI"}. Otherwise (just pointing the user somewhere)',
|
|
92
|
+
'offer a clickable Markdown hash link, e.g. [Base settings of MSI](#tab-hosts/base-settings/system.host.MSI).',
|
|
93
|
+
'NEVER say a dialog can only be opened manually or that deep-linking a dialog is not possible.',
|
|
96
94
|
'',
|
|
97
|
-
'
|
|
98
|
-
'-
|
|
99
|
-
'
|
|
100
|
-
'
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'-
|
|
104
|
-
'-
|
|
105
|
-
'
|
|
106
|
-
'
|
|
107
|
-
'
|
|
108
|
-
'
|
|
109
|
-
|
|
95
|
+
'Guidelines:',
|
|
96
|
+
'- Use the available tools to look up real data from THIS ioBroker system instead of guessing.',
|
|
97
|
+
' E.g. to list devices in a room call list_devices with the room name; to read a value call',
|
|
98
|
+
' get_states; to find objects call search_objects.',
|
|
99
|
+
'- Base your answer on the tool results. If a tool returns an error or nothing, say so briefly',
|
|
100
|
+
' and suggest what the user could check.',
|
|
101
|
+
'- Be concise and practical. Use short lists or tables when listing devices or states.',
|
|
102
|
+
'- Object IDs are case-sensitive (e.g. "hue.0.lights.1.on"); show them in `monospace`.',
|
|
103
|
+
'- IMPORTANT — this admin version SUPPORTS deep links that open specific DIALOGS directly,',
|
|
104
|
+
' not just tabs. It is FALSE to say a dialog "can only be opened with the gear/cog icon" or',
|
|
105
|
+
' that deep-linking a dialog is "technically not possible" — never say that. Instead always',
|
|
106
|
+
' offer a clickable Markdown hash link and let the USER click it; do NOT claim you opened or',
|
|
107
|
+
' navigated anywhere yourself. Examples: [Instances](#tab-instances), [Objects](#tab-objects),',
|
|
108
|
+
' [hue.0 config](#tab-instances/config/hue.0), [edit this object](#tab-objects/edit/<id>),',
|
|
109
|
+
" and a host's base-settings dialog, e.g. MSI:",
|
|
110
|
+
' [Base settings of MSI](#tab-hosts/base-settings/system.host.MSI). The exact dialog routes are',
|
|
111
|
+
' listed in the knowledge below. Prefer these links over the navigate_admin_ui tool.',
|
|
112
|
+
`- Answer in the user's language. The admin UI language is currently "${language}".`,
|
|
110
113
|
];
|
|
111
114
|
if (mode === 'act') {
|
|
112
|
-
lines.push('-
|
|
115
|
+
lines.push('- You may change things using the write/action tools (set_state, create_user_state,', ' extend_object, assign_to_enums, install_adapter, run_command, run_node_script). Every such', ' call is shown to the user for explicit confirmation before it runs, so propose the smallest', ' sensible action.', '- create_user_state only works in "0_userdata.0." or "javascript.<n>." — never elsewhere.', '- extend_object merges common/native into an existing object — use it to start/stop an', ' instance (common.enabled), change instance settings (native) or set enum members.', '- assign_to_enums adds objects to room/function enums (add-only) — use it to sort devices.', '- run_command runs an ioBroker CLI command and streams its output to the user; use it for', ' maintenance actions (restart, upload, fix, ...), not to fetch data.', '- run_node_script executes a short Node.js script on the host to CHECK things (network, files,', ' environment). Prefer read-only checks, keep it short, and put results in console.log. Never', ' use it for destructive operations.', '- run_javascript runs a one-off JS/TS script INSIDE the javascript adapter (full ioBroker API:', ' on/getState/setState/schedule/sendTo). Use it to test automation logic or do an API-based', ' check/action; it is not saved. Use run_node_script instead for plain OS checks (no ioBroker API).');
|
|
113
116
|
}
|
|
114
117
|
else {
|
|
115
|
-
lines.push('-
|
|
118
|
+
lines.push('- You currently have READ-ONLY access: you cannot change states, create objects or install', ' adapters. When a change is needed, explain the exact steps the user should take in the UI.');
|
|
116
119
|
}
|
|
117
120
|
if (uiContext?.hash) {
|
|
118
|
-
lines.push('', `The user is currently
|
|
121
|
+
lines.push('', `The user is currently in the admin UI at the route \`${uiContext.hash}\`. Take this into ` +
|
|
122
|
+
"account — don't tell them to open a tab/page they are already on, and refer to where " +
|
|
123
|
+
'they are when helpful.');
|
|
119
124
|
}
|
|
120
|
-
lines.push('', '
|
|
125
|
+
lines.push('', 'ioBroker how-to knowledge (use this to guide the user precisely):', (0, assistantKnowledge_1.renderAssistantKnowledge)());
|
|
121
126
|
return { role: 'system', content: lines.join('\n') };
|
|
122
127
|
}
|
|
123
128
|
/** Assemble the tool list offered to the model for the given mode. */
|
|
@@ -149,7 +154,7 @@ class ChatOrchestrator {
|
|
|
149
154
|
? { ...message, content: parseTextToolCalls(message.content).content }
|
|
150
155
|
: message);
|
|
151
156
|
if (!messages.some(message => message.role === 'system')) {
|
|
152
|
-
messages.unshift(this.buildSystemPrompt(language, mode, params.uiContext
|
|
157
|
+
messages.unshift(this.buildSystemPrompt(language, mode, params.uiContext));
|
|
153
158
|
}
|
|
154
159
|
const newMessages = [];
|
|
155
160
|
const steps = [];
|
|
@@ -44,7 +44,7 @@ class McpClientManager {
|
|
|
44
44
|
language: this.options.language,
|
|
45
45
|
allowSetState: this.options.allowSetState ?? false,
|
|
46
46
|
allowObjectChange: this.options.allowObjectChange ?? false,
|
|
47
|
-
clientName: '
|
|
47
|
+
clientName: 'ioBroker.admin chat helper',
|
|
48
48
|
clientVersion: '1.0.0',
|
|
49
49
|
})
|
|
50
50
|
.then(mcp => {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startNexowattStableAuth = startNexowattStableAuth;
|
|
4
|
+
const START_PASSWORD = 'nexowatt';
|
|
5
|
+
const MARKER_KEYS = ["eosFirstLoginPending", "eosMustChangePassword", "eosPasswordSetupRequired", "firstLoginRequired", "mustChangePassword", "nexowattFirstLoginPending", "nexowattMustChangePassword", "nexowattPasswordSetupRequired", "passwordChangeRequired"];
|
|
6
|
+
const timers = new WeakMap();
|
|
7
|
+
function pendingByNative(native) {
|
|
8
|
+
if (!native || typeof native !== 'object') return false;
|
|
9
|
+
return MARKER_KEYS.some(key => native[key] === true || native[key] === 'pending' || native[key] === 'required' || native[key] === 'starter');
|
|
10
|
+
}
|
|
11
|
+
async function setPassword(adapter, user, password) {
|
|
12
|
+
if (typeof adapter.setPasswordAsync === 'function') return adapter.setPasswordAsync(user, password, { user: 'system.user.admin' });
|
|
13
|
+
return new Promise((resolve, reject) => adapter.setPassword(user, password, { user: 'system.user.admin' }, err => err ? reject(err) : resolve()));
|
|
14
|
+
}
|
|
15
|
+
async function ensureGroupMembership(adapter, userId, groupId) {
|
|
16
|
+
const group = await adapter.getForeignObjectAsync(groupId);
|
|
17
|
+
if (!group || group.type !== 'group') return;
|
|
18
|
+
group.common = group.common || { name: groupId.split('.').pop(), members: [] };
|
|
19
|
+
group.common.members = Array.isArray(group.common.members) ? group.common.members : [];
|
|
20
|
+
if (!group.common.members.includes(userId)) {
|
|
21
|
+
group.common.members.push(userId);
|
|
22
|
+
await adapter.setForeignObjectAsync(groupId, group, { user: 'system.user.admin' });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function createUser(adapter, name) {
|
|
26
|
+
const id = `system.user.${name}`;
|
|
27
|
+
await adapter.setForeignObjectAsync(id, {
|
|
28
|
+
_id: id, type: 'user',
|
|
29
|
+
common: { name, enabled: true, password: '' },
|
|
30
|
+
native: { nexowattManagedAccount: true, nexowattMustChangePassword: true, nexowattPasswordState: 'starter' }
|
|
31
|
+
}, { user: 'system.user.admin' });
|
|
32
|
+
return adapter.getForeignObjectAsync(id);
|
|
33
|
+
}
|
|
34
|
+
async function prepareOne(adapter, name, group, createMissing) {
|
|
35
|
+
const id = `system.user.${name}`;
|
|
36
|
+
let obj = await adapter.getForeignObjectAsync(id);
|
|
37
|
+
let created = false;
|
|
38
|
+
if (!obj && createMissing) { obj = await createUser(adapter, name); created = true; }
|
|
39
|
+
if (!obj || obj.type !== 'user') return;
|
|
40
|
+
obj.native = obj.native || {};
|
|
41
|
+
const empty = !obj.common || !obj.common.password;
|
|
42
|
+
const pending = created || empty || pendingByNative(obj.native) || obj.native.nexowattPasswordState === 'starter';
|
|
43
|
+
if (pending) {
|
|
44
|
+
await setPassword(adapter, name, START_PASSWORD);
|
|
45
|
+
obj = await adapter.getForeignObjectAsync(id) || obj;
|
|
46
|
+
obj.native = obj.native || {};
|
|
47
|
+
obj.native.nexowattManagedAccount = true;
|
|
48
|
+
obj.native.nexowattStarterPasswordActive = true;
|
|
49
|
+
obj.native.nexowattMustChangePassword = true;
|
|
50
|
+
obj.native.nexowattPasswordState = 'starter';
|
|
51
|
+
obj.native.nexowattStarterPasswordUpdatedAt = new Date().toISOString();
|
|
52
|
+
for (const key of MARKER_KEYS) {
|
|
53
|
+
if (/required|must|pending|first/i.test(key)) obj.native[key] = true;
|
|
54
|
+
}
|
|
55
|
+
await adapter.setForeignObjectAsync(id, obj, { user: 'system.user.admin' });
|
|
56
|
+
}
|
|
57
|
+
await ensureGroupMembership(adapter, id, group);
|
|
58
|
+
}
|
|
59
|
+
async function reconcile(adapter) {
|
|
60
|
+
try {
|
|
61
|
+
await prepareOne(adapter, 'installer', 'system.group.installer', true);
|
|
62
|
+
const guest = await adapter.getForeignObjectAsync('system.user.guest');
|
|
63
|
+
const user = await adapter.getForeignObjectAsync('system.user.user');
|
|
64
|
+
if (guest || !user) await prepareOne(adapter, 'guest', 'system.group.endkunden', true);
|
|
65
|
+
if (user) await prepareOne(adapter, 'user', 'system.group.endkunden', false);
|
|
66
|
+
} catch (e) { adapter.log.warn(`[NexoWatt EOS login] Starter-account reconciliation failed: ${e && e.message || e}`); }
|
|
67
|
+
}
|
|
68
|
+
function startNexowattStableAuth(adapter) {
|
|
69
|
+
if (timers.has(adapter)) return;
|
|
70
|
+
void reconcile(adapter);
|
|
71
|
+
const timer = setInterval(() => void reconcile(adapter), 5000);
|
|
72
|
+
if (timer.unref) timer.unref();
|
|
73
|
+
timers.set(adapter, timer);
|
|
74
|
+
}
|
package/build/lib/web.js
CHANGED
|
@@ -675,7 +675,11 @@ class Web {
|
|
|
675
675
|
const account = (native.nexowattEosAccount || {});
|
|
676
676
|
const initialized = account.passwordInitialized === true
|
|
677
677
|
|| Number(account.passwordSetupVersion || account.passwordInitializationVersion || 0) >= 1;
|
|
678
|
-
const
|
|
678
|
+
const stableForced = native.nexowattPasswordChangeRequired === true
|
|
679
|
+
|| native.eosPasswordChangeRequired === true
|
|
680
|
+
|| native.nexowattFirstLoginPending === true
|
|
681
|
+
|| native.eosFirstLoginRequired === true;
|
|
682
|
+
const forced = account.forcePasswordChange === true || stableForced;
|
|
679
683
|
return { required: forced || !initialized, initialized: initialized && !forced, minLength, version: 1 };
|
|
680
684
|
}
|
|
681
685
|
catch (e) {
|
|
@@ -789,6 +793,12 @@ class Web {
|
|
|
789
793
|
account.firstLoginCompletedAt = now;
|
|
790
794
|
account.forcePasswordChange = false;
|
|
791
795
|
account.passwordlessFirstLoginAllowed = false;
|
|
796
|
+
native.nexowattPasswordChangeRequired = false;
|
|
797
|
+
native.eosPasswordChangeRequired = false;
|
|
798
|
+
native.nexowattFirstLoginPending = false;
|
|
799
|
+
native.eosFirstLoginRequired = false;
|
|
800
|
+
native.nexowattInitialPasswordApplied = false;
|
|
801
|
+
native.nexowattPasswordChangedAt = now;
|
|
792
802
|
await this.adapter.setForeignObjectAsync(access.userId, userObject);
|
|
793
803
|
await this.destroyEosRequestSessions(req);
|
|
794
804
|
for (const cookie of ['access_token', 'refresh_token', 'connect.sid']) {
|
|
@@ -1133,8 +1143,7 @@ class Web {
|
|
|
1133
1143
|
res.status(409).json({ error: 'accountDisabled' });
|
|
1134
1144
|
return;
|
|
1135
1145
|
}
|
|
1136
|
-
|
|
1137
|
-
await this.setEosUserPassword(targetUserId, bootstrapSecret);
|
|
1146
|
+
await this.setEosUserPassword(targetUserId, 'nexowatt');
|
|
1138
1147
|
const user = (await this.adapter.getForeignObjectAsync(targetUserId));
|
|
1139
1148
|
if (!user) {
|
|
1140
1149
|
throw new Error('userObjectUnavailableAfterPasswordReset');
|
|
@@ -1146,21 +1155,29 @@ class Web {
|
|
|
1146
1155
|
account.passwordSetupVersion = 0;
|
|
1147
1156
|
account.passwordInitializationVersion = 0;
|
|
1148
1157
|
account.forcePasswordChange = true;
|
|
1149
|
-
account.passwordlessFirstLoginAllowed =
|
|
1158
|
+
account.passwordlessFirstLoginAllowed = false;
|
|
1150
1159
|
account.passwordResetAt = now;
|
|
1151
1160
|
account.passwordResetBy = access.userId;
|
|
1152
|
-
account.passwordResetMode = '
|
|
1161
|
+
account.passwordResetMode = 'initial-password';
|
|
1162
|
+
native.nexowattPasswordChangeRequired = true;
|
|
1163
|
+
native.eosPasswordChangeRequired = true;
|
|
1164
|
+
native.nexowattFirstLoginPending = true;
|
|
1165
|
+
native.eosFirstLoginRequired = true;
|
|
1166
|
+
native.nexowattInitialPasswordApplied = true;
|
|
1167
|
+
native.nexowattInitialPasswordVersion = 1;
|
|
1168
|
+
native.nexowattStableInitialCredentialVersion = 1;
|
|
1153
1169
|
delete account.passwordSetAt;
|
|
1154
1170
|
delete account.firstLoginCompletedAt;
|
|
1155
1171
|
await this.adapter.setForeignObjectAsync(targetUserId, user);
|
|
1156
1172
|
this.invalidateEosPasswordClaimsForUser(targetUserId);
|
|
1157
|
-
this.adapter.log.warn(`EOS account ${targetUserId} was reset
|
|
1173
|
+
this.adapter.log.warn(`EOS account ${targetUserId} was reset to the mandatory initial-password flow by ${access.userId}`);
|
|
1158
1174
|
res.status(200).json({
|
|
1159
1175
|
success: true,
|
|
1160
1176
|
user: targetUserId,
|
|
1161
1177
|
role: explicitlyManagedRole,
|
|
1162
1178
|
firstLoginRequired: true,
|
|
1163
|
-
passwordlessFirstLoginAllowed:
|
|
1179
|
+
passwordlessFirstLoginAllowed: false,
|
|
1180
|
+
initialPasswordRequired: true,
|
|
1164
1181
|
});
|
|
1165
1182
|
}
|
|
1166
1183
|
async getEosHistoryInstances() {
|