feat!: scope per-app config and settings by environment
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m27s
CI / docker (push) Successful in 1m10s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 1m40s
SonarQube / sonarqube (push) Successful in 4m29s

BREAKING: wipe dev PostgreSQL before deploying — V1 checksum changes.
Agents must now send environmentId on registration (400 if missing).

Two tables previously keyed on app name alone caused cross-environment
data bleed: writing config for (app=X, env=dev) would overwrite the row
used by (app=X, env=prod) agents, and agent startup fetches ignored env
entirely.

- V1 schema: application_config and app_settings are now PK (app, env).
- Repositories: env-keyed finders/saves; env is the authoritative column,
  stamped on the stored JSON so the row agrees with itself.
- ApplicationConfigController.getConfig is dual-mode — AGENT role uses
  JWT env claim (agents cannot spoof env); non-agent callers provide env
  via ?environment= query param.
- AppSettingsController endpoints now require ?environment=.
- SensitiveKeysAdminController fan-out iterates (app, env) slices so each
  env gets its own merged keys.
- DiagramController ingestion stamps env on TaggedDiagram; ClickHouse
  route_diagrams INSERT + findProcessorRouteMapping are env-scoped.
- AgentRegistrationController: environmentId is required on register;
  removed all "default" fallbacks from register/refresh/heartbeat auto-heal.
- UI hooks (useApplicationConfig, useProcessorRouteMapping, useAppSettings,
  useAllAppSettings, useUpdateAppSettings) take env, wired to
  useEnvironmentStore at all call sites.
- New ConfigEnvIsolationIT covers env-isolation for both repositories.

Plan in docs/superpowers/plans/2026-04-16-environment-scoping.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-16 22:25:21 +02:00
parent c272ac6c24
commit 9b1ef51d77
33 changed files with 573 additions and 193 deletions

View File

@@ -77,7 +77,7 @@ export default function AppConfigDetailPage() {
const navigate = useNavigate();
const { toast } = useToast();
const selectedEnv = useEnvironmentStore((s) => s.environment);
const { data: config, isLoading } = useApplicationConfig(appId);
const { data: config, isLoading } = useApplicationConfig(appId, selectedEnv);
const updateConfig = useUpdateApplicationConfig();
const { data: catalog } = useCatalog();

View File

@@ -164,7 +164,7 @@ export default function AgentHealth() {
const { toast } = useToast();
const selectedEnv = useEnvironmentStore((s) => s.environment);
const { data: agents } = useAgents(undefined, appId, selectedEnv);
const { data: appConfig } = useApplicationConfig(appId);
const { data: appConfig } = useApplicationConfig(appId, selectedEnv);
const updateConfig = useUpdateApplicationConfig();
const isAdmin = useIsAdmin();

View File

@@ -941,11 +941,12 @@ interface RouteRecordingRow { id: string; routeId: string; recording: boolean; }
function ConfigSubTab({ app, environment }: { app: App; environment?: Environment }) {
const { toast } = useToast();
const navigate = useNavigate();
const { data: agentConfig } = useApplicationConfig(app.slug);
const envSlug = environment?.slug;
const { data: agentConfig } = useApplicationConfig(app.slug, envSlug);
const updateAgentConfig = useUpdateApplicationConfig();
const updateContainerConfig = useUpdateContainerConfig();
const { data: catalog } = useCatalog();
const { data: processorToRoute = {} } = useProcessorRouteMapping(app.slug);
const { data: processorToRoute = {} } = useProcessorRouteMapping(app.slug, envSlug);
const isProd = environment?.production ?? false;
const [editing, setEditing] = useState(false);
const [configTab, setConfigTab] = useState<'monitoring' | 'resources' | 'variables' | 'traces' | 'recording'>('monitoring');

View File

@@ -305,7 +305,7 @@ export default function DashboardL1() {
const { data: timeseriesByApp } = useTimeseriesByApp(timeFrom, timeTo, selectedEnv);
const { data: topErrors } = useTopErrors(timeFrom, timeTo, undefined, undefined, selectedEnv);
const { data: punchcardData } = usePunchcard(undefined, selectedEnv);
const { data: allAppSettings } = useAllAppSettings();
const { data: allAppSettings } = useAllAppSettings(selectedEnv);
// Build settings lookup map
const settingsMap = useMemo(() => {

View File

@@ -287,7 +287,7 @@ export default function DashboardL2() {
const { data: timeseriesByRoute } = useTimeseriesByRoute(timeFrom, timeTo, appId, selectedEnv);
const { data: errors } = useTopErrors(timeFrom, timeTo, appId, undefined, selectedEnv);
const { data: punchcardData } = usePunchcard(appId, selectedEnv);
const { data: appSettings } = useAppSettings(appId);
const { data: appSettings } = useAppSettings(appId, selectedEnv);
const slaThresholdMs = appSettings?.slaThresholdMs ?? 300;

View File

@@ -261,7 +261,7 @@ export default function DashboardL3() {
const { data: processorMetrics } = useProcessorMetrics(routeId ?? null, appId, selectedEnv);
const { data: topErrors } = useTopErrors(timeFrom, timeTo, appId, routeId, selectedEnv);
const { data: diagramLayout } = useDiagramByRoute(appId, routeId);
const { data: appSettings } = useAppSettings(appId);
const { data: appSettings } = useAppSettings(appId, selectedEnv);
const slaThresholdMs = appSettings?.slaThresholdMs ?? 300;

View File

@@ -209,7 +209,7 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS
}, [catalog]);
// Build nodeConfigs from app config (for TRACE/TAP badges)
const { data: appConfig } = useApplicationConfig(appId);
const { data: appConfig } = useApplicationConfig(appId, selectedEnv);
const nodeConfigs = useMemo(() => {
const map = new Map<string, NodeConfig>();
if (appConfig?.tracedProcessors) {

View File

@@ -340,7 +340,7 @@ export default function RouteDetail() {
});
// ── Application config ──────────────────────────────────────────────────────
const config = useApplicationConfig(appId);
const config = useApplicationConfig(appId, selectedEnv);
const updateConfig = useUpdateApplicationConfig();
const testExpressionMutation = useTestExpression();