Blocks

Feature Flags Block

Application section

A complete release-control section with exact configuration previews, independent approval, deterministic evaluation, guarded publication, and rollback evidence.

Release operations

Feature release control room

Use this as a complete feature-release section: choose a flag and environment, edit targeting, evaluate a customer context, request an independent approval, publish an immutable configuration, monitor its guardrails, and roll back to a known restore point.

1200px

vue
<script setup>
import { computed, onMounted, ref } from 'vue';
import {
	DomAlert,
	DomAppBottomNav,
	DomAppListItem,
	DomAppShell,
	DomAppTopBar,
	DomBadge,
	DomButton,
	DomCheckbox,
	DomDialog,
	DomEmptyState,
	DomJsonViewer,
	DomRangeInput,
	DomSelect,
	DomSkeleton,
	DomStatusPill,
	DomTabs,
	DomTextareaInput,
	DomTextInput,
	DomToggle,
} from '@getdom/studio/vue';

const apiBase = '/api/block-demos/feature-flags';
const evidenceTabs = [
	{ key: 'release', label: 'Release' },
	{ key: 'history', label: 'History' },
];

const workspace = ref(null);
const loading = ref(true);
const busyAction = ref('');
const error = ref('');
const notice = ref('');
const activeView = ref('targeting');
const evidenceTab = ref('release');
const flagSearch = ref('');
const draftEnabled = ref(true);
const draftRollout = ref(32);
const draftSegmentId = ref('beta_scale_eu');
const draftAttribute = ref('workspace_age_days');
const draftOperator = ref('greater_than');
const draftValue = ref('14');
const draftReason = ref('Expand after checkout guardrails stayed healthy.');
const approvalAcknowledged = ref(false);
const approvalNote = ref('Guardrails and rollback evidence reviewed.');
const publishAcknowledged = ref(false);
const rollbackAcknowledged = ref(false);
const contextKey = ref('northstar-workspace-42');
const contextSegmentId = ref('beta_scale_eu');
const contextRegion = ref('eu-west');
const evaluatePreview = ref(true);
const createDialogOpen = ref(false);
const createName = ref('Regional invoices');
const createKey = ref('billing.regional_invoices');
const createDescription = ref('Enable regional invoice numbering for selected workspaces.');
const createOwnerId = ref('admin');
const createType = ref('release');

const selectedFlag = computed(() => workspace.value?.selectedFlag || null);
const config = computed(() => selectedFlag.value?.config || null);
const filteredFlags = computed(getFilteredFlags);
const mobileNavigation = computed(getMobileNavigation);
const evidencePayload = computed(getEvidencePayload);
const selectedSegment = computed(() => workspace.value?.catalogs?.segments?.find((option) => option.value === draftSegmentId.value) || null);

onMounted(loadWorkspace);

/**
 * Loads the complete feature-release workspace from the repository-local API.
 *
 * @param {boolean} clearMessages Whether visible feedback should be cleared first.
 * @returns {Promise<void>}
 */
async function loadWorkspace(clearMessages = true) {
	if (clearMessages) clearFeedback();
	loading.value = true;
	try {
		const response = await fetch(`${apiBase}/bootstrap`);
		const data = await response.json();
		if (!response.ok) throw createRequestError(data, response.status);
		setWorkspace(data);
	} catch (requestError) {
		error.value = requestError.message || 'Unable to load the feature release workspace.';
	} finally {
		loading.value = false;
	}
}

/**
 * Sends one JSON mutation and applies its authoritative workspace response.
 *
 * @param {string} path API path below the feature-flags base URL.
 * @param {Record<string, unknown>} body JSON request body.
 * @param {string} action Stable busy-state key.
 * @returns {Promise<object|null>} Updated workspace or null after failure.
 */
async function mutateWorkspace(path, body, action) {
	clearFeedback();
	busyAction.value = action;
	try {
		const response = await fetch(`${apiBase}${path}`, {
			method: 'POST',
			headers: { 'content-type': 'application/json' },
			body: JSON.stringify(body),
		});
		const data = await response.json();
		if (!response.ok) throw createRequestError(data, response.status);
		setWorkspace(data);
		return data;
	} catch (requestError) {
		error.value = requestError.message || 'Feature release action failed.';
		if (requestError.status === 409) await refreshAfterConflict();
		return null;
	} finally {
		busyAction.value = '';
	}
}

/**
 * Reloads authoritative server state after an exact-revision conflict.
 *
 * @returns {Promise<void>}
 */
async function refreshAfterConflict() {
	try {
		const response = await fetch(`${apiBase}/bootstrap`);
		const data = await response.json();
		if (response.ok) setWorkspace(data);
	} catch {
		// Preserve the original conflict message when recovery also fails.
	}
}

/**
 * Replaces client state and aligns the editable draft with API evidence.
 *
 * @param {object} data Authoritative feature-release workspace.
 * @returns {void}
 */
function setWorkspace(data) {
	workspace.value = data;
	const source = data.preview?.draft || data.selectedFlag?.config;
	if (source) setDraft(source);
	if (!data.preview) {
		approvalAcknowledged.value = false;
		publishAcknowledged.value = false;
		evaluatePreview.value = false;
	}
	if (!data.rollbackPreview) rollbackAcknowledged.value = false;
}

