diff --git a/CHANGELOG.md b/CHANGELOG.md index f050535..310086d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +## [0.9.0-alpha] - 2026-02-21 + +### Added + +- **Data retention settings (PULSE-58).** Site owners can choose how long raw event data is kept (1 month to 3 years depending on plan). Events older than the retention period are automatically deleted every 24 hours. Aggregated daily stats are preserved so historical charts remain intact. +- **Data Retention section in Site Settings.** Under Data & Privacy, a dropdown lets you set retention; options are capped by your plan (free: up to 6 months, solo: 1 year, team: 2 years, business: 3 years). +- **Privacy snippet includes retention.** The generated privacy policy text now mentions when raw data is automatically deleted. + ## [0.8.0-alpha] - 2026-02-20 ### Added @@ -104,7 +112,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), --- -[Unreleased]: https://github.com/ciphera-net/pulse/compare/v0.8.0-alpha...HEAD +[Unreleased]: https://github.com/ciphera-net/pulse/compare/v0.9.0-alpha...HEAD +[0.9.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.8.0-alpha...v0.9.0-alpha [0.8.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.7.0-alpha...v0.8.0-alpha [0.7.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.6.0-alpha...v0.7.0-alpha [0.6.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.5.1-alpha...v0.6.0-alpha diff --git a/app/sites/[id]/settings/page.tsx b/app/sites/[id]/settings/page.tsx index c537b68..778ea6b 100644 --- a/app/sites/[id]/settings/page.tsx +++ b/app/sites/[id]/settings/page.tsx @@ -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,12 @@ export default function SiteSettingsPage() { // Performance insights setting enable_performance_insights: false, // Bot and noise filtering - filter_bots: true + filter_bots: true, + // Data retention (6 = free-tier max; safe default) + data_retention_months: 6 }) + const [subscription, setSubscription] = useState(null) + const [subscriptionLoadFailed, setSubscriptionLoadFailed] = useState(false) const [linkCopied, setLinkCopied] = useState(false) const [snippetCopied, setSnippetCopied] = useState(false) const [showVerificationModal, setShowVerificationModal] = useState(false) @@ -83,6 +89,7 @@ export default function SiteSettingsPage() { useEffect(() => { loadSite() + loadSubscription() }, [siteId]) useEffect(() => { @@ -91,6 +98,30 @@ export default function SiteSettingsPage() { } }, [activeTab, siteId]) + const loadSubscription = async () => { + try { + setSubscriptionLoadFailed(false) + const sub = await getSubscription() + setSubscription(sub) + } catch (e) { + setSubscriptionLoadFailed(true) + toast.error(getAuthErrorMessage(e as Error) || 'Could not load plan limits. Showing default options.') + } + } + + // * Snap data_retention_months to nearest valid option when subscription loads + useEffect(() => { + if (!subscription) return + const opts = getRetentionOptionsForPlan(subscription.plan_id) + const values = opts.map(o => o.value) + const maxVal = Math.max(...values) + setFormData(prev => { + if (values.includes(prev.data_retention_months)) return prev + const bestFit = values.filter(v => v <= prev.data_retention_months).pop() ?? maxVal + return { ...prev, data_retention_months: Math.min(bestFit, maxVal) } + }) + }, [subscription]) + const loadSite = async () => { try { setLoading(true) @@ -111,7 +142,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 (default 6 = free-tier max; avoids flash-then-clamp for existing sites) + data_retention_months: data.data_retention_months ?? 6 }) if (data.has_password) { setIsPasswordEnabled(true) @@ -226,7 +259,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 +856,58 @@ export default function SiteSettingsPage() { + {/* Data Retention */} +
+

Data Retention

+ {subscriptionLoadFailed && ( +
+

+ Plan limits could not be loaded. Options shown may be limited. +

+ +
+ )} +
+
+
+

Keep raw event data for

+

+ Events older than this are automatically deleted. Aggregated daily stats are kept permanently. +

+
+