From 360485f556fbd5d06cf6e62813b43e27f2b98194 Mon Sep 17 00:00:00 2001
From: master <>
Date: Fri, 6 Mar 2026 00:23:59 +0200
Subject: [PATCH] qa iteration 1
---
devops/compose/docker-compose.stella-ops.yml | 3 +-
.../14-platform-environment-settings.sql | 20 +++++
.../15-platform-context-and-releases.sql | 88 +++++++++++++++++++
.../postgres-init/16-release-full-schema.sql | 55 ++++++++++++
devops/compose/router-gateway-local.json | 28 +++---
.../RouterConnectionManager.cs | 40 +++++++++
6 files changed, 222 insertions(+), 12 deletions(-)
create mode 100644 devops/compose/postgres-init/14-platform-environment-settings.sql
create mode 100644 devops/compose/postgres-init/15-platform-context-and-releases.sql
create mode 100644 devops/compose/postgres-init/16-release-full-schema.sql
diff --git a/devops/compose/docker-compose.stella-ops.yml b/devops/compose/docker-compose.stella-ops.yml
index 1f024fd8c..1ca8c6a9f 100644
--- a/devops/compose/docker-compose.stella-ops.yml
+++ b/devops/compose/docker-compose.stella-ops.yml
@@ -439,7 +439,8 @@ services:
STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__KEYPATH: "/app/etc/authority/keys/ack-token-dev.pem"
STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__WEBHOOKS__ALLOWEDHOSTS__0: "notify.stella-ops.local"
STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ESCALATION__SCOPE: "notify.escalate"
- STELLAOPS_AUTHORITY_AUTHORITY__BOOTSTRAP__ENABLED: "false"
+ STELLAOPS_AUTHORITY_AUTHORITY__BOOTSTRAP__ENABLED: "${AUTHORITY_BOOTSTRAP_ENABLED:-true}"
+ STELLAOPS_AUTHORITY_AUTHORITY__BOOTSTRAP__APIKEY: "${AUTHORITY_BOOTSTRAP_APIKEY:-stellaops-dev-bootstrap-key}"
STELLAOPS_AUTHORITY_AUTHORITY__PLUGINDIRECTORIES__0: "/app"
STELLAOPS_AUTHORITY_AUTHORITY__PLUGINS__CONFIGURATIONDIRECTORY: "/app/etc/authority/plugins"
STELLAOPS_AUTHORITY_AUTHORITY__PLUGINS__DESCRIPTORS__standard__Type: "standard"
diff --git a/devops/compose/postgres-init/14-platform-environment-settings.sql b/devops/compose/postgres-init/14-platform-environment-settings.sql
new file mode 100644
index 000000000..3d7e1e30a
--- /dev/null
+++ b/devops/compose/postgres-init/14-platform-environment-settings.sql
@@ -0,0 +1,20 @@
+-- Platform environment_settings table for setup state and runtime config overrides.
+-- Used by SetupStateDetector to determine if setup wizard has been completed.
+-- This is idempotent and safe to run on new compose databases.
+
+CREATE SCHEMA IF NOT EXISTS platform;
+
+CREATE TABLE IF NOT EXISTS platform.environment_settings (
+ key VARCHAR(256) NOT NULL,
+ value TEXT NOT NULL,
+ tenant_id VARCHAR(128) NOT NULL DEFAULT '_system',
+ updated_by VARCHAR(256) NOT NULL DEFAULT 'system',
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ PRIMARY KEY (tenant_id, key)
+);
+
+-- Mark setup as complete for fresh installs (docker-compose local dev).
+-- The setup wizard can re-run and overwrite this if needed.
+INSERT INTO platform.environment_settings (key, value, tenant_id, updated_by)
+VALUES ('SetupComplete', 'true', '_system', 'postgres-init')
+ON CONFLICT (tenant_id, key) DO NOTHING;
diff --git a/devops/compose/postgres-init/15-platform-context-and-releases.sql b/devops/compose/postgres-init/15-platform-context-and-releases.sql
new file mode 100644
index 000000000..c922002a8
--- /dev/null
+++ b/devops/compose/postgres-init/15-platform-context-and-releases.sql
@@ -0,0 +1,88 @@
+-- Platform context tables (regions, environments, preferences) and release control bundles.
+-- Required for the global context/filter UI and release management read models.
+-- Idempotent: uses IF NOT EXISTS and ON CONFLICT.
+
+CREATE SCHEMA IF NOT EXISTS platform;
+CREATE SCHEMA IF NOT EXISTS release;
+CREATE SCHEMA IF NOT EXISTS release_app;
+
+-- Helper function
+CREATE OR REPLACE FUNCTION release.update_updated_at_column()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = now();
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Context regions
+CREATE TABLE IF NOT EXISTS platform.context_regions (
+ region_id text PRIMARY KEY,
+ display_name text NOT NULL,
+ sort_order integer NOT NULL,
+ enabled boolean NOT NULL DEFAULT true
+);
+CREATE UNIQUE INDEX IF NOT EXISTS ux_platform_context_regions_sort
+ ON platform.context_regions (sort_order, region_id);
+
+-- Context environments
+CREATE TABLE IF NOT EXISTS platform.context_environments (
+ environment_id text PRIMARY KEY,
+ region_id text NOT NULL REFERENCES platform.context_regions(region_id) ON DELETE RESTRICT,
+ environment_type text NOT NULL,
+ display_name text NOT NULL,
+ sort_order integer NOT NULL,
+ enabled boolean NOT NULL DEFAULT true
+);
+CREATE INDEX IF NOT EXISTS ix_platform_context_environments_region_sort
+ ON platform.context_environments (region_id, sort_order, environment_id);
+CREATE INDEX IF NOT EXISTS ix_platform_context_environments_sort
+ ON platform.context_environments (sort_order, region_id, environment_id);
+
+-- UI context preferences (per-user filter state)
+CREATE TABLE IF NOT EXISTS platform.ui_context_preferences (
+ tenant_id text NOT NULL,
+ actor_id text NOT NULL,
+ regions text[] NOT NULL DEFAULT ARRAY[]::text[],
+ environments text[] NOT NULL DEFAULT ARRAY[]::text[],
+ time_window text NOT NULL DEFAULT '24h',
+ updated_at timestamptz NOT NULL DEFAULT now(),
+ updated_by text NOT NULL DEFAULT 'system',
+ PRIMARY KEY (tenant_id, actor_id)
+);
+CREATE INDEX IF NOT EXISTS ix_platform_ui_context_preferences_updated
+ ON platform.ui_context_preferences (updated_at DESC, tenant_id, actor_id);
+
+-- Release control bundles
+CREATE TABLE IF NOT EXISTS release.control_bundles (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ tenant_id UUID NOT NULL,
+ slug TEXT NOT NULL,
+ name TEXT NOT NULL,
+ description TEXT,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ created_by TEXT NOT NULL DEFAULT 'system',
+ CONSTRAINT uq_control_bundles_tenant_slug UNIQUE (tenant_id, slug)
+);
+CREATE INDEX IF NOT EXISTS idx_control_bundles_tenant_name
+ ON release.control_bundles (tenant_id, name, id);
+CREATE INDEX IF NOT EXISTS idx_control_bundles_tenant_updated
+ ON release.control_bundles (tenant_id, updated_at DESC, id);
+
+-- Seed demo context data for local development
+INSERT INTO platform.context_regions (region_id, display_name, sort_order, enabled)
+VALUES
+ ('us-east', 'US East', 1, true),
+ ('us-west', 'US West', 2, true),
+ ('eu-west', 'EU West', 3, true)
+ON CONFLICT (region_id) DO NOTHING;
+
+INSERT INTO platform.context_environments (environment_id, region_id, environment_type, display_name, sort_order, enabled)
+VALUES
+ ('dev', 'us-east', 'development', 'Development', 1, true),
+ ('stage', 'us-east', 'staging', 'Staging', 2, true),
+ ('prod-us-east', 'us-east', 'production', 'Production US East', 3, true),
+ ('prod-us-west', 'us-west', 'production', 'Production US West', 4, true),
+ ('prod-eu-west', 'eu-west', 'production', 'Production EU West', 5, true)
+ON CONFLICT (environment_id) DO NOTHING;
diff --git a/devops/compose/postgres-init/16-release-full-schema.sql b/devops/compose/postgres-init/16-release-full-schema.sql
new file mode 100644
index 000000000..d6b2fe8be
--- /dev/null
+++ b/devops/compose/postgres-init/16-release-full-schema.sql
@@ -0,0 +1,55 @@
+-- Release module full schema bootstrap for local dev compose.
+-- Includes schemas, tenants, integration hub, environments, release management,
+-- workflow, promotion, deployment, agents, trust/signing, and read models.
+-- All statements are idempotent (IF NOT EXISTS / ON CONFLICT).
+
+-- Shared tenants (required by release.integrations FK)
+CREATE SCHEMA IF NOT EXISTS shared;
+CREATE TABLE IF NOT EXISTS shared.tenants (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ tenant_id TEXT NOT NULL UNIQUE,
+ name TEXT NOT NULL,
+ display_name TEXT,
+ status TEXT NOT NULL DEFAULT 'active',
+ settings JSONB NOT NULL DEFAULT '{}',
+ metadata JSONB NOT NULL DEFAULT '{}',
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- Seed shared tenant for local dev
+INSERT INTO shared.tenants (tenant_id, name, display_name, status)
+VALUES ('demo-prod', 'Production', 'Demo Production', 'active')
+ON CONFLICT (tenant_id) DO NOTHING;
+
+-- Release schemas
+CREATE SCHEMA IF NOT EXISTS release;
+CREATE SCHEMA IF NOT EXISTS release_app;
+
+-- Helper function for updated_at triggers
+CREATE OR REPLACE FUNCTION release.update_updated_at_column()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = now();
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Tenant isolation function
+CREATE OR REPLACE FUNCTION release_app.require_current_tenant()
+RETURNS UUID
+LANGUAGE plpgsql STABLE SECURITY DEFINER
+AS $$
+DECLARE
+ v_tenant TEXT;
+BEGIN
+ v_tenant := current_setting('app.tenant_id', true);
+ IF v_tenant IS NULL OR v_tenant = '' THEN
+ RAISE EXCEPTION 'app.tenant_id session variable not set';
+ END IF;
+ RETURN v_tenant::UUID;
+END;
+$$;
+
+-- Analytics schema
+CREATE SCHEMA IF NOT EXISTS analytics;
diff --git a/devops/compose/router-gateway-local.json b/devops/compose/router-gateway-local.json
index f83c703dd..07b0271dd 100644
--- a/devops/compose/router-gateway-local.json
+++ b/devops/compose/router-gateway-local.json
@@ -15,10 +15,16 @@
}
},
"Routes": [
+ {
+ "Type": "ReverseProxy",
+ "Path": "/api/v1/setup",
+ "TranslatesTo": "http://platform.stella-ops.local/api/v1/setup",
+ "PreserveAuthHeaders": true
+ },
{
"Type": "Microservice",
"Path": "/api/v1/release-orchestrator",
- "TranslatesTo": "http://jobengine.stella-ops.local/api/v1/release-orchestrator",
+ "TranslatesTo": "http://orchestrator.stella-ops.local/api/v1/release-orchestrator",
"PreserveAuthHeaders": true
},
{
@@ -66,7 +72,7 @@
{
"Type": "Microservice",
"Path": "/api/v1/findings",
- "TranslatesTo": "http://findings.stella-ops.local/api/v1/findings",
+ "TranslatesTo": "http://findings-ledger.stella-ops.local/api/v1/findings",
"PreserveAuthHeaders": true
},
{
@@ -114,7 +120,7 @@
{
"Type": "Microservice",
"Path": "/api/v1/jobengine",
- "TranslatesTo": "http://jobengine.stella-ops.local/api/v1/jobengine",
+ "TranslatesTo": "http://orchestrator.stella-ops.local/api/v1/jobengine",
"PreserveAuthHeaders": true
},
{
@@ -330,7 +336,7 @@
{
"Type": "Microservice",
"Path": "/api/v1/workflows",
- "TranslatesTo": "http://jobengine.stella-ops.local/api/v1/workflows",
+ "TranslatesTo": "http://orchestrator.stella-ops.local/api/v1/workflows",
"PreserveAuthHeaders": true
},
{
@@ -348,7 +354,7 @@
{
"Type": "Microservice",
"Path": "/v1/runs",
- "TranslatesTo": "http://jobengine.stella-ops.local/v1/runs",
+ "TranslatesTo": "http://orchestrator.stella-ops.local/v1/runs",
"PreserveAuthHeaders": true
},
{
@@ -402,19 +408,19 @@
{
"Type": "Microservice",
"Path": "/api/release-orchestrator",
- "TranslatesTo": "http://jobengine.stella-ops.local/api/release-orchestrator",
+ "TranslatesTo": "http://orchestrator.stella-ops.local/api/release-orchestrator",
"PreserveAuthHeaders": true
},
{
"Type": "Microservice",
"Path": "/api/releases",
- "TranslatesTo": "http://jobengine.stella-ops.local/api/releases",
+ "TranslatesTo": "http://orchestrator.stella-ops.local/api/releases",
"PreserveAuthHeaders": true
},
{
"Type": "Microservice",
"Path": "/api/approvals",
- "TranslatesTo": "http://jobengine.stella-ops.local/api/approvals",
+ "TranslatesTo": "http://orchestrator.stella-ops.local/api/approvals",
"PreserveAuthHeaders": true
},
{
@@ -462,7 +468,7 @@
{
"Type": "Microservice",
"Path": "/api/jobengine",
- "TranslatesTo": "http://jobengine.stella-ops.local/api/jobengine",
+ "TranslatesTo": "http://orchestrator.stella-ops.local/api/jobengine",
"PreserveAuthHeaders": true
},
{
@@ -630,7 +636,7 @@
{
"Type": "Microservice",
"Path": "/findingsLedger",
- "TranslatesTo": "http://findings.stella-ops.local"
+ "TranslatesTo": "http://findings-ledger.stella-ops.local"
},
{
"Type": "Microservice",
@@ -645,7 +651,7 @@
{
"Type": "Microservice",
"Path": "/jobengine",
- "TranslatesTo": "http://jobengine.stella-ops.local"
+ "TranslatesTo": "http://orchestrator.stella-ops.local"
},
{
"Type": "Microservice",
diff --git a/src/Router/__Libraries/StellaOps.Microservice/RouterConnectionManager.cs b/src/Router/__Libraries/StellaOps.Microservice/RouterConnectionManager.cs
index faa137673..286c5233a 100644
--- a/src/Router/__Libraries/StellaOps.Microservice/RouterConnectionManager.cs
+++ b/src/Router/__Libraries/StellaOps.Microservice/RouterConnectionManager.cs
@@ -31,6 +31,14 @@ public sealed class RouterConnectionManager : IRouterConnectionManager, IDisposa
private volatile InstanceHealthStatus _currentStatus = InstanceHealthStatus.Healthy;
private int _inFlightRequestCount;
private double _errorRate;
+ private int _heartbeatCount;
+
+ ///
+ /// Number of heartbeats between periodic re-registration (HELLO re-send).
+ /// Ensures the gateway picks up the service after a gateway restart.
+ /// Default: every 30 heartbeats (~5 min at 10s intervals).
+ ///
+ private const int ReRegistrationInterval = 30;
///
public IReadOnlyList Connections => [.. _connections.Values];
@@ -306,6 +314,38 @@ public sealed class RouterConnectionManager : IRouterConnectionManager, IDisposa
{
await Task.Delay(_options.HeartbeatInterval, cancellationToken);
+ _heartbeatCount++;
+
+ // Periodically re-send HELLO to handle gateway restarts.
+ // The gateway loses all connection state on restart, and services
+ // only send HELLO once on initial connect. This ensures recovery.
+ if (_heartbeatCount % ReRegistrationInterval == 0 && _microserviceTransport is not null && _endpoints is not null)
+ {
+ try
+ {
+ var instance = new InstanceDescriptor
+ {
+ InstanceId = _options.InstanceId,
+ ServiceName = _options.ServiceName,
+ Version = _options.Version,
+ Region = _options.Region
+ };
+
+ await _microserviceTransport.ConnectAsync(
+ instance,
+ _endpoints,
+ _schemas,
+ _openApiInfo,
+ cancellationToken);
+
+ _logger.LogDebug("Periodic re-registration sent (heartbeat #{Count})", _heartbeatCount);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to send periodic re-registration");
+ }
+ }
+
// Build heartbeat payload with current status and metrics
var heartbeat = new HeartbeatPayload
{