/**
 * Copies a live or previewed configuration into the release editor.
 *
 * @param {object} source Configuration fields.
 * @returns {void}
 */
function setDraft(source) {
	draftEnabled.value = Boolean(source.enabled);
	draftRollout.value = Number(source.rollout || 0);
	draftSegmentId.value = source.audienceSegmentId || 'internal';
	draftAttribute.value = source.rule?.attribute || 'plan';
	draftOperator.value = source.rule?.operator || 'is_one_of';
	draftValue.value = source.rule?.value || 'Scale';
	if (source.reason) draftReason.value = source.reason;
}

/**
 * Selects one flag while preserving exact workspace state.
 *
 * @param {string} flagId Feature flag identifier.
 * @returns {Promise<void>}
 */
async function selectFlag(flagId) {
	if (!workspace.value || flagId === workspace.value.selectedFlagId || busyAction.value) return;
	const data = await mutateWorkspace('/select', {
		revision: workspace.value.revision,
		flagId,
		environmentId: workspace.value.selectedEnvironmentId,
	}, 'select-flag');
	if (data) {
		activeView.value = 'targeting';
		evidenceTab.value = 'release';
		notice.value = `${data.selectedFlag.key} opened at configuration version ${data.selectedFlag.config.version}.`;
	}
}

/**
 * Selects one environment through the server-owned workspace context.
 *
 * @param {string} environmentId Environment identifier.
 * @returns {Promise<void>}
 */
async function selectEnvironment(environmentId) {
	if (!workspace.value || environmentId === workspace.value.selectedEnvironmentId || busyAction.value) return;
	const data = await mutateWorkspace('/select', {
		revision: workspace.value.revision,
		flagId: workspace.value.selectedFlagId,
		environmentId,
	}, 'select-environment');
	if (data) notice.value = `${data.selectedFlag.name} switched to ${environmentLabel(environmentId)}.`;
}

/**
 * Keeps disabled flags at zero traffic and gives enabled drafts a safe starting percentage.
 *
 * @param {boolean} enabled Next serving state.
 * @returns {void}
 */
function updateEnabledDraft(enabled) {
	draftEnabled.value = Boolean(enabled);
	if (!draftEnabled.value) draftRollout.value = 0;
	else if (draftRollout.value === 0) draftRollout.value = 10;
}

/**
 * Restores the editor to the current live configuration.
 *
 * @returns {void}
 */
function restoreLiveDraft() {
	if (!config.value) return;
	setDraft(config.value);
	clearFeedback();
	notice.value = 'The editor now matches the live configuration.';
}

/**
 * Previews the rollout, audience, and rule changes through the API.
 *
 * @returns {Promise<void>}
 */
async function previewChange() {
	if (!workspace.value || !selectedFlag.value || !config.value) return;
	const data = await mutateWorkspace('/changes/preview', {
		revision: workspace.value.revision,
		flagId: selectedFlag.value.id,
		environmentId: workspace.value.selectedEnvironmentId,
		flagVersion: config.value.version,
		enabled: draftEnabled.value,
		rollout: draftRollout.value,
		audienceSegmentId: draftSegmentId.value,
		attribute: draftAttribute.value,
		operator: draftOperator.value,
		value: draftValue.value,
		reason: draftReason.value,
	}, 'preview-change');
	if (data) {
		activeView.value = 'release';
		evidenceTab.value = 'release';
		notice.value = `${data.preview.checksum} locks ${data.preview.changes.length} proposed changes and ${formatNumber(data.preview.estimatedContexts)} estimated contexts.`;
	}
}

/**
 * Requests production approval for the current immutable preview.
 *
 * @returns {Promise<void>}
 */
async function requestApproval() {
	if (!workspace.value?.preview) return;
	const data = await mutateWorkspace('/approvals', {
		revision: workspace.value.revision,
		previewId: workspace.value.preview.id,
		acknowledged: approvalAcknowledged.value,
	}, 'request-approval');
	if (data) notice.value = `${data.approval.id} was routed to ${data.approval.reviewer}.`;
}

/**
 * Records the simulated independent release manager's decision.
 *
 * @param {'approved'|'rejected'} decision Approval decision.
 * @returns {Promise<void>}
 */
async function decideApproval(decision) {
	if (!workspace.value?.approval) return;
	const data = await mutateWorkspace(`/approvals/${workspace.value.approval.id}/decision`, {
		revision: workspace.value.revision,
		decision,
		note: approvalNote.value,
	}, `approval-${decision}`);
	if (data) notice.value = `${data.approval.reviewer} ${decision} ${data.approval.checksum}.`;
}

/**
 * Publishes the acknowledged and approved release preview.
 *
 * @returns {Promise<void>}
 */
async function publishChange() {
	if (!workspace.value?.preview) return;
	const data = await mutateWorkspace('/publish', {
		revision: workspace.value.revision,
		previewId: workspace.value.preview.id,
		checksum: workspace.value.preview.checksum,
		acknowledged: publishAcknowledged.value,
	}, 'publish-change');
	if (data) notice.value = `${data.publishReceipt.id} reached ${data.publishReceipt.evaluatorsAcknowledged} evaluators and entered monitoring.`;
}

