mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(settings): panel-tunable feature flags
Add a Feature Flags card to the Settings page that toggles env-gated subsystems (external/internal PR review, web research, strategy engine, pitch provisioning, RAG auto-update, transcript pruning) directly from the panel instead of hand-editing environment variables. Flags persist in system_settings as 'true'/'false' and are overlaid onto the live config singleton at startup; an unset flag keeps its environment/config default. A toggle takes effect on the next backend restart — no per-consumer re-routing. Backend: FEATURE_FLAGS registry + bool validator + get_bool accessor on SettingsService; feature_flag_effective_values and apply_persisted_feature_flags; GET /settings/feature-flags; best-effort startup overlay in the app lifespan. Frontend: settingsApi.getFeatureFlags / setFeatureFlag and a FeatureFlagsCard rendered full-width below the settings grid.
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
||||
import { toast } from "sonner";
|
||||
import { API_URL, WS_URL } from "@/lib/constants";
|
||||
import { TranscriptRetentionCard } from "@/components/settings/transcript-retention-card";
|
||||
import { FeatureFlagsCard } from "@/components/settings/feature-flags-card";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
@@ -241,6 +242,10 @@ export default function SettingsPage() {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Feature Flags — master switches for optional subsystems (full width;
|
||||
persisted server-side, applied on next restart). */}
|
||||
<FeatureFlagsCard />
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSave}>
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { settingsApi } from "@/lib/api";
|
||||
import type { FeatureFlag } from "@/lib/api/settings";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Flag } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
// One-line blurb per flag so the operator knows what each master switch gates.
|
||||
const FLAG_DESCRIPTIONS: Record<string, string> = {
|
||||
external_pr_enabled: "Discover and review inbound external/fork pull requests.",
|
||||
internal_pr_enabled: "Run the read-only safety reviewer on internal branch PRs.",
|
||||
research_enabled: "Let the Board and PMs run web research.",
|
||||
strategy_engine_enabled: "Generate and maintain company strategy artifacts.",
|
||||
provisioning_enabled: "Auto-provision projects from approved pitches.",
|
||||
rag_auto_update_enabled: "Keep the knowledge base index refreshed automatically.",
|
||||
transcript_prune_enabled: "Run the background sweep that prunes old transcripts.",
|
||||
};
|
||||
|
||||
export function FeatureFlagsCard() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["feature-flags"],
|
||||
queryFn: settingsApi.getFeatureFlags,
|
||||
});
|
||||
|
||||
const toggleMutation = useMutation({
|
||||
mutationFn: ({ key, enabled }: { key: string; enabled: boolean }) =>
|
||||
settingsApi.setFeatureFlag(key, enabled),
|
||||
onSuccess: (_data, { enabled }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["feature-flags"] });
|
||||
toast.success(
|
||||
`Feature ${enabled ? "enabled" : "disabled"} — takes effect on next restart`,
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(
|
||||
`Failed to update: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const flags: FeatureFlag[] = data?.flags ?? [];
|
||||
const note = data?.note ?? "Changes take effect on the next backend restart.";
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Flag className="h-5 w-5" />
|
||||
Feature Flags
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Master switches for optional subsystems. Unset flags fall back to the
|
||||
environment default. {note}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{isLoading && (
|
||||
<p className="text-sm text-muted-foreground">Loading feature flags…</p>
|
||||
)}
|
||||
{!isLoading && flags.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">No feature flags available.</p>
|
||||
)}
|
||||
{flags.map((flag, i) => (
|
||||
<div key={flag.key}>
|
||||
{i > 0 && <Separator className="mb-4" />}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<Label htmlFor={`flag-${flag.key}`}>{flag.label}</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{FLAG_DESCRIPTIONS[flag.key] ?? flag.key}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id={`flag-${flag.key}`}
|
||||
checked={flag.enabled}
|
||||
disabled={toggleMutation.isPending}
|
||||
onCheckedChange={(checked) =>
|
||||
toggleMutation.mutate({ key: flag.key, enabled: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,17 @@ export interface SettingsResponse {
|
||||
settings: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface FeatureFlag {
|
||||
key: string;
|
||||
label: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface FeatureFlagsResponse {
|
||||
flags: FeatureFlag[];
|
||||
note: string;
|
||||
}
|
||||
|
||||
export const settingsApi = {
|
||||
// GET /api/settings — all runtime-editable settings as a flat key→value map.
|
||||
getAll: async (): Promise<Record<string, string>> => {
|
||||
@@ -15,4 +26,13 @@ export const settingsApi = {
|
||||
const { data } = await api.put<SettingsResponse>(`/settings/${key}`, { value });
|
||||
return data.settings;
|
||||
},
|
||||
// GET /api/settings/feature-flags — effective flag values (override, else env).
|
||||
getFeatureFlags: async (): Promise<FeatureFlagsResponse> => {
|
||||
const { data } = await api.get<FeatureFlagsResponse>("/settings/feature-flags");
|
||||
return data;
|
||||
},
|
||||
// PUT /api/settings/{key} — persist a feature flag as "true"/"false".
|
||||
setFeatureFlag: async (key: string, enabled: boolean): Promise<void> => {
|
||||
await api.put<SettingsResponse>(`/settings/${key}`, { value: enabled ? "true" : "false" });
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user