'use client'; import { useEffect, useState } from 'react'; import { Loader2, Send, Save, CheckCircle2, XCircle, ExternalLink } from 'lucide-react'; import { apiFetch } from '@/lib/browser-navigation'; interface TelemetryStatus { consent: 'pending' | 'on' | 'off'; consentSource: 'env' | 'file'; endpoint: string; defaultEndpoint: string; consentedAt: string | null; lastSentAt: string | null; nextScheduledAt: string | null; payloadPreview: Record; accountCounts: { total: number; active7d: number }; } function timeAgo(iso: string | null): string { if (!iso) return 'never'; const d = Date.now() - new Date(iso).getTime(); if (d < 0) return new Date(iso).toLocaleString(); const m = Math.floor(d / 60000); if (m < 1) return 'just now'; if (m < 60) return `${m} min ago`; const h = Math.floor(m / 60); if (h < 48) return `${h} hours ago`; const days = Math.floor(h / 24); return `${days} days ago`; } export function TelemetryTab() { const [status, setStatus] = useState(null); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(null); const [endpointDraft, setEndpointDraft] = useState(''); const [sendResult, setSendResult] = useState<{ ok: boolean; msg: string } | null>(null); async function refresh(): Promise { setLoading(true); try { const r = await apiFetch('/api/admin/telemetry'); if (!r.ok) throw new Error('failed to load'); const data = (await r.json()) as TelemetryStatus; setStatus(data); setEndpointDraft(data.endpoint); } catch (err) { console.error(err); } finally { setLoading(false); } } useEffect(() => { void refresh(); }, []); async function setConsent(consent: 'on' | 'off'): Promise { setBusy('consent'); try { const r = await apiFetch('/api/admin/telemetry', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action: 'set-consent', consent }), }); if (!r.ok) { const j = (await r.json().catch(() => ({}))) as { error?: string }; alert(j.error ?? 'failed'); } await refresh(); } finally { setBusy(null); } } async function saveEndpoint(): Promise { setBusy('endpoint'); try { const r = await apiFetch('/api/admin/telemetry', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action: 'set-endpoint', endpoint: endpointDraft }), }); if (!r.ok) { const j = (await r.json().catch(() => ({}))) as { error?: string }; alert(j.error ?? 'failed'); } await refresh(); } finally { setBusy(null); } } async function sendNow(): Promise { setBusy('send'); setSendResult(null); try { const r = await apiFetch('/api/admin/telemetry', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action: 'send-now' }), }); const j = (await r.json().catch(() => ({}))) as { ok?: boolean; status?: number; error?: string }; setSendResult({ ok: !!j.ok, msg: j.ok ? `sent (HTTP ${j.status ?? '?'})` : `failed: ${j.error ?? 'unknown'}`, }); await refresh(); } finally { setBusy(null); } } if (loading || !status) { return (
loading…
); } const envOverridden = status.consentSource === 'env'; const isOn = status.consent === 'on'; return (

Anonymous Usage Stats

Bulwark can send one anonymous heartbeat per day so we can see how many instances are running, on what platforms, and which features they use. It's off by default; one click below enables it and helps us make the product better. No email addresses, no hostnames, no IPs are sent.{' '} Full schema and policy

Status
{status.consent === 'pending' && 'Initialising - no heartbeats sent yet.'} {status.consent === 'on' && 'Heartbeats are enabled. Thanks for helping us improve!'} {status.consent === 'off' && 'Heartbeats are off (default).'} {envOverridden && ( <> Locked by BULWARK_TELEMETRY env var. )}
Last sent
{timeAgo(status.lastSentAt)}
Next scheduled
{timeAgo(status.nextScheduledAt)}
Consented at
{status.consentedAt ? new Date(status.consentedAt).toLocaleString() : '-'}
Account activity

Unique accounts that have logged in over the last 90 days. Identities are stored as a per-instance HMAC, never as plaintext usernames. These are the numbers reported in the heartbeat as bucketed ranges.

Total (90d)
{status.accountCounts?.total ?? 0}
Active (7d)
{status.accountCounts?.active7d ?? 0}
Endpoint

Where heartbeats are sent. Defaults to the project's collector. Point at your own collector (open source at bulwarkmail/dashboard) or clear this field to disable sending.

setEndpointDraft(e.target.value)} placeholder={status.defaultEndpoint} className="flex-1 min-w-0 px-3 py-1.5 rounded-md border bg-background" />
Payload preview
Exactly what the next heartbeat would send from this install, right now.
{sendResult && (
{sendResult.ok ? : } {sendResult.msg}
)}
          {JSON.stringify(status.payloadPreview, null, 2)}
        
); }