/**
 * Advances the deterministic post-release monitoring window.
 *
 * @returns {Promise<void>}
 */
async function advanceMonitoring() {
	if (!workspace.value || !selectedFlag.value || !config.value) return;
	const data = await mutateWorkspace('/monitoring/advance', currentContext(), 'advance-monitoring');
	if (data) notice.value = `${data.monitoringReceipt.id} completed the ${data.monitoringReceipt.window} monitoring window.`;
}

/**
 * Evaluates a test context against the live or current preview configuration.
 *
 * @returns {Promise<void>}
 */
async function evaluateContext() {
	if (!workspace.value) return;
	const data = await mutateWorkspace('/evaluations', {
		revision: workspace.value.revision,
		contextKey: contextKey.value,
		segmentId: contextSegmentId.value,
		region: contextRegion.value,
		usePreview: evaluatePreview.value,
	}, 'evaluate-context');
	if (data) notice.value = `${data.evaluation.receipt} served ${data.evaluation.variation} from ${data.evaluation.configuration.toLowerCase()} targeting.`;
}

/**
 * Creates a rollback preview from the latest published restore point.
 *
 * @returns {Promise<void>}
 */
async function previewRollback() {
	if (!workspace.value || !selectedFlag.value || !config.value) return;
	const data = await mutateWorkspace('/rollbacks/preview', currentContext(), 'preview-rollback');
	if (data) notice.value = `${data.rollbackPreview.checksum} locks the restore from ${data.rollbackPreview.current.rollout}% to ${data.rollbackPreview.restore.rollout}%.`;
}

/**
 * Restores the exact previous configuration after acknowledgement.
 *
 * @returns {Promise<void>}
 */
async function rollbackChange() {
	if (!workspace.value?.rollbackPreview) return;
	const data = await mutateWorkspace('/rollbacks', {
		revision: workspace.value.revision,
		previewId: workspace.value.rollbackPreview.id,
		checksum: workspace.value.rollbackPreview.checksum,
		acknowledged: rollbackAcknowledged.value,
	}, 'rollback-change');
	if (data) notice.value = `${data.rollbackReceipt.id} restored the prior configuration and entered monitoring.`;
}

/**
 * Creates a disabled flag and selects its production configuration.
 *
 * @returns {Promise<void>}
 */
async function createFlag() {
	if (!workspace.value) return;
	const data = await mutateWorkspace('/flags', {
		revision: workspace.value.revision,
		name: createName.value,
		key: createKey.value,
		description: createDescription.value,
		ownerId: createOwnerId.value,
		type: createType.value,
	}, 'create-flag');
	if (data) {
		createDialogOpen.value = false;
		activeView.value = 'targeting';
		notice.value = `${data.createReceipt.id} created ${data.createReceipt.flagKey} disabled in every environment.`;
	}
}

/**
 * Restores all feature-release demo state.
 *
 * @returns {Promise<void>}
 */
async function resetWorkspace() {
	const data = await mutateWorkspace('/reset', {}, 'reset-workspace');
	if (data) {
		activeView.value = 'targeting';
		evidenceTab.value = 'release';
		notice.value = `Demo reset to workspace revision ${data.revision}.`;
	}
}

/**
 * Returns exact context fields required by configuration mutations.
 *
 * @returns {object} Current workspace, flag, environment, and configuration revisions.
 */
function currentContext() {
	return {
		revision: workspace.value.revision,
		flagId: selectedFlag.value.id,
		environmentId: workspace.value.selectedEnvironmentId,
		flagVersion: config.value.version,
	};
}

/**
 * Filters flag inventory rows against name, key, and owner.
 *
 * @returns {Array<object>} Visible feature flags.
 */
function getFilteredFlags() {
	const query = flagSearch.value.trim().toLowerCase();
	const flags = workspace.value?.flags || [];
	if (!query) return flags;
	return flags.filter((flag) => [flag.name, flag.key, flag.owner].some((value) => String(value).toLowerCase().includes(query)));
}

/**
 * Builds narrow-screen primary navigation with useful workflow badges.
 *
 * @returns {Array<object>} Bottom navigation items.
 */
function getMobileNavigation() {
	return [
		{ value: 'flags', label: 'Flags', badge: workspace.value?.flags?.length ? String(workspace.value.flags.length) : '' },
		{ value: 'targeting', label: 'Targeting' },
		{ value: 'release', label: 'Release', badge: workspace.value?.approval?.status === 'pending' ? '1' : workspace.value?.preview ? '•' : '' },
	];
}

/**
 * Builds the latest immutable evidence payload for JSON inspection.
 *
 * @returns {object} Current release, monitoring, rollback, and evaluation proof.
 */
function getEvidencePayload() {
	if (!workspace.value) return {};
	return {
		workspaceRevision: workspace.value.revision,
		flagKey: selectedFlag.value?.key,
		environment: workspace.value.selectedEnvironmentId,
		configurationVersion: config.value?.version,
		preview: workspace.value.preview ? {
			id: workspace.value.preview.id,
			checksum: workspace.value.preview.checksum,
			sourceVersion: workspace.value.preview.sourceVersion,
		} : null,
		approval: workspace.value.approval,
		publication: workspace.value.publishReceipt,
		monitoring: workspace.value.monitoringReceipt,
		rollback: workspace.value.rollbackReceipt,
		evaluation: workspace.value.evaluation,
	};
}

