@pellux/goodvibes-tui 0.19.28 → 0.19.29
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/CHANGELOG.md +6 -0
- package/README.md +3 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/cli/surface-command.ts +46 -11
- package/src/daemon/cli.ts +7 -0
- package/src/input/handler-onboarding.ts +151 -44
- package/src/input/onboarding/handler-onboarding-routes.ts +4 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +35 -8
- package/src/input/onboarding/onboarding-wizard-constants.ts +4 -5
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +93 -5
- package/src/input/onboarding/onboarding-wizard-helpers.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-rules.ts +22 -3
- package/src/input/onboarding/onboarding-wizard-state.ts +12 -7
- package/src/input/onboarding/onboarding-wizard-steps.ts +133 -59
- package/src/input/onboarding/onboarding-wizard-types.ts +10 -0
- package/src/input/onboarding/onboarding-wizard.ts +56 -4
- package/src/input/settings-modal-types.ts +2 -1
- package/src/input/settings-modal.ts +4 -0
- package/src/main.ts +33 -26
- package/src/renderer/compositor.ts +3 -3
- package/src/renderer/onboarding/onboarding-wizard.ts +38 -21
- package/src/renderer/settings-modal-helpers.ts +9 -0
- package/src/renderer/settings-modal.ts +3 -0
- package/src/runtime/bootstrap.ts +15 -0
- package/src/runtime/onboarding/apply.ts +36 -8
- package/src/runtime/onboarding/derivation.ts +7 -7
- package/src/runtime/onboarding/snapshot.ts +1 -0
- package/src/runtime/onboarding/types.ts +4 -1
- package/src/runtime/onboarding/verify.ts +1 -1
- package/src/runtime/surface-feature-flags.ts +67 -0
- package/src/version.ts +1 -1
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { OnboardingAcknowledgementTarget, OnboardingApplyOperation, OnboardingApplyRequest } from '../../runtime/onboarding/index.ts';
|
|
2
|
-
import {
|
|
2
|
+
import { getServerSurfaceFeatureFlags } from '../../runtime/surface-feature-flags.ts';
|
|
3
|
+
import {
|
|
4
|
+
EXTERNAL_SURFACE_SPECS,
|
|
5
|
+
getExternalSurfaceAutoStartDefaultValue,
|
|
6
|
+
getExternalSurfaceAutoStartFieldId,
|
|
7
|
+
isExternalSurfaceSelectedByDefault,
|
|
8
|
+
} from './onboarding-wizard-external-surfaces.ts';
|
|
3
9
|
import { buildGoodVibesSecretKey, buildGoodVibesSecretRef, isLoopbackAddress, isSecretReferenceValue } from './onboarding-wizard-helpers.ts';
|
|
4
10
|
import type { OnboardingWizardController } from './onboarding-wizard.ts';
|
|
5
11
|
|
|
@@ -8,6 +14,7 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
8
14
|
const hasServers = controller.hasServerCapabilitiesSelected();
|
|
9
15
|
const browserAccess = controller.shouldEnableBrowserSurface();
|
|
10
16
|
const httpListener = controller.shouldEnableHttpListener();
|
|
17
|
+
const httpListenerNetworkFields = controller.shouldExposeHttpListenerNetworkFields();
|
|
11
18
|
const controlPlaneRemote = controller.shouldExposeControlPlaneNetwork();
|
|
12
19
|
const networkMode = controller.getStringFieldValue('network.mode', controller.runtimeDerived.step1_5NetworkMode);
|
|
13
20
|
const customNetwork = hasServers && networkMode === 'custom';
|
|
@@ -18,6 +25,9 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
18
25
|
): void => {
|
|
19
26
|
operations.push({ kind: 'set-config', key, value });
|
|
20
27
|
};
|
|
28
|
+
const enableFeatureFlags = (flagIds: readonly string[]): void => {
|
|
29
|
+
for (const flagId of flagIds) setConfig(`featureFlags.${flagId}`, 'enabled');
|
|
30
|
+
};
|
|
21
31
|
const acknowledge = (target: OnboardingAcknowledgementTarget, fieldId: string): void => {
|
|
22
32
|
operations.push({
|
|
23
33
|
kind: 'acknowledge',
|
|
@@ -50,11 +60,13 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
50
60
|
setConfig(key, buildGoodVibesSecretRef(secretKey));
|
|
51
61
|
};
|
|
52
62
|
|
|
53
|
-
|
|
63
|
+
const requestedAdminPassword = controller.getStringFieldValue('accounts.admin-password', '');
|
|
64
|
+
const shouldEnsureAuthUser = controller.requiresAuthBootstrap() || requestedAdminPassword.length > 0;
|
|
65
|
+
if (shouldEnsureAuthUser) {
|
|
54
66
|
operations.push({
|
|
55
67
|
kind: 'ensure-auth-user',
|
|
56
68
|
username: controller.getStringFieldValue('accounts.admin-username', controller.getDefaultAdminUsername()),
|
|
57
|
-
password:
|
|
69
|
+
password: requestedAdminPassword,
|
|
58
70
|
roles: ['admin'],
|
|
59
71
|
createSession: true,
|
|
60
72
|
retireBootstrapCredential: controller.hasBootstrapCredentialPresent(),
|
|
@@ -73,7 +85,7 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
73
85
|
addNetworkOperations(controller, operations, customNetwork, {
|
|
74
86
|
controlPlane: hasServers,
|
|
75
87
|
controlPlaneRemote,
|
|
76
|
-
httpListener,
|
|
88
|
+
httpListener: httpListenerNetworkFields,
|
|
77
89
|
web: browserAccess,
|
|
78
90
|
});
|
|
79
91
|
} else {
|
|
@@ -100,11 +112,21 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
100
112
|
setSecret('OPENAI_API_KEY', controller.getStringFieldValue('providers.openai-api-key', ''));
|
|
101
113
|
|
|
102
114
|
const externalIntegrations = controller.isCapabilitySelected('external-integrations');
|
|
115
|
+
const enabledExternalSurfaceIds: string[] = [];
|
|
103
116
|
for (const surface of EXTERNAL_SURFACE_SPECS) {
|
|
104
|
-
const
|
|
105
|
-
&& controller.getBooleanFieldValue(
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
const selected = externalIntegrations
|
|
118
|
+
&& controller.getBooleanFieldValue(
|
|
119
|
+
surface.enabledFieldId,
|
|
120
|
+
isExternalSurfaceSelectedByDefault(surface, controller.runtimeSnapshot),
|
|
121
|
+
);
|
|
122
|
+
const autoStart = selected
|
|
123
|
+
&& controller.getStringFieldValue(
|
|
124
|
+
getExternalSurfaceAutoStartFieldId(surface),
|
|
125
|
+
getExternalSurfaceAutoStartDefaultValue(surface, controller.runtimeSnapshot),
|
|
126
|
+
) === 'yes';
|
|
127
|
+
setConfig(surface.enabledConfigKey, autoStart);
|
|
128
|
+
if (!selected) continue;
|
|
129
|
+
enabledExternalSurfaceIds.push(surface.id);
|
|
108
130
|
|
|
109
131
|
for (const setupField of surface.fields) {
|
|
110
132
|
const fallback = setupField.defaultValue(controller.runtimeSnapshot);
|
|
@@ -126,6 +148,11 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
126
148
|
else setConfig(setupField.configKey, value);
|
|
127
149
|
}
|
|
128
150
|
}
|
|
151
|
+
enableFeatureFlags(getServerSurfaceFeatureFlags({
|
|
152
|
+
serverBacked: hasServers,
|
|
153
|
+
web: browserAccess,
|
|
154
|
+
externalSurfaces: enabledExternalSurfaceIds,
|
|
155
|
+
}));
|
|
129
156
|
|
|
130
157
|
acknowledge('providers', 'providers.reviewed');
|
|
131
158
|
acknowledge('subscriptions', 'accounts.subscriptions');
|
|
@@ -17,19 +17,19 @@ export const DEFAULT_CAPABILITIES: readonly OnboardingStep1CapabilityItem[] = [
|
|
|
17
17
|
id: 'local-tui-only',
|
|
18
18
|
label: 'Local TUI Only (No Servers)',
|
|
19
19
|
selected: true,
|
|
20
|
-
detail: '
|
|
20
|
+
detail: 'Use GoodVibes only in this terminal. No browser access, background service, HTTP listener, external app surface, or network setup.',
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
id: 'browser-access',
|
|
24
24
|
label: 'Open GoodVibes in a Browser',
|
|
25
25
|
selected: false,
|
|
26
|
-
detail: '
|
|
26
|
+
detail: 'Run the background service and web UI. GoodVibes will use the local network by default; you can restrict or customize it next.',
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
id: 'network-access',
|
|
30
30
|
label: 'Let other devices use GoodVibes',
|
|
31
31
|
selected: false,
|
|
32
|
-
detail: '
|
|
32
|
+
detail: 'Make enabled GoodVibes services reachable from other devices on your LAN. Local authentication is required.',
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
id: 'webhook-events',
|
|
@@ -41,7 +41,7 @@ export const DEFAULT_CAPABILITIES: readonly OnboardingStep1CapabilityItem[] = [
|
|
|
41
41
|
id: 'external-integrations',
|
|
42
42
|
label: 'Connect GoodVibes to external apps and services',
|
|
43
43
|
selected: false,
|
|
44
|
-
detail: '
|
|
44
|
+
detail: 'Enable setup screens for Slack, Discord, Telegram, Teams, Matrix, and other app surfaces you choose.',
|
|
45
45
|
},
|
|
46
46
|
];
|
|
47
47
|
|
|
@@ -115,7 +115,6 @@ export const REQUIRED_EXTERNAL_SETUP_FIELD_IDS = new Set<string>([
|
|
|
115
115
|
'external-services.discord.public-key',
|
|
116
116
|
'external-services.telegram.bot-token',
|
|
117
117
|
'external-services.telegram.webhook-secret',
|
|
118
|
-
'external-services.ntfy.topic',
|
|
119
118
|
'external-services.webhook.default-target',
|
|
120
119
|
'external-services.google-chat.webhook-url',
|
|
121
120
|
'external-services.google-chat.verification-token',
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
GOODVIBES_NTFY_AGENT_TOPIC,
|
|
3
|
+
GOODVIBES_NTFY_CHAT_TOPIC,
|
|
4
|
+
GOODVIBES_NTFY_REMOTE_TOPIC,
|
|
5
|
+
resolveGoodVibesNtfyTopics,
|
|
6
|
+
} from '@pellux/goodvibes-sdk/platform/integrations/ntfy';
|
|
7
|
+
import { DEFAULT_CONFIG, type ConfigKey } from '../../config/index.ts';
|
|
2
8
|
import type { OnboardingSnapshotState } from '../../runtime/onboarding/index.ts';
|
|
3
9
|
import { TELEGRAM_MODE_OPTIONS, WHATSAPP_PROVIDER_OPTIONS } from './onboarding-wizard-constants.ts';
|
|
4
10
|
import type { OnboardingWizardRadioOption } from './onboarding-wizard-types.ts';
|
|
@@ -24,10 +30,65 @@ export interface ExternalSurfaceSpec {
|
|
|
24
30
|
readonly enabledConfigKey: ConfigKey;
|
|
25
31
|
readonly label: string;
|
|
26
32
|
readonly hint: string;
|
|
33
|
+
/**
|
|
34
|
+
* Existing SDK config key. In onboarding this maps to the per-surface
|
|
35
|
+
* auto-start choice, not to whether setup fields are shown.
|
|
36
|
+
*/
|
|
27
37
|
readonly defaultEnabled: (snapshot: OnboardingSnapshotState | null) => boolean;
|
|
28
38
|
readonly fields: readonly ExternalSurfaceSetupFieldSpec[];
|
|
29
39
|
}
|
|
30
40
|
|
|
41
|
+
function normalizeConfigValue(value: unknown): string {
|
|
42
|
+
if (value === null || value === undefined) return '';
|
|
43
|
+
return String(value).trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getDefaultConfigValue(key: ConfigKey): unknown {
|
|
47
|
+
return key.split('.').reduce<unknown>((cursor, part) => (
|
|
48
|
+
typeof cursor === 'object' && cursor !== null && part in cursor
|
|
49
|
+
? (cursor as Record<string, unknown>)[part]
|
|
50
|
+
: undefined
|
|
51
|
+
), DEFAULT_CONFIG);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getExternalSurfaceAutoStartFieldId(surface: ExternalSurfaceSpec): string {
|
|
55
|
+
return `${surface.enabledFieldId}.auto-start`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getExternalSurfaceAutoStartDefaultValue(
|
|
59
|
+
surface: ExternalSurfaceSpec,
|
|
60
|
+
snapshot: OnboardingSnapshotState | null,
|
|
61
|
+
): 'yes' | 'no' {
|
|
62
|
+
return surface.defaultEnabled(snapshot) ? 'yes' : 'no';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isExternalSurfaceSelectedByDefault(
|
|
66
|
+
surface: ExternalSurfaceSpec,
|
|
67
|
+
snapshot: OnboardingSnapshotState | null,
|
|
68
|
+
): boolean {
|
|
69
|
+
if (surface.defaultEnabled(snapshot)) return true;
|
|
70
|
+
if (!snapshot) return false;
|
|
71
|
+
|
|
72
|
+
return surface.fields.some((field) => {
|
|
73
|
+
const current = normalizeConfigValue(field.defaultValue(snapshot));
|
|
74
|
+
const defaultValue = normalizeConfigValue(getDefaultConfigValue(field.configKey));
|
|
75
|
+
return current.length > 0 && current !== defaultValue;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getNtfyProtocolTopicLines(snapshot: OnboardingSnapshotState | null): readonly string[] {
|
|
80
|
+
const topics = resolveGoodVibesNtfyTopics({
|
|
81
|
+
chatTopic: snapshot?.config.surfaces.ntfy.chatTopic,
|
|
82
|
+
agentTopic: snapshot?.config.surfaces.ntfy.agentTopic,
|
|
83
|
+
remoteTopic: snapshot?.config.surfaces.ntfy.remoteTopic,
|
|
84
|
+
});
|
|
85
|
+
return [
|
|
86
|
+
`Chat topic: ${topics.chatTopic}`,
|
|
87
|
+
`Agent topic: ${topics.agentTopic}`,
|
|
88
|
+
`Daemon-only remote topic: ${topics.remoteTopic}`,
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
|
|
31
92
|
export const EXTERNAL_SURFACE_SPECS: readonly ExternalSurfaceSpec[] = [
|
|
32
93
|
{
|
|
33
94
|
id: 'slack',
|
|
@@ -200,7 +261,7 @@ export const EXTERNAL_SURFACE_SPECS: readonly ExternalSurfaceSpec[] = [
|
|
|
200
261
|
enabledFieldId: 'external-services.ntfy',
|
|
201
262
|
enabledConfigKey: 'surfaces.ntfy.enabled',
|
|
202
263
|
label: 'ntfy surface',
|
|
203
|
-
hint: '
|
|
264
|
+
hint: 'Configure ntfy chat, agent, remote-session, and notification delivery topics.',
|
|
204
265
|
defaultEnabled: (snapshot) => snapshot?.config.surfaces.ntfy.enabled ?? false,
|
|
205
266
|
fields: [
|
|
206
267
|
{
|
|
@@ -212,12 +273,39 @@ export const EXTERNAL_SURFACE_SPECS: readonly ExternalSurfaceSpec[] = [
|
|
|
212
273
|
placeholder: 'https://ntfy.sh',
|
|
213
274
|
defaultValue: (snapshot) => snapshot?.config.surfaces.ntfy.baseUrl ?? 'https://ntfy.sh',
|
|
214
275
|
},
|
|
276
|
+
{
|
|
277
|
+
id: 'external-services.ntfy.chat-topic',
|
|
278
|
+
configKey: 'surfaces.ntfy.chatTopic',
|
|
279
|
+
kind: 'text',
|
|
280
|
+
label: 'ntfy chat topic',
|
|
281
|
+
hint: 'Messages sent here attach to the active terminal TUI session and reply back to ntfy.',
|
|
282
|
+
placeholder: GOODVIBES_NTFY_CHAT_TOPIC,
|
|
283
|
+
defaultValue: (snapshot) => snapshot?.config.surfaces.ntfy.chatTopic ?? GOODVIBES_NTFY_CHAT_TOPIC,
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
id: 'external-services.ntfy.agent-topic',
|
|
287
|
+
configKey: 'surfaces.ntfy.agentTopic',
|
|
288
|
+
kind: 'text',
|
|
289
|
+
label: 'ntfy agent topic',
|
|
290
|
+
hint: 'Messages sent here start agent work attached to the active TUI session.',
|
|
291
|
+
placeholder: GOODVIBES_NTFY_AGENT_TOPIC,
|
|
292
|
+
defaultValue: (snapshot) => snapshot?.config.surfaces.ntfy.agentTopic ?? GOODVIBES_NTFY_AGENT_TOPIC,
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
id: 'external-services.ntfy.remote-topic',
|
|
296
|
+
configKey: 'surfaces.ntfy.remoteTopic',
|
|
297
|
+
kind: 'text',
|
|
298
|
+
label: 'ntfy daemon-only remote topic',
|
|
299
|
+
hint: 'Messages sent here start an ntfy remote session in the daemon and do not appear in the TUI.',
|
|
300
|
+
placeholder: GOODVIBES_NTFY_REMOTE_TOPIC,
|
|
301
|
+
defaultValue: (snapshot) => snapshot?.config.surfaces.ntfy.remoteTopic ?? GOODVIBES_NTFY_REMOTE_TOPIC,
|
|
302
|
+
},
|
|
215
303
|
{
|
|
216
304
|
id: 'external-services.ntfy.topic',
|
|
217
305
|
configKey: 'surfaces.ntfy.topic',
|
|
218
306
|
kind: 'text',
|
|
219
|
-
label: 'ntfy topic',
|
|
220
|
-
hint: '
|
|
307
|
+
label: 'ntfy default delivery topic',
|
|
308
|
+
hint: 'Optional outbound notification topic. It does not control chat, agent, or daemon-only remote routing.',
|
|
221
309
|
placeholder: 'goodvibes',
|
|
222
310
|
defaultValue: (snapshot) => snapshot?.config.surfaces.ntfy.topic ?? '',
|
|
223
311
|
},
|
|
@@ -227,7 +315,7 @@ export const EXTERNAL_SURFACE_SPECS: readonly ExternalSurfaceSpec[] = [
|
|
|
227
315
|
kind: 'masked',
|
|
228
316
|
label: 'ntfy token',
|
|
229
317
|
hint: 'Optional token for authenticated ntfy servers.',
|
|
230
|
-
placeholder: '
|
|
318
|
+
placeholder: 'empty for anonymous ntfy',
|
|
231
319
|
defaultValue: (snapshot) => snapshot?.config.surfaces.ntfy.token ?? '',
|
|
232
320
|
},
|
|
233
321
|
{
|
|
@@ -209,9 +209,9 @@ export function getRuntimeDerivedState(hydration: OnboardingWizardRuntimeHydrati
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
export function getOnboardingWizardBodyRows(viewportHeight: number): number {
|
|
212
|
-
return Math.max(
|
|
212
|
+
return Math.max(6, viewportHeight - 5);
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
export function getOnboardingWizardVisibleFieldCount(viewportHeight: number): number {
|
|
216
|
-
return Math.max(1, getOnboardingWizardBodyRows(viewportHeight) -
|
|
216
|
+
return Math.max(1, getOnboardingWizardBodyRows(viewportHeight) - 6);
|
|
217
217
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { OnboardingStep1CapabilityId } from '../../runtime/onboarding/index.ts';
|
|
2
2
|
import { INBOUND_EXTERNAL_SURFACE_IDS, REQUIRED_EXTERNAL_SETUP_FIELD_IDS } from './onboarding-wizard-constants.ts';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
EXTERNAL_SURFACE_SPECS,
|
|
5
|
+
getExternalSurfaceAutoStartDefaultValue,
|
|
6
|
+
getExternalSurfaceAutoStartFieldId,
|
|
7
|
+
isExternalSurfaceSelectedByDefault,
|
|
8
|
+
} from './onboarding-wizard-external-surfaces.ts';
|
|
4
9
|
import { getExternalSurfaceSpecByFieldId, normalizeText, uniqueNonEmpty } from './onboarding-wizard-helpers.ts';
|
|
5
10
|
import type { OnboardingWizardController } from './onboarding-wizard.ts';
|
|
6
11
|
|
|
@@ -124,7 +129,14 @@ export function hasSelectedInboundExternalSurface(controller: OnboardingWizardCo
|
|
|
124
129
|
if (!controller.isCapabilitySelected('external-integrations')) return false;
|
|
125
130
|
return EXTERNAL_SURFACE_SPECS.some((surface) => (
|
|
126
131
|
INBOUND_EXTERNAL_SURFACE_IDS.has(surface.id)
|
|
127
|
-
&& controller.getBooleanFieldValue(
|
|
132
|
+
&& controller.getBooleanFieldValue(
|
|
133
|
+
surface.enabledFieldId,
|
|
134
|
+
isExternalSurfaceSelectedByDefault(surface, controller.runtimeSnapshot),
|
|
135
|
+
)
|
|
136
|
+
&& controller.getStringFieldValue(
|
|
137
|
+
getExternalSurfaceAutoStartFieldId(surface),
|
|
138
|
+
getExternalSurfaceAutoStartDefaultValue(surface, controller.runtimeSnapshot),
|
|
139
|
+
) === 'yes'
|
|
128
140
|
));
|
|
129
141
|
}
|
|
130
142
|
|
|
@@ -133,7 +145,10 @@ export function isRequiredExternalSetupField(controller: OnboardingWizardControl
|
|
|
133
145
|
const surface = getExternalSurfaceSpecByFieldId(fieldId);
|
|
134
146
|
if (!surface) return false;
|
|
135
147
|
if (!controller.isCapabilitySelected('external-integrations')
|
|
136
|
-
|| !controller.getBooleanFieldValue(
|
|
148
|
+
|| !controller.getBooleanFieldValue(
|
|
149
|
+
surface.enabledFieldId,
|
|
150
|
+
isExternalSurfaceSelectedByDefault(surface, controller.runtimeSnapshot),
|
|
151
|
+
)) {
|
|
137
152
|
return false;
|
|
138
153
|
}
|
|
139
154
|
if (fieldId === 'external-services.telegram.webhook-secret') {
|
|
@@ -199,6 +214,10 @@ export function hasBootstrapCredentialPresent(controller: OnboardingWizardContro
|
|
|
199
214
|
|
|
200
215
|
export function getDefaultAdminUsername(controller: OnboardingWizardController): string {
|
|
201
216
|
const users = controller.runtimeSnapshot?.auth.snapshot.users ?? [];
|
|
217
|
+
if (controller.hasBootstrapCredentialPresent()) {
|
|
218
|
+
const existingAdmin = users.find((user) => user.roles.includes('admin'));
|
|
219
|
+
if (existingAdmin) return existingAdmin.username;
|
|
220
|
+
}
|
|
202
221
|
const candidates = controller.hasBootstrapCredentialPresent()
|
|
203
222
|
? ['goodvibes-admin', 'admin']
|
|
204
223
|
: ['admin', 'goodvibes-admin'];
|
|
@@ -72,21 +72,26 @@ export function getFieldValidationError(
|
|
|
72
72
|
if (field.kind !== 'text' && field.kind !== 'masked') return null;
|
|
73
73
|
|
|
74
74
|
const value = normalizeText(controller.getFieldValue(field) as string);
|
|
75
|
-
const required = field.required === true
|
|
75
|
+
const required = field.required === true;
|
|
76
76
|
if (required && value.length === 0) {
|
|
77
77
|
return `${step.shortLabel}: ${field.label} is required.`;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
if (field.id === 'accounts.admin-username') {
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
83
|
-
return `${step.shortLabel}: ${field.label}
|
|
81
|
+
const password = normalizeText(controller.getStringFieldValue('accounts.admin-password', ''));
|
|
82
|
+
if (!required && password.length > 0 && value.length === 0) {
|
|
83
|
+
return `${step.shortLabel}: ${field.label} is required when setting a local auth password.`;
|
|
84
84
|
}
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
const existing = controller.runtimeSnapshot?.auth.snapshot.users.find((user) => user.username === value);
|
|
86
|
+
if ((required || password.length > 0) && existing && !existing.roles.includes('admin')) {
|
|
87
|
+
return `${step.shortLabel}: ${field.label} must be an existing admin user or a new username.`;
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
if (field.id === 'accounts.admin-password' && value.length > 0 && value.length < 8) {
|
|
92
|
+
return `${step.shortLabel}: ${field.label} must be at least 8 characters.`;
|
|
93
|
+
}
|
|
94
|
+
|
|
90
95
|
if (field.kind === 'masked' && isMalformedGoodVibesSecretReferenceValue(value)) {
|
|
91
96
|
return `${step.shortLabel}: ${field.label} must be a secret value or a goodvibes://secrets/... reference.`;
|
|
92
97
|
}
|
|
@@ -325,7 +330,7 @@ export function isFieldSatisfied(controller: OnboardingWizardController, field:
|
|
|
325
330
|
if (field.kind === 'radio') return true;
|
|
326
331
|
|
|
327
332
|
if (field.kind === 'text' || field.kind === 'masked') {
|
|
328
|
-
const required = field.required === true
|
|
333
|
+
const required = field.required === true;
|
|
329
334
|
if (!required) return true;
|
|
330
335
|
return normalizeText(controller.getFieldValue(field) as string).length > 0;
|
|
331
336
|
}
|