nuget reorganization
This commit is contained in:
27
src/Web/StellaOps.Web/docs/HelmReadiness.md
Normal file
27
src/Web/StellaOps.Web/docs/HelmReadiness.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Helm Readiness & Probes
|
||||
|
||||
This app serves static health endpoints for platform probes:
|
||||
|
||||
- `/assets/health/liveness.json`
|
||||
- `/assets/health/readiness.json`
|
||||
- `/assets/health/version.json`
|
||||
|
||||
These are packaged with the Angular build. Configure Helm/Nginx to route the probes directly to the web pod.
|
||||
|
||||
## Suggested Helm values
|
||||
|
||||
```yaml
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /assets/health/liveness.json
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /assets/health/readiness.json
|
||||
port: http
|
||||
```
|
||||
|
||||
## Updating
|
||||
|
||||
- Edit the JSON under `src/assets/health/*.json` for environment-specific readiness details.
|
||||
- Run `npm run build` (or CI pipeline) to bake the files into the image.
|
||||
@@ -1,6 +1,16 @@
|
||||
<div class="app-shell">
|
||||
<header class="app-header">
|
||||
<div class="app-brand">StellaOps Dashboard</div>
|
||||
<div class="app-shell">
|
||||
<section
|
||||
class="quickstart-banner"
|
||||
*ngIf="quickstartEnabled()"
|
||||
aria-label="Quickstart mode active"
|
||||
>
|
||||
<div>
|
||||
QUICKSTART MODE is enabled. Configuration and data shown are for demo/offline
|
||||
setup. See the <a routerLink="/welcome">welcome</a> page for details.
|
||||
</div>
|
||||
</section>
|
||||
<header class="app-header">
|
||||
<div class="app-brand">StellaOps Dashboard</div>
|
||||
<nav class="app-nav">
|
||||
<a routerLink="/console/profile" routerLinkActive="active">
|
||||
Console Profile
|
||||
@@ -11,10 +21,13 @@
|
||||
<a routerLink="/scans/scan-verified-001" routerLinkActive="active">
|
||||
Scan Detail
|
||||
</a>
|
||||
<a routerLink="/notify" routerLinkActive="active">
|
||||
Notify
|
||||
</a>
|
||||
</nav>
|
||||
<a routerLink="/notify" routerLinkActive="active">
|
||||
Notify
|
||||
</a>
|
||||
<a routerLink="/welcome" routerLinkActive="active">
|
||||
Welcome
|
||||
</a>
|
||||
</nav>
|
||||
<div class="app-auth">
|
||||
<ng-container *ngIf="isAuthenticated(); else signIn">
|
||||
<span class="app-user" aria-live="polite">{{ displayName() }}</span>
|
||||
|
||||
@@ -7,11 +7,25 @@
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.app-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.quickstart-banner {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
border-bottom: 1px solid #fcd34d;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.app-header {
|
||||
display: flex;
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
||||
|
||||
import { AuthorityAuthService } from './core/auth/authority-auth.service';
|
||||
import { AuthSessionStore } from './core/auth/auth-session.store';
|
||||
import { ConsoleSessionStore } from './core/console/console-session.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterOutlet, RouterLink, RouterLinkActive],
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
||||
|
||||
import { AuthorityAuthService } from './core/auth/authority-auth.service';
|
||||
import { AuthSessionStore } from './core/auth/auth-session.store';
|
||||
import { ConsoleSessionStore } from './core/console/console-session.store';
|
||||
import { AppConfigService } from './core/config/app-config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterOutlet, RouterLink, RouterLinkActive],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppComponent {
|
||||
private readonly router = inject(Router);
|
||||
private readonly auth = inject(AuthorityAuthService);
|
||||
private readonly sessionStore = inject(AuthSessionStore);
|
||||
private readonly consoleStore = inject(ConsoleSessionStore);
|
||||
|
||||
readonly status = this.sessionStore.status;
|
||||
readonly identity = this.sessionStore.identity;
|
||||
readonly subjectHint = this.sessionStore.subjectHint;
|
||||
readonly isAuthenticated = this.sessionStore.isAuthenticated;
|
||||
private readonly router = inject(Router);
|
||||
private readonly auth = inject(AuthorityAuthService);
|
||||
private readonly sessionStore = inject(AuthSessionStore);
|
||||
private readonly consoleStore = inject(ConsoleSessionStore);
|
||||
private readonly config = inject(AppConfigService);
|
||||
|
||||
readonly status = this.sessionStore.status;
|
||||
readonly identity = this.sessionStore.identity;
|
||||
readonly subjectHint = this.sessionStore.subjectHint;
|
||||
readonly isAuthenticated = this.sessionStore.isAuthenticated;
|
||||
readonly activeTenant = this.consoleStore.selectedTenantId;
|
||||
readonly freshAuthSummary = computed(() => {
|
||||
const token = this.consoleStore.tokenInfo();
|
||||
@@ -49,14 +51,18 @@ export class AppComponent {
|
||||
if (identity?.email) {
|
||||
return identity.email;
|
||||
}
|
||||
const hint = this.subjectHint();
|
||||
return hint ?? 'anonymous';
|
||||
});
|
||||
|
||||
onSignIn(): void {
|
||||
const returnUrl = this.router.url === '/' ? undefined : this.router.url;
|
||||
void this.auth.beginLogin(returnUrl);
|
||||
}
|
||||
const hint = this.subjectHint();
|
||||
return hint ?? 'anonymous';
|
||||
});
|
||||
|
||||
readonly quickstartEnabled = computed(
|
||||
() => this.config.config.quickstartMode ?? false
|
||||
);
|
||||
|
||||
onSignIn(): void {
|
||||
const returnUrl = this.router.url === '/' ? undefined : this.router.url;
|
||||
void this.auth.beginLogin(returnUrl);
|
||||
}
|
||||
|
||||
onSignOut(): void {
|
||||
void this.auth.logout();
|
||||
|
||||
@@ -15,19 +15,26 @@ export const routes: Routes = [
|
||||
(m) => m.TrivyDbSettingsPageComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'scans/:scanId',
|
||||
loadComponent: () =>
|
||||
import('./features/scans/scan-detail-page.component').then(
|
||||
(m) => m.ScanDetailPageComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'notify',
|
||||
loadComponent: () =>
|
||||
import('./features/notify/notify-panel.component').then(
|
||||
(m) => m.NotifyPanelComponent
|
||||
),
|
||||
{
|
||||
path: 'scans/:scanId',
|
||||
loadComponent: () =>
|
||||
import('./features/scans/scan-detail-page.component').then(
|
||||
(m) => m.ScanDetailPageComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'welcome',
|
||||
loadComponent: () =>
|
||||
import('./features/welcome/welcome-page.component').then(
|
||||
(m) => m.WelcomePageComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'notify',
|
||||
loadComponent: () =>
|
||||
import('./features/notify/notify-panel.component').then(
|
||||
(m) => m.NotifyPanelComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'auth/callback',
|
||||
|
||||
@@ -35,15 +35,29 @@ export interface ApiBaseUrlConfig {
|
||||
readonly scheduler?: string;
|
||||
}
|
||||
|
||||
export interface TelemetryConfig {
|
||||
readonly otlpEndpoint?: string;
|
||||
readonly sampleRate?: number;
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
readonly authority: AuthorityConfig;
|
||||
readonly apiBaseUrls: ApiBaseUrlConfig;
|
||||
readonly telemetry?: TelemetryConfig;
|
||||
}
|
||||
|
||||
export const APP_CONFIG = new InjectionToken<AppConfig>('STELLAOPS_APP_CONFIG');
|
||||
export interface TelemetryConfig {
|
||||
readonly otlpEndpoint?: string;
|
||||
readonly sampleRate?: number;
|
||||
}
|
||||
|
||||
export interface WelcomeConfig {
|
||||
readonly title?: string;
|
||||
readonly message?: string;
|
||||
readonly docsUrl?: string;
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
readonly authority: AuthorityConfig;
|
||||
readonly apiBaseUrls: ApiBaseUrlConfig;
|
||||
readonly telemetry?: TelemetryConfig;
|
||||
/**
|
||||
* Enables quickstart banner and relaxed UX defaults for demos.
|
||||
*/
|
||||
readonly quickstartMode?: boolean;
|
||||
/**
|
||||
* Optional welcome metadata surfaced at /welcome for config discovery.
|
||||
*/
|
||||
readonly welcome?: WelcomeConfig;
|
||||
}
|
||||
|
||||
export const APP_CONFIG = new InjectionToken<AppConfig>('STELLAOPS_APP_CONFIG');
|
||||
|
||||
@@ -15,14 +15,15 @@ import {
|
||||
DPoPAlgorithm,
|
||||
} from './app-config.model';
|
||||
|
||||
const DEFAULT_CONFIG_URL = '/config.json';
|
||||
const DEFAULT_DPOP_ALG: DPoPAlgorithm = 'ES256';
|
||||
const DEFAULT_REFRESH_LEEWAY_SECONDS = 60;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AppConfigService {
|
||||
const DEFAULT_CONFIG_URL = '/config.json';
|
||||
const DEFAULT_DPOP_ALG: DPoPAlgorithm = 'ES256';
|
||||
const DEFAULT_REFRESH_LEEWAY_SECONDS = 60;
|
||||
const DEFAULT_QUICKSTART = false;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AppConfigService {
|
||||
private readonly configSignal = signal<AppConfig | null>(null);
|
||||
private readonly authoritySignal = computed<AuthorityConfig | null>(() => {
|
||||
const config = this.configSignal();
|
||||
@@ -80,20 +81,21 @@ export class AppConfigService {
|
||||
return response;
|
||||
}
|
||||
|
||||
private normalizeConfig(config: AppConfig): AppConfig {
|
||||
const authority = {
|
||||
...config.authority,
|
||||
dpopAlgorithms:
|
||||
config.authority.dpopAlgorithms?.length ?? 0
|
||||
private normalizeConfig(config: AppConfig): AppConfig {
|
||||
const authority = {
|
||||
...config.authority,
|
||||
dpopAlgorithms:
|
||||
config.authority.dpopAlgorithms?.length ?? 0
|
||||
? config.authority.dpopAlgorithms
|
||||
: [DEFAULT_DPOP_ALG],
|
||||
refreshLeewaySeconds:
|
||||
config.authority.refreshLeewaySeconds ?? DEFAULT_REFRESH_LEEWAY_SECONDS,
|
||||
};
|
||||
|
||||
return {
|
||||
...config,
|
||||
authority,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...config,
|
||||
authority,
|
||||
quickstartMode: config.quickstartMode ?? DEFAULT_QUICKSTART,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
|
||||
import { AppConfigService } from '../../core/config/app-config.service';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-welcome-page',
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
<section class="welcome-card">
|
||||
<h1>{{ title() }}</h1>
|
||||
<p class="message">{{ message() }}</p>
|
||||
|
||||
<dl class="config-grid">
|
||||
<div>
|
||||
<dt>Quickstart mode</dt>
|
||||
<dd>{{ quickstartEnabled() ? 'Enabled' : 'Disabled' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Authority</dt>
|
||||
<dd>{{ config().authority.issuer }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Policy API</dt>
|
||||
<dd>{{ config().apiBaseUrls.policy }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Scanner API</dt>
|
||||
<dd>{{ config().apiBaseUrls.scanner }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<a
|
||||
*ngIf="docsUrl() as docs"
|
||||
class="docs-link"
|
||||
[href]="docs"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
View deployment guide
|
||||
</a>
|
||||
</section>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
display: block;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.welcome-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 0 0 1rem;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-size: 0.85rem;
|
||||
color: #334155;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.docs-link {
|
||||
display: inline-block;
|
||||
margin-top: 0.5rem;
|
||||
color: #4338ca;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class WelcomePageComponent {
|
||||
private readonly configService = inject(AppConfigService);
|
||||
|
||||
readonly config = computed(() => this.configService.config);
|
||||
readonly quickstartEnabled = computed(
|
||||
() => this.config().quickstartMode ?? false
|
||||
);
|
||||
readonly title = computed(
|
||||
() => this.config().welcome?.title ?? 'Welcome to StellaOps'
|
||||
);
|
||||
readonly message = computed(
|
||||
() =>
|
||||
this.config().welcome?.message ??
|
||||
'This page surfaces safe deployment configuration for operators.'
|
||||
);
|
||||
readonly docsUrl = computed(() => this.config().welcome?.docsUrl);
|
||||
}
|
||||
4
src/Web/StellaOps.Web/src/assets/health/liveness.json
Normal file
4
src/Web/StellaOps.Web/src/assets/health/liveness.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": "OK",
|
||||
"timestamp": "1970-01-01T00:00:00Z"
|
||||
}
|
||||
8
src/Web/StellaOps.Web/src/assets/health/readiness.json
Normal file
8
src/Web/StellaOps.Web/src/assets/health/readiness.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"status": "OK",
|
||||
"dependencies": {
|
||||
"api": "UNKNOWN",
|
||||
"telemetry": "DISABLED"
|
||||
},
|
||||
"timestamp": "1970-01-01T00:00:00Z"
|
||||
}
|
||||
5
src/Web/StellaOps.Web/src/assets/health/version.json
Normal file
5
src/Web/StellaOps.Web/src/assets/health/version.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "0.0.0",
|
||||
"commit": "local",
|
||||
"builtAt": "1970-01-01T00:00:00Z"
|
||||
}
|
||||
@@ -12,15 +12,21 @@
|
||||
"dpopAlgorithms": ["ES256"],
|
||||
"refreshLeewaySeconds": 60
|
||||
},
|
||||
"apiBaseUrls": {
|
||||
"authority": "https://authority.local",
|
||||
"scanner": "https://scanner.local",
|
||||
"policy": "https://scanner.local",
|
||||
"concelier": "https://concelier.local",
|
||||
"attestor": "https://attestor.local"
|
||||
},
|
||||
"telemetry": {
|
||||
"otlpEndpoint": "http://localhost:4318/v1/traces",
|
||||
"sampleRate": 0.1
|
||||
}
|
||||
}
|
||||
"apiBaseUrls": {
|
||||
"authority": "https://authority.local",
|
||||
"scanner": "https://scanner.local",
|
||||
"policy": "https://scanner.local",
|
||||
"concelier": "https://concelier.local",
|
||||
"attestor": "https://attestor.local"
|
||||
},
|
||||
"telemetry": {
|
||||
"otlpEndpoint": "http://localhost:4318/v1/traces",
|
||||
"sampleRate": 0.1
|
||||
},
|
||||
"quickstartMode": true,
|
||||
"welcome": {
|
||||
"title": "StellaOps Web Quickstart",
|
||||
"message": "Local demo configuration. Replace endpoints and disable quickstart for production.",
|
||||
"docsUrl": "https://docs.stellaops.example/quickstart"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,15 +12,21 @@
|
||||
"dpopAlgorithms": ["ES256"],
|
||||
"refreshLeewaySeconds": 60
|
||||
},
|
||||
"apiBaseUrls": {
|
||||
"authority": "https://authority.example.dev",
|
||||
"scanner": "https://scanner.example.dev",
|
||||
"policy": "https://scanner.example.dev",
|
||||
"concelier": "https://concelier.example.dev",
|
||||
"attestor": "https://attestor.example.dev"
|
||||
},
|
||||
"telemetry": {
|
||||
"otlpEndpoint": "",
|
||||
"sampleRate": 0
|
||||
}
|
||||
}
|
||||
"apiBaseUrls": {
|
||||
"authority": "https://authority.example.dev",
|
||||
"scanner": "https://scanner.example.dev",
|
||||
"policy": "https://scanner.example.dev",
|
||||
"concelier": "https://concelier.example.dev",
|
||||
"attestor": "https://attestor.example.dev"
|
||||
},
|
||||
"telemetry": {
|
||||
"otlpEndpoint": "",
|
||||
"sampleRate": 0
|
||||
},
|
||||
"quickstartMode": true,
|
||||
"welcome": {
|
||||
"title": "StellaOps Web Quickstart",
|
||||
"message": "You are viewing a demo configuration. Replace endpoints with your deployment values and disable quickstart when promoting to production.",
|
||||
"docsUrl": "https://docs.stellaops.example/quickstart"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user