/**
 * Resolves a semantic status tone.
 *
 * @param {string} status Release or check status.
 * @returns {string} DOM Studio semantic tone.
 */
function statusTone(status) {
	return {
		healthy: 'success',
		passed: 'success',
		approved: 'success',
		monitoring: 'primary',
		pending: 'warning',
		warning: 'warning',
		watch: 'warning',
		breach: 'danger',
		rejected: 'danger',
		paused: 'neutral',
		draft: 'neutral',
	}[status] || 'neutral';
}

/**
 * Formats a machine status as readable label text.
 *
 * @param {unknown} value Machine status.
 * @returns {string} Human-readable status.
 */
function statusLabel(value) {
	const text = String(value || 'unknown').replaceAll('-', ' ');
	return text.charAt(0).toUpperCase() + text.slice(1);
}

/**
 * Resolves an environment label from the server catalog.
 *
 * @param {string} environmentId Environment identifier.
 * @returns {string} Human-readable environment label.
 */
function environmentLabel(environmentId) {
	return workspace.value?.catalogs?.environments?.find((option) => option.value === environmentId)?.label || environmentId;
}

/**
 * Formats a number for compact operational evidence.
 *
 * @param {unknown} value Numeric value.
 * @returns {string} Locale-formatted number.
 */
function formatNumber(value) {
	return new Intl.NumberFormat('en-GB').format(Number(value || 0));
}

/**
 * Clears visible success and error feedback.
 *
 * @returns {void}
 */
function clearFeedback() {
	error.value = '';
	notice.value = '';
}

/**
 * Creates an Error with the originating HTTP status attached.
 *
 * @param {object} data API response payload.
 * @param {number} status HTTP status code.
 * @returns {Error & {status?: number}} Request error.
 */
function createRequestError(data, status) {
	const requestError = new Error(data?.error || `Request failed with status ${status}.`);
	requestError.status = status;
	return requestError;
}
</script>

