diff --git a/src/lib/components/CodeEditor.svelte b/src/lib/components/CodeEditor.svelte
index 4f90f78..996b439 100644
--- a/src/lib/components/CodeEditor.svelte
+++ b/src/lib/components/CodeEditor.svelte
@@ -202,7 +202,7 @@
export interface VariableMarker {
name: string;
- type: 'required' | 'optional' | 'missing';
+ type: 'required' | 'optional' | 'missing' | 'invault';
value?: string; // The value provided in env vars editor
isSecret?: boolean; // Whether to mask the value
defaultValue?: string; // The default value from compose syntax (e.g., ${VAR:-default})
@@ -239,10 +239,10 @@
// Variable marker gutter icons
class VariableGutterMarker extends GutterMarker {
- type: 'required' | 'optional' | 'missing';
+ type: 'required' | 'optional' | 'missing' | 'invault';
hasValue: boolean;
- constructor(type: 'required' | 'optional' | 'missing', hasValue: boolean = false) {
+ constructor(type: 'required' | 'optional' | 'missing' | 'invault', hasValue: boolean = false) {
super();
this.type = type;
this.hasValue = hasValue;
@@ -256,6 +256,7 @@
const dot = document.createElement('span');
dot.className = `var-marker var-marker-${this.type}`;
dot.title = this.type === 'missing' ? 'Missing required variable'
+ : this.type === 'invault' ? 'Present in the bound secret provider'
: this.type === 'required' ? 'Required variable (defined)'
: 'Optional variable (has default)';
wrapper.appendChild(dot);
@@ -278,9 +279,9 @@
class VariableValueWidget extends WidgetType {
value: string;
isSecret: boolean;
- variant: 'provided' | 'default' | 'missing';
+ variant: 'provided' | 'default' | 'missing' | 'invault';
- constructor(value: string, isSecret: boolean = false, variant: 'provided' | 'default' | 'missing' = 'provided') {
+ constructor(value: string, isSecret: boolean = false, variant: 'provided' | 'default' | 'missing' | 'invault' = 'provided') {
super();
this.value = value;
this.isSecret = isSecret;
@@ -295,6 +296,11 @@
// Red MISSING badge with icon
span.innerHTML = '⚠ MISSING';
span.title = 'Required variable not defined';
+ } else if (this.variant === 'invault') {
+ // Green IN VAULT badge - the key currently exists in the bound secret
+ // provider (live probe). Inline lucide KeyRound SVG, currentColor.
+ span.innerHTML = 'IN VAULT';
+ span.title = 'Present in the bound secret provider';
} else {
span.textContent = this.isSecret ? '••••••' : this.value;
span.title = this.isSecret ? 'Secret value' : this.value;
@@ -351,6 +357,9 @@
} else if (marker.defaultValue) {
// Has default value from marker -> BLUE
widget = new VariableValueWidget(marker.defaultValue, false, 'default');
+ } else if (marker.type === 'invault') {
+ // Present in the bound secret provider (live) -> GREEN
+ widget = new VariableValueWidget('', false, 'invault');
} else if (marker.type === 'missing') {
// Missing required variable -> RED
widget = new VariableValueWidget('', false, 'missing');
@@ -930,6 +939,11 @@
box-shadow: 0 0 4px #ef4444;
}
+ div :global(.var-marker-invault) {
+ background-color: #10b981; /* emerald-500 */
+ box-shadow: 0 0 4px #10b981;
+ }
+
/* Variable value overlay widget - base styles */
div :global(.var-value-overlay) {
display: inline-block;
@@ -968,6 +982,13 @@
font-weight: 600;
}
+ div :global(.var-value-invault) {
+ background-color: rgba(16, 185, 129, 0.15);
+ color: #10b981;
+ border: 1px solid rgba(16, 185, 129, 0.3);
+ font-weight: 600;
+ }
+
/* Light theme adjustments */
:global(.cm-editor:not(.cm-dark)) div :global(.var-value-provided) {
background-color: rgba(34, 197, 94, 0.1);
diff --git a/src/lib/components/SecretProviderPicker.svelte b/src/lib/components/SecretProviderPicker.svelte
new file mode 100644
index 0000000..4699422
--- /dev/null
+++ b/src/lib/components/SecretProviderPicker.svelte
@@ -0,0 +1,94 @@
+
+
+{#if providers.length > 0 || secretProviderId !== null}
+
+
+
+ { secretProviderId = v ? parseInt(v) : null; onchange?.(); }}
+ >
+
+ {#if secretProviderId !== null}
+ {providers.find((p) => p.id === secretProviderId)?.name ?? 'Unknown provider'}
+ {:else}
+ None — disabled
+ {/if}
+
+
+
+ None — disabled
+
+ {#each providers as provider (provider.id)}
+
+ {provider.name}
+
+ {/each}
+
+
+ {#if bulkSelectorField}
+
+ writeSelector(e.currentTarget.value)}
+ />
+ {/if}
+
+ {#if bulkSelectorField?.hint}
+
{bulkSelectorField.hint}
+ {/if}
+
+{/if}
diff --git a/src/lib/components/StackEnvVarsPanel.svelte b/src/lib/components/StackEnvVarsPanel.svelte
index 59e4ab5..3e2d05c 100644
--- a/src/lib/components/StackEnvVarsPanel.svelte
+++ b/src/lib/components/StackEnvVarsPanel.svelte
@@ -4,8 +4,10 @@
import StackEnvVarsEditor, { type EnvVar, type ValidationResult } from '$lib/components/StackEnvVarsEditor.svelte';
import CodeEditor from '$lib/components/CodeEditor.svelte';
import ConfirmPopover from '$lib/components/ConfirmPopover.svelte';
- import { Plus, Upload, Trash2, List, FileText, AlertTriangle, ShieldAlert, HelpCircle, Info } from 'lucide-svelte';
+ import { Plus, Upload, Trash2, List, FileText, AlertTriangle, ShieldAlert, HelpCircle, Info, Check, KeyRound } from 'lucide-svelte';
import * as Tooltip from '$lib/components/ui/tooltip';
+ import { getProviderIcon } from '$lib/components/provider-icons';
+ import { providerTypeLabel } from '../../routes/settings/secrets/ProviderModal.svelte';
interface Props {
variables: EnvVar[]; // Bindable - ALL variables (secrets + non-secrets)
@@ -18,6 +20,13 @@
placeholder?: { key: string; value: string };
infoText?: string;
existingSecretKeys?: Set;
+ /** Provider-injected key NAMES from the last deploy (banner). */
+ injectedSecretKeys?: string[];
+ /** Bound provider type/name, for the injected banner + pills. */
+ providerType?: string | null;
+ providerName?: string | null;
+ /** Set when the live provider probe failed - shown as an amber line. */
+ probeError?: string | null;
showInterpolationHint?: boolean;
theme?: 'light' | 'dark';
class?: string;
@@ -36,6 +45,10 @@
placeholder = { key: 'VARIABLE_NAME', value: 'value' },
infoText,
existingSecretKeys = new Set(),
+ injectedSecretKeys = [],
+ providerType = null,
+ providerName = null,
+ probeError = null,
showInterpolationHint = false,
theme = 'dark',
class: className = '',
@@ -43,6 +56,16 @@
headerActions
}: Props = $props();
+
+ // Provider-injected keys are supplied at deploy from the bound secret provider, so
+ // they are NOT "missing" even when a ${VAR} in compose has no local value. Drop them
+ // from the missing set (and expose them separately so the editor can mark them).
+ const injectedSet = $derived(new Set(injectedSecretKeys));
+ const effectiveValidation = $derived.by(() => {
+ if (!validation || injectedSet.size === 0) return validation;
+ return { ...validation, missing: validation.missing.filter((k) => !injectedSet.has(k)) };
+ });
+
const STORAGE_KEY_VIEW_MODE = 'dockhand-env-vars-view-mode';
let fileInputRef: HTMLInputElement;
@@ -333,21 +356,21 @@
- {#if validation}
+ {#if effectiveValidation}
- {#if validation.missing.length > 0}
+ {#if effectiveValidation.missing.length > 0}
- {validation.missing.length} missing
+ {effectiveValidation.missing.length} missing
{/if}
- {#if validation.required.length > 0}
+ {#if effectiveValidation.required.length > 0}
- {validation.required.length - validation.missing.length} defined
+ {effectiveValidation.required.length - effectiveValidation.missing.length} defined
{/if}
- {#if validation.optional.length > 0}
+ {#if effectiveValidation.optional.length > 0}
- {validation.optional.length} optional
+ {effectiveValidation.optional.length} optional
{/if}
@@ -472,11 +495,50 @@
{/if}
+
+ {#if injectedSecretKeys.length > 0}
+
+
+
+
+
{injectedSecretKeys.length} secret{injectedSecretKeys.length === 1 ? '' : 's'} loaded
+
from
+ {#if providerType}
+ {@const ProviderIcon = getProviderIcon(providerType)}
+
+
+ {providerTypeLabel(providerType)}
+
+ {#if providerName}
· {providerName}{/if}
+ {:else}
+
the provider
+ {/if}
+
+
Injected into the container at last deploy — never written to .env.
+
+ {#each injectedSecretKeys as key}
+
+ {key}
+
+ {/each}
+
+
+
+ {/if}
+
+ {#if probeError}
+
+
+
+ Couldn't check {providerName ?? 'the secret provider'}: {probeError}
+
+
+ {/if}
- {#if viewMode === 'form' && validation && validation.missing.length > 0 && !readonly}
+ {#if viewMode === 'form' && effectiveValidation && effectiveValidation.missing.length > 0 && !readonly}
Add missing:
- {#each validation.missing as missing}
+ {#each effectiveValidation.missing as missing}