diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3d6d634..fb52c0a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
### Fixed
+- **App switcher and site icons now load correctly.** The Pulse, Drop, and Auth logos in the app switcher — and site favicons on the dashboard — were blocked by the browser's Content Security Policy. The policy now allows images from Ciphera's own domain and Google's favicon service, so all icons display as expected.
+- **Safer campaign date handling.** Campaign analytics now use the same date validation as the rest of the app, including checks for invalid ranges and a maximum span of one year. Previously, campaigns used separate date parsing that skipped these checks.
+- **Better crash protection in goals and real-time views.** Fixed an issue where the backend could crash under rare conditions when checking permissions for goals or real-time visitor data. The app now handles unexpected values gracefully instead of crashing.
+- **Full React 19 type coverage.** Upgraded TypeScript type definitions to match the React 19 runtime. Previously, the type definitions lagged behind at React 18, which could hide bugs and miss new React 19 APIs during development.
+
+### Removed
+
+- **Cleaned up unused files.** Removed six leftover component and utility files that were no longer used anywhere in the app, along with dead backend code. This reduces clutter and keeps the codebase easier to navigate.
+
- **More reliable service health reporting.** The backend health check no longer falsely reports the service as unhealthy after sustained traffic. Previously, an internal counter grew over time and would eventually cross a fixed threshold — even under normal load — causing orchestrators to unnecessarily restart the service.
- **Lower resource usage under load.** The backend now uses a single shared connection to Redis instead of opening dozens of separate ones. Previously, each rate limiter and internal component created its own connection pool, which could waste resources and risk hitting connection limits during heavy traffic.
- **More reliable billing operations.** Billing actions like changing your plan, cancelling, and viewing invoices now benefit from the same automatic session refresh, request tracing, and error handling as the rest of the app. Previously, these used a separate internal path that could fail silently if your session expired mid-action.
diff --git a/components/PasswordInput.tsx b/components/PasswordInput.tsx
deleted file mode 100644
index a92ef38..0000000
--- a/components/PasswordInput.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-'use client'
-
-import { useState } from 'react'
-
-interface PasswordInputProps {
- value: string
- onChange: (value: string) => void
- label?: string
- placeholder?: string
- error?: string | null
- disabled?: boolean
- required?: boolean
- className?: string
- id?: string
- autoComplete?: string
- minLength?: number
- onFocus?: () => void
- onBlur?: () => void
-}
-
-export default function PasswordInput({
- value,
- onChange,
- label = 'Password',
- placeholder = 'Enter password',
- error,
- disabled = false,
- required = false,
- className = '',
- id,
- autoComplete,
- minLength,
- onFocus,
- onBlur
-}: PasswordInputProps) {
- const [showPassword, setShowPassword] = useState(false)
- const inputId = id || 'password-input'
- const errorId = `${inputId}-error`
-
- return (
-
- )
-}
diff --git a/components/WebsiteFooter.tsx b/components/WebsiteFooter.tsx
deleted file mode 100644
index e69de29..0000000
diff --git a/components/WorkspaceSwitcher.tsx b/components/WorkspaceSwitcher.tsx
deleted file mode 100644
index 6d8ff26..0000000
--- a/components/WorkspaceSwitcher.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-'use client'
-
-import { useState } from 'react'
-import { useRouter } from 'next/navigation'
-import { PlusIcon, PersonIcon, CubeIcon, CheckIcon } from '@radix-ui/react-icons'
-import { switchContext, OrganizationMember } from '@/lib/api/organization'
-import { setSessionAction } from '@/app/actions/auth'
-import { logger } from '@/lib/utils/logger'
-import Link from 'next/link'
-
-export default function OrganizationSwitcher({ orgs, activeOrgId }: { orgs: OrganizationMember[], activeOrgId: string | null }) {
- const router = useRouter()
- const [switching, setSwitching] = useState(null)
-
- const handleSwitch = async (orgId: string | null) => {
- setSwitching(orgId || 'personal')
- try {
- // * If orgId is null, we can't switch context via API in the same way if strict mode is on
- // * Pulse doesn't support personal organization context.
- // * So we should probably NOT show the "Personal" option in Pulse if strict mode is enforced.
- // * However, to match Drop exactly, we might want to show it but have it fail or redirect?
- // * Let's assume for now we want to match Drop's UI structure.
-
- if (!orgId) {
- // * Pulse doesn't support personal context.
- // * We could redirect to onboarding or show an error.
- // * For now, let's just return to avoid breaking.
- return
- }
-
- const { access_token } = await switchContext(orgId)
-
- // * Update session cookie via server action
- // * Note: switchContext only returns access_token, we keep existing refresh token
- await setSessionAction(access_token)
-
- sessionStorage.setItem('pulse_switching_org', 'true')
- window.location.reload()
-
- } catch (err) {
- logger.error('Failed to switch organization', err)
- setSwitching(null)
- }
- }
-
- return (
-
-
- Organizations
-
-
- {/* Personal organization - HIDDEN IN PULSE (Strict Mode) */}
- {/*
-
- */}
-
- {/* Organization list */}
- {orgs.map((org) => (
-
- ))}
-
- {/* Create New */}
-
-
-
-
- Create Organization
-
-
- )
-}
diff --git a/components/dashboard/Countries.tsx b/components/dashboard/Countries.tsx
deleted file mode 100644
index 21688b9..0000000
--- a/components/dashboard/Countries.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-'use client'
-
-import { useState } from 'react'
-import { formatNumber } from '@ciphera-net/ui'
-import * as Flags from 'country-flag-icons/react/3x2'
-import WorldMap from './WorldMap'
-import { GlobeIcon } from '@ciphera-net/ui'
-
-interface LocationProps {
- countries: Array<{ country: string; pageviews: number }>
- cities: Array<{ city: string; country: string; pageviews: number }>
-}
-
-type Tab = 'countries' | 'cities'
-
-export default function Locations({ countries, cities }: LocationProps) {
- const [activeTab, setActiveTab] = useState('countries')
-
- const getFlagComponent = (countryCode: string) => {
- if (!countryCode || countryCode === 'Unknown') return null
- // * The API returns 2-letter country codes (e.g. US, DE)
- // * We cast it to the flag component name
- const FlagComponent = (Flags as Record>)[countryCode]
- return FlagComponent ? : null
- }
-
- const getCountryName = (code: string) => {
- if (!code || code === 'Unknown') return 'Unknown'
- try {
- const regionNames = new Intl.DisplayNames(['en'], { type: 'region' })
- return regionNames.of(code) || code
- } catch (e) {
- return code
- }
- }
-
- const renderContent = () => {
- if (activeTab === 'countries') {
- if (!countries || countries.length === 0) {
- return (
-
-
-
-
-
- No location data yet
-
-
- Visitor locations will appear here based on anonymous geographic data.
-