more features checks. setup improvements

This commit is contained in:
master
2026-02-13 02:04:55 +02:00
parent 9911b7d73c
commit 9ca2de05df
675 changed files with 37550 additions and 1826 deletions

View File

@@ -9,6 +9,9 @@
}
@if (useShellLayout()) {
<app-shell></app-shell>
} @else if (isFullPageRoute()) {
<!-- Full-page routes (setup wizard): no app chrome, just the component -->
<router-outlet />
} @else {
<header class="app-header">
<a class="app-brand" routerLink="/">

View File

@@ -114,7 +114,9 @@ export class AppComponent {
startWith(this.router.url.split('?')[0])
);
private readonly currentUrl = toSignal(this.currentUrl$, { initialValue: '/' });
private readonly currentUrl = toSignal(this.currentUrl$, {
initialValue: (typeof window !== 'undefined' ? window.location.pathname : '/'),
});
readonly useShellLayout = computed(() => {
const url = this.currentUrl();
@@ -134,6 +136,12 @@ export class AppComponent {
return url.split('/').filter(s => s).length > 0;
});
/** Setup wizard gets a completely chrome-free viewport. */
readonly isFullPageRoute = computed(() => {
const url = this.currentUrl();
return url === '/setup' || url.startsWith('/setup/');
});
/** Hide navigation on setup/auth pages and when not authenticated. */
readonly showNavigation = computed(() => {
const url = this.currentUrl();

View File

@@ -67,25 +67,7 @@ import {
}
</header>
<!-- Status Banner -->
@if (step().status === 'completed') {
<div class="status-banner success">
<span class="status-icon">OK</span>
<span>This step has been completed successfully.</span>
</div>
}
@if (step().status === 'skipped') {
<div class="status-banner skipped">
<span class="status-icon">--</span>
<span>This step was skipped.</span>
</div>
}
@if (step().status === 'failed') {
<div class="status-banner error">
<span class="status-icon">!</span>
<span>{{ step().error ?? 'Step execution failed' }}</span>
</div>
}
<!-- Status is shown in the accordion row and inline test result; no banner needed -->
<!-- Dynamic Form Content -->
<div class="form-container">
@@ -168,21 +150,26 @@ import {
</section>
}
<!-- Action Buttons -->
<div class="step-actions">
<button
class="btn btn-secondary"
(click)="onTest()"
[disabled]="executing()">
{{ executing() ? 'Testing...' : 'Test Connection' }}
</button>
<button
class="btn btn-primary"
(click)="onExecute()"
[disabled]="executing()">
{{ executing() ? 'Configuring...' : (dryRunMode() ? 'Validate Configuration' : 'Apply Configuration') }}
</button>
</div>
<!-- Action Buttons (hidden when form has its own inline button) -->
@if (step().id !== 'database') {
<div class="step-actions">
<button
class="btn btn-primary"
(click)="onTest()"
[disabled]="executing()">
{{ executing() ? 'Validating...' : 'Validate & Test' }}
</button>
</div>
}
<!-- Inline test result (replaces on each click, with slide-in animation) -->
@if (testResult(); as tr) {
<div class="test-result" [class.test-result--ok]="tr.success"
[class.test-result--fail]="!tr.success">
<span class="test-result-dot"></span>
<span class="test-result-msg">{{ tr.message }}</span>
</div>
}
<!-- Authority Form Template -->
<ng-template #authorityForm>
@@ -275,8 +262,9 @@ import {
<input
id="users-superuser-password"
type="password"
[value]="getConfigValue('users.superuser.password')"
[value]="getConfigValue('users.superuser.password') || 'Admin@Stella1'"
(input)="onInputChange('users.superuser.password', $event)"
placeholder="Admin@Stella1"
/>
<span class="help-text">Must meet password policy requirements.</span>
</div>
@@ -348,8 +336,18 @@ import {
<!-- Database Form Template -->
<ng-template #databaseForm>
<div class="form-section">
<h3>PostgreSQL Connection</h3>
<p class="section-hint">Enter a connection string or individual connection parameters.</p>
<div class="form-section-head">
<div>
<h3>PostgreSQL Connection</h3>
<p class="section-hint">Enter a connection string or individual connection parameters.</p>
</div>
<button
class="btn btn-primary btn-inline-action"
(click)="onTest()"
[disabled]="executing()">
{{ executing() ? 'Validating...' : 'Validate & Test' }}
</button>
</div>
<div class="form-group">
<label for="db-connectionString">Connection String</label>
@@ -373,9 +371,9 @@ import {
<input
id="db-host"
type="text"
[value]="getConfigValue('database.host')"
[value]="getConfigValue('database.host') || 'db.stella-ops.local'"
(input)="onInputChange('database.host', $event)"
placeholder="localhost"
placeholder="db.stella-ops.local"
/>
</div>
<div class="form-group form-group-small">
@@ -389,26 +387,25 @@ import {
</div>
</div>
<div class="form-group">
<label for="db-database">Database*</label>
<input
id="db-database"
type="text"
[value]="getConfigValue('database.database')"
(input)="onInputChange('database.database', $event)"
placeholder="stellaops"
/>
</div>
<div class="form-row">
<div class="form-row form-row--3col">
<div class="form-group">
<label for="db-database">Database*</label>
<input
id="db-database"
type="text"
[value]="getConfigValue('database.database') || 'stellaops_platform'"
(input)="onInputChange('database.database', $event)"
placeholder="stellaops_platform"
/>
</div>
<div class="form-group">
<label for="db-user">Username*</label>
<input
id="db-user"
type="text"
[value]="getConfigValue('database.user')"
[value]="getConfigValue('database.user') || 'stellaops'"
(input)="onInputChange('database.user', $event)"
placeholder="postgres"
placeholder="stellaops"
/>
</div>
<div class="form-group">
@@ -416,7 +413,7 @@ import {
<input
id="db-password"
type="password"
[value]="getConfigValue('database.password')"
[value]="getConfigValue('database.password') || 'stellaops'"
(input)="onInputChange('database.password', $event)"
/>
</div>
@@ -463,7 +460,7 @@ import {
type="text"
[value]="getConfigValue('cache.host')"
(input)="onInputChange('cache.host', $event)"
placeholder="localhost"
placeholder="cache.stella-ops.local"
/>
</div>
<div class="form-group form-group-small">
@@ -1718,7 +1715,7 @@ import {
`,
styles: [`
.step-content {
max-width: 700px;
max-width: 100%;
}
.step-header {
@@ -1852,7 +1849,7 @@ import {
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #1976d2;
border-color: #D4922A;
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.1);
}
@@ -1944,12 +1941,12 @@ import {
}
.provider-card:hover {
border-color: #1976d2;
border-color: #D4922A;
}
.provider-card.selected {
border-color: #1976d2;
background: #e3f2fd;
border-color: #D4922A;
background: #FFF9ED;
}
.provider-name {
@@ -2019,7 +2016,7 @@ import {
width: 16px;
height: 16px;
border: 2px solid #e0e0e0;
border-top-color: #1976d2;
border-top-color: #D4922A;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@@ -2049,6 +2046,70 @@ import {
justify-content: flex-end;
}
/* Inline action button next to form heading */
.form-section-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.form-section-head h3 { margin: 0; }
.form-section-head .section-hint { margin: 4px 0 0; }
.btn-inline-action {
white-space: nowrap;
flex-shrink: 0;
margin-top: 2px;
}
/* Inline test result (static text below validate button) */
.test-result {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
margin: 12px 0 4px;
border-radius: 8px;
font-size: 12px;
line-height: 1.5;
animation: test-result-in 300ms cubic-bezier(.4,0,.2,1) both;
}
@keyframes test-result-in {
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
}
.test-result--ok {
background: rgba(34,197,94,.08);
border: 1px solid rgba(34,197,94,.25);
color: #166534;
}
.test-result--fail {
background: #fef2f2;
border: 1px solid #fca5a5;
color: #991b1b;
}
.test-result-dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
.test-result--ok .test-result-dot { background: #22c55e; }
.test-result--fail .test-result-dot { background: #ef4444; }
.test-result-msg {
font-weight: 600;
flex: 1;
}
/* 3-column row (Database, Username, Password) */
.form-row--3col {
display: flex;
gap: 16px;
}
.form-row--3col .form-group {
flex: 1;
}
.btn {
padding: 10px 20px;
border: 1px solid #ddd;
@@ -2069,13 +2130,15 @@ import {
}
.btn-primary {
background: #1976d2;
border-color: #1976d2;
color: var(--color-text-heading);
background: var(--color-brand-primary, #F5A623);
border-color: var(--color-brand-primary, #F5A623);
color: #fff;
box-shadow: 0 2px 8px rgba(245,166,35,.18);
}
.btn-primary:hover:not(:disabled) {
background: #1565c0;
background: var(--color-brand-primary-hover, #E09115);
box-shadow: 0 4px 14px rgba(245,166,35,.22);
}
.btn-secondary {
@@ -2097,11 +2160,11 @@ import {
align-items: center;
gap: 12px;
padding: 12px 16px;
background: #e3f2fd;
border: 1px solid #90caf9;
background: #FFF9ED;
border: 1px solid #F5D998;
border-radius: 8px;
margin-bottom: 24px;
color: #1565c0;
color: #B07820;
}
.info-icon {
@@ -2110,7 +2173,7 @@ import {
justify-content: center;
width: 24px;
height: 24px;
background: #1976d2;
background: #D4922A;
color: white;
border-radius: 50%;
font-size: 12px;
@@ -2208,9 +2271,9 @@ import {
}
.btn-add-user:hover {
border-color: #1976d2;
color: #1976d2;
background: #e3f2fd;
border-color: #D4922A;
color: #D4922A;
background: #FFF9ED;
}
.provider-config h4 {
@@ -2242,8 +2305,8 @@ import {
}
.event-rule-card.enabled {
border-color: #1976d2;
background: #e3f2fd;
border-color: #D4922A;
background: #FFF9ED;
}
.rule-header {
@@ -2277,8 +2340,8 @@ import {
}
.severity-info {
background: #e3f2fd;
color: #1565c0;
background: #FFF9ED;
color: #B07820;
}
.rule-description {
@@ -2318,8 +2381,8 @@ import {
}
.source-card.enabled {
border-color: #1976d2;
background: #e3f2fd;
border-color: #D4922A;
background: #FFF9ED;
}
.source-header {
@@ -2361,8 +2424,8 @@ import {
}
.status-card.checking {
background: #e3f2fd;
color: #1565c0;
background: #FFF9ED;
color: #B07820;
}
.status-card.success {
@@ -2398,7 +2461,7 @@ import {
width: 20px;
height: 20px;
border: 2px solid #e0e0e0;
border-top-color: #1976d2;
border-top-color: #D4922A;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@@ -2427,12 +2490,12 @@ import {
}
.pattern-card:hover {
border-color: #1976d2;
border-color: #D4922A;
}
.pattern-card.selected {
border-color: #1976d2;
background: #e3f2fd;
border-color: #D4922A;
background: #FFF9ED;
}
.pattern-name {
@@ -2475,7 +2538,7 @@ import {
display: flex;
align-items: center;
justify-content: center;
background: #1976d2;
background: #D4922A;
color: white;
border-radius: 50%;
font-size: 14px;
@@ -2509,9 +2572,9 @@ import {
}
.btn-add-env:hover {
border-color: #1976d2;
color: #1976d2;
background: #e3f2fd;
border-color: #D4922A;
color: #D4922A;
background: #FFF9ED;
}
.promotion-path {
@@ -2534,7 +2597,7 @@ import {
.path-env {
padding: 6px 12px;
background: #1976d2;
background: #D4922A;
color: white;
border-radius: 4px;
font-size: 13px;
@@ -2599,9 +2662,9 @@ import {
}
.btn-add-agent:hover {
border-color: #1976d2;
color: #1976d2;
background: #e3f2fd;
border-color: #D4922A;
color: #D4922A;
background: #FFF9ED;
}
code {
@@ -2626,8 +2689,8 @@ import {
}
.integration-instance.primary {
border-color: #1976d2;
background: #e3f2fd;
border-color: #D4922A;
background: #FFF9ED;
}
.instance-header {
@@ -2662,11 +2725,11 @@ import {
.primary-badge {
font-size: 11px;
font-weight: 500;
color: #1976d2;
color: #D4922A;
background: white;
padding: 2px 8px;
border-radius: 4px;
border: 1px solid #1976d2;
border: 1px solid #D4922A;
}
.instance-actions {
@@ -2760,9 +2823,9 @@ import {
}
.btn-add-integration:hover {
border-color: #1976d2;
color: #1976d2;
background: #e3f2fd;
border-color: #D4922A;
color: #D4922A;
background: #FFF9ED;
}
.empty-state {
@@ -2799,6 +2862,9 @@ export class StepContentComponent {
/** Whether dry-run mode is enabled */
readonly dryRunMode = input(true);
/** Test connection result (passed from parent) */
readonly testResult = input<{ success: boolean; message: string } | null>(null);
/** Emits configuration changes */
readonly configChange = output<{ key: string; value: string }>();
@@ -2851,6 +2917,45 @@ export class StepContentComponent {
readonly newScmProvider = signal<string | null>(null);
readonly newNotifyProvider = signal<string | null>(null);
/** Sensible defaults for local/development setup. */
private static readonly LOCAL_DEFAULTS: Record<string, Record<string, string>> = {
database: {
'database.host': 'db.stella-ops.local',
'database.port': '5432',
'database.database': 'stellaops_platform',
'database.user': 'stellaops',
'database.password': 'stellaops',
},
cache: {
'cache.host': 'cache.stella-ops.local',
'cache.port': '6379',
'cache.database': '0',
},
authority: {},
users: {
'users.superuser.username': 'admin',
'users.superuser.email': 'admin@stella-ops.local',
},
telemetry: {
'telemetry.otlpEndpoint': 'http://localhost:4317',
'telemetry.serviceName': 'stellaops',
},
};
/** Emit defaults for the current step if no values are set yet. */
private readonly defaultsEffect = effect(() => {
const step = this.step();
const config = this.configValues();
const defaults = StepContentComponent.LOCAL_DEFAULTS[step.id];
if (defaults) {
for (const [key, value] of Object.entries(defaults)) {
if (!config[key]) {
this.configChange.emit({ key, value });
}
}
}
});
// Enabled sources (track by source ID)
readonly enabledSources = signal<Set<string>>(new Set(['nvd', 'ghsa']));

View File

@@ -27,11 +27,9 @@ export type SetupStepId =
export type SetupCategory =
| 'Infrastructure'
| 'Security'
| 'Configuration'
| 'Integration'
| 'Observability'
| 'Data'
| 'Orchestration';
| 'Release Control Plane'
| 'Observability';
/** Status of an individual setup step */
export type SetupStepStatus =
@@ -1064,7 +1062,7 @@ export const DEFAULT_SETUP_STEPS: SetupStep[] = [
id: 'vault',
name: 'Secrets Vault',
description: 'Configure a secrets vault for secure credential storage (HashiCorp Vault, Azure Key Vault, AWS Secrets Manager, or GCP Secret Manager).',
category: 'Security',
category: 'Integration',
order: 60,
isRequired: false,
isSkippable: true,
@@ -1110,7 +1108,7 @@ export const DEFAULT_SETUP_STEPS: SetupStep[] = [
id: 'sources',
name: 'Advisory Data Sources',
description: 'Configure CVE/VEX advisory feeds (NVD, GHSA, OSV, distribution-specific feeds) for vulnerability data.',
category: 'Data',
category: 'Release Control Plane',
order: 90,
isRequired: false,
isSkippable: true,
@@ -1121,28 +1119,13 @@ export const DEFAULT_SETUP_STEPS: SetupStep[] = [
configureLaterCliCommand: 'stella config set sources.*',
skipWarning: 'CVE/VEX advisory feeds will require manual updates.',
},
// Phase 5: Observability (Optional)
{
id: 'telemetry',
name: 'OpenTelemetry',
description: 'Configure OpenTelemetry for distributed tracing, metrics, and logging.',
category: 'Observability',
order: 100,
isRequired: false,
isSkippable: true,
dependencies: [],
validationChecks: ['check.telemetry.otlp.connectivity'],
status: 'pending',
configureLaterUiPath: 'Settings → System → Telemetry',
configureLaterCliCommand: 'stella config set telemetry.*',
skipWarning: 'System observability will be limited. Tracing and metrics unavailable.',
},
// Phase 5: Notifications (Optional)
{
id: 'notify',
name: 'Notifications',
description: 'Configure notification channels (Email, Slack, Teams, Webhook) for alerts and events.',
category: 'Integration',
order: 110,
order: 100,
isRequired: false,
isSkippable: true,
dependencies: [],
@@ -1172,7 +1155,7 @@ export const DEFAULT_SETUP_STEPS: SetupStep[] = [
id: 'settingsstore',
name: 'Settings Store',
description: 'Configure an external settings store for application configuration and feature flags (Consul, etcd, Azure App Configuration, or AWS Parameter Store).',
category: 'Configuration',
category: 'Release Control Plane',
order: 130,
isRequired: false,
isSkippable: true,
@@ -1187,7 +1170,7 @@ export const DEFAULT_SETUP_STEPS: SetupStep[] = [
id: 'environments',
name: 'Deployment Environments',
description: 'Define deployment environments for release orchestration (e.g., dev, staging, production).',
category: 'Orchestration',
category: 'Release Control Plane',
order: 140,
isRequired: false,
isSkippable: true,
@@ -1201,7 +1184,7 @@ export const DEFAULT_SETUP_STEPS: SetupStep[] = [
id: 'agents',
name: 'Deployment Agents',
description: 'Register deployment agents that will execute releases to your environments.',
category: 'Orchestration',
category: 'Release Control Plane',
order: 150,
isRequired: false,
isSkippable: true,
@@ -1212,4 +1195,20 @@ export const DEFAULT_SETUP_STEPS: SetupStep[] = [
configureLaterCliCommand: 'stella agent register',
skipWarning: 'Release orchestration will not be available without registered agents.',
},
// Phase 9: Observability (Optional — last step)
{
id: 'telemetry',
name: 'OpenTelemetry',
description: 'Configure OpenTelemetry for distributed tracing, metrics, and logging.',
category: 'Observability',
order: 160,
isRequired: false,
isSkippable: true,
dependencies: [],
validationChecks: ['check.telemetry.otlp.connectivity'],
status: 'pending',
configureLaterUiPath: 'Settings → System → Telemetry',
configureLaterCliCommand: 'stella config set telemetry.*',
skipWarning: 'System observability will be limited. Tracing and metrics unavailable.',
},
];