/ frontend / src / app / views / settings / Settings.jsx
Settings.jsx
  1  import { useState, useEffect, useRef } from "react";
  2  import {
  3    Grid, styled, Box, Card, Divider, TextField, Button,
  4    Switch, FormControlLabel, FormHelperText, Typography, Select, MenuItem, InputLabel, FormControl,
  5    Collapse, IconButton
  6  } from "@mui/material";
  7  import useAuth from "app/hooks/useAuth";
  8  import Breadcrumb from "app/components/Breadcrumb";
  9  import { useTranslation } from "react-i18next";
 10  import { toast } from "react-toastify";
 11  import { usePlatformCapabilities } from "app/contexts/PlatformContext";
 12  import api from "app/utils/api";
 13  import { Settings as SettingsIcon, Storage, Security, ExpandMore, ExpandLess } from "@mui/icons-material";
 14  import { H4 } from "app/components/Typography";
 15  import ProjectTabNav from "app/views/projects/components/ProjectTabNav";
 16  
 17  const Container = styled("div")(({ theme }) => ({
 18    margin: 10,
 19    [theme.breakpoints.down("sm")]: { margin: 16 },
 20    "& .breadcrumb": { marginBottom: 30, [theme.breakpoints.down("sm")]: { marginBottom: 16 } }
 21  }));
 22  
 23  const FlexBox = styled(Box)({
 24    display: "flex",
 25    alignItems: "center"
 26  });
 27  
 28  export default function SettingsPage() {
 29    const { t } = useTranslation();
 30    const auth = useAuth();
 31    const { refreshCapabilities } = usePlatformCapabilities();
 32    const [active, setActive] = useState("general");
 33  
 34    const TABS = [
 35      { key: "general", name: t("settings.sections.general"), Icon: SettingsIcon },
 36      { key: "authentication", name: t("settings.sections.authentication"), Icon: Security },
 37    ];
 38  
 39    const [form, setForm] = useState({
 40      app_name: "RESTai",
 41      hide_branding: false,
 42      proxy_enabled: false,
 43      proxy_url: "",
 44      proxy_key: "",
 45      proxy_team_id: "",
 46      max_audio_upload_size: 10,
 47      data_retention_days: 0,
 48      currency: "EUR",
 49      redis_host: "",
 50      redis_port: "6379",
 51      redis_password: "",
 52      redis_database: "0",
 53      auth_disable_local: false,
 54      sso_auto_create_user: false,
 55      sso_allowed_domains: "*",
 56      sso_google_client_id: "",
 57      sso_google_client_secret: "",
 58      sso_google_redirect_uri: "",
 59      sso_google_scope: "openid email profile",
 60      sso_microsoft_client_id: "",
 61      sso_microsoft_client_secret: "",
 62      sso_microsoft_tenant_id: "",
 63      sso_microsoft_redirect_uri: "",
 64      sso_microsoft_scope: "openid email profile",
 65      sso_github_client_id: "",
 66      sso_github_client_secret: "",
 67      sso_github_redirect_uri: "",
 68      sso_github_scope: "user:email",
 69      sso_oidc_client_id: "",
 70      sso_oidc_client_secret: "",
 71      sso_oidc_provider_url: "",
 72      sso_oidc_redirect_uri: "",
 73      sso_oidc_scopes: "openid email profile",
 74      sso_oidc_provider_name: "SSO",
 75      sso_auto_restricted: true,
 76      sso_auto_team_id: "",
 77      sso_oidc_email_claim: "email",
 78      mcp_enabled: false,
 79      docker_enabled: false,
 80      docker_url: "",
 81      docker_image: "python:3.12-slim",
 82      docker_timeout: 900,
 83      docker_network: "none",
 84      docker_read_only: true,
 85      browser_enabled: false,
 86      browser_image: "mcr.microsoft.com/playwright/python:v1.48.0-jammy",
 87      browser_network: "bridge",
 88      browser_timeout: 900,
 89      system_llm: "",
 90      enforce_2fa: false,
 91    });
 92    const [teams, setTeams] = useState([]);
 93    const [llms, setLlms] = useState([]);
 94    const [saving, setSaving] = useState(false);
 95    const [dockerTest, setDockerTest] = useState(null); // null | "testing" | {status, detail}
 96    const [telemetryEnabled, setTelemetryEnabled] = useState(null);
 97    const [expanded, setExpanded] = useState({ google: false, microsoft: false, github: false, oidc: false });
 98  
 99    // Refs for deep-linkable sections. Hash like `#microsoft` auto-opens
100    // the Authentication tab, expands the Microsoft card, and scrolls to
101    // it. Useful for copy-pasting "go to my SSO setup" links in Slack /
102    // support tickets.
103    const sectionRefs = {
104      google: useRef(null),
105      microsoft: useRef(null),
106      github: useRef(null),
107      oidc: useRef(null),
108    };
109  
110    // Map each deep-linkable hash to the tab it lives on. Extend this
111    // when you add a new section that deserves a bookmark.
112    const _SECTION_TAB = {
113      google: "authentication",
114      microsoft: "authentication",
115      github: "authentication",
116      oidc: "authentication",
117    };
118  
119    const toggleExpanded = (section) => () => {
120      setExpanded((prev) => ({ ...prev, [section]: !prev[section] }));
121      // Reflect the current section in the URL so the user can bookmark /
122      // share it. `replaceState` instead of `pushState` so expand/collapse
123      // clicks don't pollute history.
124      if (typeof window !== "undefined") {
125        const targetHash = expanded[section] ? "" : `#${section}`;
126        window.history.replaceState(null, "", window.location.pathname + window.location.search + targetHash);
127      }
128    };
129  
130    // Drive the tab + section state from `window.location.hash`. Runs on
131    // mount and on hashchange so users who paste a deep-link mid-session
132    // (or hit the browser back button) land on the right section.
133    useEffect(() => {
134      const applyHash = () => {
135        const hash = (window.location.hash || "").replace(/^#/, "");
136        if (!hash) return;
137        const targetTab = _SECTION_TAB[hash];
138        if (targetTab) setActive(targetTab);
139        if (hash in sectionRefs) {
140          setExpanded((prev) => ({ ...prev, [hash]: true }));
141          // Give React a beat to render the expanded card before scrolling.
142          setTimeout(() => {
143            sectionRefs[hash].current?.scrollIntoView({ behavior: "smooth", block: "start" });
144          }, 80);
145        }
146      };
147      applyHash();
148      window.addEventListener("hashchange", applyHash);
149      return () => window.removeEventListener("hashchange", applyHash);
150      // eslint-disable-next-line react-hooks/exhaustive-deps
151    }, []);
152  
153    const fetchSettings = () => {
154      api.get("/settings", auth.user.token)
155        .then((data) => setForm(data))
156        .catch(() => {});
157    };
158  
159    useEffect(() => {
160      document.title = "RESTai - Settings";
161      fetchSettings();
162      api.get("/teams", auth.user.token).then((d) => setTeams(d.teams || [])).catch(() => {});
163      api.get("/llms", auth.user.token).then((d) => setLlms(Array.isArray(d) ? d : (d?.llms || []))).catch(() => {});
164      api.get("/version", auth.user.token, { silent: true }).then((d) => { if (d) setTelemetryEnabled(d.telemetry); }).catch(() => {});
165    }, []);
166  
167    const handleChange = (field) => (e) => {
168      const value = e.target.type === "checkbox" ? e.target.checked : e.target.value;
169      setForm((prev) => ({ ...prev, [field]: value }));
170    };
171  
172    const handleSave = () => {
173      setSaving(true);
174      const body = { ...form };
175      body.max_audio_upload_size = parseInt(body.max_audio_upload_size, 10) || 10;
176      body.data_retention_days = parseInt(body.data_retention_days, 10) || 0;
177      body.docker_timeout = parseInt(body.docker_timeout, 10) || 900;
178  
179      api.patch("/settings", body, auth.user.token)
180        .then((data) => {
181          setForm(data);
182          toast.success(t("settings.saved"));
183          refreshCapabilities();
184        })
185        .catch(() => {})
186        .finally(() => setSaving(false));
187    };
188  
189    const CollapsibleCardHeader = ({ icon: Icon, title, section }) => (
190      <FlexBox sx={{ cursor: "pointer" }} onClick={toggleExpanded(section)}>
191        <Icon sx={{ ml: 2 }} />
192        <H4 sx={{ p: 2, flex: 1 }}>{title}</H4>
193        <IconButton size="small" sx={{ mr: 2 }}>
194          {expanded[section] ? <ExpandLess /> : <ExpandMore />}
195        </IconButton>
196      </FlexBox>
197    );
198  
199    return (
200      <Container>
201        <Box className="breadcrumb">
202          <Breadcrumb routeSegments={[{ name: t("settings.title"), path: "/settings" }]} />
203        </Box>
204  
205        <Grid container spacing={3}>
206          <Grid item md={2} xs={12}>
207            <ProjectTabNav tabs={TABS} active={active} setActive={setActive} />
208          </Grid>
209  
210          <Grid item md={10} xs={12}>
211            {/* ===== GENERAL TAB ===== */}
212            {active === "general" && (
213              <Grid container spacing={3}>
214                {/* App */}
215                <Grid item xs={12}>
216                  <Card elevation={1} sx={{ p: 3 }}>
217                    <Typography variant="subtitle1" fontWeight={600} sx={{ mb: 2 }}>{t("settings.sections.platform")}</Typography>
218                    <Grid container spacing={3}>
219                      <Grid item xs={12} md={6}>
220                        <TextField fullWidth label={t("settings.fields.appName")} value={form.app_name} onChange={handleChange("app_name")} />
221                      </Grid>
222                      <Grid item xs={12} md={3}>
223                        <FormControlLabel
224                          control={<Switch checked={form.hide_branding} onChange={handleChange("hide_branding")} />}
225                          label={t("settings.fields.hideBranding")}
226                        />
227                      </Grid>
228                      <Grid item xs={12} md={3}>
229                        <FormControl fullWidth>
230                          <InputLabel>{t("settings.fields.currency")}</InputLabel>
231                          <Select value={form.currency} label={t("settings.fields.currency")} onChange={handleChange("currency")}>
232                            <MenuItem value="USD">USD ($)</MenuItem>
233                            <MenuItem value="EUR">EUR (&euro;)</MenuItem>
234                          </Select>
235                        </FormControl>
236                      </Grid>
237                      <Grid item xs={12} md={6}>
238                        <FormControl fullWidth>
239                          <InputLabel>{t("settings.fields.systemLlm")}</InputLabel>
240                          <Select
241                            value={form.system_llm ?? ""}
242                            label={t("settings.fields.systemLlm")}
243                            onChange={handleChange("system_llm")}
244                          >
245                            <MenuItem value=""><em>{t("common.none")}</em></MenuItem>
246                            {llms.map((l) => (
247                              <MenuItem key={l.id || l.name} value={l.name}>{l.name}</MenuItem>
248                            ))}
249                          </Select>
250                        </FormControl>
251                        <Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: "block" }}>
252                          {t("settings.helpers.systemLlm")}
253                        </Typography>
254                      </Grid>
255                    </Grid>
256                  </Card>
257                </Grid>
258  
259                {/* LLM Proxy */}
260                <Grid item xs={12}>
261                  <Card elevation={1} sx={{ p: 3 }}>
262                    <Typography variant="subtitle1" fontWeight={600} sx={{ mb: 2 }}>{t("settings.sections.proxy")}</Typography>
263                    <Grid container spacing={3}>
264                      <Grid item xs={12}>
265                        <FormControlLabel
266                          control={<Switch checked={form.proxy_enabled} onChange={handleChange("proxy_enabled")} />}
267                          label={t("settings.fields.enableProxy")}
268                        />
269                      </Grid>
270                      {form.proxy_enabled && (
271                        <>
272                          <Grid item xs={12} md={4}>
273                            <TextField fullWidth label={t("settings.fields.proxyUrl")} value={form.proxy_url} onChange={handleChange("proxy_url")} />
274                          </Grid>
275                          <Grid item xs={12} md={4}>
276                            <TextField fullWidth label={t("settings.fields.proxyKey")} type="password" value={form.proxy_key} onChange={handleChange("proxy_key")} />
277                          </Grid>
278                          <Grid item xs={12} md={4}>
279                            <TextField fullWidth label={t("settings.fields.proxyTeamId")} value={form.proxy_team_id} onChange={handleChange("proxy_team_id")} />
280                          </Grid>
281                        </>
282                      )}
283                    </Grid>
284                  </Card>
285                </Grid>
286  
287                {/* Limits */}
288                <Grid item xs={12}>
289                  <Card elevation={1} sx={{ p: 3 }}>
290                    <Typography variant="subtitle1" fontWeight={600} sx={{ mb: 2 }}>{t("settings.sections.limits")}</Typography>
291                    <Grid container spacing={3}>
292                      <Grid item xs={12} md={6}>
293                        <TextField fullWidth label={t("settings.fields.maxAudioUploadSize")} type="number" inputProps={{ min: 1 }}
294                          value={form.max_audio_upload_size} onChange={handleChange("max_audio_upload_size")} />
295                      </Grid>
296                      <Grid item xs={12} md={6}>
297                        <TextField fullWidth label={t("settings.fields.dataRetentionDays")} type="number" inputProps={{ min: 0 }}
298                          value={form.data_retention_days} onChange={handleChange("data_retention_days")}
299                          helperText={t("settings.helpers.retention")} />
300                      </Grid>
301                    </Grid>
302                  </Card>
303                </Grid>
304  
305                {/* Redis */}
306                <Grid item xs={12}>
307                  <Card elevation={1} sx={{ p: 3 }}>
308                    <Typography variant="subtitle1" fontWeight={600} sx={{ mb: 2 }}>{t("settings.sections.redis")}</Typography>
309                    <Grid container spacing={3}>
310                      <Grid item xs={12} md={6}>
311                        <TextField fullWidth label={t("settings.fields.redisHost")} placeholder={t("settings.fields.redisHostPlaceholder")}
312                          value={form.redis_host} onChange={handleChange("redis_host")} />
313                      </Grid>
314                      <Grid item xs={12} md={2}>
315                        <TextField fullWidth label={t("settings.fields.redisPort")} value={form.redis_port} onChange={handleChange("redis_port")} />
316                      </Grid>
317                      <Grid item xs={12} md={2}>
318                        <TextField fullWidth label={t("settings.fields.redisPassword")} type="password" value={form.redis_password} onChange={handleChange("redis_password")} />
319                      </Grid>
320                      <Grid item xs={12} md={2}>
321                        <TextField fullWidth label={t("settings.fields.redisDatabase")} value={form.redis_database} onChange={handleChange("redis_database")} />
322                      </Grid>
323                    </Grid>
324                  </Card>
325                </Grid>
326  
327                {/* MCP */}
328                <Grid item xs={12}>
329                  <Card elevation={1} sx={{ p: 3 }}>
330                    <Typography variant="subtitle1" fontWeight={600} sx={{ mb: 1 }}>{t("settings.sections.mcp")}</Typography>
331                    <FormControlLabel
332                      control={<Switch checked={form.mcp_enabled} onChange={handleChange("mcp_enabled")} />}
333                      label={t("settings.fields.enableMcp")}
334                    />
335                    <Typography variant="caption" color="text.secondary" display="block">
336                      Expose projects as MCP tools at /mcp/sse. Requires server restart.
337                    </Typography>
338                  </Card>
339                </Grid>
340  
341                {/* Docker */}
342                <Grid item xs={12}>
343                  <Card elevation={1} sx={{ p: 3 }}>
344                    <Typography variant="subtitle1" fontWeight={600} sx={{ mb: 1 }}>{t("settings.sections.docker")}</Typography>
345                    <FormControlLabel
346                      control={<Switch checked={form.docker_enabled} onChange={handleChange("docker_enabled")} />}
347                      label={t("settings.fields.enableDocker")}
348                    />
349                    <Typography variant="caption" color="text.secondary" display="block" sx={{ mb: 2 }}>
350                      Each chat session gets its own isolated container that persists across commands and is automatically removed after the idle timeout.
351                    </Typography>
352                    {form.docker_enabled && (
353                    <Grid container spacing={3}>
354                      <Grid item xs={12} md={6}>
355                        <TextField fullWidth label={t("settings.fields.dockerUrl")}
356                          value={form.docker_url || ""}
357                          onChange={handleChange("docker_url")}
358                          placeholder={t("settings.fields.dockerUrlPlaceholder")}
359                        />
360                      </Grid>
361                      <Grid item xs={12} md={6}>
362                        <TextField fullWidth label={t("settings.fields.containerImage")}
363                          value={form.docker_image ?? "python:3.12-slim"}
364                          onChange={handleChange("docker_image")}
365                          helperText={t("settings.helpers.containerImage")}
366                        />
367                      </Grid>
368                      <Grid item xs={12} md={6}>
369                        <TextField fullWidth label={t("settings.fields.idleTimeout")} type="number" inputProps={{ min: 60 }}
370                          value={form.docker_timeout || 900}
371                          onChange={handleChange("docker_timeout")}
372                          helperText={t("settings.helpers.dockerTimeout")}
373                        />
374                      </Grid>
375                      <Grid item xs={12} md={6}>
376                        <TextField fullWidth label={t("settings.fields.networkMode")}
377                          value={form.docker_network ?? "none"}
378                          onChange={handleChange("docker_network")}
379                          helperText={'"none" for no network access, "bridge" for internet access, or a custom network name'}
380                        />
381                      </Grid>
382                      <Grid item xs={12}>
383                        <FormControlLabel
384                          control={<Switch checked={!!form.docker_read_only} onChange={handleChange("docker_read_only")} />}
385                          label={t("settings.fields.readOnlyRootfs")}
386                        />
387                        <FormHelperText sx={{ mt: -0.5, ml: 4 }}>
388                          Safer sandbox. Disable to let the LLM run <code>pip install</code> and write to the container filesystem.
389                        </FormHelperText>
390                      </Grid>
391                      <Grid item xs={12}>
392                        <Button
393                          variant="outlined"
394                          size="small"
395                          disabled={!form.docker_url || dockerTest === "testing"}
396                          onClick={() => {
397                            setDockerTest("testing");
398                            api.post("/settings/docker/test", {}, auth.user.token)
399                              .then((d) => setDockerTest({ status: "ok", detail: `Connected (Docker ${d.server_version})` }))
400                              .catch((e) => setDockerTest({ status: "error", detail: e?.detail || "Connection failed" }));
401                          }}
402                        >
403                          {dockerTest === "testing" ? t("settings.fields.testing") : t("settings.fields.testDocker")}
404                        </Button>
405                        {dockerTest && dockerTest !== "testing" && (
406                          <Typography
407                            variant="caption"
408                            sx={{ ml: 2, color: dockerTest.status === "ok" ? "success.main" : "error.main" }}
409                          >
410                            {dockerTest.detail}
411                          </Typography>
412                        )}
413                      </Grid>
414                    </Grid>
415                    )}
416                  </Card>
417                </Grid>
418  
419                {/* Agentic Browser — Playwright + Chromium per-chat container */}
420                <Grid item xs={12}>
421                  <Card elevation={1} sx={{ p: 3 }}>
422                    <Typography variant="subtitle1" fontWeight={600} sx={{ mb: 2 }}>{t("settings.sections.browser")}</Typography>
423                    <Grid container spacing={3}>
424                      <Grid item xs={12}>
425                        <FormControlLabel
426                          control={<Switch checked={!!form.browser_enabled} onChange={handleChange("browser_enabled")} />}
427                          label={t("settings.fields.enableBrowser")}
428                        />
429                        <FormHelperText sx={{ mt: -0.5, ml: 4 }}>
430                          Powers the <code>browser_*</code> builtin tools (goto, fill, click, screenshot, etc.).
431                          Reuses the Docker daemon configured above. Image is pulled on first use.
432                        </FormHelperText>
433                      </Grid>
434                      {form.browser_enabled && (
435                        <>
436                          <Grid item xs={12} md={6}>
437                            <TextField
438                              fullWidth
439                              label={t("settings.fields.browserImage")}
440                              value={form.browser_image ?? "mcr.microsoft.com/playwright/python:v1.48.0-jammy"}
441                              onChange={handleChange("browser_image")}
442                              helperText={t("settings.helpers.browserImage")}
443                            />
444                          </Grid>
445                          <Grid item xs={12} md={3}>
446                            <TextField
447                              fullWidth
448                              label={t("settings.fields.browserNetwork")}
449                              value={form.browser_network ?? "bridge"}
450                              onChange={handleChange("browser_network")}
451                              helperText={"'bridge' for outbound (required — Chromium fetches remote sites). 'none' = offline only."}
452                            />
453                          </Grid>
454                          <Grid item xs={12} md={3}>
455                            <TextField
456                              fullWidth
457                              type="number"
458                              label={t("settings.fields.browserTimeout")}
459                              inputProps={{ min: 60 }}
460                              value={form.browser_timeout ?? 900}
461                              onChange={handleChange("browser_timeout")}
462                              helperText={t("settings.helpers.browserTimeout")}
463                            />
464                          </Grid>
465                        </>
466                      )}
467                    </Grid>
468                  </Card>
469                </Grid>
470              </Grid>
471            )}
472  
473            {/* ===== AUTHENTICATION TAB ===== */}
474            {active === "authentication" && (
475              <Grid container spacing={3}>
476                {/* Core auth settings */}
477                <Grid item xs={12}>
478                  <Card elevation={1} sx={{ p: 3 }}>
479                    <Typography variant="subtitle1" fontWeight={600} sx={{ mb: 2 }}>{t("settings.sections.localAuth")}</Typography>
480                    <Grid container spacing={3}>
481                      <Grid item xs={12} md={4}>
482                        <FormControlLabel
483                          control={<Switch checked={form.auth_disable_local} onChange={handleChange("auth_disable_local")} />}
484                          label={t("settings.fields.disableLocalAuth")}
485                        />
486                        <Typography variant="caption" color="text.secondary" display="block">
487                          When enabled, only SSO login is allowed
488                        </Typography>
489                      </Grid>
490                      <Grid item xs={12} md={4}>
491                        <FormControlLabel
492                          control={<Switch checked={form.enforce_2fa} onChange={handleChange("enforce_2fa")} />}
493                          label={t("settings.fields.enforce2fa")}
494                        />
495                        <Typography variant="caption" color="text.secondary" display="block">
496                          Require TOTP two-factor authentication for all local users
497                        </Typography>
498                      </Grid>
499                    </Grid>
500                  </Card>
501                </Grid>
502  
503                {/* SSO general */}
504                <Grid item xs={12}>
505                  <Card elevation={1} sx={{ p: 3 }}>
506                    <Typography variant="subtitle1" fontWeight={600} sx={{ mb: 2 }}>Single Sign-On</Typography>
507                    <Grid container spacing={3}>
508                      <Grid item xs={12} md={4}>
509                        <FormControlLabel
510                          control={<Switch checked={form.sso_auto_create_user} onChange={handleChange("sso_auto_create_user")} />}
511                          label={t("settings.fields.autoCreateUsers")}
512                        />
513                        <Typography variant="caption" color="text.secondary" display="block">
514                          Automatically create user accounts on first SSO login
515                        </Typography>
516                      </Grid>
517                      <Grid item xs={12} md={4}>
518                        <TextField fullWidth label={t("settings.fields.allowedDomains")} value={form.sso_allowed_domains}
519                          onChange={handleChange("sso_allowed_domains")} helperText={t("settings.helpers.allowedDomains")} />
520                      </Grid>
521                      <Grid item xs={12} md={4}>
522                        <FormControlLabel
523                          control={<Switch checked={form.sso_auto_restricted} onChange={handleChange("sso_auto_restricted")} />}
524                          label={t("settings.fields.restrictNewUsers")}
525                        />
526                        <Typography variant="caption" color="text.secondary" display="block">
527                          Auto-created SSO users will be in restricted (read-only) mode
528                        </Typography>
529                      </Grid>
530                      <Grid item xs={12} md={4}>
531                        <FormControl fullWidth>
532                          <InputLabel>{t("settings.fields.defaultTeam")}</InputLabel>
533                          <Select value={form.sso_auto_team_id || ""} onChange={handleChange("sso_auto_team_id")} label={t("settings.fields.defaultTeam")}>
534                            <MenuItem value="">{t("common.none")}</MenuItem>
535                            {teams.map((team) => (
536                              <MenuItem key={team.id} value={String(team.id)}>{team.name}</MenuItem>
537                            ))}
538                          </Select>
539                        </FormControl>
540                        <Typography variant="caption" color="text.secondary" display="block" sx={{ mt: 0.5 }}>
541                          {t("settings.helpers.autoTeam")}
542                        </Typography>
543                      </Grid>
544                    </Grid>
545                  </Card>
546                </Grid>
547  
548                {/* SSO — Google */}
549                <Grid item xs={12}>
550                  <Card elevation={1} ref={sectionRefs.google}>
551                    <CollapsibleCardHeader icon={Security} title="Google" section="google" />
552                    <Collapse in={expanded.google}>
553                      <Divider />
554                      <Box sx={{ p: 3 }}>
555                        <Grid container spacing={3}>
556                          <Grid item xs={12} md={6}>
557                            <TextField fullWidth label={t("settings.fields.clientId")} value={form.sso_google_client_id} onChange={handleChange("sso_google_client_id")} />
558                          </Grid>
559                          <Grid item xs={12} md={6}>
560                            <TextField fullWidth label={t("settings.fields.clientSecret")} type="password" value={form.sso_google_client_secret} onChange={handleChange("sso_google_client_secret")} />
561                          </Grid>
562                          <Grid item xs={12} md={6}>
563                            <TextField fullWidth label={t("settings.fields.redirectUri")} value={form.sso_google_redirect_uri} onChange={handleChange("sso_google_redirect_uri")} />
564                          </Grid>
565                          <Grid item xs={12} md={6}>
566                            <TextField fullWidth label={t("settings.fields.scope")} value={form.sso_google_scope} onChange={handleChange("sso_google_scope")} />
567                          </Grid>
568                        </Grid>
569                      </Box>
570                    </Collapse>
571                  </Card>
572                </Grid>
573  
574                {/* SSO — Microsoft */}
575                <Grid item xs={12}>
576                  <Card elevation={1} ref={sectionRefs.microsoft}>
577                    <CollapsibleCardHeader icon={Security} title="Microsoft" section="microsoft" />
578                    <Collapse in={expanded.microsoft}>
579                      <Divider />
580                      <Box sx={{ p: 3 }}>
581                        <Grid container spacing={3}>
582                          <Grid item xs={12} md={6}>
583                            <TextField fullWidth label={t("settings.fields.clientId")} value={form.sso_microsoft_client_id} onChange={handleChange("sso_microsoft_client_id")} />
584                          </Grid>
585                          <Grid item xs={12} md={6}>
586                            <TextField fullWidth label={t("settings.fields.clientSecret")} type="password" value={form.sso_microsoft_client_secret} onChange={handleChange("sso_microsoft_client_secret")} />
587                          </Grid>
588                          <Grid item xs={12} md={6}>
589                            <TextField fullWidth label={t("settings.fields.tenantId")} value={form.sso_microsoft_tenant_id} onChange={handleChange("sso_microsoft_tenant_id")} />
590                          </Grid>
591                          <Grid item xs={12} md={6}>
592                            <TextField fullWidth label={t("settings.fields.redirectUri")} value={form.sso_microsoft_redirect_uri} onChange={handleChange("sso_microsoft_redirect_uri")} />
593                          </Grid>
594                          <Grid item xs={12} md={6}>
595                            <TextField fullWidth label={t("settings.fields.scope")} value={form.sso_microsoft_scope} onChange={handleChange("sso_microsoft_scope")} />
596                          </Grid>
597                        </Grid>
598                      </Box>
599                    </Collapse>
600                  </Card>
601                </Grid>
602  
603                {/* SSO — GitHub */}
604                <Grid item xs={12}>
605                  <Card elevation={1} ref={sectionRefs.github}>
606                    <CollapsibleCardHeader icon={Security} title="GitHub" section="github" />
607                    <Collapse in={expanded.github}>
608                      <Divider />
609                      <Box sx={{ p: 3 }}>
610                        <Grid container spacing={3}>
611                          <Grid item xs={12} md={6}>
612                            <TextField fullWidth label={t("settings.fields.clientId")} value={form.sso_github_client_id} onChange={handleChange("sso_github_client_id")} />
613                          </Grid>
614                          <Grid item xs={12} md={6}>
615                            <TextField fullWidth label={t("settings.fields.clientSecret")} type="password" value={form.sso_github_client_secret} onChange={handleChange("sso_github_client_secret")} />
616                          </Grid>
617                          <Grid item xs={12} md={6}>
618                            <TextField fullWidth label={t("settings.fields.redirectUri")} value={form.sso_github_redirect_uri} onChange={handleChange("sso_github_redirect_uri")} />
619                          </Grid>
620                          <Grid item xs={12} md={6}>
621                            <TextField fullWidth label={t("settings.fields.scope")} value={form.sso_github_scope} onChange={handleChange("sso_github_scope")} />
622                          </Grid>
623                        </Grid>
624                      </Box>
625                    </Collapse>
626                  </Card>
627                </Grid>
628  
629                {/* SSO — Generic OIDC */}
630                <Grid item xs={12}>
631                  <Card elevation={1} ref={sectionRefs.oidc}>
632                    <CollapsibleCardHeader icon={Security} title={t("settings.sections.oidc")} section="oidc" />
633                    <Collapse in={expanded.oidc}>
634                      <Divider />
635                      <Box sx={{ p: 3 }}>
636                        <Grid container spacing={3}>
637                          <Grid item xs={12} md={6}>
638                            <TextField fullWidth label={t("settings.fields.clientId")} value={form.sso_oidc_client_id} onChange={handleChange("sso_oidc_client_id")} />
639                          </Grid>
640                          <Grid item xs={12} md={6}>
641                            <TextField fullWidth label={t("settings.fields.clientSecret")} type="password" value={form.sso_oidc_client_secret} onChange={handleChange("sso_oidc_client_secret")} />
642                          </Grid>
643                          <Grid item xs={12} md={6}>
644                            <TextField fullWidth label={t("settings.fields.providerUrl")} value={form.sso_oidc_provider_url}
645                              onChange={handleChange("sso_oidc_provider_url")} helperText={t("settings.helpers.oidcProviderUrl")} />
646                          </Grid>
647                          <Grid item xs={12} md={6}>
648                            <TextField fullWidth label={t("settings.fields.redirectUri")} value={form.sso_oidc_redirect_uri} onChange={handleChange("sso_oidc_redirect_uri")} />
649                          </Grid>
650                          <Grid item xs={12} md={6}>
651                            <TextField fullWidth label={t("settings.fields.scopes")} value={form.sso_oidc_scopes} onChange={handleChange("sso_oidc_scopes")} />
652                          </Grid>
653                          <Grid item xs={12} md={6}>
654                            <TextField fullWidth label={t("settings.fields.providerName")} value={form.sso_oidc_provider_name}
655                              onChange={handleChange("sso_oidc_provider_name")} helperText={t("settings.helpers.oidcProviderName")} />
656                          </Grid>
657                          <Grid item xs={12} md={6}>
658                            <TextField fullWidth label={t("settings.fields.emailClaim")} value={form.sso_oidc_email_claim} onChange={handleChange("sso_oidc_email_claim")} />
659                          </Grid>
660                        </Grid>
661                      </Box>
662                    </Collapse>
663                  </Card>
664                </Grid>
665              </Grid>
666            )}
667  
668            {/* Telemetry */}
669            <Grid item xs={12}>
670              <Card elevation={1} sx={{ p: 3 }}>
671                <Typography variant="subtitle1" fontWeight={600} sx={{ mb: 1 }}>{t("settings.sections.telemetry")}</Typography>
672                <Typography variant="body2" color="text.secondary">
673                  {t("settings.telemetry.description")}
674                </Typography>
675                <Typography variant="body2" sx={{ mt: 1 }}>
676                  {t("settings.helpers.telemetryStatus", { status: telemetryEnabled === null ? "..." : telemetryEnabled ? t("settings.status.enabled") : t("settings.status.disabled") })}
677                </Typography>
678                <Typography variant="caption" color="text.secondary" display="block" sx={{ mt: 0.5 }}>
679                  {t("settings.telemetry.optOut")}
680                </Typography>
681              </Card>
682            </Grid>
683  
684            {/* Save button — always visible */}
685            <Box sx={{ display: "flex", justifyContent: "flex-end", mt: 3 }}>
686              <Button variant="contained" color="primary" onClick={handleSave} disabled={saving}>
687                {saving ? t("settings.helpers.saving") : t("settings.helpers.saveSettings")}
688              </Button>
689            </Box>
690          </Grid>
691        </Grid>
692      </Container>
693    );
694  }