feat(services): env doctor — surface empty vars, bad refs, unreadable secrets

GET /services/:id/env-check (read scope) reports, without exposing values:
- unresolved: ${{service.KEY}} refs that don't match a sibling service/env
- empty: variables with blank values
- unreadable: secrets whose ciphertext fails decryption (key rotation)

install.sh now generates SECRETS_KEY explicitly (was falling back to
JWT_SECRET — a rotation would orphan ciphertext). .env.example documents it.

Surfaces: CLI `services env-check`, MCP containr_services_env_check
(90 tools), OpenAPI path, and a warning banner in the service Variables
panel listing each issue.

Verified live: injected a bogus ${{nosuchservice.PORT}} var plus an
unreadable ciphertext row; the endpoint flagged both, ok=false.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-10-09 00:27:23 +02:00
1 parent dbc87bf34c
commit d135e5fd4b
13 files changed
+269 -3

No files matched your search

+11
View File
@@ -1304,6 +1304,17 @@ export const restartService = (serviceId: string) => serviceAction(serviceId, 'r
export const sleepService = (serviceId: string) => serviceAction(serviceId, 'sleep');
export const wakeService = (serviceId: string) => serviceAction(serviceId, 'wake');
export type EnvCheckResult = {
ok: boolean;
unresolved?: string[];
empty?: string[];
unreadable?: string[];
};
export async function getServiceEnvCheck(serviceId: string): Promise<EnvCheckResult> {
return requestJson<EnvCheckResult>(`/services/${serviceId}/env-check`);
}
export async function redeployService(serviceId: string): Promise<ServiceRuntime> {
const payload = await requestJson<{ runtime?: ServiceRuntime }>(`/services/${serviceId}/redeploy`, {
method: 'POST',