<template>
	<DomAppShell class="!h-dvh">
		<template #top>
			<DomAppTopBar
				title="Release control"
				:subtitle="workspace && selectedFlag ? `${selectedFlag.name} · ${environmentLabel(workspace.selectedEnvironmentId)}` : 'Feature flags'"
			>
				<template #leading>
					<div class="grid size-9 place-items-center rounded-xl bg-primary text-xs font-bold text-primary-fg">RC</div>
				</template>
				<template #trailing>
					<DomBadge v-if="workspace" tone="neutral" size="sm" class="hidden sm:inline-flex">r{{ workspace.revision }}</DomBadge>
					<DomButton class="hidden sm:inline-flex" size="sm" variant="secondary" :loading="busyAction === 'reset-workspace'" @click="resetWorkspace">Reset</DomButton>
					<DomButton class="hidden sm:inline-flex" size="sm" @click="createDialogOpen = true">Create flag</DomButton>
				</template>
			</DomAppTopBar>
		</template>

		<div v-if="loading" class="grid min-h-full gap-0 lg:grid-cols-[15rem_minmax(0,1fr)_20rem]">
			<div v-for="column in 3" :key="column" class="space-y-3 border-r border-border p-4 last:border-r-0">
				<DomSkeleton class="h-10" />
				<DomSkeleton v-for="row in 5" :key="row" class="h-16" />
			</div>
		</div>

		<DomEmptyState
			v-else-if="!workspace || !selectedFlag || !config"
			title="Release workspace unavailable"
			description="Reload the repository-local demo API to restore feature flags and environments."
		>
			<DomButton @click="loadWorkspace">Reload workspace</DomButton>
		</DomEmptyState>

		<div v-else class="min-h-full lg:grid lg:h-full lg:grid-cols-[15rem_minmax(0,1fr)_20rem]">
			<aside
				class="min-h-full border-r border-border bg-muted/15 lg:block lg:min-h-0 lg:overflow-y-auto"
				:class="activeView === 'flags' ? 'block' : 'hidden'"
			>
				<div class="sticky top-0 z-10 border-b border-border bg-canvas/95 p-3 backdrop-blur">
					<DomSelect
						:model-value="workspace.selectedEnvironmentId"
						:options="workspace.catalogs.environments"
						label="Environment"
						chrome="none"
						width="min-w-[17rem]"
						@update:model-value="selectEnvironment"
					>
						<template #value="{ option }">
							<div v-if="option" class="min-w-0">
								<p class="truncate text-sm font-semibold">{{ option.label }}</p>
								<p class="truncate text-[11px] text-muted-fg">{{ option.description }}</p>
							</div>
						</template>
						<template #option="{ option, selected }">
							<div class="min-w-0 py-0.5">
								<p class="text-sm font-medium" :class="selected && 'font-semibold'">{{ option.label }}</p>
								<p class="mt-0.5 text-xs opacity-75">{{ option.description }}</p>
							</div>
						</template>
					</DomSelect>
					<DomTextInput v-model="flagSearch" class="mt-2" placeholder="Search flags or owners" aria-label="Search feature flags" chrome="none" />
				</div>

				<div class="divide-y divide-border">
					<DomAppListItem
						v-for="flag in filteredFlags"
						:key="flag.id"
						:label="flag.name"
						:description="flag.key"
						:meta="`${flag.rollout}%`"
						:selected="flag.id === workspace.selectedFlagId"
						@click="selectFlag(flag.id)"
					>
						<template #leading>
							<span class="size-2.5 rounded-full" :class="statusTone(flag.status) === 'success' ? 'bg-success' : statusTone(flag.status) === 'warning' ? 'bg-warning' : statusTone(flag.status) === 'primary' ? 'bg-primary' : 'bg-muted-fg/50'"></span>
						</template>
					</DomAppListItem>
				</div>

				<DomEmptyState v-if="!filteredFlags.length" title="No flags found" description="Try a flag key, product area, or owner." compact />
				<div class="p-3">
					<DomButton class="w-full" variant="secondary" @click="createDialogOpen = true">Create flag</DomButton>
					<DomButton class="mt-2 w-full sm:hidden" variant="ghost" :loading="busyAction === 'reset-workspace'" @click="resetWorkspace">Reset demo</DomButton>
				</div>
			</aside>

			<main
				class="min-h-full bg-canvas lg:block lg:min-h-0 lg:overflow-y-auto"
				:class="activeView === 'targeting' ? 'block' : 'hidden'"
			>
				<header class="border-b border-border px-4 py-4 sm:px-5">
					<div class="flex flex-wrap items-start justify-between gap-3">
						<div class="min-w-0">
							<div class="flex flex-wrap items-center gap-2">
								<DomStatusPill :tone="statusTone(config.status)" :pulse="config.status === 'monitoring'" size="sm">{{ statusLabel(config.status) }}</DomStatusPill>
								<DomBadge tone="neutral" size="sm">{{ selectedFlag.owner }}</DomBadge>
								<DomBadge :tone="selectedFlag.risk === 'high' ? 'danger' : selectedFlag.risk === 'medium' ? 'warning' : 'success'" size="sm">{{ statusLabel(selectedFlag.risk) }} risk</DomBadge>
							</div>
							<h2 class="mt-2 truncate text-xl font-semibold tracking-tight">{{ selectedFlag.name }}</h2>
							<p class="mt-1 font-mono text-xs text-muted-fg">{{ selectedFlag.key }}</p>
						</div>
						<div class="text-right text-xs text-muted-fg">
							<p>{{ formatNumber(config.evaluations) }} evaluations</p>
							<p class="mt-1">v{{ config.version }} · {{ config.updatedAt }}</p>
						</div>
					</div>
					<p class="mt-3 max-w-3xl text-sm leading-6 text-muted-fg">{{ selectedFlag.description }}</p>
				</header>

				<div class="divide-y divide-border">
					<section class="p-4 sm:p-5">
						<div class="flex items-start justify-between gap-4">
							<div>
								<p class="text-xs font-semibold uppercase tracking-[0.14em] text-muted-fg">Serving</p>
								<h3 class="mt-1 text-base font-semibold">Live configuration draft</h3>
								<p class="mt-1 text-xs leading-5 text-muted-fg">Previewing locks the exact configuration version, customer impact, and rollback evidence.</p>
							</div>
							<div class="flex items-center gap-2">
								<span class="text-xs font-medium">{{ draftEnabled ? 'Enabled' : 'Disabled' }}</span>
								<DomToggle :model-value="draftEnabled" aria-label="Flag enabled in selected environment" @update:model-value="updateEnabledDraft" />
							</div>
						</div>

						<div class="mt-5 grid gap-4 sm:grid-cols-[minmax(0,1fr)_15rem] sm:items-end">
							<DomRangeInput v-model="draftRollout" label="Percentage rollout" :disabled="!draftEnabled" :min="0" :max="100" :step="1" suffix="%" />
							<DomSelect v-model="draftSegmentId" :options="workspace.catalogs.segments" label="Audience segment" searchable width="min-w-[20rem]">
								<template #option="{ option }">
									<div class="py-0.5"><p class="text-sm font-medium">{{ option.label }}</p><p class="mt-0.5 text-xs opacity-75">{{ option.description }}</p></div>
								</template>
							</DomSelect>
						</div>
						<div v-if="selectedSegment" class="mt-2 flex items-center justify-between gap-3 text-xs text-muted-fg">
							<span>{{ selectedSegment.description }}</span>
							<span class="shrink-0 font-medium text-canvas-fg">{{ draftRollout }}% served</span>
						</div>
					</section>

					<section class="p-4 sm:p-5">
						<div class="flex items-center justify-between gap-3">
							<div>
								<p class="text-xs font-semibold uppercase tracking-[0.14em] text-muted-fg">Targeting rule</p>
								<h3 class="mt-1 text-base font-semibold">Match context attributes</h3>
							</div>
							<DomBadge tone="primary" size="sm">Then serve Available</DomBadge>
						</div>
						<div class="mt-4 grid gap-3 md:grid-cols-[minmax(0,1fr)_minmax(0,0.8fr)_minmax(0,1fr)]">
							<DomSelect v-model="draftAttribute" :options="workspace.catalogs.attributes" label="Attribute" width="min-w-[18rem]" />
							<DomSelect v-model="draftOperator" :options="workspace.catalogs.operators" label="Operator" width="min-w-[16rem]" />
							<DomTextInput v-model="draftValue" label="Value" placeholder="Scale, Growth" />
						</div>
						<DomTextareaInput v-model="draftReason" class="mt-4" label="Release reason" description="Included in approval, audit, and rollback evidence." :rows="3" />
						<div class="mt-4 flex flex-wrap justify-end gap-2">
							<DomButton variant="ghost" @click="restoreLiveDraft">Restore live values</DomButton>
							<DomButton :loading="busyAction === 'preview-change'" @click="previewChange">Review change</DomButton>
						</div>
					</section>

					<section class="p-4 sm:p-5">
						<div class="flex flex-wrap items-start justify-between gap-3">
							<div>
								<p class="text-xs font-semibold uppercase tracking-[0.14em] text-muted-fg">Evaluator</p>
								<h3 class="mt-1 text-base font-semibold">Test a context before release</h3>
								<p class="mt-1 text-xs text-muted-fg">The API returns a stable rollout bucket and the exact targeting reason.</p>
							</div>
							<DomCheckbox v-model="evaluatePreview" :disabled="!workspace.preview" label="Use current preview" description="Otherwise evaluate the live configuration." />
						</div>
						<div class="mt-4 grid gap-3 md:grid-cols-3">
							<DomTextInput v-model="contextKey" label="Context key" />
							<DomSelect v-model="contextSegmentId" :options="workspace.catalogs.contextSegments" label="Test segment" width="min-w-[18rem]" />
							<DomSelect v-model="contextRegion" :options="workspace.catalogs.regions" label="Region" width="min-w-[15rem]" />
						</div>
						<div class="mt-4 flex flex-wrap items-center justify-between gap-3">
							<div v-if="workspace.evaluation" class="flex min-w-0 items-start gap-3">
								<DomStatusPill :tone="workspace.evaluation.matched ? 'success' : 'neutral'" size="sm">{{ workspace.evaluation.variation }}</DomStatusPill>
								<div class="min-w-0 text-xs"><p class="font-medium">Bucket {{ workspace.evaluation.bucket }} · {{ workspace.evaluation.configuration }}</p><p class="mt-1 text-muted-fg">{{ workspace.evaluation.reason }}</p></div>
							</div>
							<p v-else class="text-xs text-muted-fg">No test evaluation has been run for this workspace revision.</p>
							<DomButton variant="secondary" :loading="busyAction === 'evaluate-context'" @click="evaluateContext">Evaluate context</DomButton>
						</div>
					</section>
				</div>
			</main>

			<aside
				class="min-h-full border-l border-border bg-muted/10 lg:block lg:min-h-0 lg:overflow-y-auto"
				:class="activeView === 'release' ? 'block' : 'hidden'"
			>
				<DomTabs v-model="evidenceTab" :tabs="evidenceTabs" aria-label="Release evidence" class="p-3">
					<template #release>
						<div class="space-y-4 pt-3">
							<section>
								<div class="flex items-end justify-between gap-3">
									<div><p class="text-xs font-semibold uppercase tracking-[0.14em] text-muted-fg">Readiness</p><p class="mt-1 text-sm font-semibold">Release checks</p></div>
									<p class="text-2xl font-semibold tracking-tight">{{ selectedFlag.readiness.score }}%</p>
								</div>
								<div class="mt-3 divide-y divide-border border-y border-border">
									<div v-for="check in selectedFlag.readiness.checks" :key="check.id" class="flex items-start gap-3 py-3">
										<DomStatusPill :tone="statusTone(check.state)" :dot="true" size="sm">{{ statusLabel(check.state) }}</DomStatusPill>
										<div class="min-w-0"><p class="text-xs font-semibold">{{ check.label }}</p><p class="mt-1 text-[11px] leading-4 text-muted-fg">{{ check.detail }}</p></div>
									</div>
								</div>
							</section>

							<section>
								<p class="text-xs font-semibold uppercase tracking-[0.14em] text-muted-fg">Guardrails</p>
								<div class="mt-2 divide-y divide-border border-y border-border">
									<div v-for="item in config.metrics" :key="item.id" class="grid grid-cols-[minmax(0,1fr)_auto] gap-3 py-3 text-xs">
										<div><p class="font-semibold">{{ item.label }}</p><p class="mt-1 text-muted-fg">{{ item.trend }} · threshold {{ item.threshold }}</p></div>
										<div class="text-right"><p class="font-semibold">{{ item.value }}</p><DomStatusPill class="mt-1" :tone="statusTone(item.state)" size="sm">{{ statusLabel(item.state) }}</DomStatusPill></div>
									</div>
								</div>
							</section>

							<section v-if="workspace.preview" class="border-t border-border pt-4">
								<div class="flex items-center justify-between gap-3">
									<div><p class="text-xs font-semibold uppercase tracking-[0.14em] text-muted-fg">Change set</p><p class="mt-1 font-mono text-[11px]">{{ workspace.preview.checksum }}</p></div>
									<DomBadge :tone="workspace.preview.requiresApproval ? 'warning' : 'success'" size="sm">{{ workspace.preview.requiresApproval ? 'Approval required' : 'Auto-approved' }}</DomBadge>
								</div>
								<div class="mt-3 divide-y divide-border rounded-lg border border-border bg-canvas px-3">
									<div v-for="change in workspace.preview.changes" :key="change.field" class="py-2.5 text-xs">
										<p class="font-semibold">{{ change.label }}</p>
										<p class="mt-1 text-muted-fg"><span class="line-through">{{ change.before }}</span> → <span class="font-medium text-canvas-fg">{{ change.after }}</span></p>
									</div>
								</div>
								<p class="mt-2 text-xs text-muted-fg">{{ formatNumber(workspace.preview.estimatedContexts) }} estimated contexts · {{ workspace.preview.draft.reason || draftReason }}</p>
							</section>

							<section v-if="workspace.preview && workspace.preview.requiresApproval && !workspace.approval" class="space-y-3 border-t border-border pt-4">
								<DomCheckbox v-model="approvalAcknowledged" label="I reviewed customer exposure" description="The rollback path and guardrail evidence will be included with the request." />
								<DomButton class="w-full" :disabled="!approvalAcknowledged" :loading="busyAction === 'request-approval'" @click="requestApproval">Request production approval</DomButton>
							</section>

							<section v-if="workspace.approval" class="space-y-3 border-t border-border pt-4">
								<div class="flex items-center justify-between gap-3"><p class="text-sm font-semibold">Production approval</p><DomStatusPill :tone="statusTone(workspace.approval.status)" size="sm">{{ statusLabel(workspace.approval.status) }}</DomStatusPill></div>
								<p class="text-xs leading-5 text-muted-fg">Requested by {{ workspace.approval.requestedBy }} · reviewer {{ workspace.approval.reviewer }}</p>
								<template v-if="workspace.approval.status === 'pending'">
									<DomTextareaInput v-model="approvalNote" label="Reviewer note" :rows="2" />
									<div class="grid grid-cols-2 gap-2"><DomButton variant="secondary" :loading="busyAction === 'approval-rejected'" @click="decideApproval('rejected')">Reject</DomButton><DomButton :loading="busyAction === 'approval-approved'" @click="decideApproval('approved')">Approve as Ava</DomButton></div>
								</template>
								<p v-else class="rounded-lg bg-secondary/60 p-3 text-xs leading-5">{{ workspace.approval.decisionNote }}</p>
							</section>

							<section v-if="workspace.preview && (!workspace.preview.requiresApproval || workspace.approval?.status === 'approved')" class="space-y-3 border-t border-border pt-4">
								<DomCheckbox v-model="publishAcknowledged" label="Publish this exact change" :description="`Configuration ${workspace.preview.sourceVersion} will change evaluated ${environmentLabel(workspace.selectedEnvironmentId).toLowerCase()} traffic.`" />
								<DomButton class="w-full" :disabled="!publishAcknowledged" :loading="busyAction === 'publish-change'" @click="publishChange">Publish to {{ environmentLabel(workspace.selectedEnvironmentId) }}</DomButton>
							</section>

							<DomAlert v-if="workspace.publishReceipt" tone="success" title="Release published" :description="`${workspace.publishReceipt.id} reached ${workspace.publishReceipt.evaluatorsAcknowledged} evaluators.`">
								<template #actions><DomButton v-if="config.status === 'monitoring' && !workspace.rollbackReceipt" size="sm" variant="secondary" :loading="busyAction === 'advance-monitoring'" @click="advanceMonitoring">Advance 30 min monitoring</DomButton></template>
							</DomAlert>

							<DomAlert v-if="workspace.monitoringReceipt" tone="success" title="Guardrails passed" :description="`${workspace.monitoringReceipt.id} completed the ${workspace.monitoringReceipt.window} window.`" />
							<DomAlert v-if="workspace.rollbackReceipt" tone="warning" title="Configuration restored" :description="`${workspace.rollbackReceipt.id} restored version ${workspace.rollbackReceipt.configurationVersion}.`">
								<template #actions><DomButton v-if="config.status === 'monitoring'" size="sm" variant="secondary" :loading="busyAction === 'advance-monitoring'" @click="advanceMonitoring">Advance rollback monitoring</DomButton></template>
							</DomAlert>

							<section v-if="config.previousSnapshot" class="space-y-3 border-t border-border pt-4">
								<div class="flex items-center justify-between gap-3"><div><p class="text-xs font-semibold uppercase tracking-[0.14em] text-muted-fg">Recovery</p><p class="mt-1 text-sm font-semibold">Previous configuration</p></div><DomBadge tone="neutral" size="sm">{{ config.previousSnapshot.rollout }}%</DomBadge></div>
								<template v-if="workspace.rollbackPreview">
									<p class="text-xs text-muted-fg">Restore {{ workspace.rollbackPreview.current.rollout }}% → {{ workspace.rollbackPreview.restore.rollout }}% using {{ workspace.rollbackPreview.checksum }}.</p>
									<DomCheckbox v-model="rollbackAcknowledged" label="I understand traffic will change" />
									<DomButton class="w-full" variant="danger" :disabled="!rollbackAcknowledged" :loading="busyAction === 'rollback-change'" @click="rollbackChange">Restore previous configuration</DomButton>
								</template>
								<DomButton v-else class="w-full" variant="secondary" :loading="busyAction === 'preview-rollback'" @click="previewRollback">Preview rollback</DomButton>
							</section>

							<DomJsonViewer v-if="workspace.publishReceipt || workspace.evaluation || workspace.rollbackReceipt" :value="evidencePayload" title="Provider evidence" filename="release-evidence.json" density="compact" :preview-lines="12" />
						</div>
					</template>

					<template #history>
						<div class="pt-3">
							<div v-for="item in workspace.activity" :key="item.id" class="relative border-l border-border pb-5 pl-5 text-sm last:pb-0">
								<span class="absolute -left-[5px] top-1 size-2.5 rounded-full bg-primary ring-4 ring-canvas"></span>
								<div class="flex items-baseline justify-between gap-3"><p class="font-semibold">{{ item.title }}</p><span class="shrink-0 text-[11px] text-muted-fg">{{ item.time }}</span></div>
								<p class="mt-1 text-xs text-muted-fg">{{ item.actor }} · {{ item.detail }}</p>
							</div>
						</div>
					</template>
				</DomTabs>
			</aside>
		</div>

		<template #bottom>
			<DomAppBottomNav v-if="workspace" v-model="activeView" class="lg:hidden" :items="mobileNavigation" />
		</template>

		<template #overlay>
			<div v-if="error || notice" class="pointer-events-none absolute inset-x-3 top-3 z-30 flex justify-center">
				<DomAlert
					class="pointer-events-auto w-full max-w-xl shadow-lg"
					:tone="error ? 'danger' : 'success'"
					:title="error ? 'Action needs attention' : 'Workspace updated'"
					:description="error || notice"
					dismissible
					@dismiss="clearFeedback"
				/>
			</div>
		</template>
	</DomAppShell>

	<DomDialog v-if="workspace" v-model="createDialogOpen" title="Create feature flag" description="Start disabled in every environment, then preview targeting and exposure before publication." size="lg">
		<div class="grid gap-4 sm:grid-cols-2">
			<DomTextInput v-model="createName" label="Flag name" />
			<DomTextInput v-model="createKey" label="Flag key" />
			<DomSelect v-model="createOwnerId" :options="workspace.catalogs.owners" label="Owner" width="min-w-[18rem]" />
			<DomSelect v-model="createType" :options="workspace.catalogs.flagTypes" label="Flag type" width="min-w-[18rem]" />
			<DomTextareaInput v-model="createDescription" class="sm:col-span-2" label="Behavior controlled" :rows="3" />
		</div>
		<template #footer>
			<DomButton variant="secondary" @click="createDialogOpen = false">Cancel</DomButton>
			<DomButton :loading="busyAction === 'create-flag'" @click="createFlag">Create disabled flag</DomButton>
		</template>
	</DomDialog>
