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 preferences including log display, keyboard shortcuts, backup retention, and data sharing.

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

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