Files
ss-tools/frontend/src/routes/settings/FeaturesSettings.svelte
busya 143f14d516 chore: remainder — backend test infra, agent config, docker, i18n, frontend ui
- Backend: alembic env, config manager/models, dependencies, translate plugin
- Backend tests: async_sync_regression, integration tests, git services, test_agent
- Docker: docker-compose.yml updates
- Agent: qa-tester.md update, semantics-testing SKILL.md update
- Frontend: TopNavbar, sidebarNavigation, FeaturesSettings, FeatureGate
- i18n: assistant.json en/ru locale updates
- New: frontend/src/lib/components/agent/ directory
2026-06-10 15:06:36 +03:00

155 lines
6.0 KiB
Svelte

<!-- #region FeaturesSettings [C:3] [TYPE Component] [SEMANTICS settings, features, toggle, enable] -->
<!-- @ingroup Routes -->
<!-- @BRIEF Dynamic feature flags configuration tab: enable/disable all application features. -->
<!-- @LAYER UI -->
<!-- @PRE: settings object with features config is provided. -->
<!-- @POST: User can toggle all feature flags dynamically rendered from FeaturesConfig. -->
<!-- @RATIONALE Dynamic rendering: features are derived from backend FeaturesConfig model.
Each feature gets a toggle with i18n label if available. -->
<script lang="ts">
import { t } from "$lib/i18n/index.svelte.js";
import HelpTooltip from "$lib/ui/HelpTooltip.svelte";
let { settings = $bindable(), onSave } = $props();
// Feature descriptors: ordered list for UI display.
// Each entry maps to settings.features.<id> with optional i18n keys.
// i18n keys follow pattern: feature_<id>, feature_<id>_hint, help_feature_<id>
const FEATURES = [
// Core features
{ id: "dataset_review", category: "core" },
{ id: "health_monitor", category: "core" },
// Translation subsystem
{ id: "translate", category: "translate" },
{ id: "translate_scheduling", category: "translate" },
{ id: "translate_dictionaries", category: "translate" },
// Dashboard migration & version control
{ id: "migration", category: "migration" },
{ id: "git_integration", category: "migration" },
// System tools
{ id: "backup", category: "tools" },
{ id: "dataset_mapper", category: "tools" },
{ id: "debug", category: "tools" },
{ id: "storage_manager", category: "tools" },
{ id: "search_datasets", category: "tools" },
{ id: "maintenance_banner", category: "tools" },
// LLM analysis tools
{ id: "llm_dashboard_validation", category: "llm" },
{ id: "llm_documentation", category: "llm" },
];
// Category labels
const CATEGORIES: Record<string, string> = {
core: "Core",
translate: "Translation",
migration: "Migration & Version Control",
tools: "System Tools",
llm: "LLM Analysis",
};
function featureLabel(id: string): string {
// Try i18n first
const key = `feature_${id}` as keyof typeof $t.settings;
const i18nVal = $t.settings?.[key];
if (i18nVal && typeof i18nVal === "string") return i18nVal;
// Fallback: human-readable from snake_case
return id
.replace(/_/g, " ")
.replace(/\b\w/g, (c) => c.toUpperCase());
}
function featureHint(id: string): string {
const key = `feature_${id}_hint` as keyof typeof $t.settings;
const i18nVal = $t.settings?.[key];
if (i18nVal && typeof i18nVal === "string") return i18nVal;
return "";
}
function featureHelp(id: string): string {
const key = `help_feature_${id}` as keyof typeof $t.settings;
const i18nVal = $t.settings?.[key];
if (i18nVal && typeof i18nVal === "string") return i18nVal;
return "";
}
function toggleFeature(id: string): void {
if (settings?.features && id in settings.features) {
settings.features[id] = !settings.features[id];
// Force reactivity
settings.features = { ...settings.features };
}
}
// Group features by category
const groupedFeatures = $derived.by(() => {
const grouped: Record<string, typeof FEATURES> = {};
for (const f of FEATURES) {
if (!grouped[f.category]) grouped[f.category] = [];
grouped[f.category].push(f);
}
return grouped;
});
</script>
<div class="text-lg font-medium mb-4">
<h2 class="text-xl font-bold mb-4">
{$t.settings?.features || "Features"}
</h2>
<p class="text-text-muted mb-6">
{$t.settings?.features_description || "Enable or disable application features. Disabled features are hidden from the UI and blocked at the API level."}
</p>
{#each Object.entries(groupedFeatures) as [category, features]}
<div class="mb-6">
<h3 class="text-lg font-semibold mb-3 text-text">
{CATEGORIES[category] || category}
</h3>
<div class="bg-surface-muted rounded-lg border border-border">
{#each features as feature, i}
<div class="flex items-center justify-between p-4" class:border-t={i > 0} class:border-border={i > 0}>
<div class="flex-1 mr-4">
<span class="flex items-center gap-1.5">
<label for="feature-{feature.id}" class="text-sm font-medium text-text">
{featureLabel(feature.id)}
</label>
{#if featureHelp(feature.id)}
<HelpTooltip text={featureHelp(feature.id)} />
{/if}
</span>
{#if featureHint(feature.id)}
<p class="text-xs text-text-muted mt-1">
{featureHint(feature.id)}
</p>
{/if}
</div>
<label class="relative inline-flex items-center cursor-pointer flex-shrink-0">
<input
id="feature-{feature.id}"
type="checkbox"
class="sr-only peer"
checked={settings?.features?.[feature.id] ?? true}
onchange={() => toggleFeature(feature.id)}
>
<div class="w-11 h-6 bg-surface-muted peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-ring rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-surface-card after:border-border-strong after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
</label>
</div>
{/each}
</div>
</div>
{/each}
<div class="mt-6 flex justify-end">
<button
onclick={onSave}
class="bg-primary text-white px-4 py-2 rounded hover:bg-primary-hover"
>
{$t.settings?.save_features || $t.settings?.save || "Save"}
</button>
</div>
</div>
<!-- #endregion FeaturesSettings -->