- | {{ formatTimestamp(event.timestamp) }} |
+ {{ formatTimestamp(event.timestamp ?? event.occurredAt) }} |
{{ event.eventType }}
@@ -182,7 +182,7 @@ import { StellaOpsScopes } from '../../../core/auth/scopes';
Timestamp:
- {{ formatTimestamp(selectedEvent.timestamp) }}
+ {{ formatTimestamp(selectedEvent.timestamp ?? selectedEvent.occurredAt) }}
Event Type:
@@ -208,7 +208,7 @@ import { StellaOpsScopes } from '../../../core/auth/scopes';
Metadata:
- {{ formatMetadata(selectedEvent.metadata) }}
+ {{ formatMetadata(selectedEvent.metadata ?? {}) }}
diff --git a/src/Web/StellaOps.Web/src/app/features/policy-studio/editor/monaco-loader.service.ts b/src/Web/StellaOps.Web/src/app/features/policy-studio/editor/monaco-loader.service.ts
index 2674c17e6..e663e6879 100644
--- a/src/Web/StellaOps.Web/src/app/features/policy-studio/editor/monaco-loader.service.ts
+++ b/src/Web/StellaOps.Web/src/app/features/policy-studio/editor/monaco-loader.service.ts
@@ -46,26 +46,21 @@ export class MonacoLoaderService {
/**
* Configure Monaco web workers for language services.
* Ensures deterministic, offline-friendly loading (no CDN usage).
+ *
+ * OPTIMIZATION: Only load editor core + JSON worker.
+ * Removed CSS/HTML/TypeScript workers to save ~3-4MB.
+ * Stella DSL only needs basic editor + JSON-like validation.
*/
private async configureWorkers(monaco: MonacoNamespace): Promise {
- const [editorWorker, cssWorker, htmlWorker, jsonWorker, tsWorker] = await Promise.all([
+ // Only load essential workers - saves ~3-4MB
+ const [editorWorker, jsonWorker] = await Promise.all([
import('monaco-editor/esm/vs/editor/editor.worker?worker'),
- import('monaco-editor/esm/vs/language/css/css.worker?worker'),
- import('monaco-editor/esm/vs/language/html/html.worker?worker'),
import('monaco-editor/esm/vs/language/json/json.worker?worker'),
- import('monaco-editor/esm/vs/language/typescript/ts.worker?worker'),
]);
+ // Minimal worker mapping - all non-JSON languages use base editor worker
const workerByLabel: Record Worker> = {
json: () => new (jsonWorker as any).default(),
- css: () => new (cssWorker as any).default(),
- scss: () => new (cssWorker as any).default(),
- less: () => new (cssWorker as any).default(),
- html: () => new (htmlWorker as any).default(),
- handlebars: () => new (htmlWorker as any).default(),
- razor: () => new (htmlWorker as any).default(),
- javascript: () => new (tsWorker as any).default(),
- typescript: () => new (tsWorker as any).default(),
default: () => new (editorWorker as any).default(),
};
diff --git a/src/Web/StellaOps.Web/src/styles.scss b/src/Web/StellaOps.Web/src/styles.scss
index dd9e6914e..d1e34ec58 100644
--- a/src/Web/StellaOps.Web/src/styles.scss
+++ b/src/Web/StellaOps.Web/src/styles.scss
@@ -1,4 +1,8 @@
+// Design system imports
@import './styles/tokens/motion';
+@import './styles/mixins';
+
+// Monaco Editor styles (lazy-loaded with editor)
@import 'monaco-editor/min/vs/editor/editor.main.css';
/* Global motion helpers */
diff --git a/src/Web/StellaOps.Web/src/styles/_mixins.scss b/src/Web/StellaOps.Web/src/styles/_mixins.scss
new file mode 100644
index 000000000..6f3ca1393
--- /dev/null
+++ b/src/Web/StellaOps.Web/src/styles/_mixins.scss
@@ -0,0 +1,457 @@
+// =============================================================================
+// Shared SCSS Mixins - Bundle Optimization
+// =============================================================================
+// These mixins consolidate common patterns to reduce component CSS size.
+// Import with: @use 'styles/mixins' as m;
+// =============================================================================
+
+// -----------------------------------------------------------------------------
+// Design Tokens (CSS Custom Properties fallbacks)
+// -----------------------------------------------------------------------------
+$color-surface: #ffffff !default;
+$color-surface-secondary: #f8fafc !default;
+$color-border: #e2e8f0 !default;
+$color-text-primary: #1e293b !default;
+$color-text-secondary: #64748b !default;
+$color-text-muted: #94a3b8 !default;
+$color-brand: #4f46e5 !default;
+$color-brand-light: rgba(79, 70, 229, 0.1) !default;
+
+// Severity colors
+$severity-critical: #dc2626 !default;
+$severity-high: #ea580c !default;
+$severity-medium: #f59e0b !default;
+$severity-low: #22c55e !default;
+$severity-info: #3b82f6 !default;
+
+// Spacing
+$spacing-xs: 0.25rem !default;
+$spacing-sm: 0.5rem !default;
+$spacing-md: 1rem !default;
+$spacing-lg: 1.5rem !default;
+$spacing-xl: 2rem !default;
+
+// Border radius
+$radius-sm: 0.375rem !default;
+$radius-md: 0.5rem !default;
+$radius-lg: 0.75rem !default;
+$radius-xl: 1rem !default;
+
+// Shadows
+$shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05) !default;
+$shadow-md: 0 1px 3px rgba(0, 0, 0, 0.1) !default;
+$shadow-lg: 0 4px 6px rgba(0, 0, 0, 0.1) !default;
+
+// -----------------------------------------------------------------------------
+// Layout Mixins
+// -----------------------------------------------------------------------------
+
+/// Flex container with common settings
+@mixin flex-row($gap: $spacing-md, $align: center) {
+ display: flex;
+ align-items: $align;
+ gap: $gap;
+}
+
+@mixin flex-col($gap: $spacing-md) {
+ display: flex;
+ flex-direction: column;
+ gap: $gap;
+}
+
+@mixin flex-between {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+/// Grid with auto-fit columns
+@mixin auto-grid($min-width: 200px, $gap: $spacing-md) {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax($min-width, 1fr));
+ gap: $gap;
+}
+
+// -----------------------------------------------------------------------------
+// Component Base Mixins
+// -----------------------------------------------------------------------------
+
+/// Card/Panel base styling
+@mixin card-base($padding: $spacing-md) {
+ padding: $padding;
+ background: $color-surface;
+ border-radius: $radius-lg;
+ border: 1px solid $color-border;
+ box-shadow: $shadow-md;
+}
+
+/// Panel with header section
+@mixin panel-base {
+ @include card-base($spacing-lg);
+}
+
+/// Stat card styling
+@mixin stat-card {
+ @include flex-col($spacing-xs);
+ align-items: center;
+ @include card-base;
+}
+
+/// Toolbar container
+@mixin toolbar {
+ @include flex-row;
+ flex-wrap: wrap;
+ @include card-base;
+}
+
+// -----------------------------------------------------------------------------
+// Form Element Mixins
+// -----------------------------------------------------------------------------
+
+/// Base input styling
+@mixin input-base {
+ padding: $spacing-sm $spacing-md;
+ border: 1px solid $color-border;
+ border-radius: $radius-md;
+ font-size: 0.875rem;
+ background: $color-surface;
+ outline: none;
+ transition: border-color 0.15s, box-shadow 0.15s;
+
+ &:focus {
+ border-color: $color-brand;
+ box-shadow: 0 0 0 3px $color-brand-light;
+ }
+
+ &::placeholder {
+ color: $color-text-muted;
+ }
+}
+
+/// Select dropdown
+@mixin select-base {
+ @include input-base;
+ cursor: pointer;
+ min-width: 140px;
+}
+
+/// Search box container
+@mixin search-box($max-width: 400px) {
+ display: flex;
+ flex: 1;
+ min-width: 250px;
+ max-width: $max-width;
+ position: relative;
+}
+
+/// Filter group (label + control)
+@mixin filter-group {
+ @include flex-col($spacing-xs);
+
+ label,
+ &__label {
+ font-size: 0.75rem;
+ color: $color-text-secondary;
+ font-weight: 500;
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Typography Mixins
+// -----------------------------------------------------------------------------
+
+@mixin heading-lg {
+ margin: 0;
+ font-size: 1.75rem;
+ font-weight: 600;
+ color: $color-text-primary;
+}
+
+@mixin heading-md {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: $color-text-primary;
+}
+
+@mixin text-secondary {
+ color: $color-text-secondary;
+ font-size: 0.875rem;
+}
+
+@mixin text-label {
+ font-size: 0.75rem;
+ color: $color-text-secondary;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+@mixin text-mono {
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: 0.8125rem;
+}
+
+// -----------------------------------------------------------------------------
+// Badge/Chip Mixins
+// -----------------------------------------------------------------------------
+
+/// Base badge styling
+@mixin badge-base($bg: $color-surface-secondary, $color: $color-text-primary) {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.125rem 0.5rem;
+ border-radius: 9999px;
+ font-size: 0.75rem;
+ font-weight: 500;
+ background: $bg;
+ color: $color;
+}
+
+/// Severity badge with color variants
+@mixin severity-badge($severity) {
+ $colors: (
+ 'critical': $severity-critical,
+ 'high': $severity-high,
+ 'medium': $severity-medium,
+ 'low': $severity-low,
+ 'info': $severity-info,
+ );
+
+ $color: map-get($colors, $severity);
+ @if $color {
+ @include badge-base(rgba($color, 0.1), $color);
+ border: 1px solid rgba($color, 0.2);
+ }
+}
+
+/// Generate all severity badge classes
+@mixin severity-badge-variants {
+ &--critical,
+ &.critical {
+ @include severity-badge('critical');
+ }
+ &--high,
+ &.high {
+ @include severity-badge('high');
+ }
+ &--medium,
+ &.medium {
+ @include severity-badge('medium');
+ }
+ &--low,
+ &.low {
+ @include severity-badge('low');
+ }
+ &--info,
+ &.info {
+ @include severity-badge('info');
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Message/Alert Mixins
+// -----------------------------------------------------------------------------
+
+@mixin message-base {
+ padding: $spacing-md;
+ border-radius: $radius-md;
+ font-size: 0.875rem;
+}
+
+@mixin message-info {
+ @include message-base;
+ background: #e0f2fe;
+ color: #0369a1;
+ border: 1px solid #7dd3fc;
+}
+
+@mixin message-success {
+ @include message-base;
+ background: #dcfce7;
+ color: #166534;
+ border: 1px solid #86efac;
+}
+
+@mixin message-warning {
+ @include message-base;
+ background: #fef3c7;
+ color: #92400e;
+ border: 1px solid #fcd34d;
+}
+
+@mixin message-error {
+ @include message-base;
+ background: #fef2f2;
+ color: #991b1b;
+ border: 1px solid #fca5a5;
+}
+
+// -----------------------------------------------------------------------------
+// Button Mixins
+// -----------------------------------------------------------------------------
+
+@mixin btn-base {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: $spacing-sm;
+ padding: $spacing-sm $spacing-md;
+ border: none;
+ border-radius: $radius-md;
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background-color 0.15s, opacity 0.15s;
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+}
+
+@mixin btn-primary {
+ @include btn-base;
+ background: $color-brand;
+ color: white;
+
+ &:hover:not(:disabled) {
+ background: darken($color-brand, 8%);
+ }
+}
+
+@mixin btn-secondary {
+ @include btn-base;
+ background: $color-surface-secondary;
+ color: $color-text-primary;
+ border: 1px solid $color-border;
+
+ &:hover:not(:disabled) {
+ background: darken($color-surface-secondary, 3%);
+ }
+}
+
+@mixin btn-ghost {
+ @include btn-base;
+ background: transparent;
+ color: $color-text-secondary;
+
+ &:hover:not(:disabled) {
+ background: $color-surface-secondary;
+ color: $color-text-primary;
+ }
+}
+
+@mixin btn-icon {
+ @include btn-ghost;
+ padding: $spacing-sm;
+ border-radius: $radius-md;
+}
+
+// -----------------------------------------------------------------------------
+// Table Mixins
+// -----------------------------------------------------------------------------
+
+@mixin table-base {
+ width: 100%;
+ border-collapse: collapse;
+ background: $color-surface;
+ border-radius: $radius-lg;
+ overflow: hidden;
+}
+
+@mixin table-header {
+ background: $color-surface-secondary;
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: $color-text-secondary;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+@mixin table-cell {
+ padding: $spacing-md;
+ border-bottom: 1px solid $color-border;
+ font-size: 0.875rem;
+}
+
+@mixin table-row-hover {
+ &:hover {
+ background: $color-surface-secondary;
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Scrollbar Mixins
+// -----------------------------------------------------------------------------
+
+@mixin custom-scrollbar($width: 8px) {
+ &::-webkit-scrollbar {
+ width: $width;
+ height: $width;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: $color-border;
+ border-radius: $width;
+
+ &:hover {
+ background: $color-text-muted;
+ }
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Utility Mixins
+// -----------------------------------------------------------------------------
+
+/// Truncate text with ellipsis
+@mixin truncate($max-width: 100%) {
+ max-width: $max-width;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/// Visually hidden but accessible
+@mixin visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+/// Loading skeleton
+@mixin skeleton {
+ background: linear-gradient(90deg, $color-surface-secondary 25%, $color-border 50%, $color-surface-secondary 75%);
+ background-size: 200% 100%;
+ animation: skeleton-loading 1.5s infinite;
+ border-radius: $radius-sm;
+}
+
+@keyframes skeleton-loading {
+ 0% {
+ background-position: 200% 0;
+ }
+ 100% {
+ background-position: -200% 0;
+ }
+}
+
+/// Empty state container
+@mixin empty-state {
+ @include flex-col;
+ align-items: center;
+ justify-content: center;
+ padding: $spacing-xl * 2;
+ color: $color-text-muted;
+ text-align: center;
+}
diff --git a/src/__Libraries/StellaOps.Infrastructure.Postgres/AGENTS.md b/src/__Libraries/StellaOps.Infrastructure.Postgres/AGENTS.md
index 85aafa450..e3cd3b951 100644
--- a/src/__Libraries/StellaOps.Infrastructure.Postgres/AGENTS.md
+++ b/src/__Libraries/StellaOps.Infrastructure.Postgres/AGENTS.md
@@ -3,13 +3,13 @@
## Roles
- Backend engineer: maintain the shared PostgreSQL infrastructure primitives (DataSourceBase, RepositoryBase, MigrationRunner, options/DI helpers).
- QA automation: own Postgres Testcontainers coverage, tenant-context/RLS checks, and migration idempotency tests.
-- DevOps liaison: keep provisioning values in `ops/devops/postgres` aligned with library defaults (timeouts, schema names, TLS, pooling).
+- DevOps liaison: keep provisioning values in `devops/database/postgres` aligned with library defaults (timeouts, schema names, TLS, pooling).
## Required Reading
- docs/db/README.md, SPECIFICATION.md, RULES.md, VERIFICATION.md, CONVERSION_PLAN.md
- docs/modules/platform/architecture-overview.md
- docs/airgap/airgap-mode.md
-- ops/devops/AGENTS.md (DevOps working agreement)
+- devops/AGENTS.md (DevOps working agreement)
## Working Directory & Scope
- Primary: `src/__Libraries/StellaOps.Infrastructure.Postgres`
@@ -28,5 +28,5 @@
- Treat analyzer warnings as errors; ensure nullable enabled and `LangVersion` follows repo default.
## Handoff Notes
-- Align configuration defaults with the provisioning values under `ops/devops/postgres` (ports, pool sizes, SSL/TLS).
+- Align configuration defaults with the provisioning values under `devops/database/postgres` (ports, pool sizes, SSL/TLS).
- Update this AGENTS file whenever connection/session rules or provisioning defaults change; record updates in the sprint Execution Log.
diff --git a/src/__Tests/__Datasets/seed-data/cert-bund/README.md b/src/__Tests/__Datasets/seed-data/cert-bund/README.md
index b6ed566eb..2683a1599 100644
--- a/src/__Tests/__Datasets/seed-data/cert-bund/README.md
+++ b/src/__Tests/__Datasets/seed-data/cert-bund/README.md
@@ -13,7 +13,7 @@ portal.
## Recommended layout
```
-seed-data/cert-bund/
+src/__Tests/__Datasets/seed-data/cert-bund/
├── search/ # paginated search JSON files
│ ├── certbund-search-page-00.json
│ └── …
@@ -36,7 +36,7 @@ Run the helper under `src/Tools/` to capture fresh snapshots or regenerate
the manifest:
```
-python src/Tools/certbund_offline_snapshot.py --output seed-data/cert-bund
+python src/Tools/certbund_offline_snapshot.py --output src/__Tests/__Datasets/seed-data/cert-bund
```
See the connector operations guide
diff --git a/src/__Tests/__Datasets/seed-data/kisa/README.md b/src/__Tests/__Datasets/seed-data/kisa/README.md
index 2c9e80525..fee486b93 100644
--- a/src/__Tests/__Datasets/seed-data/kisa/README.md
+++ b/src/__Tests/__Datasets/seed-data/kisa/README.md
@@ -13,10 +13,10 @@ This directory contains HTML snapshots of the KISA/KNVD advisory detail pages (`
## Regeneration
```bash
-python scripts/kisa_capture_html.py --out seed-data/kisa/html
+python devops/tools/kisa_capture_html.py --out src/__Tests/__Datasets/seed-data/kisa/html
```
-(See `scripts/kisa_capture_html.py` for exact implementation; it parses the RSS feed, walks each `IDX`, and writes `IDX.html` alongside a sha256 manifest.)
+(See `devops/tools/kisa_capture_html.py` for exact implementation; it parses the RSS feed, walks each `IDX`, and writes `IDX.html` alongside a sha256 manifest.)
## sha256 manifest
|