import { useEffect, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { PatchInstanceGeneralSettings, BackupRetentionPolicy } from "@paperclipai/shared"; import { DAILY_RETENTION_PRESETS, WEEKLY_RETENTION_PRESETS, MONTHLY_RETENTION_PRESETS, DEFAULT_BACKUP_RETENTION, } from "@paperclipai/shared"; import { LogOut, SlidersHorizontal } from "lucide-react"; import { healthApi } from "@/api/health"; import { instanceSettingsApi } from "@/api/instanceSettings"; import { ModeBadge } from "@/components/access/ModeBadge"; import { Button } from "../components/ui/button"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { queryKeys } from "../lib/queryKeys"; import { ToggleSwitch } from "@/components/ui/toggle-switch"; import { cn } from "../lib/utils"; import { useSignOut } from "@/hooks/useSignOut"; const FEEDBACK_TERMS_URL = import.meta.env.VITE_FEEDBACK_TERMS_URL?.trim() || "https://paperclip.ing/tos"; export function InstanceGeneralSettings({ embedded = false }: { embedded?: boolean }) { const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); const [actionError, setActionError] = useState(null); const signOutMutation = useSignOut(); useEffect(() => { if (embedded) return; setBreadcrumbs([ { label: "Settings", href: "/company/settings" }, { label: "General" }, ]); }, [embedded, setBreadcrumbs]); const generalQuery = useQuery({ queryKey: queryKeys.instance.generalSettings, queryFn: () => instanceSettingsApi.getGeneral(), }); const healthQuery = useQuery({ queryKey: queryKeys.health, queryFn: () => healthApi.get(), retry: false, }); const updateGeneralMutation = useMutation({ mutationFn: instanceSettingsApi.updateGeneral, onMutate: () => { setActionError(null); signOutMutation.reset(); }, onSuccess: async () => { setActionError(null); signOutMutation.reset(); await queryClient.invalidateQueries({ queryKey: queryKeys.instance.generalSettings }); }, onError: (error) => { setActionError(error instanceof Error ? error.message : "Failed to update general settings."); }, }); if (generalQuery.isLoading || healthQuery.isLoading) { return
Loading general settings...
; } if (generalQuery.error) { return (
{generalQuery.error instanceof Error ? generalQuery.error.message : "Failed to load general settings."}
); } const censorUsernameInLogs = generalQuery.data?.censorUsernameInLogs === true; const feedbackDataSharingPreference = generalQuery.data?.feedbackDataSharingPreference ?? "prompt"; const backupRetention: BackupRetentionPolicy = generalQuery.data?.backupRetention ?? DEFAULT_BACKUP_RETENTION; const hiddenSettings = new Set(healthQuery.data?.hiddenSettings ?? []); const showDeploymentStatus = !hiddenSettings.has("instance.general.deploymentStatus"); const showCensorUsernameInLogs = !hiddenSettings.has("instance.general.censorUsernameInLogs"); const showBackupRetention = !hiddenSettings.has("instance.general.backupRetention"); const showFeedbackDataSharing = !hiddenSettings.has("instance.general.feedbackDataSharingPreference"); const showSignOut = !hiddenSettings.has("instance.general.signOut"); const visibleTopics = [ ...(showCensorUsernameInLogs ? ["log display"] : []), ...(showBackupRetention ? ["backup retention"] : []), ...(showFeedbackDataSharing ? ["data sharing"] : []), ]; const topicSummary = visibleTopics.length > 2 ? `${visibleTopics.slice(0, -1).join(", ")}, and ${visibleTopics[visibleTopics.length - 1]}` : visibleTopics.join(" and "); const visibleActionError = signOutMutation.error instanceof Error ? signOutMutation.error.message : signOutMutation.error ? "Failed to sign out." : actionError; return (
{!embedded ? (

General

Configure instance-wide preferences {visibleTopics.length > 0 ? <> including {topicSummary} : null}.

) : null} {visibleActionError && (
{visibleActionError}
)} {showDeploymentStatus && (

Deployment and auth

{healthQuery.data?.deploymentMode === "local_trusted" ? "Local trusted mode is optimized for a local operator. Browser requests run as local board context and no sign-in is required." : healthQuery.data?.deploymentExposure === "public" ? "Authenticated public mode requires sign-in for board access and is intended for public URLs." : "Authenticated private mode requires sign-in and is intended for LAN, VPN, or other private-network deployments."}
)} {showCensorUsernameInLogs && (

Censor username in logs

Hide the username segment in home-directory paths and similar operator-visible log output. Standalone username mentions outside of paths are not yet masked in the live transcript view. This is off by default.

updateGeneralMutation.mutate({ censorUsernameInLogs: !censorUsernameInLogs })} disabled={updateGeneralMutation.isPending || signOutMutation.isPending} aria-label="Toggle username log censoring" />
)} {showBackupRetention && (

Backup retention

Configure how long automatic database backups are retained. Backups run roughly every hour and are compressed with gzip. Within the daily window all backups are kept; beyond that, one backup per week and one per month are preserved.

Daily

{DAILY_RETENTION_PRESETS.map((days) => { const active = backupRetention.dailyDays === days; return ( ); })}

Weekly

{WEEKLY_RETENTION_PRESETS.map((weeks) => { const active = backupRetention.weeklyWeeks === weeks; const label = weeks === 1 ? "1 week" : `${weeks} weeks`; return ( ); })}

Monthly

{MONTHLY_RETENTION_PRESETS.map((months) => { const active = backupRetention.monthlyMonths === months; const label = months === 1 ? "1 month" : `${months} months`; return ( ); })}
)} {showFeedbackDataSharing && (

AI feedback sharing

Control whether thumbs up and thumbs down votes can send the voted AI output to Paperclip Labs. Votes are always saved locally.

{FEEDBACK_TERMS_URL ? ( Read our terms of service ) : null}
{feedbackDataSharingPreference === "prompt" ? (
No default is saved yet. The next thumbs up or thumbs down choice will ask once and then save the answer here.
) : null}
{[ { value: "allowed", label: "Always allow", description: "Share voted AI outputs automatically.", }, { value: "not_allowed", label: "Don't allow", description: "Keep voted AI outputs local only.", }, ].map((option) => { const active = feedbackDataSharingPreference === option.value; return ( ); })}

To retest the first-use prompt in local dev, remove the{" "} feedbackDataSharingPreference key from the{" "} instance_settings.general JSON row for this instance, or set it back to{" "} "prompt". Unset and "prompt" both mean no default has been chosen yet.

)} {showSignOut && (

Sign out

Sign out of this Paperclip instance. You will be redirected to the login page.

)}
); } function StatusBox({ label, value }: { label: string; value: string }) { return (
{label}
{value}
); }