</template>

Working journey

What this section proves

The block is an API-backed app section, not a generated screenshot. It composes flag discovery, environment configuration, targeting, deterministic evaluation, approvals, release checks, guardrail monitoring, and rollback into one responsive workflow.

  • Select flags and environments with rich DomSelect controls, or use the dense desktop rail and focused mobile Flags view.
  • Edit the rollout, segment, normalized targeting rule, and release reason with DOM Studio inputs, then preview the exact immutable configuration version.
  • Evaluate a realistic workspace context against live or preview state and retain its stable bucket, targeting explanation, and receipt.
  • Request an independent reviewer, record a structured decision, acknowledge the release checks, and publish the approved checksum.
  • Advance deterministic monitoring, inspect guardrails and provider-style stream evidence, then preview and execute a restore-point rollback.
  • Reload to prove process persistence, or reset the deterministic demo for another complete journey.

API contract

Repository-local routes

txt
GET  /api/block-demos/feature-flags/bootstrap
POST /api/block-demos/feature-flags/select
POST /api/block-demos/feature-flags/flags
POST /api/block-demos/feature-flags/changes/preview
POST /api/block-demos/feature-flags/approvals
POST /api/block-demos/feature-flags/approvals/:approvalId/decision
POST /api/block-demos/feature-flags/publish
POST /api/block-demos/feature-flags/monitoring/advance
POST /api/block-demos/feature-flags/evaluations
POST /api/block-demos/feature-flags/rollbacks/preview
POST /api/block-demos/feature-flags/rollbacks
POST /api/block-demos/feature-flags/reset

Design references

Why this composition is different

LaunchDarkly-style targeting

Environment state, ordered targeting, percentage rollout, restore points, approvals, and monitoring read as one versioned release instead of unrelated dashboard cards.

PostHog-style evaluation

Operators can test a concrete context against a preview and see the stable rollout bucket and server-owned reason before customer traffic changes.

Focused responsive app

The embedded viewer uses a dense control room, while narrow windows switch to explicit Flags, Targeting, and Release destinations without shrinking a desktop screenshot.

Production boundary

Adopt the contract, replace the demo storage

The demo API intentionally uses deterministic process memory. A production integration should add tenant-scoped persistence, identity and permission enforcement, transactional revision checks, idempotent publication, a real evaluation SDK, queued event delivery, observability ingestion, retention policy, and operational telemetry while preserving the preview, approval, receipt, monitoring, and restore-point contracts shown here.