feat: add data retention settings to SiteSettingsPage, including subscription-based options and UI updates for user interaction

This commit is contained in:
Usman Baig
2026-02-21 18:21:43 +01:00
parent 42ed7d91dd
commit 1ae20dba4c
4 changed files with 108 additions and 3 deletions

View File

@@ -13,6 +13,8 @@ import { PasswordInput } from '@ciphera-net/ui'
import { Select, Modal, Button } from '@ciphera-net/ui'
import { APP_URL } from '@/lib/api/client'
import { generatePrivacySnippet } from '@/lib/utils/privacySnippet'
import { getSubscription, type SubscriptionDetails } from '@/lib/api/billing'
import { getRetentionOptionsForPlan, formatRetentionMonths } from '@/lib/plans'
import { motion, AnimatePresence } from 'framer-motion'
import { useAuth } from '@/lib/auth/context'
import {
@@ -68,8 +70,11 @@ export default function SiteSettingsPage() {
// Performance insights setting
enable_performance_insights: false,
// Bot and noise filtering
filter_bots: true
filter_bots: true,
// Data retention
data_retention_months: 12
})
const [subscription, setSubscription] = useState<SubscriptionDetails | null>(null)
const [linkCopied, setLinkCopied] = useState(false)
const [snippetCopied, setSnippetCopied] = useState(false)
const [showVerificationModal, setShowVerificationModal] = useState(false)
@@ -83,6 +88,7 @@ export default function SiteSettingsPage() {
useEffect(() => {
loadSite()
loadSubscription()
}, [siteId])
useEffect(() => {
@@ -91,6 +97,15 @@ export default function SiteSettingsPage() {
}
}, [activeTab, siteId])
const loadSubscription = async () => {
try {
const sub = await getSubscription()
setSubscription(sub)
} catch {
// * Non-critical; free tier assumed if billing unavailable
}
}
const loadSite = async () => {
try {
setLoading(true)
@@ -111,7 +126,9 @@ export default function SiteSettingsPage() {
// Performance insights setting (default to false)
enable_performance_insights: data.enable_performance_insights ?? false,
// Bot and noise filtering (default to true)
filter_bots: data.filter_bots ?? true
filter_bots: data.filter_bots ?? true,
// Data retention
data_retention_months: data.data_retention_months ?? 12
})
if (data.has_password) {
setIsPasswordEnabled(true)
@@ -226,7 +243,9 @@ export default function SiteSettingsPage() {
// Performance insights setting
enable_performance_insights: formData.enable_performance_insights,
// Bot and noise filtering
filter_bots: formData.filter_bots
filter_bots: formData.filter_bots,
// Data retention
data_retention_months: formData.data_retention_months
})
toast.success('Site updated successfully')
loadSite()
@@ -821,6 +840,44 @@ export default function SiteSettingsPage() {
</div>
</div>
{/* Data Retention */}
<div className="space-y-4 pt-6 border-t border-neutral-100 dark:border-neutral-800">
<h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">Data Retention</h3>
<div className="p-4 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
<div className="flex items-center justify-between">
<div>
<h4 className="font-medium text-neutral-900 dark:text-white">Keep raw event data for</h4>
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
Events older than this are automatically deleted. Aggregated daily stats are kept permanently.
</p>
</div>
<Select
value={String(formData.data_retention_months)}
onChange={(v) => setFormData({ ...formData, data_retention_months: Number(v) })}
options={getRetentionOptionsForPlan(subscription?.plan_id).map(opt => ({
value: String(opt.value),
label: opt.label,
}))}
variant="input"
align="right"
className="min-w-[160px]"
/>
</div>
{subscription?.plan_id && subscription.plan_id !== 'free' && (
<p className="text-xs text-neutral-400 dark:text-neutral-500 mt-3">
Your {subscription.plan_id} plan supports up to {formatRetentionMonths(
getRetentionOptionsForPlan(subscription.plan_id).at(-1)?.value ?? 6
)} of data retention.
</p>
)}
{(!subscription?.plan_id || subscription.plan_id === 'free') && (
<p className="text-xs text-neutral-400 dark:text-neutral-500 mt-3">
Free plan supports up to 6 months. <a href="/pricing" className="text-brand-orange hover:underline">Upgrade</a> for longer retention.
</p>
)}
</div>
</div>
{/* Excluded Paths */}
<div className="space-y-4 pt-6 border-t border-neutral-100 dark:border-neutral-800">
<h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">Path Filtering</h3>

View File

@@ -21,6 +21,8 @@ export interface Site {
enable_performance_insights?: boolean
// Bot and noise filtering
filter_bots?: boolean
// Data retention (months); 0 = keep forever
data_retention_months?: number
created_at: string
updated_at: string
}
@@ -47,6 +49,8 @@ export interface UpdateSiteRequest {
enable_performance_insights?: boolean
// Bot and noise filtering
filter_bots?: boolean
// Data retention (months); 0 = keep forever
data_retention_months?: number
}
export async function listSites(): Promise<Site[]> {

View File

@@ -40,3 +40,42 @@ export function getLimitForTierIndex(index: number): number {
if (index < 0 || index >= TRAFFIC_TIERS.length) return 100000
return TRAFFIC_TIERS[index].value
}
/** Maximum data retention (months) allowed per plan. */
export function getMaxRetentionMonthsForPlan(planId: string | null | undefined): number {
switch (planId) {
case 'business': return 60
case 'team': return 24
case 'solo': return 12
default: return 6
}
}
/** Selectable retention options (months) for the given plan. */
export function getRetentionOptionsForPlan(planId: string | null | undefined): { label: string; value: number }[] {
const base = [
{ label: '1 month', value: 1 },
{ label: '3 months', value: 3 },
{ label: '6 months', value: 6 },
]
const solo = [...base, { label: '1 year', value: 12 }]
const team = [...solo, { label: '2 years', value: 24 }]
const business = [...team, { label: '3 years', value: 36 }, { label: '5 years', value: 60 }]
switch (planId) {
case 'business': return business
case 'team': return team
case 'solo': return solo
default: return base
}
}
/** Human-readable label for a retention value in months. */
export function formatRetentionMonths(months: number): string {
if (months === 0) return 'Forever'
if (months === 1) return '1 month'
if (months < 12) return `${months} months`
const years = months / 12
if (Number.isInteger(years)) return years === 1 ? '1 year' : `${years} years`
return `${months} months`
}

View File

@@ -1,4 +1,5 @@
import type { Site } from '@/lib/api/sites'
import { formatRetentionMonths } from '@/lib/plans'
const DOCS_URL =
(typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_APP_URL)
@@ -22,6 +23,7 @@ export function generatePrivacySnippet(site: Site): string {
const screen = site.collect_screen_resolution ?? true
const perf = site.enable_performance_insights ?? false
const filterBots = site.filter_bots ?? true
const retentionMonths = site.data_retention_months ?? 12
const parts: string[] = []
if (paths) parts.push('which pages are viewed')
@@ -44,6 +46,9 @@ export function generatePrivacySnippet(site: Site): string {
if (filterBots) {
p2 += 'Known bots and referrer spam are excluded from our analytics. '
}
if (retentionMonths > 0) {
p2 += `Raw event data is automatically deleted after ${formatRetentionMonths(retentionMonths)}. `
}
p2 += `Data is processed in a privacy-preserving way and is not used to identify individuals. For more information, see Pulse's documentation: ${DOCS_URL}`
return `${p1}\n\n${p2}`