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 { authApi } from "@/api/auth"; 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"; const FEEDBACK_TERMS_URL = import.meta.env.VITE_FEEDBACK_TERMS_URL?.trim() || "https://paperclip.ing/tos"; export function InstanceGeneralSettings() { const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); const [actionError, setActionError] = useState(null); const signOutMutation = useMutation({ mutationFn: () => authApi.signOut(), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.auth.session }); }, onError: (error) => { setActionError(error instanceof Error ? error.message : "Failed to sign out."); }, }); useEffect(() => { setBreadcrumbs([ { label: "Instance Settings" }, { label: "General" }, ]); }, [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, onSuccess: async () => { setActionError(null); await queryClient.invalidateQueries({ queryKey: queryKeys.instance.generalSettings }); }, onError: (error) => { setActionError(error instanceof Error ? error.message : "Failed to update general settings."); }, }); if (generalQuery.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 keyboardShortcuts = generalQuery.data?.keyboardShortcuts === true; const feedbackDataSharingPreference = generalQuery.data?.feedbackDataSharingPreference ?? "prompt"; const backupRetention: BackupRetentionPolicy = generalQuery.data?.backupRetention ?? DEFAULT_BACKUP_RETENTION; return (

General

Configure instance-wide defaults that affect how operator-visible logs are displayed.

{actionError && (
{actionError}
)}

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."}

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} aria-label="Toggle username log censoring" />

Keyboard shortcuts

Enable app keyboard shortcuts, including inbox navigation and global shortcuts like creating issues or toggling panels. This is off by default.

updateGeneralMutation.mutate({ keyboardShortcuts: !keyboardShortcuts })} disabled={updateGeneralMutation.isPending} aria-label="Toggle keyboard shortcuts" />

Backup retention

Configure how long to keep automatic database backups at each tier. Daily backups are kept in full, then thinned to one per week and one per month. Backups are compressed with gzip.

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 ( ); })}

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.

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}
); }