From 7ba5e063ca2ddde1a53bf1da54527d5c265834df Mon Sep 17 00:00:00 2001
From: Usman Baig
Date: Fri, 13 Mar 2026 21:28:04 +0100
Subject: [PATCH 1/5] feat: add free plan to pricing page and enforce 1-site
limit
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Show the free tier (€0, 1 site, 5k pageviews, 6 months retention)
as the first card on the pricing page. Enforce a 1-site limit for
free plan users in the frontend.
---
CHANGELOG.md | 5 +++++
components/PricingSection.tsx | 37 ++++++++++++++++++++++++++++++++++-
lib/plans.ts | 4 ++--
3 files changed, 43 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d9df91f..936b2a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
## [Unreleased]
+### Added
+
+- **Free plan now visible on the Pricing page.** The free tier is no longer hidden — it's displayed as the first option on the Pricing page so you can see exactly what you get before signing up: 1 site, 5,000 monthly pageviews, and 6 months of data retention, completely free.
+- **Free plan limited to 1 site.** Free accounts are now limited to a single site. If you need more, you can upgrade to Solo or above from the Pricing page.
+
### Improved
- **Sites now show their verification status.** Each site on your dashboard now displays either a green "Active" badge (if verified) or an amber "Unverified" badge. When you verify your tracking script installation, the status is saved permanently — no more showing "Active" for sites that haven't been set up yet.
diff --git a/components/PricingSection.tsx b/components/PricingSection.tsx
index 048f942..8205816 100644
--- a/components/PricingSection.tsx
+++ b/components/PricingSection.tsx
@@ -292,7 +292,42 @@ export default function PricingSection() {
{/* Pricing Grid */}
-
+
+ {/* Free Plan */}
+
+
+
Free
+
For trying Pulse on a personal project
+
+ €0
+ /forever
+
+
+
+
+
+
+ {['1 site', '5k monthly pageviews', '6 months data retention', '100% Data ownership'].map((feature) => (
+
+
+ {feature}
+
+ ))}
+
+
+
{PLANS.map((plan) => {
const priceDetails = getPriceDetails(plan.id)
const isTeam = plan.id === 'team'
diff --git a/lib/plans.ts b/lib/plans.ts
index 5456837..a2d2ce9 100644
--- a/lib/plans.ts
+++ b/lib/plans.ts
@@ -7,9 +7,9 @@ export const PLAN_ID_SOLO = 'solo'
export const PLAN_ID_TEAM = 'team'
export const PLAN_ID_BUSINESS = 'business'
-/** Sites limit per plan. Returns null for free (no limit enforced in UI). */
+/** Sites limit per plan. */
export function getSitesLimitForPlan(planId: string | null | undefined): number | null {
- if (!planId || planId === 'free') return null
+ if (!planId || planId === 'free') return 1
switch (planId) {
case 'solo': return 1
case 'team': return 5
From 25210013d39ea892ff5e2e4f9cd15f71182f53f4 Mon Sep 17 00:00:00 2001
From: Usman Baig
Date: Sat, 14 Mar 2026 13:31:30 +0100
Subject: [PATCH 2/5] feat: centralise date/time formatting with European
conventions
All dates now use day-first ordering (14 Mar 2025) and 24-hour time
(14:30) via a single formatDate.ts module, replacing scattered inline
toLocaleDateString/toLocaleTimeString calls across 12 files.
---
CHANGELOG.md | 4 +
app/admin/orgs/[id]/page.tsx | 8 +-
app/admin/orgs/page.tsx | 5 +-
app/page.tsx | 3 +-
app/sites/[id]/settings/page.tsx | 3 +-
app/sites/[id]/uptime/page.tsx | 19 +---
components/dashboard/Chart.tsx | 13 +--
components/dashboard/ExportModal.tsx | 11 +--
components/settings/OrganizationSettings.tsx | 17 ++--
components/settings/SecurityActivityCard.tsx | 4 +-
components/settings/TrustedDevicesCard.tsx | 6 +-
lib/utils/formatDate.ts | 97 +++++++++++++++++---
lib/utils/notifications.tsx | 13 +--
13 files changed, 126 insertions(+), 77 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 936b2a3..dabfab1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
## [Unreleased]
+### Improved
+
+- **European date and time formatting.** All dates across Pulse now use day-first ordering (14 Mar 2025) and 24-hour time (14:30) instead of the US-style month-first format. This applies everywhere — dashboard charts, exports, billing dates, invoices, uptime checks, audit logs, and more.
+
### Added
- **Free plan now visible on the Pricing page.** The free tier is no longer hidden — it's displayed as the first option on the Pricing page so you can see exactly what you get before signing up: 1 site, 5,000 monthly pageviews, and 6 months of data retention, completely free.
diff --git a/app/admin/orgs/[id]/page.tsx b/app/admin/orgs/[id]/page.tsx
index 3039798..3559ff9 100644
--- a/app/admin/orgs/[id]/page.tsx
+++ b/app/admin/orgs/[id]/page.tsx
@@ -4,13 +4,7 @@ import { useEffect, useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { getAdminOrg, grantPlan, type AdminOrgDetail } from '@/lib/api/admin'
import { Button, LoadingOverlay, Select, toast } from '@ciphera-net/ui'
-
-function formatDate(d: Date) {
- return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
-}
-function formatDateTime(d: Date) {
- return d.toLocaleDateString('en-US', { dateStyle: 'long' }) + ' ' + d.toLocaleTimeString('en-US', { hour: 'numeric', minute: 'numeric' })
-}
+import { formatDate, formatDateTime } from '@/lib/utils/formatDate'
function addMonths(d: Date, months: number) {
const out = new Date(d)
out.setMonth(out.getMonth() + months)
diff --git a/app/admin/orgs/page.tsx b/app/admin/orgs/page.tsx
index 288107d..59747ec 100644
--- a/app/admin/orgs/page.tsx
+++ b/app/admin/orgs/page.tsx
@@ -4,10 +4,7 @@ import { useCallback, useEffect, useState } from 'react'
import Link from 'next/link'
import { listAdminOrgs, type AdminOrgSummary } from '@/lib/api/admin'
import { Button, LoadingOverlay, toast } from '@ciphera-net/ui'
-
-function formatDate(d: Date) {
- return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
-}
+import { formatDate } from '@/lib/utils/formatDate'
function CopyableOrgId({ id }: { id: string }) {
const [copied, setCopied] = useState(false)
diff --git a/app/page.tsx b/app/page.tsx
index 663b2c4..04dd558 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -17,6 +17,7 @@ import { BarChartIcon, LockIcon, ZapIcon, CheckCircleIcon, XIcon, GlobeIcon } fr
import { toast } from '@ciphera-net/ui'
import { getAuthErrorMessage } from '@ciphera-net/ui'
import { getSitesLimitForPlan } from '@/lib/plans'
+import { formatDate } from '@/lib/utils/formatDate'
function DashboardPreview() {
return (
@@ -461,7 +462,7 @@ export default function HomePage() {
const ts = subscription.next_invoice_period_end ?? subscription.current_period_end
const d = ts ? new Date(typeof ts === 'number' ? ts * 1000 : ts) : null
const dateStr = d && !Number.isNaN(d.getTime()) && d.getTime() !== 0
- ? d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
+ ? formatDate(d)
: null
const amount = (subscription.next_invoice_amount_due / 100).toLocaleString('en-US', {
style: 'currency',
diff --git a/app/sites/[id]/settings/page.tsx b/app/sites/[id]/settings/page.tsx
index 8d0d1eb..de54aa6 100644
--- a/app/sites/[id]/settings/page.tsx
+++ b/app/sites/[id]/settings/page.tsx
@@ -7,6 +7,7 @@ import { createGoal, updateGoal, deleteGoal, type Goal } from '@/lib/api/goals'
import { createReportSchedule, updateReportSchedule, deleteReportSchedule, testReportSchedule, type ReportSchedule, type CreateReportScheduleRequest, type EmailConfig, type WebhookConfig } from '@/lib/api/report-schedules'
import { toast } from '@ciphera-net/ui'
import { getAuthErrorMessage } from '@ciphera-net/ui'
+import { formatDateTime } from '@/lib/utils/formatDate'
import { SettingsFormSkeleton, GoalsListSkeleton, useMinimumLoading, useSkeletonFade } from '@/components/skeletons'
import VerificationModal from '@/components/sites/VerificationModal'
import ScriptSetupBlock from '@/components/sites/ScriptSetupBlock'
@@ -1341,7 +1342,7 @@ export default function SiteSettingsPage() {